@farthershore/backend 0.15.0 → 0.17.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 +83 -0
- package/README.md +1 -1
- package/dist/adapters/express.js +66 -2
- package/dist/generated/runtime-contract.js +11 -5
- package/dist/index.js +325 -154
- package/dist/testing/index.js +272 -142
- package/dist/types/adapters/express.d.ts +55 -5
- package/dist/types/core/errors.d.ts +4 -1
- package/dist/types/core/nonceCache.d.ts +25 -4
- package/dist/types/core/permissions.d.ts +14 -25
- package/dist/types/core/runtime.d.ts +23 -31
- package/dist/types/core/subject.d.ts +45 -0
- package/dist/types/core/verifyContext.d.ts +96 -19
- package/dist/types/core/verifyRequest.d.ts +34 -24
- package/dist/types/generated/runtime-contract.d.ts +7 -3
- package/dist/types/index.d.ts +17 -8
- package/dist/types/response-metering.d.ts +7 -0
- package/dist/types/runtime-signing.d.ts +7 -0
- package/dist/types/runtime-types.d.ts +12 -11
- package/dist/types/testing/devRuntime.d.ts +3 -1
- package/dist/types/testing/signers.d.ts +12 -4
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -215,7 +215,10 @@ var RUNTIME_ERROR_CODES = {
|
|
|
215
215
|
environmentMismatch: "environment_mismatch",
|
|
216
216
|
missingToken: "missing_token",
|
|
217
217
|
invalidToken: "invalid_token",
|
|
218
|
-
contextUnverified: "context_unverified"
|
|
218
|
+
contextUnverified: "context_unverified",
|
|
219
|
+
memberSubjectRequired: "member_subject_required",
|
|
220
|
+
serviceSubjectRequired: "service_subject_required",
|
|
221
|
+
surfaceNotAllowed: "surface_not_allowed"
|
|
219
222
|
};
|
|
220
223
|
var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
221
224
|
headers: {
|
|
@@ -279,7 +282,12 @@ var RUNTIME_ERROR_CODE_TO_ERROR_CODE = {
|
|
|
279
282
|
// the canonical code keeps the "dependency down" semantic for callers.
|
|
280
283
|
[RUNTIME_ERROR_CODES.jwksUnavailable]: "SERVICE_UNAVAILABLE",
|
|
281
284
|
// The single non-401 (413) — oversized request body.
|
|
282
|
-
[RUNTIME_ERROR_CODES.bodyTooLarge]: "VALIDATION_ERROR"
|
|
285
|
+
[RUNTIME_ERROR_CODES.bodyTooLarge]: "VALIDATION_ERROR",
|
|
286
|
+
// Consumer-principal wave — route subject-requirement faults → FORBIDDEN (403).
|
|
287
|
+
[RUNTIME_ERROR_CODES.memberSubjectRequired]: "FORBIDDEN",
|
|
288
|
+
[RUNTIME_ERROR_CODES.serviceSubjectRequired]: "FORBIDDEN",
|
|
289
|
+
// A visible route whose surface set excludes the caller → 403.
|
|
290
|
+
[RUNTIME_ERROR_CODES.surfaceNotAllowed]: "FORBIDDEN"
|
|
283
291
|
};
|
|
284
292
|
function runtimeErrorToErrorCode(code) {
|
|
285
293
|
return RUNTIME_ERROR_CODE_TO_ERROR_CODE[code] ?? "INTERNAL_ERROR";
|
|
@@ -289,16 +297,16 @@ var RUNTIME_TOKEN_PREFIXES = {
|
|
|
289
297
|
live: "fsrt_live_",
|
|
290
298
|
test: "fsrt_test_"
|
|
291
299
|
};
|
|
292
|
-
var
|
|
300
|
+
var RUNTIME_TOKEN_OPERATIONS = [
|
|
293
301
|
"gateway_verification",
|
|
294
302
|
"metering",
|
|
295
303
|
"health",
|
|
296
304
|
"tunnel",
|
|
297
|
-
// Hand-maintained mirror of @farthershore/contracts
|
|
305
|
+
// Hand-maintained mirror of @farthershore/contracts RUNTIME_TOKEN_OPERATIONS
|
|
298
306
|
// (`runtime.ts`). Bound to that source by the SET-EQUALITY + ORDER assertions
|
|
299
307
|
// in `deny-taxonomy-drift.test.ts` (test-only contracts devDep) — NOT by the
|
|
300
308
|
// generated runtime-contract.ts, which mirrors only RUNTIME_ERROR_CODES.
|
|
301
|
-
// `drift_report` is the opt-in
|
|
309
|
+
// `drift_report` is the opt-in operation for reporting route drift.
|
|
302
310
|
"drift_report"
|
|
303
311
|
];
|
|
304
312
|
var RUNTIME_HEADER_NAMES = {
|
|
@@ -312,10 +320,6 @@ var RUNTIME_HEADER_NAMES = {
|
|
|
312
320
|
policyVersion: "x-fs-policy-version",
|
|
313
321
|
bodyHash: "x-fs-body-hash"
|
|
314
322
|
};
|
|
315
|
-
var RUNTIME_IDENTITY_HEADER_NAMES = {
|
|
316
|
-
permissions: "x-fs-permissions",
|
|
317
|
-
roles: "x-fs-roles"
|
|
318
|
-
};
|
|
319
323
|
var RUNTIME_CLOCK_SKEW_SECONDS = 5;
|
|
320
324
|
var RUNTIME_REPLAY_WINDOW_SECONDS = 300;
|
|
321
325
|
var EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
@@ -361,31 +365,16 @@ var CANONICAL_SIGNING_FIELDS = [
|
|
|
361
365
|
"business-id",
|
|
362
366
|
"backend-id",
|
|
363
367
|
"route-id",
|
|
364
|
-
"policy-version"
|
|
368
|
+
"policy-version",
|
|
369
|
+
// Consumer-principal wave (D3) — the trailing identity-context binding line.
|
|
370
|
+
// MUST stay last so pre-binding verifiers that stop at `policy-version`
|
|
371
|
+
// fail loud on a bound request rather than silently accepting a prefix.
|
|
372
|
+
"context-hash"
|
|
365
373
|
];
|
|
366
374
|
var CANONICAL_FIELD_SEPARATOR = "\n";
|
|
367
375
|
var CANONICAL_KV_SEPARATOR = ":";
|
|
368
376
|
function canonicalizeQuery(query) {
|
|
369
|
-
|
|
370
|
-
if (raw === "")
|
|
371
|
-
return "";
|
|
372
|
-
const pairs = raw.split("&").filter((p) => p.length > 0);
|
|
373
|
-
pairs.sort((a, b) => {
|
|
374
|
-
const [an, ...arest] = a.split("=");
|
|
375
|
-
const [bn, ...brest] = b.split("=");
|
|
376
|
-
if (an < bn)
|
|
377
|
-
return -1;
|
|
378
|
-
if (an > bn)
|
|
379
|
-
return 1;
|
|
380
|
-
const av = arest.join("=");
|
|
381
|
-
const bv = brest.join("=");
|
|
382
|
-
if (av < bv)
|
|
383
|
-
return -1;
|
|
384
|
-
if (av > bv)
|
|
385
|
-
return 1;
|
|
386
|
-
return 0;
|
|
387
|
-
});
|
|
388
|
-
return pairs.join("&");
|
|
377
|
+
return query;
|
|
389
378
|
}
|
|
390
379
|
function buildCanonicalSigningString(input) {
|
|
391
380
|
const values = {
|
|
@@ -398,10 +387,14 @@ function buildCanonicalSigningString(input) {
|
|
|
398
387
|
"business-id": input.businessId,
|
|
399
388
|
"backend-id": input.backendId,
|
|
400
389
|
"route-id": input.routeId,
|
|
401
|
-
"policy-version": input.policyVersion
|
|
390
|
+
"policy-version": input.policyVersion,
|
|
391
|
+
"context-hash": input.contextHash
|
|
402
392
|
};
|
|
403
393
|
return CANONICAL_SIGNING_FIELDS.map((field) => `${field}${CANONICAL_KV_SEPARATOR}${values[field]}`).join(CANONICAL_FIELD_SEPARATOR);
|
|
404
394
|
}
|
|
395
|
+
function hashContextToken(token) {
|
|
396
|
+
return hashBody(new TextEncoder().encode(token ?? ""));
|
|
397
|
+
}
|
|
405
398
|
var ED25519_ALGORITHM = "Ed25519";
|
|
406
399
|
async function importEd25519PrivateKey(jwk) {
|
|
407
400
|
return crypto.subtle.importKey("jwk", { ...jwk, alg: void 0 }, { name: ED25519_ALGORITHM }, false, ["sign"]);
|
|
@@ -444,6 +437,7 @@ function base64UrlDecode(value) {
|
|
|
444
437
|
var hashBody2 = hashBody;
|
|
445
438
|
var canonicalizeQuery2 = canonicalizeQuery;
|
|
446
439
|
var buildCanonicalSigningString2 = buildCanonicalSigningString;
|
|
440
|
+
var hashContextToken2 = hashContextToken;
|
|
447
441
|
var signCanonicalString2 = signCanonicalString;
|
|
448
442
|
var verifyCanonicalSignature2 = verifyCanonicalSignature;
|
|
449
443
|
var runtimeTokenKind2 = runtimeTokenKind;
|
|
@@ -466,7 +460,9 @@ var FartherShoreError = class extends Error {
|
|
|
466
460
|
}
|
|
467
461
|
};
|
|
468
462
|
function statusForCode(code) {
|
|
469
|
-
|
|
463
|
+
if (code === "body_too_large") return 413;
|
|
464
|
+
if (code === "surface_not_allowed") return 403;
|
|
465
|
+
return 401;
|
|
470
466
|
}
|
|
471
467
|
|
|
472
468
|
// src/core/bootstrap.ts
|
|
@@ -941,7 +937,7 @@ async function withUsage(request, response, usage, options = {}) {
|
|
|
941
937
|
}
|
|
942
938
|
async function signResponse(request, response, usage, options, wrapOptions) {
|
|
943
939
|
const payload = buildPayload(request, usage, options, wrapOptions);
|
|
944
|
-
const requestId = request.headers.get("x-fs-request-id") ?? void 0;
|
|
940
|
+
const requestId = options.requestId ?? request.headers.get("x-fs-request-id") ?? void 0;
|
|
945
941
|
const headers = await computeMeteringHeaders(payload, {
|
|
946
942
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
947
943
|
...options.env !== void 0 ? { env: options.env } : {},
|
|
@@ -1195,8 +1191,8 @@ function resolveEndpoint2(endpoint, coreUrl) {
|
|
|
1195
1191
|
}
|
|
1196
1192
|
|
|
1197
1193
|
// src/core/nonceCache.ts
|
|
1198
|
-
var DEFAULT_MAX_ENTRIES =
|
|
1199
|
-
var DEFAULT_TTL_MS =
|
|
1194
|
+
var DEFAULT_MAX_ENTRIES = 25e4;
|
|
1195
|
+
var DEFAULT_TTL_MS = (RUNTIME_REPLAY_WINDOW_SECONDS + RUNTIME_CLOCK_SKEW_SECONDS) * 1e3;
|
|
1200
1196
|
var NonceCache = class {
|
|
1201
1197
|
maxEntries;
|
|
1202
1198
|
ttlMs;
|
|
@@ -1220,7 +1216,7 @@ var NonceCache = class {
|
|
|
1220
1216
|
this.seen.delete(id);
|
|
1221
1217
|
}
|
|
1222
1218
|
this.evictExpired(at);
|
|
1223
|
-
this.
|
|
1219
|
+
if (this.seen.size >= this.maxEntries) return true;
|
|
1224
1220
|
this.seen.set(id, at);
|
|
1225
1221
|
return false;
|
|
1226
1222
|
}
|
|
@@ -1234,13 +1230,6 @@ var NonceCache = class {
|
|
|
1234
1230
|
this.seen.delete(id);
|
|
1235
1231
|
}
|
|
1236
1232
|
}
|
|
1237
|
-
evictOverflow() {
|
|
1238
|
-
while (this.seen.size >= this.maxEntries) {
|
|
1239
|
-
const oldest = this.seen.keys().next().value;
|
|
1240
|
-
if (oldest === void 0) break;
|
|
1241
|
-
this.seen.delete(oldest);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
1233
|
};
|
|
1245
1234
|
|
|
1246
1235
|
// src/core/shutdown.ts
|
|
@@ -1575,53 +1564,82 @@ function resolvePackageBinary(require2, pkg, manifestPath) {
|
|
|
1575
1564
|
return `${root}${sep}${normalized}`;
|
|
1576
1565
|
}
|
|
1577
1566
|
|
|
1578
|
-
//
|
|
1579
|
-
var
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
status = 403;
|
|
1583
|
-
/** The permission key that was required but not held. */
|
|
1584
|
-
requiredPermission;
|
|
1585
|
-
constructor(requiredPermission, message) {
|
|
1586
|
-
super(message ?? `missing required permission: ${requiredPermission}`);
|
|
1587
|
-
this.name = "FartherShorePermissionError";
|
|
1588
|
-
this.requiredPermission = requiredPermission;
|
|
1589
|
-
}
|
|
1590
|
-
};
|
|
1591
|
-
function parsePermissionHeader(raw) {
|
|
1592
|
-
if (raw === null || raw === void 0) return void 0;
|
|
1593
|
-
return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1567
|
+
// ../contracts/dist/authz/principal.js
|
|
1568
|
+
var SUBJECT_KINDS = ["member", "service"];
|
|
1569
|
+
function isSubjectKind(value) {
|
|
1570
|
+
return typeof value === "string" && SUBJECT_KINDS.includes(value);
|
|
1594
1571
|
}
|
|
1595
|
-
function
|
|
1596
|
-
|
|
1597
|
-
if (permissions.includes(WILDCARD)) return true;
|
|
1598
|
-
return permissions.includes(key2);
|
|
1572
|
+
function isNonEmptyTrimmed(value) {
|
|
1573
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1599
1574
|
}
|
|
1600
|
-
function
|
|
1601
|
-
|
|
1602
|
-
if (granted.includes(WILDCARD)) return true;
|
|
1603
|
-
if (granted.includes(required)) return true;
|
|
1604
|
-
const idx = required.indexOf(":");
|
|
1605
|
-
if (idx > 0 && idx < required.length - 1) {
|
|
1606
|
-
const subject = required.slice(0, idx);
|
|
1607
|
-
if (granted.includes(`${subject}:${WILDCARD}`)) return true;
|
|
1608
|
-
}
|
|
1609
|
-
return false;
|
|
1575
|
+
function normalizeIdentityId(value) {
|
|
1576
|
+
return value.trim();
|
|
1610
1577
|
}
|
|
1611
|
-
function
|
|
1612
|
-
|
|
1613
|
-
return ctx.signedContext !== void 0;
|
|
1614
|
-
}
|
|
1615
|
-
return permissionSatisfies(key2, ctx.permissions);
|
|
1578
|
+
function isNonEmptyStringArray(value) {
|
|
1579
|
+
return Array.isArray(value) && value.every((item) => isNonEmptyTrimmed(item));
|
|
1616
1580
|
}
|
|
1617
|
-
function
|
|
1618
|
-
if (
|
|
1619
|
-
|
|
1581
|
+
function principalFromContextClaims(claims) {
|
|
1582
|
+
if (typeof claims !== "object" || claims === null)
|
|
1583
|
+
return null;
|
|
1584
|
+
const { sub, org: orgId, businessId, subjectKind } = claims;
|
|
1585
|
+
if (!isNonEmptyTrimmed(sub) || !isNonEmptyTrimmed(orgId) || !isNonEmptyTrimmed(businessId)) {
|
|
1586
|
+
return null;
|
|
1587
|
+
}
|
|
1588
|
+
if (!isSubjectKind(subjectKind))
|
|
1589
|
+
return null;
|
|
1590
|
+
if (claims.permissions !== void 0 && !isNonEmptyStringArray(claims.permissions)) {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
if (claims.roles !== void 0 && !isNonEmptyStringArray(claims.roles)) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
const clientId = claims.client_id;
|
|
1597
|
+
const actSub = claims.act?.sub;
|
|
1598
|
+
if (isNonEmptyTrimmed(clientId) && isNonEmptyTrimmed(actSub) && normalizeIdentityId(clientId) !== normalizeIdentityId(actSub)) {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
const org = { id: normalizeIdentityId(orgId) };
|
|
1602
|
+
if (subjectKind === "service") {
|
|
1603
|
+
const explicitServiceAccountId = claims.serviceAccountId;
|
|
1604
|
+
if (isNonEmptyTrimmed(explicitServiceAccountId) && normalizeIdentityId(explicitServiceAccountId) !== normalizeIdentityId(sub)) {
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
const serviceAccountId = normalizeIdentityId(explicitServiceAccountId ?? sub);
|
|
1608
|
+
const keyIdSource = clientId ?? actSub ?? sub;
|
|
1609
|
+
if (!isNonEmptyTrimmed(serviceAccountId) || !isNonEmptyTrimmed(keyIdSource)) {
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
org,
|
|
1614
|
+
subject: {
|
|
1615
|
+
kind: "service",
|
|
1616
|
+
serviceAccountId,
|
|
1617
|
+
keyId: normalizeIdentityId(keyIdSource)
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
const memberId = normalizeIdentityId(sub);
|
|
1622
|
+
const act = claims.act;
|
|
1623
|
+
if (act !== void 0 && act !== null) {
|
|
1624
|
+
if (typeof act !== "object" || !isNonEmptyTrimmed(act.sub))
|
|
1625
|
+
return null;
|
|
1626
|
+
return {
|
|
1627
|
+
org,
|
|
1628
|
+
subject: {
|
|
1629
|
+
kind: "member",
|
|
1630
|
+
memberId,
|
|
1631
|
+
via: "api_key",
|
|
1632
|
+
keyId: normalizeIdentityId(act.sub)
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1620
1635
|
}
|
|
1636
|
+
return { org, subject: { kind: "member", memberId, via: "session" } };
|
|
1621
1637
|
}
|
|
1622
|
-
var IDENTITY_HEADER_NAMES = RUNTIME_IDENTITY_HEADER_NAMES;
|
|
1623
1638
|
|
|
1624
1639
|
// src/core/verifyContext.ts
|
|
1640
|
+
function principalFromContextClaims2(claims) {
|
|
1641
|
+
return principalFromContextClaims(claims);
|
|
1642
|
+
}
|
|
1625
1643
|
var EXPECTED_JWT_ALG = "HS256";
|
|
1626
1644
|
function base64urlDecode(value) {
|
|
1627
1645
|
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -1661,6 +1679,7 @@ async function verifyContext(token, secrets) {
|
|
|
1661
1679
|
}
|
|
1662
1680
|
let verified = false;
|
|
1663
1681
|
for (const secret of secrets) {
|
|
1682
|
+
if (secret.trim().length === 0) continue;
|
|
1664
1683
|
try {
|
|
1665
1684
|
const key2 = await importHmacKey(secret);
|
|
1666
1685
|
if (await crypto.subtle.verify(
|
|
@@ -1676,11 +1695,20 @@ async function verifyContext(token, secrets) {
|
|
|
1676
1695
|
}
|
|
1677
1696
|
}
|
|
1678
1697
|
if (!verified) return null;
|
|
1698
|
+
return parseContextPayload(payload);
|
|
1699
|
+
}
|
|
1700
|
+
function decodeContextClaims(token) {
|
|
1701
|
+
const parts = token.split(".");
|
|
1702
|
+
if (parts.length !== 3) return null;
|
|
1703
|
+
return parseContextPayload(parts[1]);
|
|
1704
|
+
}
|
|
1705
|
+
function parseContextPayload(payload) {
|
|
1679
1706
|
try {
|
|
1680
1707
|
const parsed = JSON.parse(
|
|
1681
1708
|
new TextDecoder().decode(base64urlDecode(payload))
|
|
1682
1709
|
);
|
|
1683
1710
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1711
|
+
if (parsed.cv !== 2) return null;
|
|
1684
1712
|
return parsed;
|
|
1685
1713
|
} catch {
|
|
1686
1714
|
return null;
|
|
@@ -1689,7 +1717,7 @@ async function verifyContext(token, secrets) {
|
|
|
1689
1717
|
function contextRequiredError(reason) {
|
|
1690
1718
|
return new FartherShoreError(
|
|
1691
1719
|
"context_unverified",
|
|
1692
|
-
`X-Fs-Context ${reason}
|
|
1720
|
+
`X-Fs-Context ${reason} \u2014 signed context is required whenever context secrets are configured`
|
|
1693
1721
|
);
|
|
1694
1722
|
}
|
|
1695
1723
|
|
|
@@ -1765,6 +1793,7 @@ async function verifyRequest(input, deps) {
|
|
|
1765
1793
|
"signed route-id is not served by this backend"
|
|
1766
1794
|
);
|
|
1767
1795
|
}
|
|
1796
|
+
const contextToken = h("x-fs-context") ?? null;
|
|
1768
1797
|
const canonicalInput = {
|
|
1769
1798
|
method: input.method,
|
|
1770
1799
|
path: input.path,
|
|
@@ -1775,7 +1804,13 @@ async function verifyRequest(input, deps) {
|
|
|
1775
1804
|
businessId: signedBusinessId,
|
|
1776
1805
|
backendId: signedBackendId,
|
|
1777
1806
|
routeId: signedRouteId,
|
|
1778
|
-
policyVersion
|
|
1807
|
+
policyVersion,
|
|
1808
|
+
// Consumer-principal wave (D3): bind the presented X-Fs-Context hash into
|
|
1809
|
+
// the canonical string in exact lockstep with the gateway signer. A missing
|
|
1810
|
+
// context hashes the empty string, so an identity-less request still
|
|
1811
|
+
// verifies (it is rejected later by the fail-closed context gate when
|
|
1812
|
+
// secrets exist).
|
|
1813
|
+
contextHash: await hashContextToken2(contextToken)
|
|
1779
1814
|
};
|
|
1780
1815
|
const canonical = buildCanonicalSigningString2(canonicalInput);
|
|
1781
1816
|
const publicJwk = await deps.jwks.getKey(kid);
|
|
@@ -1794,44 +1829,28 @@ async function verifyRequest(input, deps) {
|
|
|
1794
1829
|
"Ed25519 signature verification failed"
|
|
1795
1830
|
);
|
|
1796
1831
|
}
|
|
1797
|
-
if (deps.nonceCache.checkAndRemember(requestId)) {
|
|
1832
|
+
if (await deps.nonceCache.checkAndRemember(requestId)) {
|
|
1798
1833
|
throw new FartherShoreError(
|
|
1799
1834
|
"replayed_nonce",
|
|
1800
1835
|
"x-fs-request-id has already been seen (replay)"
|
|
1801
1836
|
);
|
|
1802
1837
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
signedContext = await verifyContext(token, contextSecrets);
|
|
1812
|
-
if (signedContext === null && deps.contextVerification === "required") {
|
|
1813
|
-
throw contextRequiredError("failed verification");
|
|
1814
|
-
}
|
|
1815
|
-
if (signedContext && signedContext.productId !== signedBusinessId) {
|
|
1816
|
-
throw new FartherShoreError(
|
|
1817
|
-
"context_unverified",
|
|
1818
|
-
"X-Fs-Context was minted for a different business than the signed request"
|
|
1819
|
-
);
|
|
1820
|
-
}
|
|
1821
|
-
} else if (deps.contextVerification === "required") {
|
|
1822
|
-
throw contextRequiredError("header is missing");
|
|
1823
|
-
}
|
|
1824
|
-
} else if (deps.contextVerification === "required") {
|
|
1825
|
-
throw contextRequiredError("keyring is empty");
|
|
1826
|
-
}
|
|
1838
|
+
const signedContext = await resolveSignedContext(
|
|
1839
|
+
contextToken,
|
|
1840
|
+
deps.contextSecrets ?? [],
|
|
1841
|
+
signedBusinessId
|
|
1842
|
+
);
|
|
1843
|
+
const permissions = signedContext?.permissions;
|
|
1844
|
+
const roles = signedContext?.roles;
|
|
1845
|
+
let principal;
|
|
1827
1846
|
if (signedContext) {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1847
|
+
const derived = principalFromContextClaims2(signedContext);
|
|
1848
|
+
if (derived === null) {
|
|
1849
|
+
throw contextRequiredError(
|
|
1850
|
+
"carried an invalid or incomplete consumer principal"
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
principal = derived;
|
|
1835
1854
|
}
|
|
1836
1855
|
return {
|
|
1837
1856
|
requestId,
|
|
@@ -1841,11 +1860,32 @@ async function verifyRequest(input, deps) {
|
|
|
1841
1860
|
policyVersion,
|
|
1842
1861
|
timestamp,
|
|
1843
1862
|
bodyHash: computedBodyHash,
|
|
1863
|
+
...principal ? { principal } : {},
|
|
1844
1864
|
...permissions !== void 0 ? { permissions } : {},
|
|
1845
1865
|
...roles !== void 0 ? { roles } : {},
|
|
1846
1866
|
...signedContext ? { signedContext } : {}
|
|
1847
1867
|
};
|
|
1848
1868
|
}
|
|
1869
|
+
async function resolveSignedContext(contextToken, contextSecrets, signedBusinessId) {
|
|
1870
|
+
if (!contextToken) return null;
|
|
1871
|
+
const signedContext = decodeContextClaims(contextToken);
|
|
1872
|
+
if (signedContext === null) {
|
|
1873
|
+
throw contextRequiredError("payload could not be parsed as a cv=2 claim");
|
|
1874
|
+
}
|
|
1875
|
+
if (contextSecrets.length > 0) {
|
|
1876
|
+
const hmacVerified = await verifyContext(contextToken, contextSecrets);
|
|
1877
|
+
if (hmacVerified === null) {
|
|
1878
|
+
throw contextRequiredError("failed HS256 verification");
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (signedContext.businessId !== signedBusinessId) {
|
|
1882
|
+
throw new FartherShoreError(
|
|
1883
|
+
"context_unverified",
|
|
1884
|
+
"X-Fs-Context was minted for a different business than the signed request"
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
return signedContext;
|
|
1888
|
+
}
|
|
1849
1889
|
async function computeBodyHash(input) {
|
|
1850
1890
|
if (input.streamingExempt) return STREAMING_EXEMPT_BODY_HASH;
|
|
1851
1891
|
const body = input.body;
|
|
@@ -1878,8 +1918,7 @@ function headerGetter(headers) {
|
|
|
1878
1918
|
|
|
1879
1919
|
// src/core/runtime.ts
|
|
1880
1920
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1881
|
-
var SDK_VERSION = "0.
|
|
1882
|
-
var CONTRACTS_FP = "4b6a36b4cb1f0b68".length > 0 ? "4b6a36b4cb1f0b68" : "0000000000000000";
|
|
1921
|
+
var SDK_VERSION = "0.17.0".length > 0 ? "0.17.0" : "0.0.0-dev";
|
|
1883
1922
|
var FartherShore = class {
|
|
1884
1923
|
bootstrapClient;
|
|
1885
1924
|
fetchImpl;
|
|
@@ -1889,11 +1928,9 @@ var FartherShore = class {
|
|
|
1889
1928
|
coreUrl;
|
|
1890
1929
|
instanceId;
|
|
1891
1930
|
tunnelOptions;
|
|
1892
|
-
/**
|
|
1931
|
+
/** OPTIONAL HS256 secret(s) — defense-in-depth over the cv=2 X-Fs-Context. */
|
|
1893
1932
|
contextSecrets;
|
|
1894
|
-
|
|
1895
|
-
contextVerification;
|
|
1896
|
-
nonceCache = new NonceCache();
|
|
1933
|
+
nonceCache;
|
|
1897
1934
|
shutdownManager = new ShutdownManager();
|
|
1898
1935
|
jwks = null;
|
|
1899
1936
|
meteringClient = null;
|
|
@@ -1911,8 +1948,8 @@ var FartherShore = class {
|
|
|
1911
1948
|
this.meteringEnabledOverride = options.metering?.enabled ?? true;
|
|
1912
1949
|
this.tunnelOptions = options.tunnel ?? {};
|
|
1913
1950
|
this.instanceId = options.instanceId;
|
|
1951
|
+
this.nonceCache = options.nonceStore ?? new NonceCache();
|
|
1914
1952
|
this.contextSecrets = options.contextSecrets ?? parseContextSecrets(env.FS_CONTEXT_SECRETS);
|
|
1915
|
-
this.contextVerification = options.contextVerification ?? (env.FS_CONTEXT_VERIFICATION === "required" ? "required" : "preferred");
|
|
1916
1953
|
this.bootstrapClient = new BootstrapClient({
|
|
1917
1954
|
runtimeToken,
|
|
1918
1955
|
coreUrl,
|
|
@@ -2037,12 +2074,10 @@ var FartherShore = class {
|
|
|
2037
2074
|
knownRouteIds,
|
|
2038
2075
|
clockSkewSeconds: config.verification.clockSkewSeconds,
|
|
2039
2076
|
replayWindowSeconds: config.verification.replayWindowSeconds,
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
2042
|
-
//
|
|
2043
|
-
|
|
2044
|
-
contextSecrets: this.contextSecrets,
|
|
2045
|
-
contextVerification: this.contextVerification
|
|
2077
|
+
// Consumer-principal wave (D3): OPTIONAL defense-in-depth. The principal
|
|
2078
|
+
// is derived from the Ed25519-vouched X-Fs-Context regardless; when these
|
|
2079
|
+
// secrets are set a presented token must ALSO pass HS256.
|
|
2080
|
+
contextSecrets: this.contextSecrets
|
|
2046
2081
|
});
|
|
2047
2082
|
return {
|
|
2048
2083
|
...context,
|
|
@@ -2169,6 +2204,45 @@ function parseContextSecrets(raw) {
|
|
|
2169
2204
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2170
2205
|
}
|
|
2171
2206
|
|
|
2207
|
+
// src/core/permissions.ts
|
|
2208
|
+
var WILDCARD = "*";
|
|
2209
|
+
var FartherShorePermissionError = class extends Error {
|
|
2210
|
+
code = "permission_denied";
|
|
2211
|
+
status = 403;
|
|
2212
|
+
/** The permission key that was required but not held. */
|
|
2213
|
+
requiredPermission;
|
|
2214
|
+
constructor(requiredPermission, message) {
|
|
2215
|
+
super(message ?? `missing required permission: ${requiredPermission}`);
|
|
2216
|
+
this.name = "FartherShorePermissionError";
|
|
2217
|
+
this.requiredPermission = requiredPermission;
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
function permissionGrants(permissions, key2) {
|
|
2221
|
+
if (permissions === void 0) return true;
|
|
2222
|
+
if (permissions.includes(WILDCARD)) return true;
|
|
2223
|
+
return permissions.includes(key2);
|
|
2224
|
+
}
|
|
2225
|
+
function permissionSatisfies(required, granted) {
|
|
2226
|
+
if (granted === void 0) return true;
|
|
2227
|
+
if (granted.includes(WILDCARD)) return true;
|
|
2228
|
+
if (granted.includes(required)) return true;
|
|
2229
|
+
const idx = required.indexOf(":");
|
|
2230
|
+
if (idx > 0 && idx < required.length - 1) {
|
|
2231
|
+
const subject = required.slice(0, idx);
|
|
2232
|
+
if (granted.includes(`${subject}:${WILDCARD}`)) return true;
|
|
2233
|
+
}
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
function hasPermission(ctx, key2) {
|
|
2237
|
+
if (ctx.permissions === void 0) return false;
|
|
2238
|
+
return permissionSatisfies(key2, ctx.permissions);
|
|
2239
|
+
}
|
|
2240
|
+
function requirePermission(ctx, key2) {
|
|
2241
|
+
if (!hasPermission(ctx, key2)) {
|
|
2242
|
+
throw new FartherShorePermissionError(key2);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2172
2246
|
// src/adapters/express.ts
|
|
2173
2247
|
var STREAMING_CONTENT_TYPES = new Set(
|
|
2174
2248
|
RUNTIME_BODY_HASH_CONTRACT.streamingExemptContentTypes
|
|
@@ -2180,7 +2254,9 @@ function createExpressMiddleware(fs, options = {}) {
|
|
|
2180
2254
|
}
|
|
2181
2255
|
async function runMiddleware(fs, options, req, res, next) {
|
|
2182
2256
|
try {
|
|
2183
|
-
|
|
2257
|
+
const strict = options.always ?? true;
|
|
2258
|
+
if (!strict && !await fs.verificationRequired()) {
|
|
2259
|
+
stripFartherShoreHeaders(req);
|
|
2184
2260
|
next();
|
|
2185
2261
|
return;
|
|
2186
2262
|
}
|
|
@@ -2197,6 +2273,7 @@ async function runMiddleware(fs, options, req, res, next) {
|
|
|
2197
2273
|
streamingExempt
|
|
2198
2274
|
});
|
|
2199
2275
|
req.fartherShore = ctx;
|
|
2276
|
+
stripFartherShoreHeaders(req);
|
|
2200
2277
|
next();
|
|
2201
2278
|
} catch (error) {
|
|
2202
2279
|
fail(res, error);
|
|
@@ -2209,6 +2286,51 @@ function fail(res, error) {
|
|
|
2209
2286
|
}
|
|
2210
2287
|
res.status(401).json({ error: "bad_signature" });
|
|
2211
2288
|
}
|
|
2289
|
+
function stripFartherShoreHeaders(req) {
|
|
2290
|
+
const headers = req.headers;
|
|
2291
|
+
for (const name of Object.keys(headers)) {
|
|
2292
|
+
if (name.toLowerCase().startsWith("x-fs-")) {
|
|
2293
|
+
delete headers[name];
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const withRaw = req;
|
|
2297
|
+
const raw = withRaw.rawHeaders;
|
|
2298
|
+
if (Array.isArray(raw)) {
|
|
2299
|
+
const cleaned = [];
|
|
2300
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
2301
|
+
const key2 = raw[i];
|
|
2302
|
+
const value = raw[i + 1];
|
|
2303
|
+
if (typeof key2 !== "string" || value === void 0) continue;
|
|
2304
|
+
if (key2.toLowerCase().startsWith("x-fs-")) continue;
|
|
2305
|
+
cleaned.push(key2, value);
|
|
2306
|
+
}
|
|
2307
|
+
withRaw.rawHeaders = cleaned;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function createExpressHandler(handler) {
|
|
2311
|
+
return (req, res, next) => {
|
|
2312
|
+
const ctx = req.fartherShore;
|
|
2313
|
+
if (!ctx) {
|
|
2314
|
+
res.status(401).json({ error: "context_unverified" });
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
if (!ctx.principal) {
|
|
2318
|
+
res.status(401).json({ error: "principal_required" });
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
const verified = ctx;
|
|
2322
|
+
void Promise.resolve().then(
|
|
2323
|
+
() => handler(verified, req, res, next)
|
|
2324
|
+
).catch((error) => failHandler(res, next, error));
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
function failHandler(res, next, error) {
|
|
2328
|
+
if (error instanceof FartherShoreError || error instanceof FartherShorePermissionError) {
|
|
2329
|
+
res.status(error.status).json({ error: error.code });
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
next(error);
|
|
2333
|
+
}
|
|
2212
2334
|
function splitUrl(req) {
|
|
2213
2335
|
const raw = req.originalUrl ?? req.url ?? req.path ?? "/";
|
|
2214
2336
|
const qIndex = raw.indexOf("?");
|
|
@@ -2235,6 +2357,42 @@ function headerValue(headers, name) {
|
|
|
2235
2357
|
return value;
|
|
2236
2358
|
}
|
|
2237
2359
|
|
|
2360
|
+
// src/core/subject.ts
|
|
2361
|
+
var MEMBER_SUBJECT_REQUIRED = RUNTIME_ERROR_CODES.memberSubjectRequired;
|
|
2362
|
+
var SERVICE_SUBJECT_REQUIRED = RUNTIME_ERROR_CODES.serviceSubjectRequired;
|
|
2363
|
+
function requireMember(ctx) {
|
|
2364
|
+
const subject = ctx.principal?.subject;
|
|
2365
|
+
if (!subject || subject.kind !== "member") {
|
|
2366
|
+
throw new FartherShoreError(
|
|
2367
|
+
MEMBER_SUBJECT_REQUIRED,
|
|
2368
|
+
"this operation requires a member subject (a user session or a personal key)",
|
|
2369
|
+
403
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
return subject;
|
|
2373
|
+
}
|
|
2374
|
+
function requireService(ctx) {
|
|
2375
|
+
const subject = ctx.principal?.subject;
|
|
2376
|
+
if (!subject || subject.kind !== "service") {
|
|
2377
|
+
throw new FartherShoreError(
|
|
2378
|
+
SERVICE_SUBJECT_REQUIRED,
|
|
2379
|
+
"this operation requires a service subject (an org-owned service-account key)",
|
|
2380
|
+
403
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
return subject;
|
|
2384
|
+
}
|
|
2385
|
+
function credentialKind(ctx) {
|
|
2386
|
+
const subject = ctx.principal?.subject;
|
|
2387
|
+
if (!subject) return void 0;
|
|
2388
|
+
if (subject.kind === "service") return "api_key";
|
|
2389
|
+
return subject.via === "session" ? "portal_session" : "api_key";
|
|
2390
|
+
}
|
|
2391
|
+
function isPortalSession(ctx) {
|
|
2392
|
+
const kind = credentialKind(ctx);
|
|
2393
|
+
return kind === void 0 ? void 0 : kind === "portal_session";
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2238
2396
|
// src/testing/signers.ts
|
|
2239
2397
|
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
2240
2398
|
var TEST_KID = "fs-runtime-test-2026";
|
|
@@ -2265,7 +2423,10 @@ async function makeSignedRequest(spec = {}) {
|
|
|
2265
2423
|
businessId: spec.businessId ?? "biz_test",
|
|
2266
2424
|
backendId: spec.backendId ?? "be_test",
|
|
2267
2425
|
routeId: spec.routeId ?? "route_test",
|
|
2268
|
-
policyVersion: spec.policyVersion ?? "pv_1"
|
|
2426
|
+
policyVersion: spec.policyVersion ?? "pv_1",
|
|
2427
|
+
// Consumer-principal wave (D3): bind the X-Fs-Context hash into the
|
|
2428
|
+
// canonical string (empty context → SHA-256 of the empty string).
|
|
2429
|
+
contextHash: await hashContextToken2(spec.contextToken)
|
|
2269
2430
|
};
|
|
2270
2431
|
const canonical = buildCanonicalSigningString2(claim);
|
|
2271
2432
|
const signature = await signCanonicalString2(canonical, privateJwk);
|
|
@@ -2278,7 +2439,8 @@ async function makeSignedRequest(spec = {}) {
|
|
|
2278
2439
|
[RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
|
|
2279
2440
|
[RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
|
|
2280
2441
|
[RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
|
|
2281
|
-
[RUNTIME_HEADER_NAMES.bodyHash]: claim.bodyHash
|
|
2442
|
+
[RUNTIME_HEADER_NAMES.bodyHash]: claim.bodyHash,
|
|
2443
|
+
...spec.contextToken ? { "x-fs-context": spec.contextToken } : {}
|
|
2282
2444
|
};
|
|
2283
2445
|
return {
|
|
2284
2446
|
input: { method, path, query, body, streamingExempt },
|
|
@@ -2296,7 +2458,7 @@ function base64urlEncodeJson(value) {
|
|
|
2296
2458
|
}
|
|
2297
2459
|
async function signContextToken(claim, secret = TEST_CONTEXT_SECRET, kid = TEST_CONTEXT_KID) {
|
|
2298
2460
|
const header = base64urlEncodeJson({ alg: "HS256", typ: "JWT", kid });
|
|
2299
|
-
const payload = base64urlEncodeJson({
|
|
2461
|
+
const payload = base64urlEncodeJson({ ...claim });
|
|
2300
2462
|
const signingInput = `${header}.${payload}`;
|
|
2301
2463
|
const key2 = await crypto.subtle.importKey(
|
|
2302
2464
|
"raw",
|
|
@@ -2376,12 +2538,15 @@ function mergeHeaders(initHeaders, signedHeaders) {
|
|
|
2376
2538
|
return headers;
|
|
2377
2539
|
}
|
|
2378
2540
|
function buildContextClaim(persona, businessId) {
|
|
2541
|
+
const memberId = persona.actor?.id ?? `user_${persona.name}`;
|
|
2379
2542
|
return {
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2543
|
+
cv: 2,
|
|
2544
|
+
sub: memberId,
|
|
2545
|
+
subjectKind: "member",
|
|
2546
|
+
org: persona.orgId ?? "org_dev",
|
|
2547
|
+
// Business binding: the signed-context businessId claim MUST equal the
|
|
2548
|
+
// signed request businessId or verifyRequest rejects it as tamper evidence.
|
|
2549
|
+
businessId,
|
|
2385
2550
|
compiledPlanId: persona.compiledPlanId ?? "plan_dev",
|
|
2386
2551
|
subscriptionId: persona.subscriptionId ?? "sub_dev",
|
|
2387
2552
|
subscriberId: persona.subscriberId ?? "subscriber_dev",
|
|
@@ -2403,6 +2568,11 @@ function createPersonaClient(ctx) {
|
|
|
2403
2568
|
}
|
|
2404
2569
|
async function buildHeaders(persona, spec) {
|
|
2405
2570
|
const method = normalizeMethod(spec.method);
|
|
2571
|
+
const contextToken = persona.anonymous ? void 0 : await signContextToken(
|
|
2572
|
+
buildContextClaim(persona, ctx.businessId),
|
|
2573
|
+
ctx.contextSecret,
|
|
2574
|
+
ctx.contextKid
|
|
2575
|
+
);
|
|
2406
2576
|
const signed = await makeSignedRequest({
|
|
2407
2577
|
method,
|
|
2408
2578
|
path: spec.path ?? "/",
|
|
@@ -2414,19 +2584,11 @@ function createPersonaClient(ctx) {
|
|
|
2414
2584
|
routeId: spec.routeId ?? "",
|
|
2415
2585
|
privateJwk: ctx.keys.privateJwk,
|
|
2416
2586
|
kid: ctx.keys.kid,
|
|
2587
|
+
...contextToken ? { contextToken } : {},
|
|
2417
2588
|
...spec.requestId ? { requestId: spec.requestId } : {},
|
|
2418
2589
|
...spec.timestamp !== void 0 ? { timestamp: spec.timestamp } : {}
|
|
2419
2590
|
});
|
|
2420
|
-
|
|
2421
|
-
if (!persona.anonymous) {
|
|
2422
|
-
const claim = buildContextClaim(persona, ctx.businessId);
|
|
2423
|
-
headers["x-fs-context"] = await signContextToken(
|
|
2424
|
-
claim,
|
|
2425
|
-
ctx.contextSecret,
|
|
2426
|
-
ctx.contextKid
|
|
2427
|
-
);
|
|
2428
|
-
}
|
|
2429
|
-
return headers;
|
|
2591
|
+
return { ...signed.headers };
|
|
2430
2592
|
}
|
|
2431
2593
|
function asPersona(name) {
|
|
2432
2594
|
const persona = resolve(name);
|
|
@@ -2504,7 +2666,7 @@ function createDevGateway(options) {
|
|
|
2504
2666
|
name: "Dev Backend"
|
|
2505
2667
|
},
|
|
2506
2668
|
environment: { id: null, kind: "test" },
|
|
2507
|
-
|
|
2669
|
+
operations: ["gateway_verification", "metering", "health"],
|
|
2508
2670
|
verification: {
|
|
2509
2671
|
required: options.mode === "simulated",
|
|
2510
2672
|
jwksUrl: DEV_JWKS_URL,
|
|
@@ -2810,8 +2972,9 @@ function createDevRuntime(options) {
|
|
|
2810
2972
|
runtimeToken: keys.runtimeToken,
|
|
2811
2973
|
coreUrl: DEV_CORE_URL,
|
|
2812
2974
|
fetchImpl: gateway.fetchImpl,
|
|
2975
|
+
// Consumer-principal wave (D3): signed cv=2 context is fail-closed whenever
|
|
2976
|
+
// contextSecrets are set — no per-mode verification toggle.
|
|
2813
2977
|
contextSecrets: [keys.contextSecret],
|
|
2814
|
-
contextVerification: mode === "simulated" ? "required" : "preferred",
|
|
2815
2978
|
env: {}
|
|
2816
2979
|
});
|
|
2817
2980
|
const tracedAuthz = {
|
|
@@ -2837,7 +3000,8 @@ function createDevRuntime(options) {
|
|
|
2837
3000
|
});
|
|
2838
3001
|
}
|
|
2839
3002
|
function middleware(mwOptions) {
|
|
2840
|
-
const
|
|
3003
|
+
const resolved = mode === "passthrough" ? { always: false, ...mwOptions } : mwOptions ?? {};
|
|
3004
|
+
const inner = createExpressMiddleware(fs, resolved);
|
|
2841
3005
|
return (req, res, next) => {
|
|
2842
3006
|
const requestId = headerValue2(req.headers, "x-fs-request-id") ?? "unknown";
|
|
2843
3007
|
const { path } = splitUrl2(req);
|
|
@@ -2876,6 +3040,7 @@ function createDevRuntime(options) {
|
|
|
2876
3040
|
};
|
|
2877
3041
|
}
|
|
2878
3042
|
fs.middleware = middleware;
|
|
3043
|
+
fs.handler = createExpressHandler;
|
|
2879
3044
|
const devRuntime = {
|
|
2880
3045
|
fs,
|
|
2881
3046
|
asPersona: (name) => personaClient.asPersona(name),
|
|
@@ -3003,6 +3168,7 @@ var fartherShore = {
|
|
|
3003
3168
|
}
|
|
3004
3169
|
const fs = initFromEnv(options);
|
|
3005
3170
|
fs.middleware = (mwOptions) => createExpressMiddleware(fs, mwOptions);
|
|
3171
|
+
fs.handler = createExpressHandler;
|
|
3006
3172
|
return fs;
|
|
3007
3173
|
}
|
|
3008
3174
|
};
|
|
@@ -3018,7 +3184,6 @@ export {
|
|
|
3018
3184
|
FartherShore,
|
|
3019
3185
|
FartherShoreError,
|
|
3020
3186
|
FartherShorePermissionError,
|
|
3021
|
-
IDENTITY_HEADER_NAMES,
|
|
3022
3187
|
JwksClient,
|
|
3023
3188
|
MAX_BODY_BYTES,
|
|
3024
3189
|
METERING_PAYLOAD_HEADER,
|
|
@@ -3034,7 +3199,7 @@ export {
|
|
|
3034
3199
|
RUNTIME_ERROR_CODE_TO_ERROR_CODE,
|
|
3035
3200
|
RUNTIME_HEADER_NAMES,
|
|
3036
3201
|
RUNTIME_REPLAY_WINDOW_SECONDS,
|
|
3037
|
-
|
|
3202
|
+
RUNTIME_TOKEN_OPERATIONS,
|
|
3038
3203
|
RUNTIME_TOKEN_PREFIXES,
|
|
3039
3204
|
STREAMING_EXEMPT_BODY_HASH,
|
|
3040
3205
|
ShutdownManager,
|
|
@@ -3042,18 +3207,24 @@ export {
|
|
|
3042
3207
|
buildHealthReport,
|
|
3043
3208
|
canonicalizeQuery2 as canonicalizeQuery,
|
|
3044
3209
|
computeMeteringHeaders,
|
|
3210
|
+
createExpressHandler,
|
|
3045
3211
|
createExpressMiddleware,
|
|
3046
3212
|
createUsage,
|
|
3213
|
+
credentialKind,
|
|
3214
|
+
decodeContextClaims,
|
|
3047
3215
|
fartherShore,
|
|
3048
3216
|
hasPermission,
|
|
3049
3217
|
hashBody2 as hashBody,
|
|
3050
3218
|
initFromEnv2 as initFromEnv,
|
|
3219
|
+
isPortalSession,
|
|
3051
3220
|
nodeSpawn,
|
|
3052
|
-
parsePermissionHeader,
|
|
3053
3221
|
permissionGrants,
|
|
3054
3222
|
permissionSatisfies,
|
|
3223
|
+
principalFromContextClaims2 as principalFromContextClaims,
|
|
3055
3224
|
reportHealth,
|
|
3225
|
+
requireMember,
|
|
3056
3226
|
requirePermission,
|
|
3227
|
+
requireService,
|
|
3057
3228
|
runtimeErrorToErrorCode,
|
|
3058
3229
|
runtimeTokenKind2 as runtimeTokenKind,
|
|
3059
3230
|
signCanonicalString2 as signCanonicalString,
|