@farthershore/backend 0.1.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,37 @@
1
+ import type { FartherShore } from "../core/runtime.js";
2
+ import type { FartherShoreRequestContext } from "../core/verifyRequest.js";
3
+ /** Minimal Express-shaped types so we don't hard-depend on @types/express. */
4
+ export type ExpressRequestLike = {
5
+ method: string;
6
+ /** Original URL incl. query, e.g. "/v1/x?a=1". */
7
+ originalUrl?: string;
8
+ url?: string;
9
+ path?: string;
10
+ headers: Record<string, string | string[] | undefined>;
11
+ /** Raw body bytes if captured by an upstream raw parser. */
12
+ rawBody?: Buffer | Uint8Array;
13
+ body?: unknown;
14
+ fartherShore?: FartherShoreRequestContext;
15
+ };
16
+ export type ExpressResponseLike = {
17
+ status(code: number): ExpressResponseLike;
18
+ json(body: unknown): unknown;
19
+ setHeader(name: string, value: string): void;
20
+ };
21
+ export type ExpressNext = (err?: unknown) => void;
22
+ export type ExpressMiddleware = (req: ExpressRequestLike, res: ExpressResponseLike, next: ExpressNext) => void;
23
+ export type MiddlewareOptions = {
24
+ /**
25
+ * When true (default), a request is verified only if verification is REQUIRED
26
+ * by bootstrap. While verificationRequired=false (pre-keystone) the middleware
27
+ * passes requests through WITHOUT attaching a context — matching the deploy
28
+ * order where the signer is not yet live. Set `always: true` to verify
29
+ * regardless (the readiness harness / strict deployments).
30
+ */
31
+ always?: boolean;
32
+ };
33
+ /**
34
+ * Build the Express middleware. Captures raw body bytes, calls verifyRequest,
35
+ * and fail-closes on any error.
36
+ */
37
+ export declare function createExpressMiddleware(fs: FartherShore, options?: MiddlewareOptions): ExpressMiddleware;
@@ -0,0 +1,40 @@
1
+ import { type RuntimeBootstrapRequest, type RuntimeBootstrapResponse } from "../runtime-types.js";
2
+ /** Where to reach core's bootstrap endpoint. Derived from the token's coreUrl. */
3
+ export type BootstrapClientOptions = {
4
+ runtimeToken: string;
5
+ /** Core base URL, e.g. https://api.farthershore.com. */
6
+ coreUrl: string;
7
+ /** Optional bootstrap request metadata. */
8
+ request?: RuntimeBootstrapRequest;
9
+ /** Injectable fetch (tests). */
10
+ fetchImpl?: typeof fetch;
11
+ /** Injectable clock in ms (tests). */
12
+ now?: () => number;
13
+ /** Minimum seconds between refreshes regardless of server hint. */
14
+ minRefreshSeconds?: number;
15
+ };
16
+ /**
17
+ * Caches the bootstrap response and refreshes it lazily. `get()` returns the
18
+ * cached value when fresh, otherwise refreshes (single-flight).
19
+ */
20
+ export declare class BootstrapClient {
21
+ private readonly runtimeToken;
22
+ private readonly endpoint;
23
+ private readonly request;
24
+ private readonly fetchImpl;
25
+ private readonly now;
26
+ private readonly minRefreshSeconds;
27
+ private cached;
28
+ private fetchedAt;
29
+ private refreshAfterMs;
30
+ private inflight;
31
+ constructor(options: BootstrapClientOptions);
32
+ /** Cached config when fresh; otherwise refreshes. */
33
+ get(): Promise<RuntimeBootstrapResponse>;
34
+ /** Force a network refresh (single-flight). */
35
+ refresh(): Promise<RuntimeBootstrapResponse>;
36
+ /** Last cached value without triggering a refresh (null until bootstrapped). */
37
+ peek(): RuntimeBootstrapResponse | null;
38
+ private isStale;
39
+ private doBootstrap;
40
+ }
@@ -0,0 +1,15 @@
1
+ import type { RuntimeErrorCode } from "../generated/runtime-contract.js";
2
+ /**
3
+ * A verification / runtime failure with a stable, cross-language `code` and the
4
+ * fail-closed HTTP status the adapter should emit.
5
+ */
6
+ export declare class FartherShoreError extends Error {
7
+ readonly code: RuntimeErrorCode;
8
+ readonly status: number;
9
+ constructor(code: RuntimeErrorCode, message: string, status?: number);
10
+ }
11
+ /**
12
+ * Map a runtime error code to its fail-closed HTTP status. Oversized bodies are
13
+ * the only non-401 (413); all other verification failures are 401.
14
+ */
15
+ export declare function statusForCode(code: RuntimeErrorCode): number;
@@ -0,0 +1,23 @@
1
+ import type { RuntimeHealthReport } from "../runtime-types.js";
2
+ export type HealthStatus = "starting" | "ready" | "degraded" | "stopping";
3
+ export type HealthSnapshot = {
4
+ runtimeToken: boolean;
5
+ bootstrap: boolean;
6
+ tunnel: string | null;
7
+ verification: boolean;
8
+ metering: boolean;
9
+ };
10
+ /** Build the fs.health() report from the current snapshot. */
11
+ export declare function buildHealthReport(snapshot: HealthSnapshot): RuntimeHealthReport;
12
+ export type HeartbeatOptions = {
13
+ runtimeToken: string;
14
+ coreUrl: string;
15
+ status: HealthStatus;
16
+ instanceId?: string;
17
+ fetchImpl?: typeof fetch;
18
+ };
19
+ /**
20
+ * POST a heartbeat to core. Best-effort: resolves false on any failure rather
21
+ * than throwing (tunnel/health failure ≠ verification — must not crash the app).
22
+ */
23
+ export declare function reportHealth(options: HeartbeatOptions): Promise<boolean>;
@@ -0,0 +1,48 @@
1
+ export type Jwk = JsonWebKey & {
2
+ kid?: string;
3
+ };
4
+ export type JwksClientOptions = {
5
+ jwksUrl: string;
6
+ /** Injectable fetch (tests). Defaults to globalThis.fetch. */
7
+ fetchImpl?: typeof fetch;
8
+ /** How long a successful fetch stays fresh before a background refresh. */
9
+ cacheTtlMs?: number;
10
+ /** How long an unknown-kid result is negatively cached (avoids hammering). */
11
+ negativeCacheMs?: number;
12
+ /** Injectable clock (tests). */
13
+ now?: () => number;
14
+ };
15
+ /**
16
+ * Caching JWKS client. Holds the last successful key set indefinitely as a
17
+ * warm fallback (stale-while-revalidate) and only fails closed when it has
18
+ * NEVER successfully fetched (cold cache).
19
+ */
20
+ export declare class JwksClient {
21
+ private readonly jwksUrl;
22
+ private readonly fetchImpl;
23
+ private readonly cacheTtlMs;
24
+ private readonly negativeCacheMs;
25
+ private readonly now;
26
+ private keysByKid;
27
+ private fetchedAt;
28
+ private hasFetchedOnce;
29
+ private inflight;
30
+ private readonly negativeKids;
31
+ constructor(options: JwksClientOptions);
32
+ /**
33
+ * Resolve a public JWK for `kid`, fail-closed. Throws FartherShoreError with
34
+ * `jwks_unavailable` (cold cache + fetch failed) or `unknown_key_id`.
35
+ */
36
+ getKey(kid: string): Promise<Jwk>;
37
+ /** Record a confirmed-missing kid, evicting the oldest if at capacity. */
38
+ private rememberMissingKid;
39
+ private isStale;
40
+ /** Single-flight refresh: concurrent callers share one fetch. */
41
+ private refresh;
42
+ private doFetch;
43
+ /**
44
+ * Stale-while-revalidate: with a warm cache, swallow the refresh failure and
45
+ * keep serving the last-known keys. With a COLD cache, fail closed.
46
+ */
47
+ private failOnColdCache;
48
+ }
@@ -0,0 +1,49 @@
1
+ import type { RuntimeMeteringConfig } from "../runtime-types.js";
2
+ export type MeterOptions = {
3
+ requestId?: string;
4
+ routeId?: string;
5
+ /** Override event_id (idempotency key). Defaults to a random uuid. */
6
+ eventId?: string;
7
+ /** Override the timestamp (ISO-8601). Defaults to now. */
8
+ timestamp?: string;
9
+ };
10
+ export type MeteringClientOptions = {
11
+ config: RuntimeMeteringConfig;
12
+ productId: string;
13
+ backendId: string;
14
+ /** Core base URL when the config endpoint is a relative path. */
15
+ coreUrl?: string;
16
+ fetchImpl?: typeof fetch;
17
+ /** Max retry attempts per flush before re-buffering. */
18
+ maxRetries?: number;
19
+ /** Injectable id generator (tests). */
20
+ newId?: () => string;
21
+ now?: () => Date;
22
+ };
23
+ /**
24
+ * Buffers + flushes metering events. `meter()` validates and enqueues; `flush()`
25
+ * drains the buffer (re-buffering on failure for at-least-once delivery).
26
+ */
27
+ export declare class MeteringClient {
28
+ private readonly config;
29
+ private readonly endpoint;
30
+ private readonly productId;
31
+ private readonly backendId;
32
+ private readonly fetchImpl;
33
+ private readonly maxRetries;
34
+ private readonly newId;
35
+ private readonly now;
36
+ private readonly buffer;
37
+ constructor(options: MeteringClientOptions);
38
+ /**
39
+ * Record `qty` of `meter`. Enforces meter-key shape, non-negative finite qty,
40
+ * the bootstrap allowedMeters/allowedRoutes scope, and the per-event sanity
41
+ * max, then enqueues and flushes (best-effort; failures stay buffered).
42
+ */
43
+ meter(meter: string, qty: number, options?: MeterOptions): Promise<void>;
44
+ /** Drain the buffer. Events that fail all retries stay buffered (at-least-once). */
45
+ flush(): Promise<void>;
46
+ /** Buffered-but-unsent count (observability/tests). */
47
+ get pending(): number;
48
+ private sendWithRetry;
49
+ }
@@ -0,0 +1,29 @@
1
+ export type NonceCacheOptions = {
2
+ /** Max distinct nonces retained. Oldest are evicted first. */
3
+ maxEntries?: number;
4
+ /** TTL after which a nonce is forgotten (≥ replay window + skew). */
5
+ ttlMs?: number;
6
+ /** Injectable clock (tests). */
7
+ now?: () => number;
8
+ };
9
+ /**
10
+ * Bounded nonce cache. `checkAndRemember(id)` returns false (and records the id)
11
+ * the first time it sees an id, and true (replay) on any subsequent sighting
12
+ * while the id is still retained.
13
+ */
14
+ export declare class NonceCache {
15
+ private readonly maxEntries;
16
+ private readonly ttlMs;
17
+ private readonly now;
18
+ private readonly seen;
19
+ constructor(options?: NonceCacheOptions);
20
+ /**
21
+ * @returns true if `id` was already seen (a REPLAY); false on first sight
22
+ * (the id is then remembered).
23
+ */
24
+ checkAndRemember(id: string): boolean;
25
+ /** Number of retained nonces (test/observability hook). */
26
+ get size(): number;
27
+ private evictExpired;
28
+ private evictOverflow;
29
+ }
@@ -0,0 +1,96 @@
1
+ import { type RuntimeBootstrapResponse, type RuntimeHealthReport } from "../runtime-types.js";
2
+ import { type MeterOptions } from "./metering.js";
3
+ import { type SpawnFn } from "./tunnel.js";
4
+ import { type FartherShoreRequestContext, type VerifyRequestInput } from "./verifyRequest.js";
5
+ /** Advanced opt-in tunnel config. The managed runner is the default DX. */
6
+ export type FartherShoreTunnelOptions = {
7
+ /** Opt out of the managed cloudflared runner (e.g. sidecar mode). */
8
+ enabled?: boolean;
9
+ /** Injected spawner (tests/non-default hosts). Defaults to node:child_process. */
10
+ spawn?: SpawnFn;
11
+ /** Explicit cloudflared binary path. */
12
+ binaryPath?: string;
13
+ /** Crash the host app if the tunnel cannot start. Default: fail-open. */
14
+ failClosed?: boolean;
15
+ /** Log sink for redacted cloudflared output. */
16
+ logger?: (line: string) => void;
17
+ };
18
+ export type FartherShoreInitOptions = {
19
+ /** Explicit runtime token. Defaults to process.env.FS_RUNTIME_TOKEN. */
20
+ runtimeToken?: string;
21
+ /**
22
+ * Core base URL. Defaults to FS_CORE_URL / FARTHERSHORE_CORE_URL or
23
+ * https://api.farthershore.com.
24
+ */
25
+ coreUrl?: string;
26
+ /** Env map (tests). Defaults to process.env. */
27
+ env?: Record<string, string | undefined>;
28
+ /** Injectable fetch (tests). */
29
+ fetchImpl?: typeof fetch;
30
+ /** Optional advanced opt-outs (default: everything on). */
31
+ verification?: {
32
+ enabled?: boolean;
33
+ };
34
+ metering?: {
35
+ enabled?: boolean;
36
+ };
37
+ /** Managed-cloudflared runner config (advanced opt-in; default DX is on). */
38
+ tunnel?: FartherShoreTunnelOptions;
39
+ /** SDK metadata forwarded to bootstrap. */
40
+ instanceId?: string;
41
+ };
42
+ /**
43
+ * The runtime instance. Lazily bootstraps; holds the JWKS client, nonce cache,
44
+ * metering buffer, and shutdown hooks.
45
+ */
46
+ export declare class FartherShore {
47
+ private readonly bootstrapClient;
48
+ private readonly fetchImpl;
49
+ private readonly verificationEnabled;
50
+ private readonly meteringEnabledOverride;
51
+ private readonly runtimeToken;
52
+ private readonly coreUrl;
53
+ private readonly instanceId?;
54
+ private readonly tunnelOptions;
55
+ private readonly nonceCache;
56
+ private readonly shutdownManager;
57
+ private jwks;
58
+ private meteringClient;
59
+ private tunnel;
60
+ private bootstrapped;
61
+ constructor(options?: FartherShoreInitOptions);
62
+ /** Ensure bootstrap config is loaded; build the JWKS + metering clients. */
63
+ ensureBootstrapped(): Promise<RuntimeBootstrapResponse>;
64
+ /**
65
+ * Framework-neutral verification primitive. Fail-closed: throws a typed
66
+ * FartherShoreError on any verification failure. Returns the verified context.
67
+ */
68
+ verifyRequest(input: VerifyRequestInput): Promise<FartherShoreRequestContext>;
69
+ /** Whether verification is required (bootstrap × opt-out). */
70
+ verificationRequired(): Promise<boolean>;
71
+ /**
72
+ * Start the managed runner. For a `cloudflare_tunnel` backend whose runner is
73
+ * `managed_cloudflared`, this supervises `cloudflared` as a child process
74
+ * (spawned via the injected/default spawner) using the tunnel token from
75
+ * bootstrap. For every other transport (`public_origin`, `mtls`, or the
76
+ * `sidecar` runner) it is a no-op — there is no SDK-managed process to run.
77
+ *
78
+ * Fail-open by default: a tunnel that cannot start does NOT crash the host app
79
+ * (request verification stays fail-closed regardless — a different axis).
80
+ */
81
+ start(): Promise<void>;
82
+ /** Record metering usage (billing-only). */
83
+ meter(meter: string, qty: number, options?: MeterOptions): Promise<void>;
84
+ /** Current local health report. */
85
+ health(): RuntimeHealthReport;
86
+ /** Graceful shutdown: flush metering + send a stopping heartbeat. */
87
+ shutdown(): Promise<void>;
88
+ /** Register an additional shutdown hook (e.g. the cloudflared supervisor). */
89
+ onShutdown(hook: () => Promise<void> | void): void;
90
+ }
91
+ /**
92
+ * Construct a FartherShore instance from the environment. Derives EVERYTHING
93
+ * from FS_RUNTIME_TOKEN; throws missing_token / invalid_token eagerly if the
94
+ * token is absent or mis-prefixed.
95
+ */
96
+ export declare function initFromEnv(options?: FartherShoreInitOptions): FartherShore;
@@ -0,0 +1,10 @@
1
+ export type ShutdownHook = () => Promise<void> | void;
2
+ /** LIFO registry of shutdown hooks, each isolated from sibling failures. */
3
+ export declare class ShutdownManager {
4
+ private readonly hooks;
5
+ private done;
6
+ register(hook: ShutdownHook): void;
7
+ get isShutDown(): boolean;
8
+ /** Run all hooks in reverse order, isolating failures. Idempotent. */
9
+ shutdown(): Promise<void>;
10
+ }
@@ -0,0 +1,159 @@
1
+ import type { EventEmitter } from "node:events";
2
+ /** A line emitter — the subset of a child stdio stream we consume. */
3
+ type StdioStream = Pick<EventEmitter, "on">;
4
+ /**
5
+ * The minimal child-process surface the supervisor needs. A real
6
+ * `child_process.ChildProcess` satisfies this; tests pass a fake. We deliberately
7
+ * do NOT depend on the full ChildProcess type so the spawner stays injectable and
8
+ * the SDK does not pull a Node-only process model into the language-neutral core.
9
+ */
10
+ export interface SpawnedTunnelProcess {
11
+ readonly stdout: StdioStream | null;
12
+ readonly stderr: StdioStream | null;
13
+ readonly pid?: number;
14
+ /** Register the process-exit handler. */
15
+ on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
16
+ /** Signal the child to stop. */
17
+ kill(signal?: NodeJS.Signals | number): boolean;
18
+ }
19
+ /**
20
+ * The injected spawner port. In production this is a thin wrapper over
21
+ * `child_process.spawn`; in tests it is a fake that records argv. It MUST NOT be
22
+ * passed `local_target` — routing is owned by Cloudflare.
23
+ */
24
+ export type SpawnFn = (command: string, args: string[], options?: {
25
+ env?: NodeJS.ProcessEnv;
26
+ }) => SpawnedTunnelProcess;
27
+ /** Supervisor lifecycle states, surfaced through `status()` / `healthString()`. */
28
+ export type TunnelState = "stopped" | "starting" | "running" | "restarting" | "error";
29
+ /** The sentinel substituted for the tunnel token in any logged/serialized text. */
30
+ export declare const REDACTED_TOKEN = "***REDACTED***";
31
+ export type CloudflaredSupervisorOptions = {
32
+ /**
33
+ * The Cloudflare tunnel token (transport identity for `cloudflared`). HIGHLY
34
+ * sensitive: kept in memory, never logged, redacted everywhere. (Distinct from
35
+ * the runtime token / app identity.)
36
+ */
37
+ tunnelToken: string;
38
+ /** Injected spawner (required; tests pass a fake — never a real process). */
39
+ spawn: SpawnFn;
40
+ /**
41
+ * Explicit binary path. When omitted, `locateBinary` resolves it (Linux-first
42
+ * candidates). When neither resolves, start fails with a clear error.
43
+ */
44
+ binaryPath?: string;
45
+ /** Injected binary locator. Returns an absolute path or null if not found. */
46
+ locateBinary?: () => string | null;
47
+ /** Log sink for redacted cloudflared output. Defaults to console.error. */
48
+ logger?: (line: string) => void;
49
+ /** Error-surface callback (fail-open path). Receives a token-redacted error. */
50
+ onError?: (error: Error) => void;
51
+ /**
52
+ * When true, a spawn failure rejects `start()` (the host opted into letting a
53
+ * tunnel failure stop the app). Default false: tunnel failure ≠ app crash.
54
+ * (Request *verification* is always fail-closed regardless — different axis.)
55
+ */
56
+ failClosed?: boolean;
57
+ /** Base backoff for the first restart (doubles each consecutive failure). */
58
+ baseBackoffMs?: number;
59
+ /** Backoff ceiling. */
60
+ maxBackoffMs?: number;
61
+ /** Injectable timer (tests use fake timers / a custom scheduler). */
62
+ setTimeoutFn?: (cb: () => void, ms: number) => unknown;
63
+ clearTimeoutFn?: (handle: unknown) => void;
64
+ /** Process env passed to the child (tunnel token is NOT injected via env). */
65
+ childEnv?: NodeJS.ProcessEnv;
66
+ };
67
+ /** A read-only snapshot of the supervisor — safe to serialize (token-free). */
68
+ export type TunnelStatus = {
69
+ state: TunnelState;
70
+ pid: number | null;
71
+ restarts: number;
72
+ /** Token-redacted last error message, if any. */
73
+ lastError: string | null;
74
+ };
75
+ /**
76
+ * Supervises a single `cloudflared` child process. One supervisor ⇒ one tunnel
77
+ * (one backend). Restarts on unexpected exit; stays down after `shutdown()`.
78
+ */
79
+ export declare class CloudflaredSupervisor {
80
+ private readonly tunnelToken;
81
+ private readonly spawn;
82
+ private readonly binaryPath;
83
+ private readonly locateBinary;
84
+ private readonly logger;
85
+ private readonly onError;
86
+ private readonly failClosed;
87
+ private readonly baseBackoffMs;
88
+ private readonly maxBackoffMs;
89
+ private readonly setTimeoutFn;
90
+ private readonly clearTimeoutFn;
91
+ private readonly childEnv;
92
+ private child;
93
+ private state;
94
+ private restarts;
95
+ private consecutiveFailures;
96
+ private lastError;
97
+ private intentionalStop;
98
+ private restartTimer;
99
+ private signalsBound;
100
+ private readonly signalHandler;
101
+ constructor(options: CloudflaredSupervisorOptions);
102
+ /**
103
+ * Resolve the binary, spawn the child, wire log piping + exit handling, and
104
+ * bind SIGTERM/SIGINT. Fail-open by default (resolves; error in `status()`);
105
+ * fail-closed when configured (rejects).
106
+ *
107
+ * Async by contract: the public lifecycle API (and a future binary
108
+ * download/health-probe step) is Promise-returning even when today's body is
109
+ * synchronous, so callers can always `await fs.start()`.
110
+ */
111
+ start(): Promise<void>;
112
+ /**
113
+ * Intentional graceful stop: cancel any pending restart, signal the child
114
+ * (SIGTERM), unbind signals, and mark the supervisor stopped. Any exit that
115
+ * the kill provokes will NOT trigger a restart. Async by lifecycle contract.
116
+ */
117
+ shutdown(): Promise<void>;
118
+ /** Token-free status snapshot for diagnostics / health. */
119
+ status(): TunnelStatus;
120
+ /** Compact health string for `fs.health().tunnel`. */
121
+ healthString(): string;
122
+ private spawnChild;
123
+ /**
124
+ * Resolve the cloudflared binary path. Explicit `binaryPath` wins; otherwise
125
+ * the injected locator runs (Linux-first). Missing ⇒ a clear, redacted error.
126
+ */
127
+ private resolveBinary;
128
+ /** Pipe stdout/stderr to the logger with the tunnel token redacted. */
129
+ private pipeLogs;
130
+ /**
131
+ * Build a per-stream `data` handler that BUFFERS partial lines across chunks
132
+ * before redacting. The OS can deliver a single log line in two `data` events
133
+ * with the boundary mid-token; redacting each chunk independently would let
134
+ * the two token halves slip through. Buffering until a newline reassembles
135
+ * the full line (a token never contains a newline), so redaction always sees
136
+ * the whole token. A trailing partial is held for the next chunk, or flushed
137
+ * if it grows past a safety cap (cloudflared is line-oriented, so this is a
138
+ * belt-and-suspenders guard against unbounded buffer growth).
139
+ */
140
+ private makeLineSink;
141
+ private emitLogLine;
142
+ /** Replace every occurrence of the tunnel token with the sentinel. */
143
+ private redact;
144
+ private handleExit;
145
+ private scheduleRestart;
146
+ private backoffDelay;
147
+ private handleStartFailure;
148
+ private recordError;
149
+ private bindSignals;
150
+ private unbindSignals;
151
+ }
152
+ /**
153
+ * The default production spawner: a thin wrapper over Node's
154
+ * `child_process.spawn`. Stdio is piped (so logs flow through the redactor); the
155
+ * tunnel token is passed as an argv item, never via the environment. Tests
156
+ * always inject their own spawner instead of this.
157
+ */
158
+ export declare function nodeSpawn(): SpawnFn;
159
+ export {};
@@ -0,0 +1,54 @@
1
+ import type { JwksClient } from "./jwks.js";
2
+ import type { NonceCache } from "./nonceCache.js";
3
+ /** Per-request input. `headers` keys are matched case-insensitively. */
4
+ export type VerifyRequestInput = {
5
+ method: string;
6
+ /** Path only (no host, no query). */
7
+ path: string;
8
+ /** Raw query string (with or without leading '?'); "" when absent. */
9
+ query?: string;
10
+ headers: HeadersLike;
11
+ /** Raw request bytes captured pre-parser; null/undefined for empty body. */
12
+ body?: Uint8Array | null;
13
+ /**
14
+ * True when the request is body-hash-exempt (streaming). The signed hash must
15
+ * then be the STREAM sentinel and the body is not hashed (size cap still
16
+ * applies upstream).
17
+ */
18
+ streamingExempt?: boolean;
19
+ };
20
+ export type HeadersLike = Headers | Record<string, string | string[] | undefined>;
21
+ /** The verified request context attached to req.fartherShore. */
22
+ export type FartherShoreRequestContext = {
23
+ requestId: string;
24
+ productId: string;
25
+ backendId: string;
26
+ routeId: string;
27
+ policyVersion: string;
28
+ timestamp: number;
29
+ bodyHash: string;
30
+ /** Filled by the host bootstrap layer (tenant/customer), not by verify. */
31
+ tenantId?: string;
32
+ customerId?: string;
33
+ meters?: string[];
34
+ features?: Record<string, unknown>;
35
+ };
36
+ export type VerifyRequestDeps = {
37
+ jwks: JwksClient;
38
+ nonceCache: NonceCache;
39
+ /** Expected product id (from bootstrap). When set, must match the signed claim. */
40
+ productId?: string;
41
+ /** Expected backend id (from bootstrap). When set, must match. */
42
+ backendId?: string;
43
+ /**
44
+ * Set of route ids this backend serves (from bootstrap). When provided AND
45
+ * the signed route-id is non-empty, the signed route must be a member —
46
+ * otherwise route_mismatch.
47
+ */
48
+ knownRouteIds?: ReadonlySet<string>;
49
+ clockSkewSeconds?: number;
50
+ replayWindowSeconds?: number;
51
+ /** Injectable clock (seconds since epoch). */
52
+ nowSeconds?: () => number;
53
+ };
54
+ export declare function verifyRequest(input: VerifyRequestInput, deps: VerifyRequestDeps): Promise<FartherShoreRequestContext>;
@@ -0,0 +1,36 @@
1
+ export declare const METERING_CONTRACT_VERSION: 1;
2
+ export declare const METERING_HEADERS: {
3
+ readonly payload: "x-fs-metering";
4
+ readonly signature: "x-fs-metering-sig";
5
+ readonly token: "x-fs-metering-token";
6
+ };
7
+ export declare const METERING_TOKEN_ENV: "FARTHERSHORE_METERING_TOKEN";
8
+ export declare const METERING_TOKEN_CONTRACT: {
9
+ readonly environmentVariable: "FARTHERSHORE_METERING_TOKEN";
10
+ readonly presentation: "x-fs-metering-token";
11
+ readonly storage: "sha256-hash-only";
12
+ };
13
+ export declare const METERING_SIGNATURE_CONTRACT: {
14
+ readonly algorithm: "HMAC-SHA256";
15
+ readonly encoding: "base64url";
16
+ readonly input: "payload-json";
17
+ readonly secret: "presented-metering-token";
18
+ };
19
+ export declare const METERING_ERROR_CODES: {
20
+ readonly missingToken: "missing_token";
21
+ readonly invalidMeterKey: "invalid_meter_key";
22
+ readonly invalidMeterValue: "invalid_meter_value";
23
+ };
24
+ export type MeteringErrorCode = (typeof METERING_ERROR_CODES)[keyof typeof METERING_ERROR_CODES];
25
+ export declare const METERING_HTTP_ADAPTER_CONTRACT: {
26
+ readonly input: "Request";
27
+ readonly output: "Response";
28
+ readonly networkCalls: false;
29
+ readonly preserves: readonly ["body", "headers", "status", "statusText"];
30
+ readonly gatewayStripsInternalHeaders: true;
31
+ };
32
+ export type MeteringUsagePayload = {
33
+ method: string;
34
+ path: string;
35
+ rawDimsUnits: Record<string, number>;
36
+ };