@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 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 },
@@ -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: args.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 = resolveIncludedActionKeysForUpsert(input3, found);
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.limitsJson,
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 = resolveIncludedActionKeysForUpsert(input3, found);
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.limitsJson,
7682
- includedActionKeys: options?.deferActionKeys ? [] : resolveIncludedActionKeysForUpsert(input3),
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.limitsJson,
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.97",
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
- limitsJson: '{"rateLimit":{"calls":10,"renewalPeriodSeconds":1},"quota":{"calls":1000,"renewalPeriodDays":30}}'
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
- limitsJson: '{"rateLimit":{"calls":50,"renewalPeriodSeconds":1},"quota":{"calls":50000,"renewalPeriodDays":30}}'
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
- limitsJson: '{"rateLimit":{"calls":200,"renewalPeriodSeconds":1},"quota":{"calls":500000,"renewalPeriodDays":30}}'
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
- limitsJson: '{"quota":{"calls":-1}}'
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 buildLimitsJson(rateLimitCalls, monthlyQuota) {
19516
- return JSON.stringify({
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
- limitsJson: buildLimitsJson(input3.rateLimitCalls, input3.monthlyQuota)
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
- "Configure includedActionKeys, billing, and Stripe in the manifest before publish."
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>>>;
@@ -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
  {
@@ -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
- After OpenAPI is deployed, you can scope plans to specific API operations with
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
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
  });