@web-ts-toolkit/express-request-attestation 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -295,6 +295,13 @@ var NONCE_HEX_PATTERN = /^[0-9a-f]{32}$/;
295
295
  var BODY_HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
296
296
  var HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
297
297
  var HEX_BYTE_PATTERN = /^[0-9a-fA-F]{2}$/;
298
+ function assertEncodedTransactionIdBound(value) {
299
+ if (value.length > MAX_ENCODED_TRANSACTION_ID_BYTES) {
300
+ throw new AttestationProtocolError(
301
+ `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
302
+ );
303
+ }
304
+ }
298
305
  var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
299
306
  "authorization",
300
307
  "proxy-authenticate",
@@ -490,6 +497,7 @@ function extractSingleHeaderValue(raw) {
490
497
  if (typeof value !== "string" || value.length === 0) {
491
498
  return null;
492
499
  }
500
+ assertEncodedTransactionIdBound(value);
493
501
  if (value.includes(",")) {
494
502
  throw new AttestationProtocolError("comma-joined signature headers are not allowed");
495
503
  }
@@ -549,23 +557,18 @@ function encodeTransactionId(fields) {
549
557
  const encoded = base64urlEncode(
550
558
  canonicalJsonBytes([PROTOCOL_VERSION, fields.keyId, fields.timestampMs, fields.nonceHex, fields.mac])
551
559
  );
552
- if (utf8ByteLength(encoded) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
553
- throw new AttestationProtocolError(
554
- `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
555
- );
556
- }
560
+ assertEncodedTransactionIdBound(encoded);
557
561
  return encoded;
558
562
  }
559
563
  function decodeTransactionId(headerValue) {
560
564
  if (typeof headerValue !== "string" || headerValue.length === 0) {
561
565
  throw new AttestationProtocolError("transactionId must be a non-empty string");
562
566
  }
563
- if (utf8ByteLength(headerValue) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
564
- throw new AttestationProtocolError(
565
- `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
566
- );
567
- }
567
+ assertEncodedTransactionIdBound(headerValue);
568
568
  const bytes = base64urlDecode(headerValue);
569
+ if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
570
+ throw new AttestationProtocolError("transactionId must not contain a UTF-8 BOM");
571
+ }
569
572
  const text = utf8Decode(bytes);
570
573
  const envelope = parseCanonicalJsonArray(text, 5);
571
574
  const [version, keyId, timestampMs, nonceHex, mac] = envelope;
@@ -897,10 +900,7 @@ function copyAttestationSnapshot(snapshot) {
897
900
  }
898
901
  const currentKeyId = record.currentKeyId;
899
902
  return Object.freeze({ currentKeyId, keys: Object.freeze(copied) });
900
- } catch (error) {
901
- if (error instanceof AttestationError) {
902
- throw error;
903
- }
903
+ } catch {
904
904
  fail("ATTESTATION_KEYS_UNAVAILABLE");
905
905
  }
906
906
  }
@@ -1009,9 +1009,12 @@ function deriveReplayKey(input) {
1009
1009
  return `${REPLAY_KEY_PREFIX}${digest}`;
1010
1010
  }
1011
1011
  function verifyAttestationProof(input) {
1012
+ return preflightAttestationProof(input.headerValue)(input);
1013
+ }
1014
+ function preflightAttestationProof(rawHeader) {
1012
1015
  let headerValue;
1013
1016
  try {
1014
- headerValue = extractSingleHeaderValue(input.headerValue);
1017
+ headerValue = extractSingleHeaderValue(rawHeader);
1015
1018
  } catch {
1016
1019
  fail("ATTESTATION_MALFORMED");
1017
1020
  }
@@ -1024,6 +1027,9 @@ function verifyAttestationProof(input) {
1024
1027
  } catch {
1025
1028
  fail("ATTESTATION_MALFORMED");
1026
1029
  }
1030
+ return (input) => verifyDecodedAttestationProof(input, envelope);
1031
+ }
1032
+ function verifyDecodedAttestationProof(input, envelope) {
1027
1033
  const snapshot = copyAttestationSnapshot(input.snapshot);
1028
1034
  const keyEntry = snapshot.keys.find((entry) => entry.keyId === envelope.keyId);
1029
1035
  if (keyEntry === void 0) {
@@ -1519,43 +1525,20 @@ function createRequestAttestationMiddleware(options) {
1519
1525
  const config = resolveConfig(options);
1520
1526
  const middleware = (req, res, next) => {
1521
1527
  void Promise.resolve().then(async () => {
1528
+ const verifyProof = preflightAttestationProof(extractRawHeader(req, config.headerNameLower));
1529
+ const contentTypeHeader = extractRawHeader(req, "content-type");
1522
1530
  let nowMs = checkedNow(config.now);
1523
- let headerValue;
1524
- try {
1525
- headerValue = extractRawHeader(req, config.headerNameLower);
1526
- } catch (error) {
1527
- if (error instanceof AttestationError) {
1528
- sendControlledError(res, error, config);
1529
- return;
1530
- }
1531
- throw error;
1532
- }
1533
- let snapshotRaw;
1534
- try {
1535
- const pending = config.keyProvider.getSnapshot();
1536
- snapshotRaw = await withOperationTimeout(Promise.resolve(pending), config.operationTimeoutMs);
1537
- } catch (error) {
1538
- if (error instanceof AttestationError) {
1539
- sendControlledError(res, error, config);
1540
- return;
1541
- }
1542
- sendControlledError(res, new AttestationError("ATTESTATION_KEYS_UNAVAILABLE"), config);
1543
- return;
1544
- }
1545
1531
  let snapshot;
1546
1532
  try {
1533
+ const pending = config.keyProvider.getSnapshot();
1534
+ const snapshotRaw = await withOperationTimeout(Promise.resolve(pending), config.operationTimeoutMs);
1547
1535
  snapshot = copyAttestationSnapshot(snapshotRaw);
1548
- } catch (error) {
1549
- if (error instanceof AttestationError) {
1550
- sendControlledError(res, error, config);
1551
- return;
1552
- }
1536
+ } catch {
1553
1537
  sendControlledError(res, new AttestationError("ATTESTATION_KEYS_UNAVAILABLE"), config);
1554
1538
  return;
1555
1539
  }
1556
1540
  const record = req;
1557
1541
  const originalUrl = typeof record.originalUrl === "string" ? record.originalUrl : record.url;
1558
- const contentTypeHeader = record.headers?.["content-type"] ?? void 0;
1559
1542
  const contentEncodingHeader = record.headers?.["content-encoding"] ?? void 0;
1560
1543
  let bodyDigestHex;
1561
1544
  try {
@@ -1569,8 +1552,7 @@ function createRequestAttestationMiddleware(options) {
1569
1552
  }
1570
1553
  let verified;
1571
1554
  try {
1572
- verified = verifyAttestationProof({
1573
- headerValue,
1555
+ verified = verifyProof({
1574
1556
  method: record.method,
1575
1557
  originalUrl,
1576
1558
  publicPathPrefix: config.publicPathPrefix,
@@ -2677,16 +2659,7 @@ function createSignerBundleRouter(options) {
2677
2659
  const router = (0, import_express.Router)();
2678
2660
  router.get("/signer-meta", (req, res) => {
2679
2661
  void Promise.resolve().then(async () => {
2680
- let nowMs;
2681
- try {
2682
- nowMs = checkedNow2(config.now);
2683
- } catch (error) {
2684
- if (error instanceof AttestationError) {
2685
- sendInternalError(res);
2686
- return;
2687
- }
2688
- throw error;
2689
- }
2662
+ checkedNow2(config.now);
2690
2663
  let snapshotRaw;
2691
2664
  try {
2692
2665
  const pending = config.keyProvider.getSnapshot();
@@ -2702,6 +2675,7 @@ function createSignerBundleRouter(options) {
2702
2675
  sendKeysUnavailable(res);
2703
2676
  return;
2704
2677
  }
2678
+ const nowMs = checkedNow2(config.now);
2705
2679
  const current = snapshot.keys.find((entry) => entry.keyId === snapshot.currentKeyId);
2706
2680
  if (current === void 0) {
2707
2681
  sendKeysUnavailable(res);
@@ -2731,7 +2705,8 @@ function createSignerBundleRouter(options) {
2731
2705
  sendInternalError(res);
2732
2706
  return;
2733
2707
  }
2734
- const signerUrl = `${config.publicOrigin}${config.publicPathPrefix}${config.basePath}/signer.${keyAsset.contentHash}.mjs`;
2708
+ const assetBasePath = config.basePath === "/" ? "" : config.basePath;
2709
+ const signerUrl = `${config.publicOrigin}${config.publicPathPrefix}${assetBasePath}/${keyAsset.fileName}`;
2735
2710
  const document = {
2736
2711
  version: PROTOCOL_VERSION,
2737
2712
  keyId: current.keyId,
package/index.mjs CHANGED
@@ -171,6 +171,13 @@ var NONCE_HEX_PATTERN = /^[0-9a-f]{32}$/;
171
171
  var BODY_HASH_HEX_PATTERN = /^[0-9a-f]{64}$/;
172
172
  var HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
173
173
  var HEX_BYTE_PATTERN = /^[0-9a-fA-F]{2}$/;
174
+ function assertEncodedTransactionIdBound(value) {
175
+ if (value.length > MAX_ENCODED_TRANSACTION_ID_BYTES) {
176
+ throw new AttestationProtocolError(
177
+ `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
178
+ );
179
+ }
180
+ }
174
181
  var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
175
182
  "authorization",
176
183
  "proxy-authenticate",
@@ -366,6 +373,7 @@ function extractSingleHeaderValue(raw) {
366
373
  if (typeof value !== "string" || value.length === 0) {
367
374
  return null;
368
375
  }
376
+ assertEncodedTransactionIdBound(value);
369
377
  if (value.includes(",")) {
370
378
  throw new AttestationProtocolError("comma-joined signature headers are not allowed");
371
379
  }
@@ -425,23 +433,18 @@ function encodeTransactionId(fields) {
425
433
  const encoded = base64urlEncode(
426
434
  canonicalJsonBytes([PROTOCOL_VERSION, fields.keyId, fields.timestampMs, fields.nonceHex, fields.mac])
427
435
  );
428
- if (utf8ByteLength(encoded) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
429
- throw new AttestationProtocolError(
430
- `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
431
- );
432
- }
436
+ assertEncodedTransactionIdBound(encoded);
433
437
  return encoded;
434
438
  }
435
439
  function decodeTransactionId(headerValue) {
436
440
  if (typeof headerValue !== "string" || headerValue.length === 0) {
437
441
  throw new AttestationProtocolError("transactionId must be a non-empty string");
438
442
  }
439
- if (utf8ByteLength(headerValue) > MAX_ENCODED_TRANSACTION_ID_BYTES) {
440
- throw new AttestationProtocolError(
441
- `encoded transactionId must be at most ${MAX_ENCODED_TRANSACTION_ID_BYTES} bytes`
442
- );
443
- }
443
+ assertEncodedTransactionIdBound(headerValue);
444
444
  const bytes = base64urlDecode(headerValue);
445
+ if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
446
+ throw new AttestationProtocolError("transactionId must not contain a UTF-8 BOM");
447
+ }
445
448
  const text = utf8Decode(bytes);
446
449
  const envelope = parseCanonicalJsonArray(text, 5);
447
450
  const [version, keyId, timestampMs, nonceHex, mac] = envelope;
@@ -773,10 +776,7 @@ function copyAttestationSnapshot(snapshot) {
773
776
  }
774
777
  const currentKeyId = record.currentKeyId;
775
778
  return Object.freeze({ currentKeyId, keys: Object.freeze(copied) });
776
- } catch (error) {
777
- if (error instanceof AttestationError) {
778
- throw error;
779
- }
779
+ } catch {
780
780
  fail("ATTESTATION_KEYS_UNAVAILABLE");
781
781
  }
782
782
  }
@@ -885,9 +885,12 @@ function deriveReplayKey(input) {
885
885
  return `${REPLAY_KEY_PREFIX}${digest}`;
886
886
  }
887
887
  function verifyAttestationProof(input) {
888
+ return preflightAttestationProof(input.headerValue)(input);
889
+ }
890
+ function preflightAttestationProof(rawHeader) {
888
891
  let headerValue;
889
892
  try {
890
- headerValue = extractSingleHeaderValue(input.headerValue);
893
+ headerValue = extractSingleHeaderValue(rawHeader);
891
894
  } catch {
892
895
  fail("ATTESTATION_MALFORMED");
893
896
  }
@@ -900,6 +903,9 @@ function verifyAttestationProof(input) {
900
903
  } catch {
901
904
  fail("ATTESTATION_MALFORMED");
902
905
  }
906
+ return (input) => verifyDecodedAttestationProof(input, envelope);
907
+ }
908
+ function verifyDecodedAttestationProof(input, envelope) {
903
909
  const snapshot = copyAttestationSnapshot(input.snapshot);
904
910
  const keyEntry = snapshot.keys.find((entry) => entry.keyId === envelope.keyId);
905
911
  if (keyEntry === void 0) {
@@ -1395,43 +1401,20 @@ function createRequestAttestationMiddleware(options) {
1395
1401
  const config = resolveConfig(options);
1396
1402
  const middleware = (req, res, next) => {
1397
1403
  void Promise.resolve().then(async () => {
1404
+ const verifyProof = preflightAttestationProof(extractRawHeader(req, config.headerNameLower));
1405
+ const contentTypeHeader = extractRawHeader(req, "content-type");
1398
1406
  let nowMs = checkedNow(config.now);
1399
- let headerValue;
1400
- try {
1401
- headerValue = extractRawHeader(req, config.headerNameLower);
1402
- } catch (error) {
1403
- if (error instanceof AttestationError) {
1404
- sendControlledError(res, error, config);
1405
- return;
1406
- }
1407
- throw error;
1408
- }
1409
- let snapshotRaw;
1410
- try {
1411
- const pending = config.keyProvider.getSnapshot();
1412
- snapshotRaw = await withOperationTimeout(Promise.resolve(pending), config.operationTimeoutMs);
1413
- } catch (error) {
1414
- if (error instanceof AttestationError) {
1415
- sendControlledError(res, error, config);
1416
- return;
1417
- }
1418
- sendControlledError(res, new AttestationError("ATTESTATION_KEYS_UNAVAILABLE"), config);
1419
- return;
1420
- }
1421
1407
  let snapshot;
1422
1408
  try {
1409
+ const pending = config.keyProvider.getSnapshot();
1410
+ const snapshotRaw = await withOperationTimeout(Promise.resolve(pending), config.operationTimeoutMs);
1423
1411
  snapshot = copyAttestationSnapshot(snapshotRaw);
1424
- } catch (error) {
1425
- if (error instanceof AttestationError) {
1426
- sendControlledError(res, error, config);
1427
- return;
1428
- }
1412
+ } catch {
1429
1413
  sendControlledError(res, new AttestationError("ATTESTATION_KEYS_UNAVAILABLE"), config);
1430
1414
  return;
1431
1415
  }
1432
1416
  const record = req;
1433
1417
  const originalUrl = typeof record.originalUrl === "string" ? record.originalUrl : record.url;
1434
- const contentTypeHeader = record.headers?.["content-type"] ?? void 0;
1435
1418
  const contentEncodingHeader = record.headers?.["content-encoding"] ?? void 0;
1436
1419
  let bodyDigestHex;
1437
1420
  try {
@@ -1445,8 +1428,7 @@ function createRequestAttestationMiddleware(options) {
1445
1428
  }
1446
1429
  let verified;
1447
1430
  try {
1448
- verified = verifyAttestationProof({
1449
- headerValue,
1431
+ verified = verifyProof({
1450
1432
  method: record.method,
1451
1433
  originalUrl,
1452
1434
  publicPathPrefix: config.publicPathPrefix,
@@ -2553,16 +2535,7 @@ function createSignerBundleRouter(options) {
2553
2535
  const router = Router();
2554
2536
  router.get("/signer-meta", (req, res) => {
2555
2537
  void Promise.resolve().then(async () => {
2556
- let nowMs;
2557
- try {
2558
- nowMs = checkedNow2(config.now);
2559
- } catch (error) {
2560
- if (error instanceof AttestationError) {
2561
- sendInternalError(res);
2562
- return;
2563
- }
2564
- throw error;
2565
- }
2538
+ checkedNow2(config.now);
2566
2539
  let snapshotRaw;
2567
2540
  try {
2568
2541
  const pending = config.keyProvider.getSnapshot();
@@ -2578,6 +2551,7 @@ function createSignerBundleRouter(options) {
2578
2551
  sendKeysUnavailable(res);
2579
2552
  return;
2580
2553
  }
2554
+ const nowMs = checkedNow2(config.now);
2581
2555
  const current = snapshot.keys.find((entry) => entry.keyId === snapshot.currentKeyId);
2582
2556
  if (current === void 0) {
2583
2557
  sendKeysUnavailable(res);
@@ -2607,7 +2581,8 @@ function createSignerBundleRouter(options) {
2607
2581
  sendInternalError(res);
2608
2582
  return;
2609
2583
  }
2610
- const signerUrl = `${config.publicOrigin}${config.publicPathPrefix}${config.basePath}/signer.${keyAsset.contentHash}.mjs`;
2584
+ const assetBasePath = config.basePath === "/" ? "" : config.basePath;
2585
+ const signerUrl = `${config.publicOrigin}${config.publicPathPrefix}${assetBasePath}/${keyAsset.fileName}`;
2611
2586
  const document = {
2612
2587
  version: PROTOCOL_VERSION,
2613
2588
  keyId: current.keyId,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/express-request-attestation",
3
3
  "description": "Versioned per-request HMAC signing and replay detection for Express APIs with a browser-safe signer entry",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/express-request-attestation",
5
- "version": "0.50.0",
5
+ "version": "0.52.0",
6
6
  "sideEffects": false,
7
7
  "keywords": [
8
8
  "express",