@farthershore/backend 0.13.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 +47 -7
- package/dist/generated/runtime-contract.js +20 -5
- package/dist/index.js +388 -218
- package/dist/testing/index.js +240 -49
- package/dist/types/core/metering.d.ts +8 -2
- package/dist/types/core/post-stream-usage.d.ts +45 -0
- package/dist/types/core/runtime.d.ts +7 -3
- package/dist/types/core/verifyRequest.d.ts +6 -3
- package/dist/types/generated/runtime-contract.d.ts +20 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/runtime-types.d.ts +19 -4
- package/dist/types/testing/devGateway.d.ts +8 -4
- 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/dist/types/testing/usageSink.d.ts +9 -1
- package/package.json +1 -1
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();
|
|
@@ -899,6 +900,300 @@ function resolveEndpoint(endpoint, coreUrl) {
|
|
|
899
900
|
return `${coreUrl.replace(/\/+$/, "")}${endpoint}`;
|
|
900
901
|
}
|
|
901
902
|
|
|
903
|
+
// src/response-metering.ts
|
|
904
|
+
var RESPONSE_METERING_HEADERS = RUNTIME_RESPONSE_METERING_CONTRACT.headers;
|
|
905
|
+
var RESPONSE_METERING_ERROR_CODES = RUNTIME_RESPONSE_METERING_CONTRACT.errors;
|
|
906
|
+
var devMeteringHooks = null;
|
|
907
|
+
function __setDevMeteringHooks(hooks) {
|
|
908
|
+
devMeteringHooks = hooks;
|
|
909
|
+
}
|
|
910
|
+
var METERING_PAYLOAD_HEADER = RESPONSE_METERING_HEADERS.payload;
|
|
911
|
+
var METERING_SIGNATURE_HEADER = RESPONSE_METERING_HEADERS.signature;
|
|
912
|
+
var METERING_TOKEN_HEADER = RESPONSE_METERING_HEADERS.token;
|
|
913
|
+
var DEFAULT_TOKEN_ENV = RUNTIME_RESPONSE_METERING_CONTRACT.token.environmentVariable;
|
|
914
|
+
var MeteringError = class extends Error {
|
|
915
|
+
code;
|
|
916
|
+
constructor(code, message) {
|
|
917
|
+
super(message);
|
|
918
|
+
this.name = "MeteringError";
|
|
919
|
+
this.code = code;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
function createUsage(request, options = {}) {
|
|
923
|
+
const usage = {};
|
|
924
|
+
const reporter = {
|
|
925
|
+
report(meter, value) {
|
|
926
|
+
usage[assertMeterKey(meter)] = assertMeterValue(meter, value);
|
|
927
|
+
return reporter;
|
|
928
|
+
},
|
|
929
|
+
async wrap(response, wrapOptions = {}) {
|
|
930
|
+
return signResponse(request, response, usage, options, wrapOptions);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
return reporter;
|
|
934
|
+
}
|
|
935
|
+
async function withUsage(request, response, usage, options = {}) {
|
|
936
|
+
const reporter = createUsage(request, options);
|
|
937
|
+
for (const [meter, value] of Object.entries(usage)) {
|
|
938
|
+
reporter.report(meter, value);
|
|
939
|
+
}
|
|
940
|
+
return reporter.wrap(response);
|
|
941
|
+
}
|
|
942
|
+
async function signResponse(request, response, usage, options, wrapOptions) {
|
|
943
|
+
const payload = buildPayload(request, usage, options, wrapOptions);
|
|
944
|
+
const requestId = request.headers.get("x-fs-request-id") ?? void 0;
|
|
945
|
+
const headers = await computeMeteringHeaders(payload, {
|
|
946
|
+
...options.token !== void 0 ? { token: options.token } : {},
|
|
947
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
948
|
+
...requestId ? { requestId } : {},
|
|
949
|
+
onSkip: () => {
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
if (Object.keys(headers).length === 0) {
|
|
953
|
+
throw new MeteringError(
|
|
954
|
+
RESPONSE_METERING_ERROR_CODES.missingToken,
|
|
955
|
+
`${DEFAULT_TOKEN_ENV} is required to sign Farther Shore metering reports`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
const merged = new Headers(response.headers);
|
|
959
|
+
for (const [name, value] of Object.entries(headers)) merged.set(name, value);
|
|
960
|
+
return new Response(response.body, {
|
|
961
|
+
status: response.status,
|
|
962
|
+
statusText: response.statusText,
|
|
963
|
+
headers: merged
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
async function computeMeteringHeaders(payload, options = {}) {
|
|
967
|
+
try {
|
|
968
|
+
const token = resolveTokenSoft(options);
|
|
969
|
+
if (!token) {
|
|
970
|
+
skip(`${DEFAULT_TOKEN_ENV} is not set`, options);
|
|
971
|
+
return {};
|
|
972
|
+
}
|
|
973
|
+
const json2 = JSON.stringify(payload);
|
|
974
|
+
const signature = await signPayload(json2, token);
|
|
975
|
+
devMeteringHooks?.record?.(payload, options.requestId);
|
|
976
|
+
return {
|
|
977
|
+
[METERING_PAYLOAD_HEADER]: json2,
|
|
978
|
+
[METERING_SIGNATURE_HEADER]: signature,
|
|
979
|
+
[METERING_TOKEN_HEADER]: token
|
|
980
|
+
};
|
|
981
|
+
} catch (error) {
|
|
982
|
+
skip(error instanceof Error ? error.message : String(error), options);
|
|
983
|
+
return {};
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function skip(reason, options) {
|
|
987
|
+
if (options.onSkip) {
|
|
988
|
+
options.onSkip(reason);
|
|
989
|
+
} else {
|
|
990
|
+
console.warn(`metering headers skipped: ${reason}`);
|
|
991
|
+
}
|
|
992
|
+
devMeteringHooks?.onSkip?.(reason, options.requestId);
|
|
993
|
+
}
|
|
994
|
+
function resolveTokenSoft(options) {
|
|
995
|
+
return options.token ?? options.env?.[DEFAULT_TOKEN_ENV] ?? processEnv(DEFAULT_TOKEN_ENV) ?? devMeteringHooks?.fallbackToken?.();
|
|
996
|
+
}
|
|
997
|
+
function buildPayload(request, usage, options, wrapOptions) {
|
|
998
|
+
const url = new URL(request.url);
|
|
999
|
+
const measureContext = wrapOptions.measureContext ?? options.measureContext;
|
|
1000
|
+
const creditUnitsConsumed = wrapOptions.creditUnitsConsumed ?? options.creditUnitsConsumed;
|
|
1001
|
+
const operationKey = wrapOptions.operationKey ?? options.operationKey;
|
|
1002
|
+
const usagePolicyId = wrapOptions.usagePolicyId ?? options.usagePolicyId;
|
|
1003
|
+
const payload = {
|
|
1004
|
+
method: request.method.toUpperCase(),
|
|
1005
|
+
path: url.pathname,
|
|
1006
|
+
rawDimsUnits: sortUsage(usage),
|
|
1007
|
+
...measureContext ? { measureContext } : {},
|
|
1008
|
+
...creditUnitsConsumed ? {
|
|
1009
|
+
creditUnitsConsumed: sortUsage(
|
|
1010
|
+
validateUsageMap(creditUnitsConsumed, "creditUnitsConsumed")
|
|
1011
|
+
)
|
|
1012
|
+
} : {},
|
|
1013
|
+
...operationKey ? { operationKey: assertIdentifier(operationKey) } : {},
|
|
1014
|
+
...usagePolicyId ? { usagePolicyId: assertIdentifier(usagePolicyId) } : {}
|
|
1015
|
+
};
|
|
1016
|
+
return payload;
|
|
1017
|
+
}
|
|
1018
|
+
function sortUsage(usage) {
|
|
1019
|
+
return Object.fromEntries(
|
|
1020
|
+
Object.entries(usage).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
function validateUsageMap(usage, label) {
|
|
1024
|
+
return Object.fromEntries(
|
|
1025
|
+
Object.entries(usage).map(([meter, value]) => [
|
|
1026
|
+
assertMeterKey(meter),
|
|
1027
|
+
assertMeterValue(`${label}.${meter}`, value)
|
|
1028
|
+
])
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
function assertMeterKey(meter) {
|
|
1032
|
+
if (!/^[a-z0-9_]{1,64}$/.test(meter)) {
|
|
1033
|
+
throw new MeteringError(
|
|
1034
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1035
|
+
`meter key "${meter}" must be lowercase alphanumeric with underscores`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
return meter;
|
|
1039
|
+
}
|
|
1040
|
+
function assertMeterValue(meter, value) {
|
|
1041
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1042
|
+
throw new MeteringError(
|
|
1043
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterValue,
|
|
1044
|
+
`meter "${meter}" value must be a non-negative finite number`
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
return value;
|
|
1048
|
+
}
|
|
1049
|
+
function assertIdentifier(value) {
|
|
1050
|
+
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(value)) {
|
|
1051
|
+
throw new MeteringError(
|
|
1052
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1053
|
+
`operation and usage policy identifiers must be 1-128 URL-safe characters`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
return value;
|
|
1057
|
+
}
|
|
1058
|
+
function processEnv(key2) {
|
|
1059
|
+
const maybeProcess = globalThis.process;
|
|
1060
|
+
return maybeProcess?.env?.[key2];
|
|
1061
|
+
}
|
|
1062
|
+
async function signPayload(payload, token) {
|
|
1063
|
+
const key2 = await crypto.subtle.importKey(
|
|
1064
|
+
"raw",
|
|
1065
|
+
new TextEncoder().encode(token),
|
|
1066
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1067
|
+
false,
|
|
1068
|
+
["sign"]
|
|
1069
|
+
);
|
|
1070
|
+
const signature = await crypto.subtle.sign(
|
|
1071
|
+
"HMAC",
|
|
1072
|
+
key2,
|
|
1073
|
+
new TextEncoder().encode(payload)
|
|
1074
|
+
);
|
|
1075
|
+
return base64url(new Uint8Array(signature));
|
|
1076
|
+
}
|
|
1077
|
+
function base64url(bytes) {
|
|
1078
|
+
let binary = "";
|
|
1079
|
+
for (const byte of bytes) {
|
|
1080
|
+
binary += String.fromCharCode(byte);
|
|
1081
|
+
}
|
|
1082
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/core/post-stream-usage.ts
|
|
1086
|
+
var METER_KEY_RE2 = /^[a-z0-9_]{1,64}$/;
|
|
1087
|
+
var PostStreamUsageClient = class {
|
|
1088
|
+
config;
|
|
1089
|
+
endpoint;
|
|
1090
|
+
fetchImpl;
|
|
1091
|
+
newNonce;
|
|
1092
|
+
logger;
|
|
1093
|
+
sleep;
|
|
1094
|
+
retryDelaysMs;
|
|
1095
|
+
constructor(options) {
|
|
1096
|
+
this.config = options.config;
|
|
1097
|
+
this.endpoint = resolveEndpoint2(options.config.endpoint, options.coreUrl);
|
|
1098
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1099
|
+
this.newNonce = options.newNonce ?? (() => crypto.randomUUID());
|
|
1100
|
+
this.logger = options.logger ?? ((message) => console.warn(message));
|
|
1101
|
+
this.sleep = options.sleep ?? sleep;
|
|
1102
|
+
this.retryDelaysMs = options.retryDelaysMs ?? [100, 250, 500];
|
|
1103
|
+
}
|
|
1104
|
+
async reportUsage(input) {
|
|
1105
|
+
try {
|
|
1106
|
+
if (!this.config.enabled) throw new Error("metering is not enabled");
|
|
1107
|
+
if (!input.requestId) throw new Error("requestId is required");
|
|
1108
|
+
if (!input.subscriptionId) throw new Error("subscriptionId is required");
|
|
1109
|
+
const unsigned = {
|
|
1110
|
+
requestId: input.requestId,
|
|
1111
|
+
subscriptionId: input.subscriptionId,
|
|
1112
|
+
nonce: this.newNonce(),
|
|
1113
|
+
meters: validateAndSortUsage(input.meters, "meters", this.config, true),
|
|
1114
|
+
...input.creditUnitsConsumed ? {
|
|
1115
|
+
creditUnitsConsumed: validateAndSortUsage(
|
|
1116
|
+
input.creditUnitsConsumed,
|
|
1117
|
+
"creditUnitsConsumed",
|
|
1118
|
+
this.config,
|
|
1119
|
+
false
|
|
1120
|
+
)
|
|
1121
|
+
} : {},
|
|
1122
|
+
...input.measureContext ? { measureContext: input.measureContext } : {}
|
|
1123
|
+
};
|
|
1124
|
+
const signature = await signPayload(
|
|
1125
|
+
JSON.stringify(unsigned),
|
|
1126
|
+
this.config.credential
|
|
1127
|
+
);
|
|
1128
|
+
const event = { ...unsigned, signature };
|
|
1129
|
+
const body = JSON.stringify(event);
|
|
1130
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1131
|
+
const response = await this.fetchImpl(this.endpoint, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
headers: {
|
|
1134
|
+
authorization: `Bearer ${this.config.credential}`,
|
|
1135
|
+
"content-type": "application/json",
|
|
1136
|
+
accept: "application/json"
|
|
1137
|
+
},
|
|
1138
|
+
body
|
|
1139
|
+
});
|
|
1140
|
+
if (response.ok) return { ok: true };
|
|
1141
|
+
const requestNotFound = await isPostStreamRequestNotFound(response);
|
|
1142
|
+
const delayMs = this.retryDelaysMs[attempt];
|
|
1143
|
+
if (!requestNotFound || delayMs === void 0) {
|
|
1144
|
+
throw new Error(`metering endpoint returned ${response.status}`);
|
|
1145
|
+
}
|
|
1146
|
+
await this.sleep(delayMs);
|
|
1147
|
+
}
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1150
|
+
this.logger(`post-stream usage report skipped: ${reason}`);
|
|
1151
|
+
return { ok: false, reason };
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
async function isPostStreamRequestNotFound(response) {
|
|
1156
|
+
if (response.status !== 422) return false;
|
|
1157
|
+
try {
|
|
1158
|
+
const body = await response.json();
|
|
1159
|
+
return body.error?.code === "post_stream_request_not_found";
|
|
1160
|
+
} catch {
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function sleep(delayMs) {
|
|
1165
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1166
|
+
}
|
|
1167
|
+
function validateAndSortUsage(usage, label, config, enforceMeterScope) {
|
|
1168
|
+
const entries = Object.entries(usage).sort(
|
|
1169
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
1170
|
+
);
|
|
1171
|
+
for (const [meter, qty] of entries) {
|
|
1172
|
+
if (!METER_KEY_RE2.test(meter)) {
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`${label} key '${meter}' must be lowercase alphanumeric with underscores`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
if (!Number.isFinite(qty) || qty < 0) {
|
|
1178
|
+
throw new Error(`${label}.${meter} must be a non-negative finite number`);
|
|
1179
|
+
}
|
|
1180
|
+
if (enforceMeterScope && config.allowedMeters.length > 0 && !config.allowedMeters.includes(meter)) {
|
|
1181
|
+
throw new Error(`meter '${meter}' is not in the token's allowedMeters`);
|
|
1182
|
+
}
|
|
1183
|
+
if (enforceMeterScope && config.perEventMax > 0 && qty > config.perEventMax) {
|
|
1184
|
+
throw new Error(
|
|
1185
|
+
`meter '${meter}' qty ${qty} exceeds the per-event max ${config.perEventMax}`
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return Object.fromEntries(entries);
|
|
1190
|
+
}
|
|
1191
|
+
function resolveEndpoint2(endpoint, coreUrl) {
|
|
1192
|
+
if (/^https?:\/\//.test(endpoint)) return endpoint;
|
|
1193
|
+
if (!coreUrl) return endpoint;
|
|
1194
|
+
return `${coreUrl.replace(/\/+$/, "")}${endpoint}`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
902
1197
|
// src/core/nonceCache.ts
|
|
903
1198
|
var DEFAULT_MAX_ENTRIES = 1e5;
|
|
904
1199
|
var DEFAULT_TTL_MS = 6e5;
|
|
@@ -1411,12 +1706,12 @@ async function verifyRequest(input, deps) {
|
|
|
1411
1706
|
const kid = h(RUNTIME_HEADER_NAMES.keyId);
|
|
1412
1707
|
const requestId = h(RUNTIME_HEADER_NAMES.requestId);
|
|
1413
1708
|
const timestampRaw = h(RUNTIME_HEADER_NAMES.timestamp);
|
|
1414
|
-
const
|
|
1709
|
+
const signedBusinessId = h(RUNTIME_HEADER_NAMES.businessId);
|
|
1415
1710
|
const signedBackendId = h(RUNTIME_HEADER_NAMES.backendId);
|
|
1416
1711
|
const signedRouteId = h(RUNTIME_HEADER_NAMES.routeId) ?? "";
|
|
1417
1712
|
const policyVersion = h(RUNTIME_HEADER_NAMES.policyVersion);
|
|
1418
1713
|
const signedBodyHash = h(RUNTIME_HEADER_NAMES.bodyHash);
|
|
1419
|
-
if (!kid || !requestId || !timestampRaw || !
|
|
1714
|
+
if (!kid || !requestId || !timestampRaw || !signedBusinessId || !signedBackendId || policyVersion === void 0 || !signedBodyHash) {
|
|
1420
1715
|
throw new FartherShoreError(
|
|
1421
1716
|
"malformed_signature",
|
|
1422
1717
|
"request is missing one or more required x-fs-* headers"
|
|
@@ -1452,10 +1747,10 @@ async function verifyRequest(input, deps) {
|
|
|
1452
1747
|
"recomputed body hash does not match the signed x-fs-body-hash"
|
|
1453
1748
|
);
|
|
1454
1749
|
}
|
|
1455
|
-
if (deps.
|
|
1750
|
+
if (deps.businessId !== void 0 && signedBusinessId !== deps.businessId) {
|
|
1456
1751
|
throw new FartherShoreError(
|
|
1457
1752
|
"route_mismatch",
|
|
1458
|
-
"signed
|
|
1753
|
+
"signed business-id does not match this backend's business"
|
|
1459
1754
|
);
|
|
1460
1755
|
}
|
|
1461
1756
|
if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
@@ -1477,7 +1772,7 @@ async function verifyRequest(input, deps) {
|
|
|
1477
1772
|
bodyHash: computedBodyHash,
|
|
1478
1773
|
requestId,
|
|
1479
1774
|
timestamp,
|
|
1480
|
-
|
|
1775
|
+
businessId: signedBusinessId,
|
|
1481
1776
|
backendId: signedBackendId,
|
|
1482
1777
|
routeId: signedRouteId,
|
|
1483
1778
|
policyVersion
|
|
@@ -1517,10 +1812,10 @@ async function verifyRequest(input, deps) {
|
|
|
1517
1812
|
if (signedContext === null && deps.contextVerification === "required") {
|
|
1518
1813
|
throw contextRequiredError("failed verification");
|
|
1519
1814
|
}
|
|
1520
|
-
if (signedContext && signedContext.productId !==
|
|
1815
|
+
if (signedContext && signedContext.productId !== signedBusinessId) {
|
|
1521
1816
|
throw new FartherShoreError(
|
|
1522
1817
|
"context_unverified",
|
|
1523
|
-
"X-Fs-Context was minted for a different
|
|
1818
|
+
"X-Fs-Context was minted for a different business than the signed request"
|
|
1524
1819
|
);
|
|
1525
1820
|
}
|
|
1526
1821
|
} else if (deps.contextVerification === "required") {
|
|
@@ -1540,7 +1835,7 @@ async function verifyRequest(input, deps) {
|
|
|
1540
1835
|
}
|
|
1541
1836
|
return {
|
|
1542
1837
|
requestId,
|
|
1543
|
-
|
|
1838
|
+
businessId: signedBusinessId,
|
|
1544
1839
|
backendId: signedBackendId,
|
|
1545
1840
|
routeId: signedRouteId,
|
|
1546
1841
|
policyVersion,
|
|
@@ -1583,8 +1878,8 @@ function headerGetter(headers) {
|
|
|
1583
1878
|
|
|
1584
1879
|
// src/core/runtime.ts
|
|
1585
1880
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1586
|
-
var SDK_VERSION = "0.
|
|
1587
|
-
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";
|
|
1588
1883
|
var FartherShore = class {
|
|
1589
1884
|
bootstrapClient;
|
|
1590
1885
|
fetchImpl;
|
|
@@ -1602,6 +1897,7 @@ var FartherShore = class {
|
|
|
1602
1897
|
shutdownManager = new ShutdownManager();
|
|
1603
1898
|
jwks = null;
|
|
1604
1899
|
meteringClient = null;
|
|
1900
|
+
postStreamUsageClient = null;
|
|
1605
1901
|
tunnel = null;
|
|
1606
1902
|
bootstrapped = false;
|
|
1607
1903
|
constructor(options = {}) {
|
|
@@ -1652,11 +1948,16 @@ var FartherShore = class {
|
|
|
1652
1948
|
if (!this.meteringClient && config.metering.enabled) {
|
|
1653
1949
|
this.meteringClient = new MeteringClient({
|
|
1654
1950
|
config: config.metering,
|
|
1655
|
-
|
|
1951
|
+
businessId: config.business.id,
|
|
1656
1952
|
backendId: config.backend.id,
|
|
1657
1953
|
coreUrl: this.coreUrl,
|
|
1658
1954
|
fetchImpl: this.fetchImpl
|
|
1659
1955
|
});
|
|
1956
|
+
this.postStreamUsageClient = new PostStreamUsageClient({
|
|
1957
|
+
config: config.metering,
|
|
1958
|
+
coreUrl: this.coreUrl,
|
|
1959
|
+
fetchImpl: this.fetchImpl
|
|
1960
|
+
});
|
|
1660
1961
|
}
|
|
1661
1962
|
this.bootstrapped = true;
|
|
1662
1963
|
return config;
|
|
@@ -1728,10 +2029,10 @@ var FartherShore = class {
|
|
|
1728
2029
|
);
|
|
1729
2030
|
}
|
|
1730
2031
|
const knownRouteIds = new Set(config.routes.map((r) => r.id));
|
|
1731
|
-
|
|
2032
|
+
const context = await verifyRequest(input, {
|
|
1732
2033
|
jwks: this.jwks,
|
|
1733
2034
|
nonceCache: this.nonceCache,
|
|
1734
|
-
|
|
2035
|
+
businessId: config.business.id,
|
|
1735
2036
|
backendId: config.backend.id,
|
|
1736
2037
|
knownRouteIds,
|
|
1737
2038
|
clockSkewSeconds: config.verification.clockSkewSeconds,
|
|
@@ -1743,6 +2044,23 @@ var FartherShore = class {
|
|
|
1743
2044
|
contextSecrets: this.contextSecrets,
|
|
1744
2045
|
contextVerification: this.contextVerification
|
|
1745
2046
|
});
|
|
2047
|
+
return {
|
|
2048
|
+
...context,
|
|
2049
|
+
reportUsage: (report) => {
|
|
2050
|
+
const subscriptionId = report.subscriptionId ?? context.signedContext?.subscriptionId;
|
|
2051
|
+
if (!subscriptionId) {
|
|
2052
|
+
return Promise.resolve({
|
|
2053
|
+
ok: false,
|
|
2054
|
+
reason: "subscriptionId is required"
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
return this.reportUsage({
|
|
2058
|
+
...report,
|
|
2059
|
+
requestId: report.requestId ?? context.requestId,
|
|
2060
|
+
subscriptionId
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
1746
2064
|
}
|
|
1747
2065
|
/** Whether verification is required (bootstrap × opt-out). */
|
|
1748
2066
|
async verificationRequired() {
|
|
@@ -1803,6 +2121,20 @@ var FartherShore = class {
|
|
|
1803
2121
|
}
|
|
1804
2122
|
await this.meteringClient.meter(meter, qty, options);
|
|
1805
2123
|
}
|
|
2124
|
+
/** Best-effort attested post-stream usage callback. Never rejects. */
|
|
2125
|
+
async reportUsage(input) {
|
|
2126
|
+
try {
|
|
2127
|
+
await this.ensureBootstrapped();
|
|
2128
|
+
if (!this.meteringEnabledOverride || !this.postStreamUsageClient) {
|
|
2129
|
+
return { ok: false, reason: "metering is not enabled" };
|
|
2130
|
+
}
|
|
2131
|
+
return await this.postStreamUsageClient.reportUsage(input);
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2134
|
+
console.warn(`post-stream usage report skipped: ${reason}`);
|
|
2135
|
+
return { ok: false, reason };
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
1806
2138
|
/** Current local health report. */
|
|
1807
2139
|
health() {
|
|
1808
2140
|
const config = this.bootstrapClient.peek();
|
|
@@ -1903,188 +2235,6 @@ function headerValue(headers, name) {
|
|
|
1903
2235
|
return value;
|
|
1904
2236
|
}
|
|
1905
2237
|
|
|
1906
|
-
// src/response-metering.ts
|
|
1907
|
-
var RESPONSE_METERING_HEADERS = RUNTIME_RESPONSE_METERING_CONTRACT.headers;
|
|
1908
|
-
var RESPONSE_METERING_ERROR_CODES = RUNTIME_RESPONSE_METERING_CONTRACT.errors;
|
|
1909
|
-
var devMeteringHooks = null;
|
|
1910
|
-
function __setDevMeteringHooks(hooks) {
|
|
1911
|
-
devMeteringHooks = hooks;
|
|
1912
|
-
}
|
|
1913
|
-
var METERING_PAYLOAD_HEADER = RESPONSE_METERING_HEADERS.payload;
|
|
1914
|
-
var METERING_SIGNATURE_HEADER = RESPONSE_METERING_HEADERS.signature;
|
|
1915
|
-
var METERING_TOKEN_HEADER = RESPONSE_METERING_HEADERS.token;
|
|
1916
|
-
var DEFAULT_TOKEN_ENV = RUNTIME_RESPONSE_METERING_CONTRACT.token.environmentVariable;
|
|
1917
|
-
var MeteringError = class extends Error {
|
|
1918
|
-
code;
|
|
1919
|
-
constructor(code, message) {
|
|
1920
|
-
super(message);
|
|
1921
|
-
this.name = "MeteringError";
|
|
1922
|
-
this.code = code;
|
|
1923
|
-
}
|
|
1924
|
-
};
|
|
1925
|
-
function createUsage(request, options = {}) {
|
|
1926
|
-
const usage = {};
|
|
1927
|
-
const reporter = {
|
|
1928
|
-
report(meter, value) {
|
|
1929
|
-
usage[assertMeterKey(meter)] = assertMeterValue(meter, value);
|
|
1930
|
-
return reporter;
|
|
1931
|
-
},
|
|
1932
|
-
async wrap(response, wrapOptions = {}) {
|
|
1933
|
-
return signResponse(request, response, usage, options, wrapOptions);
|
|
1934
|
-
}
|
|
1935
|
-
};
|
|
1936
|
-
return reporter;
|
|
1937
|
-
}
|
|
1938
|
-
async function withUsage(request, response, usage, options = {}) {
|
|
1939
|
-
const reporter = createUsage(request, options);
|
|
1940
|
-
for (const [meter, value] of Object.entries(usage)) {
|
|
1941
|
-
reporter.report(meter, value);
|
|
1942
|
-
}
|
|
1943
|
-
return reporter.wrap(response);
|
|
1944
|
-
}
|
|
1945
|
-
async function signResponse(request, response, usage, options, wrapOptions) {
|
|
1946
|
-
const payload = buildPayload(request, usage, options, wrapOptions);
|
|
1947
|
-
const requestId = request.headers.get("x-fs-request-id") ?? void 0;
|
|
1948
|
-
const headers = await computeMeteringHeaders(payload, {
|
|
1949
|
-
...options.token !== void 0 ? { token: options.token } : {},
|
|
1950
|
-
...options.env !== void 0 ? { env: options.env } : {},
|
|
1951
|
-
...requestId ? { requestId } : {},
|
|
1952
|
-
onSkip: () => {
|
|
1953
|
-
}
|
|
1954
|
-
});
|
|
1955
|
-
if (Object.keys(headers).length === 0) {
|
|
1956
|
-
throw new MeteringError(
|
|
1957
|
-
RESPONSE_METERING_ERROR_CODES.missingToken,
|
|
1958
|
-
`${DEFAULT_TOKEN_ENV} is required to sign Farther Shore metering reports`
|
|
1959
|
-
);
|
|
1960
|
-
}
|
|
1961
|
-
const merged = new Headers(response.headers);
|
|
1962
|
-
for (const [name, value] of Object.entries(headers)) merged.set(name, value);
|
|
1963
|
-
return new Response(response.body, {
|
|
1964
|
-
status: response.status,
|
|
1965
|
-
statusText: response.statusText,
|
|
1966
|
-
headers: merged
|
|
1967
|
-
});
|
|
1968
|
-
}
|
|
1969
|
-
async function computeMeteringHeaders(payload, options = {}) {
|
|
1970
|
-
try {
|
|
1971
|
-
const token = resolveTokenSoft(options);
|
|
1972
|
-
if (!token) {
|
|
1973
|
-
skip(`${DEFAULT_TOKEN_ENV} is not set`, options);
|
|
1974
|
-
return {};
|
|
1975
|
-
}
|
|
1976
|
-
const json2 = JSON.stringify(payload);
|
|
1977
|
-
const signature = await signPayload(json2, token);
|
|
1978
|
-
devMeteringHooks?.record?.(payload, options.requestId);
|
|
1979
|
-
return {
|
|
1980
|
-
[METERING_PAYLOAD_HEADER]: json2,
|
|
1981
|
-
[METERING_SIGNATURE_HEADER]: signature,
|
|
1982
|
-
[METERING_TOKEN_HEADER]: token
|
|
1983
|
-
};
|
|
1984
|
-
} catch (error) {
|
|
1985
|
-
skip(error instanceof Error ? error.message : String(error), options);
|
|
1986
|
-
return {};
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
function skip(reason, options) {
|
|
1990
|
-
if (options.onSkip) {
|
|
1991
|
-
options.onSkip(reason);
|
|
1992
|
-
} else {
|
|
1993
|
-
console.warn(`metering headers skipped: ${reason}`);
|
|
1994
|
-
}
|
|
1995
|
-
devMeteringHooks?.onSkip?.(reason, options.requestId);
|
|
1996
|
-
}
|
|
1997
|
-
function resolveTokenSoft(options) {
|
|
1998
|
-
return options.token ?? options.env?.[DEFAULT_TOKEN_ENV] ?? processEnv(DEFAULT_TOKEN_ENV) ?? devMeteringHooks?.fallbackToken?.();
|
|
1999
|
-
}
|
|
2000
|
-
function buildPayload(request, usage, options, wrapOptions) {
|
|
2001
|
-
const url = new URL(request.url);
|
|
2002
|
-
const measureContext = wrapOptions.measureContext ?? options.measureContext;
|
|
2003
|
-
const creditUnitsConsumed = wrapOptions.creditUnitsConsumed ?? options.creditUnitsConsumed;
|
|
2004
|
-
const operationKey = wrapOptions.operationKey ?? options.operationKey;
|
|
2005
|
-
const usagePolicyId = wrapOptions.usagePolicyId ?? options.usagePolicyId;
|
|
2006
|
-
const payload = {
|
|
2007
|
-
method: request.method.toUpperCase(),
|
|
2008
|
-
path: url.pathname,
|
|
2009
|
-
rawDimsUnits: sortUsage(usage),
|
|
2010
|
-
...measureContext ? { measureContext } : {},
|
|
2011
|
-
...creditUnitsConsumed ? {
|
|
2012
|
-
creditUnitsConsumed: sortUsage(
|
|
2013
|
-
validateUsageMap(creditUnitsConsumed, "creditUnitsConsumed")
|
|
2014
|
-
)
|
|
2015
|
-
} : {},
|
|
2016
|
-
...operationKey ? { operationKey: assertIdentifier(operationKey) } : {},
|
|
2017
|
-
...usagePolicyId ? { usagePolicyId: assertIdentifier(usagePolicyId) } : {}
|
|
2018
|
-
};
|
|
2019
|
-
return payload;
|
|
2020
|
-
}
|
|
2021
|
-
function sortUsage(usage) {
|
|
2022
|
-
return Object.fromEntries(
|
|
2023
|
-
Object.entries(usage).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
2024
|
-
);
|
|
2025
|
-
}
|
|
2026
|
-
function validateUsageMap(usage, label) {
|
|
2027
|
-
return Object.fromEntries(
|
|
2028
|
-
Object.entries(usage).map(([meter, value]) => [
|
|
2029
|
-
assertMeterKey(meter),
|
|
2030
|
-
assertMeterValue(`${label}.${meter}`, value)
|
|
2031
|
-
])
|
|
2032
|
-
);
|
|
2033
|
-
}
|
|
2034
|
-
function assertMeterKey(meter) {
|
|
2035
|
-
if (!/^[a-z0-9_]{1,64}$/.test(meter)) {
|
|
2036
|
-
throw new MeteringError(
|
|
2037
|
-
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
2038
|
-
`meter key "${meter}" must be lowercase alphanumeric with underscores`
|
|
2039
|
-
);
|
|
2040
|
-
}
|
|
2041
|
-
return meter;
|
|
2042
|
-
}
|
|
2043
|
-
function assertMeterValue(meter, value) {
|
|
2044
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2045
|
-
throw new MeteringError(
|
|
2046
|
-
RESPONSE_METERING_ERROR_CODES.invalidMeterValue,
|
|
2047
|
-
`meter "${meter}" value must be a non-negative finite number`
|
|
2048
|
-
);
|
|
2049
|
-
}
|
|
2050
|
-
return value;
|
|
2051
|
-
}
|
|
2052
|
-
function assertIdentifier(value) {
|
|
2053
|
-
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(value)) {
|
|
2054
|
-
throw new MeteringError(
|
|
2055
|
-
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
2056
|
-
`operation and usage policy identifiers must be 1-128 URL-safe characters`
|
|
2057
|
-
);
|
|
2058
|
-
}
|
|
2059
|
-
return value;
|
|
2060
|
-
}
|
|
2061
|
-
function processEnv(key2) {
|
|
2062
|
-
const maybeProcess = globalThis.process;
|
|
2063
|
-
return maybeProcess?.env?.[key2];
|
|
2064
|
-
}
|
|
2065
|
-
async function signPayload(payload, token) {
|
|
2066
|
-
const key2 = await crypto.subtle.importKey(
|
|
2067
|
-
"raw",
|
|
2068
|
-
new TextEncoder().encode(token),
|
|
2069
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
2070
|
-
false,
|
|
2071
|
-
["sign"]
|
|
2072
|
-
);
|
|
2073
|
-
const signature = await crypto.subtle.sign(
|
|
2074
|
-
"HMAC",
|
|
2075
|
-
key2,
|
|
2076
|
-
new TextEncoder().encode(payload)
|
|
2077
|
-
);
|
|
2078
|
-
return base64url(new Uint8Array(signature));
|
|
2079
|
-
}
|
|
2080
|
-
function base64url(bytes) {
|
|
2081
|
-
let binary = "";
|
|
2082
|
-
for (const byte of bytes) {
|
|
2083
|
-
binary += String.fromCharCode(byte);
|
|
2084
|
-
}
|
|
2085
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
2238
|
// src/testing/signers.ts
|
|
2089
2239
|
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
2090
2240
|
var TEST_KID = "fs-runtime-test-2026";
|
|
@@ -2112,7 +2262,7 @@ async function makeSignedRequest(spec = {}) {
|
|
|
2112
2262
|
bodyHash,
|
|
2113
2263
|
requestId: spec.requestId ?? `req_${cryptoRandom()}`,
|
|
2114
2264
|
timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
2115
|
-
|
|
2265
|
+
businessId: spec.businessId ?? "biz_test",
|
|
2116
2266
|
backendId: spec.backendId ?? "be_test",
|
|
2117
2267
|
routeId: spec.routeId ?? "route_test",
|
|
2118
2268
|
policyVersion: spec.policyVersion ?? "pv_1"
|
|
@@ -2124,7 +2274,7 @@ async function makeSignedRequest(spec = {}) {
|
|
|
2124
2274
|
[RUNTIME_HEADER_NAMES.keyId]: kid,
|
|
2125
2275
|
[RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
|
|
2126
2276
|
[RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
|
|
2127
|
-
[RUNTIME_HEADER_NAMES.
|
|
2277
|
+
[RUNTIME_HEADER_NAMES.businessId]: claim.businessId,
|
|
2128
2278
|
[RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
|
|
2129
2279
|
[RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
|
|
2130
2280
|
[RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
|
|
@@ -2225,13 +2375,13 @@ function mergeHeaders(initHeaders, signedHeaders) {
|
|
|
2225
2375
|
}
|
|
2226
2376
|
return headers;
|
|
2227
2377
|
}
|
|
2228
|
-
function buildContextClaim(persona,
|
|
2378
|
+
function buildContextClaim(persona, businessId) {
|
|
2229
2379
|
return {
|
|
2230
2380
|
orgId: persona.orgId ?? "org_dev",
|
|
2231
2381
|
actor: persona.actor ?? { type: "user", id: `user_${persona.name}` },
|
|
2232
|
-
//
|
|
2233
|
-
// request
|
|
2234
|
-
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,
|
|
2235
2385
|
compiledPlanId: persona.compiledPlanId ?? "plan_dev",
|
|
2236
2386
|
subscriptionId: persona.subscriptionId ?? "sub_dev",
|
|
2237
2387
|
subscriberId: persona.subscriberId ?? "subscriber_dev",
|
|
@@ -2259,7 +2409,7 @@ function createPersonaClient(ctx) {
|
|
|
2259
2409
|
query: spec.query ?? "",
|
|
2260
2410
|
body: spec.body ?? null,
|
|
2261
2411
|
streamingExempt: spec.streamingExempt ?? false,
|
|
2262
|
-
|
|
2412
|
+
businessId: ctx.businessId,
|
|
2263
2413
|
backendId: ctx.backendId,
|
|
2264
2414
|
routeId: spec.routeId ?? "",
|
|
2265
2415
|
privateJwk: ctx.keys.privateJwk,
|
|
@@ -2269,7 +2419,7 @@ function createPersonaClient(ctx) {
|
|
|
2269
2419
|
});
|
|
2270
2420
|
const headers = { ...signed.headers };
|
|
2271
2421
|
if (!persona.anonymous) {
|
|
2272
|
-
const claim = buildContextClaim(persona, ctx.
|
|
2422
|
+
const claim = buildContextClaim(persona, ctx.businessId);
|
|
2273
2423
|
headers["x-fs-context"] = await signContextToken(
|
|
2274
2424
|
claim,
|
|
2275
2425
|
ctx.contextSecret,
|
|
@@ -2342,11 +2492,12 @@ var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
|
|
|
2342
2492
|
var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
|
|
2343
2493
|
var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
|
|
2344
2494
|
function createDevGateway(options) {
|
|
2345
|
-
const
|
|
2495
|
+
const businessId = options.businessId ?? "biz_dev";
|
|
2346
2496
|
const backendId = options.backendId ?? "be_dev";
|
|
2347
2497
|
const meterEvents = [];
|
|
2498
|
+
const reportUsageEvents = [];
|
|
2348
2499
|
const bootstrap = {
|
|
2349
|
-
|
|
2500
|
+
business: { id: businessId, slug: options.businessSlug ?? "dev-business" },
|
|
2350
2501
|
backend: {
|
|
2351
2502
|
id: backendId,
|
|
2352
2503
|
slug: options.backendSlug ?? "dev-backend",
|
|
@@ -2394,7 +2545,10 @@ function createDevGateway(options) {
|
|
|
2394
2545
|
}
|
|
2395
2546
|
if (url.includes("/v1/metering/events")) {
|
|
2396
2547
|
const event = await readJsonBody(init, input);
|
|
2397
|
-
if (event) {
|
|
2548
|
+
if (event && "meters" in event) {
|
|
2549
|
+
reportUsageEvents.push(event);
|
|
2550
|
+
options.onReportUsage?.(event);
|
|
2551
|
+
} else if (event) {
|
|
2398
2552
|
meterEvents.push(event);
|
|
2399
2553
|
options.onMeterEvent?.(event);
|
|
2400
2554
|
}
|
|
@@ -2412,7 +2566,8 @@ function createDevGateway(options) {
|
|
|
2412
2566
|
fetchImpl,
|
|
2413
2567
|
bootstrap,
|
|
2414
2568
|
meterEvents,
|
|
2415
|
-
|
|
2569
|
+
reportUsageEvents,
|
|
2570
|
+
businessId,
|
|
2416
2571
|
backendId,
|
|
2417
2572
|
jwksUrl: DEV_JWKS_URL
|
|
2418
2573
|
};
|
|
@@ -2488,6 +2643,16 @@ var DevUsageSink = class {
|
|
|
2488
2643
|
at: Date.now()
|
|
2489
2644
|
});
|
|
2490
2645
|
}
|
|
2646
|
+
/** Record an attested post-stream report captured by the dev gateway. */
|
|
2647
|
+
recordReportUsage(event) {
|
|
2648
|
+
this.events.push({
|
|
2649
|
+
source: "reportUsage",
|
|
2650
|
+
meters: { ...event.meters },
|
|
2651
|
+
event,
|
|
2652
|
+
requestId: event.requestId,
|
|
2653
|
+
at: Date.now()
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2491
2656
|
/** Total quantity per meter key across every recorded event. */
|
|
2492
2657
|
byMeter() {
|
|
2493
2658
|
const out = {};
|
|
@@ -2604,12 +2769,16 @@ function createDevRuntime(options) {
|
|
|
2604
2769
|
const gateway = createDevGateway({
|
|
2605
2770
|
mode,
|
|
2606
2771
|
keys,
|
|
2607
|
-
...options.
|
|
2772
|
+
...options.businessId ? { businessId: options.businessId } : {},
|
|
2608
2773
|
...options.backendId ? { backendId: options.backendId } : {},
|
|
2609
2774
|
...options.routes ? { routeIds: options.routes } : {},
|
|
2610
2775
|
onMeterEvent: (event) => {
|
|
2611
2776
|
usage.recordMeterEvent(event);
|
|
2612
2777
|
options.usageJsonl?.(JSON.stringify({ source: "meter", event }));
|
|
2778
|
+
},
|
|
2779
|
+
onReportUsage: (event) => {
|
|
2780
|
+
usage.recordReportUsage(event);
|
|
2781
|
+
options.usageJsonl?.(JSON.stringify({ source: "reportUsage", event }));
|
|
2613
2782
|
}
|
|
2614
2783
|
});
|
|
2615
2784
|
__setDevMeteringHooks({
|
|
@@ -2629,7 +2798,7 @@ function createDevRuntime(options) {
|
|
|
2629
2798
|
const personas = buildPersonaMap(options.personas);
|
|
2630
2799
|
const personaClient = createPersonaClient({
|
|
2631
2800
|
keys,
|
|
2632
|
-
|
|
2801
|
+
businessId: gateway.businessId,
|
|
2633
2802
|
backendId: gateway.backendId,
|
|
2634
2803
|
contextSecret: keys.contextSecret,
|
|
2635
2804
|
contextKid: keys.contextKid,
|
|
@@ -2759,7 +2928,7 @@ function createDevRuntimeFromEnv(env = readProcessEnv3()) {
|
|
|
2759
2928
|
version: 1,
|
|
2760
2929
|
mode,
|
|
2761
2930
|
keys,
|
|
2762
|
-
|
|
2931
|
+
businessId: runtime.gateway.businessId,
|
|
2763
2932
|
backendId: runtime.gateway.backendId,
|
|
2764
2933
|
personas: mapToRecord(runtime.personas)
|
|
2765
2934
|
};
|
|
@@ -2772,7 +2941,7 @@ function printBanner(mode, runtime, tracePath) {
|
|
|
2772
2941
|
"============================================================",
|
|
2773
2942
|
" \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
|
|
2774
2943
|
` mode: ${mode.toUpperCase()}`,
|
|
2775
|
-
`
|
|
2944
|
+
` business: ${runtime.gateway.businessId}`,
|
|
2776
2945
|
` backend: ${runtime.gateway.backendId}`,
|
|
2777
2946
|
` personas: ${[...runtime.personas.keys()].join(", ")}`,
|
|
2778
2947
|
` usage log: ${USAGE_JSONL_PATH}`,
|
|
@@ -2858,6 +3027,7 @@ export {
|
|
|
2858
3027
|
MeteringClient,
|
|
2859
3028
|
MeteringError,
|
|
2860
3029
|
NonceCache,
|
|
3030
|
+
PostStreamUsageClient,
|
|
2861
3031
|
REDACTED_TOKEN,
|
|
2862
3032
|
RUNTIME_CLOCK_SKEW_SECONDS,
|
|
2863
3033
|
RUNTIME_ERROR_CODES,
|