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