@blamejs/core 0.9.46 → 0.10.1

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +951 -893
  2. package/index.js +30 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +67 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/compliance.js +176 -8
  25. package/lib/crypto-field.js +114 -14
  26. package/lib/crypto.js +216 -20
  27. package/lib/db.js +1 -0
  28. package/lib/guard-imap-command.js +335 -0
  29. package/lib/guard-jmap.js +321 -0
  30. package/lib/guard-managesieve-command.js +566 -0
  31. package/lib/guard-pop3-command.js +317 -0
  32. package/lib/guard-smtp-command.js +58 -3
  33. package/lib/mail-agent.js +20 -7
  34. package/lib/mail-arc-sign.js +12 -8
  35. package/lib/mail-auth.js +323 -34
  36. package/lib/mail-crypto-pgp.js +934 -0
  37. package/lib/mail-crypto-smime.js +340 -0
  38. package/lib/mail-crypto.js +108 -0
  39. package/lib/mail-dav.js +1224 -0
  40. package/lib/mail-deploy.js +492 -0
  41. package/lib/mail-dkim.js +431 -26
  42. package/lib/mail-journal.js +435 -0
  43. package/lib/mail-scan.js +502 -0
  44. package/lib/mail-server-imap.js +1102 -0
  45. package/lib/mail-server-jmap.js +488 -0
  46. package/lib/mail-server-managesieve.js +853 -0
  47. package/lib/mail-server-mx.js +164 -34
  48. package/lib/mail-server-pop3.js +836 -0
  49. package/lib/mail-server-rate-limit.js +269 -0
  50. package/lib/mail-server-submission.js +1032 -0
  51. package/lib/mail-server-tls.js +445 -0
  52. package/lib/mail-sieve.js +557 -0
  53. package/lib/mail-spam-score.js +284 -0
  54. package/lib/mail.js +99 -0
  55. package/lib/metrics.js +130 -10
  56. package/lib/middleware/dpop.js +58 -3
  57. package/lib/middleware/idempotency-key.js +255 -42
  58. package/lib/middleware/protected-resource-metadata.js +114 -2
  59. package/lib/network-dns-resolver.js +33 -0
  60. package/lib/network-tls.js +46 -0
  61. package/lib/outbox.js +62 -12
  62. package/lib/pqc-agent.js +13 -5
  63. package/lib/retry.js +23 -9
  64. package/lib/router.js +23 -1
  65. package/lib/safe-ical.js +634 -0
  66. package/lib/safe-icap.js +502 -0
  67. package/lib/safe-mime.js +15 -0
  68. package/lib/safe-sieve.js +684 -0
  69. package/lib/safe-smtp.js +57 -0
  70. package/lib/safe-url.js +37 -0
  71. package/lib/safe-vcard.js +473 -0
  72. package/lib/self-update-standalone-verifier.js +32 -3
  73. package/lib/self-update.js +168 -17
  74. package/lib/vendor/MANIFEST.json +161 -156
  75. package/lib/vendor-data.js +127 -9
  76. package/lib/vex.js +324 -59
  77. package/package.json +1 -1
  78. package/sbom.cdx.json +6 -6
@@ -116,7 +116,6 @@
116
116
  */
117
117
 
118
118
  var net = require("node:net");
119
- var nodeTls = require("node:tls");
120
119
  var lazyRequire = require("./lazy-require");
121
120
  var C = require("./constants");
122
121
  var bCrypto = require("./crypto");
@@ -126,6 +125,9 @@ var safeBuffer = require("./safe-buffer");
126
125
  var safeSmtp = require("./safe-smtp");
127
126
  var validateOpts = require("./validate-opts");
128
127
  var guardSmtpCommand = require("./guard-smtp-command");
128
+ var guardDomain = require("./guard-domain");
129
+ var mailServerRateLimit = require("./mail-server-rate-limit");
130
+ var mailServerTls = require("./mail-server-tls");
129
131
  var { defineClass } = require("./framework-error");
130
132
 
131
133
  var audit = lazyRequire(function () { return require("./audit"); });
@@ -208,7 +210,10 @@ function create(opts) {
208
210
  MailServerMxError, "mail-server-mx/bad-opts");
209
211
  if (!opts.tlsContext) {
210
212
  throw new MailServerMxError("mail-server-mx/no-tls-context",
211
- "mail.server.mx.create: tlsContext is required (no implicit plaintext mode)");
213
+ "mail.server.mx.create: tlsContext is required (no implicit plaintext mode). " +
214
+ "Use b.mail.server.tls.context({ certFile, keyFile, watch: true }) to load + " +
215
+ "auto-reload a cert/key pair from disk, or pass a node:tls.createSecureContext " +
216
+ "output directly. Cert provisioning lives in b.acme (RFC 8555 + RFC 9773 ARI).");
212
217
  }
213
218
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
214
219
  ["maxLineBytes", "maxMessageBytes", "maxRcptsPerMessage", "idleTimeoutMs"],
@@ -232,6 +237,66 @@ function create(opts) {
232
237
  var relayAllowedFor = opts.relayAllowedFor || [];
233
238
  var profile = opts.profile || "strict";
234
239
 
240
+ // Default-on per-IP rate limit. Operators pass `rateLimit: false` to
241
+ // disable (only for tests / closed networks), pass a rate-limit
242
+ // handle from b.mail.server.rateLimit.create({...}) to share one
243
+ // budget across multiple listeners, or pass an opts object to
244
+ // override defaults.
245
+ var rateLimit;
246
+ if (opts.rateLimit === false) {
247
+ rateLimit = mailServerRateLimit.create({ disabled: true });
248
+ } else if (opts.rateLimit && typeof opts.rateLimit.admitConnection === "function") {
249
+ rateLimit = opts.rateLimit;
250
+ } else {
251
+ rateLimit = mailServerRateLimit.create(opts.rateLimit || {});
252
+ }
253
+
254
+ // Default-on operator-supplied-domain hardening. opts.localDomains
255
+ // and the HELO / MAIL FROM / RCPT TO domain validations all route
256
+ // through `b.guardDomain` for IDN homograph defense (CVE-2017-5469
257
+ // class), special-use-domain refusal (RFC 6761), label-length cap
258
+ // (RFC 1035 §2.3.4), and bare-IP-as-domain refusal (CVE-2021-22931
259
+ // class). Operators with a closed-network deployment can pass
260
+ // `guardDomain: false` to skip; the default keeps the protection on.
261
+ var guardDomainProfile;
262
+ if (opts.guardDomain === false) {
263
+ guardDomainProfile = null;
264
+ } else {
265
+ guardDomainProfile = guardDomain.buildProfile({
266
+ profile: opts.guardDomain && typeof opts.guardDomain === "object"
267
+ ? (opts.guardDomain.profile || profile)
268
+ : profile,
269
+ });
270
+ }
271
+ function _validateDomainHardened(d, label) {
272
+ if (!guardDomainProfile) return { ok: true };
273
+ var verdict = guardDomain.validate(d, guardDomainProfile);
274
+ if (!verdict.ok) {
275
+ _emit("mail.server.mx.domain_refused", {
276
+ reason: verdict.issues && verdict.issues[0] && verdict.issues[0].kind,
277
+ domain: d,
278
+ label: label,
279
+ }, "denied");
280
+ }
281
+ return verdict;
282
+ }
283
+
284
+ // Pre-validate operator-supplied localDomains at boot — the same
285
+ // shape they enforce on RCPT TO must itself pass the validator,
286
+ // otherwise an operator who typed an IDN homograph (or an IP) into
287
+ // their allowlist would silently weaken the gate.
288
+ if (guardDomainProfile) {
289
+ for (var __ldi = 0; __ldi < localDomains.length; __ldi += 1) {
290
+ var __ldVerdict = guardDomain.validate(localDomains[__ldi], guardDomainProfile);
291
+ if (!__ldVerdict.ok) {
292
+ throw new MailServerMxError("mail-server-mx/bad-local-domain",
293
+ "mail.server.mx.create: localDomains[" + __ldi + "] '" + localDomains[__ldi] +
294
+ "' rejected by b.guardDomain (" +
295
+ (__ldVerdict.issues && __ldVerdict.issues[0] && __ldVerdict.issues[0].kind) + ")");
296
+ }
297
+ }
298
+ }
299
+
235
300
  var tcpServer = null;
236
301
  var listening = false;
237
302
  var connections = new Set();
@@ -248,19 +313,35 @@ function create(opts) {
248
313
 
249
314
  // ---- Per-connection state machine ---------------------------------------
250
315
  function _handleConnection(socket) {
316
+ var remoteAddress = socket.remoteAddress || "0.0.0.0";
317
+ var admit = rateLimit.admitConnection(remoteAddress);
318
+ if (!admit.ok) {
319
+ // 421 4.7.0 — transient refusal; sender retries elsewhere or later.
320
+ // RFC 5321 §3.8 + §4.5.4.2 (transient negative completion).
321
+ _emit("mail.server.mx.rate_limit_refused",
322
+ { remoteAddress: remoteAddress, reason: admit.reason }, "denied");
323
+ try {
324
+ socket.write("421 4.7.0 Too many connections from your IP\r\n");
325
+ } catch (_e) { /* socket may already be torn down */ }
326
+ try { socket.destroy(); } catch (_e2) { /* idempotent */ }
327
+ return;
328
+ }
329
+ socket.once("close", function () { rateLimit.releaseConnection(remoteAddress); });
330
+
251
331
  var connectionId = "mxconn-" + bCrypto.generateToken(8); // allow:raw-byte-literal — connection-id length
252
332
  connections.add(socket);
253
333
 
254
334
  var state = {
255
335
  id: connectionId,
256
- remoteAddress: socket.remoteAddress || null,
257
- remotePort: socket.remotePort || null,
336
+ remoteAddress: remoteAddress,
337
+ remotePort: socket.remotePort || null,
258
338
  tls: false,
259
339
  stage: "connect", // connect | ehlo | mail | rcpt | data-body | done
260
340
  helo: null,
261
341
  mailFrom: null,
262
342
  rcpts: [],
263
343
  messageBytes: 0,
344
+ lastDataByteTime: 0,
264
345
  };
265
346
 
266
347
  var lineBuffer = "";
@@ -431,6 +512,21 @@ function create(opts) {
431
512
  _writeReply(socket, REPLY_501_BAD_ARGS, "5.5.4 " + verb + " requires a domain argument");
432
513
  return;
433
514
  }
515
+ // Domain hardening for HELO/EHLO greeting (RFC 5321 §4.1.1.1).
516
+ // Skip when the greeting is an address literal (`[1.2.3.4]` /
517
+ // `[IPv6:...]`) — those are RFC-5321-legitimate non-domain
518
+ // forms; the bracket syntax is already constrained by
519
+ // b.guardSmtpCommand. Bare-IP-as-domain (no brackets) IS
520
+ // refused — that's the CVE-2021-22931 class guardDomain catches.
521
+ if (helo[0] !== "[" && guardDomainProfile) {
522
+ var heloVerdict = _validateDomainHardened(helo, "helo");
523
+ if (!heloVerdict.ok) {
524
+ _writeReply(socket, REPLY_501_BAD_ARGS,
525
+ "5.5.4 " + verb + " domain refused (" +
526
+ (heloVerdict.issues && heloVerdict.issues[0] && heloVerdict.issues[0].kind) + ")");
527
+ return;
528
+ }
529
+ }
434
530
  state.helo = helo;
435
531
  state.stage = "ehlo";
436
532
  // Multi-line 250 capabilities advertisement per RFC 5321 §4.1.1.1.
@@ -461,39 +557,46 @@ function create(opts) {
461
557
  return;
462
558
  }
463
559
  _writeReply(socket, REPLY_220_READY, "2.0.0 Ready to start TLS");
464
- // STARTTLS-injection defense (CVE-2021-38371 Exim,
465
- // CVE-2021-33515 Dovecot): clear the command buffer + body
466
- // collector at upgrade time. Any commands pipelined (RFC 2920)
467
- // BEFORE the TLS handshake are discarded only commands sent
468
- // on the post-handshake TLS socket are honored.
560
+ // CVE-2021-38371 (Exim) / CVE-2021-33515 (Dovecot) STARTTLS-
561
+ // injection defense: clear the pre-handshake command buffer +
562
+ // body collector AND strip the plain-socket "data" listener
563
+ // before wrapping in TLSSocket so bytes the peer pipelined
564
+ // (RFC 2920) pre-handshake cannot reach the post-TLS state
565
+ // machine. Listener-removal + idle-timeout re-arm live in the
566
+ // shared upgradeSocket helper (b.mail.server.tls.upgradeSocket).
469
567
  lineBuffer = "";
470
568
  bodyCollector = null;
471
569
  inDataBody = false;
472
- var tlsSocket = new nodeTls.TLSSocket(socket, {
473
- isServer: true,
570
+ mailServerTls.upgradeSocket({
571
+ plainSocket: socket,
474
572
  secureContext: opts.tlsContext,
475
- });
476
- tlsSocket.on("secure", function () {
477
- state.tls = true;
478
- // After the handshake, the state machine restarts at EHLO
479
- // (per RFC 3207 §4.2 — client MUST re-issue EHLO).
480
- state.stage = "ehlo";
481
- state.helo = null;
482
- });
483
- tlsSocket.on("error", function (err) {
484
- _emit("mail.server.mx.tls_handshake_failed",
485
- { connectionId: state.id, code: (err && err.code) || "unknown",
486
- message: err && err.message }, "failure");
487
- _closeConnection(socket);
488
- });
489
- tlsSocket.on("data", function (chunk) {
490
- try { _ingestBytes(state, tlsSocket, chunk); }
491
- catch (err) {
492
- _emit("mail.server.mx.handler_threw",
493
- { connectionId: state.id, error: (err && err.message) || String(err) },
494
- "failure");
573
+ idleTimeoutMs: idleTimeoutMs,
574
+ onSecure: function (_tlsSocket) {
575
+ state.tls = true;
576
+ // After the handshake, the state machine restarts at EHLO
577
+ // (per RFC 3207 §4.2 — client MUST re-issue EHLO).
578
+ state.stage = "ehlo";
579
+ state.helo = null;
580
+ },
581
+ onData: function (tlsSocket, chunk) {
582
+ try { _ingestBytes(state, tlsSocket, chunk); }
583
+ catch (err) {
584
+ _emit("mail.server.mx.handler_threw",
585
+ { connectionId: state.id, error: (err && err.message) || String(err) },
586
+ "failure");
587
+ _closeConnection(tlsSocket);
588
+ }
589
+ },
590
+ onError: function (err) {
591
+ _emit("mail.server.mx.tls_handshake_failed",
592
+ { connectionId: state.id, code: (err && err.code) || "unknown",
593
+ message: err && err.message }, "failure");
594
+ _closeConnection(socket);
595
+ },
596
+ onTimeout: function (tlsSocket) {
597
+ _writeReply(tlsSocket, REPLY_421_SERVICE_NOT_AVAIL, "4.4.2 Idle timeout");
495
598
  _closeConnection(tlsSocket);
496
- }
599
+ },
497
600
  });
498
601
  }
499
602
 
@@ -514,6 +617,20 @@ function create(opts) {
514
617
  return;
515
618
  }
516
619
  var mailFrom = match[1].toLowerCase();
620
+ // Domain hardening on MAIL FROM domain. Skip address-literal
621
+ // and empty-reverse-path forms (RFC 5321 §4.5.5 — bounce return
622
+ // path `<>` is legitimate and has no domain).
623
+ var __mfAtIdx = mailFrom.lastIndexOf("@");
624
+ var mailFromDomain = __mfAtIdx === -1 ? "" : mailFrom.slice(__mfAtIdx + 1);
625
+ if (mailFromDomain && mailFromDomain[0] !== "[" && guardDomainProfile) {
626
+ var mfVerdict = _validateDomainHardened(mailFromDomain, "mail_from");
627
+ if (!mfVerdict.ok) {
628
+ _writeReply(socket, REPLY_501_BAD_ARGS,
629
+ "5.5.4 MAIL FROM domain refused (" +
630
+ (mfVerdict.issues && mfVerdict.issues[0] && mfVerdict.issues[0].kind) + ")");
631
+ return;
632
+ }
633
+ }
517
634
  var paramStr = match[2] || "";
518
635
  var sizeMatch = paramStr.match(RE_SIZE);
519
636
  if (sizeMatch) {
@@ -549,11 +666,24 @@ function create(opts) {
549
666
  return;
550
667
  }
551
668
  var rcpt = match[1].toLowerCase();
669
+ // Domain hardening on RCPT TO domain — skip the address-literal
670
+ // form per RFC 5321 §4.1.3 (bracket syntax already constrained
671
+ // by b.guardSmtpCommand). Refuses IDN homograph + special-use
672
+ // domains + bare-IP-as-domain on the un-bracketed form.
673
+ var _atIdx = rcpt.lastIndexOf("@");
674
+ var rcptDomain = _atIdx === -1 ? "" : rcpt.slice(_atIdx + 1);
675
+ if (rcptDomain && rcptDomain[0] !== "[" && guardDomainProfile) {
676
+ var rcptVerdict = _validateDomainHardened(rcptDomain, "rcpt_to");
677
+ if (!rcptVerdict.ok) {
678
+ _writeReply(socket, REPLY_501_BAD_ARGS,
679
+ "5.5.4 RCPT TO domain refused (" +
680
+ (rcptVerdict.issues && rcptVerdict.issues[0] && rcptVerdict.issues[0].kind) + ")");
681
+ return;
682
+ }
683
+ }
552
684
  // Local-domain check — refuse non-local recipients unless the
553
685
  // operator explicitly allowed relay for this scope.
554
686
  if (localDomains.length > 0) {
555
- var atIdx = rcpt.lastIndexOf("@");
556
- var rcptDomain = atIdx === -1 ? "" : rcpt.slice(atIdx + 1);
557
687
  if (localDomains.indexOf(rcptDomain) === -1 &&
558
688
  !_isRelayAllowed(state.remoteAddress, rcpt)) {
559
689
  _emit("mail.server.mx.relay_refused",