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