@farthershore/backend 0.19.0 → 0.21.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/CHANGELOG.md +139 -0
- package/README.md +267 -91
- package/dist/adapters/express.js +68 -12
- package/dist/generated/runtime-contract.js +21 -236
- package/dist/index.js +837 -465
- package/dist/internal/index.js +587 -0
- package/dist/testing/index.js +935 -353
- package/dist/types/adapters/express.d.ts +32 -3
- package/dist/types/core/bootstrap.d.ts +8 -0
- package/dist/types/core/deadline.d.ts +80 -0
- package/dist/types/core/jwks.d.ts +42 -7
- package/dist/types/core/permissions.d.ts +25 -13
- package/dist/types/core/post-stream-usage.d.ts +23 -4
- package/dist/types/core/replay-protection.d.ts +28 -0
- package/dist/types/core/report.d.ts +133 -0
- package/dist/types/core/runtime.d.ts +44 -20
- package/dist/types/core/verifyRequest.d.ts +21 -3
- package/dist/types/generated/runtime-contract.d.ts +14 -189
- package/dist/types/index.d.ts +30 -8
- package/dist/types/internal/index.d.ts +2 -0
- package/dist/types/response-metering.d.ts +29 -39
- package/dist/types/runtime-types.d.ts +16 -1
- package/dist/types/testing/devRuntime.d.ts +11 -2
- package/dist/types/testing/index.d.ts +1 -0
- package/dist/types/testing/usageSink.d.ts +1 -1
- package/dist/types/testing/webhooks.d.ts +30 -0
- package/dist/types/webhooks/index.d.ts +247 -0
- package/dist/types/webhooks/types.d.ts +110 -0
- package/dist/webhooks/index.js +498 -0
- package/package.json +21 -12
- package/dist/types/core/metering.d.ts +0 -68
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FartherShore } from "../core/runtime.js";
|
|
2
2
|
import type { FartherShoreRequestContext } from "../core/verifyRequest.js";
|
|
3
|
-
import type { ConsumerPrincipal } from "../core/verifyContext.js";
|
|
3
|
+
import type { ConsumerPrincipal, FartherShoreSignedContext } from "../core/verifyContext.js";
|
|
4
4
|
/** Minimal Express-shaped types so we don't hard-depend on @types/express. */
|
|
5
5
|
export type ExpressRequestLike = {
|
|
6
6
|
method: string;
|
|
@@ -18,6 +18,13 @@ export type ExpressResponseLike = {
|
|
|
18
18
|
status(code: number): ExpressResponseLike;
|
|
19
19
|
json(body: unknown): unknown;
|
|
20
20
|
setHeader(name: string, value: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Node's `ServerResponse.headersSent`. This is what makes the reporting verb's
|
|
23
|
+
* transport choice AUTOMATIC: while it is false the measurement rides signed
|
|
24
|
+
* response headers (no network call); once the response is on the wire
|
|
25
|
+
* `ctx.report()` transparently switches to the post-stream channel.
|
|
26
|
+
*/
|
|
27
|
+
headersSent?: boolean;
|
|
21
28
|
};
|
|
22
29
|
export type ExpressNext = (err?: unknown) => void;
|
|
23
30
|
export type ExpressMiddleware = (req: ExpressRequestLike, res: ExpressResponseLike, next: ExpressNext) => void;
|
|
@@ -69,14 +76,27 @@ export type MiddlewareOptions = {
|
|
|
69
76
|
*/
|
|
70
77
|
export type VerifiedPrincipalContext = FartherShoreRequestContext & {
|
|
71
78
|
principal: ConsumerPrincipal;
|
|
79
|
+
signedContext: FartherShoreSignedContext;
|
|
72
80
|
};
|
|
73
81
|
/**
|
|
74
82
|
* A route handler that runs only with a GUARANTEED verified PRINCIPAL. The first
|
|
75
|
-
* argument is the {@link VerifiedPrincipalContext} — read `ctx.principal`
|
|
76
|
-
* narrow with `requireMember`/`requireService`)
|
|
83
|
+
* argument is the {@link VerifiedPrincipalContext} — read `ctx.principal` and
|
|
84
|
+
* `ctx.signedContext` (and narrow with `requireMember`/`requireService`)
|
|
85
|
+
* without any optional-chaining.
|
|
77
86
|
* See {@link createExpressHandler}.
|
|
78
87
|
*/
|
|
79
88
|
export type VerifiedExpressHandler<Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike> = (ctx: VerifiedPrincipalContext, req: Req, res: Res, next: ExpressNext) => void | Promise<void>;
|
|
89
|
+
/** Options for the `fs.handler(options, cb)` overload. */
|
|
90
|
+
export type HandlerOptions = {
|
|
91
|
+
/**
|
|
92
|
+
* Permission key the verified principal must hold (unified grammar:
|
|
93
|
+
* `*` / `<subject>:*` / exact — custom strings work). Checked with the
|
|
94
|
+
* FAIL-CLOSED carrier gate (`requirePermission`): an ABSENT permission set
|
|
95
|
+
* denies. On failure the wrapper responds `403 { error: "permission_denied" }`
|
|
96
|
+
* before the callback runs.
|
|
97
|
+
*/
|
|
98
|
+
permission?: string;
|
|
99
|
+
};
|
|
80
100
|
/**
|
|
81
101
|
* Build the Express middleware. Captures raw body bytes, calls verifyRequest,
|
|
82
102
|
* and fail-closes on any error.
|
|
@@ -106,3 +126,12 @@ export declare function createExpressMiddleware(fs: FartherShore, options?: Midd
|
|
|
106
126
|
* non-optional guarantee AND Express compatibility.
|
|
107
127
|
*/
|
|
108
128
|
export declare function createExpressHandler<Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
|
|
129
|
+
/**
|
|
130
|
+
* Options-first overload: `fs.handler({ permission: "widgets:write" }, cb)`
|
|
131
|
+
* asserts the verified principal holds `permission` (via the fail-closed
|
|
132
|
+
* carrier gate — an absent permission set DENIES) BEFORE the callback runs,
|
|
133
|
+
* responding `403 permission_denied` otherwise. The key is checked with the
|
|
134
|
+
* unified grammar (`*` / `<subject>:*` / exact); derive route-shaped keys with
|
|
135
|
+
* `routePermission(subject, method)`.
|
|
136
|
+
*/
|
|
137
|
+
export declare function createExpressHandler<Req extends ExpressRequestLike = ExpressRequestLike, Res extends ExpressResponseLike = ExpressResponseLike>(options: HandlerOptions, handler: VerifiedExpressHandler<Req, Res>): ExpressMiddleware;
|
|
@@ -12,6 +12,11 @@ export type BootstrapClientOptions = {
|
|
|
12
12
|
now?: () => number;
|
|
13
13
|
/** Minimum seconds between refreshes regardless of server hint. */
|
|
14
14
|
minRefreshSeconds?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum age for cached bootstrap authorization metadata during transient
|
|
17
|
+
* refresh failures. Defaults to 5 minutes.
|
|
18
|
+
*/
|
|
19
|
+
maxStaleSeconds?: number;
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
22
|
* Caches the bootstrap response and refreshes it lazily. `get()` returns the
|
|
@@ -24,6 +29,7 @@ export declare class BootstrapClient {
|
|
|
24
29
|
private readonly fetchImpl;
|
|
25
30
|
private readonly now;
|
|
26
31
|
private readonly minRefreshSeconds;
|
|
32
|
+
private readonly maxStaleMs;
|
|
27
33
|
private cached;
|
|
28
34
|
private fetchedAt;
|
|
29
35
|
private refreshAfterMs;
|
|
@@ -36,5 +42,7 @@ export declare class BootstrapClient {
|
|
|
36
42
|
/** Last cached value without triggering a refresh (null until bootstrapped). */
|
|
37
43
|
peek(): RuntimeBootstrapResponse | null;
|
|
38
44
|
private isStale;
|
|
45
|
+
private isHardStale;
|
|
46
|
+
private cachedOrThrowOnHardStale;
|
|
39
47
|
private doBootstrap;
|
|
40
48
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { FartherShoreError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Per-operation deadlines. Verification-path calls are tighter than reporting
|
|
4
|
+
* calls: JWKS sits in front of every inbound request, while a metering flush is
|
|
5
|
+
* a background economic report that is retried anyway.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEADLINE_MS: {
|
|
8
|
+
/** Boot-blocking; generous because it runs once and gates startup. */
|
|
9
|
+
readonly bootstrap: 10000;
|
|
10
|
+
/** On the inbound verification path — must not hold a request open. */
|
|
11
|
+
readonly jwks: 5000;
|
|
12
|
+
/** Background economic report, retried by the caller. */
|
|
13
|
+
readonly metering: 10000;
|
|
14
|
+
/** Background attested usage callback. */
|
|
15
|
+
readonly postStreamUsage: 10000;
|
|
16
|
+
/** Best-effort heartbeat; never blocks anything. */
|
|
17
|
+
readonly health: 5000;
|
|
18
|
+
/** Boot-time route drift report; fail-open at the caller. */
|
|
19
|
+
readonly report: 10000;
|
|
20
|
+
};
|
|
21
|
+
export type DeadlineOperation = keyof typeof DEADLINE_MS;
|
|
22
|
+
/**
|
|
23
|
+
* Byte cap for response bodies the SDK parses. Core's JWKS and bootstrap
|
|
24
|
+
* documents are kilobytes; a megabyte is orders of magnitude of headroom while
|
|
25
|
+
* still refusing an unbounded stream.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_RESPONSE_BYTES = 1048576;
|
|
28
|
+
/**
|
|
29
|
+
* True when `cause` is this module's deadline firing (as opposed to a caller
|
|
30
|
+
* cancellation or a transport error). `AbortSignal.timeout` rejects with a
|
|
31
|
+
* `TimeoutError` DOMException; we also accept our own typed marker.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isDeadlineExceeded(cause: unknown): boolean;
|
|
34
|
+
/** Raised when a bounded read exceeds {@link MAX_RESPONSE_BYTES}. */
|
|
35
|
+
export declare class ResponseTooLargeError extends Error {
|
|
36
|
+
constructor(limit: number);
|
|
37
|
+
}
|
|
38
|
+
/** Marker for a deadline that fired, preserved across helper boundaries. */
|
|
39
|
+
export declare class DeadlineExceededError extends Error {
|
|
40
|
+
readonly operation: DeadlineOperation;
|
|
41
|
+
constructor(operation: DeadlineOperation, timeoutMs: number);
|
|
42
|
+
}
|
|
43
|
+
export type DeadlineOptions = {
|
|
44
|
+
/** Host cancellation to compose with the SDK deadline. */
|
|
45
|
+
callerSignal?: AbortSignal;
|
|
46
|
+
/**
|
|
47
|
+
* Override the operation default. For callers with a tighter budget than the
|
|
48
|
+
* SDK default (and for tests, which cannot wait out a 10s deadline).
|
|
49
|
+
*/
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Compose the SDK deadline for `operation` with an optional caller signal.
|
|
54
|
+
* Either aborting aborts the fetch, so host cancellation still works and the
|
|
55
|
+
* SDK can never outlive its own timeout.
|
|
56
|
+
*/
|
|
57
|
+
export declare function deadlineSignal(operation: DeadlineOperation, options?: DeadlineOptions): AbortSignal;
|
|
58
|
+
/**
|
|
59
|
+
* `fetch` with a mandatory deadline. Every outbound SDK call goes through here;
|
|
60
|
+
* the CI guard (`fetch-timeouts:check:backend-sdk`) fails the build on a raw
|
|
61
|
+
* `fetch(` added anywhere else in the package.
|
|
62
|
+
*
|
|
63
|
+
* Rethrows a {@link DeadlineExceededError} when OUR timeout fired, so the
|
|
64
|
+
* failure is classifiable even though `AbortSignal.any` erases which input
|
|
65
|
+
* aborted. A caller-driven abort propagates unchanged.
|
|
66
|
+
*/
|
|
67
|
+
export declare function fetchWithDeadline(fetchImpl: typeof fetch, input: string, init: RequestInit, operation: DeadlineOperation, options?: DeadlineOptions): Promise<Response>;
|
|
68
|
+
/**
|
|
69
|
+
* Read a response body as text, cancelling the stream once `limit` bytes have
|
|
70
|
+
* arrived. Returns the decoded text; throws {@link ResponseTooLargeError} when
|
|
71
|
+
* the cap is crossed so a caller never sees a silently truncated document.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readBoundedText(response: Response, limit?: number): Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* Bounded JSON read. Same cap as {@link readBoundedText}; a body that is not
|
|
76
|
+
* valid JSON raises the underlying SyntaxError for the caller to classify.
|
|
77
|
+
*/
|
|
78
|
+
export declare function readBoundedJson<T>(response: Response, limit?: number): Promise<T>;
|
|
79
|
+
/** Wrap a deadline failure in the SDK's typed error with a stable code. */
|
|
80
|
+
export declare function deadlineError(operation: DeadlineOperation, cause: unknown): FartherShoreError;
|
|
@@ -1,28 +1,53 @@
|
|
|
1
1
|
export type Jwk = JsonWebKey & {
|
|
2
2
|
kid?: string;
|
|
3
3
|
};
|
|
4
|
+
/** Freshness of the key set backing a verification decision. */
|
|
5
|
+
export type JwksCacheState = "fresh" | "soft_stale" | "hard_stale" | "cold";
|
|
6
|
+
export type JwksObservation = {
|
|
7
|
+
state: JwksCacheState;
|
|
8
|
+
/** Age of the cached key set in ms (0 when cold). */
|
|
9
|
+
ageMs: number;
|
|
10
|
+
/** The `kid` being resolved, when the observation is tied to one. */
|
|
11
|
+
kid?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Observability hook. Fires on every decision that depends on cache freshness,
|
|
15
|
+
* so an operator can alarm on `soft_stale` (Core is failing) long before
|
|
16
|
+
* `hard_stale` (requests are now being rejected).
|
|
17
|
+
*/
|
|
18
|
+
export type JwksObserver = (observation: JwksObservation) => void;
|
|
4
19
|
export type JwksClientOptions = {
|
|
5
20
|
jwksUrl: string;
|
|
6
21
|
/** Injectable fetch (tests). Defaults to globalThis.fetch. */
|
|
7
22
|
fetchImpl?: typeof fetch;
|
|
8
23
|
/** How long a successful fetch stays fresh before a background refresh. */
|
|
9
24
|
cacheTtlMs?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Hard ceiling on serving a key set whose refresh is failing. Past this age
|
|
27
|
+
* the client FAILS CLOSED rather than vouching for keys it can no longer
|
|
28
|
+
* confirm. Must be ≥ cacheTtlMs.
|
|
29
|
+
*/
|
|
30
|
+
hardStaleMs?: number;
|
|
10
31
|
/** How long an unknown-kid result is negatively cached (avoids hammering). */
|
|
11
32
|
negativeCacheMs?: number;
|
|
12
33
|
/** Injectable clock (tests). */
|
|
13
34
|
now?: () => number;
|
|
35
|
+
/** Freshness//staleness observations for metrics. */
|
|
36
|
+
onObservation?: JwksObserver;
|
|
14
37
|
};
|
|
15
38
|
/**
|
|
16
|
-
* Caching JWKS client.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
39
|
+
* Caching JWKS client. Serves the last successful key set as a warm fallback
|
|
40
|
+
* while a refresh is failing, but only until {@link JwksClientOptions.hardStaleMs};
|
|
41
|
+
* fails closed on a cold cache and past the hard-stale ceiling.
|
|
19
42
|
*/
|
|
20
43
|
export declare class JwksClient {
|
|
21
44
|
private readonly jwksUrl;
|
|
22
45
|
private readonly fetchImpl;
|
|
23
46
|
private readonly cacheTtlMs;
|
|
47
|
+
private readonly hardStaleMs;
|
|
24
48
|
private readonly negativeCacheMs;
|
|
25
49
|
private readonly now;
|
|
50
|
+
private readonly onObservation;
|
|
26
51
|
private keysByKid;
|
|
27
52
|
private fetchedAt;
|
|
28
53
|
private hasFetchedOnce;
|
|
@@ -31,18 +56,28 @@ export declare class JwksClient {
|
|
|
31
56
|
constructor(options: JwksClientOptions);
|
|
32
57
|
/**
|
|
33
58
|
* Resolve a public JWK for `kid`, fail-closed. Throws FartherShoreError with
|
|
34
|
-
* `jwks_unavailable` (cold cache
|
|
59
|
+
* `jwks_unavailable` (cold cache, or a hard-stale cache whose refresh is
|
|
60
|
+
* failing) or `unknown_key_id`.
|
|
35
61
|
*/
|
|
36
62
|
getKey(kid: string): Promise<Jwk>;
|
|
37
63
|
/** Record a confirmed-missing kid, evicting the oldest if at capacity. */
|
|
38
64
|
private rememberMissingKid;
|
|
65
|
+
private ageMs;
|
|
39
66
|
private isStale;
|
|
67
|
+
private isHardStale;
|
|
68
|
+
/** Current freshness of the cached key set. */
|
|
69
|
+
private cacheState;
|
|
70
|
+
private observe;
|
|
71
|
+
/** Fail closed when the cached key set is past the hard-stale ceiling. */
|
|
72
|
+
private assertWithinHardStale;
|
|
40
73
|
/** Single-flight refresh: concurrent callers share one fetch. */
|
|
41
74
|
private refresh;
|
|
42
75
|
private doFetch;
|
|
43
76
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
77
|
+
* BOUNDED stale-while-revalidate. A COLD cache fails closed. A warm cache
|
|
78
|
+
* inside the soft window swallows the failure and keeps serving. Past the
|
|
79
|
+
* hard-stale ceiling it fails closed too — availability is worth a bounded
|
|
80
|
+
* window of degraded trust, not an unbounded one.
|
|
46
81
|
*/
|
|
47
|
-
private
|
|
82
|
+
private handleRefreshFailure;
|
|
48
83
|
}
|
|
@@ -13,21 +13,33 @@ export declare class FartherShorePermissionError extends Error {
|
|
|
13
13
|
constructor(requiredPermission: string, message?: string);
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* grants everything, otherwise the key must be an exact member. Its
|
|
19
|
-
* `undefined → true` codomain is the PRIMITIVE's contract (kept byte-identical
|
|
20
|
-
* to contracts for parity); it is NOT the SDK's carrier policy. Under FAR-723
|
|
21
|
-
* the carrier gate {@link hasPermission} DENIES an absent permission set before
|
|
22
|
-
* this primitive is consulted, so absence never grants at the boundary.
|
|
16
|
+
* HTTP verbs that classify as `:read`. Everything else — including the
|
|
17
|
+
* route-catalog wildcard `*` — classifies as `:write`.
|
|
23
18
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
19
|
+
* FAITHFUL COPY of the canonical `READ_METHODS` in the permissions kernel
|
|
20
|
+
* (`@farthershore/authz/grammar/route`, re-exported by
|
|
21
|
+
* `@farthershore/contracts/rbac`); the published bundle is contracts-free, so
|
|
22
|
+
* `permissions-parity.test.ts` asserts agreement and the full corpus replay
|
|
23
|
+
* lives in packages/authz/test-node/sdk-copy-parity.test.ts.
|
|
29
24
|
*/
|
|
30
|
-
export declare
|
|
25
|
+
export declare const READ_METHODS: ReadonlySet<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Derive the permission string a credential must hold to call a route:
|
|
28
|
+
* `<subject>:read` for safe verbs (GET / HEAD / OPTIONS, any casing),
|
|
29
|
+
* `<subject>:write` for every other method INCLUDING the route-catalog
|
|
30
|
+
* wildcard `*`. Use it to build in-handler permission keys from the same
|
|
31
|
+
* grammar the edge `permission` constraint enforces — never re-spell the
|
|
32
|
+
* `:read`/`:write` suffix locally.
|
|
33
|
+
*
|
|
34
|
+
* FAITHFUL COPY of the canonical `routePermission` in the permissions kernel
|
|
35
|
+
* (`@farthershore/authz/grammar/route`, re-exported by
|
|
36
|
+
* `@farthershore/contracts/rbac`); parity asserted as for {@link READ_METHODS}.
|
|
37
|
+
*
|
|
38
|
+
* @param subject the matched route's permission subject (used verbatim)
|
|
39
|
+
* @param method the REQUEST method (case-insensitive) or a route-catalog
|
|
40
|
+
* method entry (`*` → `:write`)
|
|
41
|
+
*/
|
|
42
|
+
export declare function routePermission(subject: string, method: string): string;
|
|
31
43
|
/**
|
|
32
44
|
* Whether the granted `permissions` satisfy the required `key` under the
|
|
33
45
|
* unified grammar: `"*"` (global), `"<subject>:*"` (subject wildcard), or the
|
|
@@ -5,10 +5,23 @@ export type ReportUsageInput = {
|
|
|
5
5
|
meters: Record<string, number>;
|
|
6
6
|
creditUnitsConsumed?: Record<string, number>;
|
|
7
7
|
measureContext?: Record<string, unknown>;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/** Schema version of {@link measurements}; currently `1`. */
|
|
9
|
+
measurementsVersion?: number;
|
|
10
|
+
/**
|
|
11
|
+
* The measurement lane (`{ meter, values, dims }`) — the authoritative rating
|
|
12
|
+
* input `ctx.report()` emits. Additive over `meters`, which remains the flat
|
|
13
|
+
* projection core's existing settlement path reads.
|
|
14
|
+
*/
|
|
15
|
+
measurements?: {
|
|
16
|
+
meter: string;
|
|
17
|
+
values: Record<string, number>;
|
|
18
|
+
dims?: Record<string, string>;
|
|
19
|
+
}[];
|
|
20
|
+
/** Proposed rate input for a `backendQuoted` pricing rule (core clamps it). */
|
|
21
|
+
quote?: {
|
|
22
|
+
currency: string;
|
|
23
|
+
amountNanos: string;
|
|
24
|
+
};
|
|
12
25
|
};
|
|
13
26
|
export type ReportUsageResult = {
|
|
14
27
|
ok: true;
|
|
@@ -26,6 +39,8 @@ export type PostStreamUsageClientOptions = {
|
|
|
26
39
|
sleep?: (delayMs: number) => Promise<void>;
|
|
27
40
|
/** Backoff after each request-not-found response. */
|
|
28
41
|
retryDelaysMs?: readonly number[];
|
|
42
|
+
/** Upper bound for Retry-After sleeps; defaults to 10 seconds. */
|
|
43
|
+
maxRetryDelayMs?: number;
|
|
29
44
|
};
|
|
30
45
|
/**
|
|
31
46
|
* Best-effort, attested post-stream billing reporter. The callback is
|
|
@@ -40,6 +55,10 @@ export declare class PostStreamUsageClient {
|
|
|
40
55
|
private readonly logger;
|
|
41
56
|
private readonly sleep;
|
|
42
57
|
private readonly retryDelaysMs;
|
|
58
|
+
private readonly maxRetryDelayMs;
|
|
43
59
|
constructor(options: PostStreamUsageClientOptions);
|
|
44
60
|
reportUsage(input: ReportUsageInput): Promise<ReportUsageResult>;
|
|
61
|
+
/** Enforce the token's meter scope + per-event bounds on the measurement lane. */
|
|
62
|
+
private validateMeasurements;
|
|
63
|
+
private retryDelayForAttempt;
|
|
45
64
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type NonceStore } from "./nonceCache.js";
|
|
2
|
+
/** How far one-time-use enforcement actually reaches. */
|
|
3
|
+
export type ReplayProtectionMode = "shared" | "single-instance";
|
|
4
|
+
export type ReplayProtectionDiagnostic = {
|
|
5
|
+
mode: ReplayProtectionMode;
|
|
6
|
+
/** True when replay is enforced across every replica, not just this process. */
|
|
7
|
+
crossReplica: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ResolveReplayProtectionInput = {
|
|
10
|
+
/** Shared store injected by the host, if any. */
|
|
11
|
+
nonceStore?: NonceStore | undefined;
|
|
12
|
+
};
|
|
13
|
+
export type ResolvedReplayProtection = {
|
|
14
|
+
store: NonceStore;
|
|
15
|
+
diagnostic: ReplayProtectionDiagnostic;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Select the replay store and report which mode it is. Never throws — the
|
|
19
|
+
* zero-config default is the in-memory cache, and the time window is what
|
|
20
|
+
* bounds exposure regardless.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveReplayProtection(input?: ResolveReplayProtectionInput): ResolvedReplayProtection;
|
|
23
|
+
/**
|
|
24
|
+
* Wrap an injected shared store so an OUTAGE fails closed. Only applies when a
|
|
25
|
+
* builder opted into a shared store; the in-memory default cannot fail this
|
|
26
|
+
* way.
|
|
27
|
+
*/
|
|
28
|
+
export declare function failClosed(store: NonceStore): NonceStore;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { type ResponseMeteringUsagePayload } from "../response-metering.js";
|
|
2
|
+
/** Observed measurement values, keyed by measure key. */
|
|
3
|
+
export type MeasurementValues = Record<string, number>;
|
|
4
|
+
/** Catalog selectors the measurement was produced under, keyed by dimension. */
|
|
5
|
+
export type MeasurementDimensions = Record<string, string>;
|
|
6
|
+
/**
|
|
7
|
+
* The validated, transmitted quote — a PROPOSED rate input, never a charge.
|
|
8
|
+
* `amountNanos` is a decimal integer string of nanodollars **per unit of the
|
|
9
|
+
* entry's measure** (the platform's money unit is the nanodollar); core
|
|
10
|
+
* multiplies it by the measured quantity, clamps it into the pricing policy's
|
|
11
|
+
* declared per-unit `{min,max}`, and flags out-of-range proposals for dispute.
|
|
12
|
+
* Never send a total.
|
|
13
|
+
*/
|
|
14
|
+
export type QuoteProposal = {
|
|
15
|
+
currency: string;
|
|
16
|
+
amountNanos: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Authoring shape of {@link QuoteProposal}. `amountNanos` (nanodollars PER
|
|
20
|
+
* UNIT of the entry's measure) accepts a number, bigint, or decimal integer
|
|
21
|
+
* string; anything else is rejected.
|
|
22
|
+
*/
|
|
23
|
+
export type QuoteInput = {
|
|
24
|
+
currency: string;
|
|
25
|
+
amountNanos: number | bigint | string;
|
|
26
|
+
};
|
|
27
|
+
/** One measurement report. The complete argument surface of the verb. */
|
|
28
|
+
export type ReportInput = {
|
|
29
|
+
/** Meter key as declared in the business release (plain string at the wire). */
|
|
30
|
+
meter: string;
|
|
31
|
+
/** Observed values keyed by measure key, e.g. `{ input_tokens: 1200 }`. */
|
|
32
|
+
values: MeasurementValues;
|
|
33
|
+
/** Catalog selectors, e.g. `{ model: "acme-4", cache_status: "hit" }`. */
|
|
34
|
+
dims?: MeasurementDimensions;
|
|
35
|
+
/**
|
|
36
|
+
* OPTIONAL money proposal for a `backendQuoted` pricing rule: a PER-UNIT
|
|
37
|
+
* rate in nanodollars (multiplied by the measured quantity — never a
|
|
38
|
+
* total). Opaque at the authoring boundary (a job result carries it through
|
|
39
|
+
* untyped); validated against {@link QuoteInput} here and transmitted as a
|
|
40
|
+
* {@link QuoteProposal}. Applies to every backend-quoted component of this
|
|
41
|
+
* report; ignored by rules that are not backend-quoted.
|
|
42
|
+
*/
|
|
43
|
+
quote?: unknown;
|
|
44
|
+
};
|
|
45
|
+
/** Which channel actually carried the measurement. */
|
|
46
|
+
export type ReportTransport = "in_band" | "post_stream";
|
|
47
|
+
/**
|
|
48
|
+
* Delivery outcome. Validation faults THROW (a malformed report is a builder
|
|
49
|
+
* bug worth surfacing); delivery faults resolve `ok: false` so a metering
|
|
50
|
+
* hiccup never breaks the builder's endpoint. A served request may own only
|
|
51
|
+
* one post-stream callback identity, so later calls fail explicitly.
|
|
52
|
+
*/
|
|
53
|
+
export type ReportResult = {
|
|
54
|
+
ok: true;
|
|
55
|
+
transport: ReportTransport;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
transport: ReportTransport;
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
/** The wire shape of one validated measurement. */
|
|
62
|
+
export type Measurement = {
|
|
63
|
+
meter: string;
|
|
64
|
+
values: MeasurementValues;
|
|
65
|
+
dims?: MeasurementDimensions;
|
|
66
|
+
};
|
|
67
|
+
/** Version of the `measurements` payload lane (additive over `rawDimsUnits`). */
|
|
68
|
+
export declare const MEASUREMENTS_VERSION = 1;
|
|
69
|
+
/** The transport seam a host adapter supplies for the in-band lane. */
|
|
70
|
+
export type ResponseSink = {
|
|
71
|
+
/** True while headers can still be stamped onto the outgoing response. */
|
|
72
|
+
canStampHeaders(): boolean;
|
|
73
|
+
/** Stamp the signed metering headers onto the outgoing response. */
|
|
74
|
+
stampHeaders(headers: Record<string, string>): void;
|
|
75
|
+
};
|
|
76
|
+
/** Everything `report()` needs that only the runtime can provide. */
|
|
77
|
+
export type ReportChannels = {
|
|
78
|
+
/** Sign the payload into `x-fs-metering*` headers (may resolve `{}`). */
|
|
79
|
+
computeHeaders(payload: ResponseMeteringUsagePayload): Promise<Record<string, string>>;
|
|
80
|
+
/** Deliver over the attested post-stream channel. */
|
|
81
|
+
postStream(input: {
|
|
82
|
+
measurements: Measurement[];
|
|
83
|
+
quote?: QuoteProposal;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
ok: boolean;
|
|
86
|
+
reason?: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** Method/path of the served request, for the in-band payload binding. */
|
|
89
|
+
request?: {
|
|
90
|
+
method: string;
|
|
91
|
+
path: string;
|
|
92
|
+
};
|
|
93
|
+
/** Host-supplied response seam; absent ⇒ the post-stream lane is used. */
|
|
94
|
+
responseSink?: ResponseSink;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* `report()` accepts one measurement or an ARRAY of measurements. The array
|
|
98
|
+
* form is the sanctioned way to report several meters after the response is
|
|
99
|
+
* sent: one served request owns exactly ONE post-stream callback identity, so
|
|
100
|
+
* sequential awaited single reports after the first flush cannot be delivered
|
|
101
|
+
* — a batch rides the single callback atomically. All entries of a batch
|
|
102
|
+
* share one quote (at most one distinct quote may be supplied).
|
|
103
|
+
*/
|
|
104
|
+
export type ReportFn = (input: ReportInput | readonly ReportInput[]) => Promise<ReportResult>;
|
|
105
|
+
/**
|
|
106
|
+
* Build the bound `report()` verb for a verified context. The returned function
|
|
107
|
+
* validates, picks its own transport, and never asks the caller for identity.
|
|
108
|
+
*/
|
|
109
|
+
export declare function createReportFn(channels: ReportChannels): ReportFn;
|
|
110
|
+
/**
|
|
111
|
+
* The `report()` stub attached to a context produced by the BARE
|
|
112
|
+
* `verifyRequest()` primitive (no runtime, therefore no metering channel). It
|
|
113
|
+
* throws an error that names the fix instead of pretending the measurement was
|
|
114
|
+
* delivered.
|
|
115
|
+
*/
|
|
116
|
+
export declare function unattachedReport(): ReportFn;
|
|
117
|
+
/**
|
|
118
|
+
* Project a measurement into the flat `rawDimsUnits` lane the gateway's
|
|
119
|
+
* in-band settlement path consumes.
|
|
120
|
+
*
|
|
121
|
+
* PROJECTION RULE (P0-2): the projection keys the METER id, and its scalar is
|
|
122
|
+
* the sum of the measurement's measure values — the meter's structural
|
|
123
|
+
* quantity. The gateway masks `rawDimsUnits` by the matched route's declared
|
|
124
|
+
* METER ids (`maskCostMapByMeters`), so measure-keyed entries would be
|
|
125
|
+
* silently discarded at the edge (UNBILLED). Per-measure detail is NOT lost:
|
|
126
|
+
* the full `{ meter, values, dims }` measurement rides alongside in the
|
|
127
|
+
* versioned `measurements` lane, which is the authoritative rating input.
|
|
128
|
+
*/
|
|
129
|
+
export declare function rawDimsUnitsOf(measurement: Measurement): Record<string, number>;
|
|
130
|
+
/** Validate one report input into its wire {@link Measurement}. */
|
|
131
|
+
export declare function validateMeasurement(input: ReportInput): Measurement;
|
|
132
|
+
/** Validate an opaque authored quote into its wire {@link QuoteProposal}. */
|
|
133
|
+
export declare function validateQuote(quote: unknown): QuoteProposal;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type RuntimeBootstrapResponse, type RuntimeHealthReport } from "../runtime-types.js";
|
|
2
2
|
import type { ReconcileResult } from "../reflect/reconcile.js";
|
|
3
|
-
import { type
|
|
4
|
-
import { type ReportUsageInput, type ReportUsageResult } from "./post-stream-usage.js";
|
|
3
|
+
import { type ResponseSink } from "./report.js";
|
|
5
4
|
import { type NonceStore } from "./nonceCache.js";
|
|
5
|
+
import { type ReplayProtectionDiagnostic } from "./replay-protection.js";
|
|
6
6
|
import { type SpawnFn } from "./tunnel.js";
|
|
7
7
|
import { type FartherShoreRequestContext, type VerifyRequestInput } from "./verifyRequest.js";
|
|
8
8
|
/** Advanced opt-in tunnel config. The embedded runner is the default DX. */
|
|
@@ -21,10 +21,7 @@ export type FartherShoreTunnelOptions = {
|
|
|
21
21
|
export type FartherShoreInitOptions = {
|
|
22
22
|
/** Explicit runtime token. Defaults to process.env.FS_RUNTIME_TOKEN. */
|
|
23
23
|
runtimeToken?: string;
|
|
24
|
-
/**
|
|
25
|
-
* Core base URL. Defaults to FS_CORE_URL / FARTHERSHORE_CORE_URL or
|
|
26
|
-
* https://core.farthershore.com.
|
|
27
|
-
*/
|
|
24
|
+
/** Core base URL. Defaults to FS_CORE_URL or https://core.farthershore.com. */
|
|
28
25
|
coreUrl?: string;
|
|
29
26
|
/** Env map (tests). Defaults to process.env. */
|
|
30
27
|
env?: Record<string, string | undefined>;
|
|
@@ -57,17 +54,28 @@ export type FartherShoreInitOptions = {
|
|
|
57
54
|
*/
|
|
58
55
|
contextSecrets?: readonly string[];
|
|
59
56
|
/**
|
|
60
|
-
* OPTIONAL shared replay-prevention store.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
57
|
+
* OPTIONAL shared replay-prevention store. Not required: the signature's
|
|
58
|
+
* ~305s time window is the always-on defense, and the zero-config default is
|
|
59
|
+
* an in-memory per-process cache. Inject a shared atomic store (Redis /
|
|
60
|
+
* Memcached / Cloudflare KV / Durable Object) implementing {@link NonceStore}
|
|
61
|
+
* only if you want one-time-use enforced ACROSS replicas rather than within
|
|
62
|
+
* each one. `checkAndRemember` may be async, and should be TTL-bound to the
|
|
63
|
+
* signature validity window.
|
|
64
|
+
*
|
|
65
|
+
* If you do inject one, an outage of that store fails requests CLOSED — it
|
|
66
|
+
* never degrades to "not a replay".
|
|
68
67
|
*/
|
|
69
68
|
nonceStore?: NonceStore;
|
|
70
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Host-adapter seams for `verifyRequest`. An adapter that owns the outgoing
|
|
72
|
+
* response (the Express middleware) supplies a {@link ResponseSink} so
|
|
73
|
+
* `ctx.report()` can choose the zero-network in-band header transport while the
|
|
74
|
+
* response is still open. Without one, reports take the post-stream channel.
|
|
75
|
+
*/
|
|
76
|
+
export type VerifyRequestHostOptions = {
|
|
77
|
+
responseSink?: ResponseSink;
|
|
78
|
+
};
|
|
71
79
|
export declare const SDK_VERSION: string;
|
|
72
80
|
/**
|
|
73
81
|
* The runtime instance. Lazily bootstraps; holds the JWKS client, nonce cache,
|
|
@@ -85,9 +93,9 @@ export declare class FartherShore {
|
|
|
85
93
|
/** OPTIONAL HS256 secret(s) — defense-in-depth over the cv=2 X-Fs-Context. */
|
|
86
94
|
private readonly contextSecrets;
|
|
87
95
|
private readonly nonceCache;
|
|
96
|
+
private readonly replayProtectionDiagnostic;
|
|
88
97
|
private readonly shutdownManager;
|
|
89
98
|
private jwks;
|
|
90
|
-
private meteringClient;
|
|
91
99
|
private postStreamUsageClient;
|
|
92
100
|
private tunnel;
|
|
93
101
|
private bootstrapped;
|
|
@@ -115,7 +123,14 @@ export declare class FartherShore {
|
|
|
115
123
|
* Framework-neutral verification primitive. Fail-closed: throws a typed
|
|
116
124
|
* FartherShoreError on any verification failure. Returns the verified context.
|
|
117
125
|
*/
|
|
118
|
-
verifyRequest(input: VerifyRequestInput): Promise<FartherShoreRequestContext>;
|
|
126
|
+
verifyRequest(input: VerifyRequestInput, options?: VerifyRequestHostOptions): Promise<FartherShoreRequestContext>;
|
|
127
|
+
/**
|
|
128
|
+
* Bind the ONE reporting verb to a verified context. Identity comes from the
|
|
129
|
+
* context (`signedContext.subscriptionId` + `requestId`) — never from the
|
|
130
|
+
* caller — so a handler cannot forget it, and a background job that is handed
|
|
131
|
+
* this context keeps reporting against the SAME served identity.
|
|
132
|
+
*/
|
|
133
|
+
private buildReportFn;
|
|
119
134
|
/** Whether verification is required (bootstrap × opt-out). */
|
|
120
135
|
verificationRequired(): Promise<boolean>;
|
|
121
136
|
/**
|
|
@@ -129,10 +144,19 @@ export declare class FartherShore {
|
|
|
129
144
|
* (request verification stays fail-closed regardless — a different axis).
|
|
130
145
|
*/
|
|
131
146
|
start(): Promise<void>;
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
/**
|
|
148
|
+
* PRIVATE transport for the post-stream lane of `ctx.report()`. Never rejects
|
|
149
|
+
* — a metering hiccup must not break a builder's endpoint. This is machinery,
|
|
150
|
+
* not surface: the ONE public reporting verb is `ctx.report()`.
|
|
151
|
+
*/
|
|
152
|
+
private reportPostStreamUsage;
|
|
153
|
+
/**
|
|
154
|
+
* How far replay protection actually reaches — `"shared"` (enforced across
|
|
155
|
+
* every replica) or `"single-instance"` (this process only). Deployment
|
|
156
|
+
* diagnostic: log it at boot, or assert on it in a smoke test, so the mode is
|
|
157
|
+
* never a surprise. See {@link FartherShoreInitOptions.nonceStore}.
|
|
158
|
+
*/
|
|
159
|
+
replayProtection(): ReplayProtectionDiagnostic;
|
|
136
160
|
/** Current local health report. */
|
|
137
161
|
health(): RuntimeHealthReport;
|
|
138
162
|
/** Graceful shutdown: flush metering + send a stopping heartbeat. */
|