@farthershore/product 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -671,7 +671,7 @@ var environmentOverrideBlockSchema = z8.object({
671
671
  var environmentsBlockSchema = z8.record(z8.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 z18 } from "zod";
675
675
 
676
676
  // ../contracts/dist/plans/spec/frontend-layer.js
677
677
  import { z as z9 } from "zod";
@@ -916,49 +916,409 @@ var subscriptionAddOnSchema = z12.object({
916
916
  stripeSubscriptionItemId: z12.string().optional()
917
917
  });
918
918
 
919
- // ../contracts/dist/plans/spec/refinements.js
920
- function rejectUsagePricing(spec, ctx) {
921
- if (spec.usagePricing === void 0)
922
- return;
923
- ctx.addIssue({
924
- code: "custom",
925
- path: ["usagePricing"],
926
- message: "usagePricing is not supported. Define usage.meters.<key>.rating instead."
927
- });
919
+ // ../contracts/dist/plans/spec/backend-layer.js
920
+ import { z as z13 } from "zod";
921
+ var BACKEND_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
922
+ var backendIdSchema = z13.string().min(1).max(64).regex(BACKEND_ID_PATTERN, "backend id must be a lowercase slug ([a-z0-9][a-z0-9_-]*)");
923
+ var backendTransportModeSchema = z13.enum([
924
+ "public_origin",
925
+ "mtls",
926
+ "cloudflare_tunnel"
927
+ ]);
928
+ var backendTransportRunnerSchema = z13.enum([
929
+ "managed_cloudflared",
930
+ "sidecar"
931
+ ]);
932
+ var backendTransportSchema = z13.object({
933
+ mode: backendTransportModeSchema.default("public_origin"),
934
+ runner: backendTransportRunnerSchema.optional()
935
+ }).strict();
936
+ var backendVerificationSchema = z13.object({
937
+ required: z13.boolean().default(false)
938
+ }).strict();
939
+ var backendDefinitionSchema = z13.object({
940
+ /** Human-friendly label. Defaults to the id when omitted. */
941
+ name: z13.string().min(1).max(120).optional(),
942
+ /** Stable slug for the backend (origin-hostname / token scoping). Defaults
943
+ * to the id when omitted. */
944
+ slug: backendIdSchema.optional(),
945
+ transport: backendTransportSchema.default({ mode: "public_origin" }),
946
+ verification: backendVerificationSchema.default({ required: false }),
947
+ /** Meter allow-list. Omitted = all product meters allowed. */
948
+ meters: z13.array(z13.string().min(1).max(64)).max(100).optional(),
949
+ /** Marks the default backend when a product declares more than one. At
950
+ * most one backend may set this (compiler enforces / AMBIGUOUS_DEFAULT). */
951
+ default: z13.boolean().optional(),
952
+ /** Reachable origin for `public_origin` / `mtls`. */
953
+ originUrl: z13.string().url().optional(),
954
+ /** Access-protected `*.fs-origin` host for `cloudflare_tunnel`. */
955
+ originHostname: z13.string().min(1).max(255).optional()
956
+ }).strict();
957
+ var productBackendBlockSchema = z13.record(backendIdSchema, backendDefinitionSchema);
958
+ var routeBackendBindingSchema = backendIdSchema;
959
+ var BACKEND_DIAGNOSTIC_CODES = {
960
+ unknownBackendInRoute: "UNKNOWN_BACKEND_IN_ROUTE",
961
+ ambiguousDefaultBackend: "AMBIGUOUS_DEFAULT_BACKEND",
962
+ routeMeterNotAllowedByBackend: "ROUTE_METER_NOT_ALLOWED_BY_BACKEND",
963
+ unknownMeterInBackend: "UNKNOWN_METER_IN_BACKEND"
964
+ };
965
+ function resolveDefaultBackendId(backends) {
966
+ const ids = Object.keys(backends ?? {});
967
+ if (ids.length === 0)
968
+ return { defaultId: null, ambiguous: false };
969
+ if (ids.length === 1)
970
+ return { defaultId: ids[0], ambiguous: false };
971
+ const explicit = ids.filter((id) => backends[id]?.default === true);
972
+ if (explicit.length === 1) {
973
+ return { defaultId: explicit[0], ambiguous: false };
974
+ }
975
+ return { defaultId: null, ambiguous: true };
928
976
  }
929
- function validateFreePlans(plans, ctx) {
930
- const freePlans = plans.filter((plan) => plan.free);
931
- if (freePlans.length > 1) {
932
- ctx.addIssue({
933
- code: "custom",
934
- path: ["plans"],
935
- message: "Only one free plan is allowed per product"
936
- });
937
- }
938
- plans.forEach((plan, index) => {
939
- if (!plan.free)
940
- return;
941
- validateFreePlanPrice(plan, index, ctx);
942
- validateFreePlanHardLimit(plan, index, ctx);
943
- });
977
+
978
+ // ../contracts/dist/plans/spec/routes-layer.js
979
+ import { z as z17 } from "zod";
980
+
981
+ // ../contracts/dist/plans/spec/policies-layer.js
982
+ import { z as z15 } from "zod";
983
+
984
+ // ../contracts/dist/plans/spec/policy-types.js
985
+ import { z as z14 } from "zod";
986
+ var rateLimitWindowSchema = z14.string().min(2).max(20).regex(/^\d+(ms|s|m|h)$/, "rate_limit window must look like `60s`, `5m`, `1h`");
987
+ var rateLimitConfigSchema = z14.object({
988
+ strategy: z14.enum(["token_bucket", "sliding_window", "fixed_window"]).default("token_bucket"),
989
+ /**
990
+ * Which request dimensions identify the bucket. v0.3.0 supports a
991
+ * fixed set; extending requires a coordinated gateway/policy-engine
992
+ * change. The `subscription` dimension is the steady-state default;
993
+ * `ip` is for unauthenticated probes; `credential` is finer-grained
994
+ * than subscription (per-key throttling).
995
+ */
996
+ dimensions: z14.array(z14.enum(["subscription", "credential", "ip", "route"])).min(1).max(4).default(["subscription"]),
997
+ limits: z14.array(z14.object({
998
+ window: rateLimitWindowSchema,
999
+ max: z14.number().int().positive().max(1e7)
1000
+ })).min(1).max(10),
1001
+ /**
1002
+ * Bounded fail-open behaviour for DO outages. See architecture RFC
1003
+ * "Fail-open guardrails" section. When the policy executor observes
1004
+ * `max_consecutive_failures` DO-call failures within
1005
+ * `max_window_seconds`, it transitions to `degraded_mode` until
1006
+ * `recovery_threshold` consecutive successes restore normal
1007
+ * evaluation.
1008
+ */
1009
+ fail_open: z14.object({
1010
+ max_consecutive_failures: z14.number().int().positive().default(100),
1011
+ max_window_seconds: z14.number().int().positive().default(60),
1012
+ recovery_threshold: z14.number().int().positive().default(50),
1013
+ degraded_mode: z14.enum([
1014
+ "safe_mode_block",
1015
+ "safe_mode_throttle",
1016
+ "runtime_killswitch_trigger"
1017
+ ]).default("safe_mode_throttle")
1018
+ }).default({
1019
+ max_consecutive_failures: 100,
1020
+ max_window_seconds: 60,
1021
+ recovery_threshold: 50,
1022
+ degraded_mode: "safe_mode_throttle"
1023
+ })
1024
+ });
1025
+ var authConfigSchema = z14.object({
1026
+ header_name: z14.string().min(1).max(100).default("x-api-key"),
1027
+ /**
1028
+ * How the gateway constructs the upstream Authorization header:
1029
+ * - `none` → no upstream auth header added
1030
+ * - `static_bearer` → forward a configured static token
1031
+ * - `subscriber_jwt` → mint a per-subscriber JWT (out of scope v0.3.0)
1032
+ */
1033
+ upstream_token_source: z14.discriminatedUnion("type", [
1034
+ z14.object({ type: z14.literal("none") }),
1035
+ z14.object({
1036
+ type: z14.literal("static_bearer"),
1037
+ token_secret_ref: z14.string().min(1).max(200).describe("Reference into the secret store (e.g. CF Secret name); not the raw token")
1038
+ })
1039
+ ]).default({ type: "none" }),
1040
+ /**
1041
+ * When `true`, treat the inbound credential's scopes (if any) as
1042
+ * additional gating beyond the entitlement check. v0.3.0 ships with
1043
+ * `strict` as the default.
1044
+ */
1045
+ scope_mode: z14.enum(["strict", "advisory", "off"]).default("strict")
1046
+ });
1047
+ var concurrencyConfigSchema = z14.object({
1048
+ max_in_flight: z14.number().int().positive().max(1e4),
1049
+ /**
1050
+ * Which dimensions key the lease bucket. Matches the existing
1051
+ * ConcurrencyLease DO `idFromName` pattern (subscription | capability
1052
+ * tuple).
1053
+ */
1054
+ dimensions: z14.array(z14.enum(["subscription", "credential", "capability"])).min(1).max(3).default(["subscription"]),
1055
+ /**
1056
+ * Optional capability scope. When set, the lease bucket is keyed
1057
+ * partly by this capability name — separate buckets per capability.
1058
+ */
1059
+ capability: z14.string().min(1).max(120).optional(),
1060
+ /**
1061
+ * Lease TTL — releases automatically after this many seconds even if
1062
+ * the request never returns (defensive default 30s, mirrors existing
1063
+ * ConcurrencyLease behaviour).
1064
+ */
1065
+ lease_ttl_seconds: z14.number().int().positive().max(600).default(30),
1066
+ fail_open: z14.object({
1067
+ max_consecutive_failures: z14.number().int().positive().default(50),
1068
+ max_window_seconds: z14.number().int().positive().default(60),
1069
+ recovery_threshold: z14.number().int().positive().default(20),
1070
+ degraded_mode: z14.enum([
1071
+ "safe_mode_block",
1072
+ "safe_mode_throttle",
1073
+ "runtime_killswitch_trigger"
1074
+ ]).default("safe_mode_throttle")
1075
+ }).default({
1076
+ max_consecutive_failures: 50,
1077
+ max_window_seconds: 60,
1078
+ recovery_threshold: 20,
1079
+ degraded_mode: "safe_mode_throttle"
1080
+ })
1081
+ });
1082
+ var retryConfigSchema = z14.object({
1083
+ max_attempts: z14.number().int().min(1).max(5).default(2),
1084
+ /**
1085
+ * HTTP status codes that trigger a retry. 5xx is the default; opt
1086
+ * into 429 retries only when the upstream understands `Retry-After`.
1087
+ */
1088
+ retry_on_status: z14.array(z14.number().int().min(400).max(599)).min(1).max(20).default([502, 503, 504]),
1089
+ /**
1090
+ * Backoff curve. Total wall-clock attempt time is bounded so the
1091
+ * gateway worker cannot block past `total_budget_ms`.
1092
+ */
1093
+ backoff: z14.object({
1094
+ initial_ms: z14.number().int().positive().max(5e3).default(100),
1095
+ multiplier: z14.number().positive().max(10).default(2),
1096
+ jitter: z14.enum(["none", "full", "equal"]).default("equal"),
1097
+ total_budget_ms: z14.number().int().positive().max(3e4).default(5e3)
1098
+ }).default({
1099
+ initial_ms: 100,
1100
+ multiplier: 2,
1101
+ jitter: "equal",
1102
+ total_budget_ms: 5e3
1103
+ })
1104
+ });
1105
+ var transformConfigSchema = z14.object({
1106
+ /**
1107
+ * When the transform applies. `request` runs before upstream forward;
1108
+ * `response` runs after. Most transforms are one or the other; both
1109
+ * is rare.
1110
+ */
1111
+ applies_to: z14.enum(["request", "response", "both"]).default("request"),
1112
+ /**
1113
+ * List of key rewrites. Source path uses dot notation (`a.b.c`);
1114
+ * `target` may include the same syntax to move keys around. Drops
1115
+ * are expressed as `target: null`.
1116
+ */
1117
+ rewrites: z14.array(z14.object({
1118
+ source: z14.string().min(1).max(200),
1119
+ target: z14.string().min(1).max(200).nullable()
1120
+ })).min(1).max(20)
1121
+ });
1122
+ var policyBodySchema = z14.discriminatedUnion("type", [
1123
+ z14.object({ type: z14.literal("rate_limit"), config: rateLimitConfigSchema }),
1124
+ z14.object({ type: z14.literal("auth"), config: authConfigSchema }),
1125
+ z14.object({ type: z14.literal("concurrency"), config: concurrencyConfigSchema }),
1126
+ z14.object({ type: z14.literal("retry"), config: retryConfigSchema }),
1127
+ z14.object({ type: z14.literal("transform"), config: transformConfigSchema })
1128
+ ]);
1129
+
1130
+ // ../contracts/dist/plans/spec/policies-layer.js
1131
+ var cacheProfileSchema = z15.enum(["long", "short", "blocking"]).default("long");
1132
+ var policyCompatibilitySchema = z15.object({
1133
+ route_types: z15.array(z15.enum(["http"])).max(5).optional(),
1134
+ meters: z15.array(z15.string().min(1).max(64)).max(20).optional(),
1135
+ auth_modes: z15.array(z15.enum(["api_key", "oauth2", "anonymous"])).max(5).optional()
1136
+ });
1137
+ var policyLayerSchema = z15.intersection(z15.object({
1138
+ /**
1139
+ * Policy name. Referenced by routes via `policies: [<name>]`. Must
1140
+ * be unique across the product; the compiler enforces this in the
1141
+ * cross-layer validation pass.
1142
+ */
1143
+ name: z15.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Policy name must be lowercase alphanumeric with hyphens/underscores"),
1144
+ description: z15.string().max(500).optional(),
1145
+ compatible_with: policyCompatibilitySchema.default({}),
1146
+ /**
1147
+ * Mutation class — runtime vs contractual. Policies are operational
1148
+ * by nature so the default is `runtime`. Marking a policy as
1149
+ * `contractual` signals that changes to it require human approval
1150
+ * (invariant #16).
1151
+ */
1152
+ mutation_class: z15.enum(["runtime", "contractual"]).default("runtime"),
1153
+ cacheProfile: cacheProfileSchema
1154
+ }), policyBodySchema);
1155
+
1156
+ // ../contracts/dist/framework/actions/index.js
1157
+ import { z as z16 } from "zod";
1158
+ var actionKindSchema = z16.enum(["query", "mutation"]);
1159
+ var actionAuditPolicySchema = z16.enum(["none", "metadata", "full"]);
1160
+ var actionSubjectBindingSchema = z16.object({
1161
+ type: z16.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/),
1162
+ from: z16.enum(["header", "path_param"]),
1163
+ name: z16.string().min(1).max(120)
1164
+ });
1165
+ var actionResourceEffectSchema = z16.object({
1166
+ resource: z16.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/),
1167
+ effect: z16.enum(["create", "delete"])
1168
+ });
1169
+ var actionSpecSchema = z16.object({
1170
+ id: z16.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/),
1171
+ title: z16.string().min(1).max(160).optional(),
1172
+ kind: actionKindSchema,
1173
+ actorType: z16.string().min(1).max(64).regex(/^[a-zA-Z0-9_.:-]+$/).optional(),
1174
+ subject: actionSubjectBindingSchema.optional(),
1175
+ inputSchemaRef: z16.string().min(1).max(240).optional(),
1176
+ audit: actionAuditPolicySchema.default("metadata"),
1177
+ resource: actionResourceEffectSchema.optional()
1178
+ });
1179
+
1180
+ // ../contracts/dist/plans/spec/routes-layer.js
1181
+ var routeMatchSchema = z17.object({
1182
+ method: z17.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
1183
+ path: z17.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]")
1184
+ });
1185
+ function statusPolicyPartIsValid(part) {
1186
+ const trimmed = part.trim();
1187
+ if (!trimmed)
1188
+ return false;
1189
+ const [startRaw, endRaw, extra] = trimmed.split("-");
1190
+ if (extra !== void 0 || !/^\d{3}$/.test(startRaw ?? ""))
1191
+ return false;
1192
+ const start = Number(startRaw);
1193
+ const end = endRaw === void 0 ? start : Number(endRaw);
1194
+ if (endRaw !== void 0 && !/^\d{3}$/.test(endRaw))
1195
+ return false;
1196
+ return start >= 100 && start <= 599 && end >= 100 && end <= 599 && start <= end;
944
1197
  }
945
- function validateFreePlanPrice(plan, index, ctx) {
946
- const monthly = planMonthlyPrice(plan);
947
- if ((monthly ?? 0) === 0)
948
- return;
949
- ctx.addIssue({
950
- code: "custom",
951
- path: ["plans", index, "recurring_fee_cents"],
952
- message: "Free plans must have zero price"
953
- });
1198
+ function isRouteStatusCodePolicyString(value) {
1199
+ return value.split(",").every(statusPolicyPartIsValid);
954
1200
  }
955
- function validateFreePlanHardLimit(plan, index, ctx) {
956
- const hasHardLimit = plan.limits.some((limit) => !limit.enforcement || limit.enforcement === "enforce");
957
- if (hasHardLimit)
958
- return;
959
- ctx.addIssue({
960
- code: "custom",
961
- path: ["plans", index, "limits"],
1201
+ var routeStatusCodePolicySchema = z17.union([
1202
+ z17.string().min(1).max(100).refine(isRouteStatusCodePolicyString, {
1203
+ message: "onStatusCodes must be comma-separated HTTP status codes or numeric ranges, e.g. 200-299,304"
1204
+ }),
1205
+ z17.array(z17.number().int().min(100).max(599)).min(1).max(100)
1206
+ ]);
1207
+ var routeDefinitionSchema = z17.object({
1208
+ match: routeMatchSchema,
1209
+ metering: z17.object({
1210
+ defaults: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
1211
+ reports: z17.array(z17.string().min(1).max(64)).max(20).optional(),
1212
+ estimates: z17.record(z17.string().min(1).max(64), z17.number().finite().nonnegative()).optional(),
1213
+ onStatusCodes: routeStatusCodePolicySchema.optional()
1214
+ }).optional(),
1215
+ unmetered: z17.boolean().optional(),
1216
+ inheritDefaultMeters: z17.boolean().optional(),
1217
+ /** Optional explicit action id. When absent, the compiler derives an
1218
+ * implicit action from feature + method + path. */
1219
+ action: z17.string().min(1).max(160).regex(/^[a-z0-9_.:-]+$/).optional(),
1220
+ /** BYO-Backend V1 — optional route→backend binding. Omitted = the sole /
1221
+ * default backend (single-backend products stay zero-config). The schema
1222
+ * is `.strict()`, so this key MUST be declared here or `parse` throws on
1223
+ * it (anti-`.strict()`). */
1224
+ backend: routeBackendBindingSchema.optional()
1225
+ }).strict();
1226
+ var routeUpstreamSchema = z17.object({
1227
+ override_origin: z17.string().url("override_origin must be a valid URL").nullable().default(null)
1228
+ });
1229
+ var routeRuntimeSchema = z17.object({
1230
+ rollout_key: z17.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "rollout_key must be lowercase alphanumeric with hyphens/underscores").optional(),
1231
+ /**
1232
+ * Optional runtime flags this feature depends on. The runtime
1233
+ * evaluator AND's the feature's enablement across all referenced
1234
+ * flags. If any flag is disabled, the route returns the configured
1235
+ * fallback (404 by default — see /runtime failure matrix).
1236
+ */
1237
+ required_flags: z17.array(z17.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(10).optional()
1238
+ });
1239
+ var routeLayerSchema = z17.object({
1240
+ /**
1241
+ * Feature key — the entitlement unit. Surfaced in dashboards,
1242
+ * subscriptions, and the gateway's matched-route trace.
1243
+ */
1244
+ feature: z17.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/, "feature key must be lowercase alphanumeric with [_.:-]"),
1245
+ description: z17.string().max(500).optional(),
1246
+ /**
1247
+ * Route additions are contractual by default — they expose new API
1248
+ * surface to subscribers. Internal/non-customer-visible routes can
1249
+ * mark themselves `runtime` to allow autonomous agent flips
1250
+ * (invariant #16; see RFC approval matrix).
1251
+ */
1252
+ mutation_class: z17.enum(["runtime", "contractual"]).default("contractual"),
1253
+ cacheProfile: cacheProfileSchema,
1254
+ routes: z17.array(routeDefinitionSchema).min(1).max(50),
1255
+ upstream: routeUpstreamSchema.default({ override_origin: null }),
1256
+ /**
1257
+ * Ordered list of policy names to apply. Executed sequentially by
1258
+ * the gateway policy engine; first-deny wins. Referenced policies
1259
+ * MUST declare compatible `compatible_with` envelopes for this
1260
+ * feature's route/meter shape — the compiler enforces.
1261
+ */
1262
+ policies: z17.array(z17.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
1263
+ runtime: routeRuntimeSchema.default({}),
1264
+ /**
1265
+ * Plans that grant this feature directly. Shared feature bundles are
1266
+ * expressed in capability layers via `includes_features`; route layers do
1267
+ * not declare capability membership.
1268
+ */
1269
+ plans: z17.array(z17.string().min(1).max(64)).max(20).default([]),
1270
+ /** Explicit actions declared by this feature. Routes reference them by
1271
+ * `route.action`; routes without a binding receive implicit actions. */
1272
+ actions: z17.array(actionSpecSchema).max(100).optional(),
1273
+ /** BYO-Backend V1 — feature-level default backend binding. Routes in this
1274
+ * layer with no explicit `route.backend` inherit this; routes may still
1275
+ * override per-route. Omitted = the product's sole / default backend. */
1276
+ backend: routeBackendBindingSchema.optional()
1277
+ }).strict();
1278
+
1279
+ // ../contracts/dist/plans/spec/refinements.js
1280
+ function rejectUsagePricing(spec, ctx) {
1281
+ if (spec.usagePricing === void 0)
1282
+ return;
1283
+ ctx.addIssue({
1284
+ code: "custom",
1285
+ path: ["usagePricing"],
1286
+ message: "usagePricing is not supported. Define usage.meters.<key>.rating instead."
1287
+ });
1288
+ }
1289
+ function validateFreePlans(plans, ctx) {
1290
+ const freePlans = plans.filter((plan) => plan.free);
1291
+ if (freePlans.length > 1) {
1292
+ ctx.addIssue({
1293
+ code: "custom",
1294
+ path: ["plans"],
1295
+ message: "Only one free plan is allowed per product"
1296
+ });
1297
+ }
1298
+ plans.forEach((plan, index) => {
1299
+ if (!plan.free)
1300
+ return;
1301
+ validateFreePlanPrice(plan, index, ctx);
1302
+ validateFreePlanHardLimit(plan, index, ctx);
1303
+ });
1304
+ }
1305
+ function validateFreePlanPrice(plan, index, ctx) {
1306
+ const monthly = planMonthlyPrice(plan);
1307
+ if ((monthly ?? 0) === 0)
1308
+ return;
1309
+ ctx.addIssue({
1310
+ code: "custom",
1311
+ path: ["plans", index, "recurring_fee_cents"],
1312
+ message: "Free plans must have zero price"
1313
+ });
1314
+ }
1315
+ function validateFreePlanHardLimit(plan, index, ctx) {
1316
+ const hasHardLimit = plan.limits.some((limit) => !limit.enforcement || limit.enforcement === "enforce");
1317
+ if (hasHardLimit)
1318
+ return;
1319
+ ctx.addIssue({
1320
+ code: "custom",
1321
+ path: ["plans", index, "limits"],
962
1322
  message: "Free plans must include at least one hard enforced limit"
963
1323
  });
964
1324
  }
@@ -1158,6 +1518,68 @@ function validateLimitMeterReachability(spec, ctx) {
1158
1518
  });
1159
1519
  });
1160
1520
  }
1521
+ function validateBackendReferences(spec, ctx) {
1522
+ const backends = spec.backend;
1523
+ if (!backends || Object.keys(backends).length === 0)
1524
+ return;
1525
+ const backendIds = new Set(Object.keys(backends));
1526
+ const productMeterKeys = new Set((spec.metering?.meters ?? []).map((m) => m.key).filter((k) => typeof k === "string" && k.length > 0));
1527
+ for (const [backendId, backend] of Object.entries(backends)) {
1528
+ (backend.meters ?? []).forEach((meter, meterIdx) => {
1529
+ if (productMeterKeys.has(meter))
1530
+ return;
1531
+ ctx.addIssue({
1532
+ code: "custom",
1533
+ path: ["backend", backendId, "meters", meterIdx],
1534
+ message: `${BACKEND_DIAGNOSTIC_CODES.unknownMeterInBackend}: backend "${backendId}" allows meter "${meter}" but no metering.meters[] entry declares it.`
1535
+ });
1536
+ });
1537
+ }
1538
+ const { defaultId, ambiguous } = resolveDefaultBackendId(backends);
1539
+ let anyRouteOmitsBinding = false;
1540
+ for (const feature of Object.values(spec.features ?? {})) {
1541
+ for (const route of feature.routes ?? []) {
1542
+ if (route.backend === void 0)
1543
+ anyRouteOmitsBinding = true;
1544
+ }
1545
+ }
1546
+ if (ambiguous && anyRouteOmitsBinding) {
1547
+ ctx.addIssue({
1548
+ code: "custom",
1549
+ path: ["backend"],
1550
+ 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.`
1551
+ });
1552
+ }
1553
+ for (const [featureKey, feature] of Object.entries(spec.features ?? {})) {
1554
+ (feature.routes ?? []).forEach((route, routeIdx) => {
1555
+ const boundId = route.backend ?? defaultId;
1556
+ if (route.backend !== void 0 && !backendIds.has(route.backend)) {
1557
+ ctx.addIssue({
1558
+ code: "custom",
1559
+ path: ["features", featureKey, "routes", routeIdx, "backend"],
1560
+ message: `${BACKEND_DIAGNOSTIC_CODES.unknownBackendInRoute}: route binds backend "${route.backend}" which is not declared in the product \`backend\` block.`
1561
+ });
1562
+ return;
1563
+ }
1564
+ if (!boundId)
1565
+ return;
1566
+ const backend = backends[boundId];
1567
+ const allow = backend?.meters;
1568
+ if (!allow)
1569
+ return;
1570
+ const allowed = new Set(allow);
1571
+ for (const meter of routeMeterKeys(route)) {
1572
+ if (allowed.has(meter))
1573
+ continue;
1574
+ ctx.addIssue({
1575
+ code: "custom",
1576
+ path: ["features", featureKey, "routes", routeIdx, "metering"],
1577
+ message: `${BACKEND_DIAGNOSTIC_CODES.routeMeterNotAllowedByBackend}: route meters "${meter}" but backend "${boundId}" does not allow it (backend.meters[] = [${allow.join(", ")}]).`
1578
+ });
1579
+ }
1580
+ });
1581
+ }
1582
+ }
1161
1583
  function planMonthlyPrice(plan) {
1162
1584
  return plan.recurring_fee_cents;
1163
1585
  }
@@ -1167,18 +1589,18 @@ function planPriceKey(plan, monthly) {
1167
1589
  }
1168
1590
 
1169
1591
  // ../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")
1592
+ var productIdentitySchema = z18.object({
1593
+ subdomain: z18.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "Subdomain must be lowercase alphanumeric with optional hyphens")
1172
1594
  });
1173
- var meterEnforcementTypeSchema = z13.enum([
1595
+ var meterEnforcementTypeSchema = z18.enum([
1174
1596
  "exact_pre_request",
1175
1597
  "estimated_then_settled",
1176
1598
  "postpaid",
1177
1599
  "strict_concurrency"
1178
1600
  ]);
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),
1601
+ var meterDefinitionSchema = z18.object({
1602
+ key: z18.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Meter key must be lowercase alphanumeric with underscores"),
1603
+ display: z18.string().min(1).max(100),
1182
1604
  // v0.42.0 — `type: "built-in" | "custom"` removed. The runtime never
1183
1605
  // read it (gateway estimators key on meter NAME, Stripe meter
1184
1606
  // creation keys on meter KEY); it was schema documentation. Wizard
@@ -1186,11 +1608,11 @@ var meterDefinitionSchema = z13.object({
1186
1608
  // ai_usage → `dollars`); Custom template lets builders define keys
1187
1609
  // freely. Old specs with `type: ...` parse cleanly because Zod
1188
1610
  // strips unknown fields by default.
1189
- unit: z13.string().max(20).optional(),
1611
+ unit: z18.string().max(20).optional(),
1190
1612
  /** Reusable pre-request estimate for routes that dynamically report this meter. */
1191
- estimate: z13.number().finite().nonnegative().optional(),
1613
+ estimate: z18.number().finite().nonnegative().optional(),
1192
1614
  /** Fixed per-request default applied by Product SDK helpers. */
1193
- routeDefault: z13.number().finite().nonnegative().optional(),
1615
+ routeDefault: z18.number().finite().nonnegative().optional(),
1194
1616
  /**
1195
1617
  * Runtime enforcement semantics for this meter. This is compiled into
1196
1618
  * signed gateway artifacts so the edge chooses reservation, settlement,
@@ -1215,7 +1637,7 @@ var meterDefinitionSchema = z13.object({
1215
1637
  * current `window`. Defaults to `COUNT` (one event = one unit) so
1216
1638
  * existing meters that didn't declare aggregation continue to work.
1217
1639
  */
1218
- aggregation: z13.enum(["SUM", "COUNT", "MAX", "UNIQUE_COUNT", "LATEST"]).default("COUNT"),
1640
+ aggregation: z18.enum(["SUM", "COUNT", "MAX", "UNIQUE_COUNT", "LATEST"]).default("COUNT"),
1219
1641
  /**
1220
1642
  * Aggregation window. `billing_period` (the default) makes the
1221
1643
  * meter accumulate across the subscription's billing period.
@@ -1223,91 +1645,88 @@ var meterDefinitionSchema = z13.object({
1223
1645
  * rate-limit-shaped meters where the period boundary is a fixed
1224
1646
  * wall-clock interval, not the subscription anniversary.
1225
1647
  */
1226
- window: z13.enum(["minute", "hour", "day", "month", "billing_period"]).default("billing_period"),
1648
+ window: z18.enum(["minute", "hour", "day", "month", "billing_period"]).default("billing_period"),
1227
1649
  /**
1228
1650
  * Property on the event payload to read for `SUM` and `MAX`
1229
1651
  * aggregations. Optional at the schema level; core's `validate.ts`
1230
1652
  * (Phase 1b) enforces "required when aggregation is SUM or MAX".
1231
1653
  */
1232
- valueProperty: z13.string().optional(),
1654
+ valueProperty: z18.string().optional(),
1233
1655
  /**
1234
1656
  * Property on the event payload to dedupe on for `UNIQUE_COUNT`
1235
1657
  * aggregation. Optional at the schema level; core's validate-pass
1236
1658
  * enforces "required when aggregation is UNIQUE_COUNT".
1237
1659
  */
1238
- uniqueProperty: z13.string().optional(),
1660
+ uniqueProperty: z18.string().optional(),
1239
1661
  /**
1240
1662
  * Optional grouping dimensions. When set, the aggregation is per
1241
1663
  * unique combination of these properties' values, not a single
1242
1664
  * scalar. Used for per-region or per-model breakdowns.
1243
1665
  */
1244
- groupBy: z13.array(z13.string()).optional(),
1666
+ groupBy: z18.array(z18.string()).optional(),
1245
1667
  /**
1246
1668
  * Lago-side event code for ingress correlation. Matches Lago's
1247
1669
  * BillableMetric `code` so events sent to Lago land on the right
1248
1670
  * meter without a per-meter translation table.
1249
1671
  */
1250
- eventCode: z13.string().optional()
1672
+ eventCode: z18.string().optional()
1251
1673
  });
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([
1674
+ var usageMeasureSchema = z18.string().min(1).max(64).regex(/^[a-z0-9_]+$/, "Usage measure must be lowercase alphanumeric with underscores");
1675
+ var usageRatingPricePolicySchema = z18.enum([
1254
1676
  "pass_through",
1255
1677
  "markup",
1256
1678
  "fixed_margin",
1257
1679
  "customer_rate"
1258
1680
  ]);
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()))
1681
+ var fixedRatingSchema = z18.object({
1682
+ source: z18.literal("fixed"),
1683
+ rates: z18.record(z18.string().min(1), z18.record(usageMeasureSchema, z18.number().int().nonnegative()))
1262
1684
  });
1263
- var providerCatalogRatingSchema = z13.object({
1264
- source: z13.literal("provider_catalog"),
1265
- catalog: z13.string().min(1).max(100),
1685
+ var providerCatalogRatingSchema = z18.object({
1686
+ source: z18.literal("provider_catalog"),
1687
+ catalog: z18.string().min(1).max(100),
1266
1688
  pricePolicy: usageRatingPricePolicySchema.default("pass_through"),
1267
- markupPercent: z13.number().nonnegative().max(1e4).optional(),
1268
- marginMicros: z13.number().int().nonnegative().optional()
1689
+ markupPercent: z18.number().nonnegative().max(1e4).optional(),
1690
+ marginMicros: z18.number().int().nonnegative().optional()
1269
1691
  });
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()
1692
+ var upstreamReportedRatingSchema = z18.object({
1693
+ source: z18.literal("upstream_reported"),
1694
+ amountField: z18.string().min(1).max(500),
1695
+ currencyField: z18.string().min(1).max(500).optional()
1274
1696
  });
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()
1697
+ var externalRateApiRatingSchema = z18.object({
1698
+ source: z18.literal("external_rate_api"),
1699
+ resolver: z18.string().min(1).max(100),
1700
+ configRef: z18.string().min(1).max(200).optional()
1279
1701
  });
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()
1702
+ var customRatingSchema = z18.object({
1703
+ source: z18.literal("custom"),
1704
+ resolver: z18.string().min(1).max(100),
1705
+ configRef: z18.string().min(1).max(200).optional()
1284
1706
  });
1285
- var usageRatingSchema = z13.discriminatedUnion("source", [
1707
+ var usageRatingSchema = z18.discriminatedUnion("source", [
1286
1708
  fixedRatingSchema,
1287
1709
  providerCatalogRatingSchema,
1288
1710
  upstreamReportedRatingSchema,
1289
1711
  externalRateApiRatingSchema,
1290
1712
  customRatingSchema
1291
1713
  ]);
1292
- var usageMeterSchema = z13.object({
1293
- selector: z13.string().min(1).max(100).optional(),
1294
- measures: z13.array(usageMeasureSchema).min(1).max(20),
1714
+ var usageMeterSchema = z18.object({
1715
+ selector: z18.string().min(1).max(100).optional(),
1716
+ measures: z18.array(usageMeasureSchema).min(1).max(20),
1295
1717
  rating: usageRatingSchema.optional()
1296
1718
  });
1297
- var usageBlockSchema = z13.object({
1298
- meters: z13.record(z13.string().min(1).max(64).regex(/^[a-z0-9_]+$/), usageMeterSchema)
1719
+ var usageBlockSchema = z18.object({
1720
+ meters: z18.record(z18.string().min(1).max(64).regex(/^[a-z0-9_]+$/), usageMeterSchema)
1299
1721
  });
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()
1722
+ var routeMeteringSchema = z18.object({
1723
+ defaults: z18.record(z18.string().min(1).max(64), z18.number().finite().nonnegative()).optional(),
1724
+ reports: z18.array(z18.string().min(1).max(64)).max(20).optional(),
1725
+ estimates: z18.record(z18.string().min(1).max(64), z18.number().finite().nonnegative()).optional(),
1726
+ onStatusCodes: routeStatusCodePolicySchema.optional()
1308
1727
  });
1309
- var featureRouteSchema = z13.object({
1310
- method: z13.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
1728
+ var featureRouteSchema = z18.object({
1729
+ method: z18.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]).default("*"),
1311
1730
  // Path is the route under the product's baseUrl. OpenAPI parameter
1312
1731
  // syntax is supported and translated by the compiler:
1313
1732
  // /users/:id → /users/*
@@ -1315,44 +1734,48 @@ var featureRouteSchema = z13.object({
1315
1734
  // Path-globs `*` (one segment) and `**` (any subpath) are passed
1316
1735
  // through. The compiler rejects ambiguous compound segments like
1317
1736
  // `/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_/:.{}*-]"),
1737
+ path: z18.string().min(1).max(500).regex(/^\/[a-zA-Z0-9_/:.{}*-]*$/, "path must start with / and contain only [a-zA-Z0-9_/:.{}*-]"),
1319
1738
  // Explicit no-usage route. Dynamic/static route metering is declared
1320
1739
  // exclusively under `metering`.
1321
- unmetered: z13.boolean().optional(),
1740
+ unmetered: z18.boolean().optional(),
1322
1741
  metering: routeMeteringSchema.optional(),
1323
- inheritDefaultMeters: z13.boolean().optional()
1742
+ inheritDefaultMeters: z18.boolean().optional(),
1743
+ // BYO-Backend V1 — route→backend binding. The compiler materializes
1744
+ // per-feature route layers into this strict route shape, so the key MUST be
1745
+ // declared here or `parse` throws on it (anti-`.strict()`).
1746
+ backend: routeBackendBindingSchema.optional()
1324
1747
  }).strict();
1325
- var featureCatalogEntrySchema = z13.object({
1748
+ var featureCatalogEntrySchema = z18.object({
1326
1749
  // 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),
1750
+ description: z18.string().max(500).optional(),
1751
+ routes: z18.array(featureRouteSchema).min(1).max(50),
1329
1752
  // Plans that grant this feature. Feature-first canonical mapping —
1330
1753
  // builders declare "which plans get this feature" on the feature
1331
1754
  // itself rather than enumerating features per plan. Required and
1332
1755
  // non-empty: a feature with no plans grants nothing and is a likely
1333
1756
  // typo. Cross-reference validation (every key resolves to an
1334
1757
  // existing plan) lives in `validateFeatureReferences` below.
1335
- plans: z13.array(z13.string().min(1)).min(1).max(20)
1758
+ plans: z18.array(z18.string().min(1)).min(1).max(20)
1336
1759
  });
1337
- var featureCatalogSchema = z13.record(z13.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
1338
- var productCleanupPolicyModeSchema = z13.enum([
1760
+ var featureCatalogSchema = z18.record(z18.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
1761
+ var productCleanupPolicyModeSchema = z18.enum([
1339
1762
  "report",
1340
1763
  "pull_request"
1341
1764
  ]);
1342
- var productChangeApprovalRiskSchema = z13.enum([
1765
+ var productChangeApprovalRiskSchema = z18.enum([
1343
1766
  "safe",
1344
1767
  "non_blocking",
1345
1768
  "economic_risk",
1346
1769
  "blocking"
1347
1770
  ]);
1348
- var productOperatorPoliciesSchema = z13.object({
1771
+ var productOperatorPoliciesSchema = z18.object({
1349
1772
  /**
1350
1773
  * Route cleanup operator. Disabled by default; report-mode is the safe
1351
1774
  * default so a product can surface zero-traffic runtime-route candidates
1352
1775
  * before it opts into draft PR mutation.
1353
1776
  */
1354
- cleanup: z13.object({
1355
- enabled: z13.boolean().default(false),
1777
+ cleanup: z18.object({
1778
+ enabled: z18.boolean().default(false),
1356
1779
  mode: productCleanupPolicyModeSchema.default("report")
1357
1780
  }).default({ enabled: false, mode: "report" }),
1358
1781
  /**
@@ -1360,9 +1783,9 @@ var productOperatorPoliciesSchema = z13.object({
1360
1783
  * the policy a first-class product-as-code field even while enforcement is
1361
1784
  * still report/label-only.
1362
1785
  */
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"])
1786
+ change_approval: z18.object({
1787
+ auto_merge_max_risk: z18.enum(["none", "safe", "non_blocking"]).default("none"),
1788
+ require_human_for: z18.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
1366
1789
  }).default({
1367
1790
  auto_merge_max_risk: "none",
1368
1791
  require_human_for: ["economic_risk", "blocking"]
@@ -1374,15 +1797,15 @@ var productOperatorPoliciesSchema = z13.object({
1374
1797
  require_human_for: ["economic_risk", "blocking"]
1375
1798
  }
1376
1799
  });
1377
- var customerIdentityRequirementSchema = z13.enum([
1800
+ var customerIdentityRequirementSchema = z18.enum([
1378
1801
  "org_only",
1379
1802
  "org_and_user"
1380
1803
  ]);
1381
- var customerPortalAuthStrategySchema = z13.enum([
1804
+ var customerPortalAuthStrategySchema = z18.enum([
1382
1805
  "clerk",
1383
1806
  "test-personas"
1384
1807
  ]);
1385
- var productCustomerContextSchema = z13.object({
1808
+ var productCustomerContextSchema = z18.object({
1386
1809
  /**
1387
1810
  * Edge credential identity policy. This is intentionally Product-scoped:
1388
1811
  * B7 keeps Product as the product boundary and avoids customer-side
@@ -1394,19 +1817,19 @@ var productCustomerContextSchema = z13.object({
1394
1817
  * runtime signing secret in product/product.config.ts. Core generates/preserves the
1395
1818
  * secret when this is true and clears it when explicitly false.
1396
1819
  */
1397
- context_tokens: z13.object({
1398
- enabled: z13.boolean().default(true)
1820
+ context_tokens: z18.object({
1821
+ enabled: z18.boolean().default(true)
1399
1822
  }).optional(),
1400
1823
  /**
1401
1824
  * Portal auth strategy for environment-scoped product applies. Production
1402
1825
  * portal auth is provisioner-owned; preview/test environments can opt into
1403
1826
  * test personas through Product-as-Code.
1404
1827
  */
1405
- portal_auth: z13.object({
1828
+ portal_auth: z18.object({
1406
1829
  strategy: customerPortalAuthStrategySchema
1407
1830
  }).optional()
1408
1831
  });
1409
- var productSurfaceTypeSchema = z13.enum([
1832
+ var productSurfaceTypeSchema = z18.enum([
1410
1833
  "frontend",
1411
1834
  "api",
1412
1835
  "docs",
@@ -1416,87 +1839,99 @@ var productSurfaceTypeSchema = z13.enum([
1416
1839
  "worker",
1417
1840
  "agent"
1418
1841
  ]);
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"),
1842
+ var productSurfaceSchema = z18.object({
1843
+ key: z18.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Surface key must be lowercase alphanumeric with hyphens/underscores"),
1421
1844
  type: productSurfaceTypeSchema,
1422
- display: z13.string().min(1).max(100).optional(),
1423
- description: z13.string().max(500).optional()
1424
- });
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([
1845
+ display: z18.string().min(1).max(100).optional(),
1846
+ description: z18.string().max(500).optional()
1847
+ });
1848
+ var productEntitlementSchema = z18.object({
1849
+ key: z18.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Entitlement key must be lowercase alphanumeric with hyphens/underscores"),
1850
+ description: z18.string().max(500).optional(),
1851
+ capabilities: z18.array(z18.string().min(1).max(100)).max(100).optional(),
1852
+ featureGates: z18.record(z18.string().min(1), z18.boolean()).optional(),
1853
+ limits: z18.array(planLimitRuleSchema).max(100).optional(),
1854
+ meters: z18.array(z18.string().min(1).max(64)).max(100).optional()
1855
+ });
1856
+ var productSurfacesSchema = z18.array(productSurfaceSchema).max(20).default([]);
1857
+ var productEntitlementsSchema = z18.array(productEntitlementSchema).max(100).default([]);
1858
+ var productWorkflowKindSchema = z18.enum([
1436
1859
  "async_job",
1437
1860
  "agent_task",
1438
1861
  "scheduled",
1439
1862
  "lifecycle",
1440
1863
  "background"
1441
1864
  ]);
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)
1865
+ var productWorkflowTriggerSchema = z18.discriminatedUnion("type", [
1866
+ z18.object({ type: z18.literal("manual") }),
1867
+ z18.object({
1868
+ type: z18.literal("schedule"),
1869
+ cron: z18.string().min(1).max(120)
1447
1870
  }),
1448
- z13.object({
1449
- type: z13.literal("event"),
1450
- event: z13.string().min(1).max(120)
1871
+ z18.object({
1872
+ type: z18.literal("event"),
1873
+ event: z18.string().min(1).max(120)
1451
1874
  }),
1452
- z13.object({
1453
- type: z13.literal("api"),
1454
- path: z13.string().min(1).max(240).regex(/^\//, "path must start with /")
1875
+ z18.object({
1876
+ type: z18.literal("api"),
1877
+ path: z18.string().min(1).max(240).regex(/^\//, "path must start with /")
1455
1878
  }),
1456
- z13.object({
1457
- type: z13.literal("lifecycle"),
1458
- event: z13.string().min(1).max(120)
1879
+ z18.object({
1880
+ type: z18.literal("lifecycle"),
1881
+ event: z18.string().min(1).max(120)
1459
1882
  })
1460
1883
  ]);
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(),
1884
+ var productWorkflowSchema = z18.object({
1885
+ key: z18.string().min(1).max(64).regex(/^[a-z0-9_-]+$/, "Workflow key must be lowercase alphanumeric with hyphens/underscores"),
1886
+ title: z18.string().min(1).max(120).optional(),
1887
+ description: z18.string().max(1e3).optional(),
1465
1888
  kind: productWorkflowKindSchema,
1466
1889
  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"),
1890
+ capabilities: z18.array(z18.string().min(1).max(100)).max(100).optional(),
1891
+ meters: z18.array(z18.string().min(1).max(64)).max(100).optional(),
1892
+ estimates: z18.record(z18.string().min(1).max(64), z18.number().finite()).optional(),
1893
+ metadata: z18.record(z18.string().min(1), z18.unknown()).optional()
1894
+ });
1895
+ var productWorkflowsSchema = z18.array(productWorkflowSchema).max(100).default([]);
1896
+ var productSpecSchema = z18.object({
1897
+ product: z18.object({
1898
+ name: z18.string().min(1).max(100),
1899
+ displayName: z18.string().max(200).optional(),
1900
+ description: z18.string().max(2e3).optional(),
1901
+ baseUrl: z18.string().url("baseUrl must be a valid URL"),
1902
+ sandboxBaseUrl: z18.string().url("sandboxBaseUrl must be a valid URL").optional(),
1903
+ visibility: z18.enum(["public", "private"]).default("public"),
1481
1904
  // Branding
1482
- logoUrl: z13.string().url().optional(),
1483
- primaryColor: z13.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
1905
+ logoUrl: z18.string().url().optional(),
1906
+ primaryColor: z18.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
1484
1907
  // Environment
1485
- envBranchPrefix: z13.string().max(50).nullable().optional()
1908
+ envBranchPrefix: z18.string().max(50).nullable().optional()
1486
1909
  }),
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()
1910
+ gateway: z18.object({
1911
+ authHeader: z18.string().min(1).max(100).default("x-api-key"),
1912
+ upstreamAuth: z18.object({
1913
+ type: z18.enum(["none", "static_bearer"]),
1914
+ token: z18.string().optional()
1492
1915
  }).default({ type: "none" })
1493
1916
  }),
1494
- metering: z13.object({
1495
- meters: z13.array(meterDefinitionSchema).max(10).default([]),
1496
- billOn4xx: z13.boolean().default(false)
1917
+ metering: z18.object({
1918
+ meters: z18.array(meterDefinitionSchema).max(10).default([]),
1919
+ billOn4xx: z18.boolean().default(false)
1497
1920
  }).default({ meters: [], billOn4xx: false }),
1921
+ /**
1922
+ * BYO-Backend V1 — first-class backend declarations, keyed by backend id.
1923
+ * A product may declare MULTIPLE backends; routes bind to one (a default
1924
+ * applies when a product has exactly one backend OR exactly one is marked
1925
+ * `default: true`). Single-backend products stay zero-config.
1926
+ *
1927
+ * OPTIONAL and emits NO key when absent — a product with no `backend`
1928
+ * block hashes byte-identically to the pre-BYOB world. Cross-reference
1929
+ * validation (route→backend resolution, route-meter ∈ backend `meters[]`,
1930
+ * unambiguous default) lives in `validateBackendReferences`.
1931
+ */
1932
+ backend: productBackendBlockSchema.optional(),
1498
1933
  usage: usageBlockSchema.optional(),
1499
- usagePricing: z13.never({
1934
+ usagePricing: z18.never({
1500
1935
  error: "usagePricing is not supported. Define usage.meters.<key>.rating instead."
1501
1936
  }).optional(),
1502
1937
  features: featureCatalogSchema.optional(),
@@ -1520,12 +1955,12 @@ var productSpecSchema = z13.object({
1520
1955
  // `max_monthly_spend_cents`). The product-level `billing` block
1521
1956
  // retains the transition-policy fields (`gracePeriodDays`,
1522
1957
  // `subscriberChangePolicy`); the strategy enum is gone.
1523
- billing: z13.object({
1524
- gracePeriodDays: z13.number().int().nonnegative().default(3),
1958
+ billing: z18.object({
1959
+ gracePeriodDays: z18.number().int().nonnegative().default(3),
1525
1960
  // When true (default), a plan limit INCREASE re-projects onto active
1526
1961
  // subscribers immediately; a DECREASE always defers to period end.
1527
1962
  // Read by advanceActiveSubscribersToLatestCompiledPlans.
1528
- applyLimitUpgradesInstantly: z13.boolean().optional(),
1963
+ applyLimitUpgradesInstantly: z18.boolean().optional(),
1529
1964
  subscriberChangePolicy: subscriberChangePolicySchema.default({
1530
1965
  default: "preserve_current_period",
1531
1966
  proration: "none",
@@ -1563,7 +1998,7 @@ var productSpecSchema = z13.object({
1563
1998
  allowImmediateEntitlementReduction: false
1564
1999
  }
1565
2000
  }),
1566
- plans: z13.array(planSpecSchema).max(4).default([]),
2001
+ plans: z18.array(planSpecSchema).max(4).default([]),
1567
2002
  /**
1568
2003
  * Add-on catalog (v0.56+). Composable economic + entitlement
1569
2004
  * overlays that subscribers can pile on top of their base plan.
@@ -1606,16 +2041,16 @@ var productSpecSchema = z13.object({
1606
2041
  * require_deprecation_window_days: 90
1607
2042
  * require_successor_route: true
1608
2043
  */
1609
- lifecycle: z13.object({
1610
- breaking_changes: z13.object({
2044
+ lifecycle: z18.object({
2045
+ breaking_changes: z18.object({
1611
2046
  /** Minimum days a route must have been marked for removal
1612
2047
  * (in main-branch YAML) before the publish gate will let
1613
2048
  * it actually be removed. Set to 0 to disable. */
1614
- require_deprecation_window_days: z13.number().int().nonnegative().default(0),
2049
+ require_deprecation_window_days: z18.number().int().nonnegative().default(0),
1615
2050
  /** When true, a route removal must declare a successor
1616
2051
  * route via the lifecycle metadata (mechanics in core
1617
2052
  * 3b-2) before the publish gate accepts it. */
1618
- require_successor_route: z13.boolean().default(false)
2053
+ require_successor_route: z18.boolean().default(false)
1619
2054
  }).default({
1620
2055
  require_deprecation_window_days: 0,
1621
2056
  require_successor_route: false
@@ -1661,8 +2096,8 @@ var productSpecSchema = z13.object({
1661
2096
  * (preserves today's behaviour). Compiler validation pins the
1662
2097
  * value to a real `plans[].key`.
1663
2098
  */
1664
- ephemeral: z13.object({
1665
- defaultPlan: z13.string().min(1).optional()
2099
+ ephemeral: z18.object({
2100
+ defaultPlan: z18.string().min(1).optional()
1666
2101
  }).optional()
1667
2102
  }).superRefine((spec, ctx) => {
1668
2103
  rejectUsagePricing(spec, ctx);
@@ -1672,816 +2107,1048 @@ var productSpecSchema = z13.object({
1672
2107
  validateFeatureReferences(spec, ctx);
1673
2108
  validateRouteMeters(spec, ctx);
1674
2109
  validateLimitMeterReachability(spec, ctx);
2110
+ validateBackendReferences(spec, ctx);
1675
2111
  });
1676
- var productPhaseSchema = z13.object({
2112
+ var productPhaseSchema = z18.object({
1677
2113
  product: productSpecSchema.shape.product
1678
2114
  });
1679
- var gatewayPhaseSchema = z13.object({
2115
+ var gatewayPhaseSchema = z18.object({
1680
2116
  gateway: productSpecSchema.shape.gateway
1681
2117
  });
1682
- var meteringPhaseSchema = z13.object({
2118
+ var meteringPhaseSchema = z18.object({
1683
2119
  metering: productSpecSchema.shape.metering
1684
2120
  });
1685
- var plansPhaseSchema = z13.object({
2121
+ var plansPhaseSchema = z18.object({
1686
2122
  plans: productSpecSchema.shape.plans
1687
2123
  });
1688
2124
 
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(),
2125
+ // ../contracts/dist/plans/spec/capabilities-layer.js
2126
+ import { z as z19 } from "zod";
2127
+ var capabilityLayerSchema = z19.object({
2128
+ capability: z19.string().min(1).max(120).regex(/^[a-z0-9_-]+$/, "capability name must be lowercase alphanumeric with hyphens/underscores"),
2129
+ description: z19.string().max(500).optional(),
1765
2130
  /**
1766
- * Lease TTL releases automatically after this many seconds even if
1767
- * the request never returns (defensive default 30s, mirrors existing
1768
- * ConcurrencyLease behaviour).
2131
+ * Capability composition is contractual by default including a new
2132
+ * feature changes the customer's effective entitlement. Mark
2133
+ * `runtime` only for capabilities that compose runtime-only knobs
2134
+ * (e.g. an internal "monitoring" capability that gates dashboard
2135
+ * pages without affecting billable behaviour).
1769
2136
  */
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
- })
2137
+ mutation_class: z19.enum(["runtime", "contractual"]).default("contractual"),
2138
+ includes_features: z19.array(z19.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/)).max(20).default([]),
2139
+ includes_policies: z19.array(z19.string().min(1).max(64).regex(/^[a-z0-9_-]+$/)).max(20).default([]),
2140
+ includes_capabilities: z19.array(z19.string().min(1).max(120).regex(/^[a-z0-9_-]+$/)).max(20).default([])
1786
2141
  });
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
- })
2142
+
2143
+ // ../contracts/dist/plans/spec/manifest-ir.js
2144
+ import { createHash } from "node:crypto";
2145
+ import { z as z20 } from "zod";
2146
+ var MANIFEST_IR_VERSION = 1;
2147
+ var manifestIrSchema = z20.object({
2148
+ irVersion: z20.literal(MANIFEST_IR_VERSION),
2149
+ /** Version of @farthershore/product that emitted this envelope. */
2150
+ sdkVersion: z20.string().min(1).max(64),
2151
+ /** Legacy unified ProductSpec the live `CompileProductOptions.sourceSpec`. */
2152
+ product: productSpecSchema,
2153
+ /** One entry per feature, sorted by `feature`. */
2154
+ routes: z20.array(routeLayerSchema).max(200).default([]),
2155
+ /** Sorted by `name`. */
2156
+ policies: z20.array(policyLayerSchema).max(200).default([]),
2157
+ /** Sorted by `capability`. */
2158
+ capabilities: z20.array(capabilityLayerSchema).max(200).default([])
2159
+ }).strict();
2160
+ function canonicalManifestJson(value) {
2161
+ return stableJson(JSON.parse(JSON.stringify(value)));
2162
+ }
2163
+ function hashManifestIr(ir) {
2164
+ return createHash("sha256").update(canonicalManifestJson(ir)).digest("hex");
2165
+ }
2166
+ function stableJson(value) {
2167
+ if (Array.isArray(value))
2168
+ return `[${value.map(stableJson).join(",")}]`;
2169
+ if (value && typeof value === "object") {
2170
+ return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`).join(",")}}`;
2171
+ }
2172
+ return JSON.stringify(value);
2173
+ }
2174
+
2175
+ // ../contracts/dist/plans/presets.js
2176
+ var FREE = {
2177
+ kind: "free",
2178
+ label: "Free",
2179
+ description: "No recurring fee and no metered usage. Pure freemium tier.",
2180
+ pricing: {
2181
+ meters: [],
2182
+ recurring_fee_cents: 0,
2183
+ billing_interval: "month",
2184
+ grants: [],
2185
+ trial_days: 0
2186
+ }
2187
+ };
2188
+ var STARTER = {
2189
+ kind: "starter",
2190
+ label: "Starter \u2014 $20/mo + $20 included",
2191
+ description: "$20/month subscription with $20 of usage credit each period. Stripe-style minimum-spend; overage billed at $0.001/request by default.",
2192
+ pricing: {
2193
+ meters: [
2194
+ { dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
2195
+ ],
2196
+ recurring_fee_cents: 2e3,
2197
+ billing_interval: "month",
2198
+ grants: [{ kind: "recurring", amount_cents: 2e3 }],
2199
+ trial_days: 0
2200
+ }
2201
+ };
2202
+ var PRO = {
2203
+ kind: "pro",
2204
+ label: "Pro \u2014 $100/mo + $200 included",
2205
+ description: "$100/month subscription with $200 of usage credit each period and a 14-day trial. Overage billed at $0.0005/request by default.",
2206
+ pricing: {
2207
+ meters: [
2208
+ { dimension: "requests", kind: "linear", price_per_unit_micros: 500 }
2209
+ ],
2210
+ recurring_fee_cents: 1e4,
2211
+ billing_interval: "month",
2212
+ grants: [{ kind: "recurring", amount_cents: 2e4 }],
2213
+ trial_days: 14
2214
+ }
2215
+ };
2216
+ var PREPAID = {
2217
+ kind: "prepaid",
2218
+ label: "Prepaid \u2014 $50 sign-up credit, then PAYG",
2219
+ description: "$50 one-time credit at signup. Once depleted the subscriber pays-as-they-go at $0.001/request by default.",
2220
+ pricing: {
2221
+ meters: [
2222
+ { dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
2223
+ ],
2224
+ recurring_fee_cents: 0,
2225
+ billing_interval: "month",
2226
+ grants: [{ kind: "one_time", amount_cents: 5e3 }],
2227
+ trial_days: 0
2228
+ }
2229
+ };
2230
+ var METERED = {
2231
+ kind: "metered",
2232
+ label: "Metered (pay-as-you-go)",
2233
+ description: "No subscription fee. Pure usage billing at $0.001/request by default.",
2234
+ pricing: {
2235
+ meters: [
2236
+ { dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
2237
+ ],
2238
+ recurring_fee_cents: 0,
2239
+ billing_interval: "month",
2240
+ grants: [],
2241
+ trial_days: 0
2242
+ }
2243
+ };
2244
+ var PLAN_PRESETS = Object.freeze({
2245
+ free: FREE,
2246
+ starter: STARTER,
2247
+ pro: PRO,
2248
+ prepaid: PREPAID,
2249
+ metered: METERED
1809
2250
  });
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"),
2251
+
2252
+ // ../contracts/dist/plans/subscription-pricing-override.js
2253
+ import { z as z21 } from "zod";
2254
+ var subscriptionPricingOverrideSchema = z21.object({
2255
+ /** Override the plan's recurring fee for this subscriber. */
2256
+ recurring_fee_cents: z21.number().int().nonnegative().optional(),
1817
2257
  /**
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`.
2258
+ * Override the plan's credit grants. `grants[]` is the single credit
2259
+ * surface when present, it fully replaces the plan's grants for this
2260
+ * subscriber (recurring + one-time credit are canonical entries here;
2261
+ * the legacy recurring/one-time scalar knobs were removed).
1821
2262
  */
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)
2263
+ grants: z21.array(grantSchema).max(40).optional(),
2264
+ /** Override the minimum-spend floor. */
2265
+ min_monthly_spend_cents: z21.number().int().nonnegative().optional(),
2266
+ /** Override the maximum-spend ceiling. */
2267
+ max_monthly_spend_cents: z21.number().int().nonnegative().optional(),
2268
+ /** Replace the entire meter list for this subscriber. When set, the
2269
+ * full plan meter array is replaced (not merged) — call it explicit
2270
+ * rather than implicit so a deal can also REMOVE billable meters,
2271
+ * not just adjust rates. */
2272
+ meters: z21.array(meterSchema).optional(),
2273
+ /** Free-text notes about the deal. Surfaced in admin UI for audit. */
2274
+ notes: z21.string().max(2e3).optional()
1826
2275
  });
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
2276
 
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);
2277
+ // src/validate.ts
2278
+ function validateManifestIr(candidate) {
2279
+ const parsed = manifestIrSchema.safeParse(candidate);
2280
+ if (!parsed.success) {
2281
+ return {
2282
+ ok: false,
2283
+ issues: parsed.error.issues.map((issue) => ({
2284
+ code: issue.code.toUpperCase(),
2285
+ path: issue.path.map(String).join("."),
2286
+ message: issue.message
2287
+ }))
2288
+ };
2289
+ }
2290
+ const ir = JSON.parse(
2291
+ JSON.stringify(candidate)
2292
+ );
2293
+ return { ok: true, ir, irHash: hashIr(ir) };
2294
+ }
2295
+ function hashIr(ir) {
2296
+ return hashManifestIr(ir);
2297
+ }
2298
+ function canonicalIrJson(ir) {
2299
+ return canonicalManifestJson(ir);
2300
+ }
1861
2301
 
1862
- // ../contracts/dist/plans/spec/routes-layer.js
1863
- import { z as z17 } from "zod";
2302
+ // src/version.ts
2303
+ var SDK_VERSION = true ? "0.6.0" : "0.0.0-dev";
1864
2304
 
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
- });
2305
+ // src/refs.ts
2306
+ function isCapabilityGrant(value) {
2307
+ return typeof value === "object" && value !== null && value.kind === "capability_grant";
2308
+ }
2309
+ function keyOf(ref) {
2310
+ return typeof ref === "string" ? ref : ref.key;
2311
+ }
2312
+ function displayFromKey(key) {
2313
+ return key.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2314
+ }
1888
2315
 
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
- });
2316
+ // src/backend.ts
2317
+ var BACKEND_ID_PATTERN2 = /^[a-z0-9][a-z0-9_-]*$/;
2318
+ function createBackendNode(id, options = {}) {
2319
+ if (!BACKEND_ID_PATTERN2.test(id)) {
2320
+ throw new ManifestBuilderError(
2321
+ `backend "${id}": id must be a lowercase slug ([a-z0-9][a-z0-9_-]*)`
2322
+ );
2323
+ }
2324
+ return {
2325
+ id,
2326
+ ...options.name !== void 0 ? { name: options.name } : {},
2327
+ ...options.slug !== void 0 ? { slug: options.slug } : {},
2328
+ ...options.transport !== void 0 ? {
2329
+ transport: {
2330
+ ...options.transport.mode !== void 0 ? { mode: options.transport.mode } : {},
2331
+ ...options.transport.runner !== void 0 ? { runner: options.transport.runner } : {}
2332
+ }
2333
+ } : {},
2334
+ ...options.verification !== void 0 ? { verification: options.verification } : {},
2335
+ ...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
2336
+ ...options.default !== void 0 ? { default: options.default } : {},
2337
+ ...options.originUrl !== void 0 ? { originUrl: options.originUrl } : {},
2338
+ ...options.originHostname !== void 0 ? { originHostname: options.originHostname } : {}
2339
+ };
2340
+ }
2341
+ function buildBackendBlock(backendNodes) {
2342
+ const out = {};
2343
+ for (const node of [...backendNodes].sort(
2344
+ (a, b) => a.id.localeCompare(b.id)
2345
+ )) {
2346
+ const { id, ...definition } = node;
2347
+ out[id] = definition;
2348
+ }
2349
+ return out;
2350
+ }
2351
+ function assertBackendBindingsValid(files, backendNodes, meterDefinitions) {
2352
+ if (backendNodes.length === 0) return;
2353
+ const byId = new Map(backendNodes.map((backend) => [backend.id, backend]));
2354
+ const declaredMeters = new Set(meterDefinitions.map((meter) => meter.key));
2355
+ for (const backend of backendNodes) {
2356
+ for (const meter of backend.meters ?? []) {
2357
+ if (declaredMeters.has(meter)) continue;
2358
+ throw new ManifestBuilderError(
2359
+ `UNKNOWN_METER_IN_BACKEND: backend "${backend.id}" allows meter "${meter}" but it is not declared \u2014 call product.meter("${meter}", ...) first`
2360
+ );
2361
+ }
2362
+ }
2363
+ const ids = backendNodes.map((backend) => backend.id);
2364
+ const explicitDefaults = backendNodes.filter(
2365
+ (backend) => backend.default === true
2366
+ );
2367
+ const defaultId = ids.length === 1 ? ids[0] : explicitDefaults.length === 1 ? explicitDefaults[0].id : null;
2368
+ const ambiguous = defaultId === null;
2369
+ for (const file of files) {
2370
+ const fileBinding = file.backend;
2371
+ if (fileBinding !== void 0 && !byId.has(fileBinding)) {
2372
+ throw new ManifestBuilderError(
2373
+ `UNKNOWN_BACKEND_IN_ROUTE: feature "${file.feature}" binds backend "${fileBinding}" which is not declared \u2014 call product.backend("${fileBinding}", ...) first`
2374
+ );
2375
+ }
2376
+ file.routes.forEach((route, routeIndex) => {
2377
+ const explicit = route.backend ?? fileBinding;
2378
+ if (explicit !== void 0) {
2379
+ if (!byId.has(explicit)) {
2380
+ throw new ManifestBuilderError(
2381
+ `UNKNOWN_BACKEND_IN_ROUTE: feature "${file.feature}" route ${routeIndex} binds backend "${explicit}" which is not declared \u2014 call product.backend("${explicit}", ...) first`
2382
+ );
2383
+ }
2384
+ } else if (ambiguous) {
2385
+ throw new ManifestBuilderError(
2386
+ `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`
2387
+ );
2388
+ }
2389
+ const boundId = explicit ?? defaultId;
2390
+ if (!boundId) return;
2391
+ const backend = byId.get(boundId);
2392
+ const allow = backend?.meters;
2393
+ if (!allow) return;
2394
+ const allowed = new Set(allow);
2395
+ const routeMeters = /* @__PURE__ */ new Set([
2396
+ ...Object.keys(route.metering?.defaults ?? {}),
2397
+ ...route.metering?.reports ?? [],
2398
+ ...Object.keys(route.metering?.estimates ?? {})
2399
+ ]);
2400
+ for (const meter of routeMeters) {
2401
+ if (allowed.has(meter)) continue;
2402
+ throw new ManifestBuilderError(
2403
+ `ROUTE_METER_NOT_ALLOWED_BY_BACKEND: feature "${file.feature}" route ${routeIndex} meters "${meter}" but backend "${boundId}" does not allow it (backend.meters = [${allow.join(", ")}])`
2404
+ );
2405
+ }
2406
+ });
2407
+ }
2408
+ }
1969
2409
 
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
- });
2410
+ // src/product-assembly.ts
2411
+ function assembleProductSpec(input) {
2412
+ const { options } = input;
2413
+ const base = {
2414
+ product: {
2415
+ name: input.name,
2416
+ baseUrl: options.origin,
2417
+ ...options.displayName !== void 0 ? { displayName: options.displayName } : {},
2418
+ ...options.description !== void 0 ? { description: options.description } : {},
2419
+ ...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
2420
+ ...options.visibility !== void 0 ? { visibility: options.visibility } : {},
2421
+ ...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
2422
+ ...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
2423
+ ...options.envBranchPrefix !== void 0 ? { envBranchPrefix: options.envBranchPrefix } : {}
2424
+ },
2425
+ gateway: {
2426
+ ...options.authHeader !== void 0 ? { authHeader: options.authHeader } : {},
2427
+ ...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
2428
+ },
2429
+ metering: {
2430
+ meters: buildMeterDefinitions(
2431
+ input.meters,
2432
+ input.defaultMeterCosts,
2433
+ input.featureLayers,
2434
+ input.workflows
2435
+ ),
2436
+ ...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
2437
+ },
2438
+ ...input.backends.length ? { backend: buildBackendBlock(input.backends) } : {},
2439
+ ...options.billing !== void 0 ? { billing: options.billing } : {},
2440
+ ...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
2441
+ ...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
2442
+ ...input.surfaces.length ? { surfaces: sortBy2(input.surfaces, (surface) => surface.key) } : {},
2443
+ ...input.entitlements.length ? {
2444
+ entitlements: sortBy2(
2445
+ input.entitlements,
2446
+ (entitlement) => entitlement.key
2447
+ )
2448
+ } : {},
2449
+ ...input.workflows.length ? { workflows: sortBy2(input.workflows, (workflow) => workflow.key) } : {},
2450
+ ...input.frontendManifest !== void 0 ? { frontend: input.frontendManifest } : {},
2451
+ ...input.migrations.length ? { migrations: sortBy2(input.migrations, (migration) => migration.id) } : {},
2452
+ ...input.resources.length ? { resources: sortBy2(input.resources, (resource) => resource.name) } : {},
2453
+ plans: sortBy2(input.plans, (plan) => plan.key)
2454
+ };
2455
+ return mergeProductPatch(
2456
+ base,
2457
+ input.productPatch
2458
+ );
2459
+ }
2460
+ function buildMeterDefinitions(meters, defaultMeterCosts, featureLayers, workflows) {
2461
+ const routeValueMeters = routeValueMeterKeys(
2462
+ defaultMeterCosts,
2463
+ featureLayers,
2464
+ workflows
2465
+ );
2466
+ return sortBy2(meters, (meter) => meter.key).map((meter) => {
2467
+ if (meter.aggregation !== void 0) return meter;
2468
+ if (!routeValueMeters.has(meter.key)) return meter;
2469
+ return { ...meter, aggregation: "SUM" };
2470
+ });
2471
+ }
2472
+ function routeValueMeterKeys(defaultMeterCosts, featureLayers, workflows) {
2473
+ const keys = /* @__PURE__ */ new Set();
2474
+ for (const cost of defaultMeterCosts) {
2475
+ keys.add(cost.meter);
2476
+ }
2477
+ for (const file of featureLayers) {
2478
+ for (const route of file.routes) {
2479
+ for (const meter of Object.keys(route.metering?.defaults ?? {})) {
2480
+ keys.add(meter);
2481
+ }
2482
+ for (const meter of route.metering?.reports ?? []) {
2483
+ keys.add(meter);
2484
+ }
2485
+ }
2486
+ }
2487
+ for (const workflow of workflows) {
2488
+ for (const meter of workflow.meters ?? []) keys.add(meter);
2489
+ for (const meter of Object.keys(workflow.estimates ?? {})) keys.add(meter);
2490
+ }
2491
+ return keys;
2492
+ }
2493
+ function buildCustomerContext(options) {
2494
+ return {
2495
+ ...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
2496
+ ...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
2497
+ ...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
2498
+ };
2499
+ }
2500
+ function sortBy2(items, key) {
2501
+ return [...items].sort((a, b) => key(a).localeCompare(key(b)));
2502
+ }
2503
+ function isPlainObject(value) {
2504
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2505
+ }
2506
+ function mergeProductPatch(base, patch) {
2507
+ const out = { ...base };
2508
+ for (const [key, value] of Object.entries(patch)) {
2509
+ const existing = out[key];
2510
+ if (isPlainObject(existing) && isPlainObject(value)) {
2511
+ out[key] = mergeProductPatch(existing, value);
2512
+ } else {
2513
+ out[key] = value;
2514
+ }
2515
+ }
2516
+ return out;
2517
+ }
2084
2518
 
2085
- // ../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(),
2090
- /**
2091
- * Capability composition is contractual by default — including a new
2092
- * feature changes the customer's effective entitlement. Mark
2093
- * `runtime` only for capabilities that compose runtime-only knobs
2094
- * (e.g. an internal "monitoring" capability that gates dashboard
2095
- * pages without affecting billable behaviour).
2096
- */
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([])
2101
- });
2519
+ // src/frontend.ts
2520
+ function createFrontendManifest() {
2521
+ return { version: 1, nav: [], pages: [] };
2522
+ }
2523
+ function setFrontendNav(manifest, items) {
2524
+ manifest.nav = items.map((item) => ({
2525
+ label: item.label,
2526
+ path: item.path,
2527
+ ...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
2528
+ }));
2529
+ }
2530
+ function addFrontendPage(manifest, path, options) {
2531
+ if (manifest.pages?.some((page) => page.path === path)) {
2532
+ throw new ManifestBuilderError(
2533
+ `duplicate frontend page "${path}" \u2014 each frontend page path must be declared once`
2534
+ );
2535
+ }
2536
+ manifest.pages ??= [];
2537
+ manifest.pages.push({
2538
+ path,
2539
+ title: options.title,
2540
+ requiresAuth: options.requiresAuth,
2541
+ ...options.capability !== void 0 ? { capability: keyOf(options.capability) } : {},
2542
+ ...options.components?.length ? {
2543
+ components: options.components.map((component) => ({
2544
+ component: component.component,
2545
+ ...component.props !== void 0 ? { props: component.props } : {},
2546
+ ...component.capability !== void 0 ? { capability: keyOf(component.capability) } : {},
2547
+ ...component.gateMode !== void 0 ? { gateMode: component.gateMode } : {}
2548
+ }))
2549
+ } : {}
2550
+ });
2551
+ }
2552
+ function frontendCapabilityKeys(manifest) {
2553
+ return [
2554
+ ...(manifest.nav ?? []).flatMap(
2555
+ (item) => item.capability ? [item.capability] : []
2556
+ ),
2557
+ ...(manifest.pages ?? []).flatMap((page) => [
2558
+ ...page.capability ? [page.capability] : [],
2559
+ ...(page.components ?? []).flatMap(
2560
+ (component) => component.capability ? [component.capability] : []
2561
+ )
2562
+ ])
2563
+ ];
2564
+ }
2102
2565
 
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
2566
+ // src/route-metering.ts
2567
+ function parseRouteMatch(match) {
2568
+ const trimmed = match.trim();
2569
+ const space = trimmed.indexOf(" ");
2570
+ if (space === -1) {
2571
+ if (!trimmed.startsWith("/")) {
2572
+ throw new ManifestBuilderError(
2573
+ `route "${match}": expected "METHOD /path" or "/path"`
2574
+ );
2575
+ }
2576
+ return { method: "*", path: trimmed };
2577
+ }
2578
+ const method = trimmed.slice(0, space).toUpperCase();
2579
+ const path = trimmed.slice(space + 1).trim();
2580
+ const methods = [
2581
+ "GET",
2582
+ "POST",
2583
+ "PUT",
2584
+ "PATCH",
2585
+ "DELETE",
2586
+ "HEAD",
2587
+ "OPTIONS",
2588
+ "*"
2589
+ ];
2590
+ if (!methods.includes(method)) {
2591
+ throw new ManifestBuilderError(
2592
+ `route "${match}": unknown HTTP method "${method}"`
2593
+ );
2594
+ }
2595
+ if (!path.startsWith("/")) {
2596
+ throw new ManifestBuilderError(
2597
+ `route "${match}": path must start with "/"`
2598
+ );
2599
+ }
2600
+ return { method, path };
2601
+ }
2602
+ function makeMeterRef(key) {
2603
+ return {
2604
+ kind: "meter",
2605
+ key,
2606
+ fixed: (value) => ({ kind: "meter_cost", meter: key, value }),
2607
+ estimate: (value) => ({ kind: "meter_estimate", meter: key, value })
2608
+ };
2609
+ }
2610
+ function normalizeMeterCost(cost, hasMeter) {
2611
+ if (!cost || cost.kind !== "meter_cost") {
2612
+ throw new ManifestBuilderError(
2613
+ "meter cost must be created by meter.fixed(value)"
2614
+ );
2615
+ }
2616
+ if (!hasMeter(cost.meter)) {
2617
+ throw new ManifestBuilderError(
2618
+ `meter cost references unknown meter "${cost.meter}"`
2619
+ );
2620
+ }
2621
+ if (!Number.isFinite(cost.value) || cost.value < 0) {
2622
+ throw new ManifestBuilderError(
2623
+ `meter "${cost.meter}" fixed value must be a non-negative finite number`
2624
+ );
2625
+ }
2626
+ return cost;
2627
+ }
2628
+ function normalizeMeterCosts(costs, normalize) {
2629
+ if (!costs) return [];
2630
+ const entries = Array.isArray(costs) ? costs : [costs];
2631
+ return entries.map((cost) => normalize(cost));
2632
+ }
2633
+ function normalizeMeterRefs(refs) {
2634
+ const entries = Array.isArray(refs) ? refs : [refs];
2635
+ return entries.map(keyOf);
2636
+ }
2637
+ function statusPolicyPartIsValid2(part) {
2638
+ const trimmed = part.trim();
2639
+ if (!trimmed) return false;
2640
+ const [startRaw, endRaw, extra] = trimmed.split("-");
2641
+ if (extra !== void 0 || !/^\d{3}$/.test(startRaw ?? "")) return false;
2642
+ const start = Number(startRaw);
2643
+ const end = endRaw === void 0 ? start : Number(endRaw);
2644
+ if (endRaw !== void 0 && !/^\d{3}$/.test(endRaw)) return false;
2645
+ return start >= 100 && start <= 599 && end >= 100 && end <= 599 && start <= end;
2646
+ }
2647
+ function assertStatusCodePolicy(value) {
2648
+ if (Array.isArray(value)) {
2649
+ if (value.length === 0) {
2650
+ throw new ManifestBuilderError("onStatusCodes cannot be an empty array");
2246
2651
  }
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)));
2652
+ for (const status of value) {
2653
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
2654
+ throw new ManifestBuilderError(
2655
+ `onStatusCodes array contains invalid HTTP status "${status}"`
2656
+ );
2657
+ }
2658
+ }
2659
+ return;
2660
+ }
2661
+ if (!value.split(",").every(statusPolicyPartIsValid2)) {
2662
+ throw new ManifestBuilderError(
2663
+ 'onStatusCodes must be comma-separated HTTP status codes or numeric ranges, e.g. "200-299,304"'
2664
+ );
2665
+ }
2280
2666
  }
2281
- function hashManifestIr(ir) {
2282
- return createHash("sha256").update(canonicalManifestJson(ir)).digest("hex");
2667
+ function assertEstimateValue(meter, value) {
2668
+ if (!Number.isFinite(value) || value < 0) {
2669
+ throw new ManifestBuilderError(
2670
+ `meter "${meter}" estimate must be a non-negative finite number`
2671
+ );
2672
+ }
2283
2673
  }
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(",")}}`;
2674
+ function normalizeMeterEstimate(estimate, hasMeter) {
2675
+ if (!estimate || estimate.kind !== "meter_estimate") {
2676
+ throw new ManifestBuilderError(
2677
+ "meter estimate must be created by meter.estimate(value)"
2678
+ );
2289
2679
  }
2290
- return JSON.stringify(value);
2680
+ if (!hasMeter(estimate.meter)) {
2681
+ throw new ManifestBuilderError(
2682
+ `meter estimate references unknown meter "${estimate.meter}"`
2683
+ );
2684
+ }
2685
+ assertEstimateValue(estimate.meter, estimate.value);
2686
+ return estimate;
2291
2687
  }
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
2688
+ function isMeterEstimate(value) {
2689
+ return typeof value === "object" && value !== null && value.kind === "meter_estimate";
2690
+ }
2691
+ function normalizeMeterEstimates(estimates, hasMeter) {
2692
+ if (!estimates) return {};
2693
+ if (Array.isArray(estimates)) {
2694
+ return Object.fromEntries(
2695
+ estimates.map((estimate) => {
2696
+ const normalized = normalizeMeterEstimate(estimate, hasMeter);
2697
+ return [normalized.meter, normalized.value];
2698
+ })
2699
+ );
2304
2700
  }
2305
- };
2306
- var STARTER = {
2307
- kind: "starter",
2308
- label: "Starter \u2014 $20/mo + $20 included",
2309
- description: "$20/month subscription with $20 of usage credit each period. Stripe-style minimum-spend; overage billed at $0.001/request by default.",
2310
- pricing: {
2311
- meters: [
2312
- { dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
2313
- ],
2314
- recurring_fee_cents: 2e3,
2315
- billing_interval: "month",
2316
- grants: [{ kind: "recurring", amount_cents: 2e3 }],
2317
- trial_days: 0
2701
+ if (isMeterEstimate(estimates)) {
2702
+ const normalized = normalizeMeterEstimate(estimates, hasMeter);
2703
+ return { [normalized.meter]: normalized.value };
2318
2704
  }
2319
- };
2320
- var PRO = {
2321
- kind: "pro",
2322
- label: "Pro \u2014 $100/mo + $200 included",
2323
- description: "$100/month subscription with $200 of usage credit each period and a 14-day trial. Overage billed at $0.0005/request by default.",
2324
- pricing: {
2325
- meters: [
2326
- { dimension: "requests", kind: "linear", price_per_unit_micros: 500 }
2327
- ],
2328
- recurring_fee_cents: 1e4,
2329
- billing_interval: "month",
2330
- grants: [{ kind: "recurring", amount_cents: 2e4 }],
2331
- trial_days: 14
2705
+ const estimateMap = estimates;
2706
+ for (const [meter, value] of Object.entries(estimateMap)) {
2707
+ assertEstimateValue(meter, value);
2332
2708
  }
2333
- };
2334
- var PREPAID = {
2335
- kind: "prepaid",
2336
- label: "Prepaid \u2014 $50 sign-up credit, then PAYG",
2337
- description: "$50 one-time credit at signup. Once depleted the subscriber pays-as-they-go at $0.001/request by default.",
2338
- pricing: {
2339
- meters: [
2340
- { dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
2341
- ],
2342
- recurring_fee_cents: 0,
2343
- billing_interval: "month",
2344
- grants: [{ kind: "one_time", amount_cents: 5e3 }],
2345
- trial_days: 0
2709
+ return { ...estimateMap };
2710
+ }
2711
+ function buildRouteDefinition(match, options, deps) {
2712
+ const parsed = parseRouteMatch(match);
2713
+ const metering = buildRouteMetering(options, deps);
2714
+ return {
2715
+ match: parsed,
2716
+ ...metering ? { metering } : {},
2717
+ ...options.unmetered !== void 0 ? { unmetered: options.unmetered } : {},
2718
+ ...options.inheritDefaultMeters !== void 0 ? { inheritDefaultMeters: options.inheritDefaultMeters } : {},
2719
+ ...options.action !== void 0 ? { action: keyOf(options.action) } : {},
2720
+ ...options.backend !== void 0 ? { backend: keyOf(options.backend) } : {}
2721
+ };
2722
+ }
2723
+ function buildRouteMetering(options, deps) {
2724
+ if (options.unmetered === true) return void 0;
2725
+ const defaults = {};
2726
+ for (const cost of normalizeMeterCosts(
2727
+ options.costs,
2728
+ (cost2) => deps.normalizeMeterCost(cost2)
2729
+ )) {
2730
+ defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
2731
+ }
2732
+ const reports = [...new Set(normalizeMeterRefs(options.reports ?? []))];
2733
+ const estimates = {};
2734
+ for (const meter of reports) {
2735
+ const definition = deps.getMeterDefinition(meter);
2736
+ if (typeof definition?.estimate === "number") {
2737
+ assertEstimateValue(meter, definition.estimate);
2738
+ estimates[meter] = definition.estimate;
2739
+ }
2346
2740
  }
2347
- };
2348
- var METERED = {
2349
- kind: "metered",
2350
- label: "Metered (pay-as-you-go)",
2351
- description: "No subscription fee. Pure usage billing at $0.001/request by default.",
2352
- pricing: {
2353
- meters: [
2354
- { dimension: "requests", kind: "linear", price_per_unit_micros: 1e3 }
2355
- ],
2356
- recurring_fee_cents: 0,
2357
- billing_interval: "month",
2358
- grants: [],
2359
- trial_days: 0
2741
+ const explicitEstimates = normalizeMeterEstimates(
2742
+ options.estimates,
2743
+ (meter) => deps.getMeterDefinition(meter) !== void 0
2744
+ );
2745
+ for (const [meter, value] of Object.entries(explicitEstimates)) {
2746
+ estimates[meter] = value;
2747
+ }
2748
+ const out = {};
2749
+ if (Object.keys(defaults).length) out.defaults = defaults;
2750
+ if (reports.length) out.reports = reports;
2751
+ if (Object.keys(estimates).length) out.estimates = estimates;
2752
+ if (options.onStatusCodes !== void 0) {
2753
+ assertStatusCodePolicy(options.onStatusCodes);
2754
+ out.onStatusCodes = options.onStatusCodes;
2755
+ }
2756
+ return Object.keys(out).length ? out : void 0;
2757
+ }
2758
+ function materializeRoute(route, defaultMeterCosts) {
2759
+ if (route.unmetered === true) return route;
2760
+ const defaults = {
2761
+ ...route.metering?.defaults ?? {}
2762
+ };
2763
+ if (route.inheritDefaultMeters !== false) {
2764
+ for (const cost of defaultMeterCosts) {
2765
+ defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
2766
+ }
2360
2767
  }
2361
- };
2362
- var PLAN_PRESETS = Object.freeze({
2363
- free: FREE,
2364
- starter: STARTER,
2365
- pro: PRO,
2366
- prepaid: PREPAID,
2367
- metered: METERED
2368
- });
2768
+ const hasMetering = route.metering !== void 0 || Object.keys(defaults).length > 0;
2769
+ const metering = hasMetering ? {
2770
+ ...route.metering ?? {},
2771
+ ...Object.keys(defaults).length ? { defaults } : {}
2772
+ } : void 0;
2773
+ return {
2774
+ ...route,
2775
+ ...metering ? { metering } : {}
2776
+ };
2777
+ }
2778
+ function routeMeterDependencyKeys(route) {
2779
+ const keys = /* @__PURE__ */ new Set();
2780
+ for (const meter of Object.keys(route.metering?.defaults ?? {})) {
2781
+ keys.add(meter);
2782
+ }
2783
+ for (const meter of route.metering?.reports ?? []) keys.add(meter);
2784
+ for (const meter of Object.keys(route.metering?.estimates ?? {})) {
2785
+ keys.add(meter);
2786
+ }
2787
+ return [...keys];
2788
+ }
2789
+ function assertRouteMeteringValid(files, meterDefinitions) {
2790
+ const definitions = new Map(
2791
+ Array.from(meterDefinitions, (meter) => [meter.key, meter])
2792
+ );
2793
+ for (const file of files) {
2794
+ file.routes.forEach((route, routeIndex) => {
2795
+ if (route.unmetered === true) return;
2796
+ const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
2797
+ const reports = new Set(route.metering?.reports ?? []);
2798
+ for (const meter of defaults) {
2799
+ if (definitions.has(meter)) continue;
2800
+ throw new ManifestBuilderError(
2801
+ `feature "${file.feature}" route ${routeIndex}: fixed meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
2802
+ );
2803
+ }
2804
+ for (const meter of route.metering?.reports ?? []) {
2805
+ if (!definitions.has(meter)) {
2806
+ throw new ManifestBuilderError(
2807
+ `feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
2808
+ );
2809
+ }
2810
+ if (defaults.has(meter)) {
2811
+ throw new ManifestBuilderError(
2812
+ `feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
2813
+ );
2814
+ }
2815
+ }
2816
+ for (const meter of route.metering?.reports ?? []) {
2817
+ const definition = definitions.get(meter);
2818
+ const enforcement = definition?.enforcementType ?? "estimated_then_settled";
2819
+ const hasEstimate = route.metering?.estimates && Object.prototype.hasOwnProperty.call(route.metering.estimates, meter);
2820
+ if ((enforcement === "exact_pre_request" || enforcement === "estimated_then_settled") && !hasEstimate) {
2821
+ throw new ManifestBuilderError(
2822
+ `feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" needs an estimate for gateway admission`
2823
+ );
2824
+ }
2825
+ }
2826
+ for (const [meter, value] of Object.entries(
2827
+ route.metering?.estimates ?? {}
2828
+ )) {
2829
+ assertEstimateValue(meter, value);
2830
+ if (!definitions.has(meter)) {
2831
+ throw new ManifestBuilderError(
2832
+ `feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
2833
+ );
2834
+ }
2835
+ if (reports.has(meter)) continue;
2836
+ throw new ManifestBuilderError(
2837
+ `feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" must also be listed in reports because estimates are pre-request reservations for dynamic usage`
2838
+ );
2839
+ }
2840
+ });
2841
+ }
2842
+ }
2369
2843
 
2370
- // ../contracts/dist/plans/subscription-pricing-override.js
2371
- import { z as z22 } from "zod";
2372
- var subscriptionPricingOverrideSchema = z22.object({
2373
- /** Override the plan's recurring fee for this subscriber. */
2374
- recurring_fee_cents: z22.number().int().nonnegative().optional(),
2375
- /**
2376
- * Override the plan's credit grants. `grants[]` is the single credit
2377
- * surface — when present, it fully replaces the plan's grants for this
2378
- * subscriber (recurring + one-time credit are canonical entries here;
2379
- * the legacy recurring/one-time scalar knobs were removed).
2380
- */
2381
- grants: z22.array(grantSchema).max(40).optional(),
2382
- /** Override the minimum-spend floor. */
2383
- min_monthly_spend_cents: z22.number().int().nonnegative().optional(),
2384
- /** Override the maximum-spend ceiling. */
2385
- max_monthly_spend_cents: z22.number().int().nonnegative().optional(),
2386
- /** Replace the entire meter list for this subscriber. When set, the
2387
- * full plan meter array is replaced (not merged) — call it explicit
2388
- * rather than implicit so a deal can also REMOVE billable meters,
2389
- * not just adjust rates. */
2390
- meters: z22.array(meterSchema).optional(),
2391
- /** Free-text notes about the deal. Surfaced in admin UI for audit. */
2392
- notes: z22.string().max(2e3).optional()
2393
- });
2844
+ // src/dependencies.ts
2845
+ function capabilityDependsOn(file) {
2846
+ return [
2847
+ ...dependenciesFor("feature", file.includes_features),
2848
+ ...dependenciesFor("policy", file.includes_policies),
2849
+ ...dependenciesFor("capability", file.includes_capabilities)
2850
+ ];
2851
+ }
2852
+ function policyDependsOn(file) {
2853
+ return dependenciesFor("meter", file.compatible_with?.meters);
2854
+ }
2855
+ function entitlementDependsOn(entitlement, hasResource) {
2856
+ const limitDimensions = (entitlement.limits ?? []).map(
2857
+ (limit) => limit.dimension
2858
+ );
2859
+ return [
2860
+ ...dependenciesFor("capability", entitlement.capabilities),
2861
+ ...existingDependenciesFor(
2862
+ "meter",
2863
+ [...entitlement.meters ?? [], ...limitDimensions],
2864
+ hasResource
2865
+ )
2866
+ ];
2867
+ }
2868
+ function workflowDependsOn(workflow) {
2869
+ return [
2870
+ ...dependenciesFor("capability", workflow.capabilities),
2871
+ ...dependenciesFor("meter", [
2872
+ ...workflow.meters ?? [],
2873
+ ...Object.keys(workflow.estimates ?? {})
2874
+ ])
2875
+ ];
2876
+ }
2877
+ function featureDependsOn(file) {
2878
+ const meterKeys = file.routes.flatMap(
2879
+ (route) => routeMeterDependencyKeys(route)
2880
+ );
2881
+ const backendKeys = [
2882
+ ...file.backend !== void 0 ? [file.backend] : [],
2883
+ ...file.routes.flatMap(
2884
+ (route) => route.backend !== void 0 ? [route.backend] : []
2885
+ )
2886
+ ];
2887
+ return [
2888
+ ...dependenciesFor("policy", file.policies),
2889
+ ...dependenciesFor("plan", file.plans),
2890
+ ...dependenciesFor("meter", meterKeys),
2891
+ ...dependenciesFor("backend", backendKeys)
2892
+ ];
2893
+ }
2894
+ function actionDependsOn(featureKey, action) {
2895
+ return [
2896
+ resourceDependency("feature", featureKey),
2897
+ ...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
2898
+ ];
2899
+ }
2900
+ function planDependsOn(plan, hasResource) {
2901
+ const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
2902
+ const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
2903
+ const pricedMeterDimensions = (plan.meters ?? []).map(
2904
+ (meter) => meter.dimension
2905
+ );
2906
+ const capacityKeys = Object.keys(plan.capability_limits ?? {});
2907
+ return [
2908
+ ...dependenciesFor("capability", caps),
2909
+ ...existingDependenciesFor(
2910
+ "meter",
2911
+ [...limitDimensions, ...pricedMeterDimensions],
2912
+ hasResource
2913
+ ),
2914
+ ...existingDependenciesFor("counted_resource", capacityKeys, hasResource)
2915
+ ];
2916
+ }
2917
+ function migrationDependsOn(migration) {
2918
+ return [
2919
+ resourceDependency("plan", migration.from.plan),
2920
+ resourceDependency("plan", migration.to.plan),
2921
+ ...dependenciesFor(
2922
+ "plan",
2923
+ migration.pins?.map((pin) => pin.pinTo.plan)
2924
+ )
2925
+ ];
2926
+ }
2927
+ function frontendDependsOn(manifest) {
2928
+ return dependenciesFor("capability", frontendCapabilityKeys(manifest));
2929
+ }
2930
+ function assertResourceDependenciesSatisfied(missing) {
2931
+ if (missing.length === 0) return;
2932
+ const details = missing.slice(0, 8).map(
2933
+ ({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
2934
+ dependency
2935
+ )}`
2936
+ ).join("; ");
2937
+ const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
2938
+ throw new ManifestBuilderError(
2939
+ `manifest has unresolved resource reference(s): ${details}${suffix}`
2940
+ );
2941
+ }
2942
+ function dependenciesFor(kind, keys) {
2943
+ return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
2944
+ }
2945
+ function existingDependenciesFor(kind, keys, hasResource) {
2946
+ return [...new Set(keys)].filter((key) => hasResource(kind, key)).map((key) => resourceDependency(kind, key));
2947
+ }
2948
+ function describeResourceUrn(urn) {
2949
+ const parts = urn.split(":");
2950
+ const kind = parts[3] ?? "resource";
2951
+ const key = parts.slice(4).join(":");
2952
+ return `${kind} "${decodeURIComponent(key)}"`;
2953
+ }
2394
2954
 
2395
- // src/validate.ts
2396
- function validateManifestIr(candidate) {
2397
- const parsed = manifestIrSchema.safeParse(candidate);
2398
- if (!parsed.success) {
2399
- return {
2400
- ok: false,
2401
- issues: parsed.error.issues.map((issue) => ({
2402
- code: issue.code.toUpperCase(),
2403
- path: issue.path.map(String).join("."),
2404
- message: issue.message
2955
+ // src/declarations.ts
2956
+ function buildCapabilityLayer(key, options = {}) {
2957
+ return {
2958
+ capability: key,
2959
+ ...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
2960
+ ...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
2961
+ ...options.includesFeatures?.length ? { includes_features: options.includesFeatures.map(keyOf) } : {},
2962
+ ...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
2963
+ ...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
2964
+ };
2965
+ }
2966
+ function buildFeatureLayer(key, options, buildRoute) {
2967
+ const layer = {
2968
+ feature: key,
2969
+ routes: [],
2970
+ ...options.description !== void 0 ? { description: options.description } : {},
2971
+ ...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
2972
+ ...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
2973
+ ...options.upstreamOrigin !== void 0 ? { upstream: { override_origin: options.upstreamOrigin } } : {},
2974
+ ...options.policies?.length ? { policies: options.policies.map(keyOf) } : {},
2975
+ ...options.plans?.length ? { plans: options.plans.map(keyOf) } : {},
2976
+ ...options.actions?.length ? {
2977
+ actions: options.actions.map(({ id, ...action }) => ({
2978
+ id,
2979
+ ...action
2405
2980
  }))
2406
- };
2981
+ } : {},
2982
+ ...options.rolloutKey !== void 0 || options.requiredFlags?.length ? {
2983
+ runtime: {
2984
+ ...options.rolloutKey !== void 0 ? { rollout_key: options.rolloutKey } : {},
2985
+ ...options.requiredFlags?.length ? { required_flags: options.requiredFlags } : {}
2986
+ }
2987
+ } : {}
2988
+ };
2989
+ for (const route of options.routes ?? []) {
2990
+ layer.routes.push(buildRoute(route.match, route));
2407
2991
  }
2408
- const ir = JSON.parse(
2409
- JSON.stringify(candidate)
2410
- );
2411
- return { ok: true, ir, irHash: hashIr(ir) };
2992
+ return layer;
2993
+ }
2994
+ function buildPolicyLayer(name, options) {
2995
+ return {
2996
+ name,
2997
+ type: options.type,
2998
+ config: options.config,
2999
+ ...options.description !== void 0 ? { description: options.description } : {},
3000
+ ...options.mutationClass !== void 0 ? { mutation_class: options.mutationClass } : {},
3001
+ ...options.cacheProfile !== void 0 ? { cacheProfile: options.cacheProfile } : {},
3002
+ ...options.compatibleWith ? {
3003
+ compatible_with: {
3004
+ ...options.compatibleWith.routeTypes ? { route_types: options.compatibleWith.routeTypes } : {},
3005
+ ...options.compatibleWith.meters ? { meters: options.compatibleWith.meters.map(keyOf) } : {},
3006
+ ...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
3007
+ }
3008
+ } : {}
3009
+ };
3010
+ }
3011
+ function buildSurfaceSpec(type, options = {}) {
3012
+ const key = options.key ?? type;
3013
+ return {
3014
+ key,
3015
+ type,
3016
+ ...options.display !== void 0 ? { display: options.display } : {},
3017
+ ...options.description !== void 0 ? { description: options.description } : {}
3018
+ };
2412
3019
  }
2413
- function hashIr(ir) {
2414
- return hashManifestIr(ir);
3020
+ function buildWorkflowSpec(key, options = {}) {
3021
+ return {
3022
+ key,
3023
+ kind: options.kind ?? "async_job",
3024
+ trigger: options.trigger ?? { type: "manual" },
3025
+ ...options.title !== void 0 ? { title: options.title } : {},
3026
+ ...options.description !== void 0 ? { description: options.description } : {},
3027
+ ...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
3028
+ ...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
3029
+ ...options.estimates !== void 0 ? { estimates: options.estimates } : {},
3030
+ ...options.metadata !== void 0 ? { metadata: options.metadata } : {}
3031
+ };
2415
3032
  }
2416
- function canonicalIrJson(ir) {
2417
- return canonicalManifestJson(ir);
3033
+ function buildEntitlementSpec(key, options = {}) {
3034
+ return {
3035
+ key,
3036
+ ...options.description !== void 0 ? { description: options.description } : {},
3037
+ ...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
3038
+ ...options.featureGates !== void 0 ? { featureGates: options.featureGates } : {},
3039
+ ...options.limits?.length ? { limits: options.limits } : {},
3040
+ ...options.meters?.length ? { meters: options.meters.map(keyOf) } : {}
3041
+ };
2418
3042
  }
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";
3043
+ function buildPlanSpec(key, options) {
3044
+ const { capabilityKeys, capabilityLimits, creditGrants } = normalizePlanGrants(options);
3045
+ const rawCaps = Array.isArray(options.raw?.capabilities) ? options.raw.capabilities.map(String) : [];
3046
+ const capabilities = [.../* @__PURE__ */ new Set([...capabilityKeys, ...rawCaps])];
3047
+ const spec = {
3048
+ key,
3049
+ name: options.name,
3050
+ ...options.description !== void 0 ? { description: options.description } : {},
3051
+ ...options.details ? { details: options.details } : {},
3052
+ ...options.price ? {
3053
+ recurring_fee_cents: options.price.recurring_fee_cents,
3054
+ billing_interval: options.price.billing_interval,
3055
+ ...options.price.free ? { free: true } : {}
3056
+ } : {},
3057
+ ...options.meters ? { meters: options.meters } : {},
3058
+ ...creditGrants.length ? { grants: creditGrants } : {},
3059
+ ...options.trialDays !== void 0 ? { trial_days: options.trialDays } : {},
3060
+ ...options.maxMonthlySpendCents !== void 0 ? { max_monthly_spend_cents: options.maxMonthlySpendCents } : {},
3061
+ ...options.minMonthlySpendCents !== void 0 ? { min_monthly_spend_cents: options.minMonthlySpendCents } : {},
3062
+ ...options.limits ? { limits: options.limits } : {},
3063
+ ...options.featureGates ? { featureGates: options.featureGates } : {},
3064
+ ...Object.keys(capabilityLimits).length ? { capability_limits: capabilityLimits } : {},
3065
+ ...options.overageBehavior !== void 0 ? { overageBehavior: options.overageBehavior } : {},
3066
+ ...options.selfServeEnabled !== void 0 ? { selfServeEnabled: options.selfServeEnabled } : {},
3067
+ ...options.legacy !== void 0 ? { legacy: options.legacy } : {},
3068
+ ...options.archive ? { archive: options.archive } : {},
3069
+ ...options.raw ?? {},
3070
+ ...capabilities.length ? { capabilities } : {}
3071
+ };
3072
+ return spec;
2430
3073
  }
2431
- function keyOf(ref) {
2432
- return typeof ref === "string" ? ref : ref.key;
3074
+ function normalizePlanGrants(options) {
3075
+ const capabilityKeys = (options.capabilities ?? []).map(keyOf);
3076
+ const capabilityLimits = {
3077
+ ...options.capabilityLimits ?? {}
3078
+ };
3079
+ const creditGrants = [];
3080
+ for (const grant of options.grants ?? []) {
3081
+ if (isCapabilityGrant(grant)) {
3082
+ if (!capabilityKeys.includes(grant.capability)) {
3083
+ capabilityKeys.push(grant.capability);
3084
+ }
3085
+ Object.assign(capabilityLimits, grant.limits ?? {});
3086
+ } else {
3087
+ creditGrants.push(grant);
3088
+ }
3089
+ }
3090
+ return { capabilityKeys, capabilityLimits, creditGrants };
2433
3091
  }
2434
- function displayFromKey(key) {
2435
- return key.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
3092
+ function buildMigrationDecl(id, options) {
3093
+ return {
3094
+ id,
3095
+ from: normalizeMigrationPlanRef(options.from),
3096
+ to: normalizeMigrationTargetRef(options.to),
3097
+ newCustomers: options.newCustomers ?? "immediate",
3098
+ existingCustomers: options.existingCustomers,
3099
+ ...options.pins?.length ? {
3100
+ pins: options.pins.map((pin) => ({
3101
+ subscriber: pin.subscriber,
3102
+ pinTo: {
3103
+ plan: keyOf(pin.pinTo.plan),
3104
+ version: pin.pinTo.version
3105
+ },
3106
+ ...pin.until !== void 0 ? { until: pin.until } : {},
3107
+ ...pin.notes !== void 0 ? { notes: pin.notes } : {}
3108
+ }))
3109
+ } : {}
3110
+ };
2436
3111
  }
2437
- function parseRouteMatch(match) {
2438
- const trimmed = match.trim();
2439
- const space = trimmed.indexOf(" ");
2440
- if (space === -1) {
2441
- if (!trimmed.startsWith("/")) {
3112
+ function assertUniqueMigrationIds(migrations) {
3113
+ const seen = /* @__PURE__ */ new Set();
3114
+ for (const migration of migrations) {
3115
+ if (seen.has(migration.id)) {
2442
3116
  throw new ManifestBuilderError(
2443
- `route "${match}": expected "METHOD /path" or "/path"`
3117
+ `duplicate migration "${migration.id}" \u2014 each migration id must be declared once`
2444
3118
  );
2445
3119
  }
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
- );
3120
+ seen.add(migration.id);
2469
3121
  }
2470
- return { method, path };
2471
3122
  }
3123
+ function normalizeMigrationPlanRef(ref) {
3124
+ return {
3125
+ plan: keyOf(ref.plan),
3126
+ ...ref.version !== void 0 ? { version: ref.version } : {}
3127
+ };
3128
+ }
3129
+ function normalizeMigrationTargetRef(ref) {
3130
+ return {
3131
+ plan: keyOf(ref.plan),
3132
+ version: ref.version ?? "head"
3133
+ };
3134
+ }
3135
+
3136
+ // src/product.ts
3137
+ var PRODUCT_BRAND = Symbol.for("farthershore.product.product");
3138
+ var PRODUCT_MANIFEST_COMPILER = Symbol.for(
3139
+ "farthershore.product.manifestCompiler"
3140
+ );
2472
3141
  var Product = class {
2473
3142
  [PRODUCT_BRAND] = true;
2474
3143
  name;
2475
3144
  options;
2476
3145
  graph = new ManifestResourceGraph();
2477
3146
  defaultMeterCosts = [];
2478
- frontendManifest;
2479
3147
  productPatch = {};
2480
3148
  /** Sugar for binding API routes to features. */
2481
3149
  api;
2482
3150
  frontend;
2483
3151
  lifecycle;
2484
- offering;
2485
3152
  /** Escape hatches — raw platform-schema JSON, validated by the compiler. */
2486
3153
  raw;
2487
3154
  constructor(name, options) {
@@ -2502,7 +3169,7 @@ var Product = class {
2502
3169
  this.api = {
2503
3170
  route: (match, options2) => {
2504
3171
  const featureKey = keyOf(options2.feature);
2505
- const file = this.getFeatureFile(featureKey);
3172
+ const file = this.getFeatureLayer(featureKey);
2506
3173
  if (!file) {
2507
3174
  throw new ManifestBuilderError(
2508
3175
  `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call product.feature("${featureKey}", \u2026) first`
@@ -2516,41 +3183,17 @@ var Product = class {
2516
3183
  this.frontend = {
2517
3184
  nav: (items) => {
2518
3185
  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
- }));
3186
+ setFrontendNav(manifest, items);
2524
3187
  this.syncFrontendGraphNode(manifest);
2525
3188
  return this;
2526
3189
  },
2527
3190
  page: (path, options2) => {
2528
3191
  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
- });
3192
+ addFrontendPage(manifest, path, options2);
2549
3193
  this.syncFrontendGraphNode(manifest);
2550
3194
  return this;
2551
3195
  },
2552
3196
  manifest: (manifest) => {
2553
- this.frontendManifest = manifest;
2554
3197
  this.syncFrontendGraphNode(manifest);
2555
3198
  return this;
2556
3199
  }
@@ -2558,77 +3201,60 @@ var Product = class {
2558
3201
  this.lifecycle = {
2559
3202
  migration: (id, options2) => {
2560
3203
  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
- };
3204
+ const migration = buildMigrationDecl(id, options2);
2579
3205
  this.graph.register(
2580
3206
  "lifecycle_migration",
2581
3207
  id,
2582
3208
  migration,
2583
- this.migrationDependsOn(migration)
3209
+ migrationDependsOn(migration)
2584
3210
  );
2585
3211
  return this;
2586
3212
  },
2587
3213
  migrations: (migrations) => {
2588
- this.assertUniqueMigrationIds(migrations);
3214
+ assertUniqueMigrationIds(migrations);
2589
3215
  this.graph.clearKind("lifecycle_migration");
2590
3216
  for (const migration of migrations) {
2591
3217
  this.graph.register(
2592
3218
  "lifecycle_migration",
2593
3219
  migration.id,
2594
3220
  migration,
2595
- this.migrationDependsOn(migration)
3221
+ migrationDependsOn(migration)
2596
3222
  );
2597
3223
  }
2598
3224
  return this;
2599
3225
  }
2600
3226
  };
2601
- this.offering = {
2602
- plan: (key, options2) => this.plan(key, options2)
2603
- };
2604
3227
  this.raw = {
2605
3228
  productPatch: (patch) => {
2606
- this.productPatch = deepMerge(this.productPatch, patch);
3229
+ this.productPatch = mergeProductPatch(this.productPatch, patch);
2607
3230
  return this;
2608
3231
  },
2609
3232
  plan: (spec) => {
2610
3233
  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);
3234
+ this.graph.register(
3235
+ "plan",
3236
+ spec.key,
3237
+ spec,
3238
+ planDependsOn(
3239
+ spec,
3240
+ (kind, dependencyKey) => this.graph.has(kind, dependencyKey)
3241
+ )
3242
+ );
2617
3243
  return this;
2618
3244
  },
2619
- policyFile: (file) => {
2620
- this.assertNewKey("policy", file.name, "policy");
2621
- this.graph.register("policy", file.name, file);
3245
+ routeLayer: (layer) => {
3246
+ this.assertNewKey("feature", layer.feature, "feature");
3247
+ this.registerFeatureLayer(layer);
2622
3248
  return this;
2623
3249
  },
2624
- capabilityFile: (file) => {
2625
- this.assertNewKey("capability", file.capability, "capability");
2626
- this.graph.register("capability", file.capability, file);
3250
+ policyLayer: (layer) => {
3251
+ this.assertNewKey("policy", layer.name, "policy");
3252
+ this.graph.register("policy", layer.name, layer);
2627
3253
  return this;
2628
3254
  },
2629
- frontend: (manifest) => {
2630
- this.frontendManifest = manifest;
2631
- this.syncFrontendGraphNode(manifest);
3255
+ capabilityLayer: (layer) => {
3256
+ this.assertNewKey("capability", layer.capability, "capability");
3257
+ this.graph.register("capability", layer.capability, layer);
2632
3258
  return this;
2633
3259
  }
2634
3260
  };
@@ -2641,7 +3267,7 @@ var Product = class {
2641
3267
  display: meterOptions.display ?? displayFromKey(key),
2642
3268
  ...meterOptions
2643
3269
  });
2644
- const ref = this.makeMeterRef(key);
3270
+ const ref = makeMeterRef(key);
2645
3271
  if (routeDefault !== void 0) {
2646
3272
  this.defaultMeterCosts.push(
2647
3273
  this.normalizeMeterCost(ref.fixed(routeDefault))
@@ -2649,6 +3275,18 @@ var Product = class {
2649
3275
  }
2650
3276
  return ref;
2651
3277
  }
3278
+ /**
3279
+ * BYO-Backend V1 — declare a first-class backend (an always-running HTTP
3280
+ * container Farther Shore wraps). Products may declare MULTIPLE backends;
3281
+ * routes bind to one via `route(..., { backend })`. A product with exactly
3282
+ * one backend (or exactly one marked `default: true`) makes it the implicit
3283
+ * default, so single-backend products stay zero-config.
3284
+ */
3285
+ backend(id, options = {}) {
3286
+ this.assertNewKey("backend", id, "backend");
3287
+ this.graph.register("backend", id, createBackendNode(id, options));
3288
+ return { kind: "backend", key: id };
3289
+ }
2652
3290
  requests(options = {}) {
2653
3291
  return this.meter("requests", {
2654
3292
  display: options.display ?? "Requests",
@@ -2674,20 +3312,8 @@ var Product = class {
2674
3312
  }
2675
3313
  capability(key, options = {}) {
2676
3314
  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
- );
3315
+ const layer = buildCapabilityLayer(key, options);
3316
+ this.graph.register("capability", key, layer, capabilityDependsOn(layer));
2691
3317
  return {
2692
3318
  kind: "capability",
2693
3319
  key,
@@ -2700,52 +3326,31 @@ var Product = class {
2700
3326
  }
2701
3327
  feature(key, options = {}) {
2702
3328
  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);
3329
+ const layer = buildFeatureLayer(
3330
+ key,
3331
+ options,
3332
+ (match, routeOptions) => this.buildRoute(match, routeOptions)
3333
+ );
3334
+ this.registerFeatureLayer(layer);
2730
3335
  const ref = {
2731
3336
  kind: "feature",
2732
3337
  key,
2733
3338
  action: (id, actionOptions) => {
2734
- file.actions ??= [];
2735
- if (file.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
3339
+ layer.actions ??= [];
3340
+ if (layer.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
2736
3341
  throw new ManifestBuilderError(
2737
3342
  `duplicate action "${id}" \u2014 each action id must be declared once`
2738
3343
  );
2739
3344
  }
2740
3345
  const action = { id, ...actionOptions };
2741
- file.actions.push(action);
3346
+ layer.actions.push(action);
2742
3347
  this.registerAction(key, action);
2743
- this.syncFeatureGraphNode(file);
3348
+ this.syncFeatureGraphNode(layer);
2744
3349
  return { kind: "action", key: id };
2745
3350
  },
2746
3351
  route: (match, routeOptions) => {
2747
- file.routes.push(this.buildRoute(match, routeOptions ?? {}));
2748
- this.syncFeatureGraphNode(file);
3352
+ layer.routes.push(this.buildRoute(match, routeOptions ?? {}));
3353
+ this.syncFeatureGraphNode(layer);
2749
3354
  return ref;
2750
3355
  }
2751
3356
  };
@@ -2753,126 +3358,48 @@ var Product = class {
2753
3358
  }
2754
3359
  policy(name, options) {
2755
3360
  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));
3361
+ const layer = buildPolicyLayer(name, options);
3362
+ this.graph.register("policy", name, layer, policyDependsOn(layer));
2772
3363
  return { kind: "policy", key: name };
2773
3364
  }
2774
3365
  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 };
3366
+ const surface = buildSurfaceSpec(type, options);
3367
+ this.assertNewKey("surface", surface.key, "surface");
3368
+ this.graph.register("surface", surface.key, surface);
3369
+ return { kind: "surface", key: surface.key };
2785
3370
  }
2786
3371
  workflow(key, options = {}) {
2787
3372
  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
- );
3373
+ const workflow = buildWorkflowSpec(key, options);
3374
+ this.graph.register("workflow", key, workflow, workflowDependsOn(workflow));
2805
3375
  return { kind: "workflow", key };
2806
3376
  }
2807
3377
  entitlement(key, options = {}) {
2808
3378
  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
- };
3379
+ const entitlement = buildEntitlementSpec(key, options);
2817
3380
  this.graph.register(
2818
3381
  "entitlement",
2819
3382
  key,
2820
3383
  entitlement,
2821
- this.entitlementDependsOn(entitlement)
3384
+ entitlementDependsOn(
3385
+ entitlement,
3386
+ (kind, dependencyKey) => this.graph.has(kind, dependencyKey)
3387
+ )
2822
3388
  );
2823
3389
  return { kind: "entitlement", key };
2824
3390
  }
2825
3391
  plan(key, options) {
2826
3392
  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 = {
3393
+ const spec = buildPlanSpec(key, options);
3394
+ this.graph.register(
3395
+ "plan",
2843
3396
  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));
3397
+ spec,
3398
+ planDependsOn(
3399
+ spec,
3400
+ (kind, dependencyKey) => this.graph.has(kind, dependencyKey)
3401
+ )
3402
+ );
2876
3403
  return { kind: "plan", key };
2877
3404
  }
2878
3405
  use(...modules) {
@@ -2888,8 +3415,16 @@ var Product = class {
2888
3415
  /** @internal Internal platform compiler entrypoint. Builder code exports the
2889
3416
  * Product; the bot/CLI/build-runner decide when to compile and apply it. */
2890
3417
  [PRODUCT_MANIFEST_COMPILER]() {
2891
- const routes = this.materializeFeatureFiles();
2892
- this.assertRouteMeteringValid(routes);
3418
+ const routes = this.materializeFeatureLayers();
3419
+ assertRouteMeteringValid(
3420
+ routes,
3421
+ this.graph.values("meter")
3422
+ );
3423
+ assertBackendBindingsValid(
3424
+ routes,
3425
+ this.graph.values("backend"),
3426
+ this.graph.values("meter")
3427
+ );
2893
3428
  this.assertGraphDependenciesSatisfied();
2894
3429
  const candidate = {
2895
3430
  irVersion: 1,
@@ -2898,245 +3433,67 @@ var Product = class {
2898
3433
  routes,
2899
3434
  policies: this.graph.sortedValues(
2900
3435
  "policy",
2901
- (file) => file.name
3436
+ (layer) => layer.name
2902
3437
  ),
2903
3438
  capabilities: this.graph.sortedValues(
2904
3439
  "capability",
2905
- (file) => file.capability
2906
- ),
2907
- runtime: { rollout: null, flags: null, migrations: null }
3440
+ (layer) => layer.capability
3441
+ )
2908
3442
  };
2909
3443
  const result = validateManifestIr(candidate);
2910
3444
  if (!result.ok) throw new ManifestValidationError(result.issues);
2911
3445
  return { ir: result.ir, irHash: result.irHash };
2912
3446
  }
2913
3447
  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" };
3448
+ return assembleProductSpec({
3449
+ name: this.name,
3450
+ options: this.options,
3451
+ productPatch: this.productPatch,
3452
+ meters: this.graph.values("meter"),
3453
+ defaultMeterCosts: this.defaultMeterCosts,
3454
+ backends: this.graph.values("backend"),
3455
+ surfaces: this.graph.values("surface"),
3456
+ entitlements: this.graph.values("entitlement"),
3457
+ workflows: this.graph.values("workflow"),
3458
+ frontendManifest: this.graph.get(
3459
+ "frontend",
3460
+ "manifest"
3461
+ )?.value,
3462
+ migrations: this.graph.values("lifecycle_migration"),
3463
+ resources: this.graph.values("counted_resource"),
3464
+ plans: this.graph.values("plan"),
3465
+ featureLayers: this.graph.values("feature")
2987
3466
  });
2988
3467
  }
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
3468
  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
- };
3469
+ return buildRouteDefinition(match, options, {
3470
+ normalizeMeterCost: (cost) => this.normalizeMeterCost(cost),
3471
+ getMeterDefinition: (key) => this.graph.get("meter", key)?.value
3472
+ });
3029
3473
  }
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))
3474
+ materializeFeatureLayers() {
3475
+ return this.graph.sortedValues("feature", (layer) => layer.feature).map((layer) => ({
3476
+ ...layer,
3477
+ routes: layer.routes.map(
3478
+ (route) => materializeRoute(route, this.defaultMeterCosts)
3479
+ )
3064
3480
  }));
3065
3481
  }
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
3482
  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);
3483
+ return normalizeMeterCost(
3484
+ cost,
3485
+ (meter) => this.graph.has("meter", meter)
3486
+ );
3112
3487
  }
3113
3488
  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
- }
3489
+ const existing = this.graph.get(
3490
+ "frontend",
3491
+ "manifest"
3492
+ )?.value;
3493
+ if (existing) return existing;
3494
+ const manifest = createFrontendManifest();
3495
+ this.syncFrontendGraphNode(manifest);
3496
+ return manifest;
3140
3497
  }
3141
3498
  assertNewKey(kind, key, label) {
3142
3499
  if (this.graph.has(kind, key)) {
@@ -3146,39 +3503,24 @@ var Product = class {
3146
3503
  }
3147
3504
  }
3148
3505
  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
- );
3506
+ assertResourceDependenciesSatisfied(this.graph.missingDependencies());
3160
3507
  }
3161
- getFeatureFile(key) {
3508
+ getFeatureLayer(key) {
3162
3509
  return this.graph.get("feature", key)?.value ?? null;
3163
3510
  }
3164
- registerFeatureFile(file) {
3511
+ registerFeatureLayer(layer) {
3165
3512
  this.graph.register(
3166
3513
  "feature",
3167
- file.feature,
3168
- file,
3169
- this.featureDependsOn(file)
3514
+ layer.feature,
3515
+ layer,
3516
+ featureDependsOn(layer)
3170
3517
  );
3171
- for (const action of file.actions ?? []) {
3172
- this.registerAction(file.feature, action);
3518
+ for (const action of layer.actions ?? []) {
3519
+ this.registerAction(layer.feature, action);
3173
3520
  }
3174
3521
  }
3175
- syncFeatureGraphNode(file) {
3176
- this.graph.upsert(
3177
- "feature",
3178
- file.feature,
3179
- file,
3180
- this.featureDependsOn(file)
3181
- );
3522
+ syncFeatureGraphNode(layer) {
3523
+ this.graph.upsert("feature", layer.feature, layer, featureDependsOn(layer));
3182
3524
  }
3183
3525
  registerAction(featureKey, action) {
3184
3526
  this.assertNewKey("action", action.id, "action");
@@ -3186,7 +3528,7 @@ var Product = class {
3186
3528
  "action",
3187
3529
  action.id,
3188
3530
  action,
3189
- this.actionDependsOn(featureKey, action)
3531
+ actionDependsOn(featureKey, action)
3190
3532
  );
3191
3533
  }
3192
3534
  syncFrontendGraphNode(manifest) {
@@ -3194,163 +3536,8 @@ var Product = class {
3194
3536
  "frontend",
3195
3537
  "manifest",
3196
3538
  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
3539
+ frontendDependsOn(manifest)
3315
3540
  );
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
3541
  }
3355
3542
  };
3356
3543
  function isProduct(value) {
@@ -3361,34 +3548,6 @@ function product(name, options, configure) {
3361
3548
  if (configure) instance.use(configure);
3362
3549
  return instance;
3363
3550
  }
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
3551
 
3393
3552
  // src/price.ts
3394
3553
  function toCents(dollars, label) {