@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/csp.js CHANGED
@@ -52,7 +52,12 @@
52
52
  var nodeCrypto = require("node:crypto");
53
53
  var validateOpts = require("./validate-opts");
54
54
  var bCrypto = require("./crypto");
55
+ var pick = require("./pick");
55
56
  var { defineClass } = require("./framework-error");
57
+ // The strict framework defaults the per-route merge derives from. One-
58
+ // directional edge: security-headers requires only request-helpers +
59
+ // validate-opts, never csp, so this is not a cycle (§9 top-of-file require).
60
+ var securityHeaderDefaults = require("./middleware/security-headers");
56
61
 
57
62
  var CspError = defineClass("CspError", { alwaysPermanent: true });
58
63
 
@@ -78,6 +83,61 @@ var ALL_DIRECTIVES = [
78
83
  var UNSAFE_KEYWORDS = ["'unsafe-inline'", "'unsafe-eval'", "'unsafe-hashes'"];
79
84
  var CATCH_ALL_SOURCES = ["*", "https:"];
80
85
 
86
+ // _parseCspString — split a CSP header value into an ordered
87
+ // { directive: [sources] } map. Tolerant reader (defensive tier): a
88
+ // malformed base yields whatever directives DO parse. The merge never
89
+ // re-validates these base sources (they are the trusted default), so the
90
+ // parser only needs to be faithful, not strict. First write wins on a
91
+ // duplicate directive (matches UA "honor the first").
92
+ function _parseCspString(value) {
93
+ var out = {};
94
+ if (typeof value !== "string") return out;
95
+ var segments = value.split(";");
96
+ for (var i = 0; i < segments.length; i += 1) {
97
+ var tokens = segments[i].trim().split(/\s+/);
98
+ var name = tokens[0];
99
+ if (!name) continue;
100
+ if (Object.prototype.hasOwnProperty.call(out, name)) continue;
101
+ out[name] = tokens.slice(1);
102
+ }
103
+ return out;
104
+ }
105
+
106
+ // _parsePermissionsPolicyString — split a Permissions-Policy header value
107
+ // into a { feature: "allowlist" } map plus the feature order, preserving
108
+ // each feature's RFC-9651 value-list verbatim ("()" / "*" / "(self ...)").
109
+ function _parsePermissionsPolicyString(value) {
110
+ var out = {};
111
+ var order = [];
112
+ if (typeof value !== "string") return { map: out, order: order };
113
+ // Split on the literal comma (linear) — the per-item .trim() below handles
114
+ // the surrounding whitespace, so the prior `/\s*,\s*/` was redundant and
115
+ // ran in O(n^2) on a comma-less run of whitespace (js/polynomial-redos).
116
+ var parts = value.split(",");
117
+ for (var i = 0; i < parts.length; i += 1) {
118
+ var p = parts[i].trim();
119
+ if (!p) continue;
120
+ var eq = p.indexOf("=");
121
+ if (eq === -1) continue;
122
+ var feature = p.slice(0, eq).trim();
123
+ var allow = p.slice(eq + 1).trim();
124
+ if (!feature) continue;
125
+ if (!Object.prototype.hasOwnProperty.call(out, feature)) order.push(feature);
126
+ out[feature] = allow;
127
+ }
128
+ return { map: out, order: order };
129
+ }
130
+
131
+ // RFC-9651 value-list shape for ONE Permissions-Policy feature value (no
132
+ // leading "feature=" — just the allowlist): "*", "()", "self", or a
133
+ // parenthesised origin list "(self \"https://x\")". A hostile value with a
134
+ // comma / CRLF can't pass (no comma inside, the parens are balanced-only).
135
+ var PP_VALUE_RE = /^(?:\*|self|\([^),\r\n]*\))$/;
136
+ // Permissions-Policy feature names are RFC-9651 structured-field tokens —
137
+ // ASCII lowercase, hyphenated. Reject anything else so a hostile key can't
138
+ // inject a comma / CRLF into the header.
139
+ var PP_FEATURE_RE = /^[a-z][a-z0-9-]*$/; // allow:duplicate-regex — generic ASCII lowercase-hyphen token shape; shared shape, distinct domain (Permissions-Policy feature name)
140
+
81
141
  /**
82
142
  * @primitive b.csp.build
83
143
  * @signature b.csp.build(directives, opts?)
@@ -184,9 +244,15 @@ function build(directives, opts) {
184
244
  directives["trusted-types"] = opts.trustedTypesPolicies;
185
245
  }
186
246
 
187
- // Emit in canonical order (ALL_DIRECTIVES order) so operators
188
- // diffing two policies see structural diffs rather than ordering
189
- // noise.
247
+ return _emitCanonical(directives);
248
+ }
249
+
250
+ // Emit a directive map as a CSP header string in canonical (ALL_DIRECTIVES)
251
+ // order so operators diffing two policies see structural diffs rather than
252
+ // ordering noise. Pure serializer — it does NOT validate sources (build()
253
+ // and mergeDirectives() validate before calling it), so a trusted base
254
+ // policy round-trips through it untouched.
255
+ function _emitCanonical(directives) {
190
256
  var out = [];
191
257
  for (var di = 0; di < ALL_DIRECTIVES.length; di += 1) {
192
258
  var d = ALL_DIRECTIVES[di];
@@ -264,8 +330,178 @@ function hash(scriptBody, alg) {
264
330
  return "'" + algName + "-" + digest + "'";
265
331
  }
266
332
 
333
+ /**
334
+ * @primitive b.csp.mergeDirectives
335
+ * @signature b.csp.mergeDirectives(base, additions, opts?)
336
+ * @since 0.15.13
337
+ * @status stable
338
+ * @related b.csp.build, b.csp.mergePermissionsPolicy
339
+ *
340
+ * Derive a per-route CSP from a strict base by ADDING hosts to named
341
+ * directives, leaving every other directive exactly as the base. The fix for
342
+ * the "load a third-party SDK on one route" case: take the framework's strict
343
+ * default (pass `base` omitted / `undefined`) and add `https://js.stripe.com`
344
+ * to `script-src` + `frame-src` for the checkout route only, without
345
+ * re-typing the whole policy or relaxing `frame-ancestors` / `object-src` /
346
+ * `base-uri`.
347
+ *
348
+ * Additive only: each `additions[directive]` array is APPENDED (de-duped) to
349
+ * that directive's existing sources; a directive absent from the base is
350
+ * seeded from `default-src` (or `'self'`) first so it never lands wide-open.
351
+ * Only the ADDED sources are validated (CR/LF/NUL, catch-all `*`/`https:`,
352
+ * `data:` in img/media/font, unsafe-* in script directives) — the trusted
353
+ * base round-trips untouched. Returns a policy string for the middleware
354
+ * `csp:` opt. Throws `CspError` on an unknown directive, a hostile directive
355
+ * name, or a rejected added source.
356
+ *
357
+ * @opts
358
+ * acknowledgeUnsafe: boolean, // allow an added 'unsafe-*' in a script directive
359
+ * allowDataImages: boolean, // allow an added data: in img-src/media-src/font-src
360
+ *
361
+ * @example
362
+ * // Admit Stripe on the checkout route's script + frame directives only.
363
+ * var routeCsp = b.csp.mergeDirectives(undefined, {
364
+ * "script-src": ["https://js.stripe.com"],
365
+ * "frame-src": ["https://js.stripe.com"],
366
+ * });
367
+ * // -> the strict default with those two hosts appended; everything else unchanged
368
+ */
369
+ function mergeDirectives(base, additions, opts) {
370
+ var baseString = base === undefined ? securityHeaderDefaults.DEFAULT_CSP : base;
371
+ if (typeof baseString !== "string" || baseString.length === 0) {
372
+ throw new CspError("csp/bad-base",
373
+ "csp.mergeDirectives: base must be a non-empty CSP string (or undefined to " +
374
+ "derive from the framework DEFAULT_CSP)");
375
+ }
376
+ if (!additions || typeof additions !== "object" || Array.isArray(additions)) {
377
+ throw new CspError("csp/bad-additions",
378
+ "csp.mergeDirectives: additions must be an object keyed by CSP directive name, " +
379
+ "each value a non-empty array of source strings to ADD");
380
+ }
381
+ opts = opts || {};
382
+ validateOpts(opts, ["acknowledgeUnsafe", "allowDataImages"], "csp.mergeDirectives");
383
+
384
+ var addKeys = Object.keys(additions);
385
+ // Validate directive names (proto-safe) + that each addition is a
386
+ // non-empty array, before touching the base.
387
+ for (var ki = 0; ki < addKeys.length; ki += 1) {
388
+ var nm = addKeys[ki];
389
+ if (pick.isPoisonedKey(nm)) {
390
+ throw new CspError("csp/bad-directive-name",
391
+ "csp.mergeDirectives: '" + nm + "' is not a valid directive name");
392
+ }
393
+ if (ALL_DIRECTIVES.indexOf(nm) === -1) {
394
+ throw new CspError("csp/unknown-directive",
395
+ "csp.mergeDirectives: '" + nm + "' is not a recognized CSP3 directive");
396
+ }
397
+ if (!Array.isArray(additions[nm]) || additions[nm].length === 0) {
398
+ throw new CspError("csp/bad-directive-value",
399
+ "csp.mergeDirectives: additions['" + nm + "'] must be a non-empty array of sources");
400
+ }
401
+ }
402
+ // Validate ONLY the added sources by routing a throwaway additions-only
403
+ // map through build() (reuses every per-source rule). build() may seed
404
+ // Trusted-Types into the copy; we discard its output and keep the original
405
+ // additions for the merge.
406
+ var validationMap = {};
407
+ for (var vk = 0; vk < addKeys.length; vk += 1) validationMap[addKeys[vk]] = additions[addKeys[vk]].slice();
408
+ build(validationMap, { acknowledgeUnsafe: opts.acknowledgeUnsafe === true,
409
+ allowDataImages: opts.allowDataImages === true,
410
+ requireTrustedTypes: false });
411
+
412
+ var directives = _parseCspString(baseString);
413
+ for (var ai = 0; ai < addKeys.length; ai += 1) {
414
+ var name = addKeys[ai];
415
+ var existing = Object.prototype.hasOwnProperty.call(directives, name)
416
+ ? directives[name].slice()
417
+ : (directives["default-src"] ? directives["default-src"].slice() : ["'self'"]);
418
+ var added = additions[name];
419
+ // A directive value of 'none' MUST stand alone (CSP3 §2.3.1) — appending a
420
+ // host to it emits the malformed "'none' https://x". When merging real
421
+ // sources in, the added sources supersede 'none', so drop it first.
422
+ if (added.length && existing.length === 1 && existing[0] === "'none'") existing = [];
423
+ for (var si = 0; si < added.length; si += 1) {
424
+ if (existing.indexOf(added[si]) === -1) existing.push(added[si]);
425
+ }
426
+ directives[name] = existing;
427
+ }
428
+ return _emitCanonical(directives);
429
+ }
430
+
431
+ /**
432
+ * @primitive b.csp.mergePermissionsPolicy
433
+ * @signature b.csp.mergePermissionsPolicy(base, overrides, opts?)
434
+ * @since 0.15.13
435
+ * @status stable
436
+ * @related b.csp.mergeDirectives, b.csp.build
437
+ *
438
+ * Derive a per-route Permissions-Policy from a strict base by replacing the
439
+ * allowlist of NAMED features only, leaving every other feature at its `()`
440
+ * deny default. The companion to `mergeDirectives` for the Permissions-Policy
441
+ * header: re-enable `payment` to `(self "https://js.stripe.com")` on the
442
+ * checkout route while `camera` / `microphone` / `geolocation` stay denied.
443
+ *
444
+ * Each override value is validated as an RFC-9651 feature value-list (`*`,
445
+ * `()`, `self`, or a parenthesised origin list) — a value carrying a comma or
446
+ * CR/LF is refused so it can't inject a second feature or a header break. A
447
+ * feature not present in the base is added (opting it in); one present is
448
+ * replaced. Returns a header string for the middleware `permissionsPolicy:`
449
+ * opt. Throws `CspError` on a hostile feature name or a malformed value.
450
+ *
451
+ * @opts
452
+ * (none)
453
+ *
454
+ * @example
455
+ * var routePp = b.csp.mergePermissionsPolicy(undefined, {
456
+ * payment: '(self "https://js.stripe.com")',
457
+ * });
458
+ * // -> the strict default with payment re-enabled to that allowlist; all else denied
459
+ */
460
+ function mergePermissionsPolicy(base, overrides, opts) {
461
+ var baseString = base === undefined
462
+ ? securityHeaderDefaults.DEFAULT_PERMISSIONS.join(", ")
463
+ : base;
464
+ if (typeof baseString !== "string" || baseString.length === 0) {
465
+ throw new CspError("csp/bad-base",
466
+ "csp.mergePermissionsPolicy: base must be a non-empty Permissions-Policy string " +
467
+ "(or undefined to derive from the framework default)");
468
+ }
469
+ if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
470
+ throw new CspError("csp/bad-overrides",
471
+ "csp.mergePermissionsPolicy: overrides must be an object keyed by feature name, " +
472
+ "each value an RFC-9651 allowlist string");
473
+ }
474
+ if (opts !== undefined) validateOpts(opts || {}, [], "csp.mergePermissionsPolicy");
475
+
476
+ var parsed = _parsePermissionsPolicyString(baseString);
477
+ var map = parsed.map;
478
+ var order = parsed.order;
479
+ var keys = Object.keys(overrides);
480
+ for (var ki = 0; ki < keys.length; ki += 1) {
481
+ var feature = keys[ki];
482
+ if (pick.isPoisonedKey(feature) ||
483
+ !PP_FEATURE_RE.test(feature)) { // allow:regex-no-length-cap — anchored token shape (no backtracking); a feature name is a short RFC-9651 token
484
+ throw new CspError("csp/bad-feature-name",
485
+ "csp.mergePermissionsPolicy: '" + feature + "' is not a valid feature name");
486
+ }
487
+ var value = overrides[feature];
488
+ if (typeof value !== "string" || !PP_VALUE_RE.test(value)) { // allow:regex-no-length-cap — anchored alternation (no backtracking); a feature value is a short RFC-9651 value-list
489
+ throw new CspError("csp/bad-feature-value",
490
+ "csp.mergePermissionsPolicy: " + feature + " value must be one of *, (), self, or a " +
491
+ "parenthesised origin list (got " + JSON.stringify(value) + ")");
492
+ }
493
+ if (!Object.prototype.hasOwnProperty.call(map, feature)) order.push(feature);
494
+ map[feature] = value;
495
+ }
496
+ var out = [];
497
+ for (var oi = 0; oi < order.length; oi += 1) out.push(order[oi] + "=" + map[order[oi]]);
498
+ return out.join(", ");
499
+ }
500
+
267
501
  module.exports = {
268
502
  build: build,
503
+ mergeDirectives: mergeDirectives,
504
+ mergePermissionsPolicy: mergePermissionsPolicy,
269
505
  nonce: nonce,
270
506
  hash: hash,
271
507
  CspError: CspError,
package/lib/daemon.js CHANGED
@@ -42,7 +42,6 @@ var nodePath = require("node:path");
42
42
  var numericBounds = require("./numeric-bounds");
43
43
  var appShutdown = require("./app-shutdown");
44
44
  var processSpawn = require("./process-spawn");
45
- var lazyRequire = require("./lazy-require");
46
45
  var safeAsync = require("./safe-async");
47
46
  var atomicFile = require("./atomic-file");
48
47
  var validateOpts = require("./validate-opts");
@@ -50,7 +49,7 @@ var C = require("./constants");
50
49
  var { boot } = require("./log");
51
50
  var { defineClass } = require("./framework-error");
52
51
 
53
- var audit = lazyRequire(function () { return require("./audit"); });
52
+ var auditEmit = require("./audit-emit");
54
53
 
55
54
  var DaemonError = defineClass("DaemonError", { alwaysPermanent: true });
56
55
  var log = boot("daemon");
@@ -63,13 +62,7 @@ var DEFAULT_POLL_MS = 100;
63
62
  var DEFAULT_LOG_FILE_MODE = 0o600;
64
63
 
65
64
  function _safeAuditEmit(action, outcome, metadata) {
66
- try {
67
- audit().safeEmit({
68
- action: action,
69
- outcome: outcome || "success",
70
- metadata: metadata || {},
71
- });
72
- } catch (_e) { /* drop-silent — by design */ }
65
+ auditEmit.emit(action, metadata, outcome);
73
66
  }
74
67
 
75
68
  function _isLivePid(pid) {
@@ -80,48 +73,59 @@ function _isLivePid(pid) {
80
73
 
81
74
  function _readPidFile(pidFile) {
82
75
  try {
83
- var raw = nodeFs.readFileSync(pidFile, "utf8");
76
+ // Same fd-safe + capped + symlink-refusing read as app-shutdown's lockfile
77
+ // reader (one shape): a PID file is never a legit symlink mount, so
78
+ // refuseSymlink is safe; any throw → null ("nothing live there").
79
+ var raw = atomicFile.fdSafeReadSync(pidFile, { maxBytes: C.BYTES.kib(1), refuseSymlink: true, encoding: "utf8" });
84
80
  var pid = parseInt(String(raw).trim(), 10);
85
81
  return isFinite(pid) && pid > 0 ? pid : null;
86
82
  } catch (_e) { return null; }
87
83
  }
88
84
 
89
85
  function _validateStartOpts(opts) {
90
- validateOpts.requireObject(opts, "daemon.start", DaemonError, "daemon/bad-opts");
91
- validateOpts.requireNonEmptyString(opts.pidFile,
92
- "daemon.start: opts.pidFile (absolute path recommended)",
93
- DaemonError, "daemon/bad-pid-file");
94
- validateOpts.optionalNonEmptyString(opts.logFile,
95
- "daemon.start: opts.logFile", DaemonError, "daemon/bad-log-file");
96
- validateOpts.optionalNonEmptyStringArray(opts.signals,
97
- "daemon.start: opts.signals", DaemonError, "daemon/bad-signals");
98
- if (Array.isArray(opts.signals) && opts.signals.length === 0) {
99
- throw new DaemonError("daemon/bad-signals",
100
- "daemon.start: opts.signals must be a non-empty array of POSIX signal names");
101
- }
102
- validateOpts.optionalNonEmptyString(opts.command,
103
- "daemon.start: opts.command (path to executable)",
104
- DaemonError, "daemon/bad-command");
105
- if (opts.args !== undefined && !Array.isArray(opts.args)) {
106
- throw new DaemonError("daemon/bad-args",
107
- "daemon.start: opts.args must be an array of strings when present");
108
- }
109
- if (opts.command === undefined && opts.args !== undefined) {
110
- throw new DaemonError("daemon/bad-args",
111
- "daemon.start: opts.args requires opts.command");
112
- }
86
+ validateOpts.shape(opts, {
87
+ pidFile: { rule: "required-string", code: "daemon/bad-pid-file",
88
+ label: "daemon.start: opts.pidFile (absolute path recommended)" },
89
+ logFile: { rule: "optional-string", code: "daemon/bad-log-file",
90
+ label: "daemon.start: opts.logFile" },
91
+ signals: function (value) {
92
+ validateOpts.optionalNonEmptyStringArray(value,
93
+ "daemon.start: opts.signals", DaemonError, "daemon/bad-signals");
94
+ if (Array.isArray(value) && value.length === 0) {
95
+ throw new DaemonError("daemon/bad-signals",
96
+ "daemon.start: opts.signals must be a non-empty array of POSIX signal names");
97
+ }
98
+ },
99
+ command: { rule: "optional-string", code: "daemon/bad-command",
100
+ label: "daemon.start: opts.command (path to executable)" },
101
+ args: function (value) {
102
+ if (value !== undefined && !Array.isArray(value)) {
103
+ throw new DaemonError("daemon/bad-args",
104
+ "daemon.start: opts.args must be an array of strings when present");
105
+ }
106
+ if (opts.command === undefined && value !== undefined) {
107
+ throw new DaemonError("daemon/bad-args",
108
+ "daemon.start: opts.args requires opts.command");
109
+ }
110
+ },
111
+ }, "daemon.start", DaemonError, "daemon/bad-opts");
113
112
  }
114
113
 
115
114
  function _validateStopOpts(opts) {
116
- validateOpts.requireObject(opts, "daemon.stop", DaemonError, "daemon/bad-opts");
117
- validateOpts.requireNonEmptyString(opts.pidFile,
118
- "daemon.stop: opts.pidFile", DaemonError, "daemon/bad-pid-file");
119
- validateOpts.optionalNonEmptyString(opts.signal,
120
- "daemon.stop: opts.signal", DaemonError, "daemon/bad-signal");
121
- numericBounds.requirePositiveFiniteIntIfPresent(opts.timeoutMs,
122
- "daemon.stop: opts.timeoutMs", DaemonError, "daemon/bad-timeout");
123
- numericBounds.requirePositiveFiniteIntIfPresent(opts.pollMs,
124
- "daemon.stop: opts.pollMs", DaemonError, "daemon/bad-poll");
115
+ validateOpts.shape(opts, {
116
+ pidFile: { rule: "required-string", code: "daemon/bad-pid-file",
117
+ label: "daemon.stop: opts.pidFile" },
118
+ signal: { rule: "optional-string", code: "daemon/bad-signal",
119
+ label: "daemon.stop: opts.signal" },
120
+ timeoutMs: function (value) {
121
+ numericBounds.requirePositiveFiniteIntIfPresent(value,
122
+ "daemon.stop: opts.timeoutMs", DaemonError, "daemon/bad-timeout");
123
+ },
124
+ pollMs: function (value) {
125
+ numericBounds.requirePositiveFiniteIntIfPresent(value,
126
+ "daemon.stop: opts.pollMs", DaemonError, "daemon/bad-poll");
127
+ },
128
+ }, "daemon.stop", DaemonError, "daemon/bad-opts");
125
129
  }
126
130
 
127
131
  function _maybeReapStale(pidFile) {
package/lib/data-act.js CHANGED
@@ -89,6 +89,32 @@ var DESIGNATED_GATEKEEPERS = Object.freeze([
89
89
  "booking",
90
90
  ]);
91
91
 
92
+ // Is `recipient` a designated DMA gatekeeper? Match the gatekeeper token as a
93
+ // DNS LABEL of the recipient's host authority — any position, including the
94
+ // FINAL label. A substring `indexOf(g + ".")` test missed a gatekeeper that is
95
+ // the last label (e.g. "blog.google" / "drive.google" on the real ".google"
96
+ // TLD, or "data.amazon"), letting an Art 32 §1 third-party share through — and
97
+ // also false-flagged a non-gatekeeper whose name merely contained the token
98
+ // ("notgoogle.com"). Label matching closes the bypass and drops the false
99
+ // positive. A bare name ("Google") still matches exactly. URL / userinfo / port
100
+ // forms are normalized to the host authority first.
101
+ function _isGatekeeperRecipient(recipient) {
102
+ var s = String(recipient).toLowerCase().trim();
103
+ var scheme = s.indexOf("://");
104
+ if (scheme !== -1) s = s.slice(scheme + 3);
105
+ s = s.split("/")[0].split("?")[0].split("#")[0]; // drop path / query / fragment
106
+ var at = s.lastIndexOf("@");
107
+ if (at !== -1) s = s.slice(at + 1); // drop userinfo
108
+ s = s.split(":")[0]; // drop port
109
+ if (s.length === 0) return false;
110
+ var labels = s.split(".");
111
+ return DESIGNATED_GATEKEEPERS.some(function (g) {
112
+ if (s === g) return true;
113
+ for (var i = 0; i < labels.length; i++) { if (labels[i] === g) return true; }
114
+ return false;
115
+ });
116
+ }
117
+
92
118
  /**
93
119
  * @primitive b.dataAct.declareProduct
94
120
  * @signature b.dataAct.declareProduct(opts)
@@ -116,9 +142,12 @@ var DESIGNATED_GATEKEEPERS = Object.freeze([
116
142
  * });
117
143
  */
118
144
  function declareProduct(opts) {
119
- validateOpts.requireObject(opts, "dataAct.declareProduct", DataActError, "dataact/bad-opts");
120
- validateOpts.requireNonEmptyString(opts.productId, "productId", DataActError, "dataact/no-product-id");
121
- validateOpts.requireNonEmptyString(opts.dataHolder, "dataHolder", DataActError, "dataact/no-data-holder");
145
+ validateOpts.shape(opts, {
146
+ productId: { rule: "required-string", label: "productId", code: "dataact/no-product-id" },
147
+ dataHolder: { rule: "required-string", label: "dataHolder", code: "dataact/no-data-holder" },
148
+ productKind: "optional-string",
149
+ dataScope: "optional-string",
150
+ }, "dataAct.declareProduct", DataActError, "dataact/bad-opts");
122
151
  var kind = opts.productKind || "connected-product";
123
152
  if (kind !== "connected-product" && kind !== "related-service") {
124
153
  throw new DataActError("dataact/bad-kind",
@@ -218,14 +247,15 @@ function recordUserAccess(opts) {
218
247
  function shareWithThirdParty(opts) {
219
248
  validateOpts.requireObject(opts, "dataAct.shareWithThirdParty", DataActError, "dataact/bad-opts");
220
249
  _requireProduct(opts.productId, "shareWithThirdParty");
221
- validateOpts.requireNonEmptyString(opts.userId, "userId", DataActError, "dataact/no-user-id");
222
- validateOpts.requireNonEmptyString(opts.recipient, "recipient", DataActError, "dataact/no-recipient");
223
- validateOpts.requireNonEmptyString(opts.scope, "scope", DataActError, "dataact/no-scope");
250
+ validateOpts.shape(opts, {
251
+ productId: { rule: "required-string", label: "productId", code: "dataact/no-product-id" },
252
+ userId: { rule: "required-string", label: "userId", code: "dataact/no-user-id" },
253
+ recipient: { rule: "required-string", label: "recipient", code: "dataact/no-recipient" },
254
+ scope: { rule: "required-string", label: "scope", code: "dataact/no-scope" },
255
+ acceptGatekeeper: "optional-plain-object",
256
+ }, "dataAct.shareWithThirdParty", DataActError, "dataact/bad-opts");
224
257
 
225
- var recipientLower = opts.recipient.toLowerCase();
226
- var isGatekeeper = DESIGNATED_GATEKEEPERS.some(function (g) {
227
- return recipientLower === g || recipientLower.indexOf(g + ".") !== -1;
228
- });
258
+ var isGatekeeper = _isGatekeeperRecipient(opts.recipient);
229
259
  if (isGatekeeper) {
230
260
  if (!opts.acceptGatekeeper || typeof opts.acceptGatekeeper !== "object" ||
231
261
  typeof opts.acceptGatekeeper.reason !== "string" ||
@@ -291,9 +321,12 @@ function shareWithThirdParty(opts) {
291
321
  * });
292
322
  */
293
323
  function recordSwitchRequest(opts) {
294
- validateOpts.requireObject(opts, "dataAct.recordSwitchRequest", DataActError, "dataact/bad-opts");
295
- validateOpts.requireNonEmptyString(opts.customerId, "customerId", DataActError, "dataact/no-customer-id");
296
- validateOpts.requireNonEmptyString(opts.targetProvider, "targetProvider", DataActError, "dataact/no-target");
324
+ validateOpts.shape(opts, {
325
+ customerId: { rule: "required-string", label: "customerId", code: "dataact/no-customer-id" },
326
+ targetProvider: { rule: "required-string", label: "targetProvider", code: "dataact/no-target" },
327
+ dataSlices: "optional-string-array",
328
+ noticePeriodDays: "optional-non-negative",
329
+ }, "dataAct.recordSwitchRequest", DataActError, "dataact/bad-opts");
297
330
  if (!Array.isArray(opts.dataSlices) || opts.dataSlices.length === 0) {
298
331
  throw new DataActError("dataact/no-data-slices",
299
332
  "recordSwitchRequest: dataSlices must be a non-empty array");
@@ -172,8 +172,11 @@ function fileLifecycle(opts) {
172
172
  : nodePath.join(opts.dataDir, frameworkFiles.fileName("dbKeyEnc"));
173
173
  var flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
174
174
  var tmpDir = _resolveTmpDir(opts.tmpDir, opts.allowDiskFallback === true);
175
- if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
176
- if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true });
175
+ // 0o700 these hold the DB encryption tmpfs scratch + the data dir
176
+ // (db.key.enc, the decrypted DB while open); a default-mode mkdir leaves
177
+ // them group/other-traversable.
178
+ if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
179
+ if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true, mode: 0o700 });
177
180
 
178
181
  var dbPath = null;
179
182
  var encKey = null;
@@ -183,7 +186,9 @@ function fileLifecycle(opts) {
183
186
  function _loadOrGenerateKey() {
184
187
  if (encKey) return encKey;
185
188
  if (nodeFs.existsSync(keyPath)) {
186
- var sealedKey = nodeFs.readFileSync(keyPath, "utf8");
189
+ // Cap + fd-bound. NO refuseSymlink: dbKeyPath may be an operator-absolute
190
+ // KMS-fronted volume that legitimately symlinks the sealed key.
191
+ var sealedKey = atomicFile.fdSafeReadSync(keyPath, { maxBytes: C.BYTES.kib(64), encoding: "utf8" });
187
192
  var keyB64;
188
193
  try { keyB64 = opts.vault.unseal(sealedKey); }
189
194
  catch (e) {
@@ -212,7 +217,10 @@ function fileLifecycle(opts) {
212
217
  dbPath = nodePath.join(tmpDir, "blamejs-fl-" + label + "-" +
213
218
  generateToken(TMP_NAME_BYTES) + ".db");
214
219
  if (nodeFs.existsSync(encPath)) {
215
- var packed = nodeFs.readFileSync(encPath);
220
+ // Cap + fd-bound (deployment-tunable ceiling). NO refuseSymlink: the data
221
+ // dir may be a symlink-mounted volume (k8s PVC). db.enc is AEAD-checked
222
+ // downstream, so the cap is the OOM-before-cap defense.
223
+ var packed = atomicFile.fdSafeReadSync(encPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
216
224
  if (packed.length < 26) { // minimum envelope length
217
225
  throw new DbFileLifecycleError("db-file-lifecycle/short-envelope",
218
226
  "fileLifecycle: " + encPath + " too short to be a valid envelope (" + packed.length + " bytes)");
@@ -248,7 +256,7 @@ function fileLifecycle(opts) {
248
256
  catch (_e) { /* best-effort — operators on read-only handles or pre-init still flush */ }
249
257
  }
250
258
  if (!nodeFs.existsSync(dbPath)) return null;
251
- var plain = nodeFs.readFileSync(dbPath);
259
+ var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
252
260
  var packed = encryptPacked(plain, encKey, _aad(opts.dataDir, label));
253
261
  atomicFile.writeSync(encPath, packed);
254
262
  _emitAudit("flushed", "success", { label: label, bytes: plain.length });
@@ -269,7 +277,7 @@ function fileLifecycle(opts) {
269
277
  throw new DbFileLifecycleError("db-file-lifecycle/no-source",
270
278
  "fileLifecycle.snapshot: " + dbPath + " is missing");
271
279
  }
272
- var plain = nodeFs.readFileSync(dbPath);
280
+ var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
273
281
  return encryptPacked(plain, encKey, _aad(opts.dataDir, label));
274
282
  }
275
283