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