@farthershore/backend 0.12.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.
- package/README.md +66 -1
- package/dist/index.js +798 -19
- package/dist/testing/index.js +2716 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/response-metering.d.ts +45 -0
- package/dist/types/testing/devGateway.d.ts +36 -0
- package/dist/types/testing/devRuntime.d.ts +68 -0
- package/dist/types/testing/index.d.ts +8 -0
- package/dist/types/testing/keysFile.d.ts +30 -0
- package/dist/types/testing/personas.d.ts +104 -0
- package/dist/types/testing/prodGuard.d.ts +12 -0
- package/dist/types/testing/signers.d.ts +89 -0
- package/dist/types/testing/traceSink.d.ts +67 -0
- package/dist/types/testing/usageSink.d.ts +31 -0
- package/package.json +6 -2
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RuntimeMeteringEvent } from "../runtime-types.js";
|
|
2
|
+
/** One recorded usage observation. `source` distinguishes the two channels. */
|
|
3
|
+
export type DevUsageEvent = {
|
|
4
|
+
source: "response";
|
|
5
|
+
/** rawDimsUnits from the signed response-metering payload. */
|
|
6
|
+
meters: Record<string, number>;
|
|
7
|
+
/** The full response-metering payload (method/path/rawDimsUnits/…). */
|
|
8
|
+
payload: Record<string, unknown>;
|
|
9
|
+
/** The gateway request id this usage was reported against, when known. */
|
|
10
|
+
requestId?: string;
|
|
11
|
+
at: number;
|
|
12
|
+
} | {
|
|
13
|
+
source: "meter";
|
|
14
|
+
/** `{ [meter]: qty }` for a single background metering event. */
|
|
15
|
+
meters: Record<string, number>;
|
|
16
|
+
event: RuntimeMeteringEvent;
|
|
17
|
+
requestId?: string;
|
|
18
|
+
at: number;
|
|
19
|
+
};
|
|
20
|
+
/** In-memory, assertable sink for all dev usage. */
|
|
21
|
+
export declare class DevUsageSink {
|
|
22
|
+
readonly events: DevUsageEvent[];
|
|
23
|
+
/** Record a signed response-metering payload (withUsage / computeMeteringHeaders). */
|
|
24
|
+
recordResponse(payload: Record<string, unknown>, requestId?: string): void;
|
|
25
|
+
/** Record a background `fs.meter()` event captured by the dev gateway. */
|
|
26
|
+
recordMeterEvent(event: RuntimeMeteringEvent): void;
|
|
27
|
+
/** Total quantity per meter key across every recorded event. */
|
|
28
|
+
byMeter(): Record<string, number>;
|
|
29
|
+
/** Clear all recorded usage. */
|
|
30
|
+
reset(): void;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farthershore/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Farther Shore backend SDK for builder upstreams: signed response usage, fail-closed gateway request verification, health, and lifecycle from FS_RUNTIME_TOKEN",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"./runtime": {
|
|
23
23
|
"types": "./dist/types/generated/runtime-contract.d.ts",
|
|
24
24
|
"import": "./dist/generated/runtime-contract.js"
|
|
25
|
+
},
|
|
26
|
+
"./testing": {
|
|
27
|
+
"types": "./dist/types/testing/index.d.ts",
|
|
28
|
+
"import": "./dist/testing/index.js"
|
|
25
29
|
}
|
|
26
30
|
},
|
|
27
31
|
"files": [
|
|
@@ -33,8 +37,8 @@
|
|
|
33
37
|
},
|
|
34
38
|
"optionalDependencies": {
|
|
35
39
|
"@farthershore/cloudflared-linux-x64": "0.0.0",
|
|
36
|
-
"@farthershore/cloudflared-linux-arm64": "0.0.0",
|
|
37
40
|
"@farthershore/cloudflared-darwin-arm64": "0.0.0",
|
|
41
|
+
"@farthershore/cloudflared-linux-arm64": "0.0.0",
|
|
38
42
|
"@farthershore/cloudflared-darwin-x64": "0.0.0"
|
|
39
43
|
},
|
|
40
44
|
"peerDependencies": {
|