@farthershore/product 0.3.1 → 0.5.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/README.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  Product-as-Code SDK for Farther Shore. Builder repos use this package from
4
4
  `product/product.config.ts` to declare product contracts in TypeScript. Builders
5
- author and export a `Business`; Farther Shore compiles that program to
6
- backend-owned IR, validates it, and applies it through Core. Every product is
7
- created with a GitHub repo that contains the editable `frontend/` starter and
8
- the Product SDK entrypoint; connecting GitHub is a product-creation
9
- precondition.
5
+ author and export a `Product`; Farther Shore compiles that program to
6
+ backend-owned IR, validates it, and applies it through Core. The Product SDK
7
+ describes the sellable software product: its business logic origin,
8
+ surfaces, plans, capabilities, meters, and lifecycle. Every product is created
9
+ with a GitHub repo that contains the editable `frontend/` starter and the
10
+ Product SDK entrypoint; connecting GitHub is a product-creation precondition.
10
11
 
11
12
  ## Install
12
13
 
@@ -47,15 +48,17 @@ import { configureMeters } from "./meters.js";
47
48
  import { configureCronRoutes } from "./routes/cron.js";
48
49
  import { configurePlans } from "./plans/index.js";
49
50
 
50
- const business = fs.business("croncloud", {
51
- baseUrl: "https://api.example.com",
51
+ const product = fs.product("croncloud", {
52
+ origin: "https://app.example.com",
52
53
  displayName: "CronCloud",
53
54
  description: "Managed cron jobs",
54
55
  });
55
56
 
56
- business.use(configureMeters, configureCronRoutes, configurePlans);
57
+ product.surface("frontend");
58
+ product.surface("api");
59
+ product.use(configureMeters, configureCronRoutes, configurePlans);
57
60
 
58
- export default business;
61
+ export default product;
59
62
  ```
60
63
 
61
64
  Modules are plain synchronous functions:
@@ -100,7 +103,7 @@ plain request counting.
100
103
 
101
104
  ```ts
102
105
  const product = fs.product("croncloud", {
103
- baseUrl: "https://api.croncloud.com",
106
+ origin: "https://app.croncloud.com",
104
107
  });
105
108
 
106
109
  product.requests();
@@ -145,18 +148,18 @@ Generated product repos use GitHub as the required automation and frontend
145
148
  workspace:
146
149
 
147
150
  1. Loads `product/product.config.ts`.
148
- 2. Requires the default export to be the `Business` returned by
149
- `fs.business(...)` or `fs.product(...)`.
150
- 3. Executes imported modules and compiles the business into deterministic
151
+ 2. Requires the default export to be the `Product` returned by
152
+ `fs.product(...)`.
153
+ 3. Executes imported modules and compiles the product into deterministic
151
154
  Manifest IR.
152
155
  4. Validates the result against the deployed platform contract.
153
156
  5. Publishes the accepted release through Core so edge artifacts propagate.
154
157
 
155
- The same IR can also be applied to an already repo-backed product through
156
- trusted Core APIs, for example `farthershore build --apply <product>`. That path
157
- is useful for local validation/apply loops, but it is not a replacement for the
158
- product repo. The repo remains the required frontend customization workspace
159
- because `frontend/` is where the starter UI and all custom React code live.
158
+ The same IR can be built locally with `farthershore build`; accepted lifecycle
159
+ state is validated and applied by the GitHub bot after `product/**` changes are
160
+ committed and pushed. The repo remains the required frontend customization
161
+ workspace because `frontend/` is where the starter UI and all custom React code
162
+ live.
160
163
 
161
164
  The bundled `farthershore-manifest-build` binary is shared by the bot,
162
165
  build-runner, and CLI. It emits the deterministic Manifest IR envelope that Core
@@ -164,24 +167,32 @@ accepts; Core, not the user repo, remains the lifecycle authority.
164
167
 
165
168
  ## Public API
166
169
 
167
- - `fs.business(name, options)` / `fs.product(name, options)` — create the
168
- product builder.
169
- - `business.use(...modules)` — compose Product SDK modules from any files under
170
+ - `fs.product(name, options)` — create the product builder. `options.origin`
171
+ is required and is the business logic origin Farther Shore calls for
172
+ customer-facing actions.
173
+ - `product.use(...modules)` — compose Product SDK modules from any files under
170
174
  `product/`.
171
- - `business.meter(...)` — declare billable or enforceable dimensions.
172
- - `business.requests()` declare and inherit the platform-managed successful
175
+ - `product.surface(...)` — declare the product surfaces customers interact with
176
+ (`frontend`, `api`, `docs`, `widget`, `webhook`, `worker`, or `agent`).
177
+ - `product.entitlement(...)` — group capabilities, feature gates, limits, and
178
+ meters into reusable access metadata.
179
+ - `product.offering.plan(...)` — declare a subscription plan through the
180
+ generalized offering namespace. `product.plan(...)` remains the direct plan
181
+ helper.
182
+ - `product.meter(...)` — declare billable or enforceable dimensions.
183
+ - `product.requests()` — declare and inherit the platform-managed successful
173
184
  request meter.
174
- - `business.defaultMeters(...)` — apply reusable fixed costs to metered routes.
175
- - `business.resource(...)` — declare counted resources for resource-count
185
+ - `product.defaultMeters(...)` — apply reusable fixed costs to metered routes.
186
+ - `product.resource(...)` — declare counted resources for resource-count
176
187
  constraints.
177
- - `business.capability(...)` — declare capability bundles and plan grants.
178
- - `business.feature(...)` / `business.api.route(...)` — declare gateway routes,
188
+ - `product.capability(...)` — declare capability bundles and plan grants.
189
+ - `product.feature(...)` / `product.api.route(...)` — declare gateway routes,
179
190
  static costs, dynamic reports, estimates, and action metadata.
180
- - `business.policy(...)` — declare policy files in code.
181
- - `business.plan(...)` — declare plan pricing, limits, grants, and lifecycle
191
+ - `product.policy(...)` — declare policy files in code.
192
+ - `product.plan(...)` — declare plan pricing, limits, grants, and lifecycle
182
193
  behavior.
183
- - `business.lifecycle.*` — declare migrations.
184
- - `business.raw.*` — escape hatches for platform-schema JSON when the typed SDK
194
+ - `product.lifecycle.*` — declare migrations.
195
+ - `product.raw.*` — escape hatches for platform-schema JSON when the typed SDK
185
196
  does not yet have sugar.
186
197
 
187
198
  ## Determinism
package/dist/bin.js CHANGED
@@ -1393,7 +1393,7 @@ var customerPortalAuthStrategySchema = z13.enum([
1393
1393
  var productCustomerContextSchema = z13.object({
1394
1394
  /**
1395
1395
  * Edge credential identity policy. This is intentionally Product-scoped:
1396
- * B7 keeps Product as the business boundary and avoids customer-side
1396
+ * B7 keeps Product as the product boundary and avoids customer-side
1397
1397
  * Workspace/subject models until per-subject entitlements need them.
1398
1398
  */
1399
1399
  identity_requirement: customerIdentityRequirementSchema.optional(),
@@ -1414,6 +1414,70 @@ var productCustomerContextSchema = z13.object({
1414
1414
  strategy: customerPortalAuthStrategySchema
1415
1415
  }).optional()
1416
1416
  });
1417
+ var productSurfaceTypeSchema = z13.enum([
1418
+ "frontend",
1419
+ "api",
1420
+ "docs",
1421
+ "widget",
1422
+ "dashboard",
1423
+ "webhook",
1424
+ "worker",
1425
+ "agent"
1426
+ ]);
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"),
1429
+ 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([
1444
+ "async_job",
1445
+ "agent_task",
1446
+ "scheduled",
1447
+ "lifecycle",
1448
+ "background"
1449
+ ]);
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)
1455
+ }),
1456
+ z13.object({
1457
+ type: z13.literal("event"),
1458
+ event: z13.string().min(1).max(120)
1459
+ }),
1460
+ z13.object({
1461
+ type: z13.literal("api"),
1462
+ path: z13.string().min(1).max(240).regex(/^\//, "path must start with /")
1463
+ }),
1464
+ z13.object({
1465
+ type: z13.literal("lifecycle"),
1466
+ event: z13.string().min(1).max(120)
1467
+ })
1468
+ ]);
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(),
1473
+ kind: productWorkflowKindSchema,
1474
+ 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([]);
1417
1481
  var productSpecSchema = z13.object({
1418
1482
  product: z13.object({
1419
1483
  name: z13.string().min(1).max(100),
@@ -1447,6 +1511,9 @@ var productSpecSchema = z13.object({
1447
1511
  resources: countedResourcesSchema,
1448
1512
  policies: productOperatorPoliciesSchema,
1449
1513
  customer_context: productCustomerContextSchema.optional(),
1514
+ surfaces: productSurfacesSchema,
1515
+ entitlements: productEntitlementsSchema,
1516
+ workflows: productWorkflowsSchema,
1450
1517
  /**
1451
1518
  * Legacy/internal declarative frontend surface. New product repos customize
1452
1519
  * the generated `frontend/` project directly; this remains for existing IR
@@ -2156,6 +2223,9 @@ var productSpecV2Schema = z20.object({
2156
2223
  resources: countedResourcesSchema,
2157
2224
  policies: productOperatorPoliciesSchema,
2158
2225
  customer_context: productCustomerContextSchema.optional(),
2226
+ surfaces: productSurfacesSchema,
2227
+ entitlements: productEntitlementsSchema,
2228
+ workflows: productWorkflowsSchema,
2159
2229
  billing: z20.object({
2160
2230
  gracePeriodDays: z20.number().int().nonnegative().default(3),
2161
2231
  // When true (default), a plan limit INCREASE re-projects onto active
@@ -2353,10 +2423,13 @@ function hashIr(ir) {
2353
2423
  }
2354
2424
 
2355
2425
  // src/version.ts
2356
- var SDK_VERSION = true ? "0.3.1" : "0.0.0-dev";
2426
+ var SDK_VERSION = true ? "0.5.0" : "0.0.0-dev";
2357
2427
 
2358
- // src/business.ts
2359
- var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
2428
+ // src/product.ts
2429
+ var PRODUCT_BRAND = Symbol.for("farthershore.product.product");
2430
+ var PRODUCT_MANIFEST_COMPILER = Symbol.for(
2431
+ "farthershore.product.manifestCompiler"
2432
+ );
2360
2433
  function isCapabilityGrant(value) {
2361
2434
  return typeof value === "object" && value !== null && value.kind === "capability_grant";
2362
2435
  }
@@ -2401,8 +2474,8 @@ function parseRouteMatch(match) {
2401
2474
  }
2402
2475
  return { method, path };
2403
2476
  }
2404
- var Business = class {
2405
- [BUSINESS_BRAND] = true;
2477
+ var Product = class {
2478
+ [PRODUCT_BRAND] = true;
2406
2479
  name;
2407
2480
  options;
2408
2481
  graph = new ManifestResourceGraph();
@@ -2413,15 +2486,16 @@ var Business = class {
2413
2486
  api;
2414
2487
  frontend;
2415
2488
  lifecycle;
2416
- /** Escape hatches — raw platform-schema JSON, validated at toIR(). */
2489
+ offering;
2490
+ /** Escape hatches — raw platform-schema JSON, validated by the compiler. */
2417
2491
  raw;
2418
2492
  constructor(name, options) {
2419
2493
  if (!name || typeof name !== "string") {
2420
- throw new ManifestBuilderError("fs.business(name, \u2026): name is required");
2494
+ throw new ManifestBuilderError("fs.product(name, \u2026): name is required");
2421
2495
  }
2422
- if (!options?.baseUrl) {
2496
+ if (!options?.origin) {
2423
2497
  throw new ManifestBuilderError(
2424
- `fs.business("${name}", \u2026): options.baseUrl is required (the upstream origin the gateway proxies to)`
2498
+ `fs.product("${name}", \u2026): options.origin is required (the business logic origin Farther Shore calls for customer-facing actions)`
2425
2499
  );
2426
2500
  }
2427
2501
  this.name = name;
@@ -2436,7 +2510,7 @@ var Business = class {
2436
2510
  const file = this.getFeatureFile(featureKey);
2437
2511
  if (!file) {
2438
2512
  throw new ManifestBuilderError(
2439
- `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call business.feature("${featureKey}", \u2026) first`
2513
+ `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call product.feature("${featureKey}", \u2026) first`
2440
2514
  );
2441
2515
  }
2442
2516
  file.routes.push(this.buildRoute(match, options2));
@@ -2529,6 +2603,9 @@ var Business = class {
2529
2603
  return this;
2530
2604
  }
2531
2605
  };
2606
+ this.offering = {
2607
+ plan: (key, options2) => this.plan(key, options2)
2608
+ };
2532
2609
  this.raw = {
2533
2610
  productPatch: (patch) => {
2534
2611
  this.productPatch = deepMerge(this.productPatch, patch);
@@ -2699,6 +2776,57 @@ var Business = class {
2699
2776
  this.graph.register("policy", name, file, this.policyDependsOn(file));
2700
2777
  return { kind: "policy", key: name };
2701
2778
  }
2779
+ surface(type, options = {}) {
2780
+ const key = options.key ?? type;
2781
+ this.assertNewKey("surface", key, "surface");
2782
+ const surface = {
2783
+ key,
2784
+ type,
2785
+ ...options.display !== void 0 ? { display: options.display } : {},
2786
+ ...options.description !== void 0 ? { description: options.description } : {}
2787
+ };
2788
+ this.graph.register("surface", key, surface);
2789
+ return { kind: "surface", key };
2790
+ }
2791
+ workflow(key, options = {}) {
2792
+ this.assertNewKey("workflow", key, "workflow");
2793
+ const workflow = {
2794
+ key,
2795
+ kind: options.kind ?? "async_job",
2796
+ trigger: options.trigger ?? { type: "manual" },
2797
+ ...options.title !== void 0 ? { title: options.title } : {},
2798
+ ...options.description !== void 0 ? { description: options.description } : {},
2799
+ ...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
2800
+ ...options.meters?.length ? { meters: options.meters.map(keyOf) } : {},
2801
+ ...options.estimates !== void 0 ? { estimates: options.estimates } : {},
2802
+ ...options.metadata !== void 0 ? { metadata: options.metadata } : {}
2803
+ };
2804
+ this.graph.register(
2805
+ "workflow",
2806
+ key,
2807
+ workflow,
2808
+ this.workflowDependsOn(workflow)
2809
+ );
2810
+ return { kind: "workflow", key };
2811
+ }
2812
+ entitlement(key, options = {}) {
2813
+ this.assertNewKey("entitlement", key, "entitlement");
2814
+ const entitlement = {
2815
+ key,
2816
+ ...options.description !== void 0 ? { description: options.description } : {},
2817
+ ...options.capabilities?.length ? { capabilities: options.capabilities.map(keyOf) } : {},
2818
+ ...options.featureGates !== void 0 ? { featureGates: options.featureGates } : {},
2819
+ ...options.limits?.length ? { limits: options.limits } : {},
2820
+ ...options.meters?.length ? { meters: options.meters.map(keyOf) } : {}
2821
+ };
2822
+ this.graph.register(
2823
+ "entitlement",
2824
+ key,
2825
+ entitlement,
2826
+ this.entitlementDependsOn(entitlement)
2827
+ );
2828
+ return { kind: "entitlement", key };
2829
+ }
2702
2830
  plan(key, options) {
2703
2831
  this.assertNewKey("plan", key, "plan");
2704
2832
  const capabilityKeys = (options.capabilities ?? []).map(keyOf);
@@ -2762,19 +2890,17 @@ var Business = class {
2762
2890
  resourceGraph() {
2763
2891
  return this.graph.snapshot();
2764
2892
  }
2765
- /** Assemble + validate the Manifest IR. Throws ManifestValidationError
2766
- * with structured issues when the declared state is invalid. */
2767
- toIR() {
2893
+ /** @internal Internal platform compiler entrypoint. Builder code exports the
2894
+ * Product; the bot/CLI/build-runner decide when to compile and apply it. */
2895
+ [PRODUCT_MANIFEST_COMPILER]() {
2896
+ const routes = this.materializeFeatureFiles();
2897
+ this.assertRouteMeteringValid(routes);
2768
2898
  this.assertGraphDependenciesSatisfied();
2769
- this.assertRouteMeteringValid();
2770
2899
  const candidate = {
2771
2900
  irVersion: 1,
2772
2901
  sdkVersion: SDK_VERSION,
2773
2902
  product: this.buildProductSpec(),
2774
- routes: this.graph.sortedValues(
2775
- "feature",
2776
- (file) => file.feature
2777
- ),
2903
+ routes,
2778
2904
  policies: this.graph.sortedValues(
2779
2905
  "policy",
2780
2906
  (file) => file.name
@@ -2794,10 +2920,10 @@ var Business = class {
2794
2920
  const base = {
2795
2921
  product: {
2796
2922
  name: this.name,
2797
- baseUrl: options.baseUrl,
2923
+ baseUrl: options.origin,
2798
2924
  ...options.displayName !== void 0 ? { displayName: options.displayName } : {},
2799
2925
  ...options.description !== void 0 ? { description: options.description } : {},
2800
- ...options.sandboxBaseUrl !== void 0 ? { sandboxBaseUrl: options.sandboxBaseUrl } : {},
2926
+ ...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
2801
2927
  ...options.visibility !== void 0 ? { visibility: options.visibility } : {},
2802
2928
  ...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
2803
2929
  ...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
@@ -2814,6 +2940,24 @@ var Business = class {
2814
2940
  ...options.billing !== void 0 ? { billing: options.billing } : {},
2815
2941
  ...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
2816
2942
  ...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
2943
+ ...this.graph.values("surface").length ? {
2944
+ surfaces: this.graph.sortedValues(
2945
+ "surface",
2946
+ (surface) => surface.key
2947
+ )
2948
+ } : {},
2949
+ ...this.graph.values("entitlement").length ? {
2950
+ entitlements: this.graph.sortedValues(
2951
+ "entitlement",
2952
+ (entitlement) => entitlement.key
2953
+ )
2954
+ } : {},
2955
+ ...this.graph.values("workflow").length ? {
2956
+ workflows: this.graph.sortedValues(
2957
+ "workflow",
2958
+ (workflow) => workflow.key
2959
+ )
2960
+ } : {},
2817
2961
  ...this.graph.has("frontend", "manifest") ? {
2818
2962
  frontend: this.graph.get(
2819
2963
  "frontend",
@@ -2849,6 +2993,9 @@ var Business = class {
2849
2993
  }
2850
2994
  routeValueMeterKeys() {
2851
2995
  const keys = /* @__PURE__ */ new Set();
2996
+ for (const cost of this.defaultMeterCosts) {
2997
+ keys.add(cost.meter);
2998
+ }
2852
2999
  for (const file of this.graph.values("feature")) {
2853
3000
  for (const route of file.routes) {
2854
3001
  for (const meter of Object.keys(route.metering?.defaults ?? {})) {
@@ -2859,6 +3006,11 @@ var Business = class {
2859
3006
  }
2860
3007
  }
2861
3008
  }
3009
+ for (const workflow of this.graph.values("workflow")) {
3010
+ for (const meter of workflow.meters ?? []) keys.add(meter);
3011
+ for (const meter of Object.keys(workflow.estimates ?? {}))
3012
+ keys.add(meter);
3013
+ }
2862
3014
  return keys;
2863
3015
  }
2864
3016
  buildRoute(match, options) {
@@ -2883,11 +3035,6 @@ var Business = class {
2883
3035
  buildRouteMetering(options) {
2884
3036
  if (options.unmetered === true) return void 0;
2885
3037
  const defaults = {};
2886
- if (options.inheritDefaultMeters !== false) {
2887
- for (const cost of this.defaultMeterCosts) {
2888
- defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
2889
- }
2890
- }
2891
3038
  for (const cost of this.normalizeMeterCosts(options.costs)) {
2892
3039
  defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
2893
3040
  }
@@ -2915,6 +3062,32 @@ var Business = class {
2915
3062
  out.onStatusCodes = options.onStatusCodes;
2916
3063
  return Object.keys(out).length ? out : void 0;
2917
3064
  }
3065
+ materializeFeatureFiles() {
3066
+ return this.graph.sortedValues("feature", (file) => file.feature).map((file) => ({
3067
+ ...file,
3068
+ routes: file.routes.map((route) => this.materializeRoute(route))
3069
+ }));
3070
+ }
3071
+ materializeRoute(route) {
3072
+ if (route.unmetered === true) return route;
3073
+ const defaults = {
3074
+ ...route.metering?.defaults ?? {}
3075
+ };
3076
+ if (route.inheritDefaultMeters !== false) {
3077
+ for (const cost of this.defaultMeterCosts) {
3078
+ defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
3079
+ }
3080
+ }
3081
+ const hasMetering = route.metering !== void 0 || Object.keys(defaults).length > 0;
3082
+ const metering = hasMetering ? {
3083
+ ...route.metering ?? {},
3084
+ ...Object.keys(defaults).length ? { defaults } : {}
3085
+ } : void 0;
3086
+ return {
3087
+ ...route,
3088
+ ...metering ? { metering } : {}
3089
+ };
3090
+ }
2918
3091
  normalizeMeterCost(cost) {
2919
3092
  if (!cost || cost.kind !== "meter_cost") {
2920
3093
  throw new ManifestBuilderError(
@@ -3039,6 +3212,27 @@ var Business = class {
3039
3212
  policyDependsOn(file) {
3040
3213
  return this.dependenciesFor("meter", file.compatible_with?.meters);
3041
3214
  }
3215
+ entitlementDependsOn(entitlement) {
3216
+ const limitDimensions = (entitlement.limits ?? []).map(
3217
+ (limit) => limit.dimension
3218
+ );
3219
+ return [
3220
+ ...this.dependenciesFor("capability", entitlement.capabilities),
3221
+ ...this.existingDependenciesFor("meter", [
3222
+ ...entitlement.meters ?? [],
3223
+ ...limitDimensions
3224
+ ])
3225
+ ];
3226
+ }
3227
+ workflowDependsOn(workflow) {
3228
+ return [
3229
+ ...this.dependenciesFor("capability", workflow.capabilities),
3230
+ ...this.dependenciesFor("meter", [
3231
+ ...workflow.meters ?? [],
3232
+ ...Object.keys(workflow.estimates ?? {})
3233
+ ])
3234
+ ];
3235
+ }
3042
3236
  featureDependsOn(file) {
3043
3237
  const meterKeys = file.routes.flatMap(
3044
3238
  (route) => this.routeMeterDependencyKeys(route)
@@ -3061,12 +3255,26 @@ var Business = class {
3061
3255
  }
3062
3256
  return [...keys];
3063
3257
  }
3064
- assertRouteMeteringValid() {
3065
- for (const file of this.graph.values("feature")) {
3258
+ assertRouteMeteringValid(files) {
3259
+ const declaredMeters = new Set(
3260
+ this.graph.values("meter").map((meter) => meter.key)
3261
+ );
3262
+ for (const file of files) {
3066
3263
  file.routes.forEach((route, routeIndex) => {
3067
3264
  if (route.unmetered === true) return;
3068
3265
  const defaults = new Set(Object.keys(route.metering?.defaults ?? {}));
3266
+ for (const meter of defaults) {
3267
+ if (declaredMeters.has(meter)) continue;
3268
+ throw new ManifestBuilderError(
3269
+ `feature "${file.feature}" route ${routeIndex}: fixed meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
3270
+ );
3271
+ }
3069
3272
  for (const meter of route.metering?.reports ?? []) {
3273
+ if (!declaredMeters.has(meter)) {
3274
+ throw new ManifestBuilderError(
3275
+ `feature "${file.feature}" route ${routeIndex}: reported meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
3276
+ );
3277
+ }
3070
3278
  if (defaults.has(meter)) {
3071
3279
  throw new ManifestBuilderError(
3072
3280
  `feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
@@ -3089,6 +3297,12 @@ var Business = class {
3089
3297
  );
3090
3298
  }
3091
3299
  }
3300
+ for (const meter of Object.keys(route.metering?.estimates ?? {})) {
3301
+ if (declaredMeters.has(meter)) continue;
3302
+ throw new ManifestBuilderError(
3303
+ `feature "${file.feature}" route ${routeIndex}: estimate meter "${meter}" is not declared \u2014 declare it with product.meter("${meter}", ...) first`
3304
+ );
3305
+ }
3092
3306
  });
3093
3307
  }
3094
3308
  }
@@ -3144,8 +3358,11 @@ var Business = class {
3144
3358
  return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
3145
3359
  }
3146
3360
  };
3147
- function isBusiness(value) {
3148
- return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
3361
+ function isProduct(value) {
3362
+ return typeof value === "object" && value !== null && value[PRODUCT_BRAND] === true;
3363
+ }
3364
+ function compileProductToManifest(product) {
3365
+ return product[PRODUCT_MANIFEST_COMPILER]();
3149
3366
  }
3150
3367
  function isPlainObject(value) {
3151
3368
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -3154,7 +3371,7 @@ function buildCustomerContext(options) {
3154
3371
  return {
3155
3372
  ...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
3156
3373
  ...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
3157
- ...options.portalAuth !== void 0 ? { portal_auth: options.portalAuth } : {}
3374
+ ...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
3158
3375
  };
3159
3376
  }
3160
3377
  function deepMerge(base, patch) {
@@ -3267,16 +3484,16 @@ async function main() {
3267
3484
  }
3268
3485
  const direct = mod.default;
3269
3486
  const interop = direct?.default;
3270
- const candidate = isBusiness(direct) ? direct : interop;
3271
- if (!isBusiness(candidate)) {
3487
+ const candidate = isProduct(direct) ? direct : interop;
3488
+ if (!isProduct(candidate)) {
3272
3489
  process.stderr.write(
3273
- `farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the Business returned by fs.business(\u2026)
3490
+ `farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the Product returned by fs.product(\u2026)
3274
3491
  `
3275
3492
  );
3276
3493
  process.exit(1);
3277
3494
  }
3278
3495
  try {
3279
- const { ir, irHash } = candidate.toIR();
3496
+ const { ir, irHash } = compileProductToManifest(candidate);
3280
3497
  await writeFile(
3281
3498
  resolve(process.cwd(), args.out),
3282
3499
  `${JSON.stringify({ ir, irHash }, null, 2)}