@farthershore/backend 0.17.0 → 0.19.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 +36 -0
- package/README.md +1 -1
- package/dist/adapters/express.js +18 -6
- package/dist/index.js +29 -8
- package/dist/testing/index.js +29 -8
- package/dist/types/adapters/express.d.ts +21 -0
- package/dist/types/core/verifyRequest.d.ts +14 -1
- package/dist/types/runtime-types.d.ts +15 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to the runtime backend SDK are documented here. This SDK
|
|
|
4
4
|
versions independently from the frontend and business SDKs. Pre-1.0: minor
|
|
5
5
|
versions may include breaking changes.
|
|
6
6
|
|
|
7
|
+
## [0.19.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- One runtime token can now serve EVERY environment of a business. Bootstrap
|
|
12
|
+
reports `backendIds` — every backend the token may serve — and the SDK checks
|
|
13
|
+
the gateway's signed backend id for MEMBERSHIP of that set instead of equality
|
|
14
|
+
with a single id.
|
|
15
|
+
|
|
16
|
+
A process holds exactly one `FS_RUNTIME_TOKEN`, so an environment-scoped token
|
|
17
|
+
forced a SEPARATE deployment per environment: pointing a backend at a preview
|
|
18
|
+
environment meant repointing, and breaking, production. A business-scoped
|
|
19
|
+
token (`farthershore backend tokens create <biz>`, no `--env`) now serves them
|
|
20
|
+
all from one deployment; `--env <name>` still pins a token to one environment
|
|
21
|
+
when you want that guarantee.
|
|
22
|
+
|
|
23
|
+
Still fail-closed and still bounded by the business — this widens across
|
|
24
|
+
ENVIRONMENTS, never across businesses. Additive: an older core omits
|
|
25
|
+
`backendIds` and the single-id equality check still applies.
|
|
26
|
+
|
|
27
|
+
## [0.18.0]
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `createExpressMiddleware` accepts `onVerificationError`, called when
|
|
32
|
+
verification REJECTS a request with the diagnostic detail the response body
|
|
33
|
+
deliberately withholds (`{ code, message, status, method, path }`).
|
|
34
|
+
|
|
35
|
+
The wire body stays `{ error: <code> }` — several distinct causes share one
|
|
36
|
+
code (`route_mismatch` covers a business mismatch, a backend mismatch, AND an
|
|
37
|
+
unserved route id) and the specifics must not leak to an unauthenticated
|
|
38
|
+
caller. Previously they were discarded entirely, leaving the operator no way
|
|
39
|
+
to tell which cause fired even in their own logs. Defaults to a one-line
|
|
40
|
+
`console.warn`; pass a function to route it into a structured logger, or
|
|
41
|
+
`() => {}` to silence it. A throwing reporter cannot turn a 401 into a 500.
|
|
42
|
+
|
|
7
43
|
## [0.17.0]
|
|
8
44
|
|
|
9
45
|
### Changed — BREAKING
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ graceful lifecycle (health + shutdown). Everything else — your business, backe
|
|
|
12
12
|
and environment ids, the verification keys, and the metering endpoint — is
|
|
13
13
|
fetched automatically from the token at startup.
|
|
14
14
|
|
|
15
|
-
> **Status: `0.
|
|
15
|
+
> **Status: `0.19.0`.** Pre-1.0: minor releases may include breaking changes, so
|
|
16
16
|
> pin this package to an exact version (or a patch-only range) and upgrade
|
|
17
17
|
> deliberately.
|
|
18
18
|
|
package/dist/adapters/express.js
CHANGED
|
@@ -85,15 +85,27 @@ async function runMiddleware(fs, options, req, res, next) {
|
|
|
85
85
|
stripFartherShoreHeaders(req);
|
|
86
86
|
next();
|
|
87
87
|
} catch (error) {
|
|
88
|
-
fail(res, error);
|
|
88
|
+
fail(res, error, options, req);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
function fail(res, error) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
function fail(res, error, options, req) {
|
|
92
|
+
const code = error instanceof FartherShoreError ? error.code : "bad_signature";
|
|
93
|
+
const status = error instanceof FartherShoreError ? error.status : 401;
|
|
94
|
+
const message = error instanceof Error && error.message ? error.message : code;
|
|
95
|
+
const report = options.onVerificationError ?? ((d) => {
|
|
96
|
+
console.warn(`[farthershore] request rejected (${d.code}): ${d.message}`);
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
report({
|
|
100
|
+
code,
|
|
101
|
+
message,
|
|
102
|
+
status,
|
|
103
|
+
method: req.method ?? "",
|
|
104
|
+
path: req.url ?? ""
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
95
107
|
}
|
|
96
|
-
res.status(
|
|
108
|
+
res.status(status).json({ error: code });
|
|
97
109
|
}
|
|
98
110
|
function stripFartherShoreHeaders(req) {
|
|
99
111
|
const headers = req.headers;
|
package/dist/index.js
CHANGED
|
@@ -1781,7 +1781,14 @@ async function verifyRequest(input, deps) {
|
|
|
1781
1781
|
"signed business-id does not match this backend's business"
|
|
1782
1782
|
);
|
|
1783
1783
|
}
|
|
1784
|
-
if (deps.
|
|
1784
|
+
if (deps.backendIds !== void 0 && deps.backendIds.size > 0) {
|
|
1785
|
+
if (!deps.backendIds.has(signedBackendId)) {
|
|
1786
|
+
throw new FartherShoreError(
|
|
1787
|
+
"route_mismatch",
|
|
1788
|
+
"signed backend-id is not one this deployment serves"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
} else if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
1785
1792
|
throw new FartherShoreError(
|
|
1786
1793
|
"route_mismatch",
|
|
1787
1794
|
"signed backend-id does not match this backend"
|
|
@@ -1918,7 +1925,7 @@ function headerGetter(headers) {
|
|
|
1918
1925
|
|
|
1919
1926
|
// src/core/runtime.ts
|
|
1920
1927
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1921
|
-
var SDK_VERSION = "0.
|
|
1928
|
+
var SDK_VERSION = "0.19.0".length > 0 ? "0.19.0" : "0.0.0-dev";
|
|
1922
1929
|
var FartherShore = class {
|
|
1923
1930
|
bootstrapClient;
|
|
1924
1931
|
fetchImpl;
|
|
@@ -2066,11 +2073,13 @@ var FartherShore = class {
|
|
|
2066
2073
|
);
|
|
2067
2074
|
}
|
|
2068
2075
|
const knownRouteIds = new Set(config.routes.map((r) => r.id));
|
|
2076
|
+
const backendIds = config.backendIds?.length ? new Set(config.backendIds) : void 0;
|
|
2069
2077
|
const context = await verifyRequest(input, {
|
|
2070
2078
|
jwks: this.jwks,
|
|
2071
2079
|
nonceCache: this.nonceCache,
|
|
2072
2080
|
businessId: config.business.id,
|
|
2073
2081
|
backendId: config.backend.id,
|
|
2082
|
+
...backendIds ? { backendIds } : {},
|
|
2074
2083
|
knownRouteIds,
|
|
2075
2084
|
clockSkewSeconds: config.verification.clockSkewSeconds,
|
|
2076
2085
|
replayWindowSeconds: config.verification.replayWindowSeconds,
|
|
@@ -2276,15 +2285,27 @@ async function runMiddleware(fs, options, req, res, next) {
|
|
|
2276
2285
|
stripFartherShoreHeaders(req);
|
|
2277
2286
|
next();
|
|
2278
2287
|
} catch (error) {
|
|
2279
|
-
fail(res, error);
|
|
2288
|
+
fail(res, error, options, req);
|
|
2280
2289
|
}
|
|
2281
2290
|
}
|
|
2282
|
-
function fail(res, error) {
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2291
|
+
function fail(res, error, options, req) {
|
|
2292
|
+
const code = error instanceof FartherShoreError ? error.code : "bad_signature";
|
|
2293
|
+
const status = error instanceof FartherShoreError ? error.status : 401;
|
|
2294
|
+
const message = error instanceof Error && error.message ? error.message : code;
|
|
2295
|
+
const report = options.onVerificationError ?? ((d) => {
|
|
2296
|
+
console.warn(`[farthershore] request rejected (${d.code}): ${d.message}`);
|
|
2297
|
+
});
|
|
2298
|
+
try {
|
|
2299
|
+
report({
|
|
2300
|
+
code,
|
|
2301
|
+
message,
|
|
2302
|
+
status,
|
|
2303
|
+
method: req.method ?? "",
|
|
2304
|
+
path: req.url ?? ""
|
|
2305
|
+
});
|
|
2306
|
+
} catch {
|
|
2286
2307
|
}
|
|
2287
|
-
res.status(
|
|
2308
|
+
res.status(status).json({ error: code });
|
|
2288
2309
|
}
|
|
2289
2310
|
function stripFartherShoreHeaders(req) {
|
|
2290
2311
|
const headers = req.headers;
|
package/dist/testing/index.js
CHANGED
|
@@ -2012,7 +2012,14 @@ async function verifyRequest(input, deps) {
|
|
|
2012
2012
|
"signed business-id does not match this backend's business"
|
|
2013
2013
|
);
|
|
2014
2014
|
}
|
|
2015
|
-
if (deps.
|
|
2015
|
+
if (deps.backendIds !== void 0 && deps.backendIds.size > 0) {
|
|
2016
|
+
if (!deps.backendIds.has(signedBackendId)) {
|
|
2017
|
+
throw new FartherShoreError(
|
|
2018
|
+
"route_mismatch",
|
|
2019
|
+
"signed backend-id is not one this deployment serves"
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
} else if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
2016
2023
|
throw new FartherShoreError(
|
|
2017
2024
|
"route_mismatch",
|
|
2018
2025
|
"signed backend-id does not match this backend"
|
|
@@ -2149,7 +2156,7 @@ function headerGetter(headers) {
|
|
|
2149
2156
|
|
|
2150
2157
|
// src/core/runtime.ts
|
|
2151
2158
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
2152
|
-
var SDK_VERSION = "0.
|
|
2159
|
+
var SDK_VERSION = "0.19.0".length > 0 ? "0.19.0" : "0.0.0-dev";
|
|
2153
2160
|
var FartherShore = class {
|
|
2154
2161
|
bootstrapClient;
|
|
2155
2162
|
fetchImpl;
|
|
@@ -2297,11 +2304,13 @@ var FartherShore = class {
|
|
|
2297
2304
|
);
|
|
2298
2305
|
}
|
|
2299
2306
|
const knownRouteIds = new Set(config.routes.map((r) => r.id));
|
|
2307
|
+
const backendIds = config.backendIds?.length ? new Set(config.backendIds) : void 0;
|
|
2300
2308
|
const context = await verifyRequest(input, {
|
|
2301
2309
|
jwks: this.jwks,
|
|
2302
2310
|
nonceCache: this.nonceCache,
|
|
2303
2311
|
businessId: config.business.id,
|
|
2304
2312
|
backendId: config.backend.id,
|
|
2313
|
+
...backendIds ? { backendIds } : {},
|
|
2305
2314
|
knownRouteIds,
|
|
2306
2315
|
clockSkewSeconds: config.verification.clockSkewSeconds,
|
|
2307
2316
|
replayWindowSeconds: config.verification.replayWindowSeconds,
|
|
@@ -2502,15 +2511,27 @@ async function runMiddleware(fs, options, req, res, next) {
|
|
|
2502
2511
|
stripFartherShoreHeaders(req);
|
|
2503
2512
|
next();
|
|
2504
2513
|
} catch (error) {
|
|
2505
|
-
fail(res, error);
|
|
2514
|
+
fail(res, error, options, req);
|
|
2506
2515
|
}
|
|
2507
2516
|
}
|
|
2508
|
-
function fail(res, error) {
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2517
|
+
function fail(res, error, options, req) {
|
|
2518
|
+
const code = error instanceof FartherShoreError ? error.code : "bad_signature";
|
|
2519
|
+
const status = error instanceof FartherShoreError ? error.status : 401;
|
|
2520
|
+
const message = error instanceof Error && error.message ? error.message : code;
|
|
2521
|
+
const report = options.onVerificationError ?? ((d) => {
|
|
2522
|
+
console.warn(`[farthershore] request rejected (${d.code}): ${d.message}`);
|
|
2523
|
+
});
|
|
2524
|
+
try {
|
|
2525
|
+
report({
|
|
2526
|
+
code,
|
|
2527
|
+
message,
|
|
2528
|
+
status,
|
|
2529
|
+
method: req.method ?? "",
|
|
2530
|
+
path: req.url ?? ""
|
|
2531
|
+
});
|
|
2532
|
+
} catch {
|
|
2512
2533
|
}
|
|
2513
|
-
res.status(
|
|
2534
|
+
res.status(status).json({ error: code });
|
|
2514
2535
|
}
|
|
2515
2536
|
function stripFartherShoreHeaders(req) {
|
|
2516
2537
|
const headers = req.headers;
|
|
@@ -36,6 +36,27 @@ export type MiddlewareOptions = {
|
|
|
36
36
|
* consumes no identity; the secure default is strict.
|
|
37
37
|
*/
|
|
38
38
|
always?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Called when verification REJECTS a request, with the diagnostic detail that
|
|
41
|
+
* is deliberately withheld from the response body.
|
|
42
|
+
*
|
|
43
|
+
* The wire response is only `{ error: <code> }` — several distinct causes
|
|
44
|
+
* share one code (`route_mismatch` covers a business mismatch, a backend
|
|
45
|
+
* mismatch, AND an unserved route id), and the specifics must not leak to an
|
|
46
|
+
* unauthenticated caller. But discarding them entirely leaves the operator
|
|
47
|
+
* with no way to tell which cause fired, in their OWN logs, for their OWN
|
|
48
|
+
* server. That is what this hook restores.
|
|
49
|
+
*
|
|
50
|
+
* Defaults to a one-line `console.warn`. Pass a function to route it into a
|
|
51
|
+
* structured logger, or `() => {}` to silence it.
|
|
52
|
+
*/
|
|
53
|
+
onVerificationError?: (detail: {
|
|
54
|
+
code: string;
|
|
55
|
+
message: string;
|
|
56
|
+
status: number;
|
|
57
|
+
method: string;
|
|
58
|
+
path: string;
|
|
59
|
+
}) => void;
|
|
39
60
|
};
|
|
40
61
|
/**
|
|
41
62
|
* A verified request context whose {@link ConsumerPrincipal} is GUARANTEED
|
|
@@ -68,8 +68,21 @@ export type VerifyRequestDeps = {
|
|
|
68
68
|
nonceCache: NonceStore;
|
|
69
69
|
/** Expected business id (from bootstrap). When set, must match the signed claim. */
|
|
70
70
|
businessId?: string;
|
|
71
|
-
/** Expected backend id (from bootstrap). When set, must match
|
|
71
|
+
/** Expected backend id (from bootstrap). When set, must match — unless
|
|
72
|
+
* `backendIds` is provided, which supersedes it with a membership check. */
|
|
72
73
|
backendId?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Every backend id this deployment may serve (from bootstrap), across all of
|
|
76
|
+
* the business's environments. When provided, the signed backend id must be a
|
|
77
|
+
* MEMBER of this set — superseding the single-id equality check above.
|
|
78
|
+
*
|
|
79
|
+
* A process holds exactly one `FS_RUNTIME_TOKEN`, so pinning to one backend id
|
|
80
|
+
* forced a separate deployment per environment: serving a preview env meant
|
|
81
|
+
* repointing (and breaking) production. A business-scoped token serves them
|
|
82
|
+
* all from one deployment. Still fail-closed, still bound to this business —
|
|
83
|
+
* just no longer bound to a single environment.
|
|
84
|
+
*/
|
|
85
|
+
backendIds?: ReadonlySet<string>;
|
|
73
86
|
/**
|
|
74
87
|
* Set of route ids this backend serves (from bootstrap). When provided AND
|
|
75
88
|
* the signed route-id is non-empty, the signed route must be a member —
|
|
@@ -233,6 +233,21 @@ export type RuntimeBootstrapResponse = {
|
|
|
233
233
|
slug: string;
|
|
234
234
|
name: string;
|
|
235
235
|
};
|
|
236
|
+
/**
|
|
237
|
+
* Every backend id this token may serve, across ALL of the business's
|
|
238
|
+
* environments — `backend.id` is always a member.
|
|
239
|
+
*
|
|
240
|
+
* One deployment holds exactly one `FS_RUNTIME_TOKEN`, so an
|
|
241
|
+
* environment-scoped token forced a SEPARATE deployment per environment:
|
|
242
|
+
* serving a preview env meant repointing (and breaking) production. A
|
|
243
|
+
* business-scoped token serves every environment from one deployment, and the
|
|
244
|
+
* SDK checks the gateway's signed backend id for MEMBERSHIP of this set
|
|
245
|
+
* rather than equality with a single id.
|
|
246
|
+
*
|
|
247
|
+
* Optional and additive: an older core omits it and the SDK falls back to the
|
|
248
|
+
* single-id equality check.
|
|
249
|
+
*/
|
|
250
|
+
backendIds?: string[];
|
|
236
251
|
environment: {
|
|
237
252
|
id: string | null;
|
|
238
253
|
kind: RuntimeEnvironmentKind;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farthershore/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
|
-
"@farthershore/cloudflared-linux-x64": "0.0.0",
|
|
41
40
|
"@farthershore/cloudflared-linux-arm64": "0.0.0",
|
|
42
|
-
"@farthershore/cloudflared-
|
|
43
|
-
"@farthershore/cloudflared-darwin-arm64": "0.0.0"
|
|
41
|
+
"@farthershore/cloudflared-linux-x64": "0.0.0",
|
|
42
|
+
"@farthershore/cloudflared-darwin-arm64": "0.0.0",
|
|
43
|
+
"@farthershore/cloudflared-darwin-x64": "0.0.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"express": "^4.0.0 || ^5.0.0"
|