zudoku 0.82.2 → 0.82.4
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.
- package/dist/cli/cli.js +283 -225
- package/dist/cli/worker.js +23 -0
- package/dist/declarations/app/wrapProtectedRoutes.d.ts +5 -0
- package/dist/declarations/config/config.d.ts +3 -0
- package/dist/declarations/config/plugin-versions.d.ts +5 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +38 -0
- package/dist/declarations/lib/authentication/providers/entra.d.ts +13 -0
- package/dist/declarations/lib/authentication/providers/openid.d.ts +4 -1
- package/dist/declarations/lib/authentication/providers/util.d.ts +4 -0
- package/dist/declarations/lib/plugins/openapi/McpClientLogos.d.ts +4 -0
- package/dist/declarations/lib/plugins/openapi/mcp-configs.d.ts +3 -0
- package/dist/declarations/vite/package-root.d.ts +1 -0
- package/dist/flat-config.d.ts +17 -0
- package/docs/components/landing-page.mdx +1 -1
- package/docs/configuration/authentication-azure-ad.md +17 -15
- package/docs/configuration/authentication.md +22 -2
- package/docs/guides/mcp-servers.md +26 -0
- package/package.json +21 -17
- package/src/app/entry.server.tsx +9 -5
- package/src/app/wrapProtectedRoutes.ts +25 -0
- package/src/config/config.ts +4 -0
- package/src/config/loader.ts +15 -0
- package/src/config/plugin-versions.ts +38 -0
- package/src/config/validators/ZudokuConfig.ts +19 -0
- package/src/lib/auth/issuer.ts +7 -1
- package/src/lib/authentication/providers/entra.tsx +97 -0
- package/src/lib/authentication/providers/openid.tsx +29 -13
- package/src/lib/authentication/providers/util.ts +7 -0
- package/src/lib/components/navigation/NavigationCategory.tsx +3 -2
- package/src/lib/components/navigation/StackRows.tsx +1 -1
- package/src/lib/plugins/openapi/MCPEndpoint.tsx +347 -228
- package/src/lib/plugins/openapi/McpClientLogos.tsx +72 -0
- package/src/lib/plugins/openapi/mcp-configs.ts +55 -0
- package/src/vite/api/SchemaManager.ts +11 -25
- package/src/vite/plugin-mdx.ts +3 -0
- package/src/vite/prerender/prerender.ts +28 -3
- package/src/vite/prerender/utils.ts +33 -0
- package/src/vite/prerender/worker.ts +8 -1
- package/src/zuplo/enrich-with-zuplo-mcp.ts +14 -2
package/dist/cli/worker.js
CHANGED
|
@@ -2617,6 +2617,25 @@ var AuthenticationSchema = z4.discriminatedUnion("type", [
|
|
|
2617
2617
|
authorizationParams: z4.record(z4.string(), z4.string()).optional(),
|
|
2618
2618
|
forwardAuthorizationParams: z4.array(z4.string()).optional()
|
|
2619
2619
|
}),
|
|
2620
|
+
z4.object({
|
|
2621
|
+
type: z4.literal("entra"),
|
|
2622
|
+
basePath: z4.string().optional(),
|
|
2623
|
+
clientId: z4.string(),
|
|
2624
|
+
// Tenant id or one of Entra's multi-tenant authorities
|
|
2625
|
+
// (common/organizations/consumers). Defaults to "common".
|
|
2626
|
+
tenantId: z4.string().optional(),
|
|
2627
|
+
// Full issuer override, e.g. for CIAM tenants on *.ciamlogin.com.
|
|
2628
|
+
issuer: z4.string().optional(),
|
|
2629
|
+
audience: z4.string().optional(),
|
|
2630
|
+
scopes: z4.array(z4.string()).optional(),
|
|
2631
|
+
redirectToAfterSignUp: z4.string().optional(),
|
|
2632
|
+
redirectToAfterSignIn: z4.string().optional(),
|
|
2633
|
+
redirectToAfterSignOut: z4.string().optional(),
|
|
2634
|
+
signUp: SignUpOpenIdSchema.optional(),
|
|
2635
|
+
disableSignUp: z4.boolean().optional(),
|
|
2636
|
+
authorizationParams: z4.record(z4.string(), z4.string()).optional(),
|
|
2637
|
+
forwardAuthorizationParams: z4.array(z4.string()).optional()
|
|
2638
|
+
}),
|
|
2620
2639
|
z4.object({
|
|
2621
2640
|
type: z4.literal("azureb2c"),
|
|
2622
2641
|
basePath: z4.string().optional(),
|
|
@@ -2921,6 +2940,7 @@ var renderPage = async ({ urlPath }) => {
|
|
|
2921
2940
|
outputPath,
|
|
2922
2941
|
redirect: { from: pathname, to: redirectTo },
|
|
2923
2942
|
statusCode: response.status,
|
|
2943
|
+
indexStatusCode: response.status,
|
|
2924
2944
|
html: ""
|
|
2925
2945
|
};
|
|
2926
2946
|
}
|
|
@@ -2931,6 +2951,7 @@ var renderPage = async ({ urlPath }) => {
|
|
|
2931
2951
|
}
|
|
2932
2952
|
const fileContent = response.body ? await response.text() : "";
|
|
2933
2953
|
let html = fileContent;
|
|
2954
|
+
let indexStatusCode = response.status;
|
|
2934
2955
|
if (isProtectedRoute) {
|
|
2935
2956
|
const bypassRequest = new Request(url);
|
|
2936
2957
|
const bypassResponse = await server.handleRequest({
|
|
@@ -2946,12 +2967,14 @@ var renderPage = async ({ urlPath }) => {
|
|
|
2946
2967
|
);
|
|
2947
2968
|
}
|
|
2948
2969
|
html = bypassResponse.body ? await bypassResponse.text() : "";
|
|
2970
|
+
indexStatusCode = bypassResponse.status;
|
|
2949
2971
|
}
|
|
2950
2972
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
2951
2973
|
await fs.writeFile(outputPath, fileContent);
|
|
2952
2974
|
return {
|
|
2953
2975
|
outputPath,
|
|
2954
2976
|
statusCode: response.status,
|
|
2977
|
+
indexStatusCode,
|
|
2955
2978
|
redirect: void 0,
|
|
2956
2979
|
html
|
|
2957
2980
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { RouteObject } from "react-router";
|
|
2
2
|
import type { ProtectedRoutesInput } from "../config/validators/ProtectedRoutesSchema.js";
|
|
3
3
|
export declare const wrapProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, isAuthenticated: boolean, basePath?: string) => RouteObject[];
|
|
4
|
+
export declare const wrapProtectedRoutesForRender: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, { isAuthenticated, bypassProtection, basePath, }: {
|
|
5
|
+
isAuthenticated: boolean;
|
|
6
|
+
bypassProtection?: boolean;
|
|
7
|
+
basePath?: string;
|
|
8
|
+
}) => RouteObject[];
|
|
4
9
|
export declare const warnInlineProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, basePath?: string) => void;
|
|
@@ -22,3 +22,6 @@ export type FirebaseAuthenticationConfig = Extract<AuthenticationConfig, {
|
|
|
22
22
|
export type AzureB2CAuthenticationConfig = Extract<AuthenticationConfig, {
|
|
23
23
|
type: "azureb2c";
|
|
24
24
|
}>;
|
|
25
|
+
export type EntraAuthenticationConfig = Extract<AuthenticationConfig, {
|
|
26
|
+
type: "entra";
|
|
27
|
+
}>;
|
|
@@ -248,6 +248,25 @@ declare const AuthenticationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
248
248
|
disableSignUp: z.ZodOptional<z.ZodBoolean>;
|
|
249
249
|
authorizationParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
250
250
|
forwardAuthorizationParams: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
251
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
252
|
+
type: z.ZodLiteral<"entra">;
|
|
253
|
+
basePath: z.ZodOptional<z.ZodString>;
|
|
254
|
+
clientId: z.ZodString;
|
|
255
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
256
|
+
issuer: z.ZodOptional<z.ZodString>;
|
|
257
|
+
audience: z.ZodOptional<z.ZodString>;
|
|
258
|
+
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
259
|
+
redirectToAfterSignUp: z.ZodOptional<z.ZodString>;
|
|
260
|
+
redirectToAfterSignIn: z.ZodOptional<z.ZodString>;
|
|
261
|
+
redirectToAfterSignOut: z.ZodOptional<z.ZodString>;
|
|
262
|
+
signUp: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
263
|
+
url: z.ZodString;
|
|
264
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
265
|
+
authorizationParams: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
266
|
+
}, z.core.$strict>]>>;
|
|
267
|
+
disableSignUp: z.ZodOptional<z.ZodBoolean>;
|
|
268
|
+
authorizationParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
269
|
+
forwardAuthorizationParams: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
251
270
|
}, z.core.$strip>, z.ZodObject<{
|
|
252
271
|
type: z.ZodLiteral<"azureb2c">;
|
|
253
272
|
basePath: z.ZodOptional<z.ZodString>;
|
|
@@ -7040,6 +7059,25 @@ export declare const ZudokuConfig: z.ZodObject<{
|
|
|
7040
7059
|
disableSignUp: z.ZodOptional<z.ZodBoolean>;
|
|
7041
7060
|
authorizationParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
7042
7061
|
forwardAuthorizationParams: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7062
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
7063
|
+
type: z.ZodLiteral<"entra">;
|
|
7064
|
+
basePath: z.ZodOptional<z.ZodString>;
|
|
7065
|
+
clientId: z.ZodString;
|
|
7066
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
7067
|
+
issuer: z.ZodOptional<z.ZodString>;
|
|
7068
|
+
audience: z.ZodOptional<z.ZodString>;
|
|
7069
|
+
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7070
|
+
redirectToAfterSignUp: z.ZodOptional<z.ZodString>;
|
|
7071
|
+
redirectToAfterSignIn: z.ZodOptional<z.ZodString>;
|
|
7072
|
+
redirectToAfterSignOut: z.ZodOptional<z.ZodString>;
|
|
7073
|
+
signUp: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
7074
|
+
url: z.ZodString;
|
|
7075
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
7076
|
+
authorizationParams: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
7077
|
+
}, z.core.$strict>]>>;
|
|
7078
|
+
disableSignUp: z.ZodOptional<z.ZodBoolean>;
|
|
7079
|
+
authorizationParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
7080
|
+
forwardAuthorizationParams: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7043
7081
|
}, z.core.$strip>, z.ZodObject<{
|
|
7044
7082
|
type: z.ZodLiteral<"azureb2c">;
|
|
7045
7083
|
basePath: z.ZodOptional<z.ZodString>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as oauth from "oauth4webapi";
|
|
2
|
+
import type { EntraAuthenticationConfig } from "../../../config/config.js";
|
|
3
|
+
import type { AuthenticationPlugin, AuthenticationProviderInitializer } from "../authentication.js";
|
|
4
|
+
import type { UserProfile } from "../state.js";
|
|
5
|
+
import { OpenIDAuthenticationProvider } from "./openid.js";
|
|
6
|
+
export declare class EntraAuthenticationProvider extends OpenIDAuthenticationProvider implements AuthenticationPlugin {
|
|
7
|
+
constructor(config: EntraAuthenticationConfig);
|
|
8
|
+
protected getExpectedDiscoveryIssuer(issuerUrl: URL, response: Response): Promise<URL>;
|
|
9
|
+
protected resolveTokenIssuer(as: oauth.AuthorizationServer, response: Response): Promise<oauth.AuthorizationServer>;
|
|
10
|
+
protected buildUserProfile(userInfo: oauth.UserInfoResponse, claims: oauth.IDToken | undefined): UserProfile;
|
|
11
|
+
}
|
|
12
|
+
declare const entraAuth: AuthenticationProviderInitializer<EntraAuthenticationConfig>;
|
|
13
|
+
export default entraAuth;
|
|
@@ -3,6 +3,7 @@ import type { NavigateFunction } from "react-router";
|
|
|
3
3
|
import type { OpenIDAuthenticationConfig } from "../../../config/config.js";
|
|
4
4
|
import type { AuthActionContext, AuthActionOptions, AuthenticationPlugin, AuthenticationProviderInitializer, VerifyAccessTokenResult } from "../authentication.js";
|
|
5
5
|
import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
|
|
6
|
+
import { type UserProfile } from "../state.js";
|
|
6
7
|
export interface OpenIdProviderData {
|
|
7
8
|
type: "openid" | undefined;
|
|
8
9
|
accessToken: string;
|
|
@@ -48,6 +49,8 @@ export declare class OpenIDAuthenticationProvider extends CoreAuthenticationPlug
|
|
|
48
49
|
[oauth.allowInsecureRequests]?: undefined;
|
|
49
50
|
};
|
|
50
51
|
protected getAuthServer(): Promise<oauth.AuthorizationServer>;
|
|
52
|
+
protected getExpectedDiscoveryIssuer(issuerUrl: URL, _response: Response): Promise<URL>;
|
|
53
|
+
protected resolveTokenIssuer(as: oauth.AuthorizationServer, _response: Response): Promise<oauth.AuthorizationServer>;
|
|
51
54
|
protected setTokensFromResponse(response: oauth.TokenEndpointResponse): void;
|
|
52
55
|
signUp({ navigate }: {
|
|
53
56
|
navigate: NavigateFunction;
|
|
@@ -56,7 +59,7 @@ export declare class OpenIDAuthenticationProvider extends CoreAuthenticationPlug
|
|
|
56
59
|
replace?: boolean;
|
|
57
60
|
}): Promise<void>;
|
|
58
61
|
signIn(_: AuthActionContext, { redirectTo, replace }: AuthActionOptions): Promise<void>;
|
|
59
|
-
|
|
62
|
+
protected buildUserProfile(userInfo: oauth.UserInfoResponse, claims: oauth.IDToken | undefined): UserProfile;
|
|
60
63
|
verifyAccessToken(token: string): Promise<VerifyAccessTokenResult>;
|
|
61
64
|
refreshUserProfile(): Promise<boolean>;
|
|
62
65
|
private authorize;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { NavigateFunction } from "react-router";
|
|
2
2
|
export declare const redirectToSignUpUrl: (url: string, navigate: NavigateFunction, replace?: boolean) => void;
|
|
3
|
+
export declare const getEntraIssuer: (config: {
|
|
4
|
+
issuer?: string;
|
|
5
|
+
tenantId?: string;
|
|
6
|
+
}) => string;
|
|
3
7
|
export declare const getClerkFrontendApi: (publishableKey: string) => string;
|
|
@@ -26,3 +26,6 @@ export declare const getCursorConfig: (name: string, mcpUrl: string, auth?: Auth
|
|
|
26
26
|
export declare const getVscodeConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
|
|
27
27
|
export declare const getCodexConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
|
|
28
28
|
export declare const getGenericConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
|
|
29
|
+
export declare const getCursorDeepLink: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
|
|
30
|
+
export declare const getVscodeDeepLink: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
|
|
31
|
+
export declare const CLAUDE_CONNECTORS_URL = "https://claude.ai/customize/connectors?modal=add-custom-connector";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const findPackageRoot: (startDir: string) => Promise<string | undefined>;
|
package/dist/flat-config.d.ts
CHANGED
|
@@ -303,6 +303,23 @@ export interface FlatZudokuConfig {
|
|
|
303
303
|
[k: string]: string
|
|
304
304
|
}
|
|
305
305
|
forwardAuthorizationParams?: string[]
|
|
306
|
+
} | {
|
|
307
|
+
type: "entra"
|
|
308
|
+
basePath?: string
|
|
309
|
+
clientId: string
|
|
310
|
+
tenantId?: string
|
|
311
|
+
issuer?: string
|
|
312
|
+
audience?: string
|
|
313
|
+
scopes?: string[]
|
|
314
|
+
redirectToAfterSignUp?: string
|
|
315
|
+
redirectToAfterSignIn?: string
|
|
316
|
+
redirectToAfterSignOut?: string
|
|
317
|
+
signUp?: _Schema13
|
|
318
|
+
disableSignUp?: boolean
|
|
319
|
+
authorizationParams?: {
|
|
320
|
+
[k: string]: string
|
|
321
|
+
}
|
|
322
|
+
forwardAuthorizationParams?: string[]
|
|
306
323
|
} | {
|
|
307
324
|
type: "azureb2c"
|
|
308
325
|
basePath?: string
|
|
@@ -10,7 +10,7 @@ A ready-made, customizable landing page for your developer portal. It comes in t
|
|
|
10
10
|
covering the most common layouts: a centered hero, a two-column split hero, and a compact
|
|
11
11
|
documentation hub.
|
|
12
12
|
|
|
13
|
-
Use it as the `element` of a [custom page](/docs/custom-pages) — typically your home page:
|
|
13
|
+
Use it as the `element` of a [custom page](/docs/guides/custom-pages) — typically your home page:
|
|
14
14
|
|
|
15
15
|
```tsx title=zudoku.config.tsx
|
|
16
16
|
import { LandingPage } from "zudoku/components";
|
|
@@ -72,9 +72,9 @@ to integrate Azure AD with your Zudoku documentation site.
|
|
|
72
72
|
export default {
|
|
73
73
|
// ... other configuration
|
|
74
74
|
authentication: {
|
|
75
|
-
type: "
|
|
75
|
+
type: "entra",
|
|
76
76
|
clientId: "<your-application-client-id>",
|
|
77
|
-
|
|
77
|
+
tenantId: "<your-tenant-id>",
|
|
78
78
|
scopes: ["openid", "profile", "email"], // Optional: customize scopes
|
|
79
79
|
},
|
|
80
80
|
// ... other configuration
|
|
@@ -91,20 +91,21 @@ For single tenant (organization-only access):
|
|
|
91
91
|
|
|
92
92
|
```typescript
|
|
93
93
|
authentication: {
|
|
94
|
-
type: "
|
|
94
|
+
type: "entra",
|
|
95
95
|
clientId: "<your-application-client-id>",
|
|
96
|
-
|
|
96
|
+
tenantId: "<your-tenant-id>",
|
|
97
97
|
scopes: ["openid", "profile", "email"],
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
For multitenant (any Azure AD organization):
|
|
101
|
+
For multitenant (any Azure AD organization), use `tenantId: "common"` or `"organizations"` — this is
|
|
102
|
+
also the default if `tenantId` is omitted:
|
|
102
103
|
|
|
103
104
|
```typescript
|
|
104
105
|
authentication: {
|
|
105
|
-
type: "
|
|
106
|
+
type: "entra",
|
|
106
107
|
clientId: "<your-application-client-id>",
|
|
107
|
-
|
|
108
|
+
tenantId: "common",
|
|
108
109
|
scopes: ["openid", "profile", "email"],
|
|
109
110
|
}
|
|
110
111
|
```
|
|
@@ -115,9 +116,9 @@ Request additional Microsoft Graph API scopes:
|
|
|
115
116
|
|
|
116
117
|
```typescript
|
|
117
118
|
authentication: {
|
|
118
|
-
type: "
|
|
119
|
+
type: "entra",
|
|
119
120
|
clientId: "<your-application-client-id>",
|
|
120
|
-
|
|
121
|
+
tenantId: "<your-tenant-id>",
|
|
121
122
|
scopes: [
|
|
122
123
|
"openid",
|
|
123
124
|
"profile",
|
|
@@ -136,7 +137,7 @@ Protect specific documentation routes using the `protectedRoutes` configuration:
|
|
|
136
137
|
{
|
|
137
138
|
// ... other configuration
|
|
138
139
|
authentication: {
|
|
139
|
-
type: "
|
|
140
|
+
type: "entra",
|
|
140
141
|
// ... Azure AD config
|
|
141
142
|
},
|
|
142
143
|
protectedRoutes: [
|
|
@@ -217,16 +218,17 @@ Azure AD provides rich user profile data through OpenID Connect:
|
|
|
217
218
|
2. **Redirect URI Mismatch**: The redirect URI must exactly match one configured in Azure AD,
|
|
218
219
|
including protocol and path.
|
|
219
220
|
|
|
220
|
-
3. **Tenant Access Issues**: For single-tenant apps, ensure users are from the correct tenant
|
|
221
|
-
multi-tenant,
|
|
221
|
+
3. **Tenant Access Issues**: For single-tenant apps, ensure users are from the correct tenant and
|
|
222
|
+
`tenantId` is set to that tenant's GUID. For multi-tenant, use `tenantId: "common"` or
|
|
223
|
+
`"organizations"`.
|
|
222
224
|
|
|
223
225
|
4. **Missing User Information**: Check that required API permissions are granted and admin consent
|
|
224
226
|
is provided if needed.
|
|
225
227
|
|
|
226
|
-
5. **Token Validation Errors**: Ensure your
|
|
227
|
-
|
|
228
|
+
5. **Token Validation Errors**: Ensure your `tenantId` is correct. If you use a CIAM tenant
|
|
229
|
+
(`*.ciamlogin.com`), set the `issuer` option to override the default issuer instead.
|
|
228
230
|
|
|
229
|
-
6. **Authentication Not Working**: Verify your
|
|
231
|
+
6. **Authentication Not Working**: Verify your `tenantId` and client ID are correct, and that your
|
|
230
232
|
app registration is configured as a Single-page application (SPA) with the correct redirect URIs.
|
|
231
233
|
|
|
232
234
|
## Security Best Practices
|
|
@@ -15,8 +15,8 @@ authentication provider you use.
|
|
|
15
15
|
|
|
16
16
|
## Authentication Providers
|
|
17
17
|
|
|
18
|
-
Zudoku supports Clerk, Auth0, Supabase, Firebase, Azure B2C, and any OpenID
|
|
19
|
-
(including Okta, Keycloak, Authentik, and PingFederate).
|
|
18
|
+
Zudoku supports Clerk, Auth0, Supabase, Firebase, Microsoft Entra ID, Azure B2C, and any OpenID
|
|
19
|
+
Connect provider (including Okta, Keycloak, Authentik, and PingFederate).
|
|
20
20
|
|
|
21
21
|
Not seeing your authentication provider? [Let us know](https://github.com/zuplo/zudoku/issues)
|
|
22
22
|
|
|
@@ -99,6 +99,26 @@ providing your own array of scopes.
|
|
|
99
99
|
For provider-specific guides (Okta, Keycloak, etc.), see the
|
|
100
100
|
[OpenID Connect setup page](./authentication-openid.md).
|
|
101
101
|
|
|
102
|
+
### Microsoft Entra ID
|
|
103
|
+
|
|
104
|
+
For Microsoft Entra ID (formerly Azure AD), you will need the `clientId` from your app registration
|
|
105
|
+
and your `tenantId`.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
{
|
|
109
|
+
// ...
|
|
110
|
+
authentication: {
|
|
111
|
+
type: "entra",
|
|
112
|
+
clientId: "<your-application-client-id>",
|
|
113
|
+
tenantId: "<your-tenant-id>", // Or "common" for multitenant. Defaults to "common".
|
|
114
|
+
},
|
|
115
|
+
// ...
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For full setup instructions, see the
|
|
120
|
+
[Azure AD / Entra ID setup guide](./authentication-azure-ad.md).
|
|
121
|
+
|
|
102
122
|
### Firebase
|
|
103
123
|
|
|
104
124
|
For Firebase authentication, you will need your Firebase project configuration. You can find this in
|
|
@@ -187,3 +187,29 @@ since they use a different interaction model.
|
|
|
187
187
|
If you are using [Zuplo](https://zuplo.com) to host your API, the `x-mcp-server` extension is
|
|
188
188
|
automatically added to POST operations that use the `mcpServerHandler`. No manual schema changes are
|
|
189
189
|
needed. See the [Zuplo MCP documentation](https://zuplo.com/docs/handlers/mcp-server) for details.
|
|
190
|
+
|
|
191
|
+
The server name shown in the install snippets (for example `claude mcp add … 'MCP Server' …`) is
|
|
192
|
+
taken from the handler's `name` option — the same name your MCP server advertises to clients. Set it
|
|
193
|
+
to override the default `"MCP Server"` title:
|
|
194
|
+
|
|
195
|
+
```json title="config/routes.oas.json (paths section)"
|
|
196
|
+
{
|
|
197
|
+
"/mcp": {
|
|
198
|
+
"post": {
|
|
199
|
+
"operationId": "mcpServerHandler",
|
|
200
|
+
"x-zuplo-route": {
|
|
201
|
+
"handler": {
|
|
202
|
+
"export": "mcpServerHandler",
|
|
203
|
+
"module": "$import(@zuplo/runtime)",
|
|
204
|
+
"options": {
|
|
205
|
+
"name": "Acme API",
|
|
206
|
+
"operations": [{ "file": "./config/routes.oas.json", "id": "get-users" }]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
With this configuration the snippets read `claude mcp add --transport http 'Acme API' …`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zudoku",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0 <21.0.0 || >=22.12.0"
|
|
@@ -57,6 +57,10 @@
|
|
|
57
57
|
"types": "./dist/declarations/lib/authentication/providers/openid.d.ts",
|
|
58
58
|
"default": "./src/lib/authentication/providers/openid.tsx"
|
|
59
59
|
},
|
|
60
|
+
"./auth/entra": {
|
|
61
|
+
"types": "./dist/declarations/lib/authentication/providers/entra.d.ts",
|
|
62
|
+
"default": "./src/lib/authentication/providers/entra.tsx"
|
|
63
|
+
},
|
|
60
64
|
"./auth/supabase": {
|
|
61
65
|
"types": "./dist/declarations/lib/authentication/providers/supabase.d.ts",
|
|
62
66
|
"default": "./src/lib/authentication/providers/supabase.tsx"
|
|
@@ -173,12 +177,13 @@
|
|
|
173
177
|
},
|
|
174
178
|
"dependencies": {
|
|
175
179
|
"@apidevtools/json-schema-ref-parser": "15.3.5",
|
|
176
|
-
"@base-ui/react": "^1.
|
|
180
|
+
"@base-ui/react": "^1.6.0",
|
|
177
181
|
"@envelop/core": "5.5.1",
|
|
178
182
|
"@graphiql/toolkit": "0.12.1",
|
|
179
183
|
"@graphql-typed-document-node/core": "3.2.0",
|
|
180
|
-
"@hono/node-server": "2.0.
|
|
184
|
+
"@hono/node-server": "2.0.8",
|
|
181
185
|
"@lekoarts/rehype-meta-as-attributes": "3.0.3",
|
|
186
|
+
"@mdx-js/mdx": "3.1.1",
|
|
182
187
|
"@mdx-js/react": "3.1.1",
|
|
183
188
|
"@mdx-js/rollup": "3.1.1",
|
|
184
189
|
"@pothos/core": "4.12.0",
|
|
@@ -206,21 +211,20 @@
|
|
|
206
211
|
"@radix-ui/react-toggle-group": "1.1.11",
|
|
207
212
|
"@radix-ui/react-tooltip": "1.2.8",
|
|
208
213
|
"@radix-ui/react-visually-hidden": "1.2.4",
|
|
209
|
-
"@scalar/openapi-parser": "0.
|
|
214
|
+
"@scalar/openapi-parser": "0.28.4",
|
|
210
215
|
"@scalar/snippetz": "0.9.11",
|
|
211
|
-
"@sentry/node": "10.52.0",
|
|
212
216
|
"@shikijs/langs": "4.2.0",
|
|
213
217
|
"@shikijs/rehype": "4.2.0",
|
|
214
218
|
"@shikijs/themes": "4.2.0",
|
|
215
219
|
"@shikijs/transformers": "4.2.0",
|
|
216
|
-
"@tailwindcss/typography": "0.5.
|
|
220
|
+
"@tailwindcss/typography": "0.5.20",
|
|
217
221
|
"@tailwindcss/vite": "4.3.0",
|
|
218
222
|
"@tanem/react-nprogress": "6.0.3",
|
|
219
223
|
"@tanstack/react-query": "5.101.0",
|
|
220
224
|
"@types/react": "19.2.17",
|
|
221
225
|
"@types/react-dom": "19.2.3",
|
|
222
|
-
"@vitejs/plugin-react": "6.0.
|
|
223
|
-
"@x0k/json-schema-merge": "1.0.
|
|
226
|
+
"@vitejs/plugin-react": "6.0.3",
|
|
227
|
+
"@x0k/json-schema-merge": "1.0.4",
|
|
224
228
|
"@unhead/react": "3.1.3",
|
|
225
229
|
"@zuplo/mcp": "0.0.32",
|
|
226
230
|
"bs58": "6.0.0",
|
|
@@ -242,14 +246,14 @@
|
|
|
242
246
|
"hast-util-heading-rank": "3.0.0",
|
|
243
247
|
"hast-util-to-jsx-runtime": "2.3.6",
|
|
244
248
|
"hast-util-to-string": "3.0.1",
|
|
245
|
-
"hono": "4.12.
|
|
249
|
+
"hono": "4.12.27",
|
|
246
250
|
"http-terminator": "3.2.0",
|
|
247
251
|
"javascript-stringify": "2.1.0",
|
|
248
252
|
"jose": "6.2.3",
|
|
249
253
|
"json-schema-to-typescript-lite": "15.0.0",
|
|
250
254
|
"loglevel": "1.9.2",
|
|
251
255
|
"lucide-react": "1.16.0",
|
|
252
|
-
"mdast-util-from-markdown": "2.0.
|
|
256
|
+
"mdast-util-from-markdown": "2.0.3",
|
|
253
257
|
"mdast-util-mdx": "3.0.0",
|
|
254
258
|
"mdast-util-mdx-jsx": "3.2.0",
|
|
255
259
|
"micromark-extension-mdxjs": "3.0.0",
|
|
@@ -261,7 +265,7 @@
|
|
|
261
265
|
"pagefind": "1.5.2",
|
|
262
266
|
"picocolors": "1.1.1",
|
|
263
267
|
"piscina": "5.2.0",
|
|
264
|
-
"posthog-node": "5.
|
|
268
|
+
"posthog-node": "5.40.0",
|
|
265
269
|
"quick-lru": "7.3.0",
|
|
266
270
|
"react-error-boundary": "6.1.1",
|
|
267
271
|
"react-hook-form": "7.76.1",
|
|
@@ -277,7 +281,7 @@
|
|
|
277
281
|
"remark-frontmatter": "5.0.0",
|
|
278
282
|
"remark-gfm": "4.0.1",
|
|
279
283
|
"remark-mdx-frontmatter": "5.2.0",
|
|
280
|
-
"semver": "7.8.
|
|
284
|
+
"semver": "7.8.5",
|
|
281
285
|
"shiki": "4.2.0",
|
|
282
286
|
"sitemap": "9.0.1",
|
|
283
287
|
"strip-ansi": "7.2.0",
|
|
@@ -295,7 +299,7 @@
|
|
|
295
299
|
"zustand": "5.0.14"
|
|
296
300
|
},
|
|
297
301
|
"devDependencies": {
|
|
298
|
-
"@graphql-codegen/cli": "7.1.
|
|
302
|
+
"@graphql-codegen/cli": "7.1.3",
|
|
299
303
|
"@inkeep/cxkit-types": "0.5.119",
|
|
300
304
|
"@testing-library/dom": "10.4.1",
|
|
301
305
|
"@testing-library/jest-dom": "6.9.1",
|
|
@@ -307,15 +311,15 @@
|
|
|
307
311
|
"@types/hast": "3.0.4",
|
|
308
312
|
"@types/json-schema": "7.0.15",
|
|
309
313
|
"@types/mdast": "4.0.4",
|
|
310
|
-
"@types/mdx": "2.0.
|
|
314
|
+
"@types/mdx": "2.0.14",
|
|
311
315
|
"@types/node": "22.19.1",
|
|
312
316
|
"@types/react-is": "19.2.0",
|
|
313
317
|
"@types/semver": "7.7.1",
|
|
314
318
|
"@types/unist": "3.0.3",
|
|
315
319
|
"@types/yargs": "17.0.35",
|
|
316
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
317
|
-
"@vitest/coverage-v8": "4.1.
|
|
318
|
-
"happy-dom": "20.10.
|
|
320
|
+
"@typescript/native-preview": "7.0.0-dev.20260624.1",
|
|
321
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
322
|
+
"happy-dom": "20.10.6",
|
|
319
323
|
"oxc-parser": "^0.135.0",
|
|
320
324
|
"react": "19.2.7",
|
|
321
325
|
"react-dom": "19.2.7",
|
package/src/app/entry.server.tsx
CHANGED
|
@@ -26,7 +26,7 @@ import type { Adapter } from "./adapter.js";
|
|
|
26
26
|
import { getRoutesByConfig } from "./main.js";
|
|
27
27
|
import { protectChunks as rawProtectChunks } from "./protectChunks.js";
|
|
28
28
|
import { getSsrCacheControl } from "./ssrCacheControl.js";
|
|
29
|
-
import {
|
|
29
|
+
import { wrapProtectedRoutesForRender } from "./wrapProtectedRoutes.js";
|
|
30
30
|
|
|
31
31
|
export { getRoutesByConfig };
|
|
32
32
|
export type { Adapter, AdapterContext } from "./adapter.js";
|
|
@@ -104,12 +104,16 @@ export const handleRequest = async ({
|
|
|
104
104
|
const ssrAuth = await resolveSsrAuth(request);
|
|
105
105
|
|
|
106
106
|
// No-op lazy() on protected subtrees for unauthed requests so loaders
|
|
107
|
-
// don't run for a 401 render.
|
|
108
|
-
|
|
107
|
+
// don't run for a 401 render. A bypass render (search-index pass) keeps the
|
|
108
|
+
// real content so Pagefind can index protected routes.
|
|
109
|
+
const effectiveRoutes = wrapProtectedRoutesForRender(
|
|
109
110
|
routes,
|
|
110
111
|
config.protectedRoutes,
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
{
|
|
113
|
+
isAuthenticated: !!ssrAuth?.profile,
|
|
114
|
+
bypassProtection,
|
|
115
|
+
basePath,
|
|
116
|
+
},
|
|
113
117
|
);
|
|
114
118
|
|
|
115
119
|
const { query, dataRoutes } = createStaticHandler(effectiveRoutes, {
|
|
@@ -47,6 +47,31 @@ export const wrapProtectedRoutes = (
|
|
|
47
47
|
});
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
// Route-wrapping decision for an SSR/SSG render. The prerender does a second
|
|
51
|
+
// "bypass" pass over protected routes to build the search index; that pass must
|
|
52
|
+
// render the real (unstubbed) protected content, so it is treated as
|
|
53
|
+
// authenticated here. Without this, the bypass pass renders an empty shell and
|
|
54
|
+
// the Pagefind index for protected routes ends up empty (issue #2672).
|
|
55
|
+
export const wrapProtectedRoutesForRender = (
|
|
56
|
+
routes: RouteObject[],
|
|
57
|
+
protectedRoutes: ProtectedRoutesInput,
|
|
58
|
+
{
|
|
59
|
+
isAuthenticated,
|
|
60
|
+
bypassProtection,
|
|
61
|
+
basePath,
|
|
62
|
+
}: {
|
|
63
|
+
isAuthenticated: boolean;
|
|
64
|
+
bypassProtection?: boolean;
|
|
65
|
+
basePath?: string;
|
|
66
|
+
},
|
|
67
|
+
): RouteObject[] =>
|
|
68
|
+
wrapProtectedRoutes(
|
|
69
|
+
routes,
|
|
70
|
+
protectedRoutes,
|
|
71
|
+
isAuthenticated || bypassProtection === true,
|
|
72
|
+
basePath,
|
|
73
|
+
);
|
|
74
|
+
|
|
50
75
|
// Inline elements can't be chunk-isolated; RouteGuard still blocks render,
|
|
51
76
|
// but the JS ships in the main bundle. Only meaningful in dev.
|
|
52
77
|
export const warnInlineProtectedRoutes = (
|
package/src/config/config.ts
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { getZudokuRootDir } from "../cli/common/package-json.js";
|
|
|
12
12
|
import { runPluginTransformConfig } from "../lib/core/transform-config.js";
|
|
13
13
|
import invariant from "../lib/util/invariant.js";
|
|
14
14
|
import { fileExists } from "./file-exists.js";
|
|
15
|
+
import { getPluginVersions } from "./plugin-versions.js";
|
|
15
16
|
import type {
|
|
16
17
|
ResolvedZudokuConfig,
|
|
17
18
|
ZudokuConfig,
|
|
@@ -240,6 +241,20 @@ export async function loadZudokuConfig(
|
|
|
240
241
|
{ timestamp: true },
|
|
241
242
|
);
|
|
242
243
|
|
|
244
|
+
// Surface which versions of external plugins (created via `createPlugin`)
|
|
245
|
+
// the build is running, so build logs can pin down e.g. the monetization
|
|
246
|
+
// plugin version without each plugin having to log it itself.
|
|
247
|
+
const pluginVersions = await getPluginVersions(config.__pluginDirs ?? []);
|
|
248
|
+
if (pluginVersions.length > 0) {
|
|
249
|
+
logger.info(
|
|
250
|
+
colors.cyan(`loaded plugins `) +
|
|
251
|
+
colors.dim(
|
|
252
|
+
pluginVersions.map((p) => `${p.name}@${p.version}`).join(", "),
|
|
253
|
+
),
|
|
254
|
+
{ timestamp: true },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
243
258
|
return { config, envPrefix, publicEnv };
|
|
244
259
|
} catch (error) {
|
|
245
260
|
const lastValid = getConfig();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { findPackageRoot } from "../vite/package-root.js";
|
|
4
|
+
|
|
5
|
+
export type PluginVersion = { name: string; version: string };
|
|
6
|
+
|
|
7
|
+
// Reads the `name`@`version` from each plugin dir's nearest package.json,
|
|
8
|
+
// deduped by package name.
|
|
9
|
+
export const getPluginVersions = async (
|
|
10
|
+
pluginDirs: readonly string[],
|
|
11
|
+
): Promise<PluginVersion[]> => {
|
|
12
|
+
const roots = await Promise.all(
|
|
13
|
+
[...new Set(pluginDirs)].map(findPackageRoot),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const resolved = await Promise.all(
|
|
17
|
+
roots.map(async (root): Promise<PluginVersion | undefined> => {
|
|
18
|
+
if (!root) return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(
|
|
21
|
+
await readFile(path.join(root, "package.json"), "utf-8"),
|
|
22
|
+
) as { name?: string; version?: string };
|
|
23
|
+
if (!pkg.name) return undefined;
|
|
24
|
+
return { name: pkg.name, version: pkg.version ?? "unknown" };
|
|
25
|
+
} catch {
|
|
26
|
+
// A plugin without a readable package.json simply isn't reported.
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const byName = new Map<string, PluginVersion>();
|
|
33
|
+
for (const version of resolved) {
|
|
34
|
+
if (version && !byName.has(version.name)) byName.set(version.name, version);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [...byName.values()];
|
|
38
|
+
};
|