zudoku 0.46.2 → 0.47.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 (123) hide show
  1. package/dist/config/config.d.ts +11 -0
  2. package/dist/config/validators/InputSidebarSchema.d.ts +4 -1
  3. package/dist/config/validators/InputSidebarSchema.js +1 -0
  4. package/dist/config/validators/InputSidebarSchema.js.map +1 -1
  5. package/dist/config/validators/SidebarSchema.d.ts +1 -1
  6. package/dist/config/validators/validate.d.ts +47 -47
  7. package/dist/config/validators/validate.js +11 -1
  8. package/dist/config/validators/validate.js.map +1 -1
  9. package/dist/config/validators/validate.test.d.ts +1 -0
  10. package/dist/config/validators/validate.test.js +80 -0
  11. package/dist/config/validators/validate.test.js.map +1 -0
  12. package/dist/lib/auth/issuer.d.ts +2 -0
  13. package/dist/lib/auth/issuer.js +34 -0
  14. package/dist/lib/auth/issuer.js.map +1 -0
  15. package/dist/lib/auth/issuer.test.d.ts +1 -0
  16. package/dist/lib/auth/issuer.test.js +81 -0
  17. package/dist/lib/auth/issuer.test.js.map +1 -0
  18. package/dist/lib/authentication/hook.d.ts +6 -0
  19. package/dist/lib/authentication/providers/auth0.js +1 -1
  20. package/dist/lib/authentication/providers/auth0.js.map +1 -1
  21. package/dist/lib/authentication/providers/azureb2c.d.ts +28 -0
  22. package/dist/lib/authentication/providers/azureb2c.js +145 -0
  23. package/dist/lib/authentication/providers/azureb2c.js.map +1 -0
  24. package/dist/lib/authentication/providers/clerk.js +3 -12
  25. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  26. package/dist/lib/authentication/providers/openid.d.ts +1 -0
  27. package/dist/lib/authentication/providers/openid.js +38 -0
  28. package/dist/lib/authentication/providers/openid.js.map +1 -1
  29. package/dist/lib/authentication/providers/supabase.js +2 -9
  30. package/dist/lib/authentication/providers/supabase.js.map +1 -1
  31. package/dist/lib/authentication/state.d.ts +6 -5
  32. package/dist/lib/authentication/state.js +19 -6
  33. package/dist/lib/authentication/state.js.map +1 -1
  34. package/dist/lib/components/context/ZudokuProvider.d.ts +1 -1
  35. package/dist/lib/components/index.d.ts +6 -0
  36. package/dist/lib/components/navigation/SidebarItem.js +2 -3
  37. package/dist/lib/components/navigation/SidebarItem.js.map +1 -1
  38. package/dist/lib/hooks/index.d.ts +6 -0
  39. package/dist/lib/plugins/openapi/PlaygroundDialogWrapper.d.ts +1 -1
  40. package/dist/lib/plugins/openapi/playground/Headers.d.ts +2 -2
  41. package/dist/lib/plugins/openapi/playground/fileUtils.js +1 -1
  42. package/dist/lib/plugins/openapi/playground/fileUtils.js.map +1 -1
  43. package/dist/lib/util/MdxComponents.js +1 -1
  44. package/dist/lib/util/MdxComponents.js.map +1 -1
  45. package/dist/vite/build.js +2 -32
  46. package/dist/vite/build.js.map +1 -1
  47. package/lib/{Markdown-Cm4kj26S.js → Markdown-BUE2ViaD.js} +340 -337
  48. package/lib/{Markdown-Cm4kj26S.js.map → Markdown-BUE2ViaD.js.map} +1 -1
  49. package/lib/{MdxPage-fDGQtB5w.js → MdxPage-By4UkRkI.js} +4 -4
  50. package/lib/{MdxPage-fDGQtB5w.js.map → MdxPage-By4UkRkI.js.map} +1 -1
  51. package/lib/{OasProvider-CFBvfR3r.js → OasProvider-C6_Kx5O7.js} +2 -2
  52. package/lib/{OasProvider-CFBvfR3r.js.map → OasProvider-C6_Kx5O7.js.map} +1 -1
  53. package/lib/{OperationList-Xs4KWmsh.js → OperationList-BISd29LY.js} +6 -6
  54. package/lib/OperationList-BISd29LY.js.map +1 -0
  55. package/lib/{RouteGuard-CZ_uLv3g.js → RouteGuard-CgmsSw7T.js} +2 -2
  56. package/lib/{RouteGuard-CZ_uLv3g.js.map → RouteGuard-CgmsSw7T.js.map} +1 -1
  57. package/lib/{SchemaList-BWaNlmUJ.js → SchemaList-BqnRo5ov.js} +6 -6
  58. package/lib/{SchemaList-BWaNlmUJ.js.map → SchemaList-BqnRo5ov.js.map} +1 -1
  59. package/lib/{SchemaView-DdKJt2ln.js → SchemaView-CtYJpxQI.js} +3 -3
  60. package/lib/{SchemaView-DdKJt2ln.js.map → SchemaView-CtYJpxQI.js.map} +1 -1
  61. package/lib/{SignUp-B-1Pvc-8.js → SignUp-CrjeBbqN.js} +2 -2
  62. package/lib/{SignUp-B-1Pvc-8.js.map → SignUp-CrjeBbqN.js.map} +1 -1
  63. package/lib/{Slot-B99cbD-q.js → Slot-DANV2b7_.js} +4 -4
  64. package/lib/{Slot-B99cbD-q.js.map → Slot-DANV2b7_.js.map} +1 -1
  65. package/lib/{SyntaxHighlight-Cz6Me7-F.js → SyntaxHighlight-DtvR7RLF.js} +1291 -1265
  66. package/lib/SyntaxHighlight-DtvR7RLF.js.map +1 -0
  67. package/lib/{Toc-Qe7A4uj_.js → Toc-ClJBmdtI.js} +2 -2
  68. package/lib/{Toc-Qe7A4uj_.js.map → Toc-ClJBmdtI.js.map} +1 -1
  69. package/lib/{circular-w5eL5J8a.js → circular-pOdgLzpG.js} +2 -2
  70. package/lib/{circular-w5eL5J8a.js.map → circular-pOdgLzpG.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-p3yUA8Bu.js → createServer-qAtUf99r.js} +2625 -2615
  74. package/lib/createServer-qAtUf99r.js.map +1 -0
  75. package/lib/errors-Bpodza84.js +78 -0
  76. package/lib/errors-Bpodza84.js.map +1 -0
  77. package/lib/{hook-k7PfUIsj.js → hook-wIlTGE-2.js} +52 -31
  78. package/lib/{hook-k7PfUIsj.js.map → hook-wIlTGE-2.js.map} +1 -1
  79. package/lib/{index-yqBxBqxF.js → index-RFzRn4fM.js} +10 -10
  80. package/lib/index-RFzRn4fM.js.map +1 -0
  81. package/lib/{mutation-BSeQ8pEK.js → mutation-C-kdA_1r.js} +2 -2
  82. package/lib/{mutation-BSeQ8pEK.js.map → mutation-C-kdA_1r.js.map} +1 -1
  83. package/lib/ui/SyntaxHighlight.js +2 -2
  84. package/lib/{useMutation-CZSmsIGW.js → useMutation-3Ph3x6En.js} +3 -3
  85. package/lib/{useMutation-CZSmsIGW.js.map → useMutation-3Ph3x6En.js.map} +1 -1
  86. package/lib/zudoku.auth-auth0.js +2 -2
  87. package/lib/zudoku.auth-auth0.js.map +1 -1
  88. package/lib/zudoku.auth-azureb2c.js +9974 -0
  89. package/lib/zudoku.auth-azureb2c.js.map +1 -0
  90. package/lib/zudoku.auth-clerk.js +39 -48
  91. package/lib/zudoku.auth-clerk.js.map +1 -1
  92. package/lib/zudoku.auth-openid.js +291 -316
  93. package/lib/zudoku.auth-openid.js.map +1 -1
  94. package/lib/zudoku.components.js +1222 -1594
  95. package/lib/zudoku.components.js.map +1 -1
  96. package/lib/zudoku.hooks.js +2 -2
  97. package/lib/zudoku.plugin-api-catalog.js +2 -2
  98. package/lib/zudoku.plugin-api-keys.js +3 -3
  99. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  100. package/lib/zudoku.plugin-custom-pages.js +1 -1
  101. package/lib/zudoku.plugin-markdown.js +1 -1
  102. package/lib/zudoku.plugin-openapi.js +2 -2
  103. package/lib/zudoku.plugin-search-pagefind.js +2 -2
  104. package/package.json +37 -19
  105. package/src/lib/auth/issuer.test.ts +104 -0
  106. package/src/lib/auth/issuer.ts +38 -0
  107. package/src/lib/authentication/providers/auth0.tsx +1 -1
  108. package/src/lib/authentication/providers/azureb2c.tsx +196 -0
  109. package/src/lib/authentication/providers/clerk.tsx +3 -12
  110. package/src/lib/authentication/providers/openid.tsx +53 -0
  111. package/src/lib/authentication/providers/supabase.tsx +2 -9
  112. package/src/lib/authentication/state.ts +37 -7
  113. package/src/lib/components/context/ZudokuProvider.tsx +1 -1
  114. package/src/lib/components/navigation/SidebarItem.tsx +2 -1
  115. package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +1 -1
  116. package/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx +1 -1
  117. package/src/lib/plugins/openapi/playground/Headers.tsx +2 -2
  118. package/src/lib/plugins/openapi/playground/fileUtils.ts +1 -1
  119. package/src/lib/util/MdxComponents.tsx +1 -1
  120. package/lib/OperationList-Xs4KWmsh.js.map +0 -1
  121. package/lib/SyntaxHighlight-Cz6Me7-F.js.map +0 -1
  122. package/lib/createServer-p3yUA8Bu.js.map +0 -1
  123. package/lib/index-yqBxBqxF.js.map +0 -1
@@ -0,0 +1,104 @@
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 supabase URL for supabase authentication", async () => {
73
+ const config: ZudokuConfig = {
74
+ authentication: {
75
+ type: "supabase",
76
+ supabaseUrl: "https://project.supabase.co",
77
+ supabaseKey: "test-anon-key",
78
+ provider: "github",
79
+ },
80
+ };
81
+
82
+ const result = await getIssuer(config);
83
+ expect(result).toBe("https://project.supabase.co");
84
+ });
85
+
86
+ it("should return undefined for no authentication", async () => {
87
+ const config: ZudokuConfig = {};
88
+
89
+ const result = await getIssuer(config);
90
+ expect(result).toBeUndefined();
91
+ });
92
+
93
+ it("should throw error for unsupported authentication type", async () => {
94
+ const config = {
95
+ authentication: {
96
+ type: "unsupported" as any,
97
+ },
98
+ } as ZudokuConfig;
99
+
100
+ await expect(getIssuer(config)).rejects.toThrow(
101
+ "Unsupported authentication type",
102
+ );
103
+ });
104
+ });
@@ -0,0 +1,38 @@
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 undefined: {
32
+ return undefined;
33
+ }
34
+ default: {
35
+ throw new Error(`Unsupported authentication type`);
36
+ }
37
+ }
38
+ };
@@ -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
  });
@@ -6,12 +6,15 @@ export interface AuthState<ProviderData = unknown> {
6
6
  isPending: boolean;
7
7
  profile: UserProfile | null;
8
8
  providerData: ProviderData | null;
9
- }
10
-
11
- export class Authentication {
12
- async setLoggedIn(isLoggedIn: boolean) {}
13
- async setProfile() {}
14
- async setPersistentProviderData() {}
9
+ setAuthenticationPending: () => void;
10
+ setLoggedOut: () => void;
11
+ setLoggedIn: ({
12
+ profile,
13
+ providerData,
14
+ }: {
15
+ profile: UserProfile;
16
+ providerData: unknown;
17
+ }) => void;
15
18
  }
16
19
 
17
20
  export type StoreWithPersist<T> = Mutate<
@@ -35,11 +38,38 @@ const withStorageDOMEvents = <T>(store: StoreWithPersist<T>) => {
35
38
 
36
39
  export const useAuthState = create<AuthState>()(
37
40
  persist(
38
- (state) => ({
41
+ (set) => ({
39
42
  isAuthenticated: false,
40
43
  isPending: true,
41
44
  profile: null,
42
45
  providerData: null,
46
+ setAuthenticationPending: () =>
47
+ set(() => ({
48
+ isAuthenticated: false,
49
+ isPending: false,
50
+ profile: null,
51
+ providerData: null,
52
+ })),
53
+ setLoggedOut: () =>
54
+ set(() => ({
55
+ isAuthenticated: false,
56
+ isPending: false,
57
+ profile: null,
58
+ providerData: null,
59
+ })),
60
+ setLoggedIn: ({
61
+ profile,
62
+ providerData,
63
+ }: {
64
+ profile: UserProfile;
65
+ providerData: unknown;
66
+ }) =>
67
+ set(() => ({
68
+ isAuthenticated: true,
69
+ isPending: false,
70
+ profile,
71
+ providerData,
72
+ })),
43
73
  }),
44
74
  {
45
75
  merge: (persistedState, currentState) => {
@@ -1,6 +1,6 @@
1
1
  import { useSuspenseQuery } from "@tanstack/react-query";
2
2
  import type { PropsWithChildren } from "react";
3
- import { ZudokuContext } from "../../core/ZudokuContext.js";
3
+ import { type ZudokuContext } from "../../core/ZudokuContext.js";
4
4
  import { NO_DEHYDRATE } from "../cache.js";
5
5
  import { ZudokuReactContext } from "./ZudokuContext.js";
6
6
 
@@ -112,10 +112,10 @@ export const SidebarItem = ({
112
112
  {...{ [DATA_ANCHOR_ATTR]: item.href.split("#")[1] }}
113
113
  className={navigationListItem({
114
114
  isActive: item.href === [location.pathname, activeAnchor].join("#"),
115
- className: item.badge?.placement !== "start" && "justify-between",
116
115
  })}
117
116
  onClick={onRequestClose}
118
117
  >
118
+ {item.icon && <item.icon size={16} className="align-[-0.125em]" />}
119
119
  {item.badge ? (
120
120
  <>
121
121
  <TruncatedLabel label={item.label} />
@@ -133,6 +133,7 @@ export const SidebarItem = ({
133
133
  rel="noopener noreferrer"
134
134
  onClick={onRequestClose}
135
135
  >
136
+ {item.icon && <item.icon size={16} className="align-[-0.125em]" />}
136
137
  <span className="whitespace-normal">{item.label}</span>
137
138
  {/* This prevents that the icon would be positioned in its own line if the text fills a line entirely */}
138
139
  <span className="whitespace-nowrap">
@@ -32,7 +32,7 @@ import { Slot } from "../../components/Slot.js";
32
32
  import { Button } from "../../ui/Button.js";
33
33
  import { Input } from "../../ui/Input.js";
34
34
  import { cn } from "../../util/cn.js";
35
- import { ApiConsumer, type ApiKey, type ApiKeyService } from "./index.js";
35
+ import { type ApiConsumer, type ApiKey, type ApiKeyService } from "./index.js";
36
36
 
37
37
  export const SettingsApiKeys = ({ service }: { service: ApiKeyService }) => {
38
38
  const context = useZudoku();
@@ -1,7 +1,7 @@
1
1
  import { useAuth } from "zudoku/components";
2
2
  import type { OperationListItemResult } from "./OperationList.js";
3
3
  import { PlaygroundDialog } from "./playground/PlaygroundDialog.js";
4
- import { Content } from "./SidecarExamples.js";
4
+ import { type Content } from "./SidecarExamples.js";
5
5
 
6
6
  export const PlaygroundDialogWrapper = ({
7
7
  server,
@@ -1,7 +1,7 @@
1
1
  import { XIcon } from "lucide-react";
2
2
  import { useCallback, useEffect, useRef } from "react";
3
3
  import {
4
- Control,
4
+ type Control,
5
5
  Controller,
6
6
  useFieldArray,
7
7
  useFormContext,
@@ -12,7 +12,7 @@ import { Autocomplete } from "../../../components/Autocomplete.js";
12
12
  import { Button } from "../../../ui/Button.js";
13
13
  import { Input } from "../../../ui/Input.js";
14
14
  import ParamsGrid, { ParamsGridItem } from "./ParamsGrid.js";
15
- import { Header, type PlaygroundForm } from "./Playground.js";
15
+ import { type Header, type PlaygroundForm } from "./Playground.js";
16
16
 
17
17
  const headerOptions = Object.freeze([
18
18
  "Accept",
@@ -16,7 +16,7 @@ export const extractFileName = (
16
16
  const filenameMatch = contentDisposition.match(
17
17
  /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/,
18
18
  );
19
- if (filenameMatch && filenameMatch[1]) {
19
+ if (filenameMatch?.[1]) {
20
20
  return filenameMatch[1].replace(/['"]/g, "");
21
21
  }
22
22
  }
@@ -16,7 +16,7 @@ export const MdxComponents = {
16
16
  if (/\.(mp4|webm|mov|avi)$/.test(props.src ?? "")) {
17
17
  return <video src={props.src} controls playsInline autoPlay loop />;
18
18
  }
19
- return <img {...props} className="rounded-md" />;
19
+ return <img {...props} className={cn("rounded-md", props.className)} />;
20
20
  },
21
21
  h1: ({ children, id }) => (
22
22
  <Heading level={1} id={id}>