@lukoweb/apitogo 0.1.49 → 0.1.51

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.
@@ -1,68 +1,50 @@
1
1
  ---
2
- title: Manifest (apitogo.json)
2
+ title: Site configuration (apitogo.config.json)
3
3
  sidebar_icon: file-json
4
4
  ---
5
5
 
6
- Dev portal sites provisioned through APItoGo use JSON manifest files for plans, branding, backend
7
- paths, and gateway settings. Shared defaults live in **`apitogo.json`**; optional per-environment
8
- files override values without duplicating the full config.
6
+ Dev portal sites use a single JSON configuration file for site settings, plans,
7
+ branding, backend paths, and gateway metadata. Code-only pieces (plugins, custom
8
+ components) stay in `apitogo.config.tsx`.
9
9
 
10
10
  ## Files
11
11
 
12
- | File | When used | Purpose |
13
- | ---------------------------- | ---------------- | ------------------------------------------------------------------------- |
14
- | `apitogo.json` | Always (base) | Committed defaults: `siteName`, `branding`, `plans`, `backend`, `gateway` |
15
- | `apitogo.local.json` | `npm run dev` | Local-only overrides (optional) |
16
- | `apitogo.prod.json` | `npm run build` | Production build overrides (optional) |
17
- | `apitogo.local.secrets.json` | MCP publish only | OIDC and Stripe credentials — never loaded by the dev server |
18
-
19
- New scaffolds ship `apitogo.json` plus `apitogo.local.example.json` and `apitogo.prod.example.json`
20
- to show how overrides work. Copy or rename the examples when you need environment-specific values.
21
-
22
- ## Resolution
23
-
24
- At dev and build time the tooling merges the base file with the environment override:
25
-
26
- ```mermaid
27
- flowchart LR
28
- base["apitogo.json"]
29
- local["apitogo.local.json"]
30
- prod["apitogo.prod.json"]
31
- devMerge["merge"]
32
- buildMerge["merge"]
33
- viteDev["npm run dev"]
34
- viteBuild["npm run build"]
35
-
36
- base --> devMerge
37
- local --> devMerge
38
- base --> buildMerge
39
- prod --> buildMerge
40
- viteDev --> devMerge
41
- buildMerge --> viteBuild
42
- ```
12
+ | File | Purpose |
13
+ | --- | --- |
14
+ | `apitogo.config.json` | Committed defaults: site, modules, plans, branding, backend, gateway |
15
+ | `apitogo.config.tsx` | Plugins and other non-JSON configuration |
16
+ | `.env` / `.env.local` / `.env.production` | Per-environment overrides via `APITOGO_CONFIG_*` and `VITE_*` vars |
17
+
18
+ ## Environment overrides
43
19
 
44
- - **Local preview** (`npm run dev`): `apitogo.json` + `apitogo.local.json`
45
- - **Production build** (`npm run build`): `apitogo.json` + `apitogo.prod.json`
46
- - **Deployed site**: plans are served from the live dev-portal API (`GET /api/v1/plans`), not from
47
- JSON files in the browser bundle
20
+ Use `APITOGO_CONFIG_<path>` with double underscores for nested paths:
21
+
22
+ ```sh
23
+ APITOGO_CONFIG_site__title=My Local Site
24
+ APITOGO_CONFIG_pricing__enabled=true
25
+ APITOGO_CONFIG_projectId=your-local-project-id
26
+ APITOGO_CONFIG_backend__sourceDirectory=../my-api
27
+ ```
48
28
 
49
- If `apitogo.json` is missing but `apitogo.local.json` exists, the local file is used as the full
50
- manifest (legacy single-file projects).
29
+ Values are parsed as JSON when possible (`true`, `false`, numbers, objects).
51
30
 
52
- ## Merge rules
31
+ For the live dev-portal API URL (client-visible):
53
32
 
54
- Override files patch the base manifest:
33
+ ```sh
34
+ VITE_APITOGO_DEV_PORTAL_API_URL=https://your-dev-portal-api.example.com
35
+ ```
55
36
 
56
- - Top-level keys: override wins
57
- - `branding`, `backend`, `gateway`: deep-merged (override fields patch nested objects)
58
- - `plans`: merged by `sku` — an override plan with the same SKU updates only the fields you specify
37
+ Keep secrets (OIDC client secret, Stripe keys, etc.) in `.env.local` using the
38
+ same `APITOGO_CONFIG_*` pattern never commit them.
59
39
 
60
- Example base (`apitogo.json`):
40
+ ## Example `apitogo.config.json`
61
41
 
62
42
  ```json
63
43
  {
64
44
  "siteName": "My API",
65
45
  "branding": { "title": "My API" },
46
+ "pricing": { "enabled": true },
47
+ "authentication": { "enabled": true, "type": "dev-portal" },
66
48
  "plans": [
67
49
  {
68
50
  "sku": "pro",
@@ -71,45 +53,52 @@ Example base (`apitogo.json`):
71
53
  "priceAmount": 9.99,
72
54
  "billingPeriod": "month"
73
55
  }
74
- ]
56
+ ],
57
+ "site": {
58
+ "title": "My API",
59
+ "logo": {
60
+ "src": { "light": "/logo-light.png", "dark": "/logo-dark.png" },
61
+ "alt": "My API",
62
+ "width": "130px"
63
+ }
64
+ },
65
+ "docs": { "files": "/pages/**/*.{md,mdx}" }
75
66
  }
76
67
  ```
77
68
 
78
- Example local override (`apitogo.local.json`):
79
-
80
- ```json
81
- {
82
- "projectId": "your-local-project-id",
83
- "backend": { "sourceDirectory": "../my-api" }
84
- }
85
- ```
69
+ ## Thin `apitogo.config.tsx`
86
70
 
87
- Example production override (`apitogo.prod.json`):
71
+ ```tsx
72
+ import { buildApitogoConfig } from "@lukoweb/apitogo";
73
+ import { devPortalBillingPlugin } from "@lukoweb/apitogo-plugin-dev-portal-billing";
88
74
 
89
- ```json
90
- {
91
- "plans": [{ "sku": "pro", "priceAmount": 19.99 }],
92
- "backend": { "deployed": true }
93
- }
75
+ export default buildApitogoConfig({
76
+ plugins: [devPortalBillingPlugin()],
77
+ });
94
78
  ```
95
79
 
96
80
  ## What to put where
97
81
 
98
- | Concern | Recommended file |
99
- | -------------------------------------------- | ------------------------------------------------------------------------ |
100
- | Plans, rate limits, `includedActionKeys` | `apitogo.json` (shared) |
101
- | Branding, site name | `apitogo.json` |
102
- | Backend paths, OpenAPI location | `apitogo.json`; machine-specific paths in `apitogo.local.json` |
103
- | `projectId` after bootstrap | `apitogo.local.json` until MCP writes to the correct file |
104
- | Production-only plan pricing or deploy flags | `apitogo.prod.json` |
105
- | OIDC / Stripe | `apitogo.local.secrets.json` (see [Authentication](./authentication.md)) |
82
+ | Concern | Recommended location |
83
+ | --- | --- |
84
+ | Plans, rate limits, `includedActionKeys` | `apitogo.config.json` |
85
+ | Branding, site name, modules, navigation | `apitogo.config.json` |
86
+ | Machine-specific paths, `projectId` | `.env.local` via `APITOGO_CONFIG_*` |
87
+ | OIDC / Stripe for publish | `.env.local` via `APITOGO_CONFIG_*` |
88
+ | Plugins | `apitogo.config.tsx` |
106
89
 
107
90
  ## Hot reload
108
91
 
109
- During `npm run dev`, changes to `apitogo.json` or `apitogo.local.json` hot-reload on `/pricing`
92
+ During `npm run dev`, changes to `apitogo.config.json` hot-reload on `/pricing`
110
93
  without restarting the server. Restart the dev server after changing
111
94
  `VITE_APITOGO_DEV_PORTAL_API_URL` in `.env`.
112
95
 
96
+ ## Legacy manifests
97
+
98
+ Older projects may still have `apitogo.json` with optional
99
+ `apitogo.local.json` / `apitogo.prod.json` overrides. The tooling falls back to
100
+ those files when `apitogo.config.json` is missing.
101
+
113
102
  ## Related configuration
114
103
 
115
104
  - Site navigation, docs, and plugins: [`apitogo.config.tsx`](./overview.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -153,6 +153,14 @@
153
153
  "./local-manifest": {
154
154
  "types": "./dist/declarations/config/local-manifest.d.ts",
155
155
  "default": "./src/config/local-manifest.ts"
156
+ },
157
+ "./build-config": {
158
+ "types": "./dist/declarations/config/build-apitogo-config.d.ts",
159
+ "default": "./src/config/build-apitogo-config.ts"
160
+ },
161
+ "./config-loader": {
162
+ "types": "./dist/declarations/config/apitogo-config-loader.d.ts",
163
+ "default": "./src/config/apitogo-config-loader.ts"
156
164
  }
157
165
  },
158
166
  "scripts": {
@@ -467,6 +475,14 @@
467
475
  "./local-manifest": {
468
476
  "types": "./dist/declarations/config/local-manifest.d.ts",
469
477
  "default": "./src/config/local-manifest.ts"
478
+ },
479
+ "./build-config": {
480
+ "types": "./dist/declarations/config/build-apitogo-config.d.ts",
481
+ "default": "./src/config/build-apitogo-config.ts"
482
+ },
483
+ "./config-loader": {
484
+ "types": "./dist/declarations/config/apitogo-config-loader.d.ts",
485
+ "default": "./src/config/apitogo-config-loader.ts"
470
486
  }
471
487
  },
472
488
  "access": "public"
@@ -0,0 +1,224 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const APITOGO_CONFIG_FILENAME = "apitogo.config.json";
5
+
6
+ const ENV_PREFIX = "APITOGO_CONFIG_";
7
+
8
+ const MANIFEST_KEYS = [
9
+ "siteName",
10
+ "branding",
11
+ "pricing",
12
+ "authentication",
13
+ "plans",
14
+ "backend",
15
+ "gateway",
16
+ "projectId",
17
+ "projectName",
18
+ ] as const;
19
+
20
+ const SITE_CONFIG_KEYS = [
21
+ "site",
22
+ "metadata",
23
+ "docs",
24
+ "modules",
25
+ "navigation",
26
+ "apis",
27
+ "redirects",
28
+ "theme",
29
+ "footer",
30
+ "search",
31
+ "llms",
32
+ "protectedRoutes",
33
+ "slots",
34
+ "mdx",
35
+ "apiKeys",
36
+ "catalog",
37
+ "versions",
38
+ "options",
39
+ ] as const;
40
+
41
+ export type ApitogoJsonConfig = Record<string, unknown>;
42
+
43
+ function parseEnvValue(raw: string | undefined): unknown {
44
+ if (raw === undefined) {
45
+ return raw;
46
+ }
47
+
48
+ const trimmed = raw.trim();
49
+ if (trimmed === "true") {
50
+ return true;
51
+ }
52
+ if (trimmed === "false") {
53
+ return false;
54
+ }
55
+ if (trimmed === "null") {
56
+ return null;
57
+ }
58
+
59
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
60
+ return Number(trimmed);
61
+ }
62
+
63
+ if (
64
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
65
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
66
+ ) {
67
+ try {
68
+ return JSON.parse(trimmed) as unknown;
69
+ } catch {
70
+ return trimmed;
71
+ }
72
+ }
73
+
74
+ return trimmed;
75
+ }
76
+
77
+ function setNestedValue(
78
+ target: Record<string, unknown>,
79
+ pathParts: string[],
80
+ value: unknown,
81
+ ): void {
82
+ if (pathParts.length === 0) {
83
+ return;
84
+ }
85
+
86
+ let current: Record<string, unknown> = target;
87
+ for (let index = 0; index < pathParts.length - 1; index += 1) {
88
+ const key = pathParts[index];
89
+ if (!key) {
90
+ return;
91
+ }
92
+ const next = current[key];
93
+ if (
94
+ next === undefined ||
95
+ next === null ||
96
+ typeof next !== "object" ||
97
+ Array.isArray(next)
98
+ ) {
99
+ current[key] = {};
100
+ }
101
+ current = current[key] as Record<string, unknown>;
102
+ }
103
+
104
+ const leafKey = pathParts[pathParts.length - 1];
105
+ if (!leafKey) {
106
+ return;
107
+ }
108
+
109
+ current[leafKey] = value;
110
+ }
111
+
112
+ export function applyEnvOverrides(
113
+ config: ApitogoJsonConfig,
114
+ env: NodeJS.ProcessEnv = process.env,
115
+ ): ApitogoJsonConfig {
116
+ const result = structuredClone(config);
117
+
118
+ for (const [key, value] of Object.entries(env)) {
119
+ if (!key.startsWith(ENV_PREFIX) || value === undefined) {
120
+ continue;
121
+ }
122
+
123
+ const pathParts = key
124
+ .slice(ENV_PREFIX.length)
125
+ .split("__")
126
+ .filter(Boolean);
127
+
128
+ if (pathParts.length === 0) {
129
+ continue;
130
+ }
131
+
132
+ setNestedValue(result, pathParts, parseEnvValue(value));
133
+ }
134
+
135
+ return result;
136
+ }
137
+
138
+ export function loadApitogoConfig(rootDir = process.cwd()): ApitogoJsonConfig {
139
+ const configPath = path.join(path.resolve(rootDir), APITOGO_CONFIG_FILENAME);
140
+
141
+ if (!existsSync(configPath)) {
142
+ throw new Error(
143
+ `Missing ${APITOGO_CONFIG_FILENAME} in ${path.resolve(rootDir)}`,
144
+ );
145
+ }
146
+
147
+ const raw = readFileSync(configPath, "utf8");
148
+ const parsed = JSON.parse(raw) as ApitogoJsonConfig;
149
+ return applyEnvOverrides(parsed, process.env);
150
+ }
151
+
152
+ export function extractManifest(
153
+ config: ApitogoJsonConfig | null | undefined,
154
+ ): ApitogoJsonConfig | null {
155
+ if (!config || typeof config !== "object") {
156
+ return null;
157
+ }
158
+
159
+ const manifest: ApitogoJsonConfig = {};
160
+ for (const key of MANIFEST_KEYS) {
161
+ if (config[key] !== undefined) {
162
+ manifest[key] = config[key];
163
+ }
164
+ }
165
+
166
+ const authentication = manifest.authentication;
167
+ if (authentication && typeof authentication === "object") {
168
+ const { enabled } = authentication as { enabled?: boolean };
169
+ manifest.authentication =
170
+ enabled === undefined ? undefined : { enabled };
171
+ }
172
+
173
+ return Object.keys(manifest).length > 0 ? manifest : null;
174
+ }
175
+
176
+ export function extractSiteConfig(
177
+ config: ApitogoJsonConfig | null | undefined,
178
+ ): ApitogoJsonConfig {
179
+ if (!config || typeof config !== "object") {
180
+ return {};
181
+ }
182
+
183
+ const siteConfig: ApitogoJsonConfig = {};
184
+ for (const key of SITE_CONFIG_KEYS) {
185
+ if (config[key] !== undefined) {
186
+ siteConfig[key] = config[key];
187
+ }
188
+ }
189
+
190
+ const auth = config.authentication;
191
+ if (auth && typeof auth === "object") {
192
+ const authRecord = auth as Record<string, unknown>;
193
+ if (authRecord.type) {
194
+ const { enabled: _enabled, ...authConfig } = authRecord;
195
+ siteConfig.authentication = authConfig;
196
+ }
197
+ }
198
+
199
+ const site = siteConfig.site;
200
+ const branding = config.branding;
201
+ const title =
202
+ (site &&
203
+ typeof site === "object" &&
204
+ (site as Record<string, unknown>).title) ??
205
+ (branding &&
206
+ typeof branding === "object" &&
207
+ (branding as Record<string, unknown>).title) ??
208
+ config.siteName;
209
+
210
+ if (title) {
211
+ siteConfig.site = {
212
+ ...(site && typeof site === "object"
213
+ ? (site as Record<string, unknown>)
214
+ : {}),
215
+ title,
216
+ };
217
+ }
218
+
219
+ return siteConfig;
220
+ }
221
+
222
+ export function apitogoConfigPath(rootDir: string): string {
223
+ return path.join(path.resolve(rootDir), APITOGO_CONFIG_FILENAME);
224
+ }
@@ -0,0 +1,43 @@
1
+ import type { ApitogoConfig } from "./config.js";
2
+ import {
3
+ extractSiteConfig,
4
+ loadApitogoConfig,
5
+ } from "./apitogo-config-loader.js";
6
+
7
+ type BuildApitogoConfigOptions = {
8
+ rootDir?: string;
9
+ overrides?: Partial<ApitogoConfig>;
10
+ };
11
+
12
+ export function buildApitogoConfig({
13
+ rootDir,
14
+ overrides = {},
15
+ }: BuildApitogoConfigOptions = {}): ApitogoConfig {
16
+ const jsonConfig = loadApitogoConfig(rootDir);
17
+ const siteConfig = extractSiteConfig(jsonConfig);
18
+
19
+ return {
20
+ ...siteConfig,
21
+ ...overrides,
22
+ authentication: {
23
+ ...(siteConfig.authentication as ApitogoConfig["authentication"]),
24
+ ...overrides.authentication,
25
+ },
26
+ site: {
27
+ ...(siteConfig.site as ApitogoConfig["site"]),
28
+ ...overrides.site,
29
+ },
30
+ metadata: {
31
+ ...(siteConfig.metadata as ApitogoConfig["metadata"]),
32
+ ...overrides.metadata,
33
+ },
34
+ docs: {
35
+ ...(siteConfig.docs as ApitogoConfig["docs"]),
36
+ ...overrides.docs,
37
+ },
38
+ modules: {
39
+ ...(siteConfig.modules as ApitogoConfig["modules"]),
40
+ ...overrides.modules,
41
+ },
42
+ } as ApitogoConfig;
43
+ }
@@ -10,9 +10,17 @@ import {
10
10
  } from "vite";
11
11
  import { logger } from "../cli/common/logger.js";
12
12
  import { getZudokuRootDir } from "../cli/common/package-json.js";
13
+ import { applyAuthenticationHeaderNav } from "../lib/authentication/auth-header-nav.js";
13
14
  import { runPluginTransformConfig } from "../lib/core/transform-config.js";
14
15
  import invariant from "../lib/util/invariant.js";
15
16
  import { fileExists } from "./file-exists.js";
17
+ import {
18
+ isAuthenticationEnabled,
19
+ isPricingEnabled,
20
+ manifestPathsForEnv,
21
+ readEffectiveManifest,
22
+ resolveManifestEnv,
23
+ } from "./local-manifest.js";
16
24
  import { resolveModulesConfig } from "./resolve-modules.js";
17
25
  import type { ResolvedModulesConfig } from "./validators/ModulesSchema.js";
18
26
  import type { ZudokuConfig } from "./validators/ZudokuConfig.js";
@@ -25,6 +33,8 @@ export type ConfigWithMeta = ZudokuConfig & {
25
33
  configPath: string;
26
34
  mode: typeof process.env.ZUDOKU_ENV;
27
35
  dependencies: string[];
36
+ pricingEnabled?: boolean;
37
+ authenticationEnabled?: boolean;
28
38
  };
29
39
  __resolvedModules: ResolvedModulesConfig;
30
40
  };
@@ -157,8 +167,17 @@ function isWatchableConfigDependency(depPath: string): boolean {
157
167
  }
158
168
 
159
169
  function getWatchableConfigDependencies(config: ConfigWithMeta): string[] {
170
+ const manifestEnv = resolveManifestEnv(
171
+ config.__meta.mode === "production" ? "production" : "development",
172
+ );
173
+ const { watchPaths } = manifestPathsForEnv(
174
+ config.__meta.rootDir,
175
+ manifestEnv,
176
+ );
177
+
160
178
  return [
161
179
  config.__meta.configPath,
180
+ ...watchPaths,
162
181
  ...config.__meta.dependencies.filter(isWatchableConfigDependency),
163
182
  ];
164
183
  }
@@ -171,6 +190,10 @@ async function hasConfigChanged() {
171
190
  try {
172
191
  const hasChanged = await Promise.all(
173
192
  files.map(async (depPath) => {
193
+ if (!(await fileExists(depPath))) {
194
+ return false;
195
+ }
196
+
174
197
  const depStat = await stat(depPath);
175
198
  const cachedMtime = modifiedTimes?.get(depPath);
176
199
  return !cachedMtime || depStat.mtimeMs !== cachedMtime;
@@ -192,6 +215,10 @@ async function updateModifiedTimes() {
192
215
 
193
216
  await Promise.all(
194
217
  files.map(async (depPath) => {
218
+ if (!(await fileExists(depPath))) {
219
+ return;
220
+ }
221
+
195
222
  const depStat = await stat(depPath);
196
223
  modifiedTimes?.set(depPath, depStat.mtimeMs);
197
224
  }),
@@ -225,7 +252,19 @@ export async function loadZudokuConfig(
225
252
 
226
253
  try {
227
254
  const loadedConfig = await loadZudokuConfigWithMeta(rootDir);
228
- const transformedConfig = await runPluginTransformConfig(loadedConfig);
255
+ const manifestEnv = resolveManifestEnv(configEnv.mode);
256
+ const manifest = await readEffectiveManifest(rootDir, manifestEnv);
257
+ const configWithManifestMeta: ConfigWithMeta = {
258
+ ...loadedConfig,
259
+ __meta: {
260
+ ...loadedConfig.__meta,
261
+ pricingEnabled: isPricingEnabled(manifest),
262
+ authenticationEnabled: isAuthenticationEnabled(manifest),
263
+ },
264
+ };
265
+ const transformedConfig = applyAuthenticationHeaderNav(
266
+ await runPluginTransformConfig(configWithManifestMeta),
267
+ );
229
268
  config = {
230
269
  ...transformedConfig,
231
270
  __resolvedModules: resolveModulesConfig(transformedConfig),