@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.
- package/dist/cli/cli.js +456 -261
- package/dist/declarations/cli/platform/helpers/dev-portal-manifest.d.ts +3 -0
- package/dist/declarations/cli/platform/helpers/publish-preflight.d.ts +3 -0
- package/dist/declarations/config/gateway-limits.d.ts +29 -0
- package/dist/declarations/config/local-manifest.d.ts +32 -0
- package/dist/declarations/config/validators/ModulesSchema.d.ts +40 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +10 -0
- package/dist/flat-config.d.ts +13 -0
- package/docs/configuration/plans.md +10 -4
- package/package.json +1 -1
- package/src/config/apitogo-config-core.ts +20 -1
- package/src/config/gateway-limits.ts +123 -0
- package/src/config/local-manifest.ts +44 -4
- package/src/config/validators/ModulesSchema.ts +19 -0
- package/src/vite/config.ts +3 -0
- package/src/vite/plugin-offroot-resolve.ts +71 -0
- package/src/vite/plugin.ts +2 -0
|
@@ -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>>>;
|
package/dist/flat-config.d.ts
CHANGED
|
@@ -70,6 +70,19 @@ export type _Schema37 = {
|
|
|
70
70
|
priceAmount?: number
|
|
71
71
|
priceCurrency?: string
|
|
72
72
|
billingPeriod?: string
|
|
73
|
+
limits?: {
|
|
74
|
+
rateLimit?: {
|
|
75
|
+
calls: number
|
|
76
|
+
renewalPeriodSeconds?: number
|
|
77
|
+
[k: string]: unknown
|
|
78
|
+
}
|
|
79
|
+
quota?: {
|
|
80
|
+
calls: number
|
|
81
|
+
renewalPeriodDays?: number
|
|
82
|
+
[k: string]: unknown
|
|
83
|
+
}
|
|
84
|
+
[k: string]: unknown
|
|
85
|
+
}
|
|
73
86
|
limitsJson?: string
|
|
74
87
|
includedActionKeys?: string[]
|
|
75
88
|
}[]
|
|
@@ -39,7 +39,10 @@ npx apitogo config
|
|
|
39
39
|
"sku": "free",
|
|
40
40
|
"displayName": "Free",
|
|
41
41
|
"priceAmount": 0,
|
|
42
|
-
"
|
|
42
|
+
"limits": {
|
|
43
|
+
"rateLimit": { "calls": 100, "renewalPeriodSeconds": 60 },
|
|
44
|
+
"quota": { "calls": 50000, "renewalPeriodDays": 30 }
|
|
45
|
+
}
|
|
43
46
|
},
|
|
44
47
|
{
|
|
45
48
|
"sku": "pro",
|
|
@@ -47,7 +50,10 @@ npx apitogo config
|
|
|
47
50
|
"priceAmount": 29,
|
|
48
51
|
"priceCurrency": "usd",
|
|
49
52
|
"billingPeriod": "month",
|
|
50
|
-
"
|
|
53
|
+
"limits": {
|
|
54
|
+
"rateLimit": { "calls": 1000, "renewalPeriodSeconds": 60 },
|
|
55
|
+
"quota": { "calls": 500000, "renewalPeriodDays": 30 }
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
]
|
|
53
59
|
}
|
|
@@ -59,12 +65,12 @@ npx apitogo config
|
|
|
59
65
|
- **`sku`** — stable identifier (used in checkout URLs and gateway sync).
|
|
60
66
|
- **`displayName`** — label on `/pricing`.
|
|
61
67
|
- **`priceAmount`** — `0` for free tiers; greater than zero for paid plans (USD by default).
|
|
62
|
-
- **`
|
|
68
|
+
- **`limits`** — rate limit and quota policies (see below). Encoded `limitsJson` strings are still accepted on load.
|
|
63
69
|
|
|
64
70
|
Enable the catalog with `modules.userPanel.plans.enabled: true`. Legacy root `plans` is migrated on
|
|
65
71
|
load.
|
|
66
72
|
|
|
67
|
-
##
|
|
73
|
+
## limits schema
|
|
68
74
|
|
|
69
75
|
```json
|
|
70
76
|
{
|
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { gatewayForConfig, mergeLimitsSource, planForConfig } from "./gateway-limits.js";
|
|
2
|
+
|
|
1
3
|
export const APITOGO_CONFIG_FILENAME = "apitogo.config.json";
|
|
2
4
|
|
|
3
5
|
const ENV_PREFIX = "APITOGO_CONFIG_";
|
|
@@ -103,6 +105,7 @@ const COLLAPSED_UPPERCASE_ALIASES: Record<string, string> = {
|
|
|
103
105
|
PRICECURRENCY: "priceCurrency",
|
|
104
106
|
BILLINGPERIOD: "billingPeriod",
|
|
105
107
|
LIMITSJSON: "limitsJson",
|
|
108
|
+
LIMITS: "limits",
|
|
106
109
|
INCLUDEDACTIONKEYS: "includedActionKeys",
|
|
107
110
|
SOURCEDIRECTORY: "sourceDirectory",
|
|
108
111
|
PUBLISHDIRECTORY: "publishDirectory",
|
|
@@ -333,7 +336,7 @@ function mergePlansBySku(
|
|
|
333
336
|
continue;
|
|
334
337
|
}
|
|
335
338
|
const prior = bySku.get(plan.sku);
|
|
336
|
-
bySku.set(plan.sku, prior
|
|
339
|
+
bySku.set(plan.sku, mergeLimitsSource(prior, plan));
|
|
337
340
|
}
|
|
338
341
|
return Array.from(bySku.values());
|
|
339
342
|
}
|
|
@@ -372,6 +375,21 @@ function migratePlanCatalog(normalized: ApitogoJsonConfig): void {
|
|
|
372
375
|
delete normalized.plans;
|
|
373
376
|
}
|
|
374
377
|
|
|
378
|
+
function migratePlanLimits(normalized: ApitogoJsonConfig): void {
|
|
379
|
+
const modules = asRecord(normalized.modules);
|
|
380
|
+
const plansModule = asRecord(asRecord(modules?.userPanel)?.plans);
|
|
381
|
+
if (Array.isArray(plansModule?.items)) {
|
|
382
|
+
plansModule.items = (plansModule.items as PlanCatalogItem[]).map((plan) =>
|
|
383
|
+
planForConfig(plan),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const gateway = asRecord(normalized.gateway);
|
|
388
|
+
if (gateway) {
|
|
389
|
+
normalized.gateway = gatewayForConfig(gateway);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
375
393
|
/** Collapse legacy duplicate fields into canonical shape (mutates copy). */
|
|
376
394
|
export function normalizeApitogoConfig(
|
|
377
395
|
config: ApitogoJsonConfig,
|
|
@@ -428,6 +446,7 @@ export function normalizeApitogoConfig(
|
|
|
428
446
|
delete normalized.projectName;
|
|
429
447
|
|
|
430
448
|
migratePlanCatalog(normalized);
|
|
449
|
+
migratePlanLimits(normalized);
|
|
431
450
|
syncBillingEnabled(normalized);
|
|
432
451
|
syncAuthentication(normalized);
|
|
433
452
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type GatewayLimits = {
|
|
2
|
+
rateLimit?: {
|
|
3
|
+
calls: number;
|
|
4
|
+
renewalPeriodSeconds?: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
quota?: {
|
|
8
|
+
calls: number;
|
|
9
|
+
renewalPeriodDays?: number;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type GatewayLimitsSource = {
|
|
16
|
+
limits?: unknown;
|
|
17
|
+
limitsJson?: unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function isGatewayLimitsObject(value: unknown): value is GatewayLimits {
|
|
21
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseLimitsJson(
|
|
25
|
+
limitsJson?: string | null,
|
|
26
|
+
): GatewayLimits | undefined {
|
|
27
|
+
if (!limitsJson?.trim()) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(limitsJson) as unknown;
|
|
33
|
+
return isGatewayLimitsObject(parsed) ? parsed : undefined;
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function coerceGatewayLimits(
|
|
40
|
+
source: GatewayLimitsSource,
|
|
41
|
+
): GatewayLimits | undefined {
|
|
42
|
+
if (isGatewayLimitsObject(source.limits)) {
|
|
43
|
+
return source.limits;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parseLimitsJson(
|
|
47
|
+
typeof source.limitsJson === "string" ? source.limitsJson : undefined,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function stringifyGatewayLimits(
|
|
52
|
+
limits?: GatewayLimits | null,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
if (!isGatewayLimitsObject(limits)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(limits);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Canonical config shape: nested `limits`, no encoded `limitsJson`. */
|
|
61
|
+
export function planForConfig<T extends object>(plan: T): T {
|
|
62
|
+
const limits = coerceGatewayLimits(plan as GatewayLimitsSource);
|
|
63
|
+
const next = { ...plan } as T & GatewayLimitsSource;
|
|
64
|
+
delete next.limitsJson;
|
|
65
|
+
if (limits) {
|
|
66
|
+
next.limits = limits;
|
|
67
|
+
} else {
|
|
68
|
+
delete next.limits;
|
|
69
|
+
}
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function gatewayForConfig<T extends object>(gateway: T): T {
|
|
74
|
+
return planForConfig(gateway);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Merge patch over prior; a patch `limitsJson` string replaces existing `limits`. */
|
|
78
|
+
export function mergeLimitsSource<T extends object>(
|
|
79
|
+
prior: T | undefined,
|
|
80
|
+
patch: T,
|
|
81
|
+
): T {
|
|
82
|
+
const merged = prior ? { ...prior, ...patch } : { ...patch };
|
|
83
|
+
const patchSource = patch as GatewayLimitsSource;
|
|
84
|
+
if (patchSource.limits === undefined && patchSource.limitsJson !== undefined) {
|
|
85
|
+
delete (merged as GatewayLimitsSource).limits;
|
|
86
|
+
}
|
|
87
|
+
return planForConfig(merged);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Stringify for the platform HTTP/DB `limitsJson` field. */
|
|
91
|
+
export function planLimitsForPlatform(plan: object): string | undefined {
|
|
92
|
+
return stringifyGatewayLimits(coerceGatewayLimits(plan as GatewayLimitsSource));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateLimitsField(
|
|
96
|
+
value: unknown,
|
|
97
|
+
field: string,
|
|
98
|
+
): Array<{ field: string; message: string }> {
|
|
99
|
+
if (value == null || value === "") {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
if (!value.trim()) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(value) as unknown;
|
|
109
|
+
if (!isGatewayLimitsObject(parsed)) {
|
|
110
|
+
return [{ field, message: `${field} must be a JSON object.` }];
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
} catch {
|
|
114
|
+
return [{ field, message: `${field} is not valid JSON.` }];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isGatewayLimitsObject(value)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [{ field, message: `${field} must be a JSON object.` }];
|
|
123
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
type GatewayLimits,
|
|
6
|
+
coerceGatewayLimits,
|
|
7
|
+
planForConfig,
|
|
8
|
+
gatewayForConfig,
|
|
9
|
+
mergeLimitsSource,
|
|
10
|
+
planLimitsForPlatform,
|
|
11
|
+
} from "./gateway-limits.js";
|
|
4
12
|
import {
|
|
5
13
|
APITOGO_CONFIG_FILENAME,
|
|
6
14
|
extractManifest,
|
|
@@ -28,6 +36,25 @@ export type ApitogoManifestEnv = keyof typeof APITOGO_MANIFEST_ENV_FILES;
|
|
|
28
36
|
|
|
29
37
|
const BillingPeriodSchema = z.enum(["month", "year", "monthly", "annual"]);
|
|
30
38
|
|
|
39
|
+
const GatewayLimitsSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
rateLimit: z
|
|
42
|
+
.object({
|
|
43
|
+
calls: z.number(),
|
|
44
|
+
renewalPeriodSeconds: z.number().optional(),
|
|
45
|
+
})
|
|
46
|
+
.passthrough()
|
|
47
|
+
.optional(),
|
|
48
|
+
quota: z
|
|
49
|
+
.object({
|
|
50
|
+
calls: z.number(),
|
|
51
|
+
renewalPeriodDays: z.number().optional(),
|
|
52
|
+
})
|
|
53
|
+
.passthrough()
|
|
54
|
+
.optional(),
|
|
55
|
+
})
|
|
56
|
+
.passthrough();
|
|
57
|
+
|
|
31
58
|
const PriceCurrencySchema = z
|
|
32
59
|
.string()
|
|
33
60
|
.trim()
|
|
@@ -56,6 +83,8 @@ export const ManifestPlanInputSchema = z.object({
|
|
|
56
83
|
priceAmount: z.number().default(0),
|
|
57
84
|
priceCurrency: PriceCurrencySchema.optional(),
|
|
58
85
|
billingPeriod: BillingPeriodSchema.optional(),
|
|
86
|
+
limits: GatewayLimitsSchema.optional(),
|
|
87
|
+
/** @deprecated Use `limits`. Encoded string still accepted on load. */
|
|
59
88
|
limitsJson: z.string().optional(),
|
|
60
89
|
includedActionKeys: z.array(z.string()).optional(),
|
|
61
90
|
});
|
|
@@ -138,6 +167,8 @@ export const DevPortalLocalManifestSchema = z.object({
|
|
|
138
167
|
oidcMetadataUrl: z.string().optional(),
|
|
139
168
|
oidcAllowedIssuers: z.array(z.string()).optional(),
|
|
140
169
|
oidcAudiences: z.array(z.string()).optional(),
|
|
170
|
+
limits: GatewayLimitsSchema.optional(),
|
|
171
|
+
/** @deprecated Use `limits`. Encoded string still accepted on load. */
|
|
141
172
|
limitsJson: z.string().optional(),
|
|
142
173
|
publicHealthActionKey: z.string().optional(),
|
|
143
174
|
publicRoutes: z.array(z.string()).optional(),
|
|
@@ -169,6 +200,7 @@ export type DevPortalPreviewPlan = {
|
|
|
169
200
|
priceAmount: number;
|
|
170
201
|
priceCurrency: string;
|
|
171
202
|
billingPeriod: string;
|
|
203
|
+
limits: GatewayLimits | null;
|
|
172
204
|
limitsJson: string | null;
|
|
173
205
|
includedActionKeys: string[];
|
|
174
206
|
};
|
|
@@ -242,7 +274,8 @@ export function plansToPreviewCatalog(
|
|
|
242
274
|
priceAmount: plan.priceAmount ?? 0,
|
|
243
275
|
priceCurrency: plan.priceCurrency ?? "usd",
|
|
244
276
|
billingPeriod: normalizeBillingPeriod(plan.billingPeriod),
|
|
245
|
-
|
|
277
|
+
limits: coerceGatewayLimits(plan) ?? null,
|
|
278
|
+
limitsJson: planLimitsForPlatform(plan) ?? null,
|
|
246
279
|
includedActionKeys: plan.includedActionKeys ?? [],
|
|
247
280
|
})),
|
|
248
281
|
};
|
|
@@ -262,7 +295,7 @@ export function mergePlansBySku(
|
|
|
262
295
|
}
|
|
263
296
|
for (const plan of patch) {
|
|
264
297
|
const prior = bySku.get(plan.sku);
|
|
265
|
-
bySku.set(plan.sku, prior
|
|
298
|
+
bySku.set(plan.sku, mergeLimitsSource(prior, plan));
|
|
266
299
|
}
|
|
267
300
|
return Array.from(bySku.values());
|
|
268
301
|
}
|
|
@@ -384,7 +417,7 @@ export function mergeManifest(
|
|
|
384
417
|
merged.backend = { ...base.backend, ...override.backend };
|
|
385
418
|
}
|
|
386
419
|
if (override.gateway) {
|
|
387
|
-
merged.gateway =
|
|
420
|
+
merged.gateway = mergeLimitsSource(base.gateway, override.gateway);
|
|
388
421
|
}
|
|
389
422
|
if (override.plans) {
|
|
390
423
|
merged.plans = mergePlansBySku(base.plans, override.plans);
|
|
@@ -423,7 +456,14 @@ export function parseLocalManifestJson(
|
|
|
423
456
|
if (!result.success) {
|
|
424
457
|
return null;
|
|
425
458
|
}
|
|
426
|
-
|
|
459
|
+
const data = result.data;
|
|
460
|
+
if (data.plans) {
|
|
461
|
+
data.plans = data.plans.map((plan) => planForConfig(plan));
|
|
462
|
+
}
|
|
463
|
+
if (data.gateway) {
|
|
464
|
+
data.gateway = gatewayForConfig(data.gateway);
|
|
465
|
+
}
|
|
466
|
+
return data;
|
|
427
467
|
} catch {
|
|
428
468
|
return null;
|
|
429
469
|
}
|
|
@@ -274,6 +274,25 @@ const PlanCatalogItemSchema = z.object({
|
|
|
274
274
|
)
|
|
275
275
|
.optional(),
|
|
276
276
|
billingPeriod: z.string().optional(),
|
|
277
|
+
limits: z
|
|
278
|
+
.object({
|
|
279
|
+
rateLimit: z
|
|
280
|
+
.object({
|
|
281
|
+
calls: z.number(),
|
|
282
|
+
renewalPeriodSeconds: z.number().optional(),
|
|
283
|
+
})
|
|
284
|
+
.passthrough()
|
|
285
|
+
.optional(),
|
|
286
|
+
quota: z
|
|
287
|
+
.object({
|
|
288
|
+
calls: z.number(),
|
|
289
|
+
renewalPeriodDays: z.number().optional(),
|
|
290
|
+
})
|
|
291
|
+
.passthrough()
|
|
292
|
+
.optional(),
|
|
293
|
+
})
|
|
294
|
+
.passthrough()
|
|
295
|
+
.optional(),
|
|
277
296
|
limitsJson: z.string().optional(),
|
|
278
297
|
includedActionKeys: z.array(z.string()).optional(),
|
|
279
298
|
});
|
package/src/vite/config.ts
CHANGED
|
@@ -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
|
+
}
|