From cf787e3b4c1e5a63ce3970ced84ad56e48c0af4a Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Sun, 3 May 2026 19:02:39 +0530 Subject: [PATCH] fix(client): handle missing access token + delayed autoRefresh timer - getAccessToken(): attempt refreshToken() when access token is absent but refresh token exists (previously returned null immediately) - setupAutoRefresh(): when delay <= 0 (background tab woke up after timer window), refresh immediately instead of silently exiting - init(): when _initialized=true but session is expired, attempt silent refresh instead of returning false immediately - Add [IAMClient] debug logging to all key lifecycle points --- src/client.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/src/client.ts b/src/client.ts index 54d5587..532c942 100644 --- a/src/client.ts +++ b/src/client.ts @@ -73,7 +73,19 @@ export class StuffleIAMClient { * Safe to call multiple times — subsequent calls return the same promise. */ async init(): Promise { - if (this._initialized) return this.isAuthenticated(); + // Allow re-initialization if tokens have expired (e.g. called from a 401 handler). + // The guard only blocks duplicate concurrent inits, not recovery calls. + if (this._initialized && this.isAuthenticated()) return true; + if (this._initialized && !this.isAuthenticated()) { + // Access token expired — attempt a silent refresh instead of returning false immediately + console.debug('[IAMClient] init() called on expired session — attempting silent refresh'); + const tokens = await this.refreshToken(); + if (tokens) { + if (this.config.autoRefresh) this.setupAutoRefresh(); + return true; + } + return false; + } if (this._initPromise) return this._initPromise; this._initPromise = this._doInit(); @@ -85,14 +97,18 @@ export class StuffleIAMClient { const accessToken = this.storage.get(STORAGE_KEYS.ACCESS_TOKEN); const refreshTokenValue = this.storage.get(STORAGE_KEYS.REFRESH_TOKEN); + console.debug('[IAMClient] _doInit: accessToken present =', !!accessToken, '| refreshToken present =', !!refreshTokenValue); + if (!accessToken && !refreshTokenValue) { // No session to restore + console.debug('[IAMClient] _doInit: no tokens found — unauthenticated'); this._initialized = true; return false; } if (accessToken && !isTokenExpired(accessToken)) { // Access token still valid — just re-setup auto-refresh + console.debug('[IAMClient] _doInit: access token valid — setting up auto-refresh'); if (this.config.autoRefresh && refreshTokenValue) { this.setupAutoRefresh(); } @@ -103,6 +119,7 @@ export class StuffleIAMClient { // Access token expired (or missing) but refresh token exists — silent refresh if (refreshTokenValue) { + console.debug('[IAMClient] _doInit: access token expired — attempting silent refresh'); const tokens = await this.refreshToken(); if (tokens) { if (this.config.autoRefresh) { @@ -111,13 +128,14 @@ export class StuffleIAMClient { this._initialized = true; return true; } + console.warn('[IAMClient] _doInit: silent refresh failed — unauthenticated'); } // Refresh failed or no refresh token this._initialized = true; return false; } catch (err) { - console.error('[StuffleIAMClient] init() failed:', err); + console.error('[IAMClient] _doInit failed:', err); this._initialized = true; return false; } @@ -320,7 +338,12 @@ export class StuffleIAMClient { */ async refreshToken(): Promise { const refreshToken = this.storage.get(STORAGE_KEYS.REFRESH_TOKEN); - if (!refreshToken) return null; + if (!refreshToken) { + console.debug('[IAMClient] refreshToken: no refresh token in storage'); + return null; + } + + console.debug('[IAMClient] refreshToken: attempting token refresh'); const discovery = await this.getDiscovery(); @@ -337,6 +360,8 @@ export class StuffleIAMClient { }); if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + console.error('[IAMClient] refreshToken: FAILED', response.status, errorBody); // Refresh failed - clear tokens this.clearTokens(); this.notifyListeners(); @@ -344,6 +369,7 @@ export class StuffleIAMClient { } const tokens: TokenResponse = await response.json(); + console.debug('[IAMClient] refreshToken: SUCCESS — new token expires_in =', tokens.expires_in, 's'); this.storeTokens(tokens); this.notifyListeners(); @@ -379,11 +405,23 @@ export class StuffleIAMClient { */ async getAccessToken(): Promise { const accessToken = this.storage.get(STORAGE_KEYS.ACCESS_TOKEN); - - if (!accessToken) return null; - // Check if token needs refresh + if (!accessToken) { + // No access token — attempt silent refresh if refresh token exists + if (this.storage.get(STORAGE_KEYS.REFRESH_TOKEN)) { + console.debug('[IAMClient] getAccessToken: no access token — attempting refresh'); + const tokens = await this.refreshToken(); + if (tokens) { + if (this.config.autoRefresh) this.setupAutoRefresh(); + } + return tokens?.access_token ?? null; + } + return null; + } + + // Check if token needs refresh (within threshold window) if (isTokenExpired(accessToken, this.config.refreshThreshold)) { + console.debug('[IAMClient] getAccessToken: token within refresh threshold — refreshing'); const tokens = await this.refreshToken(); return tokens?.access_token ?? null; } @@ -519,10 +557,16 @@ export class StuffleIAMClient { const delay = refreshAt - Date.now(); if (delay > 0) { + console.debug('[IAMClient] setupAutoRefresh: timer set for', Math.round(delay / 1000), 's from now'); this.refreshTimer = setTimeout(async () => { + console.debug('[IAMClient] autoRefresh: timer fired — refreshing token'); await this.refreshToken(); this.setupAutoRefresh(); }, delay); + } else { + // Token already within threshold or expired — refresh immediately + console.debug('[IAMClient] setupAutoRefresh: delay <= 0 (', delay, 'ms) — refreshing immediately'); + this.refreshToken().then(() => this.setupAutoRefresh()); } }