@farthershore/backend 0.19.0 → 0.21.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,4 +1,4 @@
1
- import type { ReportUsageResult, RequestScopedReportUsageInput } from "./post-stream-usage.js";
1
+ import { type ReportFn } from "./report.js";
2
2
  import { type ConsumerPrincipal, type FartherShoreSignedContext } from "./verifyContext.js";
3
3
  import type { JwksClient } from "./jwks.js";
4
4
  import type { NonceStore } from "./nonceCache.js";
@@ -60,9 +60,27 @@ export type FartherShoreRequestContext = {
60
60
  signedContext?: FartherShoreSignedContext;
61
61
  /** Managed-RBAC role keys the acting subject holds (display/audit only). */
62
62
  roles?: string[];
63
- /** Request-bound post-stream reporter, attached by the runtime facade. */
64
- reportUsage?: (input: RequestScopedReportUsageInput) => Promise<ReportUsageResult>;
63
+ /**
64
+ * THE reporting verb. `report({ meter, values, dims?, quote? })` reports
65
+ * MEASUREMENTS against the served identity this context already carries —
66
+ * there is no subscription/release argument to forget. The SDK picks the
67
+ * transport (signed in-band headers before the response is sent, the attested
68
+ * post-stream channel after it, or from a background job), so the builder
69
+ * never chooses one.
70
+ *
71
+ * Attached by the runtime facade. A context produced by the BARE
72
+ * `verifyRequest()` primitive has no metering channel, so its `report()`
73
+ * throws an error naming the fix rather than dropping the measurement.
74
+ */
75
+ report: ReportFn;
65
76
  };
77
+ /**
78
+ * The verified Farther Shore context — the identity + reporting handle a
79
+ * handler (or a background job the handler hands it to) works with. Alias of
80
+ * {@link FartherShoreRequestContext}: a background job is simply given the
81
+ * already-verified context, so there is no second context type to learn.
82
+ */
83
+ export type FartherShoreContext = FartherShoreRequestContext;
66
84
  export type VerifyRequestDeps = {
67
85
  jwks: JwksClient;
68
86
  nonceCache: NonceStore;
@@ -1,127 +1,3 @@
1
- export declare const RUNTIME_CONTRACT_VERSION: 1;
2
- export declare const RUNTIME_TOKEN_ENV: "FS_RUNTIME_TOKEN";
3
- export declare const RUNTIME_TOKEN_CONTRACT: {
4
- readonly environmentVariable: "FS_RUNTIME_TOKEN";
5
- readonly prefixes: {
6
- readonly live: "fsrt_live_";
7
- readonly test: "fsrt_test_";
8
- };
9
- readonly opaque: true;
10
- readonly storage: "sha256-hash-only";
11
- readonly lastFour: true;
12
- readonly capabilities: readonly ["gateway_verification", "metering", "health", "tunnel"];
13
- };
14
- export declare const RUNTIME_BOOTSTRAP_CONTRACT: {
15
- readonly method: "POST";
16
- readonly path: "/v1/runtime/bootstrap";
17
- readonly authorization: "Bearer fsrt_...";
18
- readonly request: {
19
- readonly instanceId: "string?";
20
- readonly sdkVersion: "string?";
21
- readonly sdkLanguage: "string?";
22
- };
23
- readonly response: {
24
- readonly product: {
25
- readonly id: "string";
26
- readonly slug: "string";
27
- };
28
- readonly backend: {
29
- readonly id: "string";
30
- readonly slug: "string";
31
- readonly name: "string";
32
- };
33
- readonly environment: {
34
- readonly id: "string?";
35
- readonly kind: "live | test";
36
- };
37
- readonly capabilities: "string[]";
38
- readonly verification: {
39
- readonly required: "boolean";
40
- readonly jwksUrl: "string";
41
- readonly clockSkewSeconds: "number";
42
- readonly replayWindowSeconds: "number";
43
- readonly headerNames: "object";
44
- };
45
- readonly metering: {
46
- readonly enabled: "boolean";
47
- readonly endpoint: "string";
48
- readonly credential: "string";
49
- readonly allowedMeters: "string[]";
50
- readonly allowedRoutes: "string[]";
51
- readonly perEventMax: "number";
52
- };
53
- readonly transport: {
54
- readonly mode: "direct | tunnel";
55
- readonly runner: "embedded | sidecar | null";
56
- readonly originUrl: "string?";
57
- readonly originHostname: "string?";
58
- readonly localTarget: "string?";
59
- readonly cloudflared: "object?";
60
- };
61
- readonly routes: "object[]";
62
- readonly policyVersion: "string";
63
- readonly refreshAfterSeconds: "number";
64
- };
65
- };
66
- export declare const RUNTIME_SIGNING_CONTRACT: {
67
- readonly algorithm: "Ed25519";
68
- readonly encoding: "base64url";
69
- readonly keyMaterial: "service-jwt-jwks";
70
- readonly canonicalString: {
71
- readonly description: "Byte-exact, language-neutral serialization of the signed claim set. Fields are emitted in the FIXED order below, one per line, each as `name:value`, joined by a single newline (\\n, U+000A). NO trailing newline. The serialization depends on neither JSON key ordering nor any Node.js Buffer/serialization detail — only UTF-8 byte encoding of the field values. Empty/absent values are emitted as the empty string after the colon. Go/Python/Java/Rust reproduce this identically.";
72
- readonly fieldSeparator: "\n";
73
- readonly keyValueSeparator: ":";
74
- readonly trailingNewline: false;
75
- readonly fieldEncoding: "utf-8";
76
- readonly fields: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version", "context-hash"];
77
- readonly fieldRules: {
78
- readonly method: "Uppercased HTTP method (e.g. GET, POST).";
79
- readonly path: "Request path, percent-encoded as received, no host, no query string. Always begins with '/'.";
80
- readonly query: "The request's query string bytes VERBATIM, after the caller has stripped the URL's single leading '?' delimiter. Use the RAW wire query exactly as received — NO sorting, NO filtering, NO re-stripping a leading '?', NO dropping empty pairs, NO re-encoding. The signature binds to the exact query bytes: any reorder, added/removed/relocated pair, or re-encoding changes the bytes and MUST fail verification. Every SDK verifier MUST read the raw wire query (never a re-parsed/re-serialized form, which could reorder or re-encode and would then fail legitimate requests). Any normalization (sorting by name or value, dropping/moving empty pairs, re-stripping '?') is FORBIDDEN — each collapses distinct wire queries under one signature. Empty string when there is no query.";
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
- readonly "request-id": "Opaque unique request id minted by the gateway (also the replay-cache nonce).";
83
- readonly timestamp: "Integer Unix epoch seconds (UTC) at signing time, as a base-10 string with no padding.";
84
- readonly "business-id": "Business id the request is routed to.";
85
- readonly "backend-id": "Backend id the route binds to.";
86
- readonly "route-id": "Resolved route id; empty string if the route is unresolved.";
87
- readonly "policy-version": "Tenant artifact / policy version the gateway signed under.";
88
- readonly "context-hash": "Lowercase hex SHA-256 of the presented X-Fs-Context JWT string (identity-context binding). For a request with no signed context, the SHA-256 of the empty string (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Binds the identity context into the request signature so one verification covers both.";
89
- };
90
- };
91
- };
92
- /**
93
- * Ordered list of fields in the canonical signing string. The order here is
94
- * load-bearing and identical across all language SDKs.
95
- */
96
- export declare const RUNTIME_CANONICAL_FIELDS: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version", "context-hash"];
97
- export declare const RUNTIME_BODY_HASH_CONTRACT: {
98
- readonly algorithm: "SHA-256";
99
- readonly encoding: "hex-lower";
100
- readonly source: "raw-request-bytes";
101
- readonly emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
102
- readonly maxBodyBytes: 10485760;
103
- readonly streamingExemptToken: "STREAM";
104
- readonly streamingExemptContentTypes: readonly ["text/event-stream", "application/octet-stream", "multipart/form-data"];
105
- readonly overMaxStatus: 413;
106
- };
107
- export declare const RUNTIME_HEADERS: {
108
- readonly signature: "x-fs-signature";
109
- readonly keyId: "x-fs-key-id";
110
- readonly requestId: "x-fs-request-id";
111
- readonly timestamp: "x-fs-timestamp";
112
- readonly businessId: "x-fs-business-id";
113
- readonly backendId: "x-fs-backend-id";
114
- readonly routeId: "x-fs-route-id";
115
- readonly policyVersion: "x-fs-policy-version";
116
- readonly bodyHash: "x-fs-body-hash";
117
- };
118
- export declare const RUNTIME_REPLAY_CONTRACT: {
119
- readonly windowSeconds: 300;
120
- readonly clockSkewSeconds: 5;
121
- readonly nonce: "x-fs-request-id";
122
- readonly nonceCache: "bounded-lru";
123
- readonly policy: "fail-closed";
124
- };
125
1
  export declare const RUNTIME_ERROR_CODES: {
126
2
  readonly missingSignature: "missing_signature";
127
3
  readonly malformedSignature: "malformed_signature";
@@ -143,40 +19,15 @@ export declare const RUNTIME_ERROR_CODES: {
143
19
  readonly surfaceNotAllowed: "surface_not_allowed";
144
20
  };
145
21
  export type RuntimeErrorCode = (typeof RUNTIME_ERROR_CODES)[keyof typeof RUNTIME_ERROR_CODES];
146
- export declare const RUNTIME_METERING_CONTRACT: {
147
- readonly endpoint: "/v1/metering/events";
148
- readonly method: "POST";
149
- readonly credential: "reusable-bearer";
150
- readonly event: {
151
- readonly event_id: "string";
152
- readonly business_id: "string";
153
- readonly backend_id: "string";
154
- readonly route_id: "string?";
155
- readonly request_id: "string?";
156
- readonly requestId: "string?";
157
- readonly subscriptionId: "string";
158
- readonly nonce: "string?";
159
- readonly meter: "string";
160
- readonly qty: "number";
161
- readonly timestamp: "string";
162
- };
163
- readonly postStreamEvent: {
164
- readonly requestId: "string";
165
- readonly subscriptionId: "string?";
166
- readonly nonce: "string";
167
- readonly meters: "Record<string, number>";
168
- readonly creditUnitsConsumed: "Record<string, number>?";
169
- readonly measureContext: "Record<string, unknown>?";
170
- readonly signature: "string";
171
- };
172
- readonly idempotencyKey: "event_id";
173
- readonly delivery: "at-least-once";
174
- readonly billingOnly: true;
175
- readonly realtimeEnforced: false;
176
- readonly postStreamBillingOnly: true;
177
- readonly postStreamRealtimeEnforced: false;
178
- 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.";
179
- 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.";
22
+ export declare const RUNTIME_BODY_HASH_CONTRACT: {
23
+ readonly algorithm: "SHA-256";
24
+ readonly encoding: "hex-lower";
25
+ readonly source: "raw-request-bytes";
26
+ readonly emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
27
+ readonly maxBodyBytes: 10485760;
28
+ readonly streamingExemptToken: "STREAM";
29
+ readonly streamingExemptContentTypes: readonly ["text/event-stream", "application/octet-stream", "multipart/form-data"];
30
+ readonly overMaxStatus: 413;
180
31
  };
181
32
  export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
182
33
  readonly headers: {
@@ -198,14 +49,18 @@ export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
198
49
  readonly payload: {
199
50
  readonly method: "string";
200
51
  readonly path: "string";
201
- readonly rawDimsUnits: "Record<string, number>";
52
+ readonly rawDimsUnits: "Record<string, number>?";
202
53
  readonly measureContext: "Record<string, unknown>?";
203
54
  readonly creditUnitsConsumed: "Record<string, number>?";
55
+ readonly measurementsVersion: "1?";
56
+ readonly measurements: "Array<{ meter: string; values: Record<string, number>; dims?: Record<string, string> }>?";
57
+ readonly quote: "{ currency: string; amountNanos: string }?";
204
58
  };
205
59
  readonly errors: {
206
60
  readonly missingToken: "missing_token";
207
61
  readonly invalidMeterKey: "invalid_meter_key";
208
62
  readonly invalidMeterValue: "invalid_meter_value";
63
+ readonly invalidQuote: "invalid_quote";
209
64
  };
210
65
  readonly httpAdapter: {
211
66
  readonly input: "Request";
@@ -215,33 +70,3 @@ export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
215
70
  readonly gatewayStripsInternalHeaders: true;
216
71
  };
217
72
  };
218
- export declare const RUNTIME_HEALTH_CONTRACT: {
219
- readonly endpoint: "/v1/runtime/health";
220
- readonly method: "POST";
221
- readonly request: {
222
- readonly instanceId: "string?";
223
- readonly status: "starting | ready | degraded | stopping";
224
- };
225
- readonly readinessStates: readonly ["UNKNOWN", "WAITING", "READY", "DEGRADED", "OFFLINE"];
226
- readonly checks: readonly ["runtime_token_valid", "bootstrapped", "tunnel_running", "signed_request_2xx", "unsigned_request_401", "stale_signature_401", "wrong_route_401", "metering_observed"];
227
- readonly report: {
228
- readonly runtimeToken: "boolean";
229
- readonly bootstrap: "boolean";
230
- readonly tunnel: "string?";
231
- readonly verification: "boolean";
232
- readonly metering: "boolean";
233
- };
234
- };
235
- export declare const RUNTIME_TRANSPORT_CONTRACT: {
236
- readonly modes: {
237
- readonly direct: "Gateway fetches the builder's public origin URL; the SDK middleware fail-closed-verifies every request via Ed25519 request signing. Provisions zero Cloudflare objects. Available on all tiers; also the dev path.";
238
- readonly tunnel: "Farther Shore provisions a private outbound Cloudflare Tunnel; no inbound port. The Production-secure tier. Consumes Cloudflare tunnel/route slots.";
239
- };
240
- readonly runners: {
241
- readonly embedded: "fs.start() supervises cloudflared as a child process (default DX).";
242
- readonly sidecar: "Vanilla cloudflare/cloudflared container beside the app (production / non-Node).";
243
- };
244
- readonly channelTrust: readonly ["tunnel"];
245
- readonly requestTrust: "x-fs-signature";
246
- readonly invariant: "Channel trust (tunnel) and request trust (the X-FS-* signature) are distinct layers; both always apply. CF-Access-* headers are transport-layer only and are IGNORED by the SDK.";
247
- };
@@ -1,27 +1,29 @@
1
1
  import { FartherShore, type FartherShoreInitOptions } from "./core/runtime.js";
2
- import { type ExpressMiddleware, type ExpressRequestLike, type ExpressResponseLike, type MiddlewareOptions, type VerifiedExpressHandler } from "./adapters/express.js";
2
+ import { type ExpressMiddleware, type ExpressRequestLike, type ExpressResponseLike, type HandlerOptions, type MiddlewareOptions, type VerifiedExpressHandler } from "./adapters/express.js";
3
+ import { type PermissionCarrier } from "./core/permissions.js";
3
4
  export { FartherShore } from "./core/runtime.js";
4
5
  export type { FartherShoreInitOptions } from "./core/runtime.js";
5
6
  export { FartherShoreError, statusForCode } from "./core/errors.js";
6
- export { verifyRequest, type VerifyRequestInput, type VerifyRequestDeps, type FartherShoreRequestContext, type HeadersLike, } from "./core/verifyRequest.js";
7
+ export { verifyRequest, type VerifyRequestInput, type VerifyRequestDeps, type FartherShoreContext, type FartherShoreRequestContext, type HeadersLike, } from "./core/verifyRequest.js";
8
+ export { MEASUREMENTS_VERSION } from "./core/report.js";
9
+ export type { ReportInput, ReportResult, ReportTransport, Measurement, MeasurementValues, MeasurementDimensions, QuoteInput, QuoteProposal, } from "./core/report.js";
7
10
  export { verifyContext, decodeContextClaims, principalFromContextClaims, } from "./core/verifyContext.js";
8
11
  export type { FartherShoreSignedContext, ConsumerPrincipal, } from "./core/verifyContext.js";
9
12
  export { requireMember, requireService, credentialKind, isPortalSession, type MemberSubject, type ServiceSubject, type PrincipalCarrier, } from "./core/subject.js";
10
- export { hasPermission, requirePermission, permissionGrants, permissionSatisfies, FartherShorePermissionError, type PermissionCarrier, } from "./core/permissions.js";
13
+ export { hasPermission, requirePermission, permissionSatisfies, routePermission, READ_METHODS, FartherShorePermissionError, type PermissionCarrier, } from "./core/permissions.js";
11
14
  export { JwksClient, type Jwk, type JwksClientOptions } from "./core/jwks.js";
12
15
  export { NonceCache, type NonceCacheOptions, type NonceStore, } from "./core/nonceCache.js";
16
+ export { type ReplayProtectionDiagnostic, type ReplayProtectionMode, } from "./core/replay-protection.js";
13
17
  export { BootstrapClient, type BootstrapClientOptions, } from "./core/bootstrap.js";
14
- export { MeteringClient, type MeteringClientOptions, type MeterOptions, } from "./core/metering.js";
15
- export { PostStreamUsageClient, type PostStreamUsageClientOptions, type ReportUsageInput, type RequestScopedReportUsageInput, type ReportUsageResult, } from "./core/post-stream-usage.js";
16
18
  export { buildHealthReport, reportHealth, type HealthSnapshot, type HealthStatus, type HeartbeatOptions, } from "./core/health.js";
17
19
  export { ShutdownManager, type ShutdownHook } from "./core/shutdown.js";
18
20
  export { CloudflaredSupervisor, nodeSpawn, REDACTED_TOKEN, type SpawnFn, type SpawnedTunnelProcess, type CloudflaredSupervisorOptions, type TunnelState, type TunnelStatus, } from "./core/tunnel.js";
19
21
  export type { FartherShoreTunnelOptions } from "./core/runtime.js";
20
- export { createExpressMiddleware, createExpressHandler, type ExpressMiddleware, type ExpressRequestLike, type ExpressResponseLike, type ExpressNext, type MiddlewareOptions, type VerifiedExpressHandler, type VerifiedPrincipalContext, } from "./adapters/express.js";
22
+ export { createExpressMiddleware, createExpressHandler, type ExpressMiddleware, type ExpressRequestLike, type ExpressResponseLike, type ExpressNext, type HandlerOptions, type MiddlewareOptions, type VerifiedExpressHandler, type VerifiedPrincipalContext, } from "./adapters/express.js";
21
23
  export { FS_RUNTIME_TOKEN_ENV, RUNTIME_TOKEN_PREFIXES, RUNTIME_TOKEN_OPERATIONS, RUNTIME_HEADER_NAMES, RUNTIME_CLOCK_SKEW_SECONDS, RUNTIME_REPLAY_WINDOW_SECONDS, EMPTY_BODY_SHA256, STREAMING_EXEMPT_BODY_HASH, MAX_BODY_BYTES, type RuntimeErrorCode, type RuntimeTokenOperation, type CanonicalSigningInput, type RuntimeBootstrapResponse, type RuntimeMeteringEvent, type RuntimeHealthReport, type TransportMode, RUNTIME_ERROR_CODE_TO_ERROR_CODE, runtimeErrorToErrorCode, type LimitDescriptor, type RuntimeMappedErrorCode, } from "./runtime-types.js";
22
24
  export { RUNTIME_ERROR_CODES } from "./generated/runtime-contract.js";
23
25
  export { hashBody, buildCanonicalSigningString, canonicalizeQuery, signCanonicalString, verifyCanonicalSignature, runtimeTokenKind, } from "./runtime-signing.js";
24
- export { createUsage, withUsage, computeMeteringHeaders, MeteringError, METERING_PAYLOAD_HEADER, METERING_SIGNATURE_HEADER, METERING_TOKEN_HEADER, DEFAULT_TOKEN_ENV, type UsageMap, type UsageReporter, type MeteringOptions, type MeteringHeaders, type ComputeMeteringOptions, type ResponseMeteringUsagePayload, } from "./response-metering.js";
26
+ export { computeMeteringHeaders, MeteringError, METERING_PAYLOAD_HEADER, METERING_SIGNATURE_HEADER, METERING_TOKEN_HEADER, DEFAULT_TOKEN_ENV, type MeteringHeaders, type ComputeMeteringOptions, type ResponseMeteringUsagePayload, } from "./response-metering.js";
25
27
  /**
26
28
  * The conceptual public entrypoint. `fartherShore.initFromEnv()` mirrors the
27
29
  * language-neutral spec. The returned instance is augmented with `middleware()`
@@ -37,7 +39,27 @@ export type FartherShoreInstance = FartherShore & {
37
39
  * context is absent, `principal_required` when it is identity-less) before the
38
40
  * callback runs. Pairs with the strict `middleware()`.
39
41
  */
40
- handler<Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
42
+ handler: {
43
+ <Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
44
+ /**
45
+ * Options-first overload: `fs.handler({ permission: "widgets:write" }, cb)`
46
+ * asserts the permission (fail-closed) before the callback runs,
47
+ * responding `403 permission_denied` otherwise.
48
+ */
49
+ <Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(options: HandlerOptions, handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
50
+ };
51
+ /**
52
+ * In-handler authorization helpers bound to the instance — the SAME shape as
53
+ * the dev runtime's `rt.authz` (which additionally traces each decision), so
54
+ * dev and prod code reads identically. Both key ONLY on `ctx.permissions`
55
+ * with FAIL-CLOSED carrier semantics: an ABSENT permission set DENIES.
56
+ */
57
+ authz: {
58
+ /** True when the acting user holds `key` (`*` / `<subject>:*` / exact). */
59
+ hasPermission(ctx: PermissionCarrier, key: string): boolean;
60
+ /** Assert `key` is held; throws `FartherShorePermissionError` (403). */
61
+ requirePermission(ctx: PermissionCarrier, key: string): void;
62
+ };
41
63
  };
42
64
  export declare const fartherShore: {
43
65
  /** Derive everything from FS_RUNTIME_TOKEN via bootstrap. */
@@ -0,0 +1,2 @@
1
+ export { createReportFn, rawDimsUnitsOf, type ReportChannels, type ResponseSink, type ReportFn, } from "../core/report.js";
2
+ export { PostStreamUsageClient, type PostStreamUsageClientOptions, type ReportUsageInput, type ReportUsageResult, } from "../core/post-stream-usage.js";
@@ -1,18 +1,39 @@
1
- declare const RESPONSE_METERING_ERROR_CODES: {
1
+ /** The response-metering fault codes carried by {@link MeteringError}. */
2
+ export declare const RESPONSE_METERING_ERROR_CODES: {
2
3
  readonly missingToken: "missing_token";
3
4
  readonly invalidMeterKey: "invalid_meter_key";
4
5
  readonly invalidMeterValue: "invalid_meter_value";
6
+ readonly invalidQuote: "invalid_quote";
5
7
  };
6
- type ResponseMeteringErrorCode = (typeof RESPONSE_METERING_ERROR_CODES)[keyof typeof RESPONSE_METERING_ERROR_CODES];
8
+ export type ResponseMeteringErrorCode = (typeof RESPONSE_METERING_ERROR_CODES)[keyof typeof RESPONSE_METERING_ERROR_CODES];
7
9
  /**
8
10
  * The signed response-metering payload. `computeMeteringHeaders` accepts one of
9
11
  * these directly, so a non-Express / non-Fetch handler (or a Python/Go backend
10
- * following the wire recipe) can stamp valid headers without `withUsage`.
12
+ * following the wire recipe) can stamp valid headers with no JS SDK at all.
13
+ * `ctx.report()` is the JS surface that builds it.
14
+ *
15
+ * `measurements` (with its `measurementsVersion`) is the measurement lane the
16
+ * one reporting verb emits: `{ meter, values, dims }` as declared in the
17
+ * business release. `rawDimsUnits` remains as the flat projection the gateway's
18
+ * existing settlement lane reads.
11
19
  */
12
20
  export type ResponseMeteringUsagePayload = {
13
21
  method: string;
14
22
  path: string;
15
- rawDimsUnits: Record<string, number>;
23
+ rawDimsUnits?: Record<string, number>;
24
+ /** Schema version of {@link measurements}; currently `1`. */
25
+ measurementsVersion?: number;
26
+ /** Reported measurements — the authoritative rating input. */
27
+ measurements?: {
28
+ meter: string;
29
+ values: Record<string, number>;
30
+ dims?: Record<string, string>;
31
+ }[];
32
+ /** Proposed rate input for a `backendQuoted` pricing rule (core clamps it). */
33
+ quote?: {
34
+ currency: string;
35
+ amountNanos: string;
36
+ };
16
37
  measureContext?: Record<string, unknown>;
17
38
  creditUnitsConsumed?: Record<string, number>;
18
39
  operationKey?: string;
@@ -43,43 +64,10 @@ export declare const METERING_PAYLOAD_HEADER: "x-fs-metering";
43
64
  export declare const METERING_SIGNATURE_HEADER: "x-fs-metering-sig";
44
65
  export declare const METERING_TOKEN_HEADER: "x-fs-metering-token";
45
66
  export declare const DEFAULT_TOKEN_ENV: "FS_RUNTIME_TOKEN";
46
- export type UsageMap = Record<string, number>;
47
- export type BillableUsageMap = UsageMap;
48
- export type MeteringOptions = {
49
- token?: string;
50
- env?: Record<string, string | undefined>;
51
- /**
52
- * Gateway request id for dev-mode trace/usage association (never signed).
53
- * Thread the VERIFIED `ctx.requestId` here — the strict middleware strips the
54
- * inbound `x-fs-request-id` header, so the request object no longer carries it.
55
- * Falls back to the request's `x-fs-request-id` header when unset.
56
- */
57
- requestId?: string;
58
- measureContext?: Record<string, unknown>;
59
- creditUnitsConsumed?: BillableUsageMap;
60
- /** Gateway-validated operation identity hint. The SDK signs and transports it
61
- * but never decides billing or policy from it. */
62
- operationKey?: string;
63
- /** Gateway-validated policy hint. Advisory identity only; the gateway remains
64
- * authoritative for customerBillable/provider-cost decisions. */
65
- usagePolicyId?: string;
66
- };
67
- export type UsageWrapOptions = {
68
- measureContext?: Record<string, unknown>;
69
- creditUnitsConsumed?: BillableUsageMap;
70
- operationKey?: string;
71
- usagePolicyId?: string;
72
- };
73
- export type UsageReporter = {
74
- report(meter: string, value: number): UsageReporter;
75
- wrap(response: Response, options?: UsageWrapOptions): Promise<Response>;
76
- };
77
67
  export declare class MeteringError extends Error {
78
68
  readonly code: ResponseMeteringErrorCode;
79
69
  constructor(code: ResponseMeteringErrorCode, message: string);
80
70
  }
81
- export declare function createUsage(request: Request, options?: MeteringOptions): UsageReporter;
82
- export declare function withUsage(request: Request, response: Response, usage: UsageMap, options?: MeteringOptions): Promise<Response>;
83
71
  /**
84
72
  * Compute the three response-metering headers for a payload as a plain
85
73
  * name→value map, attachable to ANY response mechanism (Fetch `Response`,
@@ -87,7 +75,9 @@ export declare function withUsage(request: Request, response: Response, usage: U
87
75
  * primitive: it NEVER throws at request time — if no token is resolvable (or
88
76
  * signing fails) it skips stamping, reports the reason (`onSkip` / dev hook /
89
77
  * a `console.warn`), and returns `{}` so the builder's endpoint is never broken.
90
- * `withUsage`/`createUsage` are thin sugar over it.
78
+ *
79
+ * This is the framework-neutral wire recipe: `ctx.report()` is the JS surface
80
+ * over it, and a Python/Go backend can stamp identical headers by building the
81
+ * same payload (see `docs/response-metering-wire.md`).
91
82
  */
92
83
  export declare function computeMeteringHeaders(payload: ResponseMeteringUsagePayload, options?: ComputeMeteringOptions): Promise<MeteringHeaders>;
93
- export {};
@@ -37,7 +37,7 @@ export type LimitClass = "quota" | "rate" | "concurrency" | "capacity" | "spend"
37
37
  * `LimitReaction`. */
38
38
  export type LimitReaction = "none" | "backoff_retry" | "wait_then_retry" | "queue" | "reduce_then_retry" | "fallback" | "upgrade";
39
39
  /** Where a limit was decided. Mirrors contracts `LimitOrigin`. */
40
- export type LimitOrigin = "platform" | "provider";
40
+ export type LimitOrigin = "platform" | "provider" | "subscriber";
41
41
  /**
42
42
  * F1 — the `_fs` deny envelope a backend stamps on a usage-limit deny body.
43
43
  * Structurally identical to the contracts `FsDenyEnvelope`.
@@ -45,6 +45,7 @@ export type LimitOrigin = "platform" | "provider";
45
45
  export interface FsDenyEnvelope {
46
46
  limitClass: LimitClass;
47
47
  scope?: string;
48
+ actorScope?: "member" | "service_account";
48
49
  metric?: string;
49
50
  reset?: number;
50
51
  remaining?: number;
@@ -75,6 +76,7 @@ export interface FsDenyEnvelope {
75
76
  export declare const DENY_ENVELOPE_FIELDS: {
76
77
  limitClass: true;
77
78
  scope: true;
79
+ actorScope: true;
78
80
  metric: true;
79
81
  reset: true;
80
82
  remaining: true;
@@ -286,6 +288,19 @@ export type RuntimePostStreamUsageEvent = {
286
288
  meters: Record<string, number>;
287
289
  creditUnitsConsumed?: Record<string, number>;
288
290
  measureContext?: Record<string, unknown>;
291
+ /** Schema version of {@link measurements}; currently `1`. */
292
+ measurementsVersion?: number;
293
+ /** Measurement lane — `{ meter, values, dims }` as declared in the release. */
294
+ measurements?: {
295
+ meter: string;
296
+ values: Record<string, number>;
297
+ dims?: Record<string, string>;
298
+ }[];
299
+ /** Proposed rate input for a `backendQuoted` pricing rule (core clamps it). */
300
+ quote?: {
301
+ currency: string;
302
+ amountNanos: string;
303
+ };
289
304
  signature: string;
290
305
  };
291
306
  export type RuntimeHealthReport = {
@@ -1,5 +1,5 @@
1
1
  import { FartherShore } from "../core/runtime.js";
2
- import { type ExpressMiddleware, type ExpressRequestLike, type ExpressResponseLike, type MiddlewareOptions, type VerifiedExpressHandler } from "../adapters/express.js";
2
+ import { type ExpressMiddleware, type ExpressRequestLike, type ExpressResponseLike, type HandlerOptions, type MiddlewareOptions, type VerifiedExpressHandler } from "../adapters/express.js";
3
3
  import { type PermissionCarrier } from "../core/permissions.js";
4
4
  import { type DevGateway } from "./devGateway.js";
5
5
  import { DevUsageSink } from "./usageSink.js";
@@ -14,7 +14,16 @@ export type TracedCarrier = PermissionCarrier & {
14
14
  export type FartherShoreDevInstance = FartherShore & {
15
15
  middleware(options?: MiddlewareOptions): ExpressMiddleware;
16
16
  /** Strict-context handler wrapper (guaranteed non-optional ctx). */
17
- handler<Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
17
+ handler: {
18
+ <Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
19
+ /** Options-first overload — declarative `permission` gate. */
20
+ <Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(options: HandlerOptions, handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
21
+ };
22
+ /** Traced authz helpers — the SAME shape as the prod facade's `fs.authz`. */
23
+ authz: {
24
+ hasPermission(ctx: TracedCarrier, key: string): boolean;
25
+ requirePermission(ctx: TracedCarrier, key: string): void;
26
+ };
18
27
  /** The dev harness attached to this runtime. */
19
28
  dev: DevRuntime;
20
29
  };
@@ -6,3 +6,4 @@ export { writeDevKeysFile, readDevKeysFile, personaClientFromKeysFile, DEFAULT_K
6
6
  export { DevUsageSink, type DevUsageEvent } from "./usageSink.js";
7
7
  export { DevTraceSink, redactValue, type DevTrace, type DevMode, type VerificationOutcome, type AuthzDecisionEntry, type MeteringTraceEntry, } from "./traceSink.js";
8
8
  export { isProductionEnv, assertNotProduction, DevModeInProductionError, } from "./prodGuard.js";
9
+ export { signWebhookForTesting, type SignWebhookForTestingInput, type SignedTestWebhook, } from "./webhooks.js";
@@ -26,7 +26,7 @@ export type DevUsageEvent = {
26
26
  /** In-memory, assertable sink for all dev usage. */
27
27
  export declare class DevUsageSink {
28
28
  readonly events: DevUsageEvent[];
29
- /** Record a signed response-metering payload (withUsage / computeMeteringHeaders). */
29
+ /** Record a signed response-metering payload (report() in-band / computeMeteringHeaders). */
30
30
  recordResponse(payload: Record<string, unknown>, requestId?: string): void;
31
31
  /** Record a background `fs.meter()` event captured by the dev gateway. */
32
32
  recordMeterEvent(event: RuntimeMeteringEvent): void;
@@ -0,0 +1,30 @@
1
+ import type { WebhookEnvelope, WebhookEnvelopeType, WebhookEventData } from "../webhooks/types.js";
2
+ export interface SignWebhookForTestingInput<T extends WebhookEnvelopeType> {
3
+ /** The endpoint secret(s) the receiver is configured with (current first). */
4
+ secret: string | readonly string[];
5
+ type: T;
6
+ data: WebhookEventData[T];
7
+ /** Defaults to a random delivery id. */
8
+ id?: string;
9
+ businessId?: string;
10
+ environmentId?: string | null;
11
+ /** Defaults to now (ISO). */
12
+ createdAt?: string;
13
+ /** Signing timestamp (unix seconds); defaults to now. */
14
+ timestamp?: number;
15
+ }
16
+ export interface SignedTestWebhook<T extends WebhookEnvelopeType> {
17
+ envelope: WebhookEnvelope<T>;
18
+ /** The exact bytes to POST. */
19
+ body: string;
20
+ /** `webhook-id` / `webhook-timestamp` / `webhook-signature` / event / content-type. */
21
+ headers: Record<string, string>;
22
+ /** Convenience: a `Request` you can hand straight to `handler.fetch`. */
23
+ request(url?: string): Request;
24
+ }
25
+ /**
26
+ * Build + sign one delivery the way the platform does (Standard Webhooks
27
+ * over `${id}.${timestamp}.${body}`). Pass several secrets to emulate the
28
+ * 24 h dual-signing rotation window.
29
+ */
30
+ export declare function signWebhookForTesting<T extends WebhookEnvelopeType>(input: SignWebhookForTestingInput<T>): SignedTestWebhook<T>;