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