@farthershore/backend 0.14.0 → 0.16.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 +68 -0
- package/README.md +6 -6
- package/dist/adapters/express.js +63 -1
- package/dist/generated/runtime-contract.js +15 -10
- package/dist/index.js +330 -174
- package/dist/testing/index.js +294 -167
- package/dist/types/adapters/express.d.ts +55 -5
- package/dist/types/core/metering.d.ts +8 -2
- 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 -30
- package/dist/types/core/subject.d.ts +25 -0
- package/dist/types/core/verifyContext.d.ts +96 -19
- package/dist/types/core/verifyRequest.d.ts +37 -27
- package/dist/types/generated/runtime-contract.d.ts +9 -6
- package/dist/types/index.d.ts +16 -7
- 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 +18 -10
- package/dist/types/testing/devGateway.d.ts +3 -3
- package/dist/types/testing/devRuntime.d.ts +4 -2
- package/dist/types/testing/keysFile.d.ts +1 -1
- package/dist/types/testing/personas.d.ts +2 -2
- package/dist/types/testing/signers.d.ts +14 -6
- package/package.json +5 -4
package/dist/testing/index.js
CHANGED
|
@@ -218,7 +218,9 @@ var RUNTIME_ERROR_CODES = {
|
|
|
218
218
|
environmentMismatch: "environment_mismatch",
|
|
219
219
|
missingToken: "missing_token",
|
|
220
220
|
invalidToken: "invalid_token",
|
|
221
|
-
contextUnverified: "context_unverified"
|
|
221
|
+
contextUnverified: "context_unverified",
|
|
222
|
+
memberSubjectRequired: "member_subject_required",
|
|
223
|
+
serviceSubjectRequired: "service_subject_required"
|
|
222
224
|
};
|
|
223
225
|
var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
224
226
|
headers: {
|
|
@@ -282,7 +284,10 @@ var RUNTIME_ERROR_CODE_TO_ERROR_CODE = {
|
|
|
282
284
|
// the canonical code keeps the "dependency down" semantic for callers.
|
|
283
285
|
[RUNTIME_ERROR_CODES.jwksUnavailable]: "SERVICE_UNAVAILABLE",
|
|
284
286
|
// The single non-401 (413) — oversized request body.
|
|
285
|
-
[RUNTIME_ERROR_CODES.bodyTooLarge]: "VALIDATION_ERROR"
|
|
287
|
+
[RUNTIME_ERROR_CODES.bodyTooLarge]: "VALIDATION_ERROR",
|
|
288
|
+
// Consumer-principal wave — route subject-requirement faults → FORBIDDEN (403).
|
|
289
|
+
[RUNTIME_ERROR_CODES.memberSubjectRequired]: "FORBIDDEN",
|
|
290
|
+
[RUNTIME_ERROR_CODES.serviceSubjectRequired]: "FORBIDDEN"
|
|
286
291
|
};
|
|
287
292
|
var FS_RUNTIME_TOKEN_ENV = "FS_RUNTIME_TOKEN";
|
|
288
293
|
var RUNTIME_TOKEN_PREFIXES = {
|
|
@@ -294,16 +299,12 @@ var RUNTIME_HEADER_NAMES = {
|
|
|
294
299
|
keyId: "x-fs-key-id",
|
|
295
300
|
requestId: "x-fs-request-id",
|
|
296
301
|
timestamp: "x-fs-timestamp",
|
|
297
|
-
|
|
302
|
+
businessId: "x-fs-business-id",
|
|
298
303
|
backendId: "x-fs-backend-id",
|
|
299
304
|
routeId: "x-fs-route-id",
|
|
300
305
|
policyVersion: "x-fs-policy-version",
|
|
301
306
|
bodyHash: "x-fs-body-hash"
|
|
302
307
|
};
|
|
303
|
-
var RUNTIME_IDENTITY_HEADER_NAMES = {
|
|
304
|
-
permissions: "x-fs-permissions",
|
|
305
|
-
roles: "x-fs-roles"
|
|
306
|
-
};
|
|
307
308
|
var RUNTIME_CLOCK_SKEW_SECONDS = 5;
|
|
308
309
|
var RUNTIME_REPLAY_WINDOW_SECONDS = 300;
|
|
309
310
|
var EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
@@ -346,34 +347,19 @@ var CANONICAL_SIGNING_FIELDS = [
|
|
|
346
347
|
"body-hash",
|
|
347
348
|
"request-id",
|
|
348
349
|
"timestamp",
|
|
349
|
-
"
|
|
350
|
+
"business-id",
|
|
350
351
|
"backend-id",
|
|
351
352
|
"route-id",
|
|
352
|
-
"policy-version"
|
|
353
|
+
"policy-version",
|
|
354
|
+
// Consumer-principal wave (D3) — the trailing identity-context binding line.
|
|
355
|
+
// MUST stay last so pre-binding verifiers that stop at `policy-version`
|
|
356
|
+
// fail loud on a bound request rather than silently accepting a prefix.
|
|
357
|
+
"context-hash"
|
|
353
358
|
];
|
|
354
359
|
var CANONICAL_FIELD_SEPARATOR = "\n";
|
|
355
360
|
var CANONICAL_KV_SEPARATOR = ":";
|
|
356
361
|
function canonicalizeQuery(query) {
|
|
357
|
-
|
|
358
|
-
if (raw === "")
|
|
359
|
-
return "";
|
|
360
|
-
const pairs = raw.split("&").filter((p) => p.length > 0);
|
|
361
|
-
pairs.sort((a, b) => {
|
|
362
|
-
const [an, ...arest] = a.split("=");
|
|
363
|
-
const [bn, ...brest] = b.split("=");
|
|
364
|
-
if (an < bn)
|
|
365
|
-
return -1;
|
|
366
|
-
if (an > bn)
|
|
367
|
-
return 1;
|
|
368
|
-
const av = arest.join("=");
|
|
369
|
-
const bv = brest.join("=");
|
|
370
|
-
if (av < bv)
|
|
371
|
-
return -1;
|
|
372
|
-
if (av > bv)
|
|
373
|
-
return 1;
|
|
374
|
-
return 0;
|
|
375
|
-
});
|
|
376
|
-
return pairs.join("&");
|
|
362
|
+
return query;
|
|
377
363
|
}
|
|
378
364
|
function buildCanonicalSigningString(input) {
|
|
379
365
|
const values = {
|
|
@@ -383,13 +369,17 @@ function buildCanonicalSigningString(input) {
|
|
|
383
369
|
"body-hash": input.bodyHash,
|
|
384
370
|
"request-id": input.requestId,
|
|
385
371
|
timestamp: String(input.timestamp),
|
|
386
|
-
"
|
|
372
|
+
"business-id": input.businessId,
|
|
387
373
|
"backend-id": input.backendId,
|
|
388
374
|
"route-id": input.routeId,
|
|
389
|
-
"policy-version": input.policyVersion
|
|
375
|
+
"policy-version": input.policyVersion,
|
|
376
|
+
"context-hash": input.contextHash
|
|
390
377
|
};
|
|
391
378
|
return CANONICAL_SIGNING_FIELDS.map((field) => `${field}${CANONICAL_KV_SEPARATOR}${values[field]}`).join(CANONICAL_FIELD_SEPARATOR);
|
|
392
379
|
}
|
|
380
|
+
function hashContextToken(token) {
|
|
381
|
+
return hashBody(new TextEncoder().encode(token ?? ""));
|
|
382
|
+
}
|
|
393
383
|
var ED25519_ALGORITHM = "Ed25519";
|
|
394
384
|
async function importEd25519PrivateKey(jwk) {
|
|
395
385
|
return crypto.subtle.importKey("jwk", { ...jwk, alg: void 0 }, { name: ED25519_ALGORITHM }, false, ["sign"]);
|
|
@@ -431,6 +421,7 @@ function base64UrlDecode(value) {
|
|
|
431
421
|
// src/runtime-signing.ts
|
|
432
422
|
var hashBody2 = hashBody;
|
|
433
423
|
var buildCanonicalSigningString2 = buildCanonicalSigningString;
|
|
424
|
+
var hashContextToken2 = hashContextToken;
|
|
434
425
|
var signCanonicalString2 = signCanonicalString;
|
|
435
426
|
var verifyCanonicalSignature2 = verifyCanonicalSignature;
|
|
436
427
|
var runtimeTokenKind2 = runtimeTokenKind;
|
|
@@ -608,10 +599,13 @@ async function makeSignedRequest(spec = {}) {
|
|
|
608
599
|
bodyHash,
|
|
609
600
|
requestId: spec.requestId ?? `req_${cryptoRandom()}`,
|
|
610
601
|
timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
611
|
-
|
|
602
|
+
businessId: spec.businessId ?? "biz_test",
|
|
612
603
|
backendId: spec.backendId ?? "be_test",
|
|
613
604
|
routeId: spec.routeId ?? "route_test",
|
|
614
|
-
policyVersion: spec.policyVersion ?? "pv_1"
|
|
605
|
+
policyVersion: spec.policyVersion ?? "pv_1",
|
|
606
|
+
// Consumer-principal wave (D3): bind the X-Fs-Context hash into the
|
|
607
|
+
// canonical string (empty context → SHA-256 of the empty string).
|
|
608
|
+
contextHash: await hashContextToken2(spec.contextToken)
|
|
615
609
|
};
|
|
616
610
|
const canonical = buildCanonicalSigningString2(claim);
|
|
617
611
|
const signature = await signCanonicalString2(canonical, privateJwk);
|
|
@@ -620,11 +614,12 @@ async function makeSignedRequest(spec = {}) {
|
|
|
620
614
|
[RUNTIME_HEADER_NAMES.keyId]: kid,
|
|
621
615
|
[RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
|
|
622
616
|
[RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
|
|
623
|
-
[RUNTIME_HEADER_NAMES.
|
|
617
|
+
[RUNTIME_HEADER_NAMES.businessId]: claim.businessId,
|
|
624
618
|
[RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
|
|
625
619
|
[RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
|
|
626
620
|
[RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
|
|
627
|
-
[RUNTIME_HEADER_NAMES.bodyHash]: claim.bodyHash
|
|
621
|
+
[RUNTIME_HEADER_NAMES.bodyHash]: claim.bodyHash,
|
|
622
|
+
...spec.contextToken ? { "x-fs-context": spec.contextToken } : {}
|
|
628
623
|
};
|
|
629
624
|
return {
|
|
630
625
|
input: { method, path, query, body, streamingExempt },
|
|
@@ -663,7 +658,7 @@ function base64urlEncodeJson(value) {
|
|
|
663
658
|
}
|
|
664
659
|
async function signContextToken(claim, secret = TEST_CONTEXT_SECRET, kid = TEST_CONTEXT_KID) {
|
|
665
660
|
const header = base64urlEncodeJson({ alg: "HS256", typ: "JWT", kid });
|
|
666
|
-
const payload = base64urlEncodeJson({
|
|
661
|
+
const payload = base64urlEncodeJson({ ...claim });
|
|
667
662
|
const signingInput = `${header}.${payload}`;
|
|
668
663
|
const key2 = await crypto.subtle.importKey(
|
|
669
664
|
"raw",
|
|
@@ -742,13 +737,16 @@ function mergeHeaders(initHeaders, signedHeaders) {
|
|
|
742
737
|
}
|
|
743
738
|
return headers;
|
|
744
739
|
}
|
|
745
|
-
function buildContextClaim(persona,
|
|
740
|
+
function buildContextClaim(persona, businessId) {
|
|
741
|
+
const memberId = persona.actor?.id ?? `user_${persona.name}`;
|
|
746
742
|
return {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
743
|
+
cv: 2,
|
|
744
|
+
sub: memberId,
|
|
745
|
+
subjectKind: "member",
|
|
746
|
+
org: persona.orgId ?? "org_dev",
|
|
747
|
+
// Business binding: the signed-context businessId claim MUST equal the
|
|
748
|
+
// signed request businessId or verifyRequest rejects it as tamper evidence.
|
|
749
|
+
businessId,
|
|
752
750
|
compiledPlanId: persona.compiledPlanId ?? "plan_dev",
|
|
753
751
|
subscriptionId: persona.subscriptionId ?? "sub_dev",
|
|
754
752
|
subscriberId: persona.subscriberId ?? "subscriber_dev",
|
|
@@ -770,30 +768,27 @@ function createPersonaClient(ctx) {
|
|
|
770
768
|
}
|
|
771
769
|
async function buildHeaders(persona, spec) {
|
|
772
770
|
const method = normalizeMethod(spec.method);
|
|
771
|
+
const contextToken = persona.anonymous ? void 0 : await signContextToken(
|
|
772
|
+
buildContextClaim(persona, ctx.businessId),
|
|
773
|
+
ctx.contextSecret,
|
|
774
|
+
ctx.contextKid
|
|
775
|
+
);
|
|
773
776
|
const signed = await makeSignedRequest({
|
|
774
777
|
method,
|
|
775
778
|
path: spec.path ?? "/",
|
|
776
779
|
query: spec.query ?? "",
|
|
777
780
|
body: spec.body ?? null,
|
|
778
781
|
streamingExempt: spec.streamingExempt ?? false,
|
|
779
|
-
|
|
782
|
+
businessId: ctx.businessId,
|
|
780
783
|
backendId: ctx.backendId,
|
|
781
784
|
routeId: spec.routeId ?? "",
|
|
782
785
|
privateJwk: ctx.keys.privateJwk,
|
|
783
786
|
kid: ctx.keys.kid,
|
|
787
|
+
...contextToken ? { contextToken } : {},
|
|
784
788
|
...spec.requestId ? { requestId: spec.requestId } : {},
|
|
785
789
|
...spec.timestamp !== void 0 ? { timestamp: spec.timestamp } : {}
|
|
786
790
|
});
|
|
787
|
-
|
|
788
|
-
if (!persona.anonymous) {
|
|
789
|
-
const claim = buildContextClaim(persona, ctx.productId);
|
|
790
|
-
headers["x-fs-context"] = await signContextToken(
|
|
791
|
-
claim,
|
|
792
|
-
ctx.contextSecret,
|
|
793
|
-
ctx.contextKid
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
return headers;
|
|
791
|
+
return { ...signed.headers };
|
|
797
792
|
}
|
|
798
793
|
function asPersona(name) {
|
|
799
794
|
const persona = resolve(name);
|
|
@@ -861,12 +856,12 @@ var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
|
|
|
861
856
|
var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
|
|
862
857
|
var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
|
|
863
858
|
function createDevGateway(options) {
|
|
864
|
-
const
|
|
859
|
+
const businessId = options.businessId ?? "biz_dev";
|
|
865
860
|
const backendId = options.backendId ?? "be_dev";
|
|
866
861
|
const meterEvents = [];
|
|
867
862
|
const reportUsageEvents = [];
|
|
868
863
|
const bootstrap = {
|
|
869
|
-
|
|
864
|
+
business: { id: businessId, slug: options.businessSlug ?? "dev-business" },
|
|
870
865
|
backend: {
|
|
871
866
|
id: backendId,
|
|
872
867
|
slug: options.backendSlug ?? "dev-backend",
|
|
@@ -936,7 +931,7 @@ function createDevGateway(options) {
|
|
|
936
931
|
bootstrap,
|
|
937
932
|
meterEvents,
|
|
938
933
|
reportUsageEvents,
|
|
939
|
-
|
|
934
|
+
businessId,
|
|
940
935
|
backendId,
|
|
941
936
|
jwksUrl: DEV_JWKS_URL
|
|
942
937
|
};
|
|
@@ -1136,7 +1131,7 @@ var DEFAULT_MAX_RETRIES = 3;
|
|
|
1136
1131
|
var MeteringClient = class {
|
|
1137
1132
|
config;
|
|
1138
1133
|
endpoint;
|
|
1139
|
-
|
|
1134
|
+
businessId;
|
|
1140
1135
|
backendId;
|
|
1141
1136
|
fetchImpl;
|
|
1142
1137
|
maxRetries;
|
|
@@ -1150,7 +1145,7 @@ var MeteringClient = class {
|
|
|
1150
1145
|
constructor(options) {
|
|
1151
1146
|
this.config = options.config;
|
|
1152
1147
|
this.endpoint = resolveEndpoint(options.config.endpoint, options.coreUrl);
|
|
1153
|
-
this.
|
|
1148
|
+
this.businessId = options.businessId;
|
|
1154
1149
|
this.backendId = options.backendId;
|
|
1155
1150
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1156
1151
|
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
@@ -1213,13 +1208,14 @@ var MeteringClient = class {
|
|
|
1213
1208
|
}
|
|
1214
1209
|
const event = {
|
|
1215
1210
|
event_id: options.eventId ?? this.newId(),
|
|
1216
|
-
|
|
1211
|
+
business_id: this.businessId,
|
|
1217
1212
|
backend_id: this.backendId,
|
|
1218
1213
|
meter,
|
|
1219
1214
|
qty,
|
|
1220
1215
|
timestamp: options.timestamp ?? this.now().toISOString(),
|
|
1221
1216
|
...options.routeId ? { route_id: options.routeId } : {},
|
|
1222
|
-
...options.requestId ? { request_id: options.requestId } : {}
|
|
1217
|
+
...options.requestId ? { request_id: options.requestId } : {},
|
|
1218
|
+
...options.subscriptionId ? { subscription_id: options.subscriptionId } : {}
|
|
1223
1219
|
};
|
|
1224
1220
|
this.buffer.push(event);
|
|
1225
1221
|
await this.flush();
|
|
@@ -1421,8 +1417,8 @@ function resolveEndpoint2(endpoint, coreUrl) {
|
|
|
1421
1417
|
}
|
|
1422
1418
|
|
|
1423
1419
|
// src/core/nonceCache.ts
|
|
1424
|
-
var DEFAULT_MAX_ENTRIES =
|
|
1425
|
-
var DEFAULT_TTL_MS =
|
|
1420
|
+
var DEFAULT_MAX_ENTRIES = 25e4;
|
|
1421
|
+
var DEFAULT_TTL_MS = (RUNTIME_REPLAY_WINDOW_SECONDS + RUNTIME_CLOCK_SKEW_SECONDS) * 1e3;
|
|
1426
1422
|
var NonceCache = class {
|
|
1427
1423
|
maxEntries;
|
|
1428
1424
|
ttlMs;
|
|
@@ -1446,7 +1442,7 @@ var NonceCache = class {
|
|
|
1446
1442
|
this.seen.delete(id);
|
|
1447
1443
|
}
|
|
1448
1444
|
this.evictExpired(at);
|
|
1449
|
-
this.
|
|
1445
|
+
if (this.seen.size >= this.maxEntries) return true;
|
|
1450
1446
|
this.seen.set(id, at);
|
|
1451
1447
|
return false;
|
|
1452
1448
|
}
|
|
@@ -1460,13 +1456,6 @@ var NonceCache = class {
|
|
|
1460
1456
|
this.seen.delete(id);
|
|
1461
1457
|
}
|
|
1462
1458
|
}
|
|
1463
|
-
evictOverflow() {
|
|
1464
|
-
while (this.seen.size >= this.maxEntries) {
|
|
1465
|
-
const oldest = this.seen.keys().next().value;
|
|
1466
|
-
if (oldest === void 0) break;
|
|
1467
|
-
this.seen.delete(oldest);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
1459
|
};
|
|
1471
1460
|
|
|
1472
1461
|
// src/core/shutdown.ts
|
|
@@ -1801,47 +1790,82 @@ function resolvePackageBinary(require2, pkg, manifestPath) {
|
|
|
1801
1790
|
return `${root}${sep}${normalized}`;
|
|
1802
1791
|
}
|
|
1803
1792
|
|
|
1804
|
-
//
|
|
1805
|
-
var
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
status = 403;
|
|
1809
|
-
/** The permission key that was required but not held. */
|
|
1810
|
-
requiredPermission;
|
|
1811
|
-
constructor(requiredPermission, message) {
|
|
1812
|
-
super(message ?? `missing required permission: ${requiredPermission}`);
|
|
1813
|
-
this.name = "FartherShorePermissionError";
|
|
1814
|
-
this.requiredPermission = requiredPermission;
|
|
1815
|
-
}
|
|
1816
|
-
};
|
|
1817
|
-
function parsePermissionHeader(raw) {
|
|
1818
|
-
if (raw === null || raw === void 0) return void 0;
|
|
1819
|
-
return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1793
|
+
// ../contracts/dist/authz/principal.js
|
|
1794
|
+
var SUBJECT_KINDS = ["member", "service"];
|
|
1795
|
+
function isSubjectKind(value) {
|
|
1796
|
+
return typeof value === "string" && SUBJECT_KINDS.includes(value);
|
|
1820
1797
|
}
|
|
1821
|
-
function
|
|
1822
|
-
|
|
1823
|
-
if (granted.includes(WILDCARD)) return true;
|
|
1824
|
-
if (granted.includes(required)) return true;
|
|
1825
|
-
const idx = required.indexOf(":");
|
|
1826
|
-
if (idx > 0 && idx < required.length - 1) {
|
|
1827
|
-
const subject = required.slice(0, idx);
|
|
1828
|
-
if (granted.includes(`${subject}:${WILDCARD}`)) return true;
|
|
1829
|
-
}
|
|
1830
|
-
return false;
|
|
1798
|
+
function isNonEmptyTrimmed(value) {
|
|
1799
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1831
1800
|
}
|
|
1832
|
-
function
|
|
1833
|
-
|
|
1834
|
-
return ctx.signedContext !== void 0;
|
|
1835
|
-
}
|
|
1836
|
-
return permissionSatisfies(key2, ctx.permissions);
|
|
1801
|
+
function normalizeIdentityId(value) {
|
|
1802
|
+
return value.trim();
|
|
1837
1803
|
}
|
|
1838
|
-
function
|
|
1839
|
-
|
|
1840
|
-
|
|
1804
|
+
function isNonEmptyStringArray(value) {
|
|
1805
|
+
return Array.isArray(value) && value.every((item) => isNonEmptyTrimmed(item));
|
|
1806
|
+
}
|
|
1807
|
+
function principalFromContextClaims(claims) {
|
|
1808
|
+
if (typeof claims !== "object" || claims === null)
|
|
1809
|
+
return null;
|
|
1810
|
+
const { sub, org: orgId, businessId, subjectKind } = claims;
|
|
1811
|
+
if (!isNonEmptyTrimmed(sub) || !isNonEmptyTrimmed(orgId) || !isNonEmptyTrimmed(businessId)) {
|
|
1812
|
+
return null;
|
|
1841
1813
|
}
|
|
1814
|
+
if (!isSubjectKind(subjectKind))
|
|
1815
|
+
return null;
|
|
1816
|
+
if (claims.permissions !== void 0 && !isNonEmptyStringArray(claims.permissions)) {
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
if (claims.roles !== void 0 && !isNonEmptyStringArray(claims.roles)) {
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1822
|
+
const clientId = claims.client_id;
|
|
1823
|
+
const actSub = claims.act?.sub;
|
|
1824
|
+
if (isNonEmptyTrimmed(clientId) && isNonEmptyTrimmed(actSub) && normalizeIdentityId(clientId) !== normalizeIdentityId(actSub)) {
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
const org = { id: normalizeIdentityId(orgId) };
|
|
1828
|
+
if (subjectKind === "service") {
|
|
1829
|
+
const explicitServiceAccountId = claims.serviceAccountId;
|
|
1830
|
+
if (isNonEmptyTrimmed(explicitServiceAccountId) && normalizeIdentityId(explicitServiceAccountId) !== normalizeIdentityId(sub)) {
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
const serviceAccountId = normalizeIdentityId(explicitServiceAccountId ?? sub);
|
|
1834
|
+
const keyIdSource = clientId ?? actSub ?? sub;
|
|
1835
|
+
if (!isNonEmptyTrimmed(serviceAccountId) || !isNonEmptyTrimmed(keyIdSource)) {
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
org,
|
|
1840
|
+
subject: {
|
|
1841
|
+
kind: "service",
|
|
1842
|
+
serviceAccountId,
|
|
1843
|
+
keyId: normalizeIdentityId(keyIdSource)
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const memberId = normalizeIdentityId(sub);
|
|
1848
|
+
const act = claims.act;
|
|
1849
|
+
if (act !== void 0 && act !== null) {
|
|
1850
|
+
if (typeof act !== "object" || !isNonEmptyTrimmed(act.sub))
|
|
1851
|
+
return null;
|
|
1852
|
+
return {
|
|
1853
|
+
org,
|
|
1854
|
+
subject: {
|
|
1855
|
+
kind: "member",
|
|
1856
|
+
memberId,
|
|
1857
|
+
via: "api_key",
|
|
1858
|
+
keyId: normalizeIdentityId(act.sub)
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
return { org, subject: { kind: "member", memberId, via: "session" } };
|
|
1842
1863
|
}
|
|
1843
1864
|
|
|
1844
1865
|
// src/core/verifyContext.ts
|
|
1866
|
+
function principalFromContextClaims2(claims) {
|
|
1867
|
+
return principalFromContextClaims(claims);
|
|
1868
|
+
}
|
|
1845
1869
|
var EXPECTED_JWT_ALG = "HS256";
|
|
1846
1870
|
function base64urlDecode(value) {
|
|
1847
1871
|
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -1881,6 +1905,7 @@ async function verifyContext(token, secrets) {
|
|
|
1881
1905
|
}
|
|
1882
1906
|
let verified = false;
|
|
1883
1907
|
for (const secret of secrets) {
|
|
1908
|
+
if (secret.trim().length === 0) continue;
|
|
1884
1909
|
try {
|
|
1885
1910
|
const key2 = await importHmacKey(secret);
|
|
1886
1911
|
if (await crypto.subtle.verify(
|
|
@@ -1896,11 +1921,20 @@ async function verifyContext(token, secrets) {
|
|
|
1896
1921
|
}
|
|
1897
1922
|
}
|
|
1898
1923
|
if (!verified) return null;
|
|
1924
|
+
return parseContextPayload(payload);
|
|
1925
|
+
}
|
|
1926
|
+
function decodeContextClaims(token) {
|
|
1927
|
+
const parts = token.split(".");
|
|
1928
|
+
if (parts.length !== 3) return null;
|
|
1929
|
+
return parseContextPayload(parts[1]);
|
|
1930
|
+
}
|
|
1931
|
+
function parseContextPayload(payload) {
|
|
1899
1932
|
try {
|
|
1900
1933
|
const parsed = JSON.parse(
|
|
1901
1934
|
new TextDecoder().decode(base64urlDecode(payload))
|
|
1902
1935
|
);
|
|
1903
1936
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1937
|
+
if (parsed.cv !== 2) return null;
|
|
1904
1938
|
return parsed;
|
|
1905
1939
|
} catch {
|
|
1906
1940
|
return null;
|
|
@@ -1909,7 +1943,7 @@ async function verifyContext(token, secrets) {
|
|
|
1909
1943
|
function contextRequiredError(reason) {
|
|
1910
1944
|
return new FartherShoreError(
|
|
1911
1945
|
"context_unverified",
|
|
1912
|
-
`X-Fs-Context ${reason}
|
|
1946
|
+
`X-Fs-Context ${reason} \u2014 signed context is required whenever context secrets are configured`
|
|
1913
1947
|
);
|
|
1914
1948
|
}
|
|
1915
1949
|
|
|
@@ -1926,12 +1960,12 @@ async function verifyRequest(input, deps) {
|
|
|
1926
1960
|
const kid = h(RUNTIME_HEADER_NAMES.keyId);
|
|
1927
1961
|
const requestId = h(RUNTIME_HEADER_NAMES.requestId);
|
|
1928
1962
|
const timestampRaw = h(RUNTIME_HEADER_NAMES.timestamp);
|
|
1929
|
-
const
|
|
1963
|
+
const signedBusinessId = h(RUNTIME_HEADER_NAMES.businessId);
|
|
1930
1964
|
const signedBackendId = h(RUNTIME_HEADER_NAMES.backendId);
|
|
1931
1965
|
const signedRouteId = h(RUNTIME_HEADER_NAMES.routeId) ?? "";
|
|
1932
1966
|
const policyVersion = h(RUNTIME_HEADER_NAMES.policyVersion);
|
|
1933
1967
|
const signedBodyHash = h(RUNTIME_HEADER_NAMES.bodyHash);
|
|
1934
|
-
if (!kid || !requestId || !timestampRaw || !
|
|
1968
|
+
if (!kid || !requestId || !timestampRaw || !signedBusinessId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
|
|
1935
1969
|
throw new FartherShoreError(
|
|
1936
1970
|
"malformed_signature",
|
|
1937
1971
|
"request is missing one or more required x-fs-* headers"
|
|
@@ -1967,10 +2001,10 @@ async function verifyRequest(input, deps) {
|
|
|
1967
2001
|
"recomputed body hash does not match the signed x-fs-body-hash"
|
|
1968
2002
|
);
|
|
1969
2003
|
}
|
|
1970
|
-
if (deps.
|
|
2004
|
+
if (deps.businessId !== void 0 && signedBusinessId !== deps.businessId) {
|
|
1971
2005
|
throw new FartherShoreError(
|
|
1972
2006
|
"route_mismatch",
|
|
1973
|
-
"signed
|
|
2007
|
+
"signed business-id does not match this backend's business"
|
|
1974
2008
|
);
|
|
1975
2009
|
}
|
|
1976
2010
|
if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
@@ -1985,6 +2019,7 @@ async function verifyRequest(input, deps) {
|
|
|
1985
2019
|
"signed route-id is not served by this backend"
|
|
1986
2020
|
);
|
|
1987
2021
|
}
|
|
2022
|
+
const contextToken = h("x-fs-context") ?? null;
|
|
1988
2023
|
const canonicalInput = {
|
|
1989
2024
|
method: input.method,
|
|
1990
2025
|
path: input.path,
|
|
@@ -1992,10 +2027,16 @@ async function verifyRequest(input, deps) {
|
|
|
1992
2027
|
bodyHash: computedBodyHash,
|
|
1993
2028
|
requestId,
|
|
1994
2029
|
timestamp,
|
|
1995
|
-
|
|
2030
|
+
businessId: signedBusinessId,
|
|
1996
2031
|
backendId: signedBackendId,
|
|
1997
2032
|
routeId: signedRouteId,
|
|
1998
|
-
policyVersion
|
|
2033
|
+
policyVersion,
|
|
2034
|
+
// Consumer-principal wave (D3): bind the presented X-Fs-Context hash into
|
|
2035
|
+
// the canonical string in exact lockstep with the gateway signer. A missing
|
|
2036
|
+
// context hashes the empty string, so an identity-less request still
|
|
2037
|
+
// verifies (it is rejected later by the fail-closed context gate when
|
|
2038
|
+
// secrets exist).
|
|
2039
|
+
contextHash: await hashContextToken2(contextToken)
|
|
1999
2040
|
};
|
|
2000
2041
|
const canonical = buildCanonicalSigningString2(canonicalInput);
|
|
2001
2042
|
const publicJwk = await deps.jwks.getKey(kid);
|
|
@@ -2014,58 +2055,63 @@ async function verifyRequest(input, deps) {
|
|
|
2014
2055
|
"Ed25519 signature verification failed"
|
|
2015
2056
|
);
|
|
2016
2057
|
}
|
|
2017
|
-
if (deps.nonceCache.checkAndRemember(requestId)) {
|
|
2058
|
+
if (await deps.nonceCache.checkAndRemember(requestId)) {
|
|
2018
2059
|
throw new FartherShoreError(
|
|
2019
2060
|
"replayed_nonce",
|
|
2020
2061
|
"x-fs-request-id has already been seen (replay)"
|
|
2021
2062
|
);
|
|
2022
2063
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
signedContext = await verifyContext(token, contextSecrets);
|
|
2032
|
-
if (signedContext === null && deps.contextVerification === "required") {
|
|
2033
|
-
throw contextRequiredError("failed verification");
|
|
2034
|
-
}
|
|
2035
|
-
if (signedContext && signedContext.productId !== signedProductId) {
|
|
2036
|
-
throw new FartherShoreError(
|
|
2037
|
-
"context_unverified",
|
|
2038
|
-
"X-Fs-Context was minted for a different product than the signed request"
|
|
2039
|
-
);
|
|
2040
|
-
}
|
|
2041
|
-
} else if (deps.contextVerification === "required") {
|
|
2042
|
-
throw contextRequiredError("header is missing");
|
|
2043
|
-
}
|
|
2044
|
-
} else if (deps.contextVerification === "required") {
|
|
2045
|
-
throw contextRequiredError("keyring is empty");
|
|
2046
|
-
}
|
|
2064
|
+
const signedContext = await resolveSignedContext(
|
|
2065
|
+
contextToken,
|
|
2066
|
+
deps.contextSecrets ?? [],
|
|
2067
|
+
signedBusinessId
|
|
2068
|
+
);
|
|
2069
|
+
const permissions = signedContext?.permissions;
|
|
2070
|
+
const roles = signedContext?.roles;
|
|
2071
|
+
let principal;
|
|
2047
2072
|
if (signedContext) {
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2073
|
+
const derived = principalFromContextClaims2(signedContext);
|
|
2074
|
+
if (derived === null) {
|
|
2075
|
+
throw contextRequiredError(
|
|
2076
|
+
"carried an invalid or incomplete consumer principal"
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
principal = derived;
|
|
2055
2080
|
}
|
|
2056
2081
|
return {
|
|
2057
2082
|
requestId,
|
|
2058
|
-
|
|
2083
|
+
businessId: signedBusinessId,
|
|
2059
2084
|
backendId: signedBackendId,
|
|
2060
2085
|
routeId: signedRouteId,
|
|
2061
2086
|
policyVersion,
|
|
2062
2087
|
timestamp,
|
|
2063
2088
|
bodyHash: computedBodyHash,
|
|
2089
|
+
...principal ? { principal } : {},
|
|
2064
2090
|
...permissions !== void 0 ? { permissions } : {},
|
|
2065
2091
|
...roles !== void 0 ? { roles } : {},
|
|
2066
2092
|
...signedContext ? { signedContext } : {}
|
|
2067
2093
|
};
|
|
2068
2094
|
}
|
|
2095
|
+
async function resolveSignedContext(contextToken, contextSecrets, signedBusinessId) {
|
|
2096
|
+
if (!contextToken) return null;
|
|
2097
|
+
const signedContext = decodeContextClaims(contextToken);
|
|
2098
|
+
if (signedContext === null) {
|
|
2099
|
+
throw contextRequiredError("payload could not be parsed as a cv=2 claim");
|
|
2100
|
+
}
|
|
2101
|
+
if (contextSecrets.length > 0) {
|
|
2102
|
+
const hmacVerified = await verifyContext(contextToken, contextSecrets);
|
|
2103
|
+
if (hmacVerified === null) {
|
|
2104
|
+
throw contextRequiredError("failed HS256 verification");
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (signedContext.businessId !== signedBusinessId) {
|
|
2108
|
+
throw new FartherShoreError(
|
|
2109
|
+
"context_unverified",
|
|
2110
|
+
"X-Fs-Context was minted for a different business than the signed request"
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
return signedContext;
|
|
2114
|
+
}
|
|
2069
2115
|
async function computeBodyHash(input) {
|
|
2070
2116
|
if (input.streamingExempt) return STREAMING_EXEMPT_BODY_HASH;
|
|
2071
2117
|
const body = input.body;
|
|
@@ -2098,8 +2144,8 @@ function headerGetter(headers) {
|
|
|
2098
2144
|
|
|
2099
2145
|
// src/core/runtime.ts
|
|
2100
2146
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
2101
|
-
var SDK_VERSION = "0.
|
|
2102
|
-
var CONTRACTS_FP = "
|
|
2147
|
+
var SDK_VERSION = "0.16.0".length > 0 ? "0.16.0" : "0.0.0-dev";
|
|
2148
|
+
var CONTRACTS_FP = "c3961d4ea07ff178".length > 0 ? "c3961d4ea07ff178" : "0000000000000000";
|
|
2103
2149
|
var FartherShore = class {
|
|
2104
2150
|
bootstrapClient;
|
|
2105
2151
|
fetchImpl;
|
|
@@ -2109,11 +2155,9 @@ var FartherShore = class {
|
|
|
2109
2155
|
coreUrl;
|
|
2110
2156
|
instanceId;
|
|
2111
2157
|
tunnelOptions;
|
|
2112
|
-
/**
|
|
2158
|
+
/** OPTIONAL HS256 secret(s) — defense-in-depth over the cv=2 X-Fs-Context. */
|
|
2113
2159
|
contextSecrets;
|
|
2114
|
-
|
|
2115
|
-
contextVerification;
|
|
2116
|
-
nonceCache = new NonceCache();
|
|
2160
|
+
nonceCache;
|
|
2117
2161
|
shutdownManager = new ShutdownManager();
|
|
2118
2162
|
jwks = null;
|
|
2119
2163
|
meteringClient = null;
|
|
@@ -2131,8 +2175,8 @@ var FartherShore = class {
|
|
|
2131
2175
|
this.meteringEnabledOverride = options.metering?.enabled ?? true;
|
|
2132
2176
|
this.tunnelOptions = options.tunnel ?? {};
|
|
2133
2177
|
this.instanceId = options.instanceId;
|
|
2178
|
+
this.nonceCache = options.nonceStore ?? new NonceCache();
|
|
2134
2179
|
this.contextSecrets = options.contextSecrets ?? parseContextSecrets(env.FS_CONTEXT_SECRETS);
|
|
2135
|
-
this.contextVerification = options.contextVerification ?? (env.FS_CONTEXT_VERIFICATION === "required" ? "required" : "preferred");
|
|
2136
2180
|
this.bootstrapClient = new BootstrapClient({
|
|
2137
2181
|
runtimeToken,
|
|
2138
2182
|
coreUrl,
|
|
@@ -2168,7 +2212,7 @@ var FartherShore = class {
|
|
|
2168
2212
|
if (!this.meteringClient && config.metering.enabled) {
|
|
2169
2213
|
this.meteringClient = new MeteringClient({
|
|
2170
2214
|
config: config.metering,
|
|
2171
|
-
|
|
2215
|
+
businessId: config.business.id,
|
|
2172
2216
|
backendId: config.backend.id,
|
|
2173
2217
|
coreUrl: this.coreUrl,
|
|
2174
2218
|
fetchImpl: this.fetchImpl
|
|
@@ -2252,17 +2296,15 @@ var FartherShore = class {
|
|
|
2252
2296
|
const context = await verifyRequest(input, {
|
|
2253
2297
|
jwks: this.jwks,
|
|
2254
2298
|
nonceCache: this.nonceCache,
|
|
2255
|
-
|
|
2299
|
+
businessId: config.business.id,
|
|
2256
2300
|
backendId: config.backend.id,
|
|
2257
2301
|
knownRouteIds,
|
|
2258
2302
|
clockSkewSeconds: config.verification.clockSkewSeconds,
|
|
2259
2303
|
replayWindowSeconds: config.verification.replayWindowSeconds,
|
|
2260
|
-
//
|
|
2261
|
-
//
|
|
2262
|
-
//
|
|
2263
|
-
|
|
2264
|
-
contextSecrets: this.contextSecrets,
|
|
2265
|
-
contextVerification: this.contextVerification
|
|
2304
|
+
// Consumer-principal wave (D3): OPTIONAL defense-in-depth. The principal
|
|
2305
|
+
// is derived from the Ed25519-vouched X-Fs-Context regardless; when these
|
|
2306
|
+
// secrets are set a presented token must ALSO pass HS256.
|
|
2307
|
+
contextSecrets: this.contextSecrets
|
|
2266
2308
|
});
|
|
2267
2309
|
return {
|
|
2268
2310
|
...context,
|
|
@@ -2389,6 +2431,40 @@ function parseContextSecrets(raw) {
|
|
|
2389
2431
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2390
2432
|
}
|
|
2391
2433
|
|
|
2434
|
+
// src/core/permissions.ts
|
|
2435
|
+
var WILDCARD = "*";
|
|
2436
|
+
var FartherShorePermissionError = class extends Error {
|
|
2437
|
+
code = "permission_denied";
|
|
2438
|
+
status = 403;
|
|
2439
|
+
/** The permission key that was required but not held. */
|
|
2440
|
+
requiredPermission;
|
|
2441
|
+
constructor(requiredPermission, message) {
|
|
2442
|
+
super(message ?? `missing required permission: ${requiredPermission}`);
|
|
2443
|
+
this.name = "FartherShorePermissionError";
|
|
2444
|
+
this.requiredPermission = requiredPermission;
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
function permissionSatisfies(required, granted) {
|
|
2448
|
+
if (granted === void 0) return true;
|
|
2449
|
+
if (granted.includes(WILDCARD)) return true;
|
|
2450
|
+
if (granted.includes(required)) return true;
|
|
2451
|
+
const idx = required.indexOf(":");
|
|
2452
|
+
if (idx > 0 && idx < required.length - 1) {
|
|
2453
|
+
const subject = required.slice(0, idx);
|
|
2454
|
+
if (granted.includes(`${subject}:${WILDCARD}`)) return true;
|
|
2455
|
+
}
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
function hasPermission(ctx, key2) {
|
|
2459
|
+
if (ctx.permissions === void 0) return false;
|
|
2460
|
+
return permissionSatisfies(key2, ctx.permissions);
|
|
2461
|
+
}
|
|
2462
|
+
function requirePermission(ctx, key2) {
|
|
2463
|
+
if (!hasPermission(ctx, key2)) {
|
|
2464
|
+
throw new FartherShorePermissionError(key2);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2392
2468
|
// src/adapters/express.ts
|
|
2393
2469
|
var STREAMING_CONTENT_TYPES = new Set(
|
|
2394
2470
|
RUNTIME_BODY_HASH_CONTRACT.streamingExemptContentTypes
|
|
@@ -2400,7 +2476,9 @@ function createExpressMiddleware(fs, options = {}) {
|
|
|
2400
2476
|
}
|
|
2401
2477
|
async function runMiddleware(fs, options, req, res, next) {
|
|
2402
2478
|
try {
|
|
2403
|
-
|
|
2479
|
+
const strict = options.always ?? true;
|
|
2480
|
+
if (!strict && !await fs.verificationRequired()) {
|
|
2481
|
+
stripFartherShoreHeaders(req);
|
|
2404
2482
|
next();
|
|
2405
2483
|
return;
|
|
2406
2484
|
}
|
|
@@ -2417,6 +2495,7 @@ async function runMiddleware(fs, options, req, res, next) {
|
|
|
2417
2495
|
streamingExempt
|
|
2418
2496
|
});
|
|
2419
2497
|
req.fartherShore = ctx;
|
|
2498
|
+
stripFartherShoreHeaders(req);
|
|
2420
2499
|
next();
|
|
2421
2500
|
} catch (error) {
|
|
2422
2501
|
fail(res, error);
|
|
@@ -2429,6 +2508,51 @@ function fail(res, error) {
|
|
|
2429
2508
|
}
|
|
2430
2509
|
res.status(401).json({ error: "bad_signature" });
|
|
2431
2510
|
}
|
|
2511
|
+
function stripFartherShoreHeaders(req) {
|
|
2512
|
+
const headers = req.headers;
|
|
2513
|
+
for (const name of Object.keys(headers)) {
|
|
2514
|
+
if (name.toLowerCase().startsWith("x-fs-")) {
|
|
2515
|
+
delete headers[name];
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
const withRaw = req;
|
|
2519
|
+
const raw = withRaw.rawHeaders;
|
|
2520
|
+
if (Array.isArray(raw)) {
|
|
2521
|
+
const cleaned = [];
|
|
2522
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
2523
|
+
const key2 = raw[i];
|
|
2524
|
+
const value = raw[i + 1];
|
|
2525
|
+
if (typeof key2 !== "string" || value === void 0) continue;
|
|
2526
|
+
if (key2.toLowerCase().startsWith("x-fs-")) continue;
|
|
2527
|
+
cleaned.push(key2, value);
|
|
2528
|
+
}
|
|
2529
|
+
withRaw.rawHeaders = cleaned;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
function createExpressHandler(handler) {
|
|
2533
|
+
return (req, res, next) => {
|
|
2534
|
+
const ctx = req.fartherShore;
|
|
2535
|
+
if (!ctx) {
|
|
2536
|
+
res.status(401).json({ error: "context_unverified" });
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (!ctx.principal) {
|
|
2540
|
+
res.status(401).json({ error: "principal_required" });
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const verified = ctx;
|
|
2544
|
+
void Promise.resolve().then(
|
|
2545
|
+
() => handler(verified, req, res, next)
|
|
2546
|
+
).catch((error) => failHandler(res, next, error));
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function failHandler(res, next, error) {
|
|
2550
|
+
if (error instanceof FartherShoreError || error instanceof FartherShorePermissionError) {
|
|
2551
|
+
res.status(error.status).json({ error: error.code });
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
next(error);
|
|
2555
|
+
}
|
|
2432
2556
|
function splitUrl(req) {
|
|
2433
2557
|
const raw = req.originalUrl ?? req.url ?? req.path ?? "/";
|
|
2434
2558
|
const qIndex = raw.indexOf("?");
|
|
@@ -2623,7 +2747,7 @@ function personaClientFromKeysFile(path = DEFAULT_KEYS_FILE, options = {}) {
|
|
|
2623
2747
|
const file = readDevKeysFile(path);
|
|
2624
2748
|
const client = createPersonaClient({
|
|
2625
2749
|
keys: file.keys,
|
|
2626
|
-
|
|
2750
|
+
businessId: file.businessId,
|
|
2627
2751
|
backendId: file.backendId,
|
|
2628
2752
|
contextSecret: file.keys.contextSecret,
|
|
2629
2753
|
contextKid: file.keys.contextKid,
|
|
@@ -2650,7 +2774,7 @@ function createDevRuntime(options) {
|
|
|
2650
2774
|
const gateway = createDevGateway({
|
|
2651
2775
|
mode,
|
|
2652
2776
|
keys,
|
|
2653
|
-
...options.
|
|
2777
|
+
...options.businessId ? { businessId: options.businessId } : {},
|
|
2654
2778
|
...options.backendId ? { backendId: options.backendId } : {},
|
|
2655
2779
|
...options.routes ? { routeIds: options.routes } : {},
|
|
2656
2780
|
onMeterEvent: (event) => {
|
|
@@ -2679,7 +2803,7 @@ function createDevRuntime(options) {
|
|
|
2679
2803
|
const personas = buildPersonaMap(options.personas);
|
|
2680
2804
|
const personaClient = createPersonaClient({
|
|
2681
2805
|
keys,
|
|
2682
|
-
|
|
2806
|
+
businessId: gateway.businessId,
|
|
2683
2807
|
backendId: gateway.backendId,
|
|
2684
2808
|
contextSecret: keys.contextSecret,
|
|
2685
2809
|
contextKid: keys.contextKid,
|
|
@@ -2691,8 +2815,9 @@ function createDevRuntime(options) {
|
|
|
2691
2815
|
runtimeToken: keys.runtimeToken,
|
|
2692
2816
|
coreUrl: DEV_CORE_URL,
|
|
2693
2817
|
fetchImpl: gateway.fetchImpl,
|
|
2818
|
+
// Consumer-principal wave (D3): signed cv=2 context is fail-closed whenever
|
|
2819
|
+
// contextSecrets are set — no per-mode verification toggle.
|
|
2694
2820
|
contextSecrets: [keys.contextSecret],
|
|
2695
|
-
contextVerification: mode === "simulated" ? "required" : "preferred",
|
|
2696
2821
|
env: {}
|
|
2697
2822
|
});
|
|
2698
2823
|
const tracedAuthz = {
|
|
@@ -2718,7 +2843,8 @@ function createDevRuntime(options) {
|
|
|
2718
2843
|
});
|
|
2719
2844
|
}
|
|
2720
2845
|
function middleware(mwOptions) {
|
|
2721
|
-
const
|
|
2846
|
+
const resolved = mode === "passthrough" ? { always: false, ...mwOptions } : mwOptions ?? {};
|
|
2847
|
+
const inner = createExpressMiddleware(fs, resolved);
|
|
2722
2848
|
return (req, res, next) => {
|
|
2723
2849
|
const requestId = headerValue2(req.headers, "x-fs-request-id") ?? "unknown";
|
|
2724
2850
|
const { path } = splitUrl2(req);
|
|
@@ -2757,6 +2883,7 @@ function createDevRuntime(options) {
|
|
|
2757
2883
|
};
|
|
2758
2884
|
}
|
|
2759
2885
|
fs.middleware = middleware;
|
|
2886
|
+
fs.handler = createExpressHandler;
|
|
2760
2887
|
const devRuntime = {
|
|
2761
2888
|
fs,
|
|
2762
2889
|
asPersona: (name) => personaClient.asPersona(name),
|
|
@@ -2809,7 +2936,7 @@ function createDevRuntimeFromEnv(env = readProcessEnv3()) {
|
|
|
2809
2936
|
version: 1,
|
|
2810
2937
|
mode,
|
|
2811
2938
|
keys,
|
|
2812
|
-
|
|
2939
|
+
businessId: runtime.gateway.businessId,
|
|
2813
2940
|
backendId: runtime.gateway.backendId,
|
|
2814
2941
|
personas: mapToRecord(runtime.personas)
|
|
2815
2942
|
};
|
|
@@ -2822,7 +2949,7 @@ function printBanner(mode, runtime, tracePath) {
|
|
|
2822
2949
|
"============================================================",
|
|
2823
2950
|
" \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
|
|
2824
2951
|
` mode: ${mode.toUpperCase()}`,
|
|
2825
|
-
`
|
|
2952
|
+
` business: ${runtime.gateway.businessId}`,
|
|
2826
2953
|
` backend: ${runtime.gateway.backendId}`,
|
|
2827
2954
|
` personas: ${[...runtime.personas.keys()].join(", ")}`,
|
|
2828
2955
|
` usage log: ${USAGE_JSONL_PATH}`,
|