@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
@@ -45,12 +45,14 @@ var safeAsync = require("./safe-async");
45
45
  var safeSql = require("./safe-sql");
46
46
  var sql = require("./sql");
47
47
  var C = require("./constants");
48
+ var boundedMap = require("./bounded-map");
48
49
  var { FrameworkError } = require("./framework-error");
49
50
 
50
- // Allowlist of chain table names. Adding a new chain-backed table
51
- // (e.g. some future _blamejs_security_log) requires registering it here
52
- // so an operator can't accidentally point a chain-writer at a non-chain
53
- // table and corrupt the chain semantics.
51
+ // Allowlist of chain table names. The two framework chains ship registered; a
52
+ // consumer's own append-only hash-chained table is added at config time via
53
+ // registerTable() BEFORE create() accepts it. The allowlist is never bypassed
54
+ // — an unregistered table throws at create(), so a misconfig can't point a
55
+ // chain-writer at a non-chain table and corrupt the chain semantics.
54
56
  var ALLOWED_CHAIN_TABLES = new Set(["audit_log", "consent_log"]);
55
57
 
56
58
  var FRAMEWORK_SQL_TIMEOUT_MS = C.TIME.seconds(30);
@@ -74,24 +76,83 @@ class ChainWriterError extends FrameworkError {
74
76
  }
75
77
  }
76
78
 
79
+ /**
80
+ * @primitive b.chainWriter.registerTable
81
+ * @signature b.chainWriter.registerTable(table)
82
+ * @since 0.15.13
83
+ * @status stable
84
+ * @related b.chainWriter.create, b.safeSql.validateIdentifier
85
+ *
86
+ * Register a consumer-owned append-only table as chain-writable so
87
+ * `b.chainWriter.create({ table })` accepts it. Call once at boot (config
88
+ * time) for each app table carrying the chain columns (`monotonicCounter`,
89
+ * `recordedAt`, `nonce`, `prevHash`, `rowHash` — plus `fencingToken` in
90
+ * cluster mode). The framework chains (`audit_log`, `consent_log`) are
91
+ * pre-registered. Throws `ChainWriterError` (`chain-writer/invalid-config`) on
92
+ * a non-identifier name; the name is validated against the SQL identifier
93
+ * rules because it is interpolated into the chain SQL. Idempotent. Returns the
94
+ * registered name.
95
+ *
96
+ * Operator footgun to avoid on a MULTI-chain table (one configured with a
97
+ * `chainKey`): the per-key writer restarts `monotonicCounter` at 1 for each
98
+ * key, so a UNIQUE index on `monotonicCounter` ALONE (the shape the framework
99
+ * `audit_log` uses for its single chain) will reject the second key's first
100
+ * row. A keyed chain's uniqueness must be the composite
101
+ * `(chainKey, monotonicCounter)`, never `monotonicCounter` by itself.
102
+ *
103
+ * @example
104
+ * b.chainWriter.registerTable("device_event_log");
105
+ * var writer = b.chainWriter.create({
106
+ * table: "device_event_log",
107
+ * chainKey: "deviceId",
108
+ * columnsForInsert: ["_id", "deviceId", "monotonicCounter", "recordedAt",
109
+ * "kind", "payload",
110
+ * "prevHash", "rowHash", "nonce", "fencingToken"],
111
+ * hashableColumns: ["_id", "deviceId", "monotonicCounter", "recordedAt",
112
+ * "kind", "payload"],
113
+ * });
114
+ */
115
+ function registerTable(table) {
116
+ if (typeof table !== "string" || table.length === 0) {
117
+ throw new ChainWriterError(
118
+ "registerTable requires a non-empty table name",
119
+ "chain-writer/invalid-config"
120
+ );
121
+ }
122
+ // Identifier-validate before admitting to the allowlist — the name is
123
+ // interpolated into the chain SQL, so the same shape rules create() relies
124
+ // on must hold here, at the config-time entry point.
125
+ safeSql.validateIdentifier(table);
126
+ ALLOWED_CHAIN_TABLES.add(table);
127
+ return table;
128
+ }
129
+
77
130
  /**
78
131
  * @primitive b.chainWriter.create
79
132
  * @signature b.chainWriter.create(opts)
80
133
  * @since 0.8.48
81
134
  * @status stable
82
- * @related b.audit, b.consent, b.auditChain
135
+ * @related b.audit, b.consent, b.auditChain, b.chainWriter.registerTable
83
136
  *
84
137
  * Build a chain-writer bound to a single hash-chained table. Returns
85
- * `{ table, append, _resetForTest, _getMutexForTest }`. `append(logical)`
86
- * is the public surface — async, leader-gated, mutex-serialized; on
87
- * success it returns the logical row decorated with the computed
88
- * `rowHash` and `prevHash`.
138
+ * `{ table, chainKey, append, _resetForTest, _getMutexForTest }`.
139
+ * `append(logical)` is the public surface — async, leader-gated,
140
+ * mutex-serialized; on success it returns the logical row decorated with the
141
+ * computed `rowHash` and `prevHash`.
142
+ *
143
+ * A `chainKey` makes one table hold many independent chains (one per account /
144
+ * device / tenant): tip-read, counter monotonicity, and the append Mutex all
145
+ * scope per key, so concurrent appends to DIFFERENT keys run in parallel while
146
+ * same-key appends serialize. Bind `chainKey` into `hashableColumns` so the
147
+ * partition is tamper-evident in the row hash, and key the table's uniqueness
148
+ * constraint on `(chainKey, monotonicCounter)`, never `monotonicCounter` alone.
89
149
  *
90
150
  * @opts
91
- * table: string, // one of ALLOWED_CHAIN_TABLES (audit_log | consent_log)
151
+ * table: string, // a registered chain table (audit_log | consent_log | registerTable name)
152
+ * chainKey: string, // optional partition column — one independent chain per key value
92
153
  * columnsForInsert: string[], // INSERT column order (every name is identifier-validated)
93
154
  * hashableColumns: string[], // columns that participate in the rowHash canonicalization
94
- * validateInput: Function, // optional; (logical) throws on invalid shape
155
+ * validateInput: Function, // optional; (logical) -> throws on invalid shape
95
156
  *
96
157
  * @example
97
158
  * var writer = b.chainWriter.create({
@@ -136,27 +197,57 @@ function create(opts) {
136
197
  var hashableColumns = opts.hashableColumns.slice();
137
198
  var validateInput = opts.validateInput || null;
138
199
 
139
- // Per-chain Mutex serializes the read-prev-tip + compute-hash + insert
200
+ // chainKey: the partition column for many independent chains in one table
201
+ // (one chain per account / device / tenant). When set, the tip read,
202
+ // counter priming, and the append Mutex all scope to a single key value, so
203
+ // appends to DIFFERENT keys run in parallel while same-key appends
204
+ // serialize. Identifier-validated at config time (THROW tier) because it is
205
+ // interpolated as a column name; it must also appear in columnsForInsert so
206
+ // every row carries its partition key.
207
+ var chainKey = opts.chainKey || null;
208
+ if (chainKey !== null) {
209
+ safeSql.validateIdentifier(chainKey);
210
+ if (columnsForInsert.indexOf(chainKey) === -1) {
211
+ throw new ChainWriterError(
212
+ "chainKey '" + chainKey + "' must be listed in columnsForInsert so " +
213
+ "every appended row carries its partition key",
214
+ "chain-writer/invalid-config"
215
+ );
216
+ }
217
+ }
218
+
219
+ // Per-CHAIN-KEY Mutex serializes the read-prev-tip + compute-hash + insert
140
220
  // sequence. Without serialization, two concurrent awaiting append() calls
141
- // would hash against the same prev-tip and produce sibling rows with the
142
- // same prevHash forking the chain.
143
- var _chainMutex = new safeAsync.Mutex();
221
+ // would hash against the same prev-tip and fork the chain. A single-chain
222
+ // writer (no chainKey) uses one shared lock under the sentinel key; a
223
+ // multi-chain writer keys the lock by the partition value so appends to
224
+ // DIFFERENT keys run concurrently while same-key appends serialize.
225
+ var _SINGLE_CHAIN_KEY = "__single_chain__"; // sentinel; not a valid driver value
226
+ var _mutexByKey = new Map();
227
+ function _mutexFor(keyValue) {
228
+ var k = chainKey !== null ? String(keyValue) : _SINGLE_CHAIN_KEY;
229
+ return boundedMap.getOrInsert(_mutexByKey, k, function () { return new safeAsync.Mutex(); });
230
+ }
144
231
 
145
- // Lazy counter primer — first append reads MAX(monotonicCounter) and
146
- // increments from there. Once ensures concurrent first-callers share
147
- // one in-flight init Promise.
148
- var _nextCounter = 1;
149
- var _counterInit = null;
232
+ // Lazy counter primer — first append for a given key reads
233
+ // MAX(monotonicCounter) [WHERE chainKey = ?] and increments from there.
234
+ // Per-key so each chain's counter is independent; a per-key Once shares one
235
+ // in-flight init across concurrent first-callers for the same key.
236
+ var _nextCounterByKey = new Map();
237
+ var _counterInitByKey = new Map();
150
238
 
151
- function _ensureCounterInit() {
152
- if (!_counterInit) {
153
- _counterInit = new safeAsync.Once(async function () {
239
+ function _ensureCounterInit(keyValue) {
240
+ var k = chainKey !== null ? String(keyValue) : _SINGLE_CHAIN_KEY;
241
+ var once = boundedMap.getOrInsert(_counterInitByKey, k, function () {
242
+ return new safeAsync.Once(async function () {
154
243
  // BARE logical table name — clusterStorage rewrites the framework
155
- // name to the configured-prefix form and placeholderizes; b.sql
156
- // quotes the camelCase column + emits the MAX aggregate.
157
- var maxBuilt = sql.select(table, _sqlOpts())
158
- .max("monotonicCounter", "m")
159
- .toSql();
244
+ // name to the configured-prefix form (consumer tables pass through
245
+ // unchanged) and placeholderizes; b.sql quotes the camelCase column +
246
+ // emits the MAX aggregate. A keyed writer scopes the MAX to the
247
+ // partition via a bound WHERE.
248
+ var maxQ = sql.select(table, _sqlOpts()).max("monotonicCounter", "m");
249
+ if (chainKey !== null) maxQ = maxQ.where(chainKey, keyValue);
250
+ var maxBuilt = maxQ.toSql();
160
251
  var row = await safeAsync.withTimeout(
161
252
  safeAsync.asyncRetry(function () {
162
253
  return clusterStorage.executeOne(maxBuilt.sql, maxBuilt.params);
@@ -164,18 +255,21 @@ function create(opts) {
164
255
  FRAMEWORK_SQL_TIMEOUT_MS,
165
256
  { name: table + ".readMaxCounter" }
166
257
  );
167
- _nextCounter = (row && row.m ? Number(row.m) : 0) + 1;
258
+ _nextCounterByKey.set(k, (row && row.m ? Number(row.m) : 0) + 1);
168
259
  });
169
- }
170
- return _counterInit.invoke();
260
+ });
261
+ return once.invoke();
171
262
  }
172
263
 
173
- async function _readChainTipRow() {
174
- var tipBuilt = sql.select(table, _sqlOpts())
264
+ async function _readChainTipRow(keyValue) {
265
+ var tipQ = sql.select(table, _sqlOpts())
175
266
  .columns(["rowHash"])
176
267
  .orderBy("monotonicCounter", "desc")
177
- .limit(1)
178
- .toSql();
268
+ .limit(1);
269
+ // Scope the tip to the partition so per-key chains link correctly —
270
+ // bound value, never interpolated.
271
+ if (chainKey !== null) tipQ = tipQ.where(chainKey, keyValue);
272
+ var tipBuilt = tipQ.toSql();
179
273
  return await safeAsync.withTimeout(
180
274
  safeAsync.asyncRetry(function () {
181
275
  return clusterStorage.executeOne(tipBuilt.sql, tipBuilt.params);
@@ -209,15 +303,33 @@ function create(opts) {
209
303
  async function append(logical) {
210
304
  if (validateInput) validateInput(logical);
211
305
  cluster.requireLeader();
212
- await _ensureCounterInit();
213
306
 
214
- return await _chainMutex.runExclusive(async function () {
215
- return await _appendInsideMutex(logical);
307
+ // Resolve the partition key from the logical row for a multi-chain writer.
308
+ // Fail closed: a keyed writer with a missing / empty key can't pick a
309
+ // chain to append to, so refuse rather than silently fold the row into the
310
+ // wrong chain.
311
+ var keyValue = _SINGLE_CHAIN_KEY;
312
+ if (chainKey !== null) {
313
+ keyValue = logical ? logical[chainKey] : undefined;
314
+ if (keyValue === undefined || keyValue === null || String(keyValue).length === 0) {
315
+ throw new ChainWriterError(
316
+ "append: a chainKey writer requires logical['" + chainKey + "'] to be a " +
317
+ "non-empty partition value",
318
+ "chain-writer/invalid-input"
319
+ );
320
+ }
321
+ }
322
+ await _ensureCounterInit(keyValue);
323
+
324
+ return await _mutexFor(keyValue).runExclusive(async function () {
325
+ return await _appendInsideMutex(logical, keyValue);
216
326
  });
217
327
  }
218
328
 
219
- async function _appendInsideMutex(logical) {
220
- var counter = _nextCounter++;
329
+ async function _appendInsideMutex(logical, keyValue) {
330
+ var _ck = chainKey !== null ? String(keyValue) : _SINGLE_CHAIN_KEY;
331
+ var counter = _nextCounterByKey.get(_ck);
332
+ _nextCounterByKey.set(_ck, counter + 1);
221
333
  var nowMs = Date.now();
222
334
  var nonce = generateBytes(C.BYTES.bytes(16));
223
335
 
@@ -239,8 +351,8 @@ function create(opts) {
239
351
  if (!(hashableColumns[hci] in sealed)) sealed[hashableColumns[hci]] = null;
240
352
  }
241
353
 
242
- // Compute rowHash over the sealed content fields
243
- var tipRow = await _readChainTipRow();
354
+ // Compute rowHash over the sealed content fields, linking to THIS key's tip.
355
+ var tipRow = await _readChainTipRow(keyValue);
244
356
  var prevHash = tipRow ? tipRow.rowHash : auditChain.ZERO_HASH;
245
357
  var rowHash = auditChain.computeRowHash(prevHash, sealed, nonce);
246
358
 
@@ -259,22 +371,25 @@ function create(opts) {
259
371
  }
260
372
 
261
373
  function _resetForTest() {
262
- _chainMutex = new safeAsync.Mutex();
263
- _counterInit = null;
264
- _nextCounter = 1;
374
+ _mutexByKey = new Map();
375
+ _counterInitByKey = new Map();
376
+ _nextCounterByKey = new Map();
265
377
  }
266
378
 
267
379
  return {
268
380
  table: table,
381
+ chainKey: chainKey,
269
382
  append: append,
270
383
  _resetForTest: _resetForTest,
271
- // Expose for diagnostic introspection
272
- _getMutexForTest: function () { return _chainMutex; },
384
+ // Expose for diagnostic introspection — the lock for a given key (or the
385
+ // single-chain lock when no chainKey is configured).
386
+ _getMutexForTest: function (keyValue) { return _mutexFor(keyValue); },
273
387
  };
274
388
  }
275
389
 
276
390
  module.exports = {
277
391
  create: create,
392
+ registerTable: registerTable,
278
393
  ChainWriterError: ChainWriterError,
279
394
  ALLOWED_CHAIN_TABLES: ALLOWED_CHAIN_TABLES,
280
395
  FRAMEWORK_SQL_TIMEOUT_MS: FRAMEWORK_SQL_TIMEOUT_MS,
package/lib/cli.js CHANGED
@@ -39,6 +39,7 @@ var os = require("node:os");
39
39
  var nodePath = require("node:path");
40
40
  var apiSnapshot = require("./api-snapshot");
41
41
  var argParser = require("./arg-parser");
42
+ var atomicFile = require("./atomic-file");
42
43
  var auditChain = require("./audit-chain");
43
44
  var auditTools = require("./audit-tools");
44
45
  var backup = require("./backup");
@@ -1110,12 +1111,16 @@ async function _runApiKey(args, ctx) {
1110
1111
  return report.error("--scopes must contain at least one non-empty scope", 2);
1111
1112
  }
1112
1113
  var label = typeof args.flags.label === "string" ? args.flags.label : null;
1114
+ // --expires-ms is the absolute expiry as a unix-ms timestamp; the
1115
+ // registry's validated opt is `expiresAt` (apiKey.issue rejects any other
1116
+ // key). Passing `expiresMs` silently did nothing before — the lib only
1117
+ // ever read `expiresAt` — so the flag was a no-op; map it correctly.
1113
1118
  var expiresMs = args.flags["expires-ms"];
1114
1119
  var issued = await registry.issue({
1115
1120
  ownerId: String(ownerId),
1116
1121
  scopes: scopeList,
1117
1122
  metadata: label ? { label: label } : null,
1118
- expiresMs: expiresMs && expiresMs !== true ? Number(expiresMs) : undefined,
1123
+ expiresAt: expiresMs && expiresMs !== true ? Number(expiresMs) : undefined,
1119
1124
  });
1120
1125
  report.write("id: " + issued.id);
1121
1126
  report.write("key: " + issued.key);
@@ -1480,7 +1485,10 @@ async function _runMtls(args, ctx) {
1480
1485
  validityDays: daysP,
1481
1486
  });
1482
1487
  if (outPath) {
1483
- nodeFs.writeFileSync(outPath, p12.p12, { mode: 0o600 });
1488
+ // Atomic, symlink-refusing write a bare writeFileSync follows a
1489
+ // symlink an attacker pre-planted at the operator-supplied --out
1490
+ // path (CWE-59) and could expose the client key bundle through it.
1491
+ atomicFile.writeSync(outPath, p12.p12, { fileMode: 0o600 });
1484
1492
  report.write("p12 written: " + outPath);
1485
1493
  } else {
1486
1494
  // No --out: stream the bytes to stdout for piping. Operators
@@ -158,15 +158,13 @@ function _parseBrandMember(piece) {
158
158
  var brand = _parseSfString(params[0].trim());
159
159
  if (brand === null) return null;
160
160
  var member = { brand: brand, version: null, params: {} };
161
- for (var i = 1; i < params.length; i += 1) {
162
- var kv = params[i].trim();
163
- if (kv.length === 0) continue;
164
- var eq = kv.indexOf("=");
165
- if (eq === -1) { member.params[kv.toLowerCase()] = true; continue; }
166
- var k = kv.slice(0, eq).trim().toLowerCase();
167
- var v = _parseSfString(kv.slice(eq + 1));
168
- member.params[k] = v;
169
- if (k === "v" && v !== null) member.version = v;
161
+ var kvps = structuredFields.parseKeyValuePieces(params, 1);
162
+ for (var i = 0; i < kvps.length; i += 1) {
163
+ var kv = kvps[i];
164
+ if (kv.value === null) { member.params[kv.key] = true; continue; }
165
+ var v = _parseSfString(kv.value);
166
+ member.params[kv.key] = v;
167
+ if (kv.key === "v" && v !== null) member.version = v;
170
168
  }
171
169
  return member;
172
170
  }
@@ -35,6 +35,7 @@
35
35
  */
36
36
 
37
37
  var nodeCrypto = require("node:crypto");
38
+ var numericBounds = require("./numeric-bounds");
38
39
  var validateOpts = require("./validate-opts");
39
40
  var rfc3339 = require("./rfc3339");
40
41
  var safeJson = require("./safe-json");
@@ -118,21 +119,41 @@ function _genId() {
118
119
  * // → "1.0"
119
120
  */
120
121
  function wrap(opts) {
121
- validateOpts.requireObject(opts, "cloudEvents.wrap", CloudEventsError);
122
- validateOpts.requireNonEmptyString(opts.source,
123
- "cloudEvents.wrap: source", CloudEventsError, "cloud-events/bad-source");
124
- validateOpts.requireNonEmptyString(opts.type,
125
- "cloudEvents.wrap: type", CloudEventsError, "cloud-events/bad-type");
126
- validateOpts.optionalNonEmptyString(opts.id,
127
- "cloudEvents.wrap: id", CloudEventsError, "cloud-events/bad-id");
128
- validateOpts.optionalNonEmptyString(opts.subject,
129
- "cloudEvents.wrap: subject", CloudEventsError, "cloud-events/bad-subject");
130
- validateOpts.optionalNonEmptyString(opts.time,
131
- "cloudEvents.wrap: time", CloudEventsError, "cloud-events/bad-time");
132
- validateOpts.optionalNonEmptyString(opts.datacontenttype,
133
- "cloudEvents.wrap: datacontenttype", CloudEventsError, "cloud-events/bad-datacontenttype");
134
- validateOpts.optionalNonEmptyString(opts.dataschema,
135
- "cloudEvents.wrap: dataschema", CloudEventsError, "cloud-events/bad-dataschema");
122
+ validateOpts.shape(opts, {
123
+ source: { rule: "required-string", code: "cloud-events/bad-source" },
124
+ type: { rule: "required-string", code: "cloud-events/bad-type" },
125
+ id: { rule: "optional-string", code: "cloud-events/bad-id" },
126
+ subject: { rule: "optional-string", code: "cloud-events/bad-subject" },
127
+ time: { rule: "optional-string", code: "cloud-events/bad-time" },
128
+ datacontenttype: { rule: "optional-string", code: "cloud-events/bad-datacontenttype" },
129
+ dataschema: { rule: "optional-string", code: "cloud-events/bad-dataschema" },
130
+ // data is any JSON value or a Buffer — wrap routes Buffers to
131
+ // data_base64 and everything else to the data attribute, imposing no
132
+ // type constraint of its own, so accept any value here.
133
+ data: function () {},
134
+ // extensions plain object whose keys conform to the §3.1 naming
135
+ // rules (lowercase ASCII alnum, 1-20 chars) and collide with no spec
136
+ // attribute. Validation lives IN the shape; the body only copies the
137
+ // validated keys onto the envelope.
138
+ extensions: function (v) {
139
+ validateOpts.optionalPlainObject(v,
140
+ "cloudEvents.wrap: extensions", CloudEventsError, "cloud-events/bad-extensions");
141
+ if (v === undefined || v === null) return;
142
+ var extKeys = Object.keys(v);
143
+ for (var i = 0; i < extKeys.length; i += 1) {
144
+ var k = extKeys[i];
145
+ // bound BEFORE regex test — k.length > 0 && k.length <= 20
146
+ if (typeof k !== "string" || k.length === 0 || k.length > 20 || !EXT_ATTR_NAME_RE.test(k)) {
147
+ throw new CloudEventsError("cloud-events/bad-extension-name",
148
+ "cloudEvents.wrap: extension '" + k + "' must match [a-z0-9]{1,20}");
149
+ }
150
+ if (REQUIRED_ATTRS.indexOf(k) !== -1 || KNOWN_OPTIONAL_ATTRS[k]) {
151
+ throw new CloudEventsError("cloud-events/extension-conflicts-with-spec",
152
+ "cloudEvents.wrap: extension '" + k + "' conflicts with a spec attribute");
153
+ }
154
+ }
155
+ },
156
+ }, "cloudEvents.wrap", CloudEventsError);
136
157
 
137
158
  var out = {
138
159
  specversion: SPECVERSION,
@@ -158,24 +179,13 @@ function wrap(opts) {
158
179
  out.datacontenttype = opts.datacontenttype;
159
180
  }
160
181
 
161
- // Extension attributes — operator-defined, must conform to the
162
- // §3.1 naming rules (lowercase ASCII alnum, 1-20 chars).
182
+ // Extension attributes — validated in the shape above (plain object,
183
+ // §3.1 key naming, no spec collisions); copy the validated keys onto
184
+ // the envelope.
163
185
  if (opts.extensions !== undefined && opts.extensions !== null) {
164
- validateOpts.optionalPlainObject(opts.extensions,
165
- "cloudEvents.wrap: extensions", CloudEventsError, "cloud-events/bad-extensions");
166
186
  var extKeys = Object.keys(opts.extensions);
167
187
  for (var i = 0; i < extKeys.length; i += 1) {
168
- var k = extKeys[i];
169
- // bound BEFORE regex test — k.length > 0 && k.length <= 20
170
- if (typeof k !== "string" || k.length === 0 || k.length > 20 || !EXT_ATTR_NAME_RE.test(k)) {
171
- throw new CloudEventsError("cloud-events/bad-extension-name",
172
- "cloudEvents.wrap: extension '" + k + "' must match [a-z0-9]{1,20}");
173
- }
174
- if (REQUIRED_ATTRS.indexOf(k) !== -1 || KNOWN_OPTIONAL_ATTRS[k]) {
175
- throw new CloudEventsError("cloud-events/extension-conflicts-with-spec",
176
- "cloudEvents.wrap: extension '" + k + "' conflicts with a spec attribute");
177
- }
178
- out[k] = opts.extensions[k];
188
+ out[extKeys[i]] = opts.extensions[extKeys[i]];
179
189
  }
180
190
  }
181
191
  return out;
@@ -256,7 +266,7 @@ function parse(envelope) {
256
266
  for (var j = 0; j < keys.length; j += 1) {
257
267
  var key = keys[j];
258
268
  if (REQUIRED_ATTRS.indexOf(key) !== -1) continue;
259
- if (KNOWN_OPTIONAL_ATTRS[key]) continue;
269
+ if (Object.prototype.hasOwnProperty.call(KNOWN_OPTIONAL_ATTRS, key)) continue;
260
270
  extensions[key] = envelope[key];
261
271
  }
262
272
 
@@ -300,7 +310,7 @@ function _extIssue(name, v) {
300
310
  if (v === null) return null;
301
311
  if (typeof v === "string" || typeof v === "boolean") return null;
302
312
  if (typeof v === "number") {
303
- if (!isFinite(v) || Math.floor(v) !== v) return "extension '" + name + "' must be an integer (CloudEvents has no float type)";
313
+ if (!numericBounds.isFiniteInt(v)) return "extension '" + name + "' must be an integer (CloudEvents has no float type)";
304
314
  if (v < INT_MIN || v > INT_MAX) return "extension '" + name + "' integer out of 32-bit range";
305
315
  return null;
306
316
  }
@@ -668,7 +678,7 @@ function decodeBinary(headers, body, opts) {
668
678
  else if (Buffer.isBuffer(body)) raw = body;
669
679
  else if (typeof body === "string") raw = Buffer.from(body, "utf8");
670
680
  else throw new CloudEventsError("cloud-events/bad-input", "cloudEvents.http.decodeBinary: body must be a string or Buffer");
671
- if (raw.length > maxBytes) throw new CloudEventsError("cloud-events/too-large", "cloudEvents.http.decodeBinary: body exceeds maxBytes (" + maxBytes + ")");
681
+ if (safeBuffer.byteLengthOf(raw) > maxBytes) throw new CloudEventsError("cloud-events/too-large", "cloudEvents.http.decodeBinary: body exceeds maxBytes (" + maxBytes + ")");
672
682
  if (raw.length > 0) {
673
683
  if (_isJsonMedia(ct)) {
674
684
  try { event.data = safeJson.parse(raw, { maxBytes: maxBytes }); }
@@ -30,6 +30,7 @@
30
30
  var cluster = require("./cluster");
31
31
  var frameworkSchema = require("./framework-schema");
32
32
  var externalDb = require("./external-db");
33
+ var safeSql = require("./safe-sql");
33
34
  var lazyRequire = require("./lazy-require");
34
35
  var { FrameworkError } = require("./framework-error");
35
36
 
@@ -277,44 +278,7 @@ function resolveTables(sql) {
277
278
  * // → "SELECT id FROM audit_log WHERE counter > $1 AND actor = $2"
278
279
  */
279
280
  function placeholderize(sql, dialect) {
280
- if (dialect !== "postgres") return sql;
281
- var out = "";
282
- var n = 0;
283
- var i = 0;
284
- var len = sql.length;
285
- while (i < len) {
286
- var c = sql.charAt(i);
287
- var nx = i + 1 < len ? sql.charAt(i + 1) : "";
288
- // Quote contexts: ' string literal, " identifier, ` mysql identifier.
289
- // A doubled quote escapes itself within the span.
290
- if (c === "'" || c === '"' || c === "`") {
291
- out += c;
292
- i += 1;
293
- while (i < len) {
294
- var q = sql.charAt(i);
295
- if (q === c) {
296
- if (sql.charAt(i + 1) === c) { out += c + c; i += 2; continue; }
297
- out += c; i += 1; break;
298
- }
299
- out += q; i += 1;
300
- }
301
- continue;
302
- }
303
- if (c === "-" && nx === "-") { // line comment
304
- while (i < len && sql.charAt(i) !== "\n") { out += sql.charAt(i); i += 1; }
305
- continue;
306
- }
307
- if (c === "/" && nx === "*") { // block comment
308
- out += "/*"; i += 2;
309
- while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) { out += sql.charAt(i); i += 1; }
310
- if (i < len) { out += "*/"; i += 2; }
311
- continue;
312
- }
313
- if (c === "?") { n += 1; out += "$" + n; i += 1; continue; }
314
- out += c;
315
- i += 1;
316
- }
317
- return out;
281
+ return safeSql.toPositional(sql, dialect);
318
282
  }
319
283
 
320
284
  // ---- execute() ----
@@ -363,9 +327,13 @@ var _activeTx = null;
363
327
  // synchronous this runs atomically to completion with no interleaving.
364
328
  function _localExec(sql, params) {
365
329
  var stmt = _localDb().prepare(sql);
366
- // Heuristic: if the statement returns rows (SELECT or has RETURNING),
367
- // use .all(); otherwise .run() and report changes as rowCount.
368
- if (/^\s*SELECT\b/i.test(sql) || /\bRETURNING\b/i.test(sql)) {
330
+ // Choose .all() vs .run() by whether the statement returns rows. A leading-
331
+ // keyword check (`^\s*SELECT`) mis-routes a WITH (CTE) read "WITH c AS (...)
332
+ // SELECT ..." does not start with SELECT — to .run(), which on node:sqlite
333
+ // silently returns only a changes count and DROPS the rows. The shared
334
+ // CTE/EXPLAIN-aware classifier resolves the effective verb (and a top-level
335
+ // RETURNING) so every row-returning statement reaches the caller via .all().
336
+ if (externalDb._statementReturnsRows(sql)) {
369
337
  var rows = stmt.all.apply(stmt, params || []);
370
338
  return { rows: rows, rowCount: rows.length };
371
339
  }
package/lib/cms-codec.js CHANGED
@@ -68,6 +68,7 @@
68
68
  */
69
69
 
70
70
  var nodeCrypto = require("node:crypto");
71
+ var safeBuffer = require("./safe-buffer");
71
72
  var asn1 = require("./asn1-der");
72
73
  var bCrypto = require("./crypto");
73
74
  var pqcSoftware = require("./pqc-software");
@@ -322,7 +323,7 @@ function decode(buf, opts) {
322
323
  throw new CmsCodecError("cms/bad-input", "decode: buf must be a Buffer");
323
324
  }
324
325
  var maxBytes = opts.maxBytes || DEFAULT_MAX_LEN;
325
- if (buf.length > maxBytes) {
326
+ if (safeBuffer.byteLengthOf(buf) > maxBytes) {
326
327
  throw new CmsCodecError("cms/oversize",
327
328
  "decode: input " + buf.length + " bytes exceeds maxBytes=" + maxBytes);
328
329
  }