@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
@@ -1250,6 +1250,44 @@ function isIdentifierChar(cc) {
1250
1250
  return isAsciiAlnum(cc) || cc === 0x5F; // "_"
1251
1251
  }
1252
1252
 
1253
+ /**
1254
+ * @primitive b.codepointClass.hasPairWhere
1255
+ * @signature b.codepointClass.hasPairWhere(text, first, second, accept)
1256
+ * @since 0.18.54
1257
+ * @status stable
1258
+ * @related b.codepointClass.splitLines
1259
+ *
1260
+ * Is there a place where `first` is immediately followed by `second`, and
1261
+ * `accept` says that place counts? `accept(i)` is called with the index of
1262
+ * `first`, and returns whether the construct opening there is a real one.
1263
+ *
1264
+ * This is the frame every two-character construct screen was writing out by
1265
+ * hand: a path traversal opening on `..`, a regex inline-flag group on `(?`, a
1266
+ * YAML merge key on `<<`. The literals and the follow-on tests differ, the scan
1267
+ * does not, and three copies of a scan are three places for an off-by-one to
1268
+ * live. It reads the string one character at a time, which is the property a
1269
+ * guard needs: asking the question with a pattern would be a screen running the
1270
+ * construct it screens over hostile text.
1271
+ *
1272
+ * `accept` decides alone — the scan does not skip past a hit it rejected, so
1273
+ * overlapping openers are all offered. A screen that needs to COUNT
1274
+ * non-overlapping occurrences is asking a different question and keeps its own
1275
+ * cursor.
1276
+ *
1277
+ * @example
1278
+ * var CP = b.codepointClass;
1279
+ * CP.hasPairWhere("a/../b", ".", ".", function () { return true; }); // → true
1280
+ * CP.hasPairWhere("a.b", ".", ".", function () { return true; }); // → false
1281
+ */
1282
+ function hasPairWhere(text, first, second, accept) {
1283
+ if (typeof text !== "string") return false;
1284
+ for (var i = 0; i + 1 < text.length; i += 1) {
1285
+ if (text.charAt(i) !== first || text.charAt(i + 1) !== second) continue;
1286
+ if (accept(i)) return true;
1287
+ }
1288
+ return false;
1289
+ }
1290
+
1253
1291
  /**
1254
1292
  * @primitive b.codepointClass.splitLines
1255
1293
  * @signature b.codepointClass.splitLines(text)
@@ -1424,6 +1462,38 @@ function firstControlCharOffset(s, opts) {
1424
1462
  return -1;
1425
1463
  }
1426
1464
 
1465
+ /**
1466
+ * @primitive b.codepointClass.firstLineInjectionCharOffset
1467
+ * @signature b.codepointClass.firstLineInjectionCharOffset(s)
1468
+ * @since 0.18.54
1469
+ * @status stable
1470
+ * @related b.codepointClass.firstControlCharOffset
1471
+ *
1472
+ * Return the index of the first CR, LF or NUL in `s`, or `-1` when there is
1473
+ * none. These three are the bytes that end a line-oriented protocol record,
1474
+ * so a value carrying one splits the record it is written into and the
1475
+ * remainder is read as a second, attacker-chosen line: the header-injection
1476
+ * class in HTTP and Set-Cookie, and the command-injection class in SMTP,
1477
+ * POP3, IMAP and ManageSieve.
1478
+ *
1479
+ * Narrower than `firstControlCharOffset`, deliberately. That one refuses every
1480
+ * C0 control and DEL, which is right for text a human wrote; this one answers
1481
+ * the specific question a wire-protocol writer asks, so a caller adopting it
1482
+ * does not silently start refusing values it used to accept.
1483
+ *
1484
+ * @example
1485
+ * b.codepointClass.firstLineInjectionCharOffset("nonce-42"); // -1
1486
+ * b.codepointClass.firstLineInjectionCharOffset("abc\r\nOK"); // 3
1487
+ */
1488
+ function firstLineInjectionCharOffset(s) {
1489
+ if (typeof s !== "string") return -1;
1490
+ for (var i = 0; i < s.length; i += 1) {
1491
+ var c = s.charCodeAt(i);
1492
+ if (c === 0x0d || c === 0x0a || c === 0x00) return i; // CR / LF / NUL
1493
+ }
1494
+ return -1;
1495
+ }
1496
+
1427
1497
  // Decode HTML numeric character references (hex &#x..; and decimal &#..;) just
1428
1498
  // enough to expose a scheme hidden behind entity-encoding. The trailing
1429
1499
  // semicolon is OPTIONAL — a browser decodes `&#106avascript:` (no semicolon)
@@ -1743,6 +1813,7 @@ module.exports = {
1743
1813
  caseFoldPartners: caseFoldPartners,
1744
1814
  isForbiddenControlChar: isForbiddenControlChar,
1745
1815
  firstControlCharOffset: firstControlCharOffset,
1816
+ firstLineInjectionCharOffset: firstLineInjectionCharOffset,
1746
1817
  decodeNumericEntities: decodeNumericEntities,
1747
1818
  decodeMarkupEntities: decodeMarkupEntities,
1748
1819
  NAMED_ENTITY_ASCII: NAMED_ENTITY_ASCII,
@@ -1769,6 +1840,7 @@ module.exports = {
1769
1840
  isAsciiDigit: isAsciiDigit,
1770
1841
  isAsciiHexDigit: isAsciiHexDigit,
1771
1842
  isIdentifierChar: isIdentifierChar,
1843
+ hasPairWhere: hasPairWhere,
1772
1844
  splitLines: splitLines,
1773
1845
  splitLinesAny: splitLinesAny,
1774
1846
  splitOnWhitespace: splitOnWhitespace,
package/lib/cookies.js CHANGED
@@ -591,16 +591,13 @@ function parseSafe(cookieHeader, opts) {
591
591
  });
592
592
  return { jar: jar, issues: issues };
593
593
  }
594
- for (var hi = 0; hi < cookieHeader.length; hi += 1) {
595
- var ch = cookieHeader.charCodeAt(hi);
596
- if (ch === 0x0D || ch === 0x0A || ch === 0x00) { // CR / LF / NUL forbidden in cookie header
597
- issues.push({
598
- kind: "header-control-byte", severity: "high",
599
- snippet: "Cookie header contains CR / LF / NUL — proxy-side " +
600
- "header injection vector",
601
- });
602
- return { jar: jar, issues: issues };
603
- }
594
+ if (codepointClass.firstLineInjectionCharOffset(cookieHeader) !== -1) {
595
+ issues.push({
596
+ kind: "header-control-byte", severity: "high",
597
+ snippet: "Cookie header contains CR / LF / NUL — proxy-side " +
598
+ "header injection vector",
599
+ });
600
+ return { jar: jar, issues: issues };
604
601
  }
605
602
 
606
603
  var pairs = cookieHeader.split(/;\s*/);
@@ -372,8 +372,15 @@ function inspect(envelope) {
372
372
  function needsRehash(envelope, opts) {
373
373
  var decoded = _decodeEnvelope(envelope);
374
374
  if (!decoded) return true; // unrecognized → migrate aggressively
375
+ // Same validation hash() applies to the same option. Reading the table with
376
+ // a fallback to the default meant a misspelled algo silently answered a
377
+ // different question than the caller asked — "is this row still the default?"
378
+ // instead of "is this row argon2id?" — on a credential-rotation decision. It
379
+ // also read through the prototype: algo "constructor" yielded a function,
380
+ // which compares unequal to every real algorithm id.
381
+ _validateOpts(opts);
375
382
  var targetAlgoName = (opts && opts.algo) || DEFAULTS.algo;
376
- var targetId = NAME_TO_ID[targetAlgoName] || C.ACTIVE.CRED_HASH;
383
+ var targetId = NAME_TO_ID[targetAlgoName];
377
384
  if (decoded.algoId !== targetId) return true;
378
385
  if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
379
386
  // Defer the parameter-lag check to the password primitive's
package/lib/crypto.js CHANGED
@@ -1815,7 +1815,8 @@ function encryptMlkem768X25519(plaintext, recipient) {
1815
1815
  // algorithm gets a clear error rather than the generic "unsupported
1816
1816
  // KEM ID" path.
1817
1817
  //
1818
- // recipient: { privateKey, x25519PrivateKey } — operator's keys
1818
+ // recipient: the operator's two private keys, named privateKey (ML-KEM-768)
1819
+ // and x25519PrivateKey (X25519)
1819
1820
  // ciphertext: base64 envelope from encryptMlkem768X25519
1820
1821
  /**
1821
1822
  * @primitive b.crypto.decryptMlkem768X25519
@@ -1827,8 +1828,9 @@ function encryptMlkem768X25519(plaintext, recipient) {
1827
1828
  * envelope whose KEM ID byte is not `ML_KEM_768_X25519` so an
1828
1829
  * operator who calls this with a ciphertext sealed under a different
1829
1830
  * algorithm gets a clear error rather than the generic dispatch path.
1830
- * Recipient shape is `{ privateKey, x25519PrivateKey }` `privateKey`
1831
- * is the ML-KEM-768 PEM, NOT the framework default ML-KEM-1024.
1831
+ * The recipient carries two private keys: `privateKey`, which is the
1832
+ * ML-KEM-768 PEM and NOT the framework default ML-KEM-1024, and
1833
+ * `x25519PrivateKey`, which is the X25519 PEM.
1832
1834
  *
1833
1835
  * @example
1834
1836
  * var pair = b.crypto.generateMlkem768X25519KeyPair();
@@ -1845,8 +1847,8 @@ function encryptMlkem768X25519(plaintext, recipient) {
1845
1847
  function decryptMlkem768X25519(ciphertext, recipient) {
1846
1848
  if (!recipient || typeof recipient !== "object" ||
1847
1849
  !recipient.privateKey || !recipient.x25519PrivateKey) {
1848
- throw new Error("decryptMlkem768X25519 requires { privateKey, x25519PrivateKey } " +
1849
- "(privateKey is the ML-KEM-768 PEM, x25519PrivateKey is the X25519 PEM)");
1850
+ throw new Error("decryptMlkem768X25519 requires both a privateKey, which is the " +
1851
+ "ML-KEM-768 PEM, and an x25519PrivateKey, which is the X25519 PEM");
1850
1852
  }
1851
1853
  var packed = Buffer.from(ciphertext, "base64");
1852
1854
  if (packed[0] !== C.ENVELOPE_MAGIC) {
@@ -90,12 +90,17 @@ function _aad(dataDir, label) {
90
90
  return Buffer.from("blamejs.db-file-lifecycle.v1\0" + label + "\0" + (dataDir || ""), "utf8");
91
91
  }
92
92
 
93
- function _resolveTmpDir(operatorTmpDir, allowDiskFallback) {
93
+ // Platform and stat are parameters so the non-Linux branch is reachable from a
94
+ // Linux CI host. They also keep the "/dev/shm" literal off a direct fs call:
95
+ // the path is only meaningful on Linux, and a leading slash on Windows is
96
+ // drive-relative, so an unguarded probe there asks about C:\dev\shm and can be
97
+ // answered yes by any directory an unprivileged user creates.
98
+ function _resolveTmpDirFrom(operatorTmpDir, allowDiskFallback, platform, stat) {
94
99
  if (operatorTmpDir) return operatorTmpDir;
95
100
  // Linux: /dev/shm is the standard tmpfs mount.
96
- if (process.platform === "linux") {
101
+ if (platform === "linux") {
97
102
  try {
98
- var st = nodeFs.statSync("/dev/shm");
103
+ var st = stat("/dev/shm");
99
104
  if (st && st.isDirectory()) return "/dev/shm";
100
105
  } catch (_e) { /* fall through */ }
101
106
  }
@@ -108,6 +113,11 @@ function _resolveTmpDir(operatorTmpDir, allowDiskFallback) {
108
113
  "OR set opts.allowDiskFallback: true to accept disk-backed temporary storage.");
109
114
  }
110
115
 
116
+ function _resolveTmpDir(operatorTmpDir, allowDiskFallback) {
117
+ return _resolveTmpDirFrom(operatorTmpDir, allowDiskFallback,
118
+ process.platform, nodeFs.statSync);
119
+ }
120
+
111
121
  /**
112
122
  * @primitive b.db.fileLifecycle
113
123
  * @signature b.db.fileLifecycle(opts)
@@ -342,4 +352,5 @@ function fileLifecycle(opts) {
342
352
  module.exports = {
343
353
  fileLifecycle: fileLifecycle,
344
354
  DbFileLifecycleError: DbFileLifecycleError,
355
+ _resolveTmpDirFromForTest: _resolveTmpDirFrom,
345
356
  };