@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/NOTICE +1 -1
  3. package/README.md +5 -5
  4. package/lib/agent-audit.js +27 -2
  5. package/lib/ai-adverse-decision.js +18 -2
  6. package/lib/audit-sign.js +24 -5
  7. package/lib/auth/passkey.js +4 -1
  8. package/lib/codepoint-class.js +72 -0
  9. package/lib/cookies.js +7 -10
  10. package/lib/credential-hash.js +8 -1
  11. package/lib/crypto.js +7 -5
  12. package/lib/db-file-lifecycle.js +14 -3
  13. package/lib/db.js +505 -49
  14. package/lib/guard-auth.js +34 -11
  15. package/lib/guard-filename.js +41 -33
  16. package/lib/guard-html.js +10 -2
  17. package/lib/guard-list-unsubscribe.js +6 -1
  18. package/lib/guard-managesieve-command.js +73 -12
  19. package/lib/guard-regex.js +3 -5
  20. package/lib/guard-smtp-command.js +20 -4
  21. package/lib/guard-svg.js +6 -1
  22. package/lib/guard-yaml.js +60 -15
  23. package/lib/http-client.js +17 -3
  24. package/lib/mail-agent.js +29 -13
  25. package/lib/mail-arc-sign.js +40 -7
  26. package/lib/mail-auth.js +134 -22
  27. package/lib/mail-crypto-pgp.js +1 -1
  28. package/lib/mail-dkim.js +80 -11
  29. package/lib/mail-helo.js +10 -0
  30. package/lib/mail-rbl.js +10 -3
  31. package/lib/mail-send-deliver.js +151 -32
  32. package/lib/mail-server-imap.js +186 -89
  33. package/lib/mail-server-jmap.js +31 -4
  34. package/lib/mail-server-managesieve.js +198 -42
  35. package/lib/mail-server-mx.js +191 -38
  36. package/lib/mail-server-net.js +281 -1
  37. package/lib/mail-server-pop3.js +89 -41
  38. package/lib/mail-server-rate-limit.js +104 -6
  39. package/lib/mail-server-submission.js +183 -35
  40. package/lib/mail-server-tls.js +48 -3
  41. package/lib/mail-store.js +33 -11
  42. package/lib/mail.js +355 -17
  43. package/lib/mcp.js +11 -3
  44. package/lib/middleware/bearer-auth.js +6 -1
  45. package/lib/middleware/fetch-metadata.js +5 -1
  46. package/lib/middleware/headers.js +7 -10
  47. package/lib/middleware/require-mtls.js +8 -1
  48. package/lib/network-dns-resolver.js +71 -8
  49. package/lib/network-dns.js +26 -0
  50. package/lib/network-smtp-policy.js +42 -10
  51. package/lib/network-tls.js +18 -0
  52. package/lib/redact.js +13 -3
  53. package/lib/retention.js +22 -2
  54. package/lib/safe-mount-info.js +39 -6
  55. package/lib/safe-smtp.js +96 -1
  56. package/lib/safe-url.js +8 -2
  57. package/lib/self-update.js +4 -1
  58. package/lib/vendor/MANIFEST.json +12 -12
  59. package/lib/vendor/blamejs-pki.cjs +672 -75
  60. package/lib/watcher.js +31 -6
  61. package/lib/ws-client.js +17 -2
  62. package/lib/yaml-lex.js +55 -1
  63. package/package.json +1 -1
  64. package/sbom.cdx.json +6 -6
package/lib/watcher.js CHANGED
@@ -231,7 +231,7 @@ var AUTO_PROBE_POLL_FSTYPES = new Set([
231
231
  "vboxsf",
232
232
  ]);
233
233
 
234
- function _detectAutoMode(rootPath) {
234
+ function _detectAutoMode(rootPath, probe) {
235
235
  // Sync probe — looks at the kernel's view of the mount carrying the
236
236
  // watcher's root and decides whether fs.watch will deliver events.
237
237
  // Three signals contribute, in priority order:
@@ -246,7 +246,21 @@ function _detectAutoMode(rootPath) {
246
246
  // 3. Otherwise — fs.
247
247
  //
248
248
  // Returns { mode, reason, fsType, inContainer }.
249
- if (process.platform !== "linux") {
249
+ // Injectable so the branches that only exist on a restricted Linux can be
250
+ // driven from any host. A rule about what happens when mountinfo cannot be
251
+ // read is untestable while reaching it requires unmounting /proc.
252
+ var platform = (probe && probe.platform) || process.platform;
253
+ var readEntries = (probe && probe.readEntries) ||
254
+ function () { return safeMountInfo.read(); };
255
+ // Injected for the same reason as readEntries, and for one more: "/.dockerenv"
256
+ // is drive-relative on Windows, so a direct probe there asks about C:\.dockerenv
257
+ // rather than about being in a container. It is only reached below the
258
+ // non-Linux return, but reaching a POSIX path through the injected reader is
259
+ // what keeps that ordering from being the only thing holding it up.
260
+ var exists = (probe && probe.exists) ||
261
+ function (p) { return nodeFs.existsSync(p); };
262
+
263
+ if (platform !== "linux") {
250
264
  // macOS + Windows fs.watch backends use FSEvents + ReadDirectoryChangesW
251
265
  // respectively, which DO deliver recursive events natively for
252
266
  // operator-owned local filesystems. Containerized Linux is the
@@ -255,7 +269,7 @@ function _detectAutoMode(rootPath) {
255
269
  }
256
270
 
257
271
  var inContainer = false;
258
- try { inContainer = nodeFs.existsSync("/.dockerenv"); }
272
+ try { inContainer = exists("/.dockerenv"); }
259
273
  catch (_e) { inContainer = false; }
260
274
 
261
275
  // Route through b.safeMountInfo — single canonical parser that
@@ -263,13 +277,23 @@ function _detectAutoMode(rootPath) {
263
277
  // mount check below. Pre-v0.11.6 this parsed inline; centralizing
264
278
  // it means future container-escape / sealed-store / sandbox call
265
279
  // sites inherit the discipline.
266
- var entries = safeMountInfo.read();
280
+ var entries = readEntries();
281
+ // Not knowing which filesystem this is resolves toward polling, not toward
282
+ // fs.watch. On Linux mountinfo is always there, so failing to read it means
283
+ // /proc is unmounted or restricted — which is the containerized case this
284
+ // probe exists for, and the one where inotify is least likely to deliver.
285
+ // The two answers are not symmetric: polling costs stat calls the operator
286
+ // can see, while fs.watch on a filesystem that does not chain inotify drops
287
+ // events silently and nothing downstream can tell that it did.
267
288
  if (entries === null || entries.length === 0) {
268
- return { mode: "fs", reason: "no-mountinfo", fsType: null, inContainer: inContainer };
289
+ return { mode: "poll", reason: "no-mountinfo", fsType: null, inContainer: inContainer };
269
290
  }
270
291
  var bestMatch = safeMountInfo.bestMatch(entries, rootPath);
271
292
  if (!bestMatch) {
272
- return { mode: "fs", reason: "no-mount-match", fsType: null, inContainer: inContainer };
293
+ // Same reasoning: "/" matches every path on a healthy Linux, so no match
294
+ // means the mount table did not describe this root and its fstype is
295
+ // unknown rather than known-good.
296
+ return { mode: "poll", reason: "no-mount-match", fsType: null, inContainer: inContainer };
273
297
  }
274
298
  if (AUTO_PROBE_POLL_FSTYPES.has(bestMatch.fstype)) {
275
299
  return {
@@ -687,4 +711,5 @@ module.exports = {
687
711
  // create() enforces, instead of pinning hand-copied mirror constants.
688
712
  MAX_IGNORE_PATTERN_LEN: MAX_IGNORE_PATTERN_LEN,
689
713
  MAX_IGNORE_STAR_COUNT: MAX_IGNORE_STAR_COUNT,
714
+ _detectAutoModeForTest: _detectAutoMode,
690
715
  };
package/lib/ws-client.js CHANGED
@@ -361,7 +361,12 @@ class WsClient extends EventEmitter {
361
361
  allowInternal: opts.allowInternal,
362
362
  errorClass: WsClientError,
363
363
  });
364
- this._ssrfPinnedIps = probe && probe.ips ? probe.ips : null;
364
+ // `null` when the gate produced no pin at all, the array when it did —
365
+ // including an EMPTY one. An empty pin means the gate admitted no address,
366
+ // so there is nothing to connect to; treating it as "no pin" would hand the
367
+ // connect back to the ordinary resolver and re-open the rebinding window
368
+ // the pin exists to close.
369
+ this._ssrfPinnedIps = probe && Array.isArray(probe.ips) ? probe.ips : null;
365
370
  this._dialParsed = dialParsed;
366
371
  this._dialTlsOpts = dialTlsOpts;
367
372
  }
@@ -405,12 +410,22 @@ class WsClient extends EventEmitter {
405
410
  // private one between the check and the connect.
406
411
  var pinnedIps = self._ssrfPinnedIps || null;
407
412
  var lookup = null;
408
- if (pinnedIps && pinnedIps.length > 0) {
413
+ if (pinnedIps) {
409
414
  lookup = function (h, lookupOpts, cb) {
410
415
  // Node's lookup callback signatures:
411
416
  // (err, address, family) for legacy { all: false } (default)
412
417
  // (err, addresses) for { all: true }
413
418
  if (typeof lookupOpts === "function") { cb = lookupOpts; lookupOpts = {}; }
419
+ // An empty pin admits no address, so there is nothing to connect to.
420
+ // Answer the lookup with an error, which fails the dial through the
421
+ // socket's ordinary error path — reading `pinnedIps[0].address` here
422
+ // would throw a TypeError out of a lookup callback instead, which is a
423
+ // crash rather than a refusal.
424
+ if (pinnedIps.length === 0) {
425
+ var noneErr = new Error("ws.connect: the SSRF gate admitted no address for " + h);
426
+ noneErr.code = "ENOTFOUND";
427
+ return cb(noneErr);
428
+ }
414
429
  var first = pinnedIps[0];
415
430
  if (lookupOpts && lookupOpts.all) {
416
431
  return cb(null, pinnedIps.map(function (ip) {
package/lib/yaml-lex.js CHANGED
@@ -96,7 +96,44 @@ function _isVerbatimLine(line, indent) {
96
96
  return false;
97
97
  }
98
98
 
99
+ // maskNonStructural(text) → the mask alone, which is what most callers want.
99
100
  function maskNonStructural(text) {
101
+ return lexLines(text).masked;
102
+ }
103
+
104
+ // lexLines(text) → { masked, nodeStarts }.
105
+ //
106
+ // `nodeStarts` is a byte per source character, 1 where a NODE may begin: the
107
+ // first content character of a key, a value, or a flow-collection item. It
108
+ // answers the question the mask cannot, because the mask blanks a key's own text
109
+ // along with a comment's, and a screen that deliberately reports shapes which
110
+ // are not valid structure needs them apart.
111
+ //
112
+ // A byte array rather than a sparse one keyed by offset. Sparse indices this
113
+ // large put V8's array into dictionary mode, which measured ~200 bytes for each
114
+ // node in the document: a 1.7 MB file of short keys allocated 71 MB, a 40x
115
+ // amplification an attacker picks by choosing the shape of the input. A byte per
116
+ // character is 1x, flat, and known before the scan starts.
117
+ //
118
+ // The merge-key screen is that kind of screen. `<<:*d` with no space after the
119
+ // colon is not a mapping entry, and is reported anyway, because a parser variant
120
+ // may read it as a merge and that is the shape being smuggled. So its test
121
+ // cannot be "is this well-formed structure?" — it has to be "could a node begin
122
+ // here at all?", which is false inside a comment, a quoted body, a block
123
+ // scalar's body, and a plain scalar's continuation line, and true for both
124
+ // `<<: *b` and `<<:*b`.
125
+ //
126
+ // Enumerating those regions instead was tried and is the wrong shape: a first
127
+ // version excluded block-scalar bodies and quoted bodies and still reported a
128
+ // merge key written in a comment, on a plain scalar's continuation line, and
129
+ // after a document marker. There are more ways to be inside literal text than
130
+ // anyone lists from memory, and the lexer is already tracking the one property
131
+ // that separates them.
132
+ //
133
+ // Both come out of ONE pass. A second function walking the same grammar is how
134
+ // two answers to one question start disagreeing, which is the defect this
135
+ // module was written to end.
136
+ function lexLines(text) {
100
137
  var src = String(text == null ? "" : text);
101
138
  // Split on the newline ALONE, keeping any `\r` with the line it terminates.
102
139
  // `codepointClass.splitLines` strips it, and rejoining those with "\n" drops
@@ -106,6 +143,13 @@ function maskNonStructural(text) {
106
143
  // mask, because the locations it reports are confidently wrong.
107
144
  var lines = src.split("\n");
108
145
  var out = [];
146
+ var nodeStarts = new Uint8Array(src.length);
147
+ // The absolute offset line `li` begins at. Derived at the TOP of each
148
+ // iteration from the PREVIOUS line's raw length, rather than advanced at the
149
+ // bottom, because the loop body leaves by a dozen different `continue`s and
150
+ // any one of them forgetting to advance would silently shift every offset
151
+ // after it.
152
+ var lineBase = 0;
109
153
  // A block scalar's body indentation is DETECTED from its first content line,
110
154
  // which is what YAML specifies and the only thing that works. Two fixed
111
155
  // columns were tried and each failed the other's case: measuring from the
@@ -149,6 +193,7 @@ function maskNonStructural(text) {
149
193
  var plainFromDash = false;
150
194
 
151
195
  for (var li = 0; li < lines.length; li += 1) {
196
+ if (li > 0) lineBase += lines[li - 1].length + 1; // + the "\n" that ended it
152
197
  var raw = lines[li];
153
198
  // The carriage return of a CRLF pair is a line TERMINATOR, not content, so
154
199
  // it is held aside and put back verbatim. Letting it into the scan would
@@ -283,6 +328,14 @@ function maskNonStructural(text) {
283
328
  var jsonKeyBefore = prevWasJsonKey;
284
329
  prevWasJsonKey = false;
285
330
 
331
+ // The first content character of a node position. Whitespace carries the
332
+ // flag rather than clearing it, so this lands on the character that
333
+ // actually starts the node, and a `#` here opens a comment instead — no
334
+ // node begins inside one.
335
+ if (atNodeStart && !_isPlainSpace(ch) && ch !== "#") {
336
+ nodeStarts[lineBase + i] = 1;
337
+ }
338
+
286
339
  // A comment opens on `#` at the start of the content or after whitespace,
287
340
  // and runs to the end of the line. Its text is not YAML, whatever it
288
341
  // says: `x: 1 # note !bang` names no tag.
@@ -478,7 +531,7 @@ function maskNonStructural(text) {
478
531
  }
479
532
  out.push(masked + (cr ? "\r" : ""));
480
533
  }
481
- return out.join("\n");
534
+ return { masked: out.join("\n"), nodeStarts: nodeStarts };
482
535
  }
483
536
 
484
537
  // A line kept for its structure, with any comment on it masked. Used for the
@@ -530,4 +583,5 @@ function _blanked(s) {
530
583
 
531
584
  module.exports = {
532
585
  maskNonStructural: maskNonStructural,
586
+ lexLines: lexLines,
533
587
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.53",
3
+ "version": "0.18.55",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:f52f2ee6-253b-4bf4-9915-22d453a4407a",
5
+ "serialNumber": "urn:uuid:e557e6e1-623d-4598-87fa-caac0c468199",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-24T12:08:00.505Z",
8
+ "timestamp": "2026-08-27T07:20:33.894Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.18.53",
22
+ "bom-ref": "@blamejs/core@0.18.55",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.53",
25
+ "version": "0.18.55",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.18.53",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.55",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.18.53",
57
+ "ref": "@blamejs/core@0.18.55",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]