@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.
@@ -0,0 +1,41 @@
1
+ import type { BackendDefinitionJson, BackendRunnerJson, BackendTransportModeJson, MeterDefinitionJson, RouteLayerJson } from "./ir-types.js";
2
+ import { type KeyRef } from "./refs.js";
3
+ import type { MeterRef } from "./route-metering.js";
4
+ export type BackendTransportOptions = {
5
+ mode?: BackendTransportModeJson;
6
+ runner?: BackendRunnerJson;
7
+ };
8
+ export type BackendOptions = {
9
+ /** Human-friendly label (defaults to the id). */
10
+ name?: string;
11
+ /** Stable slug (defaults to the id). */
12
+ slug?: string;
13
+ transport?: BackendTransportOptions;
14
+ /** Per-request gateway->backend signature verification. Defaults false until
15
+ * the JWKS + signer keystone is proven (deploy-order hard sequence). */
16
+ verification?: {
17
+ required?: boolean;
18
+ };
19
+ /** Meter allow-list. Omitted = all product meters allowed. */
20
+ meters?: Array<KeyRef | MeterRef>;
21
+ /** Marks this as the default backend when a product declares more than one. */
22
+ default?: boolean;
23
+ originUrl?: string;
24
+ originHostname?: string;
25
+ };
26
+ export type BackendNode = {
27
+ id: string;
28
+ } & BackendDefinitionJson;
29
+ export declare function createBackendNode(id: string, options?: BackendOptions): BackendNode;
30
+ /**
31
+ * Project registered backend nodes into a record keyed by id, sorted by id for
32
+ * stable, reviewable IR. The synthetic `id` field is stripped because the map
33
+ * key carries it.
34
+ */
35
+ export declare function buildBackendBlock(backendNodes: readonly BackendNode[]): Record<string, BackendDefinitionJson>;
36
+ /**
37
+ * BYO-Backend V1 — eager build-time validation of route->backend resolution,
38
+ * mirroring the compiler's `emit-backends` diagnostics so a builder gets a
39
+ * friendly error before the bytes ever reach the platform compiler.
40
+ */
41
+ export declare function assertBackendBindingsValid(files: readonly RouteLayerJson[], backendNodes: readonly BackendNode[], meterDefinitions: readonly MeterDefinitionJson[]): void;
@@ -0,0 +1,121 @@
1
+ import type { ActionSpecJson, CacheProfile, CapabilityLayerJson, CountedResourceJson, GrantJson, MigrationDeclJson, MigrationPinJson, MutationClass, PlanLimitJson, PlanMeterJson, PlanSpecJson, PolicyLayerJson, ProductEntitlementJson, ProductSurfaceJson, ProductSurfaceTypeJson, ProductWorkflowJson, ProductWorkflowKindJson, ProductWorkflowTriggerJson, RouteDefinitionJson, RouteLayerJson } from "./ir-types.js";
2
+ import type { PriceSpec } from "./price.js";
3
+ import { type KeyRef, type PlanCapabilityGrant } from "./refs.js";
4
+ import type { MeterRef, RouteMeteringOptions } from "./route-metering.js";
5
+ type RouteDeclarationOptions = RouteMeteringOptions & {
6
+ action?: KeyRef;
7
+ backend?: KeyRef;
8
+ };
9
+ export type CapabilityDeclarationOptions = {
10
+ title?: string;
11
+ description?: string;
12
+ mutationClass?: MutationClass;
13
+ includesFeatures?: KeyRef[];
14
+ includesPolicies?: KeyRef[];
15
+ includesCapabilities?: KeyRef[];
16
+ };
17
+ export type FeatureDeclarationOptions = {
18
+ description?: string;
19
+ mutationClass?: MutationClass;
20
+ cacheProfile?: CacheProfile;
21
+ upstreamOrigin?: string | null;
22
+ policies?: KeyRef[];
23
+ plans?: KeyRef[];
24
+ rolloutKey?: string;
25
+ requiredFlags?: string[];
26
+ actions?: Array<{
27
+ id: string;
28
+ } & Omit<ActionSpecJson, "id">>;
29
+ routes?: Array<{
30
+ match: string;
31
+ } & RouteDeclarationOptions>;
32
+ };
33
+ export type PolicyDeclarationOptions = {
34
+ type: string;
35
+ config: Record<string, unknown>;
36
+ description?: string;
37
+ mutationClass?: MutationClass;
38
+ cacheProfile?: CacheProfile;
39
+ compatibleWith?: {
40
+ routeTypes?: string[];
41
+ meters?: Array<KeyRef | MeterRef>;
42
+ authModes?: string[];
43
+ };
44
+ };
45
+ export type SurfaceDeclarationOptions = {
46
+ key?: string;
47
+ display?: string;
48
+ description?: string;
49
+ };
50
+ export type WorkflowDeclarationOptions = {
51
+ title?: string;
52
+ description?: string;
53
+ kind?: ProductWorkflowKindJson;
54
+ trigger?: ProductWorkflowTriggerJson;
55
+ capabilities?: KeyRef[];
56
+ meters?: Array<KeyRef | MeterRef>;
57
+ estimates?: Record<string, number>;
58
+ metadata?: Record<string, unknown>;
59
+ };
60
+ export type EntitlementDeclarationOptions = {
61
+ description?: string;
62
+ capabilities?: KeyRef[];
63
+ featureGates?: Record<string, boolean>;
64
+ limits?: PlanLimitJson[];
65
+ meters?: Array<KeyRef | MeterRef>;
66
+ };
67
+ export type PlanDeclarationOptions = {
68
+ name: string;
69
+ description?: string;
70
+ details?: string[];
71
+ price?: PriceSpec;
72
+ meters?: PlanMeterJson[];
73
+ grants?: Array<GrantJson | PlanCapabilityGrant>;
74
+ trialDays?: number;
75
+ maxMonthlySpendCents?: number;
76
+ minMonthlySpendCents?: number;
77
+ limits?: PlanLimitJson[];
78
+ featureGates?: Record<string, boolean>;
79
+ capabilityLimits?: Record<string, number | boolean>;
80
+ capabilities?: KeyRef[];
81
+ overageBehavior?: "block" | "allow_and_bill";
82
+ selfServeEnabled?: boolean;
83
+ legacy?: boolean;
84
+ archive?: {
85
+ at?: string;
86
+ transitionTo?: string;
87
+ strategy?: "auto" | "explicit" | "block";
88
+ };
89
+ raw?: Record<string, unknown>;
90
+ };
91
+ export type MigrationPlanRefOptions = {
92
+ plan: KeyRef;
93
+ version?: string;
94
+ };
95
+ export type MigrationTargetRefOptions = {
96
+ plan: KeyRef;
97
+ version?: "head";
98
+ };
99
+ export type MigrationPinOptions = Omit<MigrationPinJson, "pinTo"> & {
100
+ pinTo: {
101
+ plan: KeyRef;
102
+ version: string;
103
+ };
104
+ };
105
+ export type MigrationDeclarationOptions = Omit<MigrationDeclJson, "id" | "from" | "to" | "newCustomers" | "pins"> & {
106
+ from: MigrationPlanRefOptions;
107
+ to: MigrationTargetRefOptions;
108
+ newCustomers?: "immediate";
109
+ pins?: MigrationPinOptions[];
110
+ };
111
+ export type ResourceDeclarationOptions = Omit<CountedResourceJson, "name">;
112
+ export declare function buildCapabilityLayer(key: string, options?: CapabilityDeclarationOptions): CapabilityLayerJson;
113
+ export declare function buildFeatureLayer(key: string, options: FeatureDeclarationOptions, buildRoute: (match: string, options: RouteDeclarationOptions) => RouteDefinitionJson): RouteLayerJson;
114
+ export declare function buildPolicyLayer(name: string, options: PolicyDeclarationOptions): PolicyLayerJson;
115
+ export declare function buildSurfaceSpec(type: ProductSurfaceTypeJson, options?: SurfaceDeclarationOptions): ProductSurfaceJson;
116
+ export declare function buildWorkflowSpec(key: string, options?: WorkflowDeclarationOptions): ProductWorkflowJson;
117
+ export declare function buildEntitlementSpec(key: string, options?: EntitlementDeclarationOptions): ProductEntitlementJson;
118
+ export declare function buildPlanSpec(key: string, options: PlanDeclarationOptions): PlanSpecJson;
119
+ export declare function buildMigrationDecl(id: string, options: MigrationDeclarationOptions): MigrationDeclJson;
120
+ export declare function assertUniqueMigrationIds(migrations: readonly MigrationDeclJson[]): void;
121
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { ActionSpecJson, CapabilityLayerJson, FrontendManifestJson, MigrationDeclJson, PlanSpecJson, PolicyLayerJson, ProductEntitlementJson, ProductWorkflowJson, RouteLayerJson } from "./ir-types.js";
2
+ import { type ManifestResourceKind, type ManifestResourceUrn, type MissingManifestResourceDependency } from "./resource-graph.js";
3
+ export type ResourceExists = (kind: ManifestResourceKind, key: string) => boolean;
4
+ export declare function capabilityDependsOn(file: CapabilityLayerJson): ManifestResourceUrn[];
5
+ export declare function policyDependsOn(file: PolicyLayerJson): ManifestResourceUrn[];
6
+ export declare function entitlementDependsOn(entitlement: ProductEntitlementJson, hasResource: ResourceExists): ManifestResourceUrn[];
7
+ export declare function workflowDependsOn(workflow: ProductWorkflowJson): ManifestResourceUrn[];
8
+ export declare function featureDependsOn(file: RouteLayerJson): ManifestResourceUrn[];
9
+ export declare function actionDependsOn(featureKey: string, action: ActionSpecJson): ManifestResourceUrn[];
10
+ export declare function planDependsOn(plan: PlanSpecJson, hasResource: ResourceExists): ManifestResourceUrn[];
11
+ export declare function migrationDependsOn(migration: MigrationDeclJson): ManifestResourceUrn[];
12
+ export declare function frontendDependsOn(manifest: FrontendManifestJson): ManifestResourceUrn[];
13
+ export declare function assertResourceDependenciesSatisfied(missing: readonly MissingManifestResourceDependency[]): void;
@@ -5,9 +5,9 @@ export type ManifestIssue = {
5
5
  message: string;
6
6
  };
7
7
  /**
8
- * Thrown by `product.toIR()` (and the manifest-build bin) when the
9
- * assembled manifest fails schema validation. Plain-data `issues` so the
10
- * error survives serialization across the runner callback boundary.
8
+ * Thrown by the platform manifest compiler when the assembled manifest fails
9
+ * schema validation. Plain-data `issues` so the error survives serialization
10
+ * across the runner callback boundary.
11
11
  */
12
12
  export declare class ManifestValidationError extends Error {
13
13
  readonly issues: ManifestIssue[];
@@ -0,0 +1,23 @@
1
+ import type { FrontendComponentId, FrontendGateMode, FrontendManifestJson } from "./ir-types.js";
2
+ import { type KeyRef } from "./refs.js";
3
+ export type FrontendNavItemInput = {
4
+ label: string;
5
+ path: string;
6
+ capability?: KeyRef;
7
+ };
8
+ export type FrontendComponentInput = {
9
+ component: FrontendComponentId;
10
+ props?: Record<string, unknown>;
11
+ capability?: KeyRef;
12
+ gateMode?: FrontendGateMode;
13
+ };
14
+ export type FrontendPageInput = {
15
+ title: string;
16
+ requiresAuth: boolean;
17
+ capability?: KeyRef;
18
+ components?: FrontendComponentInput[];
19
+ };
20
+ export declare function createFrontendManifest(): FrontendManifestJson;
21
+ export declare function setFrontendNav(manifest: FrontendManifestJson, items: FrontendNavItemInput[]): void;
22
+ export declare function addFrontendPage(manifest: FrontendManifestJson, path: string, options: FrontendPageInput): void;
23
+ export declare function frontendCapabilityKeys(manifest: FrontendManifestJson): string[];
@@ -12,9 +12,9 @@ export { price } from "./price.js";
12
12
  export { validateManifestIr, hashIr, canonicalIrJson } from "./validate.js";
13
13
  export { ManifestValidationError, ManifestBuilderError } from "./errors.js";
14
14
  export { SDK_VERSION } from "./version.js";
15
- export type { ProductOptions, MeterOptions, RequestMeterOptions, MeterCost, ResourceOptions, CapabilityOptions, ActionOptions, FrontendNavItemOptions, FrontendPageOptions, FrontendComponentOptions, MigrationOptions, FeatureOptions, RouteOptions, PolicyOptions, PlanOptions, SurfaceOptions, EntitlementOptions, MeterRef, ResourceRef, ActionRef, PolicyRef, PlanRef, SurfaceRef, EntitlementRef, FeatureRef, CapabilityRef, PlanCapabilityGrant, ProductModule, } from "./product.js";
15
+ export type { ProductOptions, MeterOptions, RequestMeterOptions, MeterCost, ResourceOptions, CapabilityOptions, ActionOptions, FrontendNavItemOptions, FrontendPageOptions, FrontendComponentOptions, MigrationOptions, FeatureOptions, RouteOptions, BackendOptions, BackendTransportOptions, PolicyOptions, PlanOptions, SurfaceOptions, EntitlementOptions, MeterRef, ResourceRef, ActionRef, BackendRef, PolicyRef, PlanRef, SurfaceRef, EntitlementRef, FeatureRef, CapabilityRef, PlanCapabilityGrant, ProductModule, } from "./product.js";
16
16
  export type { ManifestResourceGraphSnapshot, ManifestResourceKind, ManifestResourceUrn, } from "./resource-graph.js";
17
17
  export type { PriceSpec } from "./price.js";
18
18
  export type { ManifestIssue } from "./errors.js";
19
19
  export type { ValidationResult } from "./validate.js";
20
- export type { ManifestIrDocument, ManifestBuildResult, ProductSpecJson, PlanSpecJson, RoutesFileJson, PolicyFileJson, CapabilityFileJson, FrontendManifestJson, FrontendNavItemJson, FrontendPageJson, FrontendComponentJson, FrontendComponentId, FrontendGateMode, MeterDefinitionJson, PlanMeterJson, PlanLimitJson, GrantJson, HttpMethod, MutationClass, CacheProfile, } from "./ir-types.js";
20
+ export type { ManifestIrDocument, ManifestBuildResult, ProductSpecJson, PlanSpecJson, RouteLayerJson, RouteDefinitionJson, BackendDefinitionJson, BackendTransportModeJson, BackendRunnerJson, PolicyLayerJson, CapabilityLayerJson, FrontendManifestJson, FrontendNavItemJson, FrontendPageJson, FrontendComponentJson, FrontendComponentId, FrontendGateMode, MeterDefinitionJson, PlanMeterJson, PlanLimitJson, GrantJson, HttpMethod, MutationClass, CacheProfile, } from "./ir-types.js";
@@ -57,6 +57,7 @@ export type PlanSpecJson = {
57
57
  max_monthly_spend_cents?: number;
58
58
  min_monthly_spend_cents?: number;
59
59
  grants?: GrantJson[];
60
+ capabilities?: string[];
60
61
  free?: boolean;
61
62
  limits?: PlanLimitJson[];
62
63
  featureGates?: Record<string, boolean>;
@@ -82,6 +83,28 @@ export type RouteDefinitionJson = {
82
83
  unmetered?: boolean;
83
84
  inheritDefaultMeters?: boolean;
84
85
  action?: string;
86
+ /** BYO-Backend V1 — route→backend binding id. Omitted = the sole / default
87
+ * backend (single-backend products stay zero-config). */
88
+ backend?: string;
89
+ };
90
+ export type BackendTransportModeJson = "public_origin" | "mtls" | "cloudflare_tunnel";
91
+ export type BackendRunnerJson = "managed_cloudflared" | "sidecar";
92
+ export type BackendDefinitionJson = {
93
+ name?: string;
94
+ slug?: string;
95
+ transport?: {
96
+ mode?: BackendTransportModeJson;
97
+ runner?: BackendRunnerJson;
98
+ };
99
+ verification?: {
100
+ required?: boolean;
101
+ };
102
+ /** Meter allow-list. Omitted = all product meters allowed. */
103
+ meters?: string[];
104
+ /** Marks the default backend when a product declares more than one. */
105
+ default?: boolean;
106
+ originUrl?: string;
107
+ originHostname?: string;
85
108
  };
86
109
  export type ActionSpecJson = {
87
110
  id: string;
@@ -100,7 +123,7 @@ export type ActionSpecJson = {
100
123
  effect: "create" | "delete";
101
124
  };
102
125
  };
103
- export type RoutesFileJson = {
126
+ export type RouteLayerJson = {
104
127
  feature: string;
105
128
  description?: string;
106
129
  mutation_class?: MutationClass;
@@ -114,11 +137,13 @@ export type RoutesFileJson = {
114
137
  rollout_key?: string;
115
138
  required_flags?: string[];
116
139
  };
117
- capabilities?: string[];
118
140
  plans?: string[];
119
141
  actions?: ActionSpecJson[];
142
+ /** BYO-Backend V1 — feature-level default backend binding for all routes in
143
+ * this file (routes may override per-route). */
144
+ backend?: string;
120
145
  };
121
- export type PolicyFileJson = {
146
+ export type PolicyLayerJson = {
122
147
  name: string;
123
148
  description?: string;
124
149
  type: string;
@@ -131,7 +156,7 @@ export type PolicyFileJson = {
131
156
  mutation_class?: MutationClass;
132
157
  cacheProfile?: CacheProfile;
133
158
  };
134
- export type CapabilityFileJson = {
159
+ export type CapabilityLayerJson = {
135
160
  capability: string;
136
161
  description?: string;
137
162
  mutation_class?: MutationClass;
@@ -294,6 +319,10 @@ export type ProductSpecJson = {
294
319
  meters?: MeterDefinitionJson[];
295
320
  billOn4xx?: boolean;
296
321
  };
322
+ /** BYO-Backend V1 — first-class backend declarations keyed by backend id.
323
+ * Emitted only when at least one backend is declared (so single-backend /
324
+ * no-backend products keep their pre-BYOB irHash). */
325
+ backend?: Record<string, BackendDefinitionJson>;
297
326
  frontend?: FrontendManifestJson;
298
327
  migrations?: MigrationDeclJson[];
299
328
  resources?: CountedResourceJson[];
@@ -312,16 +341,11 @@ export type ManifestIrDocument = {
312
341
  irVersion: 1;
313
342
  sdkVersion: string;
314
343
  product: ProductSpecJson;
315
- routes: RoutesFileJson[];
316
- policies: PolicyFileJson[];
317
- capabilities: CapabilityFileJson[];
318
- runtime: {
319
- rollout: null;
320
- flags: null;
321
- migrations: null;
322
- };
344
+ routes: RouteLayerJson[];
345
+ policies: PolicyLayerJson[];
346
+ capabilities: CapabilityLayerJson[];
323
347
  };
324
- /** `toIR()` result: the validated envelope plus its canonical hash. */
348
+ /** Platform compiler result: the validated envelope plus its canonical hash. */
325
349
  export type ManifestBuildResult = {
326
350
  ir: ManifestIrDocument;
327
351
  irHash: string;
@@ -0,0 +1,22 @@
1
+ import { type BackendNode } from "./backend.js";
2
+ import type { CountedResourceJson, FrontendManifestJson, MeterDefinitionJson, MigrationDeclJson, PlanSpecJson, ProductEntitlementJson, ProductSpecJson, ProductSurfaceJson, ProductWorkflowJson, RouteLayerJson } from "./ir-types.js";
3
+ import type { ProductOptions } from "./product.js";
4
+ import type { MeterCost } from "./route-metering.js";
5
+ export type ProductSpecAssemblyInput = {
6
+ name: string;
7
+ options: ProductOptions;
8
+ productPatch: Record<string, unknown>;
9
+ meters: readonly MeterDefinitionJson[];
10
+ defaultMeterCosts: readonly MeterCost[];
11
+ backends: readonly BackendNode[];
12
+ surfaces: readonly ProductSurfaceJson[];
13
+ entitlements: readonly ProductEntitlementJson[];
14
+ workflows: readonly ProductWorkflowJson[];
15
+ frontendManifest?: FrontendManifestJson;
16
+ migrations: readonly MigrationDeclJson[];
17
+ resources: readonly CountedResourceJson[];
18
+ plans: readonly PlanSpecJson[];
19
+ featureLayers: readonly RouteLayerJson[];
20
+ };
21
+ export declare function assembleProductSpec(input: ProductSpecAssemblyInput): ProductSpecJson;
22
+ export declare function mergeProductPatch(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
@@ -1,6 +1,12 @@
1
- import type { CacheProfile, ActionSpecJson, CapabilityFileJson, FrontendComponentId, FrontendGateMode, FrontendManifestJson, GrantJson, ManifestBuildResult, MeterDefinitionJson, MigrationDeclJson, MigrationPinJson, MutationClass, CountedResourceJson, PlanLimitJson, PlanMeterJson, PlanSpecJson, PolicyFileJson, ProductPoliciesJson, ProductCustomerContextJson, ProductWorkflowKindJson, ProductWorkflowTriggerJson, ProductSurfaceTypeJson, RoutesFileJson } from "./ir-types.js";
1
+ import type { CacheProfile, ActionSpecJson, CapabilityLayerJson, FrontendComponentId, FrontendGateMode, FrontendManifestJson, GrantJson, MeterDefinitionJson, MigrationDeclJson, MigrationPinJson, MutationClass, CountedResourceJson, PlanLimitJson, PlanMeterJson, PlanSpecJson, PolicyLayerJson, ProductPoliciesJson, ProductCustomerContextJson, ProductWorkflowKindJson, ProductWorkflowTriggerJson, ProductSurfaceTypeJson, RouteLayerJson } from "./ir-types.js";
2
2
  import type { PriceSpec } from "./price.js";
3
3
  import { type ManifestResourceGraphSnapshot } from "./resource-graph.js";
4
+ import { type BackendOptions } from "./backend.js";
5
+ import { type PlanCapabilityGrant } from "./refs.js";
6
+ import { type MeterCost, type MeterRef, type RouteMeteringOptions } from "./route-metering.js";
7
+ export type { MeterCost, MeterEstimate, MeterRef } from "./route-metering.js";
8
+ export type { BackendOptions, BackendTransportOptions } from "./backend.js";
9
+ export type { PlanCapabilityGrant } from "./refs.js";
4
10
  /** Brand symbol so the bin can recognize a Product across SDK copies
5
11
  * (the repo's pinned SDK vs whatever loaded the entry). */
6
12
  export declare const PRODUCT_BRAND: unique symbol;
@@ -50,26 +56,12 @@ export type CapabilityOptions = {
50
56
  includesPolicies?: Array<string | PolicyRef>;
51
57
  includesCapabilities?: Array<string | CapabilityRef>;
52
58
  };
53
- export type MeterCost = {
54
- readonly kind: "meter_cost";
55
- readonly meter: string;
56
- readonly value: number;
57
- };
58
- export type RouteOptions = {
59
- /**
60
- * Dynamic meter keys the upstream may report with @farthershore/metering.
61
- */
62
- reports?: string | MeterRef | Array<string | MeterRef>;
63
- /** Fixed gateway-known route costs. */
64
- costs?: MeterCost | Array<MeterCost>;
65
- /** Route-specific pre-request estimates for dynamic reports. */
66
- estimates?: Record<string, number>;
67
- /** Override the default successful-response range. */
68
- onStatusCodes?: string | number[];
69
- /** Disable product.defaultMeters()/product.requests() for this route only. */
70
- inheritDefaultMeters?: boolean;
71
- unmetered?: boolean;
59
+ export type RouteOptions = RouteMeteringOptions & {
72
60
  action?: string | ActionRef;
61
+ /** BYO-Backend V1 — bind this route to a declared backend. Omitted = the
62
+ * product's sole / default backend (single-backend products stay
63
+ * zero-config). */
64
+ backend?: string | BackendRef;
73
65
  };
74
66
  export type ActionOptions = Omit<ActionSpecJson, "id">;
75
67
  export type FrontendNavItemOptions = {
@@ -117,7 +109,6 @@ export type FeatureOptions = {
117
109
  /** Per-feature upstream origin override (default: product origin). */
118
110
  upstreamOrigin?: string | null;
119
111
  policies?: Array<string | PolicyRef>;
120
- capabilities?: Array<string | CapabilityRef>;
121
112
  /** Plans that grant this feature directly. */
122
113
  plans?: Array<string | PlanRef>;
123
114
  rolloutKey?: string;
@@ -193,14 +184,6 @@ export type EntitlementOptions = {
193
184
  limits?: PlanLimitJson[];
194
185
  meters?: Array<string | MeterRef>;
195
186
  };
196
- export type MeterRef = {
197
- readonly kind: "meter";
198
- readonly key: string;
199
- /** Fixed gateway-known usage for route/product defaults. */
200
- fixed(value: number): MeterCost;
201
- /** Convenience value for APIs that want an explicit estimate helper. */
202
- estimate(value: number): MeterCost;
203
- };
204
187
  export type ResourceRef = {
205
188
  readonly kind: "resource";
206
189
  readonly key: string;
@@ -229,6 +212,10 @@ export type ActionRef = {
229
212
  readonly kind: "action";
230
213
  readonly key: string;
231
214
  };
215
+ export type BackendRef = {
216
+ readonly kind: "backend";
217
+ readonly key: string;
218
+ };
232
219
  export type FeatureRef = {
233
220
  readonly kind: "feature";
234
221
  readonly key: string;
@@ -246,11 +233,6 @@ export type CapabilityRef = {
246
233
  limits?: Record<string, number | boolean>;
247
234
  }): PlanCapabilityGrant;
248
235
  };
249
- export type PlanCapabilityGrant = {
250
- readonly kind: "capability_grant";
251
- readonly capability: string;
252
- readonly limits?: Record<string, number | boolean>;
253
- };
254
236
  /**
255
237
  * A synchronous Product SDK module. Use this to split a product across files:
256
238
  * `product.use(configureMeters, configureRoutes, configurePlans)`.
@@ -262,7 +244,6 @@ export declare class Product {
262
244
  private readonly options;
263
245
  private readonly graph;
264
246
  private readonly defaultMeterCosts;
265
- private frontendManifest?;
266
247
  private productPatch;
267
248
  /** Sugar for binding API routes to features. */
268
249
  readonly api: {
@@ -279,22 +260,26 @@ export declare class Product {
279
260
  migration: (id: string, options: MigrationOptions) => Product;
280
261
  migrations: (migrations: MigrationDeclJson[]) => Product;
281
262
  };
282
- readonly offering: {
283
- plan: (key: string, options: PlanOptions) => PlanRef;
284
- };
285
- /** Escape hatches — raw platform-schema JSON, validated at toIR(). */
263
+ /** Escape hatches — raw platform-schema JSON, validated by the compiler. */
286
264
  readonly raw: {
287
265
  /** Deep-merged onto the emitted product spec (usage, webhooks,
288
266
  * environments, add_ons, lifecycle, billing overrides, …). */
289
267
  productPatch: (patch: Record<string, unknown>) => Product;
290
268
  plan: (spec: PlanSpecJson) => Product;
291
- routesFile: (file: RoutesFileJson) => Product;
292
- policyFile: (file: PolicyFileJson) => Product;
293
- capabilityFile: (file: CapabilityFileJson) => Product;
294
- frontend: (manifest: FrontendManifestJson) => Product;
269
+ routeLayer: (layer: RouteLayerJson) => Product;
270
+ policyLayer: (layer: PolicyLayerJson) => Product;
271
+ capabilityLayer: (layer: CapabilityLayerJson) => Product;
295
272
  };
296
273
  constructor(name: string, options: ProductOptions);
297
274
  meter(key: string, options: MeterOptions): MeterRef;
275
+ /**
276
+ * BYO-Backend V1 — declare a first-class backend (an always-running HTTP
277
+ * container Farther Shore wraps). Products may declare MULTIPLE backends;
278
+ * routes bind to one via `route(..., { backend })`. A product with exactly
279
+ * one backend (or exactly one marked `default: true`) makes it the implicit
280
+ * default, so single-backend products stay zero-config.
281
+ */
282
+ backend(id: string, options?: BackendOptions): BackendRef;
298
283
  requests(options?: RequestMeterOptions): MeterRef;
299
284
  defaultMeters(costs: MeterCost | MeterCost[]): Product;
300
285
  resource(name: string, options?: ResourceOptions): ResourceRef;
@@ -308,44 +293,18 @@ export declare class Product {
308
293
  use(...modules: ProductModule[]): Product;
309
294
  /** Inspect the SDK-local declaration graph used to emit the Manifest IR. */
310
295
  resourceGraph(): ManifestResourceGraphSnapshot;
311
- /** Assemble + validate the Manifest IR. Throws ManifestValidationError
312
- * with structured issues when the declared state is invalid. */
313
- toIR(): ManifestBuildResult;
314
296
  private buildProductSpec;
315
- private buildMeterDefinitions;
316
- private routeValueMeterKeys;
317
297
  private buildRoute;
318
- private makeMeterRef;
319
- private buildRouteMetering;
320
- private materializeFeatureFiles;
321
- private materializeRoute;
298
+ private materializeFeatureLayers;
322
299
  private normalizeMeterCost;
323
- private normalizeMeterCosts;
324
- private normalizeMeterRefs;
325
300
  private ensureFrontendManifest;
326
- private normalizeMigrationPlanRef;
327
- private normalizeMigrationTargetRef;
328
- private assertUniqueMigrationIds;
329
301
  private assertNewKey;
330
302
  private assertGraphDependenciesSatisfied;
331
- private getFeatureFile;
332
- private registerFeatureFile;
303
+ private getFeatureLayer;
304
+ private registerFeatureLayer;
333
305
  private syncFeatureGraphNode;
334
306
  private registerAction;
335
307
  private syncFrontendGraphNode;
336
- private capabilityDependsOn;
337
- private policyDependsOn;
338
- private entitlementDependsOn;
339
- private workflowDependsOn;
340
- private featureDependsOn;
341
- private routeMeterDependencyKeys;
342
- private assertRouteMeteringValid;
343
- private actionDependsOn;
344
- private planDependsOn;
345
- private migrationDependsOn;
346
- private frontendDependsOn;
347
- private dependenciesFor;
348
- private existingDependenciesFor;
349
308
  }
350
309
  export declare function isProduct(value: unknown): value is Product;
351
310
  export declare function product(name: string, options: ProductOptions, configure?: ProductModule): Product;
@@ -0,0 +1,11 @@
1
+ export type KeyRef = string | {
2
+ key: string;
3
+ };
4
+ export type PlanCapabilityGrant = {
5
+ readonly kind: "capability_grant";
6
+ readonly capability: string;
7
+ readonly limits?: Record<string, number | boolean>;
8
+ };
9
+ export declare function isCapabilityGrant(value: unknown): value is PlanCapabilityGrant;
10
+ export declare function keyOf(ref: KeyRef): string;
11
+ export declare function displayFromKey(key: string): string;
@@ -1,4 +1,4 @@
1
- export type ManifestResourceKind = "product" | "meter" | "counted_resource" | "capability" | "surface" | "entitlement" | "workflow" | "policy" | "feature" | "action" | "plan" | "frontend" | "lifecycle_migration";
1
+ export type ManifestResourceKind = "product" | "meter" | "counted_resource" | "capability" | "surface" | "entitlement" | "workflow" | "policy" | "feature" | "action" | "plan" | "backend" | "frontend" | "lifecycle_migration";
2
2
  export type ManifestResourceUrn = `urn:farthershore:product:${ManifestResourceKind}:${string}`;
3
3
  export type ManifestResourceRef = {
4
4
  readonly kind: ManifestResourceKind;
@@ -0,0 +1,60 @@
1
+ import type { HttpMethod, MeterDefinitionJson, RouteDefinitionJson, RouteLayerJson } from "./ir-types.js";
2
+ import { type KeyRef } from "./refs.js";
3
+ export type MeterCost = {
4
+ readonly kind: "meter_cost";
5
+ readonly meter: string;
6
+ readonly value: number;
7
+ };
8
+ export type MeterEstimate = {
9
+ readonly kind: "meter_estimate";
10
+ readonly meter: string;
11
+ readonly value: number;
12
+ };
13
+ export type MeterRef = {
14
+ readonly kind: "meter";
15
+ readonly key: string;
16
+ /** Fixed gateway-known usage for route/product defaults. */
17
+ fixed(value: number): MeterCost;
18
+ /** Convenience value for APIs that want an explicit estimate helper. */
19
+ estimate(value: number): MeterEstimate;
20
+ };
21
+ export type RouteMeteringOptions = {
22
+ /**
23
+ * Dynamic meter keys the upstream may report with @farthershore/metering.
24
+ */
25
+ reports?: string | MeterRef | Array<string | MeterRef>;
26
+ /** Fixed gateway-known route costs. */
27
+ costs?: MeterCost | Array<MeterCost>;
28
+ /** Route-specific pre-request estimates for dynamic reports. */
29
+ estimates?: Record<string, number> | MeterEstimate | MeterEstimate[];
30
+ /** Override the default successful-response range. */
31
+ onStatusCodes?: string | number[];
32
+ /** Disable product.defaultMeters()/product.requests() for this route only. */
33
+ inheritDefaultMeters?: boolean;
34
+ unmetered?: boolean;
35
+ };
36
+ type RouteBindingOptions = RouteMeteringOptions & {
37
+ action?: KeyRef;
38
+ /** BYO-Backend V1 — bind this route to a declared backend. Omitted = the
39
+ * product's sole / default backend (single-backend products stay
40
+ * zero-config). */
41
+ backend?: KeyRef;
42
+ };
43
+ export type RouteMeteringDeps = {
44
+ normalizeMeterCost(cost: MeterCost): MeterCost;
45
+ getMeterDefinition(key: string): MeterDefinitionJson | undefined;
46
+ };
47
+ export declare function parseRouteMatch(match: string): {
48
+ method: HttpMethod;
49
+ path: string;
50
+ };
51
+ export declare function makeMeterRef(key: string): MeterRef;
52
+ export declare function normalizeMeterCost(cost: MeterCost, hasMeter: (meter: string) => boolean): MeterCost;
53
+ export declare function normalizeMeterCosts(costs: RouteMeteringOptions["costs"], normalize: (cost: MeterCost) => MeterCost): MeterCost[];
54
+ export declare function normalizeMeterRefs(refs: string | MeterRef | Array<string | MeterRef>): string[];
55
+ export declare function buildRouteDefinition(match: string, options: RouteBindingOptions, deps: RouteMeteringDeps): RouteDefinitionJson;
56
+ export declare function buildRouteMetering(options: RouteMeteringOptions, deps: RouteMeteringDeps): RouteDefinitionJson["metering"] | undefined;
57
+ export declare function materializeRoute(route: RouteDefinitionJson, defaultMeterCosts: readonly MeterCost[]): RouteDefinitionJson;
58
+ export declare function routeMeterDependencyKeys(route: RouteDefinitionJson): string[];
59
+ export declare function assertRouteMeteringValid(files: RouteLayerJson[], meterDefinitions: Iterable<MeterDefinitionJson>): void;
60
+ export {};
@@ -12,10 +12,8 @@ export type ValidationResult = {
12
12
  * Validate a candidate envelope against the platform schemas. On success
13
13
  * the returned `ir` is the JSON-normalized ORIGINAL candidate — NOT the
14
14
  * zod-parsed value. The platform treats the product spec as a raw document
15
- * (the YAML path never zod-transformed it before storing/compiling, and
16
- * the compiler reads some fields e.g. `plan.capabilities` off the raw
17
- * spec via casts that a default-stripping parse would erase). Validation
18
- * proves the document is acceptable; the bytes stay the author's.
15
+ * because the YAML path never zod-transformed it before storing/compiling.
16
+ * Validation proves the document is acceptable; the bytes stay the author's.
19
17
  */
20
18
  export declare function validateManifestIr(candidate: unknown): ValidationResult;
21
19
  export declare function hashIr(ir: unknown): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farthershore/product",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Farther Shore product-as-code SDK — declare your software product in TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",