@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.1

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 (106) hide show
  1. package/README.md +305 -102
  2. package/dist/esm/actions.js +35 -5
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/auth.js +71 -21
  5. package/dist/esm/auth.js.map +1 -1
  6. package/dist/esm/authkit-callback-route.js +90 -92
  7. package/dist/esm/authkit-callback-route.js.map +1 -1
  8. package/dist/esm/components/authkit-provider.js +36 -15
  9. package/dist/esm/components/authkit-provider.js.map +1 -1
  10. package/dist/esm/components/impersonation.js +17 -15
  11. package/dist/esm/components/impersonation.js.map +1 -1
  12. package/dist/esm/components/min-max-button.js +1 -1
  13. package/dist/esm/components/min-max-button.js.map +1 -1
  14. package/dist/esm/components/tokenStore.js +28 -19
  15. package/dist/esm/components/tokenStore.js.map +1 -1
  16. package/dist/esm/components/useAccessToken.js +1 -1
  17. package/dist/esm/components/useAccessToken.js.map +1 -1
  18. package/dist/esm/components/useTokenClaims.js +1 -1
  19. package/dist/esm/components/useTokenClaims.js.map +1 -1
  20. package/dist/esm/cookie.js +20 -5
  21. package/dist/esm/cookie.js.map +1 -1
  22. package/dist/esm/env-variables.js +6 -6
  23. package/dist/esm/env-variables.js.map +1 -1
  24. package/dist/esm/errors.js +36 -0
  25. package/dist/esm/errors.js.map +1 -0
  26. package/dist/esm/get-authorization-url.js +51 -12
  27. package/dist/esm/get-authorization-url.js.map +1 -1
  28. package/dist/esm/index.js +5 -2
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/interfaces.js +7 -1
  31. package/dist/esm/interfaces.js.map +1 -1
  32. package/dist/esm/middleware-helpers.js +102 -0
  33. package/dist/esm/middleware-helpers.js.map +1 -0
  34. package/dist/esm/middleware.js +3 -1
  35. package/dist/esm/middleware.js.map +1 -1
  36. package/dist/esm/pkce.js +52 -0
  37. package/dist/esm/pkce.js.map +1 -0
  38. package/dist/esm/session.js +82 -35
  39. package/dist/esm/session.js.map +1 -1
  40. package/dist/esm/test-helpers.js +1 -1
  41. package/dist/esm/test-helpers.js.map +1 -1
  42. package/dist/esm/types/actions.d.ts +34 -5
  43. package/dist/esm/types/auth.d.ts +7 -15
  44. package/dist/esm/types/components/authkit-provider.d.ts +6 -2
  45. package/dist/esm/types/components/impersonation.d.ts +2 -1
  46. package/dist/esm/types/cookie.d.ts +9 -0
  47. package/dist/esm/types/env-variables.d.ts +2 -1
  48. package/dist/esm/types/errors.d.ts +15 -0
  49. package/dist/esm/types/get-authorization-url.d.ts +2 -2
  50. package/dist/esm/types/index.d.ts +5 -2
  51. package/dist/esm/types/interfaces.d.ts +12 -0
  52. package/dist/esm/types/jwt.d.ts +9 -9
  53. package/dist/esm/types/middleware-helpers.d.ts +27 -0
  54. package/dist/esm/types/middleware.d.ts +3 -1
  55. package/dist/esm/types/pkce.d.ts +17 -0
  56. package/dist/esm/types/session.d.ts +1 -1
  57. package/dist/esm/types/utils.d.ts +5 -0
  58. package/dist/esm/types/validate-api-key.d.ts +1 -0
  59. package/dist/esm/types/workos.d.ts +1 -1
  60. package/dist/esm/utils.js +10 -2
  61. package/dist/esm/utils.js.map +1 -1
  62. package/dist/esm/validate-api-key.js +16 -0
  63. package/dist/esm/validate-api-key.js.map +1 -0
  64. package/dist/esm/workos.js +1 -1
  65. package/package.json +33 -34
  66. package/src/actions.spec.ts +91 -18
  67. package/src/actions.ts +44 -6
  68. package/src/auth.spec.ts +79 -29
  69. package/src/auth.ts +74 -42
  70. package/src/authkit-callback-route.spec.ts +372 -58
  71. package/src/authkit-callback-route.ts +121 -103
  72. package/src/components/authkit-provider.spec.tsx +264 -70
  73. package/src/components/authkit-provider.tsx +40 -15
  74. package/src/components/button.spec.tsx +4 -6
  75. package/src/components/impersonation.spec.tsx +152 -35
  76. package/src/components/impersonation.tsx +37 -30
  77. package/src/components/min-max-button.spec.tsx +2 -1
  78. package/src/components/tokenStore.spec.ts +59 -44
  79. package/src/components/tokenStore.ts +11 -3
  80. package/src/components/useAccessToken.spec.tsx +82 -83
  81. package/src/components/useTokenClaims.spec.tsx +23 -22
  82. package/src/cookie.spec.ts +63 -9
  83. package/src/cookie.ts +35 -0
  84. package/src/env-variables.ts +2 -0
  85. package/src/errors.spec.ts +108 -0
  86. package/src/errors.ts +46 -0
  87. package/src/get-authorization-url.spec.ts +170 -15
  88. package/src/get-authorization-url.ts +69 -23
  89. package/src/index.ts +20 -2
  90. package/src/interfaces.ts +15 -0
  91. package/src/jwt.ts +9 -9
  92. package/src/middleware-helpers.spec.ts +238 -0
  93. package/src/middleware-helpers.ts +134 -0
  94. package/src/middleware.spec.ts +25 -0
  95. package/src/middleware.ts +4 -1
  96. package/src/pkce.spec.ts +146 -0
  97. package/src/pkce.ts +59 -0
  98. package/src/session.spec.ts +87 -89
  99. package/src/session.ts +104 -27
  100. package/src/test-helpers.ts +1 -1
  101. package/src/utils.spec.ts +14 -31
  102. package/src/utils.ts +9 -0
  103. package/src/validate-api-key.spec.ts +111 -0
  104. package/src/validate-api-key.ts +19 -0
  105. package/src/workos.spec.ts +2 -2
  106. package/src/workos.ts +1 -1
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';
8
- import { getCookieOptions, getJwtCookie } from './cookie.js';
7
+ import { NextRequest } from 'next/server';
8
+ import { getCookieOptions, getJwtCookie, getPKCECookieOptions } 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,
@@ -17,11 +18,30 @@ import {
17
18
  Session,
18
19
  UserInfo,
19
20
  } from './interfaces.js';
21
+ import { getPKCECookieNameForState, setPKCECookie } from './pkce.js';
20
22
  import { getWorkOS } from './workos.js';
21
23
 
22
24
  import type { AuthenticationResponse } from '@workos-inc/node';
23
25
  import { parse, tokensToRegexp } from 'path-to-regexp';
24
- import { lazy, redirectWithFallback } from './utils.js';
26
+ import { handleAuthkitHeaders } from './middleware-helpers.js';
27
+ import { lazy, setCachePreventionHeaders } from './utils.js';
28
+
29
+ // Only set the PKCE cookie for initial document navigations — fetch/XHR/RSC/prefetch
30
+ // requests never follow cross-origin redirects so they'll never complete the OAuth
31
+ // flow and therefore don't need the cookie set.
32
+ // This prevents cookie bloat (HTTP 431) when multiple requests fire concurrently
33
+ // now that we are generating unique cookie names per flow, they add up quickly if
34
+ // we don't limit to just the initial navigation request
35
+ function appendPKCESetCookieHeader(request: NextRequest, headers: Headers, sealedState: string): void {
36
+ if (!isInitialDocumentRequest(request)) {
37
+ return;
38
+ }
39
+
40
+ headers.append(
41
+ 'Set-Cookie',
42
+ `${getPKCECookieNameForState(sealedState)}=${sealedState}; ${getPKCECookieOptions(request.url, true)}`,
43
+ );
44
+ }
25
45
 
26
46
  const sessionHeaderName = 'x-workos-session';
27
47
  const middlewareHeaderName = 'x-workos-middleware';
@@ -30,6 +50,49 @@ const jwtCookieName = 'workos-access-token';
30
50
 
31
51
  const JWKS = lazy(() => createRemoteJWKSet(new URL(getWorkOS().userManagement.getJwksUrl(WORKOS_CLIENT_ID))));
32
52
 
53
+ /**
54
+ * Applies cache security headers with Vary header deduplication.
55
+ * Only applies headers if the request is authenticated (has session, cookie, or Authorization header).
56
+ * Used in middleware where existing Vary headers may already be present.
57
+ * @param headers - The Headers object to set the cache security headers on.
58
+ * @param request - The NextRequest object to check for authentication.
59
+ * @param sessionData - Optional session data to check for authentication.
60
+ */
61
+ function applyCacheSecurityHeaders(
62
+ headers: Headers,
63
+ request: NextRequest,
64
+ sessionData?: { accessToken?: string } | Session,
65
+ ): void {
66
+ const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
67
+
68
+ // Only apply cache headers for authenticated requests
69
+ if (!sessionData?.accessToken && !request.cookies.has(cookieName) && !request.headers.has('authorization')) {
70
+ return;
71
+ }
72
+
73
+ const varyValues = new Set<string>(['cookie']);
74
+ if (request.headers.has('authorization')) {
75
+ varyValues.add('authorization');
76
+ }
77
+
78
+ const currentVary = headers.get('Vary');
79
+ if (currentVary) {
80
+ currentVary.split(',').forEach((v) => {
81
+ const trimmed = v.trim().toLowerCase();
82
+ if (trimmed) varyValues.add(trimmed);
83
+ });
84
+ }
85
+
86
+ headers.set(
87
+ 'Vary',
88
+ Array.from(varyValues)
89
+ .map((v) => v.charAt(0).toUpperCase() + v.slice(1))
90
+ .join(', '),
91
+ );
92
+
93
+ setCachePreventionHeaders(headers);
94
+ }
95
+
33
96
  /**
34
97
  * Determines if a request is for an initial document load (not API/RSC/prefetch)
35
98
  */
@@ -106,23 +169,23 @@ async function updateSessionMiddleware(
106
169
  eagerAuth,
107
170
  });
108
171
 
172
+ // Record the sign up paths so we can use them later
173
+ if (signUpPaths.length > 0) {
174
+ headers.set(signUpPathsHeaderName, signUpPaths.join(','));
175
+ }
176
+
177
+ applyCacheSecurityHeaders(headers, request, session);
178
+
109
179
  // If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
110
180
  if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
111
181
  if (debug) {
112
182
  console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
113
183
  }
114
184
 
115
- return redirectWithFallback(authorizationUrl as string, headers);
116
- }
117
-
118
- // Record the sign up paths so we can use them later
119
- if (signUpPaths.length > 0) {
120
- headers.set(signUpPathsHeaderName, signUpPaths.join(','));
185
+ return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl as string });
121
186
  }
122
187
 
123
- return NextResponse.next({
124
- headers,
125
- });
188
+ return handleAuthkitHeaders(request, headers);
126
189
  }
127
190
 
128
191
  async function updateSession(
@@ -157,14 +220,18 @@ async function updateSession(
157
220
  console.log('No session found from cookie');
158
221
  }
159
222
 
223
+ const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
224
+ returnPathname: getReturnPathname(request.url),
225
+ redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
226
+ screenHint: options.screenHint,
227
+ });
228
+
229
+ appendPKCESetCookieHeader(request, newRequestHeaders, sealedState);
230
+
160
231
  return {
161
232
  session: { user: null },
162
233
  headers: newRequestHeaders,
163
- authorizationUrl: await getAuthorizationUrl({
164
- returnPathname: getReturnPathname(request.url),
165
- redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
166
- screenHint: options.screenHint,
167
- }),
234
+ authorizationUrl,
168
235
  };
169
236
  }
170
237
 
@@ -172,6 +239,8 @@ async function updateSession(
172
239
 
173
240
  const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
174
241
 
242
+ applyCacheSecurityHeaders(newRequestHeaders, request, session);
243
+
175
244
  if (hasValidSession) {
176
245
  newRequestHeaders.set(sessionHeaderName, request.cookies.get(cookieName)!.value);
177
246
 
@@ -293,13 +362,17 @@ async function updateSession(
293
362
 
294
363
  options.onSessionRefreshError?.({ error: e, request });
295
364
 
365
+ const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
366
+ returnPathname: getReturnPathname(request.url),
367
+ redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
368
+ });
369
+
370
+ appendPKCESetCookieHeader(request, newRequestHeaders, sealedState);
371
+
296
372
  return {
297
373
  session: { user: null },
298
374
  headers: newRequestHeaders,
299
- authorizationUrl: await getAuthorizationUrl({
300
- returnPathname: getReturnPathname(request.url),
301
- redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
302
- }),
375
+ authorizationUrl,
303
376
  };
304
377
  }
305
378
  }
@@ -335,9 +408,11 @@ async function refreshSession({
335
408
  organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
336
409
  });
337
410
  } catch (error) {
338
- throw new Error(`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`, {
339
- cause: error,
340
- });
411
+ throw new TokenRefreshError(
412
+ `Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`,
413
+ error,
414
+ getSessionErrorContext(session),
415
+ );
341
416
  }
342
417
 
343
418
  const headersList = await headers();
@@ -404,7 +479,9 @@ async function redirectToSignIn() {
404
479
 
405
480
  const returnPathname = getReturnPathname(url);
406
481
 
407
- redirect(await getAuthorizationUrl({ returnPathname, screenHint }));
482
+ const { url: authkitUrl, sealedState } = await getAuthorizationUrl({ returnPathname, screenHint });
483
+ await setPKCECookie(sealedState);
484
+ redirect(authkitUrl);
408
485
  }
409
486
 
410
487
  export async function getTokenClaims<T = Record<string, unknown>>(
@@ -488,7 +565,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
488
565
  if (!hasMiddleware) {
489
566
  const url = headersList.get('x-url');
490
567
  throw new Error(
491
- `You are calling 'withAuth' on ${url ?? 'a route'} that isnt covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`,
568
+ `You are calling 'withAuth' on ${url ?? 'a route'} that isn't covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`,
492
569
  );
493
570
  }
494
571
 
@@ -501,7 +578,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
501
578
  function getReturnPathname(url: string): string {
502
579
  const newUrl = new URL(url);
503
580
 
504
- return `${newUrl.pathname}${newUrl.searchParams.size > 0 ? '?' + newUrl.searchParams.toString() : ''}`;
581
+ return `${newUrl.pathname}${newUrl.search}`;
505
582
  }
506
583
 
507
584
  function getScreenHint(signUpPaths: string[] | undefined, pathname: string) {
@@ -43,9 +43,9 @@ export async function generateSession(overrides: Partial<User> = {}) {
43
43
  createdAt: '2024-01-01T00:00:00Z',
44
44
  updatedAt: '2024-01-01T00:00:00Z',
45
45
  lastSignInAt: '2024-01-01T00:00:00Z',
46
- locale: null,
47
46
  externalId: null,
48
47
  metadata: {},
48
+ locale: null,
49
49
  ...overrides,
50
50
  } satisfies User;
51
51
 
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
@@ -1,5 +1,14 @@
1
1
  import { NextResponse } from 'next/server';
2
2
 
3
+ /**
4
+ * Sets cache prevention headers to prevent CDN/proxy caching.
5
+ * @param headers - The Headers object to set the cache prevention headers on.
6
+ */
7
+ export function setCachePreventionHeaders(headers: Headers): void {
8
+ headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0');
9
+ headers.set('x-middleware-cache', 'no-cache');
10
+ }
11
+
3
12
  export function redirectWithFallback(redirectUri: string, headers?: Headers) {
4
13
  const newHeaders = headers ? new Headers(headers) : new Headers();
5
14
  newHeaders.set('Location', redirectUri);
@@ -0,0 +1,111 @@
1
+ import { validateApiKey } from './validate-api-key.js';
2
+ import { getWorkOS } from './workos.js';
3
+
4
+ // These are mocked in vitest.setup.ts
5
+ import { headers } from 'next/headers';
6
+
7
+ const workos = getWorkOS();
8
+
9
+ describe('validate-api-key.ts', () => {
10
+ beforeEach(async () => {
11
+ // Clear all mocks between tests
12
+ vi.clearAllMocks();
13
+
14
+ const nextHeaders = await headers();
15
+ // @ts-expect-error - _reset is part of the mock
16
+ nextHeaders._reset();
17
+ });
18
+
19
+ describe('validateApiKey', () => {
20
+ it('should return valid API key when Bearer token is present and valid', async () => {
21
+ const mockApiKeyResponse = {
22
+ apiKey: {
23
+ id: 'api_key_123',
24
+ object: 'api_key' as const,
25
+ name: 'Test API Key',
26
+ obfuscatedValue: 'sk_…7890',
27
+ createdAt: '2024-01-01T00:00:00Z',
28
+ updatedAt: '2024-01-01T00:00:00Z',
29
+ lastUsedAt: '2024-01-01T00:00:00Z',
30
+ permissions: [],
31
+ owner: { type: 'organization' as const, id: 'org_123' },
32
+ },
33
+ };
34
+
35
+ vi.spyOn(workos.apiKeys, 'validateApiKey').mockResolvedValue(mockApiKeyResponse);
36
+
37
+ const nextHeaders = await headers();
38
+ nextHeaders.set('authorization', 'Bearer sk_test_1234567890');
39
+
40
+ const result = await validateApiKey();
41
+
42
+ expect(workos.apiKeys.validateApiKey).toHaveBeenCalledWith({
43
+ value: 'sk_test_1234567890',
44
+ });
45
+ expect(result).toEqual(mockApiKeyResponse);
46
+ });
47
+
48
+ it('should return { apiKey: null } when no authorization header is present', async () => {
49
+ // Don't set any authorization header
50
+ const result = await validateApiKey();
51
+
52
+ expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
53
+ expect(result).toEqual({ apiKey: null });
54
+ });
55
+
56
+ it('should return { apiKey: null } when authorization header is empty', async () => {
57
+ const nextHeaders = await headers();
58
+ nextHeaders.set('authorization', '');
59
+
60
+ const result = await validateApiKey();
61
+
62
+ expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
63
+ expect(result).toEqual({ apiKey: null });
64
+ });
65
+
66
+ it('should return { apiKey: null } when authorization header does not start with Bearer', async () => {
67
+ const nextHeaders = await headers();
68
+ nextHeaders.set('authorization', 'Basic dXNlcjpwYXNz');
69
+
70
+ const result = await validateApiKey();
71
+
72
+ expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
73
+ expect(result).toEqual({ apiKey: null });
74
+ });
75
+
76
+ it('should return { apiKey: null } when Bearer token is missing', async () => {
77
+ const nextHeaders = await headers();
78
+ nextHeaders.set('authorization', 'Bearer');
79
+
80
+ const result = await validateApiKey();
81
+
82
+ expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
83
+ expect(result).toEqual({ apiKey: null });
84
+ });
85
+
86
+ it('should return { apiKey: null } when Bearer token is only whitespace', async () => {
87
+ const nextHeaders = await headers();
88
+ nextHeaders.set('authorization', 'Bearer ');
89
+
90
+ const result = await validateApiKey();
91
+
92
+ expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
93
+ expect(result).toEqual({ apiKey: null });
94
+ });
95
+
96
+ it('should return { apiKey: null } when WorkOS validation fails', async () => {
97
+ const mockResponse = { apiKey: null };
98
+ vi.spyOn(workos.apiKeys, 'validateApiKey').mockResolvedValue(mockResponse);
99
+
100
+ const nextHeaders = await headers();
101
+ nextHeaders.set('authorization', 'Bearer invalid_key');
102
+
103
+ const result = await validateApiKey();
104
+
105
+ expect(workos.apiKeys.validateApiKey).toHaveBeenCalledWith({
106
+ value: 'invalid_key',
107
+ });
108
+ expect(result).toEqual({ apiKey: null });
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,19 @@
1
+ 'use server';
2
+
3
+ import { getWorkOS } from './workos.js';
4
+ import { headers } from 'next/headers';
5
+
6
+ export async function validateApiKey() {
7
+ const headersList = await headers();
8
+ const authorizationHeader = headersList.get('authorization');
9
+ if (!authorizationHeader) {
10
+ return { apiKey: null };
11
+ }
12
+
13
+ const value = authorizationHeader.match(/Bearer\s+(.*)/i)?.[1];
14
+ if (!value) {
15
+ return { apiKey: null };
16
+ }
17
+
18
+ return getWorkOS().apiKeys.validateApiKey({ value });
19
+ }
@@ -4,7 +4,7 @@ import { getWorkOS, VERSION } from './workos.js';
4
4
  describe('workos', () => {
5
5
  const workos = getWorkOS();
6
6
  beforeEach(() => {
7
- jest.clearAllMocks();
7
+ vi.clearAllMocks();
8
8
  });
9
9
 
10
10
  it('initializes WorkOS with the correct configuration', () => {
@@ -35,7 +35,7 @@ describe('workos', () => {
35
35
  const originalEnv = process.env;
36
36
 
37
37
  beforeEach(() => {
38
- jest.resetModules();
38
+ vi.resetModules();
39
39
  process.env = { ...originalEnv };
40
40
  });
41
41
 
package/src/workos.ts CHANGED
@@ -2,7 +2,7 @@ import { WorkOS } from '@workos-inc/node';
2
2
  import { WORKOS_API_HOSTNAME, WORKOS_API_KEY, WORKOS_API_HTTPS, WORKOS_API_PORT } from './env-variables.js';
3
3
  import { lazy } from './utils.js';
4
4
 
5
- export const VERSION = '2.10.0';
5
+ export const VERSION = '2.14.0';
6
6
 
7
7
  const options = {
8
8
  apiHostname: WORKOS_API_HOSTNAME,