@lukoweb/apitogo 0.1.53 → 0.1.55

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 (47) hide show
  1. package/dist/cli/cli.js +686 -373
  2. package/dist/declarations/config/apitogo-config-core.d.ts +14 -4
  3. package/dist/declarations/config/landing-manifest.d.ts +76 -0
  4. package/dist/declarations/config/local-manifest.d.ts +91 -1
  5. package/dist/declarations/config/validators/ModulesSchema.d.ts +227 -0
  6. package/dist/declarations/config/validators/ZudokuConfig.d.ts +84 -2
  7. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  8. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +15 -0
  9. package/dist/declarations/lib/authentication/providers/dev-portal-constants.d.ts +19 -1
  10. package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +5 -1
  11. package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +5 -0
  12. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  13. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  14. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  15. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  16. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  17. package/dist/flat-config.d.ts +61 -1
  18. package/package.json +1 -1
  19. package/src/app/main.css +2 -2
  20. package/src/config/apitogo-config-core.ts +140 -11
  21. package/src/config/apitogo-config-loader.browser.ts +17 -0
  22. package/src/config/apitogo-config-loader.node.ts +28 -8
  23. package/src/config/build-apitogo-config.ts +13 -4
  24. package/src/config/landing-manifest.ts +11 -0
  25. package/src/config/landing-openapi-showcase.ts +115 -0
  26. package/src/config/loader.ts +17 -3
  27. package/src/config/local-manifest.ts +146 -11
  28. package/src/config/resolve-modules.ts +22 -1
  29. package/src/config/validators/ModulesSchema.ts +84 -0
  30. package/src/config/validators/ZudokuConfig.ts +26 -1
  31. package/src/lib/authentication/auth-header-nav.ts +5 -15
  32. package/src/lib/authentication/header-nav-filter.ts +36 -0
  33. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +499 -0
  34. package/src/lib/authentication/providers/dev-portal-constants.ts +15 -1
  35. package/src/lib/authentication/providers/dev-portal-utils.ts +128 -3
  36. package/src/lib/authentication/providers/dev-portal.tsx +51 -6
  37. package/src/lib/components/Header.tsx +49 -39
  38. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  39. package/src/lib/components/HeaderNavigation.tsx +11 -9
  40. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  41. package/src/lib/components/SiteTitle.tsx +20 -0
  42. package/src/lib/components/ThemeSwitch.tsx +36 -2
  43. package/src/lib/components/TopNavigation.tsx +20 -33
  44. package/src/lib/core/ZudokuContext.ts +1 -0
  45. package/src/vite/config.ts +21 -0
  46. package/src/vite/plugin-auth.ts +4 -1
  47. package/src/vite/plugin-config.ts +39 -4
@@ -0,0 +1,15 @@
1
+ export declare function DevPortalLoginPage({ apiBaseUrl }: {
2
+ apiBaseUrl?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
4
+ export declare function DevPortalRegisterPage({ apiBaseUrl }: {
5
+ apiBaseUrl?: string;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ export declare function DevPortalVerifyEmailPage({ apiBaseUrl, }: {
8
+ apiBaseUrl?: string;
9
+ }): import("react/jsx-runtime").JSX.Element;
10
+ export declare function DevPortalForgotPasswordPage({ apiBaseUrl, }: {
11
+ apiBaseUrl?: string;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ export declare function DevPortalResetPasswordPage({ apiBaseUrl, }: {
14
+ apiBaseUrl?: string;
15
+ }): import("react/jsx-runtime").JSX.Element;
@@ -14,5 +14,23 @@ export type EndUserMeResponse = {
14
14
  };
15
15
  export type DevPortalAuthStatusResponse = {
16
16
  available: boolean;
17
- oidcConfigured: boolean;
17
+ nativeEnabled?: boolean;
18
+ oidcConfigured?: boolean;
19
+ providerCount?: number;
20
+ };
21
+ export type DevPortalAuthConfigResponse = {
22
+ native: {
23
+ enabled: boolean;
24
+ };
25
+ email?: {
26
+ provider: string;
27
+ from: string;
28
+ } | null;
29
+ providers: Array<{
30
+ id: string;
31
+ displayName: string;
32
+ authority: string;
33
+ clientId: string;
34
+ scopes: string[];
35
+ }>;
18
36
  };
@@ -3,7 +3,11 @@ import { type EndUserMeResponse } from "./dev-portal-constants.js";
3
3
  export declare function resolveDevPortalApiBaseUrl(config: Pick<DevPortalAuthenticationConfig, "apiBaseUrl">): string | undefined;
4
4
  export declare function isPlaceholderDevPortalApiUrl(apiBaseUrl: string | undefined): boolean;
5
5
  export declare function toAbsoluteReturnUrl(pathOrUrl: string): string;
6
- export declare function buildDevPortalLoginUrl(apiBaseUrl: string, returnUrl: string): string;
6
+ export declare function buildDevPortalOidcChallengeUrl(apiBaseUrl: string, providerId: string, returnUrl: string): string;
7
7
  export declare function buildDevPortalLogoutUrl(apiBaseUrl: string, returnUrl: string): string;
8
+ export declare function normalizeDevPortalAuthConfig(raw: unknown): import("./dev-portal-constants.js").DevPortalAuthConfigResponse;
9
+ export declare function isNativeAuthEnabled(config: import("./dev-portal-constants.js").DevPortalAuthConfigResponse | null | undefined): boolean;
10
+ export declare function fetchDevPortalAuthConfig(apiBaseUrl: string, fetchImpl?: typeof fetch): Promise<import("./dev-portal-constants.js").DevPortalAuthConfigResponse>;
11
+ export declare function postDevPortalAuth<T>(apiBaseUrl: string, path: string, body: unknown, fetchImpl?: typeof fetch): Promise<T>;
8
12
  export declare function probeDevPortalBackend(apiBaseUrl: string, fetchImpl?: typeof fetch): Promise<boolean>;
9
13
  export declare function mapEndUserMeToProfile(me: EndUserMeResponse): import("../state.js").UserProfile;
@@ -21,7 +21,12 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
21
21
  private backendAvailable;
22
22
  private refreshGeneration;
23
23
  private refreshInFlight;
24
+ private authConfigNativeEnabled;
24
25
  constructor({ apiBaseUrl, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
26
+ getRoutes(): {
27
+ path: string;
28
+ element: import("react/jsx-runtime").JSX.Element;
29
+ }[];
25
30
  initialize(_context: ZudokuContext): Promise<void>;
26
31
  onPageLoad: () => Promise<void>;
27
32
  private syncBackendAvailability;
@@ -0,0 +1,3 @@
1
+ export declare const HeaderAuthActions: ({ className }: {
2
+ className?: string;
3
+ }) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,6 @@
1
+ type SiteTitleProps = {
2
+ title: string;
3
+ className?: string;
4
+ };
5
+ export declare const SiteTitle: ({ title, className }: SiteTitleProps) => import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -1 +1,3 @@
1
- export declare const ThemeSwitch: () => import("react/jsx-runtime").JSX.Element;
1
+ export declare const ThemeSwitch: ({ variant, }: {
2
+ variant?: "default" | "compact";
3
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { type NavLinkProps } from "react-router";
2
2
  import type { NavigationItem } from "../../config/validators/NavigationSchema.js";
3
- export declare const TopNavigation: () => import("react/jsx-runtime").JSX.Element;
3
+ export declare const TopNavigation: () => import("react/jsx-runtime").JSX.Element | null;
4
4
  export declare const TopNavLink: ({ isActive, children, ...props }: {
5
5
  isActive?: boolean;
6
6
  children: React.ReactNode;
@@ -50,6 +50,7 @@ type Site = Partial<{
50
50
  dir?: "ltr" | "rtl";
51
51
  showPoweredBy?: boolean;
52
52
  title?: string;
53
+ showTitleInHeader?: boolean;
53
54
  logo?: {
54
55
  src: {
55
56
  light: string;
@@ -137,6 +137,7 @@ export interface FlatZudokuConfig {
137
137
  }
138
138
  site?: {
139
139
  title?: string
140
+ showTitleInHeader?: boolean
140
141
  logoUrl?: string
141
142
  dir?: ("ltr" | "rtl")
142
143
  logo?: _Schema0
@@ -319,8 +320,24 @@ export interface FlatZudokuConfig {
319
320
  redirectToAfterSignIn?: string
320
321
  redirectToAfterSignOut?: string
321
322
  } | {
322
- type: "dev-portal"
323
+ type?: "dev-portal"
323
324
  apiBaseUrl?: string
325
+ native?: {
326
+ enabled?: boolean
327
+ }
328
+ email?: {
329
+ provider?: string
330
+ from?: string
331
+ connectionString?: string
332
+ }
333
+ providers?: {
334
+ id: string
335
+ displayName?: string
336
+ authority?: string
337
+ clientId?: string
338
+ clientSecret?: string
339
+ scopes?: string
340
+ }[]
324
341
  redirectToAfterSignUp?: string
325
342
  redirectToAfterSignIn?: string
326
343
  redirectToAfterSignOut?: string
@@ -517,13 +534,56 @@ export interface _Schema21 {
517
534
  title?: string
518
535
  subtitle?: string
519
536
  description?: string
537
+ badge?: string
538
+ highlights?: string[]
539
+ codeExample?: {
540
+ filename?: string
541
+ language?: string
542
+ code: string
543
+ }
520
544
  }
521
545
  features?: {
522
546
  title: string
523
547
  description: string
548
+ icon?: string
524
549
  }[]
525
550
  primaryAction?: _Schema22
526
551
  secondaryAction?: _Schema22
552
+ ticker?: {
553
+ enabled?: boolean
554
+ items?: {
555
+ label: string
556
+ value: string
557
+ change?: string
558
+ }[]
559
+ }
560
+ trust?: {
561
+ enabled?: boolean
562
+ title?: string
563
+ logos?: string[]
564
+ }
565
+ apiShowcase?: {
566
+ enabled?: boolean
567
+ title?: string
568
+ subtitle?: string
569
+ browseAllHref?: string
570
+ browseAllLabel?: string
571
+ autoFromOpenApi?: boolean
572
+ items?: {
573
+ name: string
574
+ path: string
575
+ icon?: string
576
+ latency?: string
577
+ summary?: string
578
+ }[]
579
+ }
580
+ pricingCta?: {
581
+ enabled?: boolean
582
+ title?: string
583
+ description?: string
584
+ primaryAction?: _Schema22
585
+ secondaryAction?: _Schema22
586
+ }
527
587
  showPoweredBy?: boolean
528
588
  }
529
589
  export interface _Schema22 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
package/src/app/main.css CHANGED
@@ -102,8 +102,8 @@
102
102
 
103
103
  @layer base {
104
104
  :root {
105
- --top-header-height: 65px;
106
- --top-nav-height: 50px;
105
+ --top-header-height: 64px;
106
+ --top-nav-height: 0px;
107
107
  --banner-height: 35px;
108
108
  --header-height: calc(
109
109
  var(--top-header-height) + var(--top-nav-height) + var(--banner-height)
@@ -12,6 +12,7 @@ const DERIVED_MANIFEST_KEYS = [
12
12
  "backend",
13
13
  "gateway",
14
14
  "projectId",
15
+ "landing",
15
16
  ] as const;
16
17
 
17
18
  const SITE_CONFIG_KEYS = [
@@ -34,12 +35,20 @@ const SITE_CONFIG_KEYS = [
34
35
 
35
36
  export type ApitogoJsonConfig = Record<string, unknown>;
36
37
 
38
+ export type AuthProviderPublicConfig = {
39
+ id: string;
40
+ displayName?: string;
41
+ authority?: string;
42
+ clientId?: string;
43
+ scopes?: string;
44
+ };
45
+
37
46
  export type AuthenticationSecrets = {
38
- oidc?: {
39
- authority?: string;
40
- clientId?: string;
41
- clientSecret?: string;
47
+ email?: {
48
+ connectionString?: string;
42
49
  };
50
+ /** Provider secrets keyed by provider `id` (from env: `authentication__providers__{id}__clientSecret`). */
51
+ providers?: Record<string, { clientSecret?: string }>;
43
52
  stripe?: {
44
53
  secretKey?: string;
45
54
  webhookSecret?: string;
@@ -351,6 +360,7 @@ export function normalizeApitogoConfig(
351
360
 
352
361
  migratePlanCatalog(normalized);
353
362
  syncBillingEnabled(normalized);
363
+ syncAuthentication(normalized);
354
364
 
355
365
  return normalized;
356
366
  }
@@ -364,6 +374,119 @@ export function isBillingEnabled(config: ApitogoJsonConfig): boolean {
364
374
  return plans?.enabled === true;
365
375
  }
366
376
 
377
+ function isUserPanelEnabled(normalized: ApitogoJsonConfig): boolean {
378
+ const modules = asRecord(normalized.modules);
379
+ const userPanel = asRecord(modules?.userPanel);
380
+ if (userPanel?.enabled === true) {
381
+ return true;
382
+ }
383
+
384
+ const dashboard = asRecord(userPanel?.dashboard);
385
+ const userManagement = asRecord(userPanel?.userManagement);
386
+ const apiKeys = asRecord(userPanel?.apiKeys);
387
+ const plans = asRecord(userPanel?.plans);
388
+
389
+ return (
390
+ dashboard?.enabled === true ||
391
+ userManagement?.enabled === true ||
392
+ apiKeys?.enabled === true ||
393
+ plans?.enabled === true
394
+ );
395
+ }
396
+
397
+ /** Read public OIDC provider entries from `authentication.providers` array. */
398
+ export function readAuthProviders(
399
+ config: ApitogoJsonConfig,
400
+ ): AuthProviderPublicConfig[] {
401
+ const normalized = normalizeApitogoConfig(config);
402
+ const authentication = asRecord(normalized.authentication);
403
+ const providers = authentication?.providers;
404
+ if (!Array.isArray(providers)) {
405
+ return [];
406
+ }
407
+
408
+ return providers
409
+ .map((entry) => asRecord(entry))
410
+ .filter((entry): entry is Record<string, unknown> => entry !== undefined)
411
+ .filter((entry) => typeof entry.id === "string" && entry.id.trim())
412
+ .map((entry) => ({
413
+ id: String(entry.id).trim(),
414
+ displayName:
415
+ typeof entry.displayName === "string" ? entry.displayName : undefined,
416
+ authority:
417
+ typeof entry.authority === "string" ? entry.authority : undefined,
418
+ clientId: typeof entry.clientId === "string" ? entry.clientId : undefined,
419
+ scopes: typeof entry.scopes === "string" ? entry.scopes : undefined,
420
+ }));
421
+ }
422
+
423
+ export function isNativeAuthEnabled(config: ApitogoJsonConfig): boolean {
424
+ const normalized = normalizeApitogoConfig(config);
425
+ if (!isUserPanelEnabled(normalized)) {
426
+ return false;
427
+ }
428
+
429
+ const native = asRecord(asRecord(normalized.authentication)?.native);
430
+ return native?.enabled !== false;
431
+ }
432
+
433
+ function syncAuthentication(normalized: ApitogoJsonConfig): void {
434
+ const userPanelEnabled = isUserPanelEnabled(normalized);
435
+ const authentication = asRecord(normalized.authentication);
436
+
437
+ if (!userPanelEnabled && !authentication) {
438
+ return;
439
+ }
440
+
441
+ const auth = authentication ?? {};
442
+ auth.enabled = userPanelEnabled;
443
+ auth.type = "dev-portal";
444
+
445
+ if (userPanelEnabled) {
446
+ const native = asRecord(auth.native) ?? {};
447
+ if (native.enabled === undefined) {
448
+ native.enabled = true;
449
+ }
450
+ auth.native = native;
451
+
452
+ if (!Array.isArray(auth.providers)) {
453
+ auth.providers = [];
454
+ }
455
+ }
456
+
457
+ normalized.authentication = auth;
458
+ }
459
+
460
+ function stripAuthenticationSecrets(
461
+ authentication: Record<string, unknown>,
462
+ ): Record<string, unknown> {
463
+ const {
464
+ gateway: _gateway,
465
+ oidc: _oidc,
466
+ stripe: _stripe,
467
+ type: _type,
468
+ email,
469
+ providers,
470
+ ...authPublic
471
+ } = authentication;
472
+
473
+ const emailRecord = asRecord(email);
474
+ if (emailRecord) {
475
+ const { connectionString: _connectionString, ...emailPublic } = emailRecord;
476
+ authPublic.email = emailPublic;
477
+ }
478
+
479
+ if (Array.isArray(providers)) {
480
+ authPublic.providers = providers.map((entry) => {
481
+ const provider = asRecord(entry) ?? {};
482
+ const { clientSecret: _clientSecret, ...providerPublic } = provider;
483
+ return providerPublic;
484
+ });
485
+ }
486
+
487
+ return authPublic;
488
+ }
489
+
367
490
  function syncBillingEnabled(normalized: ApitogoJsonConfig): void {
368
491
  const legacyPricing = asRecord(normalized.pricing);
369
492
  const modules = ensureModules(normalized);
@@ -400,13 +523,7 @@ export function deriveManifestFields(
400
523
  if (key === "authentication") {
401
524
  const authentication = asRecord(normalized.authentication);
402
525
  if (authentication) {
403
- const {
404
- gateway: _gateway,
405
- oidc: _oidc,
406
- stripe: _stripe,
407
- ...authPublic
408
- } = authentication;
409
- manifest.authentication = authPublic;
526
+ manifest.authentication = stripAuthenticationSecrets(authentication);
410
527
  }
411
528
  continue;
412
529
  }
@@ -426,6 +543,18 @@ export function deriveManifestFields(
426
543
  }
427
544
  continue;
428
545
  }
546
+ if (key === "landing") {
547
+ const modules = asRecord(normalized.modules);
548
+ const landing = asRecord(modules?.landing);
549
+ if (landing?.content || landing?.enabled !== undefined) {
550
+ manifest.landing = {
551
+ enabled:
552
+ typeof landing.enabled === "boolean" ? landing.enabled : undefined,
553
+ content: landing.content,
554
+ };
555
+ }
556
+ continue;
557
+ }
429
558
  if (normalized[key] !== undefined) {
430
559
  manifest[key] = normalized[key];
431
560
  }
@@ -0,0 +1,17 @@
1
+ import type {
2
+ ApitogoJsonConfig,
3
+ AuthenticationSecrets,
4
+ } from "./apitogo-config-core.js";
5
+
6
+ /** Browser stub — JSON config is embedded by the Vite config plugin on the client. */
7
+ export function loadApitogoConfig(_rootDir?: string): ApitogoJsonConfig {
8
+ return {};
9
+ }
10
+
11
+ export function readAuthenticationSecrets(): AuthenticationSecrets {
12
+ return {};
13
+ }
14
+
15
+ export function apitogoConfigPath(rootDir: string): string {
16
+ return rootDir;
17
+ }
@@ -36,19 +36,39 @@ export function readAuthenticationSecrets(
36
36
  ): AuthenticationSecrets {
37
37
  const overrides = applyEnvOverrides({}, env);
38
38
  const authentication = asRecord(overrides.authentication);
39
- const oidc = asRecord(authentication?.oidc);
39
+ const email = asRecord(authentication?.email);
40
+ const providers = asRecord(authentication?.providers);
40
41
  const stripe = asRecord(authentication?.stripe);
41
42
 
42
43
  const secrets: AuthenticationSecrets = {};
43
- if (oidc) {
44
- secrets.oidc = {
45
- authority:
46
- typeof oidc.authority === "string" ? oidc.authority : undefined,
47
- clientId: typeof oidc.clientId === "string" ? oidc.clientId : undefined,
48
- clientSecret:
49
- typeof oidc.clientSecret === "string" ? oidc.clientSecret : undefined,
44
+
45
+ if (email?.connectionString) {
46
+ secrets.email = {
47
+ connectionString:
48
+ typeof email.connectionString === "string"
49
+ ? email.connectionString
50
+ : undefined,
50
51
  };
51
52
  }
53
+
54
+ if (providers) {
55
+ const providerSecrets: Record<string, { clientSecret?: string }> = {};
56
+ for (const [id, value] of Object.entries(providers)) {
57
+ const provider = asRecord(value);
58
+ if (provider?.clientSecret) {
59
+ providerSecrets[id] = {
60
+ clientSecret:
61
+ typeof provider.clientSecret === "string"
62
+ ? provider.clientSecret
63
+ : undefined,
64
+ };
65
+ }
66
+ }
67
+ if (Object.keys(providerSecrets).length > 0) {
68
+ secrets.providers = providerSecrets;
69
+ }
70
+ }
71
+
52
72
  if (stripe) {
53
73
  secrets.stripe = {
54
74
  secretKey:
@@ -11,6 +11,13 @@ type BuildApitogoConfigOptions = Partial<ApitogoConfig> &
11
11
  plugins?: ApitogoPlugin[];
12
12
  };
13
13
 
14
+ function mergeOptionalRecord<T extends Record<string, unknown>>(
15
+ ...records: Array<T | undefined>
16
+ ): T | undefined {
17
+ const merged = Object.assign({}, ...records.filter(Boolean)) as T;
18
+ return Object.keys(merged).length > 0 ? merged : undefined;
19
+ }
20
+
14
21
  export function buildApitogoConfig({
15
22
  plugins,
16
23
  plans: _legacyPlans,
@@ -24,14 +31,16 @@ export function buildApitogoConfig({
24
31
  const { plans: _rootPlans, ...restOverrides } =
25
32
  jsonOverrides as ApitogoJsonConfig;
26
33
 
34
+ const authentication = mergeOptionalRecord(
35
+ siteConfig.authentication as ApitogoConfig["authentication"],
36
+ jsonOverrides.authentication as ApitogoConfig["authentication"],
37
+ );
38
+
27
39
  return {
28
40
  ...siteConfig,
29
41
  ...restOverrides,
30
42
  plugins,
31
- authentication: {
32
- ...(siteConfig.authentication as ApitogoConfig["authentication"]),
33
- ...jsonOverrides.authentication,
34
- },
43
+ ...(authentication ? { authentication } : {}),
35
44
  site: {
36
45
  ...(siteConfig.site as ApitogoConfig["site"]),
37
46
  ...jsonOverrides.site,
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { LandingContentSchema } from "./validators/ModulesSchema.js";
3
+
4
+ export const LandingManifestContentSchema = LandingContentSchema;
5
+
6
+ export type LandingManifestContent = z.infer<typeof LandingContentSchema>;
7
+
8
+ export type LandingManifestInput = {
9
+ enabled?: boolean;
10
+ content?: LandingManifestContent;
11
+ };
@@ -0,0 +1,115 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { LandingApiShowcaseItemConfig } from "./validators/ModulesSchema.js";
4
+ import type { ZudokuConfig } from "./validators/ZudokuConfig.js";
5
+
6
+ const HTTP_METHODS = [
7
+ "get",
8
+ "post",
9
+ "put",
10
+ "patch",
11
+ "delete",
12
+ "head",
13
+ "options",
14
+ ] as const;
15
+
16
+ type OpenApiDocument = {
17
+ paths?: Record<
18
+ string,
19
+ Partial<Record<(typeof HTTP_METHODS)[number], { summary?: string }>>
20
+ >;
21
+ };
22
+
23
+ function readOpenApiDocument(
24
+ rootDir: string,
25
+ openApiPath: string,
26
+ ): OpenApiDocument | null {
27
+ try {
28
+ const absolutePath = path.isAbsolute(openApiPath)
29
+ ? openApiPath
30
+ : path.resolve(rootDir, openApiPath);
31
+ const raw = readFileSync(absolutePath, "utf8");
32
+ return JSON.parse(raw) as OpenApiDocument;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function resolveOpenApiPath(config: ZudokuConfig): string | undefined {
39
+ const backend = (config as { backend?: { openApiPath?: string } }).backend;
40
+ if (backend?.openApiPath) {
41
+ return backend.openApiPath;
42
+ }
43
+
44
+ const apis = config.apis;
45
+ if (!apis) {
46
+ return undefined;
47
+ }
48
+
49
+ const entries = Array.isArray(apis) ? apis : [apis];
50
+ for (const entry of entries) {
51
+ if (typeof entry === "object" && entry !== null && "type" in entry) {
52
+ const api = entry as { type?: string; input?: string };
53
+ if (api.type === "file" && typeof api.input === "string") {
54
+ return api.input;
55
+ }
56
+ }
57
+ }
58
+
59
+ return undefined;
60
+ }
61
+
62
+ export function extractApiShowcaseItemsFromOpenApi(
63
+ config: ZudokuConfig,
64
+ rootDir: string,
65
+ limit = 8,
66
+ ): LandingApiShowcaseItemConfig[] {
67
+ const openApiPath = resolveOpenApiPath(config);
68
+ if (!openApiPath) {
69
+ return [];
70
+ }
71
+
72
+ const document = readOpenApiDocument(rootDir, openApiPath);
73
+ if (!document?.paths) {
74
+ return [];
75
+ }
76
+
77
+ const items: LandingApiShowcaseItemConfig[] = [];
78
+
79
+ for (const [routePath, methods] of Object.entries(document.paths)) {
80
+ for (const method of HTTP_METHODS) {
81
+ const operation = methods?.[method];
82
+ if (!operation) {
83
+ continue;
84
+ }
85
+
86
+ items.push({
87
+ name: operation.summary ?? `${method.toUpperCase()} ${routePath}`,
88
+ path: routePath,
89
+ summary: operation.summary,
90
+ });
91
+
92
+ if (items.length >= limit) {
93
+ return items;
94
+ }
95
+ }
96
+ }
97
+
98
+ return items;
99
+ }
100
+
101
+ export function enrichResolvedLandingApiShowcase(
102
+ config: ZudokuConfig,
103
+ resolved: { landing: { content: { apiShowcase?: { autoFromOpenApi?: boolean; items?: LandingApiShowcaseItemConfig[] } } } },
104
+ rootDir: string,
105
+ ): void {
106
+ const showcase = resolved.landing.content.apiShowcase;
107
+ if (
108
+ !showcase?.autoFromOpenApi ||
109
+ (showcase.items && showcase.items.length > 0)
110
+ ) {
111
+ return;
112
+ }
113
+
114
+ showcase.items = extractApiShowcaseItemsFromOpenApi(config, rootDir);
115
+ }
@@ -18,6 +18,7 @@ import {
18
18
  loadApitogoConfig,
19
19
  readPlanCatalogItems,
20
20
  } from "./apitogo-config-loader.js";
21
+ import { buildApitogoConfig } from "./build-apitogo-config.js";
21
22
  import { fileExists } from "./file-exists.js";
22
23
  import {
23
24
  isAuthenticationEnabled,
@@ -117,7 +118,18 @@ async function loadZudokuConfigWithMeta(
117
118
  },
118
119
  });
119
120
 
120
- const config = module.default;
121
+ const importedConfig = module.default;
122
+
123
+ let config: ZudokuConfig;
124
+ try {
125
+ const fileConfig = loadApitogoConfig(rootDir);
126
+ config = buildApitogoConfig({
127
+ ...fileConfig,
128
+ plugins: importedConfig.plugins,
129
+ });
130
+ } catch {
131
+ config = importedConfig;
132
+ }
121
133
 
122
134
  validateConfig(config, configPath);
123
135
 
@@ -130,7 +142,7 @@ async function loadZudokuConfigWithMeta(
130
142
  dependencies,
131
143
  configPath,
132
144
  },
133
- __resolvedModules: resolveModulesConfig(config),
145
+ __resolvedModules: resolveModulesConfig(config, { rootDir }),
134
146
  };
135
147
 
136
148
  return configWithMetadata;
@@ -288,7 +300,9 @@ export async function loadZudokuConfig(
288
300
  ...transformedConfig.__meta,
289
301
  ...configWithManifestMeta.__meta,
290
302
  },
291
- __resolvedModules: resolveModulesConfig(transformedConfig),
303
+ __resolvedModules: resolveModulesConfig(transformedConfig, {
304
+ rootDir: transformedConfig.__meta.rootDir,
305
+ }),
292
306
  };
293
307
 
294
308
  if (!process.env.APITOGO_JSON_ONLY) {