zudoku 0.74.3 → 0.75.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 (53) hide show
  1. package/dist/cli/cli.js +121 -23
  2. package/dist/declarations/lib/authentication/providers/openid.d.ts +1 -0
  3. package/dist/declarations/lib/authentication/state.d.ts +6 -1
  4. package/dist/declarations/lib/plugins/api-catalog/Catalog.d.ts +2 -4
  5. package/dist/declarations/lib/plugins/api-catalog/index.d.ts +3 -8
  6. package/dist/declarations/lib/plugins/openapi/MCPEndpoint.d.ts +2 -1
  7. package/dist/declarations/lib/plugins/openapi/mcp-configs.d.ts +28 -0
  8. package/dist/declarations/lib/plugins/openapi/schema/SchemaRefLink.d.ts +5 -0
  9. package/dist/declarations/lib/plugins/openapi/schema/SchemaView.d.ts +2 -1
  10. package/dist/declarations/lib/plugins/openapi/schema/utils.d.ts +1 -0
  11. package/dist/declarations/lib/ui/HoverCard.d.ts +5 -5
  12. package/dist/declarations/lib/ui/Toggle.d.ts +2 -6
  13. package/dist/declarations/lib/ui/ToggleGroup.d.ts +7 -9
  14. package/docs/configuration/authentication-auth0.md +6 -3
  15. package/docs/configuration/authentication-openid.md +110 -0
  16. package/docs/configuration/authentication.md +5 -2
  17. package/docs/deploy/github-pages.md +12 -0
  18. package/docs/guides/redirects.md +186 -0
  19. package/docs/openapi-extensions/x-code-samples.md +57 -0
  20. package/docs/openapi-extensions/x-display-name.md +31 -0
  21. package/docs/openapi-extensions/x-mcp-server.md +104 -0
  22. package/docs/openapi-extensions/x-mcp.md +171 -0
  23. package/docs/openapi-extensions/x-tag-groups.md +65 -0
  24. package/docs/openapi-extensions/x-zudoku-collapsed.md +35 -0
  25. package/docs/openapi-extensions/x-zudoku-collapsible.md +35 -0
  26. package/docs/openapi-extensions/x-zudoku-playground-enabled.md +43 -0
  27. package/package.json +9 -8
  28. package/src/lib/authentication/providers/openid.tsx +18 -15
  29. package/src/lib/authentication/state.ts +14 -1
  30. package/src/lib/components/Markdown.tsx +34 -7
  31. package/src/lib/components/Meta.tsx +10 -1
  32. package/src/lib/hooks/useHotkey.ts +10 -0
  33. package/src/lib/oas/graphql/circular.ts +11 -0
  34. package/src/lib/plugins/api-catalog/Catalog.tsx +255 -52
  35. package/src/lib/plugins/api-catalog/index.tsx +7 -64
  36. package/src/lib/plugins/openapi/Endpoint.tsx +17 -26
  37. package/src/lib/plugins/openapi/MCPEndpoint.tsx +292 -209
  38. package/src/lib/plugins/openapi/ParamInfos.tsx +20 -7
  39. package/src/lib/plugins/openapi/SchemaList.tsx +1 -1
  40. package/src/lib/plugins/openapi/SecurityRequirements.tsx +86 -68
  41. package/src/lib/plugins/openapi/mcp-configs.ts +265 -0
  42. package/src/lib/plugins/openapi/playground/Playground.tsx +7 -7
  43. package/src/lib/plugins/openapi/schema/SchemaRefLink.tsx +21 -0
  44. package/src/lib/plugins/openapi/schema/SchemaView.tsx +22 -4
  45. package/src/lib/plugins/openapi/schema/utils.ts +21 -0
  46. package/src/lib/ui/Badge.tsx +1 -1
  47. package/src/lib/ui/HoverCard.tsx +35 -21
  48. package/src/lib/ui/Toggle.tsx +21 -21
  49. package/src/lib/ui/ToggleGroup.tsx +59 -32
  50. package/src/vite/build.ts +3 -1
  51. package/src/vite/plugin-api.ts +32 -0
  52. package/src/vite/prerender/prerender.ts +136 -16
  53. package/src/zuplo/enrich-with-zuplo-mcp.ts +104 -7
package/dist/cli/cli.js CHANGED
@@ -3813,7 +3813,7 @@ import {
3813
3813
  // package.json
3814
3814
  var package_default = {
3815
3815
  name: "zudoku",
3816
- version: "0.74.2",
3816
+ version: "0.75.0",
3817
3817
  type: "module",
3818
3818
  sideEffects: [
3819
3819
  "**/*.css",
@@ -3891,6 +3891,7 @@ var package_default = {
3891
3891
  },
3892
3892
  dependencies: {
3893
3893
  "@apidevtools/json-schema-ref-parser": "15.3.1",
3894
+ "@base-ui/react": "^1.4.0",
3894
3895
  "@envelop/core": "5.5.1",
3895
3896
  "@graphql-typed-document-node/core": "3.2.0",
3896
3897
  "@hono/node-server": "1.19.13",
@@ -3951,14 +3952,14 @@ var package_default = {
3951
3952
  "fast-equals": "6.0.0",
3952
3953
  glob: "13.0.6",
3953
3954
  "glob-parent": "6.0.2",
3954
- graphql: "16.13.1",
3955
+ graphql: "16.13.2",
3955
3956
  "graphql-type-json": "0.3.2",
3956
3957
  "graphql-yoga": "5.18.0",
3957
3958
  "gray-matter": "4.0.3",
3958
3959
  "hast-util-heading-rank": "3.0.0",
3959
3960
  "hast-util-to-jsx-runtime": "2.3.6",
3960
3961
  "hast-util-to-string": "3.0.1",
3961
- hono: "4.12.12",
3962
+ hono: "4.12.14",
3962
3963
  "http-terminator": "3.2.0",
3963
3964
  "javascript-stringify": "2.1.0",
3964
3965
  "json-schema-to-typescript-lite": "15.0.0",
@@ -3973,7 +3974,7 @@ var package_default = {
3973
3974
  "next-themes": "0.4.6",
3974
3975
  oauth4webapi: "3.8.5",
3975
3976
  "openapi-types": "12.1.3",
3976
- pagefind: "1.5.0-beta.1",
3977
+ pagefind: "1.5.2",
3977
3978
  picocolors: "1.1.1",
3978
3979
  piscina: "5.1.4",
3979
3980
  "posthog-node": "5.26.0",
@@ -4010,9 +4011,9 @@ var package_default = {
4010
4011
  zustand: "5.0.12"
4011
4012
  },
4012
4013
  devDependencies: {
4013
- "@clerk/clerk-js": "^6.0.0",
4014
- "@graphql-codegen/cli": "6.1.2",
4015
- "@inkeep/cxkit-types": "0.5.116",
4014
+ "@clerk/clerk-js": "^6.7.4",
4015
+ "@graphql-codegen/cli": "6.3.0",
4016
+ "@inkeep/cxkit-types": "0.5.117",
4016
4017
  "@testing-library/dom": "catalog:",
4017
4018
  "@testing-library/jest-dom": "catalog:",
4018
4019
  "@testing-library/react": "catalog:",
@@ -4030,7 +4031,7 @@ var package_default = {
4030
4031
  "@types/yargs": "17.0.35",
4031
4032
  "@vitest/coverage-v8": "4.0.18",
4032
4033
  "happy-dom": "catalog:",
4033
- "oxc-parser": "^0.119.0",
4034
+ "oxc-parser": "^0.126.0",
4034
4035
  react: "catalog:",
4035
4036
  "react-dom": "catalog:",
4036
4037
  tsx: "4.21.0",
@@ -4730,6 +4731,9 @@ var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs
4730
4731
  const result = Array.isArray(obj) ? obj.map((item, i) => recurse(item, i.toString())) : Object.fromEntries(
4731
4732
  Object.entries(obj).map(([k, v]) => [k, recurse(v, k)])
4732
4733
  );
4734
+ if (!Array.isArray(result) && typeof refPath === "string" && !("__$ref" in result)) {
4735
+ result.__$ref = refPath;
4736
+ }
4733
4737
  refs.set(obj, result);
4734
4738
  currentPath.delete(obj);
4735
4739
  if (typeof refPath === "string") currentRefPaths.delete(refPath);
@@ -6224,15 +6228,37 @@ var viteApiPlugin = async () => {
6224
6228
  );
6225
6229
  const apis = ensureArray(config2.apis);
6226
6230
  const apiMetadata = [];
6231
+ const httpMethods = /* @__PURE__ */ new Set([
6232
+ "get",
6233
+ "post",
6234
+ "put",
6235
+ "patch",
6236
+ "delete",
6237
+ "options",
6238
+ "head",
6239
+ "trace"
6240
+ ]);
6227
6241
  for (const apiConfig of apis) {
6228
6242
  if (apiConfig.type === "file" && apiConfig.path) {
6229
6243
  const latestSchema = schemaManager.getLatestSchema(apiConfig.path);
6230
6244
  if (!latestSchema?.schema.info) continue;
6245
+ const operationCount = Object.values(
6246
+ latestSchema.schema.paths ?? {}
6247
+ ).reduce((sum, pathItem) => {
6248
+ if (!pathItem || typeof pathItem !== "object") return sum;
6249
+ return sum + Object.keys(pathItem).filter(
6250
+ (m) => httpMethods.has(m.toLowerCase())
6251
+ ).length;
6252
+ }, 0);
6253
+ const rawVersion = latestSchema.schema.info.version;
6254
+ const version = rawVersion ? rawVersion.startsWith("v") || rawVersion.startsWith("V") ? rawVersion : `v${rawVersion}` : void 0;
6231
6255
  apiMetadata.push({
6232
6256
  path: apiConfig.path,
6233
6257
  label: latestSchema.schema.info.title,
6234
6258
  description: latestSchema.schema.info.description ?? "",
6235
- categories: apiConfig.categories ?? []
6259
+ categories: apiConfig.categories ?? [],
6260
+ version,
6261
+ operationCount
6236
6262
  });
6237
6263
  }
6238
6264
  }
@@ -7775,6 +7801,7 @@ async function writeOutput(dir, {
7775
7801
  // src/vite/prerender/prerender.ts
7776
7802
  init_logger();
7777
7803
  init_file_exists();
7804
+ import { readFileSync as readFileSync2 } from "node:fs";
7778
7805
  import { readFile as readFile2, rm } from "node:fs/promises";
7779
7806
  import os from "node:os";
7780
7807
  import path21 from "node:path";
@@ -7945,7 +7972,9 @@ var prerender = async ({
7945
7972
  paths.push(joinUrl(r.from));
7946
7973
  }
7947
7974
  }
7948
- const maxThreads = buildConfig?.prerender?.workers ?? Math.floor(os.cpus().length * 0.8);
7975
+ const { maxThreads, maxOldGenerationSizeMb } = getWorkerScaling(
7976
+ buildConfig?.prerender?.workers
7977
+ );
7949
7978
  const start = performance.now();
7950
7979
  const LOG_INTERVAL_MS = 3e4;
7951
7980
  let lastLogTime = start;
@@ -7974,7 +8003,11 @@ var prerender = async ({
7974
8003
  const pool = new Piscina({
7975
8004
  filename: new URL("./worker.js", import.meta.url).href,
7976
8005
  idleTimeout: 5e3,
8006
+ minThreads: 1,
7977
8007
  maxThreads,
8008
+ resourceLimits: {
8009
+ maxOldGenerationSizeMb
8010
+ },
7978
8011
  workerData: {
7979
8012
  template: html,
7980
8013
  distDir,
@@ -7986,12 +8019,6 @@ var prerender = async ({
7986
8019
  const workerResults = await Promise.all(
7987
8020
  paths.map(async (urlPath) => {
7988
8021
  const result = await pool.run({ urlPath });
7989
- if (result.statusCode < 400) {
7990
- await pagefindIndex?.addHTMLFile({
7991
- url: urlPath,
7992
- content: result.html
7993
- });
7994
- }
7995
8022
  completedCount++;
7996
8023
  if (isTTY()) {
7997
8024
  writeProgress(completedCount, paths.length, urlPath);
@@ -8009,9 +8036,6 @@ var prerender = async ({
8009
8036
  return result;
8010
8037
  })
8011
8038
  );
8012
- const pagefindWriteResult = await pagefindIndex?.writeFiles({
8013
- outputPath: path21.join(distDir, "pagefind")
8014
- });
8015
8039
  const seconds = ((performance.now() - start) / 1e3).toFixed(1);
8016
8040
  const message = `\u2713 finished prerendering ${paths.length} routes in ${seconds} seconds using ${maxThreads} workers`;
8017
8041
  if (isTTY()) {
@@ -8020,10 +8044,38 @@ var prerender = async ({
8020
8044
  } else {
8021
8045
  logger.info(colors7.blue(message));
8022
8046
  }
8023
- if (pagefindWriteResult?.outputPath) {
8024
- logger.info(
8025
- colors7.blue(`\u2713 pagefind index built: ${pagefindWriteResult.outputPath}`)
8047
+ if (pagefindIndex) {
8048
+ const pagesToIndex = workerResults.flatMap(
8049
+ ({ statusCode, html: html2 }, i) => statusCode < 400 ? { url: paths[i], html: html2 } : []
8026
8050
  );
8051
+ const BATCH_SIZE = 40;
8052
+ const pagefindStart = performance.now();
8053
+ for (let offset = 0; offset < pagesToIndex.length; offset += BATCH_SIZE) {
8054
+ const batch = pagesToIndex.slice(offset, offset + BATCH_SIZE);
8055
+ await Promise.all(
8056
+ batch.map(
8057
+ ({ url, html: html2 }) => pagefindIndex.addHTMLFile({ url, content: html2 })
8058
+ )
8059
+ );
8060
+ if (isTTY()) {
8061
+ const done = offset + batch.length;
8062
+ writeLine(
8063
+ `pagefind indexing (${done}/${pagesToIndex.length}) ${colors7.dim(batch.at(-1)?.url ?? "")}`
8064
+ );
8065
+ }
8066
+ }
8067
+ if (isTTY()) writeLine("");
8068
+ const { outputPath } = await pagefindIndex.writeFiles({
8069
+ outputPath: path21.join(distDir, "pagefind")
8070
+ });
8071
+ if (outputPath) {
8072
+ const duration = (performance.now() - pagefindStart) / 1e3;
8073
+ logger.info(
8074
+ colors7.blue(
8075
+ `\u2713 pagefind index built in ${duration.toFixed(1)} seconds: ${outputPath}`
8076
+ )
8077
+ );
8078
+ }
8027
8079
  }
8028
8080
  const redirectUrls = getRedirectUrls(workerResults, config2.basePath);
8029
8081
  await generateSitemap({
@@ -8074,6 +8126,52 @@ var prerender = async ({
8074
8126
  }
8075
8127
  return { workerResults, rewrites };
8076
8128
  };
8129
+ var getWorkerScaling = (workersOverride) => {
8130
+ const PER_WORKER_HEAP_LIMIT_MB = 4096;
8131
+ const MAX_WORKERS = 8;
8132
+ const osTotalMb = Math.floor(os.totalmem() / (1024 * 1024));
8133
+ const cgroupMemMb = getContainerMemoryLimitMb();
8134
+ const totalMemMb = cgroupMemMb !== void 0 ? Math.min(cgroupMemMb, osTotalMb) : osTotalMb;
8135
+ const reservedMb = Math.max(2048, Math.floor(totalMemMb * 0.25));
8136
+ const availableForWorkersMb = totalMemMb - reservedMb;
8137
+ const memBasedWorkers = Math.max(
8138
+ 1,
8139
+ Math.floor(availableForWorkersMb / PER_WORKER_HEAP_LIMIT_MB)
8140
+ );
8141
+ const cpuBasedWorkers = Math.max(1, Math.floor(os.cpus().length * 0.5));
8142
+ const defaultWorkers = Math.min(
8143
+ memBasedWorkers,
8144
+ cpuBasedWorkers,
8145
+ MAX_WORKERS
8146
+ );
8147
+ const validOverride = workersOverride && workersOverride > 0 ? workersOverride : void 0;
8148
+ const maxThreads = validOverride ?? defaultWorkers;
8149
+ const maxOldGenerationSizeMb = Math.min(
8150
+ PER_WORKER_HEAP_LIMIT_MB,
8151
+ Math.max(512, Math.floor(availableForWorkersMb / maxThreads))
8152
+ );
8153
+ return { maxThreads, maxOldGenerationSizeMb };
8154
+ };
8155
+ var getContainerMemoryLimitMb = () => {
8156
+ const CGROUP_PATHS = [
8157
+ "/sys/fs/cgroup/memory.max",
8158
+ // cgroup v2
8159
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes"
8160
+ // cgroup v1
8161
+ ];
8162
+ for (const filePath of CGROUP_PATHS) {
8163
+ try {
8164
+ const raw = readFileSync2(filePath, "utf8").trim();
8165
+ if (raw === "max") return void 0;
8166
+ const bytes = Number(raw);
8167
+ if (!Number.isFinite(bytes) || bytes > 2 ** 50) return void 0;
8168
+ return Math.floor(bytes / (1024 * 1024));
8169
+ } catch (err) {
8170
+ if (err.code !== "ENOENT") throw err;
8171
+ }
8172
+ }
8173
+ return void 0;
8174
+ };
8077
8175
 
8078
8176
  // src/vite/build.ts
8079
8177
  var DIST_DIR = "dist";
@@ -8160,7 +8258,7 @@ var runPrerender = async (options) => {
8160
8258
  await writeFile5(indexHtml, html, "utf-8");
8161
8259
  }
8162
8260
  const statusPages = workerResults.flatMap(
8163
- (r) => /400|404|500\.html$/.test(r.outputPath) ? r.outputPath : []
8261
+ (r) => /^(400|404|500)\.html$/.test(path22.basename(r.outputPath)) ? r.outputPath : []
8164
8262
  );
8165
8263
  for (const statusPage of statusPages) {
8166
8264
  await rename(
@@ -43,6 +43,7 @@ export declare class OpenIDAuthenticationProvider extends CoreAuthenticationPlug
43
43
  replace?: boolean;
44
44
  }): Promise<void>;
45
45
  signIn(_: AuthActionContext, { redirectTo, replace }: AuthActionOptions): Promise<void>;
46
+ private buildUserProfile;
46
47
  refreshUserProfile(): Promise<boolean>;
47
48
  private authorize;
48
49
  getAccessToken(): Promise<string>;
@@ -39,11 +39,16 @@ export declare const useAuthState: import("zustand").UseBoundStore<Omit<import("
39
39
  getOptions: () => Partial<import("zustand/middleware").PersistOptions<AuthState, unknown, unknown>>;
40
40
  };
41
41
  }>;
42
+ export type CustomClaim = string | number | boolean | null | CustomClaimRecord | CustomClaimArray | undefined;
43
+ export interface CustomClaimRecord {
44
+ [key: string]: CustomClaim;
45
+ }
46
+ export type CustomClaimArray = CustomClaim[];
42
47
  export interface UserProfile {
43
48
  sub: string;
44
49
  email: string | undefined;
45
50
  emailVerified: boolean;
46
51
  name: string | undefined;
47
52
  pictureUrl: string | undefined;
48
- [key: string]: string | boolean | undefined;
53
+ [key: string]: CustomClaim;
49
54
  }
@@ -1,4 +1,2 @@
1
- import { type ApiCatalogPluginOptions } from "./index.js";
2
- export declare const Catalog: ({ items, filterCatalogItems, label, categoryLabel, }: Omit<ApiCatalogPluginOptions, "path"> & {
3
- categoryLabel?: string;
4
- }) => import("react/jsx-runtime").JSX.Element;
1
+ import type { ApiCatalogPluginOptions } from "./index.js";
2
+ export declare const Catalog: ({ items, filterCatalogItems, label, }: Omit<ApiCatalogPluginOptions, "path">) => import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,12 @@
1
1
  import type { AuthState } from "../../authentication/state.js";
2
2
  import type { ZudokuPlugin } from "../../core/plugins.js";
3
- export declare const getKey: (category: string, tag: string) => string;
4
3
  export type ApiCatalogItem = {
5
4
  path: string;
6
5
  label: string;
7
6
  description: string;
8
7
  categories: CatalogCategory[];
8
+ version?: string;
9
+ operationCount?: number;
9
10
  };
10
11
  export type CatalogCategory = {
11
12
  label: string;
@@ -22,10 +23,4 @@ export type CatalogContext = {
22
23
  auth: AuthState;
23
24
  };
24
25
  export type FilterCatalogItemsFn = (items: ApiCatalogItem[], { auth }: CatalogContext) => ApiCatalogItem[];
25
- export declare const apiCatalogPlugin: ({ path, items, label, categories, filterCatalogItems, }: {
26
- path: string;
27
- label: string;
28
- categories?: CatalogCategory[];
29
- items: ApiCatalogItem[];
30
- filterCatalogItems?: FilterCatalogItemsFn;
31
- }) => ZudokuPlugin;
26
+ export declare const apiCatalogPlugin: ({ path, items, label, categories, filterCatalogItems, }: ApiCatalogPluginOptions) => ZudokuPlugin;
@@ -1,6 +1,7 @@
1
+ import { type McpServerData } from "./mcp-configs.js";
1
2
  export declare const MCPEndpoint: ({ serverUrl, operationPath, summary, data, }: {
2
3
  serverUrl?: string;
3
4
  operationPath?: string;
4
- data?: boolean | Record<string, unknown>;
5
+ data?: McpServerData;
5
6
  summary?: string;
6
7
  }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ export type McpServerData = boolean | Record<string, unknown>;
2
+ export interface AuthHeader {
3
+ headerName: string;
4
+ placeholder: string;
5
+ }
6
+ export type AuthType = "none" | "apiKey" | "oauth";
7
+ export declare const getAuthType: (data?: McpServerData) => AuthType;
8
+ export declare const getAuthHeader: (data?: McpServerData) => AuthHeader | undefined;
9
+ export interface McpSubApp {
10
+ id: string;
11
+ label: string;
12
+ supportedAuth: AuthType[];
13
+ }
14
+ export interface McpApp {
15
+ id: string;
16
+ label: string;
17
+ subApps: McpSubApp[];
18
+ }
19
+ export declare const MCP_APPS: McpApp[];
20
+ export declare const getVisibleApps: (authType: AuthType) => McpApp[];
21
+ export declare const getMcpServerName: (data?: McpServerData, summary?: string) => string;
22
+ export declare const getMcpUrl: (serverUrl?: string, operationPath?: string) => string;
23
+ export declare const getClaudeCodeCommand: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
24
+ export declare const getCodexCliCommand: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
25
+ export declare const getCursorConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
26
+ export declare const getVscodeConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
27
+ export declare const getCodexConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
28
+ export declare const getGenericConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
@@ -0,0 +1,5 @@
1
+ export declare const SchemaRefLink: ({ name, suffix, className, }: {
2
+ name: string;
3
+ suffix?: string;
4
+ className?: string;
5
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import type { SchemaObject } from "../../../oas/parser/index.js";
2
- export declare const SchemaView: ({ schema, defaultOpen, cardHeader, embedded, }: {
2
+ export declare const SchemaView: ({ schema, defaultOpen, cardHeader, embedded, hideRootRef, }: {
3
3
  schema?: SchemaObject | null;
4
4
  defaultOpen?: boolean;
5
5
  cardHeader?: React.ReactNode;
6
6
  embedded?: boolean;
7
+ hideRootRef?: boolean;
7
8
  }) => import("react/jsx-runtime").JSX.Element | undefined;
@@ -6,4 +6,5 @@ export declare const isCircularRef: (schema: unknown) => schema is string;
6
6
  export declare const isArrayCircularRef: (schema: SchemaObject) => schema is SchemaObject & {
7
7
  items: SchemaObject;
8
8
  };
9
+ export declare const getSchemaRefName: (schema?: SchemaObject | null) => string | undefined;
9
10
  export declare const extractCircularRefInfo: (ref?: string | SchemaObject) => string | undefined;
@@ -1,6 +1,6 @@
1
1
  import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
2
- import * as React from "react";
3
- declare const HoverCard: React.FC<HoverCardPrimitive.HoverCardProps>;
4
- declare const HoverCardTrigger: React.ForwardRefExoticComponent<HoverCardPrimitive.HoverCardTriggerProps & React.RefAttributes<HTMLAnchorElement>>;
5
- declare const HoverCardContent: React.ForwardRefExoticComponent<Omit<HoverCardPrimitive.HoverCardContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
6
- export { HoverCard, HoverCardContent, HoverCardTrigger };
2
+ import type * as React from "react";
3
+ declare function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
4
+ declare function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
5
+ declare function HoverCardContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
6
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
@@ -1,12 +1,8 @@
1
- import * as TogglePrimitive from "@radix-ui/react-toggle";
1
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
2
2
  import { type VariantProps } from "class-variance-authority";
3
- import * as React from "react";
4
3
  declare const toggleVariants: (props?: ({
5
4
  variant?: "default" | "outline" | null | undefined;
6
5
  size?: "default" | "sm" | "lg" | null | undefined;
7
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
- declare const Toggle: React.ForwardRefExoticComponent<Omit<TogglePrimitive.ToggleProps & React.RefAttributes<HTMLButtonElement>, "ref"> & VariantProps<(props?: ({
9
- variant?: "default" | "outline" | null | undefined;
10
- size?: "default" | "sm" | "lg" | null | undefined;
11
- } & import("class-variance-authority/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLButtonElement>>;
7
+ declare function Toggle({ className, variant, size, ...props }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>): import("react/jsx-runtime").JSX.Element;
12
8
  export { Toggle, toggleVariants };
@@ -1,12 +1,10 @@
1
- import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
1
+ import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
2
2
  import type { VariantProps } from "class-variance-authority";
3
3
  import * as React from "react";
4
- declare const ToggleGroup: React.ForwardRefExoticComponent<((Omit<ToggleGroupPrimitive.ToggleGroupSingleProps & React.RefAttributes<HTMLDivElement>, "ref"> | Omit<ToggleGroupPrimitive.ToggleGroupMultipleProps & React.RefAttributes<HTMLDivElement>, "ref">) & VariantProps<(props?: ({
5
- variant?: "default" | "outline" | null | undefined;
6
- size?: "default" | "sm" | "lg" | null | undefined;
7
- } & import("class-variance-authority/types").ClassProp) | undefined) => string>) & React.RefAttributes<HTMLDivElement>>;
8
- declare const ToggleGroupItem: React.ForwardRefExoticComponent<Omit<ToggleGroupPrimitive.ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>, "ref"> & VariantProps<(props?: ({
9
- variant?: "default" | "outline" | null | undefined;
10
- size?: "default" | "sm" | "lg" | null | undefined;
11
- } & import("class-variance-authority/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLButtonElement>>;
4
+ import { Toggle, toggleVariants } from "./Toggle.js";
5
+ declare function ToggleGroup({ className, variant, size, spacing, orientation, children, ...props }: ToggleGroupPrimitive.Props & VariantProps<typeof toggleVariants> & {
6
+ spacing?: number;
7
+ orientation?: "horizontal" | "vertical";
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ declare function ToggleGroupItem({ className, children, variant, size, ...props }: React.ComponentProps<typeof Toggle>): import("react/jsx-runtime").JSX.Element;
12
10
  export { ToggleGroup, ToggleGroupItem };
@@ -34,7 +34,10 @@ If you don't have an Auth0 account, you can sign up for a
34
34
  - Production: `https://your-site.com/oauth/callback`
35
35
  - Preview (wildcard): `https://*.your-domain.com/oauth/callback`
36
36
  - Local Development: `http://localhost:3000/oauth/callback`
37
- - **Allowed Logout URLs**: Same as callback URLs above
37
+ - **Allowed Logout URLs**:
38
+ - Production: `https://your-site.com/oauth/logout-callback`
39
+ - Preview (wildcard): `https://*.your-domain.com/oauth/logout-callback`
40
+ - Local Development: `http://localhost:3000/oauth/logout-callback`
38
41
 
39
42
  - **Allowed Web Origins**:
40
43
  - Production: `https://your-site.com`
@@ -112,8 +115,8 @@ To enable logout for your Auth0 application:
112
115
 
113
116
  1. Ensure your **Allowed Logout URLs** are configured in Auth0 (see
114
117
  [Configure Auth0 Application](#setup-steps) above)
115
- 2. The logout URL should match your callback URL pattern (e.g., `https://your-site.com/` for
116
- production)
118
+ 2. The logout URL must use the `/oauth/logout-callback` path (e.g.,
119
+ `https://your-site.com/oauth/logout-callback` for production)
117
120
 
118
121
  For older tenants, you may need to enable **RP-Initiated Logout** in your tenant settings. See the
119
122
  [Auth0 logout documentation](https://auth0.com/docs/authenticate/login/logout/log-users-out-of-auth0)
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: OpenID Connect (OIDC)
3
+ sidebar_label: OpenID Connect
4
+ description:
5
+ Configure any OpenID Connect compliant identity provider (Okta, Keycloak, Authentik, etc.) as the
6
+ authentication provider for Zudoku.
7
+ ---
8
+
9
+ Zudoku supports any identity provider that implements the
10
+ [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) protocol via the generic
11
+ `openid` provider type. This includes Okta, Keycloak, Authentik, Ory, ZITADEL, AWS Cognito, Google
12
+ Identity, and most enterprise IdPs.
13
+
14
+ ## Configuration
15
+
16
+ Add the `authentication` property to your [Zudoku configuration](./overview.md):
17
+
18
+ ```typescript title="zudoku.config.ts"
19
+ {
20
+ // ...
21
+ authentication: {
22
+ type: "openid",
23
+ clientId: "<your-client-id>",
24
+ issuer: "<the-issuer-url>",
25
+ scopes: ["openid", "profile", "email"], // Optional
26
+ },
27
+ // ...
28
+ }
29
+ ```
30
+
31
+ | Option | Required | Description |
32
+ | ---------- | -------- | -------------------------------------------------------------------------------------------- |
33
+ | `clientId` | Yes | The OAuth client ID issued by your provider. |
34
+ | `issuer` | Yes | The issuer URL. Zudoku discovers endpoints from `<issuer>/.well-known/openid-configuration`. |
35
+ | `scopes` | No | Scopes to request. Defaults to `["openid", "profile", "email"]`. |
36
+
37
+ ## Provider Setup
38
+
39
+ Register Zudoku as a public SPA / single page application client in your identity provider and set:
40
+
41
+ - Callback / Redirect URI to `https://your-site.com/oauth/callback`
42
+ - For local development, add `http://localhost:3000/oauth/callback`
43
+ - If your provider supports wildcards, add `https://*.your-domain.com/oauth/callback` for preview
44
+ environments
45
+ - Add your site origin to the list of allowed CORS origins
46
+ - Enable the `Authorization Code` grant with PKCE and the `Refresh Token` grant
47
+
48
+ ### Okta
49
+
50
+ 1. In the Okta admin console go to **Applications** → **Applications** → **Create App Integration**.
51
+ 2. Select **OIDC - OpenID Connect** and **Single Page Application**.
52
+ 3. Set **Sign-in redirect URIs** to `https://your-site.com/oauth/callback` (add
53
+ `http://localhost:3000/oauth/callback` for local development).
54
+ 4. Under **Assignments**, assign the users or groups that should have access.
55
+ 5. After creating the app, copy the **Client ID**. Your issuer is your Okta domain, for example
56
+ `https://your-tenant.okta.com` or a custom authorization server like
57
+ `https://your-tenant.okta.com/oauth2/default`.
58
+ 6. Under **Security** → **API** → **Trusted Origins**, add your site origin for both CORS and
59
+ Redirect.
60
+
61
+ ```typescript title="zudoku.config.ts"
62
+ {
63
+ authentication: {
64
+ type: "openid",
65
+ clientId: "<your-okta-client-id>",
66
+ issuer: "https://your-tenant.okta.com/oauth2/default",
67
+ scopes: ["openid", "profile", "email"],
68
+ },
69
+ }
70
+ ```
71
+
72
+ ### Keycloak
73
+
74
+ Use the realm issuer URL:
75
+
76
+ ```typescript title="zudoku.config.ts"
77
+ {
78
+ authentication: {
79
+ type: "openid",
80
+ clientId: "zudoku",
81
+ issuer: "https://keycloak.example.com/realms/<your-realm>",
82
+ },
83
+ }
84
+ ```
85
+
86
+ In the realm, create a client with **Client type** `OpenID Connect`, **Access type** `public`, and
87
+ enable **Standard Flow** (Authorization Code).
88
+
89
+ ## Verifying the Issuer
90
+
91
+ You can confirm your issuer URL is correct by opening `<issuer>/.well-known/openid-configuration` in
92
+ a browser. It should return a JSON document listing `authorization_endpoint`, `token_endpoint`,
93
+ `userinfo_endpoint`, and `jwks_uri`.
94
+
95
+ ## User Profile
96
+
97
+ After sign-in Zudoku calls the provider's
98
+ [UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) and reads
99
+ `name`, `email`, `picture`, and `email_verified` from the response. Map these claims in your
100
+ provider if they are not emitted by default.
101
+
102
+ ## Troubleshooting
103
+
104
+ - **Discovery fails**: verify `<issuer>/.well-known/openid-configuration` resolves and matches the
105
+ `issuer` value in the document.
106
+ - **CORS errors on token / userinfo**: add your site origin to the provider's allowed origins.
107
+ - **Redirect URI mismatch**: the URI registered with the provider must match the Zudoku origin
108
+ exactly, including protocol and port.
109
+ - **Missing profile fields**: ensure `profile` and `email` scopes are granted and that the provider
110
+ includes `name`, `email`, and `picture` claims in the UserInfo response.
@@ -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 provider that supports
19
- the OpenID Connect protocol (including PingFederate).
18
+ Zudoku supports Clerk, Auth0, Supabase, Firebase, Azure B2C, and any OpenID Connect provider
19
+ (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
 
@@ -96,6 +96,9 @@ When configuring your OpenID provider, you will need to set the following:
96
96
  By default, the scopes "openid", "profile", and "email" are requested. You can customize these by
97
97
  providing your own array of scopes.
98
98
 
99
+ For provider-specific guides (Okta, Keycloak, etc.), see the
100
+ [OpenID Connect setup page](./authentication-openid.md).
101
+
99
102
  ### Firebase
100
103
 
101
104
  For Firebase authentication, you will need your Firebase project configuration. You can find this in
@@ -30,6 +30,18 @@ const config: ZudokuConfig = {
30
30
  };
31
31
  ```
32
32
 
33
+ When `basePath` is set, Zudoku writes the site into `dist/<basePath>/`. Upload that nested directory
34
+ as your Pages artifact, not `dist/` itself. For example in a GitHub Actions workflow:
35
+
36
+ ```yaml
37
+ - uses: actions/upload-pages-artifact@v3
38
+ with:
39
+ path: dist/your-repo
40
+ ```
41
+
42
+ Uploading the outer `dist/` would nest the site under `/your-repo/your-repo/` and every asset
43
+ would 404.
44
+
33
45
  ## Accurate Last Modified Dates
34
46
 
35
47
  If you have enabled the [`showLastModified`](/docs/configuration/docs#showlastmodified) option,