@farthershore/product 0.2.0 → 0.3.0
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/README.md +83 -15
- package/dist/bin.js +231 -69
- package/dist/codegen.js +8 -1
- package/dist/index.js +231 -69
- package/dist/types/business.d.ts +42 -4
- package/dist/types/index.d.ts +1 -1
- package/dist/types/ir-types.d.ts +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Product-as-Code SDK for Farther Shore. Builder repos use this package from
|
|
4
4
|
`product/product.config.ts` to declare product contracts in TypeScript. Builders
|
|
5
|
-
author and export a `Business`;
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
author and export a `Business`; Farther Shore compiles that program to
|
|
6
|
+
backend-owned IR, validates it, and applies it through Core. Every product is
|
|
7
|
+
created with a GitHub repo that contains the editable `frontend/` starter and
|
|
8
|
+
the Product SDK entrypoint; connecting GitHub is a product-creation
|
|
9
|
+
precondition.
|
|
8
10
|
|
|
9
11
|
## Install
|
|
10
12
|
|
|
@@ -31,10 +33,11 @@ frontend/
|
|
|
31
33
|
...
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
`product/` is authored Product SDK code
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
`product/` is authored Product SDK code and may be split into any imported files
|
|
37
|
+
the builder wants. `frontend/` is the generated editable starter app and is built
|
|
38
|
+
by the frontend pipeline only. Runtime state, accepted specs, compilation
|
|
39
|
+
records, deployment runs, and compiled IR are not checked into the builder repo;
|
|
40
|
+
Farther Shore stores them in Core.
|
|
38
41
|
|
|
39
42
|
## Example
|
|
40
43
|
|
|
@@ -61,7 +64,16 @@ Modules are plain synchronous functions:
|
|
|
61
64
|
import { fs, type ProductModule } from "@farthershore/product";
|
|
62
65
|
|
|
63
66
|
export const configureMeters: ProductModule = (product) => {
|
|
64
|
-
product.
|
|
67
|
+
product.requests();
|
|
68
|
+
|
|
69
|
+
const tokens = product.meter("tokens_used", {
|
|
70
|
+
unit: "token",
|
|
71
|
+
estimate: 500,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
product.feature("runs").route("POST /v1/runs", {
|
|
75
|
+
reports: tokens,
|
|
76
|
+
});
|
|
65
77
|
};
|
|
66
78
|
|
|
67
79
|
export const configurePlans: ProductModule = (product) => {
|
|
@@ -80,10 +92,57 @@ export const configurePlans: ProductModule = (product) => {
|
|
|
80
92
|
};
|
|
81
93
|
```
|
|
82
94
|
|
|
83
|
-
##
|
|
95
|
+
## Metering
|
|
96
|
+
|
|
97
|
+
`product.requests()` declares the platform-managed request meter and applies
|
|
98
|
+
`requests = 1` to every metered route. Builders do not need backend code for
|
|
99
|
+
plain request counting.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const product = fs.product("croncloud", {
|
|
103
|
+
baseUrl: "https://api.croncloud.com",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
product.requests();
|
|
107
|
+
|
|
108
|
+
const tokens = product.meter("tokens_used", {
|
|
109
|
+
unit: "token",
|
|
110
|
+
estimate: 500,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
product.feature("runs").route("POST /v1/runs", {
|
|
114
|
+
reports: tokens,
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Route metering is explicit:
|
|
84
119
|
|
|
85
|
-
|
|
86
|
-
|
|
120
|
+
- omitted route metering inherits product defaults such as `requests = 1`
|
|
121
|
+
- `reports` declares dynamic meters the upstream may report with
|
|
122
|
+
`@farthershore/metering`
|
|
123
|
+
- `costs` declares gateway-known fixed usage values for a route
|
|
124
|
+
- `estimates` overrides reusable meter estimates for admission checks
|
|
125
|
+
- `unmetered: true` clears all inherited and explicit route metering
|
|
126
|
+
- `inheritDefaultMeters: false` disables inherited defaults only
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const credits = product.meter("api_credits", { unit: "credit" });
|
|
130
|
+
|
|
131
|
+
product.defaultMeters(credits.fixed(1));
|
|
132
|
+
|
|
133
|
+
product.feature("exports").route("POST /v1/bulk-export", {
|
|
134
|
+
costs: credits.fixed(10),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
product.feature("health").route("GET /healthz", {
|
|
138
|
+
unmetered: true,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Lifecycle apply paths
|
|
143
|
+
|
|
144
|
+
Generated product repos use GitHub as the required automation and frontend
|
|
145
|
+
workspace:
|
|
87
146
|
|
|
88
147
|
1. Loads `product/product.config.ts`.
|
|
89
148
|
2. Requires the default export to be the `Business` returned by
|
|
@@ -93,9 +152,15 @@ owns that workflow:
|
|
|
93
152
|
4. Validates the result against the deployed platform contract.
|
|
94
153
|
5. Publishes the accepted release through Core so edge artifacts propagate.
|
|
95
154
|
|
|
96
|
-
The
|
|
97
|
-
|
|
98
|
-
|
|
155
|
+
The same IR can also be applied to an already repo-backed product through
|
|
156
|
+
trusted Core APIs, for example `farthershore build --apply <product>`. That path
|
|
157
|
+
is useful for local validation/apply loops, but it is not a replacement for the
|
|
158
|
+
product repo. The repo remains the required frontend customization workspace
|
|
159
|
+
because `frontend/` is where the starter UI and all custom React code live.
|
|
160
|
+
|
|
161
|
+
The bundled `farthershore-manifest-build` binary is shared by the bot,
|
|
162
|
+
build-runner, and CLI. It emits the deterministic Manifest IR envelope that Core
|
|
163
|
+
accepts; Core, not the user repo, remains the lifecycle authority.
|
|
99
164
|
|
|
100
165
|
## Public API
|
|
101
166
|
|
|
@@ -104,11 +169,14 @@ normal builder workflow.
|
|
|
104
169
|
- `business.use(...modules)` — compose Product SDK modules from any files under
|
|
105
170
|
`product/`.
|
|
106
171
|
- `business.meter(...)` — declare billable or enforceable dimensions.
|
|
172
|
+
- `business.requests()` — declare and inherit the platform-managed successful
|
|
173
|
+
request meter.
|
|
174
|
+
- `business.defaultMeters(...)` — apply reusable fixed costs to metered routes.
|
|
107
175
|
- `business.resource(...)` — declare counted resources for resource-count
|
|
108
176
|
constraints.
|
|
109
177
|
- `business.capability(...)` — declare capability bundles and plan grants.
|
|
110
178
|
- `business.feature(...)` / `business.api.route(...)` — declare gateway routes,
|
|
111
|
-
|
|
179
|
+
static costs, dynamic reports, estimates, and action metadata.
|
|
112
180
|
- `business.policy(...)` — declare policy files in code.
|
|
113
181
|
- `business.plan(...)` — declare plan pricing, limits, grants, and lifecycle
|
|
114
182
|
behavior.
|
package/dist/bin.js
CHANGED
|
@@ -1051,10 +1051,11 @@ function validateRouteMeters(spec, ctx) {
|
|
|
1051
1051
|
for (const [featureKey, feature] of Object.entries(spec.features)) {
|
|
1052
1052
|
const routes = feature.routes ?? [];
|
|
1053
1053
|
routes.forEach((route, routeIdx) => {
|
|
1054
|
-
|
|
1054
|
+
const routeMeters = routeMeterKeys(route);
|
|
1055
|
+
if (routeMeters.length === 0)
|
|
1055
1056
|
return;
|
|
1056
1057
|
anyRouteDeclaresMeters = true;
|
|
1057
|
-
|
|
1058
|
+
routeMeters.forEach((meter, meterIdx) => {
|
|
1058
1059
|
if (meterKeys.has(meter))
|
|
1059
1060
|
return;
|
|
1060
1061
|
ctx.addIssue({
|
|
@@ -1064,7 +1065,7 @@ function validateRouteMeters(spec, ctx) {
|
|
|
1064
1065
|
featureKey,
|
|
1065
1066
|
"routes",
|
|
1066
1067
|
routeIdx,
|
|
1067
|
-
"
|
|
1068
|
+
"metering",
|
|
1068
1069
|
meterIdx
|
|
1069
1070
|
],
|
|
1070
1071
|
message: `Route references unknown meter "${meter}". Declare it in metering.meters[].`
|
|
@@ -1076,7 +1077,7 @@ function validateRouteMeters(spec, ctx) {
|
|
|
1076
1077
|
ctx.addIssue({
|
|
1077
1078
|
code: "custom",
|
|
1078
1079
|
path: ["metering", "meters"],
|
|
1079
|
-
message: "One or more routes declare
|
|
1080
|
+
message: "One or more routes declare metering keys but `metering.meters[]` is empty. Declare meters at the product level first."
|
|
1080
1081
|
});
|
|
1081
1082
|
}
|
|
1082
1083
|
}
|
|
@@ -1093,11 +1094,11 @@ function buildReachableMetersByPlan(spec, runtimeMeters) {
|
|
|
1093
1094
|
return out;
|
|
1094
1095
|
}
|
|
1095
1096
|
for (const feature of Object.values(spec.features)) {
|
|
1096
|
-
addFeatureMetersToReachable(feature,
|
|
1097
|
+
addFeatureMetersToReachable(feature, out);
|
|
1097
1098
|
}
|
|
1098
1099
|
return out;
|
|
1099
1100
|
}
|
|
1100
|
-
function addFeatureMetersToReachable(feature,
|
|
1101
|
+
function addFeatureMetersToReachable(feature, reachableByPlan) {
|
|
1101
1102
|
const grantedPlanKeys = feature.plans ?? [];
|
|
1102
1103
|
const routes = feature.routes ?? [];
|
|
1103
1104
|
for (const planKey of grantedPlanKeys) {
|
|
@@ -1107,16 +1108,25 @@ function addFeatureMetersToReachable(feature, runtimeMeters, reachableByPlan) {
|
|
|
1107
1108
|
for (const route of routes) {
|
|
1108
1109
|
if (route.unmetered === true)
|
|
1109
1110
|
continue;
|
|
1110
|
-
|
|
1111
|
-
|
|
1111
|
+
const modernRouteMeters = routeMeterKeys(route);
|
|
1112
|
+
if (modernRouteMeters.length > 0) {
|
|
1113
|
+
for (const meter of modernRouteMeters)
|
|
1112
1114
|
reachable.add(meter);
|
|
1113
1115
|
continue;
|
|
1114
1116
|
}
|
|
1115
|
-
for (const m of runtimeMeters)
|
|
1116
|
-
reachable.add(m);
|
|
1117
1117
|
}
|
|
1118
1118
|
}
|
|
1119
1119
|
}
|
|
1120
|
+
function routeMeterKeys(route) {
|
|
1121
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1122
|
+
for (const key of Object.keys(route.metering?.defaults ?? {}))
|
|
1123
|
+
keys.add(key);
|
|
1124
|
+
for (const key of route.metering?.reports ?? [])
|
|
1125
|
+
keys.add(key);
|
|
1126
|
+
for (const key of Object.keys(route.metering?.estimates ?? {}))
|
|
1127
|
+
keys.add(key);
|
|
1128
|
+
return [...keys];
|
|
1129
|
+
}
|
|
1120
1130
|
function extractLimitMeterKey(limit) {
|
|
1121
1131
|
if (!limit || typeof limit !== "object")
|
|
1122
1132
|
return null;
|
|
@@ -1185,6 +1195,10 @@ var meterDefinitionSchema = z13.object({
|
|
|
1185
1195
|
// freely. Old specs with `type: ...` parse cleanly because Zod
|
|
1186
1196
|
// strips unknown fields by default.
|
|
1187
1197
|
unit: z13.string().max(20).optional(),
|
|
1198
|
+
/** Reusable pre-request estimate for routes that dynamically report this meter. */
|
|
1199
|
+
estimate: z13.number().finite().nonnegative().optional(),
|
|
1200
|
+
/** Fixed per-request default applied by Product SDK helpers. */
|
|
1201
|
+
routeDefault: z13.number().finite().nonnegative().optional(),
|
|
1188
1202
|
/**
|
|
1189
1203
|
* Runtime enforcement semantics for this meter. This is compiled into
|
|
1190
1204
|
* signed gateway artifacts so the edge chooses reservation, settlement,
|
|
@@ -1291,6 +1305,15 @@ var usageMeterSchema = z13.object({
|
|
|
1291
1305
|
var usageBlockSchema = z13.object({
|
|
1292
1306
|
meters: z13.record(z13.string().min(1).max(64).regex(/^[a-z0-9_]+$/), usageMeterSchema)
|
|
1293
1307
|
});
|
|
1308
|
+
var routeMeteringSchema = z13.object({
|
|
1309
|
+
defaults: z13.record(z13.string().min(1).max(64), z13.number().finite().nonnegative()).optional(),
|
|
1310
|
+
reports: z13.array(z13.string().min(1).max(64)).max(20).optional(),
|
|
1311
|
+
estimates: z13.record(z13.string().min(1).max(64), z13.number().finite().nonnegative()).optional(),
|
|
1312
|
+
onStatusCodes: z13.union([
|
|
1313
|
+
z13.string().min(1).max(100),
|
|
1314
|
+
z13.array(z13.number().int().min(100).max(599)).min(1).max(100)
|
|
1315
|
+
]).optional()
|
|
1316
|
+
});
|
|
1294
1317
|
var featureRouteSchema = z13.object({
|
|
1295
1318
|
method: z13.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
|
|
1296
1319
|
// Path is the route under the product's baseUrl. OpenAPI parameter
|
|
@@ -1301,35 +1324,12 @@ var featureRouteSchema = z13.object({
|
|
|
1301
1324
|
// through. The compiler rejects ambiguous compound segments like
|
|
1302
1325
|
// `/foo/:a-:b` — parameter names must occupy whole segments.
|
|
1303
1326
|
path: z13.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]"),
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
// - non-empty → only these meters increment
|
|
1311
|
-
// - [] → REJECTED at parse (`ROUTE_METERS_EMPTY_ARRAY`).
|
|
1312
|
-
// Use `unmetered: true` for the explicit opt-out.
|
|
1313
|
-
// - null → treated as omitted (PATCH-clear UX)
|
|
1314
|
-
//
|
|
1315
|
-
// Each entry must resolve to a key in `metering.meters[].key`;
|
|
1316
|
-
// otherwise the cross-validation refinement
|
|
1317
|
-
// `validateRouteMeters` rejects with `UNKNOWN_METER_IN_ROUTE`.
|
|
1318
|
-
meters: z13.array(z13.string().min(1).max(64)).min(1, "Use `unmetered: true` instead of an empty array (typo guard against silently-unmetered routes)").max(20).nullable().optional(),
|
|
1319
|
-
// Explicit unmetered route. Mutually exclusive with `meters` (the
|
|
1320
|
-
// `superRefine` below catches any builder that sets both).
|
|
1321
|
-
// Compiles to `entitlement.fr[i].meters: []` on the wire side so
|
|
1322
|
-
// the gateway short-circuits both DO consume and event publish.
|
|
1323
|
-
unmetered: z13.boolean().optional()
|
|
1324
|
-
}).superRefine((route, ctx) => {
|
|
1325
|
-
if (route.unmetered === true && route.meters && route.meters.length > 0) {
|
|
1326
|
-
ctx.addIssue({
|
|
1327
|
-
code: "custom",
|
|
1328
|
-
message: "`unmetered: true` is mutually exclusive with `meters` \u2014 drop one",
|
|
1329
|
-
path: ["unmetered"]
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
});
|
|
1327
|
+
// Explicit no-usage route. Dynamic/static route metering is declared
|
|
1328
|
+
// exclusively under `metering`.
|
|
1329
|
+
unmetered: z13.boolean().optional(),
|
|
1330
|
+
metering: routeMeteringSchema.optional(),
|
|
1331
|
+
inheritDefaultMeters: z13.boolean().optional()
|
|
1332
|
+
}).strict();
|
|
1333
1333
|
var featureCatalogEntrySchema = z13.object({
|
|
1334
1334
|
// Optional human-friendly summary; surfaced in dashboards / settings UI.
|
|
1335
1335
|
description: z13.string().max(500).optional(),
|
|
@@ -1465,7 +1465,7 @@ var productSpecSchema = z13.object({
|
|
|
1465
1465
|
gracePeriodDays: z13.number().int().nonnegative().default(3),
|
|
1466
1466
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
1467
1467
|
// subscribers immediately; a DECREASE always defers to period end.
|
|
1468
|
-
// Read by
|
|
1468
|
+
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
1469
1469
|
applyLimitUpgradesInstantly: z13.boolean().optional(),
|
|
1470
1470
|
subscriberChangePolicy: subscriberChangePolicySchema.default({
|
|
1471
1471
|
default: "preserve_current_period",
|
|
@@ -1834,28 +1834,21 @@ var routeMatchSchema = z17.object({
|
|
|
1834
1834
|
});
|
|
1835
1835
|
var routeDefinitionSchema = z17.object({
|
|
1836
1836
|
match: routeMatchSchema,
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1837
|
+
metering: z17.object({
|
|
1838
|
+
defaults: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1839
|
+
reports: z17.array(z17.string().min(1).max(64)).max(20).optional(),
|
|
1840
|
+
estimates: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1841
|
+
onStatusCodes: z17.union([
|
|
1842
|
+
z17.string().min(1).max(100),
|
|
1843
|
+
z17.array(z17.number().int().min(100).max(599)).min(1).max(100)
|
|
1844
|
+
]).optional()
|
|
1845
|
+
}).optional(),
|
|
1846
1846
|
unmetered: z17.boolean().optional(),
|
|
1847
|
+
inheritDefaultMeters: z17.boolean().optional(),
|
|
1847
1848
|
/** Optional explicit action id. When absent, the compiler derives an
|
|
1848
1849
|
* implicit action from feature + method + path. */
|
|
1849
1850
|
action: z17.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/).optional()
|
|
1850
|
-
}).
|
|
1851
|
-
if (route.unmetered === true && route.meters && route.meters.length > 0) {
|
|
1852
|
-
ctx.addIssue({
|
|
1853
|
-
code: "custom",
|
|
1854
|
-
message: "`unmetered: true` is mutually exclusive with `meters` \u2014 drop one",
|
|
1855
|
-
path: ["unmetered"]
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
});
|
|
1851
|
+
}).strict();
|
|
1859
1852
|
var routeUpstreamSchema = z17.object({
|
|
1860
1853
|
override_origin: z17.string().url("override_origin must be a valid URL").nullable().default(null)
|
|
1861
1854
|
});
|
|
@@ -2143,7 +2136,7 @@ var productSpecV2Schema = z20.object({
|
|
|
2143
2136
|
envBranchPrefix: z20.string().max(50).nullable().optional()
|
|
2144
2137
|
}),
|
|
2145
2138
|
/**
|
|
2146
|
-
* Meter catalog. Referenced by
|
|
2139
|
+
* Meter catalog. Referenced by route `metering` metadata.
|
|
2147
2140
|
* Same shape as the legacy meterDefinitionSchema (re-exported from
|
|
2148
2141
|
* product.ts to keep one source of truth during the transition).
|
|
2149
2142
|
*
|
|
@@ -2167,7 +2160,7 @@ var productSpecV2Schema = z20.object({
|
|
|
2167
2160
|
gracePeriodDays: z20.number().int().nonnegative().default(3),
|
|
2168
2161
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
2169
2162
|
// subscribers immediately; a DECREASE always defers to period end.
|
|
2170
|
-
// Read by
|
|
2163
|
+
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
2171
2164
|
applyLimitUpgradesInstantly: z20.boolean().optional(),
|
|
2172
2165
|
subscriberChangePolicy: subscriberChangePolicySchema.default(DEFAULT_SUBSCRIBER_CHANGE_POLICY)
|
|
2173
2166
|
}).default({
|
|
@@ -2360,7 +2353,7 @@ function hashIr(ir) {
|
|
|
2360
2353
|
}
|
|
2361
2354
|
|
|
2362
2355
|
// src/version.ts
|
|
2363
|
-
var SDK_VERSION = true ? "0.
|
|
2356
|
+
var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
2364
2357
|
|
|
2365
2358
|
// src/business.ts
|
|
2366
2359
|
var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
|
|
@@ -2370,6 +2363,9 @@ function isCapabilityGrant(value) {
|
|
|
2370
2363
|
function keyOf(ref) {
|
|
2371
2364
|
return typeof ref === "string" ? ref : ref.key;
|
|
2372
2365
|
}
|
|
2366
|
+
function displayFromKey(key) {
|
|
2367
|
+
return key.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2368
|
+
}
|
|
2373
2369
|
function parseRouteMatch(match) {
|
|
2374
2370
|
const trimmed = match.trim();
|
|
2375
2371
|
const space = trimmed.indexOf(" ");
|
|
@@ -2410,6 +2406,7 @@ var Business = class {
|
|
|
2410
2406
|
name;
|
|
2411
2407
|
options;
|
|
2412
2408
|
graph = new ManifestResourceGraph();
|
|
2409
|
+
defaultMeterCosts = [];
|
|
2413
2410
|
frontendManifest;
|
|
2414
2411
|
productPatch = {};
|
|
2415
2412
|
/** Sugar for binding API routes to features. */
|
|
@@ -2566,8 +2563,37 @@ var Business = class {
|
|
|
2566
2563
|
}
|
|
2567
2564
|
meter(key, options) {
|
|
2568
2565
|
this.assertNewKey("meter", key, "meter");
|
|
2569
|
-
|
|
2570
|
-
|
|
2566
|
+
const { routeDefault, ...meterOptions } = options;
|
|
2567
|
+
this.graph.register("meter", key, {
|
|
2568
|
+
key,
|
|
2569
|
+
display: meterOptions.display ?? displayFromKey(key),
|
|
2570
|
+
...meterOptions
|
|
2571
|
+
});
|
|
2572
|
+
const ref = this.makeMeterRef(key);
|
|
2573
|
+
if (routeDefault !== void 0) {
|
|
2574
|
+
this.defaultMeterCosts.push(
|
|
2575
|
+
this.normalizeMeterCost(ref.fixed(routeDefault))
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
return ref;
|
|
2579
|
+
}
|
|
2580
|
+
requests(options = {}) {
|
|
2581
|
+
return this.meter("requests", {
|
|
2582
|
+
display: options.display ?? "Requests",
|
|
2583
|
+
unit: options.unit ?? "request",
|
|
2584
|
+
aggregation: options.aggregation ?? "COUNT",
|
|
2585
|
+
enforcementType: options.enforcementType ?? "estimated_then_settled",
|
|
2586
|
+
window: options.window,
|
|
2587
|
+
estimate: options.estimate ?? 1,
|
|
2588
|
+
routeDefault: 1
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
defaultMeters(costs) {
|
|
2592
|
+
const entries = Array.isArray(costs) ? costs : [costs];
|
|
2593
|
+
for (const cost of entries) {
|
|
2594
|
+
this.defaultMeterCosts.push(this.normalizeMeterCost(cost));
|
|
2595
|
+
}
|
|
2596
|
+
return this;
|
|
2571
2597
|
}
|
|
2572
2598
|
resource(name, options = {}) {
|
|
2573
2599
|
this.assertNewKey("counted_resource", name, "resource");
|
|
@@ -2740,6 +2766,7 @@ var Business = class {
|
|
|
2740
2766
|
* with structured issues when the declared state is invalid. */
|
|
2741
2767
|
toIR() {
|
|
2742
2768
|
this.assertGraphDependenciesSatisfied();
|
|
2769
|
+
this.assertRouteMeteringValid();
|
|
2743
2770
|
const candidate = {
|
|
2744
2771
|
irVersion: 1,
|
|
2745
2772
|
sdkVersion: SDK_VERSION,
|
|
@@ -2781,10 +2808,7 @@ var Business = class {
|
|
|
2781
2808
|
...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
|
|
2782
2809
|
},
|
|
2783
2810
|
metering: {
|
|
2784
|
-
meters: this.
|
|
2785
|
-
"meter",
|
|
2786
|
-
(meter) => meter.key
|
|
2787
|
-
),
|
|
2811
|
+
meters: this.buildMeterDefinitions(),
|
|
2788
2812
|
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2789
2813
|
},
|
|
2790
2814
|
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
@@ -2815,15 +2839,109 @@ var Business = class {
|
|
|
2815
2839
|
this.productPatch
|
|
2816
2840
|
);
|
|
2817
2841
|
}
|
|
2842
|
+
buildMeterDefinitions() {
|
|
2843
|
+
const routeValueMeters = this.routeValueMeterKeys();
|
|
2844
|
+
return this.graph.sortedValues("meter", (meter) => meter.key).map((meter) => {
|
|
2845
|
+
if (meter.aggregation !== void 0) return meter;
|
|
2846
|
+
if (!routeValueMeters.has(meter.key)) return meter;
|
|
2847
|
+
return { ...meter, aggregation: "SUM" };
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
routeValueMeterKeys() {
|
|
2851
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2852
|
+
for (const file of this.graph.values("feature")) {
|
|
2853
|
+
for (const route of file.routes) {
|
|
2854
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
2855
|
+
keys.add(meter);
|
|
2856
|
+
}
|
|
2857
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2858
|
+
keys.add(meter);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return keys;
|
|
2863
|
+
}
|
|
2818
2864
|
buildRoute(match, options) {
|
|
2819
2865
|
const parsed = parseRouteMatch(match);
|
|
2866
|
+
const metering = this.buildRouteMetering(options);
|
|
2820
2867
|
return {
|
|
2821
2868
|
match: parsed,
|
|
2822
|
-
...
|
|
2869
|
+
...metering ? { metering } : {},
|
|
2823
2870
|
...options.unmetered !== void 0 ? { unmetered: options.unmetered } : {},
|
|
2871
|
+
...options.inheritDefaultMeters !== void 0 ? { inheritDefaultMeters: options.inheritDefaultMeters } : {},
|
|
2824
2872
|
...options.action !== void 0 ? { action: keyOf(options.action) } : {}
|
|
2825
2873
|
};
|
|
2826
2874
|
}
|
|
2875
|
+
makeMeterRef(key) {
|
|
2876
|
+
return {
|
|
2877
|
+
kind: "meter",
|
|
2878
|
+
key,
|
|
2879
|
+
fixed: (value) => ({ kind: "meter_cost", meter: key, value }),
|
|
2880
|
+
estimate: (value) => ({ kind: "meter_cost", meter: key, value })
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
buildRouteMetering(options) {
|
|
2884
|
+
if (options.unmetered === true) return void 0;
|
|
2885
|
+
const defaults = {};
|
|
2886
|
+
if (options.inheritDefaultMeters !== false) {
|
|
2887
|
+
for (const cost of this.defaultMeterCosts) {
|
|
2888
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
for (const cost of this.normalizeMeterCosts(options.costs)) {
|
|
2892
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2893
|
+
}
|
|
2894
|
+
const reports = [
|
|
2895
|
+
...new Set(this.normalizeMeterRefs(options.reports ?? []))
|
|
2896
|
+
];
|
|
2897
|
+
const estimates = {};
|
|
2898
|
+
for (const meter of reports) {
|
|
2899
|
+
const definition = this.graph.get(
|
|
2900
|
+
"meter",
|
|
2901
|
+
meter
|
|
2902
|
+
)?.value;
|
|
2903
|
+
if (typeof definition?.estimate === "number") {
|
|
2904
|
+
estimates[meter] = definition.estimate;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
for (const [meter, value] of Object.entries(options.estimates ?? {})) {
|
|
2908
|
+
estimates[meter] = value;
|
|
2909
|
+
}
|
|
2910
|
+
const out = {};
|
|
2911
|
+
if (Object.keys(defaults).length) out.defaults = defaults;
|
|
2912
|
+
if (reports.length) out.reports = reports;
|
|
2913
|
+
if (Object.keys(estimates).length) out.estimates = estimates;
|
|
2914
|
+
if (options.onStatusCodes !== void 0)
|
|
2915
|
+
out.onStatusCodes = options.onStatusCodes;
|
|
2916
|
+
return Object.keys(out).length ? out : void 0;
|
|
2917
|
+
}
|
|
2918
|
+
normalizeMeterCost(cost) {
|
|
2919
|
+
if (!cost || cost.kind !== "meter_cost") {
|
|
2920
|
+
throw new ManifestBuilderError(
|
|
2921
|
+
"meter cost must be created by meter.fixed(value)"
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
if (!this.graph.has("meter", cost.meter)) {
|
|
2925
|
+
throw new ManifestBuilderError(
|
|
2926
|
+
`meter cost references unknown meter "${cost.meter}"`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
if (!Number.isFinite(cost.value) || cost.value < 0) {
|
|
2930
|
+
throw new ManifestBuilderError(
|
|
2931
|
+
`meter "${cost.meter}" fixed value must be a non-negative finite number`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
return cost;
|
|
2935
|
+
}
|
|
2936
|
+
normalizeMeterCosts(costs) {
|
|
2937
|
+
if (!costs) return [];
|
|
2938
|
+
const entries = Array.isArray(costs) ? costs : [costs];
|
|
2939
|
+
return entries.map((cost) => this.normalizeMeterCost(cost));
|
|
2940
|
+
}
|
|
2941
|
+
normalizeMeterRefs(refs) {
|
|
2942
|
+
const entries = Array.isArray(refs) ? refs : [refs];
|
|
2943
|
+
return entries.map(keyOf);
|
|
2944
|
+
}
|
|
2827
2945
|
ensureFrontendManifest() {
|
|
2828
2946
|
this.frontendManifest ??= { version: 1, nav: [], pages: [] };
|
|
2829
2947
|
this.syncFrontendGraphNode(this.frontendManifest);
|
|
@@ -2922,7 +3040,9 @@ var Business = class {
|
|
|
2922
3040
|
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
2923
3041
|
}
|
|
2924
3042
|
featureDependsOn(file) {
|
|
2925
|
-
const meterKeys = file.routes.flatMap(
|
|
3043
|
+
const meterKeys = file.routes.flatMap(
|
|
3044
|
+
(route) => this.routeMeterDependencyKeys(route)
|
|
3045
|
+
);
|
|
2926
3046
|
return [
|
|
2927
3047
|
...this.dependenciesFor("policy", file.policies),
|
|
2928
3048
|
...this.dependenciesFor("capability", file.capabilities),
|
|
@@ -2930,6 +3050,48 @@ var Business = class {
|
|
|
2930
3050
|
...this.dependenciesFor("meter", meterKeys)
|
|
2931
3051
|
];
|
|
2932
3052
|
}
|
|
3053
|
+
routeMeterDependencyKeys(route) {
|
|
3054
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3055
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
3056
|
+
keys.add(meter);
|
|
3057
|
+
}
|
|
3058
|
+
for (const meter of route.metering?.reports ?? []) keys.add(meter);
|
|
3059
|
+
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
3060
|
+
keys.add(meter);
|
|
3061
|
+
}
|
|
3062
|
+
return [...keys];
|
|
3063
|
+
}
|
|
3064
|
+
assertRouteMeteringValid() {
|
|
3065
|
+
for (const file of this.graph.values("feature")) {
|
|
3066
|
+
file.routes.forEach((route, routeIndex) => {
|
|
3067
|
+
if (route.unmetered === true) return;
|
|
3068
|
+
const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
|
|
3069
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
3070
|
+
if (defaults.has(meter)) {
|
|
3071
|
+
throw new ManifestBuilderError(
|
|
3072
|
+
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
3077
|
+
const definition = this.graph.get(
|
|
3078
|
+
"meter",
|
|
3079
|
+
meter
|
|
3080
|
+
)?.value;
|
|
3081
|
+
const enforcement = definition?.enforcementType ?? "estimated_then_settled";
|
|
3082
|
+
const hasEstimate = route.metering?.estimates && Object.prototype.hasOwnProperty.call(
|
|
3083
|
+
route.metering.estimates,
|
|
3084
|
+
meter
|
|
3085
|
+
);
|
|
3086
|
+
if ((enforcement === "exact_pre_request" || enforcement === "estimated_then_settled") && !hasEstimate) {
|
|
3087
|
+
throw new ManifestBuilderError(
|
|
3088
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" needs an estimate for gateway admission`
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
2933
3095
|
actionDependsOn(featureKey, action) {
|
|
2934
3096
|
return [
|
|
2935
3097
|
resourceDependency("feature", featureKey),
|
package/dist/codegen.js
CHANGED
|
@@ -143,7 +143,14 @@ function generateManifestSource(ir) {
|
|
|
143
143
|
options.requiredFlags = file.runtime.required_flags;
|
|
144
144
|
options.routes = file.routes.map((route) => ({
|
|
145
145
|
match: `${route.match.method ?? "*"} ${route.match.path}`,
|
|
146
|
-
...route.
|
|
146
|
+
...route.metering?.reports?.length ? { reports: route.metering.reports } : {},
|
|
147
|
+
...route.metering?.defaults && Object.keys(route.metering.defaults).length ? {
|
|
148
|
+
costs: Object.entries(route.metering.defaults).map(
|
|
149
|
+
([meter, value]) => ({ kind: "meter_cost", meter, value })
|
|
150
|
+
)
|
|
151
|
+
} : {},
|
|
152
|
+
...route.metering?.estimates && Object.keys(route.metering.estimates).length ? { estimates: route.metering.estimates } : {},
|
|
153
|
+
...route.metering?.onStatusCodes !== void 0 ? { onStatusCodes: route.metering.onStatusCodes } : {},
|
|
147
154
|
...route.unmetered !== void 0 ? { unmetered: route.unmetered } : {},
|
|
148
155
|
...route.action !== void 0 ? { action: route.action } : {}
|
|
149
156
|
}));
|
package/dist/index.js
CHANGED
|
@@ -1043,10 +1043,11 @@ function validateRouteMeters(spec, ctx) {
|
|
|
1043
1043
|
for (const [featureKey, feature] of Object.entries(spec.features)) {
|
|
1044
1044
|
const routes = feature.routes ?? [];
|
|
1045
1045
|
routes.forEach((route, routeIdx) => {
|
|
1046
|
-
|
|
1046
|
+
const routeMeters = routeMeterKeys(route);
|
|
1047
|
+
if (routeMeters.length === 0)
|
|
1047
1048
|
return;
|
|
1048
1049
|
anyRouteDeclaresMeters = true;
|
|
1049
|
-
|
|
1050
|
+
routeMeters.forEach((meter, meterIdx) => {
|
|
1050
1051
|
if (meterKeys.has(meter))
|
|
1051
1052
|
return;
|
|
1052
1053
|
ctx.addIssue({
|
|
@@ -1056,7 +1057,7 @@ function validateRouteMeters(spec, ctx) {
|
|
|
1056
1057
|
featureKey,
|
|
1057
1058
|
"routes",
|
|
1058
1059
|
routeIdx,
|
|
1059
|
-
"
|
|
1060
|
+
"metering",
|
|
1060
1061
|
meterIdx
|
|
1061
1062
|
],
|
|
1062
1063
|
message: `Route references unknown meter "${meter}". Declare it in metering.meters[].`
|
|
@@ -1068,7 +1069,7 @@ function validateRouteMeters(spec, ctx) {
|
|
|
1068
1069
|
ctx.addIssue({
|
|
1069
1070
|
code: "custom",
|
|
1070
1071
|
path: ["metering", "meters"],
|
|
1071
|
-
message: "One or more routes declare
|
|
1072
|
+
message: "One or more routes declare metering keys but `metering.meters[]` is empty. Declare meters at the product level first."
|
|
1072
1073
|
});
|
|
1073
1074
|
}
|
|
1074
1075
|
}
|
|
@@ -1085,11 +1086,11 @@ function buildReachableMetersByPlan(spec, runtimeMeters) {
|
|
|
1085
1086
|
return out;
|
|
1086
1087
|
}
|
|
1087
1088
|
for (const feature of Object.values(spec.features)) {
|
|
1088
|
-
addFeatureMetersToReachable(feature,
|
|
1089
|
+
addFeatureMetersToReachable(feature, out);
|
|
1089
1090
|
}
|
|
1090
1091
|
return out;
|
|
1091
1092
|
}
|
|
1092
|
-
function addFeatureMetersToReachable(feature,
|
|
1093
|
+
function addFeatureMetersToReachable(feature, reachableByPlan) {
|
|
1093
1094
|
const grantedPlanKeys = feature.plans ?? [];
|
|
1094
1095
|
const routes = feature.routes ?? [];
|
|
1095
1096
|
for (const planKey of grantedPlanKeys) {
|
|
@@ -1099,16 +1100,25 @@ function addFeatureMetersToReachable(feature, runtimeMeters, reachableByPlan) {
|
|
|
1099
1100
|
for (const route of routes) {
|
|
1100
1101
|
if (route.unmetered === true)
|
|
1101
1102
|
continue;
|
|
1102
|
-
|
|
1103
|
-
|
|
1103
|
+
const modernRouteMeters = routeMeterKeys(route);
|
|
1104
|
+
if (modernRouteMeters.length > 0) {
|
|
1105
|
+
for (const meter of modernRouteMeters)
|
|
1104
1106
|
reachable.add(meter);
|
|
1105
1107
|
continue;
|
|
1106
1108
|
}
|
|
1107
|
-
for (const m of runtimeMeters)
|
|
1108
|
-
reachable.add(m);
|
|
1109
1109
|
}
|
|
1110
1110
|
}
|
|
1111
1111
|
}
|
|
1112
|
+
function routeMeterKeys(route) {
|
|
1113
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1114
|
+
for (const key of Object.keys(route.metering?.defaults ?? {}))
|
|
1115
|
+
keys.add(key);
|
|
1116
|
+
for (const key of route.metering?.reports ?? [])
|
|
1117
|
+
keys.add(key);
|
|
1118
|
+
for (const key of Object.keys(route.metering?.estimates ?? {}))
|
|
1119
|
+
keys.add(key);
|
|
1120
|
+
return [...keys];
|
|
1121
|
+
}
|
|
1112
1122
|
function extractLimitMeterKey(limit) {
|
|
1113
1123
|
if (!limit || typeof limit !== "object")
|
|
1114
1124
|
return null;
|
|
@@ -1177,6 +1187,10 @@ var meterDefinitionSchema = z13.object({
|
|
|
1177
1187
|
// freely. Old specs with `type: ...` parse cleanly because Zod
|
|
1178
1188
|
// strips unknown fields by default.
|
|
1179
1189
|
unit: z13.string().max(20).optional(),
|
|
1190
|
+
/** Reusable pre-request estimate for routes that dynamically report this meter. */
|
|
1191
|
+
estimate: z13.number().finite().nonnegative().optional(),
|
|
1192
|
+
/** Fixed per-request default applied by Product SDK helpers. */
|
|
1193
|
+
routeDefault: z13.number().finite().nonnegative().optional(),
|
|
1180
1194
|
/**
|
|
1181
1195
|
* Runtime enforcement semantics for this meter. This is compiled into
|
|
1182
1196
|
* signed gateway artifacts so the edge chooses reservation, settlement,
|
|
@@ -1283,6 +1297,15 @@ var usageMeterSchema = z13.object({
|
|
|
1283
1297
|
var usageBlockSchema = z13.object({
|
|
1284
1298
|
meters: z13.record(z13.string().min(1).max(64).regex(/^[a-z0-9_]+$/), usageMeterSchema)
|
|
1285
1299
|
});
|
|
1300
|
+
var routeMeteringSchema = z13.object({
|
|
1301
|
+
defaults: z13.record(z13.string().min(1).max(64), z13.number().finite().nonnegative()).optional(),
|
|
1302
|
+
reports: z13.array(z13.string().min(1).max(64)).max(20).optional(),
|
|
1303
|
+
estimates: z13.record(z13.string().min(1).max(64), z13.number().finite().nonnegative()).optional(),
|
|
1304
|
+
onStatusCodes: z13.union([
|
|
1305
|
+
z13.string().min(1).max(100),
|
|
1306
|
+
z13.array(z13.number().int().min(100).max(599)).min(1).max(100)
|
|
1307
|
+
]).optional()
|
|
1308
|
+
});
|
|
1286
1309
|
var featureRouteSchema = z13.object({
|
|
1287
1310
|
method: z13.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
|
|
1288
1311
|
// Path is the route under the product's baseUrl. OpenAPI parameter
|
|
@@ -1293,35 +1316,12 @@ var featureRouteSchema = z13.object({
|
|
|
1293
1316
|
// through. The compiler rejects ambiguous compound segments like
|
|
1294
1317
|
// `/foo/:a-:b` — parameter names must occupy whole segments.
|
|
1295
1318
|
path: z13.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]"),
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
// - non-empty → only these meters increment
|
|
1303
|
-
// - [] → REJECTED at parse (`ROUTE_METERS_EMPTY_ARRAY`).
|
|
1304
|
-
// Use `unmetered: true` for the explicit opt-out.
|
|
1305
|
-
// - null → treated as omitted (PATCH-clear UX)
|
|
1306
|
-
//
|
|
1307
|
-
// Each entry must resolve to a key in `metering.meters[].key`;
|
|
1308
|
-
// otherwise the cross-validation refinement
|
|
1309
|
-
// `validateRouteMeters` rejects with `UNKNOWN_METER_IN_ROUTE`.
|
|
1310
|
-
meters: z13.array(z13.string().min(1).max(64)).min(1, "Use `unmetered: true` instead of an empty array (typo guard against silently-unmetered routes)").max(20).nullable().optional(),
|
|
1311
|
-
// Explicit unmetered route. Mutually exclusive with `meters` (the
|
|
1312
|
-
// `superRefine` below catches any builder that sets both).
|
|
1313
|
-
// Compiles to `entitlement.fr[i].meters: []` on the wire side so
|
|
1314
|
-
// the gateway short-circuits both DO consume and event publish.
|
|
1315
|
-
unmetered: z13.boolean().optional()
|
|
1316
|
-
}).superRefine((route, ctx) => {
|
|
1317
|
-
if (route.unmetered === true && route.meters && route.meters.length > 0) {
|
|
1318
|
-
ctx.addIssue({
|
|
1319
|
-
code: "custom",
|
|
1320
|
-
message: "`unmetered: true` is mutually exclusive with `meters` \u2014 drop one",
|
|
1321
|
-
path: ["unmetered"]
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
});
|
|
1319
|
+
// Explicit no-usage route. Dynamic/static route metering is declared
|
|
1320
|
+
// exclusively under `metering`.
|
|
1321
|
+
unmetered: z13.boolean().optional(),
|
|
1322
|
+
metering: routeMeteringSchema.optional(),
|
|
1323
|
+
inheritDefaultMeters: z13.boolean().optional()
|
|
1324
|
+
}).strict();
|
|
1325
1325
|
var featureCatalogEntrySchema = z13.object({
|
|
1326
1326
|
// Optional human-friendly summary; surfaced in dashboards / settings UI.
|
|
1327
1327
|
description: z13.string().max(500).optional(),
|
|
@@ -1457,7 +1457,7 @@ var productSpecSchema = z13.object({
|
|
|
1457
1457
|
gracePeriodDays: z13.number().int().nonnegative().default(3),
|
|
1458
1458
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
1459
1459
|
// subscribers immediately; a DECREASE always defers to period end.
|
|
1460
|
-
// Read by
|
|
1460
|
+
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
1461
1461
|
applyLimitUpgradesInstantly: z13.boolean().optional(),
|
|
1462
1462
|
subscriberChangePolicy: subscriberChangePolicySchema.default({
|
|
1463
1463
|
default: "preserve_current_period",
|
|
@@ -1826,28 +1826,21 @@ var routeMatchSchema = z17.object({
|
|
|
1826
1826
|
});
|
|
1827
1827
|
var routeDefinitionSchema = z17.object({
|
|
1828
1828
|
match: routeMatchSchema,
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1829
|
+
metering: z17.object({
|
|
1830
|
+
defaults: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1831
|
+
reports: z17.array(z17.string().min(1).max(64)).max(20).optional(),
|
|
1832
|
+
estimates: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1833
|
+
onStatusCodes: z17.union([
|
|
1834
|
+
z17.string().min(1).max(100),
|
|
1835
|
+
z17.array(z17.number().int().min(100).max(599)).min(1).max(100)
|
|
1836
|
+
]).optional()
|
|
1837
|
+
}).optional(),
|
|
1838
1838
|
unmetered: z17.boolean().optional(),
|
|
1839
|
+
inheritDefaultMeters: z17.boolean().optional(),
|
|
1839
1840
|
/** Optional explicit action id. When absent, the compiler derives an
|
|
1840
1841
|
* implicit action from feature + method + path. */
|
|
1841
1842
|
action: z17.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/).optional()
|
|
1842
|
-
}).
|
|
1843
|
-
if (route.unmetered === true && route.meters && route.meters.length > 0) {
|
|
1844
|
-
ctx.addIssue({
|
|
1845
|
-
code: "custom",
|
|
1846
|
-
message: "`unmetered: true` is mutually exclusive with `meters` \u2014 drop one",
|
|
1847
|
-
path: ["unmetered"]
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
});
|
|
1843
|
+
}).strict();
|
|
1851
1844
|
var routeUpstreamSchema = z17.object({
|
|
1852
1845
|
override_origin: z17.string().url("override_origin must be a valid URL").nullable().default(null)
|
|
1853
1846
|
});
|
|
@@ -2135,7 +2128,7 @@ var productSpecV2Schema = z20.object({
|
|
|
2135
2128
|
envBranchPrefix: z20.string().max(50).nullable().optional()
|
|
2136
2129
|
}),
|
|
2137
2130
|
/**
|
|
2138
|
-
* Meter catalog. Referenced by
|
|
2131
|
+
* Meter catalog. Referenced by route `metering` metadata.
|
|
2139
2132
|
* Same shape as the legacy meterDefinitionSchema (re-exported from
|
|
2140
2133
|
* product.ts to keep one source of truth during the transition).
|
|
2141
2134
|
*
|
|
@@ -2159,7 +2152,7 @@ var productSpecV2Schema = z20.object({
|
|
|
2159
2152
|
gracePeriodDays: z20.number().int().nonnegative().default(3),
|
|
2160
2153
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
2161
2154
|
// subscribers immediately; a DECREASE always defers to period end.
|
|
2162
|
-
// Read by
|
|
2155
|
+
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
2163
2156
|
applyLimitUpgradesInstantly: z20.boolean().optional(),
|
|
2164
2157
|
subscriberChangePolicy: subscriberChangePolicySchema.default(DEFAULT_SUBSCRIBER_CHANGE_POLICY)
|
|
2165
2158
|
}).default({
|
|
@@ -2355,7 +2348,7 @@ function canonicalIrJson(ir) {
|
|
|
2355
2348
|
}
|
|
2356
2349
|
|
|
2357
2350
|
// src/version.ts
|
|
2358
|
-
var SDK_VERSION = true ? "0.
|
|
2351
|
+
var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
2359
2352
|
|
|
2360
2353
|
// src/business.ts
|
|
2361
2354
|
var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
|
|
@@ -2365,6 +2358,9 @@ function isCapabilityGrant(value) {
|
|
|
2365
2358
|
function keyOf(ref) {
|
|
2366
2359
|
return typeof ref === "string" ? ref : ref.key;
|
|
2367
2360
|
}
|
|
2361
|
+
function displayFromKey(key) {
|
|
2362
|
+
return key.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2363
|
+
}
|
|
2368
2364
|
function parseRouteMatch(match) {
|
|
2369
2365
|
const trimmed = match.trim();
|
|
2370
2366
|
const space = trimmed.indexOf(" ");
|
|
@@ -2405,6 +2401,7 @@ var Business = class {
|
|
|
2405
2401
|
name;
|
|
2406
2402
|
options;
|
|
2407
2403
|
graph = new ManifestResourceGraph();
|
|
2404
|
+
defaultMeterCosts = [];
|
|
2408
2405
|
frontendManifest;
|
|
2409
2406
|
productPatch = {};
|
|
2410
2407
|
/** Sugar for binding API routes to features. */
|
|
@@ -2561,8 +2558,37 @@ var Business = class {
|
|
|
2561
2558
|
}
|
|
2562
2559
|
meter(key, options) {
|
|
2563
2560
|
this.assertNewKey("meter", key, "meter");
|
|
2564
|
-
|
|
2565
|
-
|
|
2561
|
+
const { routeDefault, ...meterOptions } = options;
|
|
2562
|
+
this.graph.register("meter", key, {
|
|
2563
|
+
key,
|
|
2564
|
+
display: meterOptions.display ?? displayFromKey(key),
|
|
2565
|
+
...meterOptions
|
|
2566
|
+
});
|
|
2567
|
+
const ref = this.makeMeterRef(key);
|
|
2568
|
+
if (routeDefault !== void 0) {
|
|
2569
|
+
this.defaultMeterCosts.push(
|
|
2570
|
+
this.normalizeMeterCost(ref.fixed(routeDefault))
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
return ref;
|
|
2574
|
+
}
|
|
2575
|
+
requests(options = {}) {
|
|
2576
|
+
return this.meter("requests", {
|
|
2577
|
+
display: options.display ?? "Requests",
|
|
2578
|
+
unit: options.unit ?? "request",
|
|
2579
|
+
aggregation: options.aggregation ?? "COUNT",
|
|
2580
|
+
enforcementType: options.enforcementType ?? "estimated_then_settled",
|
|
2581
|
+
window: options.window,
|
|
2582
|
+
estimate: options.estimate ?? 1,
|
|
2583
|
+
routeDefault: 1
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
defaultMeters(costs) {
|
|
2587
|
+
const entries = Array.isArray(costs) ? costs : [costs];
|
|
2588
|
+
for (const cost of entries) {
|
|
2589
|
+
this.defaultMeterCosts.push(this.normalizeMeterCost(cost));
|
|
2590
|
+
}
|
|
2591
|
+
return this;
|
|
2566
2592
|
}
|
|
2567
2593
|
resource(name, options = {}) {
|
|
2568
2594
|
this.assertNewKey("counted_resource", name, "resource");
|
|
@@ -2735,6 +2761,7 @@ var Business = class {
|
|
|
2735
2761
|
* with structured issues when the declared state is invalid. */
|
|
2736
2762
|
toIR() {
|
|
2737
2763
|
this.assertGraphDependenciesSatisfied();
|
|
2764
|
+
this.assertRouteMeteringValid();
|
|
2738
2765
|
const candidate = {
|
|
2739
2766
|
irVersion: 1,
|
|
2740
2767
|
sdkVersion: SDK_VERSION,
|
|
@@ -2776,10 +2803,7 @@ var Business = class {
|
|
|
2776
2803
|
...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
|
|
2777
2804
|
},
|
|
2778
2805
|
metering: {
|
|
2779
|
-
meters: this.
|
|
2780
|
-
"meter",
|
|
2781
|
-
(meter) => meter.key
|
|
2782
|
-
),
|
|
2806
|
+
meters: this.buildMeterDefinitions(),
|
|
2783
2807
|
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2784
2808
|
},
|
|
2785
2809
|
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
@@ -2810,15 +2834,109 @@ var Business = class {
|
|
|
2810
2834
|
this.productPatch
|
|
2811
2835
|
);
|
|
2812
2836
|
}
|
|
2837
|
+
buildMeterDefinitions() {
|
|
2838
|
+
const routeValueMeters = this.routeValueMeterKeys();
|
|
2839
|
+
return this.graph.sortedValues("meter", (meter) => meter.key).map((meter) => {
|
|
2840
|
+
if (meter.aggregation !== void 0) return meter;
|
|
2841
|
+
if (!routeValueMeters.has(meter.key)) return meter;
|
|
2842
|
+
return { ...meter, aggregation: "SUM" };
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
routeValueMeterKeys() {
|
|
2846
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2847
|
+
for (const file of this.graph.values("feature")) {
|
|
2848
|
+
for (const route of file.routes) {
|
|
2849
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
2850
|
+
keys.add(meter);
|
|
2851
|
+
}
|
|
2852
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2853
|
+
keys.add(meter);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return keys;
|
|
2858
|
+
}
|
|
2813
2859
|
buildRoute(match, options) {
|
|
2814
2860
|
const parsed = parseRouteMatch(match);
|
|
2861
|
+
const metering = this.buildRouteMetering(options);
|
|
2815
2862
|
return {
|
|
2816
2863
|
match: parsed,
|
|
2817
|
-
...
|
|
2864
|
+
...metering ? { metering } : {},
|
|
2818
2865
|
...options.unmetered !== void 0 ? { unmetered: options.unmetered } : {},
|
|
2866
|
+
...options.inheritDefaultMeters !== void 0 ? { inheritDefaultMeters: options.inheritDefaultMeters } : {},
|
|
2819
2867
|
...options.action !== void 0 ? { action: keyOf(options.action) } : {}
|
|
2820
2868
|
};
|
|
2821
2869
|
}
|
|
2870
|
+
makeMeterRef(key) {
|
|
2871
|
+
return {
|
|
2872
|
+
kind: "meter",
|
|
2873
|
+
key,
|
|
2874
|
+
fixed: (value) => ({ kind: "meter_cost", meter: key, value }),
|
|
2875
|
+
estimate: (value) => ({ kind: "meter_cost", meter: key, value })
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
buildRouteMetering(options) {
|
|
2879
|
+
if (options.unmetered === true) return void 0;
|
|
2880
|
+
const defaults = {};
|
|
2881
|
+
if (options.inheritDefaultMeters !== false) {
|
|
2882
|
+
for (const cost of this.defaultMeterCosts) {
|
|
2883
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
for (const cost of this.normalizeMeterCosts(options.costs)) {
|
|
2887
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2888
|
+
}
|
|
2889
|
+
const reports = [
|
|
2890
|
+
...new Set(this.normalizeMeterRefs(options.reports ?? []))
|
|
2891
|
+
];
|
|
2892
|
+
const estimates = {};
|
|
2893
|
+
for (const meter of reports) {
|
|
2894
|
+
const definition = this.graph.get(
|
|
2895
|
+
"meter",
|
|
2896
|
+
meter
|
|
2897
|
+
)?.value;
|
|
2898
|
+
if (typeof definition?.estimate === "number") {
|
|
2899
|
+
estimates[meter] = definition.estimate;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
for (const [meter, value] of Object.entries(options.estimates ?? {})) {
|
|
2903
|
+
estimates[meter] = value;
|
|
2904
|
+
}
|
|
2905
|
+
const out = {};
|
|
2906
|
+
if (Object.keys(defaults).length) out.defaults = defaults;
|
|
2907
|
+
if (reports.length) out.reports = reports;
|
|
2908
|
+
if (Object.keys(estimates).length) out.estimates = estimates;
|
|
2909
|
+
if (options.onStatusCodes !== void 0)
|
|
2910
|
+
out.onStatusCodes = options.onStatusCodes;
|
|
2911
|
+
return Object.keys(out).length ? out : void 0;
|
|
2912
|
+
}
|
|
2913
|
+
normalizeMeterCost(cost) {
|
|
2914
|
+
if (!cost || cost.kind !== "meter_cost") {
|
|
2915
|
+
throw new ManifestBuilderError(
|
|
2916
|
+
"meter cost must be created by meter.fixed(value)"
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
if (!this.graph.has("meter", cost.meter)) {
|
|
2920
|
+
throw new ManifestBuilderError(
|
|
2921
|
+
`meter cost references unknown meter "${cost.meter}"`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
if (!Number.isFinite(cost.value) || cost.value < 0) {
|
|
2925
|
+
throw new ManifestBuilderError(
|
|
2926
|
+
`meter "${cost.meter}" fixed value must be a non-negative finite number`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
return cost;
|
|
2930
|
+
}
|
|
2931
|
+
normalizeMeterCosts(costs) {
|
|
2932
|
+
if (!costs) return [];
|
|
2933
|
+
const entries = Array.isArray(costs) ? costs : [costs];
|
|
2934
|
+
return entries.map((cost) => this.normalizeMeterCost(cost));
|
|
2935
|
+
}
|
|
2936
|
+
normalizeMeterRefs(refs) {
|
|
2937
|
+
const entries = Array.isArray(refs) ? refs : [refs];
|
|
2938
|
+
return entries.map(keyOf);
|
|
2939
|
+
}
|
|
2822
2940
|
ensureFrontendManifest() {
|
|
2823
2941
|
this.frontendManifest ??= { version: 1, nav: [], pages: [] };
|
|
2824
2942
|
this.syncFrontendGraphNode(this.frontendManifest);
|
|
@@ -2917,7 +3035,9 @@ var Business = class {
|
|
|
2917
3035
|
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
2918
3036
|
}
|
|
2919
3037
|
featureDependsOn(file) {
|
|
2920
|
-
const meterKeys = file.routes.flatMap(
|
|
3038
|
+
const meterKeys = file.routes.flatMap(
|
|
3039
|
+
(route) => this.routeMeterDependencyKeys(route)
|
|
3040
|
+
);
|
|
2921
3041
|
return [
|
|
2922
3042
|
...this.dependenciesFor("policy", file.policies),
|
|
2923
3043
|
...this.dependenciesFor("capability", file.capabilities),
|
|
@@ -2925,6 +3045,48 @@ var Business = class {
|
|
|
2925
3045
|
...this.dependenciesFor("meter", meterKeys)
|
|
2926
3046
|
];
|
|
2927
3047
|
}
|
|
3048
|
+
routeMeterDependencyKeys(route) {
|
|
3049
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3050
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
3051
|
+
keys.add(meter);
|
|
3052
|
+
}
|
|
3053
|
+
for (const meter of route.metering?.reports ?? []) keys.add(meter);
|
|
3054
|
+
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
3055
|
+
keys.add(meter);
|
|
3056
|
+
}
|
|
3057
|
+
return [...keys];
|
|
3058
|
+
}
|
|
3059
|
+
assertRouteMeteringValid() {
|
|
3060
|
+
for (const file of this.graph.values("feature")) {
|
|
3061
|
+
file.routes.forEach((route, routeIndex) => {
|
|
3062
|
+
if (route.unmetered === true) return;
|
|
3063
|
+
const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
|
|
3064
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
3065
|
+
if (defaults.has(meter)) {
|
|
3066
|
+
throw new ManifestBuilderError(
|
|
3067
|
+
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
3072
|
+
const definition = this.graph.get(
|
|
3073
|
+
"meter",
|
|
3074
|
+
meter
|
|
3075
|
+
)?.value;
|
|
3076
|
+
const enforcement = definition?.enforcementType ?? "estimated_then_settled";
|
|
3077
|
+
const hasEstimate = route.metering?.estimates && Object.prototype.hasOwnProperty.call(
|
|
3078
|
+
route.metering.estimates,
|
|
3079
|
+
meter
|
|
3080
|
+
);
|
|
3081
|
+
if ((enforcement === "exact_pre_request" || enforcement === "estimated_then_settled") && !hasEstimate) {
|
|
3082
|
+
throw new ManifestBuilderError(
|
|
3083
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" needs an estimate for gateway admission`
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
2928
3090
|
actionDependsOn(featureKey, action) {
|
|
2929
3091
|
return [
|
|
2930
3092
|
resourceDependency("feature", featureKey),
|
package/dist/types/business.d.ts
CHANGED
|
@@ -34,7 +34,14 @@ export type BusinessOptions = {
|
|
|
34
34
|
applyLimitUpgradesInstantly?: boolean;
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
|
-
export type MeterOptions = Omit<MeterDefinitionJson, "key"
|
|
37
|
+
export type MeterOptions = Omit<MeterDefinitionJson, "key" | "display"> & {
|
|
38
|
+
display?: string;
|
|
39
|
+
/** Reusable pre-request estimate for dynamic route reports. */
|
|
40
|
+
estimate?: number;
|
|
41
|
+
/** Fixed value applied to every metered route unless the route opts out. */
|
|
42
|
+
routeDefault?: number;
|
|
43
|
+
};
|
|
44
|
+
export type RequestMeterOptions = Partial<Omit<MeterOptions, "routeDefault">>;
|
|
38
45
|
export type CapabilityOptions = {
|
|
39
46
|
title?: string;
|
|
40
47
|
description?: string;
|
|
@@ -43,9 +50,24 @@ export type CapabilityOptions = {
|
|
|
43
50
|
includesPolicies?: Array<string | PolicyRef>;
|
|
44
51
|
includesCapabilities?: Array<string | CapabilityRef>;
|
|
45
52
|
};
|
|
53
|
+
export type MeterCost = {
|
|
54
|
+
readonly kind: "meter_cost";
|
|
55
|
+
readonly meter: string;
|
|
56
|
+
readonly value: number;
|
|
57
|
+
};
|
|
46
58
|
export type RouteOptions = {
|
|
47
|
-
/**
|
|
48
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Dynamic meter keys the upstream may report with @farthershore/metering.
|
|
61
|
+
*/
|
|
62
|
+
reports?: string | MeterRef | Array<string | MeterRef>;
|
|
63
|
+
/** Fixed gateway-known route costs. */
|
|
64
|
+
costs?: MeterCost | Array<MeterCost>;
|
|
65
|
+
/** Route-specific pre-request estimates for dynamic reports. */
|
|
66
|
+
estimates?: Record<string, number>;
|
|
67
|
+
/** Override the default successful-response range. */
|
|
68
|
+
onStatusCodes?: string | number[];
|
|
69
|
+
/** Disable product.defaultMeters()/product.requests() for this route only. */
|
|
70
|
+
inheritDefaultMeters?: boolean;
|
|
49
71
|
unmetered?: boolean;
|
|
50
72
|
action?: string | ActionRef;
|
|
51
73
|
};
|
|
@@ -103,7 +125,7 @@ export type FeatureOptions = {
|
|
|
103
125
|
actions?: Array<{
|
|
104
126
|
id: string;
|
|
105
127
|
} & ActionOptions>;
|
|
106
|
-
/** Routes, e.g. `{ match: "POST /v1/jobs",
|
|
128
|
+
/** Routes, e.g. `{ match: "POST /v1/jobs", reports: [tokens] }`. */
|
|
107
129
|
routes?: Array<{
|
|
108
130
|
match: string;
|
|
109
131
|
} & RouteOptions>;
|
|
@@ -152,6 +174,10 @@ export type PlanOptions = {
|
|
|
152
174
|
export type MeterRef = {
|
|
153
175
|
readonly kind: "meter";
|
|
154
176
|
readonly key: string;
|
|
177
|
+
/** Fixed gateway-known usage for route/product defaults. */
|
|
178
|
+
fixed(value: number): MeterCost;
|
|
179
|
+
/** Convenience value for APIs that want an explicit estimate helper. */
|
|
180
|
+
estimate(value: number): MeterCost;
|
|
155
181
|
};
|
|
156
182
|
export type ResourceRef = {
|
|
157
183
|
readonly kind: "resource";
|
|
@@ -201,6 +227,7 @@ export declare class Business {
|
|
|
201
227
|
readonly name: string;
|
|
202
228
|
private readonly options;
|
|
203
229
|
private readonly graph;
|
|
230
|
+
private readonly defaultMeterCosts;
|
|
204
231
|
private frontendManifest?;
|
|
205
232
|
private productPatch;
|
|
206
233
|
/** Sugar for binding API routes to features. */
|
|
@@ -231,6 +258,8 @@ export declare class Business {
|
|
|
231
258
|
};
|
|
232
259
|
constructor(name: string, options: BusinessOptions);
|
|
233
260
|
meter(key: string, options: MeterOptions): MeterRef;
|
|
261
|
+
requests(options?: RequestMeterOptions): MeterRef;
|
|
262
|
+
defaultMeters(costs: MeterCost | MeterCost[]): Business;
|
|
234
263
|
resource(name: string, options?: ResourceOptions): ResourceRef;
|
|
235
264
|
capability(key: string, options?: CapabilityOptions): CapabilityRef;
|
|
236
265
|
feature(key: string, options?: FeatureOptions): FeatureRef;
|
|
@@ -243,7 +272,14 @@ export declare class Business {
|
|
|
243
272
|
* with structured issues when the declared state is invalid. */
|
|
244
273
|
toIR(): ManifestBuildResult;
|
|
245
274
|
private buildProductSpec;
|
|
275
|
+
private buildMeterDefinitions;
|
|
276
|
+
private routeValueMeterKeys;
|
|
246
277
|
private buildRoute;
|
|
278
|
+
private makeMeterRef;
|
|
279
|
+
private buildRouteMetering;
|
|
280
|
+
private normalizeMeterCost;
|
|
281
|
+
private normalizeMeterCosts;
|
|
282
|
+
private normalizeMeterRefs;
|
|
247
283
|
private ensureFrontendManifest;
|
|
248
284
|
private normalizeMigrationPlanRef;
|
|
249
285
|
private normalizeMigrationTargetRef;
|
|
@@ -258,6 +294,8 @@ export declare class Business {
|
|
|
258
294
|
private capabilityDependsOn;
|
|
259
295
|
private policyDependsOn;
|
|
260
296
|
private featureDependsOn;
|
|
297
|
+
private routeMeterDependencyKeys;
|
|
298
|
+
private assertRouteMeteringValid;
|
|
261
299
|
private actionDependsOn;
|
|
262
300
|
private planDependsOn;
|
|
263
301
|
private migrationDependsOn;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { price } from "./price.js";
|
|
|
14
14
|
export { validateManifestIr, hashIr, canonicalIrJson } from "./validate.js";
|
|
15
15
|
export { ManifestValidationError, ManifestBuilderError } from "./errors.js";
|
|
16
16
|
export { SDK_VERSION } from "./version.js";
|
|
17
|
-
export type { BusinessOptions, MeterOptions, ResourceOptions, CapabilityOptions, ActionOptions, FrontendNavItemOptions, FrontendPageOptions, FrontendComponentOptions, MigrationOptions, FeatureOptions, RouteOptions, PolicyOptions, PlanOptions, MeterRef, ResourceRef, ActionRef, PolicyRef, PlanRef, FeatureRef, CapabilityRef, PlanCapabilityGrant, ProductModule, } from "./business.js";
|
|
17
|
+
export type { BusinessOptions, MeterOptions, RequestMeterOptions, MeterCost, ResourceOptions, CapabilityOptions, ActionOptions, FrontendNavItemOptions, FrontendPageOptions, FrontendComponentOptions, MigrationOptions, FeatureOptions, RouteOptions, PolicyOptions, PlanOptions, MeterRef, ResourceRef, ActionRef, PolicyRef, PlanRef, FeatureRef, CapabilityRef, PlanCapabilityGrant, ProductModule, } from "./business.js";
|
|
18
18
|
export type { ManifestResourceGraphSnapshot, ManifestResourceKind, ManifestResourceUrn, } from "./resource-graph.js";
|
|
19
19
|
export type { PriceSpec } from "./price.js";
|
|
20
20
|
export type { ManifestIssue } from "./errors.js";
|
package/dist/types/ir-types.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type MeterDefinitionJson = {
|
|
|
5
5
|
key: string;
|
|
6
6
|
display: string;
|
|
7
7
|
unit?: string;
|
|
8
|
+
estimate?: number;
|
|
9
|
+
routeDefault?: number;
|
|
8
10
|
enforcementType?: "exact_pre_request" | "estimated_then_settled" | "postpaid" | "strict_concurrency";
|
|
9
11
|
aggregation?: "SUM" | "COUNT" | "MAX" | "UNIQUE_COUNT" | "LATEST";
|
|
10
12
|
window?: "minute" | "hour" | "day" | "month" | "billing_period";
|
|
@@ -71,8 +73,14 @@ export type RouteDefinitionJson = {
|
|
|
71
73
|
method?: HttpMethod;
|
|
72
74
|
path: string;
|
|
73
75
|
};
|
|
74
|
-
|
|
76
|
+
metering?: {
|
|
77
|
+
defaults?: Record<string, number>;
|
|
78
|
+
reports?: string[];
|
|
79
|
+
estimates?: Record<string, number>;
|
|
80
|
+
onStatusCodes?: string | number[];
|
|
81
|
+
};
|
|
75
82
|
unmetered?: boolean;
|
|
83
|
+
inheritDefaultMeters?: boolean;
|
|
76
84
|
action?: string;
|
|
77
85
|
};
|
|
78
86
|
export type ActionSpecJson = {
|