@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
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.server.rateLimit
4
+ * @nav Mail
5
+ * @title Mail Server Rate Limit
6
+ * @order 544
7
+ *
8
+ * @intro
9
+ * Per-IP DoS defenses shared by `b.mail.server.mx` and
10
+ * `b.mail.server.submission`. Both listeners boot with sensible
11
+ * defaults; operators tighten or relax per-deployment via the
12
+ * `rateLimit` opt on either listener.
13
+ *
14
+ * Defenses:
15
+ *
16
+ * - **Per-IP concurrent connections** — bounded to
17
+ * `maxConcurrentConnectionsPerIp` (default 10). A single hostile
18
+ * peer cannot open thousands of TCP slots and starve legitimate
19
+ * senders. Sliding-window kernel-level limits (iptables connlimit,
20
+ * ELB connection cap) are still recommended upstream — this is
21
+ * the framework's own ceiling for when the kernel limit isn't
22
+ * wired.
23
+ *
24
+ * - **Per-IP connection rate** — bounded to
25
+ * `connectionsPerIpPerMinute` (default 60). Rapid reconnect /
26
+ * scan attacks tripped here; legitimate retry-with-backoff
27
+ * traffic stays under the cap.
28
+ *
29
+ * - **Per-IP AUTH-failure budget** — bounded to
30
+ * `authFailuresPerIpPer15Min` (default 10; submission listener
31
+ * only). Credential-stuffing class — RFC 4954 §6 codes AUTH
32
+ * refusals as 535 5.7.8; we count those per remote IP in a
33
+ * rolling 15-minute window and refuse new AUTH attempts past
34
+ * the cap with 421 4.7.0. The framework's authenticator is
35
+ * unaware of this layer; the rate-limit lives at the wire-
36
+ * protocol boundary so a credential leak past the listener is
37
+ * still bounded.
38
+ *
39
+ * - **Slow-loris / minBytesPerSecond on DATA** — bounded to
40
+ * `minBytesPerSecond` (default 100 bytes/sec) during the DATA-
41
+ * body phase. The state machine's idleTimeoutMs already cuts
42
+ * fully-stalled connections; this floor cuts a hostile peer
43
+ * trickling one byte per minute to hold a connection for hours
44
+ * within the idle window.
45
+ *
46
+ * ## What this module is NOT
47
+ *
48
+ * - **Not an HTTP rate-limiter.** `b.middleware.rateLimit` covers
49
+ * the HTTP request-response shape; this module covers the
50
+ * SMTP-transactional state machine where rate-limits apply at
51
+ * the connection-boundary + the AUTH command + the DATA byte-
52
+ * rate, not per-request.
53
+ * - **Not a replacement for kernel / proxy-level limits.** This
54
+ * module is the in-process belt; iptables / NFTables / ELB /
55
+ * CloudFlare / haproxy / nginx-stream stay the suspenders. A
56
+ * framework-level limiter sees only what reaches the process;
57
+ * the kernel sees the connection floods before they cost an
58
+ * event-loop tick.
59
+ *
60
+ * ## Wire-up
61
+ *
62
+ * ```js
63
+ * var rateLimit = b.mail.server.rateLimit.create({
64
+ * maxConcurrentConnectionsPerIp: 10,
65
+ * connectionsPerIpPerMinute: 60,
66
+ * authFailuresPerIpPer15Min: 10,
67
+ * minBytesPerSecond: 100,
68
+ * });
69
+ *
70
+ * var mx = b.mail.server.mx.create({ tlsContext, rateLimit, ... });
71
+ * ```
72
+ *
73
+ * The listener calls `rateLimit.admitConnection(ip)` in the
74
+ * net.createServer callback and refuses new connections with
75
+ * `421 4.7.0 Too many connections` when the verdict is no. AUTH-
76
+ * failure budgeting (`noteAuthFailure` + `checkAuthAdmit`) is
77
+ * wired in the submission listener's AUTH handler. The slow-loris
78
+ * defense is wired in the DATA-body collector.
79
+ *
80
+ * @card
81
+ * Per-IP DoS defenses for b.mail.server.mx and b.mail.server.submission:
82
+ * concurrent-connection cap, connection-rate cap, AUTH-failure budget
83
+ * (submission), slow-loris min-bytes-per-second on DATA. Belt-and-
84
+ * suspenders to kernel/proxy-level limits.
85
+ */
86
+
87
+ var C = require("./constants");
88
+ var lazyRequire = require("./lazy-require");
89
+ var numericBounds = require("./numeric-bounds");
90
+ var validateOpts = require("./validate-opts");
91
+ var { defineClass } = require("./framework-error");
92
+
93
+ var audit = lazyRequire(function () { return require("./audit"); });
94
+
95
+ var MailServerRateLimitError = defineClass("MailServerRateLimitError", { alwaysPermanent: true });
96
+
97
+ var DEFAULTS = Object.freeze({
98
+ maxConcurrentConnectionsPerIp: 10,
99
+ connectionsPerIpPerMinute: 60, // allow:raw-time-literal — connection count, not a time value
100
+ authFailuresPerIpPer15Min: 10,
101
+ minBytesPerSecond: 100, // allow:raw-byte-literal — slow-loris byte-rate floor
102
+ disabled: false,
103
+ });
104
+
105
+ var CONNECTION_RATE_WINDOW_MS = C.TIME.minutes(1);
106
+ var AUTH_FAILURE_WINDOW_MS = C.TIME.minutes(15);
107
+
108
+ /**
109
+ * @primitive b.mail.server.rateLimit.create
110
+ * @signature b.mail.server.rateLimit.create(opts?)
111
+ * @since 0.9.47
112
+ * @status stable
113
+ * @related b.mail.server.mx.create, b.mail.server.submission.create
114
+ *
115
+ * Build a rate-limit handle. The listeners compose this internally
116
+ * with the framework defaults; operators override caps by passing
117
+ * their own `rateLimit` opt to `b.mail.server.mx.create` or
118
+ * `b.mail.server.submission.create`. Direct construction is for
119
+ * operators sharing one budget across multiple listeners (e.g. an
120
+ * MX + a submission server on the same IP space).
121
+ *
122
+ * @opts
123
+ * maxConcurrentConnectionsPerIp: number, // default 10
124
+ * connectionsPerIpPerMinute: number, // default 60
125
+ * authFailuresPerIpPer15Min: number, // default 10
126
+ * minBytesPerSecond: number, // default 100 (DATA-body slow-loris floor)
127
+ * disabled: boolean, // default false — test escape hatch
128
+ *
129
+ * @example
130
+ * var rl = b.mail.server.rateLimit.create({
131
+ * maxConcurrentConnectionsPerIp: 5,
132
+ * connectionsPerIpPerMinute: 30,
133
+ * });
134
+ * var ok = rl.admitConnection("192.0.2.1");
135
+ * // → { ok: true } or { ok: false, reason: "concurrent-per-ip" | "rate-per-ip" }
136
+ */
137
+ function create(opts) {
138
+ opts = opts || {};
139
+ if (typeof opts !== "object" || Array.isArray(opts)) {
140
+ throw new MailServerRateLimitError("mail-server-rate-limit/bad-opts",
141
+ "b.mail.server.rateLimit.create: opts must be a plain object");
142
+ }
143
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts, [
144
+ "maxConcurrentConnectionsPerIp",
145
+ "connectionsPerIpPerMinute",
146
+ "authFailuresPerIpPer15Min",
147
+ "minBytesPerSecond",
148
+ ], "b.mail.server.rateLimit.create.", MailServerRateLimitError, "mail-server-rate-limit/bad-bound");
149
+ validateOpts.optionalBoolean(opts.disabled,
150
+ "b.mail.server.rateLimit.create: opts.disabled",
151
+ MailServerRateLimitError, "mail-server-rate-limit/bad-disabled");
152
+
153
+ var cfg = {
154
+ maxConcurrentConnectionsPerIp: opts.maxConcurrentConnectionsPerIp === undefined
155
+ ? DEFAULTS.maxConcurrentConnectionsPerIp : opts.maxConcurrentConnectionsPerIp,
156
+ connectionsPerIpPerMinute: opts.connectionsPerIpPerMinute === undefined
157
+ ? DEFAULTS.connectionsPerIpPerMinute : opts.connectionsPerIpPerMinute,
158
+ authFailuresPerIpPer15Min: opts.authFailuresPerIpPer15Min === undefined
159
+ ? DEFAULTS.authFailuresPerIpPer15Min : opts.authFailuresPerIpPer15Min,
160
+ minBytesPerSecond: opts.minBytesPerSecond === undefined
161
+ ? DEFAULTS.minBytesPerSecond : opts.minBytesPerSecond,
162
+ disabled: opts.disabled === true,
163
+ };
164
+
165
+ // Per-IP state. Maps key on the remote IP string; entries are
166
+ // pruned lazily on read (any entry whose window has fully expired
167
+ // is removed instead of returned). Operators with extreme connection
168
+ // counts can wire a periodic gc() externally; the lazy prune keeps
169
+ // memory bounded under normal load.
170
+ var concurrentByIp = new Map(); // ip → integer count
171
+ var connectionTimes = new Map(); // ip → [timestampMs, ...]
172
+ var authFailureTimes = new Map(); // ip → [timestampMs, ...]
173
+
174
+ function _pruneWindow(arr, windowMs) {
175
+ var cutoff = Date.now() - windowMs;
176
+ var i = 0;
177
+ while (i < arr.length && arr[i] < cutoff) i += 1;
178
+ if (i > 0) arr.splice(0, i);
179
+ }
180
+
181
+ function _audit(action, outcome, metadata) {
182
+ try {
183
+ audit().safeEmit({ action: action, outcome: outcome || "denied", metadata: metadata || {} });
184
+ } catch (_e) { /* drop-silent — audit best-effort */ }
185
+ }
186
+
187
+ function admitConnection(ip) {
188
+ if (cfg.disabled) return { ok: true };
189
+ var concurrent = concurrentByIp.get(ip) || 0;
190
+ if (concurrent >= cfg.maxConcurrentConnectionsPerIp) {
191
+ _audit("mail.server.rate_limit.refused", "denied",
192
+ { reason: "concurrent-per-ip", ip: ip, cap: cfg.maxConcurrentConnectionsPerIp });
193
+ return { ok: false, reason: "concurrent-per-ip" };
194
+ }
195
+ var times = connectionTimes.get(ip);
196
+ if (!times) { times = []; connectionTimes.set(ip, times); }
197
+ _pruneWindow(times, CONNECTION_RATE_WINDOW_MS);
198
+ if (times.length >= cfg.connectionsPerIpPerMinute) {
199
+ _audit("mail.server.rate_limit.refused", "denied",
200
+ { reason: "rate-per-ip", ip: ip, cap: cfg.connectionsPerIpPerMinute });
201
+ return { ok: false, reason: "rate-per-ip" };
202
+ }
203
+ times.push(Date.now());
204
+ concurrentByIp.set(ip, concurrent + 1);
205
+ return { ok: true };
206
+ }
207
+
208
+ function releaseConnection(ip) {
209
+ if (cfg.disabled) return;
210
+ var concurrent = concurrentByIp.get(ip) || 0;
211
+ if (concurrent <= 1) concurrentByIp.delete(ip);
212
+ else concurrentByIp.set(ip, concurrent - 1);
213
+ // BUG-2 — CWE-400. authFailureTimes auto-deletes when its array
214
+ // empties in checkAuthAdmit; connectionTimes was the asymmetric
215
+ // case. Sweep this IP's rate-window now that it has released its
216
+ // last concurrent slot: if the per-minute window has fully
217
+ // expired AND there's no live connection, drop the entry so a
218
+ // botnet of unique IPs cannot grow the Map without bound.
219
+ if (!concurrentByIp.has(ip)) {
220
+ var arr = connectionTimes.get(ip);
221
+ if (arr) {
222
+ _pruneWindow(arr, CONNECTION_RATE_WINDOW_MS);
223
+ if (arr.length === 0) connectionTimes.delete(ip);
224
+ }
225
+ }
226
+ }
227
+
228
+ function checkAuthAdmit(ip) {
229
+ if (cfg.disabled) return { ok: true };
230
+ var times = authFailureTimes.get(ip);
231
+ if (!times) return { ok: true };
232
+ _pruneWindow(times, AUTH_FAILURE_WINDOW_MS);
233
+ if (times.length === 0) {
234
+ authFailureTimes.delete(ip);
235
+ return { ok: true };
236
+ }
237
+ if (times.length >= cfg.authFailuresPerIpPer15Min) {
238
+ _audit("mail.server.rate_limit.auth_refused", "denied",
239
+ { reason: "auth-failures-per-ip", ip: ip, cap: cfg.authFailuresPerIpPer15Min });
240
+ return { ok: false, reason: "auth-failures-per-ip" };
241
+ }
242
+ return { ok: true };
243
+ }
244
+
245
+ function noteAuthFailure(ip) {
246
+ if (cfg.disabled) return;
247
+ var times = authFailureTimes.get(ip);
248
+ if (!times) { times = []; authFailureTimes.set(ip, times); }
249
+ times.push(Date.now());
250
+ }
251
+
252
+ function minBytesPerSecond() { return cfg.disabled ? 0 : cfg.minBytesPerSecond; }
253
+ function isDisabled() { return cfg.disabled; }
254
+
255
+ return {
256
+ admitConnection: admitConnection,
257
+ releaseConnection: releaseConnection,
258
+ checkAuthAdmit: checkAuthAdmit,
259
+ noteAuthFailure: noteAuthFailure,
260
+ minBytesPerSecond: minBytesPerSecond,
261
+ isDisabled: isDisabled,
262
+ };
263
+ }
264
+
265
+ module.exports = {
266
+ create: create,
267
+ MailServerRateLimitError: MailServerRateLimitError,
268
+ DEFAULTS: DEFAULTS,
269
+ };