@lukoweb/apitogo 0.1.96 → 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.
@@ -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.96",
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
  });
@@ -160,6 +160,9 @@ export async function getViteConfig(
160
160
  server: {
161
161
  middlewareMode: true,
162
162
  open: true,
163
+ fs: {
164
+ allow: [dir, path.dirname(dir)],
165
+ },
163
166
  watch: {
164
167
  ignored: [
165
168
  `${dir}/dist`,
@@ -0,0 +1,71 @@
1
+ import path from "node:path";
2
+ import type { Plugin, ResolveIdOptions } from "vite";
3
+ import { getCurrentConfig } from "../config/loader.js";
4
+
5
+ function stripQuery(id: string): string {
6
+ const query = id.indexOf("?");
7
+ return query === -1 ? id : id.slice(0, query);
8
+ }
9
+
10
+ function isInside(parent: string, child: string): boolean {
11
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
12
+ return (
13
+ relative === "" ||
14
+ (!relative.startsWith("..") && !path.isAbsolute(relative))
15
+ );
16
+ }
17
+
18
+ function isBareSpecifier(source: string): boolean {
19
+ return (
20
+ !source.startsWith("\0") &&
21
+ !source.startsWith(".") &&
22
+ !source.startsWith("/") &&
23
+ !path.isAbsolute(source)
24
+ );
25
+ }
26
+
27
+ function isInsideNodeModules(filePath: string): boolean {
28
+ return filePath.split(path.sep).includes("node_modules");
29
+ }
30
+
31
+ /** True when Vite followed a symlink (or glob) to markdown outside the site. */
32
+ export function shouldResolveFromSiteRoot(
33
+ source: string,
34
+ importer: string | undefined,
35
+ rootDir: string,
36
+ ): boolean {
37
+ if (!importer || !isBareSpecifier(source)) {
38
+ return false;
39
+ }
40
+ const importerPath = stripQuery(importer);
41
+ if (isInside(rootDir, importerPath) || isInsideNodeModules(importerPath)) {
42
+ return false;
43
+ }
44
+ return true;
45
+ }
46
+
47
+ /**
48
+ * Linked docs live outside the site (e.g. ../doc/*.md). Vite import-analysis
49
+ * then cannot resolve `@lukoweb/apitogo/components` from that folder.
50
+ * Re-resolve bare imports as if they came from the site package.json.
51
+ */
52
+ export default function viteOffrootResolvePlugin(): Plugin {
53
+ return {
54
+ name: "zudoku-offroot-resolve",
55
+ enforce: "pre",
56
+ async resolveId(
57
+ source: string,
58
+ importer: string | undefined,
59
+ options: ResolveIdOptions,
60
+ ) {
61
+ const rootDir = getCurrentConfig().__meta.rootDir;
62
+ if (!shouldResolveFromSiteRoot(source, importer, rootDir)) {
63
+ return;
64
+ }
65
+ return this.resolve(source, path.join(rootDir, "package.json"), {
66
+ ...options,
67
+ skipSelf: true,
68
+ });
69
+ },
70
+ };
71
+ }