@blamejs/core 0.18.53 → 0.18.55
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 +228 -0
- package/NOTICE +1 -1
- package/README.md +5 -5
- package/lib/agent-audit.js +27 -2
- package/lib/ai-adverse-decision.js +18 -2
- package/lib/audit-sign.js +24 -5
- package/lib/auth/passkey.js +4 -1
- package/lib/codepoint-class.js +72 -0
- package/lib/cookies.js +7 -10
- package/lib/credential-hash.js +8 -1
- package/lib/crypto.js +7 -5
- package/lib/db-file-lifecycle.js +14 -3
- package/lib/db.js +505 -49
- package/lib/guard-auth.js +34 -11
- package/lib/guard-filename.js +41 -33
- package/lib/guard-html.js +10 -2
- package/lib/guard-list-unsubscribe.js +6 -1
- package/lib/guard-managesieve-command.js +73 -12
- package/lib/guard-regex.js +3 -5
- package/lib/guard-smtp-command.js +20 -4
- package/lib/guard-svg.js +6 -1
- package/lib/guard-yaml.js +60 -15
- package/lib/http-client.js +17 -3
- package/lib/mail-agent.js +29 -13
- package/lib/mail-arc-sign.js +40 -7
- package/lib/mail-auth.js +134 -22
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-dkim.js +80 -11
- package/lib/mail-helo.js +10 -0
- package/lib/mail-rbl.js +10 -3
- package/lib/mail-send-deliver.js +151 -32
- package/lib/mail-server-imap.js +186 -89
- package/lib/mail-server-jmap.js +31 -4
- package/lib/mail-server-managesieve.js +198 -42
- package/lib/mail-server-mx.js +191 -38
- package/lib/mail-server-net.js +281 -1
- package/lib/mail-server-pop3.js +89 -41
- package/lib/mail-server-rate-limit.js +104 -6
- package/lib/mail-server-submission.js +183 -35
- package/lib/mail-server-tls.js +48 -3
- package/lib/mail-store.js +33 -11
- package/lib/mail.js +355 -17
- package/lib/mcp.js +11 -3
- package/lib/middleware/bearer-auth.js +6 -1
- package/lib/middleware/fetch-metadata.js +5 -1
- package/lib/middleware/headers.js +7 -10
- package/lib/middleware/require-mtls.js +8 -1
- package/lib/network-dns-resolver.js +71 -8
- package/lib/network-dns.js +26 -0
- package/lib/network-smtp-policy.js +42 -10
- package/lib/network-tls.js +18 -0
- package/lib/redact.js +13 -3
- package/lib/retention.js +22 -2
- package/lib/safe-mount-info.js +39 -6
- package/lib/safe-smtp.js +96 -1
- package/lib/safe-url.js +8 -2
- package/lib/self-update.js +4 -1
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +672 -75
- package/lib/watcher.js +31 -6
- package/lib/ws-client.js +17 -2
- package/lib/yaml-lex.js +55 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-store.js
CHANGED
|
@@ -484,12 +484,7 @@ function create(opts) {
|
|
|
484
484
|
maxMessageBytes: maxMessageBytes,
|
|
485
485
|
maxBodyBytes: maxBodyBytes,
|
|
486
486
|
};
|
|
487
|
-
|
|
488
|
-
var result;
|
|
489
|
-
db.transaction(function () { result = _appendMessage(args); })();
|
|
490
|
-
return result;
|
|
491
|
-
}
|
|
492
|
-
return _appendMessage(args);
|
|
487
|
+
return _runInTransaction(db, function () { return _appendMessage(args); });
|
|
493
488
|
},
|
|
494
489
|
fetchByObjectId: function (folderName, objectid) {
|
|
495
490
|
return _fetchByObjectId({
|
|
@@ -782,11 +777,7 @@ function create(opts) {
|
|
|
782
777
|
stmtDecrementQuota.run(totalBytes, toDelete.length, folder.id);
|
|
783
778
|
}
|
|
784
779
|
}
|
|
785
|
-
|
|
786
|
-
db.transaction(_runTxn)();
|
|
787
|
-
} else {
|
|
788
|
-
_runTxn();
|
|
789
|
-
}
|
|
780
|
+
_runInTransaction(db, _runTxn);
|
|
790
781
|
return {
|
|
791
782
|
rows: rows,
|
|
792
783
|
deleted: toDelete.map(function (r) { return r.objectid; }),
|
|
@@ -800,6 +791,37 @@ function create(opts) {
|
|
|
800
791
|
|
|
801
792
|
// ---- Append --------------------------------------------------------------
|
|
802
793
|
|
|
794
|
+
// Run `fn` inside the backend's transaction, whichever calling convention the
|
|
795
|
+
// backend uses, and return what `fn` returned.
|
|
796
|
+
//
|
|
797
|
+
// There are two, and they look identical from outside. `b.db.transaction(fn)`
|
|
798
|
+
// RUNS fn between BEGIN and COMMIT and returns its result; better-sqlite3's
|
|
799
|
+
// `transaction(fn)` returns a WRAPPER you then call. The code here was written
|
|
800
|
+
// for the second and used against the first, which is the composition
|
|
801
|
+
// `b.mailStore` documents: `db.transaction(fn)()` therefore ran the work,
|
|
802
|
+
// committed it, and then threw a TypeError trying to call the result. A caller
|
|
803
|
+
// retrying on that throw appended a second copy of a message already durably
|
|
804
|
+
// stored, and `hardExpunge` deleted, committed, and threw the same way.
|
|
805
|
+
//
|
|
806
|
+
// `typeof db.transaction === "function"` is true of both and distinguishes
|
|
807
|
+
// neither, which is why the guard that was there could not have caught it. What
|
|
808
|
+
// distinguishes them is whether `fn` actually RAN, so that is what is checked —
|
|
809
|
+
// not the shape of the return value, which would misread a callback that
|
|
810
|
+
// legitimately returns a function.
|
|
811
|
+
function _runInTransaction(db, fn) {
|
|
812
|
+
if (!db || typeof db.transaction !== "function") return fn();
|
|
813
|
+
var ran = false;
|
|
814
|
+
var out = db.transaction(function () { ran = true; return fn(); });
|
|
815
|
+
if (ran) return out; // executed already: b.db's shape
|
|
816
|
+
if (typeof out === "function") return out(); // a wrapper: better-sqlite3's
|
|
817
|
+
// Neither ran the callback nor handed back something to run. Doing the work
|
|
818
|
+
// outside a transaction would be worse than saying so: a partial write is
|
|
819
|
+
// exactly what the transaction is here to prevent.
|
|
820
|
+
throw new MailStoreError("mail-store/unusable-transaction",
|
|
821
|
+
"backend.transaction(fn) neither ran the callback nor returned a function " +
|
|
822
|
+
"to run it — the write cannot be made atomic, so it is not attempted");
|
|
823
|
+
}
|
|
824
|
+
|
|
803
825
|
function _appendMessage(args) {
|
|
804
826
|
var rawBytes = args.rawBytes;
|
|
805
827
|
if (!Buffer.isBuffer(rawBytes) && typeof rawBytes !== "string") {
|
package/lib/mail.js
CHANGED
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
* the worked example). Operators can also pass any function or
|
|
26
26
|
* `{ send }` object as a custom transport.
|
|
27
27
|
*
|
|
28
|
+
* The `smtp` transport takes `dane: [tlsaRecord, ...]` — the peer's
|
|
29
|
+
* DNSSEC-validated TLSA records, as `b.network.smtp.policy.dane.tlsa`
|
|
30
|
+
* returns them. When present, the certificate chain the peer presents is
|
|
31
|
+
* matched against them once the handshake completes and the send is refused
|
|
32
|
+
* on a mismatch (RFC 7672 §2.2), on both the implicit-TLS and STARTTLS
|
|
33
|
+
* paths. `daneAllowPkixModes: true` additionally accepts the PKIX-TA /
|
|
34
|
+
* PKIX-EE usages, which need a PKIX path validation the transport does not
|
|
35
|
+
* perform. `b.mail.send.deliver` fetches the records and passes them here.
|
|
36
|
+
*
|
|
28
37
|
* DKIM-Signature header generation lives at `b.mail.dkim` (rsa-sha256
|
|
29
38
|
* default, ed25519-sha256 opt-in, dual-signer per RFC 8463 §3 for
|
|
30
39
|
* transition windows). Inbound authentication-results parsing —
|
|
@@ -82,6 +91,8 @@ var numericBounds = require("./numeric-bounds");
|
|
|
82
91
|
var nodeTls = lazyRequire(function () { return require("node:tls"); });
|
|
83
92
|
// Lazy — audit a cert-validation-disabled SMTP/TLS session at honor time.
|
|
84
93
|
var networkTls = lazyRequire(function () { return require("./network-tls"); });
|
|
94
|
+
// Lazy — dane.verifyChain, reached only when a send carries TLSA records.
|
|
95
|
+
var smtpPolicy = lazyRequire(function () { return require("./network-smtp-policy"); });
|
|
85
96
|
var safeJson = require("./safe-json");
|
|
86
97
|
var safeSchema = require("./safe-schema");
|
|
87
98
|
var validateOpts = require("./validate-opts");
|
|
@@ -310,6 +321,12 @@ function _isValidEmail(addr) {
|
|
|
310
321
|
// contains non-ASCII octets.
|
|
311
322
|
function _messageRequiresSmtpUtf8(message) {
|
|
312
323
|
if (!message) return false;
|
|
324
|
+
// For raw bytes the addresses live in the header block, and SMTPUTF8 is about
|
|
325
|
+
// ADDRESSES — a non-ASCII body is 8BITMIME's business, below. The envelope
|
|
326
|
+
// fields are still checked after this, because `deliver` passes them
|
|
327
|
+
// alongside the raw message and either can carry the non-ASCII address.
|
|
328
|
+
var rawUtf8 = _rawText(message);
|
|
329
|
+
if (rawUtf8 !== null && _hasNonAscii(_rawHeaderBlock(rawUtf8))) return true;
|
|
313
330
|
if (_hasNonAscii(message.from || "")) return true;
|
|
314
331
|
if (_hasNonAscii(message.subject || "")) return true;
|
|
315
332
|
var lists = [message.to, message.cc, message.bcc];
|
|
@@ -746,7 +763,63 @@ function _buildBodyPart(message) {
|
|
|
746
763
|
};
|
|
747
764
|
}
|
|
748
765
|
|
|
766
|
+
// A message may arrive as FIELDS or as raw RFC 822 BYTES, and everything on the
|
|
767
|
+
// send path that reads the message has to know which one it is looking at.
|
|
768
|
+
//
|
|
769
|
+
// `b.mail.send.deliver` requires its caller to supply raw bytes, validates them,
|
|
770
|
+
// and hands them over as `raw` — and every reader here ignored that and read the
|
|
771
|
+
// fields instead. A message carrying only `from`, `to` and `raw` therefore had
|
|
772
|
+
// no body parts at all, so the wire body was an empty `multipart/alternative`
|
|
773
|
+
// and the peer accepted it with a 250. Fixing only the body builder is not
|
|
774
|
+
// enough either: the three capability detectors below decide SMTPUTF8, 8BITMIME
|
|
775
|
+
// and BINARYMIME from the same fields, so a raw message would have gone out with
|
|
776
|
+
// all three un-negotiated — a body that is no longer empty but is silently
|
|
777
|
+
// mangled by a peer that needed the hint, which is the worse failure because it
|
|
778
|
+
// looks like it worked.
|
|
779
|
+
// latin1, not utf8. The rest of this file inspects the message as a string
|
|
780
|
+
// (guardEmail's smuggling screen, the DKIM signer, the dot-stuffer), and a
|
|
781
|
+
// utf8 decode of arbitrary 8-bit bytes replaces every invalid sequence with
|
|
782
|
+
// U+FFFD — measured at 21 bytes in, 29 out, four of them destroyed. That
|
|
783
|
+
// corrupts exactly the BINARYMIME payloads this transport now detects and
|
|
784
|
+
// negotiates for, which is the worst case: the send succeeds and the body is
|
|
785
|
+
// wrong. latin1 maps each byte to one codepoint in U+0000..U+00FF and back,
|
|
786
|
+
// so the string form is a faithful stand-in for the bytes and `_wireEncoding`
|
|
787
|
+
// below recovers them exactly.
|
|
788
|
+
function _rawText(message) {
|
|
789
|
+
if (!message) return null;
|
|
790
|
+
var raw = message.raw;
|
|
791
|
+
if (Buffer.isBuffer(raw)) return raw.toString("latin1");
|
|
792
|
+
if (typeof raw === "string") return raw;
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Which encoding turns this message's string form back into its wire bytes.
|
|
797
|
+
// A raw Buffer went through latin1 above and must come back the same way; a
|
|
798
|
+
// message this transport composed itself is real text and is utf8.
|
|
799
|
+
function _wireEncoding(message) {
|
|
800
|
+
return (message && Buffer.isBuffer(message.raw)) ? "latin1" : "utf8";
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// The header block of raw bytes: everything before the first blank line. Used
|
|
804
|
+
// where a decision is about the envelope and headers rather than the content —
|
|
805
|
+
// SMTPUTF8 is about addresses, not about the body.
|
|
806
|
+
function _rawHeaderBlock(raw) {
|
|
807
|
+
var end = raw.indexOf("\r\n\r\n");
|
|
808
|
+
if (end === -1) end = raw.indexOf("\n\n");
|
|
809
|
+
return end === -1 ? raw : raw.slice(0, end);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Built from its code point, never typed: a literal NUL in this source would be
|
|
813
|
+
// an invisible byte in the very check that exists to find one.
|
|
814
|
+
var _NUL_CHAR = String.fromCharCode(0);
|
|
815
|
+
|
|
749
816
|
function _buildRfc822(message) {
|
|
817
|
+
// Raw bytes ARE the message. Nothing is composed and nothing is re-ordered: a
|
|
818
|
+
// caller that went to the trouble of building RFC 822 itself is entitled to
|
|
819
|
+
// have those exact bytes reach the peer.
|
|
820
|
+
var rawMessage = _rawText(message);
|
|
821
|
+
if (rawMessage !== null) return rawMessage;
|
|
822
|
+
|
|
750
823
|
var headers = [];
|
|
751
824
|
headers.push("From: " + message.from);
|
|
752
825
|
headers.push("To: " + (Array.isArray(message.to) ? message.to.join(", ") : message.to));
|
|
@@ -826,6 +899,52 @@ function _smtpTlsOpts(cfg, extra) {
|
|
|
826
899
|
return networkTls()._stripUnreachableCertCompression(out, cfg.tlsOpts);
|
|
827
900
|
}
|
|
828
901
|
|
|
902
|
+
// Node hands the peer chain back as a linked structure, each cert pointing at
|
|
903
|
+
// its issuer; dane.verifyChain wants the DER buffers in order, leaf first. A
|
|
904
|
+
// self-signed root points at itself, so the walk stops on a cert it has already
|
|
905
|
+
// taken rather than following that link forever.
|
|
906
|
+
function _peerChainDer(sock) {
|
|
907
|
+
var chain = [];
|
|
908
|
+
var seen = Object.create(null);
|
|
909
|
+
var cert;
|
|
910
|
+
try { cert = sock.getPeerCertificate(true); } catch (_e) { return chain; } // not a TLS socket, or torn down mid-handshake
|
|
911
|
+
while (cert && Buffer.isBuffer(cert.raw)) {
|
|
912
|
+
var id = cert.fingerprint256 || cert.raw.toString("hex");
|
|
913
|
+
if (seen[id]) break;
|
|
914
|
+
seen[id] = true;
|
|
915
|
+
chain.push(cert.raw);
|
|
916
|
+
cert = cert.issuerCertificate;
|
|
917
|
+
}
|
|
918
|
+
return chain;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Match the peer's certificate chain against the TLSA records published for it
|
|
922
|
+
// (RFC 7672 §2.2). Returns a failure reason, or null when the peer authenticated.
|
|
923
|
+
// Every path that cannot reach a verdict returns a reason: an unverifiable peer
|
|
924
|
+
// is not an authenticated one, and this runs only when a caller asked for DANE.
|
|
925
|
+
function _daneChainFailure(sock, cfg) {
|
|
926
|
+
var chain = _peerChainDer(sock);
|
|
927
|
+
if (chain.length === 0) {
|
|
928
|
+
return "dane: peer presented no certificate chain to match against its " +
|
|
929
|
+
cfg.daneTlsa.length + " TLSA record(s)";
|
|
930
|
+
}
|
|
931
|
+
var rv;
|
|
932
|
+
try {
|
|
933
|
+
rv = smtpPolicy().dane.verifyChain(chain, cfg.daneTlsa,
|
|
934
|
+
{ allowPkixModes: cfg.daneAllowPkixModes });
|
|
935
|
+
} catch (e) {
|
|
936
|
+
return "dane: chain verification could not run: " + ((e && e.message) || String(e));
|
|
937
|
+
}
|
|
938
|
+
if (!rv || rv.ok !== true) {
|
|
939
|
+
var why = (rv && rv.errors && rv.errors.length)
|
|
940
|
+
? "; " + rv.errors.map(function (x) { return x.reason; }).join(", ")
|
|
941
|
+
: "";
|
|
942
|
+
return "dane: peer certificate chain matches none of the " +
|
|
943
|
+
cfg.daneTlsa.length + " TLSA record(s) published for it (RFC 7672 §2.2)" + why;
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
|
|
829
948
|
function smtpTransport(opts) {
|
|
830
949
|
opts = opts || {};
|
|
831
950
|
if (!opts.host) {
|
|
@@ -894,8 +1013,75 @@ function smtpTransport(opts) {
|
|
|
894
1013
|
// the transport's whole life. minTlsVersion stays operator-settable here,
|
|
895
1014
|
// unlike the framework's other clients: an MX that only speaks TLS 1.2 is
|
|
896
1015
|
// common, and refusing it would mean not delivering mail.
|
|
1016
|
+
// RFC 7672 DANE. `dane` carries the peer's TLSA records, already
|
|
1017
|
+
// DNSSEC-validated by whoever fetched them; the transport matches the
|
|
1018
|
+
// certificate chain the peer presents against them once the handshake
|
|
1019
|
+
// completes, and refuses to send on a mismatch. Supplying records and then
|
|
1020
|
+
// not checking them would be discovery, not authentication, so an empty
|
|
1021
|
+
// array is refused rather than quietly meaning "no DANE": a caller that
|
|
1022
|
+
// fetched nothing must omit the option and say so.
|
|
1023
|
+
//
|
|
1024
|
+
// Resolved BEFORE tlsOpts because it decides what the handshake requires.
|
|
1025
|
+
var daneTlsa = null;
|
|
1026
|
+
if (opts.dane !== undefined && opts.dane !== null) {
|
|
1027
|
+
if (!Array.isArray(opts.dane)) {
|
|
1028
|
+
throw new MailError("mail/smtp-misconfigured",
|
|
1029
|
+
"smtp transport: opts.dane must be an array of TLSA records " +
|
|
1030
|
+
"(b.network.smtp.policy.dane.tlsa output), or omitted", true);
|
|
1031
|
+
}
|
|
1032
|
+
if (opts.dane.length === 0) {
|
|
1033
|
+
throw new MailError("mail/smtp-misconfigured",
|
|
1034
|
+
"smtp transport: opts.dane is an empty array — a peer that publishes " +
|
|
1035
|
+
"no TLSA records cannot be DANE-authenticated, so omit the option " +
|
|
1036
|
+
"rather than passing nothing to match against", true);
|
|
1037
|
+
}
|
|
1038
|
+
daneTlsa = opts.dane;
|
|
1039
|
+
}
|
|
1040
|
+
// PKIX-TA (0) and PKIX-EE (1) mean "this certificate, AND it must also pass
|
|
1041
|
+
// PKIX path validation" (RFC 7672 §3.1.1). verifyChain refuses those usages
|
|
1042
|
+
// unless the caller confirms a PKIX validator ran — and one did, precisely
|
|
1043
|
+
// when the record set contains no DANE-TA/DANE-EE to replace it, because then
|
|
1044
|
+
// the WebPKI check below stays on and the handshake would have failed without
|
|
1045
|
+
// it. Tying the permission to that condition rather than to an opt-in is what
|
|
1046
|
+
// makes a PKIX-only peer deliverable at all: it published a valid record, the
|
|
1047
|
+
// chain validated, and the match was then thrown away as not-allowed.
|
|
1048
|
+
//
|
|
1049
|
+
// The converse matters as much. On a mixed set the WebPKI check is off for
|
|
1050
|
+
// the DANE usages, so the PKIX usages' precondition no longer holds and they
|
|
1051
|
+
// are not honoured — unless the operator asserts otherwise, having arranged
|
|
1052
|
+
// path validation themselves.
|
|
1053
|
+
var daneAllowPkixModes = opts.daneAllowPkixModes === true;
|
|
1054
|
+
|
|
1055
|
+
// RFC 7672 §3.1.1 — under DANE-EE (usage 3) and DANE-TA (usage 2) the TLSA
|
|
1056
|
+
// record IS the trust anchor, and PKIX path validation is not performed. That
|
|
1057
|
+
// is not a relaxation: it is what DANE is for, and it is the deployment DANE
|
|
1058
|
+
// exists to enable — a self-signed or privately-issued MX certificate bound
|
|
1059
|
+
// to its name through DNSSEC.
|
|
1060
|
+
//
|
|
1061
|
+
// Without this, Node's WebPKI check rejects such a peer during the handshake,
|
|
1062
|
+
// before the certificate can be compared against the very records that
|
|
1063
|
+
// authenticate it. Every standards-compliant DANE-only MX was deferred while
|
|
1064
|
+
// matching its published records exactly.
|
|
1065
|
+
//
|
|
1066
|
+
// Authentication is moved, not dropped: `dane` being set makes the chain
|
|
1067
|
+
// check mandatory after the handshake, and a mismatch refuses the send.
|
|
1068
|
+
// PKIX-TA (0) and PKIX-EE (1) still require PKIX, so a record set containing
|
|
1069
|
+
// only those leaves the WebPKI check exactly as configured.
|
|
1070
|
+
var daneIsTrustAnchor = false;
|
|
1071
|
+
if (daneTlsa) {
|
|
1072
|
+
for (var dt = 0; dt < daneTlsa.length; dt += 1) {
|
|
1073
|
+
var daneUsage = daneTlsa[dt] && daneTlsa[dt].usage;
|
|
1074
|
+
if (daneUsage === 2 || daneUsage === 3) { daneIsTrustAnchor = true; break; }
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Records present, none of them a DANE trust anchor, and the WebPKI check
|
|
1079
|
+
// therefore still enforced: that IS the PKIX validation the usage-0/1 records
|
|
1080
|
+
// require, so their matches count.
|
|
1081
|
+
if (daneTlsa && !daneIsTrustAnchor && rejectUnauthorized) daneAllowPkixModes = true;
|
|
1082
|
+
|
|
897
1083
|
var tlsOpts = {
|
|
898
|
-
rejectUnauthorized: rejectUnauthorized,
|
|
1084
|
+
rejectUnauthorized: daneIsTrustAnchor ? false : rejectUnauthorized,
|
|
899
1085
|
minVersion: opts.minTlsVersion || "TLSv1.3",
|
|
900
1086
|
};
|
|
901
1087
|
if (opts.ecdhCurve) tlsOpts.ecdhCurve = opts.ecdhCurve;
|
|
@@ -952,6 +1138,8 @@ function smtpTransport(opts) {
|
|
|
952
1138
|
chunkSize: chunkSize,
|
|
953
1139
|
respectPeerSize: respectPeerSize,
|
|
954
1140
|
preferFamily: preferFamily,
|
|
1141
|
+
daneTlsa: daneTlsa,
|
|
1142
|
+
daneAllowPkixModes: daneAllowPkixModes,
|
|
955
1143
|
};
|
|
956
1144
|
|
|
957
1145
|
return {
|
|
@@ -995,6 +1183,19 @@ function _smtpUtf8Suffix(requiresSmtpUtf8, peerSupportsSmtpUtf8) {
|
|
|
995
1183
|
// the "application/octet-stream" claimed content-type also count.
|
|
996
1184
|
function _messageRequiresBinaryMime(message) {
|
|
997
1185
|
if (!message) return false;
|
|
1186
|
+
// Raw bytes carry their attachments already encoded, so the question is
|
|
1187
|
+
// whether those bytes are 8-bit binary — a NUL is the giveaway, exactly as it
|
|
1188
|
+
// is for a Buffer attachment below. Scanned over the whole message rather than
|
|
1189
|
+
// a prefix, because a raw message puts its parts wherever its author did.
|
|
1190
|
+
if (Buffer.isBuffer(message.raw)) {
|
|
1191
|
+
for (var r = 0; r < message.raw.length; r += 1) {
|
|
1192
|
+
if (message.raw[r] === 0) return true;
|
|
1193
|
+
}
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
if (typeof message.raw === "string") {
|
|
1197
|
+
return message.raw.indexOf(_NUL_CHAR) !== -1;
|
|
1198
|
+
}
|
|
998
1199
|
if (!Array.isArray(message.attachments)) return false;
|
|
999
1200
|
for (var i = 0; i < message.attachments.length; i += 1) {
|
|
1000
1201
|
var att = message.attachments[i];
|
|
@@ -1024,6 +1225,12 @@ function _messageRequiresBinaryMime(message) {
|
|
|
1024
1225
|
// (NUL-bearing).
|
|
1025
1226
|
function _messageRequires8BitMime(message) {
|
|
1026
1227
|
if (!message) return false;
|
|
1228
|
+
// Anywhere in raw bytes, headers included: 8BITMIME is about the octets on
|
|
1229
|
+
// the wire, so unlike SMTPUTF8 above this is not restricted to the header
|
|
1230
|
+
// block. Without it a raw message with a non-ASCII body went out with the
|
|
1231
|
+
// hint un-negotiated and was mangled by any peer that needed it.
|
|
1232
|
+
var raw8 = _rawText(message);
|
|
1233
|
+
if (raw8 !== null) return _hasNonAscii(raw8);
|
|
1027
1234
|
var fields = ["text", "html", "subject"];
|
|
1028
1235
|
for (var i = 0; i < fields.length; i += 1) {
|
|
1029
1236
|
var v = message[fields[i]];
|
|
@@ -1068,9 +1275,9 @@ function _autoDetectFamily() {
|
|
|
1068
1275
|
// 1870 SIZE pre-check before MAIL FROM. Caller passes the already-
|
|
1069
1276
|
// CRLF-normalized + dot-stuffed wire string (the same one that goes
|
|
1070
1277
|
// into DATA / BDAT). Returns the byte count Node will write.
|
|
1071
|
-
function _messageWireSize(wire) {
|
|
1278
|
+
function _messageWireSize(wire, encoding) {
|
|
1072
1279
|
if (typeof wire !== "string") return 0;
|
|
1073
|
-
return Buffer.byteLength(wire, "utf8");
|
|
1280
|
+
return Buffer.byteLength(wire, encoding || "utf8");
|
|
1074
1281
|
}
|
|
1075
1282
|
|
|
1076
1283
|
// Parse the SIZE keyword's argument from a `SIZE 12345` EHLO line.
|
|
@@ -1124,22 +1331,51 @@ function _smtpSend(message, cfg) {
|
|
|
1124
1331
|
var requiresBinaryMime = _messageRequiresBinaryMime(message);
|
|
1125
1332
|
var requires8BitMime = _messageRequires8BitMime(message);
|
|
1126
1333
|
var dataMessage = _buildRfc822(message);
|
|
1334
|
+
// Every conversion of dataMessage back to bytes uses this, so the size the
|
|
1335
|
+
// peer is told, the bytes BDAT frames, and the bytes DATA writes are all
|
|
1336
|
+
// the same bytes. Resolved before signing because the signer needs it too.
|
|
1337
|
+
var wireEncoding = _wireEncoding(message);
|
|
1127
1338
|
if (cfg.dkimSigner) {
|
|
1128
|
-
try {
|
|
1129
|
-
|
|
1339
|
+
try {
|
|
1340
|
+
// A message carrying raw octets is handed to the signer AS octets. A
|
|
1341
|
+
// signature covers the bytes on the wire, and passing a latin1 wire
|
|
1342
|
+
// string would have the signer read it as text, sign its UTF-8
|
|
1343
|
+
// re-encoding, and hand back a decoded copy — a corrupted message
|
|
1344
|
+
// carrying a signature that verifies, which is worse than an
|
|
1345
|
+
// unsigned one.
|
|
1346
|
+
if (wireEncoding === "latin1") {
|
|
1347
|
+
var signedBuf = cfg.dkimSigner.sign(Buffer.from(dataMessage, "latin1"));
|
|
1348
|
+
dataMessage = Buffer.isBuffer(signedBuf)
|
|
1349
|
+
? signedBuf.toString("latin1")
|
|
1350
|
+
: String(signedBuf);
|
|
1351
|
+
} else {
|
|
1352
|
+
dataMessage = cfg.dkimSigner.sign(dataMessage);
|
|
1353
|
+
}
|
|
1354
|
+
} catch (e) {
|
|
1130
1355
|
reject(new MailError("mail/dkim-sign-failed",
|
|
1131
1356
|
"dkim signing failed: " + ((e && e.message) || String(e)), true));
|
|
1132
1357
|
return;
|
|
1133
1358
|
}
|
|
1134
1359
|
}
|
|
1135
|
-
var messageWireSize = _messageWireSize(dataMessage);
|
|
1360
|
+
var messageWireSize = _messageWireSize(dataMessage, wireEncoding);
|
|
1136
1361
|
|
|
1137
1362
|
// Outbound SMTP-smuggling defense — refuse before opening the
|
|
1138
1363
|
// socket if the produced RFC 822 wire contains the bare-CR / bare-
|
|
1139
1364
|
// LF + smuggled-verb shape (CVE-2023-51764 / 51765 / 51766 class).
|
|
1140
1365
|
// Operator-supplied subject / body / headers can sneak the pattern
|
|
1141
1366
|
// through _buildRfc822 if the input wasn't already gated.
|
|
1142
|
-
|
|
1367
|
+
//
|
|
1368
|
+
// For a BINARYMIME message the screen covers the header block rather than
|
|
1369
|
+
// the whole wire. A NUL is what identifies a body as binary in the first
|
|
1370
|
+
// place, so screening the body for one refused every message the transport
|
|
1371
|
+
// detects, negotiates and advertises support for: BINARYMIME was
|
|
1372
|
+
// unreachable. Narrowing the scope is safe because the smuggling class
|
|
1373
|
+
// lives in line structure, and RFC 3030 §3 sends a BINARYMIME body under
|
|
1374
|
+
// BDAT, which is length-framed — there is no dot terminator to forge and no
|
|
1375
|
+
// line scanning to confuse. The framing requirement is enforced below
|
|
1376
|
+
// rather than assumed.
|
|
1377
|
+
var screened = requiresBinaryMime ? _rawHeaderBlock(dataMessage) : dataMessage;
|
|
1378
|
+
var rv = guardEmail().validateMessage(screened, { profile: "strict" });
|
|
1143
1379
|
if (!rv.ok) {
|
|
1144
1380
|
var critical = rv.issues.filter(function (i) {
|
|
1145
1381
|
return i.severity === "critical";
|
|
@@ -1203,7 +1439,7 @@ function _smtpSend(message, cfg) {
|
|
|
1203
1439
|
// immediately by exactly N bytes of body (no CRLF terminator on
|
|
1204
1440
|
// the chunk; SMTP framing is purely length-based per RFC 3030 §2).
|
|
1205
1441
|
function sendBdatChunk() {
|
|
1206
|
-
if (!dataWireBytes) dataWireBytes = Buffer.from(dataMessage,
|
|
1442
|
+
if (!dataWireBytes) dataWireBytes = Buffer.from(dataMessage, wireEncoding);
|
|
1207
1443
|
var remaining = dataWireBytes.length - bdatOffset;
|
|
1208
1444
|
if (remaining <= 0) {
|
|
1209
1445
|
// Empty body — send `BDAT 0 LAST` to terminate gracefully.
|
|
@@ -1285,7 +1521,14 @@ function _smtpSend(message, cfg) {
|
|
|
1285
1521
|
tlsConnectOpts.port = cfg.port;
|
|
1286
1522
|
if (family === 4 || family === 6) tlsConnectOpts.family = family;
|
|
1287
1523
|
// allow:outbound-tls-posture — _smtpTlsOpts merges the live posture
|
|
1288
|
-
|
|
1524
|
+
var implicitSock = nodeTls().connect(tlsConnectOpts);
|
|
1525
|
+
if (cfg.daneTlsa) {
|
|
1526
|
+
implicitSock.on("secureConnect", function () {
|
|
1527
|
+
var bad = _daneChainFailure(implicitSock, cfg);
|
|
1528
|
+
if (bad) fail(bad);
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
attachSocket(implicitSock);
|
|
1289
1532
|
} else {
|
|
1290
1533
|
var netOpts = { host: cfg.host, port: cfg.port };
|
|
1291
1534
|
if (family === 4 || family === 6) netOpts.family = family;
|
|
@@ -1306,6 +1549,25 @@ function _smtpSend(message, cfg) {
|
|
|
1306
1549
|
peerSupportsBinaryMime = ehloLines.indexOf("BINARYMIME") !== -1;
|
|
1307
1550
|
peerSupportsChunking = ehloLines.indexOf("CHUNKING") !== -1;
|
|
1308
1551
|
peerSizeCap = _parsePeerSize(ehloFullLines);
|
|
1552
|
+
|
|
1553
|
+
// RFC 3207 §4.2 — the client MUST discard every service extension
|
|
1554
|
+
// learned before the upgrade, so nothing below may be decided from
|
|
1555
|
+
// this EHLO when a STARTTLS is still to come. The cleartext leg is
|
|
1556
|
+
// precisely what a network attacker can rewrite, and each of these
|
|
1557
|
+
// decisions is one an injected extension line would steer: a SIZE cap
|
|
1558
|
+
// that refuses every message, a CHUNKING that picks a framing the real
|
|
1559
|
+
// peer never offered, a BINARYMIME whose absence refuses a message the
|
|
1560
|
+
// peer would have taken.
|
|
1561
|
+
//
|
|
1562
|
+
// It reads as an over-refusal too. Servers commonly advertise CHUNKING
|
|
1563
|
+
// and BINARYMIME only once TLS is up, so a binary message was refused
|
|
1564
|
+
// before STARTTLS was even sent.
|
|
1565
|
+
if (!cfg.useImplicitTLS && !upgradedToTLS) {
|
|
1566
|
+
send("STARTTLS");
|
|
1567
|
+
step = SMTP_STEP_STARTTLS;
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1309
1571
|
// RFC 6531 §3.2 — if the message requires SMTPUTF8 and the
|
|
1310
1572
|
// peer does not advertise it, refuse hard rather than emit a
|
|
1311
1573
|
// mangled wire (server might still accept but headers/local
|
|
@@ -1344,12 +1606,48 @@ function _smtpSend(message, cfg) {
|
|
|
1344
1606
|
// transaction. CHUNKING + BDAT is preferred when both peer
|
|
1345
1607
|
// advertises it AND operator didn't disable it.
|
|
1346
1608
|
useBdat = peerSupportsChunking && cfg.chunkingEnabled;
|
|
1609
|
+
// RFC 6152 §3 — a body carrying octets with the high bit set needs
|
|
1610
|
+
// 8BITMIME. Falling through to 7BIT and writing those octets anyway
|
|
1611
|
+
// declares one thing and sends another: the peer may reject the
|
|
1612
|
+
// transaction, or strip the eighth bit and deliver a corrupted message
|
|
1613
|
+
// under a signature that no longer matches it. The BINARYMIME sibling
|
|
1614
|
+
// below already refused; marking a body as needing an extension and
|
|
1615
|
+
// then shipping it without one is the same defect either way.
|
|
1616
|
+
if (requires8BitMime && !peerSupports8BitMime && !requiresBinaryMime) {
|
|
1617
|
+
settled = true;
|
|
1618
|
+
clearTxTimer();
|
|
1619
|
+
try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
|
|
1620
|
+
reject(new MailError("mail/8bitmime-not-advertised",
|
|
1621
|
+
"message body has 8-bit content but peer does not advertise 8BITMIME " +
|
|
1622
|
+
"(RFC 6152); encode it as quoted-printable or base64, or deliver " +
|
|
1623
|
+
"through a peer that supports the extension", true));
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1347
1626
|
if (requiresBinaryMime) bodyMode = "BINARYMIME";
|
|
1348
1627
|
else if (requires8BitMime && peerSupports8BitMime) bodyMode = "8BITMIME";
|
|
1349
1628
|
else bodyMode = "7BIT";
|
|
1629
|
+
// RFC 3030 §3 — a BINARYMIME body travels under BDAT, never DATA. The
|
|
1630
|
+
// body may contain a bare CR, a bare LF, or a lone dot on a line, so
|
|
1631
|
+
// the dot-terminated DATA framing cannot carry it: the peer would end
|
|
1632
|
+
// the message early and read the remainder as commands. That is also
|
|
1633
|
+
// the property the header-scoped smuggling screen above relies on, so
|
|
1634
|
+
// this refusal keeps the two consistent rather than leaving the
|
|
1635
|
+
// narrower screen resting on an assumption.
|
|
1636
|
+
if (requiresBinaryMime && !useBdat) {
|
|
1637
|
+
settled = true;
|
|
1638
|
+
clearTxTimer();
|
|
1639
|
+
try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
|
|
1640
|
+
reject(new MailError("mail/binarymime-requires-chunking",
|
|
1641
|
+
"message body is binary (RFC 3030 BINARYMIME) but this transaction " +
|
|
1642
|
+
"would use DATA framing: " +
|
|
1643
|
+
(peerSupportsChunking ? "opts.chunking is disabled on this transport"
|
|
1644
|
+
: "the peer does not advertise CHUNKING") +
|
|
1645
|
+
". A binary body cannot be dot-terminated; encode it as base64 or " +
|
|
1646
|
+
"enable CHUNKING", true));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1350
1649
|
|
|
1351
|
-
if (
|
|
1352
|
-
else if (cfg.user) { send("AUTH LOGIN"); step = SMTP_STEP_AUTH_USER; }
|
|
1650
|
+
if (cfg.user) { send("AUTH LOGIN"); step = SMTP_STEP_AUTH_USER; }
|
|
1353
1651
|
else { send("MAIL FROM:<" + fromAddr + ">" + _mailFromSuffix()); step = SMTP_STEP_MAIL_FROM; }
|
|
1354
1652
|
}
|
|
1355
1653
|
else if (step === SMTP_STEP_STARTTLS) {
|
|
@@ -1361,6 +1659,19 @@ function _smtpSend(message, cfg) {
|
|
|
1361
1659
|
upgradedToTLS = true;
|
|
1362
1660
|
try { socket.removeAllListeners("data"); } catch (_e) { /* listeners migrate to upgraded socket */ }
|
|
1363
1661
|
attachSocket(tlsSocket);
|
|
1662
|
+
// Before the second EHLO, so a peer that fails DANE never sees our
|
|
1663
|
+
// credentials or the message: the handshake is done, and this is the
|
|
1664
|
+
// first moment its certificate can be judged.
|
|
1665
|
+
if (cfg.daneTlsa) {
|
|
1666
|
+
var bad = _daneChainFailure(tlsSocket, cfg);
|
|
1667
|
+
if (bad) { fail(bad); return; }
|
|
1668
|
+
}
|
|
1669
|
+
// RFC 3207 §4.2 — discarded, not appended to. The extension lines
|
|
1670
|
+
// accumulate as they arrive, so leaving the cleartext ones in place
|
|
1671
|
+
// makes the post-upgrade set the UNION of both legs, and a capability
|
|
1672
|
+
// the real peer never advertised inside TLS is still believed.
|
|
1673
|
+
ehloLines.length = 0;
|
|
1674
|
+
ehloFullLines.length = 0;
|
|
1364
1675
|
send("EHLO " + cfg.ehloName);
|
|
1365
1676
|
step = SMTP_STEP_EHLO_RESP;
|
|
1366
1677
|
});
|
|
@@ -1421,7 +1732,23 @@ function _smtpSend(message, cfg) {
|
|
|
1421
1732
|
}
|
|
1422
1733
|
else if (step === SMTP_STEP_DATA) {
|
|
1423
1734
|
if (code !== 354) { fail("data-rejected (code " + code + ")"); return; }
|
|
1424
|
-
|
|
1735
|
+
// Written as bytes, not as a string: socket.write(string) encodes utf8,
|
|
1736
|
+
// which would re-introduce the U+FFFD substitution the latin1 round
|
|
1737
|
+
// trip exists to avoid. Dot-stuffing runs on the string first because
|
|
1738
|
+
// under latin1 one character is one byte, so the line scan is
|
|
1739
|
+
// byte-accurate either way.
|
|
1740
|
+
//
|
|
1741
|
+
// RFC 5321 §4.1.1.4 — the terminator is CRLF "." CRLF, and the first
|
|
1742
|
+
// CRLF belongs to the body's last line. A serialized RFC 822 message
|
|
1743
|
+
// normally already ends in one, so appending another inserted a blank
|
|
1744
|
+
// line: the bytes were not verbatim after all, and a DKIM signature
|
|
1745
|
+
// over them no longer verified. Only the missing CRLF is added.
|
|
1746
|
+
var stuffed = _dotStuffForData(dataMessage);
|
|
1747
|
+
var terminator = (stuffed.length >= 2 && stuffed.slice(-2) === "\r\n")
|
|
1748
|
+
? ".\r\n" : "\r\n.\r\n";
|
|
1749
|
+
try {
|
|
1750
|
+
socket.write(Buffer.from(stuffed + terminator, wireEncoding));
|
|
1751
|
+
} catch (e) { fail(e.message || String(e)); return; }
|
|
1425
1752
|
step = SMTP_STEP_BODY;
|
|
1426
1753
|
}
|
|
1427
1754
|
else if (step === SMTP_STEP_BDAT) {
|
|
@@ -2075,9 +2402,10 @@ module.exports = {
|
|
|
2075
2402
|
// verify (RFC 6376, on .dkim above alongside outbound signing),
|
|
2076
2403
|
// DMARC (RFC 9989), ARC (RFC 8617). `.inbound.verify` is the
|
|
2077
2404
|
// one-call receiver pipeline — SPF + DKIM + From-header extraction +
|
|
2078
|
-
// DMARC policy + the RFC 8601
|
|
2079
|
-
// composed by b.mail.server.mx at
|
|
2080
|
-
// opt and callable directly by
|
|
2405
|
+
// DMARC policy + ARC chain status + the RFC 8601
|
|
2406
|
+
// Authentication-Results header — composed by b.mail.server.mx at
|
|
2407
|
+
// DATA time via its guardEnvelope opt and callable directly by
|
|
2408
|
+
// operator-built listeners.
|
|
2081
2409
|
spf: mailAuth.spf,
|
|
2082
2410
|
dmarc: mailAuth.dmarc,
|
|
2083
2411
|
arc: mailAuth.arc,
|
|
@@ -2085,9 +2413,19 @@ module.exports = {
|
|
|
2085
2413
|
authResults: mailAuth.authResults,
|
|
2086
2414
|
inbound: mailAuth.inbound,
|
|
2087
2415
|
bimi: mailBimi,
|
|
2088
|
-
// Test-only
|
|
2089
|
-
// standing up a TLS-capable SMTP fixture
|
|
2416
|
+
// Test-only exports: let unit tests inspect the wire format and the
|
|
2417
|
+
// capability decisions without standing up a TLS-capable SMTP fixture that
|
|
2418
|
+
// advertises SMTPUTF8 / 8BITMIME / BINARYMIME. Operators don't call these.
|
|
2419
|
+
//
|
|
2420
|
+
// The three detectors are exported for the same reason the builder is: they
|
|
2421
|
+
// decide what goes on the wire alongside the body, they read the message the
|
|
2422
|
+
// same way it does, and when the builder learned about raw bytes and they did
|
|
2423
|
+
// not, a raw message went out with every hint un-negotiated. Testing the
|
|
2424
|
+
// builder alone is what left that gap open.
|
|
2090
2425
|
_buildRfc822ForTest: _buildRfc822,
|
|
2426
|
+
_requiresSmtpUtf8ForTest: _messageRequiresSmtpUtf8,
|
|
2427
|
+
_requiresBinaryMimeForTest: _messageRequiresBinaryMime,
|
|
2428
|
+
_requires8BitMimeForTest: _messageRequires8BitMime,
|
|
2091
2429
|
transports: {
|
|
2092
2430
|
console: consoleTransport,
|
|
2093
2431
|
memory: memoryTransport,
|
package/lib/mcp.js
CHANGED
|
@@ -479,7 +479,12 @@ function _toolResultSanitize(result, opts) {
|
|
|
479
479
|
"toolResult.sanitize: posture must be 'refuse' | 'sanitize' | 'audit-only'");
|
|
480
480
|
}
|
|
481
481
|
var maxBytes = opts.maxTextBytes || DEFAULT_TOOL_OUTPUT_MAX_BYTES;
|
|
482
|
-
|
|
482
|
+
// `null` when no allowlist was supplied, the array when one was — including
|
|
483
|
+
// an EMPTY one, which asks that no URL be permitted. Normalising the absent
|
|
484
|
+
// case to `[]` made the two indistinguishable, and the length test below then
|
|
485
|
+
// read both as "no restriction". A tool result is attacker-influenced
|
|
486
|
+
// content, so this is the allowlist that stops one pointing wherever it likes.
|
|
487
|
+
var allowedHosts = Array.isArray(opts.allowedHosts) ? opts.allowedHosts : null;
|
|
483
488
|
if (!result || typeof result !== "object") {
|
|
484
489
|
throw new McpError("mcp/bad-tool-result",
|
|
485
490
|
"toolResult.sanitize: result must be an object");
|
|
@@ -522,7 +527,7 @@ function _toolResultSanitize(result, opts) {
|
|
|
522
527
|
cleaned.push({ type: "text", text: t });
|
|
523
528
|
} else if (block.type === "image" || block.type === "resource_link" || block.type === "audio") {
|
|
524
529
|
var url = block.url || (block.resource && block.resource.uri);
|
|
525
|
-
if (typeof url === "string" && url.length > 0 && allowedHosts
|
|
530
|
+
if (typeof url === "string" && url.length > 0 && allowedHosts) {
|
|
526
531
|
var u; try { u = new URL(url); } catch (_e) { u = null; } // allow:raw-new-url-parse-only — operator-supplied tool URL; allowlist enforced below
|
|
527
532
|
if (!u || allowedHosts.indexOf(u.host) === -1) {
|
|
528
533
|
issues.push({ kind: "off-allowlist-url", index: i, url: url });
|
|
@@ -950,7 +955,10 @@ function _samplingGuard(opts) {
|
|
|
950
955
|
function _elicitationGuard(opts) {
|
|
951
956
|
opts = opts || {};
|
|
952
957
|
var maxBytes = opts.maxMessageBytes || (8 * 1024); // allow:raw-byte-literal — 8 KiB elicitation message cap
|
|
953
|
-
|
|
958
|
+
// The default applies when the option is OMITTED. An explicitly empty list
|
|
959
|
+
// says no schema type is acceptable, and falling back to the default there
|
|
960
|
+
// would answer a caller's "none" with "object".
|
|
961
|
+
var allowedSchemaTypes = Array.isArray(opts.allowedSchemaTypes)
|
|
954
962
|
? opts.allowedSchemaTypes : ["object"];
|
|
955
963
|
var posture = opts.posture || "refuse";
|
|
956
964
|
|
|
@@ -198,7 +198,12 @@ function create(opts) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
function _emitObs(metric, n, tags) {
|
|
201
|
-
|
|
201
|
+
// `safeEvent`, which is the name the module exports. It was `count`, which
|
|
202
|
+
// it does not, so this threw a TypeError on every call and the drop-silent
|
|
203
|
+
// catch below swallowed it — the counter never emitted once. `safeEvent` is
|
|
204
|
+
// itself drop-silent, so the catch stays as a belt on a working emitter
|
|
205
|
+
// rather than as the thing hiding a broken one.
|
|
206
|
+
try { observability().safeEvent(metric, n, tags || {}); }
|
|
202
207
|
catch (_e) { /* best-effort */ }
|
|
203
208
|
}
|
|
204
209
|
|
|
@@ -316,7 +316,11 @@ function create(opts) {
|
|
|
316
316
|
}
|
|
317
317
|
_emitDenied(req, "cross-site-refused (mode=" + (mode || "?") +
|
|
318
318
|
", dest=" + (dest || "?") + ")");
|
|
319
|
-
|
|
319
|
+
// `safeEvent`, not `count` — the module exports no `count`, so this threw on
|
|
320
|
+
// every cross-site refusal and the drop-silent catch swallowed it. The
|
|
321
|
+
// refusals happened; the metric that was meant to make them visible never
|
|
322
|
+
// did.
|
|
323
|
+
try { observability().safeEvent("auth.fetch_metadata.cross_site_refused", 1, {}); }
|
|
320
324
|
catch (_e) { /* best-effort */ }
|
|
321
325
|
return _writeReject(req, res, "Cross-site request refused.", "cross-site-refused", onDeny, problemMode);
|
|
322
326
|
};
|