@lukoweb/apitogo 0.1.97 → 0.1.98

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 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 ? { ...prior, ...plan } : plan);
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
- limitsJson: plan.limitsJson ?? null,
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
- limitsJson: JSON.stringify({
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
- limitsJson: JSON.stringify({
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 ? { ...prior, ...plan } : plan);
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(...validateLimitsJson(plan.limitsJson, `plans[${index}].limitsJson`));
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(...validateLimitsJson(manifest.gateway.limitsJson, "gateway.limitsJson"));
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 },
@@ -7655,7 +7787,7 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
7655
7787
  planId: found.id,
7656
7788
  displayName: input3.displayName,
7657
7789
  isActive: true,
7658
- limitsJson: input3.limitsJson,
7790
+ limitsJson: planLimitsForPlatform(input3),
7659
7791
  priceAmount: input3.priceAmount,
7660
7792
  priceCurrency: input3.priceCurrency,
7661
7793
  billingPeriod: input3.billingPeriod ? normalizeBillingPeriod(input3.billingPeriod) : void 0
@@ -7678,7 +7810,7 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
7678
7810
  priceAmount: input3.priceAmount,
7679
7811
  priceCurrency: input3.priceCurrency ?? "usd",
7680
7812
  billingPeriod: normalizeBillingPeriod(input3.billingPeriod),
7681
- limitsJson: input3.limitsJson,
7813
+ limitsJson: planLimitsForPlatform(input3),
7682
7814
  includedActionKeys: options?.deferActionKeys ? [] : resolveIncludedActionKeysForUpsert(input3),
7683
7815
  sortOrder: index
7684
7816
  });
@@ -11251,6 +11383,7 @@ async function handleBuildDevPortalSite(client, config2, args, progress) {
11251
11383
  import { readFile as readFile8 } from "node:fs/promises";
11252
11384
  import { createRequire } from "node:module";
11253
11385
  import path17 from "node:path";
11386
+ init_gateway_limits();
11254
11387
  var cachedFallbackTs;
11255
11388
  async function loadTypeScriptForSite(sourceDirectory) {
11256
11389
  const require2 = createRequire(path17.join(path17.resolve(sourceDirectory), "package.json"));
@@ -11371,6 +11504,11 @@ async function validateSiteConfigStructure(config2, sourceDirectory) {
11371
11504
  `"modules.userPanel.plans.items[${index}].sku" must be a non-empty string.`
11372
11505
  );
11373
11506
  }
11507
+ if (plan.limits !== void 0 && !isGatewayLimitsObject(plan.limits)) {
11508
+ errors.push(
11509
+ `"modules.userPanel.plans.items[${index}].limits" must be a JSON object.`
11510
+ );
11511
+ }
11374
11512
  if (typeof plan.limitsJson === "string" && plan.limitsJson.trim()) {
11375
11513
  try {
11376
11514
  JSON.parse(plan.limitsJson);
@@ -11910,7 +12048,7 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
11910
12048
  const enableGatewayJwt = Boolean(explicitAuthMode || explicitOidcUrl);
11911
12049
  const baselineArgs = {
11912
12050
  projectId,
11913
- limitsJson: gateway.limitsJson,
12051
+ limitsJson: planLimitsForPlatform(gateway),
11914
12052
  publicHealthActionKey: gateway.publicHealthActionKey,
11915
12053
  publicRoutes: gateway.publicRoutes,
11916
12054
  waitForEntra: wait,
@@ -12610,7 +12748,7 @@ import {
12610
12748
  // package.json
12611
12749
  var package_default = {
12612
12750
  name: "@lukoweb/apitogo",
12613
- version: "0.1.97",
12751
+ version: "0.1.98",
12614
12752
  type: "module",
12615
12753
  sideEffects: [
12616
12754
  "**/*.css",
@@ -19307,28 +19445,37 @@ function createDefaultApitogoJsonConfig(options) {
19307
19445
  displayName: "Free",
19308
19446
  priceAmount: 0,
19309
19447
  billingPeriod: "month",
19310
- limitsJson: '{"rateLimit":{"calls":10,"renewalPeriodSeconds":1},"quota":{"calls":1000,"renewalPeriodDays":30}}'
19448
+ limits: {
19449
+ rateLimit: { calls: 10, renewalPeriodSeconds: 1 },
19450
+ quota: { calls: 1e3, renewalPeriodDays: 30 }
19451
+ }
19311
19452
  },
19312
19453
  {
19313
19454
  sku: "developer",
19314
19455
  displayName: "Developer",
19315
19456
  priceAmount: 19,
19316
19457
  billingPeriod: "month",
19317
- limitsJson: '{"rateLimit":{"calls":50,"renewalPeriodSeconds":1},"quota":{"calls":50000,"renewalPeriodDays":30}}'
19458
+ limits: {
19459
+ rateLimit: { calls: 50, renewalPeriodSeconds: 1 },
19460
+ quota: { calls: 5e4, renewalPeriodDays: 30 }
19461
+ }
19318
19462
  },
19319
19463
  {
19320
19464
  sku: "team",
19321
19465
  displayName: "Team",
19322
19466
  priceAmount: 79,
19323
19467
  billingPeriod: "month",
19324
- limitsJson: '{"rateLimit":{"calls":200,"renewalPeriodSeconds":1},"quota":{"calls":500000,"renewalPeriodDays":30}}'
19468
+ limits: {
19469
+ rateLimit: { calls: 200, renewalPeriodSeconds: 1 },
19470
+ quota: { calls: 5e5, renewalPeriodDays: 30 }
19471
+ }
19325
19472
  },
19326
19473
  {
19327
19474
  sku: "enterprise",
19328
19475
  displayName: "Enterprise",
19329
19476
  priceAmount: -1,
19330
19477
  billingPeriod: "custom",
19331
- limitsJson: '{"quota":{"calls":-1}}'
19478
+ limits: { quota: { calls: -1 } }
19332
19479
  }
19333
19480
  ]
19334
19481
  }
@@ -19512,11 +19659,11 @@ function slugifyPlanSku(displayName) {
19512
19659
  const sku = displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
19513
19660
  return sku || "plan";
19514
19661
  }
19515
- function buildLimitsJson(rateLimitCalls, monthlyQuota) {
19516
- return JSON.stringify({
19662
+ function buildLimits(rateLimitCalls, monthlyQuota) {
19663
+ return {
19517
19664
  rateLimit: { calls: rateLimitCalls, renewalPeriodSeconds: 60 },
19518
19665
  quota: { calls: monthlyQuota, renewalPeriodDays: 30 }
19519
- });
19666
+ };
19520
19667
  }
19521
19668
  function buildPlanItemFromWizard(input3) {
19522
19669
  const sku = input3.sku ?? slugifyPlanSku(input3.displayName);
@@ -19524,7 +19671,7 @@ function buildPlanItemFromWizard(input3) {
19524
19671
  sku,
19525
19672
  displayName: input3.displayName,
19526
19673
  priceAmount: input3.priceAmount,
19527
- limitsJson: buildLimitsJson(input3.rateLimitCalls, input3.monthlyQuota)
19674
+ limits: buildLimits(input3.rateLimitCalls, input3.monthlyQuota)
19528
19675
  };
19529
19676
  if (input3.priceAmount > 0) {
19530
19677
  item.priceCurrency = "usd";
@@ -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
  }
@@ -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>>>;
@@ -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
- "limitsJson": "{\"rateLimit\":{\"calls\":100,\"renewalPeriodSeconds\":60},\"quota\":{\"calls\":50000,\"renewalPeriodDays\":30}}"
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
- "limitsJson": "{\"rateLimit\":{\"calls\":1000,\"renewalPeriodSeconds\":60},\"quota\":{\"calls\":500000,\"renewalPeriodDays\":30}}"
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
- - **`limitsJson`** — JSON string with rate limit and quota policies (see below).
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
- ## limitsJson schema
73
+ ## limits schema
68
74
 
69
75
  ```json
70
76
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.97",
3
+ "version": "0.1.98",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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 ? { ...prior, ...plan } : plan);
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
- limitsJson: plan.limitsJson ?? null,
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 ? { ...prior, ...plan } : plan);
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 = { ...base.gateway, ...override.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
- return result.data;
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
  });