@blamejs/core 0.14.20 → 0.14.21
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/CHANGELOG.md +2 -0
- package/lib/auth/oid4vci.js +124 -5
- package/lib/auth/oid4vp.js +14 -4
- package/lib/break-glass.js +1 -2
- package/lib/config.js +28 -31
- package/lib/dora.js +8 -5
- package/lib/dsr.js +2 -2
- package/lib/flag-evaluation-context.js +7 -0
- package/lib/guard-html-wcag-aria.js +4 -2
- package/lib/guard-html-wcag-forms.js +4 -2
- package/lib/guard-html-wcag-tables.js +4 -2
- package/lib/guard-html-wcag-tagwalk.js +20 -0
- package/lib/guard-html-wcag.js +1 -1
- package/lib/honeytoken.js +27 -20
- package/lib/mail-deploy.js +1 -1
- package/lib/mail-send-deliver.js +13 -4
- package/lib/middleware/api-encrypt.js +140 -13
- package/lib/middleware/asyncapi-serve.js +3 -0
- package/lib/middleware/csp-report.js +13 -9
- package/lib/middleware/openapi-serve.js +3 -0
- package/lib/middleware/scim-server.js +297 -19
- package/lib/middleware/security-txt.js +1 -2
- package/lib/middleware/trace-log-correlation.js +1 -2
- package/lib/network-smtp-policy.js +4 -4
- package/lib/object-store/sigv4-bucket-ops.js +11 -2
- package/lib/observability-tracer.js +1 -1
- package/lib/problem-details.js +56 -11
- package/lib/pubsub-cluster.js +16 -3
- package/lib/queue-sqs.js +20 -2
- package/lib/redis-client.js +32 -4
- package/lib/safe-redirect.js +16 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-send-deliver.js
CHANGED
|
@@ -468,16 +468,25 @@ function create(opts) {
|
|
|
468
468
|
|
|
469
469
|
var retryOpts = opts.retry || {};
|
|
470
470
|
validateOpts(retryOpts, ["maxAttempts", "backoffMs"], "mail.send.deliver.create.retry");
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
// Config-time entry-point opts: a typo (maxAttempts:"5", mxLookupMs:-1)
|
|
472
|
+
// must fail at create(), not silently fall back to the default. Absent
|
|
473
|
+
// keeps the default; present-but-bad throws. Matches opts.port above.
|
|
474
|
+
validateOpts.optionalPositiveInt(retryOpts.maxAttempts,
|
|
475
|
+
"mail.send.deliver.create.retry.maxAttempts", DeliverError, "deliver/bad-retry-maxAttempts");
|
|
476
|
+
var maxAttempts = retryOpts.maxAttempts !== undefined
|
|
477
|
+
? retryOpts.maxAttempts : DEFAULT_RETRY_BACKOFF_MS.length;
|
|
473
478
|
var backoffMs = Array.isArray(retryOpts.backoffMs) && retryOpts.backoffMs.length > 0
|
|
474
479
|
? retryOpts.backoffMs.slice() : DEFAULT_RETRY_BACKOFF_MS.slice();
|
|
475
480
|
|
|
476
481
|
var timeouts = opts.timeouts || {};
|
|
477
482
|
validateOpts(timeouts, ["mxLookupMs", "perHostMs"], "mail.send.deliver.create.timeouts");
|
|
478
|
-
|
|
483
|
+
validateOpts.optionalPositiveInt(timeouts.mxLookupMs,
|
|
484
|
+
"mail.send.deliver.create.timeouts.mxLookupMs", DeliverError, "deliver/bad-timeout-mxLookupMs");
|
|
485
|
+
validateOpts.optionalPositiveInt(timeouts.perHostMs,
|
|
486
|
+
"mail.send.deliver.create.timeouts.perHostMs", DeliverError, "deliver/bad-timeout-perHostMs");
|
|
487
|
+
var mxLookupTimeoutMs = timeouts.mxLookupMs !== undefined
|
|
479
488
|
? timeouts.mxLookupMs : DEFAULT_MX_LOOKUP_TIMEOUT_MS;
|
|
480
|
-
var perHostTimeoutMs =
|
|
489
|
+
var perHostTimeoutMs = timeouts.perHostMs !== undefined
|
|
481
490
|
? timeouts.perHostMs : DEFAULT_PER_HOST_TIMEOUT_MS;
|
|
482
491
|
|
|
483
492
|
var dsnOpts = opts.dsn || null;
|
|
@@ -289,18 +289,28 @@ function create(opts) {
|
|
|
289
289
|
], "middleware.apiEncrypt");
|
|
290
290
|
var keypairs = _resolveKeypairs(opts);
|
|
291
291
|
var activeKeypair = keypairs[0];
|
|
292
|
+
// replayWindowMs gates the timestamp-staleness check (Math.abs(now - ts)
|
|
293
|
+
// > replayWindowMs). A non-numeric value would make that comparison
|
|
294
|
+
// always false and SILENTLY disable the staleness defense, so a typo
|
|
295
|
+
// throws at boot rather than shipping an open replay window.
|
|
296
|
+
validateOpts.optionalPositiveFinite(opts.replayWindowMs,
|
|
297
|
+
"apiEncrypt: replayWindowMs", ApiEncryptError, "BAD_OPT");
|
|
292
298
|
var replayWindowMs = opts.replayWindowMs || DEFAULT_REPLAY_WINDOW_MS;
|
|
293
299
|
// Cap on decrypted-payload size handed to safeJson.parse. Defaults
|
|
294
300
|
// to 4 MiB (bodyParser's default 1 MiB plus headroom for crypto +
|
|
295
301
|
// base64 round-trip). Operators with chunkier inbound payloads
|
|
296
302
|
// raise this; the framework refuses to parse anything larger as a
|
|
297
303
|
// parse-bomb defense.
|
|
304
|
+
validateOpts.optionalPositiveInt(opts.maxDecryptedBytes,
|
|
305
|
+
"apiEncrypt: maxDecryptedBytes", ApiEncryptError, "BAD_OPT");
|
|
298
306
|
var maxDecryptedBytes = opts.maxDecryptedBytes != null
|
|
299
307
|
? opts.maxDecryptedBytes
|
|
300
308
|
: C.BYTES.mib(4);
|
|
301
309
|
// The spec calls for a sweep cadence of replayWindowMs/2 — short
|
|
302
310
|
// enough that expired nonces don't pile up but not so frequent the
|
|
303
311
|
// sweep query becomes a hot path. Operators can override.
|
|
312
|
+
validateOpts.optionalPositiveFinite(opts.pruneIntervalMs,
|
|
313
|
+
"apiEncrypt: pruneIntervalMs", ApiEncryptError, "BAD_OPT");
|
|
304
314
|
var pruneIntervalMs = opts.pruneIntervalMs != null
|
|
305
315
|
? opts.pruneIntervalMs : Math.max(C.TIME.seconds(30), Math.floor(replayWindowMs / 2));
|
|
306
316
|
var nonceStore = opts.nonceStore || nonceStoreLib.create({ backend: "memory" });
|
|
@@ -446,7 +456,11 @@ function create(opts) {
|
|
|
446
456
|
// replayed responses with a monotonic counter check.
|
|
447
457
|
function _encodeEnvelope(data, sessionKey, sessionCtx) {
|
|
448
458
|
var ptBuf = Buffer.from(JSON.stringify(data), "utf8");
|
|
449
|
-
|
|
459
|
+
// Response AAD binds _sid/_ctr so a captured response cannot be
|
|
460
|
+
// replayed to the client under a rewritten counter.
|
|
461
|
+
var ctBuf = bCrypto.encryptPacked(ptBuf, sessionKey,
|
|
462
|
+
_responseAad(sessionCtx ? sessionCtx.sid : undefined,
|
|
463
|
+
sessionCtx ? sessionCtx.responseCtr : undefined));
|
|
450
464
|
var encrypted = { _ct: ctBuf.toString("base64") };
|
|
451
465
|
if (sessionCtx) {
|
|
452
466
|
encrypted._sid = sessionCtx.sid;
|
|
@@ -527,6 +541,10 @@ function create(opts) {
|
|
|
527
541
|
|
|
528
542
|
if (typeof ek === "string" && typeof nonce === "string") {
|
|
529
543
|
// ---- Bootstrap path (per-request mode OR first request of session) ----
|
|
544
|
+
// The window-scoped claim TTL is sufficient HERE because _ts is
|
|
545
|
+
// AEAD-bound into _ct: a captured bootstrap envelope cannot have
|
|
546
|
+
// its timestamp rewritten, so past replayWindowMs the staleness
|
|
547
|
+
// gate above refuses it independently of this claim.
|
|
530
548
|
var nonceHash = bCrypto.sha3Hash(nonce, "hex");
|
|
531
549
|
var expireAt = now + replayWindowMs;
|
|
532
550
|
var freshNonce;
|
|
@@ -553,11 +571,18 @@ function create(opts) {
|
|
|
553
571
|
_emitFailure(req, "shape");
|
|
554
572
|
return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-required" });
|
|
555
573
|
}
|
|
556
|
-
// Bootstrap a new session row keyed by sid.
|
|
574
|
+
// Bootstrap a new session row keyed by sid. responsesEmitted is
|
|
575
|
+
// set to 1 (this bootstrap emits one response) BEFORE the store
|
|
576
|
+
// write so a cluster store — which serialises a copy at set() time
|
|
577
|
+
// rather than holding a live reference — persists the same count
|
|
578
|
+
// the next subsequent request reads. Mutating the local object
|
|
579
|
+
// after set() would leave the stored row at 0, making the next
|
|
580
|
+
// response counter restart from 1 (a non-monotonic response _ctr
|
|
581
|
+
// that trips the client's strictly-increasing replay check).
|
|
557
582
|
session = {
|
|
558
583
|
sessionKey: sessionKey,
|
|
559
584
|
lastReqCtr: ctr,
|
|
560
|
-
responsesEmitted:
|
|
585
|
+
responsesEmitted: 1,
|
|
561
586
|
createdAt: now,
|
|
562
587
|
lastUsedAt: now,
|
|
563
588
|
expiresAt: now + sessionTtlMs,
|
|
@@ -574,7 +599,6 @@ function create(opts) {
|
|
|
574
599
|
requestId: req.requestId || null,
|
|
575
600
|
});
|
|
576
601
|
sessionCtx = { sid: sid, responseCtr: 1 };
|
|
577
|
-
session.responsesEmitted = 1;
|
|
578
602
|
}
|
|
579
603
|
} else if (keying === "per-session" &&
|
|
580
604
|
typeof sid === "string" && typeof ctr === "number") {
|
|
@@ -633,6 +657,47 @@ function create(opts) {
|
|
|
633
657
|
_emitFailure(req, "counter-replay");
|
|
634
658
|
return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-rejected" });
|
|
635
659
|
}
|
|
660
|
+
// Atomic replay gate (CWE-367). The monotonic counter check above is
|
|
661
|
+
// an ordering fast-path only: on a clustered session store, get(sid)
|
|
662
|
+
// returns a fresh deserialised copy per call, so two concurrent
|
|
663
|
+
// requests carrying the SAME valid ctr both observe the same
|
|
664
|
+
// lastReqCtr and both pass — double-execution replay. Claiming the
|
|
665
|
+
// (sid, ctr) tuple through the same atomic nonceStore the bootstrap
|
|
666
|
+
// path uses closes the window: exactly one concurrent request wins the
|
|
667
|
+
// insert; the loser is refused with the counter-replay shape. The
|
|
668
|
+
// "ctr:" prefix keeps this keyspace disjoint from the bootstrap
|
|
669
|
+
// nonceHash keyspace (a sha3 hex digest never starts with "ctr:").
|
|
670
|
+
// The claim must outlive the staleness window, not just span it:
|
|
671
|
+
// the post-handler sessionStore.set below is best-effort, so a
|
|
672
|
+
// failed write leaves lastReqCtr stale in the store, and _ts is
|
|
673
|
+
// plaintext envelope metadata (not bound into the AEAD) — were the
|
|
674
|
+
// claim to expire after replayWindowMs, the same captured
|
|
675
|
+
// (sid, ctr, _ct) could be replayed later with a fresh _ts, pass
|
|
676
|
+
// the stale monotonic check, re-claim the expired tuple, and
|
|
677
|
+
// execute twice. Claiming until session.expiresAt (the session is
|
|
678
|
+
// non-expired here, so that bound is in the future) keeps the
|
|
679
|
+
// tuple burned for as long as the session can accept requests;
|
|
680
|
+
// outstanding claims per session are bounded by
|
|
681
|
+
// sessionMaxResponses, and the memory nonce store fails closed at
|
|
682
|
+
// capacity.
|
|
683
|
+
var ctrKey = "ctr:" + sid + ":" + ctr;
|
|
684
|
+
var ctrFresh;
|
|
685
|
+
try { ctrFresh = await nonceStore.checkAndInsert(ctrKey, session.expiresAt); }
|
|
686
|
+
catch (_e) {
|
|
687
|
+
_emitFailure(req, "nonce-store-error");
|
|
688
|
+
return _writeRejection(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, { error: "nonce-store-unavailable" });
|
|
689
|
+
}
|
|
690
|
+
if (!ctrFresh) {
|
|
691
|
+
_emitObs("apiEncrypt.session.replay_rejected", 1, { lane: "atomic" });
|
|
692
|
+
_emitSessionAudit("apiEncrypt.session.replay_rejected", {
|
|
693
|
+
outcome: "denied",
|
|
694
|
+
actor: requestHelpers.extractActorContext(req),
|
|
695
|
+
metadata: { sid: sid, receivedCtr: ctr, lane: "atomic" },
|
|
696
|
+
requestId: req.requestId || null,
|
|
697
|
+
});
|
|
698
|
+
_emitFailure(req, "counter-replay");
|
|
699
|
+
return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-rejected" });
|
|
700
|
+
}
|
|
636
701
|
sessionKey = session.sessionKey;
|
|
637
702
|
if (Buffer.isBuffer(sessionKey) === false) {
|
|
638
703
|
// Operator-supplied store may have JSON-serialised the buffer.
|
|
@@ -660,11 +725,15 @@ function create(opts) {
|
|
|
660
725
|
return _writeRejection(res, HTTP_STATUS.BAD_REQUEST, { error: "encrypted-payload-required" });
|
|
661
726
|
}
|
|
662
727
|
|
|
663
|
-
// Decrypt _ct → cleartext payload bytes → JSON object.
|
|
728
|
+
// Decrypt _ct → cleartext payload bytes → JSON object. The request
|
|
729
|
+
// AAD authenticates the plaintext envelope fields exactly as the
|
|
730
|
+
// client bound them — a rewritten _ts/_nonce/_sid/_ctr fails the
|
|
731
|
+
// AEAD tag here, so the staleness gate above operates on a
|
|
732
|
+
// timestamp the sender cannot forge after capture.
|
|
664
733
|
var clearObj;
|
|
665
734
|
try {
|
|
666
735
|
var ctBuf = Buffer.from(ct, "base64");
|
|
667
|
-
var ptBuf = bCrypto.decryptPacked(ctBuf, sessionKey);
|
|
736
|
+
var ptBuf = bCrypto.decryptPacked(ctBuf, sessionKey, _requestAad(ts, nonce, sid, ctr));
|
|
668
737
|
clearObj = safeJson.parse(ptBuf.toString("utf8"), { maxBytes: maxDecryptedBytes });
|
|
669
738
|
} catch (_e) {
|
|
670
739
|
_emitFailure(req, "tag");
|
|
@@ -740,6 +809,8 @@ function client(opts) {
|
|
|
740
809
|
"apiEncrypt.client: pubkey.publicKey + ecPublicKey must be PEM strings", 500);
|
|
741
810
|
}
|
|
742
811
|
var pubkey = opts.pubkey;
|
|
812
|
+
validateOpts.optionalPositiveInt(opts.maxDecryptedBytes,
|
|
813
|
+
"apiEncrypt.client: maxDecryptedBytes", ApiEncryptError, "CLIENT_BAD_OPT");
|
|
743
814
|
var maxDecryptedBytes = opts.maxDecryptedBytes != null
|
|
744
815
|
? opts.maxDecryptedBytes
|
|
745
816
|
: C.BYTES.mib(4);
|
|
@@ -785,9 +856,23 @@ function client(opts) {
|
|
|
785
856
|
"apiEncrypt.client: response counter is not strictly increasing " +
|
|
786
857
|
"(got " + responseBody._ctr + ", lastSeen " + perSessionLastResCtr + ")");
|
|
787
858
|
}
|
|
788
|
-
perSessionLastResCtr = responseBody._ctr;
|
|
789
859
|
var resCtBuf = Buffer.from(responseBody._ct, "base64");
|
|
790
|
-
|
|
860
|
+
// Response AAD authenticates _sid/_ctr — the monotonic counter
|
|
861
|
+
// check above reads plaintext fields, so without this binding a
|
|
862
|
+
// captured response could be replayed under a bumped _ctr.
|
|
863
|
+
var resPtBuf;
|
|
864
|
+
try {
|
|
865
|
+
resPtBuf = bCrypto.decryptPacked(resCtBuf, perSessionKey,
|
|
866
|
+
_responseAad(responseBody._sid, responseBody._ctr));
|
|
867
|
+
} catch (_e) {
|
|
868
|
+
throw _err("CLIENT_RESPONSE_TAMPERED",
|
|
869
|
+
"apiEncrypt.client: response failed authenticated decryption (ciphertext or envelope metadata tampered)");
|
|
870
|
+
}
|
|
871
|
+
// Advance the counter only AFTER authenticated decryption — were it
|
|
872
|
+
// committed before, a forged high _ctr (which fails the AEAD above)
|
|
873
|
+
// would poison the monotonic check and refuse every subsequent
|
|
874
|
+
// genuine response for the rest of the session.
|
|
875
|
+
perSessionLastResCtr = responseBody._ctr;
|
|
791
876
|
return safeJson.parse(resPtBuf.toString("utf8"), { maxBytes: maxDecryptedBytes });
|
|
792
877
|
}
|
|
793
878
|
|
|
@@ -795,14 +880,18 @@ function client(opts) {
|
|
|
795
880
|
if (payload === undefined) payload = null;
|
|
796
881
|
if (!perSessionKey) _resetSession();
|
|
797
882
|
var ts = Date.now();
|
|
798
|
-
var ptBuf = Buffer.from(JSON.stringify(payload), "utf8");
|
|
799
|
-
var ctBuf = bCrypto.encryptPacked(ptBuf, perSessionKey);
|
|
800
883
|
perSessionReqCtr += 1;
|
|
884
|
+
var ptBuf = Buffer.from(JSON.stringify(payload), "utf8");
|
|
885
|
+
var ctBuf;
|
|
801
886
|
var body;
|
|
802
887
|
if (perSessionReqCtr === 1) {
|
|
803
888
|
// Bootstrap envelope — full _ek + _nonce; server stores sid → sessionKey.
|
|
889
|
+
// The plaintext metadata is AEAD-bound so a captured envelope
|
|
890
|
+
// cannot be replayed under a rewritten _ts/_nonce/_sid/_ctr.
|
|
804
891
|
var ek = bCrypto.encrypt(perSessionKey.toString("base64"), pubkey);
|
|
805
892
|
var nonce = bCrypto.generateBytes(REQUEST_NONCE_BYTES).toString("hex");
|
|
893
|
+
ctBuf = bCrypto.encryptPacked(ptBuf, perSessionKey,
|
|
894
|
+
_requestAad(ts, nonce, perSessionSid, perSessionReqCtr));
|
|
806
895
|
body = {
|
|
807
896
|
_ek: ek,
|
|
808
897
|
_ct: ctBuf.toString("base64"),
|
|
@@ -813,6 +902,8 @@ function client(opts) {
|
|
|
813
902
|
};
|
|
814
903
|
} else {
|
|
815
904
|
// Subsequent — sid + ctr only. KEM material amortized across the session.
|
|
905
|
+
ctBuf = bCrypto.encryptPacked(ptBuf, perSessionKey,
|
|
906
|
+
_requestAad(ts, undefined, perSessionSid, perSessionReqCtr));
|
|
816
907
|
body = {
|
|
817
908
|
_ct: ctBuf.toString("base64"),
|
|
818
909
|
_ts: ts,
|
|
@@ -827,10 +918,13 @@ function client(opts) {
|
|
|
827
918
|
if (payload === undefined) payload = null;
|
|
828
919
|
var sessionKey = bCrypto.generateBytes(SESSION_KEY_BYTES);
|
|
829
920
|
var ek = bCrypto.encrypt(sessionKey.toString("base64"), pubkey);
|
|
830
|
-
var ptBuf = Buffer.from(JSON.stringify(payload), "utf8");
|
|
831
|
-
var ctBuf = bCrypto.encryptPacked(ptBuf, sessionKey);
|
|
832
921
|
var requestNonce = bCrypto.generateBytes(REQUEST_NONCE_BYTES).toString("hex");
|
|
833
922
|
var ts = Date.now();
|
|
923
|
+
var ptBuf = Buffer.from(JSON.stringify(payload), "utf8");
|
|
924
|
+
// AEAD-bind _ts/_nonce so a captured per-request envelope cannot
|
|
925
|
+
// be replayed past the staleness window with a rewritten _ts.
|
|
926
|
+
var ctBuf = bCrypto.encryptPacked(ptBuf, sessionKey,
|
|
927
|
+
_requestAad(ts, requestNonce, undefined, undefined));
|
|
834
928
|
return {
|
|
835
929
|
body: {
|
|
836
930
|
_ek: ek,
|
|
@@ -845,7 +939,14 @@ function client(opts) {
|
|
|
845
939
|
"apiEncrypt.client: response missing _ct field");
|
|
846
940
|
}
|
|
847
941
|
var resCtBuf = Buffer.from(responseBody._ct, "base64");
|
|
848
|
-
var resPtBuf
|
|
942
|
+
var resPtBuf;
|
|
943
|
+
try {
|
|
944
|
+
resPtBuf = bCrypto.decryptPacked(resCtBuf, sessionKey,
|
|
945
|
+
_responseAad(undefined, undefined));
|
|
946
|
+
} catch (_e) {
|
|
947
|
+
throw _err("CLIENT_RESPONSE_TAMPERED",
|
|
948
|
+
"apiEncrypt.client: response failed authenticated decryption (ciphertext or envelope metadata tampered)");
|
|
949
|
+
}
|
|
849
950
|
return safeJson.parse(resPtBuf.toString("utf8"), { maxBytes: maxDecryptedBytes });
|
|
850
951
|
},
|
|
851
952
|
};
|
|
@@ -865,6 +966,30 @@ function client(opts) {
|
|
|
865
966
|
};
|
|
866
967
|
}
|
|
867
968
|
|
|
969
|
+
// AEAD associated-data builders — bind the envelope's PLAINTEXT
|
|
970
|
+
// metadata into the ciphertext so a captured envelope cannot be
|
|
971
|
+
// replayed with rewritten fields. `_ts` drives the staleness gate,
|
|
972
|
+
// `_nonce` the bootstrap replay claim, `_sid`/`_ctr` the session
|
|
973
|
+
// replay gates on requests and the client's monotonic counter check
|
|
974
|
+
// on responses; none are confidential, but every one is
|
|
975
|
+
// integrity-critical — rode plaintext, an attacker who captured an
|
|
976
|
+
// envelope could refresh `_ts` past the staleness window or replay a
|
|
977
|
+
// response under a bumped `_ctr`. Both halves of the protocol
|
|
978
|
+
// (middleware + client) live in this module and MUST build
|
|
979
|
+
// byte-identical strings; absent fields encode as the empty string so
|
|
980
|
+
// the per-request and per-session shapes stay unambiguous.
|
|
981
|
+
function _requestAad(ts, nonce, sid, ctr) {
|
|
982
|
+
return "blamejs-apienc/req/1|ts=" + String(ts) +
|
|
983
|
+
"|nonce=" + (typeof nonce === "string" ? nonce : "") +
|
|
984
|
+
"|sid=" + (typeof sid === "string" ? sid : "") +
|
|
985
|
+
"|ctr=" + (typeof ctr === "number" ? String(ctr) : "");
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function _responseAad(sid, ctr) {
|
|
989
|
+
return "blamejs-apienc/res/1|sid=" + (typeof sid === "string" ? sid : "") +
|
|
990
|
+
"|ctr=" + (typeof ctr === "number" ? String(ctr) : "");
|
|
991
|
+
}
|
|
992
|
+
|
|
868
993
|
// _generateUuidV4 — UUID v4 from 16 random bytes, formatted dash-separated.
|
|
869
994
|
// Used for client-side session-id generation in per-session keying.
|
|
870
995
|
// Slice offsets are RFC 4122 UUID hex-byte boundaries (`xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx`)
|
|
@@ -914,6 +1039,8 @@ function httpClientEncrypted(opts) {
|
|
|
914
1039
|
throw _err("CLIENT_INVALID_PUBKEY",
|
|
915
1040
|
"httpClient.encrypted: opts.pubkey is required (the callee's bootstrap doc)", 500);
|
|
916
1041
|
}
|
|
1042
|
+
validateOpts.optionalPositiveInt(opts.maxDecryptedBytes,
|
|
1043
|
+
"httpClient.encrypted: maxDecryptedBytes", ApiEncryptError, "CLIENT_BAD_OPT");
|
|
917
1044
|
var maxDecryptedBytes = opts.maxDecryptedBytes != null
|
|
918
1045
|
? opts.maxDecryptedBytes
|
|
919
1046
|
: C.BYTES.mib(4);
|
|
@@ -173,6 +173,9 @@ function create(opts) {
|
|
|
173
173
|
if (allowOriginHeader !== "*") headers["Vary"] = "Origin";
|
|
174
174
|
}
|
|
175
175
|
res.writeHead(200, headers); // HTTP 200
|
|
176
|
+
// HEAD carries the GET headers (incl. Content-Length) with no body
|
|
177
|
+
// (RFC 9110 §9.3.2).
|
|
178
|
+
if ((req.method || "GET").toUpperCase() === "HEAD") { res.end(); return; }
|
|
176
179
|
res.end(body);
|
|
177
180
|
}
|
|
178
181
|
|
|
@@ -135,8 +135,10 @@ function create(opts) {
|
|
|
135
135
|
typeof opts.onReject !== "function") {
|
|
136
136
|
throw new TypeError("middleware.cspReport: opts.onReject must be a function");
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
validateOpts.optionalPositiveInt(opts.maxBytes, "middleware.cspReport: maxBytes");
|
|
139
|
+
var maxBytes = (opts.maxBytes === undefined || opts.maxBytes === null)
|
|
140
|
+
? DEFAULT_MAX_BYTES : opts.maxBytes;
|
|
141
|
+
var auditOn = opts.audit !== false;
|
|
140
142
|
var onReport = (typeof opts.onReport === "function") ? opts.onReport : null;
|
|
141
143
|
var onReject = (typeof opts.onReject === "function") ? opts.onReject : null;
|
|
142
144
|
|
|
@@ -176,13 +178,15 @@ function create(opts) {
|
|
|
176
178
|
for (var i = 0; i < reports.length; i++) {
|
|
177
179
|
var normalized = _normalizeOne(reports[i]);
|
|
178
180
|
if (!normalized) continue;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
if (auditOn) {
|
|
182
|
+
try {
|
|
183
|
+
audit().safeEmit({
|
|
184
|
+
action: "csp.violation",
|
|
185
|
+
outcome: "failure",
|
|
186
|
+
metadata: Object.assign({ type: normalized.type, url: normalized.url }, normalized.body),
|
|
187
|
+
});
|
|
188
|
+
} catch (_e) { /* audit best-effort */ }
|
|
189
|
+
}
|
|
186
190
|
if (onReport) {
|
|
187
191
|
try { onReport(normalized); } catch (_e) { /* hook best-effort */ }
|
|
188
192
|
}
|
|
@@ -179,6 +179,9 @@ function create(opts) {
|
|
|
179
179
|
if (allowOriginHeader !== "*") headers["Vary"] = "Origin";
|
|
180
180
|
}
|
|
181
181
|
res.writeHead(200, headers); // HTTP 200
|
|
182
|
+
// HEAD carries the GET headers (incl. Content-Length) with no body
|
|
183
|
+
// (RFC 9110 §9.3.2).
|
|
184
|
+
if ((req.method || "GET").toUpperCase() === "HEAD") { res.end(); return; }
|
|
182
185
|
res.end(body);
|
|
183
186
|
}
|
|
184
187
|
|