@farthershore/backend 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/generated/runtime-contract.js +5 -5
- package/dist/index.js +35 -34
- package/dist/testing/index.js +36 -35
- package/dist/types/core/metering.d.ts +8 -2
- package/dist/types/core/runtime.d.ts +3 -3
- package/dist/types/core/verifyRequest.d.ts +3 -3
- package/dist/types/generated/runtime-contract.d.ts +5 -5
- package/dist/types/runtime-types.d.ts +9 -4
- package/dist/types/testing/devGateway.d.ts +3 -3
- package/dist/types/testing/devRuntime.d.ts +1 -1
- 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 +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @farthershore/backend
|
|
2
2
|
|
|
3
|
-
The runtime SDK for your own backend. When you run a software
|
|
3
|
+
The runtime SDK for your own backend. When you run a software business on Farther
|
|
4
4
|
Shore with a bring-your-own-backend, the platform's edge gateway sits in front of
|
|
5
5
|
your service. This package lets your backend **trust the gateway** (verify that
|
|
6
6
|
each request really came from it) and **report usage** back for metering and
|
|
@@ -8,11 +8,11 @@ billing — from a single token, `FS_RUNTIME_TOKEN`.
|
|
|
8
8
|
|
|
9
9
|
Install one package, set one environment variable, and you get fail-closed
|
|
10
10
|
gateway-to-upstream request verification, response-bound usage reporting, and
|
|
11
|
-
graceful lifecycle (health + shutdown). Everything else — your
|
|
11
|
+
graceful lifecycle (health + shutdown). Everything else — your business, backend,
|
|
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.15.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
|
|
|
@@ -76,7 +76,7 @@ process.on("SIGTERM", () => void fs.shutdown());
|
|
|
76
76
|
## What `initFromEnv()` derives
|
|
77
77
|
|
|
78
78
|
You configure exactly one thing: `FS_RUNTIME_TOKEN` (mint it for your backend
|
|
79
|
-
with the Farther Shore CLI or dashboard). Everything else —
|
|
79
|
+
with the Farther Shore CLI or dashboard). Everything else — business / backend /
|
|
80
80
|
environment ids, the JWKS url used to verify signatures, the metering endpoint
|
|
81
81
|
and credential, and verification settings — is fetched from the platform at
|
|
82
82
|
startup and cached in memory. The token is validated eagerly, so a
|
|
@@ -126,11 +126,11 @@ export async function POST(request: Request) {
|
|
|
126
126
|
|
|
127
127
|
- `measureContext` is free-form pricing/analytics context persisted with the
|
|
128
128
|
usage event.
|
|
129
|
-
- `creditUnitsConsumed` is a numeric map for credit-wallet style
|
|
129
|
+
- `creditUnitsConsumed` is a numeric map for credit-wallet style businesses; keys
|
|
130
130
|
and values are validated locally before signing.
|
|
131
131
|
|
|
132
132
|
The meter keys you report (e.g. `tokens_used`) must match meters declared in your
|
|
133
|
-
|
|
133
|
+
business. Request-count style limits are enforced by the gateway and need no
|
|
134
134
|
backend code.
|
|
135
135
|
|
|
136
136
|
## Async / background usage
|
|
@@ -83,7 +83,7 @@ var RUNTIME_SIGNING_CONTRACT = {
|
|
|
83
83
|
"body-hash",
|
|
84
84
|
"request-id",
|
|
85
85
|
"timestamp",
|
|
86
|
-
"
|
|
86
|
+
"business-id",
|
|
87
87
|
"backend-id",
|
|
88
88
|
"route-id",
|
|
89
89
|
"policy-version"
|
|
@@ -95,7 +95,7 @@ var RUNTIME_SIGNING_CONTRACT = {
|
|
|
95
95
|
"body-hash": "Lowercase hex SHA-256 of the RAW request body bytes. For an empty body, the SHA-256 of zero bytes (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Streaming-exempt requests use the literal token 'STREAM'.",
|
|
96
96
|
"request-id": "Opaque unique request id minted by the gateway (also the replay-cache nonce).",
|
|
97
97
|
timestamp: "Integer Unix epoch seconds (UTC) at signing time, as a base-10 string with no padding.",
|
|
98
|
-
"
|
|
98
|
+
"business-id": "Business id the request is routed to.",
|
|
99
99
|
"backend-id": "Backend id the route binds to.",
|
|
100
100
|
"route-id": "Resolved route id; empty string if the route is unresolved.",
|
|
101
101
|
"policy-version": "Tenant artifact / policy version the gateway signed under."
|
|
@@ -109,7 +109,7 @@ var RUNTIME_CANONICAL_FIELDS = [
|
|
|
109
109
|
"body-hash",
|
|
110
110
|
"request-id",
|
|
111
111
|
"timestamp",
|
|
112
|
-
"
|
|
112
|
+
"business-id",
|
|
113
113
|
"backend-id",
|
|
114
114
|
"route-id",
|
|
115
115
|
"policy-version"
|
|
@@ -133,7 +133,7 @@ var RUNTIME_HEADERS = {
|
|
|
133
133
|
keyId: "x-fs-key-id",
|
|
134
134
|
requestId: "x-fs-request-id",
|
|
135
135
|
timestamp: "x-fs-timestamp",
|
|
136
|
-
|
|
136
|
+
businessId: "x-fs-business-id",
|
|
137
137
|
backendId: "x-fs-backend-id",
|
|
138
138
|
routeId: "x-fs-route-id",
|
|
139
139
|
policyVersion: "x-fs-policy-version",
|
|
@@ -169,7 +169,7 @@ var RUNTIME_METERING_CONTRACT = {
|
|
|
169
169
|
credential: "reusable-bearer",
|
|
170
170
|
event: {
|
|
171
171
|
event_id: "string",
|
|
172
|
-
|
|
172
|
+
business_id: "string",
|
|
173
173
|
backend_id: "string",
|
|
174
174
|
route_id: "string?",
|
|
175
175
|
request_id: "string?",
|
package/dist/index.js
CHANGED
|
@@ -306,7 +306,7 @@ var RUNTIME_HEADER_NAMES = {
|
|
|
306
306
|
keyId: "x-fs-key-id",
|
|
307
307
|
requestId: "x-fs-request-id",
|
|
308
308
|
timestamp: "x-fs-timestamp",
|
|
309
|
-
|
|
309
|
+
businessId: "x-fs-business-id",
|
|
310
310
|
backendId: "x-fs-backend-id",
|
|
311
311
|
routeId: "x-fs-route-id",
|
|
312
312
|
policyVersion: "x-fs-policy-version",
|
|
@@ -358,7 +358,7 @@ var CANONICAL_SIGNING_FIELDS = [
|
|
|
358
358
|
"body-hash",
|
|
359
359
|
"request-id",
|
|
360
360
|
"timestamp",
|
|
361
|
-
"
|
|
361
|
+
"business-id",
|
|
362
362
|
"backend-id",
|
|
363
363
|
"route-id",
|
|
364
364
|
"policy-version"
|
|
@@ -395,7 +395,7 @@ function buildCanonicalSigningString(input) {
|
|
|
395
395
|
"body-hash": input.bodyHash,
|
|
396
396
|
"request-id": input.requestId,
|
|
397
397
|
timestamp: String(input.timestamp),
|
|
398
|
-
"
|
|
398
|
+
"business-id": input.businessId,
|
|
399
399
|
"backend-id": input.backendId,
|
|
400
400
|
"route-id": input.routeId,
|
|
401
401
|
"policy-version": input.policyVersion
|
|
@@ -761,7 +761,7 @@ var DEFAULT_MAX_RETRIES = 3;
|
|
|
761
761
|
var MeteringClient = class {
|
|
762
762
|
config;
|
|
763
763
|
endpoint;
|
|
764
|
-
|
|
764
|
+
businessId;
|
|
765
765
|
backendId;
|
|
766
766
|
fetchImpl;
|
|
767
767
|
maxRetries;
|
|
@@ -775,7 +775,7 @@ var MeteringClient = class {
|
|
|
775
775
|
constructor(options) {
|
|
776
776
|
this.config = options.config;
|
|
777
777
|
this.endpoint = resolveEndpoint(options.config.endpoint, options.coreUrl);
|
|
778
|
-
this.
|
|
778
|
+
this.businessId = options.businessId;
|
|
779
779
|
this.backendId = options.backendId;
|
|
780
780
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
781
781
|
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
@@ -838,13 +838,14 @@ var MeteringClient = class {
|
|
|
838
838
|
}
|
|
839
839
|
const event = {
|
|
840
840
|
event_id: options.eventId ?? this.newId(),
|
|
841
|
-
|
|
841
|
+
business_id: this.businessId,
|
|
842
842
|
backend_id: this.backendId,
|
|
843
843
|
meter,
|
|
844
844
|
qty,
|
|
845
845
|
timestamp: options.timestamp ?? this.now().toISOString(),
|
|
846
846
|
...options.routeId ? { route_id: options.routeId } : {},
|
|
847
|
-
...options.requestId ? { request_id: options.requestId } : {}
|
|
847
|
+
...options.requestId ? { request_id: options.requestId } : {},
|
|
848
|
+
...options.subscriptionId ? { subscription_id: options.subscriptionId } : {}
|
|
848
849
|
};
|
|
849
850
|
this.buffer.push(event);
|
|
850
851
|
await this.flush();
|
|
@@ -1705,12 +1706,12 @@ async function verifyRequest(input, deps) {
|
|
|
1705
1706
|
const kid = h(RUNTIME_HEADER_NAMES.keyId);
|
|
1706
1707
|
const requestId = h(RUNTIME_HEADER_NAMES.requestId);
|
|
1707
1708
|
const timestampRaw = h(RUNTIME_HEADER_NAMES.timestamp);
|
|
1708
|
-
const
|
|
1709
|
+
const signedBusinessId = h(RUNTIME_HEADER_NAMES.businessId);
|
|
1709
1710
|
const signedBackendId = h(RUNTIME_HEADER_NAMES.backendId);
|
|
1710
1711
|
const signedRouteId = h(RUNTIME_HEADER_NAMES.routeId) ?? "";
|
|
1711
1712
|
const policyVersion = h(RUNTIME_HEADER_NAMES.policyVersion);
|
|
1712
1713
|
const signedBodyHash = h(RUNTIME_HEADER_NAMES.bodyHash);
|
|
1713
|
-
if (!kid || !requestId || !timestampRaw || !
|
|
1714
|
+
if (!kid || !requestId || !timestampRaw || !signedBusinessId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
|
|
1714
1715
|
throw new FartherShoreError(
|
|
1715
1716
|
"malformed_signature",
|
|
1716
1717
|
"request is missing one or more required x-fs-* headers"
|
|
@@ -1746,10 +1747,10 @@ async function verifyRequest(input, deps) {
|
|
|
1746
1747
|
"recomputed body hash does not match the signed x-fs-body-hash"
|
|
1747
1748
|
);
|
|
1748
1749
|
}
|
|
1749
|
-
if (deps.
|
|
1750
|
+
if (deps.businessId !== void 0 && signedBusinessId !== deps.businessId) {
|
|
1750
1751
|
throw new FartherShoreError(
|
|
1751
1752
|
"route_mismatch",
|
|
1752
|
-
"signed
|
|
1753
|
+
"signed business-id does not match this backend's business"
|
|
1753
1754
|
);
|
|
1754
1755
|
}
|
|
1755
1756
|
if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
@@ -1771,7 +1772,7 @@ async function verifyRequest(input, deps) {
|
|
|
1771
1772
|
bodyHash: computedBodyHash,
|
|
1772
1773
|
requestId,
|
|
1773
1774
|
timestamp,
|
|
1774
|
-
|
|
1775
|
+
businessId: signedBusinessId,
|
|
1775
1776
|
backendId: signedBackendId,
|
|
1776
1777
|
routeId: signedRouteId,
|
|
1777
1778
|
policyVersion
|
|
@@ -1811,10 +1812,10 @@ async function verifyRequest(input, deps) {
|
|
|
1811
1812
|
if (signedContext === null && deps.contextVerification === "required") {
|
|
1812
1813
|
throw contextRequiredError("failed verification");
|
|
1813
1814
|
}
|
|
1814
|
-
if (signedContext && signedContext.productId !==
|
|
1815
|
+
if (signedContext && signedContext.productId !== signedBusinessId) {
|
|
1815
1816
|
throw new FartherShoreError(
|
|
1816
1817
|
"context_unverified",
|
|
1817
|
-
"X-Fs-Context was minted for a different
|
|
1818
|
+
"X-Fs-Context was minted for a different business than the signed request"
|
|
1818
1819
|
);
|
|
1819
1820
|
}
|
|
1820
1821
|
} else if (deps.contextVerification === "required") {
|
|
@@ -1834,7 +1835,7 @@ async function verifyRequest(input, deps) {
|
|
|
1834
1835
|
}
|
|
1835
1836
|
return {
|
|
1836
1837
|
requestId,
|
|
1837
|
-
|
|
1838
|
+
businessId: signedBusinessId,
|
|
1838
1839
|
backendId: signedBackendId,
|
|
1839
1840
|
routeId: signedRouteId,
|
|
1840
1841
|
policyVersion,
|
|
@@ -1877,8 +1878,8 @@ function headerGetter(headers) {
|
|
|
1877
1878
|
|
|
1878
1879
|
// src/core/runtime.ts
|
|
1879
1880
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1880
|
-
var SDK_VERSION = "0.
|
|
1881
|
-
var CONTRACTS_FP = "
|
|
1881
|
+
var SDK_VERSION = "0.15.0".length > 0 ? "0.15.0" : "0.0.0-dev";
|
|
1882
|
+
var CONTRACTS_FP = "4b6a36b4cb1f0b68".length > 0 ? "4b6a36b4cb1f0b68" : "0000000000000000";
|
|
1882
1883
|
var FartherShore = class {
|
|
1883
1884
|
bootstrapClient;
|
|
1884
1885
|
fetchImpl;
|
|
@@ -1947,7 +1948,7 @@ var FartherShore = class {
|
|
|
1947
1948
|
if (!this.meteringClient && config.metering.enabled) {
|
|
1948
1949
|
this.meteringClient = new MeteringClient({
|
|
1949
1950
|
config: config.metering,
|
|
1950
|
-
|
|
1951
|
+
businessId: config.business.id,
|
|
1951
1952
|
backendId: config.backend.id,
|
|
1952
1953
|
coreUrl: this.coreUrl,
|
|
1953
1954
|
fetchImpl: this.fetchImpl
|
|
@@ -2031,7 +2032,7 @@ var FartherShore = class {
|
|
|
2031
2032
|
const context = await verifyRequest(input, {
|
|
2032
2033
|
jwks: this.jwks,
|
|
2033
2034
|
nonceCache: this.nonceCache,
|
|
2034
|
-
|
|
2035
|
+
businessId: config.business.id,
|
|
2035
2036
|
backendId: config.backend.id,
|
|
2036
2037
|
knownRouteIds,
|
|
2037
2038
|
clockSkewSeconds: config.verification.clockSkewSeconds,
|
|
@@ -2261,7 +2262,7 @@ async function makeSignedRequest(spec = {}) {
|
|
|
2261
2262
|
bodyHash,
|
|
2262
2263
|
requestId: spec.requestId ?? `req_${cryptoRandom()}`,
|
|
2263
2264
|
timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
2264
|
-
|
|
2265
|
+
businessId: spec.businessId ?? "biz_test",
|
|
2265
2266
|
backendId: spec.backendId ?? "be_test",
|
|
2266
2267
|
routeId: spec.routeId ?? "route_test",
|
|
2267
2268
|
policyVersion: spec.policyVersion ?? "pv_1"
|
|
@@ -2273,7 +2274,7 @@ async function makeSignedRequest(spec = {}) {
|
|
|
2273
2274
|
[RUNTIME_HEADER_NAMES.keyId]: kid,
|
|
2274
2275
|
[RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
|
|
2275
2276
|
[RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
|
|
2276
|
-
[RUNTIME_HEADER_NAMES.
|
|
2277
|
+
[RUNTIME_HEADER_NAMES.businessId]: claim.businessId,
|
|
2277
2278
|
[RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
|
|
2278
2279
|
[RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
|
|
2279
2280
|
[RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
|
|
@@ -2374,13 +2375,13 @@ function mergeHeaders(initHeaders, signedHeaders) {
|
|
|
2374
2375
|
}
|
|
2375
2376
|
return headers;
|
|
2376
2377
|
}
|
|
2377
|
-
function buildContextClaim(persona,
|
|
2378
|
+
function buildContextClaim(persona, businessId) {
|
|
2378
2379
|
return {
|
|
2379
2380
|
orgId: persona.orgId ?? "org_dev",
|
|
2380
2381
|
actor: persona.actor ?? { type: "user", id: `user_${persona.name}` },
|
|
2381
|
-
//
|
|
2382
|
-
// request
|
|
2383
|
-
productId,
|
|
2382
|
+
// Business binding: the retained signed-context productId claim MUST equal
|
|
2383
|
+
// the signed request businessId or verifyRequest rejects it as tamper evidence.
|
|
2384
|
+
productId: businessId,
|
|
2384
2385
|
compiledPlanId: persona.compiledPlanId ?? "plan_dev",
|
|
2385
2386
|
subscriptionId: persona.subscriptionId ?? "sub_dev",
|
|
2386
2387
|
subscriberId: persona.subscriberId ?? "subscriber_dev",
|
|
@@ -2408,7 +2409,7 @@ function createPersonaClient(ctx) {
|
|
|
2408
2409
|
query: spec.query ?? "",
|
|
2409
2410
|
body: spec.body ?? null,
|
|
2410
2411
|
streamingExempt: spec.streamingExempt ?? false,
|
|
2411
|
-
|
|
2412
|
+
businessId: ctx.businessId,
|
|
2412
2413
|
backendId: ctx.backendId,
|
|
2413
2414
|
routeId: spec.routeId ?? "",
|
|
2414
2415
|
privateJwk: ctx.keys.privateJwk,
|
|
@@ -2418,7 +2419,7 @@ function createPersonaClient(ctx) {
|
|
|
2418
2419
|
});
|
|
2419
2420
|
const headers = { ...signed.headers };
|
|
2420
2421
|
if (!persona.anonymous) {
|
|
2421
|
-
const claim = buildContextClaim(persona, ctx.
|
|
2422
|
+
const claim = buildContextClaim(persona, ctx.businessId);
|
|
2422
2423
|
headers["x-fs-context"] = await signContextToken(
|
|
2423
2424
|
claim,
|
|
2424
2425
|
ctx.contextSecret,
|
|
@@ -2491,12 +2492,12 @@ var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
|
|
|
2491
2492
|
var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
|
|
2492
2493
|
var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
|
|
2493
2494
|
function createDevGateway(options) {
|
|
2494
|
-
const
|
|
2495
|
+
const businessId = options.businessId ?? "biz_dev";
|
|
2495
2496
|
const backendId = options.backendId ?? "be_dev";
|
|
2496
2497
|
const meterEvents = [];
|
|
2497
2498
|
const reportUsageEvents = [];
|
|
2498
2499
|
const bootstrap = {
|
|
2499
|
-
|
|
2500
|
+
business: { id: businessId, slug: options.businessSlug ?? "dev-business" },
|
|
2500
2501
|
backend: {
|
|
2501
2502
|
id: backendId,
|
|
2502
2503
|
slug: options.backendSlug ?? "dev-backend",
|
|
@@ -2566,7 +2567,7 @@ function createDevGateway(options) {
|
|
|
2566
2567
|
bootstrap,
|
|
2567
2568
|
meterEvents,
|
|
2568
2569
|
reportUsageEvents,
|
|
2569
|
-
|
|
2570
|
+
businessId,
|
|
2570
2571
|
backendId,
|
|
2571
2572
|
jwksUrl: DEV_JWKS_URL
|
|
2572
2573
|
};
|
|
@@ -2768,7 +2769,7 @@ function createDevRuntime(options) {
|
|
|
2768
2769
|
const gateway = createDevGateway({
|
|
2769
2770
|
mode,
|
|
2770
2771
|
keys,
|
|
2771
|
-
...options.
|
|
2772
|
+
...options.businessId ? { businessId: options.businessId } : {},
|
|
2772
2773
|
...options.backendId ? { backendId: options.backendId } : {},
|
|
2773
2774
|
...options.routes ? { routeIds: options.routes } : {},
|
|
2774
2775
|
onMeterEvent: (event) => {
|
|
@@ -2797,7 +2798,7 @@ function createDevRuntime(options) {
|
|
|
2797
2798
|
const personas = buildPersonaMap(options.personas);
|
|
2798
2799
|
const personaClient = createPersonaClient({
|
|
2799
2800
|
keys,
|
|
2800
|
-
|
|
2801
|
+
businessId: gateway.businessId,
|
|
2801
2802
|
backendId: gateway.backendId,
|
|
2802
2803
|
contextSecret: keys.contextSecret,
|
|
2803
2804
|
contextKid: keys.contextKid,
|
|
@@ -2927,7 +2928,7 @@ function createDevRuntimeFromEnv(env = readProcessEnv3()) {
|
|
|
2927
2928
|
version: 1,
|
|
2928
2929
|
mode,
|
|
2929
2930
|
keys,
|
|
2930
|
-
|
|
2931
|
+
businessId: runtime.gateway.businessId,
|
|
2931
2932
|
backendId: runtime.gateway.backendId,
|
|
2932
2933
|
personas: mapToRecord(runtime.personas)
|
|
2933
2934
|
};
|
|
@@ -2940,7 +2941,7 @@ function printBanner(mode, runtime, tracePath) {
|
|
|
2940
2941
|
"============================================================",
|
|
2941
2942
|
" \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
|
|
2942
2943
|
` mode: ${mode.toUpperCase()}`,
|
|
2943
|
-
`
|
|
2944
|
+
` business: ${runtime.gateway.businessId}`,
|
|
2944
2945
|
` backend: ${runtime.gateway.backendId}`,
|
|
2945
2946
|
` personas: ${[...runtime.personas.keys()].join(", ")}`,
|
|
2946
2947
|
` usage log: ${USAGE_JSONL_PATH}`,
|
package/dist/testing/index.js
CHANGED
|
@@ -294,7 +294,7 @@ var RUNTIME_HEADER_NAMES = {
|
|
|
294
294
|
keyId: "x-fs-key-id",
|
|
295
295
|
requestId: "x-fs-request-id",
|
|
296
296
|
timestamp: "x-fs-timestamp",
|
|
297
|
-
|
|
297
|
+
businessId: "x-fs-business-id",
|
|
298
298
|
backendId: "x-fs-backend-id",
|
|
299
299
|
routeId: "x-fs-route-id",
|
|
300
300
|
policyVersion: "x-fs-policy-version",
|
|
@@ -346,7 +346,7 @@ var CANONICAL_SIGNING_FIELDS = [
|
|
|
346
346
|
"body-hash",
|
|
347
347
|
"request-id",
|
|
348
348
|
"timestamp",
|
|
349
|
-
"
|
|
349
|
+
"business-id",
|
|
350
350
|
"backend-id",
|
|
351
351
|
"route-id",
|
|
352
352
|
"policy-version"
|
|
@@ -383,7 +383,7 @@ function buildCanonicalSigningString(input) {
|
|
|
383
383
|
"body-hash": input.bodyHash,
|
|
384
384
|
"request-id": input.requestId,
|
|
385
385
|
timestamp: String(input.timestamp),
|
|
386
|
-
"
|
|
386
|
+
"business-id": input.businessId,
|
|
387
387
|
"backend-id": input.backendId,
|
|
388
388
|
"route-id": input.routeId,
|
|
389
389
|
"policy-version": input.policyVersion
|
|
@@ -608,7 +608,7 @@ async function makeSignedRequest(spec = {}) {
|
|
|
608
608
|
bodyHash,
|
|
609
609
|
requestId: spec.requestId ?? `req_${cryptoRandom()}`,
|
|
610
610
|
timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
611
|
-
|
|
611
|
+
businessId: spec.businessId ?? "biz_test",
|
|
612
612
|
backendId: spec.backendId ?? "be_test",
|
|
613
613
|
routeId: spec.routeId ?? "route_test",
|
|
614
614
|
policyVersion: spec.policyVersion ?? "pv_1"
|
|
@@ -620,7 +620,7 @@ async function makeSignedRequest(spec = {}) {
|
|
|
620
620
|
[RUNTIME_HEADER_NAMES.keyId]: kid,
|
|
621
621
|
[RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
|
|
622
622
|
[RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
|
|
623
|
-
[RUNTIME_HEADER_NAMES.
|
|
623
|
+
[RUNTIME_HEADER_NAMES.businessId]: claim.businessId,
|
|
624
624
|
[RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
|
|
625
625
|
[RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
|
|
626
626
|
[RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
|
|
@@ -742,13 +742,13 @@ function mergeHeaders(initHeaders, signedHeaders) {
|
|
|
742
742
|
}
|
|
743
743
|
return headers;
|
|
744
744
|
}
|
|
745
|
-
function buildContextClaim(persona,
|
|
745
|
+
function buildContextClaim(persona, businessId) {
|
|
746
746
|
return {
|
|
747
747
|
orgId: persona.orgId ?? "org_dev",
|
|
748
748
|
actor: persona.actor ?? { type: "user", id: `user_${persona.name}` },
|
|
749
|
-
//
|
|
750
|
-
// request
|
|
751
|
-
productId,
|
|
749
|
+
// Business binding: the retained signed-context productId claim MUST equal
|
|
750
|
+
// the signed request businessId or verifyRequest rejects it as tamper evidence.
|
|
751
|
+
productId: businessId,
|
|
752
752
|
compiledPlanId: persona.compiledPlanId ?? "plan_dev",
|
|
753
753
|
subscriptionId: persona.subscriptionId ?? "sub_dev",
|
|
754
754
|
subscriberId: persona.subscriberId ?? "subscriber_dev",
|
|
@@ -776,7 +776,7 @@ function createPersonaClient(ctx) {
|
|
|
776
776
|
query: spec.query ?? "",
|
|
777
777
|
body: spec.body ?? null,
|
|
778
778
|
streamingExempt: spec.streamingExempt ?? false,
|
|
779
|
-
|
|
779
|
+
businessId: ctx.businessId,
|
|
780
780
|
backendId: ctx.backendId,
|
|
781
781
|
routeId: spec.routeId ?? "",
|
|
782
782
|
privateJwk: ctx.keys.privateJwk,
|
|
@@ -786,7 +786,7 @@ function createPersonaClient(ctx) {
|
|
|
786
786
|
});
|
|
787
787
|
const headers = { ...signed.headers };
|
|
788
788
|
if (!persona.anonymous) {
|
|
789
|
-
const claim = buildContextClaim(persona, ctx.
|
|
789
|
+
const claim = buildContextClaim(persona, ctx.businessId);
|
|
790
790
|
headers["x-fs-context"] = await signContextToken(
|
|
791
791
|
claim,
|
|
792
792
|
ctx.contextSecret,
|
|
@@ -861,12 +861,12 @@ var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
|
|
|
861
861
|
var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
|
|
862
862
|
var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
|
|
863
863
|
function createDevGateway(options) {
|
|
864
|
-
const
|
|
864
|
+
const businessId = options.businessId ?? "biz_dev";
|
|
865
865
|
const backendId = options.backendId ?? "be_dev";
|
|
866
866
|
const meterEvents = [];
|
|
867
867
|
const reportUsageEvents = [];
|
|
868
868
|
const bootstrap = {
|
|
869
|
-
|
|
869
|
+
business: { id: businessId, slug: options.businessSlug ?? "dev-business" },
|
|
870
870
|
backend: {
|
|
871
871
|
id: backendId,
|
|
872
872
|
slug: options.backendSlug ?? "dev-backend",
|
|
@@ -936,7 +936,7 @@ function createDevGateway(options) {
|
|
|
936
936
|
bootstrap,
|
|
937
937
|
meterEvents,
|
|
938
938
|
reportUsageEvents,
|
|
939
|
-
|
|
939
|
+
businessId,
|
|
940
940
|
backendId,
|
|
941
941
|
jwksUrl: DEV_JWKS_URL
|
|
942
942
|
};
|
|
@@ -1136,7 +1136,7 @@ var DEFAULT_MAX_RETRIES = 3;
|
|
|
1136
1136
|
var MeteringClient = class {
|
|
1137
1137
|
config;
|
|
1138
1138
|
endpoint;
|
|
1139
|
-
|
|
1139
|
+
businessId;
|
|
1140
1140
|
backendId;
|
|
1141
1141
|
fetchImpl;
|
|
1142
1142
|
maxRetries;
|
|
@@ -1150,7 +1150,7 @@ var MeteringClient = class {
|
|
|
1150
1150
|
constructor(options) {
|
|
1151
1151
|
this.config = options.config;
|
|
1152
1152
|
this.endpoint = resolveEndpoint(options.config.endpoint, options.coreUrl);
|
|
1153
|
-
this.
|
|
1153
|
+
this.businessId = options.businessId;
|
|
1154
1154
|
this.backendId = options.backendId;
|
|
1155
1155
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1156
1156
|
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
@@ -1213,13 +1213,14 @@ var MeteringClient = class {
|
|
|
1213
1213
|
}
|
|
1214
1214
|
const event = {
|
|
1215
1215
|
event_id: options.eventId ?? this.newId(),
|
|
1216
|
-
|
|
1216
|
+
business_id: this.businessId,
|
|
1217
1217
|
backend_id: this.backendId,
|
|
1218
1218
|
meter,
|
|
1219
1219
|
qty,
|
|
1220
1220
|
timestamp: options.timestamp ?? this.now().toISOString(),
|
|
1221
1221
|
...options.routeId ? { route_id: options.routeId } : {},
|
|
1222
|
-
...options.requestId ? { request_id: options.requestId } : {}
|
|
1222
|
+
...options.requestId ? { request_id: options.requestId } : {},
|
|
1223
|
+
...options.subscriptionId ? { subscription_id: options.subscriptionId } : {}
|
|
1223
1224
|
};
|
|
1224
1225
|
this.buffer.push(event);
|
|
1225
1226
|
await this.flush();
|
|
@@ -1926,12 +1927,12 @@ async function verifyRequest(input, deps) {
|
|
|
1926
1927
|
const kid = h(RUNTIME_HEADER_NAMES.keyId);
|
|
1927
1928
|
const requestId = h(RUNTIME_HEADER_NAMES.requestId);
|
|
1928
1929
|
const timestampRaw = h(RUNTIME_HEADER_NAMES.timestamp);
|
|
1929
|
-
const
|
|
1930
|
+
const signedBusinessId = h(RUNTIME_HEADER_NAMES.businessId);
|
|
1930
1931
|
const signedBackendId = h(RUNTIME_HEADER_NAMES.backendId);
|
|
1931
1932
|
const signedRouteId = h(RUNTIME_HEADER_NAMES.routeId) ?? "";
|
|
1932
1933
|
const policyVersion = h(RUNTIME_HEADER_NAMES.policyVersion);
|
|
1933
1934
|
const signedBodyHash = h(RUNTIME_HEADER_NAMES.bodyHash);
|
|
1934
|
-
if (!kid || !requestId || !timestampRaw || !
|
|
1935
|
+
if (!kid || !requestId || !timestampRaw || !signedBusinessId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
|
|
1935
1936
|
throw new FartherShoreError(
|
|
1936
1937
|
"malformed_signature",
|
|
1937
1938
|
"request is missing one or more required x-fs-* headers"
|
|
@@ -1967,10 +1968,10 @@ async function verifyRequest(input, deps) {
|
|
|
1967
1968
|
"recomputed body hash does not match the signed x-fs-body-hash"
|
|
1968
1969
|
);
|
|
1969
1970
|
}
|
|
1970
|
-
if (deps.
|
|
1971
|
+
if (deps.businessId !== void 0 && signedBusinessId !== deps.businessId) {
|
|
1971
1972
|
throw new FartherShoreError(
|
|
1972
1973
|
"route_mismatch",
|
|
1973
|
-
"signed
|
|
1974
|
+
"signed business-id does not match this backend's business"
|
|
1974
1975
|
);
|
|
1975
1976
|
}
|
|
1976
1977
|
if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
@@ -1992,7 +1993,7 @@ async function verifyRequest(input, deps) {
|
|
|
1992
1993
|
bodyHash: computedBodyHash,
|
|
1993
1994
|
requestId,
|
|
1994
1995
|
timestamp,
|
|
1995
|
-
|
|
1996
|
+
businessId: signedBusinessId,
|
|
1996
1997
|
backendId: signedBackendId,
|
|
1997
1998
|
routeId: signedRouteId,
|
|
1998
1999
|
policyVersion
|
|
@@ -2032,10 +2033,10 @@ async function verifyRequest(input, deps) {
|
|
|
2032
2033
|
if (signedContext === null && deps.contextVerification === "required") {
|
|
2033
2034
|
throw contextRequiredError("failed verification");
|
|
2034
2035
|
}
|
|
2035
|
-
if (signedContext && signedContext.productId !==
|
|
2036
|
+
if (signedContext && signedContext.productId !== signedBusinessId) {
|
|
2036
2037
|
throw new FartherShoreError(
|
|
2037
2038
|
"context_unverified",
|
|
2038
|
-
"X-Fs-Context was minted for a different
|
|
2039
|
+
"X-Fs-Context was minted for a different business than the signed request"
|
|
2039
2040
|
);
|
|
2040
2041
|
}
|
|
2041
2042
|
} else if (deps.contextVerification === "required") {
|
|
@@ -2055,7 +2056,7 @@ async function verifyRequest(input, deps) {
|
|
|
2055
2056
|
}
|
|
2056
2057
|
return {
|
|
2057
2058
|
requestId,
|
|
2058
|
-
|
|
2059
|
+
businessId: signedBusinessId,
|
|
2059
2060
|
backendId: signedBackendId,
|
|
2060
2061
|
routeId: signedRouteId,
|
|
2061
2062
|
policyVersion,
|
|
@@ -2098,8 +2099,8 @@ function headerGetter(headers) {
|
|
|
2098
2099
|
|
|
2099
2100
|
// src/core/runtime.ts
|
|
2100
2101
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
2101
|
-
var SDK_VERSION = "0.
|
|
2102
|
-
var CONTRACTS_FP = "
|
|
2102
|
+
var SDK_VERSION = "0.15.0".length > 0 ? "0.15.0" : "0.0.0-dev";
|
|
2103
|
+
var CONTRACTS_FP = "4b6a36b4cb1f0b68".length > 0 ? "4b6a36b4cb1f0b68" : "0000000000000000";
|
|
2103
2104
|
var FartherShore = class {
|
|
2104
2105
|
bootstrapClient;
|
|
2105
2106
|
fetchImpl;
|
|
@@ -2168,7 +2169,7 @@ var FartherShore = class {
|
|
|
2168
2169
|
if (!this.meteringClient && config.metering.enabled) {
|
|
2169
2170
|
this.meteringClient = new MeteringClient({
|
|
2170
2171
|
config: config.metering,
|
|
2171
|
-
|
|
2172
|
+
businessId: config.business.id,
|
|
2172
2173
|
backendId: config.backend.id,
|
|
2173
2174
|
coreUrl: this.coreUrl,
|
|
2174
2175
|
fetchImpl: this.fetchImpl
|
|
@@ -2252,7 +2253,7 @@ var FartherShore = class {
|
|
|
2252
2253
|
const context = await verifyRequest(input, {
|
|
2253
2254
|
jwks: this.jwks,
|
|
2254
2255
|
nonceCache: this.nonceCache,
|
|
2255
|
-
|
|
2256
|
+
businessId: config.business.id,
|
|
2256
2257
|
backendId: config.backend.id,
|
|
2257
2258
|
knownRouteIds,
|
|
2258
2259
|
clockSkewSeconds: config.verification.clockSkewSeconds,
|
|
@@ -2623,7 +2624,7 @@ function personaClientFromKeysFile(path = DEFAULT_KEYS_FILE, options = {}) {
|
|
|
2623
2624
|
const file = readDevKeysFile(path);
|
|
2624
2625
|
const client = createPersonaClient({
|
|
2625
2626
|
keys: file.keys,
|
|
2626
|
-
|
|
2627
|
+
businessId: file.businessId,
|
|
2627
2628
|
backendId: file.backendId,
|
|
2628
2629
|
contextSecret: file.keys.contextSecret,
|
|
2629
2630
|
contextKid: file.keys.contextKid,
|
|
@@ -2650,7 +2651,7 @@ function createDevRuntime(options) {
|
|
|
2650
2651
|
const gateway = createDevGateway({
|
|
2651
2652
|
mode,
|
|
2652
2653
|
keys,
|
|
2653
|
-
...options.
|
|
2654
|
+
...options.businessId ? { businessId: options.businessId } : {},
|
|
2654
2655
|
...options.backendId ? { backendId: options.backendId } : {},
|
|
2655
2656
|
...options.routes ? { routeIds: options.routes } : {},
|
|
2656
2657
|
onMeterEvent: (event) => {
|
|
@@ -2679,7 +2680,7 @@ function createDevRuntime(options) {
|
|
|
2679
2680
|
const personas = buildPersonaMap(options.personas);
|
|
2680
2681
|
const personaClient = createPersonaClient({
|
|
2681
2682
|
keys,
|
|
2682
|
-
|
|
2683
|
+
businessId: gateway.businessId,
|
|
2683
2684
|
backendId: gateway.backendId,
|
|
2684
2685
|
contextSecret: keys.contextSecret,
|
|
2685
2686
|
contextKid: keys.contextKid,
|
|
@@ -2809,7 +2810,7 @@ function createDevRuntimeFromEnv(env = readProcessEnv3()) {
|
|
|
2809
2810
|
version: 1,
|
|
2810
2811
|
mode,
|
|
2811
2812
|
keys,
|
|
2812
|
-
|
|
2813
|
+
businessId: runtime.gateway.businessId,
|
|
2813
2814
|
backendId: runtime.gateway.backendId,
|
|
2814
2815
|
personas: mapToRecord(runtime.personas)
|
|
2815
2816
|
};
|
|
@@ -2822,7 +2823,7 @@ function printBanner(mode, runtime, tracePath) {
|
|
|
2822
2823
|
"============================================================",
|
|
2823
2824
|
" \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
|
|
2824
2825
|
` mode: ${mode.toUpperCase()}`,
|
|
2825
|
-
`
|
|
2826
|
+
` business: ${runtime.gateway.businessId}`,
|
|
2826
2827
|
` backend: ${runtime.gateway.backendId}`,
|
|
2827
2828
|
` personas: ${[...runtime.personas.keys()].join(", ")}`,
|
|
2828
2829
|
` usage log: ${USAGE_JSONL_PATH}`,
|
|
@@ -2,6 +2,12 @@ import type { RuntimeMeteringConfig } from "../runtime-types.js";
|
|
|
2
2
|
export type MeterOptions = {
|
|
3
3
|
requestId?: string;
|
|
4
4
|
routeId?: string;
|
|
5
|
+
/** Subscription to attribute the usage to (billing identity). Pass the
|
|
6
|
+
* verified request context's `signedContext.subscriptionId` when metering
|
|
7
|
+
* inside a request handler. Without it (and without `requestId`, which core
|
|
8
|
+
* can resolve back to the served gateway request), core persists the event
|
|
9
|
+
* UNBILLED and flags it unattributable. */
|
|
10
|
+
subscriptionId?: string;
|
|
5
11
|
/** Override event_id (idempotency key). Defaults to a random uuid. */
|
|
6
12
|
eventId?: string;
|
|
7
13
|
/** Override the timestamp (ISO-8601). Defaults to now. */
|
|
@@ -9,7 +15,7 @@ export type MeterOptions = {
|
|
|
9
15
|
};
|
|
10
16
|
export type MeteringClientOptions = {
|
|
11
17
|
config: RuntimeMeteringConfig;
|
|
12
|
-
|
|
18
|
+
businessId: string;
|
|
13
19
|
backendId: string;
|
|
14
20
|
/** Core base URL when the config endpoint is a relative path. */
|
|
15
21
|
coreUrl?: string;
|
|
@@ -36,7 +42,7 @@ export type MeteringClientOptions = {
|
|
|
36
42
|
export declare class MeteringClient {
|
|
37
43
|
private readonly config;
|
|
38
44
|
private readonly endpoint;
|
|
39
|
-
private readonly
|
|
45
|
+
private readonly businessId;
|
|
40
46
|
private readonly backendId;
|
|
41
47
|
private readonly fetchImpl;
|
|
42
48
|
private readonly maxRetries;
|
|
@@ -45,7 +45,7 @@ export type FartherShoreInitOptions = {
|
|
|
45
45
|
* `X-Fs-Context` claim. These are the GATEWAY CONTEXT-SIGNING keyring values
|
|
46
46
|
* (`CONTEXT_SIGNING_KEYS_JSON` — the keys `forward-upstream` stamps the
|
|
47
47
|
* header with; supply every live key during rotation — try-all). They are
|
|
48
|
-
* NOT the
|
|
48
|
+
* NOT the business's `contextTokenSecret`, which signs `fsc_` INGRESS tokens
|
|
49
49
|
* verified BY the gateway — setting that here would reject every valid
|
|
50
50
|
* gateway request in `"required"` mode. When present, a VERIFIED context's
|
|
51
51
|
* `permissions`/`roles` claims are the AUTHORITATIVE identity source
|
|
@@ -54,8 +54,8 @@ export type FartherShoreInitOptions = {
|
|
|
54
54
|
*
|
|
55
55
|
* NOTE (core-side dependency, FAR-723 publish gate): no bootstrap field
|
|
56
56
|
* carries these keys yet, and handing the raw platform keyring to builder
|
|
57
|
-
* backends is NOT the end-state (it would allow cross-
|
|
58
|
-
* forgery). The distribution mechanism — per-
|
|
57
|
+
* backends is NOT the end-state (it would allow cross-business context
|
|
58
|
+
* forgery). The distribution mechanism — per-business derived keys or an
|
|
59
59
|
* asymmetric context signature verified via JWKS like the request
|
|
60
60
|
* signature — is decided at the FAR-723 gate before the headers retire;
|
|
61
61
|
* until then this option (or `FS_CONTEXT_SECRETS`) is the manual wiring for
|
|
@@ -23,7 +23,7 @@ export type HeadersLike = Headers | Record<string, string | string[] | undefined
|
|
|
23
23
|
/** The verified request context attached to req.fartherShore. */
|
|
24
24
|
export type FartherShoreRequestContext = {
|
|
25
25
|
requestId: string;
|
|
26
|
-
|
|
26
|
+
businessId: string;
|
|
27
27
|
backendId: string;
|
|
28
28
|
routeId: string;
|
|
29
29
|
policyVersion: string;
|
|
@@ -55,8 +55,8 @@ export type FartherShoreRequestContext = {
|
|
|
55
55
|
export type VerifyRequestDeps = {
|
|
56
56
|
jwks: JwksClient;
|
|
57
57
|
nonceCache: NonceCache;
|
|
58
|
-
/** Expected
|
|
59
|
-
|
|
58
|
+
/** Expected business id (from bootstrap). When set, must match the signed claim. */
|
|
59
|
+
businessId?: string;
|
|
60
60
|
/** Expected backend id (from bootstrap). When set, must match. */
|
|
61
61
|
backendId?: string;
|
|
62
62
|
/**
|
|
@@ -73,7 +73,7 @@ export declare const RUNTIME_SIGNING_CONTRACT: {
|
|
|
73
73
|
readonly keyValueSeparator: ":";
|
|
74
74
|
readonly trailingNewline: false;
|
|
75
75
|
readonly fieldEncoding: "utf-8";
|
|
76
|
-
readonly fields: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "
|
|
76
|
+
readonly fields: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version"];
|
|
77
77
|
readonly fieldRules: {
|
|
78
78
|
readonly method: "Uppercased HTTP method (e.g. GET, POST).";
|
|
79
79
|
readonly path: "Request path, percent-encoded as received, no host, no query string. Always begins with '/'.";
|
|
@@ -81,7 +81,7 @@ export declare const RUNTIME_SIGNING_CONTRACT: {
|
|
|
81
81
|
readonly "body-hash": "Lowercase hex SHA-256 of the RAW request body bytes. For an empty body, the SHA-256 of zero bytes (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Streaming-exempt requests use the literal token 'STREAM'.";
|
|
82
82
|
readonly "request-id": "Opaque unique request id minted by the gateway (also the replay-cache nonce).";
|
|
83
83
|
readonly timestamp: "Integer Unix epoch seconds (UTC) at signing time, as a base-10 string with no padding.";
|
|
84
|
-
readonly "
|
|
84
|
+
readonly "business-id": "Business id the request is routed to.";
|
|
85
85
|
readonly "backend-id": "Backend id the route binds to.";
|
|
86
86
|
readonly "route-id": "Resolved route id; empty string if the route is unresolved.";
|
|
87
87
|
readonly "policy-version": "Tenant artifact / policy version the gateway signed under.";
|
|
@@ -92,7 +92,7 @@ export declare const RUNTIME_SIGNING_CONTRACT: {
|
|
|
92
92
|
* Ordered list of fields in the canonical signing string. The order here is
|
|
93
93
|
* load-bearing and identical across all language SDKs.
|
|
94
94
|
*/
|
|
95
|
-
export declare const RUNTIME_CANONICAL_FIELDS: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "
|
|
95
|
+
export declare const RUNTIME_CANONICAL_FIELDS: readonly ["method", "path", "query", "body-hash", "request-id", "timestamp", "business-id", "backend-id", "route-id", "policy-version"];
|
|
96
96
|
export declare const RUNTIME_BODY_HASH_CONTRACT: {
|
|
97
97
|
readonly algorithm: "SHA-256";
|
|
98
98
|
readonly encoding: "hex-lower";
|
|
@@ -108,7 +108,7 @@ export declare const RUNTIME_HEADERS: {
|
|
|
108
108
|
readonly keyId: "x-fs-key-id";
|
|
109
109
|
readonly requestId: "x-fs-request-id";
|
|
110
110
|
readonly timestamp: "x-fs-timestamp";
|
|
111
|
-
readonly
|
|
111
|
+
readonly businessId: "x-fs-business-id";
|
|
112
112
|
readonly backendId: "x-fs-backend-id";
|
|
113
113
|
readonly routeId: "x-fs-route-id";
|
|
114
114
|
readonly policyVersion: "x-fs-policy-version";
|
|
@@ -145,7 +145,7 @@ export declare const RUNTIME_METERING_CONTRACT: {
|
|
|
145
145
|
readonly credential: "reusable-bearer";
|
|
146
146
|
readonly event: {
|
|
147
147
|
readonly event_id: "string";
|
|
148
|
-
readonly
|
|
148
|
+
readonly business_id: "string";
|
|
149
149
|
readonly backend_id: "string";
|
|
150
150
|
readonly route_id: "string?";
|
|
151
151
|
readonly request_id: "string?";
|
|
@@ -126,7 +126,7 @@ export declare const RUNTIME_HEADER_NAMES: {
|
|
|
126
126
|
readonly keyId: "x-fs-key-id";
|
|
127
127
|
readonly requestId: "x-fs-request-id";
|
|
128
128
|
readonly timestamp: "x-fs-timestamp";
|
|
129
|
-
readonly
|
|
129
|
+
readonly businessId: "x-fs-business-id";
|
|
130
130
|
readonly backendId: "x-fs-backend-id";
|
|
131
131
|
readonly routeId: "x-fs-route-id";
|
|
132
132
|
readonly policyVersion: "x-fs-policy-version";
|
|
@@ -166,7 +166,7 @@ export type CanonicalSigningInput = {
|
|
|
166
166
|
requestId: string;
|
|
167
167
|
/** Unix epoch seconds at signing time. */
|
|
168
168
|
timestamp: number;
|
|
169
|
-
|
|
169
|
+
businessId: string;
|
|
170
170
|
backendId: string;
|
|
171
171
|
/** Resolved route id; "" when unresolved. */
|
|
172
172
|
routeId: string;
|
|
@@ -221,7 +221,7 @@ export type RuntimeLockDescriptor = {
|
|
|
221
221
|
lockVersion: number;
|
|
222
222
|
};
|
|
223
223
|
export type RuntimeBootstrapResponse = {
|
|
224
|
-
|
|
224
|
+
business: {
|
|
225
225
|
id: string;
|
|
226
226
|
slug: string;
|
|
227
227
|
};
|
|
@@ -247,10 +247,15 @@ export type RuntimeBootstrapResponse = {
|
|
|
247
247
|
};
|
|
248
248
|
export type RuntimeMeteringEvent = {
|
|
249
249
|
event_id: string;
|
|
250
|
-
|
|
250
|
+
business_id: string;
|
|
251
251
|
backend_id: string;
|
|
252
252
|
route_id?: string;
|
|
253
253
|
request_id?: string;
|
|
254
|
+
/** Subscription the usage belongs to (billing attribution). Optional and
|
|
255
|
+
* additive: legacy emitters omit it; core then falls back to resolving the
|
|
256
|
+
* served gateway request via `request_id`, and rows that resolve neither
|
|
257
|
+
* way are persisted unbilled + flagged unattributable. */
|
|
258
|
+
subscription_id?: string;
|
|
254
259
|
meter: string;
|
|
255
260
|
qty: number;
|
|
256
261
|
timestamp: string;
|
|
@@ -7,9 +7,9 @@ export declare const DEV_METERING_ENDPOINT = "https://dev-gateway.farthershore.l
|
|
|
7
7
|
export type DevGatewayOptions = {
|
|
8
8
|
mode: DevMode;
|
|
9
9
|
keys: DevSignerKeys;
|
|
10
|
-
|
|
10
|
+
businessId?: string;
|
|
11
11
|
backendId?: string;
|
|
12
|
-
|
|
12
|
+
businessSlug?: string;
|
|
13
13
|
backendSlug?: string;
|
|
14
14
|
/** Extra route ids to expose in bootstrap for route-binding tests. */
|
|
15
15
|
routeIds?: string[];
|
|
@@ -27,7 +27,7 @@ export type DevGateway = {
|
|
|
27
27
|
meterEvents: RuntimeMeteringEvent[];
|
|
28
28
|
/** Every captured attested post-stream usage report. */
|
|
29
29
|
reportUsageEvents: RuntimePostStreamUsageEvent[];
|
|
30
|
-
|
|
30
|
+
businessId: string;
|
|
31
31
|
backendId: string;
|
|
32
32
|
jwksUrl: string;
|
|
33
33
|
};
|
|
@@ -24,7 +24,7 @@ export type CreateDevRuntimeOptions = {
|
|
|
24
24
|
routes?: string[];
|
|
25
25
|
/** Meter keys (informational — the dev gateway accepts any meter). */
|
|
26
26
|
meters?: string[];
|
|
27
|
-
|
|
27
|
+
businessId?: string;
|
|
28
28
|
backendId?: string;
|
|
29
29
|
/** Optional app transport for persona `.fetch()`; defaults to global fetch. */
|
|
30
30
|
appFetch?: typeof fetch;
|
|
@@ -53,7 +53,7 @@ export type PersonaRequestSpec = {
|
|
|
53
53
|
/** The context a persona client signs against (bootstrap ids + dev keys). */
|
|
54
54
|
export type PersonaClientContext = {
|
|
55
55
|
keys: DevSignerKeys;
|
|
56
|
-
|
|
56
|
+
businessId: string;
|
|
57
57
|
backendId: string;
|
|
58
58
|
contextSecret: string;
|
|
59
59
|
contextKid: string;
|
|
@@ -96,7 +96,7 @@ export declare const SIGNED_HEADER_NAMES: {
|
|
|
96
96
|
readonly keyId: "x-fs-key-id";
|
|
97
97
|
readonly requestId: "x-fs-request-id";
|
|
98
98
|
readonly timestamp: "x-fs-timestamp";
|
|
99
|
-
readonly
|
|
99
|
+
readonly businessId: "x-fs-business-id";
|
|
100
100
|
readonly backendId: "x-fs-backend-id";
|
|
101
101
|
readonly routeId: "x-fs-route-id";
|
|
102
102
|
readonly policyVersion: "x-fs-policy-version";
|
|
@@ -13,7 +13,7 @@ export type SignedHeaderOverrides = Partial<{
|
|
|
13
13
|
kid: string;
|
|
14
14
|
requestId: string;
|
|
15
15
|
timestamp: number;
|
|
16
|
-
|
|
16
|
+
businessId: string;
|
|
17
17
|
backendId: string;
|
|
18
18
|
routeId: string;
|
|
19
19
|
policyVersion: string;
|
|
@@ -25,7 +25,7 @@ export type SignedRequestSpec = {
|
|
|
25
25
|
query?: string;
|
|
26
26
|
body?: Uint8Array | null;
|
|
27
27
|
streamingExempt?: boolean;
|
|
28
|
-
|
|
28
|
+
businessId?: string;
|
|
29
29
|
backendId?: string;
|
|
30
30
|
routeId?: string;
|
|
31
31
|
policyVersion?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farthershore/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
39
|
"@farthershore/cloudflared-linux-x64": "0.0.0",
|
|
40
|
-
"@farthershore/cloudflared-linux-arm64": "0.0.0",
|
|
41
40
|
"@farthershore/cloudflared-darwin-arm64": "0.0.0",
|
|
41
|
+
"@farthershore/cloudflared-linux-arm64": "0.0.0",
|
|
42
42
|
"@farthershore/cloudflared-darwin-x64": "0.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|