@farthershore/backend 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ import type { RuntimeMeteringConfig } from "../runtime-types.js";
2
+ export type ReportUsageInput = {
3
+ requestId: string;
4
+ subscriptionId: string;
5
+ meters: Record<string, number>;
6
+ creditUnitsConsumed?: Record<string, number>;
7
+ measureContext?: Record<string, unknown>;
8
+ };
9
+ export type RequestScopedReportUsageInput = Omit<ReportUsageInput, "requestId" | "subscriptionId"> & {
10
+ requestId?: string;
11
+ subscriptionId?: string;
12
+ };
13
+ export type ReportUsageResult = {
14
+ ok: true;
15
+ } | {
16
+ ok: false;
17
+ reason: string;
18
+ };
19
+ export type PostStreamUsageClientOptions = {
20
+ config: RuntimeMeteringConfig;
21
+ coreUrl?: string;
22
+ fetchImpl?: typeof fetch;
23
+ newNonce?: () => string;
24
+ logger?: (message: string) => void;
25
+ /** Injectable for tests; defaults to a normal timer-backed delay. */
26
+ sleep?: (delayMs: number) => Promise<void>;
27
+ /** Backoff after each request-not-found response. */
28
+ retryDelaysMs?: readonly number[];
29
+ };
30
+ /**
31
+ * Best-effort, attested post-stream billing reporter. The callback is
32
+ * request-bound in Core but never settles a Durable Object enforcement window.
33
+ * Every failure resolves ok:false.
34
+ */
35
+ export declare class PostStreamUsageClient {
36
+ private readonly config;
37
+ private readonly endpoint;
38
+ private readonly fetchImpl;
39
+ private readonly newNonce;
40
+ private readonly logger;
41
+ private readonly sleep;
42
+ private readonly retryDelaysMs;
43
+ constructor(options: PostStreamUsageClientOptions);
44
+ reportUsage(input: ReportUsageInput): Promise<ReportUsageResult>;
45
+ }
@@ -1,6 +1,7 @@
1
1
  import { type RuntimeBootstrapResponse, type RuntimeHealthReport } from "../runtime-types.js";
2
2
  import type { ReconcileResult } from "../reflect/reconcile.js";
3
3
  import { type MeterOptions } from "./metering.js";
4
+ import { type ReportUsageInput, type ReportUsageResult } from "./post-stream-usage.js";
4
5
  import { type SpawnFn } from "./tunnel.js";
5
6
  import { type FartherShoreRequestContext, type VerifyRequestInput } from "./verifyRequest.js";
6
7
  /** Advanced opt-in tunnel config. The embedded runner is the default DX. */
@@ -95,6 +96,7 @@ export declare class FartherShore {
95
96
  private readonly shutdownManager;
96
97
  private jwks;
97
98
  private meteringClient;
99
+ private postStreamUsageClient;
98
100
  private tunnel;
99
101
  private bootstrapped;
100
102
  constructor(options?: FartherShoreInitOptions);
@@ -137,6 +139,8 @@ export declare class FartherShore {
137
139
  start(): Promise<void>;
138
140
  /** Record metering usage (billing-only). */
139
141
  meter(meter: string, qty: number, options?: MeterOptions): Promise<void>;
142
+ /** Best-effort attested post-stream usage callback. Never rejects. */
143
+ reportUsage(input: ReportUsageInput): Promise<ReportUsageResult>;
140
144
  /** Current local health report. */
141
145
  health(): RuntimeHealthReport;
142
146
  /** Graceful shutdown: flush metering + send a stopping heartbeat. */
@@ -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";
@@ -48,6 +49,8 @@ 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;
@@ -149,14 +149,29 @@ export declare const RUNTIME_METERING_CONTRACT: {
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";
@@ -19,7 +20,7 @@ export { createExpressMiddleware, type ExpressMiddleware, type ExpressRequestLik
19
20
  export { FS_RUNTIME_TOKEN_ENV, RUNTIME_TOKEN_PREFIXES, RUNTIME_TOKEN_CAPABILITIES, RUNTIME_HEADER_NAMES, RUNTIME_CLOCK_SKEW_SECONDS, RUNTIME_REPLAY_WINDOW_SECONDS, EMPTY_BODY_SHA256, STREAMING_EXEMPT_BODY_HASH, MAX_BODY_BYTES, type RuntimeErrorCode, type RuntimeTokenCapability, 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";
20
21
  export { RUNTIME_ERROR_CODES } from "./generated/runtime-contract.js";
21
22
  export { hashBody, buildCanonicalSigningString, canonicalizeQuery, signCanonicalString, verifyCanonicalSignature, runtimeTokenKind, } from "./runtime-signing.js";
22
- export { createUsage, withUsage, MeteringError, METERING_PAYLOAD_HEADER, METERING_SIGNATURE_HEADER, METERING_TOKEN_HEADER, DEFAULT_TOKEN_ENV, type UsageMap, type UsageReporter, type MeteringOptions, } from "./response-metering.js";
23
+ 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";
23
24
  /**
24
25
  * The conceptual public entrypoint. `fartherShore.initFromEnv()` mirrors the
25
26
  * language-neutral spec. The returned instance is augmented with `middleware()`
@@ -4,6 +4,41 @@ declare const RESPONSE_METERING_ERROR_CODES: {
4
4
  readonly invalidMeterValue: "invalid_meter_value";
5
5
  };
6
6
  type ResponseMeteringErrorCode = (typeof RESPONSE_METERING_ERROR_CODES)[keyof typeof RESPONSE_METERING_ERROR_CODES];
7
+ /**
8
+ * The signed response-metering payload. `computeMeteringHeaders` accepts one of
9
+ * 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`.
11
+ */
12
+ export type ResponseMeteringUsagePayload = {
13
+ method: string;
14
+ path: string;
15
+ rawDimsUnits: Record<string, number>;
16
+ measureContext?: Record<string, unknown>;
17
+ creditUnitsConsumed?: Record<string, number>;
18
+ operationKey?: string;
19
+ usagePolicyId?: string;
20
+ };
21
+ /** The three response-metering headers as a plain, attachable name→value map. */
22
+ export type MeteringHeaders = Record<string, string>;
23
+ export type ComputeMeteringOptions = {
24
+ token?: string;
25
+ env?: Record<string, string | undefined>;
26
+ /** Gateway request id, for dev-mode trace/usage association (never signed). */
27
+ requestId?: string;
28
+ /**
29
+ * Called (instead of throwing) when headers cannot be stamped at request time
30
+ * — e.g. no token. The endpoint is never broken; usage is simply not metered.
31
+ */
32
+ onSkip?: (reason: string) => void;
33
+ };
34
+ export type DevMeteringHooks = {
35
+ /** Ephemeral token used when no real token is configured (dev only). */
36
+ fallbackToken?: () => string | undefined;
37
+ /** Record a reported payload (never alters the response). */
38
+ record?: (payload: ResponseMeteringUsagePayload, requestId?: string) => void;
39
+ /** Observe a request-time skip ("metering headers skipped: <reason>"). */
40
+ onSkip?: (reason: string, requestId?: string) => void;
41
+ };
7
42
  export declare const METERING_PAYLOAD_HEADER: "x-fs-metering";
8
43
  export declare const METERING_SIGNATURE_HEADER: "x-fs-metering-sig";
9
44
  export declare const METERING_TOKEN_HEADER: "x-fs-metering-token";
@@ -38,4 +73,14 @@ export declare class MeteringError extends Error {
38
73
  }
39
74
  export declare function createUsage(request: Request, options?: MeteringOptions): UsageReporter;
40
75
  export declare function withUsage(request: Request, response: Response, usage: UsageMap, options?: MeteringOptions): Promise<Response>;
76
+ /**
77
+ * Compute the three response-metering headers for a payload as a plain
78
+ * name→value map, attachable to ANY response mechanism (Fetch `Response`,
79
+ * Express `res.set`, a raw header object). This is the metering-availability
80
+ * primitive: it NEVER throws at request time — if no token is resolvable (or
81
+ * signing fails) it skips stamping, reports the reason (`onSkip` / dev hook /
82
+ * a `console.warn`), and returns `{}` so the builder's endpoint is never broken.
83
+ * `withUsage`/`createUsage` are thin sugar over it.
84
+ */
85
+ export declare function computeMeteringHeaders(payload: ResponseMeteringUsagePayload, options?: ComputeMeteringOptions): Promise<MeteringHeaders>;
41
86
  export {};
@@ -255,6 +255,16 @@ export type RuntimeMeteringEvent = {
255
255
  qty: number;
256
256
  timestamp: string;
257
257
  };
258
+ /** Attested, request-bound billing callback sent after a stream completes. */
259
+ export type RuntimePostStreamUsageEvent = {
260
+ requestId: string;
261
+ subscriptionId: string;
262
+ nonce: string;
263
+ meters: Record<string, number>;
264
+ creditUnitsConsumed?: Record<string, number>;
265
+ measureContext?: Record<string, unknown>;
266
+ signature: string;
267
+ };
258
268
  export declare const RUNTIME_READINESS_STATES: readonly ["UNKNOWN", "WAITING", "READY", "DEGRADED", "OFFLINE"];
259
269
  export type RuntimeReadinessState = (typeof RUNTIME_READINESS_STATES)[number];
260
270
  export type RuntimeHealthReport = {
@@ -0,0 +1,40 @@
1
+ import { type RuntimeBootstrapResponse, type RuntimeMeteringEvent, type RuntimePostStreamUsageEvent } from "../runtime-types.js";
2
+ import type { DevMode } from "./traceSink.js";
3
+ import type { DevSignerKeys } from "./signers.js";
4
+ export declare const DEV_CORE_URL = "https://dev-gateway.farthershore.local";
5
+ export declare const DEV_JWKS_URL = "https://dev-gateway.farthershore.local/.well-known/jwks.json";
6
+ export declare const DEV_METERING_ENDPOINT = "https://dev-gateway.farthershore.local/v1/metering/events";
7
+ export type DevGatewayOptions = {
8
+ mode: DevMode;
9
+ keys: DevSignerKeys;
10
+ productId?: string;
11
+ backendId?: string;
12
+ productSlug?: string;
13
+ backendSlug?: string;
14
+ /** Extra route ids to expose in bootstrap for route-binding tests. */
15
+ routeIds?: string[];
16
+ /** Called for each captured metering event (at-least-once ACK). */
17
+ onMeterEvent?: (event: RuntimeMeteringEvent) => void;
18
+ /** Called for each captured attested post-stream report. */
19
+ onReportUsage?: (event: RuntimePostStreamUsageEvent) => void;
20
+ };
21
+ export type DevGateway = {
22
+ /** Inject this as `fetchImpl` when constructing the FartherShore runtime. */
23
+ fetchImpl: typeof fetch;
24
+ /** The bootstrap response this fixture serves. */
25
+ bootstrap: RuntimeBootstrapResponse;
26
+ /** Every captured background metering event (a capture == an ACK). */
27
+ meterEvents: RuntimeMeteringEvent[];
28
+ /** Every captured attested post-stream usage report. */
29
+ reportUsageEvents: RuntimePostStreamUsageEvent[];
30
+ productId: string;
31
+ backendId: string;
32
+ jwksUrl: string;
33
+ };
34
+ /**
35
+ * Build the in-process dev gateway. `mode` drives `verification.required`:
36
+ * `passthrough` → false (the adapter passes requests through unverified, matching
37
+ * the pre-keystone deploy order), `simulated` → true (fail-closed verification is
38
+ * exercised end-to-end).
39
+ */
40
+ export declare function createDevGateway(options: DevGatewayOptions): DevGateway;
@@ -0,0 +1,68 @@
1
+ import { FartherShore } from "../core/runtime.js";
2
+ import { type ExpressMiddleware, type MiddlewareOptions } from "../adapters/express.js";
3
+ import { type PermissionCarrier } from "../core/permissions.js";
4
+ import { type DevGateway } from "./devGateway.js";
5
+ import { DevUsageSink } from "./usageSink.js";
6
+ import { DevTraceSink, type DevMode } from "./traceSink.js";
7
+ import { type DevSignerKeys } from "./signers.js";
8
+ import { type PersonaDefinition, type PersonaRequest } from "./personas.js";
9
+ /** A permission carrier that also knows its request id (for trace keying). */
10
+ export type TracedCarrier = PermissionCarrier & {
11
+ requestId?: string;
12
+ };
13
+ /** The FartherShore instance augmented with a bound Express middleware. */
14
+ export type FartherShoreDevInstance = FartherShore & {
15
+ middleware(options?: MiddlewareOptions): ExpressMiddleware;
16
+ /** The dev harness attached to this runtime. */
17
+ dev: DevRuntime;
18
+ };
19
+ export type CreateDevRuntimeOptions = {
20
+ mode: DevMode;
21
+ /** Persona overrides merged over the built-in owner/admin/member/anonymous. */
22
+ personas?: Record<string, PersonaDefinition> | PersonaDefinition[];
23
+ /** Route ids to expose in bootstrap (for route-binding tests). */
24
+ routes?: string[];
25
+ /** Meter keys (informational — the dev gateway accepts any meter). */
26
+ meters?: string[];
27
+ productId?: string;
28
+ backendId?: string;
29
+ /** Optional app transport for persona `.fetch()`; defaults to global fetch. */
30
+ appFetch?: typeof fetch;
31
+ /** Reuse a fixed key set (cross-process). Defaults to a fresh ephemeral set. */
32
+ keys?: DevSignerKeys;
33
+ };
34
+ /** The dev harness returned by `createDevRuntime` and attached as `fs.dev`. */
35
+ export type DevRuntime = {
36
+ fs: FartherShoreDevInstance;
37
+ asPersona(name: string): PersonaRequest;
38
+ usage: DevUsageSink;
39
+ trace: DevTraceSink;
40
+ gateway: DevGateway;
41
+ keys: DevSignerKeys;
42
+ personas: Map<string, PersonaDefinition>;
43
+ mode: DevMode;
44
+ bootstrap: DevGateway["bootstrap"];
45
+ /** Traced authz helpers — delegate to the REAL permission functions and
46
+ * record each decision into the trace (keyed by ctx.requestId). */
47
+ authz: {
48
+ hasPermission(ctx: TracedCarrier, key: string): boolean;
49
+ requirePermission(ctx: TracedCarrier, key: string): void;
50
+ };
51
+ /** A trace-aware Express middleware (wraps the real fail-closed middleware). */
52
+ middleware(options?: MiddlewareOptions): ExpressMiddleware;
53
+ /** Clear usage + trace + captured meter events. */
54
+ reset(): void;
55
+ };
56
+ /**
57
+ * Build a dev runtime: a real `FartherShore` wired to the in-process gateway and
58
+ * signed personas, plus assertable usage/trace side channels.
59
+ */
60
+ export declare function createDevRuntime(options: CreateDevRuntimeOptions): DevRuntime;
61
+ /** Whether `FS_DEV_MODE` selects a dev mode. Returns the mode or null. */
62
+ export declare function devModeFromEnv(env: Record<string, string | undefined>): DevMode | null;
63
+ /**
64
+ * Self-construct the dev simulator from the environment. Called by `initFromEnv`
65
+ * when `FS_DEV_MODE` is set. Prints a LOUD banner, wires JSONL usage/trace sinks
66
+ * + a mode-600 dev-keys file, and returns the augmented runtime.
67
+ */
68
+ export declare function createDevRuntimeFromEnv(env?: Record<string, string | undefined>): FartherShoreDevInstance;
@@ -0,0 +1,8 @@
1
+ export { TEST_KID, TEST_PRIVATE_JWK, TEST_PUBLIC_JWK, TEST_CONTEXT_SECRET, TEST_CONTEXT_KID, makeSignedRequest, memoryJwks, unreachableJwks, signContextToken, generateDevSignerKeys, type SignedHeaderOverrides, type SignedRequestSpec, type DevSignerKeys, } from "./signers.js";
2
+ export { definePersona, createPersonaClient, buildPersonaMap, DEFAULT_PERSONAS, CONTEXT_HEADER_NAME, SIGNED_HEADER_NAMES, type PersonaDefinition, type PersonaRequest, type PersonaRequestSpec, type PersonaClientContext, type SettableRequest, } from "./personas.js";
3
+ export { createDevGateway, DEV_CORE_URL, DEV_JWKS_URL, DEV_METERING_ENDPOINT, type DevGateway, type DevGatewayOptions, } from "./devGateway.js";
4
+ export { createDevRuntime, createDevRuntimeFromEnv, devModeFromEnv, type DevRuntime, type CreateDevRuntimeOptions, type FartherShoreDevInstance, type TracedCarrier, } from "./devRuntime.js";
5
+ export { writeDevKeysFile, readDevKeysFile, personaClientFromKeysFile, DEFAULT_KEYS_FILE, type DevKeysFile, } from "./keysFile.js";
6
+ export { DevUsageSink, type DevUsageEvent } from "./usageSink.js";
7
+ export { DevTraceSink, redactValue, type DevTrace, type DevMode, type VerificationOutcome, type AuthzDecisionEntry, type MeteringTraceEntry, } from "./traceSink.js";
8
+ export { isProductionEnv, assertNotProduction, DevModeInProductionError, } from "./prodGuard.js";
@@ -0,0 +1,30 @@
1
+ import type { DevMode } from "./traceSink.js";
2
+ import type { DevSignerKeys } from "./signers.js";
3
+ import { type PersonaDefinition, type PersonaRequest } from "./personas.js";
4
+ export declare const DEFAULT_KEYS_FILE = ".farthershore/dev-keys.json";
5
+ /** The on-disk shape of the dev-keys handoff file. */
6
+ export type DevKeysFile = {
7
+ version: 1;
8
+ mode: DevMode;
9
+ keys: DevSignerKeys;
10
+ productId: string;
11
+ backendId: string;
12
+ /** Persona definitions in effect for this dev session. */
13
+ personas: Record<string, PersonaDefinition>;
14
+ };
15
+ /** Write the dev-keys file with 0600 permissions (owner read/write only). */
16
+ export declare function writeDevKeysFile(path: string, contents: DevKeysFile): void;
17
+ /** Read + parse a dev-keys file. */
18
+ export declare function readDevKeysFile(path?: string): DevKeysFile;
19
+ /**
20
+ * Construct a persona client from a dev-keys file — the cross-process entry
21
+ * point. A test runner in a separate process reads the file the running service
22
+ * wrote and can immediately `asPersona("member").fetch(url)` against it.
23
+ */
24
+ export declare function personaClientFromKeysFile(path?: string, options?: {
25
+ fetchImpl?: typeof fetch;
26
+ }): {
27
+ asPersona(name: string): PersonaRequest;
28
+ personas: Map<string, PersonaDefinition>;
29
+ file: DevKeysFile;
30
+ };
@@ -0,0 +1,104 @@
1
+ import type { DevMode } from "./traceSink.js";
2
+ import { type DevSignerKeys } from "./signers.js";
3
+ /** A named identity in the platform's test-persona claim vocabulary. */
4
+ export type PersonaDefinition = {
5
+ name: string;
6
+ orgId?: string;
7
+ actor?: {
8
+ type: string;
9
+ id: string | null;
10
+ };
11
+ productId?: string;
12
+ environmentId?: string | null;
13
+ compiledPlanId?: string;
14
+ subscriptionId?: string;
15
+ subscriberId?: string;
16
+ /**
17
+ * The unified-authz permission grant. `["*"]` = full access (org OWNER /
18
+ * RBAC-disabled). `[]` = authenticated with no grants (fail-closed on every
19
+ * check). Omit to mint a verified-but-claimless context (RBAC N/A ⇒ grant).
20
+ */
21
+ permissions?: string[];
22
+ roles?: string[];
23
+ subjectKey?: string;
24
+ /**
25
+ * When true, `asPersona` emits NO `X-Fs-Context` — an unidentified caller.
26
+ * Under `simulated` mode the request still verifies (valid signature) but
27
+ * carrier-level permission checks fail closed. Used by the `anonymous` default.
28
+ */
29
+ anonymous?: boolean;
30
+ };
31
+ /** Identity-neutral pass-through of a persona definition (readability sugar). */
32
+ export declare function definePersona(def: PersonaDefinition): PersonaDefinition;
33
+ /** The four built-in personas: owner / admin / member / anonymous. */
34
+ export declare const DEFAULT_PERSONAS: Record<string, PersonaDefinition>;
35
+ /** A minimal `.set(name, value)`-carrying request (supertest `Test`, etc.). */
36
+ export type SettableRequest = {
37
+ method?: string;
38
+ url?: string;
39
+ path?: string;
40
+ set(field: string, value: string): unknown;
41
+ };
42
+ /** A single request spec for `.headers()`. */
43
+ export type PersonaRequestSpec = {
44
+ method?: string;
45
+ path?: string;
46
+ query?: string;
47
+ body?: Uint8Array | null;
48
+ streamingExempt?: boolean;
49
+ routeId?: string;
50
+ requestId?: string;
51
+ timestamp?: number;
52
+ };
53
+ /** The context a persona client signs against (bootstrap ids + dev keys). */
54
+ export type PersonaClientContext = {
55
+ keys: DevSignerKeys;
56
+ productId: string;
57
+ backendId: string;
58
+ contextSecret: string;
59
+ contextKid: string;
60
+ personas: Map<string, PersonaDefinition>;
61
+ mode: DevMode;
62
+ /** fetch used by `.fetch()`; defaults to global fetch for real app listeners. */
63
+ fetchImpl?: typeof fetch;
64
+ };
65
+ /** The request-builder returned by `asPersona(name)`. */
66
+ export type PersonaRequest = {
67
+ /** The persona name. */
68
+ readonly persona: string;
69
+ /**
70
+ * The 9 signed `x-fs-*` headers + `X-Fs-Context` (unless anonymous).
71
+ * Defaults to GET; pass `method` for non-GET requests so the signature
72
+ * matches the request the caller sends.
73
+ */
74
+ headers(spec?: PersonaRequestSpec): Promise<Record<string, string>>;
75
+ /** Real HTTP with signed headers (uses the wired fetch). */
76
+ fetch(url: string, init?: RequestInit): Promise<Response>;
77
+ /** Set the signed headers on any `.set()`-carrying request (supertest, etc.). */
78
+ inject(req: SettableRequest, spec?: PersonaRequestSpec): Promise<SettableRequest>;
79
+ };
80
+ /**
81
+ * Build a persona client bound to a set of dev keys + bootstrap ids. Returns an
82
+ * `asPersona(name)` factory. Constructed by the dev runtime; also constructible
83
+ * standalone from a dev-keys file (see `personaClientFromKeysFile`).
84
+ */
85
+ export declare function createPersonaClient(ctx: PersonaClientContext): {
86
+ asPersona(name: string): PersonaRequest;
87
+ personas: Map<string, PersonaDefinition>;
88
+ };
89
+ /** Merge caller personas over the built-in defaults into a name→def map. */
90
+ export declare function buildPersonaMap(overrides?: Record<string, PersonaDefinition> | PersonaDefinition[]): Map<string, PersonaDefinition>;
91
+ /** The header name for the signed context (exported for assertions). */
92
+ export declare const CONTEXT_HEADER_NAME = "x-fs-context";
93
+ /** The signed request header names (exported for assertions). */
94
+ export declare const SIGNED_HEADER_NAMES: {
95
+ readonly signature: "x-fs-signature";
96
+ readonly keyId: "x-fs-key-id";
97
+ readonly requestId: "x-fs-request-id";
98
+ readonly timestamp: "x-fs-timestamp";
99
+ readonly productId: "x-fs-product-id";
100
+ readonly backendId: "x-fs-backend-id";
101
+ readonly routeId: "x-fs-route-id";
102
+ readonly policyVersion: "x-fs-policy-version";
103
+ readonly bodyHash: "x-fs-body-hash";
104
+ };
@@ -0,0 +1,12 @@
1
+ /** Case-insensitive `NODE_ENV=production` check (matches core's `isProductionEnv`). */
2
+ export declare function isProductionEnv(env?: Record<string, string | undefined>): boolean;
3
+ /** Thrown when a dev runtime is constructed in a production process. */
4
+ export declare class DevModeInProductionError extends Error {
5
+ constructor(context: string);
6
+ }
7
+ /**
8
+ * Throw {@link DevModeInProductionError} when `NODE_ENV=production`. Call at the
9
+ * TOP of every dev-runtime constructor. `context` names the entry point for the
10
+ * error message (e.g. "createDevRuntime", "FS_DEV_MODE").
11
+ */
12
+ export declare function assertNotProduction(context: string, env?: Record<string, string | undefined>): void;
@@ -0,0 +1,89 @@
1
+ import { type CanonicalSigningInput } from "../runtime-types.js";
2
+ import { JwksClient } from "../core/jwks.js";
3
+ import type { FartherShoreSignedContext } from "../core/verifyContext.js";
4
+ export declare const TEST_KID = "fs-runtime-test-2026";
5
+ export declare const TEST_PRIVATE_JWK: JsonWebKey;
6
+ export declare const TEST_PUBLIC_JWK: JsonWebKey;
7
+ /** The default HS256 context-signing secret for dev fixtures (NEVER production). */
8
+ export declare const TEST_CONTEXT_SECRET = "fs-dev-context-secret-2026";
9
+ /** The default context-signing kid stamped into the JWT header. */
10
+ export declare const TEST_CONTEXT_KID = "fs-context-test-2026";
11
+ export type SignedHeaderOverrides = Partial<{
12
+ signature: string;
13
+ kid: string;
14
+ requestId: string;
15
+ timestamp: number;
16
+ productId: string;
17
+ backendId: string;
18
+ routeId: string;
19
+ policyVersion: string;
20
+ bodyHash: string;
21
+ }>;
22
+ export type SignedRequestSpec = {
23
+ method?: string;
24
+ path?: string;
25
+ query?: string;
26
+ body?: Uint8Array | null;
27
+ streamingExempt?: boolean;
28
+ productId?: string;
29
+ backendId?: string;
30
+ routeId?: string;
31
+ policyVersion?: string;
32
+ requestId?: string;
33
+ timestamp?: number;
34
+ /**
35
+ * Ed25519 private JWK to sign with. Defaults to {@link TEST_PRIVATE_JWK}. A
36
+ * dev simulator passes its ephemeral signer here.
37
+ */
38
+ privateJwk?: JsonWebKey;
39
+ /** Key id stamped into `x-fs-key-id`. Defaults to {@link TEST_KID}. */
40
+ kid?: string;
41
+ };
42
+ /**
43
+ * Produce a valid signed claim + the X-FS-* header bag. Returns both the verify
44
+ * input shape and a mutable header record you can corrupt for negative tests.
45
+ */
46
+ export declare function makeSignedRequest(spec?: SignedRequestSpec): Promise<{
47
+ input: {
48
+ method: string;
49
+ path: string;
50
+ query: string;
51
+ body: Uint8Array | null;
52
+ streamingExempt: boolean;
53
+ };
54
+ claim: CanonicalSigningInput;
55
+ headers: Record<string, string>;
56
+ }>;
57
+ /** A JwksClient backed by an in-memory key set (no network). */
58
+ export declare function memoryJwks(keys?: Array<JsonWebKey & {
59
+ kid?: string;
60
+ }>): JwksClient;
61
+ /** A JwksClient whose fetch always throws (cold-cache failure). */
62
+ export declare function unreachableJwks(): JwksClient;
63
+ /**
64
+ * Mint an HS256-signed `X-Fs-Context` token — the exact INVERSE of
65
+ * `verifyContext.ts`. The header is `{alg:"HS256",typ:"JWT",kid}` and the
66
+ * payload is the `cv:1` claim shape (orgId / actor / productId / compiledPlanId
67
+ * / subscriptionId / subscriberId / environmentId / subjectKey plus optional
68
+ * permissions / roles). Signs with the same secret the SDK would verify against
69
+ * (`contextSecrets` / `FS_CONTEXT_SECRETS`).
70
+ */
71
+ export declare function signContextToken(claim: FartherShoreSignedContext, secret?: string, kid?: string): Promise<string>;
72
+ export type DevSignerKeys = {
73
+ /** Ed25519 signing keypair as JWKs (request-signature keys). */
74
+ kid: string;
75
+ privateJwk: JsonWebKey;
76
+ publicJwk: JsonWebKey;
77
+ /** HS256 context-signing secret + its kid (X-Fs-Context keys). */
78
+ contextKid: string;
79
+ contextSecret: string;
80
+ /** Ephemeral fsrt_test_ runtime token (bootstrap bearer + metering HMAC). */
81
+ runtimeToken: string;
82
+ };
83
+ /**
84
+ * Generate a fresh, ephemeral set of dev signer keys: an Ed25519 request-signing
85
+ * keypair, an HS256 context secret, and a `fsrt_test_` runtime token. Fully
86
+ * synchronous (uses `node:crypto`) so a dev simulator can be built without
87
+ * awaiting. NEVER call this in production (guarded upstream).
88
+ */
89
+ export declare function generateDevSignerKeys(): DevSignerKeys;
@@ -0,0 +1,67 @@
1
+ export type DevMode = "passthrough" | "simulated";
2
+ export type VerificationOutcome = "verified" | "passthrough" | "rejected";
3
+ export type AuthzDecisionEntry = {
4
+ permission: string;
5
+ decision: "allow" | "deny";
6
+ reason?: string;
7
+ };
8
+ export type MeteringTraceEntry = {
9
+ meters: Record<string, number>;
10
+ source: "response" | "meter";
11
+ };
12
+ export type DevTrace = {
13
+ requestId: string;
14
+ method?: string;
15
+ path?: string;
16
+ persona?: string;
17
+ mode: DevMode;
18
+ verification?: {
19
+ outcome: VerificationOutcome;
20
+ reason?: string;
21
+ };
22
+ authz: AuthzDecisionEntry[];
23
+ metering: MeteringTraceEntry[];
24
+ response?: {
25
+ status: number;
26
+ };
27
+ };
28
+ /** Redact anything that smells like a credential from a free-form string. */
29
+ export declare function redactValue(input: string): string;
30
+ /**
31
+ * Accumulates one `DevTrace` per request id and (optionally) appends each
32
+ * completed trace as a single JSONL line via the injected `appendLine` sink.
33
+ */
34
+ export declare class DevTraceSink {
35
+ private readonly traces;
36
+ private readonly flushed;
37
+ private readonly appendLine?;
38
+ constructor(options?: {
39
+ appendLine?: (line: string) => void;
40
+ });
41
+ private ensure;
42
+ /** Record how a request's signature/context verification resolved. */
43
+ recordVerification(requestId: string, mode: DevMode, outcome: VerificationOutcome, fields?: {
44
+ method?: string;
45
+ path?: string;
46
+ persona?: string;
47
+ reason?: string;
48
+ }): void;
49
+ /** Record a single hasPermission / requirePermission decision. */
50
+ recordAuthz(requestId: string, mode: DevMode, entry: AuthzDecisionEntry): void;
51
+ /** Record usage reported for this request. */
52
+ recordMetering(requestId: string, mode: DevMode, entry: MeteringTraceEntry): void;
53
+ /** Record the final response status and flush the trace as one JSONL line. */
54
+ recordResponse(requestId: string, mode: DevMode, status: number): void;
55
+ /**
56
+ * Append a trace's current state as one JSONL line (if a sink is wired).
57
+ * Flushes at most ONCE per request id, so wrapping several response methods
58
+ * (status/json/end) never produces duplicate JSONL lines.
59
+ */
60
+ flush(requestId: string): void;
61
+ /** The accumulated trace for a request id, or `undefined`. */
62
+ forRequest(requestId: string): DevTrace | undefined;
63
+ /** Every accumulated trace (insertion order). */
64
+ all(): DevTrace[];
65
+ /** Clear all traces. */
66
+ reset(): void;
67
+ }