@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/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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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.
|
|
5
|
+
"version": "0.53.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
8
8
|
"express",
|