@farthershore/backend 0.2.0 → 0.8.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 +4 -0
- package/dist/adapters/express.js +7 -1
- package/dist/index.js +366 -87
- package/dist/types/core/errors.d.ts +12 -1
- package/dist/types/core/runtime.d.ts +19 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/reflect/index.d.ts +16 -0
- package/dist/types/reflect/reconcile.d.ts +42 -0
- package/dist/types/reflect/route-canon.d.ts +20 -0
- package/dist/types/runtime-types.d.ts +61 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ Runtime metering and gateway-verification SDK for builder upstreams. Install one
|
|
|
4
4
|
package, set one token (`FS_RUNTIME_TOKEN`), and Farther Shore handles signed
|
|
5
5
|
gateway-to-upstream request verification plus response-bound usage reporting.
|
|
6
6
|
|
|
7
|
+
> **Status: `0.8.0` (lockstep SDK family).** Published at the SAME version as
|
|
8
|
+
> `@farthershore/farthershore-js` and `@farthershore/product` — pin the three
|
|
9
|
+
> together. Pre-1.0: minor bumps may break.
|
|
10
|
+
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```sh
|
package/dist/adapters/express.js
CHANGED
|
@@ -4,11 +4,17 @@ import { createRequire as __createRequire } from "node:module";const require=__c
|
|
|
4
4
|
var FartherShoreError = class extends Error {
|
|
5
5
|
code;
|
|
6
6
|
status;
|
|
7
|
-
|
|
7
|
+
/** Present only when this error is a self-minted plan-limit deny (rare; the
|
|
8
|
+
* backend usually relays the gateway's descriptor-bearing deny instead). */
|
|
9
|
+
limitDescriptor;
|
|
10
|
+
constructor(code, message, status, limitDescriptor) {
|
|
8
11
|
super(message);
|
|
9
12
|
this.name = "FartherShoreError";
|
|
10
13
|
this.code = code;
|
|
11
14
|
this.status = status ?? statusForCode(code);
|
|
15
|
+
if (limitDescriptor !== void 0) {
|
|
16
|
+
this.limitDescriptor = limitDescriptor;
|
|
17
|
+
}
|
|
12
18
|
}
|
|
13
19
|
};
|
|
14
20
|
function statusForCode(code) {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,285 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "node:module";const require=__createRequire(import.meta.url);
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/reflect/route-canon.ts
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
function normalizeSegment(seg) {
|
|
15
|
+
if (seg === "**") return "{rest}";
|
|
16
|
+
if (seg === "*") return "{splat}";
|
|
17
|
+
if (seg.startsWith(":")) return `{${seg.slice(1)}}`;
|
|
18
|
+
if (seg.startsWith("[") && seg.endsWith("]")) return `{${seg.slice(1, -1)}}`;
|
|
19
|
+
return seg;
|
|
20
|
+
}
|
|
21
|
+
function normalizePath(path) {
|
|
22
|
+
if (path === "/" || path === "") return "/";
|
|
23
|
+
const lead = path.startsWith("/");
|
|
24
|
+
const out = path.split("/").filter((s) => s.length > 0).map(normalizeSegment).join("/");
|
|
25
|
+
return lead ? `/${out}` : out;
|
|
26
|
+
}
|
|
27
|
+
function pathSpecificity(path) {
|
|
28
|
+
return path.split("/").filter(
|
|
29
|
+
(s) => s.length > 0 && !s.startsWith("{") && s !== "*" && s !== "**"
|
|
30
|
+
).length;
|
|
31
|
+
}
|
|
32
|
+
function canonicalSort(routes) {
|
|
33
|
+
return [...routes].sort(
|
|
34
|
+
(a, b) => pathSpecificity(b.path) - pathSpecificity(a.path) || a.method.localeCompare(b.method) || a.path.localeCompare(b.path)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function surfaceHash(routes) {
|
|
38
|
+
const canonical = canonicalSort(routes).map(
|
|
39
|
+
(r) => `${r.method.toUpperCase()} ${r.path}`
|
|
40
|
+
);
|
|
41
|
+
const h = createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
42
|
+
return `sha256:${h}`;
|
|
43
|
+
}
|
|
44
|
+
var init_route_canon = __esm({
|
|
45
|
+
"src/reflect/route-canon.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/reflect/index.ts
|
|
51
|
+
var reflect_exports = {};
|
|
52
|
+
__export(reflect_exports, {
|
|
53
|
+
reflectRoutes: () => reflectRoutes,
|
|
54
|
+
reflectRoutesDetailed: () => reflectRoutesDetailed
|
|
55
|
+
});
|
|
56
|
+
function methodsOf(route) {
|
|
57
|
+
const out = /* @__PURE__ */ new Set();
|
|
58
|
+
if (route.methods) {
|
|
59
|
+
for (const [m, on] of Object.entries(route.methods))
|
|
60
|
+
if (on) out.add(m.toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
for (const s of route.stack ?? []) {
|
|
63
|
+
if (typeof s.method === "string") out.add(s.method.toUpperCase());
|
|
64
|
+
}
|
|
65
|
+
return [...out].filter((m) => VALID_METHODS.has(m));
|
|
66
|
+
}
|
|
67
|
+
function rootStack(app) {
|
|
68
|
+
const a = app;
|
|
69
|
+
return a?.router?.stack ?? a?._router?.stack;
|
|
70
|
+
}
|
|
71
|
+
function prefixFromRegexp(re) {
|
|
72
|
+
if (!re) return void 0;
|
|
73
|
+
let s = re.source;
|
|
74
|
+
if (s === "^\\/?$" || s === "^\\/") return "";
|
|
75
|
+
s = s.replace(/^\^/, "").replace(/\\\/\?\(\?=\\\/\|\$\)$/, "").replace(/\(\?=\\\/\|\$\)$/, "").replace(/\\\/\?$/, "").replace(/\$$/, "");
|
|
76
|
+
s = s.replace(/\\\//g, "/");
|
|
77
|
+
if (/[()?:*+\[\]|]/.test(s)) return void 0;
|
|
78
|
+
return s.startsWith("/") ? s : `/${s}`;
|
|
79
|
+
}
|
|
80
|
+
function walk(stack, prefix, out, counters) {
|
|
81
|
+
for (const layer of stack) {
|
|
82
|
+
if (layer.route && typeof layer.route.path === "string") {
|
|
83
|
+
const path = normalizePath(`${prefix}${layer.route.path}`);
|
|
84
|
+
for (const method of methodsOf(layer.route)) {
|
|
85
|
+
out.push({ method, path });
|
|
86
|
+
}
|
|
87
|
+
} else if (layer.handle?.stack && Array.isArray(layer.handle.stack)) {
|
|
88
|
+
const sub = prefixFromRegexp(layer.regexp);
|
|
89
|
+
if (sub === void 0 && layer.name === "router") {
|
|
90
|
+
counters.unreflectable += layer.handle.stack.filter(
|
|
91
|
+
(l) => l.route
|
|
92
|
+
).length;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
walk(layer.handle.stack, `${prefix}${sub ?? ""}`, out, counters);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function reflectRoutesDetailed(app, _opts) {
|
|
100
|
+
const stack = rootStack(app);
|
|
101
|
+
if (!stack) return { routes: [], unreflectable: 0 };
|
|
102
|
+
const raw = [];
|
|
103
|
+
const counters = { unreflectable: 0 };
|
|
104
|
+
walk(stack, "", raw, counters);
|
|
105
|
+
const seen = /* @__PURE__ */ new Set();
|
|
106
|
+
const routes = [];
|
|
107
|
+
for (const r of raw) {
|
|
108
|
+
if (r.method === "HEAD") continue;
|
|
109
|
+
const key2 = `${r.method} ${r.path}`;
|
|
110
|
+
if (seen.has(key2)) continue;
|
|
111
|
+
seen.add(key2);
|
|
112
|
+
routes.push(r);
|
|
113
|
+
}
|
|
114
|
+
return { routes, unreflectable: counters.unreflectable };
|
|
115
|
+
}
|
|
116
|
+
function reflectRoutes(app, opts) {
|
|
117
|
+
return reflectRoutesDetailed(app, opts).routes;
|
|
118
|
+
}
|
|
119
|
+
var VALID_METHODS;
|
|
120
|
+
var init_reflect = __esm({
|
|
121
|
+
"src/reflect/index.ts"() {
|
|
122
|
+
"use strict";
|
|
123
|
+
init_route_canon();
|
|
124
|
+
VALID_METHODS = /* @__PURE__ */ new Set([
|
|
125
|
+
"GET",
|
|
126
|
+
"POST",
|
|
127
|
+
"PUT",
|
|
128
|
+
"PATCH",
|
|
129
|
+
"DELETE",
|
|
130
|
+
"OPTIONS"
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/reflect/reconcile.ts
|
|
136
|
+
var reconcile_exports = {};
|
|
137
|
+
__export(reconcile_exports, {
|
|
138
|
+
reconcileOnStartup: () => reconcileOnStartup
|
|
139
|
+
});
|
|
140
|
+
function key(r) {
|
|
141
|
+
return `${r.method.toUpperCase()} ${r.path}`;
|
|
142
|
+
}
|
|
143
|
+
async function reconcileOnStartup(args) {
|
|
144
|
+
const { reflected, bootstrap, report } = args;
|
|
145
|
+
const reflectedHash = surfaceHash(reflected);
|
|
146
|
+
const lockVersion = bootstrap.lock?.lockVersion ?? 0;
|
|
147
|
+
const inSync = bootstrap.lock?.surfaceHash === reflectedHash;
|
|
148
|
+
const result = {
|
|
149
|
+
inSync,
|
|
150
|
+
reportedDrift: false,
|
|
151
|
+
confirmedRouteIds: []
|
|
152
|
+
};
|
|
153
|
+
const servedKeys = new Set(reflected.map(key));
|
|
154
|
+
const confirmedRouteIds = (bootstrap.routes ?? []).filter(
|
|
155
|
+
(r) => r.pending === true && servedKeys.has(key(r)) && typeof r.id === "string"
|
|
156
|
+
).map((r) => r.id);
|
|
157
|
+
try {
|
|
158
|
+
if (confirmedRouteIds.length > 0) {
|
|
159
|
+
await report.confirmServed({
|
|
160
|
+
backendId: bootstrap.backendId,
|
|
161
|
+
lockVersion,
|
|
162
|
+
servedRouteIds: confirmedRouteIds
|
|
163
|
+
});
|
|
164
|
+
result.confirmedRouteIds = confirmedRouteIds;
|
|
165
|
+
}
|
|
166
|
+
if (!inSync) {
|
|
167
|
+
await report.reportDrift({
|
|
168
|
+
backendId: bootstrap.backendId,
|
|
169
|
+
lockVersion,
|
|
170
|
+
reflectedSurfaceHash: reflectedHash,
|
|
171
|
+
routes: reflected.map((r) => ({ method: r.method, path: r.path }))
|
|
172
|
+
});
|
|
173
|
+
result.reportedDrift = true;
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
result.reportError = err instanceof Error ? err.message : String(err);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
var init_reconcile = __esm({
|
|
181
|
+
"src/reflect/reconcile.ts"() {
|
|
182
|
+
"use strict";
|
|
183
|
+
init_route_canon();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// src/generated/runtime-contract.ts
|
|
188
|
+
var RUNTIME_BODY_HASH_CONTRACT = {
|
|
189
|
+
algorithm: "SHA-256",
|
|
190
|
+
encoding: "hex-lower",
|
|
191
|
+
source: "raw-request-bytes",
|
|
192
|
+
emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
193
|
+
maxBodyBytes: 10485760,
|
|
194
|
+
streamingExemptToken: "STREAM",
|
|
195
|
+
streamingExemptContentTypes: [
|
|
196
|
+
"text/event-stream",
|
|
197
|
+
"application/octet-stream",
|
|
198
|
+
"multipart/form-data"
|
|
199
|
+
],
|
|
200
|
+
overMaxStatus: 413
|
|
201
|
+
};
|
|
202
|
+
var RUNTIME_ERROR_CODES = {
|
|
203
|
+
missingSignature: "missing_signature",
|
|
204
|
+
malformedSignature: "malformed_signature",
|
|
205
|
+
unknownKeyId: "unknown_key_id",
|
|
206
|
+
jwksUnavailable: "jwks_unavailable",
|
|
207
|
+
badSignature: "bad_signature",
|
|
208
|
+
bodyHashMismatch: "body_hash_mismatch",
|
|
209
|
+
routeMismatch: "route_mismatch",
|
|
210
|
+
clockSkew: "clock_skew",
|
|
211
|
+
expiredSignature: "expired_signature",
|
|
212
|
+
replayedNonce: "replayed_nonce",
|
|
213
|
+
bodyTooLarge: "body_too_large",
|
|
214
|
+
environmentMismatch: "environment_mismatch",
|
|
215
|
+
missingToken: "missing_token",
|
|
216
|
+
invalidToken: "invalid_token"
|
|
217
|
+
};
|
|
218
|
+
var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
219
|
+
headers: {
|
|
220
|
+
payload: "x-fs-metering",
|
|
221
|
+
signature: "x-fs-metering-sig",
|
|
222
|
+
token: "x-fs-metering-token"
|
|
223
|
+
},
|
|
224
|
+
token: {
|
|
225
|
+
environmentVariable: "FS_RUNTIME_TOKEN",
|
|
226
|
+
presentation: "x-fs-metering-token",
|
|
227
|
+
storage: "sha256-hash-only"
|
|
228
|
+
},
|
|
229
|
+
signature: {
|
|
230
|
+
algorithm: "HMAC-SHA256",
|
|
231
|
+
encoding: "base64url",
|
|
232
|
+
input: "payload-json",
|
|
233
|
+
secret: "presented-runtime-token"
|
|
234
|
+
},
|
|
235
|
+
payload: {
|
|
236
|
+
method: "string",
|
|
237
|
+
path: "string",
|
|
238
|
+
rawDimsUnits: "Record<string, number>",
|
|
239
|
+
measureContext: "Record<string, unknown>?",
|
|
240
|
+
creditUnitsConsumed: "Record<string, number>?"
|
|
241
|
+
},
|
|
242
|
+
errors: {
|
|
243
|
+
missingToken: "missing_token",
|
|
244
|
+
invalidMeterKey: "invalid_meter_key",
|
|
245
|
+
invalidMeterValue: "invalid_meter_value"
|
|
246
|
+
},
|
|
247
|
+
httpAdapter: {
|
|
248
|
+
input: "Request",
|
|
249
|
+
output: "Response",
|
|
250
|
+
networkCalls: false,
|
|
251
|
+
preserves: ["body", "headers", "status", "statusText"],
|
|
252
|
+
gatewayStripsInternalHeaders: true
|
|
253
|
+
}
|
|
254
|
+
};
|
|
2
255
|
|
|
3
256
|
// src/runtime-types.ts
|
|
257
|
+
var RUNTIME_ERROR_CODE_TO_ERROR_CODE = {
|
|
258
|
+
// Credential / token presentation faults → UNAUTHORIZED (401).
|
|
259
|
+
[RUNTIME_ERROR_CODES.missingToken]: "UNAUTHORIZED",
|
|
260
|
+
[RUNTIME_ERROR_CODES.invalidToken]: "UNAUTHORIZED",
|
|
261
|
+
// Signature / key faults → UNAUTHORIZED (401, fail-closed verification).
|
|
262
|
+
[RUNTIME_ERROR_CODES.missingSignature]: "UNAUTHORIZED",
|
|
263
|
+
[RUNTIME_ERROR_CODES.malformedSignature]: "UNAUTHORIZED",
|
|
264
|
+
[RUNTIME_ERROR_CODES.unknownKeyId]: "UNAUTHORIZED",
|
|
265
|
+
[RUNTIME_ERROR_CODES.badSignature]: "UNAUTHORIZED",
|
|
266
|
+
[RUNTIME_ERROR_CODES.expiredSignature]: "UNAUTHORIZED",
|
|
267
|
+
// Replay / freshness faults → UNAUTHORIZED (401).
|
|
268
|
+
[RUNTIME_ERROR_CODES.clockSkew]: "UNAUTHORIZED",
|
|
269
|
+
[RUNTIME_ERROR_CODES.replayedNonce]: "UNAUTHORIZED",
|
|
270
|
+
// Request/route binding faults → UNAUTHORIZED (401, fail-closed).
|
|
271
|
+
[RUNTIME_ERROR_CODES.bodyHashMismatch]: "UNAUTHORIZED",
|
|
272
|
+
[RUNTIME_ERROR_CODES.routeMismatch]: "UNAUTHORIZED",
|
|
273
|
+
[RUNTIME_ERROR_CODES.environmentMismatch]: "UNAUTHORIZED",
|
|
274
|
+
// JWKS fetch unavailable — dependency fault (still 401 to the client), but
|
|
275
|
+
// the canonical code keeps the "dependency down" semantic for callers.
|
|
276
|
+
[RUNTIME_ERROR_CODES.jwksUnavailable]: "SERVICE_UNAVAILABLE",
|
|
277
|
+
// The single non-401 (413) — oversized request body.
|
|
278
|
+
[RUNTIME_ERROR_CODES.bodyTooLarge]: "VALIDATION_ERROR"
|
|
279
|
+
};
|
|
280
|
+
function runtimeErrorToErrorCode(code) {
|
|
281
|
+
return RUNTIME_ERROR_CODE_TO_ERROR_CODE[code] ?? "INTERNAL_ERROR";
|
|
282
|
+
}
|
|
4
283
|
var FS_RUNTIME_TOKEN_ENV = "FS_RUNTIME_TOKEN";
|
|
5
284
|
var RUNTIME_TOKEN_PREFIXES = {
|
|
6
285
|
live: "fsrt_live_",
|
|
@@ -10,7 +289,13 @@ var RUNTIME_TOKEN_CAPABILITIES = [
|
|
|
10
289
|
"gateway_verification",
|
|
11
290
|
"metering",
|
|
12
291
|
"health",
|
|
13
|
-
"tunnel"
|
|
292
|
+
"tunnel",
|
|
293
|
+
// Hand-maintained mirror of @farthershore/contracts RUNTIME_TOKEN_CAPABILITIES
|
|
294
|
+
// (`runtime.ts`). Bound to that source by the SET-EQUALITY + ORDER assertions
|
|
295
|
+
// in `deny-taxonomy-drift.test.ts` (test-only contracts devDep) — NOT by the
|
|
296
|
+
// generated runtime-contract.ts, which mirrors only RUNTIME_ERROR_CODES.
|
|
297
|
+
// `drift_report` is the opt-in capability for reporting route drift.
|
|
298
|
+
"drift_report"
|
|
14
299
|
];
|
|
15
300
|
var RUNTIME_HEADER_NAMES = {
|
|
16
301
|
signature: "x-fs-signature",
|
|
@@ -117,13 +402,13 @@ async function importEd25519PublicKey(jwk) {
|
|
|
117
402
|
return crypto.subtle.importKey("jwk", { ...jwk, alg: void 0 }, { name: ED25519_ALGORITHM }, false, ["verify"]);
|
|
118
403
|
}
|
|
119
404
|
async function signCanonicalString(canonical, privateJwk) {
|
|
120
|
-
const
|
|
121
|
-
const sig = await crypto.subtle.sign(ED25519_ALGORITHM,
|
|
405
|
+
const key2 = await importEd25519PrivateKey(privateJwk);
|
|
406
|
+
const sig = await crypto.subtle.sign(ED25519_ALGORITHM, key2, new TextEncoder().encode(canonical));
|
|
122
407
|
return base64UrlEncode(new Uint8Array(sig));
|
|
123
408
|
}
|
|
124
409
|
async function verifyCanonicalSignature(canonical, signatureB64Url, publicJwk) {
|
|
125
|
-
const
|
|
126
|
-
return crypto.subtle.verify(ED25519_ALGORITHM,
|
|
410
|
+
const key2 = await importEd25519PublicKey(publicJwk);
|
|
411
|
+
return crypto.subtle.verify(ED25519_ALGORITHM, key2, base64UrlDecode(signatureB64Url), new TextEncoder().encode(canonical));
|
|
127
412
|
}
|
|
128
413
|
function toHex(bytes) {
|
|
129
414
|
let out = "";
|
|
@@ -159,11 +444,17 @@ var runtimeTokenKind2 = runtimeTokenKind;
|
|
|
159
444
|
var FartherShoreError = class extends Error {
|
|
160
445
|
code;
|
|
161
446
|
status;
|
|
162
|
-
|
|
447
|
+
/** Present only when this error is a self-minted plan-limit deny (rare; the
|
|
448
|
+
* backend usually relays the gateway's descriptor-bearing deny instead). */
|
|
449
|
+
limitDescriptor;
|
|
450
|
+
constructor(code, message, status, limitDescriptor) {
|
|
163
451
|
super(message);
|
|
164
452
|
this.name = "FartherShoreError";
|
|
165
453
|
this.code = code;
|
|
166
454
|
this.status = status ?? statusForCode(code);
|
|
455
|
+
if (limitDescriptor !== void 0) {
|
|
456
|
+
this.limitDescriptor = limitDescriptor;
|
|
457
|
+
}
|
|
167
458
|
}
|
|
168
459
|
};
|
|
169
460
|
function statusForCode(code) {
|
|
@@ -345,10 +636,10 @@ var JwksClient = class {
|
|
|
345
636
|
);
|
|
346
637
|
}
|
|
347
638
|
await this.refresh();
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
639
|
+
const key2 = this.keysByKid.get(kid);
|
|
640
|
+
if (key2) {
|
|
350
641
|
this.negativeKids.delete(kid);
|
|
351
|
-
return
|
|
642
|
+
return key2;
|
|
352
643
|
}
|
|
353
644
|
this.rememberMissingKid(kid);
|
|
354
645
|
throw new FartherShoreError(
|
|
@@ -402,8 +693,8 @@ var JwksClient = class {
|
|
|
402
693
|
return;
|
|
403
694
|
}
|
|
404
695
|
const next = /* @__PURE__ */ new Map();
|
|
405
|
-
for (const
|
|
406
|
-
if (typeof
|
|
696
|
+
for (const key2 of doc.keys ?? []) {
|
|
697
|
+
if (typeof key2.kid === "string") next.set(key2.kid, key2);
|
|
407
698
|
}
|
|
408
699
|
this.keysByKid = next;
|
|
409
700
|
this.fetchedAt = this.now();
|
|
@@ -1032,12 +1323,12 @@ function headerGetter(headers) {
|
|
|
1032
1323
|
return (name) => h.get(name) ?? void 0;
|
|
1033
1324
|
}
|
|
1034
1325
|
const lower = /* @__PURE__ */ new Map();
|
|
1035
|
-
for (const [
|
|
1326
|
+
for (const [key2, value] of Object.entries(
|
|
1036
1327
|
headers
|
|
1037
1328
|
)) {
|
|
1038
1329
|
if (value === void 0) continue;
|
|
1039
1330
|
lower.set(
|
|
1040
|
-
|
|
1331
|
+
key2.toLowerCase(),
|
|
1041
1332
|
Array.isArray(value) ? value[0] ?? "" : value
|
|
1042
1333
|
);
|
|
1043
1334
|
}
|
|
@@ -1046,7 +1337,8 @@ function headerGetter(headers) {
|
|
|
1046
1337
|
|
|
1047
1338
|
// src/core/runtime.ts
|
|
1048
1339
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1049
|
-
var SDK_VERSION = "0.
|
|
1340
|
+
var SDK_VERSION = "0.8.0".length > 0 ? "0.8.0" : "0.0.0-dev";
|
|
1341
|
+
var CONTRACTS_FP = "af8995b9467842b6".length > 0 ? "af8995b9467842b6" : "0000000000000000";
|
|
1050
1342
|
var FartherShore = class {
|
|
1051
1343
|
bootstrapClient;
|
|
1052
1344
|
fetchImpl;
|
|
@@ -1117,6 +1409,60 @@ var FartherShore = class {
|
|
|
1117
1409
|
this.bootstrapped = true;
|
|
1118
1410
|
return config;
|
|
1119
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Boot-time route reconciliation (call once, before `listen()`). Reflects the
|
|
1414
|
+
* app's real route surface, diffs it against the declared lock from bootstrap,
|
|
1415
|
+
* REPORTS drift to the platform, and CONFIRMS the declared `pending` routes
|
|
1416
|
+
* this replica serves. Fail-OPEN: never throws / never blocks boot.
|
|
1417
|
+
*
|
|
1418
|
+
* The reflection code is dynamically imported so it stays OFF the per-request
|
|
1419
|
+
* verification hot path (the runtime stays route-unaware there). The report is
|
|
1420
|
+
* an OUTBOUND backend→core call (same channel as bootstrap/metering), so it
|
|
1421
|
+
* works for every transport (public_origin / mTLS / cloudflare_tunnel).
|
|
1422
|
+
*
|
|
1423
|
+
* Returns the reconcile result (or null if reflection is unavailable / boot
|
|
1424
|
+
* reporting failed). v1 reflects Express; `app` omitted → no-op.
|
|
1425
|
+
*/
|
|
1426
|
+
async ready(app) {
|
|
1427
|
+
try {
|
|
1428
|
+
const config = await this.ensureBootstrapped();
|
|
1429
|
+
if (app === void 0) return null;
|
|
1430
|
+
const [{ reflectRoutes: reflectRoutes2 }, { reconcileOnStartup: reconcileOnStartup2 }] = await Promise.all([
|
|
1431
|
+
Promise.resolve().then(() => (init_reflect(), reflect_exports)),
|
|
1432
|
+
Promise.resolve().then(() => (init_reconcile(), reconcile_exports))
|
|
1433
|
+
]);
|
|
1434
|
+
return await reconcileOnStartup2({
|
|
1435
|
+
reflected: reflectRoutes2(app),
|
|
1436
|
+
bootstrap: {
|
|
1437
|
+
backendId: config.backend.id,
|
|
1438
|
+
lock: config.lock,
|
|
1439
|
+
routes: config.routes
|
|
1440
|
+
},
|
|
1441
|
+
report: this.buildReportSink()
|
|
1442
|
+
});
|
|
1443
|
+
} catch {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
/** Outbound report sink for `ready()` — runtime-token-authed POSTs to core. */
|
|
1448
|
+
buildReportSink() {
|
|
1449
|
+
const post = async (path, body) => {
|
|
1450
|
+
const base = this.coreUrl.replace(/\/$/, "");
|
|
1451
|
+
const res = await this.fetchImpl(`${base}${path}`, {
|
|
1452
|
+
method: "POST",
|
|
1453
|
+
headers: {
|
|
1454
|
+
"content-type": "application/json",
|
|
1455
|
+
authorization: `Bearer ${this.runtimeToken}`
|
|
1456
|
+
},
|
|
1457
|
+
body: JSON.stringify(body)
|
|
1458
|
+
});
|
|
1459
|
+
if (!res.ok) throw new Error(`runtime report ${path} -> ${res.status}`);
|
|
1460
|
+
};
|
|
1461
|
+
return {
|
|
1462
|
+
reportDrift: (report) => post("/v1/runtime/drift", report),
|
|
1463
|
+
confirmServed: (confirm) => post("/v1/runtime/health", confirm)
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1120
1466
|
/**
|
|
1121
1467
|
* Framework-neutral verification primitive. Fail-closed: throws a typed
|
|
1122
1468
|
* FartherShoreError on any verification failure. Returns the verified context.
|
|
@@ -1229,75 +1575,6 @@ function readProcessEnv() {
|
|
|
1229
1575
|
return maybeProcess?.env ?? {};
|
|
1230
1576
|
}
|
|
1231
1577
|
|
|
1232
|
-
// src/generated/runtime-contract.ts
|
|
1233
|
-
var RUNTIME_BODY_HASH_CONTRACT = {
|
|
1234
|
-
algorithm: "SHA-256",
|
|
1235
|
-
encoding: "hex-lower",
|
|
1236
|
-
source: "raw-request-bytes",
|
|
1237
|
-
emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1238
|
-
maxBodyBytes: 10485760,
|
|
1239
|
-
streamingExemptToken: "STREAM",
|
|
1240
|
-
streamingExemptContentTypes: [
|
|
1241
|
-
"text/event-stream",
|
|
1242
|
-
"application/octet-stream",
|
|
1243
|
-
"multipart/form-data"
|
|
1244
|
-
],
|
|
1245
|
-
overMaxStatus: 413
|
|
1246
|
-
};
|
|
1247
|
-
var RUNTIME_ERROR_CODES = {
|
|
1248
|
-
missingSignature: "missing_signature",
|
|
1249
|
-
malformedSignature: "malformed_signature",
|
|
1250
|
-
unknownKeyId: "unknown_key_id",
|
|
1251
|
-
jwksUnavailable: "jwks_unavailable",
|
|
1252
|
-
badSignature: "bad_signature",
|
|
1253
|
-
bodyHashMismatch: "body_hash_mismatch",
|
|
1254
|
-
routeMismatch: "route_mismatch",
|
|
1255
|
-
clockSkew: "clock_skew",
|
|
1256
|
-
expiredSignature: "expired_signature",
|
|
1257
|
-
replayedNonce: "replayed_nonce",
|
|
1258
|
-
bodyTooLarge: "body_too_large",
|
|
1259
|
-
environmentMismatch: "environment_mismatch",
|
|
1260
|
-
missingToken: "missing_token",
|
|
1261
|
-
invalidToken: "invalid_token"
|
|
1262
|
-
};
|
|
1263
|
-
var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
1264
|
-
headers: {
|
|
1265
|
-
payload: "x-fs-metering",
|
|
1266
|
-
signature: "x-fs-metering-sig",
|
|
1267
|
-
token: "x-fs-metering-token"
|
|
1268
|
-
},
|
|
1269
|
-
token: {
|
|
1270
|
-
environmentVariable: "FS_RUNTIME_TOKEN",
|
|
1271
|
-
presentation: "x-fs-metering-token",
|
|
1272
|
-
storage: "sha256-hash-only"
|
|
1273
|
-
},
|
|
1274
|
-
signature: {
|
|
1275
|
-
algorithm: "HMAC-SHA256",
|
|
1276
|
-
encoding: "base64url",
|
|
1277
|
-
input: "payload-json",
|
|
1278
|
-
secret: "presented-runtime-token"
|
|
1279
|
-
},
|
|
1280
|
-
payload: {
|
|
1281
|
-
method: "string",
|
|
1282
|
-
path: "string",
|
|
1283
|
-
rawDimsUnits: "Record<string, number>",
|
|
1284
|
-
measureContext: "Record<string, unknown>?",
|
|
1285
|
-
creditUnitsConsumed: "Record<string, number>?"
|
|
1286
|
-
},
|
|
1287
|
-
errors: {
|
|
1288
|
-
missingToken: "missing_token",
|
|
1289
|
-
invalidMeterKey: "invalid_meter_key",
|
|
1290
|
-
invalidMeterValue: "invalid_meter_value"
|
|
1291
|
-
},
|
|
1292
|
-
httpAdapter: {
|
|
1293
|
-
input: "Request",
|
|
1294
|
-
output: "Response",
|
|
1295
|
-
networkCalls: false,
|
|
1296
|
-
preserves: ["body", "headers", "status", "statusText"],
|
|
1297
|
-
gatewayStripsInternalHeaders: true
|
|
1298
|
-
}
|
|
1299
|
-
};
|
|
1300
|
-
|
|
1301
1578
|
// src/adapters/express.ts
|
|
1302
1579
|
var STREAMING_CONTENT_TYPES = new Set(
|
|
1303
1580
|
RUNTIME_BODY_HASH_CONTRACT.streamingExemptContentTypes
|
|
@@ -1471,12 +1748,12 @@ function resolveToken(options) {
|
|
|
1471
1748
|
}
|
|
1472
1749
|
return token;
|
|
1473
1750
|
}
|
|
1474
|
-
function processEnv(
|
|
1751
|
+
function processEnv(key2) {
|
|
1475
1752
|
const maybeProcess = globalThis.process;
|
|
1476
|
-
return maybeProcess?.env?.[
|
|
1753
|
+
return maybeProcess?.env?.[key2];
|
|
1477
1754
|
}
|
|
1478
1755
|
async function signPayload(payload, token) {
|
|
1479
|
-
const
|
|
1756
|
+
const key2 = await crypto.subtle.importKey(
|
|
1480
1757
|
"raw",
|
|
1481
1758
|
new TextEncoder().encode(token),
|
|
1482
1759
|
{ name: "HMAC", hash: "SHA-256" },
|
|
@@ -1485,7 +1762,7 @@ async function signPayload(payload, token) {
|
|
|
1485
1762
|
);
|
|
1486
1763
|
const signature = await crypto.subtle.sign(
|
|
1487
1764
|
"HMAC",
|
|
1488
|
-
|
|
1765
|
+
key2,
|
|
1489
1766
|
new TextEncoder().encode(payload)
|
|
1490
1767
|
);
|
|
1491
1768
|
return base64url(new Uint8Array(signature));
|
|
@@ -1529,6 +1806,7 @@ export {
|
|
|
1529
1806
|
REDACTED_TOKEN,
|
|
1530
1807
|
RUNTIME_CLOCK_SKEW_SECONDS,
|
|
1531
1808
|
RUNTIME_ERROR_CODES,
|
|
1809
|
+
RUNTIME_ERROR_CODE_TO_ERROR_CODE,
|
|
1532
1810
|
RUNTIME_HEADER_NAMES,
|
|
1533
1811
|
RUNTIME_REPLAY_WINDOW_SECONDS,
|
|
1534
1812
|
RUNTIME_TOKEN_CAPABILITIES,
|
|
@@ -1545,6 +1823,7 @@ export {
|
|
|
1545
1823
|
initFromEnv2 as initFromEnv,
|
|
1546
1824
|
nodeSpawn,
|
|
1547
1825
|
reportHealth,
|
|
1826
|
+
runtimeErrorToErrorCode,
|
|
1548
1827
|
runtimeTokenKind2 as runtimeTokenKind,
|
|
1549
1828
|
signCanonicalString2 as signCanonicalString,
|
|
1550
1829
|
statusForCode,
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { RuntimeErrorCode } from "../generated/runtime-contract.js";
|
|
2
|
+
import type { LimitDescriptor } from "../runtime-types.js";
|
|
2
3
|
/**
|
|
3
4
|
* A verification / runtime failure with a stable, cross-language `code` and the
|
|
4
5
|
* fail-closed HTTP status the adapter should emit.
|
|
6
|
+
*
|
|
7
|
+
* Optionally carries a {@link LimitDescriptor} (`limitDescriptor`) when the
|
|
8
|
+
* failure is a plan-limit deny the backend chooses to surface itself, so SDKs
|
|
9
|
+
* can render an upgrade affordance from a backend-minted error. This is OPT-IN
|
|
10
|
+
* plumbing — the backend normally RELAYS the gateway's deny (which already
|
|
11
|
+
* carries the descriptor) rather than minting its own, so the field is absent on
|
|
12
|
+
* every verification/runtime failure. Additive; no behavior change.
|
|
5
13
|
*/
|
|
6
14
|
export declare class FartherShoreError extends Error {
|
|
7
15
|
readonly code: RuntimeErrorCode;
|
|
8
16
|
readonly status: number;
|
|
9
|
-
|
|
17
|
+
/** Present only when this error is a self-minted plan-limit deny (rare; the
|
|
18
|
+
* backend usually relays the gateway's descriptor-bearing deny instead). */
|
|
19
|
+
readonly limitDescriptor?: LimitDescriptor;
|
|
20
|
+
constructor(code: RuntimeErrorCode, message: string, status?: number, limitDescriptor?: LimitDescriptor);
|
|
10
21
|
}
|
|
11
22
|
/**
|
|
12
23
|
* Map a runtime error code to its fail-closed HTTP status. Oversized bodies are
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RuntimeBootstrapResponse, type RuntimeHealthReport } from "../runtime-types.js";
|
|
2
|
+
import type { ReconcileResult } from "../reflect/reconcile.js";
|
|
2
3
|
import { type MeterOptions } from "./metering.js";
|
|
3
4
|
import { type SpawnFn } from "./tunnel.js";
|
|
4
5
|
import { type FartherShoreRequestContext, type VerifyRequestInput } from "./verifyRequest.js";
|
|
@@ -40,6 +41,7 @@ export type FartherShoreInitOptions = {
|
|
|
40
41
|
instanceId?: string;
|
|
41
42
|
};
|
|
42
43
|
export declare const SDK_VERSION: string;
|
|
44
|
+
export declare const CONTRACTS_FP: string;
|
|
43
45
|
/**
|
|
44
46
|
* The runtime instance. Lazily bootstraps; holds the JWKS client, nonce cache,
|
|
45
47
|
* metering buffer, and shutdown hooks.
|
|
@@ -62,6 +64,23 @@ export declare class FartherShore {
|
|
|
62
64
|
constructor(options?: FartherShoreInitOptions);
|
|
63
65
|
/** Ensure bootstrap config is loaded; build the JWKS + metering clients. */
|
|
64
66
|
ensureBootstrapped(): Promise<RuntimeBootstrapResponse>;
|
|
67
|
+
/**
|
|
68
|
+
* Boot-time route reconciliation (call once, before `listen()`). Reflects the
|
|
69
|
+
* app's real route surface, diffs it against the declared lock from bootstrap,
|
|
70
|
+
* REPORTS drift to the platform, and CONFIRMS the declared `pending` routes
|
|
71
|
+
* this replica serves. Fail-OPEN: never throws / never blocks boot.
|
|
72
|
+
*
|
|
73
|
+
* The reflection code is dynamically imported so it stays OFF the per-request
|
|
74
|
+
* verification hot path (the runtime stays route-unaware there). The report is
|
|
75
|
+
* an OUTBOUND backend→core call (same channel as bootstrap/metering), so it
|
|
76
|
+
* works for every transport (public_origin / mTLS / cloudflare_tunnel).
|
|
77
|
+
*
|
|
78
|
+
* Returns the reconcile result (or null if reflection is unavailable / boot
|
|
79
|
+
* reporting failed). v1 reflects Express; `app` omitted → no-op.
|
|
80
|
+
*/
|
|
81
|
+
ready(app?: unknown): Promise<ReconcileResult | null>;
|
|
82
|
+
/** Outbound report sink for `ready()` — runtime-token-authed POSTs to core. */
|
|
83
|
+
private buildReportSink;
|
|
65
84
|
/**
|
|
66
85
|
* Framework-neutral verification primitive. Fail-closed: throws a typed
|
|
67
86
|
* FartherShoreError on any verification failure. Returns the verified context.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { ShutdownManager, type ShutdownHook } from "./core/shutdown.js";
|
|
|
13
13
|
export { CloudflaredSupervisor, nodeSpawn, REDACTED_TOKEN, type SpawnFn, type SpawnedTunnelProcess, type CloudflaredSupervisorOptions, type TunnelState, type TunnelStatus, } from "./core/tunnel.js";
|
|
14
14
|
export type { FartherShoreTunnelOptions } from "./core/runtime.js";
|
|
15
15
|
export { createExpressMiddleware, type ExpressMiddleware, type ExpressRequestLike, type ExpressResponseLike, type ExpressNext, type MiddlewareOptions, } from "./adapters/express.js";
|
|
16
|
-
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, } from "./runtime-types.js";
|
|
16
|
+
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";
|
|
17
17
|
export { RUNTIME_ERROR_CODES } from "./generated/runtime-contract.js";
|
|
18
18
|
export { hashBody, buildCanonicalSigningString, canonicalizeQuery, signCanonicalString, verifyCanonicalSignature, runtimeTokenKind, } from "./runtime-signing.js";
|
|
19
19
|
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";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReflectedRoute } from "./route-canon.js";
|
|
2
|
+
export interface ReflectResult {
|
|
3
|
+
routes: ReflectedRoute[];
|
|
4
|
+
/** Count of sub-mounted routes that couldn't be reflected (Express-5 mount). */
|
|
5
|
+
unreflectable: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ReflectOptions {
|
|
8
|
+
framework?: "express";
|
|
9
|
+
}
|
|
10
|
+
/** Reflect with diagnostics. Use `reflectRoutes` for just the canonical list. */
|
|
11
|
+
export declare function reflectRoutesDetailed(app: unknown, _opts?: ReflectOptions): ReflectResult;
|
|
12
|
+
/**
|
|
13
|
+
* Reflect the routes an Express app serves into the canonical `ReflectedRoute[]`.
|
|
14
|
+
* Deduped, HEAD-suppressed, canonical paths (`:id`→`{id}`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function reflectRoutes(app: unknown, opts?: ReflectOptions): ReflectedRoute[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ReflectedRoute, type DriftReport, type PendingConfirm } from "./route-canon.js";
|
|
2
|
+
/** The slice of the bootstrap response reconciliation needs. */
|
|
3
|
+
export interface StartupBootstrap {
|
|
4
|
+
backendId: string;
|
|
5
|
+
lock?: {
|
|
6
|
+
surfaceHash: string;
|
|
7
|
+
lockVersion: number;
|
|
8
|
+
};
|
|
9
|
+
routes?: Array<{
|
|
10
|
+
id?: string;
|
|
11
|
+
method: string;
|
|
12
|
+
path: string;
|
|
13
|
+
pending?: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
/** Where reconciliation sends its outbound reports (DI for tests + transport). */
|
|
17
|
+
export interface ReportSink {
|
|
18
|
+
/** PR-opening drift report → `POST /v1/runtime/drift`. */
|
|
19
|
+
reportDrift(report: DriftReport): Promise<void> | void;
|
|
20
|
+
/** Confirm served pending routes → rides `/v1/runtime/health`. */
|
|
21
|
+
confirmServed(confirm: PendingConfirm): Promise<void> | void;
|
|
22
|
+
}
|
|
23
|
+
export interface ReconcileResult {
|
|
24
|
+
/** Reflected surface matched the declared lock. */
|
|
25
|
+
inSync: boolean;
|
|
26
|
+
/** A drift report was sent. */
|
|
27
|
+
reportedDrift: boolean;
|
|
28
|
+
/** Route ids confirmed served (pending → clearable). */
|
|
29
|
+
confirmedRouteIds: string[];
|
|
30
|
+
/** A report sink call failed (swallowed — boot never blocks on it). */
|
|
31
|
+
reportError?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reconcile the reflected surface against the declared lock at startup.
|
|
35
|
+
* Idempotent + multi-replica-safe (all replicas compute the same hash; confirm
|
|
36
|
+
* carries `lockVersion` for the server's optimistic-concurrency check).
|
|
37
|
+
*/
|
|
38
|
+
export declare function reconcileOnStartup(args: {
|
|
39
|
+
reflected: readonly ReflectedRoute[];
|
|
40
|
+
bootstrap: StartupBootstrap;
|
|
41
|
+
report: ReportSink;
|
|
42
|
+
}): Promise<ReconcileResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A reflected route — method + canonical path. Mirrors contracts ReflectedRoute. */
|
|
2
|
+
export interface ReflectedRoute {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
/** Backend → core drift report. Mirrors contracts DriftReport. */
|
|
7
|
+
export interface DriftReport {
|
|
8
|
+
backendId: string;
|
|
9
|
+
lockVersion: number;
|
|
10
|
+
reflectedSurfaceHash: string;
|
|
11
|
+
routes: ReflectedRoute[];
|
|
12
|
+
}
|
|
13
|
+
/** Backend → core pending confirmation. Mirrors contracts PendingConfirm. */
|
|
14
|
+
export interface PendingConfirm {
|
|
15
|
+
backendId: string;
|
|
16
|
+
lockVersion: number;
|
|
17
|
+
servedRouteIds: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function normalizePath(path: string): string;
|
|
20
|
+
export declare function surfaceHash(routes: readonly ReflectedRoute[]): string;
|
|
@@ -1,11 +1,60 @@
|
|
|
1
|
+
import type { RuntimeErrorCode } from "./generated/runtime-contract.js";
|
|
1
2
|
export type { RuntimeErrorCode } from "./generated/runtime-contract.js";
|
|
3
|
+
/**
|
|
4
|
+
* C-1 — the machine-readable limit descriptor on a limit-deny body. A backend
|
|
5
|
+
* that surfaces a plan-limit deny carries this so SDKs can render an upgrade
|
|
6
|
+
* affordance. Structurally identical to the contracts `LimitDescriptor`.
|
|
7
|
+
*/
|
|
8
|
+
export interface LimitDescriptor {
|
|
9
|
+
/** Stable identifier for the limit hit — a `limitCode` VALUE
|
|
10
|
+
* (`quota` | `rate_limit` | `credit` | `resource:<name>`), NOT a wire code. */
|
|
11
|
+
limitCode: string;
|
|
12
|
+
/** Metered/resource dimension when known; null otherwise. */
|
|
13
|
+
dimension: string | null;
|
|
14
|
+
/** The cap the subscriber is at when known; null otherwise. */
|
|
15
|
+
currentCapacity: number | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* C-1 (BE-1) — the RUNTIME field set of the SDK-local {@link LimitDescriptor}
|
|
19
|
+
* mirror. `satisfies Record<keyof LimitDescriptor, true>` makes the compiler
|
|
20
|
+
* reject this if it drifts from the local interface; the drift guard
|
|
21
|
+
* (`deny-taxonomy-drift.test.ts`) then asserts it deep-equals the canonical
|
|
22
|
+
* contracts `LIMIT_DESCRIPTOR_FIELDS` at RUNTIME — so a hand-copy that adds or
|
|
23
|
+
* drops a field fails a test that actually runs. (Contracts-free: this is a
|
|
24
|
+
* plain local constant, never the published-types path to contracts.) */
|
|
25
|
+
export declare const LIMIT_DESCRIPTOR_FIELDS: {
|
|
26
|
+
limitCode: true;
|
|
27
|
+
dimension: true;
|
|
28
|
+
currentCapacity: true;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* C-2 — the canonical core `ErrorCode` VALUES this backend can map a
|
|
32
|
+
* `RuntimeErrorCode` onto (the codomain of {@link RUNTIME_ERROR_CODE_TO_ERROR_CODE}).
|
|
33
|
+
* The full core enum lives in the shared platform contracts; the SDK only needs
|
|
34
|
+
* the subset its bridge produces. Each is a verbatim core `ErrorCode` string —
|
|
35
|
+
* the drift guard asserts membership in the canonical enum.
|
|
36
|
+
*/
|
|
37
|
+
export type RuntimeMappedErrorCode = "UNAUTHORIZED" | "SERVICE_UNAVAILABLE" | "VALIDATION_ERROR" | "INTERNAL_ERROR";
|
|
38
|
+
/**
|
|
39
|
+
* C-2 — map every canonical {@link RuntimeErrorCode} (wire snake_case value) to
|
|
40
|
+
* the core `ErrorCode` it belongs to. Total over `RuntimeErrorCode` (the
|
|
41
|
+
* `Record<RuntimeErrorCode, …>` type makes a missing key a compile error). A
|
|
42
|
+
* faithful copy of contracts' `RUNTIME_ERROR_CODE_TO_ERROR_CODE`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const RUNTIME_ERROR_CODE_TO_ERROR_CODE: Record<RuntimeErrorCode, RuntimeMappedErrorCode>;
|
|
45
|
+
/**
|
|
46
|
+
* C-2 — translate a runtime code into the canonical core `ErrorCode`. Returns
|
|
47
|
+
* `INTERNAL_ERROR` for an unrecognized value (defensive; the map is total over
|
|
48
|
+
* known codes). Mirrors contracts' `runtimeErrorToErrorCode`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runtimeErrorToErrorCode(code: string): RuntimeMappedErrorCode;
|
|
2
51
|
export declare const FS_RUNTIME_TOKEN_ENV: "FS_RUNTIME_TOKEN";
|
|
3
52
|
export declare const RUNTIME_TOKEN_PREFIXES: {
|
|
4
53
|
readonly live: "fsrt_live_";
|
|
5
54
|
readonly test: "fsrt_test_";
|
|
6
55
|
};
|
|
7
56
|
export type RuntimeTokenKind = keyof typeof RUNTIME_TOKEN_PREFIXES;
|
|
8
|
-
export declare const RUNTIME_TOKEN_CAPABILITIES: readonly ["gateway_verification", "metering", "health", "tunnel"];
|
|
57
|
+
export declare const RUNTIME_TOKEN_CAPABILITIES: readonly ["gateway_verification", "metering", "health", "tunnel", "drift_report"];
|
|
9
58
|
export type RuntimeTokenCapability = (typeof RUNTIME_TOKEN_CAPABILITIES)[number];
|
|
10
59
|
export declare const RUNTIME_HEADER_NAMES: {
|
|
11
60
|
readonly signature: "x-fs-signature";
|
|
@@ -93,6 +142,14 @@ export type RuntimeRouteDescriptor = {
|
|
|
93
142
|
method: string;
|
|
94
143
|
path: string;
|
|
95
144
|
backendId: string;
|
|
145
|
+
/** Declared-but-not-yet-confirmed-served route. `fs.ready()` confirms the ones
|
|
146
|
+
* this replica actually serves so core can clear the flag and publish. */
|
|
147
|
+
pending?: boolean;
|
|
148
|
+
};
|
|
149
|
+
/** The declared route-surface lock the backend reconciles against at startup. */
|
|
150
|
+
export type RuntimeLockDescriptor = {
|
|
151
|
+
surfaceHash: string;
|
|
152
|
+
lockVersion: number;
|
|
96
153
|
};
|
|
97
154
|
export type RuntimeBootstrapResponse = {
|
|
98
155
|
product: {
|
|
@@ -113,6 +170,9 @@ export type RuntimeBootstrapResponse = {
|
|
|
113
170
|
metering: RuntimeMeteringConfig;
|
|
114
171
|
transport: RuntimeTransportConfig;
|
|
115
172
|
routes: RuntimeRouteDescriptor[];
|
|
173
|
+
/** Reflected route-surface lock (surfaceHash + version). Present once core
|
|
174
|
+
* serves it; `fs.ready()` diffs the reflected surface against it. */
|
|
175
|
+
lock?: RuntimeLockDescriptor;
|
|
116
176
|
policyVersion: string;
|
|
117
177
|
refreshAfterSeconds: number;
|
|
118
178
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farthershore/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Farther Shore backend SDK for builder upstreams: signed response usage, fail-closed gateway request verification, health, and lifecycle from FS_RUNTIME_TOKEN",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./dist/types/adapters/express.d.ts",
|
|
16
16
|
"import": "./dist/adapters/express.js"
|
|
17
17
|
},
|
|
18
|
+
"./reflect": {
|
|
19
|
+
"types": "./dist/types/reflect/index.d.ts",
|
|
20
|
+
"import": "./dist/reflect/index.js"
|
|
21
|
+
},
|
|
18
22
|
"./runtime": {
|
|
19
23
|
"types": "./dist/types/generated/runtime-contract.d.ts",
|
|
20
24
|
"import": "./dist/generated/runtime-contract.js"
|