@blamejs/core 0.15.11 → 0.15.13

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 (305) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +7 -23
  4. package/lib/acme.js +13 -16
  5. package/lib/agent-event-bus.js +5 -4
  6. package/lib/agent-idempotency.js +2 -5
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-tenant.js +2 -5
  10. package/lib/ai-adverse-decision.js +2 -15
  11. package/lib/ai-capability.js +1 -6
  12. package/lib/ai-dp.js +1 -5
  13. package/lib/ai-input.js +2 -2
  14. package/lib/ai-pref.js +3 -8
  15. package/lib/ai-quota.js +3 -14
  16. package/lib/api-key.js +37 -28
  17. package/lib/archive-adapters.js +2 -4
  18. package/lib/archive-entry-policy.js +32 -0
  19. package/lib/archive-read.js +5 -17
  20. package/lib/archive-tar-read.js +5 -16
  21. package/lib/archive.js +2 -10
  22. package/lib/arg-parser.js +7 -6
  23. package/lib/asyncapi-traits.js +2 -6
  24. package/lib/atomic-file.js +108 -31
  25. package/lib/audit-chain.js +133 -53
  26. package/lib/audit-daily-review.js +24 -14
  27. package/lib/audit-emit.js +82 -0
  28. package/lib/audit-sign.js +257 -0
  29. package/lib/audit.js +84 -0
  30. package/lib/auth/access-lock.js +5 -27
  31. package/lib/auth/dpop.js +23 -35
  32. package/lib/auth/fido-mds3.js +9 -15
  33. package/lib/auth/jwt-external.js +26 -8
  34. package/lib/auth/jwt.js +13 -15
  35. package/lib/auth/lockout.js +6 -25
  36. package/lib/auth/oauth.js +67 -45
  37. package/lib/auth/oid4vci.js +55 -32
  38. package/lib/auth/oid4vp.js +3 -2
  39. package/lib/auth/openid-federation.js +6 -6
  40. package/lib/auth/passkey.js +3 -3
  41. package/lib/auth/password.js +7 -1
  42. package/lib/auth/saml.js +37 -27
  43. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  44. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  45. package/lib/auth/sd-jwt-vc.js +1 -1
  46. package/lib/auth/status-list.js +7 -7
  47. package/lib/auth/step-up.js +6 -12
  48. package/lib/auth-bot-challenge.js +6 -37
  49. package/lib/backup/bundle.js +11 -18
  50. package/lib/backup/index.js +14 -47
  51. package/lib/bounded-map.js +112 -1
  52. package/lib/breach-deadline.js +1 -11
  53. package/lib/cache.js +7 -18
  54. package/lib/cbor.js +2 -1
  55. package/lib/cdn-cache-control.js +8 -9
  56. package/lib/cert.js +3 -10
  57. package/lib/chain-writer.js +162 -47
  58. package/lib/cli.js +5 -1
  59. package/lib/client-hints.js +10 -10
  60. package/lib/cloud-events.js +40 -31
  61. package/lib/cluster-storage.js +2 -38
  62. package/lib/cluster.js +4 -2
  63. package/lib/cms-codec.js +2 -1
  64. package/lib/codepoint-class.js +67 -1
  65. package/lib/compliance-ai-act-logging.js +1 -6
  66. package/lib/compliance-ai-act-transparency.js +2 -4
  67. package/lib/compliance-eaa.js +1 -11
  68. package/lib/compliance-sanctions-fetcher.js +21 -28
  69. package/lib/compliance-sanctions.js +2 -14
  70. package/lib/compliance.js +2 -9
  71. package/lib/config-drift.js +2 -12
  72. package/lib/content-digest.js +10 -8
  73. package/lib/cookies.js +5 -11
  74. package/lib/cose.js +19 -11
  75. package/lib/cra-report.js +1 -11
  76. package/lib/crypto-field.js +5 -10
  77. package/lib/crypto.js +120 -3
  78. package/lib/csp.js +235 -3
  79. package/lib/daemon.js +42 -41
  80. package/lib/data-act.js +19 -9
  81. package/lib/db-query.js +6 -40
  82. package/lib/db.js +34 -12
  83. package/lib/dbsc.js +5 -6
  84. package/lib/ddl-change-control.js +18 -14
  85. package/lib/deprecate.js +4 -5
  86. package/lib/did.js +6 -2
  87. package/lib/dsr.js +8 -12
  88. package/lib/external-db-migrate.js +21 -22
  89. package/lib/external-db.js +14 -26
  90. package/lib/fda-21cfr11.js +12 -8
  91. package/lib/fdx.js +22 -18
  92. package/lib/file-upload.js +80 -66
  93. package/lib/flag-evaluation-context.js +5 -8
  94. package/lib/framework-error.js +12 -0
  95. package/lib/framework-schema.js +19 -0
  96. package/lib/fsm.js +7 -12
  97. package/lib/gate-contract.js +869 -38
  98. package/lib/gdpr-ropa.js +4 -13
  99. package/lib/graphql-federation.js +1 -1
  100. package/lib/guard-agent-registry.js +2 -1
  101. package/lib/guard-all.js +1 -0
  102. package/lib/guard-archive.js +9 -30
  103. package/lib/guard-auth.js +23 -80
  104. package/lib/guard-cidr.js +20 -96
  105. package/lib/guard-csv.js +135 -196
  106. package/lib/guard-domain.js +23 -106
  107. package/lib/guard-dsn.js +16 -13
  108. package/lib/guard-email.js +46 -53
  109. package/lib/guard-envelope.js +1 -1
  110. package/lib/guard-event-bus-topic.js +2 -1
  111. package/lib/guard-filename.js +18 -62
  112. package/lib/guard-graphql.js +28 -75
  113. package/lib/guard-html-wcag.js +15 -2
  114. package/lib/guard-html.js +65 -117
  115. package/lib/guard-idempotency-key.js +2 -1
  116. package/lib/guard-image.js +280 -77
  117. package/lib/guard-imap-command.js +8 -9
  118. package/lib/guard-json.js +87 -103
  119. package/lib/guard-jsonpath.js +20 -88
  120. package/lib/guard-jwt.js +32 -114
  121. package/lib/guard-list-id.js +2 -7
  122. package/lib/guard-list-unsubscribe.js +2 -7
  123. package/lib/guard-mail-compose.js +5 -6
  124. package/lib/guard-mail-move.js +2 -1
  125. package/lib/guard-mail-query.js +5 -3
  126. package/lib/guard-mail-sieve.js +6 -7
  127. package/lib/guard-managesieve-command.js +5 -4
  128. package/lib/guard-markdown.js +76 -110
  129. package/lib/guard-message-id.js +5 -6
  130. package/lib/guard-mime.js +20 -99
  131. package/lib/guard-oauth.js +19 -73
  132. package/lib/guard-pdf.js +65 -72
  133. package/lib/guard-pop3-command.js +12 -13
  134. package/lib/guard-posture-chain.js +2 -1
  135. package/lib/guard-regex.js +24 -99
  136. package/lib/guard-saga-config.js +2 -1
  137. package/lib/guard-shell.js +22 -87
  138. package/lib/guard-smtp-command.js +8 -11
  139. package/lib/guard-sql.js +15 -13
  140. package/lib/guard-stream-args.js +2 -1
  141. package/lib/guard-svg.js +95 -140
  142. package/lib/guard-template.js +23 -80
  143. package/lib/guard-tenant-id.js +2 -1
  144. package/lib/guard-text.js +592 -0
  145. package/lib/guard-time.js +27 -95
  146. package/lib/guard-uuid.js +21 -93
  147. package/lib/guard-xml.js +76 -106
  148. package/lib/guard-yaml.js +24 -60
  149. package/lib/http-client-cache.js +8 -13
  150. package/lib/http-client-cookie-jar.js +2 -4
  151. package/lib/http-client.js +8 -21
  152. package/lib/http-message-signature.js +26 -8
  153. package/lib/i18n-messageformat.js +5 -7
  154. package/lib/i18n.js +83 -26
  155. package/lib/inbox.js +8 -8
  156. package/lib/incident-report.js +3 -21
  157. package/lib/ip-utils.js +49 -6
  158. package/lib/jobs.js +3 -2
  159. package/lib/keychain.js +6 -18
  160. package/lib/legal-hold.js +6 -15
  161. package/lib/log-stream-cloudwatch.js +44 -112
  162. package/lib/log-stream-otlp-grpc.js +28 -14
  163. package/lib/log-stream-otlp.js +16 -80
  164. package/lib/log-stream-syslog.js +6 -0
  165. package/lib/log-stream-webhook.js +20 -92
  166. package/lib/log.js +24 -2
  167. package/lib/mail-arc-sign.js +8 -3
  168. package/lib/mail-arf.js +3 -2
  169. package/lib/mail-auth.js +40 -66
  170. package/lib/mail-bimi.js +19 -39
  171. package/lib/mail-crypto-pgp.js +3 -2
  172. package/lib/mail-dav.js +8 -35
  173. package/lib/mail-deploy.js +6 -9
  174. package/lib/mail-dkim.js +19 -38
  175. package/lib/mail-greylist.js +15 -26
  176. package/lib/mail-helo.js +2 -3
  177. package/lib/mail-journal.js +2 -1
  178. package/lib/mail-mdn.js +4 -3
  179. package/lib/mail-rbl.js +5 -8
  180. package/lib/mail-scan.js +2 -2
  181. package/lib/mail-send-deliver.js +33 -20
  182. package/lib/mail-server-imap.js +32 -87
  183. package/lib/mail-server-jmap.js +35 -51
  184. package/lib/mail-server-managesieve.js +29 -83
  185. package/lib/mail-server-mx.js +33 -74
  186. package/lib/mail-server-net.js +177 -0
  187. package/lib/mail-server-pop3.js +30 -83
  188. package/lib/mail-server-rate-limit.js +30 -6
  189. package/lib/mail-server-submission.js +34 -73
  190. package/lib/mail-server-tls.js +89 -0
  191. package/lib/mail-spam-score.js +7 -8
  192. package/lib/mail-store.js +3 -2
  193. package/lib/mail.js +11 -11
  194. package/lib/markup-escape.js +31 -0
  195. package/lib/markup-tokenizer.js +54 -0
  196. package/lib/mcp-tool-registry.js +26 -23
  197. package/lib/mcp.js +1 -1
  198. package/lib/mdoc.js +11 -12
  199. package/lib/metrics.js +32 -30
  200. package/lib/middleware/age-gate.js +3 -23
  201. package/lib/middleware/api-encrypt.js +11 -22
  202. package/lib/middleware/assetlinks.js +2 -7
  203. package/lib/middleware/bearer-auth.js +2 -1
  204. package/lib/middleware/body-parser.js +79 -54
  205. package/lib/middleware/bot-disclose.js +7 -10
  206. package/lib/middleware/bot-guard.js +2 -10
  207. package/lib/middleware/csrf-protect.js +13 -2
  208. package/lib/middleware/daily-byte-quota.js +6 -26
  209. package/lib/middleware/deny-response.js +19 -1
  210. package/lib/middleware/fetch-metadata.js +7 -0
  211. package/lib/middleware/idempotency-key.js +4 -20
  212. package/lib/middleware/rate-limit.js +20 -28
  213. package/lib/middleware/scim-server.js +3 -2
  214. package/lib/middleware/security-headers.js +9 -1
  215. package/lib/middleware/security-txt.js +2 -6
  216. package/lib/middleware/tus-upload.js +12 -27
  217. package/lib/middleware/web-app-manifest.js +2 -7
  218. package/lib/migrations.js +4 -13
  219. package/lib/mime-parse.js +34 -17
  220. package/lib/module-loader.js +44 -0
  221. package/lib/money.js +105 -0
  222. package/lib/mtls-ca.js +116 -36
  223. package/lib/network-byte-quota.js +4 -18
  224. package/lib/network-dane.js +8 -6
  225. package/lib/network-dns-resolver.js +97 -6
  226. package/lib/network-dns.js +22 -26
  227. package/lib/network-dnssec.js +16 -16
  228. package/lib/network-heartbeat.js +6 -5
  229. package/lib/network-proxy.js +3 -7
  230. package/lib/network-smtp-policy.js +29 -41
  231. package/lib/network-tls.js +74 -55
  232. package/lib/network.js +2 -6
  233. package/lib/nis2-report.js +1 -11
  234. package/lib/nonce-store.js +81 -3
  235. package/lib/notify.js +7 -12
  236. package/lib/numeric-bounds.js +22 -8
  237. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  238. package/lib/object-store/azure-blob.js +5 -45
  239. package/lib/object-store/gcs.js +8 -71
  240. package/lib/object-store/http-put.js +1 -5
  241. package/lib/object-store/http-request.js +128 -0
  242. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  243. package/lib/object-store/sigv4.js +9 -77
  244. package/lib/observability-otlp-exporter.js +9 -25
  245. package/lib/observability.js +95 -0
  246. package/lib/openapi-paths-builder.js +7 -3
  247. package/lib/otel-export.js +7 -10
  248. package/lib/outbox.js +43 -37
  249. package/lib/pagination.js +11 -15
  250. package/lib/parsers/safe-env.js +5 -4
  251. package/lib/parsers/safe-ini.js +10 -4
  252. package/lib/parsers/safe-toml.js +11 -9
  253. package/lib/parsers/safe-xml.js +2 -2
  254. package/lib/parsers/safe-yaml.js +7 -6
  255. package/lib/pick.js +63 -5
  256. package/lib/pipl-cn.js +69 -59
  257. package/lib/privacy-pass.js +8 -6
  258. package/lib/problem-details.js +2 -5
  259. package/lib/pubsub.js +4 -8
  260. package/lib/redact.js +2 -1
  261. package/lib/render.js +4 -2
  262. package/lib/request-helpers.js +133 -6
  263. package/lib/restore.js +5 -22
  264. package/lib/retention.js +3 -5
  265. package/lib/safe-async.js +457 -0
  266. package/lib/safe-buffer.js +137 -6
  267. package/lib/safe-decompress.js +2 -1
  268. package/lib/safe-dns.js +2 -1
  269. package/lib/safe-ical.js +12 -26
  270. package/lib/safe-json.js +7 -8
  271. package/lib/safe-jsonpath.js +6 -5
  272. package/lib/safe-mime.js +25 -29
  273. package/lib/safe-redirect.js +2 -5
  274. package/lib/safe-schema.js +7 -6
  275. package/lib/safe-sql.js +126 -0
  276. package/lib/safe-vcard.js +12 -26
  277. package/lib/sandbox-worker.js +9 -2
  278. package/lib/sandbox.js +3 -2
  279. package/lib/scheduler.js +5 -12
  280. package/lib/sec-cyber.js +82 -37
  281. package/lib/seeders.js +9 -21
  282. package/lib/self-update.js +92 -69
  283. package/lib/session-device-binding.js +112 -66
  284. package/lib/session.js +194 -6
  285. package/lib/sql.js +225 -78
  286. package/lib/sse.js +6 -10
  287. package/lib/static.js +136 -98
  288. package/lib/storage.js +4 -2
  289. package/lib/structured-fields.js +313 -17
  290. package/lib/tenant-quota.js +2 -20
  291. package/lib/tsa.js +11 -15
  292. package/lib/validate-opts.js +154 -8
  293. package/lib/vault/seal-pem-file.js +30 -48
  294. package/lib/vault-aad.js +3 -2
  295. package/lib/vc.js +2 -7
  296. package/lib/vex.js +35 -13
  297. package/lib/web-push-vapid.js +23 -20
  298. package/lib/webhook-dispatcher.js +706 -0
  299. package/lib/webhook.js +43 -18
  300. package/lib/websocket-channels.js +9 -7
  301. package/lib/websocket.js +7 -3
  302. package/lib/worm.js +2 -3
  303. package/lib/ws-client.js +8 -10
  304. package/package.json +1 -1
  305. package/sbom.cdx.json +6 -6
@@ -27,6 +27,7 @@
27
27
 
28
28
  var lazyRequire = require("../lazy-require");
29
29
  var validateOpts = require("../validate-opts");
30
+ var denyResponse = require("./deny-response");
30
31
  var { defineClass } = require("../framework-error");
31
32
 
32
33
  var SecurityTxtError = defineClass("SecurityTxtError", { alwaysPermanent: true });
@@ -143,12 +144,7 @@ function create(opts) {
143
144
  (alsoAtRoot && path === "/security.txt");
144
145
  if (!matches) return next();
145
146
  if (req.method !== "GET" && req.method !== "HEAD") {
146
- res.writeHead(405, { // HTTP 405 status
147
- "Allow": "GET, HEAD",
148
- "Content-Type": "text/plain; charset=utf-8",
149
- "Content-Length": 18, // len of "Method Not Allowed"
150
- });
151
- res.end("Method Not Allowed");
147
+ denyResponse.methodNotAllowed(res, "GET, HEAD");
152
148
  return;
153
149
  }
154
150
  res.writeHead(200, { // HTTP 200 status
@@ -150,10 +150,10 @@ function _parseChecksumHeader(headerValue, allowedSet) {
150
150
  // alphabet); refuse those on the RAW value BEFORE the slice/trim
151
151
  // normalisation (same v0.8.90 trim-before-validate bug class).
152
152
  if (structuredFields.containsControlBytes(headerValue)) return { error: "malformed" };
153
- var sp = headerValue.indexOf(" ");
154
- if (sp === -1) return { error: "malformed" };
155
- var algo = headerValue.slice(0, sp).trim().toLowerCase();
156
- var digestB64 = headerValue.slice(sp + 1).trim();
153
+ var kvp = structuredFields.parseKeyValuePiece(headerValue, " ");
154
+ if (kvp.value === null) return { error: "malformed" };
155
+ var algo = kvp.key;
156
+ var digestB64 = kvp.value.trim();
157
157
  if (algo.length === 0 || digestB64.length === 0) return { error: "malformed" };
158
158
  if (!allowedSet[algo]) return { error: "algo-unsupported" };
159
159
  if (!/^[A-Za-z0-9+/=]+$/.test(digestB64)) return { error: "malformed" };
@@ -311,19 +311,11 @@ function _emitTusBaseHeaders(res, extra) {
311
311
  }
312
312
 
313
313
  function _readChunk(req, maxChunkSize) {
314
- return new Promise(function (resolve, reject) {
315
- var collector = safeBuffer.boundedChunkCollector({
316
- maxBytes: maxChunkSize,
317
- errorClass: TusError,
318
- sizeCode: "tus/chunk-too-large",
319
- sizeMessage: "PATCH body exceeded maxChunkSize",
320
- });
321
- req.on("data", function (c) {
322
- try { collector.push(c); }
323
- catch (e) { req.removeAllListeners("data"); reject(e); }
324
- });
325
- req.on("end", function () { resolve(collector.result()); });
326
- req.on("error", function (e) { reject(e); });
314
+ return safeBuffer.collectStream(req, {
315
+ maxBytes: maxChunkSize,
316
+ errorClass: TusError,
317
+ sizeCode: "tus/chunk-too-large",
318
+ sizeMessage: "PATCH body exceeded maxChunkSize",
327
319
  });
328
320
  }
329
321
 
@@ -388,11 +380,8 @@ function create(opts) {
388
380
  }
389
381
 
390
382
  var store = opts.store;
391
- if (!store || typeof store.create !== "function" || typeof store.head !== "function" ||
392
- typeof store.append !== "function" || typeof store.terminate !== "function") {
393
- throw new TusError("tus/bad-store",
394
- "middleware.tusUpload: store must implement { create, head, append, terminate }");
395
- }
383
+ validateOpts.requireMethods(store, ["create", "head", "append", "terminate"],
384
+ "middleware.tusUpload: store", TusError, "tus/bad-store");
396
385
 
397
386
  var maxSize = opts.maxSize;
398
387
  if (maxSize !== undefined && (typeof maxSize !== "number" || !isFinite(maxSize) || maxSize <= 0)) {
@@ -451,11 +440,7 @@ function create(opts) {
451
440
  return new Date(rec.expireAt).toUTCString();
452
441
  }
453
442
 
454
- function _emitMetric(verb, _outcome, _metadata) {
455
- if (!auditOn) return;
456
- try { observability().safeEvent("middleware.tusUpload." + verb, 1, {}); }
457
- catch (_e) { /* drop-silent — observability sink */ }
458
- }
443
+ var _emitMetric = observability().namespaced("middleware.tusUpload", auditOn);
459
444
 
460
445
  async function _handleOptions(req, res) {
461
446
  var headers = _emitTusBaseHeaders(res, {
@@ -29,6 +29,7 @@
29
29
  var lazyRequire = require("../lazy-require");
30
30
  var safeJson = require("../safe-json");
31
31
  var validateOpts = require("../validate-opts");
32
+ var denyResponse = require("./deny-response");
32
33
  var { defineClass } = require("../framework-error");
33
34
 
34
35
  var WebAppManifestError = defineClass("WebAppManifestError", { alwaysPermanent: true });
@@ -135,13 +136,7 @@ function create(opts) {
135
136
  (alsoAtJsonPath && path === "/manifest.json");
136
137
  if (!matches) return next();
137
138
  if (req.method !== "GET" && req.method !== "HEAD") {
138
- var bodyMsg = "Method Not Allowed";
139
- res.writeHead(405, { // HTTP 405 status
140
- "Allow": "GET, HEAD",
141
- "Content-Type": "text/plain; charset=utf-8",
142
- "Content-Length": Buffer.byteLength(bodyMsg),
143
- });
144
- res.end(bodyMsg);
139
+ denyResponse.methodNotAllowed(res, "GET, HEAD");
145
140
  return;
146
141
  }
147
142
  res.writeHead(200, { // HTTP 200 status
package/lib/migrations.js CHANGED
@@ -39,6 +39,7 @@
39
39
  */
40
40
 
41
41
  var nodePath = require("node:path");
42
+ var moduleLoader = require("./module-loader");
42
43
  var atomicFile = require("./atomic-file");
43
44
  var dbSchema = require("./db-schema");
44
45
  var frameworkSchema = require("./framework-schema");
@@ -269,21 +270,11 @@ function _resolveDb(opts) {
269
270
  }
270
271
 
271
272
  function _loadMigration(file, dir) {
272
- var fullPath = nodePath.join(dir, file);
273
- // Drop the require cache for this path before loading so a test that
274
- // changes a migration file between calls picks up the new content.
275
- // Production deployments would always restart the process, but this
276
- // keeps test fixtures sane.
277
- try { delete require.cache[require.resolve(fullPath)]; } catch (_e) { /* not yet cached */ }
278
- var mod;
279
- // Operator-supplied migration — dynamic by design, can't be bundle-
280
- // traced. Host-CLI scope; deploying via SEA / pkg drops this surface.
281
- try { mod = require(fullPath); } // allow:dynamic-require — operator-supplied migration
282
- catch (e) {
283
- throw new MigrationError("migrations/load-failed",
273
+ var mod = moduleLoader.requireFresh(nodePath.join(dir, file), function (e) {
274
+ return new MigrationError("migrations/load-failed",
284
275
  "migration '" + file + "' failed to load: " + ((e && e.message) || String(e)),
285
276
  true);
286
- }
277
+ });
287
278
  if (!mod || typeof mod.up !== "function") {
288
279
  throw new MigrationError("migrations/missing-up",
289
280
  "migration '" + file + "' must export an `up(db)` function", true);
package/lib/mime-parse.js CHANGED
@@ -37,32 +37,48 @@
37
37
  * mdn: 1 MiB) so this module's hot-path doesn't add its own cap.
38
38
  */
39
39
 
40
- function parseHeaderBlock(text) {
41
- // RFC 5322 §2.2.3continuation lines (lines starting with WSP)
42
- // fold onto the previous header value. The header section ends at
43
- // the first empty line; subsequent lines belong to the body.
44
- var lines = text.split(/\r?\n/);
45
- var unfolded = [];
40
+ function classifyHeaderBlock(text) {
41
+ // RFC 5322 §2.2 — every line of a header section is either a header field
42
+ // (`name: value`), a folding continuation (leading WSP), or the empty line
43
+ // that ends the section. Anything else is MALFORMED and a colon-less line
44
+ // sitting in the header section is the header-injection / SMTP-smuggling
45
+ // signal (attacker content on a bare line followed by an injected
46
+ // Bcc/To/From header). `fields` is the parsed headers (boundary-aware,
47
+ // continuation-unfolded); `malformed` is the injection/structure evidence the
48
+ // silent-skip parsers (email, mime, dsn, arc) all dropped on the floor.
49
+ var lines = String(text == null ? "" : text).split(/\r?\n/);
50
+ var unfolded = []; // [{ line, lineIndex }]
46
51
  for (var i = 0; i < lines.length; i += 1) {
47
52
  var line = lines[i];
48
- if (line.length === 0) break;
53
+ if (line.length === 0) break; // header/body boundary
49
54
  if ((line.charAt(0) === " " || line.charAt(0) === "\t") && unfolded.length > 0) {
50
- unfolded[unfolded.length - 1] += " " + line.replace(/^\s+/, "");
55
+ unfolded[unfolded.length - 1].line += " " + line.replace(/^\s+/, "");
51
56
  } else {
52
- unfolded.push(line);
57
+ unfolded.push({ line: line, lineIndex: i });
53
58
  }
54
59
  }
55
- var headers = [];
60
+ var fields = [];
61
+ var malformed = [];
56
62
  for (var j = 0; j < unfolded.length; j += 1) {
57
- var l = unfolded[j];
58
- var colonAt = l.indexOf(":");
59
- if (colonAt === -1) continue;
60
- headers.push({
61
- name: l.slice(0, colonAt).trim(),
62
- value: l.slice(colonAt + 1).trim(),
63
+ var entry = unfolded[j];
64
+ var colonAt = entry.line.indexOf(":");
65
+ if (colonAt === -1) {
66
+ malformed.push({ lineIndex: entry.lineIndex, line: entry.line, reason: "no-colon" });
67
+ continue;
68
+ }
69
+ fields.push({
70
+ name: entry.line.slice(0, colonAt).trim(),
71
+ value: entry.line.slice(colonAt + 1).trim(),
63
72
  });
64
73
  }
65
- return headers;
74
+ return { fields: fields, malformed: malformed };
75
+ }
76
+
77
+ function parseHeaderBlock(text) {
78
+ // The fields view of the shared classifier — kept for every caller that only
79
+ // wants the parsed headers (the malformed lines are reached via
80
+ // classifyHeaderBlock for structure/injection validation).
81
+ return classifyHeaderBlock(text).fields;
66
82
  }
67
83
 
68
84
  function splitHeadersAndBody(text) {
@@ -189,6 +205,7 @@ function addressType(addr) {
189
205
 
190
206
  module.exports = {
191
207
  parseHeaderBlock: parseHeaderBlock,
208
+ classifyHeaderBlock: classifyHeaderBlock,
192
209
  splitHeadersAndBody: splitHeadersAndBody,
193
210
  findHeader: findHeader,
194
211
  parseContentType: parseContentType,
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * lib/module-loader.js — the single audited entry point for loading an
4
+ * operator-supplied module file (a migration or a seed) by path.
5
+ *
6
+ * Operator modules execute arbitrary code on `require`, so the dynamic
7
+ * `require()` lives here behind ONE `allow:dynamic-require` marker rather
8
+ * than scattered across every host-CLI loader (`b.migrations`,
9
+ * `b.seeders`, `b.externalDb.migrate`). The require cache is busted first
10
+ * so a dev / test rewriting a fixture between calls picks up the new
11
+ * content (production deployments restart the process). The dynamic
12
+ * require does not survive SEA / esbuild bundling — operator migrations
13
+ * and seeds are host-CLI scope, not framework-internal scope.
14
+ *
15
+ * The caller owns path construction (it knows its own directory layout)
16
+ * and the typed, domain-specific error it throws on a load failure
17
+ * (parse error, throwing top-level code), so resolution stays identical
18
+ * to the per-loader form it replaces.
19
+ */
20
+
21
+ /**
22
+ * requireFresh(absPath, onLoadError) — bust the require cache for
23
+ * `absPath`, then require it fresh. `absPath` MUST be absolute (a bare
24
+ * relative path would resolve against node_modules, which is never an
25
+ * operator file). On a load failure, `onLoadError(err)` builds the
26
+ * caller's typed error, which is thrown.
27
+ *
28
+ * var mod = moduleLoader.requireFresh(path.join(dir, file), function (e) {
29
+ * return new MigrationError("migrations/load-failed",
30
+ * "migration '" + file + "' failed to load: " + ((e && e.message) || String(e)), true);
31
+ * });
32
+ */
33
+ function requireFresh(absPath, onLoadError) {
34
+ try { delete require.cache[require.resolve(absPath)]; } catch (_e) { /* not yet cached */ }
35
+ try {
36
+ return require(absPath); // allow:dynamic-require — operator-supplied module (migration / seed)
37
+ } catch (e) {
38
+ throw onLoadError(e);
39
+ }
40
+ }
41
+
42
+ module.exports = {
43
+ requireFresh: requireFresh,
44
+ };
package/lib/money.js CHANGED
@@ -277,6 +277,32 @@ Money.prototype.multiply = function (factor, opts) {
277
277
  return new Money(quotient, this.currency);
278
278
  };
279
279
 
280
+ /**
281
+ * @primitive b.money.Money.roundToIncrement
282
+ * @signature b.money.Money.roundToIncrement(step, opts?)
283
+ * @since 0.15.13
284
+ * @status stable
285
+ * @related b.money.roundMinor, b.money.Money
286
+ *
287
+ * Snap this amount to the nearest multiple of `step` minor units, returning a
288
+ * new `Money` in the same currency — the cash-rounding step a coarser
289
+ * denomination needs (CHF to the nearest 0.05, SEK to the nearest 0.10, a
290
+ * 100-unit price step) without leaving the type. Integer-only; immutable;
291
+ * negative amounts (refund previews) round on the correct side of the tie. See
292
+ * `b.money.roundMinor` for the raw-integer form and the mode semantics.
293
+ *
294
+ * @opts
295
+ * mode: "half-even" | "half-up" | "half-down" | "ceiling" | "floor", // default: half-even
296
+ *
297
+ * @example
298
+ * b.money.of("12.32", "CHF").roundToIncrement(5, { mode: "half-up" }); // → CHF 12.30
299
+ * b.money.of("19.97", "SEK").roundToIncrement(10); // → SEK 20.00
300
+ */
301
+ Money.prototype.roundToIncrement = function (step, opts) {
302
+ opts = opts || {};
303
+ return new Money(roundMinor(this._minor, step, opts.mode || "half-even"), this.currency);
304
+ };
305
+
280
306
  // _rationalFromDecimalString -- `"1.085"` -> { num: 1085n, den: 1000n }.
281
307
  // Strict shape; same refusals as _parseDecimalString.
282
308
  function _rationalFromDecimalString(s) {
@@ -333,6 +359,84 @@ function _divRound(n, d, rounding) {
333
359
  return r < 0n ? q - 1n : q + 1n;
334
360
  }
335
361
 
362
+ // Cash-rounding modes for roundMinor / Money.roundToIncrement. half-even
363
+ // (banker's) is the default and matches the rest of the module; half-up rounds
364
+ // a tie away from zero, half-down toward zero; ceiling/floor are directional
365
+ // (toward +inf / -inf) regardless of distance.
366
+ var INCREMENT_MODES = {
367
+ "half-even": true, "half-up": true, "half-down": true, "ceiling": true, "floor": true,
368
+ };
369
+
370
+ function _toIncrementBigInt(step) {
371
+ if (typeof step === "bigint") {
372
+ if (step <= 0n) throw new MoneyError("money/bad-increment",
373
+ "step must be a positive integer of minor units");
374
+ return step;
375
+ }
376
+ if (typeof step === "number" && Number.isInteger(step) && step > 0) return BigInt(step);
377
+ throw new MoneyError("money/bad-increment",
378
+ "step must be a positive integer (BigInt or safe integer Number) of minor units; got " +
379
+ (typeof step === "number" ? String(step) : typeof step));
380
+ }
381
+
382
+ /**
383
+ * @primitive b.money.roundMinor
384
+ * @signature b.money.roundMinor(minor, step, mode?)
385
+ * @since 0.15.13
386
+ * @status stable
387
+ * @related b.money.Money, b.money.fromMinorUnits
388
+ *
389
+ * Snap a raw minor-unit integer to the nearest multiple of `step` minor units —
390
+ * the cash-rounding step a coarser cash denomination needs even though the
391
+ * currency's ISO 4217 minor unit is finer (a CHF total to the nearest 5 rappen
392
+ * after the 1/2-rappen coins were retired, a SEK total to the nearest 10 öre, a
393
+ * JPY total to a 100-unit psychological-pricing step). Pure BigInt math — no
394
+ * `Number` in the value path — and the remainder sign is handled so a negative
395
+ * amount (a refund preview) rounds on the correct side of the tie.
396
+ *
397
+ * `minor` accepts a BigInt or a safe integer `Number`; the return is a BigInt.
398
+ *
399
+ * @opts
400
+ * mode: "half-even" | "half-up" | "half-down" | "ceiling" | "floor", // default: half-even
401
+ *
402
+ * @example
403
+ * b.money.roundMinor(1232n, 5n); // CHF 12.32 → nearest 0.05 → 1230n (12.30)
404
+ * b.money.roundMinor(25n, 10n, "half-even"); // tie → even multiple → 20n
405
+ * b.money.roundMinor(25n, 10n, "half-up"); // tie away from zero → 30n
406
+ * b.money.roundMinor(-25n, 10n, "half-up"); // refund tie away from zero → -30n
407
+ */
408
+ function roundMinor(minor, step, mode) {
409
+ mode = mode || "half-even";
410
+ if (typeof minor === "number" && Number.isInteger(minor)) minor = BigInt(minor);
411
+ if (typeof minor !== "bigint") {
412
+ throw new MoneyError("money/bad-minor-units",
413
+ "minor must be an integer (BigInt or safe integer Number); got " + (typeof minor));
414
+ }
415
+ if (!INCREMENT_MODES[mode]) {
416
+ throw new MoneyError("money/bad-rounding-mode",
417
+ "mode must be one of half-even | half-up | half-down | ceiling | floor; got " + String(mode));
418
+ }
419
+ var stepBig = _toIncrementBigInt(step);
420
+ var q = minor / stepBig;
421
+ var r = minor - q * stepBig;
422
+ if (r === 0n) return minor;
423
+ // lower = the multiple toward -inf, upper = toward +inf. BigInt division
424
+ // truncates toward zero, so the bracket flips by sign of `minor`.
425
+ var lower, upper;
426
+ if (minor >= 0n) { lower = q * stepBig; upper = lower + stepBig; }
427
+ else { upper = q * stepBig; lower = upper - stepBig; }
428
+ if (mode === "floor") return lower;
429
+ if (mode === "ceiling") return upper;
430
+ var distLow = minor - lower; // >= 0
431
+ var distHigh = upper - minor; // >= 0
432
+ if (distLow < distHigh) return lower;
433
+ if (distHigh < distLow) return upper;
434
+ // Exactly on the tie.
435
+ if (mode === "half-up") return (minor >= 0n) ? upper : lower; // away from zero
436
+ if (mode === "half-down") return (minor >= 0n) ? lower : upper; // toward zero
437
+ return ((lower / stepBig) % 2n === 0n) ? lower : upper; // half-even
438
+ }
439
+
336
440
  // allocate -- split `this` into `weights.length` parts proportional to
337
441
  // the weights, distributing every minor unit. Largest-remainder
338
442
  // method: floor each share, then hand out the leftover units to the
@@ -693,6 +797,7 @@ module.exports = {
693
797
  parse: parse,
694
798
  zero: zero,
695
799
  convert: convert,
800
+ roundMinor: roundMinor,
696
801
  CURRENCIES: CURRENCIES,
697
802
  Money: Money,
698
803
  MoneyError: MoneyError,
package/lib/mtls-ca.js CHANGED
@@ -58,6 +58,9 @@ var nodeCrypto = require("node:crypto");
58
58
  var atomicFile = require("./atomic-file");
59
59
  var C = require("./constants");
60
60
  var lazyRequire = require("./lazy-require");
61
+ // Lazy — the SHA3-512 fingerprint surfaced from issuance must match the one
62
+ // the require-mtls gate pins (b.crypto.sha3Hash(certPem)).
63
+ var bCrypto = lazyRequire(function () { return require("./crypto"); });
61
64
  var { boot } = require("./log");
62
65
  var safeBuffer = require("./safe-buffer");
63
66
  var safeJson = require("./safe-json");
@@ -193,7 +196,7 @@ function create(opts) {
193
196
  opts = opts || {};
194
197
  validateOpts(opts, [
195
198
  "dataDir", "paths", "vault",
196
- "caKeySealedMode", "generation", "engine",
199
+ "caKeySealedMode", "generation", "engine", "revocationStore",
197
200
  ], "b.mtlsCa");
198
201
  validateOpts.requireNonEmptyString(opts.dataDir, "mtlsCa.create: opts.dataDir", MtlsCaError, "mtls-ca/no-datadir");
199
202
  // Auto-create the dataDir with restrictive perms (CA keys live here).
@@ -370,6 +373,29 @@ function create(opts) {
370
373
  return fresh;
371
374
  }
372
375
 
376
+ // Recover the issued certificate's identity from its PEM so issuance and
377
+ // any later revocation/indexing share identifiers without a round-trip back
378
+ // through an X.509 parser. serialNumber is normalized to the same hex form
379
+ // revoke() stores; fingerprint is the SHA3-512 the require-mtls gate pins.
380
+ function _certIdentity(certPem) {
381
+ // serialNumber comes from an X.509 parse, which is best-effort: a custom
382
+ // engine (or a test double) may return a cert in a shape this Node build
383
+ // cannot parse as X.509. The fingerprint is a hash of the returned bytes,
384
+ // so it is always available and is what the require-mtls gate pins —
385
+ // revoke()/isRevoked() by fingerprint keep working even when the serial
386
+ // can't be recovered. Never let optional identity enrichment crash issuance.
387
+ var serialNumber = null;
388
+ try {
389
+ serialNumber = _normalizeSerial(new nodeCrypto.X509Certificate(certPem).serialNumber);
390
+ } catch (_e) {
391
+ serialNumber = null;
392
+ }
393
+ return {
394
+ serialNumber: serialNumber,
395
+ fingerprint: bCrypto().sha3Hash(certPem),
396
+ };
397
+ }
398
+
373
399
  async function generateClientCert(opts2) {
374
400
  opts2 = opts2 || {};
375
401
  var ca = await initCA();
@@ -379,7 +405,10 @@ function create(opts) {
379
405
  throw new MtlsCaError("mtls-ca/bad-engine-output",
380
406
  "engine.signClientCert must return { cert, key, ca?, issuedAt?, expiresAt? }");
381
407
  }
382
- return result;
408
+ // Surface the issued serial + fingerprint so the caller can track/revoke
409
+ // the cert by the same identifiers without re-parsing the PEM.
410
+ var id = _certIdentity(result.cert);
411
+ return Object.assign({}, result, { serialNumber: id.serialNumber, fingerprint: id.fingerprint });
383
412
  }
384
413
 
385
414
  async function generateClientP12(opts2) {
@@ -395,32 +424,63 @@ function create(opts) {
395
424
  throw new MtlsCaError("mtls-ca/bad-engine-output",
396
425
  "engine.packageP12 must return { p12: Buffer, certPem, issuedAt, expiresAt }");
397
426
  }
427
+ if (typeof result.certPem === "string") {
428
+ var id12 = _certIdentity(result.certPem);
429
+ return Object.assign({}, result, { serialNumber: id12.serialNumber, fingerprint: id12.fingerprint });
430
+ }
398
431
  return result;
399
432
  }
400
433
 
401
434
  // ---- Revocation registry + CRL ----
402
435
 
403
- function _loadRevocations() {
404
- if (!nodeFs.existsSync(paths.revocations)) return { revocations: [] };
405
- try {
406
- // safeJson.parse caps depth + size + protects against
407
- // proto-pollution; the revocation file is under the operator's
408
- // dataDir but a tampered or truncated file shouldn't be able to
409
- // corrupt the rotator process.
410
- var json = safeJson.parse(nodeFs.readFileSync(paths.revocations, "utf8"),
411
- { maxBytes: C.BYTES.mib(16) });
412
- if (!json || !Array.isArray(json.revocations)) return { revocations: [] };
413
- return json;
414
- } catch (e) {
415
- throw new MtlsCaError("mtls-ca/revocation-corrupt",
416
- "could not parse " + paths.revocations + ": " +
417
- ((e && e.message) || String(e)));
436
+ // Revocation entries are read + written through a store so the registry can
437
+ // live somewhere other than the default plaintext revocations.json e.g. an
438
+ // operator-supplied encrypted / clustered store (the bring-your-own-store
439
+ // precedent b.queue's config.db set). Contract (sync):
440
+ // list() -> array of revocation entries
441
+ // add(entry) -> append one entry (the caller has already deduped)
442
+ function _defaultFileStore() {
443
+ function _list() {
444
+ if (!nodeFs.existsSync(paths.revocations)) return [];
445
+ try {
446
+ // safeJson.parse caps depth + size + protects against
447
+ // proto-pollution; a tampered or truncated file shouldn't be able to
448
+ // corrupt the rotator process.
449
+ var json = safeJson.parse(nodeFs.readFileSync(paths.revocations, "utf8"),
450
+ { maxBytes: C.BYTES.mib(16) });
451
+ return (json && Array.isArray(json.revocations)) ? json.revocations : [];
452
+ } catch (e) {
453
+ throw new MtlsCaError("mtls-ca/revocation-corrupt",
454
+ "could not parse " + paths.revocations + ": " + ((e && e.message) || String(e)));
455
+ }
418
456
  }
457
+ return {
458
+ list: _list,
459
+ add: function (entry) {
460
+ var entries = _list();
461
+ entries.push(entry);
462
+ atomicFile.writeSync(paths.revocations,
463
+ JSON.stringify({ revocations: entries }, null, 2) + "\n", { mode: 0o600 });
464
+ },
465
+ };
419
466
  }
467
+ var revocationStore = opts.revocationStore || _defaultFileStore();
468
+ validateOpts.requireMethods(revocationStore, ["list", "add"],
469
+ "opts.revocationStore", MtlsCaError, "mtls-ca/bad-revocation-store");
420
470
 
421
- function _saveRevocations(state) {
422
- atomicFile.writeSync(paths.revocations,
423
- JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
471
+ // A fingerprint is the SHA3-512 hex the require-mtls gate pins. Normalize it
472
+ // like a serial (strip 0x / separators / whitespace, lowercase, hex-validate)
473
+ // so a consumer can revoke by the same value the gate compares against.
474
+ function _normalizeFingerprint(fp) {
475
+ if (!fp || typeof fp !== "string") {
476
+ throw new MtlsCaError("mtls-ca/bad-fingerprint", "fingerprint must be a non-empty string");
477
+ }
478
+ var stripped = fp.replace(/^0x/i, "").replace(/[:\-\s]/g, "");
479
+ if (!safeBuffer.isHex(stripped)) {
480
+ throw new MtlsCaError("mtls-ca/bad-fingerprint",
481
+ "fingerprint contains non-hex characters: " + JSON.stringify(fp));
482
+ }
483
+ return stripped.toLowerCase();
424
484
  }
425
485
 
426
486
  function _normalizeSerial(s) {
@@ -466,46 +526,66 @@ function create(opts) {
466
526
  "aACompromise": 10,
467
527
  };
468
528
 
469
- function revoke(serialNumber, opts3) {
470
- var serial = _normalizeSerial(serialNumber);
529
+ function revoke(idOrOpts, opts3) {
530
+ // Accept either revoke(serialString, { reason, fingerprint }) — the
531
+ // backward-compatible serial-keyed form — or revoke({ serial?,
532
+ // fingerprint?, reason? }). The require-mtls gate denies by fingerprint,
533
+ // so a fingerprint-indexed consumer can revoke by the same value it pins
534
+ // on; serial-keyed behavior stays the default. At least one key required.
535
+ var spec = (idOrOpts && typeof idOrOpts === "object") ? idOrOpts : null;
471
536
  opts3 = opts3 || {};
472
- var reasonName = opts3.reason || "unspecified";
537
+ var serialIn = spec ? spec.serial : idOrOpts;
538
+ var fingerprintIn = spec ? spec.fingerprint : opts3.fingerprint;
539
+ var reasonName = (spec ? spec.reason : opts3.reason) || "unspecified";
540
+
541
+ var serial = (serialIn !== undefined && serialIn !== null) ? _normalizeSerial(serialIn) : null;
542
+ var fingerprint = (fingerprintIn !== undefined && fingerprintIn !== null)
543
+ ? _normalizeFingerprint(fingerprintIn) : null;
544
+ if (!serial && !fingerprint) {
545
+ throw new MtlsCaError("mtls-ca/no-revocation-key",
546
+ "revoke requires a serial number or a fingerprint " +
547
+ "(revoke(serial, opts) or revoke({ serial, fingerprint }))");
548
+ }
473
549
  var reasonCode = CRL_REASON_BY_NAME[reasonName];
474
550
  if (reasonCode === undefined) {
475
551
  throw new MtlsCaError("mtls-ca/bad-reason",
476
552
  "revoke: unknown reason '" + reasonName + "' (valid: " +
477
553
  Object.keys(CRL_REASON_BY_NAME).join(", ") + ")");
478
554
  }
479
- var state = _loadRevocations();
480
- var existing = state.revocations.find(function (r) {
481
- return r.serialNumber === serial;
555
+ var existing = revocationStore.list().find(function (r) {
556
+ return (serial && r.serialNumber === serial) || (fingerprint && r.fingerprint === fingerprint);
482
557
  });
483
558
  if (existing) {
484
- // Idempotent — repeated revoke() of the same serial doesn't
559
+ // Idempotent — repeated revoke() of the same serial/fingerprint doesn't
485
560
  // shift the revokedAt timestamp.
486
561
  return existing;
487
562
  }
488
563
  var entry = {
489
564
  serialNumber: serial,
565
+ fingerprint: fingerprint,
490
566
  reason: reasonName,
491
567
  reasonCode: reasonCode,
492
568
  revokedAt: Date.now(),
493
569
  };
494
- state.revocations.push(entry);
495
- _saveRevocations(state);
570
+ revocationStore.add(entry);
496
571
  return entry;
497
572
  }
498
573
 
499
- function isRevoked(serialNumber) {
500
- var serial = _normalizeSerial(serialNumber);
501
- var state = _loadRevocations();
502
- return state.revocations.some(function (r) {
503
- return r.serialNumber === serial;
574
+ function isRevoked(serialOrFingerprint) {
575
+ // Accept a serial number OR a SHA3-512 fingerprint — both are hex, so one
576
+ // normalized form is matched against either key each entry carries.
577
+ if (!serialOrFingerprint || typeof serialOrFingerprint !== "string") {
578
+ throw new MtlsCaError("mtls-ca/bad-revocation-key",
579
+ "isRevoked requires a serial number or a fingerprint (hex string)");
580
+ }
581
+ var norm = _normalizeFingerprint(serialOrFingerprint);
582
+ return revocationStore.list().some(function (r) {
583
+ return r.serialNumber === norm || r.fingerprint === norm;
504
584
  });
505
585
  }
506
586
 
507
587
  function getRevocations() {
508
- return _loadRevocations().revocations.slice();
588
+ return revocationStore.list().slice();
509
589
  }
510
590
 
511
591
  // Generate a signed X.509 CRL covering every entry in the registry.
@@ -521,7 +601,7 @@ function create(opts) {
521
601
  "framework's bundled CA engine, which supports it");
522
602
  }
523
603
  var ca = await initCA();
524
- var revocations = _loadRevocations().revocations;
604
+ var revocations = revocationStore.list();
525
605
  var nowMs = Date.now();
526
606
  var thisUpdate = opts3.thisUpdate || new Date(nowMs);
527
607
  var nextUpdate = opts3.nextUpdate ||