@farthershore/product 0.5.0 → 0.6.1
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 +8 -13
- package/dist/bin.js +1864 -1723
- package/dist/codegen.js +203 -166
- package/dist/index.js +1866 -1725
- package/dist/types/backend.d.ts +41 -0
- package/dist/types/codegen/index.d.ts +9 -1
- package/dist/types/declarations.d.ts +123 -0
- package/dist/types/dependencies.d.ts +13 -0
- package/dist/types/frontend.d.ts +23 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/ir-types.d.ts +49 -13
- package/dist/types/product-assembly.d.ts +22 -0
- package/dist/types/product.d.ts +41 -71
- package/dist/types/refs.d.ts +11 -0
- package/dist/types/resource-graph.d.ts +1 -1
- package/dist/types/route-metering.d.ts +53 -0
- package/dist/types/validate.d.ts +2 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -146,15 +146,9 @@ var planLimitsSchema = z.array(planLimitRuleSchema).max(20);
|
|
|
146
146
|
|
|
147
147
|
// ../contracts/dist/plans/spec/subscriber-change-policy.js
|
|
148
148
|
import { z as z2 } from "zod";
|
|
149
|
-
var subscriberChangeActionSchema = z2.enum([
|
|
150
|
-
"preserve_current_period",
|
|
151
|
-
"switch_immediately",
|
|
152
|
-
"switch_immediately_prorate",
|
|
153
|
-
"new_subscribers_only"
|
|
154
|
-
]);
|
|
149
|
+
var subscriberChangeActionSchema = z2.enum(["immediate", "period_end"]);
|
|
155
150
|
var subscriberChangePolicySchema = z2.object({
|
|
156
|
-
default: subscriberChangeActionSchema.default("
|
|
157
|
-
proration: z2.enum(["none", "prorate", "credit"]).default("none"),
|
|
151
|
+
default: subscriberChangeActionSchema.default("period_end"),
|
|
158
152
|
when: z2.object({
|
|
159
153
|
price_increase: subscriberChangeActionSchema.optional(),
|
|
160
154
|
price_decrease: subscriberChangeActionSchema.optional(),
|
|
@@ -166,24 +160,20 @@ var subscriberChangePolicySchema = z2.object({
|
|
|
166
160
|
credit_reduced: subscriberChangeActionSchema.optional(),
|
|
167
161
|
rating_changed: subscriberChangeActionSchema.optional()
|
|
168
162
|
}).strict().default({
|
|
169
|
-
price_increase: "
|
|
170
|
-
price_decrease: "
|
|
171
|
-
feature_added: "
|
|
172
|
-
feature_removed: "
|
|
173
|
-
limit_increased: "
|
|
174
|
-
limit_reduced: "
|
|
175
|
-
credit_increased: "
|
|
176
|
-
credit_reduced: "
|
|
177
|
-
rating_changed: "
|
|
163
|
+
price_increase: "period_end",
|
|
164
|
+
price_decrease: "immediate",
|
|
165
|
+
feature_added: "immediate",
|
|
166
|
+
feature_removed: "period_end",
|
|
167
|
+
limit_increased: "immediate",
|
|
168
|
+
limit_reduced: "period_end",
|
|
169
|
+
credit_increased: "immediate",
|
|
170
|
+
credit_reduced: "period_end",
|
|
171
|
+
rating_changed: "period_end"
|
|
178
172
|
}),
|
|
179
173
|
allowImmediatePriceIncrease: z2.boolean().default(false),
|
|
180
174
|
allowImmediateEntitlementReduction: z2.boolean().default(false)
|
|
181
175
|
}).strict().superRefine((policy, ctx) => {
|
|
182
|
-
|
|
183
|
-
"switch_immediately",
|
|
184
|
-
"switch_immediately_prorate"
|
|
185
|
-
]);
|
|
186
|
-
if (immediate.has(policy.when.price_increase ?? policy.default) && !policy.allowImmediatePriceIncrease) {
|
|
176
|
+
if ((policy.when.price_increase ?? policy.default) === "immediate" && !policy.allowImmediatePriceIncrease) {
|
|
187
177
|
ctx.addIssue({
|
|
188
178
|
code: "custom",
|
|
189
179
|
path: ["when", "price_increase"],
|
|
@@ -195,7 +185,7 @@ var subscriberChangePolicySchema = z2.object({
|
|
|
195
185
|
"limit_reduced",
|
|
196
186
|
"credit_reduced"
|
|
197
187
|
]) {
|
|
198
|
-
if (
|
|
188
|
+
if ((policy.when[key] ?? policy.default) === "immediate" && !policy.allowImmediateEntitlementReduction) {
|
|
199
189
|
ctx.addIssue({
|
|
200
190
|
code: "custom",
|
|
201
191
|
path: ["when", key],
|
|
@@ -206,30 +196,22 @@ var subscriberChangePolicySchema = z2.object({
|
|
|
206
196
|
});
|
|
207
197
|
|
|
208
198
|
// ../contracts/dist/plans/spec/plan-pricing.js
|
|
209
|
-
import { z as
|
|
199
|
+
import { z as z5 } from "zod";
|
|
210
200
|
|
|
211
201
|
// ../contracts/dist/plans/grants.js
|
|
212
202
|
import { z as z3 } from "zod";
|
|
213
|
-
var
|
|
214
|
-
kind: z3.literal("
|
|
215
|
-
amount_cents: z3.number().int().nonnegative()
|
|
216
|
-
});
|
|
217
|
-
var oneTimeGrantSchema = z3.object({
|
|
218
|
-
kind: z3.literal("one_time"),
|
|
219
|
-
amount_cents: z3.number().int().nonnegative()
|
|
220
|
-
});
|
|
221
|
-
var promotionalGrantSchema = z3.object({
|
|
222
|
-
kind: z3.literal("promotional"),
|
|
203
|
+
var creditGrantSchema = z3.object({
|
|
204
|
+
kind: z3.literal("credit"),
|
|
223
205
|
amount_cents: z3.number().int().nonnegative(),
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
206
|
+
/** When true, the credit is re-granted at every period rollover. Default
|
|
207
|
+
* false = a one-shot grant at subscription start. */
|
|
208
|
+
recurs: z3.boolean().optional(),
|
|
209
|
+
/** Optional expiry — Stripe expires the grant this many days after issue.
|
|
210
|
+
* Mainly used by promotional credits to prevent indefinite carry-forward. */
|
|
211
|
+
expires_after_days: z3.number().int().positive().optional(),
|
|
212
|
+
/** Optional campaign label. A credit carrying a label is a promotional /
|
|
213
|
+
* ops-issued grant rather than the canonical recurring/one-time credit. */
|
|
214
|
+
label: z3.string().min(1).max(120).optional()
|
|
233
215
|
});
|
|
234
216
|
var topUpGrantSchema = z3.object({
|
|
235
217
|
kind: z3.literal("top_up"),
|
|
@@ -242,67 +224,75 @@ var topUpGrantSchema = z3.object({
|
|
|
242
224
|
* Typically >= price_cents (the difference is the bulk discount). */
|
|
243
225
|
credit_cents: z3.number().int().positive()
|
|
244
226
|
});
|
|
245
|
-
var autoRechargeGrantSchema = z3.object({
|
|
246
|
-
kind: z3.literal("auto_recharge"),
|
|
247
|
-
/** When current balance < this many cents, trigger a refill. */
|
|
248
|
-
threshold_cents: z3.number().int().nonnegative(),
|
|
249
|
-
/** Charge + grant this much on each refill, in cents. */
|
|
250
|
-
refill_cents: z3.number().int().positive()
|
|
251
|
-
});
|
|
252
227
|
var grantSchema = z3.discriminatedUnion("kind", [
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
promotionalGrantSchema,
|
|
256
|
-
trialGrantSchema,
|
|
257
|
-
rolloverGrantSchema,
|
|
258
|
-
topUpGrantSchema,
|
|
259
|
-
autoRechargeGrantSchema
|
|
228
|
+
creditGrantSchema,
|
|
229
|
+
topUpGrantSchema
|
|
260
230
|
]);
|
|
231
|
+
function isRecurringCredit(g) {
|
|
232
|
+
return g.kind === "credit" && g.recurs === true && g.label === void 0;
|
|
233
|
+
}
|
|
234
|
+
function isOneTimeCredit(g) {
|
|
235
|
+
return g.kind === "credit" && g.recurs !== true && g.label === void 0;
|
|
236
|
+
}
|
|
261
237
|
function refineSingleCanonicalGrant(grants, ctx, path = ["grants"]) {
|
|
262
238
|
if (!grants)
|
|
263
239
|
return;
|
|
264
240
|
let recurringCount = 0;
|
|
265
241
|
let oneTimeCount = 0;
|
|
266
242
|
for (const g of grants) {
|
|
267
|
-
if (g
|
|
243
|
+
if (isRecurringCredit(g))
|
|
268
244
|
recurringCount += 1;
|
|
269
|
-
else if (g
|
|
245
|
+
else if (isOneTimeCredit(g))
|
|
270
246
|
oneTimeCount += 1;
|
|
271
247
|
}
|
|
272
248
|
if (recurringCount > 1) {
|
|
273
249
|
ctx.addIssue({
|
|
274
250
|
code: "custom",
|
|
275
|
-
message: "At most one
|
|
251
|
+
message: "At most one recurring `credit` grant (recurs:true, no label) is allowed \u2014 recurring credit is a single canonical entry.",
|
|
276
252
|
path
|
|
277
253
|
});
|
|
278
254
|
}
|
|
279
255
|
if (oneTimeCount > 1) {
|
|
280
256
|
ctx.addIssue({
|
|
281
257
|
code: "custom",
|
|
282
|
-
message: "At most one `
|
|
258
|
+
message: "At most one one-time `credit` grant (recurs:false, no label) is allowed \u2014 one-time credit is a single canonical entry.",
|
|
283
259
|
path
|
|
284
260
|
});
|
|
285
261
|
}
|
|
286
262
|
}
|
|
287
263
|
|
|
264
|
+
// ../contracts/dist/plans/credit-policy.js
|
|
265
|
+
import { z as z4 } from "zod";
|
|
266
|
+
var rolloverPolicySchema = z4.object({
|
|
267
|
+
percent: z4.number().int().min(0).max(100)
|
|
268
|
+
});
|
|
269
|
+
var autoRechargePolicySchema = z4.object({
|
|
270
|
+
threshold_cents: z4.number().int().nonnegative(),
|
|
271
|
+
refill_cents: z4.number().int().positive()
|
|
272
|
+
});
|
|
273
|
+
var creditPolicySchema = z4.object({
|
|
274
|
+
rollover: rolloverPolicySchema.optional(),
|
|
275
|
+
auto_recharge: autoRechargePolicySchema.optional()
|
|
276
|
+
});
|
|
277
|
+
|
|
288
278
|
// ../contracts/dist/plans/spec/plan-pricing.js
|
|
289
|
-
var meterKindSchema =
|
|
290
|
-
var meterTierSchema =
|
|
291
|
-
up_to:
|
|
292
|
-
price_per_unit_micros:
|
|
279
|
+
var meterKindSchema = z5.enum(["linear", "active_count"]);
|
|
280
|
+
var meterTierSchema = z5.object({
|
|
281
|
+
up_to: z5.number().int().positive().nullable(),
|
|
282
|
+
price_per_unit_micros: z5.number().int().nonnegative()
|
|
293
283
|
});
|
|
294
|
-
var tieredPricingSchema =
|
|
295
|
-
strategy:
|
|
296
|
-
tiers:
|
|
284
|
+
var tieredPricingSchema = z5.object({
|
|
285
|
+
strategy: z5.enum(["graduated", "volume"]),
|
|
286
|
+
tiers: z5.array(meterTierSchema).min(1).max(20)
|
|
297
287
|
});
|
|
298
|
-
var meterSchema =
|
|
299
|
-
dimension:
|
|
288
|
+
var meterSchema = z5.object({
|
|
289
|
+
dimension: z5.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Meter dimension must be lowercase alphanumeric with underscores"),
|
|
300
290
|
/** Aggregation kind. `linear` (default) sums event quantities across
|
|
301
291
|
* the period. `active_count` samples the latest quantity in the
|
|
302
292
|
* period (use for seats / active-user / occupancy snapshots). */
|
|
303
293
|
kind: meterKindSchema.default("linear"),
|
|
304
294
|
/** Flat per-unit rate. Mutually exclusive with `tiered`. */
|
|
305
|
-
price_per_unit_micros:
|
|
295
|
+
price_per_unit_micros: z5.number().int().nonnegative().default(0),
|
|
306
296
|
/** Tiered schedule. Mutually exclusive with a non-zero
|
|
307
297
|
* `price_per_unit_micros`. */
|
|
308
298
|
tiered: tieredPricingSchema.optional(),
|
|
@@ -316,7 +306,7 @@ var meterSchema = z4.object({
|
|
|
316
306
|
* Mutually exclusive with `tiered` — a tiered schedule already
|
|
317
307
|
* expresses the full breakpoint ladder (model the free pool as a
|
|
318
308
|
* zero-priced first tier instead). Only meaningful on a flat meter. */
|
|
319
|
-
included_units:
|
|
309
|
+
included_units: z5.number().int().positive().optional()
|
|
320
310
|
}).superRefine((m, ctx) => {
|
|
321
311
|
if (m.tiered && m.price_per_unit_micros > 0) {
|
|
322
312
|
ctx.addIssue({
|
|
@@ -367,99 +357,106 @@ var meterSchema = z4.object({
|
|
|
367
357
|
}
|
|
368
358
|
}
|
|
369
359
|
});
|
|
370
|
-
var billingIntervalSchema =
|
|
371
|
-
var planPricingSchema =
|
|
360
|
+
var billingIntervalSchema = z5.enum(["month", "year"]);
|
|
361
|
+
var planPricingSchema = z5.object({
|
|
372
362
|
/** Per-dimension price list. Empty array = no metered billing. */
|
|
373
|
-
meters:
|
|
363
|
+
meters: z5.array(meterSchema).default([]),
|
|
374
364
|
/** Flat subscription fee per period, in cents. 0 = no recurring fee. */
|
|
375
|
-
recurring_fee_cents:
|
|
365
|
+
recurring_fee_cents: z5.number().int().nonnegative().default(0),
|
|
376
366
|
/** Billing cadence for the recurring fee + metered usage. Defaults to
|
|
377
367
|
* `month`; set `year` for annual billing (drives both the Stripe price
|
|
378
368
|
* `recurring.interval` and the spend-cap / quota billing-period window
|
|
379
369
|
* length). */
|
|
380
370
|
billing_interval: billingIntervalSchema.default("month"),
|
|
381
371
|
/** Unified credit-grant array — the SINGLE credit surface. Recurring +
|
|
382
|
-
* one-time credit are canonical entries here (the legacy scalar
|
|
383
|
-
* were removed). */
|
|
384
|
-
grants:
|
|
372
|
+
* one-time credit are canonical `credit` entries here (the legacy scalar
|
|
373
|
+
* knobs were removed). */
|
|
374
|
+
grants: z5.array(grantSchema).max(40).default([]),
|
|
375
|
+
/** Non-grant credit policies (rollover + auto-recharge). Optional; absent =
|
|
376
|
+
* no rollover / no auto-recharge. */
|
|
377
|
+
creditPolicy: creditPolicySchema.optional(),
|
|
385
378
|
/** Free-trial length in days. Trial enforcement runs through Stripe
|
|
386
379
|
* webhooks → `lifecycle_block` constraint at runtime. */
|
|
387
|
-
trial_days:
|
|
380
|
+
trial_days: z5.number().int().nonnegative().default(0),
|
|
388
381
|
/** Optional hard cap on monthly spend, in cents. Orthogonal to the
|
|
389
382
|
* billing math — the gateway blocks once this cap is reached even if
|
|
390
383
|
* credit balance remains. Stripe doesn't enforce this; the runtime
|
|
391
384
|
* does, via a `quota`-shaped constraint. */
|
|
392
|
-
max_monthly_spend_cents:
|
|
385
|
+
max_monthly_spend_cents: z5.number().int().nonnegative().optional()
|
|
393
386
|
});
|
|
394
387
|
|
|
395
388
|
// ../contracts/dist/plans/spec/plan-variant.js
|
|
396
|
-
import { z as
|
|
397
|
-
var proRationOnRollbackSchema =
|
|
389
|
+
import { z as z6 } from "zod";
|
|
390
|
+
var proRationOnRollbackSchema = z6.enum(["NONE", "PRORATE", "CREDIT"]).default("NONE");
|
|
398
391
|
var billingKnobsShape = {
|
|
399
|
-
meters:
|
|
400
|
-
recurring_fee_cents:
|
|
392
|
+
meters: z6.array(meterSchema).default([]),
|
|
393
|
+
recurring_fee_cents: z6.number().int().nonnegative().default(0),
|
|
401
394
|
/** Billing cadence (`month` default / `year`). Drives the Stripe price
|
|
402
395
|
* `recurring.interval` and the entitlement billing-period window. */
|
|
403
396
|
billing_interval: billingIntervalSchema.default("month"),
|
|
404
|
-
trial_days:
|
|
397
|
+
trial_days: z6.number().int().nonnegative().default(0),
|
|
405
398
|
/** Hard maximum on metered spend per period. Gateway blocks once
|
|
406
399
|
* cumulative usage cost reaches this cap. Orthogonal to the
|
|
407
400
|
* recurring fee — the fee is always charged. */
|
|
408
|
-
max_monthly_spend_cents:
|
|
401
|
+
max_monthly_spend_cents: z6.number().int().nonnegative().optional(),
|
|
409
402
|
/** Minimum metered spend per period. When set, the invoice floors
|
|
410
403
|
* the metered total at this value (Railway-style minimum-spend).
|
|
411
404
|
* Orthogonal to `recurring_fee_cents` — the fee is added on top.
|
|
412
405
|
* Set to 0 / omit to disable. */
|
|
413
|
-
min_monthly_spend_cents:
|
|
406
|
+
min_monthly_spend_cents: z6.number().int().nonnegative().optional(),
|
|
414
407
|
/**
|
|
415
|
-
* Unified grants array — the SINGLE credit surface
|
|
416
|
-
*
|
|
417
|
-
*
|
|
408
|
+
* Unified grants array — the SINGLE credit surface. Each entry expresses
|
|
409
|
+
* one credit grant primitive: a parametrized `credit` (recurring /
|
|
410
|
+
* one-time / promotional, distinguished by `recurs` / `label`) or a
|
|
411
|
+
* purchasable `top_up` pack.
|
|
418
412
|
*
|
|
419
|
-
* Recurring + one-time credit are declared here as canonical
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* consolidated list.
|
|
413
|
+
* Recurring + one-time credit are declared here as canonical `credit`
|
|
414
|
+
* entries (at most one recurring + one one-time, enforced by
|
|
415
|
+
* `refineSingleCanonicalGrant`); call `getEffectiveGrants(plan)` to read
|
|
416
|
+
* the consolidated list. Trial is driven by `trial_days`; rollover +
|
|
417
|
+
* auto-recharge live in `creditPolicy`.
|
|
425
418
|
*/
|
|
426
|
-
grants:
|
|
419
|
+
grants: z6.array(grantSchema).max(40).default([]),
|
|
420
|
+
/** Non-grant credit policies (rollover + auto-recharge). */
|
|
421
|
+
creditPolicy: creditPolicySchema.optional()
|
|
427
422
|
};
|
|
428
423
|
var billingKnobsOptionalShape = {
|
|
429
|
-
meters:
|
|
430
|
-
recurring_fee_cents:
|
|
424
|
+
meters: z6.array(meterSchema).optional(),
|
|
425
|
+
recurring_fee_cents: z6.number().int().nonnegative().optional(),
|
|
431
426
|
/** Variant override for the billing cadence. Absent → inherit the
|
|
432
427
|
* parent plan's `billing_interval`. */
|
|
433
428
|
billing_interval: billingIntervalSchema.optional(),
|
|
434
|
-
trial_days:
|
|
435
|
-
max_monthly_spend_cents:
|
|
436
|
-
min_monthly_spend_cents:
|
|
429
|
+
trial_days: z6.number().int().nonnegative().optional(),
|
|
430
|
+
max_monthly_spend_cents: z6.number().int().nonnegative().optional(),
|
|
431
|
+
min_monthly_spend_cents: z6.number().int().nonnegative().optional(),
|
|
437
432
|
/** Variant override for the grants array — the single credit surface.
|
|
438
433
|
* When present, fully replaces the parent plan's grants (no shallow
|
|
439
434
|
* merge — grants are collectively meaningful). */
|
|
440
|
-
grants:
|
|
435
|
+
grants: z6.array(grantSchema).max(40).optional(),
|
|
436
|
+
/** Variant override for the credit-policy block. */
|
|
437
|
+
creditPolicy: creditPolicySchema.optional()
|
|
441
438
|
};
|
|
442
|
-
var planVariantObjectSchema =
|
|
439
|
+
var planVariantObjectSchema = z6.object({
|
|
443
440
|
/**
|
|
444
441
|
* Stable variant id — used as the second half of the CompiledPlan
|
|
445
442
|
* lineage key, so changing it counts as create-new + archive-old.
|
|
446
443
|
* Lower-case kebab-case to match URL-share-link readability.
|
|
447
444
|
*/
|
|
448
|
-
id:
|
|
445
|
+
id: z6.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Variant id must be lowercase alphanumeric with hyphens/underscores"),
|
|
449
446
|
/** Human-readable label for dashboards / observability. */
|
|
450
|
-
label:
|
|
447
|
+
label: z6.string().max(200).optional(),
|
|
451
448
|
/**
|
|
452
449
|
* Rollout percentage (0-100). 0 = paused (variant exists but no new
|
|
453
450
|
* assignments); 100 = full takeover (effectively a forced graduation
|
|
454
451
|
* for new subscribers, but legacy subs stay on parent until period end).
|
|
455
452
|
*/
|
|
456
|
-
rolloutPercent:
|
|
453
|
+
rolloutPercent: z6.number().int().min(0).max(100),
|
|
457
454
|
/**
|
|
458
455
|
* Seed for the deterministic hash function. Rotating the seed
|
|
459
456
|
* invalidates existing variant assignments — useful for re-running an
|
|
460
457
|
* experiment with a fresh cohort.
|
|
461
458
|
*/
|
|
462
|
-
assignmentSeed:
|
|
459
|
+
assignmentSeed: z6.string().min(1).max(100).default("default"),
|
|
463
460
|
/**
|
|
464
461
|
* What happens to billing when this variant gets rolled back AND the
|
|
465
462
|
* subscriber has already been billed for the experimental price:
|
|
@@ -473,22 +470,22 @@ var planVariantObjectSchema = z5.object({
|
|
|
473
470
|
// plan. The pre-0.53 `pricing: planPricingSchema.optional()` field is
|
|
474
471
|
// gone; variants now override the knobs directly.
|
|
475
472
|
...billingKnobsOptionalShape,
|
|
476
|
-
limits:
|
|
477
|
-
featureGates:
|
|
473
|
+
limits: z6.array(planLimitRuleSchema).max(20).optional(),
|
|
474
|
+
featureGates: z6.record(z6.string(), z6.boolean()).optional(),
|
|
478
475
|
/** See `planSpecSchema.capability_limits`. Variant overrides are
|
|
479
476
|
* merged shallowly onto the parent plan's map — missing keys
|
|
480
477
|
* inherit from the parent. */
|
|
481
|
-
capability_limits:
|
|
482
|
-
overageBehavior:
|
|
478
|
+
capability_limits: z6.record(z6.string().min(1).max(120), z6.union([z6.number().int().nonnegative(), z6.boolean()])).optional(),
|
|
479
|
+
overageBehavior: z6.enum(["block", "allow_and_bill"]).optional()
|
|
483
480
|
});
|
|
484
481
|
var planVariantSchema = planVariantObjectSchema.superRefine((variant, ctx) => {
|
|
485
482
|
refineSingleCanonicalGrant(variant.grants, ctx);
|
|
486
483
|
});
|
|
487
|
-
var planSpecObjectSchema =
|
|
488
|
-
key:
|
|
489
|
-
name:
|
|
490
|
-
description:
|
|
491
|
-
details:
|
|
484
|
+
var planSpecObjectSchema = z6.object({
|
|
485
|
+
key: z6.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Plan key must be lowercase alphanumeric with hyphens/underscores"),
|
|
486
|
+
name: z6.string().min(1).max(100),
|
|
487
|
+
description: z6.string().max(500).optional(),
|
|
488
|
+
details: z6.array(z6.string().max(200)).max(10).optional(),
|
|
492
489
|
// ---------------------------------------------------------------------
|
|
493
490
|
// 5-knob billing shape (v0.53.0)
|
|
494
491
|
// ---------------------------------------------------------------------
|
|
@@ -502,7 +499,7 @@ var planSpecObjectSchema = z5.object({
|
|
|
502
499
|
// runs through Stripe webhooks → `lifecycle_block` constraint at
|
|
503
500
|
// runtime; there's no `trial_expiry` constraint kind anymore.
|
|
504
501
|
...billingKnobsShape,
|
|
505
|
-
free:
|
|
502
|
+
free: z6.boolean().default(false),
|
|
506
503
|
// `plan.features` removed in feat/feature-plans-link — features
|
|
507
504
|
// declare their plans via `feature.plans[]` (canonical, feature-first
|
|
508
505
|
// direction). Builders writing the manifest now go to the feature
|
|
@@ -510,8 +507,8 @@ var planSpecObjectSchema = z5.object({
|
|
|
510
507
|
// compiler walks `spec.features` and includes a feature's routes for
|
|
511
508
|
// a plan when the plan's key appears in `feature.plans[]`. See
|
|
512
509
|
// shared-types/src/plans/spec/product.ts (featureCatalogEntrySchema).
|
|
513
|
-
limits:
|
|
514
|
-
featureGates:
|
|
510
|
+
limits: z6.array(planLimitRuleSchema).max(20).default([]),
|
|
511
|
+
featureGates: z6.record(z6.string(), z6.boolean()).optional(),
|
|
515
512
|
/**
|
|
516
513
|
* Control-plane capability limits (Phase 1a, v0.56+).
|
|
517
514
|
*
|
|
@@ -541,9 +538,9 @@ var planSpecObjectSchema = z5.object({
|
|
|
541
538
|
* environments: 1
|
|
542
539
|
* enterprise_sso: true
|
|
543
540
|
*/
|
|
544
|
-
capability_limits:
|
|
545
|
-
overageBehavior:
|
|
546
|
-
selfServeEnabled:
|
|
541
|
+
capability_limits: z6.record(z6.string().min(1).max(120), z6.union([z6.number().int().nonnegative(), z6.boolean()])).default({}),
|
|
542
|
+
overageBehavior: z6.enum(["block", "allow_and_bill"]).default("block"),
|
|
543
|
+
selfServeEnabled: z6.boolean().default(true),
|
|
547
544
|
/**
|
|
548
545
|
* Phase A0 — multi-stable plan support.
|
|
549
546
|
*
|
|
@@ -555,7 +552,7 @@ var planSpecObjectSchema = z5.object({
|
|
|
555
552
|
* from YAML while subs are pinned to it is a compile error (would
|
|
556
553
|
* orphan the cohort).
|
|
557
554
|
*/
|
|
558
|
-
legacy:
|
|
555
|
+
legacy: z6.boolean().optional().default(false),
|
|
559
556
|
/**
|
|
560
557
|
* Phase A0 — A/B testing variants of this plan. Each variant compiles
|
|
561
558
|
* into a sibling `CompiledPlan` with status `EXPERIMENTAL` and
|
|
@@ -565,11 +562,11 @@ var planSpecObjectSchema = z5.object({
|
|
|
565
562
|
* - Cannot coexist with `legacy: true` on the same plan
|
|
566
563
|
* - Variant `id` must be unique within the variants array
|
|
567
564
|
*/
|
|
568
|
-
variants:
|
|
569
|
-
archive:
|
|
570
|
-
at:
|
|
571
|
-
transitionTo:
|
|
572
|
-
strategy:
|
|
565
|
+
variants: z6.array(planVariantSchema).max(4).optional(),
|
|
566
|
+
archive: z6.object({
|
|
567
|
+
at: z6.string().datetime().optional(),
|
|
568
|
+
transitionTo: z6.string().optional(),
|
|
569
|
+
strategy: z6.enum(["auto", "explicit", "block"]).default("auto")
|
|
573
570
|
}).optional()
|
|
574
571
|
});
|
|
575
572
|
var planSpecSchema = planSpecObjectSchema.superRefine((plan, ctx) => {
|
|
@@ -577,10 +574,10 @@ var planSpecSchema = planSpecObjectSchema.superRefine((plan, ctx) => {
|
|
|
577
574
|
});
|
|
578
575
|
|
|
579
576
|
// ../contracts/dist/plans/spec/webhooks.js
|
|
580
|
-
import { z as
|
|
577
|
+
import { z as z8 } from "zod";
|
|
581
578
|
|
|
582
579
|
// ../contracts/dist/webhooks/events.js
|
|
583
|
-
import { z as
|
|
580
|
+
import { z as z7 } from "zod";
|
|
584
581
|
var WEBHOOK_EVENT_NAMES = [
|
|
585
582
|
"subscription.created",
|
|
586
583
|
"subscription.updated",
|
|
@@ -591,26 +588,26 @@ var WEBHOOK_EVENT_NAMES = [
|
|
|
591
588
|
"entitlement.changed",
|
|
592
589
|
"usage.threshold_reached"
|
|
593
590
|
];
|
|
594
|
-
var webhookEventNameSchema =
|
|
591
|
+
var webhookEventNameSchema = z7.enum(WEBHOOK_EVENT_NAMES);
|
|
595
592
|
|
|
596
593
|
// ../contracts/dist/plans/spec/webhooks.js
|
|
597
594
|
var WEBHOOK_SECRET_PLACEHOLDER_PATTERN = /^\$\{[A-Z][A-Z0-9_]{0,127}\}$/;
|
|
598
|
-
var webhookSecretSchema =
|
|
595
|
+
var webhookSecretSchema = z8.string().min(3).max(200).refine((value) => WEBHOOK_SECRET_PLACEHOLDER_PATTERN.test(value), {
|
|
599
596
|
message: "secret must use ${VAR} interpolation syntax (e.g. ${WEBHOOK_SECRET}); raw secrets in YAML are rejected. Define the env var in the per-product secret store and reference it here."
|
|
600
597
|
});
|
|
601
|
-
var webhookRetryPolicySchema =
|
|
602
|
-
maxAttempts:
|
|
603
|
-
backoff:
|
|
598
|
+
var webhookRetryPolicySchema = z8.object({
|
|
599
|
+
maxAttempts: z8.number().int().min(1).max(20).default(5),
|
|
600
|
+
backoff: z8.enum(["exponential", "fixed"]).default("exponential")
|
|
604
601
|
});
|
|
605
|
-
var webhookEndpointSchema =
|
|
602
|
+
var webhookEndpointSchema = z8.object({
|
|
606
603
|
/**
|
|
607
604
|
* Stable endpoint id — used as the third key of the
|
|
608
605
|
* `(productId, environmentId, id)` uniqueness tuple. Idempotent upsert
|
|
609
606
|
* keys on this id; renaming an id is delete + recreate.
|
|
610
607
|
*/
|
|
611
|
-
id:
|
|
608
|
+
id: z8.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Webhook endpoint id must be lowercase alphanumeric with hyphens/underscores"),
|
|
612
609
|
/** Public HTTPS URL the dispatcher POSTs to. */
|
|
613
|
-
url:
|
|
610
|
+
url: z8.string().url("webhooks.endpoints[].url must be a valid URL"),
|
|
614
611
|
/**
|
|
615
612
|
* Signing secret. MUST be a `${VAR}` placeholder; raw secrets in YAML
|
|
616
613
|
* are rejected by invariant 8. The seal pass resolves this against the
|
|
@@ -623,22 +620,22 @@ var webhookEndpointSchema = z7.object({
|
|
|
623
620
|
* Each value is validated against `webhookEventNameSchema` —
|
|
624
621
|
* unknown events fail invariant 9.
|
|
625
622
|
*/
|
|
626
|
-
events:
|
|
627
|
-
enabled:
|
|
623
|
+
events: z8.array(webhookEventNameSchema).min(1, "webhooks.endpoints[].events must subscribe to \u2265 1 event"),
|
|
624
|
+
enabled: z8.boolean().default(true),
|
|
628
625
|
retryPolicy: webhookRetryPolicySchema.default({
|
|
629
626
|
maxAttempts: 5,
|
|
630
627
|
backoff: "exponential"
|
|
631
628
|
})
|
|
632
629
|
});
|
|
633
|
-
var webhooksBlockSchema =
|
|
634
|
-
endpoints:
|
|
630
|
+
var webhooksBlockSchema = z8.object({
|
|
631
|
+
endpoints: z8.array(webhookEndpointSchema).max(50).default([])
|
|
635
632
|
});
|
|
636
633
|
|
|
637
634
|
// ../contracts/dist/plans/spec/environments.js
|
|
638
|
-
import { z as
|
|
639
|
-
var planOverrideSchema =
|
|
640
|
-
meters:
|
|
641
|
-
recurring_fee_cents:
|
|
635
|
+
import { z as z9 } from "zod";
|
|
636
|
+
var planOverrideSchema = z9.object({
|
|
637
|
+
meters: z9.array(meterSchema).optional(),
|
|
638
|
+
recurring_fee_cents: z9.number().int().nonnegative().optional(),
|
|
642
639
|
/** Per-env override for the billing cadence. Absent → inherit the
|
|
643
640
|
* base plan's `billing_interval`. */
|
|
644
641
|
billing_interval: billingIntervalSchema.optional(),
|
|
@@ -646,35 +643,38 @@ var planOverrideSchema = z8.object({
|
|
|
646
643
|
* surface. When present it fully replaces the parent plan's grants
|
|
647
644
|
* for this environment (the legacy recurring/one-time scalar knobs
|
|
648
645
|
* were removed). */
|
|
649
|
-
grants:
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
646
|
+
grants: z9.array(grantSchema).max(40).optional(),
|
|
647
|
+
/** Per-env override for the credit-policy block (rollover +
|
|
648
|
+
* auto-recharge). */
|
|
649
|
+
creditPolicy: creditPolicySchema.optional(),
|
|
650
|
+
trial_days: z9.number().int().nonnegative().optional(),
|
|
651
|
+
max_monthly_spend_cents: z9.number().int().nonnegative().optional(),
|
|
652
|
+
limits: z9.array(planLimitRuleSchema).max(20).optional(),
|
|
653
|
+
featureGates: z9.record(z9.string(), z9.boolean()).optional(),
|
|
654
|
+
overageBehavior: z9.enum(["block", "allow_and_bill"]).optional(),
|
|
655
|
+
selfServeEnabled: z9.boolean().optional(),
|
|
656
|
+
legacy: z9.boolean().optional()
|
|
657
657
|
}).strict();
|
|
658
|
-
var webhookEndpointOverrideSchema =
|
|
659
|
-
url:
|
|
658
|
+
var webhookEndpointOverrideSchema = z9.object({
|
|
659
|
+
url: z9.string().url().optional(),
|
|
660
660
|
secret: webhookSecretSchema.optional(),
|
|
661
|
-
events:
|
|
662
|
-
enabled:
|
|
661
|
+
events: z9.array(webhookEventNameSchema).optional(),
|
|
662
|
+
enabled: z9.boolean().optional(),
|
|
663
663
|
retryPolicy: webhookRetryPolicySchema.partial().optional()
|
|
664
664
|
}).strict();
|
|
665
|
-
var environmentOverrideBlockSchema =
|
|
666
|
-
plans:
|
|
667
|
-
webhooks:
|
|
668
|
-
endpoints:
|
|
665
|
+
var environmentOverrideBlockSchema = z9.object({
|
|
666
|
+
plans: z9.record(z9.string(), planOverrideSchema).optional(),
|
|
667
|
+
webhooks: z9.object({
|
|
668
|
+
endpoints: z9.record(z9.string(), webhookEndpointOverrideSchema).optional()
|
|
669
669
|
}).strict().optional()
|
|
670
670
|
}).strict();
|
|
671
|
-
var environmentsBlockSchema =
|
|
671
|
+
var environmentsBlockSchema = z9.record(z9.string().min(1).max(64), environmentOverrideBlockSchema);
|
|
672
672
|
|
|
673
673
|
// ../contracts/dist/plans/spec/product.js
|
|
674
|
-
import { z as
|
|
674
|
+
import { z as z19 } from "zod";
|
|
675
675
|
|
|
676
676
|
// ../contracts/dist/plans/spec/frontend-layer.js
|
|
677
|
-
import { z as
|
|
677
|
+
import { z as z10 } from "zod";
|
|
678
678
|
var FRONTEND_MANIFEST_SCHEMA_VERSION = 1;
|
|
679
679
|
var KNOWN_FRONTEND_COMPONENT_IDS = [
|
|
680
680
|
"plans_table",
|
|
@@ -704,31 +704,31 @@ var RESERVED_TEMPLATE_PATHS = [
|
|
|
704
704
|
"/persona",
|
|
705
705
|
"/persona-sign-in"
|
|
706
706
|
];
|
|
707
|
-
var frontendPathSchema =
|
|
708
|
-
var frontendCapabilityRefSchema =
|
|
709
|
-
var frontendNavItemSchema =
|
|
710
|
-
label:
|
|
707
|
+
var frontendPathSchema = z10.string().min(1).max(200).regex(/^\/[a-zA-Z0-9_./-]*$/, "path must start with / and contain only [a-zA-Z0-9_./-]");
|
|
708
|
+
var frontendCapabilityRefSchema = z10.string().min(1).max(120).regex(/^[a-z0-9_-]+$/);
|
|
709
|
+
var frontendNavItemSchema = z10.object({
|
|
710
|
+
label: z10.string().min(1).max(80),
|
|
711
711
|
path: frontendPathSchema,
|
|
712
712
|
capability: frontendCapabilityRefSchema.optional()
|
|
713
713
|
});
|
|
714
|
-
var frontendComponentIdSchema =
|
|
715
|
-
var frontendComponentSchema =
|
|
714
|
+
var frontendComponentIdSchema = z10.enum(KNOWN_FRONTEND_COMPONENT_IDS);
|
|
715
|
+
var frontendComponentSchema = z10.object({
|
|
716
716
|
component: frontendComponentIdSchema,
|
|
717
|
-
props:
|
|
717
|
+
props: z10.record(z10.string().min(1).max(80), z10.unknown()).optional(),
|
|
718
718
|
capability: frontendCapabilityRefSchema.optional(),
|
|
719
|
-
gateMode:
|
|
719
|
+
gateMode: z10.enum(["hide", "disable", "upsell"]).default("hide")
|
|
720
720
|
});
|
|
721
|
-
var frontendPageSchema =
|
|
721
|
+
var frontendPageSchema = z10.object({
|
|
722
722
|
path: frontendPathSchema,
|
|
723
|
-
title:
|
|
724
|
-
requiresAuth:
|
|
723
|
+
title: z10.string().min(1).max(120),
|
|
724
|
+
requiresAuth: z10.boolean(),
|
|
725
725
|
capability: frontendCapabilityRefSchema.optional(),
|
|
726
|
-
components:
|
|
726
|
+
components: z10.array(frontendComponentSchema).max(12).default([])
|
|
727
727
|
});
|
|
728
|
-
var frontendManifestSchema =
|
|
729
|
-
version:
|
|
730
|
-
nav:
|
|
731
|
-
pages:
|
|
728
|
+
var frontendManifestSchema = z10.object({
|
|
729
|
+
version: z10.literal(FRONTEND_MANIFEST_SCHEMA_VERSION),
|
|
730
|
+
nav: z10.array(frontendNavItemSchema).max(12).default([]),
|
|
731
|
+
pages: z10.array(frontendPageSchema).max(24).default([])
|
|
732
732
|
}).superRefine((manifest, ctx) => {
|
|
733
733
|
const seenPages = /* @__PURE__ */ new Set();
|
|
734
734
|
manifest.pages.forEach((page, index) => {
|
|
@@ -760,32 +760,35 @@ function isReservedTemplatePath(path) {
|
|
|
760
760
|
}
|
|
761
761
|
|
|
762
762
|
// ../contracts/dist/plans/spec/migrations-layer.js
|
|
763
|
-
import { z as
|
|
764
|
-
var planKeySchema =
|
|
765
|
-
var versionRefSchema =
|
|
766
|
-
|
|
763
|
+
import { z as z11 } from "zod";
|
|
764
|
+
var planKeySchema = z11.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Plan key must be lowercase alphanumeric with hyphens/underscores");
|
|
765
|
+
var versionRefSchema = z11.union([
|
|
766
|
+
z11.literal("head"),
|
|
767
|
+
z11.string().regex(/^[1-9][0-9]*$/, 'version must be "head" or a positive integer')
|
|
768
|
+
]);
|
|
769
|
+
var planVersionRefSchema = z11.object({
|
|
767
770
|
plan: planKeySchema,
|
|
768
771
|
version: versionRefSchema.optional()
|
|
769
772
|
});
|
|
770
|
-
var migrationTargetSchema =
|
|
773
|
+
var migrationTargetSchema = z11.object({
|
|
771
774
|
plan: planKeySchema,
|
|
772
|
-
version:
|
|
775
|
+
version: z11.literal("head").default("head")
|
|
773
776
|
});
|
|
774
|
-
var pinnedPlanVersionSchema =
|
|
777
|
+
var pinnedPlanVersionSchema = z11.object({
|
|
775
778
|
plan: planKeySchema,
|
|
776
779
|
version: versionRefSchema
|
|
777
780
|
});
|
|
778
|
-
var migrationEffectiveSchema =
|
|
781
|
+
var migrationEffectiveSchema = z11.enum([
|
|
779
782
|
"grandfather",
|
|
780
783
|
"next_renewal",
|
|
781
784
|
"by_date",
|
|
782
785
|
"immediate",
|
|
783
786
|
"opt_in"
|
|
784
787
|
]);
|
|
785
|
-
var migrationProrationSchema =
|
|
786
|
-
var migrationExistingCustomersSchema =
|
|
788
|
+
var migrationProrationSchema = z11.enum(["none", "prorate", "credit"]);
|
|
789
|
+
var migrationExistingCustomersSchema = z11.object({
|
|
787
790
|
effective: migrationEffectiveSchema,
|
|
788
|
-
date:
|
|
791
|
+
date: z11.string().datetime().optional(),
|
|
789
792
|
proration: migrationProrationSchema.optional()
|
|
790
793
|
}).superRefine((policy, ctx) => {
|
|
791
794
|
if (policy.effective === "by_date" && policy.date === void 0) {
|
|
@@ -796,21 +799,21 @@ var migrationExistingCustomersSchema = z10.object({
|
|
|
796
799
|
});
|
|
797
800
|
}
|
|
798
801
|
});
|
|
799
|
-
var migrationPinSchema =
|
|
800
|
-
subscriber:
|
|
802
|
+
var migrationPinSchema = z11.object({
|
|
803
|
+
subscriber: z11.string().min(1).max(200),
|
|
801
804
|
pinTo: pinnedPlanVersionSchema,
|
|
802
|
-
until:
|
|
803
|
-
notes:
|
|
805
|
+
until: z11.string().datetime().optional(),
|
|
806
|
+
notes: z11.string().max(1e3).optional()
|
|
804
807
|
});
|
|
805
|
-
var migrationDeclSchema =
|
|
806
|
-
id:
|
|
808
|
+
var migrationDeclSchema = z11.object({
|
|
809
|
+
id: z11.string().min(1).max(120).regex(/^[a-z0-9][a-z0-9_.-]*$/, "Migration id must be lowercase alphanumeric with dots, hyphens, or underscores"),
|
|
807
810
|
from: planVersionRefSchema,
|
|
808
811
|
to: migrationTargetSchema,
|
|
809
|
-
newCustomers:
|
|
812
|
+
newCustomers: z11.literal("immediate").default("immediate"),
|
|
810
813
|
existingCustomers: migrationExistingCustomersSchema,
|
|
811
|
-
pins:
|
|
814
|
+
pins: z11.array(migrationPinSchema).max(50).default([])
|
|
812
815
|
});
|
|
813
|
-
var migrationDeclsSchema =
|
|
816
|
+
var migrationDeclsSchema = z11.array(migrationDeclSchema).superRefine((migrations, ctx) => {
|
|
814
817
|
const seen = /* @__PURE__ */ new Set();
|
|
815
818
|
migrations.forEach((migration, index) => {
|
|
816
819
|
if (seen.has(migration.id)) {
|
|
@@ -825,18 +828,18 @@ var migrationDeclsSchema = z10.array(migrationDeclSchema).superRefine((migration
|
|
|
825
828
|
});
|
|
826
829
|
|
|
827
830
|
// ../contracts/dist/plans/spec/counted-resources.js
|
|
828
|
-
import { z as
|
|
829
|
-
var countedResourceScopeSchema =
|
|
830
|
-
var countedResourceCountSourceSchema =
|
|
831
|
+
import { z as z12 } from "zod";
|
|
832
|
+
var countedResourceScopeSchema = z12.enum(["subscription", "subject"]);
|
|
833
|
+
var countedResourceCountSourceSchema = z12.enum([
|
|
831
834
|
"reported",
|
|
832
835
|
"action_inferred"
|
|
833
836
|
]);
|
|
834
|
-
var countedResourceNameSchema =
|
|
835
|
-
var countedResourceSchema =
|
|
837
|
+
var countedResourceNameSchema = z12.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/);
|
|
838
|
+
var countedResourceSchema = z12.object({
|
|
836
839
|
name: countedResourceNameSchema,
|
|
837
|
-
display:
|
|
840
|
+
display: z12.string().min(1).max(120).optional(),
|
|
838
841
|
scope: countedResourceScopeSchema.default("subscription"),
|
|
839
|
-
subjectType:
|
|
842
|
+
subjectType: z12.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/).optional(),
|
|
840
843
|
countSource: countedResourceCountSourceSchema.default("reported")
|
|
841
844
|
}).superRefine((resource, ctx) => {
|
|
842
845
|
if (resource.scope === "subject" && !resource.subjectType) {
|
|
@@ -854,23 +857,23 @@ var countedResourceSchema = z11.object({
|
|
|
854
857
|
});
|
|
855
858
|
}
|
|
856
859
|
});
|
|
857
|
-
var countedResourcesSchema =
|
|
860
|
+
var countedResourcesSchema = z12.array(countedResourceSchema).max(100).default([]);
|
|
858
861
|
|
|
859
862
|
// ../contracts/dist/plans/addons.js
|
|
860
|
-
import { z as
|
|
861
|
-
var addOnSchema =
|
|
863
|
+
import { z as z13 } from "zod";
|
|
864
|
+
var addOnSchema = z13.object({
|
|
862
865
|
/** Stable identifier — appears in `SubscriptionAddOn.addOnKey` and in
|
|
863
866
|
* Stripe metadata. Lowercase alphanumeric with hyphens/underscores
|
|
864
867
|
* to match the plan-key grammar. */
|
|
865
|
-
key:
|
|
868
|
+
key: z13.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "AddOn key must be lowercase alphanumeric with hyphens/underscores"),
|
|
866
869
|
/** Display name (Pricing page chip, settings list, etc.). */
|
|
867
|
-
name:
|
|
870
|
+
name: z13.string().min(1).max(100),
|
|
868
871
|
/** Short description for tooltips / chooser. */
|
|
869
|
-
description:
|
|
872
|
+
description: z13.string().max(500).optional(),
|
|
870
873
|
/** Recurring fee added to the subscription invoice while the AddOn is
|
|
871
874
|
* active. 0 = no recurring fee (e.g. for boolean feature toggles or
|
|
872
875
|
* one-time-grant-only packs). */
|
|
873
|
-
recurring_fee_cents:
|
|
876
|
+
recurring_fee_cents: z13.number().int().nonnegative().default(0),
|
|
874
877
|
/** Billing cadence for this add-on's recurring fee (`month` default /
|
|
875
878
|
* `year`). Drives the add-on's Stripe price `recurring.interval`. */
|
|
876
879
|
billing_interval: billingIntervalSchema.default("month"),
|
|
@@ -878,43 +881,403 @@ var addOnSchema = z12.object({
|
|
|
878
881
|
* into the effective entitlement's meter list (by dimension).
|
|
879
882
|
* Conflict resolution at compile time: AddOn meter wins over base
|
|
880
883
|
* plan meter for the same dimension (PR 2a-2 implements this). */
|
|
881
|
-
meters:
|
|
884
|
+
meters: z13.array(meterSchema).default([]),
|
|
882
885
|
/** Grants issued when the AddOn is activated and/or on each period
|
|
883
886
|
* rollover (kind-dependent). Same vocabulary as plan grants. */
|
|
884
|
-
grants:
|
|
887
|
+
grants: z13.array(grantSchema).max(40).default([]),
|
|
885
888
|
/** Capability bumps. Numeric limits combine with the base via MAX
|
|
886
889
|
* (so an "extra seats" addon raises the ceiling without lowering
|
|
887
890
|
* it). Boolean flags combine with OR (any true wins). */
|
|
888
|
-
capability_limits:
|
|
891
|
+
capability_limits: z13.record(z13.string().min(1).max(120), z13.union([z13.number().int().nonnegative(), z13.boolean()])).default({}),
|
|
889
892
|
/** Additive feature gates. Keys true here are appended to the
|
|
890
893
|
* effective entitlement's gate set. */
|
|
891
|
-
featureGates:
|
|
894
|
+
featureGates: z13.record(z13.string(), z13.boolean()).optional(),
|
|
892
895
|
/** Whether the AddOn is visible in the subscriber-facing catalog.
|
|
893
896
|
* When false, only admin-issued (not self-serve). Defaults true. */
|
|
894
|
-
selfServeEnabled:
|
|
897
|
+
selfServeEnabled: z13.boolean().default(true)
|
|
895
898
|
});
|
|
896
|
-
var addOnsBlockSchema =
|
|
897
|
-
var subscriptionAddOnStatusSchema =
|
|
899
|
+
var addOnsBlockSchema = z13.record(z13.string().min(1).max(64), addOnSchema).default({});
|
|
900
|
+
var subscriptionAddOnStatusSchema = z13.enum([
|
|
898
901
|
"active",
|
|
899
902
|
"canceled",
|
|
900
903
|
"paused",
|
|
901
904
|
"pending_activation"
|
|
902
905
|
]);
|
|
903
|
-
var subscriptionAddOnSchema =
|
|
906
|
+
var subscriptionAddOnSchema = z13.object({
|
|
904
907
|
/** References `product.add_ons.<addOnKey>`. */
|
|
905
|
-
addOnKey:
|
|
908
|
+
addOnKey: z13.string().min(1).max(64),
|
|
906
909
|
/** Status drives entitlement composition: only `active` AddOns
|
|
907
910
|
* contribute to the effective entitlement. */
|
|
908
911
|
status: subscriptionAddOnStatusSchema.default("active"),
|
|
909
912
|
/** When the AddOn became active (ISO-8601). Drives one-time grant
|
|
910
913
|
* issuance and recurring-fee start. */
|
|
911
|
-
activatedAt:
|
|
914
|
+
activatedAt: z13.string().datetime().optional(),
|
|
912
915
|
/** When the AddOn was canceled (ISO-8601). Null while active. */
|
|
913
|
-
canceledAt:
|
|
916
|
+
canceledAt: z13.string().datetime().nullable().optional(),
|
|
914
917
|
/** Stripe subscription-item id for the recurring-fee line, when
|
|
915
918
|
* the AddOn declares a non-zero `recurring_fee_cents`. */
|
|
916
|
-
stripeSubscriptionItemId:
|
|
919
|
+
stripeSubscriptionItemId: z13.string().optional()
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// ../contracts/dist/plans/spec/backend-layer.js
|
|
923
|
+
import { z as z14 } from "zod";
|
|
924
|
+
var BACKEND_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
925
|
+
var backendIdSchema = z14.string().min(1).max(64).regex(BACKEND_ID_PATTERN, "backend id must be a lowercase slug ([a-z0-9][a-z0-9_-]*)");
|
|
926
|
+
var backendTransportModeSchema = z14.enum([
|
|
927
|
+
"public_origin",
|
|
928
|
+
"mtls",
|
|
929
|
+
"cloudflare_tunnel"
|
|
930
|
+
]);
|
|
931
|
+
var backendTransportRunnerSchema = z14.enum([
|
|
932
|
+
"managed_cloudflared",
|
|
933
|
+
"sidecar"
|
|
934
|
+
]);
|
|
935
|
+
var backendTransportSchema = z14.object({
|
|
936
|
+
mode: backendTransportModeSchema.default("public_origin"),
|
|
937
|
+
runner: backendTransportRunnerSchema.optional()
|
|
938
|
+
}).strict();
|
|
939
|
+
var backendVerificationSchema = z14.object({
|
|
940
|
+
required: z14.boolean().default(false)
|
|
941
|
+
}).strict();
|
|
942
|
+
var backendDefinitionSchema = z14.object({
|
|
943
|
+
/** Human-friendly label. Defaults to the id when omitted. */
|
|
944
|
+
name: z14.string().min(1).max(120).optional(),
|
|
945
|
+
/** Stable slug for the backend (origin-hostname / token scoping). Defaults
|
|
946
|
+
* to the id when omitted. */
|
|
947
|
+
slug: backendIdSchema.optional(),
|
|
948
|
+
transport: backendTransportSchema.default({ mode: "public_origin" }),
|
|
949
|
+
verification: backendVerificationSchema.default({ required: false }),
|
|
950
|
+
/** Meter allow-list. Omitted = all product meters allowed. */
|
|
951
|
+
meters: z14.array(z14.string().min(1).max(64)).max(100).optional(),
|
|
952
|
+
/** Marks the default backend when a product declares more than one. At
|
|
953
|
+
* most one backend may set this (compiler enforces / AMBIGUOUS_DEFAULT). */
|
|
954
|
+
default: z14.boolean().optional(),
|
|
955
|
+
/** Reachable origin for `public_origin` / `mtls`. */
|
|
956
|
+
originUrl: z14.string().url().optional(),
|
|
957
|
+
/** Access-protected `*.fs-origin` host for `cloudflare_tunnel`. */
|
|
958
|
+
originHostname: z14.string().min(1).max(255).optional()
|
|
959
|
+
}).strict();
|
|
960
|
+
var productBackendBlockSchema = z14.record(backendIdSchema, backendDefinitionSchema);
|
|
961
|
+
var routeBackendBindingSchema = backendIdSchema;
|
|
962
|
+
var BACKEND_DIAGNOSTIC_CODES = {
|
|
963
|
+
unknownBackendInRoute: "UNKNOWN_BACKEND_IN_ROUTE",
|
|
964
|
+
ambiguousDefaultBackend: "AMBIGUOUS_DEFAULT_BACKEND",
|
|
965
|
+
routeMeterNotAllowedByBackend: "ROUTE_METER_NOT_ALLOWED_BY_BACKEND",
|
|
966
|
+
unknownMeterInBackend: "UNKNOWN_METER_IN_BACKEND"
|
|
967
|
+
};
|
|
968
|
+
function resolveDefaultBackendId(backends) {
|
|
969
|
+
const ids = Object.keys(backends ?? {});
|
|
970
|
+
if (ids.length === 0)
|
|
971
|
+
return { defaultId: null, ambiguous: false };
|
|
972
|
+
if (ids.length === 1)
|
|
973
|
+
return { defaultId: ids[0], ambiguous: false };
|
|
974
|
+
const explicit = ids.filter((id) => backends[id]?.default === true);
|
|
975
|
+
if (explicit.length === 1) {
|
|
976
|
+
return { defaultId: explicit[0], ambiguous: false };
|
|
977
|
+
}
|
|
978
|
+
return { defaultId: null, ambiguous: true };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// ../contracts/dist/plans/spec/routes-layer.js
|
|
982
|
+
import { z as z18 } from "zod";
|
|
983
|
+
|
|
984
|
+
// ../contracts/dist/plans/spec/policies-layer.js
|
|
985
|
+
import { z as z16 } from "zod";
|
|
986
|
+
|
|
987
|
+
// ../contracts/dist/plans/spec/policy-types.js
|
|
988
|
+
import { z as z15 } from "zod";
|
|
989
|
+
var rateLimitWindowSchema = z15.string().min(2).max(20).regex(/^\d+(ms|s|m|h)$/, "rate_limit window must look like `60s`, `5m`, `1h`");
|
|
990
|
+
var rateLimitConfigSchema = z15.object({
|
|
991
|
+
strategy: z15.enum(["token_bucket", "sliding_window", "fixed_window"]).default("token_bucket"),
|
|
992
|
+
/**
|
|
993
|
+
* Which request dimensions identify the bucket. v0.3.0 supports a
|
|
994
|
+
* fixed set; extending requires a coordinated gateway/policy-engine
|
|
995
|
+
* change. The `subscription` dimension is the steady-state default;
|
|
996
|
+
* `ip` is for unauthenticated probes; `credential` is finer-grained
|
|
997
|
+
* than subscription (per-key throttling).
|
|
998
|
+
*/
|
|
999
|
+
dimensions: z15.array(z15.enum(["subscription", "credential", "ip", "route"])).min(1).max(4).default(["subscription"]),
|
|
1000
|
+
limits: z15.array(z15.object({
|
|
1001
|
+
window: rateLimitWindowSchema,
|
|
1002
|
+
max: z15.number().int().positive().max(1e7)
|
|
1003
|
+
})).min(1).max(10),
|
|
1004
|
+
/**
|
|
1005
|
+
* Bounded fail-open behaviour for DO outages. See architecture RFC
|
|
1006
|
+
* "Fail-open guardrails" section. When the policy executor observes
|
|
1007
|
+
* `max_consecutive_failures` DO-call failures within
|
|
1008
|
+
* `max_window_seconds`, it transitions to `degraded_mode` until
|
|
1009
|
+
* `recovery_threshold` consecutive successes restore normal
|
|
1010
|
+
* evaluation.
|
|
1011
|
+
*/
|
|
1012
|
+
fail_open: z15.object({
|
|
1013
|
+
max_consecutive_failures: z15.number().int().positive().default(100),
|
|
1014
|
+
max_window_seconds: z15.number().int().positive().default(60),
|
|
1015
|
+
recovery_threshold: z15.number().int().positive().default(50),
|
|
1016
|
+
degraded_mode: z15.enum([
|
|
1017
|
+
"safe_mode_block",
|
|
1018
|
+
"safe_mode_throttle",
|
|
1019
|
+
"runtime_killswitch_trigger"
|
|
1020
|
+
]).default("safe_mode_throttle")
|
|
1021
|
+
}).default({
|
|
1022
|
+
max_consecutive_failures: 100,
|
|
1023
|
+
max_window_seconds: 60,
|
|
1024
|
+
recovery_threshold: 50,
|
|
1025
|
+
degraded_mode: "safe_mode_throttle"
|
|
1026
|
+
})
|
|
1027
|
+
});
|
|
1028
|
+
var authConfigSchema = z15.object({
|
|
1029
|
+
header_name: z15.string().min(1).max(100).default("x-api-key"),
|
|
1030
|
+
/**
|
|
1031
|
+
* How the gateway constructs the upstream Authorization header:
|
|
1032
|
+
* - `none` → no upstream auth header added
|
|
1033
|
+
* - `static_bearer` → forward a configured static token
|
|
1034
|
+
* - `subscriber_jwt` → mint a per-subscriber JWT (out of scope v0.3.0)
|
|
1035
|
+
*/
|
|
1036
|
+
upstream_token_source: z15.discriminatedUnion("type", [
|
|
1037
|
+
z15.object({ type: z15.literal("none") }),
|
|
1038
|
+
z15.object({
|
|
1039
|
+
type: z15.literal("static_bearer"),
|
|
1040
|
+
token_secret_ref: z15.string().min(1).max(200).describe("Reference into the secret store (e.g. CF Secret name); not the raw token")
|
|
1041
|
+
})
|
|
1042
|
+
]).default({ type: "none" }),
|
|
1043
|
+
/**
|
|
1044
|
+
* When `true`, treat the inbound credential's scopes (if any) as
|
|
1045
|
+
* additional gating beyond the entitlement check. v0.3.0 ships with
|
|
1046
|
+
* `strict` as the default.
|
|
1047
|
+
*/
|
|
1048
|
+
scope_mode: z15.enum(["strict", "advisory", "off"]).default("strict")
|
|
1049
|
+
});
|
|
1050
|
+
var concurrencyConfigSchema = z15.object({
|
|
1051
|
+
max_in_flight: z15.number().int().positive().max(1e4),
|
|
1052
|
+
/**
|
|
1053
|
+
* Which dimensions key the lease bucket. Matches the existing
|
|
1054
|
+
* ConcurrencyLease DO `idFromName` pattern (subscription | capability
|
|
1055
|
+
* tuple).
|
|
1056
|
+
*/
|
|
1057
|
+
dimensions: z15.array(z15.enum(["subscription", "credential", "capability"])).min(1).max(3).default(["subscription"]),
|
|
1058
|
+
/**
|
|
1059
|
+
* Optional capability scope. When set, the lease bucket is keyed
|
|
1060
|
+
* partly by this capability name — separate buckets per capability.
|
|
1061
|
+
*/
|
|
1062
|
+
capability: z15.string().min(1).max(120).optional(),
|
|
1063
|
+
/**
|
|
1064
|
+
* Lease TTL — releases automatically after this many seconds even if
|
|
1065
|
+
* the request never returns (defensive default 30s, mirrors existing
|
|
1066
|
+
* ConcurrencyLease behaviour).
|
|
1067
|
+
*/
|
|
1068
|
+
lease_ttl_seconds: z15.number().int().positive().max(600).default(30),
|
|
1069
|
+
fail_open: z15.object({
|
|
1070
|
+
max_consecutive_failures: z15.number().int().positive().default(50),
|
|
1071
|
+
max_window_seconds: z15.number().int().positive().default(60),
|
|
1072
|
+
recovery_threshold: z15.number().int().positive().default(20),
|
|
1073
|
+
degraded_mode: z15.enum([
|
|
1074
|
+
"safe_mode_block",
|
|
1075
|
+
"safe_mode_throttle",
|
|
1076
|
+
"runtime_killswitch_trigger"
|
|
1077
|
+
]).default("safe_mode_throttle")
|
|
1078
|
+
}).default({
|
|
1079
|
+
max_consecutive_failures: 50,
|
|
1080
|
+
max_window_seconds: 60,
|
|
1081
|
+
recovery_threshold: 20,
|
|
1082
|
+
degraded_mode: "safe_mode_throttle"
|
|
1083
|
+
})
|
|
1084
|
+
});
|
|
1085
|
+
var retryConfigSchema = z15.object({
|
|
1086
|
+
max_attempts: z15.number().int().min(1).max(5).default(2),
|
|
1087
|
+
/**
|
|
1088
|
+
* HTTP status codes that trigger a retry. 5xx is the default; opt
|
|
1089
|
+
* into 429 retries only when the upstream understands `Retry-After`.
|
|
1090
|
+
*/
|
|
1091
|
+
retry_on_status: z15.array(z15.number().int().min(400).max(599)).min(1).max(20).default([502, 503, 504]),
|
|
1092
|
+
/**
|
|
1093
|
+
* Backoff curve. Total wall-clock attempt time is bounded so the
|
|
1094
|
+
* gateway worker cannot block past `total_budget_ms`.
|
|
1095
|
+
*/
|
|
1096
|
+
backoff: z15.object({
|
|
1097
|
+
initial_ms: z15.number().int().positive().max(5e3).default(100),
|
|
1098
|
+
multiplier: z15.number().positive().max(10).default(2),
|
|
1099
|
+
jitter: z15.enum(["none", "full", "equal"]).default("equal"),
|
|
1100
|
+
total_budget_ms: z15.number().int().positive().max(3e4).default(5e3)
|
|
1101
|
+
}).default({
|
|
1102
|
+
initial_ms: 100,
|
|
1103
|
+
multiplier: 2,
|
|
1104
|
+
jitter: "equal",
|
|
1105
|
+
total_budget_ms: 5e3
|
|
1106
|
+
})
|
|
1107
|
+
});
|
|
1108
|
+
var transformConfigSchema = z15.object({
|
|
1109
|
+
/**
|
|
1110
|
+
* When the transform applies. `request` runs before upstream forward;
|
|
1111
|
+
* `response` runs after. Most transforms are one or the other; both
|
|
1112
|
+
* is rare.
|
|
1113
|
+
*/
|
|
1114
|
+
applies_to: z15.enum(["request", "response", "both"]).default("request"),
|
|
1115
|
+
/**
|
|
1116
|
+
* List of key rewrites. Source path uses dot notation (`a.b.c`);
|
|
1117
|
+
* `target` may include the same syntax to move keys around. Drops
|
|
1118
|
+
* are expressed as `target: null`.
|
|
1119
|
+
*/
|
|
1120
|
+
rewrites: z15.array(z15.object({
|
|
1121
|
+
source: z15.string().min(1).max(200),
|
|
1122
|
+
target: z15.string().min(1).max(200).nullable()
|
|
1123
|
+
})).min(1).max(20)
|
|
1124
|
+
});
|
|
1125
|
+
var policyBodySchema = z15.discriminatedUnion("type", [
|
|
1126
|
+
z15.object({ type: z15.literal("rate_limit"), config: rateLimitConfigSchema }),
|
|
1127
|
+
z15.object({ type: z15.literal("auth"), config: authConfigSchema }),
|
|
1128
|
+
z15.object({ type: z15.literal("concurrency"), config: concurrencyConfigSchema }),
|
|
1129
|
+
z15.object({ type: z15.literal("retry"), config: retryConfigSchema }),
|
|
1130
|
+
z15.object({ type: z15.literal("transform"), config: transformConfigSchema })
|
|
1131
|
+
]);
|
|
1132
|
+
|
|
1133
|
+
// ../contracts/dist/plans/spec/policies-layer.js
|
|
1134
|
+
var cacheProfileSchema = z16.enum(["long", "short", "blocking"]).default("long");
|
|
1135
|
+
var policyCompatibilitySchema = z16.object({
|
|
1136
|
+
route_types: z16.array(z16.enum(["http"])).max(5).optional(),
|
|
1137
|
+
meters: z16.array(z16.string().min(1).max(64)).max(20).optional(),
|
|
1138
|
+
auth_modes: z16.array(z16.enum(["api_key", "oauth2", "anonymous"])).max(5).optional()
|
|
1139
|
+
});
|
|
1140
|
+
var policyLayerSchema = z16.intersection(z16.object({
|
|
1141
|
+
/**
|
|
1142
|
+
* Policy name. Referenced by routes via `policies: [<name>]`. Must
|
|
1143
|
+
* be unique across the product; the compiler enforces this in the
|
|
1144
|
+
* cross-layer validation pass.
|
|
1145
|
+
*/
|
|
1146
|
+
name: z16.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Policy name must be lowercase alphanumeric with hyphens/underscores"),
|
|
1147
|
+
description: z16.string().max(500).optional(),
|
|
1148
|
+
compatible_with: policyCompatibilitySchema.default({}),
|
|
1149
|
+
/**
|
|
1150
|
+
* Mutation class — runtime vs contractual. Policies are operational
|
|
1151
|
+
* by nature so the default is `runtime`. Marking a policy as
|
|
1152
|
+
* `contractual` signals that changes to it require human approval
|
|
1153
|
+
* (invariant #16).
|
|
1154
|
+
*/
|
|
1155
|
+
mutation_class: z16.enum(["runtime", "contractual"]).default("runtime"),
|
|
1156
|
+
cacheProfile: cacheProfileSchema
|
|
1157
|
+
}), policyBodySchema);
|
|
1158
|
+
|
|
1159
|
+
// ../contracts/dist/framework/actions/index.js
|
|
1160
|
+
import { z as z17 } from "zod";
|
|
1161
|
+
var actionKindSchema = z17.enum(["query", "mutation"]);
|
|
1162
|
+
var actionAuditPolicySchema = z17.enum(["none", "metadata", "full"]);
|
|
1163
|
+
var actionSubjectBindingSchema = z17.object({
|
|
1164
|
+
type: z17.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/),
|
|
1165
|
+
from: z17.enum(["header", "path_param"]),
|
|
1166
|
+
name: z17.string().min(1).max(120)
|
|
1167
|
+
});
|
|
1168
|
+
var actionResourceEffectSchema = z17.object({
|
|
1169
|
+
resource: z17.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/),
|
|
1170
|
+
effect: z17.enum(["create", "delete"])
|
|
1171
|
+
});
|
|
1172
|
+
var actionSpecSchema = z17.object({
|
|
1173
|
+
id: z17.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/),
|
|
1174
|
+
title: z17.string().min(1).max(160).optional(),
|
|
1175
|
+
kind: actionKindSchema,
|
|
1176
|
+
actorType: z17.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/).optional(),
|
|
1177
|
+
subject: actionSubjectBindingSchema.optional(),
|
|
1178
|
+
inputSchemaRef: z17.string().min(1).max(240).optional(),
|
|
1179
|
+
audit: actionAuditPolicySchema.default("metadata"),
|
|
1180
|
+
resource: actionResourceEffectSchema.optional()
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// ../contracts/dist/plans/spec/routes-layer.js
|
|
1184
|
+
var routeMatchSchema = z18.object({
|
|
1185
|
+
method: z18.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
|
|
1186
|
+
path: z18.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]")
|
|
1187
|
+
});
|
|
1188
|
+
function statusPolicyPartIsValid(part) {
|
|
1189
|
+
const trimmed = part.trim();
|
|
1190
|
+
if (!trimmed)
|
|
1191
|
+
return false;
|
|
1192
|
+
const [startRaw, endRaw, extra] = trimmed.split("-");
|
|
1193
|
+
if (extra !== void 0 || !/^\d{3}$/.test(startRaw ?? ""))
|
|
1194
|
+
return false;
|
|
1195
|
+
const start = Number(startRaw);
|
|
1196
|
+
const end = endRaw === void 0 ? start : Number(endRaw);
|
|
1197
|
+
if (endRaw !== void 0 && !/^\d{3}$/.test(endRaw))
|
|
1198
|
+
return false;
|
|
1199
|
+
return start >= 100 && start <= 599 && end >= 100 && end <= 599 && start <= end;
|
|
1200
|
+
}
|
|
1201
|
+
function isRouteStatusCodePolicyString(value) {
|
|
1202
|
+
return value.split(",").every(statusPolicyPartIsValid);
|
|
1203
|
+
}
|
|
1204
|
+
var routeStatusCodePolicySchema = z18.union([
|
|
1205
|
+
z18.string().min(1).max(100).refine(isRouteStatusCodePolicyString, {
|
|
1206
|
+
message: "onStatusCodes must be comma-separated HTTP status codes or numeric ranges, e.g. 200-299,304"
|
|
1207
|
+
}),
|
|
1208
|
+
z18.array(z18.number().int().min(100).max(599)).min(1).max(100)
|
|
1209
|
+
]);
|
|
1210
|
+
var routeDefinitionSchema = z18.object({
|
|
1211
|
+
match: routeMatchSchema,
|
|
1212
|
+
metering: z18.object({
|
|
1213
|
+
defaults: z18.record(z18.string().min(1).max(64), z18.number().finite().nonnegative()).optional(),
|
|
1214
|
+
reports: z18.array(z18.string().min(1).max(64)).max(20).optional(),
|
|
1215
|
+
estimates: z18.record(z18.string().min(1).max(64), z18.number().finite().nonnegative()).optional(),
|
|
1216
|
+
onStatusCodes: routeStatusCodePolicySchema.optional()
|
|
1217
|
+
}).optional(),
|
|
1218
|
+
unmetered: z18.boolean().optional(),
|
|
1219
|
+
inheritDefaultMeters: z18.boolean().optional(),
|
|
1220
|
+
/** Optional explicit action id. When absent, the compiler derives an
|
|
1221
|
+
* implicit action from feature + method + path. */
|
|
1222
|
+
action: z18.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/).optional(),
|
|
1223
|
+
/** BYO-Backend V1 — optional route→backend binding. Omitted = the sole /
|
|
1224
|
+
* default backend (single-backend products stay zero-config). The schema
|
|
1225
|
+
* is `.strict()`, so this key MUST be declared here or `parse` throws on
|
|
1226
|
+
* it (anti-`.strict()`). */
|
|
1227
|
+
backend: routeBackendBindingSchema.optional()
|
|
1228
|
+
}).strict();
|
|
1229
|
+
var routeUpstreamSchema = z18.object({
|
|
1230
|
+
override_origin: z18.string().url("override_origin must be a valid URL").nullable().default(null)
|
|
1231
|
+
});
|
|
1232
|
+
var routeRuntimeSchema = z18.object({
|
|
1233
|
+
rollout_key: z18.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "rollout_key must be lowercase alphanumeric with hyphens/underscores").optional(),
|
|
1234
|
+
/**
|
|
1235
|
+
* Optional runtime flags this feature depends on. The runtime
|
|
1236
|
+
* evaluator AND's the feature's enablement across all referenced
|
|
1237
|
+
* flags. If any flag is disabled, the route returns the configured
|
|
1238
|
+
* fallback (404 by default — see /runtime failure matrix).
|
|
1239
|
+
*/
|
|
1240
|
+
required_flags: z18.array(z18.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(10).optional()
|
|
917
1241
|
});
|
|
1242
|
+
var routeLayerSchema = z18.object({
|
|
1243
|
+
/**
|
|
1244
|
+
* Feature key — the entitlement unit. Surfaced in dashboards,
|
|
1245
|
+
* subscriptions, and the gateway's matched-route trace.
|
|
1246
|
+
*/
|
|
1247
|
+
feature: z18.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/, "feature key must be lowercase alphanumeric with [_.:-]"),
|
|
1248
|
+
description: z18.string().max(500).optional(),
|
|
1249
|
+
/**
|
|
1250
|
+
* Route additions are contractual by default — they expose new API
|
|
1251
|
+
* surface to subscribers. Internal/non-customer-visible routes can
|
|
1252
|
+
* mark themselves `runtime` to allow autonomous agent flips
|
|
1253
|
+
* (invariant #16; see RFC approval matrix).
|
|
1254
|
+
*/
|
|
1255
|
+
mutation_class: z18.enum(["runtime", "contractual"]).default("contractual"),
|
|
1256
|
+
cacheProfile: cacheProfileSchema,
|
|
1257
|
+
routes: z18.array(routeDefinitionSchema).min(1).max(50),
|
|
1258
|
+
upstream: routeUpstreamSchema.default({ override_origin: null }),
|
|
1259
|
+
/**
|
|
1260
|
+
* Ordered list of policy names to apply. Executed sequentially by
|
|
1261
|
+
* the gateway policy engine; first-deny wins. Referenced policies
|
|
1262
|
+
* MUST declare compatible `compatible_with` envelopes for this
|
|
1263
|
+
* feature's route/meter shape — the compiler enforces.
|
|
1264
|
+
*/
|
|
1265
|
+
policies: z18.array(z18.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
|
|
1266
|
+
runtime: routeRuntimeSchema.default({}),
|
|
1267
|
+
/**
|
|
1268
|
+
* Plans that grant this feature directly. Shared feature bundles are
|
|
1269
|
+
* expressed in capability layers via `includes_features`; route layers do
|
|
1270
|
+
* not declare capability membership.
|
|
1271
|
+
*/
|
|
1272
|
+
plans: z18.array(z18.string().min(1).max(64)).max(20).default([]),
|
|
1273
|
+
/** Explicit actions declared by this feature. Routes reference them by
|
|
1274
|
+
* `route.action`; routes without a binding receive implicit actions. */
|
|
1275
|
+
actions: z18.array(actionSpecSchema).max(100).optional(),
|
|
1276
|
+
/** BYO-Backend V1 — feature-level default backend binding. Routes in this
|
|
1277
|
+
* layer with no explicit `route.backend` inherit this; routes may still
|
|
1278
|
+
* override per-route. Omitted = the product's sole / default backend. */
|
|
1279
|
+
backend: routeBackendBindingSchema.optional()
|
|
1280
|
+
}).strict();
|
|
918
1281
|
|
|
919
1282
|
// ../contracts/dist/plans/spec/refinements.js
|
|
920
1283
|
function rejectUsagePricing(spec, ctx) {
|
|
@@ -1158,6 +1521,68 @@ function validateLimitMeterReachability(spec, ctx) {
|
|
|
1158
1521
|
});
|
|
1159
1522
|
});
|
|
1160
1523
|
}
|
|
1524
|
+
function validateBackendReferences(spec, ctx) {
|
|
1525
|
+
const backends = spec.backend;
|
|
1526
|
+
if (!backends || Object.keys(backends).length === 0)
|
|
1527
|
+
return;
|
|
1528
|
+
const backendIds = new Set(Object.keys(backends));
|
|
1529
|
+
const productMeterKeys = new Set((spec.metering?.meters ?? []).map((m) => m.key).filter((k) => typeof k === "string" && k.length > 0));
|
|
1530
|
+
for (const [backendId, backend] of Object.entries(backends)) {
|
|
1531
|
+
(backend.meters ?? []).forEach((meter, meterIdx) => {
|
|
1532
|
+
if (productMeterKeys.has(meter))
|
|
1533
|
+
return;
|
|
1534
|
+
ctx.addIssue({
|
|
1535
|
+
code: "custom",
|
|
1536
|
+
path: ["backend", backendId, "meters", meterIdx],
|
|
1537
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.unknownMeterInBackend}: backend "${backendId}" allows meter "${meter}" but no metering.meters[] entry declares it.`
|
|
1538
|
+
});
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
const { defaultId, ambiguous } = resolveDefaultBackendId(backends);
|
|
1542
|
+
let anyRouteOmitsBinding = false;
|
|
1543
|
+
for (const feature of Object.values(spec.features ?? {})) {
|
|
1544
|
+
for (const route of feature.routes ?? []) {
|
|
1545
|
+
if (route.backend === void 0)
|
|
1546
|
+
anyRouteOmitsBinding = true;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (ambiguous && anyRouteOmitsBinding) {
|
|
1550
|
+
ctx.addIssue({
|
|
1551
|
+
code: "custom",
|
|
1552
|
+
path: ["backend"],
|
|
1553
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.ambiguousDefaultBackend}: product declares ${backendIds.size} backends but no single default \u2014 mark exactly one backend \`default: true\` or bind every route explicitly.`
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
for (const [featureKey, feature] of Object.entries(spec.features ?? {})) {
|
|
1557
|
+
(feature.routes ?? []).forEach((route, routeIdx) => {
|
|
1558
|
+
const boundId = route.backend ?? defaultId;
|
|
1559
|
+
if (route.backend !== void 0 && !backendIds.has(route.backend)) {
|
|
1560
|
+
ctx.addIssue({
|
|
1561
|
+
code: "custom",
|
|
1562
|
+
path: ["features", featureKey, "routes", routeIdx, "backend"],
|
|
1563
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.unknownBackendInRoute}: route binds backend "${route.backend}" which is not declared in the product \`backend\` block.`
|
|
1564
|
+
});
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (!boundId)
|
|
1568
|
+
return;
|
|
1569
|
+
const backend = backends[boundId];
|
|
1570
|
+
const allow = backend?.meters;
|
|
1571
|
+
if (!allow)
|
|
1572
|
+
return;
|
|
1573
|
+
const allowed = new Set(allow);
|
|
1574
|
+
for (const meter of routeMeterKeys(route)) {
|
|
1575
|
+
if (allowed.has(meter))
|
|
1576
|
+
continue;
|
|
1577
|
+
ctx.addIssue({
|
|
1578
|
+
code: "custom",
|
|
1579
|
+
path: ["features", featureKey, "routes", routeIdx, "metering"],
|
|
1580
|
+
message: `${BACKEND_DIAGNOSTIC_CODES.routeMeterNotAllowedByBackend}: route meters "${meter}" but backend "${boundId}" does not allow it (backend.meters[] = [${allow.join(", ")}]).`
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1161
1586
|
function planMonthlyPrice(plan) {
|
|
1162
1587
|
return plan.recurring_fee_cents;
|
|
1163
1588
|
}
|
|
@@ -1167,18 +1592,18 @@ function planPriceKey(plan, monthly) {
|
|
|
1167
1592
|
}
|
|
1168
1593
|
|
|
1169
1594
|
// ../contracts/dist/plans/spec/product.js
|
|
1170
|
-
var productIdentitySchema =
|
|
1171
|
-
subdomain:
|
|
1595
|
+
var productIdentitySchema = z19.object({
|
|
1596
|
+
subdomain: z19.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "Subdomain must be lowercase alphanumeric with optional hyphens")
|
|
1172
1597
|
});
|
|
1173
|
-
var meterEnforcementTypeSchema =
|
|
1598
|
+
var meterEnforcementTypeSchema = z19.enum([
|
|
1174
1599
|
"exact_pre_request",
|
|
1175
1600
|
"estimated_then_settled",
|
|
1176
1601
|
"postpaid",
|
|
1177
1602
|
"strict_concurrency"
|
|
1178
1603
|
]);
|
|
1179
|
-
var meterDefinitionSchema =
|
|
1180
|
-
key:
|
|
1181
|
-
display:
|
|
1604
|
+
var meterDefinitionSchema = z19.object({
|
|
1605
|
+
key: z19.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Meter key must be lowercase alphanumeric with underscores"),
|
|
1606
|
+
display: z19.string().min(1).max(100),
|
|
1182
1607
|
// v0.42.0 — `type: "built-in" | "custom"` removed. The runtime never
|
|
1183
1608
|
// read it (gateway estimators key on meter NAME, Stripe meter
|
|
1184
1609
|
// creation keys on meter KEY); it was schema documentation. Wizard
|
|
@@ -1186,11 +1611,11 @@ var meterDefinitionSchema = z13.object({
|
|
|
1186
1611
|
// ai_usage → `dollars`); Custom template lets builders define keys
|
|
1187
1612
|
// freely. Old specs with `type: ...` parse cleanly because Zod
|
|
1188
1613
|
// strips unknown fields by default.
|
|
1189
|
-
unit:
|
|
1614
|
+
unit: z19.string().max(20).optional(),
|
|
1190
1615
|
/** Reusable pre-request estimate for routes that dynamically report this meter. */
|
|
1191
|
-
estimate:
|
|
1616
|
+
estimate: z19.number().finite().nonnegative().optional(),
|
|
1192
1617
|
/** Fixed per-request default applied by Product SDK helpers. */
|
|
1193
|
-
routeDefault:
|
|
1618
|
+
routeDefault: z19.number().finite().nonnegative().optional(),
|
|
1194
1619
|
/**
|
|
1195
1620
|
* Runtime enforcement semantics for this meter. This is compiled into
|
|
1196
1621
|
* signed gateway artifacts so the edge chooses reservation, settlement,
|
|
@@ -1215,7 +1640,7 @@ var meterDefinitionSchema = z13.object({
|
|
|
1215
1640
|
* current `window`. Defaults to `COUNT` (one event = one unit) so
|
|
1216
1641
|
* existing meters that didn't declare aggregation continue to work.
|
|
1217
1642
|
*/
|
|
1218
|
-
aggregation:
|
|
1643
|
+
aggregation: z19.enum(["SUM", "COUNT", "MAX", "UNIQUE_COUNT", "LATEST"]).default("COUNT"),
|
|
1219
1644
|
/**
|
|
1220
1645
|
* Aggregation window. `billing_period` (the default) makes the
|
|
1221
1646
|
* meter accumulate across the subscription's billing period.
|
|
@@ -1223,91 +1648,65 @@ var meterDefinitionSchema = z13.object({
|
|
|
1223
1648
|
* rate-limit-shaped meters where the period boundary is a fixed
|
|
1224
1649
|
* wall-clock interval, not the subscription anniversary.
|
|
1225
1650
|
*/
|
|
1226
|
-
window:
|
|
1651
|
+
window: z19.enum(["minute", "hour", "day", "month", "billing_period"]).default("billing_period"),
|
|
1227
1652
|
/**
|
|
1228
1653
|
* Property on the event payload to read for `SUM` and `MAX`
|
|
1229
1654
|
* aggregations. Optional at the schema level; core's `validate.ts`
|
|
1230
1655
|
* (Phase 1b) enforces "required when aggregation is SUM or MAX".
|
|
1231
1656
|
*/
|
|
1232
|
-
valueProperty:
|
|
1657
|
+
valueProperty: z19.string().optional(),
|
|
1233
1658
|
/**
|
|
1234
1659
|
* Property on the event payload to dedupe on for `UNIQUE_COUNT`
|
|
1235
1660
|
* aggregation. Optional at the schema level; core's validate-pass
|
|
1236
1661
|
* enforces "required when aggregation is UNIQUE_COUNT".
|
|
1237
1662
|
*/
|
|
1238
|
-
uniqueProperty:
|
|
1663
|
+
uniqueProperty: z19.string().optional(),
|
|
1239
1664
|
/**
|
|
1240
1665
|
* Optional grouping dimensions. When set, the aggregation is per
|
|
1241
1666
|
* unique combination of these properties' values, not a single
|
|
1242
1667
|
* scalar. Used for per-region or per-model breakdowns.
|
|
1243
1668
|
*/
|
|
1244
|
-
groupBy:
|
|
1669
|
+
groupBy: z19.array(z19.string()).optional(),
|
|
1245
1670
|
/**
|
|
1246
1671
|
* Lago-side event code for ingress correlation. Matches Lago's
|
|
1247
1672
|
* BillableMetric `code` so events sent to Lago land on the right
|
|
1248
1673
|
* meter without a per-meter translation table.
|
|
1249
1674
|
*/
|
|
1250
|
-
eventCode:
|
|
1251
|
-
});
|
|
1252
|
-
var usageMeasureSchema = z13.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Usage measure must be lowercase alphanumeric with underscores");
|
|
1253
|
-
var usageRatingPricePolicySchema = z13.enum([
|
|
1254
|
-
"pass_through",
|
|
1255
|
-
"markup",
|
|
1256
|
-
"fixed_margin",
|
|
1257
|
-
"customer_rate"
|
|
1258
|
-
]);
|
|
1259
|
-
var fixedRatingSchema = z13.object({
|
|
1260
|
-
source: z13.literal("fixed"),
|
|
1261
|
-
rates: z13.record(z13.string().min(1), z13.record(usageMeasureSchema, z13.number().int().nonnegative()))
|
|
1262
|
-
});
|
|
1263
|
-
var providerCatalogRatingSchema = z13.object({
|
|
1264
|
-
source: z13.literal("provider_catalog"),
|
|
1265
|
-
catalog: z13.string().min(1).max(100),
|
|
1266
|
-
pricePolicy: usageRatingPricePolicySchema.default("pass_through"),
|
|
1267
|
-
markupPercent: z13.number().nonnegative().max(1e4).optional(),
|
|
1268
|
-
marginMicros: z13.number().int().nonnegative().optional()
|
|
1269
|
-
});
|
|
1270
|
-
var upstreamReportedRatingSchema = z13.object({
|
|
1271
|
-
source: z13.literal("upstream_reported"),
|
|
1272
|
-
amountField: z13.string().min(1).max(500),
|
|
1273
|
-
currencyField: z13.string().min(1).max(500).optional()
|
|
1675
|
+
eventCode: z19.string().optional()
|
|
1274
1676
|
});
|
|
1275
|
-
var
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1677
|
+
var usageMeasureSchema = z19.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Usage measure must be lowercase alphanumeric with underscores");
|
|
1678
|
+
var usageRatingPricePolicySchema = z19.enum(["pass_through", "markup"]);
|
|
1679
|
+
var fixedRatingSchema = z19.object({
|
|
1680
|
+
source: z19.literal("fixed"),
|
|
1681
|
+
rates: z19.record(z19.string().min(1), z19.record(usageMeasureSchema, z19.number().int().nonnegative()))
|
|
1279
1682
|
});
|
|
1280
|
-
var
|
|
1281
|
-
source:
|
|
1282
|
-
|
|
1283
|
-
|
|
1683
|
+
var providerCatalogRatingSchema = z19.object({
|
|
1684
|
+
source: z19.literal("provider_catalog"),
|
|
1685
|
+
catalog: z19.string().min(1).max(100),
|
|
1686
|
+
pricePolicy: usageRatingPricePolicySchema.default("pass_through"),
|
|
1687
|
+
markupPercent: z19.number().nonnegative().max(1e4).optional(),
|
|
1688
|
+
marginMicros: z19.number().int().nonnegative().optional()
|
|
1284
1689
|
});
|
|
1285
|
-
var usageRatingSchema =
|
|
1690
|
+
var usageRatingSchema = z19.discriminatedUnion("source", [
|
|
1286
1691
|
fixedRatingSchema,
|
|
1287
|
-
providerCatalogRatingSchema
|
|
1288
|
-
upstreamReportedRatingSchema,
|
|
1289
|
-
externalRateApiRatingSchema,
|
|
1290
|
-
customRatingSchema
|
|
1692
|
+
providerCatalogRatingSchema
|
|
1291
1693
|
]);
|
|
1292
|
-
var usageMeterSchema =
|
|
1293
|
-
selector:
|
|
1294
|
-
measures:
|
|
1694
|
+
var usageMeterSchema = z19.object({
|
|
1695
|
+
selector: z19.string().min(1).max(100).optional(),
|
|
1696
|
+
measures: z19.array(usageMeasureSchema).min(1).max(20),
|
|
1295
1697
|
rating: usageRatingSchema.optional()
|
|
1296
1698
|
});
|
|
1297
|
-
var usageBlockSchema =
|
|
1298
|
-
meters:
|
|
1699
|
+
var usageBlockSchema = z19.object({
|
|
1700
|
+
meters: z19.record(z19.string().min(1).max(64).regex(/^[a-z0-9_]+$/), usageMeterSchema)
|
|
1299
1701
|
});
|
|
1300
|
-
var routeMeteringSchema =
|
|
1301
|
-
defaults:
|
|
1302
|
-
reports:
|
|
1303
|
-
estimates:
|
|
1304
|
-
onStatusCodes:
|
|
1305
|
-
z13.string().min(1).max(100),
|
|
1306
|
-
z13.array(z13.number().int().min(100).max(599)).min(1).max(100)
|
|
1307
|
-
]).optional()
|
|
1702
|
+
var routeMeteringSchema = z19.object({
|
|
1703
|
+
defaults: z19.record(z19.string().min(1).max(64), z19.number().finite().nonnegative()).optional(),
|
|
1704
|
+
reports: z19.array(z19.string().min(1).max(64)).max(20).optional(),
|
|
1705
|
+
estimates: z19.record(z19.string().min(1).max(64), z19.number().finite().nonnegative()).optional(),
|
|
1706
|
+
onStatusCodes: routeStatusCodePolicySchema.optional()
|
|
1308
1707
|
});
|
|
1309
|
-
var featureRouteSchema =
|
|
1310
|
-
method:
|
|
1708
|
+
var featureRouteSchema = z19.object({
|
|
1709
|
+
method: z19.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
|
|
1311
1710
|
// Path is the route under the product's baseUrl. OpenAPI parameter
|
|
1312
1711
|
// syntax is supported and translated by the compiler:
|
|
1313
1712
|
// /users/:id → /users/*
|
|
@@ -1315,44 +1714,48 @@ var featureRouteSchema = z13.object({
|
|
|
1315
1714
|
// Path-globs `*` (one segment) and `**` (any subpath) are passed
|
|
1316
1715
|
// through. The compiler rejects ambiguous compound segments like
|
|
1317
1716
|
// `/foo/:a-:b` — parameter names must occupy whole segments.
|
|
1318
|
-
path:
|
|
1717
|
+
path: z19.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]"),
|
|
1319
1718
|
// Explicit no-usage route. Dynamic/static route metering is declared
|
|
1320
1719
|
// exclusively under `metering`.
|
|
1321
|
-
unmetered:
|
|
1720
|
+
unmetered: z19.boolean().optional(),
|
|
1322
1721
|
metering: routeMeteringSchema.optional(),
|
|
1323
|
-
inheritDefaultMeters:
|
|
1722
|
+
inheritDefaultMeters: z19.boolean().optional(),
|
|
1723
|
+
// BYO-Backend V1 — route→backend binding. The compiler materializes
|
|
1724
|
+
// per-feature route layers into this strict route shape, so the key MUST be
|
|
1725
|
+
// declared here or `parse` throws on it (anti-`.strict()`).
|
|
1726
|
+
backend: routeBackendBindingSchema.optional()
|
|
1324
1727
|
}).strict();
|
|
1325
|
-
var featureCatalogEntrySchema =
|
|
1728
|
+
var featureCatalogEntrySchema = z19.object({
|
|
1326
1729
|
// Optional human-friendly summary; surfaced in dashboards / settings UI.
|
|
1327
|
-
description:
|
|
1328
|
-
routes:
|
|
1730
|
+
description: z19.string().max(500).optional(),
|
|
1731
|
+
routes: z19.array(featureRouteSchema).min(1).max(50),
|
|
1329
1732
|
// Plans that grant this feature. Feature-first canonical mapping —
|
|
1330
1733
|
// builders declare "which plans get this feature" on the feature
|
|
1331
1734
|
// itself rather than enumerating features per plan. Required and
|
|
1332
1735
|
// non-empty: a feature with no plans grants nothing and is a likely
|
|
1333
1736
|
// typo. Cross-reference validation (every key resolves to an
|
|
1334
1737
|
// existing plan) lives in `validateFeatureReferences` below.
|
|
1335
|
-
plans:
|
|
1738
|
+
plans: z19.array(z19.string().min(1)).min(1).max(20)
|
|
1336
1739
|
});
|
|
1337
|
-
var featureCatalogSchema =
|
|
1338
|
-
var productCleanupPolicyModeSchema =
|
|
1740
|
+
var featureCatalogSchema = z19.record(z19.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
|
|
1741
|
+
var productCleanupPolicyModeSchema = z19.enum([
|
|
1339
1742
|
"report",
|
|
1340
1743
|
"pull_request"
|
|
1341
1744
|
]);
|
|
1342
|
-
var productChangeApprovalRiskSchema =
|
|
1745
|
+
var productChangeApprovalRiskSchema = z19.enum([
|
|
1343
1746
|
"safe",
|
|
1344
1747
|
"non_blocking",
|
|
1345
1748
|
"economic_risk",
|
|
1346
1749
|
"blocking"
|
|
1347
1750
|
]);
|
|
1348
|
-
var productOperatorPoliciesSchema =
|
|
1751
|
+
var productOperatorPoliciesSchema = z19.object({
|
|
1349
1752
|
/**
|
|
1350
1753
|
* Route cleanup operator. Disabled by default; report-mode is the safe
|
|
1351
1754
|
* default so a product can surface zero-traffic runtime-route candidates
|
|
1352
1755
|
* before it opts into draft PR mutation.
|
|
1353
1756
|
*/
|
|
1354
|
-
cleanup:
|
|
1355
|
-
enabled:
|
|
1757
|
+
cleanup: z19.object({
|
|
1758
|
+
enabled: z19.boolean().default(false),
|
|
1356
1759
|
mode: productCleanupPolicyModeSchema.default("report")
|
|
1357
1760
|
}).default({ enabled: false, mode: "report" }),
|
|
1358
1761
|
/**
|
|
@@ -1360,9 +1763,9 @@ var productOperatorPoliciesSchema = z13.object({
|
|
|
1360
1763
|
* the policy a first-class product-as-code field even while enforcement is
|
|
1361
1764
|
* still report/label-only.
|
|
1362
1765
|
*/
|
|
1363
|
-
change_approval:
|
|
1364
|
-
auto_merge_max_risk:
|
|
1365
|
-
require_human_for:
|
|
1766
|
+
change_approval: z19.object({
|
|
1767
|
+
auto_merge_max_risk: z19.enum(["none", "safe", "non_blocking"]).default("none"),
|
|
1768
|
+
require_human_for: z19.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
|
|
1366
1769
|
}).default({
|
|
1367
1770
|
auto_merge_max_risk: "none",
|
|
1368
1771
|
require_human_for: ["economic_risk", "blocking"]
|
|
@@ -1374,15 +1777,15 @@ var productOperatorPoliciesSchema = z13.object({
|
|
|
1374
1777
|
require_human_for: ["economic_risk", "blocking"]
|
|
1375
1778
|
}
|
|
1376
1779
|
});
|
|
1377
|
-
var customerIdentityRequirementSchema =
|
|
1780
|
+
var customerIdentityRequirementSchema = z19.enum([
|
|
1378
1781
|
"org_only",
|
|
1379
1782
|
"org_and_user"
|
|
1380
1783
|
]);
|
|
1381
|
-
var customerPortalAuthStrategySchema =
|
|
1784
|
+
var customerPortalAuthStrategySchema = z19.enum([
|
|
1382
1785
|
"clerk",
|
|
1383
1786
|
"test-personas"
|
|
1384
1787
|
]);
|
|
1385
|
-
var productCustomerContextSchema =
|
|
1788
|
+
var productCustomerContextSchema = z19.object({
|
|
1386
1789
|
/**
|
|
1387
1790
|
* Edge credential identity policy. This is intentionally Product-scoped:
|
|
1388
1791
|
* B7 keeps Product as the product boundary and avoids customer-side
|
|
@@ -1394,19 +1797,19 @@ var productCustomerContextSchema = z13.object({
|
|
|
1394
1797
|
* runtime signing secret in product/product.config.ts. Core generates/preserves the
|
|
1395
1798
|
* secret when this is true and clears it when explicitly false.
|
|
1396
1799
|
*/
|
|
1397
|
-
context_tokens:
|
|
1398
|
-
enabled:
|
|
1800
|
+
context_tokens: z19.object({
|
|
1801
|
+
enabled: z19.boolean().default(true)
|
|
1399
1802
|
}).optional(),
|
|
1400
1803
|
/**
|
|
1401
1804
|
* Portal auth strategy for environment-scoped product applies. Production
|
|
1402
1805
|
* portal auth is provisioner-owned; preview/test environments can opt into
|
|
1403
1806
|
* test personas through Product-as-Code.
|
|
1404
1807
|
*/
|
|
1405
|
-
portal_auth:
|
|
1808
|
+
portal_auth: z19.object({
|
|
1406
1809
|
strategy: customerPortalAuthStrategySchema
|
|
1407
1810
|
}).optional()
|
|
1408
1811
|
});
|
|
1409
|
-
var productSurfaceTypeSchema =
|
|
1812
|
+
var productSurfaceTypeSchema = z19.enum([
|
|
1410
1813
|
"frontend",
|
|
1411
1814
|
"api",
|
|
1412
1815
|
"docs",
|
|
@@ -1416,87 +1819,99 @@ var productSurfaceTypeSchema = z13.enum([
|
|
|
1416
1819
|
"worker",
|
|
1417
1820
|
"agent"
|
|
1418
1821
|
]);
|
|
1419
|
-
var productSurfaceSchema =
|
|
1420
|
-
key:
|
|
1822
|
+
var productSurfaceSchema = z19.object({
|
|
1823
|
+
key: z19.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Surface key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1421
1824
|
type: productSurfaceTypeSchema,
|
|
1422
|
-
display:
|
|
1423
|
-
description:
|
|
1825
|
+
display: z19.string().min(1).max(100).optional(),
|
|
1826
|
+
description: z19.string().max(500).optional()
|
|
1424
1827
|
});
|
|
1425
|
-
var productEntitlementSchema =
|
|
1426
|
-
key:
|
|
1427
|
-
description:
|
|
1428
|
-
capabilities:
|
|
1429
|
-
featureGates:
|
|
1430
|
-
limits:
|
|
1431
|
-
meters:
|
|
1432
|
-
});
|
|
1433
|
-
var productSurfacesSchema =
|
|
1434
|
-
var productEntitlementsSchema =
|
|
1435
|
-
var productWorkflowKindSchema =
|
|
1828
|
+
var productEntitlementSchema = z19.object({
|
|
1829
|
+
key: z19.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Entitlement key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1830
|
+
description: z19.string().max(500).optional(),
|
|
1831
|
+
capabilities: z19.array(z19.string().min(1).max(100)).max(100).optional(),
|
|
1832
|
+
featureGates: z19.record(z19.string().min(1), z19.boolean()).optional(),
|
|
1833
|
+
limits: z19.array(planLimitRuleSchema).max(100).optional(),
|
|
1834
|
+
meters: z19.array(z19.string().min(1).max(64)).max(100).optional()
|
|
1835
|
+
});
|
|
1836
|
+
var productSurfacesSchema = z19.array(productSurfaceSchema).max(20).default([]);
|
|
1837
|
+
var productEntitlementsSchema = z19.array(productEntitlementSchema).max(100).default([]);
|
|
1838
|
+
var productWorkflowKindSchema = z19.enum([
|
|
1436
1839
|
"async_job",
|
|
1437
1840
|
"agent_task",
|
|
1438
1841
|
"scheduled",
|
|
1439
1842
|
"lifecycle",
|
|
1440
1843
|
"background"
|
|
1441
1844
|
]);
|
|
1442
|
-
var productWorkflowTriggerSchema =
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
type:
|
|
1446
|
-
cron:
|
|
1845
|
+
var productWorkflowTriggerSchema = z19.discriminatedUnion("type", [
|
|
1846
|
+
z19.object({ type: z19.literal("manual") }),
|
|
1847
|
+
z19.object({
|
|
1848
|
+
type: z19.literal("schedule"),
|
|
1849
|
+
cron: z19.string().min(1).max(120)
|
|
1447
1850
|
}),
|
|
1448
|
-
|
|
1449
|
-
type:
|
|
1450
|
-
event:
|
|
1851
|
+
z19.object({
|
|
1852
|
+
type: z19.literal("event"),
|
|
1853
|
+
event: z19.string().min(1).max(120)
|
|
1451
1854
|
}),
|
|
1452
|
-
|
|
1453
|
-
type:
|
|
1454
|
-
path:
|
|
1855
|
+
z19.object({
|
|
1856
|
+
type: z19.literal("api"),
|
|
1857
|
+
path: z19.string().min(1).max(240).regex(/^\//, "path must start with /")
|
|
1455
1858
|
}),
|
|
1456
|
-
|
|
1457
|
-
type:
|
|
1458
|
-
event:
|
|
1859
|
+
z19.object({
|
|
1860
|
+
type: z19.literal("lifecycle"),
|
|
1861
|
+
event: z19.string().min(1).max(120)
|
|
1459
1862
|
})
|
|
1460
1863
|
]);
|
|
1461
|
-
var productWorkflowSchema =
|
|
1462
|
-
key:
|
|
1463
|
-
title:
|
|
1464
|
-
description:
|
|
1864
|
+
var productWorkflowSchema = z19.object({
|
|
1865
|
+
key: z19.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Workflow key must be lowercase alphanumeric with hyphens/underscores"),
|
|
1866
|
+
title: z19.string().min(1).max(120).optional(),
|
|
1867
|
+
description: z19.string().max(1e3).optional(),
|
|
1465
1868
|
kind: productWorkflowKindSchema,
|
|
1466
1869
|
trigger: productWorkflowTriggerSchema,
|
|
1467
|
-
capabilities:
|
|
1468
|
-
meters:
|
|
1469
|
-
estimates:
|
|
1470
|
-
metadata:
|
|
1471
|
-
});
|
|
1472
|
-
var productWorkflowsSchema =
|
|
1473
|
-
var productSpecSchema =
|
|
1474
|
-
product:
|
|
1475
|
-
name:
|
|
1476
|
-
displayName:
|
|
1477
|
-
description:
|
|
1478
|
-
baseUrl:
|
|
1479
|
-
sandboxBaseUrl:
|
|
1480
|
-
visibility:
|
|
1870
|
+
capabilities: z19.array(z19.string().min(1).max(100)).max(100).optional(),
|
|
1871
|
+
meters: z19.array(z19.string().min(1).max(64)).max(100).optional(),
|
|
1872
|
+
estimates: z19.record(z19.string().min(1).max(64), z19.number().finite()).optional(),
|
|
1873
|
+
metadata: z19.record(z19.string().min(1), z19.unknown()).optional()
|
|
1874
|
+
});
|
|
1875
|
+
var productWorkflowsSchema = z19.array(productWorkflowSchema).max(100).default([]);
|
|
1876
|
+
var productSpecSchema = z19.object({
|
|
1877
|
+
product: z19.object({
|
|
1878
|
+
name: z19.string().min(1).max(100),
|
|
1879
|
+
displayName: z19.string().max(200).optional(),
|
|
1880
|
+
description: z19.string().max(2e3).optional(),
|
|
1881
|
+
baseUrl: z19.string().url("baseUrl must be a valid URL"),
|
|
1882
|
+
sandboxBaseUrl: z19.string().url("sandboxBaseUrl must be a valid URL").optional(),
|
|
1883
|
+
visibility: z19.enum(["public", "private"]).default("public"),
|
|
1481
1884
|
// Branding
|
|
1482
|
-
logoUrl:
|
|
1483
|
-
primaryColor:
|
|
1885
|
+
logoUrl: z19.string().url().optional(),
|
|
1886
|
+
primaryColor: z19.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
1484
1887
|
// Environment
|
|
1485
|
-
envBranchPrefix:
|
|
1888
|
+
envBranchPrefix: z19.string().max(50).nullable().optional()
|
|
1486
1889
|
}),
|
|
1487
|
-
gateway:
|
|
1488
|
-
authHeader:
|
|
1489
|
-
upstreamAuth:
|
|
1490
|
-
type:
|
|
1491
|
-
token:
|
|
1890
|
+
gateway: z19.object({
|
|
1891
|
+
authHeader: z19.string().min(1).max(100).default("x-api-key"),
|
|
1892
|
+
upstreamAuth: z19.object({
|
|
1893
|
+
type: z19.enum(["none", "static_bearer"]),
|
|
1894
|
+
token: z19.string().optional()
|
|
1492
1895
|
}).default({ type: "none" })
|
|
1493
1896
|
}),
|
|
1494
|
-
metering:
|
|
1495
|
-
meters:
|
|
1496
|
-
billOn4xx:
|
|
1897
|
+
metering: z19.object({
|
|
1898
|
+
meters: z19.array(meterDefinitionSchema).max(10).default([]),
|
|
1899
|
+
billOn4xx: z19.boolean().default(false)
|
|
1497
1900
|
}).default({ meters: [], billOn4xx: false }),
|
|
1901
|
+
/**
|
|
1902
|
+
* BYO-Backend V1 — first-class backend declarations, keyed by backend id.
|
|
1903
|
+
* A product may declare MULTIPLE backends; routes bind to one (a default
|
|
1904
|
+
* applies when a product has exactly one backend OR exactly one is marked
|
|
1905
|
+
* `default: true`). Single-backend products stay zero-config.
|
|
1906
|
+
*
|
|
1907
|
+
* OPTIONAL and emits NO key when absent — a product with no `backend`
|
|
1908
|
+
* block hashes byte-identically to the pre-BYOB world. Cross-reference
|
|
1909
|
+
* validation (route→backend resolution, route-meter ∈ backend `meters[]`,
|
|
1910
|
+
* unambiguous default) lives in `validateBackendReferences`.
|
|
1911
|
+
*/
|
|
1912
|
+
backend: productBackendBlockSchema.optional(),
|
|
1498
1913
|
usage: usageBlockSchema.optional(),
|
|
1499
|
-
usagePricing:
|
|
1914
|
+
usagePricing: z19.never({
|
|
1500
1915
|
error: "usagePricing is not supported. Define usage.meters.<key>.rating instead."
|
|
1501
1916
|
}).optional(),
|
|
1502
1917
|
features: featureCatalogSchema.optional(),
|
|
@@ -1517,53 +1932,51 @@ var productSpecSchema = z13.object({
|
|
|
1517
1932
|
// billing shape via the unified 5-knob spec (`meters`,
|
|
1518
1933
|
// `recurring_fee_cents`, `recurring_credit_grant_cents`,
|
|
1519
1934
|
// `one_time_credit_grant_cents`, `trial_days`,
|
|
1520
|
-
// `max_monthly_spend_cents`). The product-level `billing` block
|
|
1521
|
-
//
|
|
1522
|
-
// `
|
|
1523
|
-
|
|
1524
|
-
|
|
1935
|
+
// `max_monthly_spend_cents`). The product-level `billing` block retains
|
|
1936
|
+
// only the live transition-policy fields. v(refactor) — the inert
|
|
1937
|
+
// `gracePeriodDays` knob was deleted (the compiler hardcoded it and
|
|
1938
|
+
// Stripe Smart Retries owns dunning); the block now carries the
|
|
1939
|
+
// subscriber-change timing policy + the limit-upgrade projection flag.
|
|
1940
|
+
billing: z19.object({
|
|
1525
1941
|
// When true (default), a plan limit INCREASE re-projects onto active
|
|
1526
1942
|
// subscribers immediately; a DECREASE always defers to period end.
|
|
1527
1943
|
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
1528
|
-
applyLimitUpgradesInstantly:
|
|
1944
|
+
applyLimitUpgradesInstantly: z19.boolean().optional(),
|
|
1529
1945
|
subscriberChangePolicy: subscriberChangePolicySchema.default({
|
|
1530
|
-
default: "
|
|
1531
|
-
proration: "none",
|
|
1946
|
+
default: "period_end",
|
|
1532
1947
|
when: {
|
|
1533
|
-
price_increase: "
|
|
1534
|
-
price_decrease: "
|
|
1535
|
-
feature_added: "
|
|
1536
|
-
feature_removed: "
|
|
1537
|
-
limit_increased: "
|
|
1538
|
-
limit_reduced: "
|
|
1539
|
-
credit_increased: "
|
|
1540
|
-
credit_reduced: "
|
|
1541
|
-
rating_changed: "
|
|
1948
|
+
price_increase: "period_end",
|
|
1949
|
+
price_decrease: "immediate",
|
|
1950
|
+
feature_added: "immediate",
|
|
1951
|
+
feature_removed: "period_end",
|
|
1952
|
+
limit_increased: "immediate",
|
|
1953
|
+
limit_reduced: "period_end",
|
|
1954
|
+
credit_increased: "immediate",
|
|
1955
|
+
credit_reduced: "period_end",
|
|
1956
|
+
rating_changed: "period_end"
|
|
1542
1957
|
},
|
|
1543
1958
|
allowImmediatePriceIncrease: false,
|
|
1544
1959
|
allowImmediateEntitlementReduction: false
|
|
1545
1960
|
})
|
|
1546
1961
|
}).default({
|
|
1547
|
-
gracePeriodDays: 3,
|
|
1548
1962
|
subscriberChangePolicy: {
|
|
1549
|
-
default: "
|
|
1550
|
-
proration: "none",
|
|
1963
|
+
default: "period_end",
|
|
1551
1964
|
when: {
|
|
1552
|
-
price_increase: "
|
|
1553
|
-
price_decrease: "
|
|
1554
|
-
feature_added: "
|
|
1555
|
-
feature_removed: "
|
|
1556
|
-
limit_increased: "
|
|
1557
|
-
limit_reduced: "
|
|
1558
|
-
credit_increased: "
|
|
1559
|
-
credit_reduced: "
|
|
1560
|
-
rating_changed: "
|
|
1965
|
+
price_increase: "period_end",
|
|
1966
|
+
price_decrease: "immediate",
|
|
1967
|
+
feature_added: "immediate",
|
|
1968
|
+
feature_removed: "period_end",
|
|
1969
|
+
limit_increased: "immediate",
|
|
1970
|
+
limit_reduced: "period_end",
|
|
1971
|
+
credit_increased: "immediate",
|
|
1972
|
+
credit_reduced: "period_end",
|
|
1973
|
+
rating_changed: "period_end"
|
|
1561
1974
|
},
|
|
1562
1975
|
allowImmediatePriceIncrease: false,
|
|
1563
1976
|
allowImmediateEntitlementReduction: false
|
|
1564
1977
|
}
|
|
1565
1978
|
}),
|
|
1566
|
-
plans:
|
|
1979
|
+
plans: z19.array(planSpecSchema).max(4).default([]),
|
|
1567
1980
|
/**
|
|
1568
1981
|
* Add-on catalog (v0.56+). Composable economic + entitlement
|
|
1569
1982
|
* overlays that subscribers can pile on top of their base plan.
|
|
@@ -1606,16 +2019,16 @@ var productSpecSchema = z13.object({
|
|
|
1606
2019
|
* require_deprecation_window_days: 90
|
|
1607
2020
|
* require_successor_route: true
|
|
1608
2021
|
*/
|
|
1609
|
-
lifecycle:
|
|
1610
|
-
breaking_changes:
|
|
2022
|
+
lifecycle: z19.object({
|
|
2023
|
+
breaking_changes: z19.object({
|
|
1611
2024
|
/** Minimum days a route must have been marked for removal
|
|
1612
2025
|
* (in main-branch YAML) before the publish gate will let
|
|
1613
2026
|
* it actually be removed. Set to 0 to disable. */
|
|
1614
|
-
require_deprecation_window_days:
|
|
2027
|
+
require_deprecation_window_days: z19.number().int().nonnegative().default(0),
|
|
1615
2028
|
/** When true, a route removal must declare a successor
|
|
1616
2029
|
* route via the lifecycle metadata (mechanics in core
|
|
1617
2030
|
* 3b-2) before the publish gate accepts it. */
|
|
1618
|
-
require_successor_route:
|
|
2031
|
+
require_successor_route: z19.boolean().default(false)
|
|
1619
2032
|
}).default({
|
|
1620
2033
|
require_deprecation_window_days: 0,
|
|
1621
2034
|
require_successor_route: false
|
|
@@ -1661,8 +2074,8 @@ var productSpecSchema = z13.object({
|
|
|
1661
2074
|
* (preserves today's behaviour). Compiler validation pins the
|
|
1662
2075
|
* value to a real `plans[].key`.
|
|
1663
2076
|
*/
|
|
1664
|
-
ephemeral:
|
|
1665
|
-
defaultPlan:
|
|
2077
|
+
ephemeral: z19.object({
|
|
2078
|
+
defaultPlan: z19.string().min(1).optional()
|
|
1666
2079
|
}).optional()
|
|
1667
2080
|
}).superRefine((spec, ctx) => {
|
|
1668
2081
|
rejectUsagePricing(spec, ctx);
|
|
@@ -1672,421 +2085,26 @@ var productSpecSchema = z13.object({
|
|
|
1672
2085
|
validateFeatureReferences(spec, ctx);
|
|
1673
2086
|
validateRouteMeters(spec, ctx);
|
|
1674
2087
|
validateLimitMeterReachability(spec, ctx);
|
|
2088
|
+
validateBackendReferences(spec, ctx);
|
|
1675
2089
|
});
|
|
1676
|
-
var productPhaseSchema =
|
|
2090
|
+
var productPhaseSchema = z19.object({
|
|
1677
2091
|
product: productSpecSchema.shape.product
|
|
1678
2092
|
});
|
|
1679
|
-
var gatewayPhaseSchema =
|
|
2093
|
+
var gatewayPhaseSchema = z19.object({
|
|
1680
2094
|
gateway: productSpecSchema.shape.gateway
|
|
1681
2095
|
});
|
|
1682
|
-
var meteringPhaseSchema =
|
|
2096
|
+
var meteringPhaseSchema = z19.object({
|
|
1683
2097
|
metering: productSpecSchema.shape.metering
|
|
1684
2098
|
});
|
|
1685
|
-
var plansPhaseSchema =
|
|
2099
|
+
var plansPhaseSchema = z19.object({
|
|
1686
2100
|
plans: productSpecSchema.shape.plans
|
|
1687
2101
|
});
|
|
1688
2102
|
|
|
1689
|
-
// ../contracts/dist/plans/spec/policy-types.js
|
|
1690
|
-
import { z as z14 } from "zod";
|
|
1691
|
-
var rateLimitWindowSchema = z14.string().min(2).max(20).regex(/^\d+(ms|s|m|h)$/, "rate_limit window must look like `60s`, `5m`, `1h`");
|
|
1692
|
-
var rateLimitConfigSchema = z14.object({
|
|
1693
|
-
strategy: z14.enum(["token_bucket", "sliding_window", "fixed_window"]).default("token_bucket"),
|
|
1694
|
-
/**
|
|
1695
|
-
* Which request dimensions identify the bucket. v0.3.0 supports a
|
|
1696
|
-
* fixed set; extending requires a coordinated gateway/policy-engine
|
|
1697
|
-
* change. The `subscription` dimension is the steady-state default;
|
|
1698
|
-
* `ip` is for unauthenticated probes; `credential` is finer-grained
|
|
1699
|
-
* than subscription (per-key throttling).
|
|
1700
|
-
*/
|
|
1701
|
-
dimensions: z14.array(z14.enum(["subscription", "credential", "ip", "route"])).min(1).max(4).default(["subscription"]),
|
|
1702
|
-
limits: z14.array(z14.object({
|
|
1703
|
-
window: rateLimitWindowSchema,
|
|
1704
|
-
max: z14.number().int().positive().max(1e7)
|
|
1705
|
-
})).min(1).max(10),
|
|
1706
|
-
/**
|
|
1707
|
-
* Bounded fail-open behaviour for DO outages. See architecture RFC
|
|
1708
|
-
* "Fail-open guardrails" section. When the policy executor observes
|
|
1709
|
-
* `max_consecutive_failures` DO-call failures within
|
|
1710
|
-
* `max_window_seconds`, it transitions to `degraded_mode` until
|
|
1711
|
-
* `recovery_threshold` consecutive successes restore normal
|
|
1712
|
-
* evaluation.
|
|
1713
|
-
*/
|
|
1714
|
-
fail_open: z14.object({
|
|
1715
|
-
max_consecutive_failures: z14.number().int().positive().default(100),
|
|
1716
|
-
max_window_seconds: z14.number().int().positive().default(60),
|
|
1717
|
-
recovery_threshold: z14.number().int().positive().default(50),
|
|
1718
|
-
degraded_mode: z14.enum([
|
|
1719
|
-
"safe_mode_block",
|
|
1720
|
-
"safe_mode_throttle",
|
|
1721
|
-
"runtime_killswitch_trigger"
|
|
1722
|
-
]).default("safe_mode_throttle")
|
|
1723
|
-
}).default({
|
|
1724
|
-
max_consecutive_failures: 100,
|
|
1725
|
-
max_window_seconds: 60,
|
|
1726
|
-
recovery_threshold: 50,
|
|
1727
|
-
degraded_mode: "safe_mode_throttle"
|
|
1728
|
-
})
|
|
1729
|
-
});
|
|
1730
|
-
var authConfigSchema = z14.object({
|
|
1731
|
-
header_name: z14.string().min(1).max(100).default("x-api-key"),
|
|
1732
|
-
/**
|
|
1733
|
-
* How the gateway constructs the upstream Authorization header:
|
|
1734
|
-
* - `none` → no upstream auth header added
|
|
1735
|
-
* - `static_bearer` → forward a configured static token
|
|
1736
|
-
* - `subscriber_jwt` → mint a per-subscriber JWT (out of scope v0.3.0)
|
|
1737
|
-
*/
|
|
1738
|
-
upstream_token_source: z14.discriminatedUnion("type", [
|
|
1739
|
-
z14.object({ type: z14.literal("none") }),
|
|
1740
|
-
z14.object({
|
|
1741
|
-
type: z14.literal("static_bearer"),
|
|
1742
|
-
token_secret_ref: z14.string().min(1).max(200).describe("Reference into the secret store (e.g. CF Secret name); not the raw token")
|
|
1743
|
-
})
|
|
1744
|
-
]).default({ type: "none" }),
|
|
1745
|
-
/**
|
|
1746
|
-
* When `true`, treat the inbound credential's scopes (if any) as
|
|
1747
|
-
* additional gating beyond the entitlement check. v0.3.0 ships with
|
|
1748
|
-
* `strict` as the default.
|
|
1749
|
-
*/
|
|
1750
|
-
scope_mode: z14.enum(["strict", "advisory", "off"]).default("strict")
|
|
1751
|
-
});
|
|
1752
|
-
var concurrencyConfigSchema = z14.object({
|
|
1753
|
-
max_in_flight: z14.number().int().positive().max(1e4),
|
|
1754
|
-
/**
|
|
1755
|
-
* Which dimensions key the lease bucket. Matches the existing
|
|
1756
|
-
* ConcurrencyLease DO `idFromName` pattern (subscription | capability
|
|
1757
|
-
* tuple).
|
|
1758
|
-
*/
|
|
1759
|
-
dimensions: z14.array(z14.enum(["subscription", "credential", "capability"])).min(1).max(3).default(["subscription"]),
|
|
1760
|
-
/**
|
|
1761
|
-
* Optional capability scope. When set, the lease bucket is keyed
|
|
1762
|
-
* partly by this capability name — separate buckets per capability.
|
|
1763
|
-
*/
|
|
1764
|
-
capability: z14.string().min(1).max(120).optional(),
|
|
1765
|
-
/**
|
|
1766
|
-
* Lease TTL — releases automatically after this many seconds even if
|
|
1767
|
-
* the request never returns (defensive default 30s, mirrors existing
|
|
1768
|
-
* ConcurrencyLease behaviour).
|
|
1769
|
-
*/
|
|
1770
|
-
lease_ttl_seconds: z14.number().int().positive().max(600).default(30),
|
|
1771
|
-
fail_open: z14.object({
|
|
1772
|
-
max_consecutive_failures: z14.number().int().positive().default(50),
|
|
1773
|
-
max_window_seconds: z14.number().int().positive().default(60),
|
|
1774
|
-
recovery_threshold: z14.number().int().positive().default(20),
|
|
1775
|
-
degraded_mode: z14.enum([
|
|
1776
|
-
"safe_mode_block",
|
|
1777
|
-
"safe_mode_throttle",
|
|
1778
|
-
"runtime_killswitch_trigger"
|
|
1779
|
-
]).default("safe_mode_throttle")
|
|
1780
|
-
}).default({
|
|
1781
|
-
max_consecutive_failures: 50,
|
|
1782
|
-
max_window_seconds: 60,
|
|
1783
|
-
recovery_threshold: 20,
|
|
1784
|
-
degraded_mode: "safe_mode_throttle"
|
|
1785
|
-
})
|
|
1786
|
-
});
|
|
1787
|
-
var retryConfigSchema = z14.object({
|
|
1788
|
-
max_attempts: z14.number().int().min(1).max(5).default(2),
|
|
1789
|
-
/**
|
|
1790
|
-
* HTTP status codes that trigger a retry. 5xx is the default; opt
|
|
1791
|
-
* into 429 retries only when the upstream understands `Retry-After`.
|
|
1792
|
-
*/
|
|
1793
|
-
retry_on_status: z14.array(z14.number().int().min(400).max(599)).min(1).max(20).default([502, 503, 504]),
|
|
1794
|
-
/**
|
|
1795
|
-
* Backoff curve. Total wall-clock attempt time is bounded so the
|
|
1796
|
-
* gateway worker cannot block past `total_budget_ms`.
|
|
1797
|
-
*/
|
|
1798
|
-
backoff: z14.object({
|
|
1799
|
-
initial_ms: z14.number().int().positive().max(5e3).default(100),
|
|
1800
|
-
multiplier: z14.number().positive().max(10).default(2),
|
|
1801
|
-
jitter: z14.enum(["none", "full", "equal"]).default("equal"),
|
|
1802
|
-
total_budget_ms: z14.number().int().positive().max(3e4).default(5e3)
|
|
1803
|
-
}).default({
|
|
1804
|
-
initial_ms: 100,
|
|
1805
|
-
multiplier: 2,
|
|
1806
|
-
jitter: "equal",
|
|
1807
|
-
total_budget_ms: 5e3
|
|
1808
|
-
})
|
|
1809
|
-
});
|
|
1810
|
-
var transformConfigSchema = z14.object({
|
|
1811
|
-
/**
|
|
1812
|
-
* When the transform applies. `request` runs before upstream forward;
|
|
1813
|
-
* `response` runs after. Most transforms are one or the other; both
|
|
1814
|
-
* is rare.
|
|
1815
|
-
*/
|
|
1816
|
-
applies_to: z14.enum(["request", "response", "both"]).default("request"),
|
|
1817
|
-
/**
|
|
1818
|
-
* List of key rewrites. Source path uses dot notation (`a.b.c`);
|
|
1819
|
-
* `target` may include the same syntax to move keys around. Drops
|
|
1820
|
-
* are expressed as `target: null`.
|
|
1821
|
-
*/
|
|
1822
|
-
rewrites: z14.array(z14.object({
|
|
1823
|
-
source: z14.string().min(1).max(200),
|
|
1824
|
-
target: z14.string().min(1).max(200).nullable()
|
|
1825
|
-
})).min(1).max(20)
|
|
1826
|
-
});
|
|
1827
|
-
var policyBodySchema = z14.discriminatedUnion("type", [
|
|
1828
|
-
z14.object({ type: z14.literal("rate_limit"), config: rateLimitConfigSchema }),
|
|
1829
|
-
z14.object({ type: z14.literal("auth"), config: authConfigSchema }),
|
|
1830
|
-
z14.object({ type: z14.literal("concurrency"), config: concurrencyConfigSchema }),
|
|
1831
|
-
z14.object({ type: z14.literal("retry"), config: retryConfigSchema }),
|
|
1832
|
-
z14.object({ type: z14.literal("transform"), config: transformConfigSchema })
|
|
1833
|
-
]);
|
|
1834
|
-
|
|
1835
|
-
// ../contracts/dist/plans/spec/policies-layer.js
|
|
1836
|
-
import { z as z15 } from "zod";
|
|
1837
|
-
var cacheProfileSchema = z15.enum(["long", "short", "blocking"]).default("long");
|
|
1838
|
-
var policyCompatibilitySchema = z15.object({
|
|
1839
|
-
route_types: z15.array(z15.enum(["http"])).max(5).optional(),
|
|
1840
|
-
meters: z15.array(z15.string().min(1).max(64)).max(20).optional(),
|
|
1841
|
-
auth_modes: z15.array(z15.enum(["api_key", "oauth2", "anonymous"])).max(5).optional()
|
|
1842
|
-
});
|
|
1843
|
-
var policyFileSchema = z15.intersection(z15.object({
|
|
1844
|
-
/**
|
|
1845
|
-
* Policy name. Referenced by routes via `policies: [<name>]`. Must
|
|
1846
|
-
* be unique across the product; the compiler enforces this in the
|
|
1847
|
-
* cross-file validation pass.
|
|
1848
|
-
*/
|
|
1849
|
-
name: z15.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Policy name must be lowercase alphanumeric with hyphens/underscores"),
|
|
1850
|
-
description: z15.string().max(500).optional(),
|
|
1851
|
-
compatible_with: policyCompatibilitySchema.default({}),
|
|
1852
|
-
/**
|
|
1853
|
-
* Mutation class — runtime vs contractual. Policies are operational
|
|
1854
|
-
* by nature so the default is `runtime`. Marking a policy as
|
|
1855
|
-
* `contractual` signals that changes to it require human approval
|
|
1856
|
-
* (invariant #16).
|
|
1857
|
-
*/
|
|
1858
|
-
mutation_class: z15.enum(["runtime", "contractual"]).default("runtime"),
|
|
1859
|
-
cacheProfile: cacheProfileSchema
|
|
1860
|
-
}), policyBodySchema);
|
|
1861
|
-
|
|
1862
|
-
// ../contracts/dist/plans/spec/routes-layer.js
|
|
1863
|
-
import { z as z17 } from "zod";
|
|
1864
|
-
|
|
1865
|
-
// ../contracts/dist/framework/actions/index.js
|
|
1866
|
-
import { z as z16 } from "zod";
|
|
1867
|
-
var actionKindSchema = z16.enum(["query", "mutation"]);
|
|
1868
|
-
var actionAuditPolicySchema = z16.enum(["none", "metadata", "full"]);
|
|
1869
|
-
var actionSubjectBindingSchema = z16.object({
|
|
1870
|
-
type: z16.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/),
|
|
1871
|
-
from: z16.enum(["header", "path_param"]),
|
|
1872
|
-
name: z16.string().min(1).max(120)
|
|
1873
|
-
});
|
|
1874
|
-
var actionResourceEffectSchema = z16.object({
|
|
1875
|
-
resource: z16.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/),
|
|
1876
|
-
effect: z16.enum(["create", "delete"])
|
|
1877
|
-
});
|
|
1878
|
-
var actionSpecSchema = z16.object({
|
|
1879
|
-
id: z16.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/),
|
|
1880
|
-
title: z16.string().min(1).max(160).optional(),
|
|
1881
|
-
kind: actionKindSchema,
|
|
1882
|
-
actorType: z16.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/).optional(),
|
|
1883
|
-
subject: actionSubjectBindingSchema.optional(),
|
|
1884
|
-
inputSchemaRef: z16.string().min(1).max(240).optional(),
|
|
1885
|
-
audit: actionAuditPolicySchema.default("metadata"),
|
|
1886
|
-
resource: actionResourceEffectSchema.optional()
|
|
1887
|
-
});
|
|
1888
|
-
|
|
1889
|
-
// ../contracts/dist/plans/spec/routes-layer.js
|
|
1890
|
-
var routeMatchSchema = z17.object({
|
|
1891
|
-
method: z17.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
|
|
1892
|
-
path: z17.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]")
|
|
1893
|
-
});
|
|
1894
|
-
var routeDefinitionSchema = z17.object({
|
|
1895
|
-
match: routeMatchSchema,
|
|
1896
|
-
metering: z17.object({
|
|
1897
|
-
defaults: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1898
|
-
reports: z17.array(z17.string().min(1).max(64)).max(20).optional(),
|
|
1899
|
-
estimates: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
|
|
1900
|
-
onStatusCodes: z17.union([
|
|
1901
|
-
z17.string().min(1).max(100),
|
|
1902
|
-
z17.array(z17.number().int().min(100).max(599)).min(1).max(100)
|
|
1903
|
-
]).optional()
|
|
1904
|
-
}).optional(),
|
|
1905
|
-
unmetered: z17.boolean().optional(),
|
|
1906
|
-
inheritDefaultMeters: z17.boolean().optional(),
|
|
1907
|
-
/** Optional explicit action id. When absent, the compiler derives an
|
|
1908
|
-
* implicit action from feature + method + path. */
|
|
1909
|
-
action: z17.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/).optional()
|
|
1910
|
-
}).strict();
|
|
1911
|
-
var routeUpstreamSchema = z17.object({
|
|
1912
|
-
override_origin: z17.string().url("override_origin must be a valid URL").nullable().default(null)
|
|
1913
|
-
});
|
|
1914
|
-
var routeRuntimeSchema = z17.object({
|
|
1915
|
-
rollout_key: z17.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "rollout_key must be lowercase alphanumeric with hyphens/underscores").optional(),
|
|
1916
|
-
/**
|
|
1917
|
-
* Optional runtime flags this feature depends on. The runtime
|
|
1918
|
-
* evaluator AND's the feature's enablement across all referenced
|
|
1919
|
-
* flags. If any flag is disabled, the route returns the configured
|
|
1920
|
-
* fallback (404 by default — see /runtime failure matrix).
|
|
1921
|
-
*/
|
|
1922
|
-
required_flags: z17.array(z17.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(10).optional()
|
|
1923
|
-
});
|
|
1924
|
-
var routesFileSchema = z17.object({
|
|
1925
|
-
/**
|
|
1926
|
-
* Feature key — the entitlement unit. Surfaced in dashboards,
|
|
1927
|
-
* subscriptions, and the gateway's matched-route trace.
|
|
1928
|
-
*/
|
|
1929
|
-
feature: z17.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/, "feature key must be lowercase alphanumeric with [_.:-]"),
|
|
1930
|
-
description: z17.string().max(500).optional(),
|
|
1931
|
-
/**
|
|
1932
|
-
* Route additions are contractual by default — they expose new API
|
|
1933
|
-
* surface to subscribers. Internal/non-customer-visible routes can
|
|
1934
|
-
* mark themselves `runtime` to allow autonomous agent flips
|
|
1935
|
-
* (invariant #16; see RFC approval matrix).
|
|
1936
|
-
*/
|
|
1937
|
-
mutation_class: z17.enum(["runtime", "contractual"]).default("contractual"),
|
|
1938
|
-
cacheProfile: cacheProfileSchema,
|
|
1939
|
-
routes: z17.array(routeDefinitionSchema).min(1).max(50),
|
|
1940
|
-
upstream: routeUpstreamSchema.default({ override_origin: null }),
|
|
1941
|
-
/**
|
|
1942
|
-
* Ordered list of policy names to apply. Executed sequentially by
|
|
1943
|
-
* the gateway policy engine; first-deny wins. Referenced policies
|
|
1944
|
-
* MUST declare compatible `compatible_with` envelopes for this
|
|
1945
|
-
* feature's route/meter shape — the compiler enforces.
|
|
1946
|
-
*/
|
|
1947
|
-
policies: z17.array(z17.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
|
|
1948
|
-
runtime: routeRuntimeSchema.default({}),
|
|
1949
|
-
/**
|
|
1950
|
-
* Capability groups this feature joins. A plan that includes any
|
|
1951
|
-
* referenced capability grants this feature. Capabilities are
|
|
1952
|
-
* resolved at compile time into the plan's expanded feature set.
|
|
1953
|
-
*/
|
|
1954
|
-
capabilities: z17.array(z17.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
|
|
1955
|
-
/**
|
|
1956
|
-
* Plans that grant this feature directly (in addition to capability
|
|
1957
|
-
* membership). Mirrors the legacy `featureCatalogEntrySchema.plans`
|
|
1958
|
-
* field for the common case where the feature isn't part of any
|
|
1959
|
-
* shared capability group.
|
|
1960
|
-
*
|
|
1961
|
-
* v0.3.0 keeps direct-plan binding for migration ergonomics; v0.4
|
|
1962
|
-
* may deprecate this in favour of capability-only composition.
|
|
1963
|
-
*/
|
|
1964
|
-
plans: z17.array(z17.string().min(1).max(64)).max(20).default([]),
|
|
1965
|
-
/** Explicit actions declared by this feature. Routes reference them by
|
|
1966
|
-
* `route.action`; routes without a binding receive implicit actions. */
|
|
1967
|
-
actions: z17.array(actionSpecSchema).max(100).optional()
|
|
1968
|
-
});
|
|
1969
|
-
|
|
1970
|
-
// ../contracts/dist/plans/spec/runtime-layer.js
|
|
1971
|
-
import { z as z18 } from "zod";
|
|
1972
|
-
var keyNameSchema = z18.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "runtime key must be lowercase alphanumeric with hyphens/underscores");
|
|
1973
|
-
var dependsOnSchema = z18.object({
|
|
1974
|
-
runtime_flags: z18.array(keyNameSchema).max(10).optional(),
|
|
1975
|
-
capabilities: z18.array(z18.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(10).optional()
|
|
1976
|
-
}).default({});
|
|
1977
|
-
var audienceSchema = z18.object({
|
|
1978
|
-
plans: z18.array(z18.string().min(1).max(64)).max(20).optional(),
|
|
1979
|
-
environments: z18.array(z18.string().min(1).max(64)).max(10).optional(),
|
|
1980
|
-
capabilities: z18.array(z18.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(10).optional()
|
|
1981
|
-
}).default({});
|
|
1982
|
-
var rolloutDefaultsSchema = z18.object({
|
|
1983
|
-
missing_behavior: z18.enum(["treat_as_zero_percent", "treat_as_full_rollout", "fail_closed"]).default("treat_as_zero_percent"),
|
|
1984
|
-
stale_behavior: z18.enum(["use_last_known", "fail_closed_on_signature"]).default("use_last_known")
|
|
1985
|
-
});
|
|
1986
|
-
var rolloutEntrySchema = z18.object({
|
|
1987
|
-
description: z18.string().max(500).optional(),
|
|
1988
|
-
audience: audienceSchema,
|
|
1989
|
-
/**
|
|
1990
|
-
* Rollout percentage (0-100). 0 = no subscribers in treatment;
|
|
1991
|
-
* 100 = full rollout. Cohort stability invariant #20: increasing
|
|
1992
|
-
* the percent admits more subscribers (existing stay); decreasing
|
|
1993
|
-
* drops subscribers whose deterministic bucket is above the new
|
|
1994
|
-
* boundary (logged as an audit event).
|
|
1995
|
-
*/
|
|
1996
|
-
percent: z18.number().int().min(0).max(100),
|
|
1997
|
-
/**
|
|
1998
|
-
* Seed for the deterministic SHA-256 hash that computes bucket
|
|
1999
|
-
* assignment. Rotating the seed is a destructive rebucketing
|
|
2000
|
-
* operation; the workflow runner refuses without approval metadata
|
|
2001
|
-
* (invariant #16).
|
|
2002
|
-
*/
|
|
2003
|
-
assignment_seed: z18.string().min(1).max(120).default("default"),
|
|
2004
|
-
depends_on: dependsOnSchema,
|
|
2005
|
-
defaults: rolloutDefaultsSchema.default({
|
|
2006
|
-
missing_behavior: "treat_as_zero_percent",
|
|
2007
|
-
stale_behavior: "use_last_known"
|
|
2008
|
-
})
|
|
2009
|
-
});
|
|
2010
|
-
var runtimeRolloutFileSchema = z18.object({
|
|
2011
|
-
cacheProfile: cacheProfileSchema.default("blocking"),
|
|
2012
|
-
rollouts: z18.record(keyNameSchema, rolloutEntrySchema).default({})
|
|
2013
|
-
});
|
|
2014
|
-
var flagDefaultsSchema = z18.object({
|
|
2015
|
-
missing_behavior: z18.enum(["disabled", "enabled", "fail_closed"]).default("disabled"),
|
|
2016
|
-
stale_behavior: z18.enum(["use_last_known", "fail_closed_on_signature"]).default("use_last_known")
|
|
2017
|
-
});
|
|
2018
|
-
var flagEntrySchema = z18.object({
|
|
2019
|
-
description: z18.string().max(500).optional(),
|
|
2020
|
-
enabled: z18.boolean(),
|
|
2021
|
-
audience: audienceSchema,
|
|
2022
|
-
depends_on: dependsOnSchema,
|
|
2023
|
-
defaults: flagDefaultsSchema.default({
|
|
2024
|
-
missing_behavior: "disabled",
|
|
2025
|
-
stale_behavior: "use_last_known"
|
|
2026
|
-
}),
|
|
2027
|
-
/**
|
|
2028
|
-
* When set, this flag acts as a killswitch — its `enabled: true`
|
|
2029
|
-
* disables traffic for the referenced subjects. Useful for
|
|
2030
|
-
* incident response (`policy_premium-rate-limit_emergency_off`).
|
|
2031
|
-
*/
|
|
2032
|
-
killswitch_target: z18.object({
|
|
2033
|
-
kind: z18.enum(["policy", "route", "rollout"]),
|
|
2034
|
-
name: z18.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)
|
|
2035
|
-
}).optional()
|
|
2036
|
-
});
|
|
2037
|
-
var runtimeFlagsFileSchema = z18.object({
|
|
2038
|
-
cacheProfile: cacheProfileSchema.default("blocking"),
|
|
2039
|
-
flags: z18.record(keyNameSchema, flagEntrySchema).default({})
|
|
2040
|
-
});
|
|
2041
|
-
var migrationDefaultsSchema = z18.object({
|
|
2042
|
-
missing_behavior: z18.enum(["disabled", "pending_review"]).default("disabled"),
|
|
2043
|
-
stale_behavior: z18.enum(["use_last_known", "fail_closed_on_signature"]).default("use_last_known")
|
|
2044
|
-
});
|
|
2045
|
-
var migrationTriggerSchema = z18.object({
|
|
2046
|
-
description: z18.string().max(500).optional(),
|
|
2047
|
-
/**
|
|
2048
|
-
* The (fromPlanKey, toPlanKey) migration this trigger configures.
|
|
2049
|
-
* Compiler validates both keys resolve.
|
|
2050
|
-
*/
|
|
2051
|
-
from_plan_key: z18.string().min(1).max(64),
|
|
2052
|
-
to_plan_key: z18.string().min(1).max(64),
|
|
2053
|
-
policy: z18.enum([
|
|
2054
|
-
"GRANDFATHER",
|
|
2055
|
-
"MIGRATE_AT_RENEWAL",
|
|
2056
|
-
"MIGRATE_IMMEDIATELY",
|
|
2057
|
-
"MIGRATE_BY_DATE",
|
|
2058
|
-
"OPT_IN"
|
|
2059
|
-
]).default("MIGRATE_AT_RENEWAL"),
|
|
2060
|
-
proration_policy: z18.enum(["NONE", "PRORATE", "CREDIT"]).default("NONE"),
|
|
2061
|
-
/**
|
|
2062
|
-
* Wall-clock cutover for `MIGRATE_BY_DATE` policy. Ignored for other
|
|
2063
|
-
* policies. Must be in the future at compile time.
|
|
2064
|
-
*/
|
|
2065
|
-
cutover_at: z18.string().datetime().optional(),
|
|
2066
|
-
/**
|
|
2067
|
-
* Freeze window per invariant #8 — while this migration is RUNNING,
|
|
2068
|
-
* contractual mutations to the product are rejected at the webhook
|
|
2069
|
-
* layer with 423 LOCKED. Set to `false` to allow concurrent
|
|
2070
|
-
* contract edits (only for migrations that don't touch entitlement
|
|
2071
|
-
* shape — e.g. pure price corrections).
|
|
2072
|
-
*/
|
|
2073
|
-
freeze_contract_mutations: z18.boolean().default(true),
|
|
2074
|
-
depends_on: dependsOnSchema,
|
|
2075
|
-
defaults: migrationDefaultsSchema.default({
|
|
2076
|
-
missing_behavior: "disabled",
|
|
2077
|
-
stale_behavior: "use_last_known"
|
|
2078
|
-
})
|
|
2079
|
-
});
|
|
2080
|
-
var runtimeMigrationsFileSchema = z18.object({
|
|
2081
|
-
cacheProfile: cacheProfileSchema.default("blocking"),
|
|
2082
|
-
migrations: z18.record(keyNameSchema, migrationTriggerSchema).default({})
|
|
2083
|
-
});
|
|
2084
|
-
|
|
2085
2103
|
// ../contracts/dist/plans/spec/capabilities-layer.js
|
|
2086
|
-
import { z as
|
|
2087
|
-
var
|
|
2088
|
-
capability:
|
|
2089
|
-
description:
|
|
2104
|
+
import { z as z20 } from "zod";
|
|
2105
|
+
var capabilityLayerSchema = z20.object({
|
|
2106
|
+
capability: z20.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "capability name must be lowercase alphanumeric with hyphens/underscores"),
|
|
2107
|
+
description: z20.string().max(500).optional(),
|
|
2090
2108
|
/**
|
|
2091
2109
|
* Capability composition is contractual by default — including a new
|
|
2092
2110
|
* feature changes the customer's effective entitlement. Mark
|
|
@@ -2094,214 +2112,56 @@ var capabilityFileSchema = z19.object({
|
|
|
2094
2112
|
* (e.g. an internal "monitoring" capability that gates dashboard
|
|
2095
2113
|
* pages without affecting billable behaviour).
|
|
2096
2114
|
*/
|
|
2097
|
-
mutation_class:
|
|
2098
|
-
includes_features:
|
|
2099
|
-
includes_policies:
|
|
2100
|
-
includes_capabilities:
|
|
2115
|
+
mutation_class: z20.enum(["runtime", "contractual"]).default("contractual"),
|
|
2116
|
+
includes_features: z20.array(z20.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/)).max(20).default([]),
|
|
2117
|
+
includes_policies: z20.array(z20.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
|
|
2118
|
+
includes_capabilities: z20.array(z20.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(20).default([])
|
|
2101
2119
|
});
|
|
2102
2120
|
|
|
2103
|
-
// ../contracts/dist/plans/spec/
|
|
2104
|
-
import {
|
|
2105
|
-
|
|
2106
|
-
var
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
/**
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
};
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
selfServeEnabled: z20.boolean().default(true),
|
|
2148
|
-
/**
|
|
2149
|
-
* Marks a plan as the pinned-cohort head (status LEGACY_STABLE). Only
|
|
2150
|
-
* existing subscribers stay on legacy plans; new subscribers cannot
|
|
2151
|
-
* join. Removing a legacy plan while subscribers are pinned is a
|
|
2152
|
-
* compile error.
|
|
2153
|
-
*/
|
|
2154
|
-
legacy: z20.boolean().optional().default(false),
|
|
2155
|
-
archive: z20.object({
|
|
2156
|
-
at: z20.string().datetime().optional(),
|
|
2157
|
-
transitionTo: z20.string().optional(),
|
|
2158
|
-
strategy: z20.enum(["auto", "explicit", "block"]).default("auto")
|
|
2159
|
-
}).optional()
|
|
2160
|
-
}).superRefine((plan, ctx) => {
|
|
2161
|
-
refineSingleCanonicalGrant(plan.grants, ctx);
|
|
2162
|
-
});
|
|
2163
|
-
var DEFAULT_SUBSCRIBER_CHANGE_POLICY = {
|
|
2164
|
-
default: "preserve_current_period",
|
|
2165
|
-
proration: "none",
|
|
2166
|
-
when: {
|
|
2167
|
-
price_increase: "preserve_current_period",
|
|
2168
|
-
price_decrease: "switch_immediately",
|
|
2169
|
-
feature_added: "switch_immediately",
|
|
2170
|
-
feature_removed: "preserve_current_period",
|
|
2171
|
-
limit_increased: "switch_immediately",
|
|
2172
|
-
limit_reduced: "preserve_current_period",
|
|
2173
|
-
credit_increased: "switch_immediately",
|
|
2174
|
-
credit_reduced: "preserve_current_period",
|
|
2175
|
-
rating_changed: "preserve_current_period"
|
|
2176
|
-
},
|
|
2177
|
-
allowImmediatePriceIncrease: false,
|
|
2178
|
-
allowImmediateEntitlementReduction: false
|
|
2179
|
-
};
|
|
2180
|
-
var productSpecV2Schema = z20.object({
|
|
2181
|
-
/**
|
|
2182
|
-
* Schema-version stamp. Forward migrations target a higher value;
|
|
2183
|
-
* readers refuse unsupported versions per invariant #2.
|
|
2184
|
-
*/
|
|
2185
|
-
artifactSchemaVersion: z20.literal(PRODUCT_V2_SCHEMA_VERSION).default(PRODUCT_V2_SCHEMA_VERSION),
|
|
2186
|
-
product: z20.object({
|
|
2187
|
-
name: z20.string().min(1).max(100),
|
|
2188
|
-
displayName: z20.string().max(200).optional(),
|
|
2189
|
-
description: z20.string().max(2e3).optional(),
|
|
2190
|
-
baseUrl: z20.string().url("baseUrl must be a valid URL"),
|
|
2191
|
-
sandboxBaseUrl: z20.string().url("sandboxBaseUrl must be a valid URL").optional(),
|
|
2192
|
-
visibility: z20.enum(["public", "private"]).default("public"),
|
|
2193
|
-
logoUrl: z20.string().url().optional(),
|
|
2194
|
-
primaryColor: z20.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
2195
|
-
envBranchPrefix: z20.string().max(50).nullable().optional()
|
|
2196
|
-
}),
|
|
2197
|
-
/**
|
|
2198
|
-
* Meter catalog. Referenced by route `metering` metadata.
|
|
2199
|
-
* Same shape as the legacy meterDefinitionSchema (re-exported from
|
|
2200
|
-
* product.ts to keep one source of truth during the transition).
|
|
2201
|
-
*
|
|
2202
|
-
* Lifted to a top-level array so the compiler can hash + invalidate
|
|
2203
|
-
* meter changes independently from plan changes (incremental
|
|
2204
|
-
* compilation per invariant #1 + Merkle DAG).
|
|
2205
|
-
*/
|
|
2206
|
-
meters: z20.array(meterDefinitionSchema).max(10).default([]),
|
|
2207
|
-
/**
|
|
2208
|
-
* Bill on 4xx responses (independent of meter setup). Stays on the
|
|
2209
|
-
* product spec because it's a contractual choice that affects
|
|
2210
|
-
* billing predictability.
|
|
2211
|
-
*/
|
|
2212
|
-
billOn4xx: z20.boolean().default(false),
|
|
2213
|
-
frontend: frontendManifestSchema.optional(),
|
|
2214
|
-
migrations: migrationDeclsSchema.optional(),
|
|
2215
|
-
resources: countedResourcesSchema,
|
|
2216
|
-
policies: productOperatorPoliciesSchema,
|
|
2217
|
-
customer_context: productCustomerContextSchema.optional(),
|
|
2218
|
-
surfaces: productSurfacesSchema,
|
|
2219
|
-
entitlements: productEntitlementsSchema,
|
|
2220
|
-
workflows: productWorkflowsSchema,
|
|
2221
|
-
billing: z20.object({
|
|
2222
|
-
gracePeriodDays: z20.number().int().nonnegative().default(3),
|
|
2223
|
-
// When true (default), a plan limit INCREASE re-projects onto active
|
|
2224
|
-
// subscribers immediately; a DECREASE always defers to period end.
|
|
2225
|
-
// Read by advanceActiveSubscribersToLatestCompiledPlans.
|
|
2226
|
-
applyLimitUpgradesInstantly: z20.boolean().optional(),
|
|
2227
|
-
subscriberChangePolicy: subscriberChangePolicySchema.default(DEFAULT_SUBSCRIBER_CHANGE_POLICY)
|
|
2228
|
-
}).default({
|
|
2229
|
-
gracePeriodDays: 3,
|
|
2230
|
-
subscriberChangePolicy: DEFAULT_SUBSCRIBER_CHANGE_POLICY
|
|
2231
|
-
}),
|
|
2232
|
-
plans: z20.array(planSpecV2Schema).max(4).default([]),
|
|
2233
|
-
add_ons: addOnsBlockSchema,
|
|
2234
|
-
lifecycle: z20.object({
|
|
2235
|
-
breaking_changes: z20.object({
|
|
2236
|
-
require_deprecation_window_days: z20.number().int().nonnegative().default(0),
|
|
2237
|
-
require_successor_route: z20.boolean().default(false)
|
|
2238
|
-
}).default({
|
|
2239
|
-
require_deprecation_window_days: 0,
|
|
2240
|
-
require_successor_route: false
|
|
2241
|
-
})
|
|
2242
|
-
}).default({
|
|
2243
|
-
breaking_changes: {
|
|
2244
|
-
require_deprecation_window_days: 0,
|
|
2245
|
-
require_successor_route: false
|
|
2246
|
-
}
|
|
2247
|
-
}),
|
|
2248
|
-
webhooks: webhooksBlockSchema.optional(),
|
|
2249
|
-
environments: environmentsBlockSchema.optional(),
|
|
2250
|
-
ephemeral: z20.object({
|
|
2251
|
-
defaultPlan: z20.string().min(1).optional()
|
|
2252
|
-
}).optional()
|
|
2253
|
-
});
|
|
2254
|
-
|
|
2255
|
-
// ../contracts/dist/plans/spec/manifest-ir.js
|
|
2256
|
-
import { createHash } from "node:crypto";
|
|
2257
|
-
import { z as z21 } from "zod";
|
|
2258
|
-
var MANIFEST_IR_VERSION = 1;
|
|
2259
|
-
var manifestIrSchema = z21.object({
|
|
2260
|
-
irVersion: z21.literal(MANIFEST_IR_VERSION),
|
|
2261
|
-
/** Version of @farthershore/product that emitted this envelope. */
|
|
2262
|
-
sdkVersion: z21.string().min(1).max(64),
|
|
2263
|
-
/** Legacy unified ProductSpec — the live `CompileProductOptions.sourceSpec`. */
|
|
2264
|
-
product: productSpecSchema,
|
|
2265
|
-
/** One entry per feature, sorted by `feature` (mirrors /routes/<feature>.yaml). */
|
|
2266
|
-
routes: z21.array(routesFileSchema).max(200).default([]),
|
|
2267
|
-
/** Sorted by `name` (mirrors /policies/<name>.yaml). */
|
|
2268
|
-
policies: z21.array(policyFileSchema).max(200).default([]),
|
|
2269
|
-
/** Sorted by `capability` (mirrors /capabilities/<name>.yaml). */
|
|
2270
|
-
capabilities: z21.array(capabilityFileSchema).max(200).default([]),
|
|
2271
|
-
/** Reserved. Always null at irVersion 1 — runtime stays YAML/dashboard. */
|
|
2272
|
-
runtime: z21.object({
|
|
2273
|
-
rollout: z21.null().default(null),
|
|
2274
|
-
flags: z21.null().default(null),
|
|
2275
|
-
migrations: z21.null().default(null)
|
|
2276
|
-
}).default({ rollout: null, flags: null, migrations: null })
|
|
2277
|
-
});
|
|
2278
|
-
function canonicalManifestJson(value) {
|
|
2279
|
-
return stableJson(JSON.parse(JSON.stringify(value)));
|
|
2280
|
-
}
|
|
2281
|
-
function hashManifestIr(ir) {
|
|
2282
|
-
return createHash("sha256").update(canonicalManifestJson(ir)).digest("hex");
|
|
2283
|
-
}
|
|
2284
|
-
function stableJson(value) {
|
|
2285
|
-
if (Array.isArray(value))
|
|
2286
|
-
return `[${value.map(stableJson).join(",")}]`;
|
|
2287
|
-
if (value && typeof value === "object") {
|
|
2288
|
-
return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`).join(",")}}`;
|
|
2289
|
-
}
|
|
2290
|
-
return JSON.stringify(value);
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
// ../contracts/dist/plans/presets.js
|
|
2294
|
-
var FREE = {
|
|
2295
|
-
kind: "free",
|
|
2296
|
-
label: "Free",
|
|
2297
|
-
description: "No recurring fee and no metered usage. Pure freemium tier.",
|
|
2298
|
-
pricing: {
|
|
2299
|
-
meters: [],
|
|
2300
|
-
recurring_fee_cents: 0,
|
|
2301
|
-
billing_interval: "month",
|
|
2302
|
-
grants: [],
|
|
2303
|
-
trial_days: 0
|
|
2304
|
-
}
|
|
2121
|
+
// ../contracts/dist/plans/spec/manifest-ir.js
|
|
2122
|
+
import { createHash } from "node:crypto";
|
|
2123
|
+
import { z as z21 } from "zod";
|
|
2124
|
+
var MANIFEST_IR_VERSION = 1;
|
|
2125
|
+
var manifestIrSchema = z21.object({
|
|
2126
|
+
irVersion: z21.literal(MANIFEST_IR_VERSION),
|
|
2127
|
+
/** Version of @farthershore/product that emitted this envelope. */
|
|
2128
|
+
sdkVersion: z21.string().min(1).max(64),
|
|
2129
|
+
/** Legacy unified ProductSpec — the live `CompileProductOptions.sourceSpec`. */
|
|
2130
|
+
product: productSpecSchema,
|
|
2131
|
+
/** One entry per feature, sorted by `feature`. */
|
|
2132
|
+
routes: z21.array(routeLayerSchema).max(200).default([]),
|
|
2133
|
+
/** Sorted by `name`. */
|
|
2134
|
+
policies: z21.array(policyLayerSchema).max(200).default([]),
|
|
2135
|
+
/** Sorted by `capability`. */
|
|
2136
|
+
capabilities: z21.array(capabilityLayerSchema).max(200).default([])
|
|
2137
|
+
}).strict();
|
|
2138
|
+
function canonicalManifestJson(value) {
|
|
2139
|
+
return stableJson(JSON.parse(JSON.stringify(value)));
|
|
2140
|
+
}
|
|
2141
|
+
function hashManifestIr(ir) {
|
|
2142
|
+
return createHash("sha256").update(canonicalManifestJson(ir)).digest("hex");
|
|
2143
|
+
}
|
|
2144
|
+
function stableJson(value) {
|
|
2145
|
+
if (Array.isArray(value))
|
|
2146
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
2147
|
+
if (value && typeof value === "object") {
|
|
2148
|
+
return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`).join(",")}}`;
|
|
2149
|
+
}
|
|
2150
|
+
return JSON.stringify(value);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// ../contracts/dist/plans/presets.js
|
|
2154
|
+
var FREE = {
|
|
2155
|
+
kind: "free",
|
|
2156
|
+
label: "Free",
|
|
2157
|
+
description: "No recurring fee and no metered usage. Pure freemium tier.",
|
|
2158
|
+
pricing: {
|
|
2159
|
+
meters: [],
|
|
2160
|
+
recurring_fee_cents: 0,
|
|
2161
|
+
billing_interval: "month",
|
|
2162
|
+
grants: [],
|
|
2163
|
+
trial_days: 0
|
|
2164
|
+
}
|
|
2305
2165
|
};
|
|
2306
2166
|
var STARTER = {
|
|
2307
2167
|
kind: "starter",
|
|
@@ -2313,7 +2173,7 @@ var STARTER = {
|
|
|
2313
2173
|
],
|
|
2314
2174
|
recurring_fee_cents: 2e3,
|
|
2315
2175
|
billing_interval: "month",
|
|
2316
|
-
grants: [{ kind: "
|
|
2176
|
+
grants: [{ kind: "credit", amount_cents: 2e3, recurs: true }],
|
|
2317
2177
|
trial_days: 0
|
|
2318
2178
|
}
|
|
2319
2179
|
};
|
|
@@ -2327,7 +2187,7 @@ var PRO = {
|
|
|
2327
2187
|
],
|
|
2328
2188
|
recurring_fee_cents: 1e4,
|
|
2329
2189
|
billing_interval: "month",
|
|
2330
|
-
grants: [{ kind: "
|
|
2190
|
+
grants: [{ kind: "credit", amount_cents: 2e4, recurs: true }],
|
|
2331
2191
|
trial_days: 14
|
|
2332
2192
|
}
|
|
2333
2193
|
};
|
|
@@ -2341,7 +2201,7 @@ var PREPAID = {
|
|
|
2341
2201
|
],
|
|
2342
2202
|
recurring_fee_cents: 0,
|
|
2343
2203
|
billing_interval: "month",
|
|
2344
|
-
grants: [{ kind: "
|
|
2204
|
+
grants: [{ kind: "credit", amount_cents: 5e3, recurs: false }],
|
|
2345
2205
|
trial_days: 0
|
|
2346
2206
|
}
|
|
2347
2207
|
};
|
|
@@ -2379,6 +2239,9 @@ var subscriptionPricingOverrideSchema = z22.object({
|
|
|
2379
2239
|
* the legacy recurring/one-time scalar knobs were removed).
|
|
2380
2240
|
*/
|
|
2381
2241
|
grants: z22.array(grantSchema).max(40).optional(),
|
|
2242
|
+
/** Override the plan's credit-policy block (rollover + auto-recharge)
|
|
2243
|
+
* for this subscriber. */
|
|
2244
|
+
creditPolicy: creditPolicySchema.optional(),
|
|
2382
2245
|
/** Override the minimum-spend floor. */
|
|
2383
2246
|
min_monthly_spend_cents: z22.number().int().nonnegative().optional(),
|
|
2384
2247
|
/** Override the maximum-spend ceiling. */
|
|
@@ -2410,78 +2273,864 @@ function validateManifestIr(candidate) {
|
|
|
2410
2273
|
);
|
|
2411
2274
|
return { ok: true, ir, irHash: hashIr(ir) };
|
|
2412
2275
|
}
|
|
2413
|
-
function hashIr(ir) {
|
|
2414
|
-
return hashManifestIr(ir);
|
|
2276
|
+
function hashIr(ir) {
|
|
2277
|
+
return hashManifestIr(ir);
|
|
2278
|
+
}
|
|
2279
|
+
function canonicalIrJson(ir) {
|
|
2280
|
+
return canonicalManifestJson(ir);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/version.ts
|
|
2284
|
+
var SDK_VERSION = true ? "0.6.1" : "0.0.0-dev";
|
|
2285
|
+
|
|
2286
|
+
// src/refs.ts
|
|
2287
|
+
function isCapabilityGrant(value) {
|
|
2288
|
+
return typeof value === "object" && value !== null && value.kind === "capability_grant";
|
|
2289
|
+
}
|
|
2290
|
+
function keyOf(ref) {
|
|
2291
|
+
return typeof ref === "string" ? ref : ref.key;
|
|
2292
|
+
}
|
|
2293
|
+
function displayFromKey(key) {
|
|
2294
|
+
return key.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// src/backend.ts
|
|
2298
|
+
var BACKEND_ID_PATTERN2 = /^[a-z0-9][a-z0-9_-]*$/;
|
|
2299
|
+
function createBackendNode(id, options = {}) {
|
|
2300
|
+
if (!BACKEND_ID_PATTERN2.test(id)) {
|
|
2301
|
+
throw new ManifestBuilderError(
|
|
2302
|
+
`backend "${id}": id must be a lowercase slug ([a-z0-9][a-z0-9_-]*)`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
return {
|
|
2306
|
+
id,
|
|
2307
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
2308
|
+
...options.slug !== void 0 ? { slug: options.slug } : {},
|
|
2309
|
+
...options.transport !== void 0 ? {
|
|
2310
|
+
transport: {
|
|
2311
|
+
...options.transport.mode !== void 0 ? { mode: options.transport.mode } : {},
|
|
2312
|
+
...options.transport.runner !== void 0 ? { runner: options.transport.runner } : {}
|
|
2313
|
+
}
|
|
2314
|
+
} : {},
|
|
2315
|
+
...options.verification !== void 0 ? { verification: options.verification } : {},
|
|
2316
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
|
|
2317
|
+
...options.default !== void 0 ? { default: options.default } : {},
|
|
2318
|
+
...options.originUrl !== void 0 ? { originUrl: options.originUrl } : {},
|
|
2319
|
+
...options.originHostname !== void 0 ? { originHostname: options.originHostname } : {}
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
function buildBackendBlock(backendNodes) {
|
|
2323
|
+
const out = {};
|
|
2324
|
+
for (const node of [...backendNodes].sort(
|
|
2325
|
+
(a, b) => a.id.localeCompare(b.id)
|
|
2326
|
+
)) {
|
|
2327
|
+
const { id, ...definition } = node;
|
|
2328
|
+
out[id] = definition;
|
|
2329
|
+
}
|
|
2330
|
+
return out;
|
|
2331
|
+
}
|
|
2332
|
+
function assertBackendBindingsValid(files, backendNodes, meterDefinitions) {
|
|
2333
|
+
if (backendNodes.length === 0) return;
|
|
2334
|
+
const byId = new Map(backendNodes.map((backend) => [backend.id, backend]));
|
|
2335
|
+
const declaredMeters = new Set(meterDefinitions.map((meter) => meter.key));
|
|
2336
|
+
for (const backend of backendNodes) {
|
|
2337
|
+
for (const meter of backend.meters ?? []) {
|
|
2338
|
+
if (declaredMeters.has(meter)) continue;
|
|
2339
|
+
throw new ManifestBuilderError(
|
|
2340
|
+
`UNKNOWN_METER_IN_BACKEND: backend "${backend.id}" allows meter "${meter}" but it is not declared \u2014 call product.meter("${meter}", ...) first`
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
const ids = backendNodes.map((backend) => backend.id);
|
|
2345
|
+
const explicitDefaults = backendNodes.filter(
|
|
2346
|
+
(backend) => backend.default === true
|
|
2347
|
+
);
|
|
2348
|
+
const defaultId = ids.length === 1 ? ids[0] : explicitDefaults.length === 1 ? explicitDefaults[0].id : null;
|
|
2349
|
+
const ambiguous = defaultId === null;
|
|
2350
|
+
for (const file of files) {
|
|
2351
|
+
const fileBinding = file.backend;
|
|
2352
|
+
if (fileBinding !== void 0 && !byId.has(fileBinding)) {
|
|
2353
|
+
throw new ManifestBuilderError(
|
|
2354
|
+
`UNKNOWN_BACKEND_IN_ROUTE: feature "${file.feature}" binds backend "${fileBinding}" which is not declared \u2014 call product.backend("${fileBinding}", ...) first`
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
file.routes.forEach((route, routeIndex) => {
|
|
2358
|
+
const explicit = route.backend ?? fileBinding;
|
|
2359
|
+
if (explicit !== void 0) {
|
|
2360
|
+
if (!byId.has(explicit)) {
|
|
2361
|
+
throw new ManifestBuilderError(
|
|
2362
|
+
`UNKNOWN_BACKEND_IN_ROUTE: feature "${file.feature}" route ${routeIndex} binds backend "${explicit}" which is not declared \u2014 call product.backend("${explicit}", ...) first`
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
} else if (ambiguous) {
|
|
2366
|
+
throw new ManifestBuilderError(
|
|
2367
|
+
`AMBIGUOUS_DEFAULT_BACKEND: feature "${file.feature}" route ${routeIndex} has no backend binding and the product declares ${ids.length} backends with no single default \u2014 mark exactly one backend \`default: true\` or bind the route explicitly`
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
const boundId = explicit ?? defaultId;
|
|
2371
|
+
if (!boundId) return;
|
|
2372
|
+
const backend = byId.get(boundId);
|
|
2373
|
+
const allow = backend?.meters;
|
|
2374
|
+
if (!allow) return;
|
|
2375
|
+
const allowed = new Set(allow);
|
|
2376
|
+
const routeMeters = /* @__PURE__ */ new Set([
|
|
2377
|
+
...Object.keys(route.metering?.defaults ?? {}),
|
|
2378
|
+
...route.metering?.reports ?? [],
|
|
2379
|
+
...Object.keys(route.metering?.estimates ?? {})
|
|
2380
|
+
]);
|
|
2381
|
+
for (const meter of routeMeters) {
|
|
2382
|
+
if (allowed.has(meter)) continue;
|
|
2383
|
+
throw new ManifestBuilderError(
|
|
2384
|
+
`ROUTE_METER_NOT_ALLOWED_BY_BACKEND: feature "${file.feature}" route ${routeIndex} meters "${meter}" but backend "${boundId}" does not allow it (backend.meters = [${allow.join(", ")}])`
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// src/product-assembly.ts
|
|
2392
|
+
function assembleProductSpec(input) {
|
|
2393
|
+
const { options } = input;
|
|
2394
|
+
const base = {
|
|
2395
|
+
product: {
|
|
2396
|
+
name: input.name,
|
|
2397
|
+
baseUrl: options.origin,
|
|
2398
|
+
...options.displayName !== void 0 ? { displayName: options.displayName } : {},
|
|
2399
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2400
|
+
...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
|
|
2401
|
+
...options.visibility !== void 0 ? { visibility: options.visibility } : {},
|
|
2402
|
+
...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
|
|
2403
|
+
...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
|
|
2404
|
+
...options.envBranchPrefix !== void 0 ? { envBranchPrefix: options.envBranchPrefix } : {}
|
|
2405
|
+
},
|
|
2406
|
+
gateway: {
|
|
2407
|
+
...options.authHeader !== void 0 ? { authHeader: options.authHeader } : {},
|
|
2408
|
+
...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
|
|
2409
|
+
},
|
|
2410
|
+
metering: {
|
|
2411
|
+
meters: buildMeterDefinitions(
|
|
2412
|
+
input.meters,
|
|
2413
|
+
input.defaultMeterCosts,
|
|
2414
|
+
input.featureLayers,
|
|
2415
|
+
input.workflows
|
|
2416
|
+
),
|
|
2417
|
+
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2418
|
+
},
|
|
2419
|
+
...input.backends.length ? { backend: buildBackendBlock(input.backends) } : {},
|
|
2420
|
+
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2421
|
+
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
2422
|
+
...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
|
|
2423
|
+
...input.surfaces.length ? { surfaces: sortBy2(input.surfaces, (surface) => surface.key) } : {},
|
|
2424
|
+
...input.entitlements.length ? {
|
|
2425
|
+
entitlements: sortBy2(
|
|
2426
|
+
input.entitlements,
|
|
2427
|
+
(entitlement) => entitlement.key
|
|
2428
|
+
)
|
|
2429
|
+
} : {},
|
|
2430
|
+
...input.workflows.length ? { workflows: sortBy2(input.workflows, (workflow) => workflow.key) } : {},
|
|
2431
|
+
...input.frontendManifest !== void 0 ? { frontend: input.frontendManifest } : {},
|
|
2432
|
+
...input.migrations.length ? { migrations: sortBy2(input.migrations, (migration) => migration.id) } : {},
|
|
2433
|
+
...input.resources.length ? { resources: sortBy2(input.resources, (resource) => resource.name) } : {},
|
|
2434
|
+
plans: sortBy2(input.plans, (plan) => plan.key)
|
|
2435
|
+
};
|
|
2436
|
+
return mergeProductPatch(
|
|
2437
|
+
base,
|
|
2438
|
+
input.productPatch
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
function buildMeterDefinitions(meters, defaultMeterCosts, featureLayers, workflows) {
|
|
2442
|
+
const routeValueMeters = routeValueMeterKeys(
|
|
2443
|
+
defaultMeterCosts,
|
|
2444
|
+
featureLayers,
|
|
2445
|
+
workflows
|
|
2446
|
+
);
|
|
2447
|
+
return sortBy2(meters, (meter) => meter.key).map((meter) => {
|
|
2448
|
+
if (meter.aggregation !== void 0) return meter;
|
|
2449
|
+
if (!routeValueMeters.has(meter.key)) return meter;
|
|
2450
|
+
return { ...meter, aggregation: "SUM" };
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
function routeValueMeterKeys(defaultMeterCosts, featureLayers, workflows) {
|
|
2454
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2455
|
+
for (const cost of defaultMeterCosts) {
|
|
2456
|
+
keys.add(cost.meter);
|
|
2457
|
+
}
|
|
2458
|
+
for (const file of featureLayers) {
|
|
2459
|
+
for (const route of file.routes) {
|
|
2460
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
2461
|
+
keys.add(meter);
|
|
2462
|
+
}
|
|
2463
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2464
|
+
keys.add(meter);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
for (const workflow of workflows) {
|
|
2469
|
+
for (const meter of workflow.meters ?? []) keys.add(meter);
|
|
2470
|
+
for (const meter of Object.keys(workflow.estimates ?? {})) keys.add(meter);
|
|
2471
|
+
}
|
|
2472
|
+
return keys;
|
|
2473
|
+
}
|
|
2474
|
+
function buildCustomerContext(options) {
|
|
2475
|
+
return {
|
|
2476
|
+
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
2477
|
+
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
2478
|
+
...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
function sortBy2(items, key) {
|
|
2482
|
+
return [...items].sort((a, b) => key(a).localeCompare(key(b)));
|
|
2483
|
+
}
|
|
2484
|
+
function isPlainObject(value) {
|
|
2485
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2486
|
+
}
|
|
2487
|
+
function mergeProductPatch(base, patch) {
|
|
2488
|
+
const out = { ...base };
|
|
2489
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
2490
|
+
const existing = out[key];
|
|
2491
|
+
if (isPlainObject(existing) && isPlainObject(value)) {
|
|
2492
|
+
out[key] = mergeProductPatch(existing, value);
|
|
2493
|
+
} else {
|
|
2494
|
+
out[key] = value;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return out;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/frontend.ts
|
|
2501
|
+
function createFrontendManifest() {
|
|
2502
|
+
return { version: 1, nav: [], pages: [] };
|
|
2503
|
+
}
|
|
2504
|
+
function setFrontendNav(manifest, items) {
|
|
2505
|
+
manifest.nav = items.map((item) => ({
|
|
2506
|
+
label: item.label,
|
|
2507
|
+
path: item.path,
|
|
2508
|
+
...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
|
|
2509
|
+
}));
|
|
2510
|
+
}
|
|
2511
|
+
function addFrontendPage(manifest, path, options) {
|
|
2512
|
+
if (manifest.pages?.some((page) => page.path === path)) {
|
|
2513
|
+
throw new ManifestBuilderError(
|
|
2514
|
+
`duplicate frontend page "${path}" \u2014 each frontend page path must be declared once`
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
manifest.pages ??= [];
|
|
2518
|
+
manifest.pages.push({
|
|
2519
|
+
path,
|
|
2520
|
+
title: options.title,
|
|
2521
|
+
requiresAuth: options.requiresAuth,
|
|
2522
|
+
...options.capability !== void 0 ? { capability: keyOf(options.capability) } : {},
|
|
2523
|
+
...options.components?.length ? {
|
|
2524
|
+
components: options.components.map((component) => ({
|
|
2525
|
+
component: component.component,
|
|
2526
|
+
...component.props !== void 0 ? { props: component.props } : {},
|
|
2527
|
+
...component.capability !== void 0 ? { capability: keyOf(component.capability) } : {},
|
|
2528
|
+
...component.gateMode !== void 0 ? { gateMode: component.gateMode } : {}
|
|
2529
|
+
}))
|
|
2530
|
+
} : {}
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
function frontendCapabilityKeys(manifest) {
|
|
2534
|
+
return [
|
|
2535
|
+
...(manifest.nav ?? []).flatMap(
|
|
2536
|
+
(item) => item.capability ? [item.capability] : []
|
|
2537
|
+
),
|
|
2538
|
+
...(manifest.pages ?? []).flatMap((page) => [
|
|
2539
|
+
...page.capability ? [page.capability] : [],
|
|
2540
|
+
...(page.components ?? []).flatMap(
|
|
2541
|
+
(component) => component.capability ? [component.capability] : []
|
|
2542
|
+
)
|
|
2543
|
+
])
|
|
2544
|
+
];
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/route-metering.ts
|
|
2548
|
+
function parseRouteMatch(match) {
|
|
2549
|
+
const trimmed = match.trim();
|
|
2550
|
+
const space = trimmed.indexOf(" ");
|
|
2551
|
+
if (space === -1) {
|
|
2552
|
+
if (!trimmed.startsWith("/")) {
|
|
2553
|
+
throw new ManifestBuilderError(
|
|
2554
|
+
`route "${match}": expected "METHOD /path" or "/path"`
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
return { method: "*", path: trimmed };
|
|
2558
|
+
}
|
|
2559
|
+
const method = trimmed.slice(0, space).toUpperCase();
|
|
2560
|
+
const path = trimmed.slice(space + 1).trim();
|
|
2561
|
+
const methods = [
|
|
2562
|
+
"GET",
|
|
2563
|
+
"POST",
|
|
2564
|
+
"PUT",
|
|
2565
|
+
"PATCH",
|
|
2566
|
+
"DELETE",
|
|
2567
|
+
"HEAD",
|
|
2568
|
+
"OPTIONS",
|
|
2569
|
+
"*"
|
|
2570
|
+
];
|
|
2571
|
+
if (!methods.includes(method)) {
|
|
2572
|
+
throw new ManifestBuilderError(
|
|
2573
|
+
`route "${match}": unknown HTTP method "${method}"`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
if (!path.startsWith("/")) {
|
|
2577
|
+
throw new ManifestBuilderError(
|
|
2578
|
+
`route "${match}": path must start with "/"`
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
return { method, path };
|
|
2582
|
+
}
|
|
2583
|
+
function makeMeterRef(key) {
|
|
2584
|
+
return {
|
|
2585
|
+
kind: "meter",
|
|
2586
|
+
key,
|
|
2587
|
+
fixed: (value) => ({ kind: "meter_cost", meter: key, value }),
|
|
2588
|
+
estimate: (value) => ({ kind: "meter_estimate", meter: key, value })
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
function normalizeMeterCost(cost, hasMeter) {
|
|
2592
|
+
if (!cost || cost.kind !== "meter_cost") {
|
|
2593
|
+
throw new ManifestBuilderError(
|
|
2594
|
+
"meter cost must be created by meter.fixed(value)"
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
if (!hasMeter(cost.meter)) {
|
|
2598
|
+
throw new ManifestBuilderError(
|
|
2599
|
+
`meter cost references unknown meter "${cost.meter}"`
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
if (!Number.isFinite(cost.value) || cost.value < 0) {
|
|
2603
|
+
throw new ManifestBuilderError(
|
|
2604
|
+
`meter "${cost.meter}" fixed value must be a non-negative finite number`
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
return cost;
|
|
2608
|
+
}
|
|
2609
|
+
function normalizeMeterCosts(costs, normalize) {
|
|
2610
|
+
if (!costs) return [];
|
|
2611
|
+
const entries = Array.isArray(costs) ? costs : [costs];
|
|
2612
|
+
return entries.map((cost) => normalize(cost));
|
|
2613
|
+
}
|
|
2614
|
+
function normalizeMeterRefs(refs) {
|
|
2615
|
+
const entries = Array.isArray(refs) ? refs : [refs];
|
|
2616
|
+
return entries.map(keyOf);
|
|
2617
|
+
}
|
|
2618
|
+
function statusPolicyPartIsValid2(part) {
|
|
2619
|
+
const trimmed = part.trim();
|
|
2620
|
+
if (!trimmed) return false;
|
|
2621
|
+
const [startRaw, endRaw, extra] = trimmed.split("-");
|
|
2622
|
+
if (extra !== void 0 || !/^\d{3}$/.test(startRaw ?? "")) return false;
|
|
2623
|
+
const start = Number(startRaw);
|
|
2624
|
+
const end = endRaw === void 0 ? start : Number(endRaw);
|
|
2625
|
+
if (endRaw !== void 0 && !/^\d{3}$/.test(endRaw)) return false;
|
|
2626
|
+
return start >= 100 && start <= 599 && end >= 100 && end <= 599 && start <= end;
|
|
2627
|
+
}
|
|
2628
|
+
function assertStatusCodePolicy(value) {
|
|
2629
|
+
if (Array.isArray(value)) {
|
|
2630
|
+
if (value.length === 0) {
|
|
2631
|
+
throw new ManifestBuilderError("onStatusCodes cannot be an empty array");
|
|
2632
|
+
}
|
|
2633
|
+
for (const status of value) {
|
|
2634
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
2635
|
+
throw new ManifestBuilderError(
|
|
2636
|
+
`onStatusCodes array contains invalid HTTP status "${status}"`
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (!value.split(",").every(statusPolicyPartIsValid2)) {
|
|
2643
|
+
throw new ManifestBuilderError(
|
|
2644
|
+
'onStatusCodes must be comma-separated HTTP status codes or numeric ranges, e.g. "200-299,304"'
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
function assertEstimateValue(meter, value) {
|
|
2649
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2650
|
+
throw new ManifestBuilderError(
|
|
2651
|
+
`meter "${meter}" estimate must be a non-negative finite number`
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
function normalizeMeterEstimate(estimate, hasMeter) {
|
|
2656
|
+
if (!estimate || estimate.kind !== "meter_estimate") {
|
|
2657
|
+
throw new ManifestBuilderError(
|
|
2658
|
+
"meter estimate must be created by meter.estimate(value)"
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
if (!hasMeter(estimate.meter)) {
|
|
2662
|
+
throw new ManifestBuilderError(
|
|
2663
|
+
`meter estimate references unknown meter "${estimate.meter}"`
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
assertEstimateValue(estimate.meter, estimate.value);
|
|
2667
|
+
return estimate;
|
|
2668
|
+
}
|
|
2669
|
+
function isMeterEstimate(value) {
|
|
2670
|
+
return typeof value === "object" && value !== null && value.kind === "meter_estimate";
|
|
2671
|
+
}
|
|
2672
|
+
function normalizeMeterEstimates(estimates, hasMeter) {
|
|
2673
|
+
if (!estimates) return {};
|
|
2674
|
+
if (Array.isArray(estimates)) {
|
|
2675
|
+
return Object.fromEntries(
|
|
2676
|
+
estimates.map((estimate) => {
|
|
2677
|
+
const normalized = normalizeMeterEstimate(estimate, hasMeter);
|
|
2678
|
+
return [normalized.meter, normalized.value];
|
|
2679
|
+
})
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
if (isMeterEstimate(estimates)) {
|
|
2683
|
+
const normalized = normalizeMeterEstimate(estimates, hasMeter);
|
|
2684
|
+
return { [normalized.meter]: normalized.value };
|
|
2685
|
+
}
|
|
2686
|
+
const estimateMap = estimates;
|
|
2687
|
+
for (const [meter, value] of Object.entries(estimateMap)) {
|
|
2688
|
+
assertEstimateValue(meter, value);
|
|
2689
|
+
}
|
|
2690
|
+
return { ...estimateMap };
|
|
2691
|
+
}
|
|
2692
|
+
function buildRouteDefinition(match, options, deps) {
|
|
2693
|
+
const parsed = parseRouteMatch(match);
|
|
2694
|
+
const metering = buildRouteMetering(options, deps);
|
|
2695
|
+
return {
|
|
2696
|
+
match: parsed,
|
|
2697
|
+
...metering ? { metering } : {},
|
|
2698
|
+
...options.unmetered !== void 0 ? { unmetered: options.unmetered } : {},
|
|
2699
|
+
...options.inheritDefaultMeters !== void 0 ? { inheritDefaultMeters: options.inheritDefaultMeters } : {},
|
|
2700
|
+
...options.action !== void 0 ? { action: keyOf(options.action) } : {},
|
|
2701
|
+
...options.backend !== void 0 ? { backend: keyOf(options.backend) } : {}
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
function buildRouteMetering(options, deps) {
|
|
2705
|
+
if (options.unmetered === true) return void 0;
|
|
2706
|
+
const defaults = {};
|
|
2707
|
+
for (const cost of normalizeMeterCosts(
|
|
2708
|
+
options.costs,
|
|
2709
|
+
(cost2) => deps.normalizeMeterCost(cost2)
|
|
2710
|
+
)) {
|
|
2711
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2712
|
+
}
|
|
2713
|
+
const reports = [...new Set(normalizeMeterRefs(options.reports ?? []))];
|
|
2714
|
+
const estimates = {};
|
|
2715
|
+
for (const meter of reports) {
|
|
2716
|
+
const definition = deps.getMeterDefinition(meter);
|
|
2717
|
+
if (typeof definition?.estimate === "number") {
|
|
2718
|
+
assertEstimateValue(meter, definition.estimate);
|
|
2719
|
+
estimates[meter] = definition.estimate;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
const explicitEstimates = normalizeMeterEstimates(
|
|
2723
|
+
options.estimates,
|
|
2724
|
+
(meter) => deps.getMeterDefinition(meter) !== void 0
|
|
2725
|
+
);
|
|
2726
|
+
for (const [meter, value] of Object.entries(explicitEstimates)) {
|
|
2727
|
+
estimates[meter] = value;
|
|
2728
|
+
}
|
|
2729
|
+
const out = {};
|
|
2730
|
+
if (Object.keys(defaults).length) out.defaults = defaults;
|
|
2731
|
+
if (reports.length) out.reports = reports;
|
|
2732
|
+
if (Object.keys(estimates).length) out.estimates = estimates;
|
|
2733
|
+
if (options.onStatusCodes !== void 0) {
|
|
2734
|
+
assertStatusCodePolicy(options.onStatusCodes);
|
|
2735
|
+
out.onStatusCodes = options.onStatusCodes;
|
|
2736
|
+
}
|
|
2737
|
+
return Object.keys(out).length ? out : void 0;
|
|
2738
|
+
}
|
|
2739
|
+
function materializeRoute(route, defaultMeterCosts) {
|
|
2740
|
+
if (route.unmetered === true) return route;
|
|
2741
|
+
const defaults = {
|
|
2742
|
+
...route.metering?.defaults ?? {}
|
|
2743
|
+
};
|
|
2744
|
+
if (route.inheritDefaultMeters !== false) {
|
|
2745
|
+
for (const cost of defaultMeterCosts) {
|
|
2746
|
+
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
const hasMetering = route.metering !== void 0 || Object.keys(defaults).length > 0;
|
|
2750
|
+
const metering = hasMetering ? {
|
|
2751
|
+
...route.metering ?? {},
|
|
2752
|
+
...Object.keys(defaults).length ? { defaults } : {}
|
|
2753
|
+
} : void 0;
|
|
2754
|
+
return {
|
|
2755
|
+
...route,
|
|
2756
|
+
...metering ? { metering } : {}
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
function routeMeterDependencyKeys(route) {
|
|
2760
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2761
|
+
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
2762
|
+
keys.add(meter);
|
|
2763
|
+
}
|
|
2764
|
+
for (const meter of route.metering?.reports ?? []) keys.add(meter);
|
|
2765
|
+
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
2766
|
+
keys.add(meter);
|
|
2767
|
+
}
|
|
2768
|
+
return [...keys];
|
|
2769
|
+
}
|
|
2770
|
+
function assertRouteMeteringValid(files, meterDefinitions) {
|
|
2771
|
+
const definitions = new Map(
|
|
2772
|
+
Array.from(meterDefinitions, (meter) => [meter.key, meter])
|
|
2773
|
+
);
|
|
2774
|
+
for (const file of files) {
|
|
2775
|
+
file.routes.forEach((route, routeIndex) => {
|
|
2776
|
+
if (route.unmetered === true) return;
|
|
2777
|
+
const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
|
|
2778
|
+
const reports = new Set(route.metering?.reports ?? []);
|
|
2779
|
+
for (const meter of defaults) {
|
|
2780
|
+
if (definitions.has(meter)) continue;
|
|
2781
|
+
throw new ManifestBuilderError(
|
|
2782
|
+
`feature "${file.feature}" route ${routeIndex}: fixed meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2786
|
+
if (!definitions.has(meter)) {
|
|
2787
|
+
throw new ManifestBuilderError(
|
|
2788
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
if (defaults.has(meter)) {
|
|
2792
|
+
throw new ManifestBuilderError(
|
|
2793
|
+
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
for (const meter of route.metering?.reports ?? []) {
|
|
2798
|
+
const definition = definitions.get(meter);
|
|
2799
|
+
const enforcement = definition?.enforcementType ?? "estimated_then_settled";
|
|
2800
|
+
const hasEstimate = route.metering?.estimates && Object.prototype.hasOwnProperty.call(route.metering.estimates, meter);
|
|
2801
|
+
if ((enforcement === "exact_pre_request" || enforcement === "estimated_then_settled") && !hasEstimate) {
|
|
2802
|
+
throw new ManifestBuilderError(
|
|
2803
|
+
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" needs an estimate for gateway admission`
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
for (const [meter, value] of Object.entries(
|
|
2808
|
+
route.metering?.estimates ?? {}
|
|
2809
|
+
)) {
|
|
2810
|
+
assertEstimateValue(meter, value);
|
|
2811
|
+
if (!definitions.has(meter)) {
|
|
2812
|
+
throw new ManifestBuilderError(
|
|
2813
|
+
`feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
if (reports.has(meter)) continue;
|
|
2817
|
+
throw new ManifestBuilderError(
|
|
2818
|
+
`feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" must also be listed in reports because estimates are pre-request reservations for dynamic usage`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// src/dependencies.ts
|
|
2826
|
+
function capabilityDependsOn(file) {
|
|
2827
|
+
return [
|
|
2828
|
+
...dependenciesFor("feature", file.includes_features),
|
|
2829
|
+
...dependenciesFor("policy", file.includes_policies),
|
|
2830
|
+
...dependenciesFor("capability", file.includes_capabilities)
|
|
2831
|
+
];
|
|
2832
|
+
}
|
|
2833
|
+
function policyDependsOn(file) {
|
|
2834
|
+
return dependenciesFor("meter", file.compatible_with?.meters);
|
|
2835
|
+
}
|
|
2836
|
+
function entitlementDependsOn(entitlement, hasResource) {
|
|
2837
|
+
const limitDimensions = (entitlement.limits ?? []).map(
|
|
2838
|
+
(limit) => limit.dimension
|
|
2839
|
+
);
|
|
2840
|
+
return [
|
|
2841
|
+
...dependenciesFor("capability", entitlement.capabilities),
|
|
2842
|
+
...existingDependenciesFor(
|
|
2843
|
+
"meter",
|
|
2844
|
+
[...entitlement.meters ?? [], ...limitDimensions],
|
|
2845
|
+
hasResource
|
|
2846
|
+
)
|
|
2847
|
+
];
|
|
2848
|
+
}
|
|
2849
|
+
function workflowDependsOn(workflow) {
|
|
2850
|
+
return [
|
|
2851
|
+
...dependenciesFor("capability", workflow.capabilities),
|
|
2852
|
+
...dependenciesFor("meter", [
|
|
2853
|
+
...workflow.meters ?? [],
|
|
2854
|
+
...Object.keys(workflow.estimates ?? {})
|
|
2855
|
+
])
|
|
2856
|
+
];
|
|
2857
|
+
}
|
|
2858
|
+
function featureDependsOn(file) {
|
|
2859
|
+
const meterKeys = file.routes.flatMap(
|
|
2860
|
+
(route) => routeMeterDependencyKeys(route)
|
|
2861
|
+
);
|
|
2862
|
+
const backendKeys = [
|
|
2863
|
+
...file.backend !== void 0 ? [file.backend] : [],
|
|
2864
|
+
...file.routes.flatMap(
|
|
2865
|
+
(route) => route.backend !== void 0 ? [route.backend] : []
|
|
2866
|
+
)
|
|
2867
|
+
];
|
|
2868
|
+
return [
|
|
2869
|
+
...dependenciesFor("policy", file.policies),
|
|
2870
|
+
...dependenciesFor("plan", file.plans),
|
|
2871
|
+
...dependenciesFor("meter", meterKeys),
|
|
2872
|
+
...dependenciesFor("backend", backendKeys)
|
|
2873
|
+
];
|
|
2874
|
+
}
|
|
2875
|
+
function actionDependsOn(featureKey, action) {
|
|
2876
|
+
return [
|
|
2877
|
+
resourceDependency("feature", featureKey),
|
|
2878
|
+
...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
|
|
2879
|
+
];
|
|
2880
|
+
}
|
|
2881
|
+
function planDependsOn(plan, hasResource) {
|
|
2882
|
+
const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
|
|
2883
|
+
const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
|
|
2884
|
+
const pricedMeterDimensions = (plan.meters ?? []).map(
|
|
2885
|
+
(meter) => meter.dimension
|
|
2886
|
+
);
|
|
2887
|
+
const capacityKeys = Object.keys(plan.capability_limits ?? {});
|
|
2888
|
+
return [
|
|
2889
|
+
...dependenciesFor("capability", caps),
|
|
2890
|
+
...existingDependenciesFor(
|
|
2891
|
+
"meter",
|
|
2892
|
+
[...limitDimensions, ...pricedMeterDimensions],
|
|
2893
|
+
hasResource
|
|
2894
|
+
),
|
|
2895
|
+
...existingDependenciesFor("counted_resource", capacityKeys, hasResource)
|
|
2896
|
+
];
|
|
2897
|
+
}
|
|
2898
|
+
function migrationDependsOn(migration) {
|
|
2899
|
+
return [
|
|
2900
|
+
resourceDependency("plan", migration.from.plan),
|
|
2901
|
+
resourceDependency("plan", migration.to.plan),
|
|
2902
|
+
...dependenciesFor(
|
|
2903
|
+
"plan",
|
|
2904
|
+
migration.pins?.map((pin) => pin.pinTo.plan)
|
|
2905
|
+
)
|
|
2906
|
+
];
|
|
2907
|
+
}
|
|
2908
|
+
function frontendDependsOn(manifest) {
|
|
2909
|
+
return dependenciesFor("capability", frontendCapabilityKeys(manifest));
|
|
2910
|
+
}
|
|
2911
|
+
function assertResourceDependenciesSatisfied(missing) {
|
|
2912
|
+
if (missing.length === 0) return;
|
|
2913
|
+
const details = missing.slice(0, 8).map(
|
|
2914
|
+
({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
|
|
2915
|
+
dependency
|
|
2916
|
+
)}`
|
|
2917
|
+
).join("; ");
|
|
2918
|
+
const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
|
|
2919
|
+
throw new ManifestBuilderError(
|
|
2920
|
+
`manifest has unresolved resource reference(s): ${details}${suffix}`
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
function dependenciesFor(kind, keys) {
|
|
2924
|
+
return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
|
|
2925
|
+
}
|
|
2926
|
+
function existingDependenciesFor(kind, keys, hasResource) {
|
|
2927
|
+
return [...new Set(keys)].filter((key) => hasResource(kind, key)).map((key) => resourceDependency(kind, key));
|
|
2928
|
+
}
|
|
2929
|
+
function describeResourceUrn(urn) {
|
|
2930
|
+
const parts = urn.split(":");
|
|
2931
|
+
const kind = parts[3] ?? "resource";
|
|
2932
|
+
const key = parts.slice(4).join(":");
|
|
2933
|
+
return `${kind} "${decodeURIComponent(key)}"`;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/declarations.ts
|
|
2937
|
+
function buildCapabilityLayer(key, options = {}) {
|
|
2938
|
+
return {
|
|
2939
|
+
capability: key,
|
|
2940
|
+
...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
|
|
2941
|
+
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2942
|
+
...options.includesFeatures?.length ? { includes_features: options.includesFeatures.map(keyOf) } : {},
|
|
2943
|
+
...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
|
|
2944
|
+
...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
function buildFeatureLayer(key, options, buildRoute) {
|
|
2948
|
+
const layer = {
|
|
2949
|
+
feature: key,
|
|
2950
|
+
routes: [],
|
|
2951
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2952
|
+
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2953
|
+
...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
|
|
2954
|
+
...options.upstreamOrigin !== void 0 ? { upstream: { override_origin: options.upstreamOrigin } } : {},
|
|
2955
|
+
...options.policies?.length ? { policies: options.policies.map(keyOf) } : {},
|
|
2956
|
+
...options.plans?.length ? { plans: options.plans.map(keyOf) } : {},
|
|
2957
|
+
...options.actions?.length ? {
|
|
2958
|
+
actions: options.actions.map(({ id, ...action }) => ({
|
|
2959
|
+
id,
|
|
2960
|
+
...action
|
|
2961
|
+
}))
|
|
2962
|
+
} : {},
|
|
2963
|
+
...options.rolloutKey !== void 0 || options.requiredFlags?.length ? {
|
|
2964
|
+
runtime: {
|
|
2965
|
+
...options.rolloutKey !== void 0 ? { rollout_key: options.rolloutKey } : {},
|
|
2966
|
+
...options.requiredFlags?.length ? { required_flags: options.requiredFlags } : {}
|
|
2967
|
+
}
|
|
2968
|
+
} : {}
|
|
2969
|
+
};
|
|
2970
|
+
for (const route of options.routes ?? []) {
|
|
2971
|
+
layer.routes.push(buildRoute(route.match, route));
|
|
2972
|
+
}
|
|
2973
|
+
return layer;
|
|
2974
|
+
}
|
|
2975
|
+
function buildPolicyLayer(name, options) {
|
|
2976
|
+
return {
|
|
2977
|
+
name,
|
|
2978
|
+
type: options.type,
|
|
2979
|
+
config: options.config,
|
|
2980
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
2981
|
+
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2982
|
+
...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
|
|
2983
|
+
...options.compatibleWith ? {
|
|
2984
|
+
compatible_with: {
|
|
2985
|
+
...options.compatibleWith.routeTypes ? { route_types: options.compatibleWith.routeTypes } : {},
|
|
2986
|
+
...options.compatibleWith.meters ? { meters: options.compatibleWith.meters.map(keyOf) } : {},
|
|
2987
|
+
...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
|
|
2988
|
+
}
|
|
2989
|
+
} : {}
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function buildSurfaceSpec(type, options = {}) {
|
|
2993
|
+
const key = options.key ?? type;
|
|
2994
|
+
return {
|
|
2995
|
+
key,
|
|
2996
|
+
type,
|
|
2997
|
+
...options.display !== void 0 ? { display: options.display } : {},
|
|
2998
|
+
...options.description !== void 0 ? { description: options.description } : {}
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
function buildWorkflowSpec(key, options = {}) {
|
|
3002
|
+
return {
|
|
3003
|
+
key,
|
|
3004
|
+
kind: options.kind ?? "async_job",
|
|
3005
|
+
trigger: options.trigger ?? { type: "manual" },
|
|
3006
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
3007
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3008
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
3009
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
|
|
3010
|
+
...options.estimates !== void 0 ? { estimates: options.estimates } : {},
|
|
3011
|
+
...options.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
3012
|
+
};
|
|
2415
3013
|
}
|
|
2416
|
-
function
|
|
2417
|
-
return
|
|
3014
|
+
function buildEntitlementSpec(key, options = {}) {
|
|
3015
|
+
return {
|
|
3016
|
+
key,
|
|
3017
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3018
|
+
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
3019
|
+
...options.featureGates !== void 0 ? { featureGates: options.featureGates } : {},
|
|
3020
|
+
...options.limits?.length ? { limits: options.limits } : {},
|
|
3021
|
+
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {}
|
|
3022
|
+
};
|
|
2418
3023
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
3024
|
+
function buildPlanSpec(key, options) {
|
|
3025
|
+
const { capabilityKeys, capabilityLimits, creditGrants } = normalizePlanGrants(options);
|
|
3026
|
+
const rawCaps = Array.isArray(options.raw?.capabilities) ? options.raw.capabilities.map(String) : [];
|
|
3027
|
+
const capabilities = [.../* @__PURE__ */ new Set([...capabilityKeys, ...rawCaps])];
|
|
3028
|
+
const spec = {
|
|
3029
|
+
key,
|
|
3030
|
+
name: options.name,
|
|
3031
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3032
|
+
...options.details ? { details: options.details } : {},
|
|
3033
|
+
...options.price ? {
|
|
3034
|
+
recurring_fee_cents: options.price.recurring_fee_cents,
|
|
3035
|
+
billing_interval: options.price.billing_interval,
|
|
3036
|
+
...options.price.free ? { free: true } : {}
|
|
3037
|
+
} : {},
|
|
3038
|
+
...options.meters ? { meters: options.meters } : {},
|
|
3039
|
+
...creditGrants.length ? { grants: creditGrants } : {},
|
|
3040
|
+
...options.creditPolicy ? { creditPolicy: options.creditPolicy } : {},
|
|
3041
|
+
...options.trialDays !== void 0 ? { trial_days: options.trialDays } : {},
|
|
3042
|
+
...options.maxMonthlySpendCents !== void 0 ? { max_monthly_spend_cents: options.maxMonthlySpendCents } : {},
|
|
3043
|
+
...options.minMonthlySpendCents !== void 0 ? { min_monthly_spend_cents: options.minMonthlySpendCents } : {},
|
|
3044
|
+
...options.limits ? { limits: options.limits } : {},
|
|
3045
|
+
...options.featureGates ? { featureGates: options.featureGates } : {},
|
|
3046
|
+
...Object.keys(capabilityLimits).length ? { capability_limits: capabilityLimits } : {},
|
|
3047
|
+
...options.overageBehavior !== void 0 ? { overageBehavior: options.overageBehavior } : {},
|
|
3048
|
+
...options.selfServeEnabled !== void 0 ? { selfServeEnabled: options.selfServeEnabled } : {},
|
|
3049
|
+
...options.legacy !== void 0 ? { legacy: options.legacy } : {},
|
|
3050
|
+
...options.archive ? { archive: options.archive } : {},
|
|
3051
|
+
...options.raw ?? {},
|
|
3052
|
+
...capabilities.length ? { capabilities } : {}
|
|
3053
|
+
};
|
|
3054
|
+
return spec;
|
|
2430
3055
|
}
|
|
2431
|
-
function
|
|
2432
|
-
|
|
3056
|
+
function normalizePlanGrants(options) {
|
|
3057
|
+
const capabilityKeys = (options.capabilities ?? []).map(keyOf);
|
|
3058
|
+
const capabilityLimits = {
|
|
3059
|
+
...options.capabilityLimits ?? {}
|
|
3060
|
+
};
|
|
3061
|
+
const creditGrants = [];
|
|
3062
|
+
for (const grant of options.grants ?? []) {
|
|
3063
|
+
if (isCapabilityGrant(grant)) {
|
|
3064
|
+
if (!capabilityKeys.includes(grant.capability)) {
|
|
3065
|
+
capabilityKeys.push(grant.capability);
|
|
3066
|
+
}
|
|
3067
|
+
Object.assign(capabilityLimits, grant.limits ?? {});
|
|
3068
|
+
} else {
|
|
3069
|
+
creditGrants.push(grant);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return { capabilityKeys, capabilityLimits, creditGrants };
|
|
2433
3073
|
}
|
|
2434
|
-
function
|
|
2435
|
-
return
|
|
3074
|
+
function buildMigrationDecl(id, options) {
|
|
3075
|
+
return {
|
|
3076
|
+
id,
|
|
3077
|
+
from: normalizeMigrationPlanRef(options.from),
|
|
3078
|
+
to: normalizeMigrationTargetRef(options.to),
|
|
3079
|
+
newCustomers: options.newCustomers ?? "immediate",
|
|
3080
|
+
existingCustomers: options.existingCustomers,
|
|
3081
|
+
...options.pins?.length ? {
|
|
3082
|
+
pins: options.pins.map((pin) => ({
|
|
3083
|
+
subscriber: pin.subscriber,
|
|
3084
|
+
pinTo: {
|
|
3085
|
+
plan: keyOf(pin.pinTo.plan),
|
|
3086
|
+
version: pin.pinTo.version
|
|
3087
|
+
},
|
|
3088
|
+
...pin.until !== void 0 ? { until: pin.until } : {},
|
|
3089
|
+
...pin.notes !== void 0 ? { notes: pin.notes } : {}
|
|
3090
|
+
}))
|
|
3091
|
+
} : {}
|
|
3092
|
+
};
|
|
2436
3093
|
}
|
|
2437
|
-
function
|
|
2438
|
-
const
|
|
2439
|
-
const
|
|
2440
|
-
|
|
2441
|
-
if (!trimmed.startsWith("/")) {
|
|
3094
|
+
function assertUniqueMigrationIds(migrations) {
|
|
3095
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3096
|
+
for (const migration of migrations) {
|
|
3097
|
+
if (seen.has(migration.id)) {
|
|
2442
3098
|
throw new ManifestBuilderError(
|
|
2443
|
-
`
|
|
3099
|
+
`duplicate migration "${migration.id}" \u2014 each migration id must be declared once`
|
|
2444
3100
|
);
|
|
2445
3101
|
}
|
|
2446
|
-
|
|
2447
|
-
}
|
|
2448
|
-
const method = trimmed.slice(0, space).toUpperCase();
|
|
2449
|
-
const path = trimmed.slice(space + 1).trim();
|
|
2450
|
-
const methods = [
|
|
2451
|
-
"GET",
|
|
2452
|
-
"POST",
|
|
2453
|
-
"PUT",
|
|
2454
|
-
"PATCH",
|
|
2455
|
-
"DELETE",
|
|
2456
|
-
"HEAD",
|
|
2457
|
-
"OPTIONS",
|
|
2458
|
-
"*"
|
|
2459
|
-
];
|
|
2460
|
-
if (!methods.includes(method)) {
|
|
2461
|
-
throw new ManifestBuilderError(
|
|
2462
|
-
`route "${match}": unknown HTTP method "${method}"`
|
|
2463
|
-
);
|
|
2464
|
-
}
|
|
2465
|
-
if (!path.startsWith("/")) {
|
|
2466
|
-
throw new ManifestBuilderError(
|
|
2467
|
-
`route "${match}": path must start with "/"`
|
|
2468
|
-
);
|
|
3102
|
+
seen.add(migration.id);
|
|
2469
3103
|
}
|
|
2470
|
-
return { method, path };
|
|
2471
3104
|
}
|
|
3105
|
+
function normalizeMigrationPlanRef(ref) {
|
|
3106
|
+
return {
|
|
3107
|
+
plan: keyOf(ref.plan),
|
|
3108
|
+
...ref.version !== void 0 ? { version: ref.version } : {}
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
function normalizeMigrationTargetRef(ref) {
|
|
3112
|
+
return {
|
|
3113
|
+
plan: keyOf(ref.plan),
|
|
3114
|
+
version: ref.version ?? "head"
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// src/product.ts
|
|
3119
|
+
var PRODUCT_BRAND = Symbol.for("farthershore.product.product");
|
|
3120
|
+
var PRODUCT_MANIFEST_COMPILER = Symbol.for(
|
|
3121
|
+
"farthershore.product.manifestCompiler"
|
|
3122
|
+
);
|
|
2472
3123
|
var Product = class {
|
|
2473
3124
|
[PRODUCT_BRAND] = true;
|
|
2474
3125
|
name;
|
|
2475
3126
|
options;
|
|
2476
3127
|
graph = new ManifestResourceGraph();
|
|
2477
3128
|
defaultMeterCosts = [];
|
|
2478
|
-
frontendManifest;
|
|
2479
3129
|
productPatch = {};
|
|
2480
3130
|
/** Sugar for binding API routes to features. */
|
|
2481
3131
|
api;
|
|
2482
3132
|
frontend;
|
|
2483
3133
|
lifecycle;
|
|
2484
|
-
offering;
|
|
2485
3134
|
/** Escape hatches — raw platform-schema JSON, validated by the compiler. */
|
|
2486
3135
|
raw;
|
|
2487
3136
|
constructor(name, options) {
|
|
@@ -2502,7 +3151,7 @@ var Product = class {
|
|
|
2502
3151
|
this.api = {
|
|
2503
3152
|
route: (match, options2) => {
|
|
2504
3153
|
const featureKey = keyOf(options2.feature);
|
|
2505
|
-
const file = this.
|
|
3154
|
+
const file = this.getFeatureLayer(featureKey);
|
|
2506
3155
|
if (!file) {
|
|
2507
3156
|
throw new ManifestBuilderError(
|
|
2508
3157
|
`api.route("${match}"): feature "${featureKey}" is not declared \u2014 call product.feature("${featureKey}", \u2026) first`
|
|
@@ -2516,41 +3165,17 @@ var Product = class {
|
|
|
2516
3165
|
this.frontend = {
|
|
2517
3166
|
nav: (items) => {
|
|
2518
3167
|
const manifest = this.ensureFrontendManifest();
|
|
2519
|
-
manifest
|
|
2520
|
-
label: item.label,
|
|
2521
|
-
path: item.path,
|
|
2522
|
-
...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
|
|
2523
|
-
}));
|
|
3168
|
+
setFrontendNav(manifest, items);
|
|
2524
3169
|
this.syncFrontendGraphNode(manifest);
|
|
2525
3170
|
return this;
|
|
2526
3171
|
},
|
|
2527
3172
|
page: (path, options2) => {
|
|
2528
3173
|
const manifest = this.ensureFrontendManifest();
|
|
2529
|
-
|
|
2530
|
-
throw new ManifestBuilderError(
|
|
2531
|
-
`duplicate frontend page "${path}" \u2014 each frontend page path must be declared once`
|
|
2532
|
-
);
|
|
2533
|
-
}
|
|
2534
|
-
manifest.pages ??= [];
|
|
2535
|
-
manifest.pages.push({
|
|
2536
|
-
path,
|
|
2537
|
-
title: options2.title,
|
|
2538
|
-
requiresAuth: options2.requiresAuth,
|
|
2539
|
-
...options2.capability !== void 0 ? { capability: keyOf(options2.capability) } : {},
|
|
2540
|
-
...options2.components?.length ? {
|
|
2541
|
-
components: options2.components.map((component) => ({
|
|
2542
|
-
component: component.component,
|
|
2543
|
-
...component.props !== void 0 ? { props: component.props } : {},
|
|
2544
|
-
...component.capability !== void 0 ? { capability: keyOf(component.capability) } : {},
|
|
2545
|
-
...component.gateMode !== void 0 ? { gateMode: component.gateMode } : {}
|
|
2546
|
-
}))
|
|
2547
|
-
} : {}
|
|
2548
|
-
});
|
|
3174
|
+
addFrontendPage(manifest, path, options2);
|
|
2549
3175
|
this.syncFrontendGraphNode(manifest);
|
|
2550
3176
|
return this;
|
|
2551
3177
|
},
|
|
2552
3178
|
manifest: (manifest) => {
|
|
2553
|
-
this.frontendManifest = manifest;
|
|
2554
3179
|
this.syncFrontendGraphNode(manifest);
|
|
2555
3180
|
return this;
|
|
2556
3181
|
}
|
|
@@ -2558,77 +3183,60 @@ var Product = class {
|
|
|
2558
3183
|
this.lifecycle = {
|
|
2559
3184
|
migration: (id, options2) => {
|
|
2560
3185
|
this.assertNewKey("lifecycle_migration", id, "migration");
|
|
2561
|
-
const migration =
|
|
2562
|
-
id,
|
|
2563
|
-
from: this.normalizeMigrationPlanRef(options2.from),
|
|
2564
|
-
to: this.normalizeMigrationTargetRef(options2.to),
|
|
2565
|
-
newCustomers: options2.newCustomers ?? "immediate",
|
|
2566
|
-
existingCustomers: options2.existingCustomers,
|
|
2567
|
-
...options2.pins?.length ? {
|
|
2568
|
-
pins: options2.pins.map((pin) => ({
|
|
2569
|
-
subscriber: pin.subscriber,
|
|
2570
|
-
pinTo: {
|
|
2571
|
-
plan: keyOf(pin.pinTo.plan),
|
|
2572
|
-
version: pin.pinTo.version
|
|
2573
|
-
},
|
|
2574
|
-
...pin.until !== void 0 ? { until: pin.until } : {},
|
|
2575
|
-
...pin.notes !== void 0 ? { notes: pin.notes } : {}
|
|
2576
|
-
}))
|
|
2577
|
-
} : {}
|
|
2578
|
-
};
|
|
3186
|
+
const migration = buildMigrationDecl(id, options2);
|
|
2579
3187
|
this.graph.register(
|
|
2580
3188
|
"lifecycle_migration",
|
|
2581
3189
|
id,
|
|
2582
3190
|
migration,
|
|
2583
|
-
|
|
3191
|
+
migrationDependsOn(migration)
|
|
2584
3192
|
);
|
|
2585
3193
|
return this;
|
|
2586
3194
|
},
|
|
2587
3195
|
migrations: (migrations) => {
|
|
2588
|
-
|
|
3196
|
+
assertUniqueMigrationIds(migrations);
|
|
2589
3197
|
this.graph.clearKind("lifecycle_migration");
|
|
2590
3198
|
for (const migration of migrations) {
|
|
2591
3199
|
this.graph.register(
|
|
2592
3200
|
"lifecycle_migration",
|
|
2593
3201
|
migration.id,
|
|
2594
3202
|
migration,
|
|
2595
|
-
|
|
3203
|
+
migrationDependsOn(migration)
|
|
2596
3204
|
);
|
|
2597
3205
|
}
|
|
2598
3206
|
return this;
|
|
2599
3207
|
}
|
|
2600
3208
|
};
|
|
2601
|
-
this.offering = {
|
|
2602
|
-
plan: (key, options2) => this.plan(key, options2)
|
|
2603
|
-
};
|
|
2604
3209
|
this.raw = {
|
|
2605
3210
|
productPatch: (patch) => {
|
|
2606
|
-
this.productPatch =
|
|
3211
|
+
this.productPatch = mergeProductPatch(this.productPatch, patch);
|
|
2607
3212
|
return this;
|
|
2608
3213
|
},
|
|
2609
3214
|
plan: (spec) => {
|
|
2610
3215
|
this.assertNewKey("plan", spec.key, "plan");
|
|
2611
|
-
this.graph.register(
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
3216
|
+
this.graph.register(
|
|
3217
|
+
"plan",
|
|
3218
|
+
spec.key,
|
|
3219
|
+
spec,
|
|
3220
|
+
planDependsOn(
|
|
3221
|
+
spec,
|
|
3222
|
+
(kind, dependencyKey) => this.graph.has(kind, dependencyKey)
|
|
3223
|
+
)
|
|
3224
|
+
);
|
|
2617
3225
|
return this;
|
|
2618
3226
|
},
|
|
2619
|
-
|
|
2620
|
-
this.assertNewKey("
|
|
2621
|
-
this.
|
|
3227
|
+
routeLayer: (layer) => {
|
|
3228
|
+
this.assertNewKey("feature", layer.feature, "feature");
|
|
3229
|
+
this.registerFeatureLayer(layer);
|
|
2622
3230
|
return this;
|
|
2623
3231
|
},
|
|
2624
|
-
|
|
2625
|
-
this.assertNewKey("
|
|
2626
|
-
this.graph.register("
|
|
3232
|
+
policyLayer: (layer) => {
|
|
3233
|
+
this.assertNewKey("policy", layer.name, "policy");
|
|
3234
|
+
this.graph.register("policy", layer.name, layer);
|
|
2627
3235
|
return this;
|
|
2628
3236
|
},
|
|
2629
|
-
|
|
2630
|
-
this.
|
|
2631
|
-
this.
|
|
3237
|
+
capabilityLayer: (layer) => {
|
|
3238
|
+
this.assertNewKey("capability", layer.capability, "capability");
|
|
3239
|
+
this.graph.register("capability", layer.capability, layer);
|
|
2632
3240
|
return this;
|
|
2633
3241
|
}
|
|
2634
3242
|
};
|
|
@@ -2641,7 +3249,7 @@ var Product = class {
|
|
|
2641
3249
|
display: meterOptions.display ?? displayFromKey(key),
|
|
2642
3250
|
...meterOptions
|
|
2643
3251
|
});
|
|
2644
|
-
const ref =
|
|
3252
|
+
const ref = makeMeterRef(key);
|
|
2645
3253
|
if (routeDefault !== void 0) {
|
|
2646
3254
|
this.defaultMeterCosts.push(
|
|
2647
3255
|
this.normalizeMeterCost(ref.fixed(routeDefault))
|
|
@@ -2649,6 +3257,18 @@ var Product = class {
|
|
|
2649
3257
|
}
|
|
2650
3258
|
return ref;
|
|
2651
3259
|
}
|
|
3260
|
+
/**
|
|
3261
|
+
* BYO-Backend V1 — declare a first-class backend (an always-running HTTP
|
|
3262
|
+
* container Farther Shore wraps). Products may declare MULTIPLE backends;
|
|
3263
|
+
* routes bind to one via `route(..., { backend })`. A product with exactly
|
|
3264
|
+
* one backend (or exactly one marked `default: true`) makes it the implicit
|
|
3265
|
+
* default, so single-backend products stay zero-config.
|
|
3266
|
+
*/
|
|
3267
|
+
backend(id, options = {}) {
|
|
3268
|
+
this.assertNewKey("backend", id, "backend");
|
|
3269
|
+
this.graph.register("backend", id, createBackendNode(id, options));
|
|
3270
|
+
return { kind: "backend", key: id };
|
|
3271
|
+
}
|
|
2652
3272
|
requests(options = {}) {
|
|
2653
3273
|
return this.meter("requests", {
|
|
2654
3274
|
display: options.display ?? "Requests",
|
|
@@ -2674,20 +3294,8 @@ var Product = class {
|
|
|
2674
3294
|
}
|
|
2675
3295
|
capability(key, options = {}) {
|
|
2676
3296
|
this.assertNewKey("capability", key, "capability");
|
|
2677
|
-
const
|
|
2678
|
-
|
|
2679
|
-
...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
|
|
2680
|
-
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2681
|
-
...options.includesFeatures?.length ? { includes_features: options.includesFeatures.map(keyOf) } : {},
|
|
2682
|
-
...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
|
|
2683
|
-
...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
|
|
2684
|
-
};
|
|
2685
|
-
this.graph.register(
|
|
2686
|
-
"capability",
|
|
2687
|
-
key,
|
|
2688
|
-
file,
|
|
2689
|
-
this.capabilityDependsOn(file)
|
|
2690
|
-
);
|
|
3297
|
+
const layer = buildCapabilityLayer(key, options);
|
|
3298
|
+
this.graph.register("capability", key, layer, capabilityDependsOn(layer));
|
|
2691
3299
|
return {
|
|
2692
3300
|
kind: "capability",
|
|
2693
3301
|
key,
|
|
@@ -2700,52 +3308,31 @@ var Product = class {
|
|
|
2700
3308
|
}
|
|
2701
3309
|
feature(key, options = {}) {
|
|
2702
3310
|
this.assertNewKey("feature", key, "feature");
|
|
2703
|
-
const
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
...options.upstreamOrigin !== void 0 ? { upstream: { override_origin: options.upstreamOrigin } } : {},
|
|
2710
|
-
...options.policies?.length ? { policies: options.policies.map(keyOf) } : {},
|
|
2711
|
-
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2712
|
-
...options.plans?.length ? { plans: options.plans.map(keyOf) } : {},
|
|
2713
|
-
...options.actions?.length ? {
|
|
2714
|
-
actions: options.actions.map(({ id, ...action }) => ({
|
|
2715
|
-
id,
|
|
2716
|
-
...action
|
|
2717
|
-
}))
|
|
2718
|
-
} : {},
|
|
2719
|
-
...options.rolloutKey !== void 0 || options.requiredFlags?.length ? {
|
|
2720
|
-
runtime: {
|
|
2721
|
-
...options.rolloutKey !== void 0 ? { rollout_key: options.rolloutKey } : {},
|
|
2722
|
-
...options.requiredFlags?.length ? { required_flags: options.requiredFlags } : {}
|
|
2723
|
-
}
|
|
2724
|
-
} : {}
|
|
2725
|
-
};
|
|
2726
|
-
for (const route of options.routes ?? []) {
|
|
2727
|
-
file.routes.push(this.buildRoute(route.match, route));
|
|
2728
|
-
}
|
|
2729
|
-
this.registerFeatureFile(file);
|
|
3311
|
+
const layer = buildFeatureLayer(
|
|
3312
|
+
key,
|
|
3313
|
+
options,
|
|
3314
|
+
(match, routeOptions) => this.buildRoute(match, routeOptions)
|
|
3315
|
+
);
|
|
3316
|
+
this.registerFeatureLayer(layer);
|
|
2730
3317
|
const ref = {
|
|
2731
3318
|
kind: "feature",
|
|
2732
3319
|
key,
|
|
2733
3320
|
action: (id, actionOptions) => {
|
|
2734
|
-
|
|
2735
|
-
if (
|
|
3321
|
+
layer.actions ??= [];
|
|
3322
|
+
if (layer.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
|
|
2736
3323
|
throw new ManifestBuilderError(
|
|
2737
3324
|
`duplicate action "${id}" \u2014 each action id must be declared once`
|
|
2738
3325
|
);
|
|
2739
3326
|
}
|
|
2740
3327
|
const action = { id, ...actionOptions };
|
|
2741
|
-
|
|
3328
|
+
layer.actions.push(action);
|
|
2742
3329
|
this.registerAction(key, action);
|
|
2743
|
-
this.syncFeatureGraphNode(
|
|
3330
|
+
this.syncFeatureGraphNode(layer);
|
|
2744
3331
|
return { kind: "action", key: id };
|
|
2745
3332
|
},
|
|
2746
3333
|
route: (match, routeOptions) => {
|
|
2747
|
-
|
|
2748
|
-
this.syncFeatureGraphNode(
|
|
3334
|
+
layer.routes.push(this.buildRoute(match, routeOptions ?? {}));
|
|
3335
|
+
this.syncFeatureGraphNode(layer);
|
|
2749
3336
|
return ref;
|
|
2750
3337
|
}
|
|
2751
3338
|
};
|
|
@@ -2753,126 +3340,48 @@ var Product = class {
|
|
|
2753
3340
|
}
|
|
2754
3341
|
policy(name, options) {
|
|
2755
3342
|
this.assertNewKey("policy", name, "policy");
|
|
2756
|
-
const
|
|
2757
|
-
|
|
2758
|
-
type: options.type,
|
|
2759
|
-
config: options.config,
|
|
2760
|
-
...options.description !== void 0 ? { description: options.description } : {},
|
|
2761
|
-
...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
|
|
2762
|
-
...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
|
|
2763
|
-
...options.compatibleWith ? {
|
|
2764
|
-
compatible_with: {
|
|
2765
|
-
...options.compatibleWith.routeTypes ? { route_types: options.compatibleWith.routeTypes } : {},
|
|
2766
|
-
...options.compatibleWith.meters ? { meters: options.compatibleWith.meters.map(keyOf) } : {},
|
|
2767
|
-
...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
|
|
2768
|
-
}
|
|
2769
|
-
} : {}
|
|
2770
|
-
};
|
|
2771
|
-
this.graph.register("policy", name, file, this.policyDependsOn(file));
|
|
3343
|
+
const layer = buildPolicyLayer(name, options);
|
|
3344
|
+
this.graph.register("policy", name, layer, policyDependsOn(layer));
|
|
2772
3345
|
return { kind: "policy", key: name };
|
|
2773
3346
|
}
|
|
2774
3347
|
surface(type, options = {}) {
|
|
2775
|
-
const
|
|
2776
|
-
this.assertNewKey("surface", key, "surface");
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
type,
|
|
2780
|
-
...options.display !== void 0 ? { display: options.display } : {},
|
|
2781
|
-
...options.description !== void 0 ? { description: options.description } : {}
|
|
2782
|
-
};
|
|
2783
|
-
this.graph.register("surface", key, surface);
|
|
2784
|
-
return { kind: "surface", key };
|
|
3348
|
+
const surface = buildSurfaceSpec(type, options);
|
|
3349
|
+
this.assertNewKey("surface", surface.key, "surface");
|
|
3350
|
+
this.graph.register("surface", surface.key, surface);
|
|
3351
|
+
return { kind: "surface", key: surface.key };
|
|
2785
3352
|
}
|
|
2786
3353
|
workflow(key, options = {}) {
|
|
2787
3354
|
this.assertNewKey("workflow", key, "workflow");
|
|
2788
|
-
const workflow =
|
|
2789
|
-
|
|
2790
|
-
kind: options.kind ?? "async_job",
|
|
2791
|
-
trigger: options.trigger ?? { type: "manual" },
|
|
2792
|
-
...options.title !== void 0 ? { title: options.title } : {},
|
|
2793
|
-
...options.description !== void 0 ? { description: options.description } : {},
|
|
2794
|
-
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2795
|
-
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
|
|
2796
|
-
...options.estimates !== void 0 ? { estimates: options.estimates } : {},
|
|
2797
|
-
...options.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
2798
|
-
};
|
|
2799
|
-
this.graph.register(
|
|
2800
|
-
"workflow",
|
|
2801
|
-
key,
|
|
2802
|
-
workflow,
|
|
2803
|
-
this.workflowDependsOn(workflow)
|
|
2804
|
-
);
|
|
3355
|
+
const workflow = buildWorkflowSpec(key, options);
|
|
3356
|
+
this.graph.register("workflow", key, workflow, workflowDependsOn(workflow));
|
|
2805
3357
|
return { kind: "workflow", key };
|
|
2806
3358
|
}
|
|
2807
3359
|
entitlement(key, options = {}) {
|
|
2808
3360
|
this.assertNewKey("entitlement", key, "entitlement");
|
|
2809
|
-
const entitlement =
|
|
2810
|
-
key,
|
|
2811
|
-
...options.description !== void 0 ? { description: options.description } : {},
|
|
2812
|
-
...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
|
|
2813
|
-
...options.featureGates !== void 0 ? { featureGates: options.featureGates } : {},
|
|
2814
|
-
...options.limits?.length ? { limits: options.limits } : {},
|
|
2815
|
-
...options.meters?.length ? { meters: options.meters.map(keyOf) } : {}
|
|
2816
|
-
};
|
|
3361
|
+
const entitlement = buildEntitlementSpec(key, options);
|
|
2817
3362
|
this.graph.register(
|
|
2818
3363
|
"entitlement",
|
|
2819
3364
|
key,
|
|
2820
3365
|
entitlement,
|
|
2821
|
-
|
|
3366
|
+
entitlementDependsOn(
|
|
3367
|
+
entitlement,
|
|
3368
|
+
(kind, dependencyKey) => this.graph.has(kind, dependencyKey)
|
|
3369
|
+
)
|
|
2822
3370
|
);
|
|
2823
3371
|
return { kind: "entitlement", key };
|
|
2824
3372
|
}
|
|
2825
3373
|
plan(key, options) {
|
|
2826
3374
|
this.assertNewKey("plan", key, "plan");
|
|
2827
|
-
const
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
};
|
|
2831
|
-
const creditGrants = [];
|
|
2832
|
-
for (const grant of options.grants ?? []) {
|
|
2833
|
-
if (isCapabilityGrant(grant)) {
|
|
2834
|
-
if (!capabilityKeys.includes(grant.capability)) {
|
|
2835
|
-
capabilityKeys.push(grant.capability);
|
|
2836
|
-
}
|
|
2837
|
-
Object.assign(capabilityLimits, grant.limits ?? {});
|
|
2838
|
-
} else {
|
|
2839
|
-
creditGrants.push(grant);
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
const spec = {
|
|
3375
|
+
const spec = buildPlanSpec(key, options);
|
|
3376
|
+
this.graph.register(
|
|
3377
|
+
"plan",
|
|
2843
3378
|
key,
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
...options.price.free ? { free: true } : {}
|
|
2851
|
-
} : {},
|
|
2852
|
-
...options.meters ? { meters: options.meters } : {},
|
|
2853
|
-
...creditGrants.length ? { grants: creditGrants } : {},
|
|
2854
|
-
...options.trialDays !== void 0 ? { trial_days: options.trialDays } : {},
|
|
2855
|
-
...options.maxMonthlySpendCents !== void 0 ? { max_monthly_spend_cents: options.maxMonthlySpendCents } : {},
|
|
2856
|
-
...options.minMonthlySpendCents !== void 0 ? { min_monthly_spend_cents: options.minMonthlySpendCents } : {},
|
|
2857
|
-
...options.limits ? { limits: options.limits } : {},
|
|
2858
|
-
...options.featureGates ? { featureGates: options.featureGates } : {},
|
|
2859
|
-
...Object.keys(capabilityLimits).length ? { capability_limits: capabilityLimits } : {},
|
|
2860
|
-
...options.overageBehavior !== void 0 ? { overageBehavior: options.overageBehavior } : {},
|
|
2861
|
-
...options.selfServeEnabled !== void 0 ? { selfServeEnabled: options.selfServeEnabled } : {},
|
|
2862
|
-
...options.legacy !== void 0 ? { legacy: options.legacy } : {},
|
|
2863
|
-
...options.archive ? { archive: options.archive } : {},
|
|
2864
|
-
...options.raw ?? {}
|
|
2865
|
-
};
|
|
2866
|
-
const rawCaps = Array.isArray(
|
|
2867
|
-
spec.capabilities
|
|
2868
|
-
) ? spec.capabilities.map(
|
|
2869
|
-
String
|
|
2870
|
-
) : [];
|
|
2871
|
-
const mergedCaps = [.../* @__PURE__ */ new Set([...capabilityKeys, ...rawCaps])];
|
|
2872
|
-
if (mergedCaps.length) {
|
|
2873
|
-
spec.capabilities = mergedCaps;
|
|
2874
|
-
}
|
|
2875
|
-
this.graph.register("plan", key, spec, this.planDependsOn(spec));
|
|
3379
|
+
spec,
|
|
3380
|
+
planDependsOn(
|
|
3381
|
+
spec,
|
|
3382
|
+
(kind, dependencyKey) => this.graph.has(kind, dependencyKey)
|
|
3383
|
+
)
|
|
3384
|
+
);
|
|
2876
3385
|
return { kind: "plan", key };
|
|
2877
3386
|
}
|
|
2878
3387
|
use(...modules) {
|
|
@@ -2888,8 +3397,16 @@ var Product = class {
|
|
|
2888
3397
|
/** @internal Internal platform compiler entrypoint. Builder code exports the
|
|
2889
3398
|
* Product; the bot/CLI/build-runner decide when to compile and apply it. */
|
|
2890
3399
|
[PRODUCT_MANIFEST_COMPILER]() {
|
|
2891
|
-
const routes = this.
|
|
2892
|
-
|
|
3400
|
+
const routes = this.materializeFeatureLayers();
|
|
3401
|
+
assertRouteMeteringValid(
|
|
3402
|
+
routes,
|
|
3403
|
+
this.graph.values("meter")
|
|
3404
|
+
);
|
|
3405
|
+
assertBackendBindingsValid(
|
|
3406
|
+
routes,
|
|
3407
|
+
this.graph.values("backend"),
|
|
3408
|
+
this.graph.values("meter")
|
|
3409
|
+
);
|
|
2893
3410
|
this.assertGraphDependenciesSatisfied();
|
|
2894
3411
|
const candidate = {
|
|
2895
3412
|
irVersion: 1,
|
|
@@ -2898,245 +3415,67 @@ var Product = class {
|
|
|
2898
3415
|
routes,
|
|
2899
3416
|
policies: this.graph.sortedValues(
|
|
2900
3417
|
"policy",
|
|
2901
|
-
(
|
|
3418
|
+
(layer) => layer.name
|
|
2902
3419
|
),
|
|
2903
3420
|
capabilities: this.graph.sortedValues(
|
|
2904
3421
|
"capability",
|
|
2905
|
-
(
|
|
2906
|
-
)
|
|
2907
|
-
runtime: { rollout: null, flags: null, migrations: null }
|
|
3422
|
+
(layer) => layer.capability
|
|
3423
|
+
)
|
|
2908
3424
|
};
|
|
2909
3425
|
const result = validateManifestIr(candidate);
|
|
2910
3426
|
if (!result.ok) throw new ManifestValidationError(result.issues);
|
|
2911
3427
|
return { ir: result.ir, irHash: result.irHash };
|
|
2912
3428
|
}
|
|
2913
3429
|
buildProductSpec() {
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
meters: this.buildMeterDefinitions(),
|
|
2933
|
-
...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
|
|
2934
|
-
},
|
|
2935
|
-
...options.billing !== void 0 ? { billing: options.billing } : {},
|
|
2936
|
-
...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
|
|
2937
|
-
...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
|
|
2938
|
-
...this.graph.values("surface").length ? {
|
|
2939
|
-
surfaces: this.graph.sortedValues(
|
|
2940
|
-
"surface",
|
|
2941
|
-
(surface) => surface.key
|
|
2942
|
-
)
|
|
2943
|
-
} : {},
|
|
2944
|
-
...this.graph.values("entitlement").length ? {
|
|
2945
|
-
entitlements: this.graph.sortedValues(
|
|
2946
|
-
"entitlement",
|
|
2947
|
-
(entitlement) => entitlement.key
|
|
2948
|
-
)
|
|
2949
|
-
} : {},
|
|
2950
|
-
...this.graph.values("workflow").length ? {
|
|
2951
|
-
workflows: this.graph.sortedValues(
|
|
2952
|
-
"workflow",
|
|
2953
|
-
(workflow) => workflow.key
|
|
2954
|
-
)
|
|
2955
|
-
} : {},
|
|
2956
|
-
...this.graph.has("frontend", "manifest") ? {
|
|
2957
|
-
frontend: this.graph.get(
|
|
2958
|
-
"frontend",
|
|
2959
|
-
"manifest"
|
|
2960
|
-
)?.value
|
|
2961
|
-
} : {},
|
|
2962
|
-
...this.graph.values("lifecycle_migration").length ? {
|
|
2963
|
-
migrations: this.graph.sortedValues(
|
|
2964
|
-
"lifecycle_migration",
|
|
2965
|
-
(migration) => migration.id
|
|
2966
|
-
)
|
|
2967
|
-
} : {},
|
|
2968
|
-
...this.graph.values("counted_resource").length ? {
|
|
2969
|
-
resources: this.graph.sortedValues(
|
|
2970
|
-
"counted_resource",
|
|
2971
|
-
(resource) => resource.name
|
|
2972
|
-
)
|
|
2973
|
-
} : {},
|
|
2974
|
-
plans: this.graph.sortedValues("plan", (plan) => plan.key)
|
|
2975
|
-
};
|
|
2976
|
-
return deepMerge(
|
|
2977
|
-
base,
|
|
2978
|
-
this.productPatch
|
|
2979
|
-
);
|
|
2980
|
-
}
|
|
2981
|
-
buildMeterDefinitions() {
|
|
2982
|
-
const routeValueMeters = this.routeValueMeterKeys();
|
|
2983
|
-
return this.graph.sortedValues("meter", (meter) => meter.key).map((meter) => {
|
|
2984
|
-
if (meter.aggregation !== void 0) return meter;
|
|
2985
|
-
if (!routeValueMeters.has(meter.key)) return meter;
|
|
2986
|
-
return { ...meter, aggregation: "SUM" };
|
|
3430
|
+
return assembleProductSpec({
|
|
3431
|
+
name: this.name,
|
|
3432
|
+
options: this.options,
|
|
3433
|
+
productPatch: this.productPatch,
|
|
3434
|
+
meters: this.graph.values("meter"),
|
|
3435
|
+
defaultMeterCosts: this.defaultMeterCosts,
|
|
3436
|
+
backends: this.graph.values("backend"),
|
|
3437
|
+
surfaces: this.graph.values("surface"),
|
|
3438
|
+
entitlements: this.graph.values("entitlement"),
|
|
3439
|
+
workflows: this.graph.values("workflow"),
|
|
3440
|
+
frontendManifest: this.graph.get(
|
|
3441
|
+
"frontend",
|
|
3442
|
+
"manifest"
|
|
3443
|
+
)?.value,
|
|
3444
|
+
migrations: this.graph.values("lifecycle_migration"),
|
|
3445
|
+
resources: this.graph.values("counted_resource"),
|
|
3446
|
+
plans: this.graph.values("plan"),
|
|
3447
|
+
featureLayers: this.graph.values("feature")
|
|
2987
3448
|
});
|
|
2988
3449
|
}
|
|
2989
|
-
routeValueMeterKeys() {
|
|
2990
|
-
const keys = /* @__PURE__ */ new Set();
|
|
2991
|
-
for (const cost of this.defaultMeterCosts) {
|
|
2992
|
-
keys.add(cost.meter);
|
|
2993
|
-
}
|
|
2994
|
-
for (const file of this.graph.values("feature")) {
|
|
2995
|
-
for (const route of file.routes) {
|
|
2996
|
-
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
2997
|
-
keys.add(meter);
|
|
2998
|
-
}
|
|
2999
|
-
for (const meter of route.metering?.reports ?? []) {
|
|
3000
|
-
keys.add(meter);
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
for (const workflow of this.graph.values("workflow")) {
|
|
3005
|
-
for (const meter of workflow.meters ?? []) keys.add(meter);
|
|
3006
|
-
for (const meter of Object.keys(workflow.estimates ?? {}))
|
|
3007
|
-
keys.add(meter);
|
|
3008
|
-
}
|
|
3009
|
-
return keys;
|
|
3010
|
-
}
|
|
3011
3450
|
buildRoute(match, options) {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
...metering ? { metering } : {},
|
|
3017
|
-
...options.unmetered !== void 0 ? { unmetered: options.unmetered } : {},
|
|
3018
|
-
...options.inheritDefaultMeters !== void 0 ? { inheritDefaultMeters: options.inheritDefaultMeters } : {},
|
|
3019
|
-
...options.action !== void 0 ? { action: keyOf(options.action) } : {}
|
|
3020
|
-
};
|
|
3021
|
-
}
|
|
3022
|
-
makeMeterRef(key) {
|
|
3023
|
-
return {
|
|
3024
|
-
kind: "meter",
|
|
3025
|
-
key,
|
|
3026
|
-
fixed: (value) => ({ kind: "meter_cost", meter: key, value }),
|
|
3027
|
-
estimate: (value) => ({ kind: "meter_cost", meter: key, value })
|
|
3028
|
-
};
|
|
3451
|
+
return buildRouteDefinition(match, options, {
|
|
3452
|
+
normalizeMeterCost: (cost) => this.normalizeMeterCost(cost),
|
|
3453
|
+
getMeterDefinition: (key) => this.graph.get("meter", key)?.value
|
|
3454
|
+
});
|
|
3029
3455
|
}
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
const reports = [
|
|
3037
|
-
...new Set(this.normalizeMeterRefs(options.reports ?? []))
|
|
3038
|
-
];
|
|
3039
|
-
const estimates = {};
|
|
3040
|
-
for (const meter of reports) {
|
|
3041
|
-
const definition = this.graph.get(
|
|
3042
|
-
"meter",
|
|
3043
|
-
meter
|
|
3044
|
-
)?.value;
|
|
3045
|
-
if (typeof definition?.estimate === "number") {
|
|
3046
|
-
estimates[meter] = definition.estimate;
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
for (const [meter, value] of Object.entries(options.estimates ?? {})) {
|
|
3050
|
-
estimates[meter] = value;
|
|
3051
|
-
}
|
|
3052
|
-
const out = {};
|
|
3053
|
-
if (Object.keys(defaults).length) out.defaults = defaults;
|
|
3054
|
-
if (reports.length) out.reports = reports;
|
|
3055
|
-
if (Object.keys(estimates).length) out.estimates = estimates;
|
|
3056
|
-
if (options.onStatusCodes !== void 0)
|
|
3057
|
-
out.onStatusCodes = options.onStatusCodes;
|
|
3058
|
-
return Object.keys(out).length ? out : void 0;
|
|
3059
|
-
}
|
|
3060
|
-
materializeFeatureFiles() {
|
|
3061
|
-
return this.graph.sortedValues("feature", (file) => file.feature).map((file) => ({
|
|
3062
|
-
...file,
|
|
3063
|
-
routes: file.routes.map((route) => this.materializeRoute(route))
|
|
3456
|
+
materializeFeatureLayers() {
|
|
3457
|
+
return this.graph.sortedValues("feature", (layer) => layer.feature).map((layer) => ({
|
|
3458
|
+
...layer,
|
|
3459
|
+
routes: layer.routes.map(
|
|
3460
|
+
(route) => materializeRoute(route, this.defaultMeterCosts)
|
|
3461
|
+
)
|
|
3064
3462
|
}));
|
|
3065
3463
|
}
|
|
3066
|
-
materializeRoute(route) {
|
|
3067
|
-
if (route.unmetered === true) return route;
|
|
3068
|
-
const defaults = {
|
|
3069
|
-
...route.metering?.defaults ?? {}
|
|
3070
|
-
};
|
|
3071
|
-
if (route.inheritDefaultMeters !== false) {
|
|
3072
|
-
for (const cost of this.defaultMeterCosts) {
|
|
3073
|
-
defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
const hasMetering = route.metering !== void 0 || Object.keys(defaults).length > 0;
|
|
3077
|
-
const metering = hasMetering ? {
|
|
3078
|
-
...route.metering ?? {},
|
|
3079
|
-
...Object.keys(defaults).length ? { defaults } : {}
|
|
3080
|
-
} : void 0;
|
|
3081
|
-
return {
|
|
3082
|
-
...route,
|
|
3083
|
-
...metering ? { metering } : {}
|
|
3084
|
-
};
|
|
3085
|
-
}
|
|
3086
3464
|
normalizeMeterCost(cost) {
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
}
|
|
3092
|
-
if (!this.graph.has("meter", cost.meter)) {
|
|
3093
|
-
throw new ManifestBuilderError(
|
|
3094
|
-
`meter cost references unknown meter "${cost.meter}"`
|
|
3095
|
-
);
|
|
3096
|
-
}
|
|
3097
|
-
if (!Number.isFinite(cost.value) || cost.value < 0) {
|
|
3098
|
-
throw new ManifestBuilderError(
|
|
3099
|
-
`meter "${cost.meter}" fixed value must be a non-negative finite number`
|
|
3100
|
-
);
|
|
3101
|
-
}
|
|
3102
|
-
return cost;
|
|
3103
|
-
}
|
|
3104
|
-
normalizeMeterCosts(costs) {
|
|
3105
|
-
if (!costs) return [];
|
|
3106
|
-
const entries = Array.isArray(costs) ? costs : [costs];
|
|
3107
|
-
return entries.map((cost) => this.normalizeMeterCost(cost));
|
|
3108
|
-
}
|
|
3109
|
-
normalizeMeterRefs(refs) {
|
|
3110
|
-
const entries = Array.isArray(refs) ? refs : [refs];
|
|
3111
|
-
return entries.map(keyOf);
|
|
3465
|
+
return normalizeMeterCost(
|
|
3466
|
+
cost,
|
|
3467
|
+
(meter) => this.graph.has("meter", meter)
|
|
3468
|
+
);
|
|
3112
3469
|
}
|
|
3113
3470
|
ensureFrontendManifest() {
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
normalizeMigrationTargetRef(ref) {
|
|
3125
|
-
return {
|
|
3126
|
-
plan: keyOf(ref.plan),
|
|
3127
|
-
version: ref.version ?? "head"
|
|
3128
|
-
};
|
|
3129
|
-
}
|
|
3130
|
-
assertUniqueMigrationIds(migrations) {
|
|
3131
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3132
|
-
for (const migration of migrations) {
|
|
3133
|
-
if (seen.has(migration.id)) {
|
|
3134
|
-
throw new ManifestBuilderError(
|
|
3135
|
-
`duplicate migration "${migration.id}" \u2014 each migration id must be declared once`
|
|
3136
|
-
);
|
|
3137
|
-
}
|
|
3138
|
-
seen.add(migration.id);
|
|
3139
|
-
}
|
|
3471
|
+
const existing = this.graph.get(
|
|
3472
|
+
"frontend",
|
|
3473
|
+
"manifest"
|
|
3474
|
+
)?.value;
|
|
3475
|
+
if (existing) return existing;
|
|
3476
|
+
const manifest = createFrontendManifest();
|
|
3477
|
+
this.syncFrontendGraphNode(manifest);
|
|
3478
|
+
return manifest;
|
|
3140
3479
|
}
|
|
3141
3480
|
assertNewKey(kind, key, label) {
|
|
3142
3481
|
if (this.graph.has(kind, key)) {
|
|
@@ -3146,39 +3485,24 @@ var Product = class {
|
|
|
3146
3485
|
}
|
|
3147
3486
|
}
|
|
3148
3487
|
assertGraphDependenciesSatisfied() {
|
|
3149
|
-
|
|
3150
|
-
if (missing.length === 0) return;
|
|
3151
|
-
const details = missing.slice(0, 8).map(
|
|
3152
|
-
({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
|
|
3153
|
-
dependency
|
|
3154
|
-
)}`
|
|
3155
|
-
).join("; ");
|
|
3156
|
-
const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
|
|
3157
|
-
throw new ManifestBuilderError(
|
|
3158
|
-
`manifest has unresolved resource reference(s): ${details}${suffix}`
|
|
3159
|
-
);
|
|
3488
|
+
assertResourceDependenciesSatisfied(this.graph.missingDependencies());
|
|
3160
3489
|
}
|
|
3161
|
-
|
|
3490
|
+
getFeatureLayer(key) {
|
|
3162
3491
|
return this.graph.get("feature", key)?.value ?? null;
|
|
3163
3492
|
}
|
|
3164
|
-
|
|
3493
|
+
registerFeatureLayer(layer) {
|
|
3165
3494
|
this.graph.register(
|
|
3166
3495
|
"feature",
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3496
|
+
layer.feature,
|
|
3497
|
+
layer,
|
|
3498
|
+
featureDependsOn(layer)
|
|
3170
3499
|
);
|
|
3171
|
-
for (const action of
|
|
3172
|
-
this.registerAction(
|
|
3500
|
+
for (const action of layer.actions ?? []) {
|
|
3501
|
+
this.registerAction(layer.feature, action);
|
|
3173
3502
|
}
|
|
3174
3503
|
}
|
|
3175
|
-
syncFeatureGraphNode(
|
|
3176
|
-
this.graph.upsert(
|
|
3177
|
-
"feature",
|
|
3178
|
-
file.feature,
|
|
3179
|
-
file,
|
|
3180
|
-
this.featureDependsOn(file)
|
|
3181
|
-
);
|
|
3504
|
+
syncFeatureGraphNode(layer) {
|
|
3505
|
+
this.graph.upsert("feature", layer.feature, layer, featureDependsOn(layer));
|
|
3182
3506
|
}
|
|
3183
3507
|
registerAction(featureKey, action) {
|
|
3184
3508
|
this.assertNewKey("action", action.id, "action");
|
|
@@ -3186,7 +3510,7 @@ var Product = class {
|
|
|
3186
3510
|
"action",
|
|
3187
3511
|
action.id,
|
|
3188
3512
|
action,
|
|
3189
|
-
|
|
3513
|
+
actionDependsOn(featureKey, action)
|
|
3190
3514
|
);
|
|
3191
3515
|
}
|
|
3192
3516
|
syncFrontendGraphNode(manifest) {
|
|
@@ -3194,163 +3518,8 @@ var Product = class {
|
|
|
3194
3518
|
"frontend",
|
|
3195
3519
|
"manifest",
|
|
3196
3520
|
manifest,
|
|
3197
|
-
|
|
3198
|
-
);
|
|
3199
|
-
}
|
|
3200
|
-
capabilityDependsOn(file) {
|
|
3201
|
-
return [
|
|
3202
|
-
...this.dependenciesFor("feature", file.includes_features),
|
|
3203
|
-
...this.dependenciesFor("policy", file.includes_policies),
|
|
3204
|
-
...this.dependenciesFor("capability", file.includes_capabilities)
|
|
3205
|
-
];
|
|
3206
|
-
}
|
|
3207
|
-
policyDependsOn(file) {
|
|
3208
|
-
return this.dependenciesFor("meter", file.compatible_with?.meters);
|
|
3209
|
-
}
|
|
3210
|
-
entitlementDependsOn(entitlement) {
|
|
3211
|
-
const limitDimensions = (entitlement.limits ?? []).map(
|
|
3212
|
-
(limit) => limit.dimension
|
|
3213
|
-
);
|
|
3214
|
-
return [
|
|
3215
|
-
...this.dependenciesFor("capability", entitlement.capabilities),
|
|
3216
|
-
...this.existingDependenciesFor("meter", [
|
|
3217
|
-
...entitlement.meters ?? [],
|
|
3218
|
-
...limitDimensions
|
|
3219
|
-
])
|
|
3220
|
-
];
|
|
3221
|
-
}
|
|
3222
|
-
workflowDependsOn(workflow) {
|
|
3223
|
-
return [
|
|
3224
|
-
...this.dependenciesFor("capability", workflow.capabilities),
|
|
3225
|
-
...this.dependenciesFor("meter", [
|
|
3226
|
-
...workflow.meters ?? [],
|
|
3227
|
-
...Object.keys(workflow.estimates ?? {})
|
|
3228
|
-
])
|
|
3229
|
-
];
|
|
3230
|
-
}
|
|
3231
|
-
featureDependsOn(file) {
|
|
3232
|
-
const meterKeys = file.routes.flatMap(
|
|
3233
|
-
(route) => this.routeMeterDependencyKeys(route)
|
|
3234
|
-
);
|
|
3235
|
-
return [
|
|
3236
|
-
...this.dependenciesFor("policy", file.policies),
|
|
3237
|
-
...this.dependenciesFor("capability", file.capabilities),
|
|
3238
|
-
...this.dependenciesFor("plan", file.plans),
|
|
3239
|
-
...this.dependenciesFor("meter", meterKeys)
|
|
3240
|
-
];
|
|
3241
|
-
}
|
|
3242
|
-
routeMeterDependencyKeys(route) {
|
|
3243
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3244
|
-
for (const meter of Object.keys(route.metering?.defaults ?? {})) {
|
|
3245
|
-
keys.add(meter);
|
|
3246
|
-
}
|
|
3247
|
-
for (const meter of route.metering?.reports ?? []) keys.add(meter);
|
|
3248
|
-
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
3249
|
-
keys.add(meter);
|
|
3250
|
-
}
|
|
3251
|
-
return [...keys];
|
|
3252
|
-
}
|
|
3253
|
-
assertRouteMeteringValid(files) {
|
|
3254
|
-
const declaredMeters = new Set(
|
|
3255
|
-
this.graph.values("meter").map((meter) => meter.key)
|
|
3256
|
-
);
|
|
3257
|
-
for (const file of files) {
|
|
3258
|
-
file.routes.forEach((route, routeIndex) => {
|
|
3259
|
-
if (route.unmetered === true) return;
|
|
3260
|
-
const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
|
|
3261
|
-
for (const meter of defaults) {
|
|
3262
|
-
if (declaredMeters.has(meter)) continue;
|
|
3263
|
-
throw new ManifestBuilderError(
|
|
3264
|
-
`feature "${file.feature}" route ${routeIndex}: fixed meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3265
|
-
);
|
|
3266
|
-
}
|
|
3267
|
-
for (const meter of route.metering?.reports ?? []) {
|
|
3268
|
-
if (!declaredMeters.has(meter)) {
|
|
3269
|
-
throw new ManifestBuilderError(
|
|
3270
|
-
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3271
|
-
);
|
|
3272
|
-
}
|
|
3273
|
-
if (defaults.has(meter)) {
|
|
3274
|
-
throw new ManifestBuilderError(
|
|
3275
|
-
`feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
|
|
3276
|
-
);
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
for (const meter of route.metering?.reports ?? []) {
|
|
3280
|
-
const definition = this.graph.get(
|
|
3281
|
-
"meter",
|
|
3282
|
-
meter
|
|
3283
|
-
)?.value;
|
|
3284
|
-
const enforcement = definition?.enforcementType ?? "estimated_then_settled";
|
|
3285
|
-
const hasEstimate = route.metering?.estimates && Object.prototype.hasOwnProperty.call(
|
|
3286
|
-
route.metering.estimates,
|
|
3287
|
-
meter
|
|
3288
|
-
);
|
|
3289
|
-
if ((enforcement === "exact_pre_request" || enforcement === "estimated_then_settled") && !hasEstimate) {
|
|
3290
|
-
throw new ManifestBuilderError(
|
|
3291
|
-
`feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" needs an estimate for gateway admission`
|
|
3292
|
-
);
|
|
3293
|
-
}
|
|
3294
|
-
}
|
|
3295
|
-
for (const meter of Object.keys(route.metering?.estimates ?? {})) {
|
|
3296
|
-
if (declaredMeters.has(meter)) continue;
|
|
3297
|
-
throw new ManifestBuilderError(
|
|
3298
|
-
`feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
|
|
3299
|
-
);
|
|
3300
|
-
}
|
|
3301
|
-
});
|
|
3302
|
-
}
|
|
3303
|
-
}
|
|
3304
|
-
actionDependsOn(featureKey, action) {
|
|
3305
|
-
return [
|
|
3306
|
-
resourceDependency("feature", featureKey),
|
|
3307
|
-
...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
|
|
3308
|
-
];
|
|
3309
|
-
}
|
|
3310
|
-
planDependsOn(plan) {
|
|
3311
|
-
const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
|
|
3312
|
-
const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
|
|
3313
|
-
const pricedMeterDimensions = (plan.meters ?? []).map(
|
|
3314
|
-
(meter) => meter.dimension
|
|
3521
|
+
frontendDependsOn(manifest)
|
|
3315
3522
|
);
|
|
3316
|
-
const capacityKeys = Object.keys(plan.capability_limits ?? {});
|
|
3317
|
-
return [
|
|
3318
|
-
...this.dependenciesFor("capability", caps),
|
|
3319
|
-
...this.existingDependenciesFor("meter", [
|
|
3320
|
-
...limitDimensions,
|
|
3321
|
-
...pricedMeterDimensions
|
|
3322
|
-
]),
|
|
3323
|
-
...this.existingDependenciesFor("counted_resource", capacityKeys)
|
|
3324
|
-
];
|
|
3325
|
-
}
|
|
3326
|
-
migrationDependsOn(migration) {
|
|
3327
|
-
return [
|
|
3328
|
-
resourceDependency("plan", migration.from.plan),
|
|
3329
|
-
resourceDependency("plan", migration.to.plan),
|
|
3330
|
-
...this.dependenciesFor(
|
|
3331
|
-
"plan",
|
|
3332
|
-
migration.pins?.map((pin) => pin.pinTo.plan)
|
|
3333
|
-
)
|
|
3334
|
-
];
|
|
3335
|
-
}
|
|
3336
|
-
frontendDependsOn(manifest) {
|
|
3337
|
-
return this.dependenciesFor("capability", [
|
|
3338
|
-
...(manifest.nav ?? []).flatMap(
|
|
3339
|
-
(item) => item.capability ? [item.capability] : []
|
|
3340
|
-
),
|
|
3341
|
-
...(manifest.pages ?? []).flatMap((page) => [
|
|
3342
|
-
...page.capability ? [page.capability] : [],
|
|
3343
|
-
...(page.components ?? []).flatMap(
|
|
3344
|
-
(component) => component.capability ? [component.capability] : []
|
|
3345
|
-
)
|
|
3346
|
-
])
|
|
3347
|
-
]);
|
|
3348
|
-
}
|
|
3349
|
-
dependenciesFor(kind, keys) {
|
|
3350
|
-
return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
|
|
3351
|
-
}
|
|
3352
|
-
existingDependenciesFor(kind, keys) {
|
|
3353
|
-
return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
|
|
3354
3523
|
}
|
|
3355
3524
|
};
|
|
3356
3525
|
function isProduct(value) {
|
|
@@ -3361,34 +3530,6 @@ function product(name, options, configure) {
|
|
|
3361
3530
|
if (configure) instance.use(configure);
|
|
3362
3531
|
return instance;
|
|
3363
3532
|
}
|
|
3364
|
-
function isPlainObject(value) {
|
|
3365
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3366
|
-
}
|
|
3367
|
-
function buildCustomerContext(options) {
|
|
3368
|
-
return {
|
|
3369
|
-
...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
|
|
3370
|
-
...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
|
|
3371
|
-
...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
|
|
3372
|
-
};
|
|
3373
|
-
}
|
|
3374
|
-
function deepMerge(base, patch) {
|
|
3375
|
-
const out = { ...base };
|
|
3376
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
3377
|
-
const existing = out[key];
|
|
3378
|
-
if (isPlainObject(existing) && isPlainObject(value)) {
|
|
3379
|
-
out[key] = deepMerge(existing, value);
|
|
3380
|
-
} else {
|
|
3381
|
-
out[key] = value;
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
return out;
|
|
3385
|
-
}
|
|
3386
|
-
function describeResourceUrn(urn) {
|
|
3387
|
-
const parts = urn.split(":");
|
|
3388
|
-
const kind = parts[3] ?? "resource";
|
|
3389
|
-
const key = parts.slice(4).join(":");
|
|
3390
|
-
return `${kind} "${decodeURIComponent(key)}"`;
|
|
3391
|
-
}
|
|
3392
3533
|
|
|
3393
3534
|
// src/price.ts
|
|
3394
3535
|
function toCents(dollars, label) {
|