@farthershore/backend 0.12.0 → 0.14.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 +106 -1
- package/dist/generated/runtime-contract.js +15 -0
- package/dist/index.js +1072 -124
- package/dist/testing/index.js +2906 -0
- package/dist/types/core/post-stream-usage.d.ts +45 -0
- package/dist/types/core/runtime.d.ts +4 -0
- package/dist/types/core/verifyRequest.d.ts +3 -0
- package/dist/types/generated/runtime-contract.d.ts +15 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/response-metering.d.ts +45 -0
- package/dist/types/runtime-types.d.ts +10 -0
- package/dist/types/testing/devGateway.d.ts +40 -0
- package/dist/types/testing/devRuntime.d.ts +68 -0
- package/dist/types/testing/index.d.ts +8 -0
- package/dist/types/testing/keysFile.d.ts +30 -0
- package/dist/types/testing/personas.d.ts +104 -0
- package/dist/types/testing/prodGuard.d.ts +12 -0
- package/dist/types/testing/signers.d.ts +89 -0
- package/dist/types/testing/traceSink.d.ts +67 -0
- package/dist/types/testing/usageSink.d.ts +39 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -899,6 +899,300 @@ function resolveEndpoint(endpoint, coreUrl) {
|
|
|
899
899
|
return `${coreUrl.replace(/\/+$/, "")}${endpoint}`;
|
|
900
900
|
}
|
|
901
901
|
|
|
902
|
+
// src/response-metering.ts
|
|
903
|
+
var RESPONSE_METERING_HEADERS = RUNTIME_RESPONSE_METERING_CONTRACT.headers;
|
|
904
|
+
var RESPONSE_METERING_ERROR_CODES = RUNTIME_RESPONSE_METERING_CONTRACT.errors;
|
|
905
|
+
var devMeteringHooks = null;
|
|
906
|
+
function __setDevMeteringHooks(hooks) {
|
|
907
|
+
devMeteringHooks = hooks;
|
|
908
|
+
}
|
|
909
|
+
var METERING_PAYLOAD_HEADER = RESPONSE_METERING_HEADERS.payload;
|
|
910
|
+
var METERING_SIGNATURE_HEADER = RESPONSE_METERING_HEADERS.signature;
|
|
911
|
+
var METERING_TOKEN_HEADER = RESPONSE_METERING_HEADERS.token;
|
|
912
|
+
var DEFAULT_TOKEN_ENV = RUNTIME_RESPONSE_METERING_CONTRACT.token.environmentVariable;
|
|
913
|
+
var MeteringError = class extends Error {
|
|
914
|
+
code;
|
|
915
|
+
constructor(code, message) {
|
|
916
|
+
super(message);
|
|
917
|
+
this.name = "MeteringError";
|
|
918
|
+
this.code = code;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
function createUsage(request, options = {}) {
|
|
922
|
+
const usage = {};
|
|
923
|
+
const reporter = {
|
|
924
|
+
report(meter, value) {
|
|
925
|
+
usage[assertMeterKey(meter)] = assertMeterValue(meter, value);
|
|
926
|
+
return reporter;
|
|
927
|
+
},
|
|
928
|
+
async wrap(response, wrapOptions = {}) {
|
|
929
|
+
return signResponse(request, response, usage, options, wrapOptions);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
return reporter;
|
|
933
|
+
}
|
|
934
|
+
async function withUsage(request, response, usage, options = {}) {
|
|
935
|
+
const reporter = createUsage(request, options);
|
|
936
|
+
for (const [meter, value] of Object.entries(usage)) {
|
|
937
|
+
reporter.report(meter, value);
|
|
938
|
+
}
|
|
939
|
+
return reporter.wrap(response);
|
|
940
|
+
}
|
|
941
|
+
async function signResponse(request, response, usage, options, wrapOptions) {
|
|
942
|
+
const payload = buildPayload(request, usage, options, wrapOptions);
|
|
943
|
+
const requestId = request.headers.get("x-fs-request-id") ?? void 0;
|
|
944
|
+
const headers = await computeMeteringHeaders(payload, {
|
|
945
|
+
...options.token !== void 0 ? { token: options.token } : {},
|
|
946
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
947
|
+
...requestId ? { requestId } : {},
|
|
948
|
+
onSkip: () => {
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
if (Object.keys(headers).length === 0) {
|
|
952
|
+
throw new MeteringError(
|
|
953
|
+
RESPONSE_METERING_ERROR_CODES.missingToken,
|
|
954
|
+
`${DEFAULT_TOKEN_ENV} is required to sign Farther Shore metering reports`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const merged = new Headers(response.headers);
|
|
958
|
+
for (const [name, value] of Object.entries(headers)) merged.set(name, value);
|
|
959
|
+
return new Response(response.body, {
|
|
960
|
+
status: response.status,
|
|
961
|
+
statusText: response.statusText,
|
|
962
|
+
headers: merged
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
async function computeMeteringHeaders(payload, options = {}) {
|
|
966
|
+
try {
|
|
967
|
+
const token = resolveTokenSoft(options);
|
|
968
|
+
if (!token) {
|
|
969
|
+
skip(`${DEFAULT_TOKEN_ENV} is not set`, options);
|
|
970
|
+
return {};
|
|
971
|
+
}
|
|
972
|
+
const json2 = JSON.stringify(payload);
|
|
973
|
+
const signature = await signPayload(json2, token);
|
|
974
|
+
devMeteringHooks?.record?.(payload, options.requestId);
|
|
975
|
+
return {
|
|
976
|
+
[METERING_PAYLOAD_HEADER]: json2,
|
|
977
|
+
[METERING_SIGNATURE_HEADER]: signature,
|
|
978
|
+
[METERING_TOKEN_HEADER]: token
|
|
979
|
+
};
|
|
980
|
+
} catch (error) {
|
|
981
|
+
skip(error instanceof Error ? error.message : String(error), options);
|
|
982
|
+
return {};
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function skip(reason, options) {
|
|
986
|
+
if (options.onSkip) {
|
|
987
|
+
options.onSkip(reason);
|
|
988
|
+
} else {
|
|
989
|
+
console.warn(`metering headers skipped: ${reason}`);
|
|
990
|
+
}
|
|
991
|
+
devMeteringHooks?.onSkip?.(reason, options.requestId);
|
|
992
|
+
}
|
|
993
|
+
function resolveTokenSoft(options) {
|
|
994
|
+
return options.token ?? options.env?.[DEFAULT_TOKEN_ENV] ?? processEnv(DEFAULT_TOKEN_ENV) ?? devMeteringHooks?.fallbackToken?.();
|
|
995
|
+
}
|
|
996
|
+
function buildPayload(request, usage, options, wrapOptions) {
|
|
997
|
+
const url = new URL(request.url);
|
|
998
|
+
const measureContext = wrapOptions.measureContext ?? options.measureContext;
|
|
999
|
+
const creditUnitsConsumed = wrapOptions.creditUnitsConsumed ?? options.creditUnitsConsumed;
|
|
1000
|
+
const operationKey = wrapOptions.operationKey ?? options.operationKey;
|
|
1001
|
+
const usagePolicyId = wrapOptions.usagePolicyId ?? options.usagePolicyId;
|
|
1002
|
+
const payload = {
|
|
1003
|
+
method: request.method.toUpperCase(),
|
|
1004
|
+
path: url.pathname,
|
|
1005
|
+
rawDimsUnits: sortUsage(usage),
|
|
1006
|
+
...measureContext ? { measureContext } : {},
|
|
1007
|
+
...creditUnitsConsumed ? {
|
|
1008
|
+
creditUnitsConsumed: sortUsage(
|
|
1009
|
+
validateUsageMap(creditUnitsConsumed, "creditUnitsConsumed")
|
|
1010
|
+
)
|
|
1011
|
+
} : {},
|
|
1012
|
+
...operationKey ? { operationKey: assertIdentifier(operationKey) } : {},
|
|
1013
|
+
...usagePolicyId ? { usagePolicyId: assertIdentifier(usagePolicyId) } : {}
|
|
1014
|
+
};
|
|
1015
|
+
return payload;
|
|
1016
|
+
}
|
|
1017
|
+
function sortUsage(usage) {
|
|
1018
|
+
return Object.fromEntries(
|
|
1019
|
+
Object.entries(usage).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
function validateUsageMap(usage, label) {
|
|
1023
|
+
return Object.fromEntries(
|
|
1024
|
+
Object.entries(usage).map(([meter, value]) => [
|
|
1025
|
+
assertMeterKey(meter),
|
|
1026
|
+
assertMeterValue(`${label}.${meter}`, value)
|
|
1027
|
+
])
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
function assertMeterKey(meter) {
|
|
1031
|
+
if (!/^[a-z0-9_]{1,64}$/.test(meter)) {
|
|
1032
|
+
throw new MeteringError(
|
|
1033
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1034
|
+
`meter key "${meter}" must be lowercase alphanumeric with underscores`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
return meter;
|
|
1038
|
+
}
|
|
1039
|
+
function assertMeterValue(meter, value) {
|
|
1040
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1041
|
+
throw new MeteringError(
|
|
1042
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterValue,
|
|
1043
|
+
`meter "${meter}" value must be a non-negative finite number`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return value;
|
|
1047
|
+
}
|
|
1048
|
+
function assertIdentifier(value) {
|
|
1049
|
+
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(value)) {
|
|
1050
|
+
throw new MeteringError(
|
|
1051
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1052
|
+
`operation and usage policy identifiers must be 1-128 URL-safe characters`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return value;
|
|
1056
|
+
}
|
|
1057
|
+
function processEnv(key2) {
|
|
1058
|
+
const maybeProcess = globalThis.process;
|
|
1059
|
+
return maybeProcess?.env?.[key2];
|
|
1060
|
+
}
|
|
1061
|
+
async function signPayload(payload, token) {
|
|
1062
|
+
const key2 = await crypto.subtle.importKey(
|
|
1063
|
+
"raw",
|
|
1064
|
+
new TextEncoder().encode(token),
|
|
1065
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1066
|
+
false,
|
|
1067
|
+
["sign"]
|
|
1068
|
+
);
|
|
1069
|
+
const signature = await crypto.subtle.sign(
|
|
1070
|
+
"HMAC",
|
|
1071
|
+
key2,
|
|
1072
|
+
new TextEncoder().encode(payload)
|
|
1073
|
+
);
|
|
1074
|
+
return base64url(new Uint8Array(signature));
|
|
1075
|
+
}
|
|
1076
|
+
function base64url(bytes) {
|
|
1077
|
+
let binary = "";
|
|
1078
|
+
for (const byte of bytes) {
|
|
1079
|
+
binary += String.fromCharCode(byte);
|
|
1080
|
+
}
|
|
1081
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/core/post-stream-usage.ts
|
|
1085
|
+
var METER_KEY_RE2 = /^[a-z0-9_]{1,64}$/;
|
|
1086
|
+
var PostStreamUsageClient = class {
|
|
1087
|
+
config;
|
|
1088
|
+
endpoint;
|
|
1089
|
+
fetchImpl;
|
|
1090
|
+
newNonce;
|
|
1091
|
+
logger;
|
|
1092
|
+
sleep;
|
|
1093
|
+
retryDelaysMs;
|
|
1094
|
+
constructor(options) {
|
|
1095
|
+
this.config = options.config;
|
|
1096
|
+
this.endpoint = resolveEndpoint2(options.config.endpoint, options.coreUrl);
|
|
1097
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1098
|
+
this.newNonce = options.newNonce ?? (() => crypto.randomUUID());
|
|
1099
|
+
this.logger = options.logger ?? ((message) => console.warn(message));
|
|
1100
|
+
this.sleep = options.sleep ?? sleep;
|
|
1101
|
+
this.retryDelaysMs = options.retryDelaysMs ?? [100, 250, 500];
|
|
1102
|
+
}
|
|
1103
|
+
async reportUsage(input) {
|
|
1104
|
+
try {
|
|
1105
|
+
if (!this.config.enabled) throw new Error("metering is not enabled");
|
|
1106
|
+
if (!input.requestId) throw new Error("requestId is required");
|
|
1107
|
+
if (!input.subscriptionId) throw new Error("subscriptionId is required");
|
|
1108
|
+
const unsigned = {
|
|
1109
|
+
requestId: input.requestId,
|
|
1110
|
+
subscriptionId: input.subscriptionId,
|
|
1111
|
+
nonce: this.newNonce(),
|
|
1112
|
+
meters: validateAndSortUsage(input.meters, "meters", this.config, true),
|
|
1113
|
+
...input.creditUnitsConsumed ? {
|
|
1114
|
+
creditUnitsConsumed: validateAndSortUsage(
|
|
1115
|
+
input.creditUnitsConsumed,
|
|
1116
|
+
"creditUnitsConsumed",
|
|
1117
|
+
this.config,
|
|
1118
|
+
false
|
|
1119
|
+
)
|
|
1120
|
+
} : {},
|
|
1121
|
+
...input.measureContext ? { measureContext: input.measureContext } : {}
|
|
1122
|
+
};
|
|
1123
|
+
const signature = await signPayload(
|
|
1124
|
+
JSON.stringify(unsigned),
|
|
1125
|
+
this.config.credential
|
|
1126
|
+
);
|
|
1127
|
+
const event = { ...unsigned, signature };
|
|
1128
|
+
const body = JSON.stringify(event);
|
|
1129
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1130
|
+
const response = await this.fetchImpl(this.endpoint, {
|
|
1131
|
+
method: "POST",
|
|
1132
|
+
headers: {
|
|
1133
|
+
authorization: `Bearer ${this.config.credential}`,
|
|
1134
|
+
"content-type": "application/json",
|
|
1135
|
+
accept: "application/json"
|
|
1136
|
+
},
|
|
1137
|
+
body
|
|
1138
|
+
});
|
|
1139
|
+
if (response.ok) return { ok: true };
|
|
1140
|
+
const requestNotFound = await isPostStreamRequestNotFound(response);
|
|
1141
|
+
const delayMs = this.retryDelaysMs[attempt];
|
|
1142
|
+
if (!requestNotFound || delayMs === void 0) {
|
|
1143
|
+
throw new Error(`metering endpoint returned ${response.status}`);
|
|
1144
|
+
}
|
|
1145
|
+
await this.sleep(delayMs);
|
|
1146
|
+
}
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1149
|
+
this.logger(`post-stream usage report skipped: ${reason}`);
|
|
1150
|
+
return { ok: false, reason };
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
async function isPostStreamRequestNotFound(response) {
|
|
1155
|
+
if (response.status !== 422) return false;
|
|
1156
|
+
try {
|
|
1157
|
+
const body = await response.json();
|
|
1158
|
+
return body.error?.code === "post_stream_request_not_found";
|
|
1159
|
+
} catch {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function sleep(delayMs) {
|
|
1164
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1165
|
+
}
|
|
1166
|
+
function validateAndSortUsage(usage, label, config, enforceMeterScope) {
|
|
1167
|
+
const entries = Object.entries(usage).sort(
|
|
1168
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
1169
|
+
);
|
|
1170
|
+
for (const [meter, qty] of entries) {
|
|
1171
|
+
if (!METER_KEY_RE2.test(meter)) {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`${label} key '${meter}' must be lowercase alphanumeric with underscores`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
if (!Number.isFinite(qty) || qty < 0) {
|
|
1177
|
+
throw new Error(`${label}.${meter} must be a non-negative finite number`);
|
|
1178
|
+
}
|
|
1179
|
+
if (enforceMeterScope && config.allowedMeters.length > 0 && !config.allowedMeters.includes(meter)) {
|
|
1180
|
+
throw new Error(`meter '${meter}' is not in the token's allowedMeters`);
|
|
1181
|
+
}
|
|
1182
|
+
if (enforceMeterScope && config.perEventMax > 0 && qty > config.perEventMax) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`meter '${meter}' qty ${qty} exceeds the per-event max ${config.perEventMax}`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return Object.fromEntries(entries);
|
|
1189
|
+
}
|
|
1190
|
+
function resolveEndpoint2(endpoint, coreUrl) {
|
|
1191
|
+
if (/^https?:\/\//.test(endpoint)) return endpoint;
|
|
1192
|
+
if (!coreUrl) return endpoint;
|
|
1193
|
+
return `${coreUrl.replace(/\/+$/, "")}${endpoint}`;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
902
1196
|
// src/core/nonceCache.ts
|
|
903
1197
|
var DEFAULT_MAX_ENTRIES = 1e5;
|
|
904
1198
|
var DEFAULT_TTL_MS = 6e5;
|
|
@@ -1583,8 +1877,8 @@ function headerGetter(headers) {
|
|
|
1583
1877
|
|
|
1584
1878
|
// src/core/runtime.ts
|
|
1585
1879
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
1586
|
-
var SDK_VERSION = "0.
|
|
1587
|
-
var CONTRACTS_FP = "
|
|
1880
|
+
var SDK_VERSION = "0.14.0".length > 0 ? "0.14.0" : "0.0.0-dev";
|
|
1881
|
+
var CONTRACTS_FP = "1feb5a4a80b447ad".length > 0 ? "1feb5a4a80b447ad" : "0000000000000000";
|
|
1588
1882
|
var FartherShore = class {
|
|
1589
1883
|
bootstrapClient;
|
|
1590
1884
|
fetchImpl;
|
|
@@ -1602,6 +1896,7 @@ var FartherShore = class {
|
|
|
1602
1896
|
shutdownManager = new ShutdownManager();
|
|
1603
1897
|
jwks = null;
|
|
1604
1898
|
meteringClient = null;
|
|
1899
|
+
postStreamUsageClient = null;
|
|
1605
1900
|
tunnel = null;
|
|
1606
1901
|
bootstrapped = false;
|
|
1607
1902
|
constructor(options = {}) {
|
|
@@ -1657,6 +1952,11 @@ var FartherShore = class {
|
|
|
1657
1952
|
coreUrl: this.coreUrl,
|
|
1658
1953
|
fetchImpl: this.fetchImpl
|
|
1659
1954
|
});
|
|
1955
|
+
this.postStreamUsageClient = new PostStreamUsageClient({
|
|
1956
|
+
config: config.metering,
|
|
1957
|
+
coreUrl: this.coreUrl,
|
|
1958
|
+
fetchImpl: this.fetchImpl
|
|
1959
|
+
});
|
|
1660
1960
|
}
|
|
1661
1961
|
this.bootstrapped = true;
|
|
1662
1962
|
return config;
|
|
@@ -1728,7 +2028,7 @@ var FartherShore = class {
|
|
|
1728
2028
|
);
|
|
1729
2029
|
}
|
|
1730
2030
|
const knownRouteIds = new Set(config.routes.map((r) => r.id));
|
|
1731
|
-
|
|
2031
|
+
const context = await verifyRequest(input, {
|
|
1732
2032
|
jwks: this.jwks,
|
|
1733
2033
|
nonceCache: this.nonceCache,
|
|
1734
2034
|
productId: config.product.id,
|
|
@@ -1743,6 +2043,23 @@ var FartherShore = class {
|
|
|
1743
2043
|
contextSecrets: this.contextSecrets,
|
|
1744
2044
|
contextVerification: this.contextVerification
|
|
1745
2045
|
});
|
|
2046
|
+
return {
|
|
2047
|
+
...context,
|
|
2048
|
+
reportUsage: (report) => {
|
|
2049
|
+
const subscriptionId = report.subscriptionId ?? context.signedContext?.subscriptionId;
|
|
2050
|
+
if (!subscriptionId) {
|
|
2051
|
+
return Promise.resolve({
|
|
2052
|
+
ok: false,
|
|
2053
|
+
reason: "subscriptionId is required"
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
return this.reportUsage({
|
|
2057
|
+
...report,
|
|
2058
|
+
requestId: report.requestId ?? context.requestId,
|
|
2059
|
+
subscriptionId
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
1746
2063
|
}
|
|
1747
2064
|
/** Whether verification is required (bootstrap × opt-out). */
|
|
1748
2065
|
async verificationRequired() {
|
|
@@ -1803,6 +2120,20 @@ var FartherShore = class {
|
|
|
1803
2120
|
}
|
|
1804
2121
|
await this.meteringClient.meter(meter, qty, options);
|
|
1805
2122
|
}
|
|
2123
|
+
/** Best-effort attested post-stream usage callback. Never rejects. */
|
|
2124
|
+
async reportUsage(input) {
|
|
2125
|
+
try {
|
|
2126
|
+
await this.ensureBootstrapped();
|
|
2127
|
+
if (!this.meteringEnabledOverride || !this.postStreamUsageClient) {
|
|
2128
|
+
return { ok: false, reason: "metering is not enabled" };
|
|
2129
|
+
}
|
|
2130
|
+
return await this.postStreamUsageClient.reportUsage(input);
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2133
|
+
console.warn(`post-stream usage report skipped: ${reason}`);
|
|
2134
|
+
return { ok: false, reason };
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
1806
2137
|
/** Current local health report. */
|
|
1807
2138
|
health() {
|
|
1808
2139
|
const config = this.bootstrapClient.peek();
|
|
@@ -1903,157 +2234,772 @@ function headerValue(headers, name) {
|
|
|
1903
2234
|
return value;
|
|
1904
2235
|
}
|
|
1905
2236
|
|
|
1906
|
-
// src/
|
|
1907
|
-
|
|
1908
|
-
var
|
|
1909
|
-
var
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
code;
|
|
1915
|
-
constructor(code, message) {
|
|
1916
|
-
super(message);
|
|
1917
|
-
this.name = "MeteringError";
|
|
1918
|
-
this.code = code;
|
|
1919
|
-
}
|
|
2237
|
+
// src/testing/signers.ts
|
|
2238
|
+
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
2239
|
+
var TEST_KID = "fs-runtime-test-2026";
|
|
2240
|
+
var TEST_PRIVATE_JWK = {
|
|
2241
|
+
kty: "OKP",
|
|
2242
|
+
crv: "Ed25519",
|
|
2243
|
+
d: "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
|
|
2244
|
+
x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
|
|
1920
2245
|
};
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2246
|
+
var TEST_CONTEXT_SECRET = "fs-dev-context-secret-2026";
|
|
2247
|
+
var TEST_CONTEXT_KID = "fs-context-test-2026";
|
|
2248
|
+
async function makeSignedRequest(spec = {}) {
|
|
2249
|
+
const method = spec.method ?? "POST";
|
|
2250
|
+
const path = spec.path ?? "/v1/chat/completions";
|
|
2251
|
+
const query = spec.query ?? "";
|
|
2252
|
+
const body = spec.body ?? null;
|
|
2253
|
+
const streamingExempt = spec.streamingExempt ?? false;
|
|
2254
|
+
const privateJwk = spec.privateJwk ?? TEST_PRIVATE_JWK;
|
|
2255
|
+
const kid = spec.kid ?? TEST_KID;
|
|
2256
|
+
const bodyHash = streamingExempt ? "STREAM" : body && body.byteLength > 0 ? await hashBody2(body) : EMPTY_BODY_SHA256;
|
|
2257
|
+
const claim = {
|
|
2258
|
+
method,
|
|
2259
|
+
path,
|
|
2260
|
+
query,
|
|
2261
|
+
bodyHash,
|
|
2262
|
+
requestId: spec.requestId ?? `req_${cryptoRandom()}`,
|
|
2263
|
+
timestamp: spec.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
2264
|
+
productId: spec.productId ?? "prod_test",
|
|
2265
|
+
backendId: spec.backendId ?? "be_test",
|
|
2266
|
+
routeId: spec.routeId ?? "route_test",
|
|
2267
|
+
policyVersion: spec.policyVersion ?? "pv_1"
|
|
2268
|
+
};
|
|
2269
|
+
const canonical = buildCanonicalSigningString2(claim);
|
|
2270
|
+
const signature = await signCanonicalString2(canonical, privateJwk);
|
|
2271
|
+
const headers = {
|
|
2272
|
+
[RUNTIME_HEADER_NAMES.signature]: signature,
|
|
2273
|
+
[RUNTIME_HEADER_NAMES.keyId]: kid,
|
|
2274
|
+
[RUNTIME_HEADER_NAMES.requestId]: claim.requestId,
|
|
2275
|
+
[RUNTIME_HEADER_NAMES.timestamp]: String(claim.timestamp),
|
|
2276
|
+
[RUNTIME_HEADER_NAMES.productId]: claim.productId,
|
|
2277
|
+
[RUNTIME_HEADER_NAMES.backendId]: claim.backendId,
|
|
2278
|
+
[RUNTIME_HEADER_NAMES.routeId]: claim.routeId,
|
|
2279
|
+
[RUNTIME_HEADER_NAMES.policyVersion]: claim.policyVersion,
|
|
2280
|
+
[RUNTIME_HEADER_NAMES.bodyHash]: claim.bodyHash
|
|
2281
|
+
};
|
|
2282
|
+
return {
|
|
2283
|
+
input: { method, path, query, body, streamingExempt },
|
|
2284
|
+
claim,
|
|
2285
|
+
headers
|
|
1931
2286
|
};
|
|
1932
|
-
return reporter;
|
|
1933
2287
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
for (const
|
|
1937
|
-
|
|
1938
|
-
}
|
|
1939
|
-
return reporter.wrap(response);
|
|
2288
|
+
function base64urlEncodeBytes(bytes) {
|
|
2289
|
+
let binary = "";
|
|
2290
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2291
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1940
2292
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
const payload = buildPayload(request, usage, options, wrapOptions);
|
|
1944
|
-
const signature = await signPayload(payload, token);
|
|
1945
|
-
const headers = new Headers(response.headers);
|
|
1946
|
-
headers.set(METERING_PAYLOAD_HEADER, payload);
|
|
1947
|
-
headers.set(METERING_SIGNATURE_HEADER, signature);
|
|
1948
|
-
headers.set(METERING_TOKEN_HEADER, token);
|
|
1949
|
-
return new Response(response.body, {
|
|
1950
|
-
status: response.status,
|
|
1951
|
-
statusText: response.statusText,
|
|
1952
|
-
headers
|
|
1953
|
-
});
|
|
2293
|
+
function base64urlEncodeJson(value) {
|
|
2294
|
+
return base64urlEncodeBytes(new TextEncoder().encode(JSON.stringify(value)));
|
|
1954
2295
|
}
|
|
1955
|
-
function
|
|
1956
|
-
const
|
|
1957
|
-
const
|
|
1958
|
-
const
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2296
|
+
async function signContextToken(claim, secret = TEST_CONTEXT_SECRET, kid = TEST_CONTEXT_KID) {
|
|
2297
|
+
const header = base64urlEncodeJson({ alg: "HS256", typ: "JWT", kid });
|
|
2298
|
+
const payload = base64urlEncodeJson({ cv: 1, ...claim });
|
|
2299
|
+
const signingInput = `${header}.${payload}`;
|
|
2300
|
+
const key2 = await crypto.subtle.importKey(
|
|
2301
|
+
"raw",
|
|
2302
|
+
new TextEncoder().encode(secret),
|
|
2303
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2304
|
+
false,
|
|
2305
|
+
["sign"]
|
|
2306
|
+
);
|
|
2307
|
+
const signature = await crypto.subtle.sign(
|
|
2308
|
+
"HMAC",
|
|
2309
|
+
key2,
|
|
2310
|
+
new TextEncoder().encode(signingInput)
|
|
2311
|
+
);
|
|
2312
|
+
return `${signingInput}.${base64urlEncodeBytes(new Uint8Array(signature))}`;
|
|
2313
|
+
}
|
|
2314
|
+
function generateDevSignerKeys() {
|
|
2315
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
2316
|
+
const privateJwk = privateKey.export({ format: "jwk" });
|
|
2317
|
+
const publicJwk = publicKey.export({ format: "jwk" });
|
|
2318
|
+
const kid = `fs-dev-${randomBytes(6).toString("hex")}`;
|
|
2319
|
+
const contextKid = `fs-dev-ctx-${randomBytes(6).toString("hex")}`;
|
|
2320
|
+
const contextSecret = randomBytes(32).toString("base64url");
|
|
2321
|
+
const runtimeToken = `${RUNTIME_TOKEN_PREFIXES.test}${randomBytes(24).toString("base64url")}`;
|
|
2322
|
+
return {
|
|
2323
|
+
kid,
|
|
2324
|
+
privateJwk,
|
|
2325
|
+
publicJwk,
|
|
2326
|
+
contextKid,
|
|
2327
|
+
contextSecret,
|
|
2328
|
+
runtimeToken
|
|
1973
2329
|
};
|
|
1974
|
-
return JSON.stringify(payload);
|
|
1975
2330
|
}
|
|
1976
|
-
function
|
|
1977
|
-
return
|
|
1978
|
-
Object.entries(usage).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
1979
|
-
);
|
|
2331
|
+
function cryptoRandom() {
|
|
2332
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
1980
2333
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2334
|
+
|
|
2335
|
+
// src/testing/personas.ts
|
|
2336
|
+
function definePersona(def) {
|
|
2337
|
+
return def;
|
|
2338
|
+
}
|
|
2339
|
+
var DEFAULT_PERSONAS = {
|
|
2340
|
+
owner: definePersona({
|
|
2341
|
+
name: "owner",
|
|
2342
|
+
permissions: ["*"],
|
|
2343
|
+
roles: ["owner"],
|
|
2344
|
+
subjectKey: "owner"
|
|
2345
|
+
}),
|
|
2346
|
+
admin: definePersona({
|
|
2347
|
+
name: "admin",
|
|
2348
|
+
permissions: ["*"],
|
|
2349
|
+
roles: ["admin"],
|
|
2350
|
+
subjectKey: "admin"
|
|
2351
|
+
}),
|
|
2352
|
+
member: definePersona({
|
|
2353
|
+
name: "member",
|
|
2354
|
+
permissions: [],
|
|
2355
|
+
roles: ["member"],
|
|
2356
|
+
subjectKey: "member"
|
|
2357
|
+
}),
|
|
2358
|
+
anonymous: definePersona({ name: "anonymous", anonymous: true })
|
|
2359
|
+
};
|
|
2360
|
+
function normalizeBody(body) {
|
|
2361
|
+
if (body === null || body === void 0) return null;
|
|
2362
|
+
if (typeof body === "string") return new TextEncoder().encode(body);
|
|
2363
|
+
if (body instanceof Uint8Array) return body;
|
|
2364
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
2365
|
+
return null;
|
|
1988
2366
|
}
|
|
1989
|
-
function
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2367
|
+
function normalizeMethod(method) {
|
|
2368
|
+
return (method ?? "GET").toUpperCase();
|
|
2369
|
+
}
|
|
2370
|
+
function mergeHeaders(initHeaders, signedHeaders) {
|
|
2371
|
+
const headers = new Headers(initHeaders);
|
|
2372
|
+
for (const [name, value] of Object.entries(signedHeaders)) {
|
|
2373
|
+
headers.set(name, value);
|
|
1995
2374
|
}
|
|
1996
|
-
return
|
|
2375
|
+
return headers;
|
|
1997
2376
|
}
|
|
1998
|
-
function
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2377
|
+
function buildContextClaim(persona, productId) {
|
|
2378
|
+
return {
|
|
2379
|
+
orgId: persona.orgId ?? "org_dev",
|
|
2380
|
+
actor: persona.actor ?? { type: "user", id: `user_${persona.name}` },
|
|
2381
|
+
// Product binding: the signed context productId MUST equal the signed
|
|
2382
|
+
// request productId or verifyRequest rejects it as tamper evidence.
|
|
2383
|
+
productId,
|
|
2384
|
+
compiledPlanId: persona.compiledPlanId ?? "plan_dev",
|
|
2385
|
+
subscriptionId: persona.subscriptionId ?? "sub_dev",
|
|
2386
|
+
subscriberId: persona.subscriberId ?? "subscriber_dev",
|
|
2387
|
+
environmentId: persona.environmentId ?? null,
|
|
2388
|
+
subjectKey: persona.subjectKey ?? persona.name,
|
|
2389
|
+
...persona.permissions !== void 0 ? { permissions: persona.permissions } : {},
|
|
2390
|
+
...persona.roles !== void 0 ? { roles: persona.roles } : {}
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
function createPersonaClient(ctx) {
|
|
2394
|
+
function resolve(name) {
|
|
2395
|
+
const persona = ctx.personas.get(name);
|
|
2396
|
+
if (!persona) {
|
|
2397
|
+
throw new Error(
|
|
2398
|
+
`unknown persona "${name}" (known: ${[...ctx.personas.keys()].join(", ")})`
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
return persona;
|
|
2402
|
+
}
|
|
2403
|
+
async function buildHeaders(persona, spec) {
|
|
2404
|
+
const method = normalizeMethod(spec.method);
|
|
2405
|
+
const signed = await makeSignedRequest({
|
|
2406
|
+
method,
|
|
2407
|
+
path: spec.path ?? "/",
|
|
2408
|
+
query: spec.query ?? "",
|
|
2409
|
+
body: spec.body ?? null,
|
|
2410
|
+
streamingExempt: spec.streamingExempt ?? false,
|
|
2411
|
+
productId: ctx.productId,
|
|
2412
|
+
backendId: ctx.backendId,
|
|
2413
|
+
routeId: spec.routeId ?? "",
|
|
2414
|
+
privateJwk: ctx.keys.privateJwk,
|
|
2415
|
+
kid: ctx.keys.kid,
|
|
2416
|
+
...spec.requestId ? { requestId: spec.requestId } : {},
|
|
2417
|
+
...spec.timestamp !== void 0 ? { timestamp: spec.timestamp } : {}
|
|
2418
|
+
});
|
|
2419
|
+
const headers = { ...signed.headers };
|
|
2420
|
+
if (!persona.anonymous) {
|
|
2421
|
+
const claim = buildContextClaim(persona, ctx.productId);
|
|
2422
|
+
headers["x-fs-context"] = await signContextToken(
|
|
2423
|
+
claim,
|
|
2424
|
+
ctx.contextSecret,
|
|
2425
|
+
ctx.contextKid
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
return headers;
|
|
2004
2429
|
}
|
|
2005
|
-
|
|
2430
|
+
function asPersona(name) {
|
|
2431
|
+
const persona = resolve(name);
|
|
2432
|
+
return {
|
|
2433
|
+
persona: name,
|
|
2434
|
+
headers: (spec = {}) => buildHeaders(persona, spec),
|
|
2435
|
+
async fetch(url, init = {}) {
|
|
2436
|
+
const parsed = new URL(url);
|
|
2437
|
+
const method = normalizeMethod(init.method);
|
|
2438
|
+
const headers = await buildHeaders(persona, {
|
|
2439
|
+
method,
|
|
2440
|
+
path: parsed.pathname,
|
|
2441
|
+
query: parsed.search.replace(/^\?/, ""),
|
|
2442
|
+
body: normalizeBody(init.body)
|
|
2443
|
+
});
|
|
2444
|
+
const doFetch = ctx.fetchImpl ?? globalThis.fetch;
|
|
2445
|
+
return doFetch(url, {
|
|
2446
|
+
...init,
|
|
2447
|
+
method,
|
|
2448
|
+
headers: mergeHeaders(init.headers, headers)
|
|
2449
|
+
});
|
|
2450
|
+
},
|
|
2451
|
+
async inject(req, spec = {}) {
|
|
2452
|
+
const rawUrl = spec.path ?? req.url ?? req.path ?? "/";
|
|
2453
|
+
const qIndex = rawUrl.indexOf("?");
|
|
2454
|
+
const path = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
2455
|
+
const query = spec.query ?? (qIndex === -1 ? "" : rawUrl.slice(qIndex + 1));
|
|
2456
|
+
const method = normalizeMethod(spec.method ?? req.method);
|
|
2457
|
+
const headers = await buildHeaders(persona, {
|
|
2458
|
+
method,
|
|
2459
|
+
path,
|
|
2460
|
+
query,
|
|
2461
|
+
body: spec.body ?? null,
|
|
2462
|
+
...spec.streamingExempt !== void 0 ? { streamingExempt: spec.streamingExempt } : {},
|
|
2463
|
+
...spec.routeId ? { routeId: spec.routeId } : {}
|
|
2464
|
+
});
|
|
2465
|
+
for (const [field, value] of Object.entries(headers)) {
|
|
2466
|
+
req.set(field, value);
|
|
2467
|
+
}
|
|
2468
|
+
return req;
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
return { asPersona, personas: ctx.personas };
|
|
2006
2473
|
}
|
|
2007
|
-
function
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2474
|
+
function buildPersonaMap(overrides) {
|
|
2475
|
+
const map = /* @__PURE__ */ new Map();
|
|
2476
|
+
for (const [name, def] of Object.entries(DEFAULT_PERSONAS)) {
|
|
2477
|
+
map.set(name, def);
|
|
2478
|
+
}
|
|
2479
|
+
if (Array.isArray(overrides)) {
|
|
2480
|
+
for (const def of overrides) map.set(def.name, def);
|
|
2481
|
+
} else if (overrides) {
|
|
2482
|
+
for (const [name, def] of Object.entries(overrides)) {
|
|
2483
|
+
map.set(name, { ...def, name });
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return map;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/testing/devGateway.ts
|
|
2490
|
+
var DEV_CORE_URL = "https://dev-gateway.farthershore.local";
|
|
2491
|
+
var DEV_JWKS_URL = `${DEV_CORE_URL}/.well-known/jwks.json`;
|
|
2492
|
+
var DEV_METERING_ENDPOINT = `${DEV_CORE_URL}/v1/metering/events`;
|
|
2493
|
+
function createDevGateway(options) {
|
|
2494
|
+
const productId = options.productId ?? "prod_dev";
|
|
2495
|
+
const backendId = options.backendId ?? "be_dev";
|
|
2496
|
+
const meterEvents = [];
|
|
2497
|
+
const reportUsageEvents = [];
|
|
2498
|
+
const bootstrap = {
|
|
2499
|
+
product: { id: productId, slug: options.productSlug ?? "dev-product" },
|
|
2500
|
+
backend: {
|
|
2501
|
+
id: backendId,
|
|
2502
|
+
slug: options.backendSlug ?? "dev-backend",
|
|
2503
|
+
name: "Dev Backend"
|
|
2504
|
+
},
|
|
2505
|
+
environment: { id: null, kind: "test" },
|
|
2506
|
+
capabilities: ["gateway_verification", "metering", "health"],
|
|
2507
|
+
verification: {
|
|
2508
|
+
required: options.mode === "simulated",
|
|
2509
|
+
jwksUrl: DEV_JWKS_URL,
|
|
2510
|
+
clockSkewSeconds: 5,
|
|
2511
|
+
replayWindowSeconds: 300,
|
|
2512
|
+
headerNames: RUNTIME_HEADER_NAMES
|
|
2513
|
+
},
|
|
2514
|
+
metering: {
|
|
2515
|
+
enabled: true,
|
|
2516
|
+
endpoint: DEV_METERING_ENDPOINT,
|
|
2517
|
+
credential: options.keys.runtimeToken,
|
|
2518
|
+
allowedMeters: [],
|
|
2519
|
+
allowedRoutes: [],
|
|
2520
|
+
perEventMax: 0
|
|
2521
|
+
},
|
|
2522
|
+
transport: { mode: "direct", runner: null },
|
|
2523
|
+
routes: (options.routeIds ?? []).map((id) => ({
|
|
2524
|
+
id,
|
|
2525
|
+
method: "POST",
|
|
2526
|
+
path: `/${id}`,
|
|
2527
|
+
backendId
|
|
2528
|
+
})),
|
|
2529
|
+
policyVersion: "pv_dev",
|
|
2530
|
+
refreshAfterSeconds: 3600
|
|
2531
|
+
};
|
|
2532
|
+
const jwksDoc = {
|
|
2533
|
+
keys: [{ ...options.keys.publicJwk, kid: options.keys.kid }]
|
|
2534
|
+
};
|
|
2535
|
+
const fetchImpl = async (input, init) => {
|
|
2536
|
+
const url = String(
|
|
2537
|
+
typeof input === "string" || input instanceof URL ? input : input.url
|
|
2012
2538
|
);
|
|
2539
|
+
if (url.includes("/v1/runtime/bootstrap")) {
|
|
2540
|
+
return json(bootstrap);
|
|
2541
|
+
}
|
|
2542
|
+
if (url.includes("/.well-known/jwks.json") || url.includes("jwks")) {
|
|
2543
|
+
return json(jwksDoc);
|
|
2544
|
+
}
|
|
2545
|
+
if (url.includes("/v1/metering/events")) {
|
|
2546
|
+
const event = await readJsonBody(init, input);
|
|
2547
|
+
if (event && "meters" in event) {
|
|
2548
|
+
reportUsageEvents.push(event);
|
|
2549
|
+
options.onReportUsage?.(event);
|
|
2550
|
+
} else if (event) {
|
|
2551
|
+
meterEvents.push(event);
|
|
2552
|
+
options.onMeterEvent?.(event);
|
|
2553
|
+
}
|
|
2554
|
+
return json({ ok: true });
|
|
2555
|
+
}
|
|
2556
|
+
if (url.includes("/v1/runtime/health") || url.includes("/v1/runtime/drift")) {
|
|
2557
|
+
return json({ ok: true });
|
|
2558
|
+
}
|
|
2559
|
+
return new Response(JSON.stringify({ error: "not_found" }), {
|
|
2560
|
+
status: 404,
|
|
2561
|
+
headers: { "content-type": "application/json" }
|
|
2562
|
+
});
|
|
2563
|
+
};
|
|
2564
|
+
return {
|
|
2565
|
+
fetchImpl,
|
|
2566
|
+
bootstrap,
|
|
2567
|
+
meterEvents,
|
|
2568
|
+
reportUsageEvents,
|
|
2569
|
+
productId,
|
|
2570
|
+
backendId,
|
|
2571
|
+
jwksUrl: DEV_JWKS_URL
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
function json(body, status = 200) {
|
|
2575
|
+
return new Response(JSON.stringify(body), {
|
|
2576
|
+
status,
|
|
2577
|
+
headers: { "content-type": "application/json" }
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
async function readJsonBody(init, input) {
|
|
2581
|
+
try {
|
|
2582
|
+
if (init?.body && typeof init.body === "string") {
|
|
2583
|
+
return JSON.parse(init.body);
|
|
2584
|
+
}
|
|
2585
|
+
if (input instanceof Request) {
|
|
2586
|
+
return await input.clone().json();
|
|
2587
|
+
}
|
|
2588
|
+
} catch {
|
|
2589
|
+
return null;
|
|
2013
2590
|
}
|
|
2014
|
-
return
|
|
2591
|
+
return null;
|
|
2015
2592
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2593
|
+
|
|
2594
|
+
// src/testing/devRuntime.ts
|
|
2595
|
+
import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
2596
|
+
import { dirname as dirname2 } from "node:path";
|
|
2597
|
+
|
|
2598
|
+
// src/testing/prodGuard.ts
|
|
2599
|
+
function isProductionEnv(env = readProcessEnv2()) {
|
|
2600
|
+
return (env.NODE_ENV ?? "").trim().toLowerCase() === "production";
|
|
2601
|
+
}
|
|
2602
|
+
var DevModeInProductionError = class extends Error {
|
|
2603
|
+
constructor(context) {
|
|
2604
|
+
super(
|
|
2605
|
+
`Farther Shore dev mode (${context}) is disabled in this process: NODE_ENV=production. Dev mode mints ephemeral signing keys and signed identity and must never run in production.`
|
|
2022
2606
|
);
|
|
2607
|
+
this.name = "DevModeInProductionError";
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
function assertNotProduction(context, env = readProcessEnv2()) {
|
|
2611
|
+
if (isProductionEnv(env)) {
|
|
2612
|
+
throw new DevModeInProductionError(context);
|
|
2023
2613
|
}
|
|
2024
|
-
return token;
|
|
2025
2614
|
}
|
|
2026
|
-
function
|
|
2615
|
+
function readProcessEnv2() {
|
|
2027
2616
|
const maybeProcess = globalThis.process;
|
|
2028
|
-
return maybeProcess?.env
|
|
2617
|
+
return maybeProcess?.env ?? {};
|
|
2029
2618
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2619
|
+
|
|
2620
|
+
// src/testing/usageSink.ts
|
|
2621
|
+
var DevUsageSink = class {
|
|
2622
|
+
events = [];
|
|
2623
|
+
/** Record a signed response-metering payload (withUsage / computeMeteringHeaders). */
|
|
2624
|
+
recordResponse(payload, requestId) {
|
|
2625
|
+
const raw = payload.rawDimsUnits;
|
|
2626
|
+
const meters = raw && typeof raw === "object" ? raw : {};
|
|
2627
|
+
this.events.push({
|
|
2628
|
+
source: "response",
|
|
2629
|
+
meters: { ...meters },
|
|
2630
|
+
payload,
|
|
2631
|
+
...requestId ? { requestId } : {},
|
|
2632
|
+
at: Date.now()
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
/** Record a background `fs.meter()` event captured by the dev gateway. */
|
|
2636
|
+
recordMeterEvent(event) {
|
|
2637
|
+
this.events.push({
|
|
2638
|
+
source: "meter",
|
|
2639
|
+
meters: { [event.meter]: event.qty },
|
|
2640
|
+
event,
|
|
2641
|
+
...event.request_id ? { requestId: event.request_id } : {},
|
|
2642
|
+
at: Date.now()
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
/** Record an attested post-stream report captured by the dev gateway. */
|
|
2646
|
+
recordReportUsage(event) {
|
|
2647
|
+
this.events.push({
|
|
2648
|
+
source: "reportUsage",
|
|
2649
|
+
meters: { ...event.meters },
|
|
2650
|
+
event,
|
|
2651
|
+
requestId: event.requestId,
|
|
2652
|
+
at: Date.now()
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
/** Total quantity per meter key across every recorded event. */
|
|
2656
|
+
byMeter() {
|
|
2657
|
+
const out = {};
|
|
2658
|
+
for (const evt of this.events) {
|
|
2659
|
+
for (const [meter, qty] of Object.entries(evt.meters)) {
|
|
2660
|
+
out[meter] = (out[meter] ?? 0) + qty;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return out;
|
|
2664
|
+
}
|
|
2665
|
+
/** Clear all recorded usage. */
|
|
2666
|
+
reset() {
|
|
2667
|
+
this.events.length = 0;
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
|
|
2671
|
+
// src/testing/traceSink.ts
|
|
2672
|
+
function redactValue(input) {
|
|
2673
|
+
return input.replace(/fsrt_(?:live|test)_[A-Za-z0-9_-]+/g, "fsrt_[redacted]").replace(/fsk_[A-Za-z0-9_-]+/g, "fsk_[redacted]").replace(/fsc_[A-Za-z0-9_-]+/g, "fsc_[redacted]").replace(/[Bb]earer\s+[A-Za-z0-9._-]+/g, "Bearer [redacted]");
|
|
2674
|
+
}
|
|
2675
|
+
var DevTraceSink = class {
|
|
2676
|
+
traces = /* @__PURE__ */ new Map();
|
|
2677
|
+
flushed = /* @__PURE__ */ new Set();
|
|
2678
|
+
appendLine;
|
|
2679
|
+
constructor(options = {}) {
|
|
2680
|
+
this.appendLine = options.appendLine;
|
|
2681
|
+
}
|
|
2682
|
+
ensure(requestId, mode) {
|
|
2683
|
+
let trace = this.traces.get(requestId);
|
|
2684
|
+
if (!trace) {
|
|
2685
|
+
trace = { requestId, mode, authz: [], metering: [] };
|
|
2686
|
+
this.traces.set(requestId, trace);
|
|
2687
|
+
}
|
|
2688
|
+
return trace;
|
|
2689
|
+
}
|
|
2690
|
+
/** Record how a request's signature/context verification resolved. */
|
|
2691
|
+
recordVerification(requestId, mode, outcome, fields = {}) {
|
|
2692
|
+
const trace = this.ensure(requestId, mode);
|
|
2693
|
+
if (fields.method) trace.method = fields.method;
|
|
2694
|
+
if (fields.path) trace.path = fields.path;
|
|
2695
|
+
if (fields.persona) trace.persona = fields.persona;
|
|
2696
|
+
trace.verification = {
|
|
2697
|
+
outcome,
|
|
2698
|
+
...fields.reason ? { reason: redactValue(fields.reason) } : {}
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
/** Record a single hasPermission / requirePermission decision. */
|
|
2702
|
+
recordAuthz(requestId, mode, entry) {
|
|
2703
|
+
const trace = this.ensure(requestId, mode);
|
|
2704
|
+
trace.authz.push({
|
|
2705
|
+
permission: entry.permission,
|
|
2706
|
+
decision: entry.decision,
|
|
2707
|
+
...entry.reason ? { reason: redactValue(entry.reason) } : {}
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
/** Record usage reported for this request. */
|
|
2711
|
+
recordMetering(requestId, mode, entry) {
|
|
2712
|
+
const trace = this.ensure(requestId, mode);
|
|
2713
|
+
trace.metering.push(entry);
|
|
2714
|
+
}
|
|
2715
|
+
/** Record the final response status and flush the trace as one JSONL line. */
|
|
2716
|
+
recordResponse(requestId, mode, status) {
|
|
2717
|
+
const trace = this.ensure(requestId, mode);
|
|
2718
|
+
trace.response = { status };
|
|
2719
|
+
this.flush(requestId);
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Append a trace's current state as one JSONL line (if a sink is wired).
|
|
2723
|
+
* Flushes at most ONCE per request id, so wrapping several response methods
|
|
2724
|
+
* (status/json/end) never produces duplicate JSONL lines.
|
|
2725
|
+
*/
|
|
2726
|
+
flush(requestId) {
|
|
2727
|
+
if (this.flushed.has(requestId)) return;
|
|
2728
|
+
const trace = this.traces.get(requestId);
|
|
2729
|
+
if (trace && this.appendLine) {
|
|
2730
|
+
this.appendLine(JSON.stringify(trace));
|
|
2731
|
+
this.flushed.add(requestId);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
/** The accumulated trace for a request id, or `undefined`. */
|
|
2735
|
+
forRequest(requestId) {
|
|
2736
|
+
return this.traces.get(requestId);
|
|
2737
|
+
}
|
|
2738
|
+
/** Every accumulated trace (insertion order). */
|
|
2739
|
+
all() {
|
|
2740
|
+
return [...this.traces.values()];
|
|
2741
|
+
}
|
|
2742
|
+
/** Clear all traces. */
|
|
2743
|
+
reset() {
|
|
2744
|
+
this.traces.clear();
|
|
2745
|
+
this.flushed.clear();
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
|
|
2749
|
+
// src/testing/keysFile.ts
|
|
2750
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2751
|
+
import { dirname } from "node:path";
|
|
2752
|
+
var DEFAULT_KEYS_FILE = ".farthershore/dev-keys.json";
|
|
2753
|
+
function writeDevKeysFile(path, contents) {
|
|
2754
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2755
|
+
writeFileSync(path, JSON.stringify(contents, null, 2), { mode: 384 });
|
|
2756
|
+
chmodSync(path, 384);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// src/testing/devRuntime.ts
|
|
2760
|
+
function createDevRuntime(options) {
|
|
2761
|
+
assertNotProduction("createDevRuntime", options.env);
|
|
2762
|
+
const mode = options.mode;
|
|
2763
|
+
const keys = options.keys ?? generateDevSignerKeys();
|
|
2764
|
+
const usage = new DevUsageSink();
|
|
2765
|
+
const trace = new DevTraceSink(
|
|
2766
|
+
options.traceJsonl ? { appendLine: options.traceJsonl } : {}
|
|
2042
2767
|
);
|
|
2043
|
-
|
|
2768
|
+
const gateway = createDevGateway({
|
|
2769
|
+
mode,
|
|
2770
|
+
keys,
|
|
2771
|
+
...options.productId ? { productId: options.productId } : {},
|
|
2772
|
+
...options.backendId ? { backendId: options.backendId } : {},
|
|
2773
|
+
...options.routes ? { routeIds: options.routes } : {},
|
|
2774
|
+
onMeterEvent: (event) => {
|
|
2775
|
+
usage.recordMeterEvent(event);
|
|
2776
|
+
options.usageJsonl?.(JSON.stringify({ source: "meter", event }));
|
|
2777
|
+
},
|
|
2778
|
+
onReportUsage: (event) => {
|
|
2779
|
+
usage.recordReportUsage(event);
|
|
2780
|
+
options.usageJsonl?.(JSON.stringify({ source: "reportUsage", event }));
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
__setDevMeteringHooks({
|
|
2784
|
+
fallbackToken: () => keys.runtimeToken,
|
|
2785
|
+
record: (payload, requestId) => {
|
|
2786
|
+
usage.recordResponse(payload, requestId);
|
|
2787
|
+
options.usageJsonl?.(JSON.stringify({ source: "response", payload }));
|
|
2788
|
+
if (requestId) {
|
|
2789
|
+
const raw = payload.rawDimsUnits;
|
|
2790
|
+
trace.recordMetering(requestId, mode, {
|
|
2791
|
+
meters: raw && typeof raw === "object" ? raw : {},
|
|
2792
|
+
source: "response"
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
const personas = buildPersonaMap(options.personas);
|
|
2798
|
+
const personaClient = createPersonaClient({
|
|
2799
|
+
keys,
|
|
2800
|
+
productId: gateway.productId,
|
|
2801
|
+
backendId: gateway.backendId,
|
|
2802
|
+
contextSecret: keys.contextSecret,
|
|
2803
|
+
contextKid: keys.contextKid,
|
|
2804
|
+
personas,
|
|
2805
|
+
mode,
|
|
2806
|
+
...options.appFetch ? { fetchImpl: options.appFetch } : {}
|
|
2807
|
+
});
|
|
2808
|
+
const fs = initFromEnv({
|
|
2809
|
+
runtimeToken: keys.runtimeToken,
|
|
2810
|
+
coreUrl: DEV_CORE_URL,
|
|
2811
|
+
fetchImpl: gateway.fetchImpl,
|
|
2812
|
+
contextSecrets: [keys.contextSecret],
|
|
2813
|
+
contextVerification: mode === "simulated" ? "required" : "preferred",
|
|
2814
|
+
env: {}
|
|
2815
|
+
});
|
|
2816
|
+
const tracedAuthz = {
|
|
2817
|
+
hasPermission(ctx, key2) {
|
|
2818
|
+
const decision = hasPermission(ctx, key2);
|
|
2819
|
+
recordDecision(ctx, key2, decision);
|
|
2820
|
+
return decision;
|
|
2821
|
+
},
|
|
2822
|
+
requirePermission(ctx, key2) {
|
|
2823
|
+
const decision = hasPermission(ctx, key2);
|
|
2824
|
+
recordDecision(ctx, key2, decision);
|
|
2825
|
+
if (!decision) throw new FartherShorePermissionError(key2);
|
|
2826
|
+
requirePermission(ctx, key2);
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
function recordDecision(ctx, key2, decision) {
|
|
2830
|
+
const requestId = ctx.requestId ?? "unknown";
|
|
2831
|
+
const subject = ctx.signedContext?.subjectKey ?? "unknown";
|
|
2832
|
+
trace.recordAuthz(requestId, mode, {
|
|
2833
|
+
permission: key2,
|
|
2834
|
+
decision: decision ? "allow" : "deny",
|
|
2835
|
+
...decision ? {} : { reason: `permission_denied: ${key2} (subject=${subject})` }
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
function middleware(mwOptions) {
|
|
2839
|
+
const inner = createExpressMiddleware(fs, mwOptions);
|
|
2840
|
+
return (req, res, next) => {
|
|
2841
|
+
const requestId = headerValue2(req.headers, "x-fs-request-id") ?? "unknown";
|
|
2842
|
+
const { path } = splitUrl2(req);
|
|
2843
|
+
const method = req.method;
|
|
2844
|
+
let nextCalled = false;
|
|
2845
|
+
let lastStatus = 200;
|
|
2846
|
+
const origStatus = res.status.bind(res);
|
|
2847
|
+
res.status = (code) => {
|
|
2848
|
+
lastStatus = code;
|
|
2849
|
+
return origStatus(code);
|
|
2850
|
+
};
|
|
2851
|
+
const origJson = res.json.bind(res);
|
|
2852
|
+
res.json = (body) => {
|
|
2853
|
+
if (!nextCalled) {
|
|
2854
|
+
const reason = typeof body === "object" && body !== null && "error" in body ? String(body.error) : void 0;
|
|
2855
|
+
trace.recordVerification(requestId, mode, "rejected", {
|
|
2856
|
+
method,
|
|
2857
|
+
path,
|
|
2858
|
+
...reason ? { reason } : {}
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
trace.recordResponse(requestId, mode, lastStatus);
|
|
2862
|
+
return origJson(body);
|
|
2863
|
+
};
|
|
2864
|
+
const wrappedNext = (err) => {
|
|
2865
|
+
nextCalled = true;
|
|
2866
|
+
trace.recordVerification(
|
|
2867
|
+
requestId,
|
|
2868
|
+
mode,
|
|
2869
|
+
mode === "simulated" ? "verified" : "passthrough",
|
|
2870
|
+
{ method, path }
|
|
2871
|
+
);
|
|
2872
|
+
next(err);
|
|
2873
|
+
};
|
|
2874
|
+
inner(req, res, wrappedNext);
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
fs.middleware = middleware;
|
|
2878
|
+
const devRuntime = {
|
|
2879
|
+
fs,
|
|
2880
|
+
asPersona: (name) => personaClient.asPersona(name),
|
|
2881
|
+
usage,
|
|
2882
|
+
trace,
|
|
2883
|
+
gateway,
|
|
2884
|
+
keys,
|
|
2885
|
+
personas,
|
|
2886
|
+
mode,
|
|
2887
|
+
bootstrap: gateway.bootstrap,
|
|
2888
|
+
authz: tracedAuthz,
|
|
2889
|
+
middleware,
|
|
2890
|
+
reset() {
|
|
2891
|
+
usage.reset();
|
|
2892
|
+
trace.reset();
|
|
2893
|
+
gateway.meterEvents.length = 0;
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
fs.dev = devRuntime;
|
|
2897
|
+
return devRuntime;
|
|
2898
|
+
}
|
|
2899
|
+
var USAGE_JSONL_PATH = ".farthershore/dev-usage.jsonl";
|
|
2900
|
+
var TRACE_JSONL_DEFAULT = ".farthershore/dev-trace.jsonl";
|
|
2901
|
+
var PERSONAS_FILE = "fs.dev.personas.json";
|
|
2902
|
+
function devModeFromEnv(env) {
|
|
2903
|
+
const raw = (env.FS_DEV_MODE ?? "").trim().toLowerCase();
|
|
2904
|
+
if (raw === "passthrough" || raw === "simulated") return raw;
|
|
2905
|
+
return null;
|
|
2044
2906
|
}
|
|
2045
|
-
function
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2907
|
+
function createDevRuntimeFromEnv(env = readProcessEnv3()) {
|
|
2908
|
+
const mode = devModeFromEnv(env);
|
|
2909
|
+
if (!mode) {
|
|
2910
|
+
throw new Error(
|
|
2911
|
+
"createDevRuntimeFromEnv called without FS_DEV_MODE=passthrough|simulated"
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
assertNotProduction("FS_DEV_MODE", env);
|
|
2915
|
+
const keys = generateDevSignerKeys();
|
|
2916
|
+
const personas = loadPersonasFile();
|
|
2917
|
+
const tracePath = env.FS_DEV_TRACE?.trim() || TRACE_JSONL_DEFAULT;
|
|
2918
|
+
const runtime = createDevRuntime({
|
|
2919
|
+
mode,
|
|
2920
|
+
...personas ? { personas } : {},
|
|
2921
|
+
keys,
|
|
2922
|
+
env,
|
|
2923
|
+
usageJsonl: (line) => appendJsonl(USAGE_JSONL_PATH, line),
|
|
2924
|
+
traceJsonl: (line) => appendJsonl(tracePath, line)
|
|
2925
|
+
});
|
|
2926
|
+
const keysFile = {
|
|
2927
|
+
version: 1,
|
|
2928
|
+
mode,
|
|
2929
|
+
keys,
|
|
2930
|
+
productId: runtime.gateway.productId,
|
|
2931
|
+
backendId: runtime.gateway.backendId,
|
|
2932
|
+
personas: mapToRecord(runtime.personas)
|
|
2933
|
+
};
|
|
2934
|
+
writeDevKeysFile(DEFAULT_KEYS_FILE, keysFile);
|
|
2935
|
+
printBanner(mode, runtime, tracePath);
|
|
2936
|
+
return runtime.fs;
|
|
2937
|
+
}
|
|
2938
|
+
function printBanner(mode, runtime, tracePath) {
|
|
2939
|
+
const lines = [
|
|
2940
|
+
"============================================================",
|
|
2941
|
+
" \u26A0 FARTHER SHORE DEV MODE ACTIVE \u2014 NOT FOR PRODUCTION",
|
|
2942
|
+
` mode: ${mode.toUpperCase()}`,
|
|
2943
|
+
` product: ${runtime.gateway.productId}`,
|
|
2944
|
+
` backend: ${runtime.gateway.backendId}`,
|
|
2945
|
+
` personas: ${[...runtime.personas.keys()].join(", ")}`,
|
|
2946
|
+
` usage log: ${USAGE_JSONL_PATH}`,
|
|
2947
|
+
` trace log: ${tracePath}`,
|
|
2948
|
+
` dev keys: ${DEFAULT_KEYS_FILE} (mode 600, ephemeral)`,
|
|
2949
|
+
mode === "simulated" ? " verification REQUIRED \u2014 requests must carry a signed persona." : " verification OFF (passthrough) \u2014 middleware passes through.",
|
|
2950
|
+
"============================================================"
|
|
2951
|
+
];
|
|
2952
|
+
console.warn(lines.join("\n"));
|
|
2953
|
+
}
|
|
2954
|
+
function loadPersonasFile() {
|
|
2955
|
+
if (!existsSync(PERSONAS_FILE)) return void 0;
|
|
2956
|
+
try {
|
|
2957
|
+
return JSON.parse(readFileSync2(PERSONAS_FILE, "utf8"));
|
|
2958
|
+
} catch {
|
|
2959
|
+
return void 0;
|
|
2049
2960
|
}
|
|
2050
|
-
|
|
2961
|
+
}
|
|
2962
|
+
function appendJsonl(path, line) {
|
|
2963
|
+
try {
|
|
2964
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2965
|
+
appendFileSync(path, `${line}
|
|
2966
|
+
`);
|
|
2967
|
+
} catch {
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
function mapToRecord(map) {
|
|
2971
|
+
const out = {};
|
|
2972
|
+
for (const [name, def] of map) out[name] = def;
|
|
2973
|
+
return out;
|
|
2974
|
+
}
|
|
2975
|
+
function readProcessEnv3() {
|
|
2976
|
+
const maybeProcess = globalThis.process;
|
|
2977
|
+
return maybeProcess?.env ?? {};
|
|
2978
|
+
}
|
|
2979
|
+
function headerValue2(headers, name) {
|
|
2980
|
+
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
2981
|
+
if (Array.isArray(value)) return value[0];
|
|
2982
|
+
return value;
|
|
2983
|
+
}
|
|
2984
|
+
function splitUrl2(req) {
|
|
2985
|
+
const raw = req.originalUrl ?? req.url ?? req.path ?? "/";
|
|
2986
|
+
const qIndex = raw.indexOf("?");
|
|
2987
|
+
if (qIndex === -1) return { path: raw, query: "" };
|
|
2988
|
+
return { path: raw.slice(0, qIndex), query: raw.slice(qIndex + 1) };
|
|
2051
2989
|
}
|
|
2052
2990
|
|
|
2053
2991
|
// src/index.ts
|
|
2992
|
+
function readProcessEnv4() {
|
|
2993
|
+
const maybeProcess = globalThis.process;
|
|
2994
|
+
return maybeProcess?.env ?? {};
|
|
2995
|
+
}
|
|
2054
2996
|
var fartherShore = {
|
|
2055
2997
|
/** Derive everything from FS_RUNTIME_TOKEN via bootstrap. */
|
|
2056
2998
|
initFromEnv(options = {}) {
|
|
2999
|
+
const env = options.env ?? readProcessEnv4();
|
|
3000
|
+
if (devModeFromEnv(env)) {
|
|
3001
|
+
return createDevRuntimeFromEnv(env);
|
|
3002
|
+
}
|
|
2057
3003
|
const fs = initFromEnv(options);
|
|
2058
3004
|
fs.middleware = (mwOptions) => createExpressMiddleware(fs, mwOptions);
|
|
2059
3005
|
return fs;
|
|
@@ -2080,6 +3026,7 @@ export {
|
|
|
2080
3026
|
MeteringClient,
|
|
2081
3027
|
MeteringError,
|
|
2082
3028
|
NonceCache,
|
|
3029
|
+
PostStreamUsageClient,
|
|
2083
3030
|
REDACTED_TOKEN,
|
|
2084
3031
|
RUNTIME_CLOCK_SKEW_SECONDS,
|
|
2085
3032
|
RUNTIME_ERROR_CODES,
|
|
@@ -2093,6 +3040,7 @@ export {
|
|
|
2093
3040
|
buildCanonicalSigningString2 as buildCanonicalSigningString,
|
|
2094
3041
|
buildHealthReport,
|
|
2095
3042
|
canonicalizeQuery2 as canonicalizeQuery,
|
|
3043
|
+
computeMeteringHeaders,
|
|
2096
3044
|
createExpressMiddleware,
|
|
2097
3045
|
createUsage,
|
|
2098
3046
|
fartherShore,
|