@web-ts-toolkit/express-request-attestation 0.50.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/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
- if (utf8ByteLength(encoded) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
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
- if (utf8ByteLength(headerValue) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
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 = null;
815
- throw pending;
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, fetchImplName) {
1037
- void fetchImplName;
1038
- let bytes = null;
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
- let raw;
1078
- if (text !== null) {
1079
- raw = text;
1080
- } else {
1081
- try {
1082
- raw = utf8Decode(bytes);
1083
- } catch {
1084
- throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata is not valid UTF-8.");
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
- response = await config.fetchImpl(config.metadataUrl, {
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, "fetch");
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(moduleUrl);
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
- if (timer !== void 0) {
1341
- clearTimeout(timer);
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
- if (pending === tracked) {
1348
- pending = null;
1349
- }
1350
- currentAborter = null;
1485
+ awaitingReplacement = false;
1351
1486
  return signer;
1352
1487
  },
1353
1488
  (error) => {
1354
- if (timer !== void 0) {
1355
- clearTimeout(timer);
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
- if (typeof observedKeyId !== "string" || observedKeyId.length === 0) {
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
- active = null;
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
- let reader;
1540
- try {
1541
- reader = body.getReader();
1542
- } catch {
1543
- throw new SignerClientError(
1544
- "SIGNER_PREPARATION_FAILED",
1545
- "Request body is locked. Clone the Request before sending or pass a fresh body."
1546
- );
1547
- }
1548
- const chunks = [];
1549
- let total = 0;
1550
- let timer;
1551
- let onAbort;
1552
- let settled = false;
1553
- function cleanup() {
1554
- settled = true;
1555
- if (timer !== void 0) {
1556
- clearTimeout(timer);
1557
- timer = void 0;
1558
- }
1559
- if (onAbort !== void 0 && signal !== null && signal !== void 0) {
1560
- try {
1561
- signal.removeEventListener("abort", onAbort);
1562
- } catch {
1563
- }
1564
- onAbort = void 0;
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 now = options.now ?? Date.now;
1779
- if (typeof now !== "function") {
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(now) {
1845
+ function checkedTimestampMs(now2) {
1794
1846
  let value;
1795
1847
  try {
1796
- value = now();
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
- return new Request(snapshot.url, init);
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 initRecord = init ?? {};
1947
- if (initRecord.body !== void 0 && initRecord.body !== null) {
1948
- assertSupportedBodyInit(initRecord.body);
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 === true) {
1959
- const overridingBody = initRecord.body !== void 0 && initRecord.body !== null;
1960
- if (!overridingBody) {
1961
- throw new SignerClientError(
1962
- "SIGNER_PREPARATION_FAILED",
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
- const message = error instanceof Error ? `: ${error.message}` : "";
1989
- throw new SignerClientError("SIGNER_PREPARATION_FAILED", `Request could not be prepared${message}.`);
1990
- }
1991
- let requestOrigin;
1992
- try {
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 bodyBytes = await materializeRequestBodyBytes(base, {
2012
- maxBodyBytes: config.maxBodyBytes,
2013
- signal: base.signal,
2014
- timeoutMs: config.bodyReadTimeoutMs
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
- const first = await signWithFreshProof();
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 config.fetchImpl(firstRequest);
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 firstResponse.body?.cancel();
2051
- } catch {
2052
- }
2053
- let secondSignerTransaction;
2054
- try {
2055
- secondSignerTransaction = await signWithFreshProof();
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
- if (secondSignerTransaction.transactionId === first.transactionId) {
2060
- return firstResponse;
2061
- }
2062
- const secondRequest = buildSignedRequest(snapshot, config.headerName, secondSignerTransaction.transactionId);
2063
- return config.fetchImpl(secondRequest);
2190
+ cancelOwnedBody(firstResponse);
2191
+ return send(secondRequest);
2064
2192
  }