@web-ts-toolkit/express-request-attestation 0.48.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 ADDED
@@ -0,0 +1,2041 @@
1
+ // src/shared/types.ts
2
+ var PROTOCOL_VERSION = 1;
3
+ var MAC_INPUT_LABEL = "wtt-request-signature";
4
+ var DEFAULT_ATTESTATION_HEADER = "x-client-transaction-id";
5
+ var ATTESTATION_ERROR_RESPONSE_HEADER = "x-attestation-error";
6
+ var REPLAY_KEY_PREFIX = "att:v1:";
7
+ var KEY_BYTES_LENGTH = 32;
8
+ var NONCE_BYTES_LENGTH = 16;
9
+ var MAC_BYTES_LENGTH = 32;
10
+ var MAX_KEY_ID_LENGTH = 64;
11
+ var MAX_REPLAY_NAMESPACE_LENGTH = 128;
12
+ var MAX_ENCODED_TRANSACTION_ID_BYTES = 1024;
13
+ var MAX_REQUEST_TARGET_BYTES = 8192;
14
+ var MAX_CONTENT_TYPE_BYTES = 256;
15
+ var MAX_PUBLIC_ORIGIN_BYTES = 512;
16
+ var MIN_REPLAY_KEY_BYTES = 1;
17
+ var MAX_REPLAY_KEY_BYTES = 256;
18
+ var MAX_HEADER_NAME_BYTES = 128;
19
+ var DEFAULT_MAX_BODY_BYTES = 1048576;
20
+ var MAX_BODY_BYTES_HARD_CAP = 16777216;
21
+ var DEFAULT_MAX_AGE_MS = 3e4;
22
+ var MIN_MAX_AGE_MS = 1e3;
23
+ var MAX_MAX_AGE_MS = 12e4;
24
+ var DEFAULT_CLOCK_SKEW_MS = 5e3;
25
+ var MIN_CLOCK_SKEW_MS = 0;
26
+ var MAX_CLOCK_SKEW_MS = 3e4;
27
+ var DEFAULT_CLUSTER_CLOCK_GUARD_MS = 5e3;
28
+ var MIN_CLUSTER_CLOCK_GUARD_MS = 0;
29
+ var MAX_CLUSTER_CLOCK_GUARD_MS = 3e4;
30
+ var GLOBAL_MAX_RETENTION_MS = 24e4;
31
+ var EMPTY_BODY_SHA256_HEX = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
32
+ var AttestationProtocolError = class extends Error {
33
+ name = "AttestationProtocolError";
34
+ constructor(message) {
35
+ super(`[express-request-attestation] ${message}`);
36
+ }
37
+ };
38
+
39
+ // src/shared/canonical.ts
40
+ var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
41
+ var BASE64URL_DECODE_TABLE = (() => {
42
+ const table = /* @__PURE__ */ Object.create(null);
43
+ for (let index = 0; index < BASE64URL_ALPHABET.length; index += 1) {
44
+ table[BASE64URL_ALPHABET[index]] = index;
45
+ }
46
+ return table;
47
+ })();
48
+ var textEncoder = new TextEncoder();
49
+ var textDecoder = new TextDecoder("utf-8", { fatal: true });
50
+ function utf8Encode(text) {
51
+ if (typeof text !== "string") {
52
+ throw new AttestationProtocolError("expected a string for UTF-8 encoding");
53
+ }
54
+ return textEncoder.encode(text);
55
+ }
56
+ function utf8Decode(bytes) {
57
+ if (!(bytes instanceof Uint8Array)) {
58
+ throw new AttestationProtocolError("expected a Uint8Array for UTF-8 decoding");
59
+ }
60
+ try {
61
+ return textDecoder.decode(bytes);
62
+ } catch {
63
+ throw new AttestationProtocolError("input is not valid UTF-8");
64
+ }
65
+ }
66
+ function utf8ByteLength(text) {
67
+ return utf8Encode(text).length;
68
+ }
69
+ function base64urlEncode(bytes) {
70
+ if (!(bytes instanceof Uint8Array)) {
71
+ throw new AttestationProtocolError("expected a Uint8Array for base64url encoding");
72
+ }
73
+ let out = "";
74
+ for (let index = 0; index < bytes.length; index += 3) {
75
+ const first = bytes[index];
76
+ const second = index + 1 < bytes.length ? bytes[index + 1] : 0;
77
+ const third = index + 2 < bytes.length ? bytes[index + 2] : 0;
78
+ const triplet = first << 16 | second << 8 | third;
79
+ out += BASE64URL_ALPHABET[triplet >> 18 & 63];
80
+ out += BASE64URL_ALPHABET[triplet >> 12 & 63];
81
+ if (index + 1 < bytes.length) {
82
+ out += BASE64URL_ALPHABET[triplet >> 6 & 63];
83
+ }
84
+ if (index + 2 < bytes.length) {
85
+ out += BASE64URL_ALPHABET[triplet & 63];
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ function base64urlDecode(input) {
91
+ if (typeof input !== "string" || input.length === 0) {
92
+ throw new AttestationProtocolError("base64url input must be a non-empty string");
93
+ }
94
+ for (const char of input) {
95
+ if (!(char in BASE64URL_DECODE_TABLE)) {
96
+ throw new AttestationProtocolError("base64url input uses a non-canonical alphabet or padding");
97
+ }
98
+ }
99
+ if (input.length % 4 === 1) {
100
+ throw new AttestationProtocolError("base64url input has an impossible length");
101
+ }
102
+ const outLength = Math.floor(input.length * 6 / 8);
103
+ const out = new Uint8Array(outLength);
104
+ let cursor = 0;
105
+ let buffer = 0;
106
+ let bits = 0;
107
+ for (const char of input) {
108
+ buffer = buffer << 6 | BASE64URL_DECODE_TABLE[char];
109
+ bits += 6;
110
+ if (bits >= 8) {
111
+ bits -= 8;
112
+ out[cursor] = buffer >> bits & 255;
113
+ cursor += 1;
114
+ }
115
+ }
116
+ if (bits > 0 && (buffer & (1 << bits) - 1) !== 0) {
117
+ throw new AttestationProtocolError("base64url input has non-canonical trailing bits");
118
+ }
119
+ const canonical = base64urlEncode(out);
120
+ if (canonical !== input) {
121
+ throw new AttestationProtocolError("base64url input is not canonical");
122
+ }
123
+ return out;
124
+ }
125
+ function canonicalJsonBytes(value) {
126
+ let text;
127
+ try {
128
+ text = JSON.stringify(value);
129
+ } catch {
130
+ throw new AttestationProtocolError("value is not JSON-serializable");
131
+ }
132
+ if (typeof text !== "string") {
133
+ throw new AttestationProtocolError("value is not JSON-serializable");
134
+ }
135
+ return utf8Encode(text);
136
+ }
137
+ function parseCanonicalJsonArray(text, expectedLength) {
138
+ if (typeof text !== "string") {
139
+ throw new AttestationProtocolError("canonical JSON input must be a string");
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(text);
144
+ } catch {
145
+ throw new AttestationProtocolError("input is not valid JSON");
146
+ }
147
+ if (!Array.isArray(parsed)) {
148
+ throw new AttestationProtocolError("canonical JSON input must be an array");
149
+ }
150
+ if (JSON.stringify(parsed) !== text) {
151
+ throw new AttestationProtocolError("JSON input is not in canonical form");
152
+ }
153
+ if (expectedLength !== void 0 && parsed.length !== expectedLength) {
154
+ throw new AttestationProtocolError(`canonical JSON array must have exactly ${expectedLength} elements`);
155
+ }
156
+ return parsed;
157
+ }
158
+ function assertTimestampMs(name, value) {
159
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
160
+ throw new AttestationProtocolError(`${name} must be a non-negative safe integer`);
161
+ }
162
+ }
163
+
164
+ // src/shared/codec.ts
165
+ var KEY_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
166
+ var REPLAY_NAMESPACE_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
167
+ var NONCE_HEX_PATTERN = /^[0-9a-f]{32}$/;
168
+ var BODY_HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
169
+ var HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
170
+ var HEX_BYTE_PATTERN = /^[0-9a-fA-F]{2}$/;
171
+ var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
172
+ "authorization",
173
+ "proxy-authenticate",
174
+ "proxy-authorization",
175
+ "www-authenticate",
176
+ "cookie",
177
+ "set-cookie",
178
+ "host",
179
+ "origin",
180
+ "content-encoding",
181
+ "content-length",
182
+ "content-type",
183
+ "transfer-encoding",
184
+ ATTESTATION_ERROR_RESPONSE_HEADER
185
+ ]);
186
+ function isLoopbackHostname(hostname) {
187
+ return hostname === "localhost" || hostname === "[::1]" || /^127\./.test(hostname);
188
+ }
189
+ function validateKeyId(keyId) {
190
+ if (typeof keyId !== "string" || !KEY_ID_PATTERN.test(keyId) || keyId.length > MAX_KEY_ID_LENGTH) {
191
+ throw new AttestationProtocolError("keyId must match [A-Za-z0-9_-]{1,64}");
192
+ }
193
+ }
194
+ function validateReplayNamespace(namespace) {
195
+ if (typeof namespace !== "string" || !REPLAY_NAMESPACE_PATTERN.test(namespace) || namespace.length > MAX_REPLAY_NAMESPACE_LENGTH) {
196
+ throw new AttestationProtocolError("replayNamespace must match [A-Za-z0-9_-]{1,128}");
197
+ }
198
+ }
199
+ function validateNonceHex(nonceHex) {
200
+ if (typeof nonceHex !== "string" || !NONCE_HEX_PATTERN.test(nonceHex)) {
201
+ throw new AttestationProtocolError("nonceHex must be exactly 32 lowercase hex characters");
202
+ }
203
+ }
204
+ function validateBodyHashHex(bodyHashHex) {
205
+ if (typeof bodyHashHex !== "string" || !BODY_HASH_HEX_PATTERN.test(bodyHashHex)) {
206
+ throw new AttestationProtocolError("bodyHashHex must be exactly 64 lowercase hex characters");
207
+ }
208
+ }
209
+ function validateMac(mac) {
210
+ if (typeof mac !== "string" || mac.length !== 43) {
211
+ throw new AttestationProtocolError("mac must be exactly 43 base64url characters");
212
+ }
213
+ const decoded = base64urlDecode(mac);
214
+ if (decoded.length !== MAC_BYTES_LENGTH) {
215
+ throw new AttestationProtocolError("mac must decode to 32 bytes");
216
+ }
217
+ }
218
+ function validateKeyBytes(key) {
219
+ if (!(key instanceof Uint8Array) || key.length !== KEY_BYTES_LENGTH) {
220
+ throw new AttestationProtocolError("signing key must be exactly 32 bytes");
221
+ }
222
+ }
223
+ function validatePublicOrigin(origin) {
224
+ if (typeof origin !== "string") {
225
+ throw new AttestationProtocolError("publicOrigin must be a string");
226
+ }
227
+ if (origin.length === 0 || utf8ByteLength(origin) > MAX_PUBLIC_ORIGIN_BYTES) {
228
+ throw new AttestationProtocolError(`publicOrigin must be 1-${MAX_PUBLIC_ORIGIN_BYTES} UTF-8 bytes`);
229
+ }
230
+ let url;
231
+ try {
232
+ url = new URL(origin);
233
+ } catch {
234
+ throw new AttestationProtocolError("publicOrigin must be a valid HTTP(S) origin");
235
+ }
236
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
237
+ throw new AttestationProtocolError("publicOrigin must use http or https");
238
+ }
239
+ if (url.username !== "" || url.password !== "") {
240
+ throw new AttestationProtocolError("publicOrigin must not contain userinfo");
241
+ }
242
+ const canonical = `${url.protocol}//${url.host}`;
243
+ if (origin !== canonical) {
244
+ throw new AttestationProtocolError(
245
+ "publicOrigin must be canonical (lowercase, no path, no trailing slash, no default port)"
246
+ );
247
+ }
248
+ if (url.protocol === "http:" && !isLoopbackHostname(url.hostname)) {
249
+ throw new AttestationProtocolError("http publicOrigin is allowed only for loopback development");
250
+ }
251
+ }
252
+ function normalizeMethod(method) {
253
+ if (typeof method !== "string" || method.length === 0 || !HTTP_TOKEN_PATTERN.test(method)) {
254
+ throw new AttestationProtocolError("method must be a non-empty valid HTTP token");
255
+ }
256
+ return method.toUpperCase();
257
+ }
258
+ function normalizeContentType(contentType) {
259
+ if (contentType === void 0 || contentType === null) {
260
+ return "";
261
+ }
262
+ if (typeof contentType !== "string") {
263
+ throw new AttestationProtocolError("contentType must be a string");
264
+ }
265
+ const trimmed = contentType.replace(/^[ \t]+|[ \t]+$/g, "");
266
+ for (const char of trimmed) {
267
+ const code = char.codePointAt(0);
268
+ if (code === 9 || code === 32) {
269
+ continue;
270
+ }
271
+ if (code < 32 || code === 127) {
272
+ throw new AttestationProtocolError("contentType must not contain control characters");
273
+ }
274
+ }
275
+ if (utf8ByteLength(trimmed) > MAX_CONTENT_TYPE_BYTES) {
276
+ throw new AttestationProtocolError(`contentType must be at most ${MAX_CONTENT_TYPE_BYTES} UTF-8 bytes`);
277
+ }
278
+ return trimmed;
279
+ }
280
+ function assertValidPercentEscapes(target) {
281
+ for (let index = 0; index < target.length; index += 1) {
282
+ if (target[index] !== "%") {
283
+ continue;
284
+ }
285
+ const hex = target.slice(index + 1, index + 3);
286
+ if (!HEX_BYTE_PATTERN.test(hex)) {
287
+ throw new AttestationProtocolError("requestTarget contains a malformed percent escape");
288
+ }
289
+ index += 2;
290
+ }
291
+ }
292
+ function percentDecodeSegment(segment) {
293
+ return segment.replace(/%[0-9a-fA-F]{2}/g, (escape) => String.fromCharCode(Number.parseInt(escape.slice(1), 16)));
294
+ }
295
+ function canonicalizeRequestTarget(target) {
296
+ if (typeof target !== "string") {
297
+ throw new AttestationProtocolError("requestTarget must be a string");
298
+ }
299
+ if (target.length === 0) {
300
+ throw new AttestationProtocolError("requestTarget must not be empty");
301
+ }
302
+ if (utf8ByteLength(target) > MAX_REQUEST_TARGET_BYTES) {
303
+ throw new AttestationProtocolError(`requestTarget must be at most ${MAX_REQUEST_TARGET_BYTES} UTF-8 bytes`);
304
+ }
305
+ if (!target.startsWith("/")) {
306
+ throw new AttestationProtocolError("requestTarget must be an origin-form target starting with /");
307
+ }
308
+ if (target.startsWith("//")) {
309
+ throw new AttestationProtocolError("requestTarget must not resolve as an authority");
310
+ }
311
+ for (const char of target) {
312
+ const code = char.codePointAt(0);
313
+ if (code <= 32 || code === 127) {
314
+ throw new AttestationProtocolError("requestTarget must not contain controls or raw spaces");
315
+ }
316
+ if (char === "#" || char === "\\") {
317
+ throw new AttestationProtocolError("requestTarget must not contain fragments or backslashes");
318
+ }
319
+ }
320
+ assertValidPercentEscapes(target);
321
+ const queryIndex = target.indexOf("?");
322
+ const path = queryIndex === -1 ? target : target.slice(0, queryIndex);
323
+ for (const segment of path.split("/")) {
324
+ if (segment === "." || segment === "..") {
325
+ throw new AttestationProtocolError("requestTarget must not contain dot segments");
326
+ }
327
+ const decoded = percentDecodeSegment(segment).toLowerCase();
328
+ if (decoded === "." || decoded === "..") {
329
+ throw new AttestationProtocolError("requestTarget must not contain encoded dot segments");
330
+ }
331
+ }
332
+ if (queryIndex === target.length - 1) {
333
+ return target.slice(0, queryIndex);
334
+ }
335
+ return target;
336
+ }
337
+ function validateHeaderName(name) {
338
+ if (typeof name !== "string" || name.length === 0) {
339
+ throw new AttestationProtocolError("headerName must be a non-empty string");
340
+ }
341
+ if (utf8ByteLength(name) > MAX_HEADER_NAME_BYTES) {
342
+ throw new AttestationProtocolError(`headerName must be at most ${MAX_HEADER_NAME_BYTES} UTF-8 bytes`);
343
+ }
344
+ if (!HTTP_TOKEN_PATTERN.test(name)) {
345
+ throw new AttestationProtocolError("headerName must be a valid HTTP token");
346
+ }
347
+ if (RESERVED_HEADER_NAMES.has(name.toLowerCase())) {
348
+ throw new AttestationProtocolError(`headerName must not collide with reserved header ${name}`);
349
+ }
350
+ }
351
+ function extractSingleHeaderValue(raw) {
352
+ if (raw === void 0) {
353
+ return null;
354
+ }
355
+ const values = (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry !== void 0);
356
+ if (values.length === 0) {
357
+ return null;
358
+ }
359
+ if (values.length > 1) {
360
+ throw new AttestationProtocolError("duplicate signature headers are not allowed");
361
+ }
362
+ const value = values[0];
363
+ if (typeof value !== "string" || value.length === 0) {
364
+ return null;
365
+ }
366
+ if (value.includes(",")) {
367
+ throw new AttestationProtocolError("comma-joined signature headers are not allowed");
368
+ }
369
+ return value;
370
+ }
371
+ function validateReplayKey(replayKey) {
372
+ if (typeof replayKey !== "string") {
373
+ throw new AttestationProtocolError("replayKey must be a string");
374
+ }
375
+ const length = replayKey.length;
376
+ if (length < MIN_REPLAY_KEY_BYTES || length > MAX_REPLAY_KEY_BYTES) {
377
+ throw new AttestationProtocolError(`replayKey must be ${MIN_REPLAY_KEY_BYTES}-${MAX_REPLAY_KEY_BYTES} characters`);
378
+ }
379
+ for (const char of replayKey) {
380
+ const code = char.codePointAt(0);
381
+ if (code < 32 || code > 126) {
382
+ throw new AttestationProtocolError("replayKey must be printable ASCII");
383
+ }
384
+ }
385
+ }
386
+ function buildMacInputBytes(fields) {
387
+ validateReplayNamespace(fields.replayNamespace);
388
+ validatePublicOrigin(fields.publicOrigin);
389
+ validateKeyId(fields.keyId);
390
+ assertTimestampMs("timestampMs", fields.timestampMs);
391
+ validateNonceHex(fields.nonceHex);
392
+ const method = normalizeMethod(fields.method);
393
+ const requestTarget = canonicalizeRequestTarget(fields.requestTarget);
394
+ const contentType = normalizeContentType(fields.contentType);
395
+ validateBodyHashHex(fields.bodyHashHex);
396
+ const tuple = [
397
+ MAC_INPUT_LABEL,
398
+ PROTOCOL_VERSION,
399
+ fields.replayNamespace,
400
+ fields.publicOrigin,
401
+ fields.keyId,
402
+ fields.timestampMs,
403
+ fields.nonceHex,
404
+ method,
405
+ requestTarget,
406
+ contentType,
407
+ fields.bodyHashHex
408
+ ];
409
+ return canonicalJsonBytes(tuple);
410
+ }
411
+ function buildReplayKeyPreimageBytes(input) {
412
+ validateReplayNamespace(input.replayNamespace);
413
+ validatePublicOrigin(input.publicOrigin);
414
+ validateNonceHex(input.nonceHex);
415
+ return canonicalJsonBytes([input.replayNamespace, input.publicOrigin, input.nonceHex]);
416
+ }
417
+ function encodeTransactionId(fields) {
418
+ validateKeyId(fields.keyId);
419
+ assertTimestampMs("timestampMs", fields.timestampMs);
420
+ validateNonceHex(fields.nonceHex);
421
+ validateMac(fields.mac);
422
+ const encoded = base64urlEncode(
423
+ canonicalJsonBytes([PROTOCOL_VERSION, fields.keyId, fields.timestampMs, fields.nonceHex, fields.mac])
424
+ );
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
+ }
430
+ return encoded;
431
+ }
432
+ function decodeTransactionId(headerValue) {
433
+ if (typeof headerValue !== "string" || headerValue.length === 0) {
434
+ throw new AttestationProtocolError("transactionId must be a non-empty string");
435
+ }
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
+ }
441
+ const bytes = base64urlDecode(headerValue);
442
+ const text = utf8Decode(bytes);
443
+ const envelope = parseCanonicalJsonArray(text, 5);
444
+ const [version, keyId, timestampMs, nonceHex, mac] = envelope;
445
+ if (version !== PROTOCOL_VERSION) {
446
+ throw new AttestationProtocolError(`unsupported transactionId version ${String(version)}`);
447
+ }
448
+ validateKeyId(keyId);
449
+ assertTimestampMs("timestampMs", timestampMs);
450
+ validateNonceHex(nonceHex);
451
+ validateMac(mac);
452
+ return {
453
+ keyId,
454
+ timestampMs,
455
+ nonceHex,
456
+ mac
457
+ };
458
+ }
459
+
460
+ // src/shared/time-policy.ts
461
+ function assertPolicyField(name, value, min, max) {
462
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
463
+ throw new AttestationProtocolError(`${name} must be a safe integer in [${min}, ${max}]`);
464
+ }
465
+ }
466
+ function resolveTimePolicy(options = {}) {
467
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
468
+ throw new AttestationProtocolError("time policy options must be an object");
469
+ }
470
+ const allowed = /* @__PURE__ */ new Set(["maxAgeMs", "clockSkewMs", "clusterClockGuardMs"]);
471
+ for (const key of Object.keys(options)) {
472
+ if (!allowed.has(key)) {
473
+ throw new AttestationProtocolError(`unknown time policy option ${key}`);
474
+ }
475
+ }
476
+ const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
477
+ const clockSkewMs = options.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS;
478
+ const clusterClockGuardMs = options.clusterClockGuardMs ?? DEFAULT_CLUSTER_CLOCK_GUARD_MS;
479
+ assertPolicyField("maxAgeMs", maxAgeMs, MIN_MAX_AGE_MS, MAX_MAX_AGE_MS);
480
+ assertPolicyField("clockSkewMs", clockSkewMs, MIN_CLOCK_SKEW_MS, MAX_CLOCK_SKEW_MS);
481
+ assertPolicyField("clusterClockGuardMs", clusterClockGuardMs, MIN_CLUSTER_CLOCK_GUARD_MS, MAX_CLUSTER_CLOCK_GUARD_MS);
482
+ return Object.freeze({ maxAgeMs, clockSkewMs, clusterClockGuardMs });
483
+ }
484
+ function evaluateProofTiming(input) {
485
+ assertTimestampMs("timestampMs", input.timestampMs);
486
+ assertTimestampMs("nowMs", input.nowMs);
487
+ assertTimestampMs("acceptFrom", input.acceptFrom);
488
+ assertTimestampMs("acceptUntil", input.acceptUntil);
489
+ if (input.acceptFrom >= input.acceptUntil) {
490
+ throw new AttestationProtocolError("key acceptance window must satisfy acceptFrom < acceptUntil");
491
+ }
492
+ const { timestampMs, nowMs, acceptFrom, acceptUntil, policy } = input;
493
+ const active = acceptFrom <= nowMs && nowMs < acceptUntil;
494
+ if (!active) {
495
+ const timeDeadlineMs2 = safeAdd3(timestampMs, policy.maxAgeMs, policy.clockSkewMs);
496
+ const proofDeadlineMs2 = timeDeadlineMs2 === null ? acceptUntil : Math.min(timeDeadlineMs2, acceptUntil);
497
+ return {
498
+ decision: "stale-key",
499
+ timeDeadlineMs: timeDeadlineMs2,
500
+ proofDeadlineMs: proofDeadlineMs2,
501
+ retainUntilMs: safeAdd(proofDeadlineMs2, policy.clusterClockGuardMs)
502
+ };
503
+ }
504
+ if (timestampMs > nowMs + policy.clockSkewMs) {
505
+ const timeDeadlineMs2 = safeAdd3(timestampMs, policy.maxAgeMs, policy.clockSkewMs);
506
+ const proofDeadlineMs2 = timeDeadlineMs2 === null ? acceptUntil : Math.min(timeDeadlineMs2, acceptUntil);
507
+ return {
508
+ decision: "future",
509
+ timeDeadlineMs: timeDeadlineMs2,
510
+ proofDeadlineMs: proofDeadlineMs2,
511
+ retainUntilMs: safeAdd(proofDeadlineMs2, policy.clusterClockGuardMs)
512
+ };
513
+ }
514
+ const timeDeadlineMs = safeAdd3(timestampMs, policy.maxAgeMs, policy.clockSkewMs);
515
+ if (timeDeadlineMs === null) {
516
+ return { decision: "expired", timeDeadlineMs: null, proofDeadlineMs: null, retainUntilMs: null };
517
+ }
518
+ const proofDeadlineMs = Math.min(timeDeadlineMs, acceptUntil);
519
+ if (nowMs >= proofDeadlineMs) {
520
+ return {
521
+ decision: "expired",
522
+ timeDeadlineMs,
523
+ proofDeadlineMs,
524
+ retainUntilMs: safeAdd(proofDeadlineMs, policy.clusterClockGuardMs)
525
+ };
526
+ }
527
+ return {
528
+ decision: "valid",
529
+ timeDeadlineMs,
530
+ proofDeadlineMs,
531
+ retainUntilMs: safeAdd(proofDeadlineMs, policy.clusterClockGuardMs)
532
+ };
533
+ }
534
+ function safeAdd(first, second) {
535
+ const sum = first + second;
536
+ return Number.isSafeInteger(sum) ? sum : null;
537
+ }
538
+ function safeAdd3(first, second, third) {
539
+ const sum = first + second + third;
540
+ return Number.isSafeInteger(sum) ? sum : null;
541
+ }
542
+ function maxVerifierRemainingMs(policy) {
543
+ return policy.maxAgeMs + 2 * policy.clockSkewMs + policy.clusterClockGuardMs;
544
+ }
545
+ function maxStoreAdmissionMs(policy) {
546
+ return Math.min(GLOBAL_MAX_RETENTION_MS, policy.maxAgeMs + 2 * policy.clockSkewMs + 2 * policy.clusterClockGuardMs);
547
+ }
548
+ function checkRetentionAdmissible(input) {
549
+ assertTimestampMs("retainUntilMs", input.retainUntilMs);
550
+ assertTimestampMs("nowMs", input.nowMs);
551
+ const { retainUntilMs, nowMs, policy } = input;
552
+ if (retainUntilMs <= nowMs) {
553
+ return "expired";
554
+ }
555
+ const remaining = retainUntilMs - nowMs;
556
+ if (!Number.isSafeInteger(remaining)) {
557
+ return "overlong";
558
+ }
559
+ if (remaining > GLOBAL_MAX_RETENTION_MS) {
560
+ return "overlong";
561
+ }
562
+ if (remaining > maxStoreAdmissionMs(policy)) {
563
+ return "overlong";
564
+ }
565
+ return "admissible";
566
+ }
567
+
568
+ // src/client-errors.ts
569
+ var SignerClientError = class extends Error {
570
+ name = "SignerClientError";
571
+ code;
572
+ constructor(code, message, options) {
573
+ super(`[express-request-attestation] ${message}`);
574
+ this.code = code;
575
+ if (options?.cause !== void 0) {
576
+ this.cause = options.cause;
577
+ }
578
+ }
579
+ };
580
+ function isSignerClientError(error) {
581
+ return error instanceof SignerClientError;
582
+ }
583
+
584
+ // src/request-signer.ts
585
+ var REQUEST_SIGNER_OPTION_KEYS = /* @__PURE__ */ new Set(["keyId", "key", "publicOrigin", "replayNamespace"]);
586
+ var SIGN_INPUT_KEYS = /* @__PURE__ */ new Set([
587
+ "method",
588
+ "requestTarget",
589
+ "contentType",
590
+ "bodyHashHex",
591
+ "timestampMs",
592
+ "nonceHex"
593
+ ]);
594
+ function requireTextEncoder() {
595
+ const encoder = globalThis.TextEncoder;
596
+ if (typeof encoder !== "function") {
597
+ throw new SignerClientError(
598
+ "SIGNER_PREPARATION_FAILED",
599
+ "TextEncoder is unavailable. Signing requires a browser secure context with TextEncoder support."
600
+ );
601
+ }
602
+ }
603
+ function requireSecureContext() {
604
+ const flag = globalThis.isSecureContext;
605
+ if (flag === false) {
606
+ throw new SignerClientError(
607
+ "SIGNER_PREPARATION_FAILED",
608
+ "Signing requires a secure context (https or loopback http). crypto.subtle is unavailable in insecure contexts."
609
+ );
610
+ }
611
+ }
612
+ function requireSubtle() {
613
+ requireSecureContext();
614
+ requireTextEncoder();
615
+ const cryptoRef = globalThis.crypto;
616
+ const subtle = cryptoRef?.subtle;
617
+ if (subtle === void 0 || typeof subtle.importKey !== "function" || typeof subtle.sign !== "function" || typeof subtle.digest !== "function") {
618
+ throw new SignerClientError(
619
+ "SIGNER_PREPARATION_FAILED",
620
+ "WebCrypto subtle is unavailable. Signing requires a secure context with crypto.subtle (HMAC-SHA-256) support."
621
+ );
622
+ }
623
+ return subtle;
624
+ }
625
+ function requireGetRandomValues() {
626
+ const cryptoRef = globalThis.crypto;
627
+ const getRandomValues = cryptoRef?.getRandomValues;
628
+ if (typeof getRandomValues !== "function") {
629
+ throw new SignerClientError(
630
+ "SIGNER_PREPARATION_FAILED",
631
+ "crypto.getRandomValues is unavailable. Nonce generation requires a secure context with getRandomValues support; no Math.random fallback is used."
632
+ );
633
+ }
634
+ return getRandomValues.bind(cryptoRef);
635
+ }
636
+ function hexEncode(bytes) {
637
+ let out = "";
638
+ for (const byte of bytes) {
639
+ out += byte.toString(16).padStart(2, "0");
640
+ }
641
+ return out;
642
+ }
643
+ async function hashBodySha256Hex(body) {
644
+ if (!(body instanceof Uint8Array)) {
645
+ throw new AttestationProtocolError("body must be a Uint8Array");
646
+ }
647
+ const subtle = requireSubtle();
648
+ let digest;
649
+ try {
650
+ digest = await subtle.digest("SHA-256", body);
651
+ } catch (error) {
652
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Body hashing failed: WebCrypto digest is unavailable.", {
653
+ cause: error
654
+ });
655
+ }
656
+ return hexEncode(new Uint8Array(digest));
657
+ }
658
+ function generateNonceHex() {
659
+ const getRandomValues = requireGetRandomValues();
660
+ const bytes = new Uint8Array(16);
661
+ try {
662
+ getRandomValues(bytes);
663
+ } catch (error) {
664
+ throw new SignerClientError(
665
+ "SIGNER_PREPARATION_FAILED",
666
+ "Nonce generation failed: crypto.getRandomValues is unavailable.",
667
+ { cause: error }
668
+ );
669
+ }
670
+ return hexEncode(bytes);
671
+ }
672
+ function createRequestSigner(options) {
673
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
674
+ throw new AttestationProtocolError("request signer options must be an object");
675
+ }
676
+ for (const key of Object.keys(options)) {
677
+ if (!REQUEST_SIGNER_OPTION_KEYS.has(key)) {
678
+ throw new AttestationProtocolError(`unknown request signer option ${key}`);
679
+ }
680
+ }
681
+ validateKeyId(options.keyId);
682
+ validatePublicOrigin(options.publicOrigin);
683
+ validateReplayNamespace(options.replayNamespace);
684
+ validateKeyBytes(options.key);
685
+ const keyId = options.keyId;
686
+ const publicOrigin = options.publicOrigin;
687
+ const replayNamespace = options.replayNamespace;
688
+ const keyCopy = new Uint8Array(options.key);
689
+ let cachedKey = null;
690
+ async function getCryptoKey() {
691
+ if (cachedKey !== null) {
692
+ return cachedKey;
693
+ }
694
+ const subtle = requireSubtle();
695
+ const pending = (async () => {
696
+ try {
697
+ return await subtle.importKey("raw", keyCopy, { name: "HMAC", hash: "SHA-256" }, false, [
698
+ "sign"
699
+ ]);
700
+ } catch (error) {
701
+ throw new SignerClientError(
702
+ "SIGNER_PREPARATION_FAILED",
703
+ "WebCrypto key import failed: HMAC-SHA-256 with a non-extractable key is required.",
704
+ { cause: error }
705
+ );
706
+ }
707
+ })();
708
+ cachedKey = pending;
709
+ try {
710
+ await pending;
711
+ } catch {
712
+ cachedKey = null;
713
+ throw pending;
714
+ }
715
+ return pending;
716
+ }
717
+ async function sign(input) {
718
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
719
+ throw new AttestationProtocolError("sign input must be an object");
720
+ }
721
+ for (const key of Object.keys(input)) {
722
+ if (!SIGN_INPUT_KEYS.has(key)) {
723
+ throw new AttestationProtocolError(`unknown sign input field ${key}`);
724
+ }
725
+ }
726
+ const record = input;
727
+ const method = normalizeMethod(record.method);
728
+ const requestTarget = canonicalizeRequestTarget(record.requestTarget);
729
+ const contentType = normalizeContentType(record.contentType);
730
+ validateBodyHashHex(record.bodyHashHex);
731
+ assertTimestampMs("timestampMs", record.timestampMs);
732
+ validateNonceHex(record.nonceHex);
733
+ const timestampMs = record.timestampMs;
734
+ const nonceHex = record.nonceHex;
735
+ const bodyHashHex = record.bodyHashHex;
736
+ const macInput = buildMacInputBytes({
737
+ replayNamespace,
738
+ publicOrigin,
739
+ keyId,
740
+ timestampMs,
741
+ nonceHex,
742
+ method,
743
+ requestTarget,
744
+ contentType,
745
+ bodyHashHex
746
+ });
747
+ const subtle = requireSubtle();
748
+ const cryptoKey = await getCryptoKey();
749
+ let signature;
750
+ try {
751
+ signature = await subtle.sign("HMAC", cryptoKey, macInput);
752
+ } catch (error) {
753
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "WebCrypto signing failed.", {
754
+ cause: error
755
+ });
756
+ }
757
+ const mac = base64urlEncode(new Uint8Array(signature));
758
+ return encodeTransactionId({ keyId, timestampMs, nonceHex, mac });
759
+ }
760
+ const signer = {
761
+ version: 1,
762
+ keyId,
763
+ publicOrigin,
764
+ replayNamespace,
765
+ sign
766
+ };
767
+ return Object.freeze(signer);
768
+ }
769
+
770
+ // src/signer-client.ts
771
+ var SIGNER_METADATA_BYTE_CAP = 4096;
772
+ var DEFAULT_SIGNER_LOAD_TIMEOUT_MS = 5e3;
773
+ var MIN_SIGNER_LOAD_TIMEOUT_MS = 1;
774
+ var MAX_SIGNER_LOAD_TIMEOUT_MS = 6e4;
775
+ var FETCH_BUNDLE_OPTION_KEYS = /* @__PURE__ */ new Set([
776
+ "metadataUrl",
777
+ "apiOrigin",
778
+ "replayNamespace",
779
+ "fetch",
780
+ "loadModule",
781
+ "signal"
782
+ ]);
783
+ var SIGNER_CLIENT_OPTION_KEYS = /* @__PURE__ */ new Set([
784
+ "metadataUrl",
785
+ "apiOrigin",
786
+ "replayNamespace",
787
+ "fetch",
788
+ "loadModule",
789
+ "loadTimeoutMs"
790
+ ]);
791
+ function isLoopbackHostname2(hostname) {
792
+ return hostname === "localhost" || hostname === "[::1]" || /^127\./.test(hostname);
793
+ }
794
+ function resolveFetch(candidate) {
795
+ if (candidate === void 0) {
796
+ const globalFetch = globalThis.fetch;
797
+ if (typeof globalFetch !== "function") {
798
+ throw new SignerClientError(
799
+ "SIGNER_PREPARATION_FAILED",
800
+ "fetch is unavailable. Signer discovery requires a fetch implementation."
801
+ );
802
+ }
803
+ return globalFetch;
804
+ }
805
+ if (typeof candidate !== "function") {
806
+ throw new AttestationProtocolError("signer client fetch must be a function");
807
+ }
808
+ return candidate;
809
+ }
810
+ function resolveLoadModule(candidate) {
811
+ if (candidate === void 0) {
812
+ const loader = (moduleUrl) => import(moduleUrl);
813
+ return loader;
814
+ }
815
+ if (typeof candidate !== "function") {
816
+ throw new AttestationProtocolError("signer client loadModule must be a function");
817
+ }
818
+ return candidate;
819
+ }
820
+ function validateApiOrigin(value) {
821
+ validatePublicOrigin(value);
822
+ return value;
823
+ }
824
+ function validateMetadataUrl(value, apiOrigin) {
825
+ if (typeof value !== "string" || value.length === 0) {
826
+ throw new AttestationProtocolError("metadataUrl must be a non-empty string");
827
+ }
828
+ let parsed;
829
+ try {
830
+ parsed = new URL(value);
831
+ } catch {
832
+ throw new AttestationProtocolError("metadataUrl must be an absolute URL");
833
+ }
834
+ if (parsed.username !== "" || parsed.password !== "") {
835
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer metadata URL must not contain userinfo.");
836
+ }
837
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
838
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer metadata URL must use http or https.");
839
+ }
840
+ if (parsed.protocol === "http:" && !isLoopbackHostname2(parsed.hostname)) {
841
+ throw new SignerClientError(
842
+ "SIGNER_ORIGIN_MISMATCH",
843
+ "Signer metadata http URL is allowed only for loopback development."
844
+ );
845
+ }
846
+ let expected;
847
+ try {
848
+ expected = new URL(apiOrigin);
849
+ } catch {
850
+ throw new AttestationProtocolError("apiOrigin must be a valid origin");
851
+ }
852
+ if (parsed.origin !== expected.origin) {
853
+ throw new SignerClientError(
854
+ "SIGNER_ORIGIN_MISMATCH",
855
+ "Signer metadata URL origin does not match the configured API origin."
856
+ );
857
+ }
858
+ return value;
859
+ }
860
+ function validateLoadTimeoutMs(value) {
861
+ if (value === void 0) {
862
+ return DEFAULT_SIGNER_LOAD_TIMEOUT_MS;
863
+ }
864
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_SIGNER_LOAD_TIMEOUT_MS || value > MAX_SIGNER_LOAD_TIMEOUT_MS) {
865
+ throw new AttestationProtocolError(
866
+ `loadTimeoutMs must be a safe integer in [${MIN_SIGNER_LOAD_TIMEOUT_MS}, ${MAX_SIGNER_LOAD_TIMEOUT_MS}]`
867
+ );
868
+ }
869
+ return value;
870
+ }
871
+ function validateBundleConfig(options) {
872
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
873
+ throw new AttestationProtocolError("fetchSignerBundle options must be an object");
874
+ }
875
+ for (const key of Object.keys(options)) {
876
+ if (!FETCH_BUNDLE_OPTION_KEYS.has(key)) {
877
+ throw new AttestationProtocolError(`unknown fetchSignerBundle option ${key}`);
878
+ }
879
+ }
880
+ const apiOrigin = validateApiOrigin(options.apiOrigin);
881
+ validateReplayNamespace(options.replayNamespace);
882
+ const replayNamespace = options.replayNamespace;
883
+ const metadataUrl = validateMetadataUrl(options.metadataUrl, apiOrigin);
884
+ const fetchImpl = resolveFetch(options.fetch);
885
+ const loadModule = resolveLoadModule(options.loadModule);
886
+ const signal = options.signal;
887
+ if (signal !== void 0 && !(signal instanceof AbortSignal)) {
888
+ throw new AttestationProtocolError("fetchSignerBundle signal must be an AbortSignal");
889
+ }
890
+ return { metadataUrl, apiOrigin, replayNamespace, fetchImpl, loadModule, signal };
891
+ }
892
+ function resolveSignerModuleUrl(signerUrl, metadataUrl, apiOrigin) {
893
+ if (typeof signerUrl !== "string" || signerUrl.length === 0) {
894
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer metadata signerUrl must be a non-empty string.");
895
+ }
896
+ let resolved;
897
+ try {
898
+ resolved = new URL(signerUrl, metadataUrl);
899
+ } catch {
900
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer module URL is not a valid URL.");
901
+ }
902
+ if (resolved.username !== "" || resolved.password !== "") {
903
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer module URL must not contain userinfo.");
904
+ }
905
+ if (resolved.protocol !== "https:" && resolved.protocol !== "http:") {
906
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer module URL must use http or https.");
907
+ }
908
+ if (resolved.protocol === "http:" && !isLoopbackHostname2(resolved.hostname)) {
909
+ throw new SignerClientError(
910
+ "SIGNER_ORIGIN_MISMATCH",
911
+ "Signer module http URL is allowed only for loopback development."
912
+ );
913
+ }
914
+ let expected;
915
+ try {
916
+ expected = new URL(apiOrigin);
917
+ } catch {
918
+ throw new AttestationProtocolError("apiOrigin must be a valid origin");
919
+ }
920
+ if (resolved.origin !== expected.origin) {
921
+ throw new SignerClientError(
922
+ "SIGNER_ORIGIN_MISMATCH",
923
+ "Signer module origin does not match the configured API origin."
924
+ );
925
+ }
926
+ if (!resolved.pathname.endsWith(".mjs")) {
927
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer module URL must target a .mjs module path.");
928
+ }
929
+ if (resolved.hash !== "") {
930
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer module URL must not contain a fragment.");
931
+ }
932
+ return resolved.href;
933
+ }
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 {
973
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata response is unreadable.");
974
+ }
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.");
983
+ }
984
+ }
985
+ try {
986
+ return JSON.parse(raw);
987
+ } catch {
988
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata is not valid JSON.");
989
+ }
990
+ }
991
+ function validateMetadataDocument(document, expected) {
992
+ if (document === null || typeof document !== "object" || Array.isArray(document)) {
993
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata must be a JSON object.");
994
+ }
995
+ const record = document;
996
+ if (record["version"] !== PROTOCOL_VERSION) {
997
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Signer metadata version must be 1.");
998
+ }
999
+ try {
1000
+ validateKeyId(record["keyId"]);
1001
+ } catch {
1002
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Signer metadata keyId is invalid.");
1003
+ }
1004
+ if (typeof record["signerUrl"] !== "string" || record["signerUrl"].length === 0) {
1005
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer metadata signerUrl is invalid.");
1006
+ }
1007
+ try {
1008
+ validatePublicOrigin(record["publicOrigin"]);
1009
+ } catch {
1010
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer metadata publicOrigin is invalid.");
1011
+ }
1012
+ try {
1013
+ validateReplayNamespace(record["replayNamespace"]);
1014
+ } catch {
1015
+ throw new SignerClientError("SIGNER_ORIGIN_MISMATCH", "Signer metadata replayNamespace is invalid.");
1016
+ }
1017
+ if (record["publicOrigin"] !== expected.apiOrigin) {
1018
+ throw new SignerClientError(
1019
+ "SIGNER_ORIGIN_MISMATCH",
1020
+ "Signer metadata origin does not match the configured API origin."
1021
+ );
1022
+ }
1023
+ if (record["replayNamespace"] !== expected.replayNamespace) {
1024
+ throw new SignerClientError(
1025
+ "SIGNER_ORIGIN_MISMATCH",
1026
+ "Signer metadata namespace does not match the configured replay namespace."
1027
+ );
1028
+ }
1029
+ return {
1030
+ version: 1,
1031
+ keyId: record["keyId"],
1032
+ signerUrl: record["signerUrl"],
1033
+ publicOrigin: record["publicOrigin"],
1034
+ replayNamespace: record["replayNamespace"]
1035
+ };
1036
+ }
1037
+ function validateSignerModule(candidate, metadata) {
1038
+ if (typeof candidate === "string") {
1039
+ throw new SignerClientError(
1040
+ "SIGNER_MODULE_INVALID",
1041
+ "Signer module must be an ESM namespace with a signer export, not a JS source string."
1042
+ );
1043
+ }
1044
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
1045
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Signer module namespace is invalid.");
1046
+ }
1047
+ const record = candidate;
1048
+ if ("signSource" in record) {
1049
+ throw new SignerClientError(
1050
+ "SIGNER_MODULE_INVALID",
1051
+ "Signer module must export signer without evaluated source strings."
1052
+ );
1053
+ }
1054
+ const signer = record["signer"];
1055
+ if (signer === null || typeof signer !== "object" || Array.isArray(signer)) {
1056
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Signer module must export a signer object.");
1057
+ }
1058
+ const entry = signer;
1059
+ if (entry["version"] !== PROTOCOL_VERSION) {
1060
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Imported signer version must be 1.");
1061
+ }
1062
+ try {
1063
+ validateKeyId(entry["keyId"]);
1064
+ } catch {
1065
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Imported signer keyId is invalid.");
1066
+ }
1067
+ try {
1068
+ validatePublicOrigin(entry["publicOrigin"]);
1069
+ } catch {
1070
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Imported signer origin is invalid.");
1071
+ }
1072
+ try {
1073
+ validateReplayNamespace(entry["replayNamespace"]);
1074
+ } catch {
1075
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Imported signer namespace is invalid.");
1076
+ }
1077
+ if (entry["keyId"] !== metadata.keyId) {
1078
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Imported signer key does not match discovery metadata.");
1079
+ }
1080
+ if (entry["publicOrigin"] !== metadata.publicOrigin) {
1081
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Imported signer origin does not match discovery metadata.");
1082
+ }
1083
+ if (entry["replayNamespace"] !== metadata.replayNamespace) {
1084
+ throw new SignerClientError(
1085
+ "SIGNER_MODULE_INVALID",
1086
+ "Imported signer namespace does not match discovery metadata."
1087
+ );
1088
+ }
1089
+ if (typeof entry["sign"] !== "function") {
1090
+ throw new SignerClientError("SIGNER_MODULE_INVALID", "Imported signer sign function is invalid.");
1091
+ }
1092
+ return { signer };
1093
+ }
1094
+ async function fetchMetadataDocument(config) {
1095
+ if (config.signal?.aborted) {
1096
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
1097
+ }
1098
+ let response;
1099
+ try {
1100
+ response = await config.fetchImpl(config.metadataUrl, {
1101
+ cache: "no-store",
1102
+ credentials: "omit",
1103
+ redirect: "error",
1104
+ ...config.signal === void 0 ? {} : { signal: config.signal }
1105
+ });
1106
+ } catch (error) {
1107
+ if (error instanceof SignerClientError) {
1108
+ throw error;
1109
+ }
1110
+ const name = error.name;
1111
+ if (name === "AbortError" || config.signal?.aborted) {
1112
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer discovery was aborted.");
1113
+ }
1114
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
1115
+ }
1116
+ if (response.ok !== true) {
1117
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer metadata fetch failed.");
1118
+ }
1119
+ const document = await readBoundedMetadataDocument(response, "fetch");
1120
+ return validateMetadataDocument(document, {
1121
+ apiOrigin: config.apiOrigin,
1122
+ replayNamespace: config.replayNamespace
1123
+ });
1124
+ }
1125
+ async function importValidatedModule(config, metadata) {
1126
+ const moduleUrl = resolveSignerModuleUrl(metadata.signerUrl, config.metadataUrl, config.apiOrigin);
1127
+ let candidate;
1128
+ try {
1129
+ candidate = await config.loadModule(moduleUrl);
1130
+ } catch (error) {
1131
+ if (error instanceof SignerClientError) {
1132
+ throw error;
1133
+ }
1134
+ const name = error.name;
1135
+ if (name === "AbortError" || config.signal?.aborted) {
1136
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer module load was aborted.");
1137
+ }
1138
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer module import failed.");
1139
+ }
1140
+ return validateSignerModule(candidate, metadata);
1141
+ }
1142
+ async function fetchSignerBundle(options) {
1143
+ const config = validateBundleConfig(options);
1144
+ const metadata = await fetchMetadataDocument(config);
1145
+ return importValidatedModule(config, metadata);
1146
+ }
1147
+ function isRetirementRetryCandidate(error) {
1148
+ if (!(error instanceof SignerClientError)) {
1149
+ return false;
1150
+ }
1151
+ return error.code === "SIGNER_LOAD_FAILED" || error.code === "SIGNER_MODULE_INVALID";
1152
+ }
1153
+ function createSignerClient(options) {
1154
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
1155
+ throw new AttestationProtocolError("signer client options must be an object");
1156
+ }
1157
+ for (const key of Object.keys(options)) {
1158
+ if (!SIGNER_CLIENT_OPTION_KEYS.has(key)) {
1159
+ throw new AttestationProtocolError(`unknown signer client option ${key}`);
1160
+ }
1161
+ }
1162
+ const apiOrigin = validateApiOrigin(options.apiOrigin);
1163
+ validateReplayNamespace(options.replayNamespace);
1164
+ const replayNamespace = options.replayNamespace;
1165
+ const metadataUrl = validateMetadataUrl(options.metadataUrl, apiOrigin);
1166
+ const fetchImpl = resolveFetch(options.fetch);
1167
+ const loadModule = resolveLoadModule(options.loadModule);
1168
+ const loadTimeoutMs = validateLoadTimeoutMs(options.loadTimeoutMs);
1169
+ let active = null;
1170
+ let pending = null;
1171
+ let pendingGeneration = 0;
1172
+ let generation = 0;
1173
+ let disposed = false;
1174
+ let currentAborter = null;
1175
+ function throwIfDisposed() {
1176
+ if (disposed) {
1177
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer client is disposed.");
1178
+ }
1179
+ }
1180
+ async function loadWithSingleRetirement(signal) {
1181
+ const base = {
1182
+ metadataUrl,
1183
+ apiOrigin,
1184
+ replayNamespace,
1185
+ fetchImpl,
1186
+ loadModule,
1187
+ signal
1188
+ };
1189
+ const first = await fetchMetadataDocument(base);
1190
+ try {
1191
+ const module = await importValidatedModule(base, first);
1192
+ return module.signer;
1193
+ } catch (importError) {
1194
+ if (!isRetirementRetryCandidate(importError)) {
1195
+ throw importError;
1196
+ }
1197
+ if (signal.aborted) {
1198
+ throw importError;
1199
+ }
1200
+ const second = await fetchMetadataDocument(base);
1201
+ if (second.keyId === first.keyId) {
1202
+ throw importError;
1203
+ }
1204
+ if (signal.aborted) {
1205
+ throw importError;
1206
+ }
1207
+ const module = await importValidatedModule(base, second);
1208
+ return module.signer;
1209
+ }
1210
+ }
1211
+ function startLoad() {
1212
+ const myGeneration = generation;
1213
+ pendingGeneration = myGeneration;
1214
+ const aborter = new AbortController();
1215
+ currentAborter = aborter;
1216
+ let timer;
1217
+ const timeout = new Promise((_resolve, reject) => {
1218
+ timer = setTimeout(() => {
1219
+ try {
1220
+ aborter.abort();
1221
+ } catch {
1222
+ }
1223
+ reject(new SignerClientError("SIGNER_LOAD_TIMEOUT", "Signer load timed out."));
1224
+ }, loadTimeoutMs);
1225
+ const handle = timer;
1226
+ if (typeof handle.unref === "function") {
1227
+ handle.unref();
1228
+ }
1229
+ });
1230
+ const actual = loadWithSingleRetirement(aborter.signal);
1231
+ const raced = Promise.race([actual, timeout]);
1232
+ actual.then(
1233
+ () => void 0,
1234
+ () => void 0
1235
+ );
1236
+ const tracked = raced.then(
1237
+ (signer) => {
1238
+ if (timer !== void 0) {
1239
+ clearTimeout(timer);
1240
+ }
1241
+ if (disposed || myGeneration !== generation) {
1242
+ throw new SignerClientError("SIGNER_LOAD_FAILED", "Signer load was superseded.");
1243
+ }
1244
+ active = signer;
1245
+ if (pending === tracked) {
1246
+ pending = null;
1247
+ }
1248
+ currentAborter = null;
1249
+ return signer;
1250
+ },
1251
+ (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") {
1259
+ generation += 1;
1260
+ }
1261
+ currentAborter = null;
1262
+ throw error;
1263
+ }
1264
+ );
1265
+ pending = tracked;
1266
+ return tracked;
1267
+ }
1268
+ const client = {
1269
+ getSigner() {
1270
+ throwIfDisposed();
1271
+ if (active !== null) {
1272
+ return Promise.resolve(active);
1273
+ }
1274
+ if (pending !== null && pendingGeneration === generation) {
1275
+ return pending;
1276
+ }
1277
+ if (pending !== null && pendingGeneration !== generation) {
1278
+ pending = null;
1279
+ }
1280
+ return startLoad();
1281
+ },
1282
+ invalidate(observedKeyId) {
1283
+ if (disposed) {
1284
+ return;
1285
+ }
1286
+ if (typeof observedKeyId !== "string" || observedKeyId.length === 0) {
1287
+ return;
1288
+ }
1289
+ if (active !== null) {
1290
+ let matches;
1291
+ try {
1292
+ matches = active.keyId === observedKeyId;
1293
+ } catch {
1294
+ return;
1295
+ }
1296
+ if (!matches) {
1297
+ return;
1298
+ }
1299
+ active = null;
1300
+ generation += 1;
1301
+ pending = null;
1302
+ return;
1303
+ }
1304
+ generation += 1;
1305
+ pending = null;
1306
+ },
1307
+ dispose() {
1308
+ if (disposed) {
1309
+ return;
1310
+ }
1311
+ disposed = true;
1312
+ active = null;
1313
+ generation += 1;
1314
+ try {
1315
+ currentAborter?.abort();
1316
+ } catch {
1317
+ }
1318
+ currentAborter = null;
1319
+ pending = null;
1320
+ }
1321
+ };
1322
+ return Object.freeze(client);
1323
+ }
1324
+
1325
+ // src/request-body.ts
1326
+ var DEFAULT_ATTESTED_FETCH_MAX_BODY_BYTES = DEFAULT_MAX_BODY_BYTES;
1327
+ var ATTESTED_FETCH_MAX_BODY_BYTES_HARD_CAP = MAX_BODY_BYTES_HARD_CAP;
1328
+ var DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS = 5e3;
1329
+ var MIN_BODY_READ_TIMEOUT_MS = 1;
1330
+ var MAX_BODY_READ_TIMEOUT_MS = 6e4;
1331
+ function validateMaxBodyBytes(value) {
1332
+ if (value === void 0) {
1333
+ return DEFAULT_ATTESTED_FETCH_MAX_BODY_BYTES;
1334
+ }
1335
+ if (!Number.isSafeInteger(value) || value <= 0) {
1336
+ throw new AttestationProtocolError("maxBodyBytes must be a positive safe integer");
1337
+ }
1338
+ const bytes = value;
1339
+ if (bytes > ATTESTED_FETCH_MAX_BODY_BYTES_HARD_CAP) {
1340
+ throw new AttestationProtocolError(
1341
+ `maxBodyBytes must be at most ${ATTESTED_FETCH_MAX_BODY_BYTES_HARD_CAP} bytes (16 MiB v1 hard cap)`
1342
+ );
1343
+ }
1344
+ return bytes;
1345
+ }
1346
+ function validateBodyReadTimeoutMs(value) {
1347
+ if (value === void 0) {
1348
+ return DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS;
1349
+ }
1350
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_BODY_READ_TIMEOUT_MS || value > MAX_BODY_READ_TIMEOUT_MS) {
1351
+ throw new AttestationProtocolError(
1352
+ `bodyReadTimeoutMs must be a safe integer in [${MIN_BODY_READ_TIMEOUT_MS}, ${MAX_BODY_READ_TIMEOUT_MS}]`
1353
+ );
1354
+ }
1355
+ return value;
1356
+ }
1357
+ function getGlobalConstructor(name) {
1358
+ const candidate = globalThis[name];
1359
+ if (typeof candidate === "function") {
1360
+ return candidate;
1361
+ }
1362
+ return void 0;
1363
+ }
1364
+ function assertSupportedBodyInit(body) {
1365
+ if (body === void 0 || body === null) {
1366
+ return;
1367
+ }
1368
+ if (typeof body === "string") {
1369
+ return;
1370
+ }
1371
+ const BlobCtor = getGlobalConstructor("Blob");
1372
+ if (BlobCtor !== void 0 && body instanceof BlobCtor) {
1373
+ return;
1374
+ }
1375
+ const FormDataCtor = getGlobalConstructor("FormData");
1376
+ if (FormDataCtor !== void 0 && body instanceof FormDataCtor) {
1377
+ return;
1378
+ }
1379
+ const URLSearchParamsCtor = getGlobalConstructor("URLSearchParams");
1380
+ if (URLSearchParamsCtor !== void 0 && body instanceof URLSearchParamsCtor) {
1381
+ return;
1382
+ }
1383
+ const ReadableStreamCtor = getGlobalConstructor("ReadableStream");
1384
+ if (ReadableStreamCtor !== void 0 && body instanceof ReadableStreamCtor) {
1385
+ return;
1386
+ }
1387
+ if (body instanceof ArrayBuffer) {
1388
+ return;
1389
+ }
1390
+ if (ArrayBuffer.isView(body)) {
1391
+ return;
1392
+ }
1393
+ if (typeof body === "object") {
1394
+ throw new SignerClientError(
1395
+ "SIGNER_PREPARATION_FAILED",
1396
+ "Request body must be a valid Fetch BodyInit (string, URLSearchParams, Blob, ArrayBuffer/view, FormData, or stream). Caller JSON objects must be serialized with JSON.stringify first."
1397
+ );
1398
+ }
1399
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request body type is not supported.");
1400
+ }
1401
+ function toAbortError(signal) {
1402
+ if (signal !== null && signal !== void 0) {
1403
+ const reason = signal.reason;
1404
+ if (reason !== void 0) {
1405
+ return reason;
1406
+ }
1407
+ }
1408
+ const DOMExceptionCtor = getGlobalConstructor("DOMException");
1409
+ if (DOMExceptionCtor !== void 0) {
1410
+ return new DOMExceptionCtor("The operation was aborted.", "AbortError");
1411
+ }
1412
+ return new Error("The operation was aborted.");
1413
+ }
1414
+ async function materializeRequestBodyBytes(request, options = {}) {
1415
+ if (request === null || typeof request !== "object") {
1416
+ throw new AttestationProtocolError("materializeRequestBodyBytes request must be a Request");
1417
+ }
1418
+ if (typeof request.arrayBuffer !== "function") {
1419
+ throw new AttestationProtocolError("materializeRequestBodyBytes request must be a Request");
1420
+ }
1421
+ const maxBodyBytes = validateMaxBodyBytes(options.maxBodyBytes);
1422
+ const timeoutMs = validateBodyReadTimeoutMs(options.timeoutMs);
1423
+ const signal = options.signal ?? null;
1424
+ if (signal?.aborted === true) {
1425
+ throw toAbortError(signal);
1426
+ }
1427
+ if (request.bodyUsed === true) {
1428
+ throw new SignerClientError(
1429
+ "SIGNER_PREPARATION_FAILED",
1430
+ "Request body has already been used. Clone the Request before sending or pass a fresh body."
1431
+ );
1432
+ }
1433
+ const body = request.body;
1434
+ if (body === null || body === void 0) {
1435
+ return new Uint8Array(0);
1436
+ }
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 {
1473
+ }
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
+ }
1480
+ });
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
+ }
1591
+
1592
+ // src/attested-fetch.ts
1593
+ var DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS2 = 5e3;
1594
+ var MIN_FETCH_TIMEOUT_MS = 1;
1595
+ var MAX_FETCH_TIMEOUT_MS = 6e4;
1596
+ var FETCH_WITH_ATTESTATION_OPTION_KEYS = /* @__PURE__ */ new Set([
1597
+ "signerClient",
1598
+ "apiOrigin",
1599
+ "replayNamespace",
1600
+ "fetch",
1601
+ "fetchFn",
1602
+ "headerName",
1603
+ "maxBodyBytes",
1604
+ "bodyReadTimeoutMs",
1605
+ "now"
1606
+ ]);
1607
+ function resolveFetchImpl(fetchAlias, fetchFnAlias) {
1608
+ if (fetchAlias !== void 0 && fetchFnAlias !== void 0) {
1609
+ throw new AttestationProtocolError("fetchWithAttestation options must set at most one of fetch/fetchFn");
1610
+ }
1611
+ const candidate = fetchAlias ?? fetchFnAlias;
1612
+ if (candidate === void 0) {
1613
+ const globalFetch = globalThis.fetch;
1614
+ if (typeof globalFetch !== "function") {
1615
+ throw new SignerClientError(
1616
+ "SIGNER_PREPARATION_FAILED",
1617
+ "fetch is unavailable. Prepared fetch requires a fetch implementation."
1618
+ );
1619
+ }
1620
+ return globalFetch;
1621
+ }
1622
+ if (typeof candidate !== "function") {
1623
+ throw new AttestationProtocolError("fetchWithAttestation fetch must be a function");
1624
+ }
1625
+ return candidate;
1626
+ }
1627
+ function validateMaxBodyBytes2(value) {
1628
+ if (value === void 0) {
1629
+ return DEFAULT_MAX_BODY_BYTES;
1630
+ }
1631
+ if (!Number.isSafeInteger(value) || value <= 0) {
1632
+ throw new AttestationProtocolError("maxBodyBytes must be a positive safe integer");
1633
+ }
1634
+ const bytes = value;
1635
+ if (bytes > MAX_BODY_BYTES_HARD_CAP) {
1636
+ throw new AttestationProtocolError(
1637
+ `maxBodyBytes must be at most ${MAX_BODY_BYTES_HARD_CAP} bytes (16 MiB v1 hard cap)`
1638
+ );
1639
+ }
1640
+ return bytes;
1641
+ }
1642
+ function validateBodyReadTimeoutMs2(value) {
1643
+ if (value === void 0) {
1644
+ return DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS2;
1645
+ }
1646
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_FETCH_TIMEOUT_MS || value > MAX_FETCH_TIMEOUT_MS) {
1647
+ throw new AttestationProtocolError(
1648
+ `bodyReadTimeoutMs must be a safe integer in [${MIN_FETCH_TIMEOUT_MS}, ${MAX_FETCH_TIMEOUT_MS}]`
1649
+ );
1650
+ }
1651
+ return value;
1652
+ }
1653
+ function resolveOptions(options) {
1654
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
1655
+ throw new AttestationProtocolError("fetchWithAttestation options must be an object");
1656
+ }
1657
+ for (const key of Object.keys(options)) {
1658
+ if (!FETCH_WITH_ATTESTATION_OPTION_KEYS.has(key)) {
1659
+ throw new AttestationProtocolError(`unknown fetchWithAttestation option ${key}`);
1660
+ }
1661
+ }
1662
+ const client = options.signerClient;
1663
+ if (client === null || typeof client !== "object" || typeof client.getSigner !== "function" || typeof client.invalidate !== "function") {
1664
+ throw new AttestationProtocolError("fetchWithAttestation signerClient must expose getSigner()/invalidate()");
1665
+ }
1666
+ validatePublicOrigin(options.apiOrigin);
1667
+ validateReplayNamespace(options.replayNamespace);
1668
+ const fetchImpl = resolveFetchImpl(
1669
+ options.fetch,
1670
+ options.fetchFn
1671
+ );
1672
+ const headerName = options.headerName ?? DEFAULT_ATTESTATION_HEADER;
1673
+ validateHeaderName(headerName);
1674
+ const maxBodyBytes = validateMaxBodyBytes2(options.maxBodyBytes);
1675
+ const bodyReadTimeoutMs = validateBodyReadTimeoutMs2(options.bodyReadTimeoutMs);
1676
+ const now = options.now ?? Date.now;
1677
+ if (typeof now !== "function") {
1678
+ throw new AttestationProtocolError("fetchWithAttestation now must be a function");
1679
+ }
1680
+ return Object.freeze({
1681
+ signerClient: client,
1682
+ apiOrigin: options.apiOrigin,
1683
+ replayNamespace: options.replayNamespace,
1684
+ fetchImpl,
1685
+ headerName,
1686
+ maxBodyBytes,
1687
+ bodyReadTimeoutMs,
1688
+ now
1689
+ });
1690
+ }
1691
+ function checkedTimestampMs(now) {
1692
+ let value;
1693
+ try {
1694
+ value = now();
1695
+ } catch {
1696
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Attestation clock failed.");
1697
+ }
1698
+ try {
1699
+ assertTimestampMs("timestampMs", value);
1700
+ } catch {
1701
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Attestation clock produced an invalid timestamp.");
1702
+ }
1703
+ return value;
1704
+ }
1705
+ function deriveRequestTarget(requestUrl) {
1706
+ let parsed;
1707
+ try {
1708
+ parsed = new URL(requestUrl);
1709
+ } catch {
1710
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request URL is invalid.");
1711
+ }
1712
+ const target = `${parsed.pathname}${parsed.search}`;
1713
+ return canonicalizeRequestTarget(target);
1714
+ }
1715
+ function assertAllowedContentEncoding(headers) {
1716
+ const raw = headers.get("content-encoding");
1717
+ if (raw === null) {
1718
+ return;
1719
+ }
1720
+ const tokens = [];
1721
+ for (const part of raw.split(",")) {
1722
+ const trimmed = part.trim();
1723
+ if (trimmed === "") {
1724
+ continue;
1725
+ }
1726
+ tokens.push(trimmed.toLowerCase());
1727
+ }
1728
+ for (const token of tokens) {
1729
+ if (token !== "identity") {
1730
+ throw new SignerClientError(
1731
+ "SIGNER_PREPARATION_FAILED",
1732
+ "Request content-encoding is unsupported. V1 accepts identity/no content-encoding only."
1733
+ );
1734
+ }
1735
+ }
1736
+ }
1737
+ function readRequestProperty(request, name) {
1738
+ return request[name];
1739
+ }
1740
+ function readWithFallback(base, name, fallback) {
1741
+ try {
1742
+ const value = readRequestProperty(base, name);
1743
+ return value ?? fallback;
1744
+ } catch {
1745
+ return fallback;
1746
+ }
1747
+ }
1748
+ function buildPreparedSnapshot(base, bodyBytes) {
1749
+ const method = normalizeMethod(readRequestProperty(base, "method"));
1750
+ const requestTarget = deriveRequestTarget(base.url);
1751
+ const contentType = normalizeContentType(base.headers.get("content-type"));
1752
+ assertAllowedContentEncoding(base.headers);
1753
+ const headers = new Headers(base.headers);
1754
+ const credentials = readWithFallback(base, "credentials", "same-origin");
1755
+ const signal = readWithFallback(base, "signal", null);
1756
+ const mode = readWithFallback(base, "mode", "cors");
1757
+ const cache = readWithFallback(base, "cache", "default");
1758
+ const referrerRaw = readWithFallback(base, "referrer", "");
1759
+ const referrer = typeof referrerRaw === "string" ? referrerRaw : "";
1760
+ const referrerPolicyRaw = readWithFallback(base, "referrerPolicy", "");
1761
+ const referrerPolicy = typeof referrerPolicyRaw === "string" ? referrerPolicyRaw : "";
1762
+ const integrityRaw = readWithFallback(base, "integrity", "");
1763
+ const integrity = typeof integrityRaw === "string" ? integrityRaw : "";
1764
+ const keepalive = readWithFallback(base, "keepalive", false) === true;
1765
+ return Object.freeze({
1766
+ url: base.url,
1767
+ method,
1768
+ requestTarget,
1769
+ contentType,
1770
+ bodyBytes,
1771
+ headers,
1772
+ credentials,
1773
+ signal,
1774
+ mode,
1775
+ cache,
1776
+ referrer,
1777
+ referrerPolicy,
1778
+ integrity,
1779
+ keepalive
1780
+ });
1781
+ }
1782
+ function buildSignedRequest(snapshot, headerName, transactionId) {
1783
+ const headers = new Headers(snapshot.headers);
1784
+ headers.set(headerName, transactionId);
1785
+ if (snapshot.contentType !== "") {
1786
+ try {
1787
+ headers.set("content-type", snapshot.contentType);
1788
+ } catch {
1789
+ }
1790
+ }
1791
+ const bodyForFetch = snapshot.bodyBytes.length === 0 ? null : snapshot.bodyBytes;
1792
+ const init = {
1793
+ method: snapshot.method,
1794
+ headers,
1795
+ redirect: "manual",
1796
+ credentials: snapshot.credentials,
1797
+ mode: snapshot.mode,
1798
+ cache: snapshot.cache,
1799
+ integrity: snapshot.integrity,
1800
+ keepalive: snapshot.keepalive
1801
+ };
1802
+ if (snapshot.signal !== null) {
1803
+ init.signal = snapshot.signal;
1804
+ }
1805
+ if (snapshot.referrer !== "") {
1806
+ try {
1807
+ init.referrer = snapshot.referrer;
1808
+ } catch {
1809
+ }
1810
+ }
1811
+ if (snapshot.referrerPolicy !== "") {
1812
+ try {
1813
+ init.referrerPolicy = snapshot.referrerPolicy;
1814
+ } catch {
1815
+ }
1816
+ }
1817
+ if (bodyForFetch !== null) {
1818
+ init.body = bodyForFetch;
1819
+ }
1820
+ try {
1821
+ return new Request(snapshot.url, init);
1822
+ } catch (error) {
1823
+ if (error instanceof SignerClientError || error instanceof AttestationProtocolError) {
1824
+ throw error;
1825
+ }
1826
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Prepared request could not be built.");
1827
+ }
1828
+ }
1829
+ function isStaleKeyRetryCandidate(response) {
1830
+ if (response.status !== 403) {
1831
+ return false;
1832
+ }
1833
+ try {
1834
+ return response.headers.get(ATTESTATION_ERROR_RESPONSE_HEADER) === "stale-key";
1835
+ } catch {
1836
+ return false;
1837
+ }
1838
+ }
1839
+ async function fetchWithAttestation(input, init, options) {
1840
+ const config = resolveOptions(options);
1841
+ if (init !== void 0 && (init === null || typeof init !== "object" || Array.isArray(init))) {
1842
+ throw new AttestationProtocolError("fetchWithAttestation init must be an object");
1843
+ }
1844
+ const initRecord = init ?? {};
1845
+ if (initRecord.body !== void 0 && initRecord.body !== null) {
1846
+ assertSupportedBodyInit(initRecord.body);
1847
+ }
1848
+ if (typeof input === "string") {
1849
+ if (input.length === 0) {
1850
+ throw new AttestationProtocolError("fetchWithAttestation input must be a non-empty URL or Request");
1851
+ }
1852
+ } else if (!(input instanceof Request) && !(input instanceof URL)) {
1853
+ throw new AttestationProtocolError("fetchWithAttestation input must be a URL, string, or Request");
1854
+ }
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;
1885
+ }
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;
1906
+ }
1907
+ throw new SignerClientError("SIGNER_PREPARATION_FAILED", "Request was aborted before signing.");
1908
+ }
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;
1917
+ const signer = await config.signerClient.getSigner();
1918
+ if (signer.version !== 1 || signer.publicOrigin !== config.apiOrigin || signer.replayNamespace !== config.replayNamespace) {
1919
+ throw new SignerClientError(
1920
+ "SIGNER_ORIGIN_MISMATCH",
1921
+ "Loaded signer protection space does not match the configured API origin/namespace."
1922
+ );
1923
+ }
1924
+ const timestampMs = checkedTimestampMs(config.now);
1925
+ const nonceHex = generateNonceHex();
1926
+ const bodyHashHex = await hashBodySha256Hex(snapshot.bodyBytes);
1927
+ const transactionId = await signer.sign({
1928
+ method: snapshot.method,
1929
+ requestTarget: snapshot.requestTarget,
1930
+ contentType: snapshot.contentType,
1931
+ bodyHashHex,
1932
+ timestampMs,
1933
+ nonceHex
1934
+ });
1935
+ return { transactionId, observedKeyId: signer.keyId };
1936
+ }
1937
+ const first = await signWithFreshProof();
1938
+ const firstRequest = buildSignedRequest(snapshot, config.headerName, first.transactionId);
1939
+ const firstResponse = await config.fetchImpl(firstRequest);
1940
+ if (!isStaleKeyRetryCandidate(firstResponse)) {
1941
+ return firstResponse;
1942
+ }
1943
+ try {
1944
+ config.signerClient.invalidate(first.observedKeyId);
1945
+ } catch {
1946
+ }
1947
+ try {
1948
+ await firstResponse.body?.cancel();
1949
+ } catch {
1950
+ }
1951
+ let secondSignerTransaction;
1952
+ try {
1953
+ secondSignerTransaction = await signWithFreshProof();
1954
+ } catch {
1955
+ return firstResponse;
1956
+ }
1957
+ if (secondSignerTransaction.transactionId === first.transactionId) {
1958
+ return firstResponse;
1959
+ }
1960
+ const secondRequest = buildSignedRequest(snapshot, config.headerName, secondSignerTransaction.transactionId);
1961
+ return config.fetchImpl(secondRequest);
1962
+ }
1963
+ export {
1964
+ ATTESTATION_ERROR_RESPONSE_HEADER,
1965
+ ATTESTED_FETCH_MAX_BODY_BYTES_HARD_CAP,
1966
+ AttestationProtocolError,
1967
+ DEFAULT_ATTESTATION_HEADER,
1968
+ DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS,
1969
+ DEFAULT_ATTESTED_FETCH_BODY_READ_TIMEOUT_MS2 as DEFAULT_ATTESTED_FETCH_BODY_TIMEOUT_MS,
1970
+ DEFAULT_ATTESTED_FETCH_MAX_BODY_BYTES,
1971
+ DEFAULT_CLOCK_SKEW_MS,
1972
+ DEFAULT_CLUSTER_CLOCK_GUARD_MS,
1973
+ DEFAULT_MAX_AGE_MS,
1974
+ DEFAULT_MAX_BODY_BYTES,
1975
+ DEFAULT_SIGNER_LOAD_TIMEOUT_MS,
1976
+ EMPTY_BODY_SHA256_HEX,
1977
+ GLOBAL_MAX_RETENTION_MS,
1978
+ KEY_BYTES_LENGTH,
1979
+ MAC_BYTES_LENGTH,
1980
+ MAC_INPUT_LABEL,
1981
+ MAX_BODY_BYTES_HARD_CAP,
1982
+ MAX_CLOCK_SKEW_MS,
1983
+ MAX_CLUSTER_CLOCK_GUARD_MS,
1984
+ MAX_CONTENT_TYPE_BYTES,
1985
+ MAX_ENCODED_TRANSACTION_ID_BYTES,
1986
+ MAX_HEADER_NAME_BYTES,
1987
+ MAX_KEY_ID_LENGTH,
1988
+ MAX_MAX_AGE_MS,
1989
+ MAX_PUBLIC_ORIGIN_BYTES,
1990
+ MAX_REPLAY_KEY_BYTES,
1991
+ MAX_REPLAY_NAMESPACE_LENGTH,
1992
+ MAX_REQUEST_TARGET_BYTES,
1993
+ MIN_CLOCK_SKEW_MS,
1994
+ MIN_CLUSTER_CLOCK_GUARD_MS,
1995
+ MIN_MAX_AGE_MS,
1996
+ MIN_REPLAY_KEY_BYTES,
1997
+ NONCE_BYTES_LENGTH,
1998
+ PROTOCOL_VERSION,
1999
+ REPLAY_KEY_PREFIX,
2000
+ SIGNER_METADATA_BYTE_CAP,
2001
+ SignerClientError,
2002
+ assertSupportedBodyInit,
2003
+ assertTimestampMs,
2004
+ base64urlDecode,
2005
+ base64urlEncode,
2006
+ buildMacInputBytes,
2007
+ buildReplayKeyPreimageBytes,
2008
+ canonicalJsonBytes,
2009
+ canonicalizeRequestTarget,
2010
+ checkRetentionAdmissible,
2011
+ createRequestSigner,
2012
+ createSignerClient,
2013
+ decodeTransactionId,
2014
+ encodeTransactionId,
2015
+ evaluateProofTiming,
2016
+ extractSingleHeaderValue,
2017
+ fetchSignerBundle,
2018
+ fetchWithAttestation,
2019
+ generateNonceHex,
2020
+ hashBodySha256Hex,
2021
+ isSignerClientError,
2022
+ materializeRequestBodyBytes,
2023
+ maxStoreAdmissionMs,
2024
+ maxVerifierRemainingMs,
2025
+ normalizeContentType,
2026
+ normalizeMethod,
2027
+ parseCanonicalJsonArray,
2028
+ resolveTimePolicy,
2029
+ utf8ByteLength,
2030
+ utf8Decode,
2031
+ utf8Encode,
2032
+ validateBodyHashHex,
2033
+ validateHeaderName,
2034
+ validateKeyBytes,
2035
+ validateKeyId,
2036
+ validateMac,
2037
+ validateNonceHex,
2038
+ validatePublicOrigin,
2039
+ validateReplayKey,
2040
+ validateReplayNamespace
2041
+ };