@farthershore/backend 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/README.md +239 -91
- package/dist/adapters/express.js +68 -12
- package/dist/generated/runtime-contract.js +21 -236
- package/dist/index.js +519 -417
- package/dist/internal/index.js +587 -0
- package/dist/testing/index.js +614 -302
- package/dist/types/adapters/express.d.ts +32 -3
- package/dist/types/core/permissions.d.ts +25 -13
- package/dist/types/core/post-stream-usage.d.ts +19 -4
- package/dist/types/core/report.d.ts +133 -0
- package/dist/types/core/runtime.d.ts +25 -12
- package/dist/types/core/verifyRequest.d.ts +21 -3
- package/dist/types/generated/runtime-contract.d.ts +14 -189
- package/dist/types/index.d.ts +29 -8
- package/dist/types/internal/index.d.ts +2 -0
- package/dist/types/response-metering.d.ts +29 -39
- package/dist/types/runtime-types.d.ts +16 -1
- package/dist/types/testing/devRuntime.d.ts +11 -2
- package/dist/types/testing/index.d.ts +1 -0
- package/dist/types/testing/usageSink.d.ts +1 -1
- package/dist/types/testing/webhooks.d.ts +30 -0
- package/dist/types/webhooks/index.d.ts +247 -0
- package/dist/types/webhooks/types.d.ts +110 -0
- package/dist/webhooks/index.js +498 -0
- package/package.json +12 -3
- package/dist/types/core/metering.d.ts +0 -68
package/dist/index.js
CHANGED
|
@@ -191,20 +191,6 @@ var init_reconcile = __esm({
|
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
// src/generated/runtime-contract.ts
|
|
194
|
-
var RUNTIME_BODY_HASH_CONTRACT = {
|
|
195
|
-
algorithm: "SHA-256",
|
|
196
|
-
encoding: "hex-lower",
|
|
197
|
-
source: "raw-request-bytes",
|
|
198
|
-
emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
199
|
-
maxBodyBytes: 10485760,
|
|
200
|
-
streamingExemptToken: "STREAM",
|
|
201
|
-
streamingExemptContentTypes: [
|
|
202
|
-
"text/event-stream",
|
|
203
|
-
"application/octet-stream",
|
|
204
|
-
"multipart/form-data"
|
|
205
|
-
],
|
|
206
|
-
overMaxStatus: 413
|
|
207
|
-
};
|
|
208
194
|
var RUNTIME_ERROR_CODES = {
|
|
209
195
|
missingSignature: "missing_signature",
|
|
210
196
|
malformedSignature: "malformed_signature",
|
|
@@ -225,6 +211,20 @@ var RUNTIME_ERROR_CODES = {
|
|
|
225
211
|
serviceSubjectRequired: "service_subject_required",
|
|
226
212
|
surfaceNotAllowed: "surface_not_allowed"
|
|
227
213
|
};
|
|
214
|
+
var RUNTIME_BODY_HASH_CONTRACT = {
|
|
215
|
+
algorithm: "SHA-256",
|
|
216
|
+
encoding: "hex-lower",
|
|
217
|
+
source: "raw-request-bytes",
|
|
218
|
+
emptyBodyHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
219
|
+
maxBodyBytes: 10485760,
|
|
220
|
+
streamingExemptToken: "STREAM",
|
|
221
|
+
streamingExemptContentTypes: [
|
|
222
|
+
"text/event-stream",
|
|
223
|
+
"application/octet-stream",
|
|
224
|
+
"multipart/form-data"
|
|
225
|
+
],
|
|
226
|
+
overMaxStatus: 413
|
|
227
|
+
};
|
|
228
228
|
var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
229
229
|
headers: {
|
|
230
230
|
payload: "x-fs-metering",
|
|
@@ -245,14 +245,18 @@ var RUNTIME_RESPONSE_METERING_CONTRACT = {
|
|
|
245
245
|
payload: {
|
|
246
246
|
method: "string",
|
|
247
247
|
path: "string",
|
|
248
|
-
rawDimsUnits: "Record<string, number
|
|
248
|
+
rawDimsUnits: "Record<string, number>?",
|
|
249
249
|
measureContext: "Record<string, unknown>?",
|
|
250
|
-
creditUnitsConsumed: "Record<string, number>?"
|
|
250
|
+
creditUnitsConsumed: "Record<string, number>?",
|
|
251
|
+
measurementsVersion: "1?",
|
|
252
|
+
measurements: "Array<{ meter: string; values: Record<string, number>; dims?: Record<string, string> }>?",
|
|
253
|
+
quote: "{ currency: string; amountNanos: string }?"
|
|
251
254
|
},
|
|
252
255
|
errors: {
|
|
253
256
|
missingToken: "missing_token",
|
|
254
257
|
invalidMeterKey: "invalid_meter_key",
|
|
255
|
-
invalidMeterValue: "invalid_meter_value"
|
|
258
|
+
invalidMeterValue: "invalid_meter_value",
|
|
259
|
+
invalidQuote: "invalid_quote"
|
|
256
260
|
},
|
|
257
261
|
httpAdapter: {
|
|
258
262
|
input: "Request",
|
|
@@ -902,185 +906,6 @@ function stringifyCause(cause) {
|
|
|
902
906
|
return String(cause);
|
|
903
907
|
}
|
|
904
908
|
|
|
905
|
-
// src/core/backoff.ts
|
|
906
|
-
function computeBackoff(attempt, options) {
|
|
907
|
-
const { baseMs, maxMs, jitter = "equal", random = Math.random } = options;
|
|
908
|
-
const exponent = Math.max(0, attempt - 1);
|
|
909
|
-
const cap = Math.min(baseMs * 2 ** exponent, maxMs);
|
|
910
|
-
switch (jitter) {
|
|
911
|
-
case "none":
|
|
912
|
-
return cap;
|
|
913
|
-
case "full":
|
|
914
|
-
return random() * cap;
|
|
915
|
-
case "equal":
|
|
916
|
-
default:
|
|
917
|
-
return cap / 2 + random() * (cap / 2);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// src/core/metering.ts
|
|
922
|
-
var METER_KEY_RE = /^[a-z0-9_]{1,64}$/;
|
|
923
|
-
var DEFAULT_BASE_DELAY_MS = 200;
|
|
924
|
-
var DEFAULT_MAX_DELAY_MS = 1e4;
|
|
925
|
-
function isTransientStatus(status) {
|
|
926
|
-
return status === 429 || status >= 500;
|
|
927
|
-
}
|
|
928
|
-
function retryAfterMs(headers) {
|
|
929
|
-
const raw = headers.get("retry-after");
|
|
930
|
-
if (raw === null) return null;
|
|
931
|
-
const trimmed = raw.trim();
|
|
932
|
-
if (!/^\d+$/.test(trimmed)) return null;
|
|
933
|
-
const secs = Number(trimmed);
|
|
934
|
-
return Number.isFinite(secs) ? secs * 1e3 : null;
|
|
935
|
-
}
|
|
936
|
-
var DEFAULT_MAX_RETRIES = 3;
|
|
937
|
-
var MeteringClient = class {
|
|
938
|
-
config;
|
|
939
|
-
endpoint;
|
|
940
|
-
businessId;
|
|
941
|
-
backendId;
|
|
942
|
-
fetchImpl;
|
|
943
|
-
maxRetries;
|
|
944
|
-
baseDelayMs;
|
|
945
|
-
maxDelayMs;
|
|
946
|
-
sleep;
|
|
947
|
-
random;
|
|
948
|
-
newId;
|
|
949
|
-
now;
|
|
950
|
-
buffer = [];
|
|
951
|
-
constructor(options) {
|
|
952
|
-
this.config = options.config;
|
|
953
|
-
this.endpoint = resolveEndpoint(options.config.endpoint, options.coreUrl);
|
|
954
|
-
this.businessId = options.businessId;
|
|
955
|
-
this.backendId = options.backendId;
|
|
956
|
-
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
957
|
-
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
958
|
-
this.baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
959
|
-
this.maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
960
|
-
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
961
|
-
this.random = options.random ?? Math.random;
|
|
962
|
-
this.newId = options.newId ?? (() => crypto.randomUUID());
|
|
963
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Record `qty` of `meter`. Enforces meter-key shape, non-negative finite qty,
|
|
967
|
-
* the bootstrap allowedMeters/allowedRoutes scope, and the per-event sanity
|
|
968
|
-
* max, then enqueues and flushes (best-effort; failures stay buffered).
|
|
969
|
-
*/
|
|
970
|
-
async meter(meter, qty, options = {}) {
|
|
971
|
-
if (!this.config.enabled) {
|
|
972
|
-
throw new FartherShoreError(
|
|
973
|
-
"invalid_token",
|
|
974
|
-
"metering is not enabled for this runtime token"
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
if (!METER_KEY_RE.test(meter)) {
|
|
978
|
-
throw new FartherShoreError(
|
|
979
|
-
"invalid_token",
|
|
980
|
-
`meter key '${meter}' must be lowercase alphanumeric with underscores`
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
if (!Number.isFinite(qty) || qty < 0) {
|
|
984
|
-
throw new FartherShoreError(
|
|
985
|
-
"invalid_token",
|
|
986
|
-
`meter '${meter}' qty must be a non-negative finite number`
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
if (this.config.allowedMeters.length > 0 && !this.config.allowedMeters.includes(meter)) {
|
|
990
|
-
throw new FartherShoreError(
|
|
991
|
-
"invalid_token",
|
|
992
|
-
`meter '${meter}' is not in the token's allowedMeters`
|
|
993
|
-
);
|
|
994
|
-
}
|
|
995
|
-
if (this.config.allowedRoutes.length > 0) {
|
|
996
|
-
if (!options.routeId) {
|
|
997
|
-
throw new FartherShoreError(
|
|
998
|
-
"invalid_token",
|
|
999
|
-
"routeId is required because this runtime token is route-scoped"
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
if (!this.config.allowedRoutes.includes(options.routeId)) {
|
|
1003
|
-
throw new FartherShoreError(
|
|
1004
|
-
"invalid_token",
|
|
1005
|
-
`route '${options.routeId}' is not in the token's allowedRoutes`
|
|
1006
|
-
);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
if (this.config.perEventMax > 0 && qty > this.config.perEventMax) {
|
|
1010
|
-
throw new FartherShoreError(
|
|
1011
|
-
"invalid_token",
|
|
1012
|
-
`meter '${meter}' qty ${qty} exceeds the per-event max ${this.config.perEventMax}`
|
|
1013
|
-
);
|
|
1014
|
-
}
|
|
1015
|
-
const event = {
|
|
1016
|
-
event_id: options.eventId ?? this.newId(),
|
|
1017
|
-
business_id: this.businessId,
|
|
1018
|
-
backend_id: this.backendId,
|
|
1019
|
-
meter,
|
|
1020
|
-
qty,
|
|
1021
|
-
timestamp: options.timestamp ?? this.now().toISOString(),
|
|
1022
|
-
...options.routeId ? { route_id: options.routeId } : {},
|
|
1023
|
-
...options.requestId ? { request_id: options.requestId } : {},
|
|
1024
|
-
...options.subscriptionId ? { subscription_id: options.subscriptionId } : {}
|
|
1025
|
-
};
|
|
1026
|
-
this.buffer.push(event);
|
|
1027
|
-
await this.flush();
|
|
1028
|
-
}
|
|
1029
|
-
/** Drain the buffer. Events that fail all retries stay buffered (at-least-once). */
|
|
1030
|
-
async flush() {
|
|
1031
|
-
const pending = this.buffer.splice(0, this.buffer.length);
|
|
1032
|
-
const stillPending = [];
|
|
1033
|
-
for (const event of pending) {
|
|
1034
|
-
const sent = await this.sendWithRetry(event);
|
|
1035
|
-
if (!sent) stillPending.push(event);
|
|
1036
|
-
}
|
|
1037
|
-
if (stillPending.length > 0) this.buffer.unshift(...stillPending);
|
|
1038
|
-
}
|
|
1039
|
-
/** Buffered-but-unsent count (observability/tests). */
|
|
1040
|
-
get pending() {
|
|
1041
|
-
return this.buffer.length;
|
|
1042
|
-
}
|
|
1043
|
-
async sendWithRetry(event) {
|
|
1044
|
-
for (let attempt = 0; attempt < this.maxRetries; attempt += 1) {
|
|
1045
|
-
let retryAfter = null;
|
|
1046
|
-
try {
|
|
1047
|
-
const response = await fetchWithDeadline(
|
|
1048
|
-
this.fetchImpl,
|
|
1049
|
-
this.endpoint,
|
|
1050
|
-
{
|
|
1051
|
-
method: "POST",
|
|
1052
|
-
headers: {
|
|
1053
|
-
authorization: `Bearer ${this.config.credential}`,
|
|
1054
|
-
"content-type": "application/json",
|
|
1055
|
-
accept: "application/json"
|
|
1056
|
-
},
|
|
1057
|
-
body: JSON.stringify(event)
|
|
1058
|
-
},
|
|
1059
|
-
"metering"
|
|
1060
|
-
);
|
|
1061
|
-
if (response.ok) return true;
|
|
1062
|
-
if (!isTransientStatus(response.status)) return false;
|
|
1063
|
-
retryAfter = retryAfterMs(response.headers);
|
|
1064
|
-
} catch {
|
|
1065
|
-
}
|
|
1066
|
-
const isLast = attempt === this.maxRetries - 1;
|
|
1067
|
-
if (isLast) break;
|
|
1068
|
-
const delay = retryAfter !== null ? Math.min(retryAfter, this.maxDelayMs) : computeBackoff(attempt + 1, {
|
|
1069
|
-
baseMs: this.baseDelayMs,
|
|
1070
|
-
maxMs: this.maxDelayMs,
|
|
1071
|
-
random: this.random
|
|
1072
|
-
});
|
|
1073
|
-
await this.sleep(delay);
|
|
1074
|
-
}
|
|
1075
|
-
return false;
|
|
1076
|
-
}
|
|
1077
|
-
};
|
|
1078
|
-
function resolveEndpoint(endpoint, coreUrl) {
|
|
1079
|
-
if (/^https?:\/\//.test(endpoint)) return endpoint;
|
|
1080
|
-
if (!coreUrl) return endpoint;
|
|
1081
|
-
return `${coreUrl.replace(/\/+$/, "")}${endpoint}`;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
909
|
// src/response-metering.ts
|
|
1085
910
|
var RESPONSE_METERING_HEADERS = RUNTIME_RESPONSE_METERING_CONTRACT.headers;
|
|
1086
911
|
var RESPONSE_METERING_ERROR_CODES = RUNTIME_RESPONSE_METERING_CONTRACT.errors;
|
|
@@ -1100,50 +925,6 @@ var MeteringError = class extends Error {
|
|
|
1100
925
|
this.code = code;
|
|
1101
926
|
}
|
|
1102
927
|
};
|
|
1103
|
-
function createUsage(request, options = {}) {
|
|
1104
|
-
const usage = {};
|
|
1105
|
-
const reporter = {
|
|
1106
|
-
report(meter, value) {
|
|
1107
|
-
usage[assertMeterKey(meter)] = assertMeterValue(meter, value);
|
|
1108
|
-
return reporter;
|
|
1109
|
-
},
|
|
1110
|
-
async wrap(response, wrapOptions = {}) {
|
|
1111
|
-
return signResponse(request, response, usage, options, wrapOptions);
|
|
1112
|
-
}
|
|
1113
|
-
};
|
|
1114
|
-
return reporter;
|
|
1115
|
-
}
|
|
1116
|
-
async function withUsage(request, response, usage, options = {}) {
|
|
1117
|
-
const reporter = createUsage(request, options);
|
|
1118
|
-
for (const [meter, value] of Object.entries(usage)) {
|
|
1119
|
-
reporter.report(meter, value);
|
|
1120
|
-
}
|
|
1121
|
-
return reporter.wrap(response);
|
|
1122
|
-
}
|
|
1123
|
-
async function signResponse(request, response, usage, options, wrapOptions) {
|
|
1124
|
-
const payload = buildPayload(request, usage, options, wrapOptions);
|
|
1125
|
-
const requestId = options.requestId ?? request.headers.get("x-fs-request-id") ?? void 0;
|
|
1126
|
-
const headers = await computeMeteringHeaders(payload, {
|
|
1127
|
-
...options.token !== void 0 ? { token: options.token } : {},
|
|
1128
|
-
...options.env !== void 0 ? { env: options.env } : {},
|
|
1129
|
-
...requestId ? { requestId } : {},
|
|
1130
|
-
onSkip: () => {
|
|
1131
|
-
}
|
|
1132
|
-
});
|
|
1133
|
-
if (Object.keys(headers).length === 0) {
|
|
1134
|
-
throw new MeteringError(
|
|
1135
|
-
RESPONSE_METERING_ERROR_CODES.missingToken,
|
|
1136
|
-
`${DEFAULT_TOKEN_ENV} is required to sign Farther Shore metering reports`
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
const merged = new Headers(response.headers);
|
|
1140
|
-
for (const [name, value] of Object.entries(headers)) merged.set(name, value);
|
|
1141
|
-
return new Response(response.body, {
|
|
1142
|
-
status: response.status,
|
|
1143
|
-
statusText: response.statusText,
|
|
1144
|
-
headers: merged
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
928
|
async function computeMeteringHeaders(payload, options = {}) {
|
|
1148
929
|
try {
|
|
1149
930
|
const token = resolveTokenSoft(options);
|
|
@@ -1175,67 +956,6 @@ function skip(reason, options) {
|
|
|
1175
956
|
function resolveTokenSoft(options) {
|
|
1176
957
|
return options.token ?? options.env?.[DEFAULT_TOKEN_ENV] ?? processEnv(DEFAULT_TOKEN_ENV) ?? devMeteringHooks?.fallbackToken?.();
|
|
1177
958
|
}
|
|
1178
|
-
function buildPayload(request, usage, options, wrapOptions) {
|
|
1179
|
-
const url = new URL(request.url);
|
|
1180
|
-
const measureContext = wrapOptions.measureContext ?? options.measureContext;
|
|
1181
|
-
const creditUnitsConsumed = wrapOptions.creditUnitsConsumed ?? options.creditUnitsConsumed;
|
|
1182
|
-
const operationKey = wrapOptions.operationKey ?? options.operationKey;
|
|
1183
|
-
const usagePolicyId = wrapOptions.usagePolicyId ?? options.usagePolicyId;
|
|
1184
|
-
const payload = {
|
|
1185
|
-
method: request.method.toUpperCase(),
|
|
1186
|
-
path: url.pathname,
|
|
1187
|
-
rawDimsUnits: sortUsage(usage),
|
|
1188
|
-
...measureContext ? { measureContext } : {},
|
|
1189
|
-
...creditUnitsConsumed ? {
|
|
1190
|
-
creditUnitsConsumed: sortUsage(
|
|
1191
|
-
validateUsageMap(creditUnitsConsumed, "creditUnitsConsumed")
|
|
1192
|
-
)
|
|
1193
|
-
} : {},
|
|
1194
|
-
...operationKey ? { operationKey: assertIdentifier(operationKey) } : {},
|
|
1195
|
-
...usagePolicyId ? { usagePolicyId: assertIdentifier(usagePolicyId) } : {}
|
|
1196
|
-
};
|
|
1197
|
-
return payload;
|
|
1198
|
-
}
|
|
1199
|
-
function sortUsage(usage) {
|
|
1200
|
-
return Object.fromEntries(
|
|
1201
|
-
Object.entries(usage).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
function validateUsageMap(usage, label) {
|
|
1205
|
-
return Object.fromEntries(
|
|
1206
|
-
Object.entries(usage).map(([meter, value]) => [
|
|
1207
|
-
assertMeterKey(meter),
|
|
1208
|
-
assertMeterValue(`${label}.${meter}`, value)
|
|
1209
|
-
])
|
|
1210
|
-
);
|
|
1211
|
-
}
|
|
1212
|
-
function assertMeterKey(meter) {
|
|
1213
|
-
if (!/^[a-z0-9_]{1,64}$/.test(meter)) {
|
|
1214
|
-
throw new MeteringError(
|
|
1215
|
-
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1216
|
-
`meter key "${meter}" must be lowercase alphanumeric with underscores`
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
return meter;
|
|
1220
|
-
}
|
|
1221
|
-
function assertMeterValue(meter, value) {
|
|
1222
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
1223
|
-
throw new MeteringError(
|
|
1224
|
-
RESPONSE_METERING_ERROR_CODES.invalidMeterValue,
|
|
1225
|
-
`meter "${meter}" value must be a non-negative finite number`
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
return value;
|
|
1229
|
-
}
|
|
1230
|
-
function assertIdentifier(value) {
|
|
1231
|
-
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(value)) {
|
|
1232
|
-
throw new MeteringError(
|
|
1233
|
-
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1234
|
-
`operation and usage policy identifiers must be 1-128 URL-safe characters`
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
return value;
|
|
1238
|
-
}
|
|
1239
959
|
function processEnv(key2) {
|
|
1240
960
|
const maybeProcess = globalThis.process;
|
|
1241
961
|
return maybeProcess?.env?.[key2];
|
|
@@ -1264,7 +984,7 @@ function base64url(bytes) {
|
|
|
1264
984
|
}
|
|
1265
985
|
|
|
1266
986
|
// src/core/post-stream-usage.ts
|
|
1267
|
-
var
|
|
987
|
+
var METER_KEY_RE = /^[a-z0-9_]{1,64}$/;
|
|
1268
988
|
var PostStreamUsageClient = class {
|
|
1269
989
|
config;
|
|
1270
990
|
endpoint;
|
|
@@ -1276,7 +996,7 @@ var PostStreamUsageClient = class {
|
|
|
1276
996
|
maxRetryDelayMs;
|
|
1277
997
|
constructor(options) {
|
|
1278
998
|
this.config = options.config;
|
|
1279
|
-
this.endpoint =
|
|
999
|
+
this.endpoint = resolveEndpoint(options.config.endpoint, options.coreUrl);
|
|
1280
1000
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1281
1001
|
this.newNonce = options.newNonce ?? (() => crypto.randomUUID());
|
|
1282
1002
|
this.logger = options.logger ?? ((message) => console.warn(message));
|
|
@@ -1293,6 +1013,11 @@ var PostStreamUsageClient = class {
|
|
|
1293
1013
|
requestId: input.requestId,
|
|
1294
1014
|
subscriptionId: input.subscriptionId,
|
|
1295
1015
|
nonce: this.newNonce(),
|
|
1016
|
+
// The token's `allowedMeters` scope is enforced on BOTH lanes
|
|
1017
|
+
// independently (P0-1): `meters` is the flat METER-keyed projection
|
|
1018
|
+
// (the billed lane), so its keys must be in scope regardless of
|
|
1019
|
+
// whether the measurement lane is also present; `measurements[].meter`
|
|
1020
|
+
// is scoped in validateMeasurements below.
|
|
1296
1021
|
meters: validateAndSortUsage(input.meters, "meters", this.config, true),
|
|
1297
1022
|
...input.creditUnitsConsumed ? {
|
|
1298
1023
|
creditUnitsConsumed: validateAndSortUsage(
|
|
@@ -1302,7 +1027,13 @@ var PostStreamUsageClient = class {
|
|
|
1302
1027
|
false
|
|
1303
1028
|
)
|
|
1304
1029
|
} : {},
|
|
1305
|
-
...input.measureContext ? { measureContext: input.measureContext } : {}
|
|
1030
|
+
...input.measureContext ? { measureContext: input.measureContext } : {},
|
|
1031
|
+
// Key ORDER is load-bearing: core recomputes the HMAC over
|
|
1032
|
+
// JSON.stringify(unsigned) rebuilt in its zod schema's field order, so
|
|
1033
|
+
// these additive fields must sit in the same position on both sides.
|
|
1034
|
+
...input.measurementsVersion !== void 0 ? { measurementsVersion: input.measurementsVersion } : {},
|
|
1035
|
+
...input.measurements ? { measurements: this.validateMeasurements(input.measurements) } : {},
|
|
1036
|
+
...input.quote ? { quote: input.quote } : {}
|
|
1306
1037
|
};
|
|
1307
1038
|
const signature = await signPayload(
|
|
1308
1039
|
JSON.stringify(unsigned),
|
|
@@ -1310,6 +1041,7 @@ var PostStreamUsageClient = class {
|
|
|
1310
1041
|
);
|
|
1311
1042
|
const event = { ...unsigned, signature };
|
|
1312
1043
|
const body = JSON.stringify(event);
|
|
1044
|
+
const headerSignature = await signPayload(body, this.config.credential);
|
|
1313
1045
|
for (let attempt = 0; ; attempt += 1) {
|
|
1314
1046
|
let response;
|
|
1315
1047
|
try {
|
|
@@ -1321,7 +1053,8 @@ var PostStreamUsageClient = class {
|
|
|
1321
1053
|
headers: {
|
|
1322
1054
|
authorization: `Bearer ${this.config.credential}`,
|
|
1323
1055
|
"content-type": "application/json",
|
|
1324
|
-
accept: "application/json"
|
|
1056
|
+
accept: "application/json",
|
|
1057
|
+
[RUNTIME_RESPONSE_METERING_CONTRACT.headers.signature]: headerSignature
|
|
1325
1058
|
},
|
|
1326
1059
|
body
|
|
1327
1060
|
},
|
|
@@ -1338,7 +1071,7 @@ var PostStreamUsageClient = class {
|
|
|
1338
1071
|
const retryable = requestNotFound || isRetryableStatus(response.status);
|
|
1339
1072
|
const delayMs = this.retryDelayForAttempt(
|
|
1340
1073
|
attempt,
|
|
1341
|
-
|
|
1074
|
+
retryAfterMs(response.headers)
|
|
1342
1075
|
);
|
|
1343
1076
|
if (!retryable || delayMs === null) {
|
|
1344
1077
|
throw new Error(`metering endpoint returned ${response.status}`);
|
|
@@ -1351,11 +1084,30 @@ var PostStreamUsageClient = class {
|
|
|
1351
1084
|
return { ok: false, reason };
|
|
1352
1085
|
}
|
|
1353
1086
|
}
|
|
1354
|
-
|
|
1087
|
+
/** Enforce the token's meter scope + per-event bounds on the measurement lane. */
|
|
1088
|
+
validateMeasurements(measurements) {
|
|
1089
|
+
const allowed = this.config.allowedMeters;
|
|
1090
|
+
for (const measurement of measurements) {
|
|
1091
|
+
if (allowed.length > 0 && !allowed.includes(measurement.meter)) {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
`meter '${measurement.meter}' is not in the token's allowedMeters`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
for (const [measure, value] of Object.entries(measurement.values)) {
|
|
1097
|
+
if (this.config.perEventMax > 0 && value > this.config.perEventMax) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`measure '${measure}' value ${value} exceeds the per-event max ${this.config.perEventMax}`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return measurements;
|
|
1105
|
+
}
|
|
1106
|
+
retryDelayForAttempt(attempt, retryAfterMs2) {
|
|
1355
1107
|
const fallback = this.retryDelaysMs[attempt];
|
|
1356
1108
|
if (fallback === void 0) return null;
|
|
1357
|
-
if (
|
|
1358
|
-
return Math.min(
|
|
1109
|
+
if (retryAfterMs2 === null) return fallback;
|
|
1110
|
+
return Math.min(retryAfterMs2, this.maxRetryDelayMs);
|
|
1359
1111
|
}
|
|
1360
1112
|
};
|
|
1361
1113
|
async function isPostStreamRequestNotFound(response) {
|
|
@@ -1370,7 +1122,7 @@ async function isPostStreamRequestNotFound(response) {
|
|
|
1370
1122
|
function isRetryableStatus(status) {
|
|
1371
1123
|
return status === 429 || status >= 500 && status <= 599;
|
|
1372
1124
|
}
|
|
1373
|
-
function
|
|
1125
|
+
function retryAfterMs(headers) {
|
|
1374
1126
|
const raw = headers.get("retry-after");
|
|
1375
1127
|
if (!raw) return null;
|
|
1376
1128
|
const seconds = Number(raw);
|
|
@@ -1387,7 +1139,7 @@ function validateAndSortUsage(usage, label, config, enforceMeterScope) {
|
|
|
1387
1139
|
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
1388
1140
|
);
|
|
1389
1141
|
for (const [meter, qty] of entries) {
|
|
1390
|
-
if (!
|
|
1142
|
+
if (!METER_KEY_RE.test(meter)) {
|
|
1391
1143
|
throw new Error(
|
|
1392
1144
|
`${label} key '${meter}' must be lowercase alphanumeric with underscores`
|
|
1393
1145
|
);
|
|
@@ -1406,12 +1158,303 @@ function validateAndSortUsage(usage, label, config, enforceMeterScope) {
|
|
|
1406
1158
|
}
|
|
1407
1159
|
return Object.fromEntries(entries);
|
|
1408
1160
|
}
|
|
1409
|
-
function
|
|
1161
|
+
function resolveEndpoint(endpoint, coreUrl) {
|
|
1410
1162
|
if (/^https?:\/\//.test(endpoint)) return endpoint;
|
|
1411
1163
|
if (!coreUrl) return endpoint;
|
|
1412
1164
|
return `${coreUrl.replace(/\/+$/, "")}${endpoint}`;
|
|
1413
1165
|
}
|
|
1414
1166
|
|
|
1167
|
+
// src/core/report.ts
|
|
1168
|
+
var MEASUREMENTS_VERSION = 1;
|
|
1169
|
+
var KEY_RE = /^[a-z0-9_]{1,64}$/;
|
|
1170
|
+
var DIMENSION_VALUE_RE = /^[\w.:-]{1,128}$/;
|
|
1171
|
+
var CURRENCY_RE = /^[A-Za-z]{3}$/;
|
|
1172
|
+
var DECIMAL_INTEGER_RE = /^\d{1,30}$/;
|
|
1173
|
+
function createReportFn(channels) {
|
|
1174
|
+
let stampedMeasurements = [];
|
|
1175
|
+
let stampedQuote;
|
|
1176
|
+
let inBandTail = Promise.resolve();
|
|
1177
|
+
let postStreamFinalized = false;
|
|
1178
|
+
let pendingPostStreamBatch = null;
|
|
1179
|
+
const deliverPostStream = async (reported, quote) => {
|
|
1180
|
+
if (pendingPostStreamBatch) {
|
|
1181
|
+
if (!quotesEqual(pendingPostStreamBatch.quote, quote)) {
|
|
1182
|
+
return {
|
|
1183
|
+
ok: false,
|
|
1184
|
+
transport: "post_stream",
|
|
1185
|
+
reason: "quote conflicts with this request's pending post-stream batch: one served request carries one quote across all measurements"
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
if (reported.some(
|
|
1189
|
+
(measurement) => !dimsEqual(
|
|
1190
|
+
pendingPostStreamBatch.measurements[0]?.dims,
|
|
1191
|
+
measurement.dims
|
|
1192
|
+
)
|
|
1193
|
+
)) {
|
|
1194
|
+
return {
|
|
1195
|
+
ok: false,
|
|
1196
|
+
transport: "post_stream",
|
|
1197
|
+
reason: "dims conflict with this request's pending post-stream batch: the request receipt rates under ONE dims tuple"
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
pendingPostStreamBatch.measurements.push(...reported);
|
|
1201
|
+
return pendingPostStreamBatch.flush;
|
|
1202
|
+
}
|
|
1203
|
+
if (postStreamFinalized) {
|
|
1204
|
+
return {
|
|
1205
|
+
ok: false,
|
|
1206
|
+
transport: "post_stream",
|
|
1207
|
+
reason: "the served request already used its post-stream callback; report multiple meters in ONE call \u2014 ctx.report([a, b]) \u2014 or before the flush"
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (stampedMeasurements.length > 0) {
|
|
1211
|
+
return {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
transport: "post_stream",
|
|
1214
|
+
reason: "this request already reported in-band; every report on one request must share the stamped aggregate (same quote, before the response is sent)"
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
postStreamFinalized = true;
|
|
1218
|
+
const batch = {
|
|
1219
|
+
measurements: [...reported],
|
|
1220
|
+
quote,
|
|
1221
|
+
flush: void 0
|
|
1222
|
+
};
|
|
1223
|
+
batch.flush = new Promise((resolve) => setTimeout(resolve, 0)).then(
|
|
1224
|
+
async () => {
|
|
1225
|
+
pendingPostStreamBatch = null;
|
|
1226
|
+
const result = await channels.postStream({
|
|
1227
|
+
measurements: batch.measurements,
|
|
1228
|
+
...batch.quote ? { quote: batch.quote } : {}
|
|
1229
|
+
});
|
|
1230
|
+
return result.ok ? { ok: true, transport: "post_stream" } : {
|
|
1231
|
+
ok: false,
|
|
1232
|
+
transport: "post_stream",
|
|
1233
|
+
reason: result.reason ?? "post-stream delivery failed"
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
);
|
|
1237
|
+
pendingPostStreamBatch = batch;
|
|
1238
|
+
return batch.flush;
|
|
1239
|
+
};
|
|
1240
|
+
const tryInBand = (reported, quote) => {
|
|
1241
|
+
const run = inBandTail.then(async () => {
|
|
1242
|
+
const sink = channels.responseSink;
|
|
1243
|
+
if (!sink || !channels.request || !sink.canStampHeaders()) return null;
|
|
1244
|
+
if (postStreamFinalized) return null;
|
|
1245
|
+
if (!quotesEqual(stampedQuote, quote) && stampedMeasurements.length > 0) {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
if (stampedMeasurements.length > 0 && reported.some(
|
|
1249
|
+
(measurement) => !dimsEqual(stampedMeasurements[0].dims, measurement.dims)
|
|
1250
|
+
)) {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
const measurements = [...stampedMeasurements, ...reported];
|
|
1254
|
+
const payload = buildInBandPayload(channels.request, measurements, quote);
|
|
1255
|
+
const headers = await channels.computeHeaders(payload);
|
|
1256
|
+
if (Object.keys(headers).length === 0 || !sink.canStampHeaders()) {
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
sink.stampHeaders(headers);
|
|
1260
|
+
stampedMeasurements = measurements;
|
|
1261
|
+
stampedQuote = quote;
|
|
1262
|
+
return { ok: true, transport: "in_band" };
|
|
1263
|
+
});
|
|
1264
|
+
inBandTail = run.then(
|
|
1265
|
+
() => void 0,
|
|
1266
|
+
() => void 0
|
|
1267
|
+
);
|
|
1268
|
+
return run;
|
|
1269
|
+
};
|
|
1270
|
+
return async (input) => {
|
|
1271
|
+
const inputs = Array.isArray(input) ? input : [input];
|
|
1272
|
+
if (inputs.length === 0) {
|
|
1273
|
+
throw new MeteringError(
|
|
1274
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1275
|
+
"report([]) is empty: a batched report needs at least one measurement"
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
const measurements = inputs.map((entry) => validateMeasurement(entry));
|
|
1279
|
+
for (const measurement of measurements) {
|
|
1280
|
+
if (!dimsEqual(measurements[0].dims, measurement.dims)) {
|
|
1281
|
+
throw new MeteringError(
|
|
1282
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterValue,
|
|
1283
|
+
"a batched report carries ONE dims tuple: the request receipt rates under (route, dims), so mixed dims are unratable \u2014 report each dims tuple on its own request"
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
let quote;
|
|
1288
|
+
for (const entry of inputs) {
|
|
1289
|
+
if (entry.quote === void 0) continue;
|
|
1290
|
+
const validated = validateQuote(entry.quote);
|
|
1291
|
+
if (quote === void 0) {
|
|
1292
|
+
quote = validated;
|
|
1293
|
+
} else if (!quotesEqual(quote, validated)) {
|
|
1294
|
+
throw new MeteringError(
|
|
1295
|
+
RESPONSE_METERING_ERROR_CODES.invalidQuote,
|
|
1296
|
+
"a batched report carries ONE quote: two entries supplied different quotes"
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const inBand = await tryInBand(measurements, quote);
|
|
1301
|
+
if (inBand) return inBand;
|
|
1302
|
+
return deliverPostStream(measurements, quote);
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function unattachedReport() {
|
|
1306
|
+
return () => Promise.reject(
|
|
1307
|
+
new MeteringError(
|
|
1308
|
+
RESPONSE_METERING_ERROR_CODES.missingToken,
|
|
1309
|
+
"report() has no metering channel on this context: verify through the runtime (fs.middleware() / fs.verifyRequest()) instead of the bare verifyRequest() primitive, and pass that context to background jobs"
|
|
1310
|
+
)
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
function rawDimsUnitsOf(measurement) {
|
|
1314
|
+
let total = 0;
|
|
1315
|
+
for (const value of Object.values(measurement.values)) total += value;
|
|
1316
|
+
return { [measurement.meter]: total };
|
|
1317
|
+
}
|
|
1318
|
+
function buildInBandPayload(request, measurements, quote) {
|
|
1319
|
+
const rawDimsUnits = {};
|
|
1320
|
+
for (const measurement of measurements) {
|
|
1321
|
+
for (const [meter, units] of Object.entries(rawDimsUnitsOf(measurement))) {
|
|
1322
|
+
rawDimsUnits[meter] = (rawDimsUnits[meter] ?? 0) + units;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
method: request.method.toUpperCase(),
|
|
1327
|
+
path: request.path,
|
|
1328
|
+
rawDimsUnits,
|
|
1329
|
+
measurementsVersion: MEASUREMENTS_VERSION,
|
|
1330
|
+
measurements,
|
|
1331
|
+
...quote ? { quote } : {}
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function dimsEqual(left, right) {
|
|
1335
|
+
const l = Object.entries(left ?? {}).sort(([a], [b]) => a < b ? -1 : 1);
|
|
1336
|
+
const r = Object.entries(right ?? {}).sort(([a], [b]) => a < b ? -1 : 1);
|
|
1337
|
+
if (l.length !== r.length) return false;
|
|
1338
|
+
return l.every(([k, v], i) => r[i][0] === k && r[i][1] === v);
|
|
1339
|
+
}
|
|
1340
|
+
function quotesEqual(left, right) {
|
|
1341
|
+
return left === right || left !== void 0 && right !== void 0 && left.currency === right.currency && left.amountNanos === right.amountNanos;
|
|
1342
|
+
}
|
|
1343
|
+
function validateMeasurement(input) {
|
|
1344
|
+
if (!input || typeof input !== "object") {
|
|
1345
|
+
throw invalidKey("report() requires a { meter, values } object");
|
|
1346
|
+
}
|
|
1347
|
+
const meter = assertKey(input.meter, "meter");
|
|
1348
|
+
const values = assertValues(input.values);
|
|
1349
|
+
const dims = input.dims === void 0 ? void 0 : assertDims(input.dims);
|
|
1350
|
+
return {
|
|
1351
|
+
meter,
|
|
1352
|
+
values,
|
|
1353
|
+
...dims && Object.keys(dims).length > 0 ? { dims } : {}
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function validateQuote(quote) {
|
|
1357
|
+
if (!quote || typeof quote !== "object" || Array.isArray(quote)) {
|
|
1358
|
+
throw invalidQuote(
|
|
1359
|
+
"quote must be an object of the form { currency, amountNanos }"
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const { currency, amountNanos } = quote;
|
|
1363
|
+
if (typeof currency !== "string" || !CURRENCY_RE.test(currency)) {
|
|
1364
|
+
throw invalidQuote("quote.currency must be a 3-letter currency code");
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
currency: currency.toLowerCase(),
|
|
1368
|
+
amountNanos: assertAmountNanos(amountNanos)
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
function assertAmountNanos(value) {
|
|
1372
|
+
if (typeof value === "bigint") {
|
|
1373
|
+
if (value < 0n) throw negativeAmountNanos();
|
|
1374
|
+
return value.toString();
|
|
1375
|
+
}
|
|
1376
|
+
if (typeof value === "number") {
|
|
1377
|
+
if (!Number.isSafeInteger(value)) {
|
|
1378
|
+
throw invalidQuote(
|
|
1379
|
+
"quote.amountNanos must be a safe integer number of nanodollars (pass a string or bigint for larger amounts)"
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
if (value < 0) throw negativeAmountNanos();
|
|
1383
|
+
return String(value);
|
|
1384
|
+
}
|
|
1385
|
+
if (typeof value === "string") {
|
|
1386
|
+
if (/^-/.test(value)) throw negativeAmountNanos();
|
|
1387
|
+
if (DECIMAL_INTEGER_RE.test(value)) return value;
|
|
1388
|
+
}
|
|
1389
|
+
throw invalidQuote(
|
|
1390
|
+
"quote.amountNanos must be a non-negative integer number of nanodollars"
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
function negativeAmountNanos() {
|
|
1394
|
+
return invalidQuote(
|
|
1395
|
+
"quote.amountNanos must be non-negative: a quote is a proposed rate, never a credit \u2014 refunds are platform operations"
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
function assertValues(values) {
|
|
1399
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) {
|
|
1400
|
+
throw invalidKey("report() requires a values object");
|
|
1401
|
+
}
|
|
1402
|
+
const entries = Object.entries(values).sort(
|
|
1403
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
1404
|
+
);
|
|
1405
|
+
if (entries.length === 0) {
|
|
1406
|
+
throw invalidKey("report() requires at least one measure in values");
|
|
1407
|
+
}
|
|
1408
|
+
const out = {};
|
|
1409
|
+
for (const [measure, value] of entries) {
|
|
1410
|
+
assertKey(measure, "measure");
|
|
1411
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
1412
|
+
throw new MeteringError(
|
|
1413
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterValue,
|
|
1414
|
+
`values.${measure} must be a non-negative safe integer`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
out[measure] = value;
|
|
1418
|
+
}
|
|
1419
|
+
return out;
|
|
1420
|
+
}
|
|
1421
|
+
function assertDims(dims) {
|
|
1422
|
+
if (!dims || typeof dims !== "object" || Array.isArray(dims)) {
|
|
1423
|
+
throw invalidKey("report() dims must be an object of dimension selectors");
|
|
1424
|
+
}
|
|
1425
|
+
const entries = Object.entries(dims).sort(
|
|
1426
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
1427
|
+
);
|
|
1428
|
+
const out = {};
|
|
1429
|
+
for (const [dimension, value] of entries) {
|
|
1430
|
+
assertKey(dimension, "dimension");
|
|
1431
|
+
if (typeof value !== "string" || !DIMENSION_VALUE_RE.test(value)) {
|
|
1432
|
+
throw invalidKey(
|
|
1433
|
+
`dims.${dimension} must be a 1-128 character selector value`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
out[dimension] = value;
|
|
1437
|
+
}
|
|
1438
|
+
return out;
|
|
1439
|
+
}
|
|
1440
|
+
function assertKey(value, label) {
|
|
1441
|
+
if (typeof value !== "string" || !KEY_RE.test(value)) {
|
|
1442
|
+
throw invalidKey(
|
|
1443
|
+
`${label} key ${JSON.stringify(value)} must be 1-64 lowercase alphanumeric characters or underscores`
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
return value;
|
|
1447
|
+
}
|
|
1448
|
+
function invalidKey(message) {
|
|
1449
|
+
return new MeteringError(
|
|
1450
|
+
RESPONSE_METERING_ERROR_CODES.invalidMeterKey,
|
|
1451
|
+
message
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
function invalidQuote(message) {
|
|
1455
|
+
return new MeteringError(RESPONSE_METERING_ERROR_CODES.invalidQuote, message);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1415
1458
|
// src/core/nonceCache.ts
|
|
1416
1459
|
var DEFAULT_MAX_ENTRIES = 25e4;
|
|
1417
1460
|
var DEFAULT_TTL_MS = (RUNTIME_REPLAY_WINDOW_SECONDS + RUNTIME_CLOCK_SKEW_SECONDS) * 1e3;
|
|
@@ -2005,22 +2048,7 @@ async function verifyRequest(input, deps) {
|
|
|
2005
2048
|
"x-fs-timestamp is not an integer"
|
|
2006
2049
|
);
|
|
2007
2050
|
}
|
|
2008
|
-
|
|
2009
|
-
const window = deps.replayWindowSeconds ?? RUNTIME_REPLAY_WINDOW_SECONDS;
|
|
2010
|
-
const now = (deps.nowSeconds ?? (() => Math.floor(Date.now() / 1e3)))();
|
|
2011
|
-
const delta = now - timestamp;
|
|
2012
|
-
if (Math.abs(delta) > window) {
|
|
2013
|
-
if (Math.abs(delta) <= window + skew) {
|
|
2014
|
-
throw new FartherShoreError(
|
|
2015
|
-
"clock_skew",
|
|
2016
|
-
"x-fs-timestamp is outside the replay window but within clock-skew tolerance"
|
|
2017
|
-
);
|
|
2018
|
-
}
|
|
2019
|
-
throw new FartherShoreError(
|
|
2020
|
-
"expired_signature",
|
|
2021
|
-
"x-fs-timestamp is outside the replay window"
|
|
2022
|
-
);
|
|
2023
|
-
}
|
|
2051
|
+
assertTimestampWithinWindow(timestamp, deps);
|
|
2024
2052
|
const computedBodyHash = await computeBodyHash(input);
|
|
2025
2053
|
if (signedBodyHash !== computedBodyHash) {
|
|
2026
2054
|
throw new FartherShoreError(
|
|
@@ -2028,31 +2056,7 @@ async function verifyRequest(input, deps) {
|
|
|
2028
2056
|
"recomputed body hash does not match the signed x-fs-body-hash"
|
|
2029
2057
|
);
|
|
2030
2058
|
}
|
|
2031
|
-
|
|
2032
|
-
throw new FartherShoreError(
|
|
2033
|
-
"route_mismatch",
|
|
2034
|
-
"signed business-id does not match this backend's business"
|
|
2035
|
-
);
|
|
2036
|
-
}
|
|
2037
|
-
if (deps.backendIds !== void 0 && deps.backendIds.size > 0) {
|
|
2038
|
-
if (!deps.backendIds.has(signedBackendId)) {
|
|
2039
|
-
throw new FartherShoreError(
|
|
2040
|
-
"route_mismatch",
|
|
2041
|
-
"signed backend-id is not one this deployment serves"
|
|
2042
|
-
);
|
|
2043
|
-
}
|
|
2044
|
-
} else if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
2045
|
-
throw new FartherShoreError(
|
|
2046
|
-
"route_mismatch",
|
|
2047
|
-
"signed backend-id does not match this backend"
|
|
2048
|
-
);
|
|
2049
|
-
}
|
|
2050
|
-
if (deps.knownRouteIds !== void 0 && signedRouteId !== "" && !deps.knownRouteIds.has(signedRouteId)) {
|
|
2051
|
-
throw new FartherShoreError(
|
|
2052
|
-
"route_mismatch",
|
|
2053
|
-
"signed route-id is not served by this backend"
|
|
2054
|
-
);
|
|
2055
|
-
}
|
|
2059
|
+
assertClaimBinding(deps, signedBusinessId, signedBackendId, signedRouteId);
|
|
2056
2060
|
const contextToken = h("x-fs-context") ?? null;
|
|
2057
2061
|
const canonicalInput = {
|
|
2058
2062
|
method: input.method,
|
|
@@ -2123,9 +2127,57 @@ async function verifyRequest(input, deps) {
|
|
|
2123
2127
|
...principal ? { principal } : {},
|
|
2124
2128
|
...permissions !== void 0 ? { permissions } : {},
|
|
2125
2129
|
...roles !== void 0 ? { roles } : {},
|
|
2126
|
-
...signedContext ? { signedContext } : {}
|
|
2130
|
+
...signedContext ? { signedContext } : {},
|
|
2131
|
+
// The bare primitive has no metering channel; the runtime facade replaces
|
|
2132
|
+
// this with the real bound verb (see FartherShore.verifyRequest).
|
|
2133
|
+
report: unattachedReport()
|
|
2127
2134
|
};
|
|
2128
2135
|
}
|
|
2136
|
+
function assertTimestampWithinWindow(timestamp, deps) {
|
|
2137
|
+
const skew = deps.clockSkewSeconds ?? RUNTIME_CLOCK_SKEW_SECONDS;
|
|
2138
|
+
const window = deps.replayWindowSeconds ?? RUNTIME_REPLAY_WINDOW_SECONDS;
|
|
2139
|
+
const now = (deps.nowSeconds ?? (() => Math.floor(Date.now() / 1e3)))();
|
|
2140
|
+
const delta = now - timestamp;
|
|
2141
|
+
if (Math.abs(delta) > window) {
|
|
2142
|
+
if (Math.abs(delta) <= window + skew) {
|
|
2143
|
+
throw new FartherShoreError(
|
|
2144
|
+
"clock_skew",
|
|
2145
|
+
"x-fs-timestamp is outside the replay window but within clock-skew tolerance"
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
throw new FartherShoreError(
|
|
2149
|
+
"expired_signature",
|
|
2150
|
+
"x-fs-timestamp is outside the replay window"
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
function assertClaimBinding(deps, signedBusinessId, signedBackendId, signedRouteId) {
|
|
2155
|
+
if (deps.businessId !== void 0 && signedBusinessId !== deps.businessId) {
|
|
2156
|
+
throw new FartherShoreError(
|
|
2157
|
+
"route_mismatch",
|
|
2158
|
+
"signed business-id does not match this backend's business"
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
if (deps.backendIds !== void 0 && deps.backendIds.size > 0) {
|
|
2162
|
+
if (!deps.backendIds.has(signedBackendId)) {
|
|
2163
|
+
throw new FartherShoreError(
|
|
2164
|
+
"route_mismatch",
|
|
2165
|
+
"signed backend-id is not one this deployment serves"
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
} else if (deps.backendId !== void 0 && signedBackendId !== deps.backendId) {
|
|
2169
|
+
throw new FartherShoreError(
|
|
2170
|
+
"route_mismatch",
|
|
2171
|
+
"signed backend-id does not match this backend"
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
if (deps.knownRouteIds !== void 0 && signedRouteId !== "" && !deps.knownRouteIds.has(signedRouteId)) {
|
|
2175
|
+
throw new FartherShoreError(
|
|
2176
|
+
"route_mismatch",
|
|
2177
|
+
"signed route-id is not served by this backend"
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2129
2181
|
async function resolveSignedContext(contextToken, contextSecrets, signedBusinessId) {
|
|
2130
2182
|
if (!contextToken) return null;
|
|
2131
2183
|
const signedContext = decodeContextClaims(contextToken);
|
|
@@ -2178,7 +2230,7 @@ function headerGetter(headers) {
|
|
|
2178
2230
|
|
|
2179
2231
|
// src/core/runtime.ts
|
|
2180
2232
|
var DEFAULT_CORE_URL = "https://core.farthershore.com";
|
|
2181
|
-
var SDK_VERSION = "0.
|
|
2233
|
+
var SDK_VERSION = "0.21.0".length > 0 ? "0.21.0" : "0.0.0-dev";
|
|
2182
2234
|
var FartherShore = class {
|
|
2183
2235
|
bootstrapClient;
|
|
2184
2236
|
fetchImpl;
|
|
@@ -2194,14 +2246,13 @@ var FartherShore = class {
|
|
|
2194
2246
|
replayProtectionDiagnostic;
|
|
2195
2247
|
shutdownManager = new ShutdownManager();
|
|
2196
2248
|
jwks = null;
|
|
2197
|
-
meteringClient = null;
|
|
2198
2249
|
postStreamUsageClient = null;
|
|
2199
2250
|
tunnel = null;
|
|
2200
2251
|
bootstrapped = false;
|
|
2201
2252
|
constructor(options = {}) {
|
|
2202
2253
|
const env = options.env ?? readProcessEnv();
|
|
2203
2254
|
const runtimeToken = options.runtimeToken ?? env[FS_RUNTIME_TOKEN_ENV] ?? "";
|
|
2204
|
-
const coreUrl = options.coreUrl ?? env.FS_CORE_URL ??
|
|
2255
|
+
const coreUrl = options.coreUrl ?? env.FS_CORE_URL ?? DEFAULT_CORE_URL;
|
|
2205
2256
|
this.runtimeToken = runtimeToken;
|
|
2206
2257
|
this.coreUrl = coreUrl;
|
|
2207
2258
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
@@ -2223,9 +2274,6 @@ var FartherShore = class {
|
|
|
2223
2274
|
...options.instanceId ? { instanceId: options.instanceId } : {}
|
|
2224
2275
|
}
|
|
2225
2276
|
});
|
|
2226
|
-
this.shutdownManager.register(async () => {
|
|
2227
|
-
await this.meteringClient?.flush();
|
|
2228
|
-
});
|
|
2229
2277
|
this.shutdownManager.register(async () => {
|
|
2230
2278
|
await reportHealth({
|
|
2231
2279
|
runtimeToken: this.runtimeToken,
|
|
@@ -2245,14 +2293,7 @@ var FartherShore = class {
|
|
|
2245
2293
|
fetchImpl: this.fetchImpl
|
|
2246
2294
|
});
|
|
2247
2295
|
}
|
|
2248
|
-
if (!this.
|
|
2249
|
-
this.meteringClient = new MeteringClient({
|
|
2250
|
-
config: config.metering,
|
|
2251
|
-
businessId: config.business.id,
|
|
2252
|
-
backendId: config.backend.id,
|
|
2253
|
-
coreUrl: this.coreUrl,
|
|
2254
|
-
fetchImpl: this.fetchImpl
|
|
2255
|
-
});
|
|
2296
|
+
if (!this.postStreamUsageClient && config.metering.enabled) {
|
|
2256
2297
|
this.postStreamUsageClient = new PostStreamUsageClient({
|
|
2257
2298
|
config: config.metering,
|
|
2258
2299
|
coreUrl: this.coreUrl,
|
|
@@ -2325,7 +2366,7 @@ var FartherShore = class {
|
|
|
2325
2366
|
* Framework-neutral verification primitive. Fail-closed: throws a typed
|
|
2326
2367
|
* FartherShoreError on any verification failure. Returns the verified context.
|
|
2327
2368
|
*/
|
|
2328
|
-
async verifyRequest(input) {
|
|
2369
|
+
async verifyRequest(input, options = {}) {
|
|
2329
2370
|
const config = await this.ensureBootstrapped();
|
|
2330
2371
|
if (!this.jwks) {
|
|
2331
2372
|
throw new FartherShoreError(
|
|
@@ -2351,21 +2392,52 @@ var FartherShore = class {
|
|
|
2351
2392
|
});
|
|
2352
2393
|
return {
|
|
2353
2394
|
...context,
|
|
2354
|
-
|
|
2355
|
-
|
|
2395
|
+
report: this.buildReportFn(context, input, options.responseSink)
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Bind the ONE reporting verb to a verified context. Identity comes from the
|
|
2400
|
+
* context (`signedContext.subscriptionId` + `requestId`) — never from the
|
|
2401
|
+
* caller — so a handler cannot forget it, and a background job that is handed
|
|
2402
|
+
* this context keeps reporting against the SAME served identity.
|
|
2403
|
+
*/
|
|
2404
|
+
buildReportFn(context, input, responseSink) {
|
|
2405
|
+
const channels = {
|
|
2406
|
+
request: { method: input.method, path: input.path },
|
|
2407
|
+
...responseSink ? { responseSink } : {},
|
|
2408
|
+
computeHeaders: (payload) => computeMeteringHeaders(payload, {
|
|
2409
|
+
token: this.runtimeToken,
|
|
2410
|
+
requestId: context.requestId,
|
|
2411
|
+
onSkip: () => {
|
|
2412
|
+
}
|
|
2413
|
+
}),
|
|
2414
|
+
postStream: async ({ measurements, quote }) => {
|
|
2415
|
+
const subscriptionId = context.signedContext?.subscriptionId;
|
|
2356
2416
|
if (!subscriptionId) {
|
|
2357
|
-
return
|
|
2417
|
+
return {
|
|
2358
2418
|
ok: false,
|
|
2359
|
-
reason: "
|
|
2360
|
-
}
|
|
2419
|
+
reason: "this request carries no subscription identity, so late usage cannot be attributed \u2014 report before the response is sent, or serve the route through a subscribed surface"
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
const meters = {};
|
|
2423
|
+
for (const measurement of measurements) {
|
|
2424
|
+
for (const [meter, qty] of Object.entries(
|
|
2425
|
+
rawDimsUnitsOf(measurement)
|
|
2426
|
+
)) {
|
|
2427
|
+
meters[meter] = (meters[meter] ?? 0) + qty;
|
|
2428
|
+
}
|
|
2361
2429
|
}
|
|
2362
|
-
return this.
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2430
|
+
return this.reportPostStreamUsage({
|
|
2431
|
+
requestId: context.requestId,
|
|
2432
|
+
subscriptionId,
|
|
2433
|
+
meters,
|
|
2434
|
+
measurementsVersion: MEASUREMENTS_VERSION,
|
|
2435
|
+
measurements,
|
|
2436
|
+
...quote ? { quote } : {}
|
|
2366
2437
|
});
|
|
2367
2438
|
}
|
|
2368
2439
|
};
|
|
2440
|
+
return createReportFn(channels);
|
|
2369
2441
|
}
|
|
2370
2442
|
/** Whether verification is required (bootstrap × opt-out). */
|
|
2371
2443
|
async verificationRequired() {
|
|
@@ -2414,20 +2486,12 @@ var FartherShore = class {
|
|
|
2414
2486
|
});
|
|
2415
2487
|
await supervisor.start();
|
|
2416
2488
|
}
|
|
2417
|
-
/**
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
"invalid_token",
|
|
2424
|
-
"metering is not enabled for this runtime token"
|
|
2425
|
-
);
|
|
2426
|
-
}
|
|
2427
|
-
await this.meteringClient.meter(meter, qty, options);
|
|
2428
|
-
}
|
|
2429
|
-
/** Best-effort attested post-stream usage callback. Never rejects. */
|
|
2430
|
-
async reportUsage(input) {
|
|
2489
|
+
/**
|
|
2490
|
+
* PRIVATE transport for the post-stream lane of `ctx.report()`. Never rejects
|
|
2491
|
+
* — a metering hiccup must not break a builder's endpoint. This is machinery,
|
|
2492
|
+
* not surface: the ONE public reporting verb is `ctx.report()`.
|
|
2493
|
+
*/
|
|
2494
|
+
async reportPostStreamUsage(input) {
|
|
2431
2495
|
try {
|
|
2432
2496
|
await this.ensureBootstrapped();
|
|
2433
2497
|
if (!this.meteringEnabledOverride || !this.postStreamUsageClient) {
|
|
@@ -2459,7 +2523,7 @@ var FartherShore = class {
|
|
|
2459
2523
|
// fs.start() launches an embedded tunnel; otherwise the supervisor state.
|
|
2460
2524
|
tunnel: this.tunnel ? this.tunnel.healthString() : null,
|
|
2461
2525
|
verification: this.verificationEnabled && config !== null,
|
|
2462
|
-
metering: this.
|
|
2526
|
+
metering: this.postStreamUsageClient !== null
|
|
2463
2527
|
});
|
|
2464
2528
|
}
|
|
2465
2529
|
/** Graceful shutdown: flush metering + send a stopping heartbeat. */
|
|
@@ -2496,10 +2560,13 @@ var FartherShorePermissionError = class extends Error {
|
|
|
2496
2560
|
this.requiredPermission = requiredPermission;
|
|
2497
2561
|
}
|
|
2498
2562
|
};
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2563
|
+
var READ_METHODS = /* @__PURE__ */ new Set([
|
|
2564
|
+
"GET",
|
|
2565
|
+
"HEAD",
|
|
2566
|
+
"OPTIONS"
|
|
2567
|
+
]);
|
|
2568
|
+
function routePermission(subject, method) {
|
|
2569
|
+
return READ_METHODS.has(method.toUpperCase()) ? `${subject}:read` : `${subject}:write`;
|
|
2503
2570
|
}
|
|
2504
2571
|
function permissionSatisfies(required, granted) {
|
|
2505
2572
|
if (granted === void 0) return true;
|
|
@@ -2508,7 +2575,12 @@ function permissionSatisfies(required, granted) {
|
|
|
2508
2575
|
const idx = required.indexOf(":");
|
|
2509
2576
|
if (idx > 0 && idx < required.length - 1) {
|
|
2510
2577
|
const subject = required.slice(0, idx);
|
|
2511
|
-
if (granted.includes(`${subject}:${WILDCARD}`))
|
|
2578
|
+
if (subject !== WILDCARD && granted.includes(`${subject}:${WILDCARD}`))
|
|
2579
|
+
return true;
|
|
2580
|
+
if (required.slice(idx + 1) === WILDCARD && subject !== WILDCARD) {
|
|
2581
|
+
const prefix = `${subject}:`;
|
|
2582
|
+
return granted.some((permission) => permission.startsWith(prefix));
|
|
2583
|
+
}
|
|
2512
2584
|
}
|
|
2513
2585
|
return false;
|
|
2514
2586
|
}
|
|
@@ -2543,14 +2615,17 @@ async function runMiddleware(fs, options, req, res, next) {
|
|
|
2543
2615
|
const contentType = headerValue(req.headers, "content-type");
|
|
2544
2616
|
const streamingExempt = isStreamingExempt(contentType);
|
|
2545
2617
|
const body = streamingExempt ? null : extractRawBody(req);
|
|
2546
|
-
const ctx = await fs.verifyRequest(
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2618
|
+
const ctx = await fs.verifyRequest(
|
|
2619
|
+
{
|
|
2620
|
+
method: req.method,
|
|
2621
|
+
path,
|
|
2622
|
+
query,
|
|
2623
|
+
headers: req.headers,
|
|
2624
|
+
body,
|
|
2625
|
+
streamingExempt
|
|
2626
|
+
},
|
|
2627
|
+
{ responseSink: expressResponseSink(res) }
|
|
2628
|
+
);
|
|
2554
2629
|
req.fartherShore = ctx;
|
|
2555
2630
|
stripFartherShoreHeaders(req);
|
|
2556
2631
|
next();
|
|
@@ -2558,6 +2633,16 @@ async function runMiddleware(fs, options, req, res, next) {
|
|
|
2558
2633
|
fail(res, error, options, req);
|
|
2559
2634
|
}
|
|
2560
2635
|
}
|
|
2636
|
+
function expressResponseSink(res) {
|
|
2637
|
+
return {
|
|
2638
|
+
canStampHeaders: () => res.headersSent !== true,
|
|
2639
|
+
stampHeaders: (headers) => {
|
|
2640
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2641
|
+
res.setHeader(name, value);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2561
2646
|
function fail(res, error, options, req) {
|
|
2562
2647
|
const code = error instanceof FartherShoreError ? error.code : "bad_signature";
|
|
2563
2648
|
const status = error instanceof FartherShoreError ? error.status : 401;
|
|
@@ -2598,7 +2683,12 @@ function stripFartherShoreHeaders(req) {
|
|
|
2598
2683
|
withRaw.rawHeaders = cleaned;
|
|
2599
2684
|
}
|
|
2600
2685
|
}
|
|
2601
|
-
function createExpressHandler(
|
|
2686
|
+
function createExpressHandler(optionsOrHandler, maybeHandler) {
|
|
2687
|
+
const options = typeof optionsOrHandler === "function" ? {} : optionsOrHandler;
|
|
2688
|
+
const handler = typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler;
|
|
2689
|
+
if (typeof handler !== "function") {
|
|
2690
|
+
throw new TypeError("fs.handler(options, cb) requires a handler callback");
|
|
2691
|
+
}
|
|
2602
2692
|
return (req, res, next) => {
|
|
2603
2693
|
const ctx = req.fartherShore;
|
|
2604
2694
|
if (!ctx) {
|
|
@@ -2609,10 +2699,22 @@ function createExpressHandler(handler) {
|
|
|
2609
2699
|
res.status(401).json({ error: "principal_required" });
|
|
2610
2700
|
return;
|
|
2611
2701
|
}
|
|
2702
|
+
if (!ctx.signedContext) {
|
|
2703
|
+
res.status(401).json({ error: "context_unverified" });
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2612
2706
|
const verified = ctx;
|
|
2613
|
-
void Promise.resolve().then(
|
|
2614
|
-
|
|
2615
|
-
|
|
2707
|
+
void Promise.resolve().then(() => {
|
|
2708
|
+
if (options.permission !== void 0) {
|
|
2709
|
+
requirePermission(verified, options.permission);
|
|
2710
|
+
}
|
|
2711
|
+
return handler(
|
|
2712
|
+
verified,
|
|
2713
|
+
req,
|
|
2714
|
+
res,
|
|
2715
|
+
next
|
|
2716
|
+
);
|
|
2717
|
+
}).catch((error) => failHandler(res, next, error));
|
|
2616
2718
|
};
|
|
2617
2719
|
}
|
|
2618
2720
|
function failHandler(res, next, error) {
|
|
@@ -3074,7 +3176,7 @@ function readProcessEnv2() {
|
|
|
3074
3176
|
// src/testing/usageSink.ts
|
|
3075
3177
|
var DevUsageSink = class {
|
|
3076
3178
|
events = [];
|
|
3077
|
-
/** Record a signed response-metering payload (
|
|
3179
|
+
/** Record a signed response-metering payload (report() in-band / computeMeteringHeaders). */
|
|
3078
3180
|
recordResponse(payload, requestId) {
|
|
3079
3181
|
const raw = payload.rawDimsUnits;
|
|
3080
3182
|
const meters = raw && typeof raw === "object" ? raw : {};
|
|
@@ -3332,6 +3434,7 @@ function createDevRuntime(options) {
|
|
|
3332
3434
|
}
|
|
3333
3435
|
fs.middleware = middleware;
|
|
3334
3436
|
fs.handler = createExpressHandler;
|
|
3437
|
+
fs.authz = tracedAuthz;
|
|
3335
3438
|
const devRuntime = {
|
|
3336
3439
|
fs,
|
|
3337
3440
|
asPersona: (name) => personaClient.asPersona(name),
|
|
@@ -3460,6 +3563,7 @@ var fartherShore = {
|
|
|
3460
3563
|
const fs = initFromEnv(options);
|
|
3461
3564
|
fs.middleware = (mwOptions) => createExpressMiddleware(fs, mwOptions);
|
|
3462
3565
|
fs.handler = createExpressHandler;
|
|
3566
|
+
fs.authz = { hasPermission, requirePermission };
|
|
3463
3567
|
return fs;
|
|
3464
3568
|
}
|
|
3465
3569
|
};
|
|
@@ -3477,13 +3581,13 @@ export {
|
|
|
3477
3581
|
FartherShorePermissionError,
|
|
3478
3582
|
JwksClient,
|
|
3479
3583
|
MAX_BODY_BYTES,
|
|
3584
|
+
MEASUREMENTS_VERSION,
|
|
3480
3585
|
METERING_PAYLOAD_HEADER,
|
|
3481
3586
|
METERING_SIGNATURE_HEADER,
|
|
3482
3587
|
METERING_TOKEN_HEADER,
|
|
3483
|
-
MeteringClient,
|
|
3484
3588
|
MeteringError,
|
|
3485
3589
|
NonceCache,
|
|
3486
|
-
|
|
3590
|
+
READ_METHODS,
|
|
3487
3591
|
REDACTED_TOKEN,
|
|
3488
3592
|
RUNTIME_CLOCK_SKEW_SECONDS,
|
|
3489
3593
|
RUNTIME_ERROR_CODES,
|
|
@@ -3500,7 +3604,6 @@ export {
|
|
|
3500
3604
|
computeMeteringHeaders,
|
|
3501
3605
|
createExpressHandler,
|
|
3502
3606
|
createExpressMiddleware,
|
|
3503
|
-
createUsage,
|
|
3504
3607
|
credentialKind,
|
|
3505
3608
|
decodeContextClaims,
|
|
3506
3609
|
fartherShore,
|
|
@@ -3509,19 +3612,18 @@ export {
|
|
|
3509
3612
|
initFromEnv2 as initFromEnv,
|
|
3510
3613
|
isPortalSession,
|
|
3511
3614
|
nodeSpawn,
|
|
3512
|
-
permissionGrants,
|
|
3513
3615
|
permissionSatisfies,
|
|
3514
3616
|
principalFromContextClaims2 as principalFromContextClaims,
|
|
3515
3617
|
reportHealth,
|
|
3516
3618
|
requireMember,
|
|
3517
3619
|
requirePermission,
|
|
3518
3620
|
requireService,
|
|
3621
|
+
routePermission,
|
|
3519
3622
|
runtimeErrorToErrorCode,
|
|
3520
3623
|
runtimeTokenKind2 as runtimeTokenKind,
|
|
3521
3624
|
signCanonicalString2 as signCanonicalString,
|
|
3522
3625
|
statusForCode,
|
|
3523
3626
|
verifyCanonicalSignature2 as verifyCanonicalSignature,
|
|
3524
3627
|
verifyContext,
|
|
3525
|
-
verifyRequest
|
|
3526
|
-
withUsage
|
|
3628
|
+
verifyRequest
|
|
3527
3629
|
};
|