@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.mjs CHANGED
@@ -168,6 +168,13 @@ var NONCE_HEX_PATTERN = /^[0-9a-f]{32}$/;
168
168
  var BODY_HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
169
169
  var HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
170
170
  var HEX_BYTE_PATTERN = /^[0-9a-fA-F]{2}$/;
171
+ function assertEncodedTransactionIdBound(value) {
172
+ if (value.length > MAX_ENCODED_TRANSACTION_ID_BYTES) {
173
+ throw new AttestationProtocolError(
174
+ `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
175
+ );
176
+ }
177
+ }
171
178
  var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
172
179
  "authorization",
173
180
  "proxy-authenticate",
@@ -363,6 +370,7 @@ function extractSingleHeaderValue(raw) {
363
370
  if (typeof value !== "string" || value.length === 0) {
364
371
  return null;
365
372
  }
373
+ assertEncodedTransactionIdBound(value);
366
374
  if (value.includes(",")) {
367
375
  throw new AttestationProtocolError("comma-joined signature headers are not allowed");
368
376
  }
@@ -422,23 +430,18 @@ function encodeTransactionId(fields) {
422
430
  const encoded = base64urlEncode(
423
431
  canonicalJsonBytes([PROTOCOL_VERSION, fields.keyId, fields.timestampMs, fields.nonceHex, fields.mac])
424
432
  );
425
- if (utf8ByteLength(encoded) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
426
- throw new AttestationProtocolError(
427
- `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
428
- );
429
- }
433
+ assertEncodedTransactionIdBound(encoded);
430
434
  return encoded;
431
435
  }
432
436
  function decodeTransactionId(headerValue) {
433
437
  if (typeof headerValue !== "string" || headerValue.length === 0) {
434
438
  throw new AttestationProtocolError("transactionId must be a non-empty string");
435
439
  }
436
- if (utf8ByteLength(headerValue) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
437
- throw new AttestationProtocolError(
438
- `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
439
- );
440
- }
440
+ assertEncodedTransactionIdBound(headerValue);
441
441
  const bytes = base64urlDecode(headerValue);
442
+ if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
443
+ throw new AttestationProtocolError("transactionId must not contain a UTF-8 BOM");
444
+ }
442
445
  const text = utf8Decode(bytes);
443
446
  const envelope = parseCanonicalJsonArray(text, 5);
444
447
  const [version, keyId, timestampMs, nonceHex, mac] = envelope;
@@ -707,12 +710,13 @@ function createRequestSigner(options) {
707
710
  })();
708
711
  cachedKey = pending;
709
712
  try {
710
- await pending;
711
- } catch {
712
- cachedKey = null;
713
- throw pending;
713
+ return await pending;
714
+ } catch (error) {
715
+ if (cachedKey === pending) {
716
+ cachedKey = null;
717
+ }
718
+ throw error;
714
719
  }
715
- return pending;
716
720
  }
717
721
  async function sign(input) {
718
722
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
@@ -767,9 +771,137 @@ function createRequestSigner(options) {
767
771
  return Object.freeze(signer);
768
772
  }
769
773
 
774
+ // src/bounded-byte-stream.ts
775
+ var INITIAL_CAPACITY = 1024;
776
+ var READS_PER_TURN = 128;
777
+ function now() {
778
+ return typeof globalThis.performance?.now === "function" ? globalThis.performance.now() : Date.now();
779
+ }
780
+ async function readBoundedByteStream(stream, options) {
781
+ const { maxBytes, timeoutMs, signal, error: readError } = options;
782
+ const started = timeoutMs === void 0 ? 0 : now();
783
+ let reader;
784
+ try {
785
+ reader = stream.getReader();
786
+ } catch (cause) {
787
+ throw readError("locked", cause);
788
+ }
789
+ let storage;
790
+ let total = 0;
791
+ let stopped = false;
792
+ let stopReason;
793
+ let deadlineTimer;
794
+ let yieldTimer;
795
+ let finishYield;
796
+ let rejectStop;
797
+ const stop = new Promise((_resolve, reject) => {
798
+ rejectStop = reject;
799
+ });
800
+ function stopWith(reason) {
801
+ if (stopped) return;
802
+ stopped = true;
803
+ stopReason = reason;
804
+ rejectStop(reason);
805
+ }
806
+ function onAbort() {
807
+ stopWith(readError("abort"));
808
+ }
809
+ function checkBounds() {
810
+ if (stopped) throw stopReason;
811
+ if (signal?.aborted) throw readError("abort");
812
+ if (timeoutMs !== void 0 && now() - started >= timeoutMs) throw readError("timeout");
813
+ }
814
+ async function readLoop() {
815
+ let readsThisTurn = 0;
816
+ for (; ; ) {
817
+ checkBounds();
818
+ let chunk;
819
+ try {
820
+ chunk = await reader.read();
821
+ } catch (cause) {
822
+ checkBounds();
823
+ throw readError("read", cause);
824
+ }
825
+ checkBounds();
826
+ if (chunk.done) {
827
+ const result = storage === void 0 ? new Uint8Array(0) : storage.length === total ? storage : storage.slice(0, total);
828
+ checkBounds();
829
+ return result;
830
+ }
831
+ const value = chunk.value;
832
+ if (!(value instanceof Uint8Array)) throw readError("chunk");
833
+ if (value.byteLength > maxBytes - total) throw readError("limit");
834
+ if (value.byteLength > 0) {
835
+ const required = total + value.byteLength;
836
+ if (storage === void 0 || required > storage.length) {
837
+ const capacity = Math.min(
838
+ maxBytes,
839
+ Math.max(required, storage === void 0 ? INITIAL_CAPACITY : storage.length * 2)
840
+ );
841
+ const grown = new Uint8Array(capacity);
842
+ if (storage !== void 0) grown.set(storage);
843
+ storage = grown;
844
+ }
845
+ storage.set(value, total);
846
+ total = required;
847
+ }
848
+ if (++readsThisTurn === READS_PER_TURN) {
849
+ readsThisTurn = 0;
850
+ await new Promise((resolve) => {
851
+ finishYield = resolve;
852
+ yieldTimer = setTimeout(() => {
853
+ yieldTimer = void 0;
854
+ finishYield = void 0;
855
+ resolve();
856
+ }, 0);
857
+ });
858
+ }
859
+ }
860
+ }
861
+ try {
862
+ if (signal !== null && signal !== void 0) {
863
+ try {
864
+ signal.addEventListener("abort", onAbort, { once: true });
865
+ } catch {
866
+ }
867
+ }
868
+ if (timeoutMs !== void 0) {
869
+ deadlineTimer = setTimeout(() => stopWith(readError("timeout")), timeoutMs);
870
+ const handle = deadlineTimer;
871
+ handle.unref?.();
872
+ }
873
+ return await Promise.race([readLoop(), stop]);
874
+ } catch (reason) {
875
+ stopped = true;
876
+ stopReason = reason;
877
+ try {
878
+ void reader.cancel(reason).catch(() => void 0);
879
+ } catch {
880
+ }
881
+ throw reason;
882
+ } finally {
883
+ stopped = true;
884
+ storage = void 0;
885
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
886
+ if (yieldTimer !== void 0) clearTimeout(yieldTimer);
887
+ finishYield?.();
888
+ if (signal !== null && signal !== void 0) {
889
+ try {
890
+ signal.removeEventListener("abort", onAbort);
891
+ } catch {
892
+ }
893
+ }
894
+ try {
895
+ reader.releaseLock();
896
+ } catch {
897
+ }
898
+ }
899
+ }
900
+
770
901
  // src/signer-client.ts
771
902
  var SIGNER_METADATA_BYTE_CAP = 4096;
772
903
  var DEFAULT_SIGNER_LOAD_TIMEOUT_MS = 5e3;
904
+ var SIGNER_METADATA_READ_TIMEOUT_MS = 5e3;
773
905
  var MIN_SIGNER_LOAD_TIMEOUT_MS = 1;
774
906
  var MAX_SIGNER_LOAD_TIMEOUT_MS = 6e4;
775
907
  var FETCH_BUNDLE_OPTION_KEYS = /* @__PURE__ */ new Set([
@@ -800,7 +932,7 @@ function resolveFetch(candidate) {
800
932
  "fetch is unavailable. Signer discovery requires a fetch implementation."
801
933
  );
802
934
  }
803
- return globalFetch;
935
+ return globalFetch.bind(globalThis);
804
936
  }
805
937
  if (typeof candidate !== "function") {
806
938
  throw new AttestationProtocolError("signer client fetch must be a function");
@@ -931,56 +1063,46 @@ function resolveSignerModuleUrl(signerUrl, metadataUrl, apiOrigin) {
931
1063
  }
932
1064
  return resolved.href;
933
1065
  }
934
- async function readBoundedMetadataDocument(response, fetchImplName) {
935
- void fetchImplName;
936
- let bytes = null;
937
- let text = null;
938
- if (typeof response.arrayBuffer === "function") {
939
- let buffer;
940
- try {
941
- buffer = await response.arrayBuffer();
942
- } catch (error) {
943
- if (error instanceof SignerClientError) {
944
- throw error;
945
- }
946
- const name = error.name;
947
- if (name === "AbortError" || response.signal !== void 0) {
948
- throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
949
- }
950
- throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
951
- }
952
- const view = new Uint8Array(buffer);
953
- if (view.byteLength > SIGNER_METADATA_BYTE_CAP) {
954
- throw new SignerClientError(
955
- "SIGNER_LOAD_FAILED",
956
- `Signer metadata exceeds the ${SIGNER_METADATA_BYTE_CAP}-byte cap.`
957
- );
958
- }
959
- bytes = view;
960
- } else if (typeof response.text === "function") {
961
- try {
962
- text = await response.text();
963
- } catch {
964
- throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
965
- }
966
- if (utf8ByteLength(text) > SIGNER_METADATA_BYTE_CAP) {
967
- throw new SignerClientError(
968
- "SIGNER_LOAD_FAILED",
969
- `Signer metadata exceeds the ${SIGNER_METADATA_BYTE_CAP}-byte cap.`
970
- );
971
- }
972
- } else {
1066
+ async function readBoundedMetadataDocument(response, signal, timeoutMs) {
1067
+ const body = response.body;
1068
+ if (body === void 0 || body !== null && typeof body.getReader !== "function") {
973
1069
  throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata response is unreadable.");
974
1070
  }
975
- let raw;
976
- if (text !== null) {
977
- raw = text;
978
- } else {
979
- try {
980
- raw = utf8Decode(bytes);
981
- } catch {
982
- throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata is not valid UTF-8.");
1071
+ const bytes = body === null ? new Uint8Array(0) : await readBoundedByteStream(body, {
1072
+ maxBytes: SIGNER_METADATA_BYTE_CAP,
1073
+ signal,
1074
+ timeoutMs,
1075
+ error(failure, cause) {
1076
+ switch (failure) {
1077
+ case "abort":
1078
+ return new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
1079
+ case "limit":
1080
+ return new SignerClientError(
1081
+ "SIGNER_LOAD_FAILED",
1082
+ `Signer metadata exceeds the ${SIGNER_METADATA_BYTE_CAP}-byte cap.`
1083
+ );
1084
+ case "timeout":
1085
+ return new SignerClientError(
1086
+ "SIGNER_LOAD_FAILED",
1087
+ `Signer metadata body read timed out after ${timeoutMs} ms.`
1088
+ );
1089
+ case "locked":
1090
+ case "chunk":
1091
+ return new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata response is unreadable.");
1092
+ case "read":
1093
+ if (cause instanceof SignerClientError) return cause;
1094
+ if (cause?.name === "AbortError") {
1095
+ return new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
1096
+ }
1097
+ return new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
1098
+ }
983
1099
  }
1100
+ });
1101
+ let raw;
1102
+ try {
1103
+ raw = utf8Decode(bytes);
1104
+ } catch {
1105
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata is not valid UTF-8.");
984
1106
  }
985
1107
  try {
986
1108
  return JSON.parse(raw);
@@ -1029,7 +1151,7 @@ function validateMetadataDocument(document, expected) {
1029
1151
  return {
1030
1152
  version: 1,
1031
1153
  keyId: record["keyId"],
1032
- signerUrl: record["signerUrl"],
1154
+ signerUrl: resolveSignerModuleUrl(record["signerUrl"], expected.metadataUrl, expected.apiOrigin),
1033
1155
  publicOrigin: record["publicOrigin"],
1034
1156
  replayNamespace: record["replayNamespace"]
1035
1157
  };
@@ -1091,13 +1213,14 @@ function validateSignerModule(candidate, metadata) {
1091
1213
  }
1092
1214
  return { signer };
1093
1215
  }
1094
- async function fetchMetadataDocument(config) {
1216
+ async function fetchMetadataDocument(config, readTimeoutMs) {
1095
1217
  if (config.signal?.aborted) {
1096
1218
  throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
1097
1219
  }
1098
1220
  let response;
1099
1221
  try {
1100
- response = await config.fetchImpl(config.metadataUrl, {
1222
+ const fetchImpl = config.fetchImpl;
1223
+ response = await fetchImpl(config.metadataUrl, {
1101
1224
  cache: "no-store",
1102
1225
  credentials: "omit",
1103
1226
  redirect: "error",
@@ -1114,19 +1237,23 @@ async function fetchMetadataDocument(config) {
1114
1237
  throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
1115
1238
  }
1116
1239
  if (response.ok !== true) {
1240
+ try {
1241
+ void response.body?.cancel().catch(() => void 0);
1242
+ } catch {
1243
+ }
1117
1244
  throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
1118
1245
  }
1119
- const document = await readBoundedMetadataDocument(response, "fetch");
1246
+ const document = await readBoundedMetadataDocument(response, config.signal, readTimeoutMs);
1120
1247
  return validateMetadataDocument(document, {
1121
1248
  apiOrigin: config.apiOrigin,
1122
- replayNamespace: config.replayNamespace
1249
+ replayNamespace: config.replayNamespace,
1250
+ metadataUrl: config.metadataUrl
1123
1251
  });
1124
1252
  }
1125
1253
  async function importValidatedModule(config, metadata) {
1126
- const moduleUrl = resolveSignerModuleUrl(metadata.signerUrl, config.metadataUrl, config.apiOrigin);
1127
1254
  let candidate;
1128
1255
  try {
1129
- candidate = await config.loadModule(moduleUrl);
1256
+ candidate = await config.loadModule(metadata.signerUrl);
1130
1257
  } catch (error) {
1131
1258
  if (error instanceof SignerClientError) {
1132
1259
  throw error;
@@ -1141,7 +1268,7 @@ async function importValidatedModule(config, metadata) {
1141
1268
  }
1142
1269
  async function fetchSignerBundle(options) {
1143
1270
  const config = validateBundleConfig(options);
1144
- const metadata = await fetchMetadataDocument(config);
1271
+ const metadata = await fetchMetadataDocument(config, SIGNER_METADATA_READ_TIMEOUT_MS);
1145
1272
  return importValidatedModule(config, metadata);
1146
1273
  }
1147
1274
  function isRetirementRetryCandidate(error) {
@@ -1172,6 +1299,7 @@ function createSignerClient(options) {
1172
1299
  let generation = 0;
1173
1300
  let disposed = false;
1174
1301
  let currentAborter = null;
1302
+ let awaitingReplacement = false;
1175
1303
  function throwIfDisposed() {
1176
1304
  if (disposed) {
1177
1305
  throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer client is disposed.");
@@ -1198,7 +1326,7 @@ function createSignerClient(options) {
1198
1326
  throw importError;
1199
1327
  }
1200
1328
  const second = await fetchMetadataDocument(base);
1201
- if (second.keyId === first.keyId) {
1329
+ if (second.keyId === first.keyId && second.signerUrl === first.signerUrl) {
1202
1330
  throw importError;
1203
1331
  }
1204
1332
  if (signal.aborted) {
@@ -1233,32 +1361,34 @@ function createSignerClient(options) {
1233
1361
  () => void 0,
1234
1362
  () => void 0
1235
1363
  );
1364
+ function releaseLoad() {
1365
+ if (timer !== void 0) {
1366
+ clearTimeout(timer);
1367
+ }
1368
+ if (pending === tracked) {
1369
+ pending = null;
1370
+ }
1371
+ if (currentAborter === aborter) {
1372
+ currentAborter = null;
1373
+ }
1374
+ }
1236
1375
  const tracked = raced.then(
1237
1376
  (signer) => {
1238
- if (timer !== void 0) {
1239
- clearTimeout(timer);
1240
- }
1241
- if (disposed || myGeneration !== generation) {
1377
+ const ownsGeneration = pending === tracked && myGeneration === generation;
1378
+ releaseLoad();
1379
+ if (disposed || !ownsGeneration) {
1242
1380
  throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer load was superseded.");
1243
1381
  }
1244
1382
  active = signer;
1245
- if (pending === tracked) {
1246
- pending = null;
1247
- }
1248
- currentAborter = null;
1383
+ awaitingReplacement = false;
1249
1384
  return signer;
1250
1385
  },
1251
1386
  (error) => {
1252
- if (timer !== void 0) {
1253
- clearTimeout(timer);
1254
- }
1255
- if (pending === tracked) {
1256
- pending = null;
1257
- }
1258
- if (error instanceof SignerClientError && error.code === "SIGNER_LOAD_TIMEOUT") {
1387
+ const ownsGeneration = pending === tracked && myGeneration === generation;
1388
+ releaseLoad();
1389
+ if (ownsGeneration && error instanceof SignerClientError && error.code === "SIGNER_LOAD_TIMEOUT") {
1259
1390
  generation += 1;
1260
1391
  }
1261
- currentAborter = null;
1262
1392
  throw error;
1263
1393
  }
1264
1394
  );
@@ -1283,7 +1413,9 @@ function createSignerClient(options) {
1283
1413
  if (disposed) {
1284
1414
  return;
1285
1415
  }
1286
- if (typeof observedKeyId !== "string" || observedKeyId.length === 0) {
1416
+ try {
1417
+ validateKeyId(observedKeyId);
1418
+ } catch {
1287
1419
  return;
1288
1420
  }
1289
1421
  if (active !== null) {
@@ -1296,11 +1428,11 @@ function createSignerClient(options) {
1296
1428
  if (!matches) {
1297
1429
  return;
1298
1430
  }
1299
- active = null;
1300
- generation += 1;
1301
- pending = null;
1431
+ } else if (awaitingReplacement) {
1302
1432
  return;
1303
1433
  }
1434
+ active = null;
1435
+ awaitingReplacement = true;
1304
1436
  generation += 1;
1305
1437
  pending = null;
1306
1438
  },
@@ -1310,6 +1442,7 @@ function createSignerClient(options) {
1310
1442
  }
1311
1443
  disposed = true;
1312
1444
  active = null;
1445
+ awaitingReplacement = false;
1313
1446
  generation += 1;
1314
1447
  try {
1315
1448
  currentAborter?.abort();
@@ -1434,165 +1567,66 @@ async function materializeRequestBodyBytes(request, options = {}) {
1434
1567
  if (body === null || body === void 0) {
1435
1568
  return new Uint8Array(0);
1436
1569
  }
1437
- let reader;
1438
- try {
1439
- reader = body.getReader();
1440
- } catch {
1441
- throw new SignerClientError(
1442
- "SIGNER_PREPARATION_FAILED",
1443
- "Request body is locked. Clone the Request before sending or pass a fresh body."
1444
- );
1445
- }
1446
- const chunks = [];
1447
- let total = 0;
1448
- let timer;
1449
- let onAbort;
1450
- let settled = false;
1451
- function cleanup() {
1452
- settled = true;
1453
- if (timer !== void 0) {
1454
- clearTimeout(timer);
1455
- timer = void 0;
1456
- }
1457
- if (onAbort !== void 0 && signal !== null && signal !== void 0) {
1458
- try {
1459
- signal.removeEventListener("abort", onAbort);
1460
- } catch {
1461
- }
1462
- onAbort = void 0;
1463
- }
1464
- }
1465
- const deadline = new Promise((_resolve, reject) => {
1466
- timer = setTimeout(() => {
1467
- if (settled) {
1468
- return;
1469
- }
1470
- try {
1471
- void reader.cancel();
1472
- } catch {
1570
+ return readBoundedByteStream(body, {
1571
+ maxBytes: maxBodyBytes,
1572
+ timeoutMs,
1573
+ signal,
1574
+ error(failure, cause) {
1575
+ switch (failure) {
1576
+ case "abort":
1577
+ return toAbortError(signal);
1578
+ case "locked":
1579
+ return new SignerClientError(
1580
+ "SIGNER_PREPARATION_FAILED",
1581
+ "Request body is locked. Clone the Request before sending or pass a fresh body."
1582
+ );
1583
+ case "chunk":
1584
+ return new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body chunk is not bytes.");
1585
+ case "limit":
1586
+ return new SignerClientError(
1587
+ "SIGNER_PREPARATION_FAILED",
1588
+ `Request body exceeds the configured ${maxBodyBytes}-byte limit.`
1589
+ );
1590
+ case "timeout":
1591
+ return new SignerClientError(
1592
+ "SIGNER_PREPARATION_FAILED",
1593
+ `Request body read timed out after ${timeoutMs} ms.`
1594
+ );
1595
+ case "read":
1596
+ return cause instanceof SignerClientError ? cause : new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body read failed.");
1473
1597
  }
1474
- reject(new SignerClientError("SIGNER_PREPARATION_FAILED", `Request body read timed out after ${timeoutMs} ms.`));
1475
- }, timeoutMs);
1476
- const handle = timer;
1477
- if (typeof handle.unref === "function") {
1478
- handle.unref();
1479
1598
  }
1480
1599
  });
1481
- deadline.then(
1482
- () => void 0,
1483
- () => void 0
1484
- );
1485
- if (signal !== null && signal !== void 0) {
1486
- const abortPromise = new Promise((_resolve, reject) => {
1487
- onAbort = () => {
1488
- try {
1489
- void reader.cancel();
1490
- } catch {
1491
- }
1492
- reject(toAbortError(signal));
1493
- };
1494
- try {
1495
- signal.addEventListener("abort", onAbort, { once: true });
1496
- } catch {
1497
- }
1498
- });
1499
- abortPromise.then(
1500
- () => void 0,
1501
- () => void 0
1502
- );
1503
- try {
1504
- const result = await Promise.race([readLoop(), deadline, abortPromise]);
1505
- cleanup();
1506
- try {
1507
- reader.releaseLock();
1508
- } catch {
1509
- }
1510
- return result;
1511
- } catch (error) {
1512
- cleanup();
1513
- try {
1514
- reader.releaseLock();
1515
- } catch {
1516
- }
1517
- throw error;
1518
- }
1519
- }
1520
- async function readLoop() {
1521
- for (; ; ) {
1522
- if (signal?.aborted === true) {
1523
- try {
1524
- await reader.cancel();
1525
- } catch {
1526
- }
1527
- throw toAbortError(signal);
1528
- }
1529
- let chunk;
1530
- try {
1531
- chunk = await reader.read();
1532
- } catch (error) {
1533
- try {
1534
- await reader.cancel();
1535
- } catch {
1536
- }
1537
- if (error instanceof SignerClientError) {
1538
- throw error;
1539
- }
1540
- throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body read failed.");
1541
- }
1542
- if (chunk.done === true) {
1543
- break;
1544
- }
1545
- const value = chunk.value;
1546
- if (!(value instanceof Uint8Array)) {
1547
- try {
1548
- await reader.cancel();
1549
- } catch {
1550
- }
1551
- throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body chunk is not bytes.");
1552
- }
1553
- total += value.byteLength;
1554
- if (total > maxBodyBytes) {
1555
- try {
1556
- await reader.cancel();
1557
- } catch {
1558
- }
1559
- throw new SignerClientError(
1560
- "SIGNER_PREPARATION_FAILED",
1561
- `Request body exceeds the configured ${maxBodyBytes}-byte limit.`
1562
- );
1563
- }
1564
- chunks.push(value.slice());
1565
- }
1566
- const out = new Uint8Array(total);
1567
- let offset = 0;
1568
- for (const part of chunks) {
1569
- out.set(part, offset);
1570
- offset += part.byteLength;
1571
- }
1572
- return out;
1573
- }
1574
- try {
1575
- const result = await Promise.race([readLoop(), deadline]);
1576
- cleanup();
1577
- try {
1578
- reader.releaseLock();
1579
- } catch {
1580
- }
1581
- return result;
1582
- } catch (error) {
1583
- cleanup();
1584
- try {
1585
- reader.releaseLock();
1586
- } catch {
1587
- }
1588
- throw error;
1589
- }
1590
1600
  }
1591
1601
 
1592
1602
  // src/attested-fetch.ts
1593
1603
  var DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS2 = 5e3;
1594
1604
  var MIN_FETCH_TIMEOUT_MS = 1;
1595
1605
  var MAX_FETCH_TIMEOUT_MS = 6e4;
1606
+ var FORBIDDEN_FETCH_HEADER_NAMES = /* @__PURE__ */ new Set([
1607
+ "accept-charset",
1608
+ "accept-encoding",
1609
+ "access-control-request-headers",
1610
+ "access-control-request-method",
1611
+ "connection",
1612
+ "content-length",
1613
+ "cookie",
1614
+ "cookie2",
1615
+ "date",
1616
+ "dnt",
1617
+ "expect",
1618
+ "host",
1619
+ "keep-alive",
1620
+ "origin",
1621
+ "referer",
1622
+ "set-cookie",
1623
+ "te",
1624
+ "trailer",
1625
+ "transfer-encoding",
1626
+ "upgrade",
1627
+ "user-agent",
1628
+ "via"
1629
+ ]);
1596
1630
  var FETCH_WITH_ATTESTATION_OPTION_KEYS = /* @__PURE__ */ new Set([
1597
1631
  "signerClient",
1598
1632
  "apiOrigin",
@@ -1617,7 +1651,7 @@ function resolveFetchImpl(fetchAlias, fetchFnAlias) {
1617
1651
  "fetch is unavailable. Prepared fetch requires a fetch implementation."
1618
1652
  );
1619
1653
  }
1620
- return globalFetch;
1654
+ return globalFetch.bind(globalThis);
1621
1655
  }
1622
1656
  if (typeof candidate !== "function") {
1623
1657
  throw new AttestationProtocolError("fetchWithAttestation fetch must be a function");
@@ -1650,6 +1684,23 @@ function validateBodyReadTimeoutMs2(value) {
1650
1684
  }
1651
1685
  return value;
1652
1686
  }
1687
+ function assertFetchProofHeaderName(headerName) {
1688
+ const lower = headerName.toLowerCase();
1689
+ if (lower.startsWith("sec-") || lower.startsWith("proxy-") || FORBIDDEN_FETCH_HEADER_NAMES.has(lower)) {
1690
+ throw new SignerClientError(
1691
+ "SIGNER_PREPARATION_FAILED",
1692
+ "Attestation headerName must be writable by browser Fetch; browser-forbidden request headers cannot carry a proof."
1693
+ );
1694
+ }
1695
+ }
1696
+ function assertAttestableMode(mode) {
1697
+ if (mode === "no-cors") {
1698
+ throw new SignerClientError(
1699
+ "SIGNER_PREPARATION_FAILED",
1700
+ "Request mode no-cors cannot carry an attestation proof. Use cors or same-origin."
1701
+ );
1702
+ }
1703
+ }
1653
1704
  function resolveOptions(options) {
1654
1705
  if (options === null || typeof options !== "object" || Array.isArray(options)) {
1655
1706
  throw new AttestationProtocolError("fetchWithAttestation options must be an object");
@@ -1671,10 +1722,11 @@ function resolveOptions(options) {
1671
1722
  );
1672
1723
  const headerName = options.headerName ?? DEFAULT_ATTESTATION_HEADER;
1673
1724
  validateHeaderName(headerName);
1725
+ assertFetchProofHeaderName(headerName);
1674
1726
  const maxBodyBytes = validateMaxBodyBytes2(options.maxBodyBytes);
1675
1727
  const bodyReadTimeoutMs = validateBodyReadTimeoutMs2(options.bodyReadTimeoutMs);
1676
- const now = options.now ?? Date.now;
1677
- if (typeof now !== "function") {
1728
+ const now2 = options.now ?? Date.now;
1729
+ if (typeof now2 !== "function") {
1678
1730
  throw new AttestationProtocolError("fetchWithAttestation now must be a function");
1679
1731
  }
1680
1732
  return Object.freeze({
@@ -1685,13 +1737,13 @@ function resolveOptions(options) {
1685
1737
  headerName,
1686
1738
  maxBodyBytes,
1687
1739
  bodyReadTimeoutMs,
1688
- now
1740
+ now: now2
1689
1741
  });
1690
1742
  }
1691
- function checkedTimestampMs(now) {
1743
+ function checkedTimestampMs(now2) {
1692
1744
  let value;
1693
1745
  try {
1694
- value = now();
1746
+ value = now2();
1695
1747
  } catch {
1696
1748
  throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Attestation clock failed.");
1697
1749
  }
@@ -1817,9 +1869,18 @@ function buildSignedRequest(snapshot, headerName, transactionId) {
1817
1869
  if (bodyForFetch !== null) {
1818
1870
  init.body = bodyForFetch;
1819
1871
  }
1872
+ let request;
1820
1873
  try {
1821
- return new Request(snapshot.url, init);
1874
+ request = new Request(snapshot.url, init);
1875
+ if (request.headers.get(headerName) !== transactionId) {
1876
+ throw new SignerClientError(
1877
+ "SIGNER_PREPARATION_FAILED",
1878
+ "Prepared request did not retain the exact attestation proof header."
1879
+ );
1880
+ }
1881
+ return request;
1822
1882
  } catch (error) {
1883
+ cancelOwnedBody(request);
1823
1884
  if (error instanceof SignerClientError || error instanceof AttestationProtocolError) {
1824
1885
  throw error;
1825
1886
  }
@@ -1836,15 +1897,111 @@ function isStaleKeyRetryCandidate(response) {
1836
1897
  return false;
1837
1898
  }
1838
1899
  }
1900
+ function cancelOwnedBody(owner) {
1901
+ try {
1902
+ void owner?.body?.cancel().catch(() => void 0);
1903
+ } catch {
1904
+ }
1905
+ }
1906
+ function throwIfAborted(signal) {
1907
+ if (signal?.aborted) {
1908
+ if (signal.reason !== void 0) throw signal.reason;
1909
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request was aborted before sending.");
1910
+ }
1911
+ }
1912
+ async function withPreparationAbort(signal, prepare) {
1913
+ throwIfAborted(signal);
1914
+ if (signal === null) return prepare();
1915
+ let onAbort;
1916
+ const aborted = new Promise((_resolve, reject) => {
1917
+ onAbort = () => {
1918
+ try {
1919
+ throwIfAborted(signal);
1920
+ } catch (reason) {
1921
+ reject(reason);
1922
+ }
1923
+ };
1924
+ signal.addEventListener("abort", onAbort, { once: true });
1925
+ });
1926
+ try {
1927
+ return await Promise.race([Promise.resolve().then(prepare), aborted]);
1928
+ } finally {
1929
+ signal.removeEventListener("abort", onAbort);
1930
+ }
1931
+ }
1932
+ function copyRequestWithoutBody(input) {
1933
+ return new Request(input.url, {
1934
+ method: input.method,
1935
+ headers: input.headers,
1936
+ credentials: input.credentials,
1937
+ signal: input.signal,
1938
+ mode: input.mode === "navigate" ? "same-origin" : input.mode,
1939
+ cache: input.cache,
1940
+ referrer: input.referrer,
1941
+ referrerPolicy: input.referrerPolicy,
1942
+ integrity: input.integrity,
1943
+ keepalive: input.keepalive,
1944
+ redirect: input.redirect
1945
+ });
1946
+ }
1947
+ async function prepareSnapshot(input, init, overridingBody, config) {
1948
+ let unreadOwner;
1949
+ try {
1950
+ let base;
1951
+ try {
1952
+ let source = input;
1953
+ if (input instanceof Request) {
1954
+ if (overridingBody) {
1955
+ source = copyRequestWithoutBody(input);
1956
+ } else if (input.body !== null) {
1957
+ unreadOwner = input.clone();
1958
+ source = unreadOwner;
1959
+ }
1960
+ }
1961
+ base = new Request(source, init);
1962
+ unreadOwner = base;
1963
+ } catch (error) {
1964
+ if (error instanceof SignerClientError || error instanceof AttestationProtocolError) throw error;
1965
+ const message = error instanceof Error ? `: ${error.message}` : "";
1966
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", `Request could not be prepared${message}.`);
1967
+ }
1968
+ assertAttestableMode(base.mode);
1969
+ if (new URL(base.url).origin !== config.apiOrigin) {
1970
+ throw new SignerClientError(
1971
+ "SIGNER_ORIGIN_MISMATCH",
1972
+ "Request origin does not match the configured API origin. Signatures are only attached to the configured API origin."
1973
+ );
1974
+ }
1975
+ assertAllowedContentEncoding(base.headers);
1976
+ throwIfAborted(base.signal);
1977
+ unreadOwner = void 0;
1978
+ const bodyBytes = await materializeRequestBodyBytes(base, {
1979
+ maxBodyBytes: config.maxBodyBytes,
1980
+ signal: base.signal,
1981
+ timeoutMs: config.bodyReadTimeoutMs
1982
+ });
1983
+ return buildPreparedSnapshot(base, bodyBytes);
1984
+ } finally {
1985
+ cancelOwnedBody(unreadOwner);
1986
+ }
1987
+ }
1839
1988
  async function fetchWithAttestation(input, init, options) {
1840
1989
  const config = resolveOptions(options);
1841
1990
  if (init !== void 0 && (init === null || typeof init !== "object" || Array.isArray(init))) {
1842
1991
  throw new AttestationProtocolError("fetchWithAttestation init must be an object");
1843
1992
  }
1844
- const initRecord = init ?? {};
1845
- if (initRecord.body !== void 0 && initRecord.body !== null) {
1846
- assertSupportedBodyInit(initRecord.body);
1847
- }
1993
+ const initMode = init?.mode;
1994
+ assertAttestableMode(initMode === void 0 && input instanceof Request ? input.mode : initMode);
1995
+ const initBody = init?.body;
1996
+ const overridingBody = initBody !== void 0 && initBody !== null;
1997
+ if (overridingBody) assertSupportedBodyInit(initBody);
1998
+ const stableInit = init === void 0 ? void 0 : new Proxy(init, {
1999
+ get(target, property) {
2000
+ if (property === "body") return initBody;
2001
+ if (property === "mode") return initMode;
2002
+ return Reflect.get(target, property, target);
2003
+ }
2004
+ });
1848
2005
  if (typeof input === "string") {
1849
2006
  if (input.length === 0) {
1850
2007
  throw new AttestationProtocolError("fetchWithAttestation input must be a non-empty URL or Request");
@@ -1852,78 +2009,36 @@ async function fetchWithAttestation(input, init, options) {
1852
2009
  } else if (!(input instanceof Request) && !(input instanceof URL)) {
1853
2010
  throw new AttestationProtocolError("fetchWithAttestation input must be a URL, string, or Request");
1854
2011
  }
1855
- if (input instanceof Request) {
1856
- if (input.bodyUsed === true) {
1857
- const overridingBody = initRecord.body !== void 0 && initRecord.body !== null;
1858
- if (!overridingBody) {
1859
- throw new SignerClientError(
1860
- "SIGNER_PREPARATION_FAILED",
1861
- "Request body has already been used. Clone the Request before sending or pass a fresh body."
1862
- );
1863
- }
1864
- }
1865
- try {
1866
- const locked = input.body?.locked;
1867
- if (locked === true && (initRecord.body === void 0 || initRecord.body === null)) {
1868
- throw new SignerClientError(
1869
- "SIGNER_PREPARATION_FAILED",
1870
- "Request body is locked. Clone the Request before sending or pass a fresh body."
1871
- );
1872
- }
1873
- } catch (error) {
1874
- if (error instanceof SignerClientError) {
1875
- throw error;
1876
- }
1877
- }
1878
- }
1879
- let base;
1880
- try {
1881
- base = new Request(input, init ?? {});
1882
- } catch (error) {
1883
- if (error instanceof SignerClientError || error instanceof AttestationProtocolError) {
1884
- throw error;
2012
+ if (input instanceof Request && !overridingBody) {
2013
+ if (input.bodyUsed) {
2014
+ throw new SignerClientError(
2015
+ "SIGNER_PREPARATION_FAILED",
2016
+ "Request body has already been used. Clone the Request before sending or pass a fresh body."
2017
+ );
1885
2018
  }
1886
- const message = error instanceof Error ? `: ${error.message}` : "";
1887
- throw new SignerClientError("SIGNER_PREPARATION_FAILED", `Request could not be prepared${message}.`);
1888
- }
1889
- let requestOrigin;
1890
- try {
1891
- requestOrigin = new URL(base.url).origin;
1892
- } catch {
1893
- throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request URL is invalid.");
1894
- }
1895
- if (requestOrigin !== config.apiOrigin) {
1896
- throw new SignerClientError(
1897
- "SIGNER_ORIGIN_MISMATCH",
1898
- "Request origin does not match the configured API origin. Signatures are only attached to the configured API origin."
1899
- );
1900
- }
1901
- assertAllowedContentEncoding(base.headers);
1902
- if (base.signal?.aborted === true) {
1903
- const reason = base.signal.reason;
1904
- if (reason !== void 0) {
1905
- throw reason;
2019
+ if (input.body?.locked) {
2020
+ throw new SignerClientError(
2021
+ "SIGNER_PREPARATION_FAILED",
2022
+ "Request body is locked. Clone the Request before sending or pass a fresh body."
2023
+ );
1906
2024
  }
1907
- throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request was aborted before signing.");
1908
2025
  }
1909
- const bodyBytes = await materializeRequestBodyBytes(base, {
1910
- maxBodyBytes: config.maxBodyBytes,
1911
- signal: base.signal,
1912
- timeoutMs: config.bodyReadTimeoutMs
1913
- });
1914
- const snapshot = buildPreparedSnapshot(base, bodyBytes);
1915
- async function signWithFreshProof(signerKeyIdCheck) {
1916
- void signerKeyIdCheck;
2026
+ const snapshot = await prepareSnapshot(input, stableInit, overridingBody, config);
2027
+ let bodyHashHex;
2028
+ async function signWithFreshProof() {
2029
+ throwIfAborted(snapshot.signal);
1917
2030
  const signer = await config.signerClient.getSigner();
2031
+ throwIfAborted(snapshot.signal);
1918
2032
  if (signer.version !== 1 || signer.publicOrigin !== config.apiOrigin || signer.replayNamespace !== config.replayNamespace) {
1919
2033
  throw new SignerClientError(
1920
2034
  "SIGNER_ORIGIN_MISMATCH",
1921
2035
  "Loaded signer protection space does not match the configured API origin/namespace."
1922
2036
  );
1923
2037
  }
2038
+ bodyHashHex ??= await hashBodySha256Hex(snapshot.bodyBytes);
2039
+ throwIfAborted(snapshot.signal);
1924
2040
  const timestampMs = checkedTimestampMs(config.now);
1925
2041
  const nonceHex = generateNonceHex();
1926
- const bodyHashHex = await hashBodySha256Hex(snapshot.bodyBytes);
1927
2042
  const transactionId = await signer.sign({
1928
2043
  method: snapshot.method,
1929
2044
  requestTarget: snapshot.requestTarget,
@@ -1932,11 +2047,22 @@ async function fetchWithAttestation(input, init, options) {
1932
2047
  timestampMs,
1933
2048
  nonceHex
1934
2049
  });
2050
+ throwIfAborted(snapshot.signal);
1935
2051
  return { transactionId, observedKeyId: signer.keyId };
1936
2052
  }
1937
- const first = await signWithFreshProof();
2053
+ async function send(request) {
2054
+ try {
2055
+ throwIfAborted(snapshot.signal);
2056
+ const fetchImpl = config.fetchImpl;
2057
+ return await fetchImpl(request);
2058
+ } catch (reason) {
2059
+ cancelOwnedBody(request);
2060
+ throw reason;
2061
+ }
2062
+ }
2063
+ const first = await withPreparationAbort(snapshot.signal, signWithFreshProof);
1938
2064
  const firstRequest = buildSignedRequest(snapshot, config.headerName, first.transactionId);
1939
- const firstResponse = await config.fetchImpl(firstRequest);
2065
+ const firstResponse = await send(firstRequest);
1940
2066
  if (!isStaleKeyRetryCandidate(firstResponse)) {
1941
2067
  return firstResponse;
1942
2068
  }
@@ -1944,21 +2070,23 @@ async function fetchWithAttestation(input, init, options) {
1944
2070
  config.signerClient.invalidate(first.observedKeyId);
1945
2071
  } catch {
1946
2072
  }
2073
+ let secondRequest;
1947
2074
  try {
1948
- await firstResponse.body?.cancel();
1949
- } catch {
1950
- }
1951
- let secondSignerTransaction;
1952
- try {
1953
- secondSignerTransaction = await signWithFreshProof();
2075
+ const second = await withPreparationAbort(snapshot.signal, signWithFreshProof);
2076
+ throwIfAborted(snapshot.signal);
2077
+ if (second.transactionId === first.transactionId) {
2078
+ return firstResponse;
2079
+ }
2080
+ secondRequest = buildSignedRequest(snapshot, config.headerName, second.transactionId);
1954
2081
  } catch {
2082
+ if (snapshot.signal?.aborted) {
2083
+ cancelOwnedBody(firstResponse);
2084
+ throwIfAborted(snapshot.signal);
2085
+ }
1955
2086
  return firstResponse;
1956
2087
  }
1957
- if (secondSignerTransaction.transactionId === first.transactionId) {
1958
- return firstResponse;
1959
- }
1960
- const secondRequest = buildSignedRequest(snapshot, config.headerName, secondSignerTransaction.transactionId);
1961
- return config.fetchImpl(secondRequest);
2088
+ cancelOwnedBody(firstResponse);
2089
+ return send(secondRequest);
1962
2090
  }
1963
2091
  export {
1964
2092
  ATTESTATION_ERROR_RESPONSE_HEADER,