From 7808ee380b18b1c13e0e61792440c4e22be9b14b Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 19 Mar 2026 01:28:15 -0700 Subject: [PATCH] fix: resolve team_id during auth and preserve across token refresh P1 from Codex review: interactive auth saved team_id: '' making all subsequent sync operations fail. Now resolves team_id from team_members table immediately after OAuth callback. Also fixes token refresh in sync.ts to preserve the existing team_id instead of resetting it to empty, and removes order=created_at.desc from pullTable() default query since sync_heartbeats and team_members tables don't have that column (P2). Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/auth.ts | 40 +++++++++++++++++++++++++++++++++++++--- lib/sync.ts | 7 ++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/lib/auth.ts b/lib/auth.ts index 79470714..cfb5913a 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -32,7 +32,7 @@ export async function runDeviceAuth(team: TeamConfig): Promise { reject(new Error('Auth timed out after 5 minutes. Please try again.')); }, AUTH_TIMEOUT_MS); - const server = http.createServer((req, res) => { + const server = http.createServer(async (req, res) => { const url = new URL(req.url || '/', `http://localhost:${AUTH_CALLBACK_PORT}`); // Handle the OAuth callback @@ -53,7 +53,7 @@ export async function runDeviceAuth(team: TeamConfig): Promise { refresh_token: refreshToken, expires_at: Math.floor(Date.now() / 1000) + expiresIn, user_id: url.searchParams.get('user_id') || '', - team_id: '', // filled in by sync.ts after first API call + team_id: '', email: url.searchParams.get('email') || '', }; @@ -63,6 +63,12 @@ export async function runDeviceAuth(team: TeamConfig): Promise { clearTimeout(timeout); server.close(); + // Resolve team_id from team_members table before saving + try { + const teamId = await resolveTeamId(team, tokens.access_token, tokens.user_id); + if (teamId) tokens.team_id = teamId; + } catch { /* non-fatal — team_id can be resolved later */ } + // Save tokens try { saveAuthTokens(team.supabase_url, tokens); @@ -79,7 +85,7 @@ export async function runDeviceAuth(team: TeamConfig): Promise { if (url.pathname === '/auth/token' && req.method === 'POST') { let body = ''; req.on('data', (chunk: Buffer) => { body += chunk.toString(); }); - req.on('end', () => { + req.on('end', async () => { try { const data = JSON.parse(body); const tokens: AuthTokens = { @@ -97,6 +103,12 @@ export async function runDeviceAuth(team: TeamConfig): Promise { clearTimeout(timeout); server.close(); + // Resolve team_id before saving + try { + const teamId = await resolveTeamId(team, tokens.access_token, tokens.user_id); + if (teamId) tokens.team_id = teamId; + } catch { /* non-fatal */ } + saveAuthTokens(team.supabase_url, tokens); resolve(tokens); } catch (err: any) { @@ -209,3 +221,25 @@ export function isTokenExpired(tokens: AuthTokens): boolean { const buffer = 300; // 5-minute buffer return Math.floor(Date.now() / 1000) >= tokens.expires_at - buffer; } + +/** + * Look up the user's team_id from team_members table after auth. + * Returns the first team_id found, or null if the lookup fails. + */ +async function resolveTeamId(team: TeamConfig, accessToken: string, userId: string): Promise { + if (!userId) return null; + try { + const url = `${team.supabase_url}/rest/v1/team_members?user_id=eq.${userId}&select=team_id&limit=1`; + const res = await fetch(url, { + headers: { + 'apikey': team.supabase_anon_key, + 'Authorization': `Bearer ${accessToken}`, + }, + }); + if (!res.ok) return null; + const rows = await res.json() as Array<{ team_id: string }>; + return rows.length > 0 ? rows[0].team_id : null; + } catch { + return null; + } +} diff --git a/lib/sync.ts b/lib/sync.ts index af3660d6..b425b92e 100644 --- a/lib/sync.ts +++ b/lib/sync.ts @@ -39,7 +39,7 @@ interface CacheMeta { * Refresh an expired access token using the refresh token. * Returns new tokens on success, null on failure. */ -async function refreshToken(supabaseUrl: string, refreshToken: string, anonKey: string): Promise { +async function refreshToken(supabaseUrl: string, refreshToken: string, anonKey: string, existingTeamId?: string): Promise { try { const res = await fetchWithTimeout(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, { method: 'POST', @@ -58,7 +58,7 @@ async function refreshToken(supabaseUrl: string, refreshToken: string, anonKey: refresh_token: data.refresh_token as string || refreshToken, expires_at: Math.floor(Date.now() / 1000) + ((data.expires_in as number) || 3600), user_id: (data.user as any)?.id || '', - team_id: '', + team_id: existingTeamId || '', // preserve existing team_id across refresh email: (data.user as any)?.email || '', }; } catch { @@ -78,6 +78,7 @@ export async function getValidToken(config: SyncConfig): Promise config.team.supabase_url, config.auth.refresh_token, config.team.supabase_anon_key, + config.auth.team_id, ); if (!newTokens) return null; @@ -234,7 +235,7 @@ export async function pullTable(table: string, query?: string): Promise