@lukoweb/apitogo 0.1.63 → 0.1.65
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 +72 -10
- package/dist/declarations/config/apitogo-config-core.d.ts +1 -0
- package/docs/configuration/authentication.md +3 -3
- package/docs/configuration/manifest.md +12 -12
- package/docs/guides/environment-variables.md +1 -1
- package/package.json +1 -1
- package/src/config/apitogo-config-core.ts +87 -6
- package/src/config/local-manifest.ts +7 -2
- package/src/config/validators/ModulesSchema.ts +4 -1
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.hasOwn(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().trim().toLowerCase().regex(
|
|
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()
|
|
@@ -999,7 +1056,10 @@ var init_local_manifest = __esm({
|
|
|
999
1056
|
init_apitogo_config_loader();
|
|
1000
1057
|
init_landing_manifest();
|
|
1001
1058
|
BillingPeriodSchema = z4.enum(["month", "year", "monthly", "annual"]);
|
|
1002
|
-
PriceCurrencySchema = z4.string().trim().toLowerCase().regex(
|
|
1059
|
+
PriceCurrencySchema = z4.string().trim().toLowerCase().regex(
|
|
1060
|
+
/^[a-z]{3}$/,
|
|
1061
|
+
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)"
|
|
1062
|
+
);
|
|
1003
1063
|
ALLOWED_RUNTIME_VERSIONS = [
|
|
1004
1064
|
"DOTNET_10",
|
|
1005
1065
|
"DOTNET_9",
|
|
@@ -1057,7 +1117,9 @@ var init_local_manifest = __esm({
|
|
|
1057
1117
|
openApiPath: z4.string().optional(),
|
|
1058
1118
|
runtime: z4.enum(["dotnet", "nodejs", "python"]).or(z4.string()).optional(),
|
|
1059
1119
|
runtimeVersion: z4.string().optional().refine(
|
|
1060
|
-
(value) => value === void 0 || ALLOWED_RUNTIME_VERSIONS.includes(
|
|
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(
|
|
1061
1123
|
value
|
|
1062
1124
|
),
|
|
1063
1125
|
{
|
|
@@ -5273,7 +5335,7 @@ import {
|
|
|
5273
5335
|
// package.json
|
|
5274
5336
|
var package_default = {
|
|
5275
5337
|
name: "@lukoweb/apitogo",
|
|
5276
|
-
version: "0.1.
|
|
5338
|
+
version: "0.1.65",
|
|
5277
5339
|
type: "module",
|
|
5278
5340
|
sideEffects: [
|
|
5279
5341
|
"**/*.css",
|
|
@@ -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;
|
|
@@ -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
|
|
73
|
+
| OIDC and Stripe for publish | `.env` 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
|
|
@@ -9,7 +9,7 @@ metadata. Code-only pieces (plugins, custom components) stay in `apitogo.config.
|
|
|
9
9
|
| --------------------- | ----------------------------------------------------------- |
|
|
10
10
|
| `apitogo.config.json` | Committed defaults: site, modules, authentication, backend |
|
|
11
11
|
| `apitogo.config.tsx` | Plugins and other non-JSON configuration |
|
|
12
|
-
| `.env`
|
|
12
|
+
| `.env` | Public client vars + publish secrets via `APITOGO_CONFIG_*` |
|
|
13
13
|
|
|
14
14
|
## Canonical shape
|
|
15
15
|
|
|
@@ -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
|
-
Publish secrets (`.env
|
|
82
|
+
Publish secrets (uncomment in `.env`, 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`
|
|
@@ -26,7 +26,7 @@ environment-specific variables. See the
|
|
|
26
26
|
[Vite documentation](https://vite.dev/guide/env-and-mode.html#env-files) for more information on
|
|
27
27
|
supported files.
|
|
28
28
|
|
|
29
|
-
Here is an example of a `.env
|
|
29
|
+
Here is an example of a `.env` file:
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
APITOGO_PUBLIC_PAGE_TITLE=My Page Title
|
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,81 @@ 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 = segment === segment.toUpperCase() && /[A-Z]/.test(segment);
|
|
140
|
+
if (!isAllUpper) {
|
|
141
|
+
return segment;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (segment.includes("_")) {
|
|
145
|
+
return screamingSnakeToCamel(segment);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return COLLAPSED_UPPERCASE_ALIASES[segment] ?? segment.toLowerCase();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveExistingKey(
|
|
152
|
+
target: Record<string, unknown>,
|
|
153
|
+
key: string,
|
|
154
|
+
): string {
|
|
155
|
+
if (Object.hasOwn(target, key)) {
|
|
156
|
+
return key;
|
|
157
|
+
}
|
|
158
|
+
const lower = key.toLowerCase();
|
|
159
|
+
for (const existing of Object.keys(target)) {
|
|
160
|
+
if (existing.toLowerCase() === lower) {
|
|
161
|
+
return existing;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return key;
|
|
165
|
+
}
|
|
166
|
+
|
|
92
167
|
function setNestedValue(
|
|
93
168
|
target: Record<string, unknown>,
|
|
94
169
|
pathParts: string[],
|
|
@@ -100,10 +175,11 @@ function setNestedValue(
|
|
|
100
175
|
|
|
101
176
|
let current: Record<string, unknown> = target;
|
|
102
177
|
for (let index = 0; index < pathParts.length - 1; index += 1) {
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
178
|
+
const rawKey = pathParts[index];
|
|
179
|
+
if (!rawKey) {
|
|
105
180
|
return;
|
|
106
181
|
}
|
|
182
|
+
const key = resolveExistingKey(current, rawKey);
|
|
107
183
|
const next = current[key];
|
|
108
184
|
if (
|
|
109
185
|
next === undefined ||
|
|
@@ -116,11 +192,12 @@ function setNestedValue(
|
|
|
116
192
|
current = current[key] as Record<string, unknown>;
|
|
117
193
|
}
|
|
118
194
|
|
|
119
|
-
const
|
|
120
|
-
if (!
|
|
195
|
+
const rawLeaf = pathParts[pathParts.length - 1];
|
|
196
|
+
if (!rawLeaf) {
|
|
121
197
|
return;
|
|
122
198
|
}
|
|
123
199
|
|
|
200
|
+
const leafKey = resolveExistingKey(current, rawLeaf);
|
|
124
201
|
current[leafKey] = value;
|
|
125
202
|
}
|
|
126
203
|
|
|
@@ -144,7 +221,11 @@ export function applyEnvOverrides(
|
|
|
144
221
|
continue;
|
|
145
222
|
}
|
|
146
223
|
|
|
147
|
-
const pathParts = key
|
|
224
|
+
const pathParts = key
|
|
225
|
+
.slice(ENV_PREFIX.length)
|
|
226
|
+
.split("__")
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.map(normalizeEnvPathSegment);
|
|
148
229
|
|
|
149
230
|
if (pathParts.length === 0) {
|
|
150
231
|
continue;
|
|
@@ -32,7 +32,10 @@ const PriceCurrencySchema = z
|
|
|
32
32
|
.string()
|
|
33
33
|
.trim()
|
|
34
34
|
.toLowerCase()
|
|
35
|
-
.regex(
|
|
35
|
+
.regex(
|
|
36
|
+
/^[a-z]{3}$/,
|
|
37
|
+
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)",
|
|
38
|
+
);
|
|
36
39
|
|
|
37
40
|
export const ALLOWED_RUNTIME_VERSIONS = [
|
|
38
41
|
"DOTNET_10",
|
|
@@ -116,7 +119,9 @@ export const DevPortalLocalManifestSchema = z.object({
|
|
|
116
119
|
.refine(
|
|
117
120
|
(value) =>
|
|
118
121
|
value === undefined ||
|
|
119
|
-
(ALLOWED_RUNTIME_VERSIONS as readonly string[]).includes(
|
|
122
|
+
(ALLOWED_RUNTIME_VERSIONS as readonly string[]).includes(
|
|
123
|
+
value.toUpperCase(),
|
|
124
|
+
) ||
|
|
120
125
|
/^(8(\.0)?|9(\.0)?|10(\.0)?|net8\.0|net9\.0|net10\.0|24|22|3\.(10|11|12|13|14))$/i.test(
|
|
121
126
|
value,
|
|
122
127
|
),
|
|
@@ -266,7 +266,10 @@ const PlanCatalogItemSchema = z.object({
|
|
|
266
266
|
.string()
|
|
267
267
|
.trim()
|
|
268
268
|
.toLowerCase()
|
|
269
|
-
.regex(
|
|
269
|
+
.regex(
|
|
270
|
+
/^[a-z]{3}$/,
|
|
271
|
+
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)",
|
|
272
|
+
)
|
|
270
273
|
.optional(),
|
|
271
274
|
billingPeriod: z.string().optional(),
|
|
272
275
|
limitsJson: z.string().optional(),
|