@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/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("preserve_current_period"),
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: "preserve_current_period",
178
- price_decrease: "switch_immediately",
179
- feature_added: "switch_immediately",
180
- feature_removed: "preserve_current_period",
181
- limit_increased: "switch_immediately",
182
- limit_reduced: "preserve_current_period",
183
- credit_increased: "switch_immediately",
184
- credit_reduced: "preserve_current_period",
185
- rating_changed: "preserve_current_period"
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
- const immediate = /* @__PURE__ */ new Set([
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 (immediate.has(policy.when[key] ?? policy.default) && !policy.allowImmediateEntitlementReduction) {
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 z4 } from "zod";
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 recurringGrantSchema = z3.object({
222
- kind: z3.literal("recurring"),
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
- label: z3.string().min(1).max(120),
233
- expires_after_days: z3.number().int().positive().optional()
234
- });
235
- var trialGrantSchema = z3.object({
236
- kind: z3.literal("trial")
237
- });
238
- var rolloverGrantSchema = z3.object({
239
- kind: z3.literal("rollover"),
240
- percent: z3.number().int().min(0).max(100)
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
- recurringGrantSchema,
262
- oneTimeGrantSchema,
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.kind === "recurring")
251
+ if (isRecurringCredit(g))
276
252
  recurringCount += 1;
277
- else if (g.kind === "one_time")
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 `recurring` grant is allowed \u2014 recurring credit is a single canonical entry.",
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 `one_time` grant is allowed \u2014 one-time credit is a single canonical entry.",
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 = z4.enum(["linear", "active_count"]);
298
- var meterTierSchema = z4.object({
299
- up_to: z4.number().int().positive().nullable(),
300
- price_per_unit_micros: z4.number().int().nonnegative()
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 = z4.object({
303
- strategy: z4.enum(["graduated", "volume"]),
304
- tiers: z4.array(meterTierSchema).min(1).max(20)
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 = z4.object({
307
- dimension: z4.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Meter dimension must be lowercase alphanumeric with underscores"),
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: z4.number().int().nonnegative().default(0),
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: z4.number().int().positive().optional()
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 = z4.enum(["month", "year"]);
379
- var planPricingSchema = z4.object({
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: z4.array(meterSchema).default([]),
371
+ meters: z5.array(meterSchema).default([]),
382
372
  /** Flat subscription fee per period, in cents. 0 = no recurring fee. */
383
- recurring_fee_cents: z4.number().int().nonnegative().default(0),
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 knobs
391
- * were removed). */
392
- grants: z4.array(grantSchema).max(40).default([]),
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: z4.number().int().nonnegative().default(0),
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: z4.number().int().nonnegative().optional()
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 z5 } from "zod";
405
- var proRationOnRollbackSchema = z5.enum(["NONE", "PRORATE", "CREDIT"]).default("NONE");
397
+ import { z as z6 } from "zod";
398
+ var proRationOnRollbackSchema = z6.enum(["NONE", "PRORATE", "CREDIT"]).default("NONE");
406
399
  var billingKnobsShape = {
407
- meters: z5.array(meterSchema).default([]),
408
- recurring_fee_cents: z5.number().int().nonnegative().default(0),
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: z5.number().int().nonnegative().default(0),
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: z5.number().int().nonnegative().optional(),
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: z5.number().int().nonnegative().optional(),
414
+ min_monthly_spend_cents: z6.number().int().nonnegative().optional(),
422
415
  /**
423
- * Unified grants array — the SINGLE credit surface (v0.56+). Each entry
424
- * expresses one credit grant primitive: `recurring`, `one_time`,
425
- * `promotional`, `trial`, `rollover`, `top_up`, `auto_recharge`.
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
- * `{kind:"recurring"}` / `{kind:"one_time"}` entries (at most one of
429
- * each — enforced by `refineSingleCanonicalGrant`). The legacy
430
- * `recurring_credit_grant_cents` / `one_time_credit_grant_cents` scalar
431
- * knobs were removed; call `getEffectiveGrants(plan)` to read the
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: z5.array(grantSchema).max(40).default([])
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: z5.array(meterSchema).optional(),
438
- recurring_fee_cents: z5.number().int().nonnegative().optional(),
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: z5.number().int().nonnegative().optional(),
443
- max_monthly_spend_cents: z5.number().int().nonnegative().optional(),
444
- min_monthly_spend_cents: z5.number().int().nonnegative().optional(),
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: z5.array(grantSchema).max(40).optional()
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 = z5.object({
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: z5.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Variant id must be lowercase alphanumeric with hyphens/underscores"),
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: z5.string().max(200).optional(),
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: z5.number().int().min(0).max(100),
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: z5.string().min(1).max(100).default("default"),
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: z5.array(planLimitRuleSchema).max(20).optional(),
485
- featureGates: z5.record(z5.string(), z5.boolean()).optional(),
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: z5.record(z5.string().min(1).max(120), z5.union([z5.number().int().nonnegative(), z5.boolean()])).optional(),
490
- overageBehavior: z5.enum(["block", "allow_and_bill"]).optional()
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 = z5.object({
496
- key: z5.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Plan key must be lowercase alphanumeric with hyphens/underscores"),
497
- name: z5.string().min(1).max(100),
498
- description: z5.string().max(500).optional(),
499
- details: z5.array(z5.string().max(200)).max(10).optional(),
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: z5.boolean().default(false),
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: z5.array(planLimitRuleSchema).max(20).default([]),
522
- featureGates: z5.record(z5.string(), z5.boolean()).optional(),
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: z5.record(z5.string().min(1).max(120), z5.union([z5.number().int().nonnegative(), z5.boolean()])).default({}),
553
- overageBehavior: z5.enum(["block", "allow_and_bill"]).default("block"),
554
- selfServeEnabled: z5.boolean().default(true),
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: z5.boolean().optional().default(false),
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: z5.array(planVariantSchema).max(4).optional(),
577
- archive: z5.object({
578
- at: z5.string().datetime().optional(),
579
- transitionTo: z5.string().optional(),
580
- strategy: z5.enum(["auto", "explicit", "block"]).default("auto")
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 z7 } from "zod";
585
+ import { z as z8 } from "zod";
589
586
 
590
587
  // ../contracts/dist/webhooks/events.js
591
- import { z as z6 } from "zod";
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 = z6.enum(WEBHOOK_EVENT_NAMES);
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 = z7.string().min(3).max(200).refine((value) => WEBHOOK_SECRET_PLACEHOLDER_PATTERN.test(value), {
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 = z7.object({
610
- maxAttempts: z7.number().int().min(1).max(20).default(5),
611
- backoff: z7.enum(["exponential", "fixed"]).default("exponential")
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 = z7.object({
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: z7.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Webhook endpoint id must be lowercase alphanumeric with hyphens/underscores"),
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: z7.string().url("webhooks.endpoints[].url must be a valid 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: z7.array(webhookEventNameSchema).min(1, "webhooks.endpoints[].events must subscribe to \u2265 1 event"),
635
- enabled: z7.boolean().default(true),
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 = z7.object({
642
- endpoints: z7.array(webhookEndpointSchema).max(50).default([])
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 z8 } from "zod";
647
- var planOverrideSchema = z8.object({
648
- meters: z8.array(meterSchema).optional(),
649
- recurring_fee_cents: z8.number().int().nonnegative().optional(),
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: z8.array(grantSchema).max(40).optional(),
658
- trial_days: z8.number().int().nonnegative().optional(),
659
- max_monthly_spend_cents: z8.number().int().nonnegative().optional(),
660
- limits: z8.array(planLimitRuleSchema).max(20).optional(),
661
- featureGates: z8.record(z8.string(), z8.boolean()).optional(),
662
- overageBehavior: z8.enum(["block", "allow_and_bill"]).optional(),
663
- selfServeEnabled: z8.boolean().optional(),
664
- legacy: z8.boolean().optional()
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 = z8.object({
667
- url: z8.string().url().optional(),
666
+ var webhookEndpointOverrideSchema = z9.object({
667
+ url: z9.string().url().optional(),
668
668
  secret: webhookSecretSchema.optional(),
669
- events: z8.array(webhookEventNameSchema).optional(),
670
- enabled: z8.boolean().optional(),
669
+ events: z9.array(webhookEventNameSchema).optional(),
670
+ enabled: z9.boolean().optional(),
671
671
  retryPolicy: webhookRetryPolicySchema.partial().optional()
672
672
  }).strict();
673
- var environmentOverrideBlockSchema = z8.object({
674
- plans: z8.record(z8.string(), planOverrideSchema).optional(),
675
- webhooks: z8.object({
676
- endpoints: z8.record(z8.string(), webhookEndpointOverrideSchema).optional()
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 = z8.record(z8.string().min(1).max(64), environmentOverrideBlockSchema);
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 z13 } from "zod";
682
+ import { z as z19 } from "zod";
683
683
 
684
684
  // ../contracts/dist/plans/spec/frontend-layer.js
685
- import { z as z9 } from "zod";
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 = z9.string().min(1).max(200).regex(/^\/[a-zA-Z0-9_./-]*$/, "path must start with / and contain only [a-zA-Z0-9_./-]");
716
- var frontendCapabilityRefSchema = z9.string().min(1).max(120).regex(/^[a-z0-9_-]+$/);
717
- var frontendNavItemSchema = z9.object({
718
- label: z9.string().min(1).max(80),
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 = z9.enum(KNOWN_FRONTEND_COMPONENT_IDS);
723
- var frontendComponentSchema = z9.object({
722
+ var frontendComponentIdSchema = z10.enum(KNOWN_FRONTEND_COMPONENT_IDS);
723
+ var frontendComponentSchema = z10.object({
724
724
  component: frontendComponentIdSchema,
725
- props: z9.record(z9.string().min(1).max(80), z9.unknown()).optional(),
725
+ props: z10.record(z10.string().min(1).max(80), z10.unknown()).optional(),
726
726
  capability: frontendCapabilityRefSchema.optional(),
727
- gateMode: z9.enum(["hide", "disable", "upsell"]).default("hide")
727
+ gateMode: z10.enum(["hide", "disable", "upsell"]).default("hide")
728
728
  });
729
- var frontendPageSchema = z9.object({
729
+ var frontendPageSchema = z10.object({
730
730
  path: frontendPathSchema,
731
- title: z9.string().min(1).max(120),
732
- requiresAuth: z9.boolean(),
731
+ title: z10.string().min(1).max(120),
732
+ requiresAuth: z10.boolean(),
733
733
  capability: frontendCapabilityRefSchema.optional(),
734
- components: z9.array(frontendComponentSchema).max(12).default([])
734
+ components: z10.array(frontendComponentSchema).max(12).default([])
735
735
  });
736
- var frontendManifestSchema = z9.object({
737
- version: z9.literal(FRONTEND_MANIFEST_SCHEMA_VERSION),
738
- nav: z9.array(frontendNavItemSchema).max(12).default([]),
739
- pages: z9.array(frontendPageSchema).max(24).default([])
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 z10 } from "zod";
772
- var planKeySchema = z10.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Plan key must be lowercase alphanumeric with hyphens/underscores");
773
- var versionRefSchema = z10.string().min(1).max(100);
774
- var planVersionRefSchema = z10.object({
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 = z10.object({
781
+ var migrationTargetSchema = z11.object({
779
782
  plan: planKeySchema,
780
- version: z10.literal("head").default("head")
783
+ version: z11.literal("head").default("head")
781
784
  });
782
- var pinnedPlanVersionSchema = z10.object({
785
+ var pinnedPlanVersionSchema = z11.object({
783
786
  plan: planKeySchema,
784
787
  version: versionRefSchema
785
788
  });
786
- var migrationEffectiveSchema = z10.enum([
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 = z10.enum(["none", "prorate", "credit"]);
794
- var migrationExistingCustomersSchema = z10.object({
796
+ var migrationProrationSchema = z11.enum(["none", "prorate", "credit"]);
797
+ var migrationExistingCustomersSchema = z11.object({
795
798
  effective: migrationEffectiveSchema,
796
- date: z10.string().datetime().optional(),
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 = z10.object({
808
- subscriber: z10.string().min(1).max(200),
810
+ var migrationPinSchema = z11.object({
811
+ subscriber: z11.string().min(1).max(200),
809
812
  pinTo: pinnedPlanVersionSchema,
810
- until: z10.string().datetime().optional(),
811
- notes: z10.string().max(1e3).optional()
813
+ until: z11.string().datetime().optional(),
814
+ notes: z11.string().max(1e3).optional()
812
815
  });
813
- var migrationDeclSchema = z10.object({
814
- id: z10.string().min(1).max(120).regex(/^[a-z0-9][a-z0-9_.-]*$/, "Migration id must be lowercase alphanumeric with dots, hyphens, or underscores"),
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: z10.literal("immediate").default("immediate"),
820
+ newCustomers: z11.literal("immediate").default("immediate"),
818
821
  existingCustomers: migrationExistingCustomersSchema,
819
- pins: z10.array(migrationPinSchema).max(50).default([])
822
+ pins: z11.array(migrationPinSchema).max(50).default([])
820
823
  });
821
- var migrationDeclsSchema = z10.array(migrationDeclSchema).superRefine((migrations, ctx) => {
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 z11 } from "zod";
837
- var countedResourceScopeSchema = z11.enum(["subscription", "subject"]);
838
- var countedResourceCountSourceSchema = z11.enum([
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 = z11.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/);
843
- var countedResourceSchema = z11.object({
845
+ var countedResourceNameSchema = z12.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/);
846
+ var countedResourceSchema = z12.object({
844
847
  name: countedResourceNameSchema,
845
- display: z11.string().min(1).max(120).optional(),
848
+ display: z12.string().min(1).max(120).optional(),
846
849
  scope: countedResourceScopeSchema.default("subscription"),
847
- subjectType: z11.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/).optional(),
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 = z11.array(countedResourceSchema).max(100).default([]);
868
+ var countedResourcesSchema = z12.array(countedResourceSchema).max(100).default([]);
866
869
 
867
870
  // ../contracts/dist/plans/addons.js
868
- import { z as z12 } from "zod";
869
- var addOnSchema = z12.object({
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: z12.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "AddOn key must be lowercase alphanumeric with hyphens/underscores"),
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: z12.string().min(1).max(100),
878
+ name: z13.string().min(1).max(100),
876
879
  /** Short description for tooltips / chooser. */
877
- description: z12.string().max(500).optional(),
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: z12.number().int().nonnegative().default(0),
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: z12.array(meterSchema).default([]),
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: z12.array(grantSchema).max(40).default([]),
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: z12.record(z12.string().min(1).max(120), z12.union([z12.number().int().nonnegative(), z12.boolean()])).default({}),
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: z12.record(z12.string(), z12.boolean()).optional(),
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: z12.boolean().default(true)
905
+ selfServeEnabled: z13.boolean().default(true)
903
906
  });
904
- var addOnsBlockSchema = z12.record(z12.string().min(1).max(64), addOnSchema).default({});
905
- var subscriptionAddOnStatusSchema = z12.enum([
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 = z12.object({
914
+ var subscriptionAddOnSchema = z13.object({
912
915
  /** References `product.add_ons.<addOnKey>`. */
913
- addOnKey: z12.string().min(1).max(64),
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: z12.string().datetime().optional(),
922
+ activatedAt: z13.string().datetime().optional(),
920
923
  /** When the AddOn was canceled (ISO-8601). Null while active. */
921
- canceledAt: z12.string().datetime().nullable().optional(),
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: z12.string().optional()
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 = z13.object({
1179
- subdomain: z13.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "Subdomain must be lowercase alphanumeric with optional hyphens")
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 = z13.enum([
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 = z13.object({
1188
- key: z13.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Meter key must be lowercase alphanumeric with underscores"),
1189
- display: z13.string().min(1).max(100),
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: z13.string().max(20).optional(),
1622
+ unit: z19.string().max(20).optional(),
1198
1623
  /** Reusable pre-request estimate for routes that dynamically report this meter. */
1199
- estimate: z13.number().finite().nonnegative().optional(),
1624
+ estimate: z19.number().finite().nonnegative().optional(),
1200
1625
  /** Fixed per-request default applied by Product SDK helpers. */
1201
- routeDefault: z13.number().finite().nonnegative().optional(),
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: z13.enum(["SUM", "COUNT", "MAX", "UNIQUE_COUNT", "LATEST"]).default("COUNT"),
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: z13.enum(["minute", "hour", "day", "month", "billing_period"]).default("billing_period"),
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: z13.string().optional(),
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: z13.string().optional(),
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: z13.array(z13.string()).optional(),
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: z13.string().optional()
1259
- });
1260
- var usageMeasureSchema = z13.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Usage measure must be lowercase alphanumeric with underscores");
1261
- var usageRatingPricePolicySchema = z13.enum([
1262
- "pass_through",
1263
- "markup",
1264
- "fixed_margin",
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 = z13.object({
1272
- source: z13.literal("provider_catalog"),
1273
- catalog: z13.string().min(1).max(100),
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: z13.number().nonnegative().max(1e4).optional(),
1276
- marginMicros: z13.number().int().nonnegative().optional()
1695
+ markupPercent: z19.number().nonnegative().max(1e4).optional(),
1696
+ marginMicros: z19.number().int().nonnegative().optional()
1277
1697
  });
1278
- var upstreamReportedRatingSchema = z13.object({
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 = z13.object({
1301
- selector: z13.string().min(1).max(100).optional(),
1302
- measures: z13.array(usageMeasureSchema).min(1).max(20),
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 = z13.object({
1306
- meters: z13.record(z13.string().min(1).max(64).regex(/^[a-z0-9_]+$/), usageMeterSchema)
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 = z13.object({
1309
- defaults: z13.record(z13.string().min(1).max(64), z13.number().finite().nonnegative()).optional(),
1310
- reports: z13.array(z13.string().min(1).max(64)).max(20).optional(),
1311
- estimates: z13.record(z13.string().min(1).max(64), z13.number().finite().nonnegative()).optional(),
1312
- onStatusCodes: z13.union([
1313
- z13.string().min(1).max(100),
1314
- z13.array(z13.number().int().min(100).max(599)).min(1).max(100)
1315
- ]).optional()
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 = z13.object({
1318
- method: z13.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
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: z13.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]"),
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: z13.boolean().optional(),
1728
+ unmetered: z19.boolean().optional(),
1330
1729
  metering: routeMeteringSchema.optional(),
1331
- inheritDefaultMeters: z13.boolean().optional()
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 = z13.object({
1736
+ var featureCatalogEntrySchema = z19.object({
1334
1737
  // Optional human-friendly summary; surfaced in dashboards / settings UI.
1335
- description: z13.string().max(500).optional(),
1336
- routes: z13.array(featureRouteSchema).min(1).max(50),
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: z13.array(z13.string().min(1)).min(1).max(20)
1746
+ plans: z19.array(z19.string().min(1)).min(1).max(20)
1344
1747
  });
1345
- var featureCatalogSchema = z13.record(z13.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
1346
- var productCleanupPolicyModeSchema = z13.enum([
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 = z13.enum([
1753
+ var productChangeApprovalRiskSchema = z19.enum([
1351
1754
  "safe",
1352
1755
  "non_blocking",
1353
1756
  "economic_risk",
1354
1757
  "blocking"
1355
1758
  ]);
1356
- var productOperatorPoliciesSchema = z13.object({
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: z13.object({
1363
- enabled: z13.boolean().default(false),
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: z13.object({
1372
- auto_merge_max_risk: z13.enum(["none", "safe", "non_blocking"]).default("none"),
1373
- require_human_for: z13.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
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 = z13.enum([
1788
+ var customerIdentityRequirementSchema = z19.enum([
1386
1789
  "org_only",
1387
1790
  "org_and_user"
1388
1791
  ]);
1389
- var customerPortalAuthStrategySchema = z13.enum([
1792
+ var customerPortalAuthStrategySchema = z19.enum([
1390
1793
  "clerk",
1391
1794
  "test-personas"
1392
1795
  ]);
1393
- var productCustomerContextSchema = z13.object({
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: z13.object({
1406
- enabled: z13.boolean().default(true)
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: z13.object({
1816
+ portal_auth: z19.object({
1414
1817
  strategy: customerPortalAuthStrategySchema
1415
1818
  }).optional()
1416
1819
  });
1417
- var productSurfaceTypeSchema = z13.enum([
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 = z13.object({
1428
- key: z13.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Surface key must be lowercase alphanumeric with hyphens/underscores"),
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: z13.string().min(1).max(100).optional(),
1431
- description: z13.string().max(500).optional()
1833
+ display: z19.string().min(1).max(100).optional(),
1834
+ description: z19.string().max(500).optional()
1432
1835
  });
1433
- var productEntitlementSchema = z13.object({
1434
- key: z13.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Entitlement key must be lowercase alphanumeric with hyphens/underscores"),
1435
- description: z13.string().max(500).optional(),
1436
- capabilities: z13.array(z13.string().min(1).max(100)).max(100).optional(),
1437
- featureGates: z13.record(z13.string().min(1), z13.boolean()).optional(),
1438
- limits: z13.array(planLimitRuleSchema).max(100).optional(),
1439
- meters: z13.array(z13.string().min(1).max(64)).max(100).optional()
1440
- });
1441
- var productSurfacesSchema = z13.array(productSurfaceSchema).max(20).default([]);
1442
- var productEntitlementsSchema = z13.array(productEntitlementSchema).max(100).default([]);
1443
- var productWorkflowKindSchema = z13.enum([
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 = z13.discriminatedUnion("type", [
1451
- z13.object({ type: z13.literal("manual") }),
1452
- z13.object({
1453
- type: z13.literal("schedule"),
1454
- cron: z13.string().min(1).max(120)
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
- z13.object({
1457
- type: z13.literal("event"),
1458
- event: z13.string().min(1).max(120)
1859
+ z19.object({
1860
+ type: z19.literal("event"),
1861
+ event: z19.string().min(1).max(120)
1459
1862
  }),
1460
- z13.object({
1461
- type: z13.literal("api"),
1462
- path: z13.string().min(1).max(240).regex(/^\//, "path must start with /")
1863
+ z19.object({
1864
+ type: z19.literal("api"),
1865
+ path: z19.string().min(1).max(240).regex(/^\//, "path must start with /")
1463
1866
  }),
1464
- z13.object({
1465
- type: z13.literal("lifecycle"),
1466
- event: z13.string().min(1).max(120)
1867
+ z19.object({
1868
+ type: z19.literal("lifecycle"),
1869
+ event: z19.string().min(1).max(120)
1467
1870
  })
1468
1871
  ]);
1469
- var productWorkflowSchema = z13.object({
1470
- key: z13.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Workflow key must be lowercase alphanumeric with hyphens/underscores"),
1471
- title: z13.string().min(1).max(120).optional(),
1472
- description: z13.string().max(1e3).optional(),
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: z13.array(z13.string().min(1).max(100)).max(100).optional(),
1476
- meters: z13.array(z13.string().min(1).max(64)).max(100).optional(),
1477
- estimates: z13.record(z13.string().min(1).max(64), z13.number().finite()).optional(),
1478
- metadata: z13.record(z13.string().min(1), z13.unknown()).optional()
1479
- });
1480
- var productWorkflowsSchema = z13.array(productWorkflowSchema).max(100).default([]);
1481
- var productSpecSchema = z13.object({
1482
- product: z13.object({
1483
- name: z13.string().min(1).max(100),
1484
- displayName: z13.string().max(200).optional(),
1485
- description: z13.string().max(2e3).optional(),
1486
- baseUrl: z13.string().url("baseUrl must be a valid URL"),
1487
- sandboxBaseUrl: z13.string().url("sandboxBaseUrl must be a valid URL").optional(),
1488
- visibility: z13.enum(["public", "private"]).default("public"),
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: z13.string().url().optional(),
1491
- primaryColor: z13.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
1893
+ logoUrl: z19.string().url().optional(),
1894
+ primaryColor: z19.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
1492
1895
  // Environment
1493
- envBranchPrefix: z13.string().max(50).nullable().optional()
1896
+ envBranchPrefix: z19.string().max(50).nullable().optional()
1494
1897
  }),
1495
- gateway: z13.object({
1496
- authHeader: z13.string().min(1).max(100).default("x-api-key"),
1497
- upstreamAuth: z13.object({
1498
- type: z13.enum(["none", "static_bearer"]),
1499
- token: z13.string().optional()
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: z13.object({
1503
- meters: z13.array(meterDefinitionSchema).max(10).default([]),
1504
- billOn4xx: z13.boolean().default(false)
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: z13.never({
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
- // retains the transition-policy fields (`gracePeriodDays`,
1530
- // `subscriberChangePolicy`); the strategy enum is gone.
1531
- billing: z13.object({
1532
- gracePeriodDays: z13.number().int().nonnegative().default(3),
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: z13.boolean().optional(),
1952
+ applyLimitUpgradesInstantly: z19.boolean().optional(),
1537
1953
  subscriberChangePolicy: subscriberChangePolicySchema.default({
1538
- default: "preserve_current_period",
1539
- proration: "none",
1954
+ default: "period_end",
1540
1955
  when: {
1541
- price_increase: "preserve_current_period",
1542
- price_decrease: "switch_immediately",
1543
- feature_added: "switch_immediately",
1544
- feature_removed: "preserve_current_period",
1545
- limit_increased: "switch_immediately",
1546
- limit_reduced: "preserve_current_period",
1547
- credit_increased: "switch_immediately",
1548
- credit_reduced: "preserve_current_period",
1549
- rating_changed: "preserve_current_period"
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: "preserve_current_period",
1558
- proration: "none",
1971
+ default: "period_end",
1559
1972
  when: {
1560
- price_increase: "preserve_current_period",
1561
- price_decrease: "switch_immediately",
1562
- feature_added: "switch_immediately",
1563
- feature_removed: "preserve_current_period",
1564
- limit_increased: "switch_immediately",
1565
- limit_reduced: "preserve_current_period",
1566
- credit_increased: "switch_immediately",
1567
- credit_reduced: "preserve_current_period",
1568
- rating_changed: "preserve_current_period"
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: z13.array(planSpecSchema).max(4).default([]),
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: z13.object({
1618
- breaking_changes: z13.object({
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: z13.number().int().nonnegative().default(0),
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: z13.boolean().default(false)
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: z13.object({
1673
- defaultPlan: z13.string().min(1).optional()
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 = z13.object({
2098
+ var productPhaseSchema = z19.object({
1685
2099
  product: productSpecSchema.shape.product
1686
2100
  });
1687
- var gatewayPhaseSchema = z13.object({
2101
+ var gatewayPhaseSchema = z19.object({
1688
2102
  gateway: productSpecSchema.shape.gateway
1689
2103
  });
1690
- var meteringPhaseSchema = z13.object({
2104
+ var meteringPhaseSchema = z19.object({
1691
2105
  metering: productSpecSchema.shape.metering
1692
2106
  });
1693
- var plansPhaseSchema = z13.object({
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 z19 } from "zod";
2095
- var capabilityFileSchema = z19.object({
2096
- capability: z19.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "capability name must be lowercase alphanumeric with hyphens/underscores"),
2097
- description: z19.string().max(500).optional(),
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: z19.enum(["runtime", "contractual"]).default("contractual"),
2106
- includes_features: z19.array(z19.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/)).max(20).default([]),
2107
- includes_policies: z19.array(z19.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
2108
- includes_capabilities: z19.array(z19.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(20).default([])
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/product-v2.js
2112
- import { z as z20 } from "zod";
2113
- var PRODUCT_V2_SCHEMA_VERSION = 1;
2114
- var billingKnobsShape2 = {
2115
- meters: z20.array(meterSchema).default([]),
2116
- recurring_fee_cents: z20.number().int().nonnegative().default(0),
2117
- /** Billing cadence (`month` default / `year`). Drives the Stripe price
2118
- * `recurring.interval` and the entitlement billing-period window. */
2119
- billing_interval: billingIntervalSchema.default("month"),
2120
- trial_days: z20.number().int().nonnegative().default(0),
2121
- max_monthly_spend_cents: z20.number().int().nonnegative().optional(),
2122
- min_monthly_spend_cents: z20.number().int().nonnegative().optional(),
2123
- // Unified grants array — the SINGLE credit surface. Recurring +
2124
- // one-time credit are canonical entries here (the legacy scalar knobs
2125
- // were removed).
2126
- grants: z20.array(grantSchema).max(40).default([])
2127
- };
2128
- var planSpecV2Schema = z20.object({
2129
- key: z20.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Plan key must be lowercase alphanumeric with hyphens/underscores"),
2130
- name: z20.string().min(1).max(100),
2131
- description: z20.string().max(500).optional(),
2132
- details: z20.array(z20.string().max(200)).max(10).optional(),
2133
- ...billingKnobsShape2,
2134
- free: z20.boolean().default(false),
2135
- /**
2136
- * Capability composition references. The compiler's
2137
- * `resolve-capabilities` pass walks the graph and expands this into
2138
- * concrete (features, policies) sets stored on the CompiledPlan.
2139
- *
2140
- * Replaces the legacy `featureGates` block (which is now expressed
2141
- * via capability composition) and the per-plan inverse feature
2142
- * mapping (`featureCatalogEntry.plans[]`).
2143
- *
2144
- * A plan may reference zero capabilities — useful for free / starter
2145
- * plans that grant access only via direct route bindings.
2146
- */
2147
- capabilities: z20.array(z20.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
2148
- limits: z20.array(planLimitRuleSchema).max(20).default([]),
2149
- /**
2150
- * Control-plane capability caps (count limits + boolean toggles).
2151
- * Same semantics as the legacy `capability_limits` field.
2152
- */
2153
- capability_limits: z20.record(z20.string().min(1).max(120), z20.union([z20.number().int().nonnegative(), z20.boolean()])).default({}),
2154
- overageBehavior: z20.enum(["block", "allow_and_bill"]).default("block"),
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: "recurring", amount_cents: 2e3 }],
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: "recurring", amount_cents: 2e4 }],
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: "one_time", amount_cents: 5e3 }],
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
- // src/version.ts
2426
- var SDK_VERSION = true ? "0.5.0" : "0.0.0-dev";
2427
-
2428
- // src/product.ts
2429
- var PRODUCT_BRAND = Symbol.for("farthershore.product.product");
2430
- var PRODUCT_MANIFEST_COMPILER = Symbol.for(
2431
- "farthershore.product.manifestCompiler"
2432
- );
2433
- function isCapabilityGrant(value) {
2434
- return typeof value === "object" && value !== null && value.kind === "capability_grant";
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 keyOf(ref) {
2437
- return typeof ref === "string" ? ref : ref.key;
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 displayFromKey(key) {
2440
- return key.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
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 parseRouteMatch(match) {
2443
- const trimmed = match.trim();
2444
- const space = trimmed.indexOf(" ");
2445
- if (space === -1) {
2446
- if (!trimmed.startsWith("/")) {
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
- `route "${match}": expected "METHOD /path" or "/path"`
3104
+ `duplicate migration "${migration.id}" \u2014 each migration id must be declared once`
2449
3105
  );
2450
3106
  }
2451
- return { method: "*", path: trimmed };
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.getFeatureFile(featureKey);
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.nav = items.map((item) => ({
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
- if (manifest.pages?.some((page) => page.path === path)) {
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
- this.migrationDependsOn(migration)
3196
+ migrationDependsOn(migration)
2589
3197
  );
2590
3198
  return this;
2591
3199
  },
2592
3200
  migrations: (migrations) => {
2593
- this.assertUniqueMigrationIds(migrations);
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
- this.migrationDependsOn(migration)
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 = deepMerge(this.productPatch, patch);
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("plan", spec.key, spec, this.planDependsOn(spec));
2617
- return this;
2618
- },
2619
- routesFile: (file) => {
2620
- this.assertNewKey("feature", file.feature, "feature");
2621
- this.registerFeatureFile(file);
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
- policyFile: (file) => {
2625
- this.assertNewKey("policy", file.name, "policy");
2626
- this.graph.register("policy", file.name, file);
3232
+ routeLayer: (layer) => {
3233
+ this.assertNewKey("feature", layer.feature, "feature");
3234
+ this.registerFeatureLayer(layer);
2627
3235
  return this;
2628
3236
  },
2629
- capabilityFile: (file) => {
2630
- this.assertNewKey("capability", file.capability, "capability");
2631
- this.graph.register("capability", file.capability, file);
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
- frontend: (manifest) => {
2635
- this.frontendManifest = manifest;
2636
- this.syncFrontendGraphNode(manifest);
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 = this.makeMeterRef(key);
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 file = {
2683
- capability: key,
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 file = {
2709
- feature: key,
2710
- routes: [],
2711
- ...options.description !== void 0 ? { description: options.description } : {},
2712
- ...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
2713
- ...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
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
- file.actions ??= [];
2740
- if (file.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
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
- file.actions.push(action);
3333
+ layer.actions.push(action);
2747
3334
  this.registerAction(key, action);
2748
- this.syncFeatureGraphNode(file);
3335
+ this.syncFeatureGraphNode(layer);
2749
3336
  return { kind: "action", key: id };
2750
3337
  },
2751
3338
  route: (match, routeOptions) => {
2752
- file.routes.push(this.buildRoute(match, routeOptions ?? {}));
2753
- this.syncFeatureGraphNode(file);
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 file = {
2762
- name,
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 key = options.key ?? type;
2781
- this.assertNewKey("surface", key, "surface");
2782
- const surface = {
2783
- key,
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
- key,
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
- this.entitlementDependsOn(entitlement)
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 capabilityKeys = (options.capabilities ?? []).map(keyOf);
2833
- const capabilityLimits = {
2834
- ...options.capabilityLimits ?? {}
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
- name: options.name,
2850
- ...options.description !== void 0 ? { description: options.description } : {},
2851
- ...options.details ? { details: options.details } : {},
2852
- ...options.price ? {
2853
- recurring_fee_cents: options.price.recurring_fee_cents,
2854
- billing_interval: options.price.billing_interval,
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.materializeFeatureFiles();
2897
- this.assertRouteMeteringValid(routes);
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
- (file) => file.name
3423
+ (layer) => layer.name
2907
3424
  ),
2908
3425
  capabilities: this.graph.sortedValues(
2909
3426
  "capability",
2910
- (file) => file.capability
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
- const options = this.options;
2920
- const base = {
2921
- product: {
2922
- name: this.name,
2923
- baseUrl: options.origin,
2924
- ...options.displayName !== void 0 ? { displayName: options.displayName } : {},
2925
- ...options.description !== void 0 ? { description: options.description } : {},
2926
- ...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
2927
- ...options.visibility !== void 0 ? { visibility: options.visibility } : {},
2928
- ...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
2929
- ...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
2930
- ...options.envBranchPrefix !== void 0 ? { envBranchPrefix: options.envBranchPrefix } : {}
2931
- },
2932
- gateway: {
2933
- ...options.authHeader !== void 0 ? { authHeader: options.authHeader } : {},
2934
- ...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
2935
- },
2936
- metering: {
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
- const parsed = parseRouteMatch(match);
3018
- const metering = this.buildRouteMetering(options);
3019
- return {
3020
- match: parsed,
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
- buildRouteMetering(options) {
3036
- if (options.unmetered === true) return void 0;
3037
- const defaults = {};
3038
- for (const cost of this.normalizeMeterCosts(options.costs)) {
3039
- defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
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
- if (!cost || cost.kind !== "meter_cost") {
3093
- throw new ManifestBuilderError(
3094
- "meter cost must be created by meter.fixed(value)"
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
- this.frontendManifest ??= { version: 1, nav: [], pages: [] };
3120
- this.syncFrontendGraphNode(this.frontendManifest);
3121
- return this.frontendManifest;
3122
- }
3123
- normalizeMigrationPlanRef(ref) {
3124
- return {
3125
- plan: keyOf(ref.plan),
3126
- ...ref.version !== void 0 ? { version: ref.version } : {}
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
- const missing = this.graph.missingDependencies();
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
- getFeatureFile(key) {
3495
+ getFeatureLayer(key) {
3167
3496
  return this.graph.get("feature", key)?.value ?? null;
3168
3497
  }
3169
- registerFeatureFile(file) {
3498
+ registerFeatureLayer(layer) {
3170
3499
  this.graph.register(
3171
3500
  "feature",
3172
- file.feature,
3173
- file,
3174
- this.featureDependsOn(file)
3501
+ layer.feature,
3502
+ layer,
3503
+ featureDependsOn(layer)
3175
3504
  );
3176
- for (const action of file.actions ?? []) {
3177
- this.registerAction(file.feature, action);
3505
+ for (const action of layer.actions ?? []) {
3506
+ this.registerAction(layer.feature, action);
3178
3507
  }
3179
3508
  }
3180
- syncFeatureGraphNode(file) {
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
- this.actionDependsOn(featureKey, action)
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
- this.frontendDependsOn(manifest)
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 = [