@lukoweb/apitogo 0.1.62 → 0.1.64
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.
- package/dist/cli/cli.js +115 -20
- package/dist/declarations/config/apitogo-config-core.d.ts +1 -0
- package/dist/declarations/config/loader.d.ts +1 -0
- package/dist/declarations/config/local-manifest.d.ts +6 -1
- package/docs/configuration/authentication.md +3 -3
- package/docs/configuration/manifest.md +10 -10
- package/package.json +1 -1
- package/src/config/apitogo-config-core.ts +88 -6
- package/src/config/loader.ts +27 -5
- package/src/config/local-manifest.ts +40 -3
- package/src/config/validators/ModulesSchema.ts +9 -1
- package/src/vite/plugin-config-reload.ts +1 -3
package/dist/cli/cli.js
CHANGED
|
@@ -187,26 +187,58 @@ function parseEnvValue(raw) {
|
|
|
187
187
|
}
|
|
188
188
|
return trimmed;
|
|
189
189
|
}
|
|
190
|
+
function screamingSnakeToCamel(segment) {
|
|
191
|
+
return segment.toLowerCase().split("_").filter(Boolean).map(
|
|
192
|
+
(part, index) => index === 0 ? part : `${part.charAt(0).toUpperCase()}${part.slice(1)}`
|
|
193
|
+
).join("");
|
|
194
|
+
}
|
|
195
|
+
function normalizeEnvPathSegment(segment) {
|
|
196
|
+
if (!segment) {
|
|
197
|
+
return segment;
|
|
198
|
+
}
|
|
199
|
+
const isAllUpper = segment === segment.toUpperCase() && /[A-Z]/.test(segment);
|
|
200
|
+
if (!isAllUpper) {
|
|
201
|
+
return segment;
|
|
202
|
+
}
|
|
203
|
+
if (segment.includes("_")) {
|
|
204
|
+
return screamingSnakeToCamel(segment);
|
|
205
|
+
}
|
|
206
|
+
return COLLAPSED_UPPERCASE_ALIASES[segment] ?? segment.toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
function resolveExistingKey(target, key) {
|
|
209
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
210
|
+
return key;
|
|
211
|
+
}
|
|
212
|
+
const lower = key.toLowerCase();
|
|
213
|
+
for (const existing of Object.keys(target)) {
|
|
214
|
+
if (existing.toLowerCase() === lower) {
|
|
215
|
+
return existing;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return key;
|
|
219
|
+
}
|
|
190
220
|
function setNestedValue(target, pathParts, value) {
|
|
191
221
|
if (pathParts.length === 0) {
|
|
192
222
|
return;
|
|
193
223
|
}
|
|
194
224
|
let current = target;
|
|
195
225
|
for (let index = 0; index < pathParts.length - 1; index += 1) {
|
|
196
|
-
const
|
|
197
|
-
if (!
|
|
226
|
+
const rawKey = pathParts[index];
|
|
227
|
+
if (!rawKey) {
|
|
198
228
|
return;
|
|
199
229
|
}
|
|
230
|
+
const key = resolveExistingKey(current, rawKey);
|
|
200
231
|
const next = current[key];
|
|
201
232
|
if (next === void 0 || next === null || typeof next !== "object" || Array.isArray(next)) {
|
|
202
233
|
current[key] = {};
|
|
203
234
|
}
|
|
204
235
|
current = current[key];
|
|
205
236
|
}
|
|
206
|
-
const
|
|
207
|
-
if (!
|
|
237
|
+
const rawLeaf = pathParts[pathParts.length - 1];
|
|
238
|
+
if (!rawLeaf) {
|
|
208
239
|
return;
|
|
209
240
|
}
|
|
241
|
+
const leafKey = resolveExistingKey(current, rawLeaf);
|
|
210
242
|
current[leafKey] = value;
|
|
211
243
|
}
|
|
212
244
|
function stripRuntimeConfigFields(config2) {
|
|
@@ -219,7 +251,7 @@ function applyEnvOverrides(config2, env = process.env) {
|
|
|
219
251
|
if (!key.startsWith(ENV_PREFIX) || value === void 0) {
|
|
220
252
|
continue;
|
|
221
253
|
}
|
|
222
|
-
const pathParts = key.slice(ENV_PREFIX.length).split("__").filter(Boolean);
|
|
254
|
+
const pathParts = key.slice(ENV_PREFIX.length).split("__").filter(Boolean).map(normalizeEnvPathSegment);
|
|
223
255
|
if (pathParts.length === 0) {
|
|
224
256
|
continue;
|
|
225
257
|
}
|
|
@@ -550,7 +582,7 @@ function extractSiteConfig(config2) {
|
|
|
550
582
|
}
|
|
551
583
|
return siteConfig;
|
|
552
584
|
}
|
|
553
|
-
var APITOGO_CONFIG_FILENAME, ENV_PREFIX, DERIVED_MANIFEST_KEYS, SITE_CONFIG_KEYS;
|
|
585
|
+
var APITOGO_CONFIG_FILENAME, ENV_PREFIX, DERIVED_MANIFEST_KEYS, SITE_CONFIG_KEYS, COLLAPSED_UPPERCASE_ALIASES;
|
|
554
586
|
var init_apitogo_config_core = __esm({
|
|
555
587
|
"src/config/apitogo-config-core.ts"() {
|
|
556
588
|
APITOGO_CONFIG_FILENAME = "apitogo.config.json";
|
|
@@ -583,6 +615,28 @@ var init_apitogo_config_core = __esm({
|
|
|
583
615
|
"versions",
|
|
584
616
|
"options"
|
|
585
617
|
];
|
|
618
|
+
COLLAPSED_UPPERCASE_ALIASES = {
|
|
619
|
+
SECRETKEY: "secretKey",
|
|
620
|
+
WEBHOOKSECRET: "webhookSecret",
|
|
621
|
+
APIBASEURL: "apiBaseUrl",
|
|
622
|
+
CLIENTID: "clientId",
|
|
623
|
+
CLIENTSECRET: "clientSecret",
|
|
624
|
+
CONNECTIONSTRING: "connectionString",
|
|
625
|
+
DEMOMODE: "demoMode",
|
|
626
|
+
DISPLAYNAME: "displayName",
|
|
627
|
+
PRICEAMOUNT: "priceAmount",
|
|
628
|
+
PRICECURRENCY: "priceCurrency",
|
|
629
|
+
BILLINGPERIOD: "billingPeriod",
|
|
630
|
+
LIMITSJSON: "limitsJson",
|
|
631
|
+
INCLUDEDACTIONKEYS: "includedActionKeys",
|
|
632
|
+
SOURCEDIRECTORY: "sourceDirectory",
|
|
633
|
+
PUBLISHDIRECTORY: "publishDirectory",
|
|
634
|
+
OPENAPIPATH: "openApiPath",
|
|
635
|
+
RUNTIMEVERSION: "runtimeVersion",
|
|
636
|
+
PROJECTID: "projectId",
|
|
637
|
+
SITENAME: "siteName",
|
|
638
|
+
LOGOURL: "logoUrl"
|
|
639
|
+
};
|
|
586
640
|
}
|
|
587
641
|
});
|
|
588
642
|
|
|
@@ -852,7 +906,10 @@ var init_ModulesSchema = __esm({
|
|
|
852
906
|
sku: z3.string(),
|
|
853
907
|
displayName: z3.string(),
|
|
854
908
|
priceAmount: z3.number().default(0),
|
|
855
|
-
priceCurrency: z3.string().
|
|
909
|
+
priceCurrency: z3.string().trim().toLowerCase().regex(
|
|
910
|
+
/^[a-z]{3}$/,
|
|
911
|
+
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)"
|
|
912
|
+
).optional(),
|
|
856
913
|
billingPeriod: z3.string().optional(),
|
|
857
914
|
limitsJson: z3.string().optional(),
|
|
858
915
|
includedActionKeys: z3.array(z3.string()).optional()
|
|
@@ -993,17 +1050,33 @@ async function readEffectiveManifest(rootDir, _env) {
|
|
|
993
1050
|
return null;
|
|
994
1051
|
}
|
|
995
1052
|
}
|
|
996
|
-
var BillingPeriodSchema, ManifestPlanInputSchema, DevPortalLocalManifestSchema;
|
|
1053
|
+
var BillingPeriodSchema, PriceCurrencySchema, ALLOWED_RUNTIME_VERSIONS, ManifestPlanInputSchema, DevPortalLocalManifestSchema;
|
|
997
1054
|
var init_local_manifest = __esm({
|
|
998
1055
|
"src/config/local-manifest.ts"() {
|
|
999
1056
|
init_apitogo_config_loader();
|
|
1000
1057
|
init_landing_manifest();
|
|
1001
1058
|
BillingPeriodSchema = z4.enum(["month", "year", "monthly", "annual"]);
|
|
1059
|
+
PriceCurrencySchema = z4.string().trim().toLowerCase().regex(
|
|
1060
|
+
/^[a-z]{3}$/,
|
|
1061
|
+
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)"
|
|
1062
|
+
);
|
|
1063
|
+
ALLOWED_RUNTIME_VERSIONS = [
|
|
1064
|
+
"DOTNET_10",
|
|
1065
|
+
"DOTNET_9",
|
|
1066
|
+
"DOTNET_8",
|
|
1067
|
+
"NODE_24_LTS",
|
|
1068
|
+
"NODE_22_LTS",
|
|
1069
|
+
"PYTHON_314",
|
|
1070
|
+
"PYTHON_313",
|
|
1071
|
+
"PYTHON_312",
|
|
1072
|
+
"PYTHON_311",
|
|
1073
|
+
"PYTHON_310"
|
|
1074
|
+
];
|
|
1002
1075
|
ManifestPlanInputSchema = z4.object({
|
|
1003
1076
|
sku: z4.string().min(1),
|
|
1004
1077
|
displayName: z4.string().min(1),
|
|
1005
1078
|
priceAmount: z4.number().default(0),
|
|
1006
|
-
priceCurrency:
|
|
1079
|
+
priceCurrency: PriceCurrencySchema.optional(),
|
|
1007
1080
|
billingPeriod: BillingPeriodSchema.optional(),
|
|
1008
1081
|
limitsJson: z4.string().optional(),
|
|
1009
1082
|
includedActionKeys: z4.array(z4.string()).optional()
|
|
@@ -1042,8 +1115,17 @@ var init_local_manifest = __esm({
|
|
|
1042
1115
|
sourceDirectory: z4.string().optional(),
|
|
1043
1116
|
publishDirectory: z4.string().optional(),
|
|
1044
1117
|
openApiPath: z4.string().optional(),
|
|
1045
|
-
runtime: z4.string().optional(),
|
|
1046
|
-
runtimeVersion: z4.string().optional()
|
|
1118
|
+
runtime: z4.enum(["dotnet", "nodejs", "python"]).or(z4.string()).optional(),
|
|
1119
|
+
runtimeVersion: z4.string().optional().refine(
|
|
1120
|
+
(value) => value === void 0 || ALLOWED_RUNTIME_VERSIONS.includes(
|
|
1121
|
+
value.toUpperCase()
|
|
1122
|
+
) || /^(8(\.0)?|9(\.0)?|10(\.0)?|net8\.0|net9\.0|net10\.0|24|22|3\.(10|11|12|13|14))$/i.test(
|
|
1123
|
+
value
|
|
1124
|
+
),
|
|
1125
|
+
{
|
|
1126
|
+
message: `runtimeVersion must be a platform slug (${ALLOWED_RUNTIME_VERSIONS.join(", ")}) or a known alias such as 8.0`
|
|
1127
|
+
}
|
|
1128
|
+
),
|
|
1047
1129
|
deployed: z4.boolean().optional()
|
|
1048
1130
|
}).optional(),
|
|
1049
1131
|
gateway: z4.object({
|
|
@@ -4772,6 +4854,18 @@ import {
|
|
|
4772
4854
|
runnerImport,
|
|
4773
4855
|
loadEnv as viteLoadEnv
|
|
4774
4856
|
} from "vite";
|
|
4857
|
+
function mergeJsonConfigWithRuntimeOverrides(fileConfig, importedConfig) {
|
|
4858
|
+
const jsonConfig = buildApitogoConfig({
|
|
4859
|
+
...fileConfig,
|
|
4860
|
+
plugins: importedConfig.plugins
|
|
4861
|
+
});
|
|
4862
|
+
return {
|
|
4863
|
+
...jsonConfig,
|
|
4864
|
+
// Custom pages carry React elements and therefore only exist in the
|
|
4865
|
+
// imported TSX config. They cannot be read from or cloned through JSON.
|
|
4866
|
+
navigation: importedConfig.navigation ?? jsonConfig.navigation
|
|
4867
|
+
};
|
|
4868
|
+
}
|
|
4775
4869
|
async function getConfigFilePath(rootDir) {
|
|
4776
4870
|
for (const fileName of zudokuConfigFiles) {
|
|
4777
4871
|
const filepath = path5.join(rootDir, fileName);
|
|
@@ -4806,10 +4900,7 @@ async function loadZudokuConfigWithMeta(rootDir) {
|
|
|
4806
4900
|
let config2;
|
|
4807
4901
|
try {
|
|
4808
4902
|
const fileConfig = loadApitogoConfig(rootDir);
|
|
4809
|
-
config2 =
|
|
4810
|
-
...fileConfig,
|
|
4811
|
-
plugins: importedConfig.plugins
|
|
4812
|
-
});
|
|
4903
|
+
config2 = mergeJsonConfigWithRuntimeOverrides(fileConfig, importedConfig);
|
|
4813
4904
|
} catch {
|
|
4814
4905
|
config2 = importedConfig;
|
|
4815
4906
|
}
|
|
@@ -4906,6 +4997,7 @@ async function loadZudokuConfig(configEnv, rootDir) {
|
|
|
4906
4997
|
return { config, envPrefix, publicEnv };
|
|
4907
4998
|
}
|
|
4908
4999
|
({ publicEnv, envPrefix } = loadEnv(configEnv, rootDir));
|
|
5000
|
+
let loadSucceeded = false;
|
|
4909
5001
|
try {
|
|
4910
5002
|
const loadedConfig = await loadZudokuConfigWithMeta(rootDir);
|
|
4911
5003
|
const manifestEnv = resolveManifestEnv(configEnv.mode);
|
|
@@ -4943,6 +5035,7 @@ async function loadZudokuConfig(configEnv, rootDir) {
|
|
|
4943
5035
|
{ timestamp: true }
|
|
4944
5036
|
);
|
|
4945
5037
|
}
|
|
5038
|
+
loadSucceeded = true;
|
|
4946
5039
|
return { config, envPrefix, publicEnv };
|
|
4947
5040
|
} catch (error) {
|
|
4948
5041
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -4951,7 +5044,11 @@ async function loadZudokuConfig(configEnv, rootDir) {
|
|
|
4951
5044
|
}
|
|
4952
5045
|
throw new Error(errorMessage, { cause: error });
|
|
4953
5046
|
} finally {
|
|
4954
|
-
|
|
5047
|
+
if (loadSucceeded) {
|
|
5048
|
+
await updateModifiedTimes();
|
|
5049
|
+
} else {
|
|
5050
|
+
modifiedTimes = void 0;
|
|
5051
|
+
}
|
|
4955
5052
|
}
|
|
4956
5053
|
}
|
|
4957
5054
|
var config, envPrefix, publicEnv, modifiedTimes, zudokuConfigFiles, serverConfigOutputFiles, virtualModuleStubPlugin, getCurrentConfig;
|
|
@@ -5238,7 +5335,7 @@ import {
|
|
|
5238
5335
|
// package.json
|
|
5239
5336
|
var package_default = {
|
|
5240
5337
|
name: "@lukoweb/apitogo",
|
|
5241
|
-
version: "0.1.
|
|
5338
|
+
version: "0.1.64",
|
|
5242
5339
|
type: "module",
|
|
5243
5340
|
sideEffects: [
|
|
5244
5341
|
"**/*.css",
|
|
@@ -7583,9 +7680,7 @@ var viteConfigReloadPlugin = () => ({
|
|
|
7583
7680
|
if (file !== currentConfig.__meta.configPath && file.endsWith(".tsx"))
|
|
7584
7681
|
return;
|
|
7585
7682
|
invalidate(server);
|
|
7586
|
-
|
|
7587
|
-
env.moduleGraph.invalidateAll();
|
|
7588
|
-
});
|
|
7683
|
+
reload(server);
|
|
7589
7684
|
logger.info(
|
|
7590
7685
|
colors3.blue(
|
|
7591
7686
|
`Config ${path13.basename(currentConfig.__meta.configPath)} changed. Reloading...`
|
|
@@ -19,6 +19,7 @@ export type AuthenticationSecrets = {
|
|
|
19
19
|
webhookSecret?: string;
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
+
export declare function normalizeEnvPathSegment(segment: string): string;
|
|
22
23
|
export declare function applyEnvOverrides(config: ApitogoJsonConfig, env?: NodeJS.ProcessEnv): ApitogoJsonConfig;
|
|
23
24
|
type PlanCatalogItem = Record<string, unknown> & {
|
|
24
25
|
sku?: string;
|
|
@@ -16,6 +16,7 @@ export type ConfigWithMeta = ZudokuConfig & {
|
|
|
16
16
|
};
|
|
17
17
|
__resolvedModules: ResolvedModulesConfig;
|
|
18
18
|
};
|
|
19
|
+
export declare function mergeJsonConfigWithRuntimeOverrides(fileConfig: Record<string, unknown>, importedConfig: ZudokuConfig): ZudokuConfig;
|
|
19
20
|
export declare function findOutputPathOfServerConfig(output: RollupOutput | RollupOutput[] | RollupWatcher): string;
|
|
20
21
|
export declare const getCurrentConfig: () => ConfigWithMeta;
|
|
21
22
|
export declare const setCurrentConfig: (newConfig: ConfigWithMeta) => void;
|
|
@@ -8,6 +8,7 @@ export declare const APITOGO_MANIFEST_ENV_FILES: {
|
|
|
8
8
|
};
|
|
9
9
|
export { APITOGO_CONFIG_FILENAME };
|
|
10
10
|
export type ApitogoManifestEnv = keyof typeof APITOGO_MANIFEST_ENV_FILES;
|
|
11
|
+
export declare const ALLOWED_RUNTIME_VERSIONS: readonly ["DOTNET_10", "DOTNET_9", "DOTNET_8", "NODE_24_LTS", "NODE_22_LTS", "PYTHON_314", "PYTHON_313", "PYTHON_312", "PYTHON_311", "PYTHON_310"];
|
|
11
12
|
export declare const ManifestPlanInputSchema: z.ZodObject<{
|
|
12
13
|
sku: z.ZodString;
|
|
13
14
|
displayName: z.ZodString;
|
|
@@ -70,7 +71,11 @@ export declare const DevPortalLocalManifestSchema: z.ZodObject<{
|
|
|
70
71
|
sourceDirectory: z.ZodOptional<z.ZodString>;
|
|
71
72
|
publishDirectory: z.ZodOptional<z.ZodString>;
|
|
72
73
|
openApiPath: z.ZodOptional<z.ZodString>;
|
|
73
|
-
runtime: z.ZodOptional<z.
|
|
74
|
+
runtime: z.ZodOptional<z.ZodUnion<[z.ZodEnum<{
|
|
75
|
+
python: "python";
|
|
76
|
+
dotnet: "dotnet";
|
|
77
|
+
nodejs: "nodejs";
|
|
78
|
+
}>, z.ZodString]>>;
|
|
74
79
|
runtimeVersion: z.ZodOptional<z.ZodString>;
|
|
75
80
|
deployed: z.ZodOptional<z.ZodBoolean>;
|
|
76
81
|
}, z.core.$strip>>;
|
|
@@ -45,7 +45,7 @@ VITE_APITOGO_AUTHENTICATION_API_BASE_URL=https://your-dev-portal-api.example.com
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
You can also set `authentication.apiBaseUrl` in `apitogo.config.json` or
|
|
48
|
-
`
|
|
48
|
+
`APITOGO_CONFIG_AUTHENTICATION__API_BASE_URL` in `.env`.
|
|
49
49
|
|
|
50
50
|
**Local demo mode:** In development (`npm run dev`), `demoMode` is enabled by default when the user
|
|
51
51
|
panel is on. You do not need `VITE_APITOGO_AUTHENTICATION_API_BASE_URL`, OIDC, or a running
|
|
@@ -55,7 +55,7 @@ dev-portal backend to explore protected account pages locally. Sign in at `/logi
|
|
|
55
55
|
demo@apitogo.local / password
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
Disable with `
|
|
58
|
+
Disable with `APITOGO_CONFIG_DEMO_MODE=false` in `.env` or `"demoMode": false` in
|
|
59
59
|
`apitogo.config.json`. Demo mode is never active in production builds.
|
|
60
60
|
|
|
61
61
|
**Local preview (demo off):** When the backend URL is a placeholder or unreachable and demo mode is
|
|
@@ -70,7 +70,7 @@ without restarting the dev server.
|
|
|
70
70
|
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
71
71
|
| Site, plans, modules, branding, backend/gateway metadata | `apitogo.config.json` (+ `.env` overrides — see [Manifest](./manifest.md)) |
|
|
72
72
|
| Plugins and custom code | `apitogo.config.tsx` |
|
|
73
|
-
| OIDC and Stripe for publish | `.env.local` via `
|
|
73
|
+
| OIDC and Stripe for publish | `.env.local` via `APITOGO_CONFIG_AUTHENTICATION__*` |
|
|
74
74
|
|
|
75
75
|
**Production:** Register one OIDC app with redirect URI `https://{backend}/signin-oidc` (see MCP
|
|
76
76
|
`manualSteps.oidcRedirectUri`). The frontend uses cookie sessions via `credentials: "include"` on
|
|
@@ -70,23 +70,23 @@ Public client URL (mirrors `authentication.apiBaseUrl`):
|
|
|
70
70
|
VITE_APITOGO_AUTHENTICATION_API_BASE_URL=https://your-dev-portal-api.example.com
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
Nested config overrides:
|
|
73
|
+
Nested config overrides (uppercase SCREAMING_SNAKE path segments; camelCase still accepted):
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
APITOGO_CONFIG_SITE__TITLE=My Local Site
|
|
77
|
+
APITOGO_CONFIG_PRICING__ENABLED=true
|
|
78
|
+
APITOGO_CONFIG_PROJECT_ID=your-local-project-id
|
|
79
|
+
APITOGO_CONFIG_AUTHENTICATION__API_BASE_URL=https://your-dev-portal-api.example.com
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
Publish secrets (`.env.local`, never commit):
|
|
83
83
|
|
|
84
84
|
```sh
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
APITOGO_CONFIG_AUTHENTICATION__OIDC__AUTHORITY=https://login.example.com
|
|
86
|
+
APITOGO_CONFIG_AUTHENTICATION__OIDC__CLIENT_ID=your-client-id
|
|
87
|
+
APITOGO_CONFIG_AUTHENTICATION__OIDC__CLIENT_SECRET=your-client-secret
|
|
88
|
+
APITOGO_CONFIG_AUTHENTICATION__STRIPE__SECRET_KEY=sk_test_...
|
|
89
|
+
APITOGO_CONFIG_AUTHENTICATION__STRIPE__WEBHOOK_SECRET=whsec_...
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
## Thin `apitogo.config.tsx`
|
package/package.json
CHANGED
|
@@ -47,7 +47,7 @@ export type AuthenticationSecrets = {
|
|
|
47
47
|
email?: {
|
|
48
48
|
connectionString?: string;
|
|
49
49
|
};
|
|
50
|
-
/** Provider secrets keyed by provider `id` (from env: `
|
|
50
|
+
/** Provider secrets keyed by provider `id` (from env: `AUTHENTICATION__PROVIDERS__{id}__CLIENT_SECRET`). */
|
|
51
51
|
providers?: Record<string, { clientSecret?: string }>;
|
|
52
52
|
stripe?: {
|
|
53
53
|
secretKey?: string;
|
|
@@ -89,6 +89,82 @@ function parseEnvValue(raw: string | undefined): unknown {
|
|
|
89
89
|
return trimmed;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/** Collapsed ALLCAPS aliases for multi-word camelCase config keys. */
|
|
93
|
+
const COLLAPSED_UPPERCASE_ALIASES: Record<string, string> = {
|
|
94
|
+
SECRETKEY: "secretKey",
|
|
95
|
+
WEBHOOKSECRET: "webhookSecret",
|
|
96
|
+
APIBASEURL: "apiBaseUrl",
|
|
97
|
+
CLIENTID: "clientId",
|
|
98
|
+
CLIENTSECRET: "clientSecret",
|
|
99
|
+
CONNECTIONSTRING: "connectionString",
|
|
100
|
+
DEMOMODE: "demoMode",
|
|
101
|
+
DISPLAYNAME: "displayName",
|
|
102
|
+
PRICEAMOUNT: "priceAmount",
|
|
103
|
+
PRICECURRENCY: "priceCurrency",
|
|
104
|
+
BILLINGPERIOD: "billingPeriod",
|
|
105
|
+
LIMITSJSON: "limitsJson",
|
|
106
|
+
INCLUDEDACTIONKEYS: "includedActionKeys",
|
|
107
|
+
SOURCEDIRECTORY: "sourceDirectory",
|
|
108
|
+
PUBLISHDIRECTORY: "publishDirectory",
|
|
109
|
+
OPENAPIPATH: "openApiPath",
|
|
110
|
+
RUNTIMEVERSION: "runtimeVersion",
|
|
111
|
+
PROJECTID: "projectId",
|
|
112
|
+
SITENAME: "siteName",
|
|
113
|
+
LOGOURL: "logoUrl",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function screamingSnakeToCamel(segment: string): string {
|
|
117
|
+
return segment
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.split("_")
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.map((part, index) =>
|
|
122
|
+
index === 0 ? part : `${part.charAt(0).toUpperCase()}${part.slice(1)}`,
|
|
123
|
+
)
|
|
124
|
+
.join("");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Normalize one `APITOGO_CONFIG_*` path segment to the JSON key casing.
|
|
129
|
+
* - Mixed/camelCase → leave as-is (legacy)
|
|
130
|
+
* - `SECRET_KEY` → `secretKey`
|
|
131
|
+
* - `AUTHENTICATION` → `authentication`
|
|
132
|
+
* - `SECRETKEY` → `secretKey` (collapsed alias)
|
|
133
|
+
*/
|
|
134
|
+
export function normalizeEnvPathSegment(segment: string): string {
|
|
135
|
+
if (!segment) {
|
|
136
|
+
return segment;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isAllUpper =
|
|
140
|
+
segment === segment.toUpperCase() && /[A-Z]/.test(segment);
|
|
141
|
+
if (!isAllUpper) {
|
|
142
|
+
return segment;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (segment.includes("_")) {
|
|
146
|
+
return screamingSnakeToCamel(segment);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return COLLAPSED_UPPERCASE_ALIASES[segment] ?? segment.toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveExistingKey(
|
|
153
|
+
target: Record<string, unknown>,
|
|
154
|
+
key: string,
|
|
155
|
+
): string {
|
|
156
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
157
|
+
return key;
|
|
158
|
+
}
|
|
159
|
+
const lower = key.toLowerCase();
|
|
160
|
+
for (const existing of Object.keys(target)) {
|
|
161
|
+
if (existing.toLowerCase() === lower) {
|
|
162
|
+
return existing;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return key;
|
|
166
|
+
}
|
|
167
|
+
|
|
92
168
|
function setNestedValue(
|
|
93
169
|
target: Record<string, unknown>,
|
|
94
170
|
pathParts: string[],
|
|
@@ -100,10 +176,11 @@ function setNestedValue(
|
|
|
100
176
|
|
|
101
177
|
let current: Record<string, unknown> = target;
|
|
102
178
|
for (let index = 0; index < pathParts.length - 1; index += 1) {
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
179
|
+
const rawKey = pathParts[index];
|
|
180
|
+
if (!rawKey) {
|
|
105
181
|
return;
|
|
106
182
|
}
|
|
183
|
+
const key = resolveExistingKey(current, rawKey);
|
|
107
184
|
const next = current[key];
|
|
108
185
|
if (
|
|
109
186
|
next === undefined ||
|
|
@@ -116,11 +193,12 @@ function setNestedValue(
|
|
|
116
193
|
current = current[key] as Record<string, unknown>;
|
|
117
194
|
}
|
|
118
195
|
|
|
119
|
-
const
|
|
120
|
-
if (!
|
|
196
|
+
const rawLeaf = pathParts[pathParts.length - 1];
|
|
197
|
+
if (!rawLeaf) {
|
|
121
198
|
return;
|
|
122
199
|
}
|
|
123
200
|
|
|
201
|
+
const leafKey = resolveExistingKey(current, rawLeaf);
|
|
124
202
|
current[leafKey] = value;
|
|
125
203
|
}
|
|
126
204
|
|
|
@@ -144,7 +222,11 @@ export function applyEnvOverrides(
|
|
|
144
222
|
continue;
|
|
145
223
|
}
|
|
146
224
|
|
|
147
|
-
const pathParts = key
|
|
225
|
+
const pathParts = key
|
|
226
|
+
.slice(ENV_PREFIX.length)
|
|
227
|
+
.split("__")
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.map(normalizeEnvPathSegment);
|
|
148
230
|
|
|
149
231
|
if (pathParts.length === 0) {
|
|
150
232
|
continue;
|
package/src/config/loader.ts
CHANGED
|
@@ -69,6 +69,23 @@ const zudokuConfigFiles = [
|
|
|
69
69
|
|
|
70
70
|
const serverConfigOutputFiles = ["apitogo.config.js", "zudoku.config.js"];
|
|
71
71
|
|
|
72
|
+
export function mergeJsonConfigWithRuntimeOverrides(
|
|
73
|
+
fileConfig: Record<string, unknown>,
|
|
74
|
+
importedConfig: ZudokuConfig,
|
|
75
|
+
): ZudokuConfig {
|
|
76
|
+
const jsonConfig = buildApitogoConfig({
|
|
77
|
+
...fileConfig,
|
|
78
|
+
plugins: importedConfig.plugins,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...jsonConfig,
|
|
83
|
+
// Custom pages carry React elements and therefore only exist in the
|
|
84
|
+
// imported TSX config. They cannot be read from or cloned through JSON.
|
|
85
|
+
navigation: importedConfig.navigation ?? jsonConfig.navigation,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
async function getConfigFilePath(rootDir: string) {
|
|
73
90
|
for (const fileName of zudokuConfigFiles) {
|
|
74
91
|
const filepath = path.join(rootDir, fileName);
|
|
@@ -123,10 +140,7 @@ async function loadZudokuConfigWithMeta(
|
|
|
123
140
|
let config: ZudokuConfig;
|
|
124
141
|
try {
|
|
125
142
|
const fileConfig = loadApitogoConfig(rootDir);
|
|
126
|
-
config =
|
|
127
|
-
...fileConfig,
|
|
128
|
-
plugins: importedConfig.plugins,
|
|
129
|
-
});
|
|
143
|
+
config = mergeJsonConfigWithRuntimeOverrides(fileConfig, importedConfig);
|
|
130
144
|
} catch {
|
|
131
145
|
config = importedConfig;
|
|
132
146
|
}
|
|
@@ -271,6 +285,7 @@ export async function loadZudokuConfig(
|
|
|
271
285
|
|
|
272
286
|
({ publicEnv, envPrefix } = loadEnv(configEnv, rootDir));
|
|
273
287
|
|
|
288
|
+
let loadSucceeded = false;
|
|
274
289
|
try {
|
|
275
290
|
const loadedConfig = await loadZudokuConfigWithMeta(rootDir);
|
|
276
291
|
const manifestEnv = resolveManifestEnv(configEnv.mode);
|
|
@@ -313,6 +328,7 @@ export async function loadZudokuConfig(
|
|
|
313
328
|
);
|
|
314
329
|
}
|
|
315
330
|
|
|
331
|
+
loadSucceeded = true;
|
|
316
332
|
return { config, envPrefix, publicEnv };
|
|
317
333
|
} catch (error) {
|
|
318
334
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -324,7 +340,13 @@ export async function loadZudokuConfig(
|
|
|
324
340
|
|
|
325
341
|
throw new Error(errorMessage, { cause: error });
|
|
326
342
|
} finally {
|
|
327
|
-
|
|
343
|
+
if (loadSucceeded) {
|
|
344
|
+
await updateModifiedTimes();
|
|
345
|
+
} else {
|
|
346
|
+
// A watcher may have observed a file while it was being replaced.
|
|
347
|
+
// Retry on the next request instead of caching the failed file mtimes.
|
|
348
|
+
modifiedTimes = undefined;
|
|
349
|
+
}
|
|
328
350
|
}
|
|
329
351
|
}
|
|
330
352
|
|
|
@@ -28,11 +28,33 @@ export type ApitogoManifestEnv = keyof typeof APITOGO_MANIFEST_ENV_FILES;
|
|
|
28
28
|
|
|
29
29
|
const BillingPeriodSchema = z.enum(["month", "year", "monthly", "annual"]);
|
|
30
30
|
|
|
31
|
+
const PriceCurrencySchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.regex(
|
|
36
|
+
/^[a-z]{3}$/,
|
|
37
|
+
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export const ALLOWED_RUNTIME_VERSIONS = [
|
|
41
|
+
"DOTNET_10",
|
|
42
|
+
"DOTNET_9",
|
|
43
|
+
"DOTNET_8",
|
|
44
|
+
"NODE_24_LTS",
|
|
45
|
+
"NODE_22_LTS",
|
|
46
|
+
"PYTHON_314",
|
|
47
|
+
"PYTHON_313",
|
|
48
|
+
"PYTHON_312",
|
|
49
|
+
"PYTHON_311",
|
|
50
|
+
"PYTHON_310",
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
31
53
|
export const ManifestPlanInputSchema = z.object({
|
|
32
54
|
sku: z.string().min(1),
|
|
33
55
|
displayName: z.string().min(1),
|
|
34
56
|
priceAmount: z.number().default(0),
|
|
35
|
-
priceCurrency:
|
|
57
|
+
priceCurrency: PriceCurrencySchema.optional(),
|
|
36
58
|
billingPeriod: BillingPeriodSchema.optional(),
|
|
37
59
|
limitsJson: z.string().optional(),
|
|
38
60
|
includedActionKeys: z.array(z.string()).optional(),
|
|
@@ -90,8 +112,23 @@ export const DevPortalLocalManifestSchema = z.object({
|
|
|
90
112
|
sourceDirectory: z.string().optional(),
|
|
91
113
|
publishDirectory: z.string().optional(),
|
|
92
114
|
openApiPath: z.string().optional(),
|
|
93
|
-
runtime: z.string().optional(),
|
|
94
|
-
runtimeVersion: z
|
|
115
|
+
runtime: z.enum(["dotnet", "nodejs", "python"]).or(z.string()).optional(),
|
|
116
|
+
runtimeVersion: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.refine(
|
|
120
|
+
(value) =>
|
|
121
|
+
value === undefined ||
|
|
122
|
+
(ALLOWED_RUNTIME_VERSIONS as readonly string[]).includes(
|
|
123
|
+
value.toUpperCase(),
|
|
124
|
+
) ||
|
|
125
|
+
/^(8(\.0)?|9(\.0)?|10(\.0)?|net8\.0|net9\.0|net10\.0|24|22|3\.(10|11|12|13|14))$/i.test(
|
|
126
|
+
value,
|
|
127
|
+
),
|
|
128
|
+
{
|
|
129
|
+
message: `runtimeVersion must be a platform slug (${ALLOWED_RUNTIME_VERSIONS.join(", ")}) or a known alias such as 8.0`,
|
|
130
|
+
},
|
|
131
|
+
),
|
|
95
132
|
deployed: z.boolean().optional(),
|
|
96
133
|
})
|
|
97
134
|
.optional(),
|
|
@@ -262,7 +262,15 @@ const PlanCatalogItemSchema = z.object({
|
|
|
262
262
|
sku: z.string(),
|
|
263
263
|
displayName: z.string(),
|
|
264
264
|
priceAmount: z.number().default(0),
|
|
265
|
-
priceCurrency: z
|
|
265
|
+
priceCurrency: z
|
|
266
|
+
.string()
|
|
267
|
+
.trim()
|
|
268
|
+
.toLowerCase()
|
|
269
|
+
.regex(
|
|
270
|
+
/^[a-z]{3}$/,
|
|
271
|
+
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)",
|
|
272
|
+
)
|
|
273
|
+
.optional(),
|
|
266
274
|
billingPeriod: z.string().optional(),
|
|
267
275
|
limitsJson: z.string().optional(),
|
|
268
276
|
includedActionKeys: z.array(z.string()).optional(),
|