@blamejs/core 0.18.54 → 0.18.56

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 (48) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/NOTICE +5 -5
  3. package/README.md +9 -9
  4. package/lib/agent-audit.js +27 -2
  5. package/lib/audit-sign.js +24 -5
  6. package/lib/audit.js +26 -24
  7. package/lib/auth/passkey.js +4 -1
  8. package/lib/chain-writer.js +17 -0
  9. package/lib/db-file-lifecycle.js +14 -3
  10. package/lib/db.js +505 -49
  11. package/lib/guard-filename.js +8 -1
  12. package/lib/guard-html.js +10 -2
  13. package/lib/guard-list-unsubscribe.js +6 -1
  14. package/lib/guard-managesieve-command.js +24 -3
  15. package/lib/guard-smtp-command.js +20 -4
  16. package/lib/guard-svg.js +6 -1
  17. package/lib/http-client.js +17 -3
  18. package/lib/mail-agent.js +6 -4
  19. package/lib/mail-auth.js +59 -2
  20. package/lib/mail-server-imap.js +65 -34
  21. package/lib/mail-server-managesieve.js +65 -42
  22. package/lib/mail-server-mx.js +313 -40
  23. package/lib/mail-server-net.js +155 -1
  24. package/lib/mail-server-pop3.js +16 -19
  25. package/lib/mail-server-rate-limit.js +104 -6
  26. package/lib/mail-server-submission.js +162 -33
  27. package/lib/mail-server-tls.js +71 -11
  28. package/lib/mcp.js +11 -3
  29. package/lib/middleware/csrf-protect.js +37 -20
  30. package/lib/middleware/require-mtls.js +8 -1
  31. package/lib/network-tls.js +18 -0
  32. package/lib/safe-mount-info.js +39 -6
  33. package/lib/safe-smtp.js +96 -1
  34. package/lib/safe-url.js +8 -2
  35. package/lib/self-update.js +4 -1
  36. package/lib/session-stores.js +6 -3
  37. package/lib/vendor/MANIFEST.json +34 -34
  38. package/lib/vendor/blamejs-pki.cjs +396 -37
  39. package/lib/vendor/browser/noble-ciphers.mjs +15 -1
  40. package/lib/vendor/browser/noble-hashes.mjs +12 -4
  41. package/lib/vendor/browser/noble-post-quantum.mjs +78 -37
  42. package/lib/vendor/noble-ciphers.cjs +15 -1
  43. package/lib/vendor/noble-curves.cjs +46 -21
  44. package/lib/vendor/noble-post-quantum.cjs +184 -75
  45. package/lib/watcher.js +31 -6
  46. package/lib/ws-client.js +17 -2
  47. package/package.json +1 -1
  48. package/sbom.cdx.json +6 -6
@@ -133,7 +133,6 @@ var safeBuffer = require("./safe-buffer");
133
133
  var mailServerTls = require("./mail-server-tls");
134
134
  var mailServerNet = require("./mail-server-net");
135
135
  var C = require("./constants");
136
- var bCrypto = require("./crypto");
137
136
  var numericBounds = require("./numeric-bounds");
138
137
  var validateOpts = require("./validate-opts");
139
138
  var guardManageSieveCommand = require("./guard-managesieve-command");
@@ -172,6 +171,7 @@ var ERR_CLAMP = 200;
172
171
  * greeting: string, // default "blamejs ManageSieve"
173
172
  * maxLineBytes: number, // default 8192
174
173
  * idleTimeoutMs: number, // default 5 min
174
+ * maxConnections: number, // default 1024 — listener-wide ceiling
175
175
  * profile: "strict" | "balanced" | "permissive", // default "strict"
176
176
  * auth: {
177
177
  * mechanisms: ["SCRAM-SHA-256", "OAUTHBEARER", ...], // SASL mechs to advertise
@@ -212,7 +212,7 @@ function create(opts) {
212
212
  "operator-supplied backend)");
213
213
  }
214
214
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
215
- ["maxLineBytes", "idleTimeoutMs"],
215
+ ["maxLineBytes", "idleTimeoutMs", "maxConnections"],
216
216
  "mail.server.managesieve.", MailServerManageSieveError, "mail-server-managesieve/bad-bound");
217
217
 
218
218
  var greeting = opts.greeting || DEFAULT_GREETING_VENDOR;
@@ -222,7 +222,16 @@ function create(opts) {
222
222
  var authConfig = opts.auth || null;
223
223
  var mailStore = opts.mailStore;
224
224
  var allowPlaintext = opts.allowPlaintext === true;
225
- var tlsContext = opts.tlsContext || null;
225
+
226
+ // Read at the point of use, never captured. A consumer wiring the watching
227
+ // loader from mail.server.tls.context installs an accessor on this option, and
228
+ // capturing it here would call that accessor exactly once and freeze whatever
229
+ // certificate was current at boot — for a listener whose siblings all rotate.
230
+ // Certificates renew every 60 to 90 days on any automated CA, so the frozen
231
+ // one expires while the operator's evidence says rotation is working: the
232
+ // watcher fired, the context rebuilt, and it simply never reached here. The
233
+ // four sibling listeners already read the option per connection.
234
+ function _tlsContext() { return opts.tlsContext || null; }
226
235
 
227
236
  // safeSieve cap matches the guard's per-profile script cap (the
228
237
  // guard caps the literal-byte announcement; safeSieve.parse caps
@@ -236,22 +245,18 @@ function create(opts) {
236
245
  var _emit = auditEmit.emit;
237
246
 
238
247
  function _handleConnection(rawSocket) {
239
- var remoteAddress = mailServerNet.admitConnection(rawSocket, rateLimit, _emit, {
248
+ var accepted = mailServerNet.acceptConnection(rawSocket, {
249
+ rateLimit: rateLimit,
250
+ connections: connections,
251
+ emit: _emit,
240
252
  refusedEvent: "mail.server.managesieve.rate_limit_refused",
241
253
  refusalLine: 'NO "Too many connections from your IP"\r\n',
254
+ idPrefix: "msvconn-",
242
255
  });
243
- if (remoteAddress === null) return;
244
- var connectionId = "msvconn-" + bCrypto.generateToken(8); // connection-id length
245
- var socket = rawSocket;
246
- connections.add(socket);
247
- // Single close handler covers BOTH operator-driven `_close(socket)`
248
- // and client-initiated disconnects (TCP FIN / RST). Releases the
249
- // rate-limit slot AND removes the socket from the tracking set so
250
- // long-lived deployments don't accumulate stale entries.
251
- rawSocket.once("close", function () {
252
- rateLimit.releaseConnection(remoteAddress);
253
- connections.delete(socket);
254
- });
256
+ if (accepted === null) return;
257
+ var remoteAddress = accepted.remoteAddress;
258
+ var connectionId = accepted.connectionId;
259
+ var socket = accepted.socket;
255
260
 
256
261
  var state = {
257
262
  id: connectionId,
@@ -284,7 +289,7 @@ function create(opts) {
284
289
  _emitCapabilityBanner(state, socket);
285
290
  _writeOk(socket, greeting + " ready");
286
291
 
287
- if (allowPlaintext && !tlsContext) {
292
+ if (allowPlaintext && !_tlsContext()) {
288
293
  _emit("mail.server.managesieve.plaintext_warning",
289
294
  { connectionId: connectionId,
290
295
  remark: "allowPlaintext=true; no STARTTLS available — operators MUST gate at network layer" },
@@ -302,6 +307,31 @@ function create(opts) {
302
307
  // AUTHENTICATE with a non-synchronizing initial-response), the next
303
308
  // N bytes are the literal-payload; accumulate them before resuming
304
309
  // line-mode dispatch.
310
+ // The CRLF that ends the LINE a literal sat on. RFC 5804 §4 puts it after
311
+ // the payload, outside the declared octet count, so every literal owes one
312
+ // and no literal's size includes it.
313
+ //
314
+ // Consumed here when it is already buffered, and otherwise awaited: it may
315
+ // arrive in a later segment, and read as a line it is an EMPTY line. On the
316
+ // SASL path an empty line is a second response and the pipelining guard
317
+ // fails the exchange for it; on the PUTSCRIPT path it draws a spurious
318
+ // "empty command line" refusal AFTER the script was accepted, so one command
319
+ // is answered twice and a client that reads one reply per command spends the
320
+ // rest of the session attributing each answer to the command before it.
321
+ //
322
+ // A peek alone is not enough, because whether the terminator has arrived yet
323
+ // is a property of the network rather than of the protocol. Shared by both
324
+ // literal paths: one of them used to do this and the other did not, which is
325
+ // the entire defect.
326
+ function _consumeLiteralTerminator(state) {
327
+ if (state.lineBuffer.length >= 2 &&
328
+ state.lineBuffer[0] === 0x0d && state.lineBuffer[1] === 0x0a) { // CR LF
329
+ state.lineBuffer = state.lineBuffer.subarray(2);
330
+ } else {
331
+ state.awaitingLiteralTerminator = true;
332
+ }
333
+ }
334
+
305
335
  function _drainBuffer(state, socket) {
306
336
  while (true) {
307
337
  if (state.pendingLiteral) {
@@ -314,6 +344,7 @@ function create(opts) {
314
344
  }
315
345
  pl.body = Buffer.concat([pl.body, state.lineBuffer.subarray(0, need)]);
316
346
  state.lineBuffer = state.lineBuffer.subarray(need);
347
+ _consumeLiteralTerminator(state);
317
348
  state.pendingLiteral = null;
318
349
  _completePutscript(state, socket, pl);
319
350
  if (state.stage === "closed") return;
@@ -329,26 +360,7 @@ function create(opts) {
329
360
  }
330
361
  pa.irBody = Buffer.concat([pa.irBody, state.lineBuffer.subarray(0, needA)]);
331
362
  state.lineBuffer = state.lineBuffer.subarray(needA);
332
- // The CRLF that ends the line the literal sat on. It was left in the
333
- // buffer, so the next pass read it as an empty LINE — harmless while
334
- // nothing was in flight, and not harmless during a multi-step SASL
335
- // exchange, where an empty line is a second response and the pipelining
336
- // guard fails the exchange for it. The bug only appeared when the two
337
- // writes arrived as separate TCP segments, which is why the host run
338
- // was green and the container run was not.
339
- // The CRLF that ends the line the literal sat on. Consumed here when it
340
- // is already buffered, and otherwise awaited: it may arrive in a later
341
- // segment, and read as a line it is an EMPTY line. That was harmless
342
- // while nothing was in flight, and during a multi-step SASL exchange it
343
- // is a second response — the pipelining guard fails the exchange for
344
- // it. A peek alone is not enough, because whether the terminator has
345
- // arrived yet is a property of the network, not of the protocol.
346
- if (state.lineBuffer.length >= 2 &&
347
- state.lineBuffer[0] === 0x0d && state.lineBuffer[1] === 0x0a) { // CR LF
348
- state.lineBuffer = state.lineBuffer.subarray(2);
349
- } else {
350
- state.awaitingLiteralTerminator = true;
351
- }
363
+ _consumeLiteralTerminator(state);
352
364
  pa.irBytes = null;
353
365
  _completeAuthenticate(state, socket);
354
366
  if (state.stage === "closed") return;
@@ -508,7 +520,7 @@ function create(opts) {
508
520
  } else {
509
521
  socket.write('"SASL" ""\r\n');
510
522
  }
511
- if (!state.tls && tlsContext) {
523
+ if (!state.tls && _tlsContext()) {
512
524
  socket.write('"STARTTLS"\r\n');
513
525
  }
514
526
  }
@@ -536,7 +548,8 @@ function create(opts) {
536
548
  _writeNo(socket, "STARTTLS only valid pre-AUTH (RFC 5804 §2.2)");
537
549
  return;
538
550
  }
539
- if (!tlsContext) {
551
+ var startTlsContext = _tlsContext();
552
+ if (!startTlsContext) {
540
553
  _writeNo(socket, "STARTTLS unavailable (listener configured with allowPlaintext=true and no tlsContext)");
541
554
  return;
542
555
  }
@@ -549,7 +562,7 @@ function create(opts) {
549
562
  mailServerTls.upgradeLineProtocol({
550
563
  state: state,
551
564
  socket: socket,
552
- secureContext: tlsContext,
565
+ secureContext: startTlsContext,
553
566
  idleTimeoutMs: idleTimeoutMs,
554
567
  clearFields: ["pendingLiteral", "pendingAuth"],
555
568
  drain: _drainBuffer,
@@ -625,10 +638,19 @@ function create(opts) {
625
638
  mech: mech,
626
639
  irBytes: parsed.literalBytes,
627
640
  irPlus: parsed.literalPlus,
628
- irBody: Buffer.alloc(0),
641
+ // RFC 5804 §2.1 lets the initial response come inline as a quoted string
642
+ // instead of a literal. It arrives complete on this line, so there is
643
+ // nothing to wait for — the exchange runs now, with the response the
644
+ // client actually sent. This used to fall through to the no-initial-
645
+ // response branch and answer a conforming client as though it had said
646
+ // nothing.
647
+ irBody: parsed.initialResponse === null || parsed.initialResponse === undefined
648
+ ? Buffer.alloc(0)
649
+ : Buffer.from(parsed.initialResponse, "utf8"),
629
650
  };
630
651
  if (parsed.literalBytes === null) {
631
- // No initial-response call verify with empty client response.
652
+ // Either an inline initial response, complete above, or none at all
653
+ // in which case verify is called with an empty client response.
632
654
  _completeAuthenticate(state, socket);
633
655
  return;
634
656
  }
@@ -987,6 +1009,7 @@ function create(opts) {
987
1009
  // ---- Lifecycle ----------------------------------------------------------
988
1010
  return mailServerNet.createStoreServer(net, {
989
1011
  defaultPort: DEFAULT_PORT,
1012
+ maxConnections: opts.maxConnections,
990
1013
  handleConnection: _handleConnection,
991
1014
  errorClass: MailServerManageSieveError,
992
1015
  errorCodePrefix: "mail-server-managesieve/",