@farthershore/product 0.3.1 → 0.4.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,10 @@ 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.4.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");
2360
2430
  function isCapabilityGrant(value) {
2361
2431
  return typeof value === "object" && value !== null && value.kind === "capability_grant";
2362
2432
  }
@@ -2401,8 +2471,8 @@ function parseRouteMatch(match) {
2401
2471
  }
2402
2472
  return { method, path };
2403
2473
  }
2404
- var Business = class {
2405
- [BUSINESS_BRAND] = true;
2474
+ var Product = class {
2475
+ [PRODUCT_BRAND] = true;
2406
2476
  name;
2407
2477
  options;
2408
2478
  graph = new ManifestResourceGraph();
@@ -2413,15 +2483,16 @@ var Business = class {
2413
2483
  api;
2414
2484
  frontend;
2415
2485
  lifecycle;
2486
+ offering;
2416
2487
  /** Escape hatches — raw platform-schema JSON, validated at toIR(). */
2417
2488
  raw;
2418
2489
  constructor(name, options) {
2419
2490
  if (!name || typeof name !== "string") {
2420
- throw new ManifestBuilderError("fs.business(name, \u2026): name is required");
2491
+ throw new ManifestBuilderError("fs.product(name, \u2026): name is required");
2421
2492
  }
2422
- if (!options?.baseUrl) {
2493
+ if (!options?.origin) {
2423
2494
  throw new ManifestBuilderError(
2424
- `fs.business("${name}", \u2026): options.baseUrl is required (the upstream origin the gateway proxies to)`
2495
+ `fs.product("${name}", \u2026): options.origin is required (the business logic origin Farther Shore calls for customer-facing actions)`
2425
2496
  );
2426
2497
  }
2427
2498
  this.name = name;
@@ -2436,7 +2507,7 @@ var Business = class {
2436
2507
  const file = this.getFeatureFile(featureKey);
2437
2508
  if (!file) {
2438
2509
  throw new ManifestBuilderError(
2439
- `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call business.feature("${featureKey}", \u2026) first`
2510
+ `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call product.feature("${featureKey}", \u2026) first`
2440
2511
  );
2441
2512
  }
2442
2513
  file.routes.push(this.buildRoute(match, options2));
@@ -2529,6 +2600,9 @@ var Business = class {
2529
2600
  return this;
2530
2601
  }
2531
2602
  };
2603
+ this.offering = {
2604
+ plan: (key, options2) => this.plan(key, options2)
2605
+ };
2532
2606
  this.raw = {
2533
2607
  productPatch: (patch) => {
2534
2608
  this.productPatch = deepMerge(this.productPatch, patch);
@@ -2699,6 +2773,57 @@ var Business = class {
2699
2773
  this.graph.register("policy", name, file, this.policyDependsOn(file));
2700
2774
  return { kind: "policy", key: name };
2701
2775
  }
2776
+ 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 };
2787
+ }
2788
+ workflow(key, options = {}) {
2789
+ 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
+ );
2807
+ return { kind: "workflow", key };
2808
+ }
2809
+ entitlement(key, options = {}) {
2810
+ 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
+ };
2819
+ this.graph.register(
2820
+ "entitlement",
2821
+ key,
2822
+ entitlement,
2823
+ this.entitlementDependsOn(entitlement)
2824
+ );
2825
+ return { kind: "entitlement", key };
2826
+ }
2702
2827
  plan(key, options) {
2703
2828
  this.assertNewKey("plan", key, "plan");
2704
2829
  const capabilityKeys = (options.capabilities ?? []).map(keyOf);
@@ -2765,16 +2890,14 @@ var Business = class {
2765
2890
  /** Assemble + validate the Manifest IR. Throws ManifestValidationError
2766
2891
  * with structured issues when the declared state is invalid. */
2767
2892
  toIR() {
2893
+ const routes = this.materializeFeatureFiles();
2894
+ this.assertRouteMeteringValid(routes);
2768
2895
  this.assertGraphDependenciesSatisfied();
2769
- this.assertRouteMeteringValid();
2770
2896
  const candidate = {
2771
2897
  irVersion: 1,
2772
2898
  sdkVersion: SDK_VERSION,
2773
2899
  product: this.buildProductSpec(),
2774
- routes: this.graph.sortedValues(
2775
- "feature",
2776
- (file) => file.feature
2777
- ),
2900
+ routes,
2778
2901
  policies: this.graph.sortedValues(
2779
2902
  "policy",
2780
2903
  (file) => file.name
@@ -2794,10 +2917,10 @@ var Business = class {
2794
2917
  const base = {
2795
2918
  product: {
2796
2919
  name: this.name,
2797
- baseUrl: options.baseUrl,
2920
+ baseUrl: options.origin,
2798
2921
  ...options.displayName !== void 0 ? { displayName: options.displayName } : {},
2799
2922
  ...options.description !== void 0 ? { description: options.description } : {},
2800
- ...options.sandboxBaseUrl !== void 0 ? { sandboxBaseUrl: options.sandboxBaseUrl } : {},
2923
+ ...options.sandboxOrigin !== void 0 ? { sandboxBaseUrl: options.sandboxOrigin } : {},
2801
2924
  ...options.visibility !== void 0 ? { visibility: options.visibility } : {},
2802
2925
  ...options.logoUrl !== void 0 ? { logoUrl: options.logoUrl } : {},
2803
2926
  ...options.primaryColor !== void 0 ? { primaryColor: options.primaryColor } : {},
@@ -2814,6 +2937,24 @@ var Business = class {
2814
2937
  ...options.billing !== void 0 ? { billing: options.billing } : {},
2815
2938
  ...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
2816
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
+ } : {},
2817
2958
  ...this.graph.has("frontend", "manifest") ? {
2818
2959
  frontend: this.graph.get(
2819
2960
  "frontend",
@@ -2849,6 +2990,9 @@ var Business = class {
2849
2990
  }
2850
2991
  routeValueMeterKeys() {
2851
2992
  const keys = /* @__PURE__ */ new Set();
2993
+ for (const cost of this.defaultMeterCosts) {
2994
+ keys.add(cost.meter);
2995
+ }
2852
2996
  for (const file of this.graph.values("feature")) {
2853
2997
  for (const route of file.routes) {
2854
2998
  for (const meter of Object.keys(route.metering?.defaults ?? {})) {
@@ -2859,6 +3003,11 @@ var Business = class {
2859
3003
  }
2860
3004
  }
2861
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
+ }
2862
3011
  return keys;
2863
3012
  }
2864
3013
  buildRoute(match, options) {
@@ -2883,11 +3032,6 @@ var Business = class {
2883
3032
  buildRouteMetering(options) {
2884
3033
  if (options.unmetered === true) return void 0;
2885
3034
  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
3035
  for (const cost of this.normalizeMeterCosts(options.costs)) {
2892
3036
  defaults[cost.meter] = (defaults[cost.meter] ?? 0) + cost.value;
2893
3037
  }
@@ -2915,6 +3059,32 @@ var Business = class {
2915
3059
  out.onStatusCodes = options.onStatusCodes;
2916
3060
  return Object.keys(out).length ? out : void 0;
2917
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))
3066
+ }));
3067
+ }
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
+ }
2918
3088
  normalizeMeterCost(cost) {
2919
3089
  if (!cost || cost.kind !== "meter_cost") {
2920
3090
  throw new ManifestBuilderError(
@@ -3039,6 +3209,27 @@ var Business = class {
3039
3209
  policyDependsOn(file) {
3040
3210
  return this.dependenciesFor("meter", file.compatible_with?.meters);
3041
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
+ }
3042
3233
  featureDependsOn(file) {
3043
3234
  const meterKeys = file.routes.flatMap(
3044
3235
  (route) => this.routeMeterDependencyKeys(route)
@@ -3061,12 +3252,26 @@ var Business = class {
3061
3252
  }
3062
3253
  return [...keys];
3063
3254
  }
3064
- assertRouteMeteringValid() {
3065
- for (const file of this.graph.values("feature")) {
3255
+ assertRouteMeteringValid(files) {
3256
+ const declaredMeters = new Set(
3257
+ this.graph.values("meter").map((meter) => meter.key)
3258
+ );
3259
+ for (const file of files) {
3066
3260
  file.routes.forEach((route, routeIndex) => {
3067
3261
  if (route.unmetered === true) return;
3068
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
+ }
3069
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
+ }
3070
3275
  if (defaults.has(meter)) {
3071
3276
  throw new ManifestBuilderError(
3072
3277
  `feature "${file.feature}" route ${routeIndex}: meter "${meter}" cannot be both a fixed route cost and a dynamic report`
@@ -3089,6 +3294,12 @@ var Business = class {
3089
3294
  );
3090
3295
  }
3091
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
+ }
3092
3303
  });
3093
3304
  }
3094
3305
  }
@@ -3144,8 +3355,8 @@ var Business = class {
3144
3355
  return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
3145
3356
  }
3146
3357
  };
3147
- function isBusiness(value) {
3148
- return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
3358
+ function isProduct(value) {
3359
+ return typeof value === "object" && value !== null && value[PRODUCT_BRAND] === true;
3149
3360
  }
3150
3361
  function isPlainObject(value) {
3151
3362
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -3154,7 +3365,7 @@ function buildCustomerContext(options) {
3154
3365
  return {
3155
3366
  ...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
3156
3367
  ...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
3157
- ...options.portalAuth !== void 0 ? { portal_auth: options.portalAuth } : {}
3368
+ ...options.customerAuth !== void 0 ? { portal_auth: options.customerAuth } : {}
3158
3369
  };
3159
3370
  }
3160
3371
  function deepMerge(base, patch) {
@@ -3267,10 +3478,10 @@ async function main() {
3267
3478
  }
3268
3479
  const direct = mod.default;
3269
3480
  const interop = direct?.default;
3270
- const candidate = isBusiness(direct) ? direct : interop;
3271
- if (!isBusiness(candidate)) {
3481
+ const candidate = isProduct(direct) ? direct : interop;
3482
+ if (!isProduct(candidate)) {
3272
3483
  process.stderr.write(
3273
- `farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the Business returned by fs.business(\u2026)
3484
+ `farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the Product returned by fs.product(\u2026)
3274
3485
  `
3275
3486
  );
3276
3487
  process.exit(1);