@farthershore/backend 0.13.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.
@@ -1,3 +1,4 @@
1
+ import type { ReportUsageResult, RequestScopedReportUsageInput } from "./post-stream-usage.js";
1
2
  import { type FartherShoreSignedContext } from "./verifyContext.js";
2
3
  import type { JwksClient } from "./jwks.js";
3
4
  import type { NonceCache } from "./nonceCache.js";
@@ -22,7 +23,7 @@ export type HeadersLike = Headers | Record<string, string | string[] | undefined
22
23
  /** The verified request context attached to req.fartherShore. */
23
24
  export type FartherShoreRequestContext = {
24
25
  requestId: string;
25
- productId: string;
26
+ businessId: string;
26
27
  backendId: string;
27
28
  routeId: string;
28
29
  policyVersion: string;
@@ -48,12 +49,14 @@ export type FartherShoreRequestContext = {
48
49
  signedContext?: FartherShoreSignedContext;
49
50
  /** Managed-RBAC role keys the acting user holds (display/audit only). */
50
51
  roles?: string[];
52
+ /** Request-bound post-stream reporter, attached by the runtime facade. */
53
+ reportUsage?: (input: RequestScopedReportUsageInput) => Promise<ReportUsageResult>;
51
54
  };
52
55
  export type VerifyRequestDeps = {
53
56
  jwks: JwksClient;
54
57
  nonceCache: NonceCache;
55
- /** Expected product id (from bootstrap). When set, must match the signed claim. */
56
- productId?: string;
58
+ /** Expected business id (from bootstrap). When set, must match the signed claim. */
59
+ businessId?: string;
57
60
  /** Expected backend id (from bootstrap). When set, must match. */
58
61
  backendId?: string;
59
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,18 +145,33 @@ 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?";
152
+ readonly requestId: "string?";
153
+ readonly subscriptionId: "string";
154
+ readonly nonce: "string?";
152
155
  readonly meter: "string";
153
156
  readonly qty: "number";
154
157
  readonly timestamp: "string";
155
158
  };
159
+ readonly postStreamEvent: {
160
+ readonly requestId: "string";
161
+ readonly subscriptionId: "string?";
162
+ readonly nonce: "string";
163
+ readonly meters: "Record<string, number>";
164
+ readonly creditUnitsConsumed: "Record<string, number>?";
165
+ readonly measureContext: "Record<string, unknown>?";
166
+ readonly signature: "string";
167
+ };
156
168
  readonly idempotencyKey: "event_id";
157
169
  readonly delivery: "at-least-once";
158
170
  readonly billingOnly: true;
159
171
  readonly realtimeEnforced: false;
172
+ readonly postStreamBillingOnly: true;
173
+ readonly postStreamRealtimeEnforced: false;
174
+ readonly postStreamTrustModel: "HMAC-attested and bound to one served postStreamBilling gateway request. Core writes one billable UsageEvent using the served plan and time. The callback never mutates Durable Object enforcement windows.";
160
175
  readonly trustModel: "upstream-reported values are NOT cryptographically attested; a buggy or compromised upstream can self-report arbitrary values for its OWN product only. Core enforces allowedMeters/allowedRoutes from the authoritative token record at ingest, applies a per-event sanity max (perEventMax), and raises an implausible-volume alert.";
161
176
  };
162
177
  export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
@@ -11,6 +11,7 @@ export { JwksClient, type Jwk, type JwksClientOptions } from "./core/jwks.js";
11
11
  export { NonceCache, type NonceCacheOptions } from "./core/nonceCache.js";
12
12
  export { BootstrapClient, type BootstrapClientOptions, } from "./core/bootstrap.js";
13
13
  export { MeteringClient, type MeteringClientOptions, type MeterOptions, } from "./core/metering.js";
14
+ export { PostStreamUsageClient, type PostStreamUsageClientOptions, type ReportUsageInput, type RequestScopedReportUsageInput, type ReportUsageResult, } from "./core/post-stream-usage.js";
14
15
  export { buildHealthReport, reportHealth, type HealthSnapshot, type HealthStatus, type HeartbeatOptions, } from "./core/health.js";
15
16
  export { ShutdownManager, type ShutdownHook } from "./core/shutdown.js";
16
17
  export { CloudflaredSupervisor, nodeSpawn, REDACTED_TOKEN, type SpawnFn, type SpawnedTunnelProcess, type CloudflaredSupervisorOptions, type TunnelState, type TunnelStatus, } from "./core/tunnel.js";
@@ -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,14 +247,29 @@ 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;
257
262
  };
263
+ /** Attested, request-bound billing callback sent after a stream completes. */
264
+ export type RuntimePostStreamUsageEvent = {
265
+ requestId: string;
266
+ subscriptionId: string;
267
+ nonce: string;
268
+ meters: Record<string, number>;
269
+ creditUnitsConsumed?: Record<string, number>;
270
+ measureContext?: Record<string, unknown>;
271
+ signature: string;
272
+ };
258
273
  export declare const RUNTIME_READINESS_STATES: readonly ["UNKNOWN", "WAITING", "READY", "DEGRADED", "OFFLINE"];
259
274
  export type RuntimeReadinessState = (typeof RUNTIME_READINESS_STATES)[number];
260
275
  export type RuntimeHealthReport = {
@@ -1,4 +1,4 @@
1
- import { type RuntimeBootstrapResponse, type RuntimeMeteringEvent } from "../runtime-types.js";
1
+ import { type RuntimeBootstrapResponse, type RuntimeMeteringEvent, type RuntimePostStreamUsageEvent } from "../runtime-types.js";
2
2
  import type { DevMode } from "./traceSink.js";
3
3
  import type { DevSignerKeys } from "./signers.js";
4
4
  export declare const DEV_CORE_URL = "https://dev-gateway.farthershore.local";
@@ -7,14 +7,16 @@ 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[];
16
16
  /** Called for each captured metering event (at-least-once ACK). */
17
17
  onMeterEvent?: (event: RuntimeMeteringEvent) => void;
18
+ /** Called for each captured attested post-stream report. */
19
+ onReportUsage?: (event: RuntimePostStreamUsageEvent) => void;
18
20
  };
19
21
  export type DevGateway = {
20
22
  /** Inject this as `fetchImpl` when constructing the FartherShore runtime. */
@@ -23,7 +25,9 @@ export type DevGateway = {
23
25
  bootstrap: RuntimeBootstrapResponse;
24
26
  /** Every captured background metering event (a capture == an ACK). */
25
27
  meterEvents: RuntimeMeteringEvent[];
26
- productId: string;
28
+ /** Every captured attested post-stream usage report. */
29
+ reportUsageEvents: RuntimePostStreamUsageEvent[];
30
+ businessId: string;
27
31
  backendId: string;
28
32
  jwksUrl: string;
29
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;
@@ -1,4 +1,4 @@
1
- import type { RuntimeMeteringEvent } from "../runtime-types.js";
1
+ import type { RuntimeMeteringEvent, RuntimePostStreamUsageEvent } from "../runtime-types.js";
2
2
  /** One recorded usage observation. `source` distinguishes the two channels. */
3
3
  export type DevUsageEvent = {
4
4
  source: "response";
@@ -16,6 +16,12 @@ export type DevUsageEvent = {
16
16
  event: RuntimeMeteringEvent;
17
17
  requestId?: string;
18
18
  at: number;
19
+ } | {
20
+ source: "reportUsage";
21
+ meters: Record<string, number>;
22
+ event: RuntimePostStreamUsageEvent;
23
+ requestId: string;
24
+ at: number;
19
25
  };
20
26
  /** In-memory, assertable sink for all dev usage. */
21
27
  export declare class DevUsageSink {
@@ -24,6 +30,8 @@ export declare class DevUsageSink {
24
30
  recordResponse(payload: Record<string, unknown>, requestId?: string): void;
25
31
  /** Record a background `fs.meter()` event captured by the dev gateway. */
26
32
  recordMeterEvent(event: RuntimeMeteringEvent): void;
33
+ /** Record an attested post-stream report captured by the dev gateway. */
34
+ recordReportUsage(event: RuntimePostStreamUsageEvent): void;
27
35
  /** Total quantity per meter key across every recorded event. */
28
36
  byMeter(): Record<string, number>;
29
37
  /** Clear all recorded usage. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farthershore/backend",
3
- "version": "0.13.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",