@imtbl/auth-next-client 2.12.7 → 2.12.8-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -288,11 +288,12 @@ function useImmutableSession() {
288
288
  setIsRefreshingRef.current = setIsRefreshing;
289
289
  (0, import_react3.useEffect)(() => {
290
290
  if (!session?.accessTokenExpires) return;
291
+ if (session?.error) return;
291
292
  const timeUntilExpiry = session.accessTokenExpires - Date.now() - TOKEN_EXPIRY_BUFFER_MS;
292
293
  if (timeUntilExpiry <= 0) {
293
294
  deduplicatedUpdate(() => updateRef.current());
294
295
  }
295
- }, [session?.accessTokenExpires]);
296
+ }, [session?.accessTokenExpires, session?.error]);
296
297
  (0, import_react3.useEffect)(() => {
297
298
  if (session?.idToken) {
298
299
  storeIdToken(session.idToken);
@@ -261,11 +261,12 @@ function useImmutableSession() {
261
261
  setIsRefreshingRef.current = setIsRefreshing;
262
262
  useEffect2(() => {
263
263
  if (!session?.accessTokenExpires) return;
264
+ if (session?.error) return;
264
265
  const timeUntilExpiry = session.accessTokenExpires - Date.now() - TOKEN_EXPIRY_BUFFER_MS;
265
266
  if (timeUntilExpiry <= 0) {
266
267
  deduplicatedUpdate(() => updateRef.current());
267
268
  }
268
- }, [session?.accessTokenExpires]);
269
+ }, [session?.accessTokenExpires, session?.error]);
269
270
  useEffect2(() => {
270
271
  if (session?.idToken) {
271
272
  storeIdToken(session.idToken);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imtbl/auth-next-client",
3
- "version": "2.12.7",
3
+ "version": "2.12.8-alpha.0",
4
4
  "description": "Immutable Auth.js v5 integration for Next.js - Client-side components",
5
5
  "author": "Immutable",
6
6
  "license": "Apache-2.0",
@@ -27,8 +27,8 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "@imtbl/auth": "2.12.7",
31
- "@imtbl/auth-next-server": "2.12.7"
30
+ "@imtbl/auth": "2.12.8-alpha.0",
31
+ "@imtbl/auth-next-server": "2.12.8-alpha.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "next": "^14.0.0 || ^15.0.0",
@@ -291,6 +291,23 @@ describe('useImmutableSession', () => {
291
291
  // Should NOT have called update -- token is still valid
292
292
  expect(mockUpdate).not.toHaveBeenCalled();
293
293
  });
294
+
295
+ it('does not trigger refresh when session has error (prevents infinite loop)', async () => {
296
+ // Simulate: token expired and last refresh failed (e.g. RefreshTokenError)
297
+ const sessionWithError = createSession({
298
+ accessTokenExpires: Date.now() - 1000, // expired
299
+ error: 'RefreshTokenError',
300
+ });
301
+ setupUseSession(sessionWithError);
302
+
303
+ await act(async () => {
304
+ renderHook(() => useImmutableSession());
305
+ });
306
+
307
+ // Must NOT call update - otherwise we would retry refresh repeatedly
308
+ // and cause an infinite loop (update -> same session with error -> effect re-runs -> update again).
309
+ expect(mockUpdate).not.toHaveBeenCalled();
310
+ });
294
311
  });
295
312
 
296
313
  describe('getUser() respects pending refresh', () => {
@@ -317,5 +334,35 @@ describe('useImmutableSession', () => {
317
334
  // getUser() should have waited for the refresh and gotten the fresh token
318
335
  expect(user?.accessToken).toBe('user-fresh-token');
319
336
  });
337
+
338
+ it('getUser(true) still calls update with forceRefresh even when session has error', async () => {
339
+ // Session is in error state (e.g. previous refresh failed)
340
+ const sessionWithError = createSession({
341
+ accessTokenExpires: Date.now() - 1000,
342
+ error: 'RefreshTokenError',
343
+ });
344
+ setupUseSession(sessionWithError);
345
+
346
+ // Server recovers and returns a valid session (e.g. user re-authenticated elsewhere)
347
+ const recoveredSession = createSession({
348
+ accessToken: 'recovered-token',
349
+ accessTokenExpires: Date.now() + 10 * 60 * 1000,
350
+ user: { sub: 'user-1', email: 'recovered@test.com' },
351
+ });
352
+ mockUpdate.mockResolvedValue(recoveredSession);
353
+
354
+ const { result } = renderHook(() => useImmutableSession());
355
+
356
+ let user: any;
357
+ await act(async () => {
358
+ user = await result.current.getUser(true);
359
+ });
360
+
361
+ // forceRefresh must have been attempted (proactive effect does NOT run when session.error is set)
362
+ expect(mockUpdate).toHaveBeenCalledWith({ forceRefresh: true });
363
+ // When server returns a good session, we get the user
364
+ expect(user?.accessToken).toBe('recovered-token');
365
+ expect(user?.profile?.email).toBe('recovered@test.com');
366
+ });
320
367
  });
321
368
  });
package/src/hooks.tsx CHANGED
@@ -221,6 +221,8 @@ export function useImmutableSession(): UseImmutableSessionReturn {
221
221
  // `!isRefreshing` to briefly lose their cached data, resulting in UI flicker.
222
222
  useEffect(() => {
223
223
  if (!session?.accessTokenExpires) return;
224
+ // Don't retry if the last refresh already failed - prevents infinite loops
225
+ if (session?.error) return;
224
226
 
225
227
  const timeUntilExpiry = session.accessTokenExpires - Date.now() - TOKEN_EXPIRY_BUFFER_MS;
226
228
 
@@ -228,7 +230,7 @@ export function useImmutableSession(): UseImmutableSessionReturn {
228
230
  // Already expired -- refresh silently
229
231
  deduplicatedUpdate(() => updateRef.current());
230
232
  }
231
- }, [session?.accessTokenExpires]);
233
+ }, [session?.accessTokenExpires, session?.error]);
232
234
 
233
235
  // ---------------------------------------------------------------------------
234
236
  // Sync idToken to localStorage