@farthershore/backend 0.20.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 +103 -0
- package/README.md +239 -91
- package/dist/adapters/express.js +68 -12
- package/dist/generated/runtime-contract.js +21 -236
- package/dist/index.js +519 -417
- package/dist/internal/index.js +587 -0
- package/dist/testing/index.js +614 -302
- package/dist/types/adapters/express.d.ts +32 -3
- package/dist/types/core/permissions.d.ts +25 -13
- package/dist/types/core/post-stream-usage.d.ts +19 -4
- package/dist/types/core/report.d.ts +133 -0
- package/dist/types/core/runtime.d.ts +25 -12
- 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 +29 -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 +12 -3
- 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;
|
|
@@ -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;
|
|
@@ -45,5 +58,7 @@ export declare class PostStreamUsageClient {
|
|
|
45
58
|
private readonly maxRetryDelayMs;
|
|
46
59
|
constructor(options: PostStreamUsageClientOptions);
|
|
47
60
|
reportUsage(input: ReportUsageInput): Promise<ReportUsageResult>;
|
|
61
|
+
/** Enforce the token's meter scope + per-event bounds on the measurement lane. */
|
|
62
|
+
private validateMeasurements;
|
|
48
63
|
private retryDelayForAttempt;
|
|
49
64
|
}
|
|
@@ -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,7 +1,6 @@
|
|
|
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";
|
|
6
5
|
import { type ReplayProtectionDiagnostic } from "./replay-protection.js";
|
|
7
6
|
import { type SpawnFn } from "./tunnel.js";
|
|
@@ -22,10 +21,7 @@ export type FartherShoreTunnelOptions = {
|
|
|
22
21
|
export type FartherShoreInitOptions = {
|
|
23
22
|
/** Explicit runtime token. Defaults to process.env.FS_RUNTIME_TOKEN. */
|
|
24
23
|
runtimeToken?: string;
|
|
25
|
-
/**
|
|
26
|
-
* Core base URL. Defaults to FS_CORE_URL / FARTHERSHORE_CORE_URL or
|
|
27
|
-
* https://core.farthershore.com.
|
|
28
|
-
*/
|
|
24
|
+
/** Core base URL. Defaults to FS_CORE_URL or https://core.farthershore.com. */
|
|
29
25
|
coreUrl?: string;
|
|
30
26
|
/** Env map (tests). Defaults to process.env. */
|
|
31
27
|
env?: Record<string, string | undefined>;
|
|
@@ -71,6 +67,15 @@ export type FartherShoreInitOptions = {
|
|
|
71
67
|
*/
|
|
72
68
|
nonceStore?: NonceStore;
|
|
73
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
|
+
};
|
|
74
79
|
export declare const SDK_VERSION: string;
|
|
75
80
|
/**
|
|
76
81
|
* The runtime instance. Lazily bootstraps; holds the JWKS client, nonce cache,
|
|
@@ -91,7 +96,6 @@ export declare class FartherShore {
|
|
|
91
96
|
private readonly replayProtectionDiagnostic;
|
|
92
97
|
private readonly shutdownManager;
|
|
93
98
|
private jwks;
|
|
94
|
-
private meteringClient;
|
|
95
99
|
private postStreamUsageClient;
|
|
96
100
|
private tunnel;
|
|
97
101
|
private bootstrapped;
|
|
@@ -119,7 +123,14 @@ export declare class FartherShore {
|
|
|
119
123
|
* Framework-neutral verification primitive. Fail-closed: throws a typed
|
|
120
124
|
* FartherShoreError on any verification failure. Returns the verified context.
|
|
121
125
|
*/
|
|
122
|
-
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;
|
|
123
134
|
/** Whether verification is required (bootstrap × opt-out). */
|
|
124
135
|
verificationRequired(): Promise<boolean>;
|
|
125
136
|
/**
|
|
@@ -133,10 +144,12 @@ export declare class FartherShore {
|
|
|
133
144
|
* (request verification stays fail-closed regardless — a different axis).
|
|
134
145
|
*/
|
|
135
146
|
start(): Promise<void>;
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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;
|
|
140
153
|
/**
|
|
141
154
|
* How far replay protection actually reaches — `"shared"` (enforced across
|
|
142
155
|
* every replica) or `"single-instance"` (this process only). Deployment
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ReportFn } from "./report.js";
|
|
2
2
|
import { type ConsumerPrincipal, type FartherShoreSignedContext } from "./verifyContext.js";
|
|
3
3
|
import type { JwksClient } from "./jwks.js";
|
|
4
4
|
import type { NonceStore } from "./nonceCache.js";
|
|
@@ -60,9 +60,27 @@ export type FartherShoreRequestContext = {
|
|
|
60
60
|
signedContext?: FartherShoreSignedContext;
|
|
61
61
|
/** Managed-RBAC role keys the acting subject holds (display/audit only). */
|
|
62
62
|
roles?: string[];
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* THE reporting verb. `report({ meter, values, dims?, quote? })` reports
|
|
65
|
+
* MEASUREMENTS against the served identity this context already carries —
|
|
66
|
+
* there is no subscription/release argument to forget. The SDK picks the
|
|
67
|
+
* transport (signed in-band headers before the response is sent, the attested
|
|
68
|
+
* post-stream channel after it, or from a background job), so the builder
|
|
69
|
+
* never chooses one.
|
|
70
|
+
*
|
|
71
|
+
* Attached by the runtime facade. A context produced by the BARE
|
|
72
|
+
* `verifyRequest()` primitive has no metering channel, so its `report()`
|
|
73
|
+
* throws an error naming the fix rather than dropping the measurement.
|
|
74
|
+
*/
|
|
75
|
+
report: ReportFn;
|
|
65
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* The verified Farther Shore context — the identity + reporting handle a
|
|
79
|
+
* handler (or a background job the handler hands it to) works with. Alias of
|
|
80
|
+
* {@link FartherShoreRequestContext}: a background job is simply given the
|
|
81
|
+
* already-verified context, so there is no second context type to learn.
|
|
82
|
+
*/
|
|
83
|
+
export type FartherShoreContext = FartherShoreRequestContext;
|
|
66
84
|
export type VerifyRequestDeps = {
|
|
67
85
|
jwks: JwksClient;
|
|
68
86
|
nonceCache: NonceStore;
|
|
@@ -1,127 +1,3 @@
|
|
|
1
|
-
export declare const RUNTIME_CONTRACT_VERSION: 1;
|
|
2
|
-
export declare const RUNTIME_TOKEN_ENV: "FS_RUNTIME_TOKEN";
|
|
3
|
-
export declare const RUNTIME_TOKEN_CONTRACT: {
|
|
4
|
-
readonly environmentVariable: "FS_RUNTIME_TOKEN";
|
|
5
|
-
readonly prefixes: {
|
|
6
|
-
readonly live: "fsrt_live_";
|
|
7
|
-
readonly test: "fsrt_test_";
|
|
8
|
-
};
|
|
9
|
-
readonly opaque: true;
|
|
10
|
-
readonly storage: "sha256-hash-only";
|
|
11
|
-
readonly lastFour: true;
|
|
12
|
-
readonly capabilities: readonly ["gateway_verification", "metering", "health", "tunnel"];
|
|
13
|
-
};
|
|
14
|
-
export declare const RUNTIME_BOOTSTRAP_CONTRACT: {
|
|
15
|
-
readonly method: "POST";
|
|
16
|
-
readonly path: "/v1/runtime/bootstrap";
|
|
17
|
-
readonly authorization: "Bearer fsrt_...";
|
|
18
|
-
readonly request: {
|
|
19
|
-
readonly instanceId: "string?";
|
|
20
|
-
readonly sdkVersion: "string?";
|
|
21
|
-
readonly sdkLanguage: "string?";
|
|
22
|
-
};
|
|
23
|
-
readonly response: {
|
|
24
|
-
readonly product: {
|
|
25
|
-
readonly id: "string";
|
|
26
|
-
readonly slug: "string";
|
|
27
|
-
};
|
|
28
|
-
readonly backend: {
|
|
29
|
-
readonly id: "string";
|
|
30
|
-
readonly slug: "string";
|
|
31
|
-
readonly name: "string";
|
|
32
|
-
};
|
|
33
|
-
readonly environment: {
|
|
34
|
-
readonly id: "string?";
|
|
35
|
-
readonly kind: "live | test";
|
|
36
|
-
};
|
|
37
|
-
readonly capabilities: "string[]";
|
|
38
|
-
readonly verification: {
|
|
39
|
-
readonly required: "boolean";
|
|
40
|
-
readonly jwksUrl: "string";
|
|
41
|
-
readonly clockSkewSeconds: "number";
|
|
42
|
-
readonly replayWindowSeconds: "number";
|
|
43
|
-
readonly headerNames: "object";
|
|
44
|
-
};
|
|
45
|
-
readonly metering: {
|
|
46
|
-
readonly enabled: "boolean";
|
|
47
|
-
readonly endpoint: "string";
|
|
48
|
-
readonly credential: "string";
|
|
49
|
-
readonly allowedMeters: "string[]";
|
|
50
|
-
readonly allowedRoutes: "string[]";
|
|
51
|
-
readonly perEventMax: "number";
|
|
52
|
-
};
|
|
53
|
-
readonly transport: {
|
|
54
|
-
readonly mode: "direct | tunnel";
|
|
55
|
-
readonly runner: "embedded | sidecar | null";
|
|
56
|
-
readonly originUrl: "string?";
|
|
57
|
-
readonly originHostname: "string?";
|
|
58
|
-
readonly localTarget: "string?";
|
|
59
|
-
readonly cloudflared: "object?";
|
|
60
|
-
};
|
|
61
|
-
readonly routes: "object[]";
|
|
62
|
-
readonly policyVersion: "string";
|
|
63
|
-
readonly refreshAfterSeconds: "number";
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
export declare const RUNTIME_SIGNING_CONTRACT: {
|
|
67
|
-
readonly algorithm: "Ed25519";
|
|
68
|
-
readonly encoding: "base64url";
|
|
69
|
-
readonly keyMaterial: "service-jwt-jwks";
|
|
70
|
-
readonly canonicalString: {
|
|
71
|
-
readonly description: "Byte-exact, language-neutral serialization of the signed claim set. Fields are emitted in the FIXED order below, one per line, each as `name:value`, joined by a single newline (\\n, U+000A). NO trailing newline. The serialization depends on neither JSON key ordering nor any Node.js Buffer/serialization detail — only UTF-8 byte encoding of the field values. Empty/absent values are emitted as the empty string after the colon. Go/Python/Java/Rust reproduce this identically.";
|
|
72
|
-
readonly fieldSeparator: "\n";
|
|
73
|
-
readonly keyValueSeparator: ":";
|
|
74
|
-
readonly trailingNewline: false;
|
|
75
|
-
readonly fieldEncoding: "utf-8";
|
|
76
|
-
readonly fields: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version", "context-hash"];
|
|
77
|
-
readonly fieldRules: {
|
|
78
|
-
readonly method: "Uppercased HTTP method (e.g. GET, POST).";
|
|
79
|
-
readonly path: "Request path, percent-encoded as received, no host, no query string. Always begins with '/'.";
|
|
80
|
-
readonly query: "The request's query string bytes VERBATIM, after the caller has stripped the URL's single leading '?' delimiter. Use the RAW wire query exactly as received — NO sorting, NO filtering, NO re-stripping a leading '?', NO dropping empty pairs, NO re-encoding. The signature binds to the exact query bytes: any reorder, added/removed/relocated pair, or re-encoding changes the bytes and MUST fail verification. Every SDK verifier MUST read the raw wire query (never a re-parsed/re-serialized form, which could reorder or re-encode and would then fail legitimate requests). Any normalization (sorting by name or value, dropping/moving empty pairs, re-stripping '?') is FORBIDDEN — each collapses distinct wire queries under one signature. Empty string when there is no query.";
|
|
81
|
-
readonly "body-hash": "Lowercase hex SHA-256 of the RAW request body bytes. For an empty body, the SHA-256 of zero bytes (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Streaming-exempt requests use the literal token 'STREAM'.";
|
|
82
|
-
readonly "request-id": "Opaque unique request id minted by the gateway (also the replay-cache nonce).";
|
|
83
|
-
readonly timestamp: "Integer Unix epoch seconds (UTC) at signing time, as a base-10 string with no padding.";
|
|
84
|
-
readonly "business-id": "Business id the request is routed to.";
|
|
85
|
-
readonly "backend-id": "Backend id the route binds to.";
|
|
86
|
-
readonly "route-id": "Resolved route id; empty string if the route is unresolved.";
|
|
87
|
-
readonly "policy-version": "Tenant artifact / policy version the gateway signed under.";
|
|
88
|
-
readonly "context-hash": "Lowercase hex SHA-256 of the presented X-Fs-Context JWT string (identity-context binding). For a request with no signed context, the SHA-256 of the empty string (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Binds the identity context into the request signature so one verification covers both.";
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
};
|
|
92
|
-
/**
|
|
93
|
-
* Ordered list of fields in the canonical signing string. The order here is
|
|
94
|
-
* load-bearing and identical across all language SDKs.
|
|
95
|
-
*/
|
|
96
|
-
export declare const RUNTIME_CANONICAL_FIELDS: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version", "context-hash"];
|
|
97
|
-
export declare const RUNTIME_BODY_HASH_CONTRACT: {
|
|
98
|
-
readonly algorithm: "SHA-256";
|
|
99
|
-
readonly encoding: "hex-lower";
|
|
100
|
-
readonly source: "raw-request-bytes";
|
|
101
|
-
readonly emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
102
|
-
readonly maxBodyBytes: 10485760;
|
|
103
|
-
readonly streamingExemptToken: "STREAM";
|
|
104
|
-
readonly streamingExemptContentTypes: readonly ["text/event-stream", "application/octet-stream", "multipart/form-data"];
|
|
105
|
-
readonly overMaxStatus: 413;
|
|
106
|
-
};
|
|
107
|
-
export declare const RUNTIME_HEADERS: {
|
|
108
|
-
readonly signature: "x-fs-signature";
|
|
109
|
-
readonly keyId: "x-fs-key-id";
|
|
110
|
-
readonly requestId: "x-fs-request-id";
|
|
111
|
-
readonly timestamp: "x-fs-timestamp";
|
|
112
|
-
readonly businessId: "x-fs-business-id";
|
|
113
|
-
readonly backendId: "x-fs-backend-id";
|
|
114
|
-
readonly routeId: "x-fs-route-id";
|
|
115
|
-
readonly policyVersion: "x-fs-policy-version";
|
|
116
|
-
readonly bodyHash: "x-fs-body-hash";
|
|
117
|
-
};
|
|
118
|
-
export declare const RUNTIME_REPLAY_CONTRACT: {
|
|
119
|
-
readonly windowSeconds: 300;
|
|
120
|
-
readonly clockSkewSeconds: 5;
|
|
121
|
-
readonly nonce: "x-fs-request-id";
|
|
122
|
-
readonly nonceCache: "bounded-lru";
|
|
123
|
-
readonly policy: "fail-closed";
|
|
124
|
-
};
|
|
125
1
|
export declare const RUNTIME_ERROR_CODES: {
|
|
126
2
|
readonly missingSignature: "missing_signature";
|
|
127
3
|
readonly malformedSignature: "malformed_signature";
|
|
@@ -143,40 +19,15 @@ export declare const RUNTIME_ERROR_CODES: {
|
|
|
143
19
|
readonly surfaceNotAllowed: "surface_not_allowed";
|
|
144
20
|
};
|
|
145
21
|
export type RuntimeErrorCode = (typeof RUNTIME_ERROR_CODES)[keyof typeof RUNTIME_ERROR_CODES];
|
|
146
|
-
export declare const
|
|
147
|
-
readonly
|
|
148
|
-
readonly
|
|
149
|
-
readonly
|
|
150
|
-
readonly
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
readonly request_id: "string?";
|
|
156
|
-
readonly requestId: "string?";
|
|
157
|
-
readonly subscriptionId: "string";
|
|
158
|
-
readonly nonce: "string?";
|
|
159
|
-
readonly meter: "string";
|
|
160
|
-
readonly qty: "number";
|
|
161
|
-
readonly timestamp: "string";
|
|
162
|
-
};
|
|
163
|
-
readonly postStreamEvent: {
|
|
164
|
-
readonly requestId: "string";
|
|
165
|
-
readonly subscriptionId: "string?";
|
|
166
|
-
readonly nonce: "string";
|
|
167
|
-
readonly meters: "Record<string, number>";
|
|
168
|
-
readonly creditUnitsConsumed: "Record<string, number>?";
|
|
169
|
-
readonly measureContext: "Record<string, unknown>?";
|
|
170
|
-
readonly signature: "string";
|
|
171
|
-
};
|
|
172
|
-
readonly idempotencyKey: "event_id";
|
|
173
|
-
readonly delivery: "at-least-once";
|
|
174
|
-
readonly billingOnly: true;
|
|
175
|
-
readonly realtimeEnforced: false;
|
|
176
|
-
readonly postStreamBillingOnly: true;
|
|
177
|
-
readonly postStreamRealtimeEnforced: false;
|
|
178
|
-
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.";
|
|
179
|
-
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.";
|
|
22
|
+
export declare const RUNTIME_BODY_HASH_CONTRACT: {
|
|
23
|
+
readonly algorithm: "SHA-256";
|
|
24
|
+
readonly encoding: "hex-lower";
|
|
25
|
+
readonly source: "raw-request-bytes";
|
|
26
|
+
readonly emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
27
|
+
readonly maxBodyBytes: 10485760;
|
|
28
|
+
readonly streamingExemptToken: "STREAM";
|
|
29
|
+
readonly streamingExemptContentTypes: readonly ["text/event-stream", "application/octet-stream", "multipart/form-data"];
|
|
30
|
+
readonly overMaxStatus: 413;
|
|
180
31
|
};
|
|
181
32
|
export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
|
|
182
33
|
readonly headers: {
|
|
@@ -198,14 +49,18 @@ export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
|
|
|
198
49
|
readonly payload: {
|
|
199
50
|
readonly method: "string";
|
|
200
51
|
readonly path: "string";
|
|
201
|
-
readonly rawDimsUnits: "Record<string, number
|
|
52
|
+
readonly rawDimsUnits: "Record<string, number>?";
|
|
202
53
|
readonly measureContext: "Record<string, unknown>?";
|
|
203
54
|
readonly creditUnitsConsumed: "Record<string, number>?";
|
|
55
|
+
readonly measurementsVersion: "1?";
|
|
56
|
+
readonly measurements: "Array<{ meter: string; values: Record<string, number>; dims?: Record<string, string> }>?";
|
|
57
|
+
readonly quote: "{ currency: string; amountNanos: string }?";
|
|
204
58
|
};
|
|
205
59
|
readonly errors: {
|
|
206
60
|
readonly missingToken: "missing_token";
|
|
207
61
|
readonly invalidMeterKey: "invalid_meter_key";
|
|
208
62
|
readonly invalidMeterValue: "invalid_meter_value";
|
|
63
|
+
readonly invalidQuote: "invalid_quote";
|
|
209
64
|
};
|
|
210
65
|
readonly httpAdapter: {
|
|
211
66
|
readonly input: "Request";
|
|
@@ -215,33 +70,3 @@ export declare const RUNTIME_RESPONSE_METERING_CONTRACT: {
|
|
|
215
70
|
readonly gatewayStripsInternalHeaders: true;
|
|
216
71
|
};
|
|
217
72
|
};
|
|
218
|
-
export declare const RUNTIME_HEALTH_CONTRACT: {
|
|
219
|
-
readonly endpoint: "/v1/runtime/health";
|
|
220
|
-
readonly method: "POST";
|
|
221
|
-
readonly request: {
|
|
222
|
-
readonly instanceId: "string?";
|
|
223
|
-
readonly status: "starting | ready | degraded | stopping";
|
|
224
|
-
};
|
|
225
|
-
readonly readinessStates: readonly ["UNKNOWN", "WAITING", "READY", "DEGRADED", "OFFLINE"];
|
|
226
|
-
readonly checks: readonly ["runtime_token_valid", "bootstrapped", "tunnel_running", "signed_request_2xx", "unsigned_request_401", "stale_signature_401", "wrong_route_401", "metering_observed"];
|
|
227
|
-
readonly report: {
|
|
228
|
-
readonly runtimeToken: "boolean";
|
|
229
|
-
readonly bootstrap: "boolean";
|
|
230
|
-
readonly tunnel: "string?";
|
|
231
|
-
readonly verification: "boolean";
|
|
232
|
-
readonly metering: "boolean";
|
|
233
|
-
};
|
|
234
|
-
};
|
|
235
|
-
export declare const RUNTIME_TRANSPORT_CONTRACT: {
|
|
236
|
-
readonly modes: {
|
|
237
|
-
readonly direct: "Gateway fetches the builder's public origin URL; the SDK middleware fail-closed-verifies every request via Ed25519 request signing. Provisions zero Cloudflare objects. Available on all tiers; also the dev path.";
|
|
238
|
-
readonly tunnel: "Farther Shore provisions a private outbound Cloudflare Tunnel; no inbound port. The Production-secure tier. Consumes Cloudflare tunnel/route slots.";
|
|
239
|
-
};
|
|
240
|
-
readonly runners: {
|
|
241
|
-
readonly embedded: "fs.start() supervises cloudflared as a child process (default DX).";
|
|
242
|
-
readonly sidecar: "Vanilla cloudflare/cloudflared container beside the app (production / non-Node).";
|
|
243
|
-
};
|
|
244
|
-
readonly channelTrust: readonly ["tunnel"];
|
|
245
|
-
readonly requestTrust: "x-fs-signature";
|
|
246
|
-
readonly invariant: "Channel trust (tunnel) and request trust (the X-FS-* signature) are distinct layers; both always apply. CF-Access-* headers are transport-layer only and are IGNORED by the SDK.";
|
|
247
|
-
};
|