@workos-inc/authkit-nextjs 2.12.2 → 2.14.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.
Files changed (46) hide show
  1. package/README.md +138 -73
  2. package/dist/esm/errors.js +33 -0
  3. package/dist/esm/errors.js.map +1 -0
  4. package/dist/esm/get-authorization-url.js +7 -2
  5. package/dist/esm/get-authorization-url.js.map +1 -1
  6. package/dist/esm/index.js +3 -1
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/esm/middleware-helpers.js +99 -0
  9. package/dist/esm/middleware-helpers.js.map +1 -0
  10. package/dist/esm/session.js +11 -35
  11. package/dist/esm/session.js.map +1 -1
  12. package/dist/esm/types/errors.d.ts +15 -0
  13. package/dist/esm/types/index.d.ts +3 -1
  14. package/dist/esm/types/middleware-helpers.d.ts +25 -0
  15. package/dist/esm/types/session.d.ts +1 -1
  16. package/dist/esm/types/validate-api-key.d.ts +1 -1
  17. package/dist/esm/types/workos.d.ts +1 -1
  18. package/dist/esm/utils.js +0 -2
  19. package/dist/esm/utils.js.map +1 -1
  20. package/dist/esm/workos.js +1 -1
  21. package/package.json +20 -21
  22. package/src/actions.spec.ts +14 -12
  23. package/src/auth.spec.ts +27 -29
  24. package/src/authkit-callback-route.spec.ts +31 -29
  25. package/src/components/authkit-provider.spec.tsx +67 -71
  26. package/src/components/button.spec.tsx +4 -6
  27. package/src/components/impersonation.spec.tsx +25 -25
  28. package/src/components/min-max-button.spec.tsx +2 -1
  29. package/src/components/tokenStore.spec.ts +21 -21
  30. package/src/components/useAccessToken.spec.tsx +73 -77
  31. package/src/components/useTokenClaims.spec.tsx +22 -22
  32. package/src/cookie.spec.ts +14 -9
  33. package/src/errors.spec.ts +108 -0
  34. package/src/errors.ts +46 -0
  35. package/src/get-authorization-url.spec.ts +12 -13
  36. package/src/get-authorization-url.ts +6 -10
  37. package/src/index.ts +16 -2
  38. package/src/middleware-helpers.spec.ts +231 -0
  39. package/src/middleware-helpers.ts +130 -0
  40. package/src/session.spec.ts +81 -73
  41. package/src/session.ts +16 -38
  42. package/src/utils.spec.ts +14 -31
  43. package/src/utils.ts +0 -2
  44. package/src/validate-api-key.spec.ts +4 -6
  45. package/src/workos.spec.ts +2 -2
  46. package/src/workos.ts +1 -1
@@ -10,12 +10,15 @@ import { jwtVerify } from 'jose';
10
10
  import { sealData } from 'iron-session';
11
11
  import { User } from '@workos-inc/node';
12
12
 
13
- jest.mock('jose', () => ({
14
- jwtVerify: jest.fn(),
15
- createRemoteJWKSet: jest.fn(),
16
- SignJWT: jest.requireActual('jose').SignJWT,
17
- decodeJwt: jest.requireActual('jose').decodeJwt,
18
- }));
13
+ vi.mock('jose', async () => {
14
+ const actual = await vi.importActual<typeof import('jose')>('jose');
15
+ return {
16
+ jwtVerify: vi.fn(),
17
+ createRemoteJWKSet: vi.fn(),
18
+ SignJWT: actual.SignJWT,
19
+ decodeJwt: actual.decodeJwt,
20
+ };
21
+ });
19
22
 
20
23
  // logging is disabled by default, flip this to true to still have logs in the console
21
24
  const DEBUG = false;
@@ -47,11 +50,11 @@ describe('session.ts', () => {
47
50
  } as User,
48
51
  };
49
52
 
50
- let consoleLogSpy: jest.SpyInstance;
53
+ let consoleLogSpy: MockInstance;
51
54
 
52
55
  beforeEach(async () => {
53
56
  // Clear all mocks between tests
54
- jest.clearAllMocks();
57
+ vi.clearAllMocks();
55
58
 
56
59
  // Reset the cookie store
57
60
  const nextCookies = await cookies();
@@ -63,9 +66,9 @@ describe('session.ts', () => {
63
66
  nextHeaders._reset();
64
67
  nextHeaders.set('x-workos-middleware', 'true');
65
68
 
66
- (jwtVerify as jest.Mock).mockReset();
69
+ (jwtVerify as Mock).mockReset();
67
70
 
68
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((...args) => {
71
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation((...args) => {
69
72
  if (DEBUG) {
70
73
  console.info(...args);
71
74
  }
@@ -74,7 +77,7 @@ describe('session.ts', () => {
74
77
 
75
78
  afterEach(() => {
76
79
  consoleLogSpy.mockRestore();
77
- jest.resetModules();
80
+ vi.resetModules();
78
81
  });
79
82
 
80
83
  describe('withAuth', () => {
@@ -159,7 +162,7 @@ describe('session.ts', () => {
159
162
  it('should throw an error if the redirect URI is not set', async () => {
160
163
  const originalWorkosRedirectUri = envVariables.WORKOS_REDIRECT_URI;
161
164
 
162
- jest.replaceProperty(envVariables, 'WORKOS_REDIRECT_URI', '');
165
+ Object.defineProperty(envVariables, 'WORKOS_REDIRECT_URI', { value: '', configurable: true });
163
166
 
164
167
  await expect(async () => {
165
168
  await updateSessionMiddleware(
@@ -174,13 +177,16 @@ describe('session.ts', () => {
174
177
  );
175
178
  }).rejects.toThrow('You must provide a redirect URI in the AuthKit middleware or in the environment variables.');
176
179
 
177
- jest.replaceProperty(envVariables, 'WORKOS_REDIRECT_URI', originalWorkosRedirectUri);
180
+ Object.defineProperty(envVariables, 'WORKOS_REDIRECT_URI', {
181
+ value: originalWorkosRedirectUri,
182
+ configurable: true,
183
+ });
178
184
  });
179
185
 
180
186
  it('should throw an error if the cookie password is not set', async () => {
181
187
  const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;
182
188
 
183
- jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', '');
189
+ Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', { value: '', configurable: true });
184
190
 
185
191
  await expect(async () => {
186
192
  await updateSessionMiddleware(
@@ -197,13 +203,16 @@ describe('session.ts', () => {
197
203
  'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
198
204
  );
199
205
 
200
- jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', originalWorkosCookiePassword);
206
+ Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', {
207
+ value: originalWorkosCookiePassword,
208
+ configurable: true,
209
+ });
201
210
  });
202
211
 
203
212
  it('should throw an error if the cookie password is less than 32 characters', async () => {
204
213
  const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;
205
214
 
206
- jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', 'short');
215
+ Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', { value: 'short', configurable: true });
207
216
 
208
217
  await expect(async () => {
209
218
  await updateSessionMiddleware(
@@ -220,7 +229,10 @@ describe('session.ts', () => {
220
229
  'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
221
230
  );
222
231
 
223
- jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', originalWorkosCookiePassword);
232
+ Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', {
233
+ value: originalWorkosCookiePassword,
234
+ configurable: true,
235
+ });
224
236
  });
225
237
 
226
238
  it('should return early if there is no session', async () => {
@@ -241,7 +253,7 @@ describe('session.ts', () => {
241
253
  });
242
254
 
243
255
  it('should return 200 if the session is valid', async () => {
244
- jest.spyOn(console, 'log').mockImplementation(() => {});
256
+ vi.spyOn(console, 'log').mockImplementation(() => {});
245
257
 
246
258
  const nextCookies = await cookies();
247
259
  nextCookies.set(
@@ -249,7 +261,7 @@ describe('session.ts', () => {
249
261
  await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
250
262
  );
251
263
 
252
- (jwtVerify as jest.Mock).mockImplementation(() => {
264
+ (jwtVerify as Mock).mockImplementation(() => {
253
265
  return true;
254
266
  });
255
267
 
@@ -272,11 +284,11 @@ describe('session.ts', () => {
272
284
  it('should attempt to refresh the session when the access token is invalid', async () => {
273
285
  mockSession.accessToken = await generateTestToken({}, true);
274
286
 
275
- (jwtVerify as jest.Mock).mockImplementation(() => {
287
+ (jwtVerify as Mock).mockImplementation(() => {
276
288
  throw new Error('Invalid token');
277
289
  });
278
290
 
279
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
291
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
280
292
  accessToken: await generateTestToken(),
281
293
  refreshToken: 'new-refresh-token',
282
294
  user: mockSession.user,
@@ -308,17 +320,15 @@ describe('session.ts', () => {
308
320
  });
309
321
 
310
322
  it('should delete the cookie when refreshing fails', async () => {
311
- jest.spyOn(console, 'log').mockImplementation(() => {});
323
+ vi.spyOn(console, 'log').mockImplementation(() => {});
312
324
 
313
325
  mockSession.accessToken = await generateTestToken({}, true);
314
326
 
315
- (jwtVerify as jest.Mock).mockImplementation(() => {
327
+ (jwtVerify as Mock).mockImplementation(() => {
316
328
  throw new Error('Invalid token');
317
329
  });
318
330
 
319
- jest
320
- .spyOn(workos.userManagement, 'authenticateWithRefreshToken')
321
- .mockRejectedValue(new Error('Failed to refresh'));
331
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Failed to refresh'));
322
332
 
323
333
  const request = new NextRequest(new URL('http://example.com'));
324
334
 
@@ -354,7 +364,7 @@ describe('session.ts', () => {
354
364
 
355
365
  describe('middleware auth', () => {
356
366
  it('should redirect unauthenticated users on protected routes', async () => {
357
- jest.spyOn(console, 'log').mockImplementation(() => {});
367
+ vi.spyOn(console, 'log').mockImplementation(() => {});
358
368
 
359
369
  const request = new NextRequest(new URL('http://example.com/protected'));
360
370
  const result = await updateSessionMiddleware(
@@ -374,10 +384,7 @@ describe('session.ts', () => {
374
384
  );
375
385
  });
376
386
 
377
- it('should use Response if NextResponse.redirect is not available', async () => {
378
- const originalRedirect = NextResponse.redirect;
379
- (NextResponse as Partial<typeof NextResponse>).redirect = undefined;
380
-
387
+ it('should return a redirect response when middlewareAuth is enabled and user is not authenticated', async () => {
381
388
  const request = new NextRequest(new URL('http://example.com/protected'));
382
389
  const result = await updateSessionMiddleware(
383
390
  request,
@@ -390,10 +397,9 @@ describe('session.ts', () => {
390
397
  [],
391
398
  );
392
399
 
393
- expect(result).toBeInstanceOf(Response);
394
-
395
- // Restore the original redirect method
396
- (NextResponse as Partial<typeof NextResponse>).redirect = originalRedirect;
400
+ expect(result).toBeInstanceOf(NextResponse);
401
+ expect(result.status).toBe(307);
402
+ expect(result.headers.get('Location')).toContain('workos.com');
397
403
  });
398
404
 
399
405
  it('should automatically add the redirect URI to unauthenticatedPaths when middleware is enabled', async () => {
@@ -429,7 +435,7 @@ describe('session.ts', () => {
429
435
  expect(result.headers.get('Location')).toContain('screen_hint=sign-up');
430
436
  });
431
437
 
432
- it('should set the sign up paths in the headers', async () => {
438
+ it('should not leak sign-up paths header to the browser', async () => {
433
439
  const request = new NextRequest(new URL('http://example.com/protected-signup'));
434
440
  const result = await updateSessionMiddleware(
435
441
  request,
@@ -442,7 +448,8 @@ describe('session.ts', () => {
442
448
  ['/protected-signup'],
443
449
  );
444
450
 
445
- expect(result.headers.get('x-sign-up-paths')).toBe('/protected-signup');
451
+ // x-sign-up-paths is an internal header that should not leak to the browser
452
+ expect(result.headers.get('x-sign-up-paths')).toBeNull();
446
453
  });
447
454
 
448
455
  it('should allow logged out users on unauthenticated paths', async () => {
@@ -479,11 +486,11 @@ describe('session.ts', () => {
479
486
 
480
487
  it('should throw an error if the provided regex is invalid and a non-Error object is thrown', async () => {
481
488
  // Reset modules to ensure clean import state
482
- jest.resetModules();
489
+ vi.resetModules();
483
490
 
484
491
  // Import first, then spy
485
492
  const pathToRegexp = await import('path-to-regexp');
486
- const parseSpy = jest.spyOn(pathToRegexp, 'parse').mockImplementation(() => {
493
+ const parseSpy = vi.spyOn(pathToRegexp, 'parse').mockImplementation(() => {
487
494
  throw 'invalid regex';
488
495
  });
489
496
 
@@ -507,6 +514,9 @@ describe('session.ts', () => {
507
514
 
508
515
  // Verify the mock was called
509
516
  expect(parseSpy).toHaveBeenCalled();
517
+
518
+ // Restore the spy to prevent leaking to subsequent tests
519
+ parseSpy.mockRestore();
510
520
  });
511
521
 
512
522
  it('should default to the WORKOS_REDIRECT_URI environment variable if no redirect URI is provided', async () => {
@@ -526,17 +536,17 @@ describe('session.ts', () => {
526
536
  });
527
537
 
528
538
  it('should delete the cookie and redirect when refreshing fails', async () => {
529
- jest.spyOn(console, 'log').mockImplementation(() => {});
539
+ vi.spyOn(console, 'log').mockImplementation(() => {});
530
540
 
531
541
  mockSession.accessToken = await generateTestToken({}, true);
532
542
 
533
- (jwtVerify as jest.Mock).mockImplementation(() => {
543
+ (jwtVerify as Mock).mockImplementation(() => {
534
544
  throw new Error('Invalid token');
535
545
  });
536
546
 
537
- jest
538
- .spyOn(workos.userManagement, 'authenticateWithRefreshToken')
539
- .mockRejectedValue(new Error('Failed to refresh'));
547
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(
548
+ new Error('Failed to refresh'),
549
+ );
540
550
 
541
551
  const request = new NextRequest(new URL('http://example.com'));
542
552
 
@@ -637,12 +647,12 @@ describe('session.ts', () => {
637
647
  mockSession.accessToken = await generateTestToken({}, true);
638
648
 
639
649
  // Mock token verification to fail
640
- (jwtVerify as jest.Mock).mockImplementation(() => {
650
+ (jwtVerify as Mock).mockImplementation(() => {
641
651
  throw new Error('Invalid token');
642
652
  });
643
653
 
644
654
  // Mock successful refresh
645
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
655
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
646
656
  accessToken: await generateTestToken(),
647
657
  refreshToken: 'new-refresh-token',
648
658
  user: mockSession.user,
@@ -670,12 +680,12 @@ describe('session.ts', () => {
670
680
  mockSession.accessToken = await generateTestToken({}, true);
671
681
 
672
682
  // Mock token verification to fail
673
- (jwtVerify as jest.Mock).mockImplementation(() => {
683
+ (jwtVerify as Mock).mockImplementation(() => {
674
684
  throw new Error('Invalid token');
675
685
  });
676
686
 
677
687
  // Mock refresh failure
678
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Refresh failed'));
688
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Refresh failed'));
679
689
 
680
690
  const request = new NextRequest(new URL('http://example.com/protected'));
681
691
  request.cookies.set(
@@ -697,15 +707,15 @@ describe('session.ts', () => {
697
707
  mockSession.accessToken = await generateTestToken({}, true);
698
708
 
699
709
  // Mock token verification to fail
700
- (jwtVerify as jest.Mock).mockImplementation(() => {
710
+ (jwtVerify as Mock).mockImplementation(() => {
701
711
  throw new Error('Invalid token');
702
712
  });
703
713
 
704
714
  const newAccessToken = await generateTestToken();
705
- const mockSuccessCallback = jest.fn();
715
+ const mockSuccessCallback = vi.fn();
706
716
 
707
717
  // Mock successful refresh
708
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
718
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
709
719
  accessToken: newAccessToken,
710
720
  refreshToken: 'new-refresh-token',
711
721
  user: mockSession.user,
@@ -736,15 +746,15 @@ describe('session.ts', () => {
736
746
  mockSession.accessToken = await generateTestToken({}, true);
737
747
 
738
748
  // Mock token verification to fail
739
- (jwtVerify as jest.Mock).mockImplementation(() => {
749
+ (jwtVerify as Mock).mockImplementation(() => {
740
750
  throw new Error('Invalid token');
741
751
  });
742
752
 
743
753
  const mockError = new Error('Refresh failed');
744
- const mockErrorCallback = jest.fn();
754
+ const mockErrorCallback = vi.fn();
745
755
 
746
756
  // Mock refresh failure
747
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(mockError);
757
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(mockError);
748
758
 
749
759
  const request = new NextRequest(new URL('http://example.com/protected'));
750
760
  request.cookies.set(
@@ -767,15 +777,15 @@ describe('session.ts', () => {
767
777
 
768
778
  describe('refreshSession', () => {
769
779
  it('should refresh session successfully', async () => {
770
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
780
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
771
781
  accessToken: await generateTestToken(),
772
782
  refreshToken: 'new-refresh-token',
773
783
  user: mockSession.user,
774
784
  });
775
785
 
776
- jest
777
- .spyOn(workos.userManagement, 'getJwksUrl')
778
- .mockReturnValue('https://api.workos.com/sso/jwks/client_1234567890');
786
+ vi.spyOn(workos.userManagement, 'getJwksUrl').mockReturnValue(
787
+ 'https://api.workos.com/sso/jwks/client_1234567890',
788
+ );
779
789
 
780
790
  const nextCookies = await cookies();
781
791
  nextCookies.set(
@@ -811,15 +821,15 @@ describe('session.ts', () => {
811
821
  await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
812
822
  );
813
823
 
814
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
824
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
815
825
  accessToken: await generateTestToken({ org_id: 'org_456' }),
816
826
  refreshToken: 'new-refresh-token',
817
827
  user: mockSession.user,
818
828
  });
819
829
 
820
- jest
821
- .spyOn(workos.userManagement, 'getJwksUrl')
822
- .mockReturnValue('https://api.workos.com/sso/jwks/client_1234567890');
830
+ vi.spyOn(workos.userManagement, 'getJwksUrl').mockReturnValue(
831
+ 'https://api.workos.com/sso/jwks/client_1234567890',
832
+ );
823
833
 
824
834
  const result = await refreshSession({ organizationId: 'org_456' });
825
835
 
@@ -838,8 +848,8 @@ describe('session.ts', () => {
838
848
  'wos-session',
839
849
  await sealData(mockSessionWithValidJWT, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
840
850
  );
841
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue('fail');
842
- expect(refreshSession({ ensureSignedIn: false })).rejects.toThrow('Failed to refresh session: fail');
851
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue('fail');
852
+ await expect(refreshSession({ ensureSignedIn: false })).rejects.toThrow('Failed to refresh session: fail');
843
853
  });
844
854
 
845
855
  it('throws if authenticateWithRefreshToken fails with error', async () => {
@@ -853,7 +863,7 @@ describe('session.ts', () => {
853
863
  'wos-session',
854
864
  await sealData(mockSessionWithValidJWT, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
855
865
  );
856
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('error'));
866
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('error'));
857
867
  await expect(refreshSession()).rejects.toThrow('Failed to refresh session: error');
858
868
  });
859
869
  });
@@ -863,7 +873,7 @@ describe('session.ts', () => {
863
873
  const nextCookies = await cookies();
864
874
  // @ts-expect-error - _reset is part of the mock
865
875
  nextCookies._reset();
866
- jest.clearAllMocks();
876
+ vi.clearAllMocks();
867
877
  });
868
878
 
869
879
  it('should return all token claims when accessToken is provided', async () => {
@@ -957,7 +967,7 @@ describe('session.ts', () => {
957
967
 
958
968
  describe('eager auth functionality', () => {
959
969
  beforeEach(() => {
960
- jest.clearAllMocks();
970
+ vi.clearAllMocks();
961
971
  });
962
972
 
963
973
  describe('isInitialDocumentRequest', () => {
@@ -1051,12 +1061,12 @@ describe('session.ts', () => {
1051
1061
  // Setup invalid session that needs refresh
1052
1062
  mockSession.accessToken = await generateTestToken({}, true);
1053
1063
 
1054
- (jwtVerify as jest.Mock).mockImplementation(() => {
1064
+ (jwtVerify as Mock).mockImplementation(() => {
1055
1065
  throw new Error('Invalid token');
1056
1066
  });
1057
1067
 
1058
1068
  const newAccessToken = await generateTestToken();
1059
- jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
1069
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
1060
1070
  accessToken: newAccessToken,
1061
1071
  refreshToken: 'new-refresh-token',
1062
1072
  user: mockSession.user,
@@ -1082,13 +1092,11 @@ describe('session.ts', () => {
1082
1092
  // Setup invalid session
1083
1093
  mockSession.accessToken = await generateTestToken({}, true);
1084
1094
 
1085
- (jwtVerify as jest.Mock).mockImplementation(() => {
1095
+ (jwtVerify as Mock).mockImplementation(() => {
1086
1096
  throw new Error('Invalid token');
1087
1097
  });
1088
1098
 
1089
- jest
1090
- .spyOn(workos.userManagement, 'authenticateWithRefreshToken')
1091
- .mockRejectedValue(new Error('Refresh failed'));
1099
+ vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Refresh failed'));
1092
1100
 
1093
1101
  const request = new NextRequest(new URL('http://example.com/page'));
1094
1102
  request.headers.set('accept', 'text/html');
package/src/session.ts CHANGED
@@ -4,9 +4,10 @@ import { sealData, unsealData } from 'iron-session';
4
4
  import { JWTPayload, createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
5
5
  import { cookies, headers } from 'next/headers';
6
6
  import { redirect } from 'next/navigation';
7
- import { NextRequest, NextResponse } from 'next/server';
7
+ import { NextRequest } from 'next/server';
8
8
  import { getCookieOptions, getJwtCookie } from './cookie.js';
9
9
  import { WORKOS_CLIENT_ID, WORKOS_COOKIE_NAME, WORKOS_COOKIE_PASSWORD, WORKOS_REDIRECT_URI } from './env-variables.js';
10
+ import { TokenRefreshError, getSessionErrorContext } from './errors.js';
10
11
  import { getAuthorizationUrl } from './get-authorization-url.js';
11
12
  import {
12
13
  AccessToken,
@@ -21,7 +22,8 @@ import { getWorkOS } from './workos.js';
21
22
 
22
23
  import type { AuthenticationResponse } from '@workos-inc/node';
23
24
  import { parse, tokensToRegexp } from 'path-to-regexp';
24
- import { lazy, redirectWithFallback, setCachePreventionHeaders } from './utils.js';
25
+ import { handleAuthkitHeaders } from './middleware-helpers.js';
26
+ import { lazy, setCachePreventionHeaders } from './utils.js';
25
27
 
26
28
  const sessionHeaderName = 'x-workos-session';
27
29
  const middlewareHeaderName = 'x-workos-middleware';
@@ -149,15 +151,6 @@ async function updateSessionMiddleware(
149
151
  eagerAuth,
150
152
  });
151
153
 
152
- // If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
153
- if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
154
- if (debug) {
155
- console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
156
- }
157
-
158
- return redirectWithFallback(authorizationUrl as string, headers);
159
- }
160
-
161
154
  // Record the sign up paths so we can use them later
162
155
  if (signUpPaths.length > 0) {
163
156
  headers.set(signUpPathsHeaderName, signUpPaths.join(','));
@@ -165,33 +158,16 @@ async function updateSessionMiddleware(
165
158
 
166
159
  applyCacheSecurityHeaders(headers, request, session);
167
160
 
168
- // Create a new request with modified headers (for page handlers)
169
- const requestHeaders = new Headers(request.headers);
170
- requestHeaders.set(middlewareHeaderName, headers.get(middlewareHeaderName)!);
171
- requestHeaders.set('x-url', headers.get('x-url')!);
172
- if (headers.has('x-redirect-uri')) {
173
- requestHeaders.set('x-redirect-uri', headers.get('x-redirect-uri')!);
174
- }
175
- if (headers.has(signUpPathsHeaderName)) {
176
- requestHeaders.set(signUpPathsHeaderName, headers.get(signUpPathsHeaderName)!);
177
- }
161
+ // If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
162
+ if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
163
+ if (debug) {
164
+ console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
165
+ }
178
166
 
179
- // Pass session to page handlers via request header
180
- // This ensures handlers see refreshed sessions immediately (before Set-Cookie reaches browser)
181
- const sessionHeader = headers.get(sessionHeaderName);
182
- if (sessionHeader) {
183
- requestHeaders.set(sessionHeaderName, sessionHeader);
167
+ return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl as string });
184
168
  }
185
169
 
186
- // Remove session header from response headers to prevent leakage
187
- headers.delete(sessionHeaderName);
188
-
189
- return NextResponse.next({
190
- request: {
191
- headers: requestHeaders,
192
- },
193
- headers,
194
- });
170
+ return handleAuthkitHeaders(request, headers);
195
171
  }
196
172
 
197
173
  async function updateSession(
@@ -406,9 +382,11 @@ async function refreshSession({
406
382
  organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
407
383
  });
408
384
  } catch (error) {
409
- throw new Error(`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`, {
410
- cause: error,
411
- });
385
+ throw new TokenRefreshError(
386
+ `Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`,
387
+ error,
388
+ getSessionErrorContext(session),
389
+ );
412
390
  }
413
391
 
414
392
  const headersList = await headers();
package/src/utils.spec.ts CHANGED
@@ -3,23 +3,21 @@ import { redirectWithFallback, errorResponseWithFallback } from './utils.js';
3
3
 
4
4
  describe('utils', () => {
5
5
  afterEach(() => {
6
- jest.resetModules();
6
+ vi.resetModules();
7
+ vi.restoreAllMocks();
7
8
  });
8
9
 
9
10
  describe('redirectWithFallback', () => {
10
11
  it('uses NextResponse.redirect when available', () => {
11
12
  const redirectUrl = 'https://example.com';
12
- const mockRedirect = jest.fn().mockReturnValue('redirected');
13
- const originalRedirect = NextResponse.redirect;
13
+ const mockRedirect = vi.fn().mockReturnValue('redirected');
14
14
 
15
- NextResponse.redirect = mockRedirect;
15
+ vi.spyOn(NextResponse, 'redirect').mockImplementation(mockRedirect);
16
16
 
17
17
  const result = redirectWithFallback(redirectUrl);
18
18
 
19
19
  expect(mockRedirect).toHaveBeenCalledWith(redirectUrl, { headers: undefined });
20
20
  expect(result).toBe('redirected');
21
-
22
- NextResponse.redirect = originalRedirect;
23
21
  });
24
22
 
25
23
  it('uses headers when provided', () => {
@@ -35,9 +33,9 @@ describe('utils', () => {
35
33
  it('falls back to standard Response when NextResponse exists but redirect is undefined', async () => {
36
34
  const redirectUrl = 'https://example.com';
37
35
 
38
- jest.resetModules();
36
+ vi.resetModules();
39
37
 
40
- jest.mock('next/server', () => ({
38
+ vi.doMock('next/server', () => ({
41
39
  NextResponse: {
42
40
  // exists but has no redirect method
43
41
  },
@@ -55,10 +53,10 @@ describe('utils', () => {
55
53
  it('falls back to standard Response when NextResponse is undefined', async () => {
56
54
  const redirectUrl = 'https://example.com';
57
55
 
58
- jest.resetModules();
56
+ vi.resetModules();
59
57
 
60
58
  // Mock with undefined NextResponse
61
- jest.mock('next/server', () => ({
59
+ vi.doMock('next/server', () => ({
62
60
  NextResponse: undefined,
63
61
  }));
64
62
 
@@ -81,8 +79,8 @@ describe('utils', () => {
81
79
  };
82
80
 
83
81
  it('uses NextResponse.json when available', () => {
84
- const mockJson = jest.fn().mockReturnValue('error json response');
85
- NextResponse.json = mockJson;
82
+ const mockJson = vi.fn().mockReturnValue('error json response');
83
+ vi.spyOn(NextResponse, 'json').mockImplementation(mockJson);
86
84
 
87
85
  const result = errorResponseWithFallback(errorBody);
88
86
 
@@ -90,25 +88,10 @@ describe('utils', () => {
90
88
  expect(result).toBe('error json response');
91
89
  });
92
90
 
93
- it('falls back to standard Response when NextResponse is not available', () => {
94
- const originalJson = NextResponse.json;
95
-
96
- // @ts-expect-error - This is to test the fallback
97
- delete NextResponse.json;
98
-
99
- const result = errorResponseWithFallback(errorBody);
100
-
101
- expect(result).toBeInstanceOf(Response);
102
- expect(result.status).toBe(500);
103
- expect(result.headers.get('Content-Type')).toBe('application/json');
104
-
105
- NextResponse.json = originalJson;
106
- });
107
-
108
91
  it('falls back to standard Response when NextResponse exists but json is undefined', async () => {
109
- jest.resetModules();
92
+ vi.resetModules();
110
93
 
111
- jest.mock('next/server', () => ({
94
+ vi.doMock('next/server', () => ({
112
95
  NextResponse: {
113
96
  // exists but has no json method
114
97
  },
@@ -124,9 +107,9 @@ describe('utils', () => {
124
107
  });
125
108
 
126
109
  it('falls back to standard Response when NextResponse is undefined', async () => {
127
- jest.resetModules();
110
+ vi.resetModules();
128
111
 
129
- jest.mock('next/server', () => ({
112
+ vi.doMock('next/server', () => ({
130
113
  NextResponse: undefined,
131
114
  }));
132
115
 
package/src/utils.ts CHANGED
@@ -6,8 +6,6 @@ import { NextResponse } from 'next/server';
6
6
  */
7
7
  export function setCachePreventionHeaders(headers: Headers): void {
8
8
  headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0');
9
- headers.set('Pragma', 'no-cache');
10
- headers.set('Expires', '0');
11
9
  headers.set('x-middleware-cache', 'no-cache');
12
10
  }
13
11