@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/db-query.js CHANGED
@@ -51,6 +51,7 @@ var sql = require("./sql");
51
51
  var audit = require("./audit");
52
52
  var lazyRequire = require("./lazy-require");
53
53
  var { DbQueryError } = require("./framework-error");
54
+ var numericBounds = require("./numeric-bounds");
54
55
 
55
56
  // Circular load — db.js requires db-query at module scope, so the
56
57
  // residency gate reaches back for getDataResidency() lazily.
@@ -682,11 +683,8 @@ class Query {
682
683
  var dbModule = require("./db"); // allow:inline-require — circular-load defense (db imports db-query)
683
684
  perCallLimit = dbModule.getStreamLimit();
684
685
  if (opts && opts.streamLimit !== undefined) {
685
- if (typeof opts.streamLimit !== "number" || !isFinite(opts.streamLimit) ||
686
- opts.streamLimit <= 0 || Math.floor(opts.streamLimit) !== opts.streamLimit) {
687
- throw new Error("Query.stream: opts.streamLimit must be a positive finite integer; got " +
688
- JSON.stringify(opts.streamLimit));
689
- }
686
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.streamLimit,
687
+ "Query.stream: opts.streamLimit", DbQueryError, "db-query/bad-stream-limit");
690
688
  perCallLimit = opts.streamLimit;
691
689
  }
692
690
  var stmt = this._db.prepare(built.sql);
@@ -1299,41 +1297,9 @@ class WhereBuilder {
1299
1297
  // Single linear pass, no backtracking regex; shares the scan shape with
1300
1298
  // b.safeSql.countPlaceholders.
1301
1299
  function _assertRawNoStringLiteral(rawSql, where) {
1302
- var i = 0;
1303
- var len = rawSql.length;
1304
- while (i < len) {
1305
- var ch = rawSql.charAt(i);
1306
- var next = i + 1 < len ? rawSql.charAt(i + 1) : "";
1307
- if (ch === '"') {
1308
- i += 1;
1309
- while (i < len) {
1310
- if (rawSql.charAt(i) === '"') {
1311
- if (rawSql.charAt(i + 1) === '"') { i += 2; continue; }
1312
- i += 1; break;
1313
- }
1314
- i += 1;
1315
- }
1316
- continue;
1317
- }
1318
- if (ch === "-" && next === "-") {
1319
- while (i < len && rawSql.charAt(i) !== "\n") i += 1;
1320
- continue;
1321
- }
1322
- if (ch === "/" && next === "*") {
1323
- i += 2;
1324
- while (i < len && !(rawSql.charAt(i) === "*" && rawSql.charAt(i + 1) === "/")) i += 1;
1325
- i += 2;
1326
- continue;
1327
- }
1328
- if (ch === "'") {
1329
- throw new safeSql.SafeSqlError(
1330
- where + ": raw SQL must not contain a string literal ('...') — bind every " +
1331
- "value with a ? placeholder, or pass { allowLiterals: true } when the literal " +
1332
- "is static and operator-controlled.",
1333
- "sql/raw-literal");
1334
- }
1335
- i += 1;
1336
- }
1300
+ // Routes through the shared safeSql scanner; the default error reproduces
1301
+ // this module's exact SafeSqlError("sql/raw-literal") message.
1302
+ safeSql.assertNoRawStringLiteral(rawSql, where);
1337
1303
  }
1338
1304
 
1339
1305
  // Apply one recorded WhereBuilder part onto a b.sql Predicate. `or`
@@ -1412,12 +1378,62 @@ function _unquoteIdent(s) {
1412
1378
  return s;
1413
1379
  }
1414
1380
 
1381
+ // Strip LEADING SQL comments + whitespace so the ^-anchored write-detection
1382
+ // regexes see the real statement head. Without this, a residency write smuggled
1383
+ // behind a leading "/* x */" or "-- x\n" comment is NOT recognized as a write →
1384
+ // the residency gate is skipped entirely (a cross-border-write BYPASS). The
1385
+ // executed SQL is unchanged; this normalized copy is only for the gate's parse.
1386
+ // Each replace is ^-anchored single-pass; the loop terminates when nothing more
1387
+ // is stripped (an unterminated /* leaves the head intact → write still detected
1388
+ // or fails closed downstream).
1389
+ function _stripLeadingSqlComments(sql) {
1390
+ var s = String(sql), prev;
1391
+ do {
1392
+ prev = s;
1393
+ s = s.replace(/^\s+/, ""); // allow:regex-no-length-cap — anchored, single leading run
1394
+ s = s.replace(/^--[^\n]*\r?\n?/, ""); // allow:regex-no-length-cap — anchored leading line comment
1395
+ s = s.replace(/^\/\*[\s\S]*?\*\//, ""); // allow:regex-no-length-cap — anchored leading block comment (lazy, single scan)
1396
+ } while (s !== prev);
1397
+ return s;
1398
+ }
1399
+
1400
+ // Non-anchored write-target scan for the writable-CTE / EXPLAIN-prefixed case.
1401
+ // A SQLite `WITH c AS (...) INSERT INTO residents ...` / `WITH ... UPDATE residents
1402
+ // SET ...` is a real write, but its effective verb is hidden behind the prefix so
1403
+ // the ^-anchored _RAW_WRITE_KEYWORD_RE misses it. This matches every INSERT/REPLACE
1404
+ // INTO, MERGE INTO, and UPDATE ... SET target token anywhere in the statement and
1405
+ // returns the first that names a residency table — so such a write still ENGAGES
1406
+ // the residency gate (_assertRawWriteResidency then fails CLOSED: the ^-anchored
1407
+ // body parsers can't read a CTE body, so it throws row-residency-raw-unparseable
1408
+ // directing the operator to b.db.from().insertOne/.updateOne). DELETE moves no
1409
+ // residency value across a border, so it is not a residency write. Linear scan.
1410
+ var _CTE_WRITE_TARGET_RE = /(?:\b(?:INSERT|REPLACE)\s+(?:OR\s+[A-Za-z]+\s+)?INTO|\bMERGE\s+INTO)\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?|\bUPDATE\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?\s+SET\b/ig; // allow:regex-no-length-cap — alternation, no nested quantifiers; linear
1411
+ function _firstResidencyWriteTarget(s) {
1412
+ _CTE_WRITE_TARGET_RE.lastIndex = 0;
1413
+ var m;
1414
+ while ((m = _CTE_WRITE_TARGET_RE.exec(s)) !== null) { // allow:regex-no-length-cap
1415
+ var t = _unquoteIdent(m[1] || m[2]);
1416
+ if (t && (cryptoField.getPerRowResidency(t) || cryptoField.getColumnResidency(t))) return t;
1417
+ }
1418
+ return null;
1419
+ }
1420
+
1415
1421
  function _rawWriteTable(sql) {
1416
- // Both regexes are ^-anchored (leading write keyword + table head): they scan
1417
- // only the statement head, so they are constant-time regardless of SQL length.
1418
- if (typeof sql !== "string" || !_RAW_WRITE_KEYWORD_RE.test(sql)) return null; // allow:regex-no-length-cap
1419
- var m = _RAW_TABLE_RE.exec(sql); // allow:regex-no-length-cap
1420
- return m ? _unquoteIdent(m[1] || m[2]) : null;
1422
+ // The ^-anchored regexes scan only the statement head (constant-time). Strip
1423
+ // leading comments first so a commented-out head can't hide the write.
1424
+ if (typeof sql !== "string") return null;
1425
+ var s = _stripLeadingSqlComments(sql);
1426
+ if (_RAW_WRITE_KEYWORD_RE.test(s)) { // allow:regex-no-length-cap
1427
+ var m = _RAW_TABLE_RE.exec(s); // allow:regex-no-length-cap
1428
+ return m ? _unquoteIdent(m[1] || m[2]) : null;
1429
+ }
1430
+ // Writable-CTE / EXPLAIN-prefixed write: the effective write verb is hidden
1431
+ // behind the prefix the ^-anchored test misses. If it writes a residency table,
1432
+ // return it so the gate engages and then fails closed on the unparseable body.
1433
+ if (/^\s*(?:WITH|EXPLAIN)\b/i.test(s)) { // allow:regex-no-length-cap
1434
+ return _firstResidencyWriteTarget(s);
1435
+ }
1436
+ return null;
1421
1437
  }
1422
1438
 
1423
1439
  // Cheap prepare-time pre-check so only writes to a residency table get wrapped.
@@ -1497,17 +1513,23 @@ function _assertRawWriteResidency(sql, boundParams) {
1497
1513
  if (!cryptoField.getPerRowResidency(table) && !cryptoField.getColumnResidency(table)) return;
1498
1514
  boundParams = _flattenRunParams(boundParams);
1499
1515
 
1516
+ // Parse the comment-stripped head: a leading "/* x */" / "-- x" comment must
1517
+ // not hide the INSERT/UPDATE body from the ^-anchored regexes below (that would
1518
+ // let a residency-restricted write through the gate). The executed SQL is
1519
+ // unchanged; this normalized copy is only for residency parsing.
1520
+ var norm = _stripLeadingSqlComments(sql);
1521
+
1500
1522
  // The INSERT/UPDATE body regexes below scan with [\s\S]+; bound the input
1501
1523
  // first and fail CLOSED on an over-long statement - a residency write the
1502
1524
  // framework cannot safely parse must be refused, never let past the gate.
1503
- if (sql.length > 100000) {
1525
+ if (norm.length > 100000) {
1504
1526
  throw new DbQueryError("db-query/row-residency-raw-unparseable",
1505
1527
  "raw write to residency table '" + table + "' exceeds the parse limit (" +
1506
- sql.length + " chars) - use b.db.from(\"" + table + "\") so residency is validated", true);
1528
+ norm.length + " chars) - use b.db.from(\"" + table + "\") so residency is validated", true);
1507
1529
  }
1508
1530
 
1509
- var mi = _RAW_INSERT_RE.exec(sql); // allow:regex-no-length-cap — input length-capped above
1510
- var mu = mi ? null : _RAW_UPDATE_RE.exec(sql); // allow:regex-no-length-cap — input length-capped above
1531
+ var mi = _RAW_INSERT_RE.exec(norm); // allow:regex-no-length-cap — input length-capped above
1532
+ var mu = mi ? null : _RAW_UPDATE_RE.exec(norm); // allow:regex-no-length-cap — input length-capped above
1511
1533
  if (!mi && !mu) {
1512
1534
  throw new DbQueryError("db-query/row-residency-raw-unparseable",
1513
1535
  "raw write to residency table '" + table + "' cannot be parsed to validate its " +
@@ -1543,4 +1565,8 @@ module.exports = {
1543
1565
  Query: Query,
1544
1566
  _isRawWriteToResidencyTable: _isRawWriteToResidencyTable,
1545
1567
  _assertRawWriteResidency: _assertRawWriteResidency,
1568
+ // Shared leading-comment stripper so db.js's storage-low write-gate sees the
1569
+ // real statement head (a `/* x */ INSERT` / WITH-prefixed write must not slip
1570
+ // the ENOSPC gate the way it slipped the residency gate).
1571
+ _stripLeadingSqlComments: _stripLeadingSqlComments,
1546
1572
  };
package/lib/db.js CHANGED
@@ -57,7 +57,7 @@ var { generateToken, generateBytes, encryptPacked, decryptPacked, sha3Hash } = r
57
57
  var cryptoField = require("./crypto-field");
58
58
  var dbDeclareRowPolicy = require("./db-declare-row-policy");
59
59
  var dbDeclareView = require("./db-declare-view");
60
- var { Query, _isRawWriteToResidencyTable, _assertRawWriteResidency } = require("./db-query");
60
+ var { Query, _isRawWriteToResidencyTable, _assertRawWriteResidency, _stripLeadingSqlComments } = require("./db-query");
61
61
  var dbSchema = require("./db-schema");
62
62
  var { defineClass } = require("./framework-error");
63
63
  var frameworkFiles = require("./framework-files");
@@ -161,6 +161,14 @@ var statfsProbe = null; // free-space reader (fs.statfsSync; injectable f
161
161
  var _exitHandlerRegistered = false;
162
162
  var dataDir = null;
163
163
  var initialized = false;
164
+ // Monotonic identity of the live `database` handle. Bumped on every (re)open,
165
+ // close, and reset so a deferred operation that captured the generation while
166
+ // one database was live can detect — before it writes — that the handle has
167
+ // since changed (closed / replaced). Anchors the fix for a fire-and-forget
168
+ // audit.checkpoint() launched by close() whose deferred insert would otherwise
169
+ // land in whatever database is live when its continuation resumes.
170
+ var _dbGenerationCounter = 0;
171
+ function dbGeneration() { return _dbGenerationCounter; }
164
172
  var dataResidency = null; // operator's declared region config (validated by storage backends)
165
173
  var subjectTables = []; // [{ name, subjectField, personalDataCategories }] — for subject.export/erase
166
174
  var tableMetadata = {}; // table name → metadata snapshot (PK/FK/sealed/derived) for getTableMetadata
@@ -193,6 +201,7 @@ var RESERVED_TABLE_NAMES = new Set([
193
201
  "_blamejs_subject_restrictions", // allow:hand-rolled-sql — canonical reserved local table-name declaration
194
202
  "_blamejs_subject_erasures", // allow:hand-rolled-sql — canonical reserved local table-name declaration
195
203
  "_blamejs_sessions", // allow:hand-rolled-sql — canonical reserved local table-name declaration
204
+ "_blamejs_session_valid_from", // allow:hand-rolled-sql — canonical reserved local table-name declaration
196
205
  "_blamejs_jobs", // allow:hand-rolled-sql — canonical reserved local table-name declaration
197
206
  "_blamejs_migrations", // allow:hand-rolled-sql — canonical reserved local table-name declaration
198
207
  "_blamejs_counters", // allow:hand-rolled-sql — canonical reserved local table-name declaration
@@ -480,6 +489,23 @@ var FRAMEWORK_SCHEMA = [
480
489
  sealedFields: ["userId", "data"],
481
490
  derivedHashes: { userIdHash: { from: "userId" } },
482
491
  },
492
+ {
493
+ // _blamejs_session_valid_from — per-subject valid-from boundary for
494
+ // stateless-token revocation (b.session.bump / check / validFrom). Same
495
+ // dual-storage pattern as _blamejs_sessions: mirrors the cluster-mode DDL
496
+ // in framework-schema.js so cluster-storage routes to either backend.
497
+ // subjectHash is the PRIMARY KEY — the plaintext subject id never lands
498
+ // here. No sealed columns: the hash is non-reversible and the epoch is not
499
+ // personal data.
500
+ name: "_blamejs_session_valid_from", // allow:hand-rolled-sql — canonical local-schema table-name declaration
501
+ columns: {
502
+ subjectHash: "TEXT PRIMARY KEY",
503
+ validFromEpoch: "INTEGER NOT NULL",
504
+ updatedAt: "INTEGER NOT NULL",
505
+ },
506
+ indexes: [],
507
+ sealedFields: [],
508
+ },
483
509
  {
484
510
  // _blamejs_api_keys — operator-facing API-key registry. Sealed
485
511
  // columns: ownerId / scopes / metadata. The secret never lands
@@ -722,7 +748,7 @@ function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
722
748
  var keyPath = keyPathOverride || nodePath.join(dataDirPath, frameworkFiles.fileName("dbKeyEnc"));
723
749
  var aad = _dbKeyAad(dataDirPath, keyPath);
724
750
  if (nodeFs.existsSync(keyPath)) {
725
- var sealed = atomicFile.readSync(keyPath, { encoding: "utf8" }).trim();
751
+ var sealed = atomicFile.readSync(keyPath, { encoding: "utf8", maxBytes: C.BYTES.kib(64) }).trim();
726
752
  var b64;
727
753
  // isAadSealed is checked FIRST and is load-bearing: AAD_PREFIX
728
754
  // ("vault.aad:") is NOT a prefix of VAULT_PREFIX ("vault:"), so a
@@ -785,7 +811,10 @@ function decryptToTmp() {
785
811
  try { nodeFs.unlinkSync(dbPath + "-shm"); } catch (_e) { /* may not exist */ }
786
812
  }
787
813
  }
788
- var packed = nodeFs.readFileSync(encPath);
814
+ // Cap + fd-bound db.enc read at db.init. NO refuseSymlink: the data dir may be
815
+ // a symlink-mounted volume (k8s PVC). db.enc is AEAD-verified downstream, so
816
+ // the cap is the OOM-before-cap defense (was an uncapped read).
817
+ var packed = atomicFile.fdSafeReadSync(encPath, { maxBytes: C.BYTES.gib(2) });
789
818
  if (packed.length < 26) return; // too short to be a valid envelope
790
819
  // AAD binds the envelope to this deployment's data dir so two
791
820
  // installs sharing the same operator passphrase can't swap each
@@ -864,11 +893,27 @@ function _probeStorageHeadroom() {
864
893
  // DELETE, PRAGMA, and DDL pass through ungated. Called once in init() after
865
894
  // schema setup so init's own writes are never gated (writesRefused is false
866
895
  // until the first probe anyway).
896
+ // A growth write for the storage-low gate: INSERT/UPDATE/REPLACE at the head
897
+ // AFTER stripping leading comments, OR a writable-CTE / EXPLAIN-prefixed write
898
+ // (`WITH c AS (...) INSERT INTO t ...`) whose effective verb places rows. The
899
+ // leading-keyword-only test missed both a `/* x */ INSERT` and a WITH-prefixed
900
+ // growth write, letting them proceed into a near-full tmpfs (the ENOSPC the gate
901
+ // exists to prevent). Matches write SYNTAX (INTO / UPDATE..SET) so a WITH..SELECT
902
+ // carrying the word "insert" in a value is not refused.
903
+ function _isGrowthWrite(sql) {
904
+ if (typeof sql !== "string") return false;
905
+ var s = _stripLeadingSqlComments(sql);
906
+ if (/^\s*(?:INSERT|UPDATE|REPLACE)\b/i.test(s)) return true;
907
+ if (/^\s*(?:WITH|EXPLAIN)\b/i.test(s) &&
908
+ /\b(?:(?:INSERT|REPLACE|MERGE)\s+(?:OR\s+[A-Za-z]+\s+)?INTO|UPDATE\s+[\w".`]+\s+SET)\b/i.test(s)) return true;
909
+ return false;
910
+ }
911
+
867
912
  function _installWriteGate() {
868
913
  var rawPrepare = database.prepare.bind(database);
869
914
  database.prepare = function (sql) {
870
915
  var stmt = rawPrepare(sql);
871
- if (/^\s*(?:INSERT|UPDATE|REPLACE)\b/i.test(sql)) {
916
+ if (_isGrowthWrite(sql)) {
872
917
  var rawRun = stmt.run.bind(stmt);
873
918
  stmt.run = function () {
874
919
  if (writesRefused) {
@@ -889,7 +934,7 @@ function encryptToDisk() {
889
934
  // Force WAL checkpoint so the .db file holds all committed transactions.
890
935
  try { runSql(database, "PRAGMA wal_checkpoint(TRUNCATE)"); } catch (_e) { /* best effort */ }
891
936
  if (!nodeFs.existsSync(dbPath)) return;
892
- atomicFile.writeSync(encPath, encryptPacked(nodeFs.readFileSync(dbPath), encKey, _dbEncAad(dataDir)));
937
+ atomicFile.writeSync(encPath, encryptPacked(atomicFile.fdSafeReadSync(dbPath, { maxBytes: C.BYTES.gib(2) }), encKey, _dbEncAad(dataDir)));
893
938
  }
894
939
 
895
940
  /**
@@ -926,7 +971,7 @@ function snapshot() {
926
971
  throw _dbErr("db/snapshot-no-source",
927
972
  "snapshot: plaintext DB at " + dbPath + " is missing — did init complete?");
928
973
  }
929
- var plain = nodeFs.readFileSync(dbPath);
974
+ var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: C.BYTES.gib(2) });
930
975
  if (!encPath || !encKey) {
931
976
  // atRest: 'plain' — return the raw bytes. Operators wanting an
932
977
  // encrypted snapshot under plain mode wrap with their own
@@ -1043,12 +1088,8 @@ async function init(opts) {
1043
1088
  // on bad shape so a typo surfaces at boot rather than as an
1044
1089
  // unbounded stream at first export.
1045
1090
  if (opts.streamLimit !== undefined) {
1046
- if (typeof opts.streamLimit !== "number" || !isFinite(opts.streamLimit) ||
1047
- opts.streamLimit <= 0 || Math.floor(opts.streamLimit) !== opts.streamLimit) {
1048
- throw new DbError("db/bad-init",
1049
- "db.init: streamLimit must be a positive finite integer; got " +
1050
- JSON.stringify(opts.streamLimit));
1051
- }
1091
+ require("./numeric-bounds").requirePositiveFiniteIntIfPresent(opts.streamLimit,
1092
+ "db.init: streamLimit", DbError, "db/bad-init");
1052
1093
  streamLimit = opts.streamLimit;
1053
1094
  }
1054
1095
  // Column-membership gate mode — throw at config-time on a typo so it
@@ -1171,6 +1212,7 @@ async function init(opts) {
1171
1212
  sqlLength: C.BYTES.mib(1),
1172
1213
  },
1173
1214
  });
1215
+ _dbGenerationCounter++; // a new live handle — see dbGeneration()
1174
1216
 
1175
1217
  // Performance pragmas
1176
1218
  runSql(database, "PRAGMA journal_mode=WAL");
@@ -1813,12 +1855,8 @@ function stream(sql) {
1813
1855
  // typo surfaces instead of an unbounded stream.
1814
1856
  var perCallLimit = streamLimit;
1815
1857
  if (opts && opts.streamLimit !== undefined) {
1816
- if (typeof opts.streamLimit !== "number" || !isFinite(opts.streamLimit) ||
1817
- opts.streamLimit <= 0 || Math.floor(opts.streamLimit) !== opts.streamLimit) {
1818
- throw new DbError("db/bad-stream-limit",
1819
- "db.stream: opts.streamLimit must be a positive finite integer; got " +
1820
- JSON.stringify(opts.streamLimit));
1821
- }
1858
+ require("./numeric-bounds").requirePositiveFiniteIntIfPresent(opts.streamLimit,
1859
+ "db.stream: opts.streamLimit", DbError, "db/bad-stream-limit");
1822
1860
  perCallLimit = opts.streamLimit;
1823
1861
  }
1824
1862
 
@@ -2355,6 +2393,7 @@ function close() {
2355
2393
  // boot (integrity-probed, falling back to db.enc if it is itself corrupt).
2356
2394
  if (atRest === "encrypted" && encryptOk) removePlaintextFiles();
2357
2395
  database = null;
2396
+ _dbGenerationCounter++; // handle gone — invalidate any deferred op that captured the prior generation
2358
2397
  initialized = false;
2359
2398
  }
2360
2399
 
@@ -2591,14 +2630,12 @@ function declareRequireDualControl(args) {
2591
2630
  }
2592
2631
  var m = args.m === undefined ? 2 : args.m;
2593
2632
  var n = args.n === undefined ? Math.max(2, m) : args.n;
2594
- if (typeof m !== "number" || !isFinite(m) || m < 2 || Math.floor(m) !== m) {
2595
- throw new DbError("db/dual-control-bad-quorum",
2596
- "declareRequireDualControl: m must be an integer >= 2");
2597
- }
2598
- if (typeof n !== "number" || !isFinite(n) || n < m || Math.floor(n) !== n) {
2599
- throw new DbError("db/dual-control-bad-quorum",
2600
- "declareRequireDualControl: n must be an integer >= m");
2601
- }
2633
+ // m >= 2, n >= m, both integers via numericBounds (matches db.js's existing
2634
+ // inline numeric-bounds usage; throws DbError("db/dual-control-bad-quorum")).
2635
+ require("./numeric-bounds").requirePositiveFiniteInt(m,
2636
+ "declareRequireDualControl: m", DbError, "db/dual-control-bad-quorum", { min: 2 });
2637
+ require("./numeric-bounds").requirePositiveFiniteInt(n,
2638
+ "declareRequireDualControl: n", DbError, "db/dual-control-bad-quorum", { min: m });
2602
2639
  if (args.posture !== undefined && args.posture !== null &&
2603
2640
  (typeof args.posture !== "string" || args.posture.length === 0)) {
2604
2641
  throw new DbError("db/dual-control-bad-posture",
@@ -2765,7 +2802,7 @@ function _checkRollback(dataDirPath) {
2765
2802
  }
2766
2803
  var tip;
2767
2804
  try {
2768
- tip = safeJson.parse(atomicFile.readSync(tipPath), { schema: AUDIT_TIP_SCHEMA });
2805
+ tip = safeJson.parse(atomicFile.readSync(tipPath, { maxBytes: C.BYTES.kib(64) }), { schema: AUDIT_TIP_SCHEMA });
2769
2806
  } catch (e) {
2770
2807
  throw _dbErr("db/audit-tip-unreadable",
2771
2808
  "FATAL: audit.tip unreadable or schema-invalid at " + tipPath + " — " + e.message +
@@ -2879,6 +2916,7 @@ function _resetForTest() {
2879
2916
  try { if (database) database.close(); }
2880
2917
  catch (e) { log.debug("test-reset close failed", { error: e.message }); }
2881
2918
  database = null;
2919
+ _dbGenerationCounter++; // handle gone — see dbGeneration()
2882
2920
  dbPath = null;
2883
2921
  encPath = null;
2884
2922
  encKey = null;
@@ -3308,6 +3346,7 @@ function getActivePosture() { return _activePosture; }
3308
3346
 
3309
3347
  module.exports = {
3310
3348
  init: init,
3349
+ _dbGeneration: dbGeneration,
3311
3350
  applyPosture: applyPosture,
3312
3351
  getActivePosture: getActivePosture,
3313
3352
  vacuumAfterErase: vacuumAfterErase,
package/lib/dbsc.js CHANGED
@@ -198,12 +198,11 @@ function verifyBindingAssertion(assertion, opts) {
198
198
  // import boundary (alg-confusion defense — JWT_KEY_CONFUSION-class
199
199
  // attacks pass an HS256 jwk for an ES256-claimed token).
200
200
  jwtExternal._assertAlgKtyMatch(headerJson.alg, headerJson.jwk);
201
- var pubKey;
202
- try { pubKey = nodeCrypto.createPublicKey({ key: headerJson.jwk, format: "jwk" }); }
203
- catch (e) {
204
- throw new DbscError("dbsc/bad-jwk",
205
- "verifyBindingAssertion: jwk could not be imported: " + ((e && e.message) || String(e)));
206
- }
201
+ var pubKey = bCrypto.importPublicJwk(headerJson.jwk, {
202
+ errorClass: DbscError,
203
+ code: "dbsc/bad-jwk",
204
+ messagePrefix: "verifyBindingAssertion: jwk could not be imported: ",
205
+ });
207
206
  var signingInput = parts[0] + "." + parts[1];
208
207
  var sigBytes = Buffer.from(parts[2], "base64url");
209
208
  var ok;
@@ -207,20 +207,24 @@ function _memoryStore() {
207
207
  */
208
208
  function create(opts) {
209
209
  opts = opts || {};
210
- validateOpts(opts, [
211
- "audit", "approvers", "windowSpec", "posture", "signWith",
212
- "verifyWith", "store", "now", "selfApproval",
213
- ], "ddlChangeControl.create");
214
- validateOpts.auditShape(opts.audit, "ddlChangeControl",
215
- DdlChangeControlError, "ddl-change-control/bad-audit");
216
- validateOpts.optionalFunction(opts.signWith,
217
- "ddlChangeControl: signWith", DdlChangeControlError, "ddl-change-control/bad-signer");
218
- validateOpts.optionalFunction(opts.verifyWith,
219
- "ddlChangeControl: verifyWith", DdlChangeControlError, "ddl-change-control/bad-verifier");
220
- validateOpts.optionalFunction(opts.now,
221
- "ddlChangeControl: now", DdlChangeControlError, "ddl-change-control/bad-now");
222
- validateOpts.optionalNonEmptyString(opts.posture,
223
- "ddlChangeControl: posture", DdlChangeControlError, "ddl-change-control/bad-posture");
210
+ validateOpts.shape(opts, {
211
+ audit: function (value) {
212
+ validateOpts.auditShape(value, "ddlChangeControl",
213
+ DdlChangeControlError, "ddl-change-control/bad-audit");
214
+ },
215
+ signWith: { rule: "optional-function", code: "ddl-change-control/bad-signer" },
216
+ verifyWith: { rule: "optional-function", code: "ddl-change-control/bad-verifier" },
217
+ now: { rule: "optional-function", code: "ddl-change-control/bad-now" },
218
+ posture: { rule: "optional-string", code: "ddl-change-control/bad-posture" },
219
+ // approvers / windowSpec / store keep their own bespoke validation in
220
+ // the factory body (distinct error codes, floor semantics, the window
221
+ // grammar parser, the silent in-memory fallback) — declared here so the
222
+ // exhaustive contract accepts them while the body remains authoritative.
223
+ approvers: function () {},
224
+ windowSpec: function () {},
225
+ store: function () {},
226
+ selfApproval: { rule: "optional-boolean", code: "ddl-change-control/bad-self-approval" },
227
+ }, "ddlChangeControl", DdlChangeControlError, "ddl-change-control/bad-posture");
224
228
 
225
229
  var approvers = 2;
226
230
  if (opts.approvers !== undefined) {
package/lib/deprecate.js CHANGED
@@ -48,6 +48,7 @@
48
48
 
49
49
  var safeEnv = require("./parsers/safe-env");
50
50
  var validateOpts = require("./validate-opts");
51
+ var boundedMap = require("./bounded-map");
51
52
  var { FrameworkError } = require("./framework-error");
52
53
 
53
54
  class DeprecateError extends FrameworkError {
@@ -131,9 +132,8 @@ function warn(name, opts) {
131
132
  _validateOpts(opts, "warn");
132
133
 
133
134
  var key = name + ":" + opts.since;
134
- var entry = _seen.get(key);
135
- if (!entry) {
136
- entry = {
135
+ var entry = boundedMap.getOrInsert(_seen, key, function () {
136
+ return {
137
137
  name: name,
138
138
  since: opts.since,
139
139
  removeIn: opts.removeIn,
@@ -142,8 +142,7 @@ function warn(name, opts) {
142
142
  callCount: 0,
143
143
  firstSeen: new Date().toISOString(),
144
144
  };
145
- _seen.set(key, entry);
146
- }
145
+ });
147
146
  entry.callCount++;
148
147
 
149
148
  var mode = _modeFromEnv();
package/lib/did.js CHANGED
@@ -46,6 +46,7 @@
46
46
  */
47
47
 
48
48
  var nodeCrypto = require("node:crypto");
49
+ var bCrypto = require("./crypto");
49
50
  var safeJson = require("./safe-json");
50
51
  var validateOpts = require("./validate-opts");
51
52
  var { defineClass } = require("./framework-error");
@@ -380,8 +381,11 @@ function _jwkToKey(jwk) {
380
381
  throw new DidError("did/unsupported-key",
381
382
  "did: verificationMethod publicKeyJwk has unsupported kty/crv (" + jwk.kty + "/" + jwk.crv + ")");
382
383
  }
383
- try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
384
- catch (e) { throw new DidError("did/bad-key", "did: verificationMethod publicKeyJwk is invalid: " + ((e && e.message) || e)); }
384
+ return bCrypto.importPublicJwk(jwk, {
385
+ errorClass: DidError,
386
+ code: "did/bad-key",
387
+ messagePrefix: "did: verificationMethod publicKeyJwk is invalid: ",
388
+ });
385
389
  }
386
390
 
387
391
  // Extract verification methods from a DID document → KeyObjects.
package/lib/dora.js CHANGED
@@ -62,12 +62,19 @@ var SIGNIFICANT_INCIDENT_THRESHOLDS = Object.freeze({
62
62
  durationCriticalProcessMs: C.TIME.hours(2), // 2h
63
63
  });
64
64
 
65
- // Article 19 initial report deadline: 24h from "first awareness".
66
- // Article 19(4) intermediate update: within 72h of initial.
67
- // Article 19(6) final report: within 1 month of initial.
68
- var INITIAL_REPORT_DEADLINE_MS = C.TIME.hours(24);
69
- var INTERMEDIATE_REPORT_DEADLINE_MS = C.TIME.hours(72);
70
- var FINAL_REPORT_DEADLINE_MS = C.TIME.days(30);
65
+ // DORA major-incident reporting timeline (Art. 19 + RTS (EU) 2024/1772 Art. 5):
66
+ // - Initial notification: as early as possible and WITHIN 4 HOURS of
67
+ // classifying the incident as major (the outer bound is 24h from first
68
+ // awareness). classify() returns mustReportInitialByMs as a duration FROM
69
+ // classification, so the operative window encoded here is 4h — the
70
+ // 24h-from-awareness outer bound is enforced separately via
71
+ // INITIAL_REPORT_OUTER_DEADLINE_MS below.
72
+ // - Intermediate update: within 72h of the INITIAL notification's submission.
73
+ // - Final report: within 1 month of the intermediate report's submission.
74
+ var INITIAL_REPORT_DEADLINE_MS = C.TIME.hours(4);
75
+ var INITIAL_REPORT_OUTER_DEADLINE_MS = C.TIME.hours(24);
76
+ var INTERMEDIATE_REPORT_DEADLINE_MS = C.TIME.hours(72);
77
+ var FINAL_REPORT_DEADLINE_MS = C.TIME.days(30);
71
78
 
72
79
  // Adjacent-regulation incident-reporting deadlines — operators wiring
73
80
  // NIS2 / CRA / HIPAA breach notification reach for these constants
@@ -124,14 +131,26 @@ function _classifyImpl(input) {
124
131
  reasons.push("severity-high");
125
132
  }
126
133
 
127
- // 2. Affected clients (absolute).
134
+ // 2. Affected clients absolute count OR share of the client base. The
135
+ // percentile thresholds (RTS 2024/1772 Art. 1(1)(a): >10% major, ESA-
136
+ // guideline 1% significant) were declared but never evaluated, so a
137
+ // large-share-but-small-absolute incident (e.g. 16% of a 50k client base)
138
+ // under-classified. Operators pass clientBase (total clients) to enable
139
+ // the percentage criterion; absolute-only behavior is unchanged when it
140
+ // is omitted.
128
141
  if (typeof input.affectedClients === "number" && input.affectedClients > 0) {
129
- if (input.affectedClients >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsAbsolute) {
142
+ var clientBase = (typeof input.clientBase === "number" && input.clientBase > 0) ? input.clientBase : null;
143
+ var clientPct = clientBase ? (input.affectedClients / clientBase) : 0;
144
+ var majorByClients = input.affectedClients >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsAbsolute ||
145
+ (clientBase !== null && clientPct >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsPercentile);
146
+ var significantByClients = input.affectedClients >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsAbsolute ||
147
+ (clientBase !== null && clientPct >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsPercentile);
148
+ if (majorByClients) {
130
149
  hitsMajor += 1;
131
- reasons.push("clients-major-absolute");
132
- } else if (input.affectedClients >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsAbsolute) {
150
+ reasons.push("clients-major");
151
+ } else if (significantByClients) {
133
152
  hitsSignificant += 1;
134
- reasons.push("clients-significant-absolute");
153
+ reasons.push("clients-significant");
135
154
  }
136
155
  }
137
156
 
@@ -188,7 +207,11 @@ function _classifyImpl(input) {
188
207
  return {
189
208
  classification: classification,
190
209
  mustReport: mustReport,
210
+ // Operative initial-notification window: 4h from this classification.
191
211
  mustReportInitialByMs: mustReport ? INITIAL_REPORT_DEADLINE_MS : null,
212
+ // Outer bound: no later than 24h from first awareness — surfaced so an
213
+ // operator scheduling from `detectedAt` can enforce min(class+4h, aware+24h).
214
+ mustReportInitialOuterByMs: mustReport ? INITIAL_REPORT_OUTER_DEADLINE_MS : null,
192
215
  reasons: reasons,
193
216
  };
194
217
  }
@@ -343,10 +366,16 @@ function create(opts) {
343
366
  // Article 19 deadline — operator-side scheduler uses this.
344
367
  nextStageDueAt: null,
345
368
  };
369
+ // Anchor each downstream deadline on the SUBMISSION time of the report
370
+ // being filed (record.reportedAt), not on detectedAt: the intermediate is
371
+ // due 72h after the initial is *submitted*, and the final one month after
372
+ // the intermediate is *submitted* (DORA RTS 2024/1772 reporting timeline).
373
+ // Anchoring on detectedAt understates the deadline whenever a report is
374
+ // filed later than detection.
346
375
  if (input.stage === "initial") {
347
- record.nextStageDueAt = input.detectedAt + INTERMEDIATE_REPORT_DEADLINE_MS;
376
+ record.nextStageDueAt = record.reportedAt + INTERMEDIATE_REPORT_DEADLINE_MS;
348
377
  } else if (input.stage === "intermediate") {
349
- record.nextStageDueAt = input.detectedAt + FINAL_REPORT_DEADLINE_MS;
378
+ record.nextStageDueAt = record.reportedAt + FINAL_REPORT_DEADLINE_MS;
350
379
  }
351
380
  _emit("dora.incident.reported", {
352
381
  metadata: {
@@ -396,6 +425,7 @@ module.exports = {
396
425
  MAJOR_INCIDENT_THRESHOLDS: MAJOR_INCIDENT_THRESHOLDS,
397
426
  SIGNIFICANT_INCIDENT_THRESHOLDS: SIGNIFICANT_INCIDENT_THRESHOLDS,
398
427
  INITIAL_REPORT_DEADLINE_MS: INITIAL_REPORT_DEADLINE_MS,
428
+ INITIAL_REPORT_OUTER_DEADLINE_MS: INITIAL_REPORT_OUTER_DEADLINE_MS,
399
429
  INTERMEDIATE_REPORT_DEADLINE_MS: INTERMEDIATE_REPORT_DEADLINE_MS,
400
430
  FINAL_REPORT_DEADLINE_MS: FINAL_REPORT_DEADLINE_MS,
401
431
  DEADLINES_NIS2: DEADLINES_NIS2,
package/lib/dr-runbook.js CHANGED
@@ -275,7 +275,7 @@ async function emit(opts) {
275
275
  "drRunbook.emit: outDir", DrRunbookError, "dr-runbook/no-outdir");
276
276
  validateOpts.requireNonEmptyString(opts.posture,
277
277
  "drRunbook.emit: posture", DrRunbookError, "dr-runbook/no-posture");
278
- if (!POSTURE_BLOCKS[opts.posture]) {
278
+ if (!Object.prototype.hasOwnProperty.call(POSTURE_BLOCKS, opts.posture)) {
279
279
  throw new DrRunbookError("dr-runbook/unknown-posture",
280
280
  "drRunbook.emit: posture '" + opts.posture + "' not in supported list (" +
281
281
  Object.keys(POSTURE_BLOCKS).join(", ") + ")");
package/lib/dsa.js CHANGED
@@ -36,6 +36,7 @@
36
36
 
37
37
  var validateOpts = require("./validate-opts");
38
38
  var lazyRequire = require("./lazy-require");
39
+ var numericBounds = require("./numeric-bounds");
39
40
  var C = require("./constants");
40
41
  var { DsaError } = require("./framework-error");
41
42
 
@@ -414,7 +415,7 @@ function transparencyReport(opts) {
414
415
  METRIC_FIELDS.forEach(function (field) {
415
416
  var v = supplied[field];
416
417
  if (v === undefined || v === null) { metrics[field] = 0; return; }
417
- if (typeof v !== "number" || !isFinite(v) || v < 0 || Math.floor(v) !== v) {
418
+ if (!numericBounds.isNonNegativeFiniteInt(v)) {
418
419
  throw new DsaError("dsa/bad-metric-value",
419
420
  "b.dsa.transparencyReport: metrics." + field +
420
421
  " must be a non-negative integer, got " +