@lukoweb/apitogo 0.1.97 → 0.1.99
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 +245 -46
- package/dist/declarations/cli/platform/helpers/dev-portal-manifest.d.ts +3 -0
- package/dist/declarations/cli/platform/helpers/publish-preflight.d.ts +3 -0
- package/dist/declarations/cli/platform/tools/gateway.d.ts +5 -0
- package/dist/declarations/config/gateway-limits.d.ts +29 -0
- package/dist/declarations/config/local-manifest.d.ts +32 -0
- package/dist/declarations/config/validators/ModulesSchema.d.ts +40 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +10 -0
- package/dist/flat-config.d.ts +13 -0
- package/docs/configuration/plans.md +12 -5
- package/package.json +1 -1
- package/src/config/apitogo-config-core.ts +20 -1
- package/src/config/gateway-limits.ts +123 -0
- package/src/config/local-manifest.ts +44 -4
- package/src/config/validators/ModulesSchema.ts +19 -0
package/dist/cli/cli.js
CHANGED
|
@@ -8,6 +8,88 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/config/gateway-limits.ts
|
|
12
|
+
function isGatewayLimitsObject(value) {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function parseLimitsJson(limitsJson) {
|
|
16
|
+
if (!limitsJson?.trim()) {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(limitsJson);
|
|
21
|
+
return isGatewayLimitsObject(parsed) ? parsed : void 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function coerceGatewayLimits(source) {
|
|
27
|
+
if (isGatewayLimitsObject(source.limits)) {
|
|
28
|
+
return source.limits;
|
|
29
|
+
}
|
|
30
|
+
return parseLimitsJson(
|
|
31
|
+
typeof source.limitsJson === "string" ? source.limitsJson : void 0
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function stringifyGatewayLimits(limits) {
|
|
35
|
+
if (!isGatewayLimitsObject(limits)) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(limits);
|
|
39
|
+
}
|
|
40
|
+
function planForConfig(plan) {
|
|
41
|
+
const limits = coerceGatewayLimits(plan);
|
|
42
|
+
const next = { ...plan };
|
|
43
|
+
delete next.limitsJson;
|
|
44
|
+
if (limits) {
|
|
45
|
+
next.limits = limits;
|
|
46
|
+
} else {
|
|
47
|
+
delete next.limits;
|
|
48
|
+
}
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
function gatewayForConfig(gateway) {
|
|
52
|
+
return planForConfig(gateway);
|
|
53
|
+
}
|
|
54
|
+
function mergeLimitsSource(prior, patch) {
|
|
55
|
+
const merged = prior ? { ...prior, ...patch } : { ...patch };
|
|
56
|
+
const patchSource = patch;
|
|
57
|
+
if (patchSource.limits === void 0 && patchSource.limitsJson !== void 0) {
|
|
58
|
+
delete merged.limits;
|
|
59
|
+
}
|
|
60
|
+
return planForConfig(merged);
|
|
61
|
+
}
|
|
62
|
+
function planLimitsForPlatform(plan) {
|
|
63
|
+
return stringifyGatewayLimits(coerceGatewayLimits(plan));
|
|
64
|
+
}
|
|
65
|
+
function validateLimitsField(value, field) {
|
|
66
|
+
if (value == null || value === "") {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
if (!value.trim()) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(value);
|
|
75
|
+
if (!isGatewayLimitsObject(parsed)) {
|
|
76
|
+
return [{ field, message: `${field} must be a JSON object.` }];
|
|
77
|
+
}
|
|
78
|
+
return [];
|
|
79
|
+
} catch {
|
|
80
|
+
return [{ field, message: `${field} is not valid JSON.` }];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (isGatewayLimitsObject(value)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return [{ field, message: `${field} must be a JSON object.` }];
|
|
87
|
+
}
|
|
88
|
+
var init_gateway_limits = __esm({
|
|
89
|
+
"src/config/gateway-limits.ts"() {
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
11
93
|
// src/cli/common/package-json.ts
|
|
12
94
|
import { readFileSync } from "node:fs";
|
|
13
95
|
import path19 from "node:path";
|
|
@@ -328,7 +410,7 @@ function mergePlansBySku2(existing, patch) {
|
|
|
328
410
|
continue;
|
|
329
411
|
}
|
|
330
412
|
const prior = bySku.get(plan.sku);
|
|
331
|
-
bySku.set(plan.sku, prior
|
|
413
|
+
bySku.set(plan.sku, mergeLimitsSource(prior, plan));
|
|
332
414
|
}
|
|
333
415
|
return Array.from(bySku.values());
|
|
334
416
|
}
|
|
@@ -355,6 +437,19 @@ function migratePlanCatalog(normalized) {
|
|
|
355
437
|
}
|
|
356
438
|
delete normalized.plans;
|
|
357
439
|
}
|
|
440
|
+
function migratePlanLimits(normalized) {
|
|
441
|
+
const modules = asRecord2(normalized.modules);
|
|
442
|
+
const plansModule = asRecord2(asRecord2(modules?.userPanel)?.plans);
|
|
443
|
+
if (Array.isArray(plansModule?.items)) {
|
|
444
|
+
plansModule.items = plansModule.items.map(
|
|
445
|
+
(plan) => planForConfig(plan)
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
const gateway = asRecord2(normalized.gateway);
|
|
449
|
+
if (gateway) {
|
|
450
|
+
normalized.gateway = gatewayForConfig(gateway);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
358
453
|
function normalizeApitogoConfig(config2) {
|
|
359
454
|
const normalized = structuredClone(stripRuntimeConfigFields(config2));
|
|
360
455
|
const title = readSiteTitle2(normalized);
|
|
@@ -401,6 +496,7 @@ function normalizeApitogoConfig(config2) {
|
|
|
401
496
|
delete normalized.branding;
|
|
402
497
|
delete normalized.projectName;
|
|
403
498
|
migratePlanCatalog(normalized);
|
|
499
|
+
migratePlanLimits(normalized);
|
|
404
500
|
syncBillingEnabled(normalized);
|
|
405
501
|
syncAuthentication(normalized);
|
|
406
502
|
return normalized;
|
|
@@ -597,6 +693,7 @@ function extractSiteConfig(config2) {
|
|
|
597
693
|
var APITOGO_CONFIG_FILENAME2, ENV_PREFIX, DERIVED_MANIFEST_KEYS, SITE_CONFIG_KEYS, COLLAPSED_UPPERCASE_ALIASES2;
|
|
598
694
|
var init_apitogo_config_core = __esm({
|
|
599
695
|
"src/config/apitogo-config-core.ts"() {
|
|
696
|
+
init_gateway_limits();
|
|
600
697
|
APITOGO_CONFIG_FILENAME2 = "apitogo.config.json";
|
|
601
698
|
ENV_PREFIX = "APITOGO_CONFIG_";
|
|
602
699
|
DERIVED_MANIFEST_KEYS = [
|
|
@@ -640,6 +737,7 @@ var init_apitogo_config_core = __esm({
|
|
|
640
737
|
PRICECURRENCY: "priceCurrency",
|
|
641
738
|
BILLINGPERIOD: "billingPeriod",
|
|
642
739
|
LIMITSJSON: "limitsJson",
|
|
740
|
+
LIMITS: "limits",
|
|
643
741
|
INCLUDEDACTIONKEYS: "includedActionKeys",
|
|
644
742
|
SOURCEDIRECTORY: "sourceDirectory",
|
|
645
743
|
PUBLISHDIRECTORY: "publishDirectory",
|
|
@@ -925,6 +1023,16 @@ var init_ModulesSchema = __esm({
|
|
|
925
1023
|
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)"
|
|
926
1024
|
).optional(),
|
|
927
1025
|
billingPeriod: z3.string().optional(),
|
|
1026
|
+
limits: z3.object({
|
|
1027
|
+
rateLimit: z3.object({
|
|
1028
|
+
calls: z3.number(),
|
|
1029
|
+
renewalPeriodSeconds: z3.number().optional()
|
|
1030
|
+
}).passthrough().optional(),
|
|
1031
|
+
quota: z3.object({
|
|
1032
|
+
calls: z3.number(),
|
|
1033
|
+
renewalPeriodDays: z3.number().optional()
|
|
1034
|
+
}).passthrough().optional()
|
|
1035
|
+
}).passthrough().optional(),
|
|
928
1036
|
limitsJson: z3.string().optional(),
|
|
929
1037
|
includedActionKeys: z3.array(z3.string()).optional()
|
|
930
1038
|
});
|
|
@@ -1019,7 +1127,8 @@ function plansToPreviewCatalog(plans) {
|
|
|
1019
1127
|
priceAmount: plan.priceAmount ?? 0,
|
|
1020
1128
|
priceCurrency: plan.priceCurrency ?? "usd",
|
|
1021
1129
|
billingPeriod: normalizeBillingPeriod2(plan.billingPeriod),
|
|
1022
|
-
|
|
1130
|
+
limits: coerceGatewayLimits(plan) ?? null,
|
|
1131
|
+
limitsJson: planLimitsForPlatform(plan) ?? null,
|
|
1023
1132
|
includedActionKeys: plan.includedActionKeys ?? []
|
|
1024
1133
|
}))
|
|
1025
1134
|
};
|
|
@@ -1064,12 +1173,23 @@ async function readEffectiveManifest(rootDir, _env) {
|
|
|
1064
1173
|
return null;
|
|
1065
1174
|
}
|
|
1066
1175
|
}
|
|
1067
|
-
var BillingPeriodSchema, PriceCurrencySchema, ALLOWED_RUNTIME_VERSIONS2, ManifestPlanInputSchema, DevPortalLocalManifestSchema;
|
|
1176
|
+
var BillingPeriodSchema, GatewayLimitsSchema, PriceCurrencySchema, ALLOWED_RUNTIME_VERSIONS2, ManifestPlanInputSchema, DevPortalLocalManifestSchema;
|
|
1068
1177
|
var init_local_manifest = __esm({
|
|
1069
1178
|
"src/config/local-manifest.ts"() {
|
|
1179
|
+
init_gateway_limits();
|
|
1070
1180
|
init_apitogo_config_loader();
|
|
1071
1181
|
init_landing_manifest();
|
|
1072
1182
|
BillingPeriodSchema = z4.enum(["month", "year", "monthly", "annual"]);
|
|
1183
|
+
GatewayLimitsSchema = z4.object({
|
|
1184
|
+
rateLimit: z4.object({
|
|
1185
|
+
calls: z4.number(),
|
|
1186
|
+
renewalPeriodSeconds: z4.number().optional()
|
|
1187
|
+
}).passthrough().optional(),
|
|
1188
|
+
quota: z4.object({
|
|
1189
|
+
calls: z4.number(),
|
|
1190
|
+
renewalPeriodDays: z4.number().optional()
|
|
1191
|
+
}).passthrough().optional()
|
|
1192
|
+
}).passthrough();
|
|
1073
1193
|
PriceCurrencySchema = z4.string().trim().toLowerCase().regex(
|
|
1074
1194
|
/^[a-z]{3}$/,
|
|
1075
1195
|
"priceCurrency must be a 3-letter ISO 4217 code (lowercase)"
|
|
@@ -1092,6 +1212,8 @@ var init_local_manifest = __esm({
|
|
|
1092
1212
|
priceAmount: z4.number().default(0),
|
|
1093
1213
|
priceCurrency: PriceCurrencySchema.optional(),
|
|
1094
1214
|
billingPeriod: BillingPeriodSchema.optional(),
|
|
1215
|
+
limits: GatewayLimitsSchema.optional(),
|
|
1216
|
+
/** @deprecated Use `limits`. Encoded string still accepted on load. */
|
|
1095
1217
|
limitsJson: z4.string().optional(),
|
|
1096
1218
|
includedActionKeys: z4.array(z4.string()).optional()
|
|
1097
1219
|
});
|
|
@@ -1147,6 +1269,8 @@ var init_local_manifest = __esm({
|
|
|
1147
1269
|
oidcMetadataUrl: z4.string().optional(),
|
|
1148
1270
|
oidcAllowedIssuers: z4.array(z4.string()).optional(),
|
|
1149
1271
|
oidcAudiences: z4.array(z4.string()).optional(),
|
|
1272
|
+
limits: GatewayLimitsSchema.optional(),
|
|
1273
|
+
/** @deprecated Use `limits`. Encoded string still accepted on load. */
|
|
1150
1274
|
limitsJson: z4.string().optional(),
|
|
1151
1275
|
publicHealthActionKey: z4.string().optional(),
|
|
1152
1276
|
publicRoutes: z4.array(z4.string()).optional(),
|
|
@@ -5961,6 +6085,9 @@ function resolveDevPortalDeployConfig(config2) {
|
|
|
5961
6085
|
return { endpointUrl, apiKey, defaultEnv };
|
|
5962
6086
|
}
|
|
5963
6087
|
|
|
6088
|
+
// src/cli/platform/tools/dev-portal-publish.ts
|
|
6089
|
+
init_gateway_limits();
|
|
6090
|
+
|
|
5964
6091
|
// src/cli/platform/helpers/dev-portal-manifest.ts
|
|
5965
6092
|
import { existsSync } from "node:fs";
|
|
5966
6093
|
import { access, mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
@@ -6017,6 +6144,7 @@ function readLandingFromConfig(config2) {
|
|
|
6017
6144
|
}
|
|
6018
6145
|
|
|
6019
6146
|
// src/cli/platform/helpers/dev-portal-manifest.ts
|
|
6147
|
+
init_gateway_limits();
|
|
6020
6148
|
var APITOGO_CONFIG_FILENAME = "apitogo.config.json";
|
|
6021
6149
|
var PLACEHOLDER_DEV_PORTAL_API_URL = "https://dp-example.azurecontainerapps.io";
|
|
6022
6150
|
function isPlanFree(plan) {
|
|
@@ -6037,11 +6165,11 @@ function readPlanItemsFromConfig(config2) {
|
|
|
6037
6165
|
const plansModule = userPanel?.plans;
|
|
6038
6166
|
const items = plansModule?.items;
|
|
6039
6167
|
if (Array.isArray(items) && items.length) {
|
|
6040
|
-
return items;
|
|
6168
|
+
return items.map((item) => planForConfig(item));
|
|
6041
6169
|
}
|
|
6042
6170
|
const rootPlans = config2.plans;
|
|
6043
6171
|
if (Array.isArray(rootPlans) && rootPlans.length) {
|
|
6044
|
-
return rootPlans;
|
|
6172
|
+
return rootPlans.map((item) => planForConfig(item));
|
|
6045
6173
|
}
|
|
6046
6174
|
return void 0;
|
|
6047
6175
|
}
|
|
@@ -6063,7 +6191,7 @@ function applyPlansToUserPanel(config2, plans, pricingEnabled) {
|
|
|
6063
6191
|
const userPanel = modules.userPanel ?? {};
|
|
6064
6192
|
const plansModule = userPanel.plans ?? {};
|
|
6065
6193
|
if (plans.length > 0) {
|
|
6066
|
-
plansModule.items = plans;
|
|
6194
|
+
plansModule.items = plans.map((plan) => planForConfig(plan));
|
|
6067
6195
|
if (pricingEnabled !== false) {
|
|
6068
6196
|
plansModule.enabled = plansModule.enabled ?? true;
|
|
6069
6197
|
}
|
|
@@ -6177,6 +6305,9 @@ function extractManifestFromConfig(config2) {
|
|
|
6177
6305
|
manifest[key] = config2[key];
|
|
6178
6306
|
}
|
|
6179
6307
|
}
|
|
6308
|
+
if (manifest.gateway) {
|
|
6309
|
+
manifest.gateway = gatewayForConfig(manifest.gateway);
|
|
6310
|
+
}
|
|
6180
6311
|
const plans = readPlanItemsFromConfig(config2);
|
|
6181
6312
|
if (plans) {
|
|
6182
6313
|
manifest.plans = plans;
|
|
@@ -6210,7 +6341,7 @@ function applyManifestToConfig(config2, manifest) {
|
|
|
6210
6341
|
for (const key of STORED_MANIFEST_KEYS) {
|
|
6211
6342
|
const value = manifest[key];
|
|
6212
6343
|
if (value !== void 0) {
|
|
6213
|
-
next[key] = value;
|
|
6344
|
+
next[key] = key === "gateway" ? gatewayForConfig(value) : value;
|
|
6214
6345
|
} else {
|
|
6215
6346
|
delete next[key];
|
|
6216
6347
|
}
|
|
@@ -6262,9 +6393,9 @@ var DEFAULT_PLANS = [
|
|
|
6262
6393
|
sku: "free",
|
|
6263
6394
|
displayName: "Free",
|
|
6264
6395
|
priceAmount: 0,
|
|
6265
|
-
|
|
6396
|
+
limits: {
|
|
6266
6397
|
rateLimit: { calls: 10, renewalPeriodSeconds: 60 }
|
|
6267
|
-
}
|
|
6398
|
+
},
|
|
6268
6399
|
includedActionKeys: []
|
|
6269
6400
|
},
|
|
6270
6401
|
{
|
|
@@ -6272,10 +6403,10 @@ var DEFAULT_PLANS = [
|
|
|
6272
6403
|
displayName: "Pro",
|
|
6273
6404
|
priceAmount: 9.99,
|
|
6274
6405
|
billingPeriod: "month",
|
|
6275
|
-
|
|
6406
|
+
limits: {
|
|
6276
6407
|
rateLimit: { calls: 100, renewalPeriodSeconds: 60 },
|
|
6277
6408
|
quota: { calls: 5e4, renewalPeriodDays: 1 }
|
|
6278
|
-
}
|
|
6409
|
+
},
|
|
6279
6410
|
includedActionKeys: []
|
|
6280
6411
|
}
|
|
6281
6412
|
];
|
|
@@ -6472,7 +6603,7 @@ function mergePlansBySku(existing, patch) {
|
|
|
6472
6603
|
}
|
|
6473
6604
|
for (const plan of patch) {
|
|
6474
6605
|
const prior = bySku.get(plan.sku);
|
|
6475
|
-
bySku.set(plan.sku, prior
|
|
6606
|
+
bySku.set(plan.sku, mergeLimitsSource(prior, plan));
|
|
6476
6607
|
}
|
|
6477
6608
|
return Array.from(bySku.values());
|
|
6478
6609
|
}
|
|
@@ -6564,6 +6695,7 @@ async function writeManifest(siteDirectory, manifest) {
|
|
|
6564
6695
|
}
|
|
6565
6696
|
|
|
6566
6697
|
// src/cli/platform/helpers/publish-preflight.ts
|
|
6698
|
+
init_gateway_limits();
|
|
6567
6699
|
var ALLOWED_RUNTIME_VERSIONS = [
|
|
6568
6700
|
"DOTNET_10",
|
|
6569
6701
|
"DOTNET_9",
|
|
@@ -6683,20 +6815,6 @@ function normalizeRuntime(runtime) {
|
|
|
6683
6815
|
if (raw === "python") return "python";
|
|
6684
6816
|
throw new Error(`runtime must be one of: dotnet, nodejs, python; got '${runtime}'.`);
|
|
6685
6817
|
}
|
|
6686
|
-
function validateLimitsJson(limitsJson, field) {
|
|
6687
|
-
if (!limitsJson?.trim()) {
|
|
6688
|
-
return [];
|
|
6689
|
-
}
|
|
6690
|
-
try {
|
|
6691
|
-
const parsed = JSON.parse(limitsJson);
|
|
6692
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6693
|
-
return [{ field, message: `${field} must be a JSON object.` }];
|
|
6694
|
-
}
|
|
6695
|
-
return [];
|
|
6696
|
-
} catch {
|
|
6697
|
-
return [{ field, message: `${field} is not valid JSON.` }];
|
|
6698
|
-
}
|
|
6699
|
-
}
|
|
6700
6818
|
function normalizePlanForGatewayApi(plan) {
|
|
6701
6819
|
return {
|
|
6702
6820
|
...plan,
|
|
@@ -6811,7 +6929,12 @@ function validateManifestForPlatformPublish(manifest) {
|
|
|
6811
6929
|
});
|
|
6812
6930
|
}
|
|
6813
6931
|
}
|
|
6814
|
-
issues.push(
|
|
6932
|
+
issues.push(
|
|
6933
|
+
...validateLimitsField(
|
|
6934
|
+
plan.limits ?? plan.limitsJson,
|
|
6935
|
+
`plans[${index}].limits`
|
|
6936
|
+
)
|
|
6937
|
+
);
|
|
6815
6938
|
}
|
|
6816
6939
|
}
|
|
6817
6940
|
if (manifest.backend?.sourceDirectory?.trim() || manifest.backend?.runtimeVersion) {
|
|
@@ -6846,8 +6969,13 @@ function validateManifestForPlatformPublish(manifest) {
|
|
|
6846
6969
|
});
|
|
6847
6970
|
}
|
|
6848
6971
|
}
|
|
6849
|
-
if (manifest.gateway?.limitsJson) {
|
|
6850
|
-
issues.push(
|
|
6972
|
+
if (manifest.gateway?.limits ?? manifest.gateway?.limitsJson) {
|
|
6973
|
+
issues.push(
|
|
6974
|
+
...validateLimitsField(
|
|
6975
|
+
manifest.gateway.limits ?? manifest.gateway.limitsJson,
|
|
6976
|
+
"gateway.limits"
|
|
6977
|
+
)
|
|
6978
|
+
);
|
|
6851
6979
|
}
|
|
6852
6980
|
return {
|
|
6853
6981
|
issues,
|
|
@@ -6874,6 +7002,7 @@ var COLLAPSED_UPPERCASE_ALIASES = {
|
|
|
6874
7002
|
PRICECURRENCY: "priceCurrency",
|
|
6875
7003
|
BILLINGPERIOD: "billingPeriod",
|
|
6876
7004
|
LIMITSJSON: "limitsJson",
|
|
7005
|
+
LIMITS: "limits",
|
|
6877
7006
|
INCLUDEDACTIONKEYS: "includedActionKeys",
|
|
6878
7007
|
SOURCEDIRECTORY: "sourceDirectory",
|
|
6879
7008
|
PUBLISHDIRECTORY: "publishDirectory",
|
|
@@ -7246,6 +7375,9 @@ function formatDevPortalBackendResponse(projectId, backend) {
|
|
|
7246
7375
|
};
|
|
7247
7376
|
}
|
|
7248
7377
|
|
|
7378
|
+
// src/cli/platform/helpers/gateway-plan-sync.ts
|
|
7379
|
+
init_gateway_limits();
|
|
7380
|
+
|
|
7249
7381
|
// src/cli/platform/tools/gateway.ts
|
|
7250
7382
|
var DEFAULT_BASELINE_LIMITS_JSON = JSON.stringify({
|
|
7251
7383
|
rateLimit: { calls: 100, renewalPeriodSeconds: 60 },
|
|
@@ -7393,13 +7525,33 @@ async function listAllGatewayActions(client, projectId, pageSize = 100) {
|
|
|
7393
7525
|
}
|
|
7394
7526
|
return items;
|
|
7395
7527
|
}
|
|
7528
|
+
async function resolveIncludedActionKeysForCreate(client, projectId, includedActionKeys, options) {
|
|
7529
|
+
if (options?.skipDefaultActions) {
|
|
7530
|
+
return includedActionKeys ?? [];
|
|
7531
|
+
}
|
|
7532
|
+
if (includedActionKeys && includedActionKeys.length > 0) {
|
|
7533
|
+
return includedActionKeys;
|
|
7534
|
+
}
|
|
7535
|
+
try {
|
|
7536
|
+
const actions = await listAllGatewayActions(client, projectId);
|
|
7537
|
+
return actions.map((action) => action.actionKey);
|
|
7538
|
+
} catch {
|
|
7539
|
+
return includedActionKeys ?? [];
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7396
7542
|
async function handleCreateGatewayPlan(client, config2, args) {
|
|
7397
7543
|
const projectId = resolveProjectId(args.projectId, config2);
|
|
7544
|
+
const includedActionKeys = await resolveIncludedActionKeysForCreate(
|
|
7545
|
+
client,
|
|
7546
|
+
projectId,
|
|
7547
|
+
args.includedActionKeys,
|
|
7548
|
+
{ skipDefaultActions: args.skipDefaultActions }
|
|
7549
|
+
);
|
|
7398
7550
|
const body = {
|
|
7399
7551
|
sku: args.sku,
|
|
7400
7552
|
displayName: args.displayName,
|
|
7401
7553
|
limitsJson: args.limitsJson ?? null,
|
|
7402
|
-
includedActionKeys
|
|
7554
|
+
includedActionKeys,
|
|
7403
7555
|
priceAmount: args.priceAmount,
|
|
7404
7556
|
priceCurrency: args.priceCurrency,
|
|
7405
7557
|
billingPeriod: args.billingPeriod,
|
|
@@ -7593,6 +7745,14 @@ function resolveIncludedActionKeysForUpsert(manifestPlan, existingPlan) {
|
|
|
7593
7745
|
}
|
|
7594
7746
|
return [];
|
|
7595
7747
|
}
|
|
7748
|
+
async function resolveActionKeysOrAllProjectActions(manifestPlan, existingPlan, getAllKeys) {
|
|
7749
|
+
const resolved = resolveIncludedActionKeysForUpsert(manifestPlan, existingPlan);
|
|
7750
|
+
if (resolved.length > 0) {
|
|
7751
|
+
return resolved;
|
|
7752
|
+
}
|
|
7753
|
+
const allKeys = await getAllKeys();
|
|
7754
|
+
return allKeys.length > 0 ? allKeys : resolved;
|
|
7755
|
+
}
|
|
7596
7756
|
var GATEWAY_PLAN_SORT_TEMP_OFFSET = 1e3;
|
|
7597
7757
|
function buildGatewayPlanSortTargetsFromIds(manifestPlans, planIdsBySku, skuById) {
|
|
7598
7758
|
const targets = [];
|
|
@@ -7627,17 +7787,36 @@ async function applyGatewayPlanSortOrders(client, config2, projectId, targets) {
|
|
|
7627
7787
|
}
|
|
7628
7788
|
return results;
|
|
7629
7789
|
}
|
|
7790
|
+
async function listProjectActionKeys(client, projectId) {
|
|
7791
|
+
try {
|
|
7792
|
+
const actions = await listAllGatewayActions(client, projectId);
|
|
7793
|
+
return actions.map((action) => action.actionKey);
|
|
7794
|
+
} catch {
|
|
7795
|
+
return [];
|
|
7796
|
+
}
|
|
7797
|
+
}
|
|
7630
7798
|
async function upsertGatewayPlansFromManifest(client, config2, projectId, plans, options) {
|
|
7631
7799
|
const existing = await client.listGatewayPlans(projectId);
|
|
7632
7800
|
const bySku = new Map(existing.map((plan) => [plan.sku.toLowerCase(), plan]));
|
|
7633
7801
|
const results = [];
|
|
7634
7802
|
const planIdsBySku = /* @__PURE__ */ new Map();
|
|
7803
|
+
let cachedProjectActionKeys;
|
|
7804
|
+
const projectActionKeys = async () => {
|
|
7805
|
+
if (cachedProjectActionKeys === void 0) {
|
|
7806
|
+
cachedProjectActionKeys = await listProjectActionKeys(client, projectId);
|
|
7807
|
+
}
|
|
7808
|
+
return cachedProjectActionKeys;
|
|
7809
|
+
};
|
|
7635
7810
|
for (const [index, inputRaw] of plans.entries()) {
|
|
7636
7811
|
const input3 = normalizePlanForGatewayApi(inputRaw);
|
|
7637
7812
|
const found = bySku.get(input3.sku.toLowerCase());
|
|
7638
7813
|
if (found) {
|
|
7639
7814
|
if (options?.syncActionsOnly) {
|
|
7640
|
-
const actionKeys =
|
|
7815
|
+
const actionKeys = await resolveActionKeysOrAllProjectActions(
|
|
7816
|
+
input3,
|
|
7817
|
+
found,
|
|
7818
|
+
projectActionKeys
|
|
7819
|
+
);
|
|
7641
7820
|
if (actionKeys.length === 0) {
|
|
7642
7821
|
continue;
|
|
7643
7822
|
}
|
|
@@ -7655,13 +7834,17 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
|
|
|
7655
7834
|
planId: found.id,
|
|
7656
7835
|
displayName: input3.displayName,
|
|
7657
7836
|
isActive: true,
|
|
7658
|
-
limitsJson: input3
|
|
7837
|
+
limitsJson: planLimitsForPlatform(input3),
|
|
7659
7838
|
priceAmount: input3.priceAmount,
|
|
7660
7839
|
priceCurrency: input3.priceCurrency,
|
|
7661
7840
|
billingPeriod: input3.billingPeriod ? normalizeBillingPeriod(input3.billingPeriod) : void 0
|
|
7662
7841
|
};
|
|
7663
7842
|
if (!options?.deferActionKeys) {
|
|
7664
|
-
patchArgs.includedActionKeys =
|
|
7843
|
+
patchArgs.includedActionKeys = await resolveActionKeysOrAllProjectActions(
|
|
7844
|
+
input3,
|
|
7845
|
+
found,
|
|
7846
|
+
projectActionKeys
|
|
7847
|
+
);
|
|
7665
7848
|
}
|
|
7666
7849
|
const patchedJson = await handlePatchGatewayPlan(client, config2, patchArgs);
|
|
7667
7850
|
results.push(JSON.parse(patchedJson));
|
|
@@ -7678,8 +7861,9 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
|
|
|
7678
7861
|
priceAmount: input3.priceAmount,
|
|
7679
7862
|
priceCurrency: input3.priceCurrency ?? "usd",
|
|
7680
7863
|
billingPeriod: normalizeBillingPeriod(input3.billingPeriod),
|
|
7681
|
-
limitsJson: input3
|
|
7682
|
-
includedActionKeys: options?.deferActionKeys ? [] :
|
|
7864
|
+
limitsJson: planLimitsForPlatform(input3),
|
|
7865
|
+
includedActionKeys: options?.deferActionKeys ? [] : await resolveActionKeysOrAllProjectActions(input3, void 0, projectActionKeys),
|
|
7866
|
+
skipDefaultActions: true,
|
|
7683
7867
|
sortOrder: index
|
|
7684
7868
|
});
|
|
7685
7869
|
const created = JSON.parse(createdJson);
|
|
@@ -11251,6 +11435,7 @@ async function handleBuildDevPortalSite(client, config2, args, progress) {
|
|
|
11251
11435
|
import { readFile as readFile8 } from "node:fs/promises";
|
|
11252
11436
|
import { createRequire } from "node:module";
|
|
11253
11437
|
import path17 from "node:path";
|
|
11438
|
+
init_gateway_limits();
|
|
11254
11439
|
var cachedFallbackTs;
|
|
11255
11440
|
async function loadTypeScriptForSite(sourceDirectory) {
|
|
11256
11441
|
const require2 = createRequire(path17.join(path17.resolve(sourceDirectory), "package.json"));
|
|
@@ -11371,6 +11556,11 @@ async function validateSiteConfigStructure(config2, sourceDirectory) {
|
|
|
11371
11556
|
`"modules.userPanel.plans.items[${index}].sku" must be a non-empty string.`
|
|
11372
11557
|
);
|
|
11373
11558
|
}
|
|
11559
|
+
if (plan.limits !== void 0 && !isGatewayLimitsObject(plan.limits)) {
|
|
11560
|
+
errors.push(
|
|
11561
|
+
`"modules.userPanel.plans.items[${index}].limits" must be a JSON object.`
|
|
11562
|
+
);
|
|
11563
|
+
}
|
|
11374
11564
|
if (typeof plan.limitsJson === "string" && plan.limitsJson.trim()) {
|
|
11375
11565
|
try {
|
|
11376
11566
|
JSON.parse(plan.limitsJson);
|
|
@@ -11910,7 +12100,7 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
|
|
|
11910
12100
|
const enableGatewayJwt = Boolean(explicitAuthMode || explicitOidcUrl);
|
|
11911
12101
|
const baselineArgs = {
|
|
11912
12102
|
projectId,
|
|
11913
|
-
limitsJson: gateway
|
|
12103
|
+
limitsJson: planLimitsForPlatform(gateway),
|
|
11914
12104
|
publicHealthActionKey: gateway.publicHealthActionKey,
|
|
11915
12105
|
publicRoutes: gateway.publicRoutes,
|
|
11916
12106
|
waitForEntra: wait,
|
|
@@ -12610,7 +12800,7 @@ import {
|
|
|
12610
12800
|
// package.json
|
|
12611
12801
|
var package_default = {
|
|
12612
12802
|
name: "@lukoweb/apitogo",
|
|
12613
|
-
version: "0.1.
|
|
12803
|
+
version: "0.1.99",
|
|
12614
12804
|
type: "module",
|
|
12615
12805
|
sideEffects: [
|
|
12616
12806
|
"**/*.css",
|
|
@@ -19307,28 +19497,37 @@ function createDefaultApitogoJsonConfig(options) {
|
|
|
19307
19497
|
displayName: "Free",
|
|
19308
19498
|
priceAmount: 0,
|
|
19309
19499
|
billingPeriod: "month",
|
|
19310
|
-
|
|
19500
|
+
limits: {
|
|
19501
|
+
rateLimit: { calls: 10, renewalPeriodSeconds: 1 },
|
|
19502
|
+
quota: { calls: 1e3, renewalPeriodDays: 30 }
|
|
19503
|
+
}
|
|
19311
19504
|
},
|
|
19312
19505
|
{
|
|
19313
19506
|
sku: "developer",
|
|
19314
19507
|
displayName: "Developer",
|
|
19315
19508
|
priceAmount: 19,
|
|
19316
19509
|
billingPeriod: "month",
|
|
19317
|
-
|
|
19510
|
+
limits: {
|
|
19511
|
+
rateLimit: { calls: 50, renewalPeriodSeconds: 1 },
|
|
19512
|
+
quota: { calls: 5e4, renewalPeriodDays: 30 }
|
|
19513
|
+
}
|
|
19318
19514
|
},
|
|
19319
19515
|
{
|
|
19320
19516
|
sku: "team",
|
|
19321
19517
|
displayName: "Team",
|
|
19322
19518
|
priceAmount: 79,
|
|
19323
19519
|
billingPeriod: "month",
|
|
19324
|
-
|
|
19520
|
+
limits: {
|
|
19521
|
+
rateLimit: { calls: 200, renewalPeriodSeconds: 1 },
|
|
19522
|
+
quota: { calls: 5e5, renewalPeriodDays: 30 }
|
|
19523
|
+
}
|
|
19325
19524
|
},
|
|
19326
19525
|
{
|
|
19327
19526
|
sku: "enterprise",
|
|
19328
19527
|
displayName: "Enterprise",
|
|
19329
19528
|
priceAmount: -1,
|
|
19330
19529
|
billingPeriod: "custom",
|
|
19331
|
-
|
|
19530
|
+
limits: { quota: { calls: -1 } }
|
|
19332
19531
|
}
|
|
19333
19532
|
]
|
|
19334
19533
|
}
|
|
@@ -19512,11 +19711,11 @@ function slugifyPlanSku(displayName) {
|
|
|
19512
19711
|
const sku = displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
19513
19712
|
return sku || "plan";
|
|
19514
19713
|
}
|
|
19515
|
-
function
|
|
19516
|
-
return
|
|
19714
|
+
function buildLimits(rateLimitCalls, monthlyQuota) {
|
|
19715
|
+
return {
|
|
19517
19716
|
rateLimit: { calls: rateLimitCalls, renewalPeriodSeconds: 60 },
|
|
19518
19717
|
quota: { calls: monthlyQuota, renewalPeriodDays: 30 }
|
|
19519
|
-
}
|
|
19718
|
+
};
|
|
19520
19719
|
}
|
|
19521
19720
|
function buildPlanItemFromWizard(input3) {
|
|
19522
19721
|
const sku = input3.sku ?? slugifyPlanSku(input3.displayName);
|
|
@@ -19524,7 +19723,7 @@ function buildPlanItemFromWizard(input3) {
|
|
|
19524
19723
|
sku,
|
|
19525
19724
|
displayName: input3.displayName,
|
|
19526
19725
|
priceAmount: input3.priceAmount,
|
|
19527
|
-
|
|
19726
|
+
limits: buildLimits(input3.rateLimitCalls, input3.monthlyQuota)
|
|
19528
19727
|
};
|
|
19529
19728
|
if (input3.priceAmount > 0) {
|
|
19530
19729
|
item.priceCurrency = "usd";
|
|
@@ -19713,7 +19912,7 @@ function printPlanAddedHint(displayName) {
|
|
|
19713
19912
|
`Plan "${displayName}" added to modules.userPanel.plans.items in apitogo.config.json.`
|
|
19714
19913
|
);
|
|
19715
19914
|
printDiagnosticsToConsole(
|
|
19716
|
-
"
|
|
19915
|
+
"New plans include all API operations by default. Set includedActionKeys to restrict, and configure Stripe before publishing paid plans."
|
|
19717
19916
|
);
|
|
19718
19917
|
printDiagnosticsToConsole(`Docs: ${DOCS}/docs/configuration/plans`);
|
|
19719
19918
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LandingManifestInput } from "./dev-portal-landing.js";
|
|
2
|
+
import { type GatewayLimits } from "../../../config/gateway-limits.js";
|
|
2
3
|
export declare const APITOGO_CONFIG_FILENAME = "apitogo.config.json";
|
|
3
4
|
export declare const DEV_PORTAL_MANIFEST_FILENAME = "apitogo.local.json";
|
|
4
5
|
export declare const PLACEHOLDER_DEV_PORTAL_API_URL = "https://dp-example.azurecontainerapps.io";
|
|
@@ -8,6 +9,7 @@ export interface ManifestPlanInput {
|
|
|
8
9
|
priceAmount?: number;
|
|
9
10
|
priceCurrency?: string;
|
|
10
11
|
billingPeriod?: "month" | "year" | "monthly" | "annual";
|
|
12
|
+
limits?: GatewayLimits;
|
|
11
13
|
limitsJson?: string;
|
|
12
14
|
includedActionKeys?: string[];
|
|
13
15
|
}
|
|
@@ -55,6 +57,7 @@ export interface DevPortalLocalManifest {
|
|
|
55
57
|
oidcMetadataUrl?: string;
|
|
56
58
|
oidcAllowedIssuers?: string[];
|
|
57
59
|
oidcAudiences?: string[];
|
|
60
|
+
limits?: GatewayLimits;
|
|
58
61
|
limitsJson?: string;
|
|
59
62
|
publicHealthActionKey?: string;
|
|
60
63
|
publicRoutes?: string[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GatewayLimits } from "../../../config/gateway-limits.js";
|
|
1
2
|
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"];
|
|
2
3
|
export type AllowedRuntimeVersion = (typeof ALLOWED_RUNTIME_VERSIONS)[number];
|
|
3
4
|
export interface PublishPlanInput {
|
|
@@ -6,6 +7,7 @@ export interface PublishPlanInput {
|
|
|
6
7
|
priceAmount?: number;
|
|
7
8
|
priceCurrency?: string;
|
|
8
9
|
billingPeriod?: string;
|
|
10
|
+
limits?: GatewayLimits;
|
|
9
11
|
limitsJson?: string;
|
|
10
12
|
includedActionKeys?: string[];
|
|
11
13
|
}
|
|
@@ -21,6 +23,7 @@ export interface PublishManifestLike {
|
|
|
21
23
|
plans?: PublishPlanInput[];
|
|
22
24
|
backend?: PublishBackendInput;
|
|
23
25
|
gateway?: {
|
|
26
|
+
limits?: GatewayLimits;
|
|
24
27
|
limitsJson?: string;
|
|
25
28
|
};
|
|
26
29
|
}
|
|
@@ -28,9 +28,13 @@ export interface ApplyPublicRoutesResult {
|
|
|
28
28
|
export declare function applyGatewayPublicRoutes(client: PlatformApiClient, projectId: string, routes: string[], options?: {
|
|
29
29
|
knownActions?: GatewayActionDto[];
|
|
30
30
|
}): Promise<ApplyPublicRoutesResult>;
|
|
31
|
+
export declare function listAllGatewayActions(client: PlatformApiClient, projectId: string, pageSize?: number): Promise<GatewayActionDto[]>;
|
|
31
32
|
export declare function handleListGatewayPlans(client: PlatformApiClient, config: ApitogoConfig, args: {
|
|
32
33
|
projectId?: string;
|
|
33
34
|
}): Promise<string>;
|
|
35
|
+
export declare function resolveIncludedActionKeysForCreate(client: PlatformApiClient, projectId: string, includedActionKeys: string[] | undefined, options?: {
|
|
36
|
+
skipDefaultActions?: boolean;
|
|
37
|
+
}): Promise<string[]>;
|
|
34
38
|
export declare function handleCreateGatewayPlan(client: PlatformApiClient, config: ApitogoConfig, args: {
|
|
35
39
|
projectId?: string;
|
|
36
40
|
sku: string;
|
|
@@ -41,6 +45,7 @@ export declare function handleCreateGatewayPlan(client: PlatformApiClient, confi
|
|
|
41
45
|
priceCurrency?: string;
|
|
42
46
|
billingPeriod?: string;
|
|
43
47
|
sortOrder?: number;
|
|
48
|
+
skipDefaultActions?: boolean;
|
|
44
49
|
}): Promise<string>;
|
|
45
50
|
export declare function handleGetGatewayPlan(client: PlatformApiClient, config: ApitogoConfig, args: {
|
|
46
51
|
projectId?: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type GatewayLimits = {
|
|
2
|
+
rateLimit?: {
|
|
3
|
+
calls: number;
|
|
4
|
+
renewalPeriodSeconds?: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
quota?: {
|
|
8
|
+
calls: number;
|
|
9
|
+
renewalPeriodDays?: number;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
export type GatewayLimitsSource = {
|
|
15
|
+
limits?: unknown;
|
|
16
|
+
limitsJson?: unknown;
|
|
17
|
+
};
|
|
18
|
+
export declare function isGatewayLimitsObject(value: unknown): value is GatewayLimits;
|
|
19
|
+
export declare function parseLimitsJson(limitsJson?: string | null): GatewayLimits | undefined;
|
|
20
|
+
export declare function coerceGatewayLimits(source: GatewayLimitsSource): GatewayLimits | undefined;
|
|
21
|
+
export declare function stringifyGatewayLimits(limits?: GatewayLimits | null): string | undefined;
|
|
22
|
+
export declare function planForConfig<T extends object>(plan: T): T;
|
|
23
|
+
export declare function gatewayForConfig<T extends object>(gateway: T): T;
|
|
24
|
+
export declare function mergeLimitsSource<T extends object>(prior: T | undefined, patch: T): T;
|
|
25
|
+
export declare function planLimitsForPlatform(plan: object): string | undefined;
|
|
26
|
+
export declare function validateLimitsField(value: unknown, field: string): Array<{
|
|
27
|
+
field: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { type GatewayLimits } from "./gateway-limits.js";
|
|
2
3
|
import { APITOGO_CONFIG_FILENAME } from "./apitogo-config-loader.js";
|
|
3
4
|
import { type LandingManifestInput } from "./landing-manifest.js";
|
|
4
5
|
export declare const APITOGO_MANIFEST_BASE_FILENAME = "apitogo.json";
|
|
@@ -20,6 +21,16 @@ export declare const ManifestPlanInputSchema: z.ZodObject<{
|
|
|
20
21
|
year: "year";
|
|
21
22
|
annual: "annual";
|
|
22
23
|
}>>;
|
|
24
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
25
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
calls: z.ZodNumber;
|
|
27
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
}, z.core.$loose>>;
|
|
29
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
calls: z.ZodNumber;
|
|
31
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
32
|
+
}, z.core.$loose>>;
|
|
33
|
+
}, z.core.$loose>>;
|
|
23
34
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
24
35
|
includedActionKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
36
|
}, z.core.$strip>;
|
|
@@ -64,6 +75,16 @@ export declare const DevPortalLocalManifestSchema: z.ZodObject<{
|
|
|
64
75
|
year: "year";
|
|
65
76
|
annual: "annual";
|
|
66
77
|
}>>;
|
|
78
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
79
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
80
|
+
calls: z.ZodNumber;
|
|
81
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
}, z.core.$loose>>;
|
|
83
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
84
|
+
calls: z.ZodNumber;
|
|
85
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
86
|
+
}, z.core.$loose>>;
|
|
87
|
+
}, z.core.$loose>>;
|
|
67
88
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
68
89
|
includedActionKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
69
90
|
}, z.core.$strip>>>;
|
|
@@ -87,6 +108,16 @@ export declare const DevPortalLocalManifestSchema: z.ZodObject<{
|
|
|
87
108
|
oidcMetadataUrl: z.ZodOptional<z.ZodString>;
|
|
88
109
|
oidcAllowedIssuers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
89
110
|
oidcAudiences: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
111
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
112
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
113
|
+
calls: z.ZodNumber;
|
|
114
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
115
|
+
}, z.core.$loose>>;
|
|
116
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
117
|
+
calls: z.ZodNumber;
|
|
118
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
119
|
+
}, z.core.$loose>>;
|
|
120
|
+
}, z.core.$loose>>;
|
|
90
121
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
91
122
|
publicHealthActionKey: z.ZodOptional<z.ZodString>;
|
|
92
123
|
publicRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -201,6 +232,7 @@ export type DevPortalPreviewPlan = {
|
|
|
201
232
|
priceAmount: number;
|
|
202
233
|
priceCurrency: string;
|
|
203
234
|
billingPeriod: string;
|
|
235
|
+
limits: GatewayLimits | null;
|
|
204
236
|
limitsJson: string | null;
|
|
205
237
|
includedActionKeys: string[];
|
|
206
238
|
};
|
|
@@ -390,6 +390,16 @@ declare const PlanCatalogItemSchema: z.ZodObject<{
|
|
|
390
390
|
priceAmount: z.ZodDefault<z.ZodNumber>;
|
|
391
391
|
priceCurrency: z.ZodOptional<z.ZodString>;
|
|
392
392
|
billingPeriod: z.ZodOptional<z.ZodString>;
|
|
393
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
394
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
395
|
+
calls: z.ZodNumber;
|
|
396
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
397
|
+
}, z.core.$loose>>;
|
|
398
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
399
|
+
calls: z.ZodNumber;
|
|
400
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
401
|
+
}, z.core.$loose>>;
|
|
402
|
+
}, z.core.$loose>>;
|
|
393
403
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
394
404
|
includedActionKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
395
405
|
}, z.core.$strip>;
|
|
@@ -413,6 +423,16 @@ export declare const PlansModuleSchema: z.ZodObject<{
|
|
|
413
423
|
priceAmount: z.ZodDefault<z.ZodNumber>;
|
|
414
424
|
priceCurrency: z.ZodOptional<z.ZodString>;
|
|
415
425
|
billingPeriod: z.ZodOptional<z.ZodString>;
|
|
426
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
427
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
428
|
+
calls: z.ZodNumber;
|
|
429
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
430
|
+
}, z.core.$loose>>;
|
|
431
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
432
|
+
calls: z.ZodNumber;
|
|
433
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
434
|
+
}, z.core.$loose>>;
|
|
435
|
+
}, z.core.$loose>>;
|
|
416
436
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
417
437
|
includedActionKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
418
438
|
}, z.core.$strip>>>;
|
|
@@ -489,6 +509,16 @@ export declare const UserPanelModuleSchema: z.ZodObject<{
|
|
|
489
509
|
priceAmount: z.ZodDefault<z.ZodNumber>;
|
|
490
510
|
priceCurrency: z.ZodOptional<z.ZodString>;
|
|
491
511
|
billingPeriod: z.ZodOptional<z.ZodString>;
|
|
512
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
513
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
514
|
+
calls: z.ZodNumber;
|
|
515
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
516
|
+
}, z.core.$loose>>;
|
|
517
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
518
|
+
calls: z.ZodNumber;
|
|
519
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
520
|
+
}, z.core.$loose>>;
|
|
521
|
+
}, z.core.$loose>>;
|
|
492
522
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
493
523
|
includedActionKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
494
524
|
}, z.core.$strip>>>;
|
|
@@ -677,6 +707,16 @@ export declare const ModulesSchema: z.ZodOptional<z.ZodObject<{
|
|
|
677
707
|
priceAmount: z.ZodDefault<z.ZodNumber>;
|
|
678
708
|
priceCurrency: z.ZodOptional<z.ZodString>;
|
|
679
709
|
billingPeriod: z.ZodOptional<z.ZodString>;
|
|
710
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
711
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
712
|
+
calls: z.ZodNumber;
|
|
713
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
714
|
+
}, z.core.$loose>>;
|
|
715
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
716
|
+
calls: z.ZodNumber;
|
|
717
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
718
|
+
}, z.core.$loose>>;
|
|
719
|
+
}, z.core.$loose>>;
|
|
680
720
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
681
721
|
includedActionKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
682
722
|
}, z.core.$strip>>>;
|
|
@@ -7041,6 +7041,16 @@ export declare const ZudokuConfig: z.ZodObject<{
|
|
|
7041
7041
|
priceAmount: z.ZodDefault<z.ZodNumber>;
|
|
7042
7042
|
priceCurrency: z.ZodOptional<z.ZodString>;
|
|
7043
7043
|
billingPeriod: z.ZodOptional<z.ZodString>;
|
|
7044
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
7045
|
+
rateLimit: z.ZodOptional<z.ZodObject<{
|
|
7046
|
+
calls: z.ZodNumber;
|
|
7047
|
+
renewalPeriodSeconds: z.ZodOptional<z.ZodNumber>;
|
|
7048
|
+
}, z.core.$loose>>;
|
|
7049
|
+
quota: z.ZodOptional<z.ZodObject<{
|
|
7050
|
+
calls: z.ZodNumber;
|
|
7051
|
+
renewalPeriodDays: z.ZodOptional<z.ZodNumber>;
|
|
7052
|
+
}, z.core.$loose>>;
|
|
7053
|
+
}, z.core.$loose>>;
|
|
7044
7054
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
7045
7055
|
includedActionKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7046
7056
|
}, z.core.$strip>>>;
|
package/dist/flat-config.d.ts
CHANGED
|
@@ -70,6 +70,19 @@ export type _Schema37 = {
|
|
|
70
70
|
priceAmount?: number
|
|
71
71
|
priceCurrency?: string
|
|
72
72
|
billingPeriod?: string
|
|
73
|
+
limits?: {
|
|
74
|
+
rateLimit?: {
|
|
75
|
+
calls: number
|
|
76
|
+
renewalPeriodSeconds?: number
|
|
77
|
+
[k: string]: unknown
|
|
78
|
+
}
|
|
79
|
+
quota?: {
|
|
80
|
+
calls: number
|
|
81
|
+
renewalPeriodDays?: number
|
|
82
|
+
[k: string]: unknown
|
|
83
|
+
}
|
|
84
|
+
[k: string]: unknown
|
|
85
|
+
}
|
|
73
86
|
limitsJson?: string
|
|
74
87
|
includedActionKeys?: string[]
|
|
75
88
|
}[]
|
|
@@ -39,7 +39,10 @@ npx apitogo config
|
|
|
39
39
|
"sku": "free",
|
|
40
40
|
"displayName": "Free",
|
|
41
41
|
"priceAmount": 0,
|
|
42
|
-
"
|
|
42
|
+
"limits": {
|
|
43
|
+
"rateLimit": { "calls": 100, "renewalPeriodSeconds": 60 },
|
|
44
|
+
"quota": { "calls": 50000, "renewalPeriodDays": 30 }
|
|
45
|
+
}
|
|
43
46
|
},
|
|
44
47
|
{
|
|
45
48
|
"sku": "pro",
|
|
@@ -47,7 +50,10 @@ npx apitogo config
|
|
|
47
50
|
"priceAmount": 29,
|
|
48
51
|
"priceCurrency": "usd",
|
|
49
52
|
"billingPeriod": "month",
|
|
50
|
-
"
|
|
53
|
+
"limits": {
|
|
54
|
+
"rateLimit": { "calls": 1000, "renewalPeriodSeconds": 60 },
|
|
55
|
+
"quota": { "calls": 500000, "renewalPeriodDays": 30 }
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
]
|
|
53
59
|
}
|
|
@@ -59,12 +65,12 @@ npx apitogo config
|
|
|
59
65
|
- **`sku`** — stable identifier (used in checkout URLs and gateway sync).
|
|
60
66
|
- **`displayName`** — label on `/pricing`.
|
|
61
67
|
- **`priceAmount`** — `0` for free tiers; greater than zero for paid plans (USD by default).
|
|
62
|
-
- **`
|
|
68
|
+
- **`limits`** — rate limit and quota policies (see below). Encoded `limitsJson` strings are still accepted on load.
|
|
63
69
|
|
|
64
70
|
Enable the catalog with `modules.userPanel.plans.enabled: true`. Legacy root `plans` is migrated on
|
|
65
71
|
load.
|
|
66
72
|
|
|
67
|
-
##
|
|
73
|
+
## limits schema
|
|
68
74
|
|
|
69
75
|
```json
|
|
70
76
|
{
|
|
@@ -76,7 +82,8 @@ load.
|
|
|
76
82
|
- **rateLimit** — maximum calls per renewal window (per minute when `renewalPeriodSeconds` is 60).
|
|
77
83
|
- **quota** — longer-window cap (per 30 days when `renewalPeriodDays` is 30).
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
New plans include **all** discovered OpenAPI operations unless you set `includedActionKeys`.
|
|
86
|
+
After OpenAPI is deployed, you can scope a plan to specific operations with
|
|
80
87
|
`includedActionKeys` (for example `get:/orders/{id}`). Edit those fields in
|
|
81
88
|
`apitogo.config.json` — see [Manifest](./manifest.md).
|
|
82
89
|
|
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { gatewayForConfig, mergeLimitsSource, planForConfig } from "./gateway-limits.js";
|
|
2
|
+
|
|
1
3
|
export const APITOGO_CONFIG_FILENAME = "apitogo.config.json";
|
|
2
4
|
|
|
3
5
|
const ENV_PREFIX = "APITOGO_CONFIG_";
|
|
@@ -103,6 +105,7 @@ const COLLAPSED_UPPERCASE_ALIASES: Record<string, string> = {
|
|
|
103
105
|
PRICECURRENCY: "priceCurrency",
|
|
104
106
|
BILLINGPERIOD: "billingPeriod",
|
|
105
107
|
LIMITSJSON: "limitsJson",
|
|
108
|
+
LIMITS: "limits",
|
|
106
109
|
INCLUDEDACTIONKEYS: "includedActionKeys",
|
|
107
110
|
SOURCEDIRECTORY: "sourceDirectory",
|
|
108
111
|
PUBLISHDIRECTORY: "publishDirectory",
|
|
@@ -333,7 +336,7 @@ function mergePlansBySku(
|
|
|
333
336
|
continue;
|
|
334
337
|
}
|
|
335
338
|
const prior = bySku.get(plan.sku);
|
|
336
|
-
bySku.set(plan.sku, prior
|
|
339
|
+
bySku.set(plan.sku, mergeLimitsSource(prior, plan));
|
|
337
340
|
}
|
|
338
341
|
return Array.from(bySku.values());
|
|
339
342
|
}
|
|
@@ -372,6 +375,21 @@ function migratePlanCatalog(normalized: ApitogoJsonConfig): void {
|
|
|
372
375
|
delete normalized.plans;
|
|
373
376
|
}
|
|
374
377
|
|
|
378
|
+
function migratePlanLimits(normalized: ApitogoJsonConfig): void {
|
|
379
|
+
const modules = asRecord(normalized.modules);
|
|
380
|
+
const plansModule = asRecord(asRecord(modules?.userPanel)?.plans);
|
|
381
|
+
if (Array.isArray(plansModule?.items)) {
|
|
382
|
+
plansModule.items = (plansModule.items as PlanCatalogItem[]).map((plan) =>
|
|
383
|
+
planForConfig(plan),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const gateway = asRecord(normalized.gateway);
|
|
388
|
+
if (gateway) {
|
|
389
|
+
normalized.gateway = gatewayForConfig(gateway);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
375
393
|
/** Collapse legacy duplicate fields into canonical shape (mutates copy). */
|
|
376
394
|
export function normalizeApitogoConfig(
|
|
377
395
|
config: ApitogoJsonConfig,
|
|
@@ -428,6 +446,7 @@ export function normalizeApitogoConfig(
|
|
|
428
446
|
delete normalized.projectName;
|
|
429
447
|
|
|
430
448
|
migratePlanCatalog(normalized);
|
|
449
|
+
migratePlanLimits(normalized);
|
|
431
450
|
syncBillingEnabled(normalized);
|
|
432
451
|
syncAuthentication(normalized);
|
|
433
452
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type GatewayLimits = {
|
|
2
|
+
rateLimit?: {
|
|
3
|
+
calls: number;
|
|
4
|
+
renewalPeriodSeconds?: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
quota?: {
|
|
8
|
+
calls: number;
|
|
9
|
+
renewalPeriodDays?: number;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type GatewayLimitsSource = {
|
|
16
|
+
limits?: unknown;
|
|
17
|
+
limitsJson?: unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function isGatewayLimitsObject(value: unknown): value is GatewayLimits {
|
|
21
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseLimitsJson(
|
|
25
|
+
limitsJson?: string | null,
|
|
26
|
+
): GatewayLimits | undefined {
|
|
27
|
+
if (!limitsJson?.trim()) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(limitsJson) as unknown;
|
|
33
|
+
return isGatewayLimitsObject(parsed) ? parsed : undefined;
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function coerceGatewayLimits(
|
|
40
|
+
source: GatewayLimitsSource,
|
|
41
|
+
): GatewayLimits | undefined {
|
|
42
|
+
if (isGatewayLimitsObject(source.limits)) {
|
|
43
|
+
return source.limits;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parseLimitsJson(
|
|
47
|
+
typeof source.limitsJson === "string" ? source.limitsJson : undefined,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function stringifyGatewayLimits(
|
|
52
|
+
limits?: GatewayLimits | null,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
if (!isGatewayLimitsObject(limits)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(limits);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Canonical config shape: nested `limits`, no encoded `limitsJson`. */
|
|
61
|
+
export function planForConfig<T extends object>(plan: T): T {
|
|
62
|
+
const limits = coerceGatewayLimits(plan as GatewayLimitsSource);
|
|
63
|
+
const next = { ...plan } as T & GatewayLimitsSource;
|
|
64
|
+
delete next.limitsJson;
|
|
65
|
+
if (limits) {
|
|
66
|
+
next.limits = limits;
|
|
67
|
+
} else {
|
|
68
|
+
delete next.limits;
|
|
69
|
+
}
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function gatewayForConfig<T extends object>(gateway: T): T {
|
|
74
|
+
return planForConfig(gateway);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Merge patch over prior; a patch `limitsJson` string replaces existing `limits`. */
|
|
78
|
+
export function mergeLimitsSource<T extends object>(
|
|
79
|
+
prior: T | undefined,
|
|
80
|
+
patch: T,
|
|
81
|
+
): T {
|
|
82
|
+
const merged = prior ? { ...prior, ...patch } : { ...patch };
|
|
83
|
+
const patchSource = patch as GatewayLimitsSource;
|
|
84
|
+
if (patchSource.limits === undefined && patchSource.limitsJson !== undefined) {
|
|
85
|
+
delete (merged as GatewayLimitsSource).limits;
|
|
86
|
+
}
|
|
87
|
+
return planForConfig(merged);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Stringify for the platform HTTP/DB `limitsJson` field. */
|
|
91
|
+
export function planLimitsForPlatform(plan: object): string | undefined {
|
|
92
|
+
return stringifyGatewayLimits(coerceGatewayLimits(plan as GatewayLimitsSource));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateLimitsField(
|
|
96
|
+
value: unknown,
|
|
97
|
+
field: string,
|
|
98
|
+
): Array<{ field: string; message: string }> {
|
|
99
|
+
if (value == null || value === "") {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
if (!value.trim()) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(value) as unknown;
|
|
109
|
+
if (!isGatewayLimitsObject(parsed)) {
|
|
110
|
+
return [{ field, message: `${field} must be a JSON object.` }];
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
} catch {
|
|
114
|
+
return [{ field, message: `${field} is not valid JSON.` }];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isGatewayLimitsObject(value)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [{ field, message: `${field} must be a JSON object.` }];
|
|
123
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
type GatewayLimits,
|
|
6
|
+
coerceGatewayLimits,
|
|
7
|
+
planForConfig,
|
|
8
|
+
gatewayForConfig,
|
|
9
|
+
mergeLimitsSource,
|
|
10
|
+
planLimitsForPlatform,
|
|
11
|
+
} from "./gateway-limits.js";
|
|
4
12
|
import {
|
|
5
13
|
APITOGO_CONFIG_FILENAME,
|
|
6
14
|
extractManifest,
|
|
@@ -28,6 +36,25 @@ export type ApitogoManifestEnv = keyof typeof APITOGO_MANIFEST_ENV_FILES;
|
|
|
28
36
|
|
|
29
37
|
const BillingPeriodSchema = z.enum(["month", "year", "monthly", "annual"]);
|
|
30
38
|
|
|
39
|
+
const GatewayLimitsSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
rateLimit: z
|
|
42
|
+
.object({
|
|
43
|
+
calls: z.number(),
|
|
44
|
+
renewalPeriodSeconds: z.number().optional(),
|
|
45
|
+
})
|
|
46
|
+
.passthrough()
|
|
47
|
+
.optional(),
|
|
48
|
+
quota: z
|
|
49
|
+
.object({
|
|
50
|
+
calls: z.number(),
|
|
51
|
+
renewalPeriodDays: z.number().optional(),
|
|
52
|
+
})
|
|
53
|
+
.passthrough()
|
|
54
|
+
.optional(),
|
|
55
|
+
})
|
|
56
|
+
.passthrough();
|
|
57
|
+
|
|
31
58
|
const PriceCurrencySchema = z
|
|
32
59
|
.string()
|
|
33
60
|
.trim()
|
|
@@ -56,6 +83,8 @@ export const ManifestPlanInputSchema = z.object({
|
|
|
56
83
|
priceAmount: z.number().default(0),
|
|
57
84
|
priceCurrency: PriceCurrencySchema.optional(),
|
|
58
85
|
billingPeriod: BillingPeriodSchema.optional(),
|
|
86
|
+
limits: GatewayLimitsSchema.optional(),
|
|
87
|
+
/** @deprecated Use `limits`. Encoded string still accepted on load. */
|
|
59
88
|
limitsJson: z.string().optional(),
|
|
60
89
|
includedActionKeys: z.array(z.string()).optional(),
|
|
61
90
|
});
|
|
@@ -138,6 +167,8 @@ export const DevPortalLocalManifestSchema = z.object({
|
|
|
138
167
|
oidcMetadataUrl: z.string().optional(),
|
|
139
168
|
oidcAllowedIssuers: z.array(z.string()).optional(),
|
|
140
169
|
oidcAudiences: z.array(z.string()).optional(),
|
|
170
|
+
limits: GatewayLimitsSchema.optional(),
|
|
171
|
+
/** @deprecated Use `limits`. Encoded string still accepted on load. */
|
|
141
172
|
limitsJson: z.string().optional(),
|
|
142
173
|
publicHealthActionKey: z.string().optional(),
|
|
143
174
|
publicRoutes: z.array(z.string()).optional(),
|
|
@@ -169,6 +200,7 @@ export type DevPortalPreviewPlan = {
|
|
|
169
200
|
priceAmount: number;
|
|
170
201
|
priceCurrency: string;
|
|
171
202
|
billingPeriod: string;
|
|
203
|
+
limits: GatewayLimits | null;
|
|
172
204
|
limitsJson: string | null;
|
|
173
205
|
includedActionKeys: string[];
|
|
174
206
|
};
|
|
@@ -242,7 +274,8 @@ export function plansToPreviewCatalog(
|
|
|
242
274
|
priceAmount: plan.priceAmount ?? 0,
|
|
243
275
|
priceCurrency: plan.priceCurrency ?? "usd",
|
|
244
276
|
billingPeriod: normalizeBillingPeriod(plan.billingPeriod),
|
|
245
|
-
|
|
277
|
+
limits: coerceGatewayLimits(plan) ?? null,
|
|
278
|
+
limitsJson: planLimitsForPlatform(plan) ?? null,
|
|
246
279
|
includedActionKeys: plan.includedActionKeys ?? [],
|
|
247
280
|
})),
|
|
248
281
|
};
|
|
@@ -262,7 +295,7 @@ export function mergePlansBySku(
|
|
|
262
295
|
}
|
|
263
296
|
for (const plan of patch) {
|
|
264
297
|
const prior = bySku.get(plan.sku);
|
|
265
|
-
bySku.set(plan.sku, prior
|
|
298
|
+
bySku.set(plan.sku, mergeLimitsSource(prior, plan));
|
|
266
299
|
}
|
|
267
300
|
return Array.from(bySku.values());
|
|
268
301
|
}
|
|
@@ -384,7 +417,7 @@ export function mergeManifest(
|
|
|
384
417
|
merged.backend = { ...base.backend, ...override.backend };
|
|
385
418
|
}
|
|
386
419
|
if (override.gateway) {
|
|
387
|
-
merged.gateway =
|
|
420
|
+
merged.gateway = mergeLimitsSource(base.gateway, override.gateway);
|
|
388
421
|
}
|
|
389
422
|
if (override.plans) {
|
|
390
423
|
merged.plans = mergePlansBySku(base.plans, override.plans);
|
|
@@ -423,7 +456,14 @@ export function parseLocalManifestJson(
|
|
|
423
456
|
if (!result.success) {
|
|
424
457
|
return null;
|
|
425
458
|
}
|
|
426
|
-
|
|
459
|
+
const data = result.data;
|
|
460
|
+
if (data.plans) {
|
|
461
|
+
data.plans = data.plans.map((plan) => planForConfig(plan));
|
|
462
|
+
}
|
|
463
|
+
if (data.gateway) {
|
|
464
|
+
data.gateway = gatewayForConfig(data.gateway);
|
|
465
|
+
}
|
|
466
|
+
return data;
|
|
427
467
|
} catch {
|
|
428
468
|
return null;
|
|
429
469
|
}
|
|
@@ -274,6 +274,25 @@ const PlanCatalogItemSchema = z.object({
|
|
|
274
274
|
)
|
|
275
275
|
.optional(),
|
|
276
276
|
billingPeriod: z.string().optional(),
|
|
277
|
+
limits: z
|
|
278
|
+
.object({
|
|
279
|
+
rateLimit: z
|
|
280
|
+
.object({
|
|
281
|
+
calls: z.number(),
|
|
282
|
+
renewalPeriodSeconds: z.number().optional(),
|
|
283
|
+
})
|
|
284
|
+
.passthrough()
|
|
285
|
+
.optional(),
|
|
286
|
+
quota: z
|
|
287
|
+
.object({
|
|
288
|
+
calls: z.number(),
|
|
289
|
+
renewalPeriodDays: z.number().optional(),
|
|
290
|
+
})
|
|
291
|
+
.passthrough()
|
|
292
|
+
.optional(),
|
|
293
|
+
})
|
|
294
|
+
.passthrough()
|
|
295
|
+
.optional(),
|
|
277
296
|
limitsJson: z.string().optional(),
|
|
278
297
|
includedActionKeys: z.array(z.string()).optional(),
|
|
279
298
|
});
|