@lukoweb/apitogo 0.2.54 → 0.2.57

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 (82) hide show
  1. package/dist/cli/cli.js +2196 -1352
  2. package/dist/declarations/cli/platform/client/platform-api.d.ts +29 -0
  3. package/dist/declarations/cli/platform/helpers/platform-plan-catalog.d.ts +6 -0
  4. package/dist/declarations/config/validators/ZudokuConfig.d.ts +2 -0
  5. package/dist/declarations/lib/authentication/providers/openid.d.ts +2 -1
  6. package/dist/declarations/lib/plugins/markdown/index.d.ts +2 -1
  7. package/dist/flat-config.d.ts +1 -0
  8. package/docs/cli/commands.md +50 -32
  9. package/docs/cli/go-live.md +66 -93
  10. package/docs/cli/overview.md +18 -17
  11. package/docs/cli/setup.md +130 -141
  12. package/docs/cli/shape.md +94 -11
  13. package/docs/cli/walkthrough.md +1 -1
  14. package/docs/components/slot.mdx +116 -119
  15. package/docs/configuration/api-reference.md +98 -397
  16. package/docs/configuration/authentication.md +118 -159
  17. package/docs/configuration/footer.mdx +89 -118
  18. package/docs/configuration/make-config.mdx +2 -2
  19. package/docs/configuration/overview.md +97 -166
  20. package/docs/configuration/plans.md +100 -110
  21. package/docs/configuration/portal.md +116 -0
  22. package/docs/customization/colors-theme.mdx +255 -278
  23. package/docs/customization/fonts.md +105 -110
  24. package/docs/customization/landing-page.md +189 -188
  25. package/docs/deploy/apitogo.md +13 -17
  26. package/docs/deployment.md +15 -9
  27. package/docs/guides/stripe-setup.mdx +123 -0
  28. package/docs/markdown/frontmatter.md +172 -173
  29. package/docs/mcp/go-live.md +100 -100
  30. package/docs/mcp/overview.md +76 -76
  31. package/docs/quickstart.md +88 -72
  32. package/docs/writing.mdx +68 -72
  33. package/package.json +1 -1
  34. package/src/config/v2/docs-navigation.ts +7 -2
  35. package/src/config/v2/index.ts +1 -0
  36. package/src/config/v2/resolve.ts +28 -0
  37. package/src/config/v2/rules.ts +32 -1
  38. package/src/config/v2/schema.ts +9 -0
  39. package/src/config/v2/services.ts +90 -15
  40. package/src/config/v2/starter-landing.ts +41 -0
  41. package/src/config/v2/wrapper.ts +141 -7
  42. package/src/config/validators/ZudokuConfig.ts +794 -792
  43. package/src/lib/authentication/providers/openid.tsx +6 -1
  44. package/src/lib/plugins/markdown/index.tsx +86 -82
  45. package/src/vite/dev-server.ts +9 -1
  46. package/src/vite/dev-ssr-config.ts +32 -0
  47. package/src/vite/plugin-config.ts +4 -2
  48. package/src/vite/plugin-docs.ts +213 -205
  49. package/docs/concepts/auth-provider-api-identities.md +0 -105
  50. package/docs/configuration/ai-assistants.md +0 -65
  51. package/docs/configuration/api-catalog.md +0 -109
  52. package/docs/configuration/authentication-auth0.md +0 -181
  53. package/docs/configuration/authentication-azure-ad.md +0 -245
  54. package/docs/configuration/authentication-clerk.md +0 -117
  55. package/docs/configuration/authentication-firebase.md +0 -69
  56. package/docs/configuration/authentication-pingfederate.md +0 -143
  57. package/docs/configuration/authentication-supabase.md +0 -233
  58. package/docs/configuration/build-configuration.mdx +0 -147
  59. package/docs/configuration/docs.md +0 -282
  60. package/docs/configuration/llms.md +0 -90
  61. package/docs/configuration/manifest.md +0 -129
  62. package/docs/configuration/navigation.mdx +0 -415
  63. package/docs/configuration/protected-routes.md +0 -150
  64. package/docs/configuration/search.md +0 -164
  65. package/docs/configuration/sentry.mdx +0 -44
  66. package/docs/configuration/site.md +0 -124
  67. package/docs/configuration/slots.mdx +0 -125
  68. package/docs/configuration/vite-config.md +0 -18
  69. package/docs/custom-plugins.md +0 -285
  70. package/docs/deploy/apache-nginx.md +0 -41
  71. package/docs/deploy/cloudflare-pages.md +0 -74
  72. package/docs/deploy/direct-upload.md +0 -17
  73. package/docs/deploy/github-pages.md +0 -49
  74. package/docs/deploy/vercel.md +0 -101
  75. package/docs/extending/events.md +0 -124
  76. package/docs/guides/environment-variables.md +0 -94
  77. package/docs/guides/managing-api-keys-and-identities.md +0 -89
  78. package/docs/guides/navigation-rules.mdx +0 -203
  79. package/docs/guides/processors.mdx +0 -234
  80. package/docs/guides/transforming-examples.md +0 -156
  81. package/docs/guides/using-multiple-apis.md +0 -87
  82. package/docs/plugins.md +0 -24
@@ -1,792 +1,794 @@
1
- import type { Options } from "@mdx-js/rollup";
2
- import colors from "picocolors";
3
- import type { ComponentType, ReactNode } from "react";
4
- import { isValidElement } from "react";
5
- import type { BundledLanguage, BundledTheme } from "shiki";
6
- import { z } from "zod";
7
- import type { UseAuthReturn } from "../../lib/authentication/hook.js";
8
- import type { AuthState } from "../../lib/authentication/state.js";
9
- import type { SlotType } from "../../lib/components/context/SlotProvider.js";
10
- import type { ApitogoPlugin } from "../../lib/core/plugins.js";
11
- import type { ZudokuContext } from "../../lib/core/ZudokuContext.js";
12
- import type { FilterCatalogItemsFn } from "../../lib/plugins/api-catalog/index.js";
13
- import type { ApiConsumer } from "../../lib/plugins/api-keys/index.js";
14
- import type {
15
- GenerateCodeSnippetFn,
16
- TransformExamplesFn,
17
- } from "../../lib/plugins/openapi/interfaces.js";
18
- import type { PagefindSearchFragment } from "../../lib/plugins/search-pagefind/types.js";
19
- import type { MdxComponentsType } from "../../lib/util/MdxComponents.js";
20
- import type { ExposedComponentProps } from "../../lib/util/useExposedProps.js";
21
- import { GOOGLE_FONTS } from "../../vite/plugin-theme.js";
22
- import { HeaderNavigationSchema } from "./HeaderNavigationSchema.js";
23
- import {
24
- InputNavigationSchema,
25
- NavigationRulesSchema,
26
- } from "./InputNavigationSchema.js";
27
- import { ModulesSchema } from "./ModulesSchema.js";
28
- import { ProtectedRoutesSchema } from "./ProtectedRoutesSchema.js";
29
-
30
- const ThemeSchema = z
31
- .object({
32
- background: z.string(),
33
- foreground: z.string(),
34
- card: z.string(),
35
- cardForeground: z.string(),
36
- popover: z.string(),
37
- popoverForeground: z.string(),
38
- primary: z.string(),
39
- primaryForeground: z.string(),
40
- secondary: z.string(),
41
- secondaryForeground: z.string(),
42
- muted: z.string(),
43
- mutedForeground: z.string(),
44
- accent: z.string(),
45
- accentForeground: z.string(),
46
- destructive: z.string(),
47
- destructiveForeground: z.string(),
48
- border: z.string(),
49
- input: z.string(),
50
- ring: z.string(),
51
- radius: z.string(),
52
- })
53
- .partial();
54
-
55
- const ApiCatalogCategorySchema = z.object({
56
- label: z.string(),
57
- tags: z.array(z.string()),
58
- });
59
-
60
- const LanguageOption = z.object({
61
- value: z.string().min(1),
62
- label: z.string().min(1),
63
- });
64
-
65
- const AiAssistantCustomSchema = z.object({
66
- label: z.string(),
67
- icon: z.custom<ReactNode>().optional(),
68
- url: z.union([
69
- z.string(),
70
- z.custom<
71
- (context: { pageUrl: string; type: "docs" | "openapi" }) => string
72
- >((val) => typeof val === "function"),
73
- ]),
74
- });
75
-
76
- export type AiAssistantCustom = z.infer<typeof AiAssistantCustomSchema>;
77
-
78
- const AiAssistantPresets = ["claude", "chatgpt"] as const;
79
-
80
- const AiAssistantsSchema = z
81
- .union([
82
- z.literal(false),
83
- z.array(z.union([z.enum(AiAssistantPresets), AiAssistantCustomSchema])),
84
- ])
85
- .optional();
86
-
87
- export type AiAssistantsConfig = z.infer<typeof AiAssistantsSchema>;
88
-
89
- const ApiOptionsSchema = z
90
- .object({
91
- examplesLanguage: z.string(),
92
- supportedLanguages: z.array(LanguageOption),
93
- disablePlayground: z.boolean(),
94
- disableSidecar: z.boolean(),
95
- showVersionSelect: z.enum(["always", "if-available", "hide"]),
96
- expandAllTags: z.boolean(),
97
- showInfoPage: z.boolean(),
98
- schemaDownload: z.object({ enabled: z.boolean() }).partial(),
99
- transformExamples: z.custom<TransformExamplesFn>(
100
- (val) => typeof val === "function",
101
- ),
102
- generateCodeSnippet: z.custom<GenerateCodeSnippetFn>(
103
- (val) => typeof val === "function",
104
- ),
105
- })
106
- .partial();
107
-
108
- const ApiConfigSchema = z
109
- .object({
110
- server: z.string(),
111
- path: z.string(),
112
- categories: z.array(ApiCatalogCategorySchema),
113
- options: ApiOptionsSchema,
114
- })
115
- .partial();
116
-
117
- const VersionConfigSchema = z.object({
118
- path: z.string(),
119
- input: z.string(),
120
- label: z.string().optional(),
121
- });
122
-
123
- const ApiSchema = z.discriminatedUnion("type", [
124
- z.object({
125
- type: z.literal("url"),
126
- input: z.union([z.string(), z.array(VersionConfigSchema)]),
127
- ...ApiConfigSchema.shape,
128
- }),
129
- z.object({
130
- type: z.literal("file"),
131
- input: z.union([
132
- z.string(),
133
- z.array(z.union([z.string(), VersionConfigSchema])),
134
- ]),
135
- ...ApiConfigSchema.shape,
136
- }),
137
- z.object({
138
- type: z.literal("raw"),
139
- input: z.string(),
140
- ...ApiConfigSchema.shape,
141
- }),
142
- ]);
143
-
144
- const ApiKeysSchema = z.object({
145
- enabled: z.boolean(),
146
- getConsumers: z
147
- .custom<(context: ZudokuContext) => Promise<ApiConsumer[]>>(
148
- (val) => typeof val === "function",
149
- )
150
- .optional(),
151
- rollKey: z
152
- .custom<(id: string, context: ZudokuContext) => Promise<void>>(
153
- (val) => typeof val === "function",
154
- )
155
- .optional(),
156
- deleteKey: z
157
- .custom<
158
- (
159
- consumerId: string,
160
- keyId: string,
161
- context: ZudokuContext,
162
- ) => Promise<void>
163
- >((val) => typeof val === "function")
164
- .optional(),
165
- updateKeyDescription: z
166
- .custom<
167
- (
168
- apiKey: { id: string; description: string },
169
- context: ZudokuContext,
170
- ) => Promise<void>
171
- >((val) => typeof val === "function")
172
- .optional(),
173
- createKey: z
174
- .custom<
175
- ({
176
- apiKey,
177
- context,
178
- auth,
179
- }: {
180
- apiKey: { description: string; expiresOn?: string };
181
- context: ZudokuContext;
182
- auth: UseAuthReturn;
183
- }) => Promise<void>
184
- >((val) => typeof val === "function")
185
- .optional(),
186
- });
187
-
188
- export type ApiKeysOptions = z.infer<typeof ApiKeysSchema>;
189
-
190
- const LogoSchema = z.object({
191
- src: z.object({ light: z.string(), dark: z.string() }),
192
- alt: z.string().optional(),
193
- width: z.string().or(z.number()).optional(),
194
- href: z.string().optional(),
195
- reloadDocument: z.boolean().optional(),
196
- });
197
-
198
- export const FooterSocialIcons = [
199
- "reddit",
200
- "discord",
201
- "github",
202
- "x",
203
- "linkedin",
204
- "facebook",
205
- "instagram",
206
- "youtube",
207
- "tiktok",
208
- "twitch",
209
- "pinterest",
210
- "snapchat",
211
- "whatsapp",
212
- "telegram",
213
- ] as const;
214
-
215
- export const FooterSocialSchema = z.object({
216
- label: z.string().optional(),
217
- href: z.string(),
218
- icon: z
219
- .union([
220
- z.enum(FooterSocialIcons),
221
- z.custom<ReactNode>((val) => isValidElement(val)),
222
- ])
223
- .optional(),
224
- });
225
-
226
- export const FooterSchema = z
227
- .object({
228
- columns: z
229
- .array(
230
- z.object({
231
- position: z.enum(["start", "center", "end"]).optional(),
232
- title: z.string(),
233
- links: z.array(z.object({ label: z.string(), href: z.string() })),
234
- }),
235
- )
236
- .optional(),
237
- social: z.array(FooterSocialSchema).optional(),
238
- copyright: z.string().optional(),
239
- logo: LogoSchema.optional(),
240
- position: z.enum(["start", "center", "end"]).optional(),
241
- })
242
- .optional();
243
-
244
- const SiteMapSchema = z
245
- .object({
246
- /**
247
- * Base url of your website
248
- */
249
- siteUrl: z.string(),
250
- /**
251
- * Change frequency.
252
- * @default 'daily'
253
- */
254
- changefreq: z.optional(
255
- z.enum([
256
- "always",
257
- "hourly",
258
- "daily",
259
- "weekly",
260
- "monthly",
261
- "yearly",
262
- "never",
263
- ]),
264
- ),
265
- /**
266
- * Priority
267
- * @default 0.7
268
- */
269
- priority: z.optional(z.number()),
270
- outDir: z.string().optional(),
271
- /**
272
- * Add <lastmod/> property.
273
- * @default true
274
- */
275
- autoLastmod: z.boolean().optional(),
276
- /**
277
- * Array of relative paths to exclude from listing on sitemap.xml or sitemap-*.xml.
278
- * @example ['/page-0', '/page/example']
279
- */
280
- exclude: z
281
- .union([
282
- z.custom<() => Promise<string[]>>((val) => typeof val === "function"),
283
- z.array(z.string()),
284
- ])
285
- .optional(),
286
- })
287
- .optional();
288
-
289
- const DEFAULT_DOCS_FILES = "/pages/**/*.{md,mdx}";
290
-
291
- const LlmsConfigSchema = z
292
- .object({
293
- llmsTxt: z
294
- .boolean()
295
- .default(false)
296
- .describe(
297
- "When enabled, generates an llms.txt file following the spec at https://llmstxt.org/. This file provides a summary with links to all documentation.",
298
- ),
299
- llmsTxtFull: z
300
- .boolean()
301
- .default(false)
302
- .describe(
303
- "When enabled, generates an llms-full.txt file with the complete content of all markdown documents for LLM consumption.",
304
- ),
305
- includeProtected: z
306
- .boolean()
307
- .default(false)
308
- .describe(
309
- "When enabled, includes content from protected routes in the generated .md files and llms.txt files. By default, protected routes are excluded.",
310
- ),
311
- })
312
- .partial();
313
-
314
- export const DocsConfigSchema = z.object({
315
- files: z
316
- .union([z.string(), z.array(z.string())])
317
- .transform((val) => (typeof val === "string" ? [val] : val))
318
- .default([DEFAULT_DOCS_FILES]),
319
- publishMarkdown: z
320
- .boolean()
321
- .default(false)
322
- .describe(
323
- "When enabled, generates .md files for each document during build. Access documents at their URL path with .md extension (e.g., /foo/hello.md). Markdown files are generated without frontmatter.",
324
- ),
325
- defaultOptions: z
326
- .object({
327
- toc: z.boolean(),
328
- copyPage: z.boolean().optional(),
329
- disablePager: z.boolean(),
330
- showLastModified: z.boolean(),
331
- suggestEdit: z
332
- .object({
333
- url: z.string(),
334
- text: z.string().optional(),
335
- })
336
- .optional(),
337
- })
338
- .partial()
339
- .optional(),
340
- llms: LlmsConfigSchema.optional(),
341
- });
342
-
343
- const Redirect = z.object({
344
- from: z.string(),
345
- to: z.string(),
346
- });
347
-
348
- const SearchSchema = z
349
- .discriminatedUnion("type", [
350
- // looseObject to allow additional properties so the
351
- // user can set other inkeep settings
352
- z.looseObject({
353
- type: z.literal("inkeep"),
354
- apiKey: z.string(),
355
- integrationId: z.string(),
356
- organizationId: z.string(),
357
- primaryBrandColor: z.string(),
358
- organizationDisplayName: z.string(),
359
- }),
360
- z.object({
361
- type: z.literal("pagefind"),
362
-
363
- ranking: z
364
- .object({
365
- termFrequency: z.number(),
366
- pageLength: z.number(),
367
- termSimilarity: z.number(),
368
- termSaturation: z.number(),
369
- })
370
- .optional(),
371
- maxResults: z.number().optional(),
372
- maxSubResults: z.number().optional(),
373
- transformResults: z
374
- .custom<
375
- (data: {
376
- result: PagefindSearchFragment;
377
- auth: AuthState;
378
- context: ZudokuContext;
379
- }) => PagefindSearchFragment | boolean | undefined | void
380
- >((val) => typeof val === "function")
381
- .optional(),
382
- }),
383
- ])
384
- .optional();
385
-
386
- const AuthenticationSchema = z.discriminatedUnion("type", [
387
- z.object({
388
- type: z.literal("clerk"),
389
- clerkPubKey: z
390
- .custom<`pk_test_${string}` | `pk_live_${string}`>()
391
- .refine((val) => /^pk_(test|live)_\w+$/.test(val), {
392
- message: "Clerk public key invalid, must start with pk_test or pk_live",
393
- }),
394
- jwtTemplateName: z.string().optional().default("dev-portal"),
395
- redirectToAfterSignUp: z.string().optional(),
396
- redirectToAfterSignIn: z.string().optional(),
397
- redirectToAfterSignOut: z.string().optional(),
398
- }),
399
- z.object({
400
- type: z.literal("firebase"),
401
- apiKey: z.string(),
402
- authDomain: z.string(),
403
- projectId: z.string(),
404
- storageBucket: z.string().optional(),
405
- messagingSenderId: z.string().optional(),
406
- appId: z.string(),
407
- measurementId: z.string().optional(),
408
- providers: z
409
- .array(
410
- z.enum([
411
- "google",
412
- "facebook",
413
- "twitter",
414
- "github",
415
- "microsoft",
416
- "apple",
417
- "yahoo",
418
- "password",
419
- "phone",
420
- "emailLink",
421
- ]),
422
- )
423
- .optional(),
424
- redirectToAfterSignUp: z.string().optional(),
425
- redirectToAfterSignIn: z.string().optional(),
426
- redirectToAfterSignOut: z.string().optional(),
427
- }),
428
- z.object({
429
- type: z.literal("openid"),
430
- basePath: z.string().optional(),
431
- clientId: z.string(),
432
- issuer: z.string(),
433
- audience: z.string().optional(),
434
- scopes: z.array(z.string()).optional(),
435
- redirectToAfterSignUp: z.string().optional(),
436
- redirectToAfterSignIn: z.string().optional(),
437
- redirectToAfterSignOut: z.string().optional(),
438
- }),
439
- z.object({
440
- type: z.literal("azureb2c"),
441
- basePath: z.string().optional(),
442
- clientId: z.string(),
443
- tenantName: z.string(),
444
- policyName: z.string(),
445
- scopes: z.array(z.string()).optional(),
446
- issuer: z.string(),
447
- redirectToAfterSignUp: z.string().optional(),
448
- redirectToAfterSignIn: z.string().optional(),
449
- redirectToAfterSignOut: z.string().optional(),
450
- }),
451
-
452
- z.object({
453
- type: z.literal("auth0"),
454
- clientId: z.string(),
455
- domain: z.string().refine(
456
- (val) => {
457
- if (val.startsWith("http://") || val.startsWith("https://")) {
458
- return false;
459
- }
460
- if (val.includes("/")) {
461
- return false;
462
- }
463
- return val.includes(".") && val.length > 0;
464
- },
465
- {
466
- message:
467
- "Domain must be a host only (e.g., 'example.com') without protocol or slashes",
468
- },
469
- ),
470
- audience: z.string().optional(),
471
- scopes: z.array(z.string()).optional(),
472
- redirectToAfterSignUp: z.string().optional(),
473
- redirectToAfterSignIn: z.string().optional(),
474
- redirectToAfterSignOut: z.string().optional(),
475
- options: z
476
- .object({
477
- alwaysPromptLogin: z.boolean().optional(),
478
- prompt: z.string().optional(),
479
- })
480
- .optional(),
481
- }),
482
- z.object({
483
- type: z.literal("supabase"),
484
- basePath: z.string().optional(),
485
- supabaseUrl: z.string(),
486
- supabaseKey: z.string(),
487
- provider: z.string().optional(),
488
- providers: z.array(z.string()).optional(),
489
- onlyThirdPartyProviders: z.boolean().optional(),
490
- redirectToAfterSignUp: z.string().optional(),
491
- redirectToAfterSignIn: z.string().optional(),
492
- redirectToAfterSignOut: z.string().optional(),
493
- }),
494
- z.object({
495
- type: z.literal("dev-portal").optional().default("dev-portal"),
496
- apiBaseUrl: z.string().optional(),
497
- demoMode: z.boolean().optional(),
498
- native: z
499
- .object({
500
- enabled: z.boolean().optional(),
501
- })
502
- .optional(),
503
- email: z
504
- .object({
505
- provider: z.string().optional(),
506
- from: z.string().optional(),
507
- connectionString: z.string().optional(),
508
- })
509
- .optional(),
510
- providers: z
511
- .array(
512
- z.object({
513
- id: z.string().min(1),
514
- displayName: z.string().optional(),
515
- authority: z.string().optional(),
516
- clientId: z.string().optional(),
517
- clientSecret: z.string().optional(),
518
- scopes: z.string().optional(),
519
- }),
520
- )
521
- .optional(),
522
- redirectToAfterSignUp: z.string().optional(),
523
- redirectToAfterSignIn: z.string().optional(),
524
- redirectToAfterSignOut: z.string().optional(),
525
- }),
526
- ]);
527
-
528
- const MetadataSchema = z
529
- .object({
530
- title: z.string(),
531
- defaultTitle: z.string().optional(),
532
- description: z.string(),
533
- logo: z.string(),
534
- favicon: z.string(),
535
- generator: z.string(),
536
- applicationName: z.string(),
537
- referrer: z.string(),
538
- keywords: z.array(z.string()),
539
- authors: z.array(z.string()),
540
- creator: z.string(),
541
- publisher: z.string(),
542
- })
543
- .partial();
544
-
545
- const FontConfigSchema = z.union([
546
- z.enum(GOOGLE_FONTS),
547
- z.object({
548
- url: z.string(),
549
- fontFamily: z.string().optional(),
550
- }),
551
- ]);
552
-
553
- export type FontConfig = z.infer<typeof FontConfigSchema>;
554
-
555
- const FontsConfigSchema = z.object({
556
- sans: FontConfigSchema.optional(),
557
- serif: FontConfigSchema.optional(),
558
- mono: FontConfigSchema.optional(),
559
- });
560
-
561
- const CssObject = z.record(
562
- z.string(),
563
- z.lazy(() =>
564
- z.union([
565
- z.string(),
566
- z.record(
567
- z.string(),
568
- z.union([z.string(), z.record(z.string(), z.string())]),
569
- ),
570
- ]),
571
- ),
572
- );
573
-
574
- const ThemeConfigSchema = z.object({
575
- registryUrl: z.string().url().optional(),
576
- customCss: z.union([z.string(), CssObject]).optional(),
577
- light: ThemeSchema.optional(),
578
- dark: ThemeSchema.optional(),
579
- fonts: FontsConfigSchema.optional(),
580
- noDefaultTheme: z.boolean().optional(),
581
- });
582
-
583
- const SiteSchema = z
584
- .object({
585
- title: z.string(),
586
- showTitleInHeader: z.boolean().optional(),
587
- logoUrl: z.string(),
588
- dir: z.enum(["ltr", "rtl"]).optional(),
589
- logo: LogoSchema,
590
- banner: z.object({
591
- message: z.custom<NonNullable<ReactNode>>(),
592
- color: z
593
- .custom<"note" | "tip" | "info" | "caution" | "danger" | (string & {})>(
594
- (val) => typeof val === "string",
595
- )
596
- .optional(),
597
- dismissible: z.boolean().optional(),
598
- }),
599
- footer: FooterSchema,
600
- })
601
- .partial();
602
-
603
- const PlacementPosition = z.enum(["start", "center", "end"]);
604
-
605
- const HeaderConfigSchema = z
606
- .object({
607
- navigation: HeaderNavigationSchema,
608
- placements: z
609
- .object({
610
- navigation: PlacementPosition,
611
- search: PlacementPosition,
612
- auth: z.enum(["start", "center", "end", "navigation"]),
613
- })
614
- .partial()
615
- .optional(),
616
- })
617
- .partial();
618
-
619
- const ApiCatalogSchema = z.object({
620
- path: z.string(),
621
- label: z.string(),
622
- items: z.array(z.string()).optional(),
623
- filterItems: z
624
- .custom<FilterCatalogItemsFn>((val) => typeof val === "function")
625
- .optional(),
626
- });
627
-
628
- export const CdnUrlSchema = z
629
- .union([
630
- z.string(),
631
- z.object({
632
- base: z.string().optional(),
633
- media: z.string().optional(),
634
- }),
635
- ])
636
- .transform((val) => {
637
- if (typeof val === "string") {
638
- return { base: val, media: val };
639
- }
640
- return { base: val.base, media: val.media };
641
- })
642
- .optional();
643
-
644
- const BaseConfigSchema = z.object({
645
- slots: z.record(z.string(), z.custom<SlotType>()),
646
- /**
647
- * @deprecated Use `slots` instead
648
- */
649
- UNSAFE_slotlets: z.record(z.string(), z.custom<SlotType>()),
650
- mdx: z
651
- .object({
652
- components: z.custom<MdxComponentsType>(),
653
- })
654
- .partial(),
655
- customPages: z.array(
656
- z.object({
657
- path: z.string(),
658
- element: z.custom<NonNullable<ReactNode>>().optional(),
659
- render: z.custom<ComponentType<ExposedComponentProps>>().optional(),
660
- prose: z.boolean().optional(),
661
- }),
662
- ),
663
- plugins: z.array(z.custom<ApitogoPlugin>()),
664
- build: z.custom<{
665
- remarkPlugins?: Options["remarkPlugins"];
666
- rehypePlugins?: Options["rehypePlugins"];
667
- }>(),
668
- protectedRoutes: ProtectedRoutesSchema,
669
- basePath: z.string().optional(),
670
- canonicalUrlOrigin: z.string().optional(),
671
- cdnUrl: CdnUrlSchema.optional(),
672
- port: z.number().optional(),
673
- https: z
674
- .object({
675
- key: z.string(),
676
- cert: z.string(),
677
- ca: z.string().optional(),
678
- })
679
- .optional(),
680
- site: SiteSchema,
681
- header: HeaderConfigSchema.optional(),
682
- navigation: InputNavigationSchema,
683
- navigationRules: NavigationRulesSchema.optional(),
684
- theme: ThemeConfigSchema,
685
- syntaxHighlighting: z
686
- .object({
687
- languages: z.array(z.custom<BundledLanguage>()),
688
- themes: z.object({
689
- light: z.custom<BundledTheme>(),
690
- dark: z.custom<BundledTheme>(),
691
- }),
692
- })
693
- .partial()
694
- .optional(),
695
- metadata: MetadataSchema,
696
- authentication: AuthenticationSchema,
697
- search: SearchSchema,
698
- docs: DocsConfigSchema.optional(),
699
- modules: ModulesSchema,
700
- apis: z.union([ApiSchema, z.array(ApiSchema)]),
701
- catalogs: z.union([ApiCatalogSchema, z.array(ApiCatalogSchema)]),
702
- apiKeys: ApiKeysSchema,
703
- aiAssistants: AiAssistantsSchema,
704
- redirects: z.array(Redirect),
705
- sitemap: SiteMapSchema,
706
- enableStatusPages: z.boolean().optional(),
707
- defaults: z.object({
708
- apis: ApiOptionsSchema,
709
- /**
710
- * @deprecated Use `apis.examplesLanguage` or `defaults.apis.examplesLanguage` instead
711
- */
712
- examplesLanguage: z.string().optional(),
713
- }),
714
- // Internal: populated by plugins via `transformConfig` to track plugin directories
715
- __pluginDirs: z.array(z.string()),
716
- });
717
-
718
- export const ZudokuConfig = BaseConfigSchema.partial();
719
-
720
- export type ZudokuApiConfig = z.infer<typeof ApiSchema>;
721
- export type VersionConfig = z.infer<typeof VersionConfigSchema>;
722
- export type ZudokuSiteMapConfig = z.infer<typeof SiteMapSchema>;
723
- export type ZudokuDocsConfig = z.infer<typeof DocsConfigSchema>;
724
- export type { ModulesConfig } from "./ModulesSchema.js";
725
- export type {
726
- ResolvedModulesConfig,
727
- DocsModuleConfig,
728
- LandingModuleConfig,
729
- LandingContentConfig,
730
- LandingFeatureConfig,
731
- LandingLinkConfig,
732
- ResolvedLandingContent,
733
- ResolvedDashboardContent,
734
- ResolvedProfileContent,
735
- ResolvedPlansContent,
736
- ResolvedLandingModule,
737
- ResolvedUserPanelModule,
738
- ResolvedDashboardModule,
739
- ResolvedUserManagementModule,
740
- ResolvedApiKeysModule,
741
- ResolvedPlansModule,
742
- UserPanelModuleConfig,
743
- DashboardModuleConfig,
744
- UserManagementModuleConfig,
745
- ApiKeysModuleConfig,
746
- PlansModuleConfig,
747
- DashboardContentConfig,
748
- ProfileContentConfig,
749
- PlansContentConfig,
750
- PlanTierConfig,
751
- } from "./ModulesSchema.js";
752
- export type ZudokuLlmsConfig = z.infer<typeof LlmsConfigSchema>;
753
- export type ZudokuRedirect = z.infer<typeof Redirect>;
754
- export type AuthenticationConfig = z.infer<typeof AuthenticationSchema>;
755
-
756
- // Use `z.input` type for flexibility with transforms,
757
- // but override navigation with `z.infer` for strict validation
758
- type BaseZudokuConfig = z.input<typeof ZudokuConfig>;
759
- export type ZudokuConfig = Omit<
760
- BaseZudokuConfig,
761
- "header" | "navigation" | "navigationRules"
762
- > & {
763
- header?: {
764
- navigation?: z.infer<typeof HeaderNavigationSchema>;
765
- placements?: {
766
- navigation?: "start" | "center" | "end";
767
- search?: "start" | "center" | "end";
768
- auth?: "start" | "center" | "end" | "navigation";
769
- };
770
- };
771
- navigation?: z.infer<typeof InputNavigationSchema>;
772
- navigationRules?: z.infer<typeof NavigationRulesSchema>;
773
- };
774
-
775
- export function validateConfig(config: unknown, configPath?: string) {
776
- const validationResult = ZudokuConfig.safeParse(config);
777
-
778
- if (!validationResult.success) {
779
- // In production (build mode), throw an error to fail the build
780
- if (process.env.NODE_ENV === "production") {
781
- throw new Error(
782
- `Whoops, looks like there's an issue with your ${configPath ?? "config"}:\n${z.prettifyError(validationResult.error)}`,
783
- );
784
- }
785
-
786
- // In development mode, log warnings but don't fail
787
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
788
- console.log(colors.yellow("Validation errors:"));
789
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
790
- console.log(colors.yellow(z.prettifyError(validationResult.error)));
791
- }
792
- }
1
+ import type { Options } from "@mdx-js/rollup";
2
+ import colors from "picocolors";
3
+ import type { ComponentType, ReactNode } from "react";
4
+ import { isValidElement } from "react";
5
+ import type { BundledLanguage, BundledTheme } from "shiki";
6
+ import { z } from "zod";
7
+ import type { UseAuthReturn } from "../../lib/authentication/hook.js";
8
+ import type { AuthState } from "../../lib/authentication/state.js";
9
+ import type { SlotType } from "../../lib/components/context/SlotProvider.js";
10
+ import type { ApitogoPlugin } from "../../lib/core/plugins.js";
11
+ import type { ZudokuContext } from "../../lib/core/ZudokuContext.js";
12
+ import type { FilterCatalogItemsFn } from "../../lib/plugins/api-catalog/index.js";
13
+ import type { ApiConsumer } from "../../lib/plugins/api-keys/index.js";
14
+ import type {
15
+ GenerateCodeSnippetFn,
16
+ TransformExamplesFn,
17
+ } from "../../lib/plugins/openapi/interfaces.js";
18
+ import type { PagefindSearchFragment } from "../../lib/plugins/search-pagefind/types.js";
19
+ import type { MdxComponentsType } from "../../lib/util/MdxComponents.js";
20
+ import type { ExposedComponentProps } from "../../lib/util/useExposedProps.js";
21
+ import { GOOGLE_FONTS } from "../../vite/plugin-theme.js";
22
+ import { HeaderNavigationSchema } from "./HeaderNavigationSchema.js";
23
+ import {
24
+ InputNavigationSchema,
25
+ NavigationRulesSchema,
26
+ } from "./InputNavigationSchema.js";
27
+ import { ModulesSchema } from "./ModulesSchema.js";
28
+ import { ProtectedRoutesSchema } from "./ProtectedRoutesSchema.js";
29
+
30
+ const ThemeSchema = z
31
+ .object({
32
+ background: z.string(),
33
+ foreground: z.string(),
34
+ card: z.string(),
35
+ cardForeground: z.string(),
36
+ popover: z.string(),
37
+ popoverForeground: z.string(),
38
+ primary: z.string(),
39
+ primaryForeground: z.string(),
40
+ secondary: z.string(),
41
+ secondaryForeground: z.string(),
42
+ muted: z.string(),
43
+ mutedForeground: z.string(),
44
+ accent: z.string(),
45
+ accentForeground: z.string(),
46
+ destructive: z.string(),
47
+ destructiveForeground: z.string(),
48
+ border: z.string(),
49
+ input: z.string(),
50
+ ring: z.string(),
51
+ radius: z.string(),
52
+ })
53
+ .partial();
54
+
55
+ const ApiCatalogCategorySchema = z.object({
56
+ label: z.string(),
57
+ tags: z.array(z.string()),
58
+ });
59
+
60
+ const LanguageOption = z.object({
61
+ value: z.string().min(1),
62
+ label: z.string().min(1),
63
+ });
64
+
65
+ const AiAssistantCustomSchema = z.object({
66
+ label: z.string(),
67
+ icon: z.custom<ReactNode>().optional(),
68
+ url: z.union([
69
+ z.string(),
70
+ z.custom<
71
+ (context: { pageUrl: string; type: "docs" | "openapi" }) => string
72
+ >((val) => typeof val === "function"),
73
+ ]),
74
+ });
75
+
76
+ export type AiAssistantCustom = z.infer<typeof AiAssistantCustomSchema>;
77
+
78
+ const AiAssistantPresets = ["claude", "chatgpt"] as const;
79
+
80
+ const AiAssistantsSchema = z
81
+ .union([
82
+ z.literal(false),
83
+ z.array(z.union([z.enum(AiAssistantPresets), AiAssistantCustomSchema])),
84
+ ])
85
+ .optional();
86
+
87
+ export type AiAssistantsConfig = z.infer<typeof AiAssistantsSchema>;
88
+
89
+ const ApiOptionsSchema = z
90
+ .object({
91
+ examplesLanguage: z.string(),
92
+ supportedLanguages: z.array(LanguageOption),
93
+ disablePlayground: z.boolean(),
94
+ disableSidecar: z.boolean(),
95
+ showVersionSelect: z.enum(["always", "if-available", "hide"]),
96
+ expandAllTags: z.boolean(),
97
+ showInfoPage: z.boolean(),
98
+ schemaDownload: z.object({ enabled: z.boolean() }).partial(),
99
+ transformExamples: z.custom<TransformExamplesFn>(
100
+ (val) => typeof val === "function",
101
+ ),
102
+ generateCodeSnippet: z.custom<GenerateCodeSnippetFn>(
103
+ (val) => typeof val === "function",
104
+ ),
105
+ })
106
+ .partial();
107
+
108
+ const ApiConfigSchema = z
109
+ .object({
110
+ server: z.string(),
111
+ path: z.string(),
112
+ categories: z.array(ApiCatalogCategorySchema),
113
+ options: ApiOptionsSchema,
114
+ })
115
+ .partial();
116
+
117
+ const VersionConfigSchema = z.object({
118
+ path: z.string(),
119
+ input: z.string(),
120
+ label: z.string().optional(),
121
+ });
122
+
123
+ const ApiSchema = z.discriminatedUnion("type", [
124
+ z.object({
125
+ type: z.literal("url"),
126
+ input: z.union([z.string(), z.array(VersionConfigSchema)]),
127
+ ...ApiConfigSchema.shape,
128
+ }),
129
+ z.object({
130
+ type: z.literal("file"),
131
+ input: z.union([
132
+ z.string(),
133
+ z.array(z.union([z.string(), VersionConfigSchema])),
134
+ ]),
135
+ ...ApiConfigSchema.shape,
136
+ }),
137
+ z.object({
138
+ type: z.literal("raw"),
139
+ input: z.string(),
140
+ ...ApiConfigSchema.shape,
141
+ }),
142
+ ]);
143
+
144
+ const ApiKeysSchema = z.object({
145
+ enabled: z.boolean(),
146
+ getConsumers: z
147
+ .custom<(context: ZudokuContext) => Promise<ApiConsumer[]>>(
148
+ (val) => typeof val === "function",
149
+ )
150
+ .optional(),
151
+ rollKey: z
152
+ .custom<(id: string, context: ZudokuContext) => Promise<void>>(
153
+ (val) => typeof val === "function",
154
+ )
155
+ .optional(),
156
+ deleteKey: z
157
+ .custom<
158
+ (
159
+ consumerId: string,
160
+ keyId: string,
161
+ context: ZudokuContext,
162
+ ) => Promise<void>
163
+ >((val) => typeof val === "function")
164
+ .optional(),
165
+ updateKeyDescription: z
166
+ .custom<
167
+ (
168
+ apiKey: { id: string; description: string },
169
+ context: ZudokuContext,
170
+ ) => Promise<void>
171
+ >((val) => typeof val === "function")
172
+ .optional(),
173
+ createKey: z
174
+ .custom<
175
+ ({
176
+ apiKey,
177
+ context,
178
+ auth,
179
+ }: {
180
+ apiKey: { description: string; expiresOn?: string };
181
+ context: ZudokuContext;
182
+ auth: UseAuthReturn;
183
+ }) => Promise<void>
184
+ >((val) => typeof val === "function")
185
+ .optional(),
186
+ });
187
+
188
+ export type ApiKeysOptions = z.infer<typeof ApiKeysSchema>;
189
+
190
+ const LogoSchema = z.object({
191
+ src: z.object({ light: z.string(), dark: z.string() }),
192
+ alt: z.string().optional(),
193
+ width: z.string().or(z.number()).optional(),
194
+ href: z.string().optional(),
195
+ reloadDocument: z.boolean().optional(),
196
+ });
197
+
198
+ export const FooterSocialIcons = [
199
+ "reddit",
200
+ "discord",
201
+ "github",
202
+ "x",
203
+ "linkedin",
204
+ "facebook",
205
+ "instagram",
206
+ "youtube",
207
+ "tiktok",
208
+ "twitch",
209
+ "pinterest",
210
+ "snapchat",
211
+ "whatsapp",
212
+ "telegram",
213
+ ] as const;
214
+
215
+ export const FooterSocialSchema = z.object({
216
+ label: z.string().optional(),
217
+ href: z.string(),
218
+ icon: z
219
+ .union([
220
+ z.enum(FooterSocialIcons),
221
+ z.custom<ReactNode>((val) => isValidElement(val)),
222
+ ])
223
+ .optional(),
224
+ });
225
+
226
+ export const FooterSchema = z
227
+ .object({
228
+ columns: z
229
+ .array(
230
+ z.object({
231
+ position: z.enum(["start", "center", "end"]).optional(),
232
+ title: z.string(),
233
+ links: z.array(z.object({ label: z.string(), href: z.string() })),
234
+ }),
235
+ )
236
+ .optional(),
237
+ social: z.array(FooterSocialSchema).optional(),
238
+ copyright: z.string().optional(),
239
+ logo: LogoSchema.optional(),
240
+ position: z.enum(["start", "center", "end"]).optional(),
241
+ })
242
+ .optional();
243
+
244
+ const SiteMapSchema = z
245
+ .object({
246
+ /**
247
+ * Base url of your website
248
+ */
249
+ siteUrl: z.string(),
250
+ /**
251
+ * Change frequency.
252
+ * @default 'daily'
253
+ */
254
+ changefreq: z.optional(
255
+ z.enum([
256
+ "always",
257
+ "hourly",
258
+ "daily",
259
+ "weekly",
260
+ "monthly",
261
+ "yearly",
262
+ "never",
263
+ ]),
264
+ ),
265
+ /**
266
+ * Priority
267
+ * @default 0.7
268
+ */
269
+ priority: z.optional(z.number()),
270
+ outDir: z.string().optional(),
271
+ /**
272
+ * Add <lastmod/> property.
273
+ * @default true
274
+ */
275
+ autoLastmod: z.boolean().optional(),
276
+ /**
277
+ * Array of relative paths to exclude from listing on sitemap.xml or sitemap-*.xml.
278
+ * @example ['/page-0', '/page/example']
279
+ */
280
+ exclude: z
281
+ .union([
282
+ z.custom<() => Promise<string[]>>((val) => typeof val === "function"),
283
+ z.array(z.string()),
284
+ ])
285
+ .optional(),
286
+ })
287
+ .optional();
288
+
289
+ const DEFAULT_DOCS_FILES = "/pages/**/*.{md,mdx}";
290
+
291
+ const LlmsConfigSchema = z
292
+ .object({
293
+ llmsTxt: z
294
+ .boolean()
295
+ .default(false)
296
+ .describe(
297
+ "When enabled, generates an llms.txt file following the spec at https://llmstxt.org/. This file provides a summary with links to all documentation.",
298
+ ),
299
+ llmsTxtFull: z
300
+ .boolean()
301
+ .default(false)
302
+ .describe(
303
+ "When enabled, generates an llms-full.txt file with the complete content of all markdown documents for LLM consumption.",
304
+ ),
305
+ includeProtected: z
306
+ .boolean()
307
+ .default(false)
308
+ .describe(
309
+ "When enabled, includes content from protected routes in the generated .md files and llms.txt files. By default, protected routes are excluded.",
310
+ ),
311
+ })
312
+ .partial();
313
+
314
+ export const DocsConfigSchema = z.object({
315
+ files: z
316
+ .union([z.string(), z.array(z.string())])
317
+ .transform((val) => (typeof val === "string" ? [val] : val))
318
+ .default([DEFAULT_DOCS_FILES]),
319
+ publishMarkdown: z
320
+ .boolean()
321
+ .default(false)
322
+ .describe(
323
+ "When enabled, generates .md files for each document during build. Access documents at their URL path with .md extension (e.g., /foo/hello.md). Markdown files are generated without frontmatter.",
324
+ ),
325
+ defaultOptions: z
326
+ .object({
327
+ toc: z.boolean(),
328
+ copyPage: z.boolean().optional(),
329
+ disablePager: z.boolean(),
330
+ showLastModified: z.boolean(),
331
+ suggestEdit: z
332
+ .object({
333
+ url: z.string(),
334
+ text: z.string().optional(),
335
+ })
336
+ .optional(),
337
+ })
338
+ .partial()
339
+ .optional(),
340
+ llms: LlmsConfigSchema.optional(),
341
+ });
342
+
343
+ const Redirect = z.object({
344
+ from: z.string(),
345
+ to: z.string(),
346
+ });
347
+
348
+ const SearchSchema = z
349
+ .discriminatedUnion("type", [
350
+ // looseObject to allow additional properties so the
351
+ // user can set other inkeep settings
352
+ z.looseObject({
353
+ type: z.literal("inkeep"),
354
+ apiKey: z.string(),
355
+ integrationId: z.string(),
356
+ organizationId: z.string(),
357
+ primaryBrandColor: z.string(),
358
+ organizationDisplayName: z.string(),
359
+ }),
360
+ z.object({
361
+ type: z.literal("pagefind"),
362
+
363
+ ranking: z
364
+ .object({
365
+ termFrequency: z.number(),
366
+ pageLength: z.number(),
367
+ termSimilarity: z.number(),
368
+ termSaturation: z.number(),
369
+ })
370
+ .optional(),
371
+ maxResults: z.number().optional(),
372
+ maxSubResults: z.number().optional(),
373
+ transformResults: z
374
+ .custom<
375
+ (data: {
376
+ result: PagefindSearchFragment;
377
+ auth: AuthState;
378
+ context: ZudokuContext;
379
+ }) => PagefindSearchFragment | boolean | undefined | void
380
+ >((val) => typeof val === "function")
381
+ .optional(),
382
+ }),
383
+ ])
384
+ .optional();
385
+
386
+ const AuthenticationSchema = z.discriminatedUnion("type", [
387
+ z.object({
388
+ type: z.literal("clerk"),
389
+ clerkPubKey: z
390
+ .custom<`pk_test_${string}` | `pk_live_${string}`>()
391
+ .refine((val) => /^pk_(test|live)_\w+$/.test(val), {
392
+ message: "Clerk public key invalid, must start with pk_test or pk_live",
393
+ }),
394
+ jwtTemplateName: z.string().optional().default("dev-portal"),
395
+ redirectToAfterSignUp: z.string().optional(),
396
+ redirectToAfterSignIn: z.string().optional(),
397
+ redirectToAfterSignOut: z.string().optional(),
398
+ }),
399
+ z.object({
400
+ type: z.literal("firebase"),
401
+ apiKey: z.string(),
402
+ authDomain: z.string(),
403
+ projectId: z.string(),
404
+ storageBucket: z.string().optional(),
405
+ messagingSenderId: z.string().optional(),
406
+ appId: z.string(),
407
+ measurementId: z.string().optional(),
408
+ providers: z
409
+ .array(
410
+ z.enum([
411
+ "google",
412
+ "facebook",
413
+ "twitter",
414
+ "github",
415
+ "microsoft",
416
+ "apple",
417
+ "yahoo",
418
+ "password",
419
+ "phone",
420
+ "emailLink",
421
+ ]),
422
+ )
423
+ .optional(),
424
+ redirectToAfterSignUp: z.string().optional(),
425
+ redirectToAfterSignIn: z.string().optional(),
426
+ redirectToAfterSignOut: z.string().optional(),
427
+ }),
428
+ z.object({
429
+ type: z.literal("openid"),
430
+ basePath: z.string().optional(),
431
+ clientId: z.string(),
432
+ issuer: z.string(),
433
+ audience: z.string().optional(),
434
+ scopes: z.array(z.string()).optional(),
435
+ redirectToAfterSignUp: z.string().optional(),
436
+ redirectToAfterSignIn: z.string().optional(),
437
+ redirectToAfterSignOut: z.string().optional(),
438
+ /** Try it offers "Signed-in user" (the visitor's token); false when the gateway checks keys, not tokens (API-129). */
439
+ tokenIdentity: z.boolean().optional(),
440
+ }),
441
+ z.object({
442
+ type: z.literal("azureb2c"),
443
+ basePath: z.string().optional(),
444
+ clientId: z.string(),
445
+ tenantName: z.string(),
446
+ policyName: z.string(),
447
+ scopes: z.array(z.string()).optional(),
448
+ issuer: z.string(),
449
+ redirectToAfterSignUp: z.string().optional(),
450
+ redirectToAfterSignIn: z.string().optional(),
451
+ redirectToAfterSignOut: z.string().optional(),
452
+ }),
453
+
454
+ z.object({
455
+ type: z.literal("auth0"),
456
+ clientId: z.string(),
457
+ domain: z.string().refine(
458
+ (val) => {
459
+ if (val.startsWith("http://") || val.startsWith("https://")) {
460
+ return false;
461
+ }
462
+ if (val.includes("/")) {
463
+ return false;
464
+ }
465
+ return val.includes(".") && val.length > 0;
466
+ },
467
+ {
468
+ message:
469
+ "Domain must be a host only (e.g., 'example.com') without protocol or slashes",
470
+ },
471
+ ),
472
+ audience: z.string().optional(),
473
+ scopes: z.array(z.string()).optional(),
474
+ redirectToAfterSignUp: z.string().optional(),
475
+ redirectToAfterSignIn: z.string().optional(),
476
+ redirectToAfterSignOut: z.string().optional(),
477
+ options: z
478
+ .object({
479
+ alwaysPromptLogin: z.boolean().optional(),
480
+ prompt: z.string().optional(),
481
+ })
482
+ .optional(),
483
+ }),
484
+ z.object({
485
+ type: z.literal("supabase"),
486
+ basePath: z.string().optional(),
487
+ supabaseUrl: z.string(),
488
+ supabaseKey: z.string(),
489
+ provider: z.string().optional(),
490
+ providers: z.array(z.string()).optional(),
491
+ onlyThirdPartyProviders: z.boolean().optional(),
492
+ redirectToAfterSignUp: z.string().optional(),
493
+ redirectToAfterSignIn: z.string().optional(),
494
+ redirectToAfterSignOut: z.string().optional(),
495
+ }),
496
+ z.object({
497
+ type: z.literal("dev-portal").optional().default("dev-portal"),
498
+ apiBaseUrl: z.string().optional(),
499
+ demoMode: z.boolean().optional(),
500
+ native: z
501
+ .object({
502
+ enabled: z.boolean().optional(),
503
+ })
504
+ .optional(),
505
+ email: z
506
+ .object({
507
+ provider: z.string().optional(),
508
+ from: z.string().optional(),
509
+ connectionString: z.string().optional(),
510
+ })
511
+ .optional(),
512
+ providers: z
513
+ .array(
514
+ z.object({
515
+ id: z.string().min(1),
516
+ displayName: z.string().optional(),
517
+ authority: z.string().optional(),
518
+ clientId: z.string().optional(),
519
+ clientSecret: z.string().optional(),
520
+ scopes: z.string().optional(),
521
+ }),
522
+ )
523
+ .optional(),
524
+ redirectToAfterSignUp: z.string().optional(),
525
+ redirectToAfterSignIn: z.string().optional(),
526
+ redirectToAfterSignOut: z.string().optional(),
527
+ }),
528
+ ]);
529
+
530
+ const MetadataSchema = z
531
+ .object({
532
+ title: z.string(),
533
+ defaultTitle: z.string().optional(),
534
+ description: z.string(),
535
+ logo: z.string(),
536
+ favicon: z.string(),
537
+ generator: z.string(),
538
+ applicationName: z.string(),
539
+ referrer: z.string(),
540
+ keywords: z.array(z.string()),
541
+ authors: z.array(z.string()),
542
+ creator: z.string(),
543
+ publisher: z.string(),
544
+ })
545
+ .partial();
546
+
547
+ const FontConfigSchema = z.union([
548
+ z.enum(GOOGLE_FONTS),
549
+ z.object({
550
+ url: z.string(),
551
+ fontFamily: z.string().optional(),
552
+ }),
553
+ ]);
554
+
555
+ export type FontConfig = z.infer<typeof FontConfigSchema>;
556
+
557
+ const FontsConfigSchema = z.object({
558
+ sans: FontConfigSchema.optional(),
559
+ serif: FontConfigSchema.optional(),
560
+ mono: FontConfigSchema.optional(),
561
+ });
562
+
563
+ const CssObject = z.record(
564
+ z.string(),
565
+ z.lazy(() =>
566
+ z.union([
567
+ z.string(),
568
+ z.record(
569
+ z.string(),
570
+ z.union([z.string(), z.record(z.string(), z.string())]),
571
+ ),
572
+ ]),
573
+ ),
574
+ );
575
+
576
+ const ThemeConfigSchema = z.object({
577
+ registryUrl: z.string().url().optional(),
578
+ customCss: z.union([z.string(), CssObject]).optional(),
579
+ light: ThemeSchema.optional(),
580
+ dark: ThemeSchema.optional(),
581
+ fonts: FontsConfigSchema.optional(),
582
+ noDefaultTheme: z.boolean().optional(),
583
+ });
584
+
585
+ const SiteSchema = z
586
+ .object({
587
+ title: z.string(),
588
+ showTitleInHeader: z.boolean().optional(),
589
+ logoUrl: z.string(),
590
+ dir: z.enum(["ltr", "rtl"]).optional(),
591
+ logo: LogoSchema,
592
+ banner: z.object({
593
+ message: z.custom<NonNullable<ReactNode>>(),
594
+ color: z
595
+ .custom<"note" | "tip" | "info" | "caution" | "danger" | (string & {})>(
596
+ (val) => typeof val === "string",
597
+ )
598
+ .optional(),
599
+ dismissible: z.boolean().optional(),
600
+ }),
601
+ footer: FooterSchema,
602
+ })
603
+ .partial();
604
+
605
+ const PlacementPosition = z.enum(["start", "center", "end"]);
606
+
607
+ const HeaderConfigSchema = z
608
+ .object({
609
+ navigation: HeaderNavigationSchema,
610
+ placements: z
611
+ .object({
612
+ navigation: PlacementPosition,
613
+ search: PlacementPosition,
614
+ auth: z.enum(["start", "center", "end", "navigation"]),
615
+ })
616
+ .partial()
617
+ .optional(),
618
+ })
619
+ .partial();
620
+
621
+ const ApiCatalogSchema = z.object({
622
+ path: z.string(),
623
+ label: z.string(),
624
+ items: z.array(z.string()).optional(),
625
+ filterItems: z
626
+ .custom<FilterCatalogItemsFn>((val) => typeof val === "function")
627
+ .optional(),
628
+ });
629
+
630
+ export const CdnUrlSchema = z
631
+ .union([
632
+ z.string(),
633
+ z.object({
634
+ base: z.string().optional(),
635
+ media: z.string().optional(),
636
+ }),
637
+ ])
638
+ .transform((val) => {
639
+ if (typeof val === "string") {
640
+ return { base: val, media: val };
641
+ }
642
+ return { base: val.base, media: val.media };
643
+ })
644
+ .optional();
645
+
646
+ const BaseConfigSchema = z.object({
647
+ slots: z.record(z.string(), z.custom<SlotType>()),
648
+ /**
649
+ * @deprecated Use `slots` instead
650
+ */
651
+ UNSAFE_slotlets: z.record(z.string(), z.custom<SlotType>()),
652
+ mdx: z
653
+ .object({
654
+ components: z.custom<MdxComponentsType>(),
655
+ })
656
+ .partial(),
657
+ customPages: z.array(
658
+ z.object({
659
+ path: z.string(),
660
+ element: z.custom<NonNullable<ReactNode>>().optional(),
661
+ render: z.custom<ComponentType<ExposedComponentProps>>().optional(),
662
+ prose: z.boolean().optional(),
663
+ }),
664
+ ),
665
+ plugins: z.array(z.custom<ApitogoPlugin>()),
666
+ build: z.custom<{
667
+ remarkPlugins?: Options["remarkPlugins"];
668
+ rehypePlugins?: Options["rehypePlugins"];
669
+ }>(),
670
+ protectedRoutes: ProtectedRoutesSchema,
671
+ basePath: z.string().optional(),
672
+ canonicalUrlOrigin: z.string().optional(),
673
+ cdnUrl: CdnUrlSchema.optional(),
674
+ port: z.number().optional(),
675
+ https: z
676
+ .object({
677
+ key: z.string(),
678
+ cert: z.string(),
679
+ ca: z.string().optional(),
680
+ })
681
+ .optional(),
682
+ site: SiteSchema,
683
+ header: HeaderConfigSchema.optional(),
684
+ navigation: InputNavigationSchema,
685
+ navigationRules: NavigationRulesSchema.optional(),
686
+ theme: ThemeConfigSchema,
687
+ syntaxHighlighting: z
688
+ .object({
689
+ languages: z.array(z.custom<BundledLanguage>()),
690
+ themes: z.object({
691
+ light: z.custom<BundledTheme>(),
692
+ dark: z.custom<BundledTheme>(),
693
+ }),
694
+ })
695
+ .partial()
696
+ .optional(),
697
+ metadata: MetadataSchema,
698
+ authentication: AuthenticationSchema,
699
+ search: SearchSchema,
700
+ docs: DocsConfigSchema.optional(),
701
+ modules: ModulesSchema,
702
+ apis: z.union([ApiSchema, z.array(ApiSchema)]),
703
+ catalogs: z.union([ApiCatalogSchema, z.array(ApiCatalogSchema)]),
704
+ apiKeys: ApiKeysSchema,
705
+ aiAssistants: AiAssistantsSchema,
706
+ redirects: z.array(Redirect),
707
+ sitemap: SiteMapSchema,
708
+ enableStatusPages: z.boolean().optional(),
709
+ defaults: z.object({
710
+ apis: ApiOptionsSchema,
711
+ /**
712
+ * @deprecated Use `apis.examplesLanguage` or `defaults.apis.examplesLanguage` instead
713
+ */
714
+ examplesLanguage: z.string().optional(),
715
+ }),
716
+ // Internal: populated by plugins via `transformConfig` to track plugin directories
717
+ __pluginDirs: z.array(z.string()),
718
+ });
719
+
720
+ export const ZudokuConfig = BaseConfigSchema.partial();
721
+
722
+ export type ZudokuApiConfig = z.infer<typeof ApiSchema>;
723
+ export type VersionConfig = z.infer<typeof VersionConfigSchema>;
724
+ export type ZudokuSiteMapConfig = z.infer<typeof SiteMapSchema>;
725
+ export type ZudokuDocsConfig = z.infer<typeof DocsConfigSchema>;
726
+ export type { ModulesConfig } from "./ModulesSchema.js";
727
+ export type {
728
+ ResolvedModulesConfig,
729
+ DocsModuleConfig,
730
+ LandingModuleConfig,
731
+ LandingContentConfig,
732
+ LandingFeatureConfig,
733
+ LandingLinkConfig,
734
+ ResolvedLandingContent,
735
+ ResolvedDashboardContent,
736
+ ResolvedProfileContent,
737
+ ResolvedPlansContent,
738
+ ResolvedLandingModule,
739
+ ResolvedUserPanelModule,
740
+ ResolvedDashboardModule,
741
+ ResolvedUserManagementModule,
742
+ ResolvedApiKeysModule,
743
+ ResolvedPlansModule,
744
+ UserPanelModuleConfig,
745
+ DashboardModuleConfig,
746
+ UserManagementModuleConfig,
747
+ ApiKeysModuleConfig,
748
+ PlansModuleConfig,
749
+ DashboardContentConfig,
750
+ ProfileContentConfig,
751
+ PlansContentConfig,
752
+ PlanTierConfig,
753
+ } from "./ModulesSchema.js";
754
+ export type ZudokuLlmsConfig = z.infer<typeof LlmsConfigSchema>;
755
+ export type ZudokuRedirect = z.infer<typeof Redirect>;
756
+ export type AuthenticationConfig = z.infer<typeof AuthenticationSchema>;
757
+
758
+ // Use `z.input` type for flexibility with transforms,
759
+ // but override navigation with `z.infer` for strict validation
760
+ type BaseZudokuConfig = z.input<typeof ZudokuConfig>;
761
+ export type ZudokuConfig = Omit<
762
+ BaseZudokuConfig,
763
+ "header" | "navigation" | "navigationRules"
764
+ > & {
765
+ header?: {
766
+ navigation?: z.infer<typeof HeaderNavigationSchema>;
767
+ placements?: {
768
+ navigation?: "start" | "center" | "end";
769
+ search?: "start" | "center" | "end";
770
+ auth?: "start" | "center" | "end" | "navigation";
771
+ };
772
+ };
773
+ navigation?: z.infer<typeof InputNavigationSchema>;
774
+ navigationRules?: z.infer<typeof NavigationRulesSchema>;
775
+ };
776
+
777
+ export function validateConfig(config: unknown, configPath?: string) {
778
+ const validationResult = ZudokuConfig.safeParse(config);
779
+
780
+ if (!validationResult.success) {
781
+ // In production (build mode), throw an error to fail the build
782
+ if (process.env.NODE_ENV === "production") {
783
+ throw new Error(
784
+ `Whoops, looks like there's an issue with your ${configPath ?? "config"}:\n${z.prettifyError(validationResult.error)}`,
785
+ );
786
+ }
787
+
788
+ // In development mode, log warnings but don't fail
789
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
790
+ console.log(colors.yellow("Validation errors:"));
791
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
792
+ console.log(colors.yellow(z.prettifyError(validationResult.error)));
793
+ }
794
+ }