@web-ts-toolkit/express-request-attestation 0.51.0 → 0.52.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 +352 -56
- package/index.d.mts +212 -103
- package/index.d.ts +212 -103
- package/index.js +31 -56
- package/index.mjs +31 -56
- package/package.json +1 -1
- package/signer.d.mts +196 -93
- package/signer.d.ts +196 -93
- package/signer.js +454 -326
- package/signer.mjs +454 -326
- package/{time-policy-DlER-7rf.d.mts → time-policy-CquS0Zgm.d.mts} +46 -25
- package/{time-policy-DlER-7rf.d.ts → time-policy-CquS0Zgm.d.ts} +46 -25
package/signer.js
CHANGED
|
@@ -270,6 +270,13 @@ var NONCE_HEX_PATTERN = /^[0-9a-f]{32}$/;
|
|
|
270
270
|
var BODY_HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
271
271
|
var HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
272
272
|
var HEX_BYTE_PATTERN = /^[0-9a-fA-F]{2}$/;
|
|
273
|
+
function assertEncodedTransactionIdBound(value) {
|
|
274
|
+
if (value.length > MAX_ENCODED_TRANSACTION_ID_BYTES) {
|
|
275
|
+
throw new AttestationProtocolError(
|
|
276
|
+
`encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
273
280
|
var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
274
281
|
"authorization",
|
|
275
282
|
"proxy-authenticate",
|
|
@@ -465,6 +472,7 @@ function extractSingleHeaderValue(raw) {
|
|
|
465
472
|
if (typeof value !== "string" || value.length === 0) {
|
|
466
473
|
return null;
|
|
467
474
|
}
|
|
475
|
+
assertEncodedTransactionIdBound(value);
|
|
468
476
|
if (value.includes(",")) {
|
|
469
477
|
throw new AttestationProtocolError("comma-joined signature headers are not allowed");
|
|
470
478
|
}
|
|
@@ -524,23 +532,18 @@ function encodeTransactionId(fields) {
|
|
|
524
532
|
const encoded = base64urlEncode(
|
|
525
533
|
canonicalJsonBytes([PROTOCOL_VERSION, fields.keyId, fields.timestampMs, fields.nonceHex, fields.mac])
|
|
526
534
|
);
|
|
527
|
-
|
|
528
|
-
throw new AttestationProtocolError(
|
|
529
|
-
`encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
|
|
530
|
-
);
|
|
531
|
-
}
|
|
535
|
+
assertEncodedTransactionIdBound(encoded);
|
|
532
536
|
return encoded;
|
|
533
537
|
}
|
|
534
538
|
function decodeTransactionId(headerValue) {
|
|
535
539
|
if (typeof headerValue !== "string" || headerValue.length === 0) {
|
|
536
540
|
throw new AttestationProtocolError("transactionId must be a non-empty string");
|
|
537
541
|
}
|
|
538
|
-
|
|
539
|
-
throw new AttestationProtocolError(
|
|
540
|
-
`encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
|
|
541
|
-
);
|
|
542
|
-
}
|
|
542
|
+
assertEncodedTransactionIdBound(headerValue);
|
|
543
543
|
const bytes = base64urlDecode(headerValue);
|
|
544
|
+
if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
|
|
545
|
+
throw new AttestationProtocolError("transactionId must not contain a UTF-8 BOM");
|
|
546
|
+
}
|
|
544
547
|
const text = utf8Decode(bytes);
|
|
545
548
|
const envelope = parseCanonicalJsonArray(text, 5);
|
|
546
549
|
const [version, keyId, timestampMs, nonceHex, mac] = envelope;
|
|
@@ -809,12 +812,13 @@ function createRequestSigner(options) {
|
|
|
809
812
|
})();
|
|
810
813
|
cachedKey = pending;
|
|
811
814
|
try {
|
|
812
|
-
await pending;
|
|
813
|
-
} catch {
|
|
814
|
-
cachedKey
|
|
815
|
-
|
|
815
|
+
return await pending;
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (cachedKey === pending) {
|
|
818
|
+
cachedKey = null;
|
|
819
|
+
}
|
|
820
|
+
throw error;
|
|
816
821
|
}
|
|
817
|
-
return pending;
|
|
818
822
|
}
|
|
819
823
|
async function sign(input) {
|
|
820
824
|
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -869,9 +873,137 @@ function createRequestSigner(options) {
|
|
|
869
873
|
return Object.freeze(signer);
|
|
870
874
|
}
|
|
871
875
|
|
|
876
|
+
// src/bounded-byte-stream.ts
|
|
877
|
+
var INITIAL_CAPACITY = 1024;
|
|
878
|
+
var READS_PER_TURN = 128;
|
|
879
|
+
function now() {
|
|
880
|
+
return typeof globalThis.performance?.now === "function" ? globalThis.performance.now() : Date.now();
|
|
881
|
+
}
|
|
882
|
+
async function readBoundedByteStream(stream, options) {
|
|
883
|
+
const { maxBytes, timeoutMs, signal, error: readError } = options;
|
|
884
|
+
const started = timeoutMs === void 0 ? 0 : now();
|
|
885
|
+
let reader;
|
|
886
|
+
try {
|
|
887
|
+
reader = stream.getReader();
|
|
888
|
+
} catch (cause) {
|
|
889
|
+
throw readError("locked", cause);
|
|
890
|
+
}
|
|
891
|
+
let storage;
|
|
892
|
+
let total = 0;
|
|
893
|
+
let stopped = false;
|
|
894
|
+
let stopReason;
|
|
895
|
+
let deadlineTimer;
|
|
896
|
+
let yieldTimer;
|
|
897
|
+
let finishYield;
|
|
898
|
+
let rejectStop;
|
|
899
|
+
const stop = new Promise((_resolve, reject) => {
|
|
900
|
+
rejectStop = reject;
|
|
901
|
+
});
|
|
902
|
+
function stopWith(reason) {
|
|
903
|
+
if (stopped) return;
|
|
904
|
+
stopped = true;
|
|
905
|
+
stopReason = reason;
|
|
906
|
+
rejectStop(reason);
|
|
907
|
+
}
|
|
908
|
+
function onAbort() {
|
|
909
|
+
stopWith(readError("abort"));
|
|
910
|
+
}
|
|
911
|
+
function checkBounds() {
|
|
912
|
+
if (stopped) throw stopReason;
|
|
913
|
+
if (signal?.aborted) throw readError("abort");
|
|
914
|
+
if (timeoutMs !== void 0 && now() - started >= timeoutMs) throw readError("timeout");
|
|
915
|
+
}
|
|
916
|
+
async function readLoop() {
|
|
917
|
+
let readsThisTurn = 0;
|
|
918
|
+
for (; ; ) {
|
|
919
|
+
checkBounds();
|
|
920
|
+
let chunk;
|
|
921
|
+
try {
|
|
922
|
+
chunk = await reader.read();
|
|
923
|
+
} catch (cause) {
|
|
924
|
+
checkBounds();
|
|
925
|
+
throw readError("read", cause);
|
|
926
|
+
}
|
|
927
|
+
checkBounds();
|
|
928
|
+
if (chunk.done) {
|
|
929
|
+
const result = storage === void 0 ? new Uint8Array(0) : storage.length === total ? storage : storage.slice(0, total);
|
|
930
|
+
checkBounds();
|
|
931
|
+
return result;
|
|
932
|
+
}
|
|
933
|
+
const value = chunk.value;
|
|
934
|
+
if (!(value instanceof Uint8Array)) throw readError("chunk");
|
|
935
|
+
if (value.byteLength > maxBytes - total) throw readError("limit");
|
|
936
|
+
if (value.byteLength > 0) {
|
|
937
|
+
const required = total + value.byteLength;
|
|
938
|
+
if (storage === void 0 || required > storage.length) {
|
|
939
|
+
const capacity = Math.min(
|
|
940
|
+
maxBytes,
|
|
941
|
+
Math.max(required, storage === void 0 ? INITIAL_CAPACITY : storage.length * 2)
|
|
942
|
+
);
|
|
943
|
+
const grown = new Uint8Array(capacity);
|
|
944
|
+
if (storage !== void 0) grown.set(storage);
|
|
945
|
+
storage = grown;
|
|
946
|
+
}
|
|
947
|
+
storage.set(value, total);
|
|
948
|
+
total = required;
|
|
949
|
+
}
|
|
950
|
+
if (++readsThisTurn === READS_PER_TURN) {
|
|
951
|
+
readsThisTurn = 0;
|
|
952
|
+
await new Promise((resolve) => {
|
|
953
|
+
finishYield = resolve;
|
|
954
|
+
yieldTimer = setTimeout(() => {
|
|
955
|
+
yieldTimer = void 0;
|
|
956
|
+
finishYield = void 0;
|
|
957
|
+
resolve();
|
|
958
|
+
}, 0);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
if (signal !== null && signal !== void 0) {
|
|
965
|
+
try {
|
|
966
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (timeoutMs !== void 0) {
|
|
971
|
+
deadlineTimer = setTimeout(() => stopWith(readError("timeout")), timeoutMs);
|
|
972
|
+
const handle = deadlineTimer;
|
|
973
|
+
handle.unref?.();
|
|
974
|
+
}
|
|
975
|
+
return await Promise.race([readLoop(), stop]);
|
|
976
|
+
} catch (reason) {
|
|
977
|
+
stopped = true;
|
|
978
|
+
stopReason = reason;
|
|
979
|
+
try {
|
|
980
|
+
void reader.cancel(reason).catch(() => void 0);
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
throw reason;
|
|
984
|
+
} finally {
|
|
985
|
+
stopped = true;
|
|
986
|
+
storage = void 0;
|
|
987
|
+
if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
|
|
988
|
+
if (yieldTimer !== void 0) clearTimeout(yieldTimer);
|
|
989
|
+
finishYield?.();
|
|
990
|
+
if (signal !== null && signal !== void 0) {
|
|
991
|
+
try {
|
|
992
|
+
signal.removeEventListener("abort", onAbort);
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
reader.releaseLock();
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
872
1003
|
// src/signer-client.ts
|
|
873
1004
|
var SIGNER_METADATA_BYTE_CAP = 4096;
|
|
874
1005
|
var DEFAULT_SIGNER_LOAD_TIMEOUT_MS = 5e3;
|
|
1006
|
+
var SIGNER_METADATA_READ_TIMEOUT_MS = 5e3;
|
|
875
1007
|
var MIN_SIGNER_LOAD_TIMEOUT_MS = 1;
|
|
876
1008
|
var MAX_SIGNER_LOAD_TIMEOUT_MS = 6e4;
|
|
877
1009
|
var FETCH_BUNDLE_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -902,7 +1034,7 @@ function resolveFetch(candidate) {
|
|
|
902
1034
|
"fetch is unavailable. Signer discovery requires a fetch implementation."
|
|
903
1035
|
);
|
|
904
1036
|
}
|
|
905
|
-
return globalFetch;
|
|
1037
|
+
return globalFetch.bind(globalThis);
|
|
906
1038
|
}
|
|
907
1039
|
if (typeof candidate !== "function") {
|
|
908
1040
|
throw new AttestationProtocolError("signer client fetch must be a function");
|
|
@@ -1033,56 +1165,46 @@ function resolveSignerModuleUrl(signerUrl, metadataUrl, apiOrigin) {
|
|
|
1033
1165
|
}
|
|
1034
1166
|
return resolved.href;
|
|
1035
1167
|
}
|
|
1036
|
-
async function readBoundedMetadataDocument(response,
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
let text = null;
|
|
1040
|
-
if (typeof response.arrayBuffer === "function") {
|
|
1041
|
-
let buffer;
|
|
1042
|
-
try {
|
|
1043
|
-
buffer = await response.arrayBuffer();
|
|
1044
|
-
} catch (error) {
|
|
1045
|
-
if (error instanceof SignerClientError) {
|
|
1046
|
-
throw error;
|
|
1047
|
-
}
|
|
1048
|
-
const name = error.name;
|
|
1049
|
-
if (name === "AbortError" || response.signal !== void 0) {
|
|
1050
|
-
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
|
|
1051
|
-
}
|
|
1052
|
-
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
|
|
1053
|
-
}
|
|
1054
|
-
const view = new Uint8Array(buffer);
|
|
1055
|
-
if (view.byteLength > SIGNER_METADATA_BYTE_CAP) {
|
|
1056
|
-
throw new SignerClientError(
|
|
1057
|
-
"SIGNER_LOAD_FAILED",
|
|
1058
|
-
`Signer metadata exceeds the ${SIGNER_METADATA_BYTE_CAP}-byte cap.`
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
bytes = view;
|
|
1062
|
-
} else if (typeof response.text === "function") {
|
|
1063
|
-
try {
|
|
1064
|
-
text = await response.text();
|
|
1065
|
-
} catch {
|
|
1066
|
-
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
|
|
1067
|
-
}
|
|
1068
|
-
if (utf8ByteLength(text) > SIGNER_METADATA_BYTE_CAP) {
|
|
1069
|
-
throw new SignerClientError(
|
|
1070
|
-
"SIGNER_LOAD_FAILED",
|
|
1071
|
-
`Signer metadata exceeds the ${SIGNER_METADATA_BYTE_CAP}-byte cap.`
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
} else {
|
|
1168
|
+
async function readBoundedMetadataDocument(response, signal, timeoutMs) {
|
|
1169
|
+
const body = response.body;
|
|
1170
|
+
if (body === void 0 || body !== null && typeof body.getReader !== "function") {
|
|
1075
1171
|
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata response is unreadable.");
|
|
1076
1172
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1173
|
+
const bytes = body === null ? new Uint8Array(0) : await readBoundedByteStream(body, {
|
|
1174
|
+
maxBytes: SIGNER_METADATA_BYTE_CAP,
|
|
1175
|
+
signal,
|
|
1176
|
+
timeoutMs,
|
|
1177
|
+
error(failure, cause) {
|
|
1178
|
+
switch (failure) {
|
|
1179
|
+
case "abort":
|
|
1180
|
+
return new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
|
|
1181
|
+
case "limit":
|
|
1182
|
+
return new SignerClientError(
|
|
1183
|
+
"SIGNER_LOAD_FAILED",
|
|
1184
|
+
`Signer metadata exceeds the ${SIGNER_METADATA_BYTE_CAP}-byte cap.`
|
|
1185
|
+
);
|
|
1186
|
+
case "timeout":
|
|
1187
|
+
return new SignerClientError(
|
|
1188
|
+
"SIGNER_LOAD_FAILED",
|
|
1189
|
+
`Signer metadata body read timed out after ${timeoutMs} ms.`
|
|
1190
|
+
);
|
|
1191
|
+
case "locked":
|
|
1192
|
+
case "chunk":
|
|
1193
|
+
return new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata response is unreadable.");
|
|
1194
|
+
case "read":
|
|
1195
|
+
if (cause instanceof SignerClientError) return cause;
|
|
1196
|
+
if (cause?.name === "AbortError") {
|
|
1197
|
+
return new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
|
|
1198
|
+
}
|
|
1199
|
+
return new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
|
|
1200
|
+
}
|
|
1085
1201
|
}
|
|
1202
|
+
});
|
|
1203
|
+
let raw;
|
|
1204
|
+
try {
|
|
1205
|
+
raw = utf8Decode(bytes);
|
|
1206
|
+
} catch {
|
|
1207
|
+
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata is not valid UTF-8.");
|
|
1086
1208
|
}
|
|
1087
1209
|
try {
|
|
1088
1210
|
return JSON.parse(raw);
|
|
@@ -1131,7 +1253,7 @@ function validateMetadataDocument(document, expected) {
|
|
|
1131
1253
|
return {
|
|
1132
1254
|
version: 1,
|
|
1133
1255
|
keyId: record["keyId"],
|
|
1134
|
-
signerUrl: record["signerUrl"],
|
|
1256
|
+
signerUrl: resolveSignerModuleUrl(record["signerUrl"], expected.metadataUrl, expected.apiOrigin),
|
|
1135
1257
|
publicOrigin: record["publicOrigin"],
|
|
1136
1258
|
replayNamespace: record["replayNamespace"]
|
|
1137
1259
|
};
|
|
@@ -1193,13 +1315,14 @@ function validateSignerModule(candidate, metadata) {
|
|
|
1193
1315
|
}
|
|
1194
1316
|
return { signer };
|
|
1195
1317
|
}
|
|
1196
|
-
async function fetchMetadataDocument(config) {
|
|
1318
|
+
async function fetchMetadataDocument(config, readTimeoutMs) {
|
|
1197
1319
|
if (config.signal?.aborted) {
|
|
1198
1320
|
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
|
|
1199
1321
|
}
|
|
1200
1322
|
let response;
|
|
1201
1323
|
try {
|
|
1202
|
-
|
|
1324
|
+
const fetchImpl = config.fetchImpl;
|
|
1325
|
+
response = await fetchImpl(config.metadataUrl, {
|
|
1203
1326
|
cache: "no-store",
|
|
1204
1327
|
credentials: "omit",
|
|
1205
1328
|
redirect: "error",
|
|
@@ -1216,19 +1339,23 @@ async function fetchMetadataDocument(config) {
|
|
|
1216
1339
|
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
|
|
1217
1340
|
}
|
|
1218
1341
|
if (response.ok !== true) {
|
|
1342
|
+
try {
|
|
1343
|
+
void response.body?.cancel().catch(() => void 0);
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1219
1346
|
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
|
|
1220
1347
|
}
|
|
1221
|
-
const document = await readBoundedMetadataDocument(response,
|
|
1348
|
+
const document = await readBoundedMetadataDocument(response, config.signal, readTimeoutMs);
|
|
1222
1349
|
return validateMetadataDocument(document, {
|
|
1223
1350
|
apiOrigin: config.apiOrigin,
|
|
1224
|
-
replayNamespace: config.replayNamespace
|
|
1351
|
+
replayNamespace: config.replayNamespace,
|
|
1352
|
+
metadataUrl: config.metadataUrl
|
|
1225
1353
|
});
|
|
1226
1354
|
}
|
|
1227
1355
|
async function importValidatedModule(config, metadata) {
|
|
1228
|
-
const moduleUrl = resolveSignerModuleUrl(metadata.signerUrl, config.metadataUrl, config.apiOrigin);
|
|
1229
1356
|
let candidate;
|
|
1230
1357
|
try {
|
|
1231
|
-
candidate = await config.loadModule(
|
|
1358
|
+
candidate = await config.loadModule(metadata.signerUrl);
|
|
1232
1359
|
} catch (error) {
|
|
1233
1360
|
if (error instanceof SignerClientError) {
|
|
1234
1361
|
throw error;
|
|
@@ -1243,7 +1370,7 @@ async function importValidatedModule(config, metadata) {
|
|
|
1243
1370
|
}
|
|
1244
1371
|
async function fetchSignerBundle(options) {
|
|
1245
1372
|
const config = validateBundleConfig(options);
|
|
1246
|
-
const metadata = await fetchMetadataDocument(config);
|
|
1373
|
+
const metadata = await fetchMetadataDocument(config, SIGNER_METADATA_READ_TIMEOUT_MS);
|
|
1247
1374
|
return importValidatedModule(config, metadata);
|
|
1248
1375
|
}
|
|
1249
1376
|
function isRetirementRetryCandidate(error) {
|
|
@@ -1274,6 +1401,7 @@ function createSignerClient(options) {
|
|
|
1274
1401
|
let generation = 0;
|
|
1275
1402
|
let disposed = false;
|
|
1276
1403
|
let currentAborter = null;
|
|
1404
|
+
let awaitingReplacement = false;
|
|
1277
1405
|
function throwIfDisposed() {
|
|
1278
1406
|
if (disposed) {
|
|
1279
1407
|
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer client is disposed.");
|
|
@@ -1300,7 +1428,7 @@ function createSignerClient(options) {
|
|
|
1300
1428
|
throw importError;
|
|
1301
1429
|
}
|
|
1302
1430
|
const second = await fetchMetadataDocument(base);
|
|
1303
|
-
if (second.keyId === first.keyId) {
|
|
1431
|
+
if (second.keyId === first.keyId && second.signerUrl === first.signerUrl) {
|
|
1304
1432
|
throw importError;
|
|
1305
1433
|
}
|
|
1306
1434
|
if (signal.aborted) {
|
|
@@ -1335,32 +1463,34 @@ function createSignerClient(options) {
|
|
|
1335
1463
|
() => void 0,
|
|
1336
1464
|
() => void 0
|
|
1337
1465
|
);
|
|
1466
|
+
function releaseLoad() {
|
|
1467
|
+
if (timer !== void 0) {
|
|
1468
|
+
clearTimeout(timer);
|
|
1469
|
+
}
|
|
1470
|
+
if (pending === tracked) {
|
|
1471
|
+
pending = null;
|
|
1472
|
+
}
|
|
1473
|
+
if (currentAborter === aborter) {
|
|
1474
|
+
currentAborter = null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1338
1477
|
const tracked = raced.then(
|
|
1339
1478
|
(signer) => {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (disposed || myGeneration !== generation) {
|
|
1479
|
+
const ownsGeneration = pending === tracked && myGeneration === generation;
|
|
1480
|
+
releaseLoad();
|
|
1481
|
+
if (disposed || !ownsGeneration) {
|
|
1344
1482
|
throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer load was superseded.");
|
|
1345
1483
|
}
|
|
1346
1484
|
active = signer;
|
|
1347
|
-
|
|
1348
|
-
pending = null;
|
|
1349
|
-
}
|
|
1350
|
-
currentAborter = null;
|
|
1485
|
+
awaitingReplacement = false;
|
|
1351
1486
|
return signer;
|
|
1352
1487
|
},
|
|
1353
1488
|
(error) => {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
if (pending === tracked) {
|
|
1358
|
-
pending = null;
|
|
1359
|
-
}
|
|
1360
|
-
if (error instanceof SignerClientError && error.code === "SIGNER_LOAD_TIMEOUT") {
|
|
1489
|
+
const ownsGeneration = pending === tracked && myGeneration === generation;
|
|
1490
|
+
releaseLoad();
|
|
1491
|
+
if (ownsGeneration && error instanceof SignerClientError && error.code === "SIGNER_LOAD_TIMEOUT") {
|
|
1361
1492
|
generation += 1;
|
|
1362
1493
|
}
|
|
1363
|
-
currentAborter = null;
|
|
1364
1494
|
throw error;
|
|
1365
1495
|
}
|
|
1366
1496
|
);
|
|
@@ -1385,7 +1515,9 @@ function createSignerClient(options) {
|
|
|
1385
1515
|
if (disposed) {
|
|
1386
1516
|
return;
|
|
1387
1517
|
}
|
|
1388
|
-
|
|
1518
|
+
try {
|
|
1519
|
+
validateKeyId(observedKeyId);
|
|
1520
|
+
} catch {
|
|
1389
1521
|
return;
|
|
1390
1522
|
}
|
|
1391
1523
|
if (active !== null) {
|
|
@@ -1398,11 +1530,11 @@ function createSignerClient(options) {
|
|
|
1398
1530
|
if (!matches) {
|
|
1399
1531
|
return;
|
|
1400
1532
|
}
|
|
1401
|
-
|
|
1402
|
-
generation += 1;
|
|
1403
|
-
pending = null;
|
|
1533
|
+
} else if (awaitingReplacement) {
|
|
1404
1534
|
return;
|
|
1405
1535
|
}
|
|
1536
|
+
active = null;
|
|
1537
|
+
awaitingReplacement = true;
|
|
1406
1538
|
generation += 1;
|
|
1407
1539
|
pending = null;
|
|
1408
1540
|
},
|
|
@@ -1412,6 +1544,7 @@ function createSignerClient(options) {
|
|
|
1412
1544
|
}
|
|
1413
1545
|
disposed = true;
|
|
1414
1546
|
active = null;
|
|
1547
|
+
awaitingReplacement = false;
|
|
1415
1548
|
generation += 1;
|
|
1416
1549
|
try {
|
|
1417
1550
|
currentAborter?.abort();
|
|
@@ -1536,165 +1669,66 @@ async function materializeRequestBodyBytes(request, options = {}) {
|
|
|
1536
1669
|
if (body === null || body === void 0) {
|
|
1537
1670
|
return new Uint8Array(0);
|
|
1538
1671
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
}
|
|
1567
|
-
const deadline = new Promise((_resolve, reject) => {
|
|
1568
|
-
timer = setTimeout(() => {
|
|
1569
|
-
if (settled) {
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
try {
|
|
1573
|
-
void reader.cancel();
|
|
1574
|
-
} catch {
|
|
1672
|
+
return readBoundedByteStream(body, {
|
|
1673
|
+
maxBytes: maxBodyBytes,
|
|
1674
|
+
timeoutMs,
|
|
1675
|
+
signal,
|
|
1676
|
+
error(failure, cause) {
|
|
1677
|
+
switch (failure) {
|
|
1678
|
+
case "abort":
|
|
1679
|
+
return toAbortError(signal);
|
|
1680
|
+
case "locked":
|
|
1681
|
+
return new SignerClientError(
|
|
1682
|
+
"SIGNER_PREPARATION_FAILED",
|
|
1683
|
+
"Request body is locked. Clone the Request before sending or pass a fresh body."
|
|
1684
|
+
);
|
|
1685
|
+
case "chunk":
|
|
1686
|
+
return new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body chunk is not bytes.");
|
|
1687
|
+
case "limit":
|
|
1688
|
+
return new SignerClientError(
|
|
1689
|
+
"SIGNER_PREPARATION_FAILED",
|
|
1690
|
+
`Request body exceeds the configured ${maxBodyBytes}-byte limit.`
|
|
1691
|
+
);
|
|
1692
|
+
case "timeout":
|
|
1693
|
+
return new SignerClientError(
|
|
1694
|
+
"SIGNER_PREPARATION_FAILED",
|
|
1695
|
+
`Request body read timed out after ${timeoutMs} ms.`
|
|
1696
|
+
);
|
|
1697
|
+
case "read":
|
|
1698
|
+
return cause instanceof SignerClientError ? cause : new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body read failed.");
|
|
1575
1699
|
}
|
|
1576
|
-
reject(new SignerClientError("SIGNER_PREPARATION_FAILED", `Request body read timed out after ${timeoutMs} ms.`));
|
|
1577
|
-
}, timeoutMs);
|
|
1578
|
-
const handle = timer;
|
|
1579
|
-
if (typeof handle.unref === "function") {
|
|
1580
|
-
handle.unref();
|
|
1581
1700
|
}
|
|
1582
1701
|
});
|
|
1583
|
-
deadline.then(
|
|
1584
|
-
() => void 0,
|
|
1585
|
-
() => void 0
|
|
1586
|
-
);
|
|
1587
|
-
if (signal !== null && signal !== void 0) {
|
|
1588
|
-
const abortPromise = new Promise((_resolve, reject) => {
|
|
1589
|
-
onAbort = () => {
|
|
1590
|
-
try {
|
|
1591
|
-
void reader.cancel();
|
|
1592
|
-
} catch {
|
|
1593
|
-
}
|
|
1594
|
-
reject(toAbortError(signal));
|
|
1595
|
-
};
|
|
1596
|
-
try {
|
|
1597
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1598
|
-
} catch {
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
abortPromise.then(
|
|
1602
|
-
() => void 0,
|
|
1603
|
-
() => void 0
|
|
1604
|
-
);
|
|
1605
|
-
try {
|
|
1606
|
-
const result = await Promise.race([readLoop(), deadline, abortPromise]);
|
|
1607
|
-
cleanup();
|
|
1608
|
-
try {
|
|
1609
|
-
reader.releaseLock();
|
|
1610
|
-
} catch {
|
|
1611
|
-
}
|
|
1612
|
-
return result;
|
|
1613
|
-
} catch (error) {
|
|
1614
|
-
cleanup();
|
|
1615
|
-
try {
|
|
1616
|
-
reader.releaseLock();
|
|
1617
|
-
} catch {
|
|
1618
|
-
}
|
|
1619
|
-
throw error;
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
async function readLoop() {
|
|
1623
|
-
for (; ; ) {
|
|
1624
|
-
if (signal?.aborted === true) {
|
|
1625
|
-
try {
|
|
1626
|
-
await reader.cancel();
|
|
1627
|
-
} catch {
|
|
1628
|
-
}
|
|
1629
|
-
throw toAbortError(signal);
|
|
1630
|
-
}
|
|
1631
|
-
let chunk;
|
|
1632
|
-
try {
|
|
1633
|
-
chunk = await reader.read();
|
|
1634
|
-
} catch (error) {
|
|
1635
|
-
try {
|
|
1636
|
-
await reader.cancel();
|
|
1637
|
-
} catch {
|
|
1638
|
-
}
|
|
1639
|
-
if (error instanceof SignerClientError) {
|
|
1640
|
-
throw error;
|
|
1641
|
-
}
|
|
1642
|
-
throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body read failed.");
|
|
1643
|
-
}
|
|
1644
|
-
if (chunk.done === true) {
|
|
1645
|
-
break;
|
|
1646
|
-
}
|
|
1647
|
-
const value = chunk.value;
|
|
1648
|
-
if (!(value instanceof Uint8Array)) {
|
|
1649
|
-
try {
|
|
1650
|
-
await reader.cancel();
|
|
1651
|
-
} catch {
|
|
1652
|
-
}
|
|
1653
|
-
throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body chunk is not bytes.");
|
|
1654
|
-
}
|
|
1655
|
-
total += value.byteLength;
|
|
1656
|
-
if (total > maxBodyBytes) {
|
|
1657
|
-
try {
|
|
1658
|
-
await reader.cancel();
|
|
1659
|
-
} catch {
|
|
1660
|
-
}
|
|
1661
|
-
throw new SignerClientError(
|
|
1662
|
-
"SIGNER_PREPARATION_FAILED",
|
|
1663
|
-
`Request body exceeds the configured ${maxBodyBytes}-byte limit.`
|
|
1664
|
-
);
|
|
1665
|
-
}
|
|
1666
|
-
chunks.push(value.slice());
|
|
1667
|
-
}
|
|
1668
|
-
const out = new Uint8Array(total);
|
|
1669
|
-
let offset = 0;
|
|
1670
|
-
for (const part of chunks) {
|
|
1671
|
-
out.set(part, offset);
|
|
1672
|
-
offset += part.byteLength;
|
|
1673
|
-
}
|
|
1674
|
-
return out;
|
|
1675
|
-
}
|
|
1676
|
-
try {
|
|
1677
|
-
const result = await Promise.race([readLoop(), deadline]);
|
|
1678
|
-
cleanup();
|
|
1679
|
-
try {
|
|
1680
|
-
reader.releaseLock();
|
|
1681
|
-
} catch {
|
|
1682
|
-
}
|
|
1683
|
-
return result;
|
|
1684
|
-
} catch (error) {
|
|
1685
|
-
cleanup();
|
|
1686
|
-
try {
|
|
1687
|
-
reader.releaseLock();
|
|
1688
|
-
} catch {
|
|
1689
|
-
}
|
|
1690
|
-
throw error;
|
|
1691
|
-
}
|
|
1692
1702
|
}
|
|
1693
1703
|
|
|
1694
1704
|
// src/attested-fetch.ts
|
|
1695
1705
|
var DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS2 = 5e3;
|
|
1696
1706
|
var MIN_FETCH_TIMEOUT_MS = 1;
|
|
1697
1707
|
var MAX_FETCH_TIMEOUT_MS = 6e4;
|
|
1708
|
+
var FORBIDDEN_FETCH_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
1709
|
+
"accept-charset",
|
|
1710
|
+
"accept-encoding",
|
|
1711
|
+
"access-control-request-headers",
|
|
1712
|
+
"access-control-request-method",
|
|
1713
|
+
"connection",
|
|
1714
|
+
"content-length",
|
|
1715
|
+
"cookie",
|
|
1716
|
+
"cookie2",
|
|
1717
|
+
"date",
|
|
1718
|
+
"dnt",
|
|
1719
|
+
"expect",
|
|
1720
|
+
"host",
|
|
1721
|
+
"keep-alive",
|
|
1722
|
+
"origin",
|
|
1723
|
+
"referer",
|
|
1724
|
+
"set-cookie",
|
|
1725
|
+
"te",
|
|
1726
|
+
"trailer",
|
|
1727
|
+
"transfer-encoding",
|
|
1728
|
+
"upgrade",
|
|
1729
|
+
"user-agent",
|
|
1730
|
+
"via"
|
|
1731
|
+
]);
|
|
1698
1732
|
var FETCH_WITH_ATTESTATION_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
1699
1733
|
"signerClient",
|
|
1700
1734
|
"apiOrigin",
|
|
@@ -1719,7 +1753,7 @@ function resolveFetchImpl(fetchAlias, fetchFnAlias) {
|
|
|
1719
1753
|
"fetch is unavailable. Prepared fetch requires a fetch implementation."
|
|
1720
1754
|
);
|
|
1721
1755
|
}
|
|
1722
|
-
return globalFetch;
|
|
1756
|
+
return globalFetch.bind(globalThis);
|
|
1723
1757
|
}
|
|
1724
1758
|
if (typeof candidate !== "function") {
|
|
1725
1759
|
throw new AttestationProtocolError("fetchWithAttestation fetch must be a function");
|
|
@@ -1752,6 +1786,23 @@ function validateBodyReadTimeoutMs2(value) {
|
|
|
1752
1786
|
}
|
|
1753
1787
|
return value;
|
|
1754
1788
|
}
|
|
1789
|
+
function assertFetchProofHeaderName(headerName) {
|
|
1790
|
+
const lower = headerName.toLowerCase();
|
|
1791
|
+
if (lower.startsWith("sec-") || lower.startsWith("proxy-") || FORBIDDEN_FETCH_HEADER_NAMES.has(lower)) {
|
|
1792
|
+
throw new SignerClientError(
|
|
1793
|
+
"SIGNER_PREPARATION_FAILED",
|
|
1794
|
+
"Attestation headerName must be writable by browser Fetch; browser-forbidden request headers cannot carry a proof."
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
function assertAttestableMode(mode) {
|
|
1799
|
+
if (mode === "no-cors") {
|
|
1800
|
+
throw new SignerClientError(
|
|
1801
|
+
"SIGNER_PREPARATION_FAILED",
|
|
1802
|
+
"Request mode no-cors cannot carry an attestation proof. Use cors or same-origin."
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1755
1806
|
function resolveOptions(options) {
|
|
1756
1807
|
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
1757
1808
|
throw new AttestationProtocolError("fetchWithAttestation options must be an object");
|
|
@@ -1773,10 +1824,11 @@ function resolveOptions(options) {
|
|
|
1773
1824
|
);
|
|
1774
1825
|
const headerName = options.headerName ?? DEFAULT_ATTESTATION_HEADER;
|
|
1775
1826
|
validateHeaderName(headerName);
|
|
1827
|
+
assertFetchProofHeaderName(headerName);
|
|
1776
1828
|
const maxBodyBytes = validateMaxBodyBytes2(options.maxBodyBytes);
|
|
1777
1829
|
const bodyReadTimeoutMs = validateBodyReadTimeoutMs2(options.bodyReadTimeoutMs);
|
|
1778
|
-
const
|
|
1779
|
-
if (typeof
|
|
1830
|
+
const now2 = options.now ?? Date.now;
|
|
1831
|
+
if (typeof now2 !== "function") {
|
|
1780
1832
|
throw new AttestationProtocolError("fetchWithAttestation now must be a function");
|
|
1781
1833
|
}
|
|
1782
1834
|
return Object.freeze({
|
|
@@ -1787,13 +1839,13 @@ function resolveOptions(options) {
|
|
|
1787
1839
|
headerName,
|
|
1788
1840
|
maxBodyBytes,
|
|
1789
1841
|
bodyReadTimeoutMs,
|
|
1790
|
-
now
|
|
1842
|
+
now: now2
|
|
1791
1843
|
});
|
|
1792
1844
|
}
|
|
1793
|
-
function checkedTimestampMs(
|
|
1845
|
+
function checkedTimestampMs(now2) {
|
|
1794
1846
|
let value;
|
|
1795
1847
|
try {
|
|
1796
|
-
value =
|
|
1848
|
+
value = now2();
|
|
1797
1849
|
} catch {
|
|
1798
1850
|
throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Attestation clock failed.");
|
|
1799
1851
|
}
|
|
@@ -1919,9 +1971,18 @@ function buildSignedRequest(snapshot, headerName, transactionId) {
|
|
|
1919
1971
|
if (bodyForFetch !== null) {
|
|
1920
1972
|
init.body = bodyForFetch;
|
|
1921
1973
|
}
|
|
1974
|
+
let request;
|
|
1922
1975
|
try {
|
|
1923
|
-
|
|
1976
|
+
request = new Request(snapshot.url, init);
|
|
1977
|
+
if (request.headers.get(headerName) !== transactionId) {
|
|
1978
|
+
throw new SignerClientError(
|
|
1979
|
+
"SIGNER_PREPARATION_FAILED",
|
|
1980
|
+
"Prepared request did not retain the exact attestation proof header."
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
return request;
|
|
1924
1984
|
} catch (error) {
|
|
1985
|
+
cancelOwnedBody(request);
|
|
1925
1986
|
if (error instanceof SignerClientError || error instanceof AttestationProtocolError) {
|
|
1926
1987
|
throw error;
|
|
1927
1988
|
}
|
|
@@ -1938,15 +1999,111 @@ function isStaleKeyRetryCandidate(response) {
|
|
|
1938
1999
|
return false;
|
|
1939
2000
|
}
|
|
1940
2001
|
}
|
|
2002
|
+
function cancelOwnedBody(owner) {
|
|
2003
|
+
try {
|
|
2004
|
+
void owner?.body?.cancel().catch(() => void 0);
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function throwIfAborted(signal) {
|
|
2009
|
+
if (signal?.aborted) {
|
|
2010
|
+
if (signal.reason !== void 0) throw signal.reason;
|
|
2011
|
+
throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request was aborted before sending.");
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
async function withPreparationAbort(signal, prepare) {
|
|
2015
|
+
throwIfAborted(signal);
|
|
2016
|
+
if (signal === null) return prepare();
|
|
2017
|
+
let onAbort;
|
|
2018
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
2019
|
+
onAbort = () => {
|
|
2020
|
+
try {
|
|
2021
|
+
throwIfAborted(signal);
|
|
2022
|
+
} catch (reason) {
|
|
2023
|
+
reject(reason);
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2027
|
+
});
|
|
2028
|
+
try {
|
|
2029
|
+
return await Promise.race([Promise.resolve().then(prepare), aborted]);
|
|
2030
|
+
} finally {
|
|
2031
|
+
signal.removeEventListener("abort", onAbort);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function copyRequestWithoutBody(input) {
|
|
2035
|
+
return new Request(input.url, {
|
|
2036
|
+
method: input.method,
|
|
2037
|
+
headers: input.headers,
|
|
2038
|
+
credentials: input.credentials,
|
|
2039
|
+
signal: input.signal,
|
|
2040
|
+
mode: input.mode === "navigate" ? "same-origin" : input.mode,
|
|
2041
|
+
cache: input.cache,
|
|
2042
|
+
referrer: input.referrer,
|
|
2043
|
+
referrerPolicy: input.referrerPolicy,
|
|
2044
|
+
integrity: input.integrity,
|
|
2045
|
+
keepalive: input.keepalive,
|
|
2046
|
+
redirect: input.redirect
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
async function prepareSnapshot(input, init, overridingBody, config) {
|
|
2050
|
+
let unreadOwner;
|
|
2051
|
+
try {
|
|
2052
|
+
let base;
|
|
2053
|
+
try {
|
|
2054
|
+
let source = input;
|
|
2055
|
+
if (input instanceof Request) {
|
|
2056
|
+
if (overridingBody) {
|
|
2057
|
+
source = copyRequestWithoutBody(input);
|
|
2058
|
+
} else if (input.body !== null) {
|
|
2059
|
+
unreadOwner = input.clone();
|
|
2060
|
+
source = unreadOwner;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
base = new Request(source, init);
|
|
2064
|
+
unreadOwner = base;
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
if (error instanceof SignerClientError || error instanceof AttestationProtocolError) throw error;
|
|
2067
|
+
const message = error instanceof Error ? `: ${error.message}` : "";
|
|
2068
|
+
throw new SignerClientError("SIGNER_PREPARATION_FAILED", `Request could not be prepared${message}.`);
|
|
2069
|
+
}
|
|
2070
|
+
assertAttestableMode(base.mode);
|
|
2071
|
+
if (new URL(base.url).origin !== config.apiOrigin) {
|
|
2072
|
+
throw new SignerClientError(
|
|
2073
|
+
"SIGNER_ORIGIN_MISMATCH",
|
|
2074
|
+
"Request origin does not match the configured API origin. Signatures are only attached to the configured API origin."
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
assertAllowedContentEncoding(base.headers);
|
|
2078
|
+
throwIfAborted(base.signal);
|
|
2079
|
+
unreadOwner = void 0;
|
|
2080
|
+
const bodyBytes = await materializeRequestBodyBytes(base, {
|
|
2081
|
+
maxBodyBytes: config.maxBodyBytes,
|
|
2082
|
+
signal: base.signal,
|
|
2083
|
+
timeoutMs: config.bodyReadTimeoutMs
|
|
2084
|
+
});
|
|
2085
|
+
return buildPreparedSnapshot(base, bodyBytes);
|
|
2086
|
+
} finally {
|
|
2087
|
+
cancelOwnedBody(unreadOwner);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
1941
2090
|
async function fetchWithAttestation(input, init, options) {
|
|
1942
2091
|
const config = resolveOptions(options);
|
|
1943
2092
|
if (init !== void 0 && (init === null || typeof init !== "object" || Array.isArray(init))) {
|
|
1944
2093
|
throw new AttestationProtocolError("fetchWithAttestation init must be an object");
|
|
1945
2094
|
}
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2095
|
+
const initMode = init?.mode;
|
|
2096
|
+
assertAttestableMode(initMode === void 0 && input instanceof Request ? input.mode : initMode);
|
|
2097
|
+
const initBody = init?.body;
|
|
2098
|
+
const overridingBody = initBody !== void 0 && initBody !== null;
|
|
2099
|
+
if (overridingBody) assertSupportedBodyInit(initBody);
|
|
2100
|
+
const stableInit = init === void 0 ? void 0 : new Proxy(init, {
|
|
2101
|
+
get(target, property) {
|
|
2102
|
+
if (property === "body") return initBody;
|
|
2103
|
+
if (property === "mode") return initMode;
|
|
2104
|
+
return Reflect.get(target, property, target);
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
1950
2107
|
if (typeof input === "string") {
|
|
1951
2108
|
if (input.length === 0) {
|
|
1952
2109
|
throw new AttestationProtocolError("fetchWithAttestation input must be a non-empty URL or Request");
|
|
@@ -1954,78 +2111,36 @@ async function fetchWithAttestation(input, init, options) {
|
|
|
1954
2111
|
} else if (!(input instanceof Request) && !(input instanceof URL)) {
|
|
1955
2112
|
throw new AttestationProtocolError("fetchWithAttestation input must be a URL, string, or Request");
|
|
1956
2113
|
}
|
|
1957
|
-
if (input instanceof Request) {
|
|
1958
|
-
if (input.bodyUsed
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
"Request body has already been used. Clone the Request before sending or pass a fresh body."
|
|
1964
|
-
);
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
try {
|
|
1968
|
-
const locked = input.body?.locked;
|
|
1969
|
-
if (locked === true && (initRecord.body === void 0 || initRecord.body === null)) {
|
|
1970
|
-
throw new SignerClientError(
|
|
1971
|
-
"SIGNER_PREPARATION_FAILED",
|
|
1972
|
-
"Request body is locked. Clone the Request before sending or pass a fresh body."
|
|
1973
|
-
);
|
|
1974
|
-
}
|
|
1975
|
-
} catch (error) {
|
|
1976
|
-
if (error instanceof SignerClientError) {
|
|
1977
|
-
throw error;
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
let base;
|
|
1982
|
-
try {
|
|
1983
|
-
base = new Request(input, init ?? {});
|
|
1984
|
-
} catch (error) {
|
|
1985
|
-
if (error instanceof SignerClientError || error instanceof AttestationProtocolError) {
|
|
1986
|
-
throw error;
|
|
2114
|
+
if (input instanceof Request && !overridingBody) {
|
|
2115
|
+
if (input.bodyUsed) {
|
|
2116
|
+
throw new SignerClientError(
|
|
2117
|
+
"SIGNER_PREPARATION_FAILED",
|
|
2118
|
+
"Request body has already been used. Clone the Request before sending or pass a fresh body."
|
|
2119
|
+
);
|
|
1987
2120
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
requestOrigin = new URL(base.url).origin;
|
|
1994
|
-
} catch {
|
|
1995
|
-
throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request URL is invalid.");
|
|
1996
|
-
}
|
|
1997
|
-
if (requestOrigin !== config.apiOrigin) {
|
|
1998
|
-
throw new SignerClientError(
|
|
1999
|
-
"SIGNER_ORIGIN_MISMATCH",
|
|
2000
|
-
"Request origin does not match the configured API origin. Signatures are only attached to the configured API origin."
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
assertAllowedContentEncoding(base.headers);
|
|
2004
|
-
if (base.signal?.aborted === true) {
|
|
2005
|
-
const reason = base.signal.reason;
|
|
2006
|
-
if (reason !== void 0) {
|
|
2007
|
-
throw reason;
|
|
2121
|
+
if (input.body?.locked) {
|
|
2122
|
+
throw new SignerClientError(
|
|
2123
|
+
"SIGNER_PREPARATION_FAILED",
|
|
2124
|
+
"Request body is locked. Clone the Request before sending or pass a fresh body."
|
|
2125
|
+
);
|
|
2008
2126
|
}
|
|
2009
|
-
throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request was aborted before signing.");
|
|
2010
2127
|
}
|
|
2011
|
-
const
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
});
|
|
2016
|
-
const snapshot = buildPreparedSnapshot(base, bodyBytes);
|
|
2017
|
-
async function signWithFreshProof(signerKeyIdCheck) {
|
|
2018
|
-
void signerKeyIdCheck;
|
|
2128
|
+
const snapshot = await prepareSnapshot(input, stableInit, overridingBody, config);
|
|
2129
|
+
let bodyHashHex;
|
|
2130
|
+
async function signWithFreshProof() {
|
|
2131
|
+
throwIfAborted(snapshot.signal);
|
|
2019
2132
|
const signer = await config.signerClient.getSigner();
|
|
2133
|
+
throwIfAborted(snapshot.signal);
|
|
2020
2134
|
if (signer.version !== 1 || signer.publicOrigin !== config.apiOrigin || signer.replayNamespace !== config.replayNamespace) {
|
|
2021
2135
|
throw new SignerClientError(
|
|
2022
2136
|
"SIGNER_ORIGIN_MISMATCH",
|
|
2023
2137
|
"Loaded signer protection space does not match the configured API origin/namespace."
|
|
2024
2138
|
);
|
|
2025
2139
|
}
|
|
2140
|
+
bodyHashHex ??= await hashBodySha256Hex(snapshot.bodyBytes);
|
|
2141
|
+
throwIfAborted(snapshot.signal);
|
|
2026
2142
|
const timestampMs = checkedTimestampMs(config.now);
|
|
2027
2143
|
const nonceHex = generateNonceHex();
|
|
2028
|
-
const bodyHashHex = await hashBodySha256Hex(snapshot.bodyBytes);
|
|
2029
2144
|
const transactionId = await signer.sign({
|
|
2030
2145
|
method: snapshot.method,
|
|
2031
2146
|
requestTarget: snapshot.requestTarget,
|
|
@@ -2034,11 +2149,22 @@ async function fetchWithAttestation(input, init, options) {
|
|
|
2034
2149
|
timestampMs,
|
|
2035
2150
|
nonceHex
|
|
2036
2151
|
});
|
|
2152
|
+
throwIfAborted(snapshot.signal);
|
|
2037
2153
|
return { transactionId, observedKeyId: signer.keyId };
|
|
2038
2154
|
}
|
|
2039
|
-
|
|
2155
|
+
async function send(request) {
|
|
2156
|
+
try {
|
|
2157
|
+
throwIfAborted(snapshot.signal);
|
|
2158
|
+
const fetchImpl = config.fetchImpl;
|
|
2159
|
+
return await fetchImpl(request);
|
|
2160
|
+
} catch (reason) {
|
|
2161
|
+
cancelOwnedBody(request);
|
|
2162
|
+
throw reason;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
const first = await withPreparationAbort(snapshot.signal, signWithFreshProof);
|
|
2040
2166
|
const firstRequest = buildSignedRequest(snapshot, config.headerName, first.transactionId);
|
|
2041
|
-
const firstResponse = await
|
|
2167
|
+
const firstResponse = await send(firstRequest);
|
|
2042
2168
|
if (!isStaleKeyRetryCandidate(firstResponse)) {
|
|
2043
2169
|
return firstResponse;
|
|
2044
2170
|
}
|
|
@@ -2046,19 +2172,21 @@ async function fetchWithAttestation(input, init, options) {
|
|
|
2046
2172
|
config.signerClient.invalidate(first.observedKeyId);
|
|
2047
2173
|
} catch {
|
|
2048
2174
|
}
|
|
2175
|
+
let secondRequest;
|
|
2049
2176
|
try {
|
|
2050
|
-
await
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2177
|
+
const second = await withPreparationAbort(snapshot.signal, signWithFreshProof);
|
|
2178
|
+
throwIfAborted(snapshot.signal);
|
|
2179
|
+
if (second.transactionId === first.transactionId) {
|
|
2180
|
+
return firstResponse;
|
|
2181
|
+
}
|
|
2182
|
+
secondRequest = buildSignedRequest(snapshot, config.headerName, second.transactionId);
|
|
2056
2183
|
} catch {
|
|
2184
|
+
if (snapshot.signal?.aborted) {
|
|
2185
|
+
cancelOwnedBody(firstResponse);
|
|
2186
|
+
throwIfAborted(snapshot.signal);
|
|
2187
|
+
}
|
|
2057
2188
|
return firstResponse;
|
|
2058
2189
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
}
|
|
2062
|
-
const secondRequest = buildSignedRequest(snapshot, config.headerName, secondSignerTransaction.transactionId);
|
|
2063
|
-
return config.fetchImpl(secondRequest);
|
|
2190
|
+
cancelOwnedBody(firstResponse);
|
|
2191
|
+
return send(secondRequest);
|
|
2064
2192
|
}
|