@farthershore/backend 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import type { FartherShoreSignedContext } from "./verifyContext.js";
1
2
  /**
2
3
  * Thrown by {@link requirePermission} when the acting user lacks a permission.
3
4
  * Distinct from {@link FartherShoreError} (which models signing/verification
@@ -13,25 +14,26 @@ export declare class FartherShorePermissionError extends Error {
13
14
  }
14
15
  /**
15
16
  * Parse the comma-joined `x-fs-permissions` header into a permission list.
16
- * Returns `undefined` when the header is absent (full-access grace) and `[]`
17
- * for a present-but-empty header (deny-all — an authenticated user with no
18
- * grants). Whitespace-trimmed; empty segments dropped.
17
+ * Returns `undefined` when the header is absent (no permission source
18
+ * carrier-level DENY under FAR-723) and `[]` for a present-but-empty header
19
+ * (deny-all — an authenticated user with no grants). Whitespace-trimmed; empty
20
+ * segments dropped.
19
21
  */
20
22
  export declare function parsePermissionHeader(raw: string | null | undefined): string[] | undefined;
21
23
  /**
22
- * Pure grant check. `undefined` permissions (no header) grant everything
23
- * this backend SDK's LOCAL policy, parity with the edge treating an absent
24
- * claim as `["*"]`. Over a DEFINED array the rule is the shared core primitive:
25
- * a `"*"` entry grants everything, otherwise the key must be an exact member.
24
+ * Pure grant check a FAITHFUL COPY of the canonical `permissionGrants` in
25
+ * `@farthershore/contracts` (`rbac.ts`). Over a DEFINED array: a `"*"` entry
26
+ * grants everything, otherwise the key must be an exact member. Its
27
+ * `undefined true` codomain is the PRIMITIVE's contract (kept byte-identical
28
+ * to contracts for parity); it is NOT the SDK's carrier policy. Under FAR-723
29
+ * the carrier gate {@link hasPermission} DENIES an absent permission set before
30
+ * this primitive is consulted, so absence never grants at the boundary.
26
31
  *
27
- * The defined-array branch is a FAITHFUL COPY of the canonical
28
- * `permissionGrants` in `@farthershore/contracts` (`rbac.ts`) — the published
29
- * SDK surface must stay contracts-free, so it can't import it. A TEST-ONLY
30
- * parity guard (`permissions-parity.test.ts`, which CAN import contracts as a
31
- * devDependency) asserts this copy agrees with the canonical primitive across a
32
- * shared golden vector table, so the copy can't silently drift. The
33
- * `undefined → grant-all` grace is documented here as the SDK's own policy; the
34
- * shared primitive only covers the defined-array rule.
32
+ * The published SDK surface must stay contracts-free, so it can't import the
33
+ * canonical primitive. A TEST-ONLY parity guard (`permissions-parity.test.ts`,
34
+ * which CAN import contracts as a devDependency) asserts this copy agrees with
35
+ * the canonical primitive across a shared golden vector table, so the copy
36
+ * can't silently drift.
35
37
  */
36
38
  export declare function permissionGrants(permissions: readonly string[] | undefined, key: string): boolean;
37
39
  /**
@@ -39,8 +41,11 @@ export declare function permissionGrants(permissions: readonly string[] | undefi
39
41
  * unified grammar: `"*"` (global), `"<subject>:*"` (subject wildcard), or the
40
42
  * EXACT key. NO verb-class widening (class forms are expanded to concrete verbs
41
43
  * server-side at save time). Superset of {@link permissionGrants} — it adds the
42
- * `<subject>:*` rung and keeps the backend SDK's `undefined → grant-all`
43
- * grace (absent `x-fs-permissions` header full access, today's behavior).
44
+ * `<subject>:*` rung. NOTE: the `granted === undefined → true` codomain here is
45
+ * the PRIMITIVE's contract (kept byte-identical to contracts for parity); it is
46
+ * NOT the SDK's carrier policy. Callers gate through {@link hasPermission},
47
+ * which under FAR-723 DENIES an absent permission set before ever reaching this
48
+ * primitive — so absence never grants at the carrier level.
44
49
  *
45
50
  * FAITHFUL COPY of the canonical `permissionSatisfies` in
46
51
  * `@farthershore/contracts` (`authz/verbs.ts`); the published bundle is
@@ -51,13 +56,26 @@ export declare function permissionSatisfies(required: string, granted: readonly
51
56
  /** The subset of a verified context these helpers read. */
52
57
  export interface PermissionCarrier {
53
58
  permissions?: readonly string[];
59
+ /** The verified X-Fs-Context claims, when the request carried a valid token. */
60
+ signedContext?: FartherShoreSignedContext;
54
61
  }
55
62
  /**
56
63
  * True when the acting user holds `key`. Call only with a verified request
57
64
  * context ({@link parsePermissionHeader} output lives on `context.permissions`).
58
65
  * NOTE: this is a convenience for in-handler gating; the edge `permission`
59
- * constraint is the security boundary for route-level access. Uses the unified
60
- * {@link permissionSatisfies} rule (`*` / `<subject>:*` / exact).
66
+ * constraint is the security boundary for route-level access.
67
+ *
68
+ * FAR-723 FAIL-CLOSED: an ABSENT permission set (`ctx.permissions === undefined`)
69
+ * DENIES — the reverse of the former grant-all grace — UNLESS the request
70
+ * carried a cryptographically VERIFIED context token that simply omits the
71
+ * `perms` claim. Core deliberately mints ORG-actor (and unidentified-user-actor)
72
+ * `fsc_` tokens with no `perms` claim — RBAC never applies to them, and the edge
73
+ * `permission` constraint treats a verified claimless token as full access. The
74
+ * carrier mirrors the edge: verified-but-claimless ⇒ grant; no verified context
75
+ * AND no permission set ⇒ no trusted permission source ⇒ deny. A DEFINED array
76
+ * uses the unified {@link permissionSatisfies} rule (`*` / `<subject>:*` /
77
+ * exact), so an explicit `["*"]` (org OWNER / RBAC-disabled) still grants
78
+ * everything and `[]` denies.
61
79
  */
62
80
  export declare function hasPermission(ctx: PermissionCarrier, key: string): boolean;
63
81
  /**
@@ -39,6 +39,38 @@ export type FartherShoreInitOptions = {
39
39
  tunnel?: FartherShoreTunnelOptions;
40
40
  /** SDK metadata forwarded to bootstrap. */
41
41
  instanceId?: string;
42
+ /**
43
+ * FAR-723 / UA-6 — HS256 secret(s) for verifying the gateway's SIGNED
44
+ * `X-Fs-Context` claim. These are the GATEWAY CONTEXT-SIGNING keyring values
45
+ * (`CONTEXT_SIGNING_KEYS_JSON` — the keys `forward-upstream` stamps the
46
+ * header with; supply every live key during rotation — try-all). They are
47
+ * NOT the product's `contextTokenSecret`, which signs `fsc_` INGRESS tokens
48
+ * verified BY the gateway — setting that here would reject every valid
49
+ * gateway request in `"required"` mode. When present, a VERIFIED context's
50
+ * `permissions`/`roles` claims are the AUTHORITATIVE identity source
51
+ * (preferred over the transitional unsigned `x-fs-permissions`/`x-fs-roles`
52
+ * headers). Defaults to `FS_CONTEXT_SECRETS` (comma-separated) from the env.
53
+ *
54
+ * NOTE (core-side dependency, FAR-723 publish gate): no bootstrap field
55
+ * carries these keys yet, and handing the raw platform keyring to builder
56
+ * backends is NOT the end-state (it would allow cross-product context
57
+ * forgery). The distribution mechanism — per-product derived keys or an
58
+ * asymmetric context signature verified via JWKS like the request
59
+ * signature — is decided at the FAR-723 gate before the headers retire;
60
+ * until then this option (or `FS_CONTEXT_SECRETS`) is the manual wiring for
61
+ * platform-operated deployments.
62
+ */
63
+ contextSecrets?: readonly string[];
64
+ /**
65
+ * FAR-723 / UA-6 — `"preferred"` (default): a missing/unverifiable signed
66
+ * context falls back to the transitional unsigned headers.
67
+ * `"required"`: a missing or invalid signed context REJECTS the request
68
+ * (fail-closed), including when the context-secret keyring is empty or
69
+ * unconfigured. Defaults to `FS_CONTEXT_VERIFICATION` from the env, else
70
+ * `"preferred"`. The unsigned fallback is transitional and is removed after
71
+ * the backend-v* publish gate.
72
+ */
73
+ contextVerification?: "preferred" | "required";
42
74
  };
43
75
  export declare const SDK_VERSION: string;
44
76
  export declare const CONTRACTS_FP: string;
@@ -55,6 +87,10 @@ export declare class FartherShore {
55
87
  private readonly coreUrl;
56
88
  private readonly instanceId?;
57
89
  private readonly tunnelOptions;
90
+ /** FAR-723 — HS256 secret(s) for verifying the signed X-Fs-Context claim. */
91
+ private readonly contextSecrets;
92
+ /** FAR-723 — "preferred" (fallback to unsigned) | "required" (fail-closed). */
93
+ private readonly contextVerification;
58
94
  private readonly nonceCache;
59
95
  private readonly shutdownManager;
60
96
  private jwks;
@@ -34,11 +34,12 @@ export type FartherShoreRequestContext = {
34
34
  meters?: string[];
35
35
  features?: Record<string, unknown>;
36
36
  /**
37
- * Managed-RBAC permissions the gateway resolved for the acting user, from
38
- * the UNSIGNED `x-fs-permissions` identity header (trusted transitively on a
39
- * verified request see permissions.ts). `undefined` when the header is
40
- * absent (full-access grace); `[]` for an authenticated user with no grants.
41
- * Read via {@link hasPermission} / {@link requirePermission}.
37
+ * Managed-RBAC permissions the gateway resolved for the acting user
38
+ * preferred from the VERIFIED signed `X-Fs-Context` claim, else the UNSIGNED
39
+ * `x-fs-permissions` identity header (both trusted transitively on a verified
40
+ * request see permissions.ts). `undefined` when NO permission source was
41
+ * present FAR-723 carrier-level DENY; `[]` for an authenticated user with
42
+ * no grants. Read via {@link hasPermission} / {@link requirePermission}.
42
43
  */
43
44
  permissions?: string[];
44
45
  /** UA-6 — the VERIFIED signed context payload, when context secrets are
@@ -19,7 +19,7 @@ export { createExpressMiddleware, type ExpressMiddleware, type ExpressRequestLik
19
19
  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
20
  export { RUNTIME_ERROR_CODES } from "./generated/runtime-contract.js";
21
21
  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";
22
+ 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
23
  /**
24
24
  * The conceptual public entrypoint. `fartherShore.initFromEnv()` mirrors the
25
25
  * 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 {};
@@ -0,0 +1,36 @@
1
+ import { type RuntimeBootstrapResponse, type RuntimeMeteringEvent } 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
+ };
19
+ export type DevGateway = {
20
+ /** Inject this as `fetchImpl` when constructing the FartherShore runtime. */
21
+ fetchImpl: typeof fetch;
22
+ /** The bootstrap response this fixture serves. */
23
+ bootstrap: RuntimeBootstrapResponse;
24
+ /** Every captured background metering event (a capture == an ACK). */
25
+ meterEvents: RuntimeMeteringEvent[];
26
+ productId: string;
27
+ backendId: string;
28
+ jwksUrl: string;
29
+ };
30
+ /**
31
+ * Build the in-process dev gateway. `mode` drives `verification.required`:
32
+ * `passthrough` → false (the adapter passes requests through unverified, matching
33
+ * the pre-keystone deploy order), `simulated` → true (fail-closed verification is
34
+ * exercised end-to-end).
35
+ */
36
+ 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
+ }