@web-ts-toolkit/express-request-attestation 0.51.0 → 0.53.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.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
|
-
|
|
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
|
-
|
|
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
|
|
713
|
-
|
|
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,
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1246
|
-
pending = null;
|
|
1247
|
-
}
|
|
1248
|
-
currentAborter = null;
|
|
1383
|
+
awaitingReplacement = false;
|
|
1249
1384
|
return signer;
|
|
1250
1385
|
},
|
|
1251
1386
|
(error) => {
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
|
1677
|
-
if (typeof
|
|
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(
|
|
1743
|
+
function checkedTimestampMs(now2) {
|
|
1692
1744
|
let value;
|
|
1693
1745
|
try {
|
|
1694
|
-
value =
|
|
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
|
-
|
|
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
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
-
|
|
1958
|
-
|
|
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,
|