@lukoweb/apitogo 0.2.56 → 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 (80) hide show
  1. package/dist/cli/cli.js +1026 -540
  2. package/dist/declarations/cli/platform/client/platform-api.d.ts +6 -0
  3. package/dist/declarations/config/validators/ZudokuConfig.d.ts +2 -0
  4. package/dist/declarations/lib/authentication/providers/openid.d.ts +2 -1
  5. package/dist/declarations/lib/plugins/markdown/index.d.ts +2 -1
  6. package/dist/flat-config.d.ts +1 -0
  7. package/docs/cli/commands.md +50 -32
  8. package/docs/cli/go-live.md +66 -93
  9. package/docs/cli/overview.md +18 -17
  10. package/docs/cli/setup.md +130 -141
  11. package/docs/cli/shape.md +94 -11
  12. package/docs/components/slot.mdx +116 -119
  13. package/docs/configuration/api-reference.md +98 -397
  14. package/docs/configuration/authentication.md +118 -159
  15. package/docs/configuration/footer.mdx +89 -118
  16. package/docs/configuration/make-config.mdx +2 -2
  17. package/docs/configuration/overview.md +97 -166
  18. package/docs/configuration/plans.md +100 -110
  19. package/docs/configuration/portal.md +116 -0
  20. package/docs/customization/colors-theme.mdx +255 -278
  21. package/docs/customization/fonts.md +105 -110
  22. package/docs/customization/landing-page.md +189 -188
  23. package/docs/deploy/apitogo.md +13 -17
  24. package/docs/deployment.md +15 -9
  25. package/docs/guides/stripe-setup.mdx +123 -0
  26. package/docs/markdown/frontmatter.md +172 -173
  27. package/docs/mcp/go-live.md +100 -100
  28. package/docs/mcp/overview.md +76 -76
  29. package/docs/quickstart.md +88 -72
  30. package/docs/writing.mdx +68 -72
  31. package/package.json +1 -1
  32. package/src/config/v2/docs-navigation.ts +7 -2
  33. package/src/config/v2/index.ts +1 -0
  34. package/src/config/v2/resolve.ts +28 -0
  35. package/src/config/v2/rules.ts +32 -1
  36. package/src/config/v2/schema.ts +9 -0
  37. package/src/config/v2/services.ts +78 -2
  38. package/src/config/v2/starter-landing.ts +41 -0
  39. package/src/config/v2/wrapper.ts +127 -7
  40. package/src/config/validators/ZudokuConfig.ts +794 -792
  41. package/src/lib/authentication/providers/openid.tsx +6 -1
  42. package/src/lib/plugins/markdown/index.tsx +86 -82
  43. package/src/vite/dev-server.ts +9 -1
  44. package/src/vite/dev-ssr-config.ts +32 -0
  45. package/src/vite/plugin-config.ts +4 -2
  46. package/src/vite/plugin-docs.ts +213 -205
  47. package/docs/concepts/auth-provider-api-identities.md +0 -105
  48. package/docs/configuration/ai-assistants.md +0 -65
  49. package/docs/configuration/api-catalog.md +0 -109
  50. package/docs/configuration/authentication-auth0.md +0 -181
  51. package/docs/configuration/authentication-azure-ad.md +0 -245
  52. package/docs/configuration/authentication-clerk.md +0 -117
  53. package/docs/configuration/authentication-firebase.md +0 -69
  54. package/docs/configuration/authentication-pingfederate.md +0 -143
  55. package/docs/configuration/authentication-supabase.md +0 -233
  56. package/docs/configuration/build-configuration.mdx +0 -147
  57. package/docs/configuration/docs.md +0 -282
  58. package/docs/configuration/llms.md +0 -90
  59. package/docs/configuration/manifest.md +0 -129
  60. package/docs/configuration/navigation.mdx +0 -415
  61. package/docs/configuration/protected-routes.md +0 -150
  62. package/docs/configuration/search.md +0 -164
  63. package/docs/configuration/sentry.mdx +0 -44
  64. package/docs/configuration/site.md +0 -124
  65. package/docs/configuration/slots.mdx +0 -125
  66. package/docs/configuration/vite-config.md +0 -18
  67. package/docs/custom-plugins.md +0 -285
  68. package/docs/deploy/apache-nginx.md +0 -41
  69. package/docs/deploy/cloudflare-pages.md +0 -74
  70. package/docs/deploy/direct-upload.md +0 -17
  71. package/docs/deploy/github-pages.md +0 -49
  72. package/docs/deploy/vercel.md +0 -101
  73. package/docs/extending/events.md +0 -124
  74. package/docs/guides/environment-variables.md +0 -94
  75. package/docs/guides/managing-api-keys-and-identities.md +0 -89
  76. package/docs/guides/navigation-rules.mdx +0 -203
  77. package/docs/guides/processors.mdx +0 -234
  78. package/docs/guides/transforming-examples.md +0 -156
  79. package/docs/guides/using-multiple-apis.md +0 -87
  80. 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
+ }