@@ -11,6 +11,11 @@ import {
1111import { getLogger } from '../../../common/logger/index.js' ;
1212import { SupabaseSessionStorage } from '../../auth/services/supabase-session-storage.js' ;
1313import { AuthenticationError } from '../../auth/types.js' ;
14+ import {
15+ isSupabaseAuthError ,
16+ isRecoverableStaleSessionError ,
17+ toAuthenticationError
18+ } from '../../auth/utils/index.js' ;
1419
1520export class SupabaseAuthClient {
1621 private static instance : SupabaseAuthClient | null = null ;
@@ -98,7 +103,10 @@ export class SupabaseAuthClient {
98103 } = await client . auth . getSession ( ) ;
99104
100105 if ( error ) {
101- this . logger . warn ( 'Failed to restore session:' , error ) ;
106+ // MFA-expected errors are normal during auth flows - don't log warnings
107+ if ( ! isRecoverableStaleSessionError ( error ) ) {
108+ this . logger . warn ( 'Failed to restore session:' , error ) ;
109+ }
102110 return null ;
103111 }
104112
@@ -108,7 +116,14 @@ export class SupabaseAuthClient {
108116
109117 return session ;
110118 } catch ( error ) {
111- this . logger . error ( 'Error initializing session:' , error ) ;
119+ // MFA-expected errors (refresh_token_not_found, etc.) are normal during auth flows
120+ if ( isRecoverableStaleSessionError ( error ) ) {
121+ this . logger . debug ( 'Session not available (expected during MFA flow)' ) ;
122+ } else if ( isSupabaseAuthError ( error ) ) {
123+ this . logger . warn ( 'Session expired or invalid' ) ;
124+ } else {
125+ this . logger . error ( 'Error initializing session:' , error ) ;
126+ }
112127 return null ;
113128 }
114129 }
@@ -213,13 +228,23 @@ export class SupabaseAuthClient {
213228 } = await client . auth . getSession ( ) ;
214229
215230 if ( error ) {
216- this . logger . warn ( 'Failed to get session:' , error ) ;
231+ // MFA-expected errors are normal during auth flows - don't log warnings
232+ if ( ! isRecoverableStaleSessionError ( error ) ) {
233+ this . logger . warn ( 'Failed to get session:' , error ) ;
234+ }
217235 return null ;
218236 }
219237
220238 return session ;
221239 } catch ( error ) {
222- this . logger . error ( 'Error getting session:' , error ) ;
240+ // MFA-expected errors (refresh_token_not_found, etc.) are normal during auth flows
241+ if ( isRecoverableStaleSessionError ( error ) ) {
242+ this . logger . debug ( 'Session not available (expected during MFA flow)' ) ;
243+ } else if ( isSupabaseAuthError ( error ) ) {
244+ this . logger . warn ( 'Session expired or invalid' ) ;
245+ } else {
246+ this . logger . error ( 'Error getting session:' , error ) ;
247+ }
223248 return null ;
224249 }
225250 }
@@ -241,10 +266,8 @@ export class SupabaseAuthClient {
241266
242267 if ( error ) {
243268 this . logger . error ( 'Failed to refresh session:' , error ) ;
244- throw new AuthenticationError (
245- `Failed to refresh session: ${ error . message } ` ,
246- 'REFRESH_FAILED'
247- ) ;
269+ // Use user-friendly error message for known Supabase auth errors
270+ throw toAuthenticationError ( error , 'Failed to refresh session' ) ;
248271 }
249272
250273 if ( session ) {
@@ -257,6 +280,11 @@ export class SupabaseAuthClient {
257280 throw error ;
258281 }
259282
283+ // Handle raw Supabase auth errors that might be thrown
284+ if ( isSupabaseAuthError ( error ) ) {
285+ throw toAuthenticationError ( error , 'Session refresh failed' ) ;
286+ }
287+
260288 throw new AuthenticationError (
261289 `Failed to refresh session: ${ ( error as Error ) . message } ` ,
262290 'REFRESH_FAILED'
@@ -341,12 +369,36 @@ export class SupabaseAuthClient {
341369 }
342370 }
343371
372+ /**
373+ * Handle recoverable stale session errors by clearing storage and retrying.
374+ * Returns the result of the retry if applicable, or null if no retry was attempted.
375+ */
376+ private async handleRecoverableError (
377+ error : unknown ,
378+ isRetry : boolean ,
379+ retryFn : ( ) => Promise < Session >
380+ ) : Promise < Session | null > {
381+ if ( ! isRetry && isRecoverableStaleSessionError ( error ) ) {
382+ this . logger . debug (
383+ 'MFA-expected error during token verification, clearing stale session and retrying'
384+ ) ;
385+ await this . sessionStorage . clear ( ) ;
386+ return retryFn ( ) ;
387+ }
388+ return null ;
389+ }
390+
344391 /**
345392 * Verify a one-time token and create a session
346393 * Used for CLI authentication with pre-generated tokens
394+ *
395+ * Note: If MFA is enabled and there's a stale session, Supabase might throw
396+ * refresh_token_not_found errors. We handle this by clearing the stale session
397+ * and retrying once.
347398 */
348- async verifyOneTimeCode ( token : string ) : Promise < Session > {
399+ async verifyOneTimeCode ( token : string , isRetry = false ) : Promise < Session > {
349400 const client = this . getClient ( ) ;
401+ const retryFn = ( ) => this . verifyOneTimeCode ( token , true ) ;
350402
351403 try {
352404 this . logger . info ( 'Verifying authentication token...' ) ;
@@ -359,11 +411,18 @@ export class SupabaseAuthClient {
359411 } ) ;
360412
361413 if ( error ) {
362- this . logger . error ( 'Failed to verify token:' , error ) ;
363- throw new AuthenticationError (
364- `Failed to verify token: ${ error . message } ` ,
365- 'INVALID_CODE'
414+ // If this is an MFA-expected error (like refresh_token_not_found),
415+ // it might be due to a stale session interfering. Clear and retry once.
416+ const retryResult = await this . handleRecoverableError (
417+ error ,
418+ isRetry ,
419+ retryFn
366420 ) ;
421+ if ( retryResult ) return retryResult ;
422+
423+ this . logger . error ( 'Failed to verify token:' , error ) ;
424+ // Use user-friendly error message for known Supabase auth errors
425+ throw toAuthenticationError ( error , 'Failed to verify token' ) ;
367426 }
368427
369428 if ( ! data ?. session ) {
@@ -380,6 +439,17 @@ export class SupabaseAuthClient {
380439 throw error ;
381440 }
382441
442+ // Handle raw Supabase auth errors that might be thrown
443+ if ( isSupabaseAuthError ( error ) ) {
444+ const retryResult = await this . handleRecoverableError (
445+ error ,
446+ isRetry ,
447+ retryFn
448+ ) ;
449+ if ( retryResult ) return retryResult ;
450+ throw toAuthenticationError ( error , 'Token verification failed' ) ;
451+ }
452+
383453 throw new AuthenticationError (
384454 `Token verification failed: ${ ( error as Error ) . message } ` ,
385455 'CODE_AUTH_FAILED'
0 commit comments