@farthershore/backend 0.3.0 → 0.8.1
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 +114 -75
- package/dist/reflect/index.js +94 -0
- package/dist/types/core/errors.d.ts +12 -1
- package/dist/types/core/runtime.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/runtime-types.d.ts +49 -0
- package/package.json +1 -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.1` (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
|
@@ -12,7 +12,7 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/reflect/route-canon.ts
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
function normalizeSegment(seg) {
|
|
15
|
-
if (seg === "**") return "{
|
|
15
|
+
if (seg === "**") return "{__fs_rest}";
|
|
16
16
|
if (seg === "*") return "{splat}";
|
|
17
17
|
if (seg.startsWith(":")) return `{${seg.slice(1)}}`;
|
|
18
18
|
if (seg.startsWith("[") && seg.endsWith("]")) return `{${seg.slice(1, -1)}}`;
|
|
@@ -62,6 +62,7 @@ function methodsOf(route) {
|
|
|
62
62
|
for (const s of route.stack ?? []) {
|
|
63
63
|
if (typeof s.method === "string") out.add(s.method.toUpperCase());
|
|
64
64
|
}
|
|
65
|
+
if (out.has("GET")) out.delete("HEAD");
|
|
65
66
|
return [...out].filter((m) => VALID_METHODS.has(m));
|
|
66
67
|
}
|
|
67
68
|
function rootStack(app) {
|
|
@@ -105,7 +106,6 @@ function reflectRoutesDetailed(app, _opts) {
|
|
|
105
106
|
const seen = /* @__PURE__ */ new Set();
|
|
106
107
|
const routes = [];
|
|
107
108
|
for (const r of raw) {
|
|
108
|
-
if (r.method === "HEAD") continue;
|
|
109
109
|
const key2 = `${r.method} ${r.path}`;
|
|
110
110
|
if (seen.has(key2)) continue;
|
|
111
111
|
seen.add(key2);
|
|
@@ -123,6 +123,7 @@ var init_reflect = __esm({
|
|
|
123
123
|
init_route_canon();
|
|
124
124
|
VALID_METHODS = /* @__PURE__ */ new Set([
|
|
125
125
|
"GET",
|
|
126
|
+
"HEAD",
|
|
126
127
|
"POST",
|
|
127
128
|
"PUT",
|
|
128
129
|
"PATCH",
|
|
@@ -184,7 +185,102 @@ var init_reconcile = __esm({
|
|
|
184
185
|
}
|
|
185
186
|
});
|
|
186
187
|
|
|
188
|
+
// src/generated/runtime-contract.ts
|
|
189
|
+
var RUNTIME_BODY_HASH_CONTRACT = {
|
|
190
|
+
algorithm: "SHA-256",
|
|
191
|
+
encoding: "hex-lower",
|
|
192
|
+
source: "raw-request-bytes",
|
|
193
|
+
emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
194
|
+
maxBodyBytes: 10485760,
|
|
195
|
+
streamingExemptToken: "STREAM",
|
|
196
|
+
streamingExemptContentTypes: [
|
|
197
|
+
"text/event-stream",
|
|
198
|
+
"application/octet-stream",
|
|
199
|
+
"multipart/form-data"
|
|
200
|
+
],
|
|
201
|
+
overMaxStatus: 413
|
|
202
|
+
};
|
|
203
|
+
var RUNTIME_ERROR_CODES = {
|
|
204
|
+
missingSignature: "missing_signature",
|
|
205
|
+
malformedSignature: "malformed_signature",
|
|
206
|
+
unknownKeyId: "unknown_key_id",
|
|
207
|
+
jwksUnavailable: "jwks_unavailable",
|
|
208
|
+
badSignature: "bad_signature",
|
|
209
|
+
bodyHashMismatch: "body_hash_mismatch",
|
|
210
|
+
routeMismatch: "route_mismatch",
|
|
211
|
+
clockSkew: "clock_skew",
|
|
212
|
+
expiredSignature: "expired_signature",
|
|
213
|
+
replayedNonce: "replayed_nonce",
|
|
214
|
+
bodyTooLarge: "body_too_large",
|
|
215
|
+
environmentMismatch: "environment_mismatch",
|
|
216
|
+
missingToken: "missing_token",
|
|
217
|
+
invalidToken: "invalid_token"
|
|
218
|
+
};
|
|
219
|
+
var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
220
|
+
headers: {
|
|
221
|
+
payload: "x-fs-metering",
|
|
222
|
+
signature: "x-fs-metering-sig",
|
|
223
|
+
token: "x-fs-metering-token"
|
|
224
|
+
},
|
|
225
|
+
token: {
|
|
226
|
+
environmentVariable: "FS_RUNTIME_TOKEN",
|
|
227
|
+
presentation: "x-fs-metering-token",
|
|
228
|
+
storage: "sha256-hash-only"
|
|
229
|
+
},
|
|
230
|
+
signature: {
|
|
231
|
+
algorithm: "HMAC-SHA256",
|
|
232
|
+
encoding: "base64url",
|
|
233
|
+
input: "payload-json",
|
|
234
|
+
secret: "presented-runtime-token"
|
|
235
|
+
},
|
|
236
|
+
payload: {
|
|
237
|
+
method: "string",
|
|
238
|
+
path: "string",
|
|
239
|
+
rawDimsUnits: "Record<string, number>",
|
|
240
|
+
measureContext: "Record<string, unknown>?",
|
|
241
|
+
creditUnitsConsumed: "Record<string, number>?"
|
|
242
|
+
},
|
|
243
|
+
errors: {
|
|
244
|
+
missingToken: "missing_token",
|
|
245
|
+
invalidMeterKey: "invalid_meter_key",
|
|
246
|
+
invalidMeterValue: "invalid_meter_value"
|
|
247
|
+
},
|
|
248
|
+
httpAdapter: {
|
|
249
|
+
input: "Request",
|
|
250
|
+
output: "Response",
|
|
251
|
+
networkCalls: false,
|
|
252
|
+
preserves: ["body", "headers", "status", "statusText"],
|
|
253
|
+
gatewayStripsInternalHeaders: true
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
187
257
|
// src/runtime-types.ts
|
|
258
|
+
var RUNTIME_ERROR_CODE_TO_ERROR_CODE = {
|
|
259
|
+
// Credential / token presentation faults → UNAUTHORIZED (401).
|
|
260
|
+
[RUNTIME_ERROR_CODES.missingToken]: "UNAUTHORIZED",
|
|
261
|
+
[RUNTIME_ERROR_CODES.invalidToken]: "UNAUTHORIZED",
|
|
262
|
+
// Signature / key faults → UNAUTHORIZED (401, fail-closed verification).
|
|
263
|
+
[RUNTIME_ERROR_CODES.missingSignature]: "UNAUTHORIZED",
|
|
264
|
+
[RUNTIME_ERROR_CODES.malformedSignature]: "UNAUTHORIZED",
|
|
265
|
+
[RUNTIME_ERROR_CODES.unknownKeyId]: "UNAUTHORIZED",
|
|
266
|
+
[RUNTIME_ERROR_CODES.badSignature]: "UNAUTHORIZED",
|
|
267
|
+
[RUNTIME_ERROR_CODES.expiredSignature]: "UNAUTHORIZED",
|
|
268
|
+
// Replay / freshness faults → UNAUTHORIZED (401).
|
|
269
|
+
[RUNTIME_ERROR_CODES.clockSkew]: "UNAUTHORIZED",
|
|
270
|
+
[RUNTIME_ERROR_CODES.replayedNonce]: "UNAUTHORIZED",
|
|
271
|
+
// Request/route binding faults → UNAUTHORIZED (401, fail-closed).
|
|
272
|
+
[RUNTIME_ERROR_CODES.bodyHashMismatch]: "UNAUTHORIZED",
|
|
273
|
+
[RUNTIME_ERROR_CODES.routeMismatch]: "UNAUTHORIZED",
|
|
274
|
+
[RUNTIME_ERROR_CODES.environmentMismatch]: "UNAUTHORIZED",
|
|
275
|
+
// JWKS fetch unavailable — dependency fault (still 401 to the client), but
|
|
276
|
+
// the canonical code keeps the "dependency down" semantic for callers.
|
|
277
|
+
[RUNTIME_ERROR_CODES.jwksUnavailable]: "SERVICE_UNAVAILABLE",
|
|
278
|
+
// The single non-401 (413) — oversized request body.
|
|
279
|
+
[RUNTIME_ERROR_CODES.bodyTooLarge]: "VALIDATION_ERROR"
|
|
280
|
+
};
|
|
281
|
+
function runtimeErrorToErrorCode(code) {
|
|
282
|
+
return RUNTIME_ERROR_CODE_TO_ERROR_CODE[code] ?? "INTERNAL_ERROR";
|
|
283
|
+
}
|
|
188
284
|
var FS_RUNTIME_TOKEN_ENV = "FS_RUNTIME_TOKEN";
|
|
189
285
|
var RUNTIME_TOKEN_PREFIXES = {
|
|
190
286
|
live: "fsrt_live_",
|
|
@@ -195,8 +291,11 @@ var RUNTIME_TOKEN_CAPABILITIES = [
|
|
|
195
291
|
"metering",
|
|
196
292
|
"health",
|
|
197
293
|
"tunnel",
|
|
198
|
-
//
|
|
199
|
-
//
|
|
294
|
+
// Hand-maintained mirror of @farthershore/contracts RUNTIME_TOKEN_CAPABILITIES
|
|
295
|
+
// (`runtime.ts`). Bound to that source by the SET-EQUALITY + ORDER assertions
|
|
296
|
+
// in `deny-taxonomy-drift.test.ts` (test-only contracts devDep) — NOT by the
|
|
297
|
+
// generated runtime-contract.ts, which mirrors only RUNTIME_ERROR_CODES.
|
|
298
|
+
// `drift_report` is the opt-in capability for reporting route drift.
|
|
200
299
|
"drift_report"
|
|
201
300
|
];
|
|
202
301
|
var RUNTIME_HEADER_NAMES = {
|
|
@@ -346,11 +445,17 @@ var runtimeTokenKind2 = runtimeTokenKind;
|
|
|
346
445
|
var FartherShoreError = class extends Error {
|
|
347
446
|
code;
|
|
348
447
|
status;
|
|
349
|
-
|
|
448
|
+
/** Present only when this error is a self-minted plan-limit deny (rare; the
|
|
449
|
+
* backend usually relays the gateway's descriptor-bearing deny instead). */
|
|
450
|
+
limitDescriptor;
|
|
451
|
+
constructor(code, message, status, limitDescriptor) {
|
|
350
452
|
super(message);
|
|
351
453
|
this.name = "FartherShoreError";
|
|
352
454
|
this.code = code;
|
|
353
455
|
this.status = status ?? statusForCode(code);
|
|
456
|
+
if (limitDescriptor !== void 0) {
|
|
457
|
+
this.limitDescriptor = limitDescriptor;
|
|
458
|
+
}
|
|
354
459
|
}
|
|
355
460
|
};
|
|
356
461
|
function statusForCode(code) {
|
|
@@ -1233,7 +1338,8 @@ function headerGetter(headers) {
|
|
|
1233
1338
|
|
|
1234
1339
|
// src/core/runtime.ts
|
|
1235
1340
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1236
|
-
var SDK_VERSION = "0.
|
|
1341
|
+
var SDK_VERSION = "0.8.1".length > 0 ? "0.8.1" : "0.0.0-dev";
|
|
1342
|
+
var CONTRACTS_FP = "3c1f8e3d44799bd4".length > 0 ? "3c1f8e3d44799bd4" : "0000000000000000";
|
|
1237
1343
|
var FartherShore = class {
|
|
1238
1344
|
bootstrapClient;
|
|
1239
1345
|
fetchImpl;
|
|
@@ -1470,75 +1576,6 @@ function readProcessEnv() {
|
|
|
1470
1576
|
return maybeProcess?.env ?? {};
|
|
1471
1577
|
}
|
|
1472
1578
|
|
|
1473
|
-
// src/generated/runtime-contract.ts
|
|
1474
|
-
var RUNTIME_BODY_HASH_CONTRACT = {
|
|
1475
|
-
algorithm: "SHA-256",
|
|
1476
|
-
encoding: "hex-lower",
|
|
1477
|
-
source: "raw-request-bytes",
|
|
1478
|
-
emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1479
|
-
maxBodyBytes: 10485760,
|
|
1480
|
-
streamingExemptToken: "STREAM",
|
|
1481
|
-
streamingExemptContentTypes: [
|
|
1482
|
-
"text/event-stream",
|
|
1483
|
-
"application/octet-stream",
|
|
1484
|
-
"multipart/form-data"
|
|
1485
|
-
],
|
|
1486
|
-
overMaxStatus: 413
|
|
1487
|
-
};
|
|
1488
|
-
var RUNTIME_ERROR_CODES = {
|
|
1489
|
-
missingSignature: "missing_signature",
|
|
1490
|
-
malformedSignature: "malformed_signature",
|
|
1491
|
-
unknownKeyId: "unknown_key_id",
|
|
1492
|
-
jwksUnavailable: "jwks_unavailable",
|
|
1493
|
-
badSignature: "bad_signature",
|
|
1494
|
-
bodyHashMismatch: "body_hash_mismatch",
|
|
1495
|
-
routeMismatch: "route_mismatch",
|
|
1496
|
-
clockSkew: "clock_skew",
|
|
1497
|
-
expiredSignature: "expired_signature",
|
|
1498
|
-
replayedNonce: "replayed_nonce",
|
|
1499
|
-
bodyTooLarge: "body_too_large",
|
|
1500
|
-
environmentMismatch: "environment_mismatch",
|
|
1501
|
-
missingToken: "missing_token",
|
|
1502
|
-
invalidToken: "invalid_token"
|
|
1503
|
-
};
|
|
1504
|
-
var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
1505
|
-
headers: {
|
|
1506
|
-
payload: "x-fs-metering",
|
|
1507
|
-
signature: "x-fs-metering-sig",
|
|
1508
|
-
token: "x-fs-metering-token"
|
|
1509
|
-
},
|
|
1510
|
-
token: {
|
|
1511
|
-
environmentVariable: "FS_RUNTIME_TOKEN",
|
|
1512
|
-
presentation: "x-fs-metering-token",
|
|
1513
|
-
storage: "sha256-hash-only"
|
|
1514
|
-
},
|
|
1515
|
-
signature: {
|
|
1516
|
-
algorithm: "HMAC-SHA256",
|
|
1517
|
-
encoding: "base64url",
|
|
1518
|
-
input: "payload-json",
|
|
1519
|
-
secret: "presented-runtime-token"
|
|
1520
|
-
},
|
|
1521
|
-
payload: {
|
|
1522
|
-
method: "string",
|
|
1523
|
-
path: "string",
|
|
1524
|
-
rawDimsUnits: "Record<string, number>",
|
|
1525
|
-
measureContext: "Record<string, unknown>?",
|
|
1526
|
-
creditUnitsConsumed: "Record<string, number>?"
|
|
1527
|
-
},
|
|
1528
|
-
errors: {
|
|
1529
|
-
missingToken: "missing_token",
|
|
1530
|
-
invalidMeterKey: "invalid_meter_key",
|
|
1531
|
-
invalidMeterValue: "invalid_meter_value"
|
|
1532
|
-
},
|
|
1533
|
-
httpAdapter: {
|
|
1534
|
-
input: "Request",
|
|
1535
|
-
output: "Response",
|
|
1536
|
-
networkCalls: false,
|
|
1537
|
-
preserves: ["body", "headers", "status", "statusText"],
|
|
1538
|
-
gatewayStripsInternalHeaders: true
|
|
1539
|
-
}
|
|
1540
|
-
};
|
|
1541
|
-
|
|
1542
1579
|
// src/adapters/express.ts
|
|
1543
1580
|
var STREAMING_CONTENT_TYPES = new Set(
|
|
1544
1581
|
RUNTIME_BODY_HASH_CONTRACT.streamingExemptContentTypes
|
|
@@ -1770,6 +1807,7 @@ export {
|
|
|
1770
1807
|
REDACTED_TOKEN,
|
|
1771
1808
|
RUNTIME_CLOCK_SKEW_SECONDS,
|
|
1772
1809
|
RUNTIME_ERROR_CODES,
|
|
1810
|
+
RUNTIME_ERROR_CODE_TO_ERROR_CODE,
|
|
1773
1811
|
RUNTIME_HEADER_NAMES,
|
|
1774
1812
|
RUNTIME_REPLAY_WINDOW_SECONDS,
|
|
1775
1813
|
RUNTIME_TOKEN_CAPABILITIES,
|
|
@@ -1786,6 +1824,7 @@ export {
|
|
|
1786
1824
|
initFromEnv2 as initFromEnv,
|
|
1787
1825
|
nodeSpawn,
|
|
1788
1826
|
reportHealth,
|
|
1827
|
+
runtimeErrorToErrorCode,
|
|
1789
1828
|
runtimeTokenKind2 as runtimeTokenKind,
|
|
1790
1829
|
signCanonicalString2 as signCanonicalString,
|
|
1791
1830
|
statusForCode,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "node:module";const require=__createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/reflect/route-canon.ts
|
|
4
|
+
function normalizeSegment(seg) {
|
|
5
|
+
if (seg === "**") return "{__fs_rest}";
|
|
6
|
+
if (seg === "*") return "{splat}";
|
|
7
|
+
if (seg.startsWith(":")) return `{${seg.slice(1)}}`;
|
|
8
|
+
if (seg.startsWith("[") && seg.endsWith("]")) return `{${seg.slice(1, -1)}}`;
|
|
9
|
+
return seg;
|
|
10
|
+
}
|
|
11
|
+
function normalizePath(path) {
|
|
12
|
+
if (path === "/" || path === "") return "/";
|
|
13
|
+
const lead = path.startsWith("/");
|
|
14
|
+
const out = path.split("/").filter((s) => s.length > 0).map(normalizeSegment).join("/");
|
|
15
|
+
return lead ? `/${out}` : out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/reflect/index.ts
|
|
19
|
+
var VALID_METHODS = /* @__PURE__ */ new Set([
|
|
20
|
+
"GET",
|
|
21
|
+
"HEAD",
|
|
22
|
+
"POST",
|
|
23
|
+
"PUT",
|
|
24
|
+
"PATCH",
|
|
25
|
+
"DELETE",
|
|
26
|
+
"OPTIONS"
|
|
27
|
+
]);
|
|
28
|
+
function methodsOf(route) {
|
|
29
|
+
const out = /* @__PURE__ */ new Set();
|
|
30
|
+
if (route.methods) {
|
|
31
|
+
for (const [m, on] of Object.entries(route.methods))
|
|
32
|
+
if (on) out.add(m.toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
for (const s of route.stack ?? []) {
|
|
35
|
+
if (typeof s.method === "string") out.add(s.method.toUpperCase());
|
|
36
|
+
}
|
|
37
|
+
if (out.has("GET")) out.delete("HEAD");
|
|
38
|
+
return [...out].filter((m) => VALID_METHODS.has(m));
|
|
39
|
+
}
|
|
40
|
+
function rootStack(app) {
|
|
41
|
+
const a = app;
|
|
42
|
+
return a?.router?.stack ?? a?._router?.stack;
|
|
43
|
+
}
|
|
44
|
+
function prefixFromRegexp(re) {
|
|
45
|
+
if (!re) return void 0;
|
|
46
|
+
let s = re.source;
|
|
47
|
+
if (s === "^\\/?$" || s === "^\\/") return "";
|
|
48
|
+
s = s.replace(/^\^/, "").replace(/\\\/\?\(\?=\\\/\|\$\)$/, "").replace(/\(\?=\\\/\|\$\)$/, "").replace(/\\\/\?$/, "").replace(/\$$/, "");
|
|
49
|
+
s = s.replace(/\\\//g, "/");
|
|
50
|
+
if (/[()?:*+\[\]|]/.test(s)) return void 0;
|
|
51
|
+
return s.startsWith("/") ? s : `/${s}`;
|
|
52
|
+
}
|
|
53
|
+
function walk(stack, prefix, out, counters) {
|
|
54
|
+
for (const layer of stack) {
|
|
55
|
+
if (layer.route && typeof layer.route.path === "string") {
|
|
56
|
+
const path = normalizePath(`${prefix}${layer.route.path}`);
|
|
57
|
+
for (const method of methodsOf(layer.route)) {
|
|
58
|
+
out.push({ method, path });
|
|
59
|
+
}
|
|
60
|
+
} else if (layer.handle?.stack && Array.isArray(layer.handle.stack)) {
|
|
61
|
+
const sub = prefixFromRegexp(layer.regexp);
|
|
62
|
+
if (sub === void 0 && layer.name === "router") {
|
|
63
|
+
counters.unreflectable += layer.handle.stack.filter(
|
|
64
|
+
(l) => l.route
|
|
65
|
+
).length;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
walk(layer.handle.stack, `${prefix}${sub ?? ""}`, out, counters);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function reflectRoutesDetailed(app, _opts) {
|
|
73
|
+
const stack = rootStack(app);
|
|
74
|
+
if (!stack) return { routes: [], unreflectable: 0 };
|
|
75
|
+
const raw = [];
|
|
76
|
+
const counters = { unreflectable: 0 };
|
|
77
|
+
walk(stack, "", raw, counters);
|
|
78
|
+
const seen = /* @__PURE__ */ new Set();
|
|
79
|
+
const routes = [];
|
|
80
|
+
for (const r of raw) {
|
|
81
|
+
const key = `${r.method} ${r.path}`;
|
|
82
|
+
if (seen.has(key)) continue;
|
|
83
|
+
seen.add(key);
|
|
84
|
+
routes.push(r);
|
|
85
|
+
}
|
|
86
|
+
return { routes, unreflectable: counters.unreflectable };
|
|
87
|
+
}
|
|
88
|
+
function reflectRoutes(app, opts) {
|
|
89
|
+
return reflectRoutesDetailed(app, opts).routes;
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
reflectRoutes,
|
|
93
|
+
reflectRoutesDetailed
|
|
94
|
+
};
|
|
@@ -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
|
|
@@ -41,6 +41,7 @@ export type FartherShoreInitOptions = {
|
|
|
41
41
|
instanceId?: string;
|
|
42
42
|
};
|
|
43
43
|
export declare const SDK_VERSION: string;
|
|
44
|
+
export declare const CONTRACTS_FP: string;
|
|
44
45
|
/**
|
|
45
46
|
* The runtime instance. Lazily bootstraps; holds the JWKS client, nonce cache,
|
|
46
47
|
* metering buffer, and shutdown hooks.
|
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";
|
|
@@ -1,4 +1,53 @@
|
|
|
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_";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farthershore/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|