zudoku 0.46.3 → 0.47.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 (128) hide show
  1. package/dist/app/main.js +2 -2
  2. package/dist/app/main.js.map +1 -1
  3. package/dist/config/config.d.ts +11 -0
  4. package/dist/config/validators/InputSidebarSchema.d.ts +1 -1
  5. package/dist/config/validators/validate.d.ts +167 -47
  6. package/dist/config/validators/validate.js +22 -1
  7. package/dist/config/validators/validate.js.map +1 -1
  8. package/dist/config/validators/validate.test.d.ts +1 -0
  9. package/dist/config/validators/validate.test.js +80 -0
  10. package/dist/config/validators/validate.test.js.map +1 -0
  11. package/dist/lib/auth/issuer.d.ts +2 -0
  12. package/dist/lib/auth/issuer.js +37 -0
  13. package/dist/lib/auth/issuer.js.map +1 -0
  14. package/dist/lib/auth/issuer.test.d.ts +1 -0
  15. package/dist/lib/auth/issuer.test.js +94 -0
  16. package/dist/lib/auth/issuer.test.js.map +1 -0
  17. package/dist/lib/authentication/hook.d.ts +6 -0
  18. package/dist/lib/authentication/providers/auth0.js +1 -1
  19. package/dist/lib/authentication/providers/auth0.js.map +1 -1
  20. package/dist/lib/authentication/providers/azureb2c.d.ts +28 -0
  21. package/dist/lib/authentication/providers/azureb2c.js +145 -0
  22. package/dist/lib/authentication/providers/azureb2c.js.map +1 -0
  23. package/dist/lib/authentication/providers/clerk.js +3 -12
  24. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  25. package/dist/lib/authentication/providers/openid.d.ts +1 -0
  26. package/dist/lib/authentication/providers/openid.js +38 -0
  27. package/dist/lib/authentication/providers/openid.js.map +1 -1
  28. package/dist/lib/authentication/providers/supabase.js +2 -9
  29. package/dist/lib/authentication/providers/supabase.js.map +1 -1
  30. package/dist/lib/authentication/state.d.ts +6 -5
  31. package/dist/lib/authentication/state.js +19 -6
  32. package/dist/lib/authentication/state.js.map +1 -1
  33. package/dist/lib/components/context/ZudokuProvider.d.ts +1 -1
  34. package/dist/lib/components/index.d.ts +6 -0
  35. package/dist/lib/hooks/index.d.ts +6 -0
  36. package/dist/lib/plugins/api-keys/index.js +1 -3
  37. package/dist/lib/plugins/api-keys/index.js.map +1 -1
  38. package/dist/lib/plugins/openapi/PlaygroundDialogWrapper.d.ts +1 -1
  39. package/dist/lib/plugins/openapi/playground/Headers.d.ts +2 -2
  40. package/dist/lib/plugins/openapi/playground/fileUtils.js +1 -1
  41. package/dist/lib/plugins/openapi/playground/fileUtils.js.map +1 -1
  42. package/dist/lib/util/MdxComponents.js +1 -1
  43. package/dist/lib/util/MdxComponents.js.map +1 -1
  44. package/dist/vite/build.js +1 -28
  45. package/dist/vite/build.js.map +1 -1
  46. package/lib/Drawer-BzkOKwgC.js.map +1 -1
  47. package/lib/{Markdown-D81l28Ib.js → Markdown-r4buN85T.js} +37 -37
  48. package/lib/{Markdown-D81l28Ib.js.map → Markdown-r4buN85T.js.map} +1 -1
  49. package/lib/{MdxPage-S_CxlNmX.js → MdxPage-DYKsTerz.js} +4 -4
  50. package/lib/{MdxPage-S_CxlNmX.js.map → MdxPage-DYKsTerz.js.map} +1 -1
  51. package/lib/{OasProvider-D5rt6WMb.js → OasProvider-8vNiLpIG.js} +2 -2
  52. package/lib/{OasProvider-D5rt6WMb.js.map → OasProvider-8vNiLpIG.js.map} +1 -1
  53. package/lib/{OperationList-CNhg654C.js → OperationList-BCVHtZNK.js} +6 -6
  54. package/lib/OperationList-BCVHtZNK.js.map +1 -0
  55. package/lib/{RouteGuard-CZ_uLv3g.js → RouteGuard-B7GVW4oL.js} +2 -2
  56. package/lib/{RouteGuard-CZ_uLv3g.js.map → RouteGuard-B7GVW4oL.js.map} +1 -1
  57. package/lib/{SchemaList-BvzCrTYg.js → SchemaList-1oJKvBxh.js} +6 -6
  58. package/lib/{SchemaList-BvzCrTYg.js.map → SchemaList-1oJKvBxh.js.map} +1 -1
  59. package/lib/{SchemaView-Br1RupCp.js → SchemaView-CTqaB-79.js} +3 -3
  60. package/lib/{SchemaView-Br1RupCp.js.map → SchemaView-CTqaB-79.js.map} +1 -1
  61. package/lib/{SignUp-B-1Pvc-8.js → SignUp-CRIKdWt9.js} +2 -2
  62. package/lib/{SignUp-B-1Pvc-8.js.map → SignUp-CRIKdWt9.js.map} +1 -1
  63. package/lib/{Slot-T8NJUkm4.js → Slot-B5qSAnwR.js} +4 -4
  64. package/lib/{Slot-T8NJUkm4.js.map → Slot-B5qSAnwR.js.map} +1 -1
  65. package/lib/{SyntaxHighlight-Cz6Me7-F.js → SyntaxHighlight-CqKHkyEy.js} +1291 -1265
  66. package/lib/SyntaxHighlight-CqKHkyEy.js.map +1 -0
  67. package/lib/{Toc-PA-j0gEu.js → Toc-lxYQEOzX.js} +2 -2
  68. package/lib/{Toc-PA-j0gEu.js.map → Toc-lxYQEOzX.js.map} +1 -1
  69. package/lib/{circular-5FeDWJOn.js → circular-ZGGPtwMq.js} +2 -2
  70. package/lib/{circular-5FeDWJOn.js.map → circular-ZGGPtwMq.js.map} +1 -1
  71. package/lib/clerk-yAKDC3Qz.js +24812 -0
  72. package/lib/clerk-yAKDC3Qz.js.map +1 -0
  73. package/lib/{createServer-BC2RZgmW.js → createServer-DUBpXfvA.js} +2627 -2617
  74. package/lib/createServer-DUBpXfvA.js.map +1 -0
  75. package/lib/errors-D27ZTQgx.js +78 -0
  76. package/lib/errors-D27ZTQgx.js.map +1 -0
  77. package/lib/{hook-k7PfUIsj.js → hook-7wZANGJP.js} +53 -35
  78. package/lib/{hook-k7PfUIsj.js.map → hook-7wZANGJP.js.map} +1 -1
  79. package/lib/index-CrcNWbel.js.map +1 -1
  80. package/lib/{index-CJZthJSj.js → index-Cucjfk3D.js} +10 -10
  81. package/lib/index-Cucjfk3D.js.map +1 -0
  82. package/lib/{index-zddirpDj.js → index-DmNq2fbN.js} +226 -221
  83. package/lib/index-DmNq2fbN.js.map +1 -0
  84. package/lib/{mutation-BSeQ8pEK.js → mutation-C1XCQTQL.js} +2 -2
  85. package/lib/{mutation-BSeQ8pEK.js.map → mutation-C1XCQTQL.js.map} +1 -1
  86. package/lib/ui/SyntaxHighlight.js +2 -2
  87. package/lib/{useMutation-CZSmsIGW.js → useMutation-BKvPttRn.js} +3 -3
  88. package/lib/{useMutation-CZSmsIGW.js.map → useMutation-BKvPttRn.js.map} +1 -1
  89. package/lib/zudoku.auth-auth0.js +2 -2
  90. package/lib/zudoku.auth-auth0.js.map +1 -1
  91. package/lib/zudoku.auth-azureb2c.js +9974 -0
  92. package/lib/zudoku.auth-azureb2c.js.map +1 -0
  93. package/lib/zudoku.auth-clerk.js +39 -48
  94. package/lib/zudoku.auth-clerk.js.map +1 -1
  95. package/lib/zudoku.auth-openid.js +291 -316
  96. package/lib/zudoku.auth-openid.js.map +1 -1
  97. package/lib/zudoku.components.js +1222 -1594
  98. package/lib/zudoku.components.js.map +1 -1
  99. package/lib/zudoku.hooks.js +2 -2
  100. package/lib/zudoku.plugin-api-catalog.js +2 -2
  101. package/lib/zudoku.plugin-api-keys.js +19 -21
  102. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  103. package/lib/zudoku.plugin-custom-pages.js +1 -1
  104. package/lib/zudoku.plugin-markdown.js +1 -1
  105. package/lib/zudoku.plugin-openapi.js +2 -2
  106. package/lib/zudoku.plugin-search-pagefind.js +2 -2
  107. package/package.json +40 -22
  108. package/src/app/main.tsx +2 -2
  109. package/src/lib/auth/issuer.test.ts +120 -0
  110. package/src/lib/auth/issuer.ts +41 -0
  111. package/src/lib/authentication/providers/auth0.tsx +1 -1
  112. package/src/lib/authentication/providers/azureb2c.tsx +196 -0
  113. package/src/lib/authentication/providers/clerk.tsx +3 -12
  114. package/src/lib/authentication/providers/openid.tsx +53 -0
  115. package/src/lib/authentication/providers/supabase.tsx +2 -9
  116. package/src/lib/authentication/state.ts +37 -7
  117. package/src/lib/components/context/ZudokuProvider.tsx +1 -1
  118. package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +1 -1
  119. package/src/lib/plugins/api-keys/index.tsx +1 -3
  120. package/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx +1 -1
  121. package/src/lib/plugins/openapi/playground/Headers.tsx +2 -2
  122. package/src/lib/plugins/openapi/playground/fileUtils.ts +1 -1
  123. package/src/lib/util/MdxComponents.tsx +1 -1
  124. package/lib/OperationList-CNhg654C.js.map +0 -1
  125. package/lib/SyntaxHighlight-Cz6Me7-F.js.map +0 -1
  126. package/lib/createServer-BC2RZgmW.js.map +0 -1
  127. package/lib/index-CJZthJSj.js.map +0 -1
  128. package/lib/index-zddirpDj.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.46.3",
3
+ "version": "0.47.1",
4
4
  "type": "module",
5
5
  "homepage": "https://zudoku.dev",
6
6
  "repository": {
@@ -36,10 +36,6 @@
36
36
  "import": "./lib/ui/*.js",
37
37
  "types": "./dist/lib/ui/*.d.ts"
38
38
  },
39
- "./hooks": {
40
- "import": "./lib/hooks/index.js",
41
- "types": "./dist/lib/hooks/index.d.ts"
42
- },
43
39
  "./client": {
44
40
  "types": "./client.d.ts"
45
41
  },
@@ -59,6 +55,10 @@
59
55
  "import": "./lib/zudoku.auth-supabase.js",
60
56
  "types": "./dist/lib/authentication/providers/supabase.d.ts"
61
57
  },
58
+ "./auth/azureb2c": {
59
+ "import": "./lib/zudoku.auth-azureb2c.js",
60
+ "types": "./dist/lib/authentication/providers/azureb2c.d.ts"
61
+ },
62
62
  "./plugins": {
63
63
  "import": "./lib/zudoku.plugins.js",
64
64
  "types": "./dist/lib/core/plugins.d.ts"
@@ -95,6 +95,10 @@
95
95
  "import": "./lib/zudoku.plugin-search-pagefind.js",
96
96
  "types": "./dist/lib/plugins/search-pagefind/index.d.ts"
97
97
  },
98
+ "./hooks": {
99
+ "import": "./lib/zudoku.hooks.js",
100
+ "types": "./dist/lib/hooks/index.d.ts"
101
+ },
98
102
  "./components": {
99
103
  "import": "./lib/zudoku.components.js",
100
104
  "types": "./dist/lib/components/index.d.ts"
@@ -159,11 +163,11 @@
159
163
  "@radix-ui/react-tooltip": "1.2.7",
160
164
  "@radix-ui/react-visually-hidden": "1.2.3",
161
165
  "@scalar/openapi-parser": "0.10.17",
162
- "@sentry/node": "9.12.0",
163
- "@shikijs/langs": "3.4.2",
164
- "@shikijs/rehype": "3.4.2",
165
- "@shikijs/themes": "3.4.2",
166
- "@shikijs/transformers": "3.4.2",
166
+ "@sentry/node": "9.26.0",
167
+ "@shikijs/langs": "3.5.0",
168
+ "@shikijs/rehype": "3.5.0",
169
+ "@shikijs/themes": "3.5.0",
170
+ "@shikijs/transformers": "3.5.0",
167
171
  "@sindresorhus/slugify": "2.2.1",
168
172
  "@stefanprobst/rehype-extract-toc": "3.0.0",
169
173
  "@tailwindcss/typography": "0.5.16",
@@ -171,7 +175,7 @@
171
175
  "@tanem/react-nprogress": "5.0.55",
172
176
  "@tanstack/react-query": "5.74.3",
173
177
  "@types/react": "19.1.1",
174
- "@types/react-dom": "19.1.2",
178
+ "@types/react-dom": "19.1.6",
175
179
  "@vitejs/plugin-react": "4.4.1",
176
180
  "@zudoku/httpsnippet": "10.0.9",
177
181
  "@zudoku/react-helmet-async": "2.0.5",
@@ -183,14 +187,14 @@
183
187
  "devlop": "^1.1.0",
184
188
  "dotenv": "16.5.0",
185
189
  "embla-carousel-react": "8.6.0",
186
- "estree-util-value-to-estree": "3.3.3",
190
+ "estree-util-value-to-estree": "3.4.0",
187
191
  "express": "5.1.0",
188
192
  "fast-equals": "5.2.2",
189
193
  "framer-motion": "^12.12.2",
190
- "glob": "11.0.1",
194
+ "glob": "11.0.2",
191
195
  "graphql": "16.11.0",
192
196
  "graphql-type-json": "0.3.2",
193
- "graphql-yoga": "5.13.3",
197
+ "graphql-yoga": "5.13.5",
194
198
  "gray-matter": "4.0.3",
195
199
  "hast-util-to-jsx-runtime": "^2.3.6",
196
200
  "hast-util-to-string": "3.0.1",
@@ -227,7 +231,7 @@
227
231
  "remark-rehype": "^11.1.2",
228
232
  "rollup": "4.41.1",
229
233
  "semver": "7.7.1",
230
- "shiki": "3.4.2",
234
+ "shiki": "3.5.0",
231
235
  "sitemap": "8.0.0",
232
236
  "spin-delay": "2.0.1",
233
237
  "strip-ansi": "7.1.0",
@@ -239,11 +243,11 @@
239
243
  "vaul": "1.1.2",
240
244
  "vfile": "6.0.3",
241
245
  "vite": "6.3.5",
242
- "yaml": "2.7.1",
246
+ "yaml": "2.8.0",
243
247
  "yargs": "17.7.2",
244
- "zod": "3.24.2",
248
+ "zod": "3.25.51",
245
249
  "zod-validation-error": "3.4.1",
246
- "zustand": "5.0.3"
250
+ "zustand": "5.0.5"
247
251
  },
248
252
  "devDependencies": {
249
253
  "@graphql-codegen/cli": "5.0.6",
@@ -264,7 +268,7 @@
264
268
  "@types/semver": "7.7.0",
265
269
  "@types/unist": "^3.0.3",
266
270
  "@types/yargs": "17.0.33",
267
- "@vitest/coverage-v8": "3.1.1",
271
+ "@vitest/coverage-v8": "3.2.1",
268
272
  "esbuild": "0.25.1",
269
273
  "happy-dom": "17.5.6",
270
274
  "mdast-util-mdx": "3.0.0",
@@ -276,13 +280,27 @@
276
280
  },
277
281
  "peerDependencies": {
278
282
  "react": ">=19",
279
- "react-dom": ">=19"
280
- },
281
- "optionalDependencies": {
283
+ "react-dom": ">=19",
284
+ "@azure/msal-browser": "^4.13.0",
282
285
  "@clerk/clerk-js": "^5.63.1",
283
286
  "@sentry/react": "^9.12.0",
284
287
  "@supabase/supabase-js": "^2.49.4"
285
288
  },
289
+ "optionalDependencies": {},
290
+ "peerDependenciesMeta": {
291
+ "@azure/msal-browser": {
292
+ "optional": true
293
+ },
294
+ "@clerk/clerk-js": {
295
+ "optional": true
296
+ },
297
+ "@sentry/react": {
298
+ "optional": true
299
+ },
300
+ "@supabase/supabase-js": {
301
+ "optional": true
302
+ }
303
+ },
286
304
  "scripts": {
287
305
  "build": "tsc --project tsconfig.app.json",
288
306
  "build:dev": "esbuild './src/**/*.ts' --format=esm --platform=node --target=node22 --outdir=dist --splitting --log-level=warning",
package/src/app/main.tsx CHANGED
@@ -71,7 +71,7 @@ export const convertZudokuConfigToOptions = (
71
71
 
72
72
  export const getRoutesByOptions = (
73
73
  options: ZudokuContextOptions,
74
- enableStatusPages = false,
74
+ enableStatusPages = true,
75
75
  ) => {
76
76
  const allPlugins = [
77
77
  ...(options.plugins ?? []),
@@ -84,7 +84,7 @@ export const getRoutesByOptions = (
84
84
  enableStatusPages
85
85
  ? [400, 403, 404, 405, 414, 416, 500, 501, 502, 503, 504].map(
86
86
  (statusCode) => ({
87
- path: `/.static/${statusCode}`,
87
+ path: `/${statusCode}`,
88
88
  element: <StatusPage statusCode={statusCode} />,
89
89
  }),
90
90
  )
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ZudokuConfig } from "../../config/validators/validate.js";
3
+ import { getIssuer } from "./issuer.js";
4
+
5
+ describe("getIssuer", () => {
6
+ it("should return clerk frontend API for clerk authentication", async () => {
7
+ // Using a valid base64 encoded string: "example.example$test" -> "ZXhhbXBsZS5leGFtcGxlJHRlc3Q="
8
+ const config: ZudokuConfig = {
9
+ authentication: {
10
+ type: "clerk",
11
+ clerkPubKey:
12
+ "pk_test_dG9sZXJhbnQtaG9ybmV0LTQ2LmNsZXJrLmFjY291bnRzLmRldiQ",
13
+ },
14
+ };
15
+
16
+ const result = await getIssuer(config);
17
+ expect(result).toBe("tolerant-hornet-46.clerk.accounts.dev");
18
+ });
19
+
20
+ it("should throw error for invalid clerk public key format", async () => {
21
+ const config: ZudokuConfig = {
22
+ authentication: {
23
+ type: "clerk",
24
+ clerkPubKey: "pk_test_invalid",
25
+ },
26
+ };
27
+
28
+ await expect(getIssuer(config)).rejects.toThrow(
29
+ "Clerk public key is invalid",
30
+ );
31
+ });
32
+
33
+ it("should throw error for clerk key with invalid base64", async () => {
34
+ const config: ZudokuConfig = {
35
+ authentication: {
36
+ type: "clerk",
37
+ clerkPubKey: "pk_test_invalid_base64",
38
+ },
39
+ };
40
+
41
+ await expect(getIssuer(config)).rejects.toThrow(
42
+ "Clerk public key is invalid",
43
+ );
44
+ });
45
+
46
+ it("should return auth0 issuer URL for auth0 authentication", async () => {
47
+ const config: ZudokuConfig = {
48
+ authentication: {
49
+ type: "auth0",
50
+ domain: "example.auth0.com",
51
+ clientId: "test-client-id",
52
+ },
53
+ };
54
+
55
+ const result = await getIssuer(config);
56
+ expect(result).toBe("https://example.auth0.com/");
57
+ });
58
+
59
+ it("should return openid issuer for openid authentication", async () => {
60
+ const config: ZudokuConfig = {
61
+ authentication: {
62
+ type: "openid",
63
+ issuer: "https://example.com/auth",
64
+ clientId: "test-client-id",
65
+ },
66
+ };
67
+
68
+ const result = await getIssuer(config);
69
+ expect(result).toBe("https://example.com/auth");
70
+ });
71
+
72
+ it("should return azureb2c issuer for azureb2c authentication", async () => {
73
+ const config: ZudokuConfig = {
74
+ authentication: {
75
+ type: "azureb2c",
76
+ tenantName: "example",
77
+ policyName: "B2C_1_SignUpSignIn",
78
+ issuer: "https://example.b2clogin.com/example.onmicrosoft.com/v2.0/",
79
+ clientId: "test-client-id",
80
+ },
81
+ };
82
+ const result = await getIssuer(config);
83
+ expect(result).toBe(
84
+ "https://example.b2clogin.com/example.onmicrosoft.com/v2.0/",
85
+ );
86
+ });
87
+
88
+ it("should return supabase URL for supabase authentication", async () => {
89
+ const config: ZudokuConfig = {
90
+ authentication: {
91
+ type: "supabase",
92
+ supabaseUrl: "https://project.supabase.co",
93
+ supabaseKey: "test-anon-key",
94
+ provider: "github",
95
+ },
96
+ };
97
+
98
+ const result = await getIssuer(config);
99
+ expect(result).toBe("https://project.supabase.co");
100
+ });
101
+
102
+ it("should return undefined for no authentication", async () => {
103
+ const config: ZudokuConfig = {};
104
+
105
+ const result = await getIssuer(config);
106
+ expect(result).toBeUndefined();
107
+ });
108
+
109
+ it("should throw error for unsupported authentication type", async () => {
110
+ const config = {
111
+ authentication: {
112
+ type: "unsupported" as any,
113
+ },
114
+ } as ZudokuConfig;
115
+
116
+ await expect(getIssuer(config)).rejects.toThrow(
117
+ "Unsupported authentication type",
118
+ );
119
+ });
120
+ });
@@ -0,0 +1,41 @@
1
+ import type { ZudokuConfig } from "../../config/validators/validate.js";
2
+ import invariant from "../util/invariant.js";
3
+
4
+ export const getIssuer = async (config: ZudokuConfig) => {
5
+ switch (config.authentication?.type) {
6
+ case "clerk": {
7
+ const frontendApiEncoded = config.authentication.clerkPubKey
8
+ .split("_")
9
+ .at(-1);
10
+ invariant(frontendApiEncoded, "Clerk public key is invalid");
11
+ const frontendApiParts = atob(frontendApiEncoded).split("$");
12
+
13
+ if (frontendApiParts.length !== 2) {
14
+ throw new Error("Clerk public key is invalid");
15
+ }
16
+
17
+ const frontendApi = frontendApiParts.at(0);
18
+ invariant(frontendApi, "Clerk public key is invalid");
19
+
20
+ return frontendApi;
21
+ }
22
+ case "auth0": {
23
+ return `https://${config.authentication.domain}/`;
24
+ }
25
+ case "openid": {
26
+ return config.authentication.issuer;
27
+ }
28
+ case "supabase": {
29
+ return config.authentication.supabaseUrl;
30
+ }
31
+ case "azureb2c": {
32
+ return config.authentication.issuer;
33
+ }
34
+ case undefined: {
35
+ return undefined;
36
+ }
37
+ default: {
38
+ throw new Error(`Unsupported authentication type`);
39
+ }
40
+ }
41
+ };
@@ -14,7 +14,7 @@ class Auth0AuthenticationProvider
14
14
  super({
15
15
  ...config,
16
16
  type: "openid",
17
- issuer: `https://${config.domain}`,
17
+ issuer: `https://${config.domain}/`,
18
18
  clientId: config.clientId,
19
19
  audience: config.audience,
20
20
  scopes: config.scopes,
@@ -0,0 +1,196 @@
1
+ import type { AuthenticationResult, EventMessage } from "@azure/msal-browser";
2
+ import { EventType, PublicClientApplication } from "@azure/msal-browser";
3
+ import { type AzureB2CAuthenticationConfig } from "../../../config/config.js";
4
+ import { ClientOnly } from "../../components/ClientOnly.js";
5
+ import { joinUrl } from "../../util/joinUrl.js";
6
+ import {
7
+ type AuthenticationPlugin,
8
+ type AuthenticationProviderInitializer,
9
+ } from "../authentication.js";
10
+ import { CallbackHandler } from "../components/CallbackHandler.js";
11
+ import { AuthorizationError } from "../errors.js";
12
+ import { useAuthState } from "../state.js";
13
+
14
+ import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
15
+
16
+ const AZUREB2C_CALLBACK_PATH = "/oauth/callback";
17
+
18
+ export class AzureB2CAuthPlugin
19
+ extends CoreAuthenticationPlugin
20
+ implements AuthenticationPlugin
21
+ {
22
+ private msalInstance: PublicClientApplication;
23
+ private readonly scopes: string[];
24
+ private readonly redirectToAfterSignUp?: string;
25
+ private readonly redirectToAfterSignIn?: string;
26
+ private readonly redirectToAfterSignOut: string;
27
+
28
+ constructor({
29
+ clientId,
30
+ tenantName,
31
+ policyName,
32
+ scopes,
33
+ redirectToAfterSignUp,
34
+ redirectToAfterSignIn,
35
+ redirectToAfterSignOut = "/",
36
+ basePath = "",
37
+ }: AzureB2CAuthenticationConfig) {
38
+ super();
39
+ this.scopes = scopes ?? ["openid", "profile", "email"];
40
+ this.redirectToAfterSignUp = redirectToAfterSignUp;
41
+ this.redirectToAfterSignIn = redirectToAfterSignIn;
42
+ this.redirectToAfterSignOut = redirectToAfterSignOut;
43
+
44
+ const authority = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${policyName}`;
45
+ const redirectUri = joinUrl(basePath, AZUREB2C_CALLBACK_PATH);
46
+
47
+ this.msalInstance = new PublicClientApplication({
48
+ auth: {
49
+ clientId,
50
+ authority,
51
+ redirectUri,
52
+ knownAuthorities: [`${tenantName}.b2clogin.com`],
53
+ },
54
+ cache: {
55
+ cacheLocation: "sessionStorage",
56
+ storeAuthStateInCookie: false,
57
+ },
58
+ });
59
+
60
+ void this.msalInstance.initialize().then(async () => {
61
+ void this.msalInstance
62
+ .handleRedirectPromise()
63
+ .then((response: AuthenticationResult | null) => {
64
+ if (response) {
65
+ this.handleAuthResponse(response);
66
+ }
67
+ });
68
+
69
+ // Add event callback
70
+ void this.msalInstance.addEventCallback((event: EventMessage) => {
71
+ if (event.eventType === EventType.LOGIN_SUCCESS) {
72
+ this.handleAuthResponse(event.payload as AuthenticationResult);
73
+ }
74
+ });
75
+ });
76
+ }
77
+
78
+ private handleAuthResponse(response: AuthenticationResult) {
79
+ const { accessToken, idToken, scopes, account } = response;
80
+
81
+ if (!account) {
82
+ throw new AuthorizationError("No account information in response");
83
+ }
84
+
85
+ // Get the user's name from Azure B2C claims
86
+ const name =
87
+ [account.idTokenClaims?.given_name, account.idTokenClaims?.family_name]
88
+ .filter(Boolean)
89
+ .join(" ") || account.username;
90
+
91
+ useAuthState.getState().setLoggedIn({
92
+ providerData: {
93
+ accessToken,
94
+ idToken,
95
+ scopes,
96
+ account,
97
+ },
98
+ profile: {
99
+ sub: account.localAccountId,
100
+ email: account.username,
101
+ name,
102
+ emailVerified: true, // Azure B2C emails are verified
103
+ pictureUrl: undefined, // Azure B2C doesn't provide profile pictures by default
104
+ },
105
+ });
106
+ }
107
+
108
+ async signUp({ redirectTo }: { redirectTo?: string } = {}) {
109
+ const redirectUri = this.redirectToAfterSignUp ?? redirectTo ?? "/";
110
+ sessionStorage.setItem("redirect-to", redirectUri);
111
+
112
+ await this.msalInstance.loginRedirect({
113
+ scopes: this.scopes,
114
+ prompt: "select_account",
115
+ });
116
+ }
117
+
118
+ async signIn({ redirectTo }: { redirectTo?: string } = {}) {
119
+ const redirectUri = this.redirectToAfterSignIn ?? redirectTo ?? "/";
120
+ sessionStorage.setItem("redirect-to", redirectUri);
121
+
122
+ await this.msalInstance.loginRedirect({
123
+ scopes: this.scopes,
124
+ });
125
+ }
126
+
127
+ async getAccessToken(): Promise<string> {
128
+ const account = this.msalInstance.getAllAccounts()[0];
129
+ if (!account) {
130
+ throw new AuthorizationError("No active account");
131
+ }
132
+
133
+ try {
134
+ const response = await this.msalInstance.acquireTokenSilent({
135
+ scopes: this.scopes,
136
+ account,
137
+ });
138
+ return response.accessToken;
139
+ } catch {
140
+ // If silent token acquisition fails, try interactive
141
+ await this.msalInstance.acquireTokenRedirect({
142
+ scopes: this.scopes,
143
+ account,
144
+ });
145
+
146
+ throw new AuthorizationError(
147
+ "Token acquisition failed after interactive attempt",
148
+ );
149
+ }
150
+ }
151
+
152
+ signRequest = async (request: Request): Promise<Request> => {
153
+ const accessToken = await this.getAccessToken();
154
+ request.headers.set("Authorization", `Bearer ${accessToken}`);
155
+ return request;
156
+ };
157
+
158
+ signOut = async () => {
159
+ const account = this.msalInstance.getAllAccounts()[0];
160
+ if (account) {
161
+ await this.msalInstance.logoutRedirect({
162
+ account,
163
+ postLogoutRedirectUri:
164
+ window.location.origin + this.redirectToAfterSignOut,
165
+ });
166
+ }
167
+
168
+ useAuthState.getState().setLoggedOut();
169
+ };
170
+
171
+ handleCallback = async () => {
172
+ const redirectTo = sessionStorage.getItem("redirect-to") ?? "/";
173
+ sessionStorage.removeItem("redirect-to");
174
+ return redirectTo;
175
+ };
176
+
177
+ getRoutes() {
178
+ return [
179
+ ...super.getRoutes(),
180
+ {
181
+ path: AZUREB2C_CALLBACK_PATH,
182
+ element: (
183
+ <ClientOnly>
184
+ <CallbackHandler handleCallback={this.handleCallback} />
185
+ </ClientOnly>
186
+ ),
187
+ },
188
+ ];
189
+ }
190
+ }
191
+
192
+ const azureB2CAuth: AuthenticationProviderInitializer<
193
+ AzureB2CAuthenticationConfig
194
+ > = (options) => new AzureB2CAuthPlugin(options);
195
+
196
+ export default azureB2CAuth;
@@ -31,9 +31,7 @@ const clerkAuth: AuthenticationProviderInitializer<
31
31
  const verifiedEmail = clerkApi.user.emailAddresses.find(
32
32
  (email) => email.verification.status === "verified",
33
33
  );
34
- useAuthState.setState({
35
- isAuthenticated: true,
36
- isPending: false,
34
+ useAuthState.getState().setLoggedIn({
37
35
  profile: {
38
36
  sub: clerkApi.user.id,
39
37
  name: clerkApi.user.fullName ?? undefined,
@@ -115,9 +113,7 @@ const clerkAuth: AuthenticationProviderInitializer<
115
113
  const verifiedEmail = clerk.session.user.emailAddresses.find(
116
114
  (email) => email.verification.status === "verified",
117
115
  );
118
- useAuthState.setState({
119
- isAuthenticated: true,
120
- isPending: false,
116
+ useAuthState.getState().setLoggedIn({
121
117
  profile: {
122
118
  sub: clerk.session.user.id,
123
119
  name: clerk.session.user.fullName ?? undefined,
@@ -146,12 +142,7 @@ const clerkAuth: AuthenticationProviderInitializer<
146
142
  await clerkApi?.signOut({
147
143
  redirectUrl: window.location.origin + redirectToAfterSignOut,
148
144
  });
149
- useAuthState.setState({
150
- isAuthenticated: false,
151
- isPending: false,
152
- profile: null,
153
- providerData: null,
154
- });
145
+ useAuthState.getState().setLoggedOut();
155
146
  },
156
147
  signIn: async ({ redirectTo }: { redirectTo?: string } = {}) => {
157
148
  await ensureLoaded;
@@ -273,6 +273,59 @@ export class OpenIDAuthenticationProvider
273
273
  }
274
274
  };
275
275
 
276
+ onPageLoad = async () => {
277
+ const { providerData } = useAuthState.getState();
278
+
279
+ if (!providerData) {
280
+ useAuthState.setState({ isPending: false });
281
+ return;
282
+ }
283
+
284
+ const tokenState = providerData as OpenIdProviderData;
285
+
286
+ if (new Date(tokenState.expiresOn) < new Date()) {
287
+ if (!tokenState.refreshToken) {
288
+ useAuthState.setState({
289
+ isAuthenticated: false,
290
+ isPending: false,
291
+ profile: null,
292
+ providerData: null,
293
+ });
294
+ return;
295
+ }
296
+
297
+ try {
298
+ const as = await this.getAuthServer();
299
+ const request = await oauth.refreshTokenGrantRequest(
300
+ as,
301
+ this.client,
302
+ tokenState.refreshToken,
303
+ );
304
+ const response = await oauth.processRefreshTokenResponse(
305
+ as,
306
+ this.client,
307
+ request,
308
+ );
309
+
310
+ if (!response.access_token) {
311
+ throw new AuthorizationError("No access token in response");
312
+ }
313
+
314
+ this.setTokensFromResponse(response);
315
+ } catch {
316
+ useAuthState.setState({
317
+ isAuthenticated: false,
318
+ isPending: false,
319
+ profile: null,
320
+ providerData: null,
321
+ });
322
+ return;
323
+ }
324
+ }
325
+
326
+ useAuthState.setState({ isPending: false });
327
+ };
328
+
276
329
  handleCallback = async () => {
277
330
  const url = new URL(window.location.href);
278
331
  const state = url.searchParams.get("state");
@@ -51,12 +51,7 @@ class SupabaseAuthenticationProvider
51
51
  if (session && (event === "SIGNED_IN" || event === "TOKEN_REFRESHED")) {
52
52
  await this.updateUserState(session);
53
53
  } else if (event === "SIGNED_OUT") {
54
- useAuthState.setState({
55
- isAuthenticated: false,
56
- isPending: false,
57
- profile: undefined,
58
- providerData: undefined,
59
- });
54
+ useAuthState.getState().setLoggedOut();
60
55
  }
61
56
  });
62
57
  }
@@ -72,9 +67,7 @@ class SupabaseAuthenticationProvider
72
67
  pictureUrl: user.user_metadata.avatar_url,
73
68
  };
74
69
 
75
- useAuthState.setState({
76
- isAuthenticated: true,
77
- isPending: false,
70
+ useAuthState.getState().setLoggedIn({
78
71
  profile,
79
72
  providerData: { session },
80
73
  });