@farthershore/backend 0.14.0 → 0.15.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
@@ -1,6 +1,6 @@
1
1
  # @farthershore/backend
2
2
 
3
- The runtime SDK for your own backend. When you run a software product on Farther
3
+ The runtime SDK for your own backend. When you run a software business on Farther
4
4
  Shore with a bring-your-own-backend, the platform's edge gateway sits in front of
5
5
  your service. This package lets your backend **trust the gateway** (verify that
6
6
  each request really came from it) and **report usage** back for metering and
@@ -8,11 +8,11 @@ billing — from a single token, `FS_RUNTIME_TOKEN`.
8
8
 
9
9
  Install one package, set one environment variable, and you get fail-closed
10
10
  gateway-to-upstream request verification, response-bound usage reporting, and
11
- graceful lifecycle (health + shutdown). Everything else — your product, backend,
11
+ graceful lifecycle (health + shutdown). Everything else — your business, backend,
12
12
  and environment ids, the verification keys, and the metering endpoint — is
13
13
  fetched automatically from the token at startup.
14
14
 
15
- > **Status: `0.14.0`.** Pre-1.0: minor releases may include breaking changes, so
15
+ > **Status: `0.15.0`.** Pre-1.0: minor releases may include breaking changes, so
16
16
  > pin this package to an exact version (or a patch-only range) and upgrade
17
17
  > deliberately.
18
18
 
@@ -76,7 +76,7 @@ process.on("SIGTERM", () => void fs.shutdown());
76
76
  ## What `initFromEnv()` derives
77
77
 
78
78
  You configure exactly one thing: `FS_RUNTIME_TOKEN` (mint it for your backend
79
- with the Farther Shore CLI or dashboard). Everything else — product / backend /
79
+ with the Farther Shore CLI or dashboard). Everything else — business / backend /
80
80
  environment ids, the JWKS url used to verify signatures, the metering endpoint
81
81
  and credential, and verification settings — is fetched from the platform at
82
82
  startup and cached in memory. The token is validated eagerly, so a
@@ -126,11 +126,11 @@ export async function POST(request: Request) {
126
126
 
127
127
  - `measureContext` is free-form pricing/analytics context persisted with the
128
128
  usage event.
129
- - `creditUnitsConsumed` is a numeric map for credit-wallet style products; keys
129
+ - `creditUnitsConsumed` is a numeric map for credit-wallet style businesses; keys
130
130
  and values are validated locally before signing.
131
131
 
132
132
  The meter keys you report (e.g. `tokens_used`) must match meters declared in your
133
- product. Request-count style limits are enforced by the gateway and need no
133
+ business. Request-count style limits are enforced by the gateway and need no
134
134
  backend code.
135
135
 
136
136
  ## Async / background usage
@@ -83,7 +83,7 @@ var RUNTIME_SIGNING_CONTRACT = {
83
83
  "body-hash",
84
84
  "request-id",
85
85
  "timestamp",
86
- "product-id",
86
+ "business-id",
87
87
  "backend-id",
88
88
  "route-id",
89
89
  "policy-version"
@@ -95,7 +95,7 @@ var RUNTIME_SIGNING_CONTRACT = {
95
95
  "body-hash": "Lowercase hex SHA-256 of the RAW request body bytes. For an empty body, the SHA-256 of zero bytes (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Streaming-exempt requests use the literal token 'STREAM'.",
96
96
  "request-id": "Opaque unique request id minted by the gateway (also the replay-cache nonce).",
97
97
  timestamp: "Integer Unix epoch seconds (UTC) at signing time, as a base-10 string with no padding.",
98
- "product-id": "Product id the request is routed to.",
98
+ "business-id": "Business id the request is routed to.",
99
99
  "backend-id": "Backend id the route binds to.",
100
100
  "route-id": "Resolved route id; empty string if the route is unresolved.",
101
101
  "policy-version": "Tenant artifact / policy version the gateway signed under."
@@ -109,7 +109,7 @@ var RUNTIME_CANONICAL_FIELDS = [
109
109
  "body-hash",
110
110
  "request-id",
111
111
  "timestamp",
112
- "product-id",
112
+ "business-id",
113
113
  "backend-id",
114
114
  "route-id",
115
115
  "policy-version"
@@ -133,7 +133,7 @@ var RUNTIME_HEADERS = {
133
133
  keyId: "x-fs-key-id",
134
134
  requestId: "x-fs-request-id",
135
135
  timestamp: "x-fs-timestamp",
136
- productId: "x-fs-product-id",
136
+ businessId: "x-fs-business-id",
137
137
  backendId: "x-fs-backend-id",
138
138
  routeId: "x-fs-route-id",
139
139
  policyVersion: "x-fs-policy-version",
@@ -169,7 +169,7 @@ var RUNTIME_METERING_CONTRACT = {
169
169
  credential: "reusable-bearer",
170
170
  event: {
171
171
  event_id: "string",
172
- product_id: "string",
172
+ business_id: "string",
173
173
  backend_id: "string",
174
174
  route_id: "string?",
175
175
  request_id: "string?",
package/dist/index.js CHANGED
@@ -306,7 +306,7 @@ var RUNTIME_HEADER_NAMES = {
306
306
  keyId: "x-fs-key-id",
307
307
  requestId: "x-fs-request-id",
308
308
  timestamp: "x-fs-timestamp",
309
- productId: "x-fs-product-id",
309
+ businessId: "x-fs-business-id",
310
310
  backendId: "x-fs-backend-id",
311
311
  routeId: "x-fs-route-id",
312
312
  policyVersion: "x-fs-policy-version",
@@ -358,7 +358,7 @@ var CANONICAL_SIGNING_FIELDS = [
358
358
  "body-hash",
359
359
  "request-id",
360
360
  "timestamp",
361
- "product-id",
361
+ "business-id",
362
362
  "backend-id",
363
363
  "route-id",
364
364
  "policy-version"
@@ -395,7 +395,7 @@ function buildCanonicalSigningString(input) {
395
395
  "body-hash": input.bodyHash,
396
396
  "request-id": input.requestId,
397
397
  timestamp: String(input.timestamp),
398
- "product-id": input.productId,
398
+ "business-id": input.businessId,
399
399
  "backend-id": input.backendId,
400
400
  "route-id": input.routeId,
401
401
  "policy-version": input.policyVersion
@@ -761,7 +761,7 @@ var DEFAULT_MAX_RETRIES = 3;
761
761
  var MeteringClient = class {
762
762
  config;
763
763
  endpoint;
764
- productId;
764
+ businessId;
765
765
  backendId;
766
766
  fetchImpl;
767
767
  maxRetries;
@@ -775,7 +775,7 @@ var MeteringClient = class {
775
775
  constructor(options) {
776
776
  this.config = options.config;
777
777
  this.endpoint = resolveEndpoint(options.config.endpoint, options.coreUrl);
778
- this.productId = options.productId;
778
+ this.businessId = options.businessId;
779
779
  this.backendId = options.backendId;
780
780
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
781
781
  this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -838,13 +838,14 @@ var MeteringClient = class {
838
838
  }
839
839
  const event = {
840
840
  event_id: options.eventId ?? this.newId(),
841
- product_id: this.productId,
841
+ business_id: this.businessId,
842
842
  backend_id: this.backendId,
843
843
  meter,
844
844
  qty,
845
845
  timestamp: options.timestamp ?? this.now().toISOString(),
846
846
  ...options.routeId ? { route_id: options.routeId } : {},
847
- ...options.requestId ? { request_id: options.requestId } : {}
847
+ ...options.requestId ? { request_id: options.requestId } : {},
848
+ ...options.subscriptionId ? { subscription_id: options.subscriptionId } : {}
848
849
  };
849
850
  this.buffer.push(event);
850
851
  await this.flush();
@@ -1705,12 +1706,12 @@ async function verifyRequest(input, deps) {
1705
1706
  const kid = h(RUNTIME_HEADER_NAMES.keyId);
1706
1707
  const requestId = h(RUNTIME_HEADER_NAMES.requestId);
1707
1708
  const timestampRaw = h(RUNTIME_HEADER_NAMES.timestamp);
1708
- const signedProductId = h(RUNTIME_HEADER_NAMES.productId);
1709
+ const signedBusinessId = h(RUNTIME_HEADER_NAMES.businessId);
1709
1710
  const signedBackendId = h(RUNTIME_HEADER_NAMES.backendId);
1710
1711
  const signedRouteId = h(RUNTIME_HEADER_NAMES.routeId) ?? "";
1711
1712
  const policyVersion = h(RUNTIME_HEADER_NAMES.policyVersion);
1712
1713
  const signedBodyHash = h(RUNTIME_HEADER_NAMES.bodyHash);
1713
- if (!kid || !requestId || !timestampRaw || !signedProductId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
1714
+ if (!kid || !requestId || !timestampRaw || !signedBusinessId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
1714
1715
  throw new FartherShoreError(
1715
1716
  "malformed_signature",
1716
1717
  "request is missing one or more required x-fs-* headers"
@@ -1746,10 +1747,10 @@ async function verifyRequest(input, deps) {
1746
1747
  "recomputed body hash does not match the signed x-fs-body-hash"
1747
1748
  );
1748
1749
  }
1749
- if (deps.productId !== void 0 && signedProductId !== deps.productId) {
1750
+ if (deps.businessId !== void 0 && signedBusinessId !== deps.businessId) {
1750
1751
  throw new FartherShoreError(
1751
1752
  "route_mismatch",
1752
- "signed product-id does not match this backend's product"
1753
+ "signed business-id does not match this backend's business"
1753
1754
  );
1754
1755
  }
1755
1756
  if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
@@ -1771,7 +1772,7 @@ async function verifyRequest(input, deps) {
1771
1772
  bodyHash: computedBodyHash,
1772
1773
  requestId,
1773
1774
  timestamp,
1774
- productId: signedProductId,
1775
+ businessId: signedBusinessId,
1775
1776
  backendId: signedBackendId,
1776
1777
  routeId: signedRouteId,
1777
1778
  policyVersion
@@ -1811,10 +1812,10 @@ async function verifyRequest(input, deps) {
1811
1812
  if (signedContext === null && deps.contextVerification === "required") {
1812
1813
  throw contextRequiredError("failed verification");
1813
1814
  }
1814
- if (signedContext && signedContext.productId !== signedProductId) {
1815
+ if (signedContext && signedContext.productId !== signedBusinessId) {
1815
1816
  throw new FartherShoreError(
1816
1817
  "context_unverified",
1817
- "X-Fs-Context was minted for a different product than the signed request"
1818
+ "X-Fs-Context was minted for a different business than the signed request"
1818
1819
  );
1819
1820
  }
1820
1821
  } else if (deps.contextVerification === "required") {
@@ -1834,7 +1835,7 @@ async function verifyRequest(input, deps) {
1834
1835
  }
1835
1836
  return {
1836
1837
  requestId,
1837
- productId: signedProductId,
1838
+ businessId: signedBusinessId,
1838
1839
  backendId: signedBackendId,
1839
1840
  routeId: signedRouteId,
1840
1841
  policyVersion,
@@ -1877,8 +1878,8 @@ function headerGetter(headers) {
1877
1878
 
1878
1879
  // src/core/runtime.ts
1879
1880
  var DEFAULT_CORE_URL = "https://core.farthershore.com";
1880
- var SDK_VERSION = "0.14.0".length > 0 ? "0.14.0" : "0.0.0-dev";
1881
- var CONTRACTS_FP = "1feb5a4a80b447ad".length > 0 ? "1feb5a4a80b447ad" : "0000000000000000";
1881
+ var SDK_VERSION = "0.15.0".length > 0 ? "0.15.0" : "0.0.0-dev";
1882
+ var CONTRACTS_FP = "4b6a36b4cb1f0b68".length > 0 ? "4b6a36b4cb1f0b68" : "0000000000000000";
1882
1883
  var FartherShore = class {
1883
1884
  bootstrapClient;
1884
1885
  fetchImpl;
@@ -1947,7 +1948,7 @@ var FartherShore = class {
1947
1948
  if (!this.meteringClient && config.metering.enabled) {
1948
1949
  this.meteringClient = new MeteringClient({
1949
1950
  config: config.metering,
1950
- productId: config.product.id,
1951
+ businessId: config.business.id,
1951
1952
  backendId: config.backend.id,
1952
1953
  coreUrl: this.coreUrl,
1953
1954
  fetchImpl: this.fetchImpl
@@ -2031,7 +2032,7 @@ var FartherShore = class {
2031
2032
  const context = await verifyRequest(input, {
2032
2033
  jwks: this.jwks,
2033
2034
  nonceCache: this.nonceCache,
2034
- productId: config.product.id,
2035
+ businessId: config.business.id,
2035
2036
  backendId: config.backend.id,
2036
2037
  knownRouteIds,
2037
2038
  clockSkewSeconds: config.verification.clockSkewSeconds,
@@ -2261,7 +2262,7 @@ async function makeSignedRequest(spec = {}) {
2261
2262
  bodyHash,
2262
2263
  requestId: spec.requestId ?? `req_${cryptoRandom()}`,
2263
2264
  timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
2264
- productId: spec.productId ?? "prod_test",
2265
+ businessId: spec.businessId ?? "biz_test",
2265
2266
  backendId: spec.backendId ?? "be_test",
2266
2267
  routeId: spec.routeId ?? "route_test",
2267
2268
  policyVersion: spec.policyVersion ?? "pv_1"
@@ -2273,7 +2274,7 @@ async function makeSignedRequest(spec = {}) {
2273
2274
  [RUNTIME_HEADER_NAMES.keyId]: kid,
2274
2275
  [RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
2275
2276
  [RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
2276
- [RUNTIME_HEADER_NAMES.productId]: claim.productId,
2277
+ [RUNTIME_HEADER_NAMES.businessId]: claim.businessId,
2277
2278
  [RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
2278
2279
  [RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
2279
2280
  [RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
@@ -2374,13 +2375,13 @@ function mergeHeaders(initHeaders, signedHeaders) {
2374
2375
  }
2375
2376
  return headers;
2376
2377
  }
2377
- function buildContextClaim(persona, productId) {
2378
+ function buildContextClaim(persona, businessId) {
2378
2379
  return {
2379
2380
  orgId: persona.orgId ?? "org_dev",
2380
2381
  actor: persona.actor ?? { type: "user", id: `user_${persona.name}` },
2381
- // Product binding: the signed context productId MUST equal the signed
2382
- // request productId or verifyRequest rejects it as tamper evidence.
2383
- productId,
2382
+ // Business binding: the retained signed-context productId claim MUST equal
2383
+ // the signed request businessId or verifyRequest rejects it as tamper evidence.
2384
+ productId: businessId,
2384
2385
  compiledPlanId: persona.compiledPlanId ?? "plan_dev",
2385
2386
  subscriptionId: persona.subscriptionId ?? "sub_dev",
2386
2387
  subscriberId: persona.subscriberId ?? "subscriber_dev",
@@ -2408,7 +2409,7 @@ function createPersonaClient(ctx) {
2408
2409
  query: spec.query ?? "",
2409
2410
  body: spec.body ?? null,
2410
2411
  streamingExempt: spec.streamingExempt ?? false,
2411
- productId: ctx.productId,
2412
+ businessId: ctx.businessId,
2412
2413
  backendId: ctx.backendId,
2413
2414
  routeId: spec.routeId ?? "",
2414
2415
  privateJwk: ctx.keys.privateJwk,
@@ -2418,7 +2419,7 @@ function createPersonaClient(ctx) {
2418
2419
  });
2419
2420
  const headers = { ...signed.headers };
2420
2421
  if (!persona.anonymous) {
2421
- const claim = buildContextClaim(persona, ctx.productId);
2422
+ const claim = buildContextClaim(persona, ctx.businessId);
2422
2423
  headers["x-fs-context"] = await signContextToken(
2423
2424
  claim,
2424
2425
  ctx.contextSecret,
@@ -2491,12 +2492,12 @@ var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
2491
2492
  var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
2492
2493
  var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
2493
2494
  function createDevGateway(options) {
2494
- const productId = options.productId ?? "prod_dev";
2495
+ const businessId = options.businessId ?? "biz_dev";
2495
2496
  const backendId = options.backendId ?? "be_dev";
2496
2497
  const meterEvents = [];
2497
2498
  const reportUsageEvents = [];
2498
2499
  const bootstrap = {
2499
- product: { id: productId, slug: options.productSlug ?? "dev-product" },
2500
+ business: { id: businessId, slug: options.businessSlug ?? "dev-business" },
2500
2501
  backend: {
2501
2502
  id: backendId,
2502
2503
  slug: options.backendSlug ?? "dev-backend",
@@ -2566,7 +2567,7 @@ function createDevGateway(options) {
2566
2567
  bootstrap,
2567
2568
  meterEvents,
2568
2569
  reportUsageEvents,
2569
- productId,
2570
+ businessId,
2570
2571
  backendId,
2571
2572
  jwksUrl: DEV_JWKS_URL
2572
2573
  };
@@ -2768,7 +2769,7 @@ function createDevRuntime(options) {
2768
2769
  const gateway = createDevGateway({
2769
2770
  mode,
2770
2771
  keys,
2771
- ...options.productId ? { productId: options.productId } : {},
2772
+ ...options.businessId ? { businessId: options.businessId } : {},
2772
2773
  ...options.backendId ? { backendId: options.backendId } : {},
2773
2774
  ...options.routes ? { routeIds: options.routes } : {},
2774
2775
  onMeterEvent: (event) => {
@@ -2797,7 +2798,7 @@ function createDevRuntime(options) {
2797
2798
  const personas = buildPersonaMap(options.personas);
2798
2799
  const personaClient = createPersonaClient({
2799
2800
  keys,
2800
- productId: gateway.productId,
2801
+ businessId: gateway.businessId,
2801
2802
  backendId: gateway.backendId,
2802
2803
  contextSecret: keys.contextSecret,
2803
2804
  contextKid: keys.contextKid,
@@ -2927,7 +2928,7 @@ function createDevRuntimeFromEnv(env = readProcessEnv3()) {
2927
2928
  version: 1,
2928
2929
  mode,
2929
2930
  keys,
2930
- productId: runtime.gateway.productId,
2931
+ businessId: runtime.gateway.businessId,
2931
2932
  backendId: runtime.gateway.backendId,
2932
2933
  personas: mapToRecord(runtime.personas)
2933
2934
  };
@@ -2940,7 +2941,7 @@ function printBanner(mode, runtime, tracePath) {
2940
2941
  "============================================================",
2941
2942
  " \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
2942
2943
  ` mode: ${mode.toUpperCase()}`,
2943
- ` product: ${runtime.gateway.productId}`,
2944
+ ` business: ${runtime.gateway.businessId}`,
2944
2945
  ` backend: ${runtime.gateway.backendId}`,
2945
2946
  ` personas: ${[...runtime.personas.keys()].join(", ")}`,
2946
2947
  ` usage log: ${USAGE_JSONL_PATH}`,
@@ -294,7 +294,7 @@ var RUNTIME_HEADER_NAMES = {
294
294
  keyId: "x-fs-key-id",
295
295
  requestId: "x-fs-request-id",
296
296
  timestamp: "x-fs-timestamp",
297
- productId: "x-fs-product-id",
297
+ businessId: "x-fs-business-id",
298
298
  backendId: "x-fs-backend-id",
299
299
  routeId: "x-fs-route-id",
300
300
  policyVersion: "x-fs-policy-version",
@@ -346,7 +346,7 @@ var CANONICAL_SIGNING_FIELDS = [
346
346
  "body-hash",
347
347
  "request-id",
348
348
  "timestamp",
349
- "product-id",
349
+ "business-id",
350
350
  "backend-id",
351
351
  "route-id",
352
352
  "policy-version"
@@ -383,7 +383,7 @@ function buildCanonicalSigningString(input) {
383
383
  "body-hash": input.bodyHash,
384
384
  "request-id": input.requestId,
385
385
  timestamp: String(input.timestamp),
386
- "product-id": input.productId,
386
+ "business-id": input.businessId,
387
387
  "backend-id": input.backendId,
388
388
  "route-id": input.routeId,
389
389
  "policy-version": input.policyVersion
@@ -608,7 +608,7 @@ async function makeSignedRequest(spec = {}) {
608
608
  bodyHash,
609
609
  requestId: spec.requestId ?? `req_${cryptoRandom()}`,
610
610
  timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
611
- productId: spec.productId ?? "prod_test",
611
+ businessId: spec.businessId ?? "biz_test",
612
612
  backendId: spec.backendId ?? "be_test",
613
613
  routeId: spec.routeId ?? "route_test",
614
614
  policyVersion: spec.policyVersion ?? "pv_1"
@@ -620,7 +620,7 @@ async function makeSignedRequest(spec = {}) {
620
620
  [RUNTIME_HEADER_NAMES.keyId]: kid,
621
621
  [RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
622
622
  [RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
623
- [RUNTIME_HEADER_NAMES.productId]: claim.productId,
623
+ [RUNTIME_HEADER_NAMES.businessId]: claim.businessId,
624
624
  [RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
625
625
  [RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
626
626
  [RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
@@ -742,13 +742,13 @@ function mergeHeaders(initHeaders, signedHeaders) {
742
742
  }
743
743
  return headers;
744
744
  }
745
- function buildContextClaim(persona, productId) {
745
+ function buildContextClaim(persona, businessId) {
746
746
  return {
747
747
  orgId: persona.orgId ?? "org_dev",
748
748
  actor: persona.actor ?? { type: "user", id: `user_${persona.name}` },
749
- // Product binding: the signed context productId MUST equal the signed
750
- // request productId or verifyRequest rejects it as tamper evidence.
751
- productId,
749
+ // Business binding: the retained signed-context productId claim MUST equal
750
+ // the signed request businessId or verifyRequest rejects it as tamper evidence.
751
+ productId: businessId,
752
752
  compiledPlanId: persona.compiledPlanId ?? "plan_dev",
753
753
  subscriptionId: persona.subscriptionId ?? "sub_dev",
754
754
  subscriberId: persona.subscriberId ?? "subscriber_dev",
@@ -776,7 +776,7 @@ function createPersonaClient(ctx) {
776
776
  query: spec.query ?? "",
777
777
  body: spec.body ?? null,
778
778
  streamingExempt: spec.streamingExempt ?? false,
779
- productId: ctx.productId,
779
+ businessId: ctx.businessId,
780
780
  backendId: ctx.backendId,
781
781
  routeId: spec.routeId ?? "",
782
782
  privateJwk: ctx.keys.privateJwk,
@@ -786,7 +786,7 @@ function createPersonaClient(ctx) {
786
786
  });
787
787
  const headers = { ...signed.headers };
788
788
  if (!persona.anonymous) {
789
- const claim = buildContextClaim(persona, ctx.productId);
789
+ const claim = buildContextClaim(persona, ctx.businessId);
790
790
  headers["x-fs-context"] = await signContextToken(
791
791
  claim,
792
792
  ctx.contextSecret,
@@ -861,12 +861,12 @@ var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
861
861
  var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
862
862
  var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
863
863
  function createDevGateway(options) {
864
- const productId = options.productId ?? "prod_dev";
864
+ const businessId = options.businessId ?? "biz_dev";
865
865
  const backendId = options.backendId ?? "be_dev";
866
866
  const meterEvents = [];
867
867
  const reportUsageEvents = [];
868
868
  const bootstrap = {
869
- product: { id: productId, slug: options.productSlug ?? "dev-product" },
869
+ business: { id: businessId, slug: options.businessSlug ?? "dev-business" },
870
870
  backend: {
871
871
  id: backendId,
872
872
  slug: options.backendSlug ?? "dev-backend",
@@ -936,7 +936,7 @@ function createDevGateway(options) {
936
936
  bootstrap,
937
937
  meterEvents,
938
938
  reportUsageEvents,
939
- productId,
939
+ businessId,
940
940
  backendId,
941
941
  jwksUrl: DEV_JWKS_URL
942
942
  };
@@ -1136,7 +1136,7 @@ var DEFAULT_MAX_RETRIES = 3;
1136
1136
  var MeteringClient = class {
1137
1137
  config;
1138
1138
  endpoint;
1139
- productId;
1139
+ businessId;
1140
1140
  backendId;
1141
1141
  fetchImpl;
1142
1142
  maxRetries;
@@ -1150,7 +1150,7 @@ var MeteringClient = class {
1150
1150
  constructor(options) {
1151
1151
  this.config = options.config;
1152
1152
  this.endpoint = resolveEndpoint(options.config.endpoint, options.coreUrl);
1153
- this.productId = options.productId;
1153
+ this.businessId = options.businessId;
1154
1154
  this.backendId = options.backendId;
1155
1155
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
1156
1156
  this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -1213,13 +1213,14 @@ var MeteringClient = class {
1213
1213
  }
1214
1214
  const event = {
1215
1215
  event_id: options.eventId ?? this.newId(),
1216
- product_id: this.productId,
1216
+ business_id: this.businessId,
1217
1217
  backend_id: this.backendId,
1218
1218
  meter,
1219
1219
  qty,
1220
1220
  timestamp: options.timestamp ?? this.now().toISOString(),
1221
1221
  ...options.routeId ? { route_id: options.routeId } : {},
1222
- ...options.requestId ? { request_id: options.requestId } : {}
1222
+ ...options.requestId ? { request_id: options.requestId } : {},
1223
+ ...options.subscriptionId ? { subscription_id: options.subscriptionId } : {}
1223
1224
  };
1224
1225
  this.buffer.push(event);
1225
1226
  await this.flush();
@@ -1926,12 +1927,12 @@ async function verifyRequest(input, deps) {
1926
1927
  const kid = h(RUNTIME_HEADER_NAMES.keyId);
1927
1928
  const requestId = h(RUNTIME_HEADER_NAMES.requestId);
1928
1929
  const timestampRaw = h(RUNTIME_HEADER_NAMES.timestamp);
1929
- const signedProductId = h(RUNTIME_HEADER_NAMES.productId);
1930
+ const signedBusinessId = h(RUNTIME_HEADER_NAMES.businessId);
1930
1931
  const signedBackendId = h(RUNTIME_HEADER_NAMES.backendId);
1931
1932
  const signedRouteId = h(RUNTIME_HEADER_NAMES.routeId) ?? "";
1932
1933
  const policyVersion = h(RUNTIME_HEADER_NAMES.policyVersion);
1933
1934
  const signedBodyHash = h(RUNTIME_HEADER_NAMES.bodyHash);
1934
- if (!kid || !requestId || !timestampRaw || !signedProductId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
1935
+ if (!kid || !requestId || !timestampRaw || !signedBusinessId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
1935
1936
  throw new FartherShoreError(
1936
1937
  "malformed_signature",
1937
1938
  "request is missing one or more required x-fs-* headers"
@@ -1967,10 +1968,10 @@ async function verifyRequest(input, deps) {
1967
1968
  "recomputed body hash does not match the signed x-fs-body-hash"
1968
1969
  );
1969
1970
  }
1970
- if (deps.productId !== void 0 && signedProductId !== deps.productId) {
1971
+ if (deps.businessId !== void 0 && signedBusinessId !== deps.businessId) {
1971
1972
  throw new FartherShoreError(
1972
1973
  "route_mismatch",
1973
- "signed product-id does not match this backend's product"
1974
+ "signed business-id does not match this backend's business"
1974
1975
  );
1975
1976
  }
1976
1977
  if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
@@ -1992,7 +1993,7 @@ async function verifyRequest(input, deps) {
1992
1993
  bodyHash: computedBodyHash,
1993
1994
  requestId,
1994
1995
  timestamp,
1995
- productId: signedProductId,
1996
+ businessId: signedBusinessId,
1996
1997
  backendId: signedBackendId,
1997
1998
  routeId: signedRouteId,
1998
1999
  policyVersion
@@ -2032,10 +2033,10 @@ async function verifyRequest(input, deps) {
2032
2033
  if (signedContext === null && deps.contextVerification === "required") {
2033
2034
  throw contextRequiredError("failed verification");
2034
2035
  }
2035
- if (signedContext && signedContext.productId !== signedProductId) {
2036
+ if (signedContext && signedContext.productId !== signedBusinessId) {
2036
2037
  throw new FartherShoreError(
2037
2038
  "context_unverified",
2038
- "X-Fs-Context was minted for a different product than the signed request"
2039
+ "X-Fs-Context was minted for a different business than the signed request"
2039
2040
  );
2040
2041
  }
2041
2042
  } else if (deps.contextVerification === "required") {
@@ -2055,7 +2056,7 @@ async function verifyRequest(input, deps) {
2055
2056
  }
2056
2057
  return {
2057
2058
  requestId,
2058
- productId: signedProductId,
2059
+ businessId: signedBusinessId,
2059
2060
  backendId: signedBackendId,
2060
2061
  routeId: signedRouteId,
2061
2062
  policyVersion,
@@ -2098,8 +2099,8 @@ function headerGetter(headers) {
2098
2099
 
2099
2100
  // src/core/runtime.ts
2100
2101
  var DEFAULT_CORE_URL = "https://core.farthershore.com";
2101
- var SDK_VERSION = "0.14.0".length > 0 ? "0.14.0" : "0.0.0-dev";
2102
- var CONTRACTS_FP = "1feb5a4a80b447ad".length > 0 ? "1feb5a4a80b447ad" : "0000000000000000";
2102
+ var SDK_VERSION = "0.15.0".length > 0 ? "0.15.0" : "0.0.0-dev";
2103
+ var CONTRACTS_FP = "4b6a36b4cb1f0b68".length > 0 ? "4b6a36b4cb1f0b68" : "0000000000000000";
2103
2104
  var FartherShore = class {
2104
2105
  bootstrapClient;
2105
2106
  fetchImpl;
@@ -2168,7 +2169,7 @@ var FartherShore = class {
2168
2169
  if (!this.meteringClient && config.metering.enabled) {
2169
2170
  this.meteringClient = new MeteringClient({
2170
2171
  config: config.metering,
2171
- productId: config.product.id,
2172
+ businessId: config.business.id,
2172
2173
  backendId: config.backend.id,
2173
2174
  coreUrl: this.coreUrl,
2174
2175
  fetchImpl: this.fetchImpl
@@ -2252,7 +2253,7 @@ var FartherShore = class {
2252
2253
  const context = await verifyRequest(input, {
2253
2254
  jwks: this.jwks,
2254
2255
  nonceCache: this.nonceCache,
2255
- productId: config.product.id,
2256
+ businessId: config.business.id,
2256
2257
  backendId: config.backend.id,
2257
2258
  knownRouteIds,
2258
2259
  clockSkewSeconds: config.verification.clockSkewSeconds,
@@ -2623,7 +2624,7 @@ function personaClientFromKeysFile(path = DEFAULT_KEYS_FILE, options = {}) {
2623
2624
  const file = readDevKeysFile(path);
2624
2625
  const client = createPersonaClient({
2625
2626
  keys: file.keys,
2626
- productId: file.productId,
2627
+ businessId: file.businessId,
2627
2628
  backendId: file.backendId,
2628
2629
  contextSecret: file.keys.contextSecret,
2629
2630
  contextKid: file.keys.contextKid,
@@ -2650,7 +2651,7 @@ function createDevRuntime(options) {
2650
2651
  const gateway = createDevGateway({
2651
2652
  mode,
2652
2653
  keys,
2653
- ...options.productId ? { productId: options.productId } : {},
2654
+ ...options.businessId ? { businessId: options.businessId } : {},
2654
2655
  ...options.backendId ? { backendId: options.backendId } : {},
2655
2656
  ...options.routes ? { routeIds: options.routes } : {},
2656
2657
  onMeterEvent: (event) => {
@@ -2679,7 +2680,7 @@ function createDevRuntime(options) {
2679
2680
  const personas = buildPersonaMap(options.personas);
2680
2681
  const personaClient = createPersonaClient({
2681
2682
  keys,
2682
- productId: gateway.productId,
2683
+ businessId: gateway.businessId,
2683
2684
  backendId: gateway.backendId,
2684
2685
  contextSecret: keys.contextSecret,
2685
2686
  contextKid: keys.contextKid,
@@ -2809,7 +2810,7 @@ function createDevRuntimeFromEnv(env = readProcessEnv3()) {
2809
2810
  version: 1,
2810
2811
  mode,
2811
2812
  keys,
2812
- productId: runtime.gateway.productId,
2813
+ businessId: runtime.gateway.businessId,
2813
2814
  backendId: runtime.gateway.backendId,
2814
2815
  personas: mapToRecord(runtime.personas)
2815
2816
  };
@@ -2822,7 +2823,7 @@ function printBanner(mode, runtime, tracePath) {
2822
2823
  "============================================================",
2823
2824
  " \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
2824
2825
  ` mode: ${mode.toUpperCase()}`,
2825
- ` product: ${runtime.gateway.productId}`,
2826
+ ` business: ${runtime.gateway.businessId}`,
2826
2827
  ` backend: ${runtime.gateway.backendId}`,
2827
2828
  ` personas: ${[...runtime.personas.keys()].join(", ")}`,
2828
2829
  ` usage log: ${USAGE_JSONL_PATH}`,
@@ -2,6 +2,12 @@ import type { RuntimeMeteringConfig } from "../runtime-types.js";
2
2
  export type MeterOptions = {
3
3
  requestId?: string;
4
4
  routeId?: string;
5
+ /** Subscription to attribute the usage to (billing identity). Pass the
6
+ * verified request context's `signedContext.subscriptionId` when metering
7
+ * inside a request handler. Without it (and without `requestId`, which core
8
+ * can resolve back to the served gateway request), core persists the event
9
+ * UNBILLED and flags it unattributable. */
10
+ subscriptionId?: string;
5
11
  /** Override event_id (idempotency key). Defaults to a random uuid. */
6
12
  eventId?: string;
7
13
  /** Override the timestamp (ISO-8601). Defaults to now. */
@@ -9,7 +15,7 @@ export type MeterOptions = {
9
15
  };
10
16
  export type MeteringClientOptions = {
11
17
  config: RuntimeMeteringConfig;
12
- productId: string;
18
+ businessId: string;
13
19
  backendId: string;
14
20
  /** Core base URL when the config endpoint is a relative path. */
15
21
  coreUrl?: string;
@@ -36,7 +42,7 @@ export type MeteringClientOptions = {
36
42
  export declare class MeteringClient {
37
43
  private readonly config;
38
44
  private readonly endpoint;
39
- private readonly productId;
45
+ private readonly businessId;
40
46
  private readonly backendId;
41
47
  private readonly fetchImpl;
42
48
  private readonly maxRetries;
@@ -45,7 +45,7 @@ export type FartherShoreInitOptions = {
45
45
  * `X-Fs-Context` claim. These are the GATEWAY CONTEXT-SIGNING keyring values
46
46
  * (`CONTEXT_SIGNING_KEYS_JSON` — the keys `forward-upstream` stamps the
47
47
  * header with; supply every live key during rotation — try-all). They are
48
- * NOT the product's `contextTokenSecret`, which signs `fsc_` INGRESS tokens
48
+ * NOT the business's `contextTokenSecret`, which signs `fsc_` INGRESS tokens
49
49
  * verified BY the gateway — setting that here would reject every valid
50
50
  * gateway request in `"required"` mode. When present, a VERIFIED context's
51
51
  * `permissions`/`roles` claims are the AUTHORITATIVE identity source
@@ -54,8 +54,8 @@ export type FartherShoreInitOptions = {
54
54
  *
55
55
  * NOTE (core-side dependency, FAR-723 publish gate): no bootstrap field
56
56
  * carries these keys yet, and handing the raw platform keyring to builder
57
- * backends is NOT the end-state (it would allow cross-product context
58
- * forgery). The distribution mechanism — per-product derived keys or an
57
+ * backends is NOT the end-state (it would allow cross-business context
58
+ * forgery). The distribution mechanism — per-business derived keys or an
59
59
  * asymmetric context signature verified via JWKS like the request
60
60
  * signature — is decided at the FAR-723 gate before the headers retire;
61
61
  * until then this option (or `FS_CONTEXT_SECRETS`) is the manual wiring for
@@ -23,7 +23,7 @@ export type HeadersLike = Headers | Record<string, string | string[] | undefined
23
23
  /** The verified request context attached to req.fartherShore. */
24
24
  export type FartherShoreRequestContext = {
25
25
  requestId: string;
26
- productId: string;
26
+ businessId: string;
27
27
  backendId: string;
28
28
  routeId: string;
29
29
  policyVersion: string;
@@ -55,8 +55,8 @@ export type FartherShoreRequestContext = {
55
55
  export type VerifyRequestDeps = {
56
56
  jwks: JwksClient;
57
57
  nonceCache: NonceCache;
58
- /** Expected product id (from bootstrap). When set, must match the signed claim. */
59
- productId?: string;
58
+ /** Expected business id (from bootstrap). When set, must match the signed claim. */
59
+ businessId?: string;
60
60
  /** Expected backend id (from bootstrap). When set, must match. */
61
61
  backendId?: string;
62
62
  /**
@@ -73,7 +73,7 @@ export declare const RUNTIME_SIGNING_CONTRACT: {
73
73
  readonly keyValueSeparator: ":";
74
74
  readonly trailingNewline: false;
75
75
  readonly fieldEncoding: "utf-8";
76
- readonly fields: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "product-id", "backend-id", "route-id", "policy-version"];
76
+ readonly fields: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version"];
77
77
  readonly fieldRules: {
78
78
  readonly method: "Uppercased HTTP method (e.g. GET, POST).";
79
79
  readonly path: "Request path, percent-encoded as received, no host, no query string. Always begins with '/'.";
@@ -81,7 +81,7 @@ export declare const RUNTIME_SIGNING_CONTRACT: {
81
81
  readonly "body-hash": "Lowercase hex SHA-256 of the RAW request body bytes. For an empty body, the SHA-256 of zero bytes (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Streaming-exempt requests use the literal token 'STREAM'.";
82
82
  readonly "request-id": "Opaque unique request id minted by the gateway (also the replay-cache nonce).";
83
83
  readonly timestamp: "Integer Unix epoch seconds (UTC) at signing time, as a base-10 string with no padding.";
84
- readonly "product-id": "Product id the request is routed to.";
84
+ readonly "business-id": "Business id the request is routed to.";
85
85
  readonly "backend-id": "Backend id the route binds to.";
86
86
  readonly "route-id": "Resolved route id; empty string if the route is unresolved.";
87
87
  readonly "policy-version": "Tenant artifact / policy version the gateway signed under.";
@@ -92,7 +92,7 @@ export declare const RUNTIME_SIGNING_CONTRACT: {
92
92
  * Ordered list of fields in the canonical signing string. The order here is
93
93
  * load-bearing and identical across all language SDKs.
94
94
  */
95
- export declare const RUNTIME_CANONICAL_FIELDS: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "product-id", "backend-id", "route-id", "policy-version"];
95
+ export declare const RUNTIME_CANONICAL_FIELDS: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version"];
96
96
  export declare const RUNTIME_BODY_HASH_CONTRACT: {
97
97
  readonly algorithm: "SHA-256";
98
98
  readonly encoding: "hex-lower";
@@ -108,7 +108,7 @@ export declare const RUNTIME_HEADERS: {
108
108
  readonly keyId: "x-fs-key-id";
109
109
  readonly requestId: "x-fs-request-id";
110
110
  readonly timestamp: "x-fs-timestamp";
111
- readonly productId: "x-fs-product-id";
111
+ readonly businessId: "x-fs-business-id";
112
112
  readonly backendId: "x-fs-backend-id";
113
113
  readonly routeId: "x-fs-route-id";
114
114
  readonly policyVersion: "x-fs-policy-version";
@@ -145,7 +145,7 @@ export declare const RUNTIME_METERING_CONTRACT: {
145
145
  readonly credential: "reusable-bearer";
146
146
  readonly event: {
147
147
  readonly event_id: "string";
148
- readonly product_id: "string";
148
+ readonly business_id: "string";
149
149
  readonly backend_id: "string";
150
150
  readonly route_id: "string?";
151
151
  readonly request_id: "string?";
@@ -126,7 +126,7 @@ export declare const RUNTIME_HEADER_NAMES: {
126
126
  readonly keyId: "x-fs-key-id";
127
127
  readonly requestId: "x-fs-request-id";
128
128
  readonly timestamp: "x-fs-timestamp";
129
- readonly productId: "x-fs-product-id";
129
+ readonly businessId: "x-fs-business-id";
130
130
  readonly backendId: "x-fs-backend-id";
131
131
  readonly routeId: "x-fs-route-id";
132
132
  readonly policyVersion: "x-fs-policy-version";
@@ -166,7 +166,7 @@ export type CanonicalSigningInput = {
166
166
  requestId: string;
167
167
  /** Unix epoch seconds at signing time. */
168
168
  timestamp: number;
169
- productId: string;
169
+ businessId: string;
170
170
  backendId: string;
171
171
  /** Resolved route id; "" when unresolved. */
172
172
  routeId: string;
@@ -221,7 +221,7 @@ export type RuntimeLockDescriptor = {
221
221
  lockVersion: number;
222
222
  };
223
223
  export type RuntimeBootstrapResponse = {
224
- product: {
224
+ business: {
225
225
  id: string;
226
226
  slug: string;
227
227
  };
@@ -247,10 +247,15 @@ export type RuntimeBootstrapResponse = {
247
247
  };
248
248
  export type RuntimeMeteringEvent = {
249
249
  event_id: string;
250
- product_id: string;
250
+ business_id: string;
251
251
  backend_id: string;
252
252
  route_id?: string;
253
253
  request_id?: string;
254
+ /** Subscription the usage belongs to (billing attribution). Optional and
255
+ * additive: legacy emitters omit it; core then falls back to resolving the
256
+ * served gateway request via `request_id`, and rows that resolve neither
257
+ * way are persisted unbilled + flagged unattributable. */
258
+ subscription_id?: string;
254
259
  meter: string;
255
260
  qty: number;
256
261
  timestamp: string;
@@ -7,9 +7,9 @@ export declare const DEV_METERING_ENDPOINT = "https://dev-gateway.farthershore.l
7
7
  export type DevGatewayOptions = {
8
8
  mode: DevMode;
9
9
  keys: DevSignerKeys;
10
- productId?: string;
10
+ businessId?: string;
11
11
  backendId?: string;
12
- productSlug?: string;
12
+ businessSlug?: string;
13
13
  backendSlug?: string;
14
14
  /** Extra route ids to expose in bootstrap for route-binding tests. */
15
15
  routeIds?: string[];
@@ -27,7 +27,7 @@ export type DevGateway = {
27
27
  meterEvents: RuntimeMeteringEvent[];
28
28
  /** Every captured attested post-stream usage report. */
29
29
  reportUsageEvents: RuntimePostStreamUsageEvent[];
30
- productId: string;
30
+ businessId: string;
31
31
  backendId: string;
32
32
  jwksUrl: string;
33
33
  };
@@ -24,7 +24,7 @@ export type CreateDevRuntimeOptions = {
24
24
  routes?: string[];
25
25
  /** Meter keys (informational — the dev gateway accepts any meter). */
26
26
  meters?: string[];
27
- productId?: string;
27
+ businessId?: string;
28
28
  backendId?: string;
29
29
  /** Optional app transport for persona `.fetch()`; defaults to global fetch. */
30
30
  appFetch?: typeof fetch;
@@ -7,7 +7,7 @@ export type DevKeysFile = {
7
7
  version: 1;
8
8
  mode: DevMode;
9
9
  keys: DevSignerKeys;
10
- productId: string;
10
+ businessId: string;
11
11
  backendId: string;
12
12
  /** Persona definitions in effect for this dev session. */
13
13
  personas: Record<string, PersonaDefinition>;
@@ -53,7 +53,7 @@ export type PersonaRequestSpec = {
53
53
  /** The context a persona client signs against (bootstrap ids + dev keys). */
54
54
  export type PersonaClientContext = {
55
55
  keys: DevSignerKeys;
56
- productId: string;
56
+ businessId: string;
57
57
  backendId: string;
58
58
  contextSecret: string;
59
59
  contextKid: string;
@@ -96,7 +96,7 @@ export declare const SIGNED_HEADER_NAMES: {
96
96
  readonly keyId: "x-fs-key-id";
97
97
  readonly requestId: "x-fs-request-id";
98
98
  readonly timestamp: "x-fs-timestamp";
99
- readonly productId: "x-fs-product-id";
99
+ readonly businessId: "x-fs-business-id";
100
100
  readonly backendId: "x-fs-backend-id";
101
101
  readonly routeId: "x-fs-route-id";
102
102
  readonly policyVersion: "x-fs-policy-version";
@@ -13,7 +13,7 @@ export type SignedHeaderOverrides = Partial<{
13
13
  kid: string;
14
14
  requestId: string;
15
15
  timestamp: number;
16
- productId: string;
16
+ businessId: string;
17
17
  backendId: string;
18
18
  routeId: string;
19
19
  policyVersion: string;
@@ -25,7 +25,7 @@ export type SignedRequestSpec = {
25
25
  query?: string;
26
26
  body?: Uint8Array | null;
27
27
  streamingExempt?: boolean;
28
- productId?: string;
28
+ businessId?: string;
29
29
  backendId?: string;
30
30
  routeId?: string;
31
31
  policyVersion?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farthershore/backend",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Farther Shore backend SDK for builder upstreams: signed response usage, fail-closed gateway request verification, health, and lifecycle from FS_RUNTIME_TOKEN",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -37,8 +37,8 @@
37
37
  },
38
38
  "optionalDependencies": {
39
39
  "@farthershore/cloudflared-linux-x64": "0.0.0",
40
- "@farthershore/cloudflared-linux-arm64": "0.0.0",
41
40
  "@farthershore/cloudflared-darwin-arm64": "0.0.0",
41
+ "@farthershore/cloudflared-linux-arm64": "0.0.0",
42
42
  "@farthershore/cloudflared-darwin-x64": "0.0.0"
43
43
  },
44
44
  "peerDependencies": {