@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.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 (106) hide show
  1. package/README.md +276 -102
  2. package/dist/esm/actions.js +35 -4
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/auth.js +51 -20
  5. package/dist/esm/auth.js.map +1 -1
  6. package/dist/esm/authkit-callback-route.js +82 -93
  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 +16 -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 +38 -0
  37. package/dist/esm/pkce.js.map +1 -0
  38. package/dist/esm/session.js +73 -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 +8 -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 +12 -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 +32 -34
  66. package/src/actions.spec.ts +94 -17
  67. package/src/actions.ts +44 -5
  68. package/src/auth.spec.ts +60 -29
  69. package/src/auth.ts +55 -41
  70. package/src/authkit-callback-route.spec.ts +310 -58
  71. package/src/authkit-callback-route.ts +106 -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 +14 -9
  83. package/src/cookie.ts +29 -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 +125 -0
  97. package/src/pkce.ts +42 -0
  98. package/src/session.spec.ts +87 -89
  99. package/src/session.ts +91 -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
@@ -0,0 +1,12 @@
1
+ import { State } from './interfaces.js';
2
+ export declare const PKCE_COOKIE_NAME = "wos-auth-verifier";
3
+ /**
4
+ * Set the PKCE verifier cookie in server action context.
5
+ * In middleware context, callers must set the cookie via Set-Cookie headers instead.
6
+ */
7
+ export declare function setPKCECookie(sealedState: string): Promise<void>;
8
+ /**
9
+ * Read and unseal the auth cookie containing PKCE code verifier and OAuth state.
10
+ * Throws if the cookie is not in the required state
11
+ */
12
+ export declare function getStateFromPKCECookieValue(cookieValue: string): Promise<State>;
@@ -3,7 +3,7 @@ import { NextRequest } from 'next/server';
3
3
  import { AuthkitMiddlewareAuth, AuthkitOptions, AuthkitResponse, NoUserInfo, Session, UserInfo } from './interfaces.js';
4
4
  import type { AuthenticationResponse } from '@workos-inc/node';
5
5
  declare function encryptSession(session: Session): Promise<string>;
6
- declare function updateSessionMiddleware(request: NextRequest, debug: boolean, middlewareAuth: AuthkitMiddlewareAuth, redirectUri: string, signUpPaths: string[], eagerAuth?: boolean): Promise<Response>;
6
+ declare function updateSessionMiddleware(request: NextRequest, debug: boolean, middlewareAuth: AuthkitMiddlewareAuth, redirectUri: string, signUpPaths: string[], eagerAuth?: boolean): Promise<import("next/server.js").NextResponse<unknown>>;
7
7
  declare function updateSession(request: NextRequest, options?: AuthkitOptions): Promise<AuthkitResponse>;
8
8
  declare function refreshSession(options: {
9
9
  organizationId?: string;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Sets cache prevention headers to prevent CDN/proxy caching.
3
+ * @param headers - The Headers object to set the cache prevention headers on.
4
+ */
5
+ export declare function setCachePreventionHeaders(headers: Headers): void;
1
6
  export declare function redirectWithFallback(redirectUri: string, headers?: Headers): Response;
2
7
  export declare function errorResponseWithFallback(errorBody: {
3
8
  error: {
@@ -0,0 +1 @@
1
+ export declare function validateApiKey(): Promise<import("@workos-inc/node").ValidateApiKeyResponse>;
@@ -1,5 +1,5 @@
1
1
  import { WorkOS } from '@workos-inc/node';
2
- export declare const VERSION = "2.10.0";
2
+ export declare const VERSION = "2.14.0";
3
3
  /**
4
4
  * Create a WorkOS instance with the provided API key and options.
5
5
  * If an instance already exists, it returns the existing instance.
package/dist/esm/utils.js CHANGED
@@ -1,17 +1,25 @@
1
1
  import { NextResponse } from 'next/server';
2
+ /**
3
+ * Sets cache prevention headers to prevent CDN/proxy caching.
4
+ * @param headers - The Headers object to set the cache prevention headers on.
5
+ */
6
+ export function setCachePreventionHeaders(headers) {
7
+ headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0');
8
+ headers.set('x-middleware-cache', 'no-cache');
9
+ }
2
10
  export function redirectWithFallback(redirectUri, headers) {
3
11
  const newHeaders = headers ? new Headers(headers) : new Headers();
4
12
  newHeaders.set('Location', redirectUri);
5
13
  // Fall back to standard Response if NextResponse is not available.
6
14
  // This is to support Next.js 13.
7
- return (NextResponse === null || NextResponse === void 0 ? void 0 : NextResponse.redirect)
15
+ return NextResponse?.redirect
8
16
  ? NextResponse.redirect(redirectUri, { headers })
9
17
  : new Response(null, { status: 307, headers: newHeaders });
10
18
  }
11
19
  export function errorResponseWithFallback(errorBody) {
12
20
  // Fall back to standard Response if NextResponse is not available.
13
21
  // This is to support Next.js 13.
14
- return (NextResponse === null || NextResponse === void 0 ? void 0 : NextResponse.json)
22
+ return NextResponse?.json
15
23
  ? NextResponse.json(errorBody, { status: 500 })
16
24
  : new Response(JSON.stringify(errorBody), {
17
25
  status: 500,
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,UAAU,oBAAoB,CAAC,WAAmB,EAAE,OAAiB;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;IAClE,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAExC,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,CAAA,YAAY,aAAZ,YAAY,uBAAZ,YAAY,CAAE,QAAQ;QAC3B,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC;QACjD,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,SAA8D;IACtG,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,CAAA,YAAY,aAAZ,YAAY,uBAAZ,YAAY,CAAE,IAAI;QACvB,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC/C,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE;YACtC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;AACT,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAAI,EAAW;IACjC,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAS,CAAC;IACd,OAAO,GAAG,EAAE;QACV,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,EAAE,CAAC;YACd,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,yDAAyD,CAAC,CAAC;IACxF,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,WAAmB,EAAE,OAAiB;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;IAClE,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAExC,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,QAAQ;QAC3B,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC;QACjD,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,SAA8D;IACtG,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,IAAI;QACvB,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC/C,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE;YACtC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;AACT,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAAI,EAAW;IACjC,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAS,CAAC;IACd,OAAO,GAAG,EAAE;QACV,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,EAAE,CAAC;YACd,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,16 @@
1
+ 'use server';
2
+ import { getWorkOS } from './workos.js';
3
+ import { headers } from 'next/headers';
4
+ export async function validateApiKey() {
5
+ const headersList = await headers();
6
+ const authorizationHeader = headersList.get('authorization');
7
+ if (!authorizationHeader) {
8
+ return { apiKey: null };
9
+ }
10
+ const value = authorizationHeader.match(/Bearer\s+(.*)/i)?.[1];
11
+ if (!value) {
12
+ return { apiKey: null };
13
+ }
14
+ return getWorkOS().apiKeys.validateApiKey({ value });
15
+ }
16
+ //# sourceMappingURL=validate-api-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-api-key.js","sourceRoot":"","sources":["../../src/validate-api-key.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;IACpC,MAAM,mBAAmB,GAAG,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC7D,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,KAAK,GAAG,mBAAmB,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED,OAAO,SAAS,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACvD,CAAC"}
@@ -1,7 +1,7 @@
1
1
  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
- export const VERSION = '2.10.0';
4
+ export const VERSION = '2.14.0';
5
5
  const options = {
6
6
  apiHostname: WORKOS_API_HOSTNAME,
7
7
  https: WORKOS_API_HTTPS ? WORKOS_API_HTTPS === 'true' : true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos-inc/authkit-nextjs",
3
- "version": "3.0.0-beta.1",
3
+ "version": "3.0.0",
4
4
  "description": "Authentication and session helpers for using WorkOS & AuthKit with Next.js",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -22,47 +22,33 @@
22
22
  "import": "./dist/esm/index.js"
23
23
  }
24
24
  },
25
- "scripts": {
26
- "clean": "rm -rf dist",
27
- "prebuild": "npm run clean",
28
- "build": "tsc --project tsconfig.json",
29
- "prepublishOnly": "npm run lint",
30
- "lint": "eslint \"src/**/*.ts*\"",
31
- "test": "jest",
32
- "test:watch": "jest --watch",
33
- "prettier": "prettier \"src/**/*.{js,ts,tsx}\" --check",
34
- "format": "prettier \"src/**/*.{js,ts,tsx}\" --write",
35
- "type-check": "tsc --project tsconfig.json --noEmit"
36
- },
37
25
  "dependencies": {
38
- "@workos-inc/node": "^8.0.0-rc.1",
39
- "iron-session": "^8.0.1",
40
- "jose": "^5.2.3",
41
- "path-to-regexp": "^6.2.2"
26
+ "@workos-inc/node": "^8.9.0",
27
+ "iron-session": "^8.0.4",
28
+ "jose": "^5.10.0",
29
+ "path-to-regexp": "^6.3.0",
30
+ "valibot": "^1.2.0"
42
31
  },
43
32
  "peerDependencies": {
44
- "next": "^13.5.9 || ^14.2.26 || ^15.2.3",
33
+ "next": "^13.5.9 || ^14.2.26 || ^15.2.3 || ^16",
45
34
  "react": "^18.0 || ^19.0.0",
46
35
  "react-dom": "^18.0 || ^19.0.0"
47
36
  },
48
37
  "devDependencies": {
49
- "@testing-library/jest-dom": "^6.6.3",
50
- "@testing-library/react": "^16.0.1",
51
- "@types/jest": "^29.5.14",
52
- "@types/node": "^20.11.28",
38
+ "@testing-library/jest-dom": "^6.9.1",
39
+ "@testing-library/react": "^16.3.2",
40
+ "@types/node": "^20.19.35",
53
41
  "@types/react": "18.2.67",
54
42
  "@types/react-dom": "18.2.22",
55
- "eslint": "^8.29.0",
56
- "eslint-config-prettier": "^9.1.0",
57
- "eslint-plugin-require-extensions": "^0.1.3",
58
- "jest": "^29.7.0",
59
- "jest-environment-jsdom": "^29.7.0",
60
- "next": "^15.0.1",
61
- "prettier": "^3.3.3",
62
- "ts-jest": "^29.2.5",
63
- "ts-node": "^10.9.2",
64
- "typescript": "5.4.2",
65
- "typescript-eslint": "^7.2.0"
43
+ "@vitest/coverage-v8": "^3.2.4",
44
+ "jsdom": "^26.1.0",
45
+ "next": "^16.2.1",
46
+ "oxfmt": "^0.42.0",
47
+ "oxlint": "^1.57.0",
48
+ "tslib": "^2.8.1",
49
+ "typescript": "6.0.2",
50
+ "typescript-eslint": "^7.18.0",
51
+ "vitest": "^3.2.4"
66
52
  },
67
53
  "license": "MIT",
68
54
  "homepage": "https://github.com/workos/authkit-nextjs#readme",
@@ -72,5 +58,17 @@
72
58
  },
73
59
  "bugs": {
74
60
  "url": "https://github.com/workos/authkit-nextjs/issues"
61
+ },
62
+ "scripts": {
63
+ "clean": "rm -rf dist",
64
+ "prebuild": "pnpm run clean",
65
+ "build": "tsc --project tsconfig.app.json",
66
+ "test": "vitest run",
67
+ "test:watch": "vitest",
68
+ "test:coverage": "vitest run --coverage",
69
+ "lint": "oxlint",
70
+ "format": "oxfmt .",
71
+ "format:check": "oxfmt --check .",
72
+ "typecheck": "tsc --project tsconfig.app.json --noEmit && tsc --project tsconfig.test.json --noEmit"
75
73
  }
76
- }
74
+ }
@@ -11,28 +11,51 @@ import {
11
11
  import { signOut, switchToOrganization } from './auth.js';
12
12
  import { getWorkOS } from '../src/workos.js';
13
13
  import { withAuth, refreshSession } from '../src/session.js';
14
+ import { getAuthorizationUrl } from '../src/get-authorization-url.js';
14
15
 
15
- jest.mock('../src/auth.js', () => ({
16
- signOut: jest.fn().mockResolvedValue(true),
17
- switchToOrganization: jest.fn().mockResolvedValue({ organizationId: 'org_123' }),
16
+ vi.mock('../src/auth.js', () => ({
17
+ signOut: vi.fn().mockResolvedValue(true),
18
+ switchToOrganization: vi.fn().mockResolvedValue({ organizationId: 'org_123' }),
18
19
  }));
19
20
 
20
- const fakeWorkosInstance = {
21
- organizations: {
22
- getOrganization: jest.fn().mockResolvedValue({ id: 'org_123', name: 'Test Org' }),
21
+ const { fakeWorkosInstance } = vi.hoisted(() => ({
22
+ fakeWorkosInstance: {
23
+ organizations: {
24
+ getOrganization: vi.fn().mockResolvedValue({ id: 'org_123', name: 'Test Org' }),
25
+ },
23
26
  },
24
- };
25
- jest.mock('../src/workos.js', () => ({
26
- getWorkOS: jest.fn(() => fakeWorkosInstance),
27
+ }));
28
+ vi.mock('../src/workos.js', () => ({
29
+ getWorkOS: vi.fn(() => fakeWorkosInstance),
30
+ }));
31
+
32
+ vi.mock('../src/session.js', () => ({
33
+ withAuth: vi.fn().mockResolvedValue({ user: 'testUser', accessToken: 'access_token' }),
34
+ refreshSession: vi.fn().mockResolvedValue({ user: 'testUser', accessToken: 'refreshed_token' }),
27
35
  }));
28
36
 
29
- jest.mock('../src/session.js', () => ({
30
- withAuth: jest.fn().mockResolvedValue({ user: 'testUser', accessToken: 'access_token' }),
31
- refreshSession: jest.fn().mockResolvedValue({ session: 'newSession', accessToken: 'refreshed_token' }),
37
+ vi.mock('../src/get-authorization-url.js', () => ({
38
+ getAuthorizationUrl: vi.fn().mockResolvedValue('https://api.workos.com/authorize?...'),
32
39
  }));
33
40
 
34
41
  describe('actions', () => {
35
42
  const workos = getWorkOS();
43
+
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ // Restore default mock implementations
47
+ vi.mocked(withAuth).mockResolvedValue({
48
+ user: 'testUser' as never,
49
+ sessionId: 'session_123',
50
+ accessToken: 'access_token',
51
+ });
52
+ vi.mocked(refreshSession).mockResolvedValue({
53
+ user: 'testUser' as never,
54
+ sessionId: 'session_123',
55
+ accessToken: 'refreshed_token',
56
+ });
57
+ });
58
+
36
59
  describe('checkSessionAction', () => {
37
60
  it('should return true for authenticated users', async () => {
38
61
  const result = await checkSessionAction();
@@ -60,16 +83,52 @@ describe('actions', () => {
60
83
  it('should return auth details', async () => {
61
84
  const result = await getAuthAction();
62
85
  expect(withAuth).toHaveBeenCalled();
63
- expect(result).toEqual({ user: 'testUser' });
86
+ expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
87
+ });
88
+
89
+ it('should not pass ensureSignedIn to withAuth', async () => {
90
+ await getAuthAction({ ensureSignedIn: true });
91
+ expect(withAuth).toHaveBeenCalledWith();
92
+ });
93
+
94
+ it('should return signInUrl when ensureSignedIn is true and no user', async () => {
95
+ vi.mocked(withAuth).mockResolvedValueOnce({ user: null });
96
+ const result = await getAuthAction({ ensureSignedIn: true });
97
+ expect(getAuthorizationUrl).toHaveBeenCalledWith({ screenHint: 'sign-in' });
98
+ expect(result).toEqual({ user: null, signInUrl: 'https://api.workos.com/authorize?...' });
99
+ });
100
+
101
+ it('should not return signInUrl when ensureSignedIn is true and user exists', async () => {
102
+ const result = await getAuthAction({ ensureSignedIn: true });
103
+ expect(getAuthorizationUrl).not.toHaveBeenCalled();
104
+ expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
64
105
  });
65
106
  });
66
107
 
67
108
  describe('refreshAuthAction', () => {
68
109
  it('should refresh session', async () => {
69
- const params = { ensureSignedIn: true, organizationId: 'org_123' };
110
+ const params = { ensureSignedIn: false, organizationId: 'org_123' };
70
111
  const result = await refreshAuthAction(params);
71
- expect(refreshSession).toHaveBeenCalledWith(params);
72
- expect(result).toEqual({ session: 'newSession' });
112
+ expect(refreshSession).toHaveBeenCalledWith({ organizationId: 'org_123' });
113
+ expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
114
+ });
115
+
116
+ it('should not pass ensureSignedIn to refreshSession', async () => {
117
+ await refreshAuthAction({ ensureSignedIn: true, organizationId: 'org_123' });
118
+ expect(refreshSession).toHaveBeenCalledWith({ organizationId: 'org_123' });
119
+ });
120
+
121
+ it('should return signInUrl when ensureSignedIn is true and no user', async () => {
122
+ vi.mocked(refreshSession).mockResolvedValueOnce({ user: null });
123
+ const result = await refreshAuthAction({ ensureSignedIn: true });
124
+ expect(getAuthorizationUrl).toHaveBeenCalledWith({ screenHint: 'sign-in' });
125
+ expect(result).toEqual({ user: null, signInUrl: 'https://api.workos.com/authorize?...' });
126
+ });
127
+
128
+ it('should not return signInUrl when ensureSignedIn is true and user exists', async () => {
129
+ const result = await refreshAuthAction({ ensureSignedIn: true });
130
+ expect(getAuthorizationUrl).not.toHaveBeenCalled();
131
+ expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
73
132
  });
74
133
  });
75
134
 
@@ -94,7 +153,25 @@ describe('actions', () => {
94
153
  it('should refresh access token', async () => {
95
154
  const result = await refreshAccessTokenAction();
96
155
  expect(refreshSession).toHaveBeenCalled();
97
- expect(result).toEqual('refreshed_token');
156
+ expect(result).toEqual({ accessToken: 'refreshed_token' });
157
+ });
158
+
159
+ it('should catch errors and return a generic error instead of throwing', async () => {
160
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
161
+ vi.mocked(refreshSession).mockRejectedValueOnce(new Error('Rate limit exceeded'));
162
+ const result = await refreshAccessTokenAction();
163
+ expect(result).toEqual({ accessToken: undefined, error: 'Failed to refresh access token' });
164
+ expect(warnSpy).toHaveBeenCalledWith('Failed to refresh access token:', 'Rate limit exceeded');
165
+ warnSpy.mockRestore();
166
+ });
167
+
168
+ it('should handle non-Error objects in catch', async () => {
169
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
170
+ vi.mocked(refreshSession).mockRejectedValueOnce('string error');
171
+ const result = await refreshAccessTokenAction();
172
+ expect(result).toEqual({ accessToken: undefined, error: 'Failed to refresh access token' });
173
+ expect(warnSpy).toHaveBeenCalledWith('Failed to refresh access token:', 'string error');
174
+ warnSpy.mockRestore();
98
175
  });
99
176
  });
100
177
  });
package/src/actions.ts CHANGED
@@ -3,8 +3,14 @@
3
3
  import { signOut, switchToOrganization } from './auth.js';
4
4
  import { NoUserInfo, UserInfo, SwitchToOrganizationOptions } from './interfaces.js';
5
5
  import { refreshSession, withAuth } from './session.js';
6
+ import { getAuthorizationUrl } from './get-authorization-url.js';
6
7
  import { getWorkOS } from './workos.js';
7
8
 
9
+ export interface RefreshAccessTokenActionResult {
10
+ accessToken: string | undefined;
11
+ error?: string;
12
+ }
13
+
8
14
  /**
9
15
  * This function is used to sanitize the auth object.
10
16
  * Remove the accessToken from the auth object as it is not needed on the client side.
@@ -35,7 +41,19 @@ export const getOrganizationAction = async (organizationId: string) => {
35
41
  };
36
42
 
37
43
  export const getAuthAction = async (options?: { ensureSignedIn?: boolean }) => {
38
- return sanitize(await withAuth(options));
44
+ // Never pass ensureSignedIn to withAuth from a server action, because withAuth
45
+ // would call redirect() to an external URL, which causes CORS errors when
46
+ // invoked via a client-side fetch. Instead, return the sign-in URL so the
47
+ // client can redirect via window.location.href.
48
+ const auth = await withAuth();
49
+ const sanitized = sanitize(auth);
50
+
51
+ if (options?.ensureSignedIn && !auth.user) {
52
+ const signInUrl = await getAuthorizationUrl({ screenHint: 'sign-in' });
53
+ return { ...sanitized, signInUrl };
54
+ }
55
+
56
+ return sanitized;
39
57
  };
40
58
 
41
59
  export const refreshAuthAction = async ({
@@ -45,7 +63,17 @@ export const refreshAuthAction = async ({
45
63
  ensureSignedIn?: boolean;
46
64
  organizationId?: string;
47
65
  }) => {
48
- return sanitize(await refreshSession({ ensureSignedIn, organizationId }));
66
+ // Never pass ensureSignedIn to refreshSession from a server action for the
67
+ // same CORS reason as getAuthAction above.
68
+ const auth = await refreshSession({ organizationId });
69
+ const sanitized = sanitize(auth);
70
+
71
+ if (ensureSignedIn && !auth.user) {
72
+ const signInUrl = await getAuthorizationUrl({ screenHint: 'sign-in' });
73
+ return { ...sanitized, signInUrl };
74
+ }
75
+
76
+ return sanitized;
49
77
  };
50
78
 
51
79
  export const switchToOrganizationAction = async (organizationId: string, options?: SwitchToOrganizationOptions) => {
@@ -64,8 +92,19 @@ export async function getAccessTokenAction() {
64
92
  /**
65
93
  * This action is used to refresh the access token from the auth object.
66
94
  * It is used to fetch the access token from the server.
95
+ *
96
+ * Errors are caught and returned as data rather than thrown, to prevent
97
+ * Next.js from returning 500 responses for server action failures.
67
98
  */
68
- export async function refreshAccessTokenAction() {
69
- const auth = await refreshSession();
70
- return auth.accessToken;
99
+ export async function refreshAccessTokenAction(): Promise<RefreshAccessTokenActionResult> {
100
+ try {
101
+ const auth = await refreshSession();
102
+ return { accessToken: auth.accessToken };
103
+ } catch (error) {
104
+ console.warn('Failed to refresh access token:', error instanceof Error ? error.message : String(error));
105
+ return {
106
+ accessToken: undefined,
107
+ error: 'Failed to refresh access token',
108
+ };
109
+ }
71
110
  }
package/src/auth.spec.ts CHANGED
@@ -1,57 +1,63 @@
1
- import { describe, it, expect, beforeEach, jest } from '@jest/globals';
2
-
3
1
  import { getSignInUrl, getSignUpUrl, signOut, switchToOrganization } from './auth.js';
4
2
  import * as session from './session.js';
5
3
  import * as cache from 'next/cache';
6
4
  import * as workosModule from './workos.js';
7
5
 
8
- // These are mocked in jest.setup.ts
6
+ // These are mocked in vitest.setup.ts
9
7
  import { cookies, headers } from 'next/headers';
10
8
  import { redirect } from 'next/navigation';
11
9
  import { generateSession, generateTestToken } from './test-helpers.js';
12
10
  import { sealData } from 'iron-session';
13
11
  import { getWorkOS } from './workos.js';
12
+ import { getStateFromPKCECookieValue } from './pkce.js';
14
13
 
15
14
  const workos = getWorkOS();
16
15
 
17
- jest.mock('next/cache', () => {
18
- const actual = jest.requireActual<typeof cache>('next/cache');
16
+ vi.mock('next/cache', async () => {
17
+ const actual = await vi.importActual<typeof cache>('next/cache');
19
18
  return {
20
19
  ...actual,
21
- revalidateTag: jest.fn(),
22
- revalidatePath: jest.fn(),
20
+ revalidateTag: vi.fn(),
21
+ revalidatePath: vi.fn(),
23
22
  };
24
23
  });
25
24
 
26
25
  // Create a fake WorkOS instance that will be used only in the "on error" tests
27
26
  const fakeWorkosInstance = {
28
27
  userManagement: {
29
- authenticateWithRefreshToken: jest.fn(),
30
- getAuthorizationUrl: jest.fn(),
31
- getJwksUrl: jest.fn(() => 'https://api.workos.com/sso/jwks/client_1234567890'),
32
- getLogoutUrl: jest.fn(),
28
+ authenticateWithRefreshToken: vi.fn(),
29
+ getAuthorizationUrl: vi.fn(),
30
+ getJwksUrl: vi.fn(() => 'https://api.workos.com/sso/jwks/client_1234567890'),
31
+ getLogoutUrl: vi.fn(),
32
+ },
33
+ pkce: {
34
+ generate: vi.fn().mockResolvedValue({
35
+ codeVerifier: 'test-code-verifier',
36
+ codeChallenge: 'test-code-challenge',
37
+ codeChallengeMethod: 'S256' as const,
38
+ }),
33
39
  },
34
40
  };
35
41
 
36
- const revalidatePath = jest.mocked(cache.revalidatePath);
37
- const revalidateTag = jest.mocked(cache.revalidateTag);
42
+ const revalidatePath = vi.mocked(cache.revalidatePath);
43
+ const revalidateTag = vi.mocked(cache.revalidateTag);
38
44
  // We'll only use these in the "on error" tests
39
45
  const authenticateWithRefreshToken = fakeWorkosInstance.userManagement.authenticateWithRefreshToken;
40
46
  const getAuthorizationUrl = fakeWorkosInstance.userManagement.getAuthorizationUrl;
41
47
 
42
- jest.mock('../src/session', () => {
43
- const actual = jest.requireActual<typeof session>('../src/session');
48
+ vi.mock('../src/session', async () => {
49
+ const actual = await vi.importActual<typeof session>('../src/session');
44
50
 
45
51
  return {
46
52
  ...actual,
47
- refreshSession: jest.fn(actual.refreshSession),
53
+ refreshSession: vi.fn(actual.refreshSession),
48
54
  };
49
55
  });
50
56
 
51
57
  describe('auth.ts', () => {
52
58
  beforeEach(async () => {
53
59
  // Clear all mocks between tests
54
- jest.clearAllMocks();
60
+ vi.clearAllMocks();
55
61
 
56
62
  // Reset the cookie store
57
63
  const nextCookies = await cookies();
@@ -76,6 +82,15 @@ describe('auth.ts', () => {
76
82
  expect(url).toBeDefined();
77
83
  expect(() => new URL(url)).not.toThrow();
78
84
  });
85
+
86
+ it('should include returnTo as returnPathname in the state parameter', async () => {
87
+ const url = await getSignInUrl({ returnTo: '/dashboard' });
88
+ const parsedUrl = new URL(url);
89
+ const state = parsedUrl.searchParams.get('state');
90
+ expect(state).toBeDefined();
91
+ const decoded = await getStateFromPKCECookieValue(state!);
92
+ expect(decoded.returnPathname).toBe('/dashboard');
93
+ });
79
94
  });
80
95
 
81
96
  it('should not include prompt when not specified for getSignInUrl', async () => {
@@ -103,6 +118,15 @@ describe('auth.ts', () => {
103
118
  const url = await getSignUpUrl({ prompt: 'consent' });
104
119
  expect(url).toContain('prompt=consent');
105
120
  });
121
+
122
+ it('should include returnTo as returnPathname in the state parameter', async () => {
123
+ const url = await getSignUpUrl({ returnTo: '/welcome' });
124
+ const parsedUrl = new URL(url);
125
+ const state = parsedUrl.searchParams.get('state');
126
+ expect(state).toBeDefined();
127
+ const decoded = await getStateFromPKCECookieValue(state!);
128
+ expect(decoded.returnPathname).toBe('/welcome');
129
+ });
106
130
  });
107
131
 
108
132
  describe('switchToOrganization', () => {
@@ -135,14 +159,21 @@ describe('auth.ts', () => {
135
159
  nextHeaders.set('x-url', 'http://localhost/test');
136
160
  await generateSession();
137
161
 
162
+ fakeWorkosInstance.pkce.generate.mockResolvedValue({
163
+ codeVerifier: 'test-code-verifier',
164
+ codeChallenge: 'test-code-challenge',
165
+ codeChallengeMethod: 'S256' as const,
166
+ });
167
+
138
168
  // Create a WorkOS-like object that matches what our tests need
139
169
  const mockWorkOS = {
140
170
  userManagement: fakeWorkosInstance.userManagement,
171
+ pkce: fakeWorkosInstance.pkce,
141
172
  // Add minimal properties to satisfy TypeScript
142
- createHttpClient: jest.fn(),
143
- createWebhookClient: jest.fn(),
144
- createActionsClient: jest.fn(),
145
- createIronSessionProvider: jest.fn(),
173
+ createHttpClient: vi.fn(),
174
+ createWebhookClient: vi.fn(),
175
+ createActionsClient: vi.fn(),
176
+ createIronSessionProvider: vi.fn(),
146
177
  apiKey: 'test',
147
178
  clientId: 'test',
148
179
  host: 'test',
@@ -154,12 +185,12 @@ describe('auth.ts', () => {
154
185
 
155
186
  // Apply the mock for these tests only
156
187
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
- jest.spyOn(workosModule, 'getWorkOS').mockImplementation(() => mockWorkOS as any);
188
+ vi.spyOn(workosModule, 'getWorkOS').mockImplementation(() => mockWorkOS as any);
158
189
  });
159
190
 
160
191
  afterEach(() => {
161
192
  // Restore all mocks after each test
162
- jest.restoreAllMocks();
193
+ vi.restoreAllMocks();
163
194
  });
164
195
 
165
196
  it('should redirect to sign in when error is "sso_required"', async () => {
@@ -245,9 +276,9 @@ describe('auth.ts', () => {
245
276
 
246
277
  describe('when given a `returnTo` parameter', () => {
247
278
  it('passes the `returnTo` through to the `getLogoutUrl` call', async () => {
248
- jest
249
- .spyOn(workos.userManagement, 'getLogoutUrl')
250
- .mockReturnValue('https://user-management-logout.com/signed-out');
279
+ vi.spyOn(workos.userManagement, 'getLogoutUrl').mockReturnValue(
280
+ 'https://user-management-logout.com/signed-out',
281
+ );
251
282
  const mockSession = {
252
283
  accessToken: await generateTestToken(),
253
284
  sessionId: 'session_123',
@@ -306,9 +337,9 @@ describe('auth.ts', () => {
306
337
 
307
338
  nextCookies.set('wos-session', encryptedSession);
308
339
 
309
- jest
310
- .spyOn(workos.userManagement, 'getLogoutUrl')
311
- .mockReturnValue('https://api.workos.com/user_management/sessions/logout?session_id=session_123');
340
+ vi.spyOn(workos.userManagement, 'getLogoutUrl').mockReturnValue(
341
+ 'https://api.workos.com/user_management/sessions/logout?session_id=session_123',
342
+ );
312
343
 
313
344
  await signOut();
314
345