fix(client): handle missing access token + delayed autoRefresh timer
Some checks failed
Stuffle/iam-client-sdk/pipeline/head There was a failure building this commit
Stuffle/iam-client-sdk/pipeline/pr-main There was a failure building this commit

- 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
This commit is contained in:
2026-05-03 19:02:39 +05:30
parent 4a83568090
commit cf787e3b4c

View File

@@ -73,7 +73,19 @@ export class StuffleIAMClient {
* Safe to call multiple times — subsequent calls return the same promise.
*/
async init(): Promise<boolean> {
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<TokenResponse | null> {
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<string | null> {
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());
}
}