@blamejs/core 0.15.12 → 0.15.14

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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
package/lib/static.js CHANGED
@@ -42,6 +42,7 @@ var nodeFs = require("node:fs");
42
42
  var fsp = require("node:fs/promises");
43
43
  var nodeCrypto = require("node:crypto");
44
44
  var nodePath = require("node:path");
45
+ var atomicFile = require("./atomic-file");
45
46
  var C = require("./constants");
46
47
  var gateContract = require("./gate-contract");
47
48
  var lazyRequire = require("./lazy-require");
@@ -240,10 +241,16 @@ async function _readMeta(root, candidate) {
240
241
  var sri = nodeCrypto.createHash("sha384");
241
242
  var sha3 = nodeCrypto.createHash("sha3-512");
242
243
  await new Promise(function (resolve, reject) {
243
- // The path handed to createReadStream is the confined output of
244
- // `_assertInsideRoot(root, candidate)` above (lexical resolve +
245
- // root-prefix containment), not the request-derived candidate.
246
- var s = nodeFs.createReadStream(absPath);
244
+ // The path is the confined output of `_assertInsideRoot(root, candidate)`
245
+ // above (lexical resolve + root-prefix containment). Open it O_NOFOLLOW and
246
+ // stream from that fd: lexical confinement still leaves a window where a
247
+ // symlink swapped in at the final component after the check would be
248
+ // followed by a plain createReadStream (CWE-22 / CWE-367); O_NOFOLLOW refuses
249
+ // it. The open throws synchronously on a symlink (ELOOP) / missing file —
250
+ // reject the hash promise so the caller falls back exactly as on a read error.
251
+ var s;
252
+ try { s = nodeFs.createReadStream(absPath, { fd: atomicFile.openNoFollowSync(absPath) }); }
253
+ catch (e) { reject(e); return; }
247
254
  s.on("data", function (chunk) { sri.update(chunk); sha3.update(chunk); });
248
255
  s.on("end", resolve);
249
256
  s.on("error", reject);
@@ -399,7 +406,7 @@ function _shouldForceAttachment(contentType, ext, contentSafetyMap, allowSvgRend
399
406
  return true;
400
407
  }
401
408
  if (bare.indexOf("text/") === 0) return false;
402
- if (SAFE_RENDER_RASTER_MIMES[bare]) return false;
409
+ if (Object.prototype.hasOwnProperty.call(SAFE_RENDER_RASTER_MIMES, bare)) return false;
403
410
  if (bare === "image/svg+xml") {
404
411
  if (!allowSvgRender) return true;
405
412
  if (!contentSafetyMap || typeof contentSafetyMap !== "object") return true;
@@ -489,84 +496,147 @@ function _httpDate(date) {
489
496
  }
490
497
 
491
498
  function _validateCreateOpts(opts) {
492
- validateOpts.requireObject(opts, "staticServe.create", StaticServeError);
493
- validateOpts.requireNonEmptyString(opts.root, "staticServe.create: root", StaticServeError, "BAD_OPT");
499
+ // Declarative per-field validation. shape() runs requireObject(opts)
500
+ // itself (same "opts must be an object" / BAD_OPT contract the inline
501
+ // requireObject produced), then dispatches each field. A field whose
502
+ // check is one of the rule tokens maps to the token directly; a field
503
+ // with a bespoke message (mountPath / hashedPathPattern), a
504
+ // duck-typed-handle shape (permissions / cache / fileType / retention /
505
+ // revokeStore — each carrying its own operator-facing description), a
506
+ // numeric bound, or an audit/observability shape uses the
507
+ // validator-function hatch so the exact thrown code + message + label
508
+ // are preserved. Cross-field business logic (root existence,
509
+ // allowedFileTypes↔fileType wiring, the contentSafety map, the
510
+ // mountType enum, the quota↔cache requirement) stays below shape.
511
+ validateOpts.shape(opts, {
512
+ root: "required-string",
513
+ mountPath: function (value, _label, errorClass, code) {
514
+ // empty string is operator-permissible: "no mount, root is request URL"
515
+ if (typeof value === "string" && value.length === 0) return;
516
+ if (value !== undefined && value !== null && typeof value !== "string") {
517
+ throw errorClass.factory(code, "staticServe.create: mountPath must be a string");
518
+ }
519
+ },
520
+ hashedPathPattern: function (value, _label, errorClass, code) {
521
+ if (value !== undefined && value !== null && !(value instanceof RegExp)) {
522
+ throw errorClass.factory(code, "staticServe.create: hashedPathPattern must be a RegExp");
523
+ }
524
+ },
525
+ // indexFile === null is the operator's "disable" sentinel; the helper
526
+ // returns null/undefined unchanged so we keep that semantic.
527
+ indexFile: "optional-string",
528
+ defaultMaxAge: function (value, label, errorClass, code) {
529
+ numericBounds.requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code);
530
+ },
531
+ contentTypes: "optional-plain-object",
532
+ // contentSafety — extension-keyed gate map. undefined → the framework
533
+ // wires b.guardAll.byExtension({ profile: "strict" }) so every shipped
534
+ // guard is ON by default; null is the explicit opt-out (audited at
535
+ // create() time); a plain { ext: gate } object validates each value is a
536
+ // gate (a .check fn). The label / code / message match the prior inline
537
+ // check exactly so a test asserting them still holds.
538
+ contentSafety: function (value, _label, errorClass, code) {
539
+ if (value === undefined || value === null) return;
540
+ validateOpts.optionalPlainObject(value,
541
+ "staticServe.create: contentSafety", errorClass, code,
542
+ "must be a plain { ext: gate } object, null to opt out, or " +
543
+ "undefined for the default-on b.guardAll wiring");
544
+ var safetyKeys = Object.keys(value);
545
+ for (var sk = 0; sk < safetyKeys.length; sk++) {
546
+ var ext = safetyKeys[sk];
547
+ var g = value[ext];
548
+ if (!g || typeof g.check !== "function") {
549
+ throw errorClass.factory(code,
550
+ "staticServe.create: contentSafety[" + JSON.stringify(ext) +
551
+ "] must be a gate (b.guardCsv.gate / b.guardHtml.gate / etc.)");
552
+ }
553
+ }
554
+ },
555
+ permissions: function (value, label, errorClass, code) {
556
+ validateOpts.optionalObjectWithMethod(value, "check", label, errorClass, code,
557
+ "must be a b.permissions instance (check fn)");
558
+ },
559
+ cache: function (value, label, errorClass, code) {
560
+ validateOpts.optionalObjectWithMethod(value, "get", label, errorClass, code,
561
+ "must be a b.cache instance (used for cluster-shared bandwidth + concurrency tracking)");
562
+ },
563
+ fileType: function (value, label, errorClass, code) {
564
+ validateOpts.optionalObjectWithMethod(value, "detect", label, errorClass, code,
565
+ "must be a b.fileType instance (magic-byte MIME detection)");
566
+ },
567
+ retention: function (value, label, errorClass, code) {
568
+ validateOpts.optionalObjectWithMethod(value, "isServable", label, errorClass, code,
569
+ "must expose isServable(absPath, ctx) → boolean (compliance retention check)");
570
+ },
571
+ revokeStore: function (value, label, errorClass, code) {
572
+ validateOpts.optionalObjectWithMethod(value, "isRevoked", label, errorClass, code,
573
+ "must expose isRevoked(key) and revoke(key) for force-revoke support");
574
+ },
575
+ allowedFileTypes: "optional-string-array",
576
+ audit: function (value, _label, errorClass, _code) {
577
+ validateOpts.auditShape(value, "staticServe.create", errorClass);
578
+ },
579
+ observability: function (value, _label, errorClass, _code) {
580
+ validateOpts.observabilityShape(value, "staticServe.create", errorClass);
581
+ },
582
+ onServe: "optional-function",
583
+ onError: "optional-function",
584
+ acceptRanges: "optional-boolean",
585
+ auditSuccess: "optional-boolean",
586
+ auditFailures: "optional-boolean",
587
+ safeAttachmentForRiskyMimes: "optional-boolean",
588
+ forceAttachmentForNonText: "optional-boolean",
589
+ safeRenderSvg: "optional-boolean",
590
+ safeRenderPdf: "optional-boolean",
591
+ maxBytesPerActorPerWindowMs: function (value, label, errorClass, code) {
592
+ numericBounds.requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code);
593
+ },
594
+ maxBytesAllActorsPerWindowMs: function (value, label, errorClass, code) {
595
+ numericBounds.requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code);
596
+ },
597
+ bandwidthWindowMs: function (value, label, errorClass, code) {
598
+ numericBounds.requirePositiveFiniteIntIfPresent(value, label, errorClass, code);
599
+ },
600
+ maxConcurrentDownloadsPerActor: function (value, label, errorClass, code) {
601
+ numericBounds.requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code);
602
+ },
603
+ maxIdleMs: function (value, label, errorClass, code) {
604
+ numericBounds.requirePositiveFiniteIntIfPresent(value, label, errorClass, code);
605
+ },
606
+ // maxRangeBytes — per-range byte cap (slowloris-range defense). A
607
+ // positive finite integer caps Range requests; Infinity is the
608
+ // documented opt-out, so the numericBounds finite-int helpers (which
609
+ // reject Infinity) can't be used directly here.
610
+ maxRangeBytes: function (value, label, errorClass, code) {
611
+ if (value === undefined || value === null || value === Infinity) return;
612
+ numericBounds.requirePositiveFiniteInt(value, label, errorClass, code);
613
+ },
614
+ }, "staticServe.create", StaticServeError, "BAD_OPT", {
615
+ // contentSafetyDisabledReason (audit-row string) and mountType (curated
616
+ // | user-content enum) carry dedicated cross-field validation below
617
+ // shape — the audit reason read and the enum throw whose exact message a
618
+ // test asserts. Listing them here keeps that bespoke logic the single
619
+ // validator rather than re-checking the same key inside shape.
620
+ allow: ["contentSafetyDisabledReason", "mountType"],
621
+ });
622
+
494
623
  if (!nodeFs.existsSync(opts.root)) {
495
624
  throw _err("BAD_OPT", "staticServe.create: root does not exist: " + opts.root);
496
625
  }
497
- if (typeof opts.mountPath === "string" && opts.mountPath.length === 0) {
498
- // empty string is operator-permissible: "no mount, root is request URL"
499
- } else if (opts.mountPath !== undefined && opts.mountPath !== null &&
500
- typeof opts.mountPath !== "string") {
501
- throw _err("BAD_OPT", "staticServe.create: mountPath must be a string");
502
- }
503
- if (opts.hashedPathPattern !== undefined && opts.hashedPathPattern !== null &&
504
- !(opts.hashedPathPattern instanceof RegExp)) {
505
- throw _err("BAD_OPT", "staticServe.create: hashedPathPattern must be a RegExp");
506
- }
507
- // indexFile === null is the operator's "disable" sentinel; the helper
508
- // returns null/undefined unchanged so we keep that semantic.
509
- validateOpts.optionalNonEmptyString(opts.indexFile,
510
- "staticServe.create: indexFile", StaticServeError, "BAD_OPT");
511
- numericBounds.requireNonNegativeFiniteIntIfPresent(opts.defaultMaxAge,
512
- "staticServe.create: defaultMaxAge", StaticServeError, "BAD_OPT");
513
- validateOpts.optionalPlainObject(opts.contentTypes,
514
- "staticServe.create: contentTypes", StaticServeError, "BAD_OPT");
515
- validateOpts.optionalObjectWithMethod(opts.permissions, "check",
516
- "staticServe.create: permissions", StaticServeError, "BAD_OPT",
517
- "must be a b.permissions instance (check fn)");
518
- validateOpts.optionalObjectWithMethod(opts.cache, "get",
519
- "staticServe.create: cache", StaticServeError, "BAD_OPT",
520
- "must be a b.cache instance (used for cluster-shared bandwidth + concurrency tracking)");
521
- validateOpts.optionalObjectWithMethod(opts.fileType, "detect",
522
- "staticServe.create: fileType", StaticServeError, "BAD_OPT",
523
- "must be a b.fileType instance (magic-byte MIME detection)");
524
- validateOpts.optionalObjectWithMethod(opts.retention, "isServable",
525
- "staticServe.create: retention", StaticServeError, "BAD_OPT",
526
- "must expose isServable(absPath, ctx) → boolean (compliance retention check)");
527
- validateOpts.optionalObjectWithMethod(opts.revokeStore, "isRevoked",
528
- "staticServe.create: revokeStore", StaticServeError, "BAD_OPT",
529
- "must expose isRevoked(key) and revoke(key) for force-revoke support");
530
- validateOpts.optionalNonEmptyStringArray(opts.allowedFileTypes,
531
- "staticServe.create: allowedFileTypes", StaticServeError, "BAD_OPT");
532
626
  if (Array.isArray(opts.allowedFileTypes) && opts.allowedFileTypes.length > 0 &&
533
627
  (!opts.fileType || typeof opts.fileType.detect !== "function")) {
534
628
  throw _err("BAD_OPT",
535
629
  "staticServe.create: allowedFileTypes is set but fileType primitive is not wired " +
536
630
  "(pass fileType: b.fileType so the framework can sniff magic bytes before serving)");
537
631
  }
538
- validateOpts.auditShape(opts.audit, "staticServe.create", StaticServeError);
539
- validateOpts.observabilityShape(opts.observability, "staticServe.create", StaticServeError);
540
- validateOpts.optionalFunction(opts.onServe, "staticServe.create: onServe", StaticServeError);
541
- validateOpts.optionalFunction(opts.onError, "staticServe.create: onError", StaticServeError);
542
- // contentSafety extension-keyed gate map. Default behaviour: when
543
- // undefined, the framework wires b.guardAll.byExtension({ profile:
544
- // "strict" }) automatically so every shipped guard is ON by default.
545
- // Explicit opt-out: contentSafety: null (audited at create() time so
546
- // a security review can reconstruct which deploys disabled the
547
- // default-on protection).
632
+ // contentSafety — extension-keyed gate map; validated in the shape above
633
+ // (plain { ext: gate } object / null opt-out / undefined default-on).
634
+ // When undefined, the framework wires b.guardAll.byExtension({ profile:
635
+ // "strict" }) automatically so every shipped guard is ON by default;
636
+ // contentSafety: null is the explicit opt-out, audited at create() time so
637
+ // a security review can reconstruct which deploys disabled the default-on
638
+ // protection.
548
639
  // Example: contentSafety: { ".csv": b.guardCsv.gate({ profile: "strict" }) }
549
- if (opts.contentSafety !== undefined && opts.contentSafety !== null) {
550
- validateOpts.optionalPlainObject(opts.contentSafety,
551
- "staticServe.create: contentSafety", StaticServeError, "BAD_OPT",
552
- "must be a plain { ext: gate } object, null to opt out, or " +
553
- "undefined for the default-on b.guardAll wiring");
554
- var safetyKeys = Object.keys(opts.contentSafety);
555
- for (var sk = 0; sk < safetyKeys.length; sk++) {
556
- var ext = safetyKeys[sk];
557
- var g = opts.contentSafety[ext];
558
- if (!g || typeof g.check !== "function") {
559
- throw _err("BAD_OPT",
560
- "staticServe.create: contentSafety[" + JSON.stringify(ext) +
561
- "] must be a gate (b.guardCsv.gate / b.guardHtml.gate / etc.)");
562
- }
563
- }
564
- }
565
- validateOpts.optionalBoolean(opts.acceptRanges, "staticServe.create: acceptRanges", StaticServeError);
566
- validateOpts.optionalBoolean(opts.auditSuccess, "staticServe.create: auditSuccess", StaticServeError);
567
- validateOpts.optionalBoolean(opts.auditFailures, "staticServe.create: auditFailures", StaticServeError);
568
- validateOpts.optionalBoolean(opts.safeAttachmentForRiskyMimes,
569
- "staticServe.create: safeAttachmentForRiskyMimes", StaticServeError);
570
640
  // mountType — config-time enum. A typo ("usercontent", "uploads")
571
641
  // would silently fall back to the curated default and serve untrusted
572
642
  // HTML inline, so THROW at boot rather than mis-type the mount.
@@ -576,22 +646,6 @@ function _validateCreateOpts(opts) {
576
646
  "staticServe.create: mountType must be 'curated' (default) or " +
577
647
  "'user-content'; got " + JSON.stringify(opts.mountType));
578
648
  }
579
- validateOpts.optionalBoolean(opts.forceAttachmentForNonText,
580
- "staticServe.create: forceAttachmentForNonText", StaticServeError);
581
- validateOpts.optionalBoolean(opts.safeRenderSvg,
582
- "staticServe.create: safeRenderSvg", StaticServeError);
583
- validateOpts.optionalBoolean(opts.safeRenderPdf,
584
- "staticServe.create: safeRenderPdf", StaticServeError);
585
- numericBounds.requireNonNegativeFiniteIntIfPresent(opts.maxBytesPerActorPerWindowMs,
586
- "staticServe.create: maxBytesPerActorPerWindowMs", StaticServeError, "BAD_OPT");
587
- numericBounds.requireNonNegativeFiniteIntIfPresent(opts.maxBytesAllActorsPerWindowMs,
588
- "staticServe.create: maxBytesAllActorsPerWindowMs", StaticServeError, "BAD_OPT");
589
- numericBounds.requirePositiveFiniteIntIfPresent(opts.bandwidthWindowMs,
590
- "staticServe.create: bandwidthWindowMs", StaticServeError, "BAD_OPT");
591
- numericBounds.requireNonNegativeFiniteIntIfPresent(opts.maxConcurrentDownloadsPerActor,
592
- "staticServe.create: maxConcurrentDownloadsPerActor", StaticServeError, "BAD_OPT");
593
- numericBounds.requirePositiveFiniteIntIfPresent(opts.maxIdleMs,
594
- "staticServe.create: maxIdleMs", StaticServeError, "BAD_OPT");
595
649
  // Quotas require a cache for cluster-shared coordination.
596
650
  if ((opts.maxBytesPerActorPerWindowMs > 0 ||
597
651
  opts.maxBytesAllActorsPerWindowMs > 0 ||
@@ -701,20 +755,11 @@ async function integrity(absPath) {
701
755
 
702
756
  function create(opts) {
703
757
  opts = opts || {};
704
- // The v0.6.x test surface called `validateOpts(opts, [...allowed], label)`
705
- // for the unknown-key check. Preserve that gate in addition to the
706
- // throw-at-config-time validation so tests catch typos.
707
- validateOpts(opts, [
708
- "root", "mountPath", "hashedPathPattern",
709
- "indexFile", "defaultMaxAge", "contentTypes",
710
- "permissions", "cache", "fileType", "retention", "revokeStore",
711
- "allowedFileTypes", "audit", "observability", "onServe", "onError",
712
- "acceptRanges", "auditSuccess", "auditFailures",
713
- "maxBytesPerActorPerWindowMs", "maxBytesAllActorsPerWindowMs",
714
- "bandwidthWindowMs", "maxConcurrentDownloadsPerActor", "maxIdleMs",
715
- "contentSafety", "contentSafetyDisabledReason",
716
- "mountType", "forceAttachmentForNonText", "safeRenderSvg", "safeRenderPdf",
717
- ], "staticServe.create");
758
+ // The exhaustive shape() in _validateCreateOpts is the authoritative
759
+ // unknown-key gate: any opt not declared in its schema (nor listed in
760
+ // its options.allow pass-through) is rejected as an unknown opt, so the
761
+ // typo-catching the v0.6.x `validateOpts(opts, [...allowed])` call gave
762
+ // is now subsumed by the per-field validation below.
718
763
  _validateCreateOpts(opts);
719
764
  var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
720
765
  var root = nodePath.resolve(opts.root);
@@ -846,10 +891,21 @@ function create(opts) {
846
891
  // `_assertInsideRoot`, not the request-derived candidate.
847
892
  var confined = _assertInsideRoot(root, absPath);
848
893
  if (!confined) return { ok: false, reason: "read-failed" };
894
+ // Only the first 64 KiB is ever inspected, so read JUST that prefix from an
895
+ // O_NOFOLLOW fd: the previous fsp.readFile slurped the WHOLE file into memory
896
+ // just to sniff its header — a request-reachable OOM on a user-content mount
897
+ // — and followed a post-confinement symlink swap. A PREFIX read (not a
898
+ // maxBytes refusal) is required so a large but allowed file still serves.
849
899
  var sample;
850
- try { sample = await fsp.readFile(confined, { flag: "r" }); }
851
- catch (_e) { return { ok: false, reason: "read-failed" }; }
852
- var detected = fileType.detect(sample.slice(0, C.BYTES.kib(64))) || {};
900
+ try {
901
+ var sniffFd = atomicFile.openNoFollowSync(confined);
902
+ try {
903
+ var sniffBuf = Buffer.alloc(C.BYTES.kib(64));
904
+ var sniffN = nodeFs.readSync(sniffFd, sniffBuf, 0, sniffBuf.length, 0);
905
+ sample = sniffBuf.slice(0, sniffN);
906
+ } finally { nodeFs.closeSync(sniffFd); }
907
+ } catch (_e) { return { ok: false, reason: "read-failed" }; }
908
+ var detected = fileType.detect(sample) || {};
853
909
  if (!detected.mime) return { ok: false, reason: "indeterminate" };
854
910
  if (allowedFileTypes.indexOf(detected.mime) === -1) {
855
911
  return { ok: false, reason: "not-allowed", detected: detected.mime };
@@ -1013,6 +1069,23 @@ function create(opts) {
1013
1069
  // creation can ever ride this code path.
1014
1070
  gateHandle = await fsp.open(gateConfined, gateOpenFlags, 0o600);
1015
1071
  var gateStat = await gateHandle.stat();
1072
+ // Cap before Buffer.alloc(gateStat.size): the gate buffers the WHOLE
1073
+ // file to inspect it, so a multi-GiB file on a user-content mount is a
1074
+ // request-reachable OOM lever. A file too large for the gate to vet is
1075
+ // refused (the gate's own "refuse" disposition) rather than buffered.
1076
+ if (gateStat.size > C.BYTES.mib(16)) {
1077
+ stats.failures += 1;
1078
+ _emitObs("staticServe.content_safety_refused", 1, { route: urlPath });
1079
+ try { await gateHandle.close(); } catch (_ce) { /* close best-effort */ }
1080
+ if (auditFailures) {
1081
+ emitAudit("staticServe.serve.failure", Object.assign({
1082
+ outcome: "failure", reason: "content_safety_too_large",
1083
+ resource: urlPath, ext: ext, sizeBytes: gateStat.size,
1084
+ }, actorCtx));
1085
+ }
1086
+ return writeErr(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
1087
+ "content_safety_refused", "Unsupported Media Type");
1088
+ }
1016
1089
  gateBuf = Buffer.alloc(gateStat.size);
1017
1090
  var gateRead = 0;
1018
1091
  while (gateRead < gateStat.size) {
@@ -1097,8 +1170,11 @@ function create(opts) {
1097
1170
  "precondition_failed", "Precondition Failed");
1098
1171
  }
1099
1172
 
1100
- // Conditional: If-Modified-Since (304)
1101
- var ifModSince = headersIn["if-modified-since"];
1173
+ // Conditional: If-Modified-Since (304) — RFC 7232 §6: evaluated ONLY when
1174
+ // If-None-Match is absent. Otherwise a changed-ETag resource rewritten
1175
+ // within the same wall-clock second (mtime compared at second granularity)
1176
+ // would be falsely reported 304 despite the content hash differing.
1177
+ var ifModSince = !ifNone && headersIn["if-modified-since"];
1102
1178
  if (ifModSince) {
1103
1179
  var ims = Date.parse(ifModSince);
1104
1180
  if (isFinite(ims) && Math.floor(meta.mtimeMs / C.TIME.seconds(1)) <= Math.floor(ims / C.TIME.seconds(1))) {
@@ -1113,8 +1189,9 @@ function create(opts) {
1113
1189
  }
1114
1190
  }
1115
1191
 
1116
- // Conditional: If-Unmodified-Since (412)
1117
- var ifUnmodSince = headersIn["if-unmodified-since"];
1192
+ // Conditional: If-Unmodified-Since (412) — RFC 7232 §6: evaluated ONLY
1193
+ // when If-Match is absent.
1194
+ var ifUnmodSince = !ifMatch && headersIn["if-unmodified-since"];
1118
1195
  if (ifUnmodSince) {
1119
1196
  var ius = Date.parse(ifUnmodSince);
1120
1197
  if (isFinite(ius) && Math.floor(meta.mtimeMs / C.TIME.seconds(1)) > Math.floor(ius / C.TIME.seconds(1))) {
@@ -1310,7 +1387,20 @@ function create(opts) {
1310
1387
  }
1311
1388
 
1312
1389
  var streamOpts = range ? { start: range.start, end: range.end } : {};
1313
- var fileStream = nodeFs.createReadStream(streamTarget, streamOpts);
1390
+ // Open streamTarget (the re-confined _assertInsideRoot output) O_NOFOLLOW and
1391
+ // stream from that fd, so a symlink swapped in at the final component after
1392
+ // confinement is refused (ELOOP) rather than followed (CWE-22 / CWE-367).
1393
+ // Headers are already committed here, so a sync refusal can't re-issue a 404
1394
+ // — abort the connection instead of serving the swapped target.
1395
+ var fileStream;
1396
+ try {
1397
+ fileStream = nodeFs.createReadStream(streamTarget,
1398
+ Object.assign({ fd: atomicFile.openNoFollowSync(streamTarget) }, streamOpts));
1399
+ } catch (_openErr) {
1400
+ releaseSlot();
1401
+ try { res.destroy(); } catch (_d) { /* already torn down */ }
1402
+ return;
1403
+ }
1314
1404
 
1315
1405
  // Idle timeout — close the connection if the client stalls. Pattern is
1316
1406
  // a deadline-style debounce (clearTimeout + setTimeout) tied directly
package/lib/storage.js CHANGED
@@ -38,10 +38,12 @@
38
38
  * Filesystem-and-cloud-backed object storage with sealed per-file encryption keys, classification routing, and residency enforcement.
39
39
  */
40
40
  var C = require("./constants");
41
+ var safeBuffer = require("./safe-buffer");
41
42
  var { generateBytes, encryptPacked, decryptPacked } = require("./crypto");
42
43
  var objectStore = require("./object-store");
43
44
  var lazyRequire = require("./lazy-require");
44
45
  var numericBounds = require("./numeric-bounds");
46
+ var codepointClass = require("./codepoint-class");
45
47
  var canonicalJson = require("./canonical-json");
46
48
  var { StorageError } = require("./framework-error");
47
49
 
@@ -950,7 +952,7 @@ function _validateAssemblyId(id) {
950
952
  for (var i = 0; i < id.length; i += 1) {
951
953
  var c = id.charCodeAt(i);
952
954
  // Refuse: C0 (0x00-0x1F), DEL (0x7F), slash, backslash, dot-prefix
953
- if (c < 0x20 || c === 0x2F || c === 0x5C || c === 0x7F) {
955
+ if (codepointClass.isForbiddenControlChar(c, { forbidTab: true }) || c === 0x2F || c === 0x5C) {
954
956
  throw _err("INVALID_ARGUMENT",
955
957
  "chunkScratch: assemblyId carries forbidden character at byte " + i, true);
956
958
  }
@@ -1083,7 +1085,7 @@ function chunkScratch(opts) {
1083
1085
  if (!Buffer.isBuffer(args.data)) {
1084
1086
  throw _err("INVALID_ARGUMENT", "chunkScratch.saveChunk: data must be a Buffer", true);
1085
1087
  }
1086
- if (args.data.length > maxChunkBytes) {
1088
+ if (safeBuffer.byteLengthOf(args.data) > maxChunkBytes) {
1087
1089
  throw _err("INVALID_ARGUMENT",
1088
1090
  "chunkScratch.saveChunk: chunk exceeds maxChunkBytes (" + args.data.length + " > " + maxChunkBytes + ")", true);
1089
1091
  }