@blamejs/blamejs-shop 0.4.31 → 0.4.33

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 (343) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/vendor/MANIFEST.json +400 -282
  5. package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
  6. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
  7. package/lib/vendor/blamejs/.gitignore +6 -0
  8. package/lib/vendor/blamejs/CHANGELOG.md +28 -0
  9. package/lib/vendor/blamejs/MIGRATING.md +55 -0
  10. package/lib/vendor/blamejs/README.md +8 -6
  11. package/lib/vendor/blamejs/SECURITY.md +19 -3
  12. package/lib/vendor/blamejs/api-snapshot.json +2190 -664
  13. package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
  14. package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
  15. package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
  16. package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
  17. package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
  18. package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
  19. package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
  20. package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
  21. package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
  22. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
  23. package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
  24. package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
  25. package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
  26. package/lib/vendor/blamejs/index.js +4 -0
  27. package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
  28. package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
  29. package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
  30. package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
  31. package/lib/vendor/blamejs/lib/api-key.js +158 -77
  32. package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
  33. package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
  34. package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
  35. package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
  36. package/lib/vendor/blamejs/lib/audit.js +259 -123
  37. package/lib/vendor/blamejs/lib/auth/elevation-grant.js +6 -2
  38. package/lib/vendor/blamejs/lib/auth/oauth.js +66 -9
  39. package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
  40. package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
  41. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +36 -7
  42. package/lib/vendor/blamejs/lib/backup/index.js +45 -10
  43. package/lib/vendor/blamejs/lib/break-glass.js +355 -147
  44. package/lib/vendor/blamejs/lib/cache.js +174 -105
  45. package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
  46. package/lib/vendor/blamejs/lib/cli.js +19 -14
  47. package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
  48. package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
  49. package/lib/vendor/blamejs/lib/cluster.js +119 -71
  50. package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
  51. package/lib/vendor/blamejs/lib/compliance.js +210 -4
  52. package/lib/vendor/blamejs/lib/consent.js +82 -29
  53. package/lib/vendor/blamejs/lib/constants.js +27 -11
  54. package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
  55. package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
  56. package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
  57. package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
  58. package/lib/vendor/blamejs/lib/db-query.js +882 -260
  59. package/lib/vendor/blamejs/lib/db-schema.js +228 -44
  60. package/lib/vendor/blamejs/lib/db.js +249 -99
  61. package/lib/vendor/blamejs/lib/dsr.js +385 -55
  62. package/lib/vendor/blamejs/lib/error-page.js +14 -1
  63. package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
  64. package/lib/vendor/blamejs/lib/external-db.js +549 -34
  65. package/lib/vendor/blamejs/lib/file-upload.js +52 -7
  66. package/lib/vendor/blamejs/lib/framework-error.js +20 -1
  67. package/lib/vendor/blamejs/lib/framework-files.js +73 -0
  68. package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
  69. package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
  70. package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
  71. package/lib/vendor/blamejs/lib/guard-all.js +1 -0
  72. package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
  73. package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
  74. package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
  75. package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
  76. package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
  77. package/lib/vendor/blamejs/lib/guard-email.js +47 -69
  78. package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
  79. package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
  80. package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
  81. package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
  82. package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
  83. package/lib/vendor/blamejs/lib/guard-html.js +53 -108
  84. package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
  85. package/lib/vendor/blamejs/lib/guard-image.js +46 -103
  86. package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
  87. package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
  88. package/lib/vendor/blamejs/lib/guard-json.js +38 -108
  89. package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
  90. package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
  91. package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
  92. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
  93. package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
  94. package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
  95. package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
  96. package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
  97. package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
  98. package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
  99. package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
  100. package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
  101. package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
  102. package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
  103. package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
  104. package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
  105. package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
  106. package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
  107. package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
  108. package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
  109. package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
  110. package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
  111. package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
  112. package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
  113. package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
  114. package/lib/vendor/blamejs/lib/guard-template.js +35 -172
  115. package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
  116. package/lib/vendor/blamejs/lib/guard-time.js +32 -154
  117. package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
  118. package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
  119. package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
  120. package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
  121. package/lib/vendor/blamejs/lib/http-client.js +37 -9
  122. package/lib/vendor/blamejs/lib/inbox.js +120 -107
  123. package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
  124. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
  125. package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
  126. package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
  127. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
  128. package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
  129. package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
  130. package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
  131. package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
  132. package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
  133. package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
  134. package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
  135. package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
  136. package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
  137. package/lib/vendor/blamejs/lib/mail-store.js +293 -154
  138. package/lib/vendor/blamejs/lib/mail.js +8 -4
  139. package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
  140. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
  141. package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
  142. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
  143. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
  144. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
  145. package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
  146. package/lib/vendor/blamejs/lib/migrations.js +108 -66
  147. package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
  148. package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
  149. package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
  150. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
  151. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
  152. package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
  153. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
  154. package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
  155. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
  156. package/lib/vendor/blamejs/lib/observability.js +124 -0
  157. package/lib/vendor/blamejs/lib/otel-export.js +12 -3
  158. package/lib/vendor/blamejs/lib/outbox.js +184 -83
  159. package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
  160. package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
  161. package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
  162. package/lib/vendor/blamejs/lib/queue-local.js +225 -140
  163. package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
  164. package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
  165. package/lib/vendor/blamejs/lib/queue.js +7 -0
  166. package/lib/vendor/blamejs/lib/redact.js +68 -11
  167. package/lib/vendor/blamejs/lib/redis-client.js +160 -31
  168. package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
  169. package/lib/vendor/blamejs/lib/retention.js +117 -42
  170. package/lib/vendor/blamejs/lib/router.js +212 -5
  171. package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
  172. package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
  173. package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
  174. package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
  175. package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
  176. package/lib/vendor/blamejs/lib/safe-url.js +170 -3
  177. package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
  178. package/lib/vendor/blamejs/lib/scheduler.js +47 -12
  179. package/lib/vendor/blamejs/lib/seeders.js +122 -74
  180. package/lib/vendor/blamejs/lib/session-stores.js +42 -14
  181. package/lib/vendor/blamejs/lib/session.js +175 -77
  182. package/lib/vendor/blamejs/lib/sql.js +3842 -0
  183. package/lib/vendor/blamejs/lib/sse.js +26 -0
  184. package/lib/vendor/blamejs/lib/ssrf-guard.js +169 -4
  185. package/lib/vendor/blamejs/lib/static.js +177 -34
  186. package/lib/vendor/blamejs/lib/subject.js +96 -49
  187. package/lib/vendor/blamejs/lib/vault/index.js +3 -2
  188. package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
  189. package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
  190. package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
  191. package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
  192. package/lib/vendor/blamejs/lib/websocket.js +35 -5
  193. package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
  194. package/lib/vendor/blamejs/package.json +2 -2
  195. package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
  196. package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
  197. package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
  198. package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
  199. package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
  200. package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
  201. package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
  202. package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
  203. package/lib/vendor/blamejs/release-notes/v0.15.7.json +43 -0
  204. package/lib/vendor/blamejs/scripts/check-services.js +21 -0
  205. package/lib/vendor/blamejs/scripts/gen-migrating.js +67 -0
  206. package/lib/vendor/blamejs/scripts/release.js +398 -38
  207. package/lib/vendor/blamejs/test/00-primitives.js +168 -0
  208. package/lib/vendor/blamejs/test/10-state.js +140 -14
  209. package/lib/vendor/blamejs/test/20-db.js +65 -2
  210. package/lib/vendor/blamejs/test/helpers/db.js +9 -0
  211. package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
  212. package/lib/vendor/blamejs/test/helpers/services.js +21 -0
  213. package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
  214. package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
  215. package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
  216. package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
  217. package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
  218. package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
  219. package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
  220. package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
  221. package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
  222. package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
  223. package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
  224. package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
  225. package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
  226. package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
  227. package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
  228. package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
  229. package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
  230. package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
  231. package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
  232. package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
  233. package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
  234. package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
  235. package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
  236. package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
  237. package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
  238. package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
  239. package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
  240. package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
  241. package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
  242. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
  243. package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
  244. package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
  245. package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
  246. package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
  247. package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
  248. package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
  249. package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
  250. package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
  251. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
  252. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1196 -14
  253. package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
  254. package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -0
  255. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
  256. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
  257. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
  258. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
  259. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
  260. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
  261. package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
  262. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
  263. package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
  264. package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
  265. package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
  266. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
  267. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
  268. package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
  269. package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
  270. package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
  271. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
  272. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
  273. package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
  274. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
  275. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
  276. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
  277. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
  278. package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
  279. package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
  280. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
  281. package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
  282. package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
  283. package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
  284. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
  285. package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
  286. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
  287. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
  288. package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
  289. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
  290. package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
  291. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
  292. package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
  293. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
  294. package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
  295. package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
  296. package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
  297. package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
  298. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
  299. package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
  300. package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +59 -0
  301. package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
  302. package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +362 -0
  303. package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
  304. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
  305. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +71 -0
  306. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
  307. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
  308. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
  309. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
  310. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
  311. package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
  312. package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
  313. package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
  314. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
  315. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
  316. package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
  317. package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
  318. package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
  319. package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
  320. package/lib/vendor/blamejs/test/smoke.js +79 -21
  321. package/package.json +2 -2
  322. package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
  323. package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
  324. package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
  325. package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
  326. package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
  327. package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
  328. package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
  329. package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
  330. package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
  331. package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
  332. package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
  333. package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
  334. package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
  335. package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
  336. package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
  337. package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
  338. package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
  339. package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
  340. package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
  341. package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
  342. package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
  343. package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
@@ -17,6 +17,20 @@
17
17
  * updateOne(changes), updateMany(changes),
18
18
  * deleteOne(), deleteMany().
19
19
  *
20
+ * SQL construction composes b.sql (lib/sql.js): every terminal builds a
21
+ * b.sql verb builder ({ dialect: "sqlite" }, the local node:sqlite
22
+ * backend), replays the recorded structured WHERE conditions onto it, and
23
+ * calls .toSql() for the { sql, params } pair — which db-query then
24
+ * prepares + runs on the local sqlite handle. b.sql owns identifier
25
+ * quoting (through b.safeSql), value binding (every value a `?`
26
+ * placeholder), IN-list expansion, LIKE auto-escape, and the output
27
+ * validator (_assertEmittable). db-query keeps everything b.sql cannot
28
+ * know about: the residency write-gate, sealed-row seal/unseal, _id
29
+ * auto-generation, per-row-key materialization, the column-membership
30
+ * gate, sealed-field → derived-hash translation, and the JSONB/JSON-path
31
+ * value guard — all applied at condition-record / row-build time, before
32
+ * the structured shape reaches b.sql.
33
+ *
20
34
  * Sealed-field semantics:
21
35
  * - On insert/update, sealed columns are vault.seal()'d and their derived
22
36
  * hashes computed automatically.
@@ -33,7 +47,124 @@ var { generateToken } = require("./crypto");
33
47
  var safeJson = require("./safe-json");
34
48
  var safeJsonPath = require("./safe-jsonpath");
35
49
  var safeSql = require("./safe-sql");
50
+ var sql = require("./sql");
36
51
  var audit = require("./audit");
52
+ var lazyRequire = require("./lazy-require");
53
+ var { DbQueryError } = require("./framework-error");
54
+
55
+ // Circular load — db.js requires db-query at module scope, so the
56
+ // residency gate reaches back for getDataResidency() lazily.
57
+ var db = lazyRequire(function () { return require("./db"); });
58
+
59
+ // Cross-border regulated postures live on b.compliance
60
+ // (CROSS_BORDER_REGULATED_POSTURES — one vocabulary shared with
61
+ // external-db's gate): under these, a residency mismatch REFUSES the
62
+ // write; under anything else the gates emit an advisory audit and
63
+ // pass (backward-compatible).
64
+ function _postureState() {
65
+ try {
66
+ var compliance = require("./compliance"); // allow:inline-require — defensive against optional load
67
+ var posture = compliance.current();
68
+ return { posture: posture, regulated: compliance.isCrossBorderRegulated(posture) };
69
+ } catch (_e) { return { posture: null, regulated: false }; }
70
+ }
71
+
72
+ // Local-SQLite write-residency gate (GDPR Art 44-46 / PIPL Art 38 /
73
+ // DPDP §16 cross-border transfer restrictions). Runs on the PLAINTEXT
74
+ // row before sealRow so the tag column is readable even when other
75
+ // columns seal. Two layers:
76
+ //
77
+ // 1. Per-ROW tag (declarePerRowResidency): on INSERT the declared
78
+ // column must be present and within allowedTags; under a
79
+ // regulated posture a tag outside the deployment's region set
80
+ // ({ region } + allowedStorageRegions from db.init's
81
+ // dataResidency) refuses the write. UPDATEs gate only when the
82
+ // change set touches the residency column (an update that does
83
+ // not move residency is not a transfer).
84
+ // 2. Per-COLUMN tags (declareColumnResidency): the long-advertised
85
+ // assertColumnResidency gate, enforced here against the
86
+ // deployment region. Operators tag columns with the region
87
+ // value their dataResidency declares.
88
+ //
89
+ // Unregulated postures audit (drop-silent) and pass; tables with no
90
+ // declaration are untouched.
91
+ function _assertLocalResidency(table, plaintextRow, op) {
92
+ var spec = cryptoField.getPerRowResidency(table);
93
+ var colMap = cryptoField.getColumnResidency(table);
94
+ if (!spec && !colMap) return;
95
+
96
+ var residency = null;
97
+ try { residency = db().getDataResidency(); } catch (_e) { residency = null; }
98
+ var region = residency && residency.region ? residency.region : null;
99
+ var allowedRegions = region
100
+ ? [region].concat(Array.isArray(residency.allowedStorageRegions)
101
+ ? residency.allowedStorageRegions : [])
102
+ : null;
103
+ var state = _postureState();
104
+ var posture = state.posture;
105
+ var regulated = state.regulated;
106
+
107
+ if (spec) {
108
+ var tag = plaintextRow[spec.residencyColumn];
109
+ var tagPresent = tag !== undefined && tag !== null;
110
+ var colInChangeSet = Object.prototype.hasOwnProperty.call(plaintextRow, spec.residencyColumn);
111
+ if (op === "insert" && !tagPresent) {
112
+ throw new DbQueryError("db-query/row-residency-tag-missing",
113
+ op + ": table '" + table + "' declares per-row residency on column '" +
114
+ spec.residencyColumn + "' — every inserted row must carry a tag from [" +
115
+ spec.allowedTags.join(", ") + "]", true);
116
+ }
117
+ // An UPDATE that explicitly sets the residency column to null /
118
+ // undefined would clear the row's region binding (INSERT refuses a
119
+ // missing tag; the same row must not be nullable into an untagged
120
+ // state on update). UPDATEs that don't touch the column pass.
121
+ if (op === "update" && colInChangeSet && !tagPresent) {
122
+ throw new DbQueryError("db-query/row-residency-tag-missing",
123
+ op + ": table '" + table + "' residency column '" + spec.residencyColumn +
124
+ "' cannot be cleared — set a tag from [" + spec.allowedTags.join(", ") + "]", true);
125
+ }
126
+ if (tagPresent) {
127
+ if (typeof tag !== "string" || spec.allowedTags.indexOf(tag) === -1) {
128
+ throw new DbQueryError("db-query/row-residency-tag-invalid",
129
+ op + ": table '" + table + "' residency tag '" + tag +
130
+ "' is not in allowedTags [" + spec.allowedTags.join(", ") + "]", true);
131
+ }
132
+ if (tag !== "global" && tag !== "unrestricted" && allowedRegions &&
133
+ allowedRegions.indexOf(tag) === -1) {
134
+ if (regulated) {
135
+ audit.safeEmit({ action: "db.residency.gate.rejected", outcome: "denied",
136
+ metadata: { table: table, rowTag: tag, region: region, posture: posture,
137
+ operation: op, scope: "local" } });
138
+ throw new DbQueryError("db-query/row-residency-local-mismatch",
139
+ op + ": row residency tag '" + tag + "' is outside this deployment's " +
140
+ "region set [" + allowedRegions.join(", ") + "] under '" + posture +
141
+ "' posture (cross-border transfer refused)", true);
142
+ }
143
+ audit.safeEmit({ action: "db.residency.gate.advisory", outcome: "info",
144
+ metadata: { table: table, rowTag: tag, region: region, posture: posture || null,
145
+ operation: op, scope: "local" } });
146
+ }
147
+ }
148
+ }
149
+
150
+ if (colMap && region) {
151
+ var refusal = cryptoField.assertColumnResidency(table, plaintextRow, { backendTag: region });
152
+ if (refusal) {
153
+ if (regulated) {
154
+ audit.safeEmit({ action: "db.column_residency.gate.rejected", outcome: "denied",
155
+ metadata: { table: refusal.table, column: refusal.column, want: refusal.want,
156
+ got: refusal.got, posture: posture, operation: op, scope: "local" } });
157
+ throw new DbQueryError("db-query/column-residency-mismatch",
158
+ op + ": column '" + refusal.column + "' on table '" + refusal.table +
159
+ "' is bound to residency '" + refusal.want + "' but this deployment's " +
160
+ "region is '" + refusal.got + "' under '" + posture + "' posture", true);
161
+ }
162
+ audit.safeEmit({ action: "db.residency.gate.advisory", outcome: "info",
163
+ metadata: { table: refusal.table, column: refusal.column, want: refusal.want,
164
+ got: refusal.got, posture: posture || null, operation: op, scope: "local" } });
165
+ }
166
+ }
167
+ }
37
168
 
38
169
  // "@>" / "?" / "?|" / "?&" are JSONB containment + key-existence
39
170
  // operators. Routed through safeJsonPath validation before binding so
@@ -79,8 +210,15 @@ class Query {
79
210
  this._schema = schema;
80
211
  this._table = table;
81
212
  this._qualifiedKey = schema ? schema + "." + table : table;
82
- this._where = [];
83
- this._whereParams = [];
213
+ // Recorded WHERE chain — an ordered list of leaves. Each leaf is
214
+ // { joiner, apply(predicate) } where apply() replays the leaf onto a
215
+ // b.sql Predicate (or builder) using its where-family methods. The
216
+ // sealed-field translation, JSONB value guard, and column-membership
217
+ // gate run at record time (in _addCondition / whereRaw / search /
218
+ // whereGroup / orWhere), so the recorded shape is already safe; the
219
+ // terminal just replays it through b.sql, which owns quoting +
220
+ // binding + the output validator.
221
+ this._conditions = [];
84
222
  this._select = null;
85
223
  this._orderBy = null;
86
224
  this._limit = null;
@@ -99,6 +237,16 @@ class Query {
99
237
  : (Array.isArray(opts.declaredColumns) ? new Set(opts.declaredColumns) : null);
100
238
  this._columnGateMode = opts.columnGateMode || "reject";
101
239
  this._allowedColumns = null;
240
+ // PRIMARY KEY column for the dialect-aware single-row write idiom on
241
+ // non-sqlite handles (sqlite uses the implicit rowid). db.from() tables
242
+ // key on `_id`; a table with a different PK declares it here. Validated
243
+ // as an identifier so it can splice into SQL as a quoted column.
244
+ if (opts.primaryKey !== undefined && opts.primaryKey !== null) {
245
+ safeSql.validateIdentifier(opts.primaryKey, { allowReserved: true });
246
+ this._primaryKey = opts.primaryKey;
247
+ } else {
248
+ this._primaryKey = null;
249
+ }
102
250
  }
103
251
 
104
252
  // Restrict the operator-allowable columns to an explicit subset
@@ -143,11 +291,55 @@ class Query {
143
291
  ". Use .allowedColumns([...]) or db.init({ columnGate: 'off' }) to bypass.");
144
292
  }
145
293
 
146
- // Quoted SQL form: `"schema"."table"` if schema-qualified, else `"table"`.
147
- _quotedTable() {
294
+ // Resolve the SQL dialect for the handle this Query runs against.
295
+ // db.from() drives the framework's local node:sqlite handle (dialect
296
+ // "sqlite", the default). An operator who constructs `new Query(handle,
297
+ // table)` over their OWN Postgres / MySQL handle declares the dialect on
298
+ // the handle via `handle.dialect` ("postgres" | "mysql"), so b.sql emits
299
+ // the matching identifier quoting + single-row-write idiom. An unknown /
300
+ // absent value falls back to "sqlite" — the historical default — so every
301
+ // existing caller is byte-identical.
302
+ _dialect() {
303
+ var d = this._db && this._db.dialect;
304
+ if (d === "postgres" || d === "mysql" || d === "sqlite") return d;
305
+ return "sqlite";
306
+ }
307
+
308
+ // The b.sql opts for every terminal's verb builder. The dialect is
309
+ // resolved from the handle (sqlite by default; the operator's external
310
+ // handle can declare postgres / mysql). quoteName forces b.sql to QUOTE
311
+ // the resolved table name: db-query does NO clusterStorage prefix rewrite,
312
+ // so it never needs the bare-unquoted form — and quoting preserves
313
+ // db-query's reserved-word / case-sensitive table-name support (`"name"`
314
+ // is the safe identifier form). The schema qualifier (when present) makes
315
+ // b.sql emit the quoted `"schema"."table"` form. db-query owns the column
316
+ // gate (sealed-field rewrite happens before b.sql sees a column), so the
317
+ // builder's own gate stays off.
318
+ _sqlOpts() {
148
319
  return this._schema
149
- ? '"' + this._schema + '"."' + this._table + '"'
150
- : '"' + this._table + '"';
320
+ ? { dialect: this._dialect(), schema: this._schema, quoteName: true }
321
+ : { dialect: this._dialect(), quoteName: true };
322
+ }
323
+
324
+ // Whether any WHERE condition has been recorded — drives the
325
+ // unconditional-update / -delete / -increment refusals.
326
+ _hasConditions() {
327
+ return this._conditions.length > 0;
328
+ }
329
+
330
+ // Replay the recorded WHERE chain onto a b.sql verb builder. The whole
331
+ // chain is wrapped in one b.sql whereGroup so the leaves' AND/OR
332
+ // joiners compose at a single precedence level (and a no-condition
333
+ // chain leaves the builder's where untouched). Returns the builder.
334
+ _applyConditions(builder) {
335
+ if (this._conditions.length === 0) return builder;
336
+ var conds = this._conditions;
337
+ builder.whereGroup(function (pred) {
338
+ for (var i = 0; i < conds.length; i++) {
339
+ conds[i].apply(pred);
340
+ }
341
+ });
342
+ return builder;
151
343
  }
152
344
 
153
345
  // ---- Chainable filters ----
@@ -167,7 +359,20 @@ class Query {
167
359
  return this._addCondition(fieldOrObj, op, value);
168
360
  }
169
361
 
170
- _addCondition(field, op, value) {
362
+ // whereIn(field, values) — AND an `IN (...)` membership predicate. Facade
363
+ // over where(field, "IN", values) symmetric with b.sql's whereIn, so a
364
+ // caller can match a column against a value list (e.g. the dual-read
365
+ // derived-hash candidate set) without spelling the "IN" operator.
366
+ whereIn(field, values) {
367
+ return this.where(field, "IN", values);
368
+ }
369
+
370
+ // Resolve a (field, op, value) predicate through the framework gates
371
+ // (JSONB value guard, sealed-field → derived-hash rewrite, column
372
+ // membership) and return the post-rewrite { field, op, value } that
373
+ // b.sql will emit. Shared by _addCondition and the WhereBuilder so the
374
+ // gates run identically whether the leaf is top-level or grouped.
375
+ _resolvePredicate(field, op, value) {
171
376
  if (!ALLOWED_OPS.has(op)) {
172
377
  throw new Error("invalid where operator: " + op);
173
378
  }
@@ -216,54 +421,101 @@ class Query {
216
421
  }
217
422
  // Sealed-field translation: rewrite predicate to use derived hash if available
218
423
  if (this._isSealedField(field)) {
219
- var lookup = cryptoField.lookupHash(this._cryptoFieldKey(), field, value);
220
- if (!lookup) {
221
- throw new Error(
222
- "cannot query sealed column '" + this._cryptoFieldKey() + "." + field +
223
- "' without a derived hash. Declare derivedHashes: { <name>: { from: '" + field + "' } } " +
224
- "in the table's schema config."
225
- );
424
+ var missingHashMsg =
425
+ "cannot query sealed column '" + this._cryptoFieldKey() + "." + field +
426
+ "' without a derived hash. Declare derivedHashes: { <name>: { from: '" + field + "' } } " +
427
+ "in the table's schema config.";
428
+ if (op === "IN") {
429
+ // Membership query on a sealed column: each candidate plaintext maps
430
+ // to its own derived hash. Hashing the whole array as one value (the
431
+ // scalar path below) never matches — whereIn/$in on a sealed column
432
+ // would throw or silently miss. Expand to the per-element hash set,
433
+ // and for each element ALSO include the legacy salted-sha3 digest so
434
+ // membership dual-reads across the v0.15.0 keyed-MAC flip exactly as
435
+ // the "=" path does (un-migrated rows must still be found).
436
+ if (!Array.isArray(value) || value.length === 0) {
437
+ throw new Error("where IN on sealed column '" + this._cryptoFieldKey() +
438
+ "." + field + "' requires a non-empty array of values");
439
+ }
440
+ var sealedField = null;
441
+ var hashedValues = [];
442
+ for (var inI = 0; inI < value.length; inI++) {
443
+ var elemLookup = cryptoField.lookupHash(this._cryptoFieldKey(), field, value[inI]);
444
+ if (!elemLookup) throw new Error(missingHashMsg);
445
+ sealedField = elemLookup.field;
446
+ hashedValues.push(elemLookup.value);
447
+ if (elemLookup.legacyValue != null && elemLookup.legacyValue !== elemLookup.value) {
448
+ hashedValues.push(elemLookup.legacyValue);
449
+ }
450
+ }
451
+ field = sealedField;
452
+ value = hashedValues;
453
+ } else {
454
+ var lookup = cryptoField.lookupHash(this._cryptoFieldKey(), field, value);
455
+ if (!lookup) throw new Error(missingHashMsg);
456
+ field = lookup.field;
457
+ if (op === "=" && lookup.legacyValue != null && lookup.legacyValue !== lookup.value) {
458
+ // Dual-read across the v0.15.0 keyed-MAC default flip: a row written
459
+ // before the flip carries the legacy salted-sha3 digest, so an
460
+ // equality lookup on a sealed field must match BOTH the active
461
+ // keyed-MAC digest and the legacy one — otherwise the flip silently
462
+ // drops every un-migrated row from the result. b.sql expands the
463
+ // IN-list to (?, ?) and binds each digest.
464
+ op = "IN";
465
+ value = [lookup.value, lookup.legacyValue];
466
+ } else {
467
+ value = lookup.value;
468
+ }
226
469
  }
227
- field = lookup.field;
228
- value = lookup.value;
229
470
  }
230
- cryptoField && _validateField(field);
471
+ _validateField(field);
231
472
  // Gate the post-sealed-rewrite physical column (derived-hash
232
473
  // columns are declared physical columns, so the rewrite target
233
474
  // passes membership).
234
475
  this._assertColumnMember(field, "where");
235
476
  if (op === "IN") {
236
- // node:sqlite ? does not support array-binding. Pre-v0.8.18
237
- // `where(field, "IN", [1,2,3])` silently bound the entire
238
- // array to a single placeholder and matched zero rows.
239
- // Expand to (?, ?, ?) and push each value separately.
477
+ // node:sqlite ? does not support array-binding; b.sql expands the
478
+ // IN-list to (?, ?, ?) and binds each element. Validate the shape
479
+ // here so the failure is db-query's clear message, not a builder
480
+ // error deeper in the stack.
240
481
  if (!Array.isArray(value) || value.length === 0) {
241
482
  throw new Error("where IN requires a non-empty array of values");
242
483
  }
243
- var placeholders = value.map(function () { return "?"; }).join(", ");
244
- this._where.push('"' + field + '" IN (' + placeholders + ")");
245
- for (var i = 0; i < value.length; i += 1) this._whereParams.push(value[i]);
246
- return this;
247
484
  }
248
- if (op === "LIKE" && typeof value === "string") {
249
- // Escape SQL LIKE metacharacters % and _ in operator-supplied
250
- // input. Without this, a single `%` in untrusted input becomes
251
- // a wildcard that matches everything a column-disclosure
252
- // class (`q=%@%` enumerates entire table). Use a backslash as
253
- // the escape character (uniform across SQLite + Postgres) and
254
- // emit the corresponding ESCAPE clause so the engine treats it
255
- // as the escape token. Operators who deliberately want LIKE
256
- // wildcards in their value bypass via whereRaw().
257
- var escaped = value.replace(/[\\%_]/g, "\\$&");
258
- this._where.push('"' + field + '" LIKE ? ESCAPE ' + "'\\\\'");
259
- this._whereParams.push(escaped);
260
- return this;
485
+ return { field: field, op: op, value: value };
486
+ }
487
+
488
+ // Apply a resolved predicate onto a b.sql Predicate using the given
489
+ // joiner ("AND" via where* / "OR" via orWhere*). LIKE auto-escape,
490
+ // IN-list expansion, IS NULL, and JSONB emission are all owned by
491
+ // b.sql's _cmp from here.
492
+ _emitPredicate(pred, joiner, field, op, value) {
493
+ if (op === "IN") {
494
+ if (joiner === "OR") pred.orWhereIn(field, value);
495
+ else pred.whereIn(field, value);
496
+ return;
261
497
  }
262
- this._where.push('"' + field + '" ' + op + " ?");
263
- this._whereParams.push(value);
498
+ if (joiner === "OR") pred.orWhereOp(field, op, value);
499
+ else pred.whereOp(field, op, value);
500
+ }
501
+
502
+ _addCondition(field, op, value) {
503
+ var resolved = this._resolvePredicate(field, op, value);
504
+ var self = this;
505
+ this._pushLeaf("AND", function (pred) {
506
+ self._emitPredicate(pred, "AND", resolved.field, resolved.op, resolved.value);
507
+ });
264
508
  return this;
265
509
  }
266
510
 
511
+ // Append a WHERE leaf. `apply(pred)` replays it onto a b.sql Predicate
512
+ // (AND-joined at the chain level — the leaf's own apply decides AND vs
513
+ // OR internally). orWhere() rewrites the last leaf rather than
514
+ // appending, to preserve `(prev OR new)` grouping precedence.
515
+ _pushLeaf(joiner, apply) {
516
+ this._conditions.push({ joiner: joiner, apply: apply });
517
+ }
518
+
267
519
  _isSealedField(field) {
268
520
  var sealed = cryptoField.getSealedFields(this._cryptoFieldKey());
269
521
  return sealed.indexOf(field) !== -1;
@@ -284,31 +536,35 @@ class Query {
284
536
 
285
537
  // whereRaw — append a parenthesized raw SQL fragment with positional
286
538
  // placeholders and the parameter values that fill them. Composes with
287
- // .where() (AND-joined via the same `_where` array). The fragment
288
- // must NOT contain operator-supplied SQL — it's caller-controlled
289
- // text used to build expressions the chainable .where() can't express
290
- // (compound OR, row-value comparison for cursor pagination, etc.).
291
- // Placeholder count must match params.length.
292
- whereRaw(sql, params, opts) {
293
- if (typeof sql !== "string" || sql.length === 0) {
539
+ // .where() (AND-joined). The fragment must NOT contain operator-
540
+ // supplied SQL — it's caller-controlled text used to build expressions
541
+ // the chainable .where() can't express (compound OR, row-value
542
+ // comparison for cursor pagination, etc.). b.sql's whereRaw guards the
543
+ // fragment (b.guardSql + embedded-literal + placeholder-count); the
544
+ // count + literal validation that db-query historically did inline now
545
+ // lives in that one choke-point.
546
+ whereRaw(sql_, params, opts) {
547
+ if (typeof sql_ !== "string" || sql_.length === 0) {
294
548
  throw new Error("whereRaw: sql must be a non-empty string");
295
549
  }
296
- if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql, "whereRaw");
297
- var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
298
- // Count `?` placeholders, but skip occurrences inside string
299
- // literals ('...' or "..."), line comments (-- to EOL), and
300
- // block comments (/* ... */). Pre-v0.8.18 the naive regex
301
- // counted `?` inside literals (e.g. `WHERE name = 'a?b' AND id
302
- // = ?`) which caused mismatched-count errors OR — worse — let
303
- // through fragments where the literal-`?` placebo masked a
304
- // missed real placeholder.
305
- var holders = _countPlaceholders(sql);
550
+ var p = Array.isArray(params) ? params.slice() : (params == null ? [] : [params]);
551
+ // Fail-fast at the chain-build boundary (matching the pre-b.sql
552
+ // contract the operator catches a bad fragment at the whereRaw call,
553
+ // not deep inside a terminal). The embedded-literal + placeholder-count
554
+ // refusals keep db-query's stable SafeSqlError `sql/raw-literal` /
555
+ // explicit count-mismatch contract; b.sql's whereRaw (applied at the
556
+ // terminal) is the additional emission-time guard (b.guardSql, stacked-
557
+ // statement, encoding). allowLiterals opts the operator out of the
558
+ // literal refusal for a static, operator-controlled literal.
559
+ if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql_, "whereRaw");
560
+ var holders = safeSql.countPlaceholders(sql_);
306
561
  if (holders !== p.length) {
307
562
  throw new Error("whereRaw: " + holders + " placeholder(s) in sql but " +
308
563
  p.length + " param(s) supplied");
309
564
  }
310
- this._where.push("(" + sql + ")");
311
- for (var i = 0; i < p.length; i++) this._whereParams.push(p[i]);
565
+ this._pushLeaf("AND", function (pred) {
566
+ pred.whereRaw(sql_, p, opts);
567
+ });
312
568
  return this;
313
569
  }
314
570
 
@@ -360,51 +616,51 @@ class Query {
360
616
  return this;
361
617
  }
362
618
 
363
- // ---- Build SELECT components ----
364
-
365
- _whereClause() {
366
- return this._where.length === 0 ? "" : " WHERE " + this._where.join(" AND ");
367
- }
619
+ // ---- Build SELECT components on a b.sql builder ----
368
620
 
369
- _orderLimitOffset() {
370
- var s = "";
621
+ // Apply the recorded projection / order / limit / offset onto a b.sql
622
+ // SELECT builder. Projection columns + orderBy fields already passed
623
+ // _validateField + the column gate at record time.
624
+ _applySelectClauses(qb) {
625
+ if (this._select) qb.columns(this._select);
371
626
  if (this._orderBy) {
372
627
  var entries = Array.isArray(this._orderBy) ? this._orderBy : [this._orderBy];
373
- var fragments = [];
374
628
  for (var i = 0; i < entries.length; i++) {
375
- fragments.push('"' + entries[i].field + '" ' + entries[i].direction);
629
+ qb.orderBy(entries[i].field, entries[i].direction === "DESC" ? "desc" : "asc");
376
630
  }
377
- s += " ORDER BY " + fragments.join(", ");
378
631
  }
379
- if (this._limit !== null) s += " LIMIT " + this._limit;
380
- if (this._offset !== null) s += " OFFSET " + this._offset;
381
- return s;
382
- }
383
-
384
- _projection() {
385
- if (!this._select) return "*";
386
- return this._select.map(function (c) { return '"' + c + '"'; }).join(", ");
632
+ if (this._limit !== null) qb.limit(this._limit);
633
+ if (this._offset !== null) qb.offset(this._offset);
634
+ return qb;
387
635
  }
388
636
 
389
637
  // ---- Terminal methods (sync) ----
390
638
 
391
639
  first() {
392
- var sql = "SELECT " + this._projection() + " FROM " + this._quotedTable() +
393
- this._whereClause() + this._orderLimitOffset() + " LIMIT 1";
394
- var stmt = this._db.prepare(sql);
395
- var row = stmt.get.apply(stmt, this._whereParams);
396
- return row ? cryptoField.unsealRow(this._cryptoFieldKey(), row) : null;
640
+ var qb = sql.select(this._table, this._sqlOpts());
641
+ this._applyConditions(qb);
642
+ this._applySelectClauses(qb);
643
+ qb.limit(1);
644
+ var built = qb.toSql();
645
+ var stmt = this._db.prepare(built.sql);
646
+ var row = stmt.get.apply(stmt, built.params);
647
+ // 4th arg (dbHandle) lets unsealRow fetch + unwrap the row-scoped
648
+ // K_row for vault.row: cells (declarePerRowKey tables).
649
+ return row ? cryptoField.unsealRow(this._cryptoFieldKey(), row, undefined, this._db) : null;
397
650
  }
398
651
 
399
652
  all() {
400
- var sql = "SELECT " + this._projection() + " FROM " + this._quotedTable() +
401
- this._whereClause() + this._orderLimitOffset();
402
- var stmt = this._db.prepare(sql);
403
- var rows = stmt.all.apply(stmt, this._whereParams);
653
+ var qb = sql.select(this._table, this._sqlOpts());
654
+ this._applyConditions(qb);
655
+ this._applySelectClauses(qb);
656
+ var built = qb.toSql();
657
+ var stmt = this._db.prepare(built.sql);
658
+ var rows = stmt.all.apply(stmt, built.params);
404
659
  var out = new Array(rows.length);
405
660
  var key = this._cryptoFieldKey();
661
+ var dbHandle = this._db;
406
662
  for (var i = 0; i < rows.length; i++) {
407
- out[i] = cryptoField.unsealRow(key, rows[i]);
663
+ out[i] = cryptoField.unsealRow(key, rows[i], undefined, dbHandle);
408
664
  }
409
665
  return out;
410
666
  }
@@ -416,8 +672,10 @@ class Query {
416
672
  // StreamLimit ceiling enforced from the module-level db
417
673
  // config; per-call opts.streamLimit overrides for one-off bumps.
418
674
  stream(opts) {
419
- var sql = "SELECT " + this._projection() + " FROM " + this._quotedTable() +
420
- this._whereClause() + this._orderLimitOffset();
675
+ var qb = sql.select(this._table, this._sqlOpts());
676
+ this._applyConditions(qb);
677
+ this._applySelectClauses(qb);
678
+ var built = qb.toSql();
421
679
  var perCallLimit;
422
680
  // db.js exports getStreamLimit so this module reads the live
423
681
  // ceiling without bouncing through the lib's circular load.
@@ -431,10 +689,11 @@ class Query {
431
689
  }
432
690
  perCallLimit = opts.streamLimit;
433
691
  }
434
- var stmt = this._db.prepare(sql);
692
+ var stmt = this._db.prepare(built.sql);
435
693
  var key = this._cryptoFieldKey();
694
+ var dbHandle = this._db;
436
695
  var iter;
437
- try { iter = stmt.iterate.apply(stmt, this._whereParams); }
696
+ try { iter = stmt.iterate.apply(stmt, built.params); }
438
697
  catch (e) {
439
698
  var r = new Readable({ objectMode: true, read: function () {} });
440
699
  setImmediate(function () { r.destroy(e); });
@@ -454,7 +713,7 @@ class Query {
454
713
  var step = iter.next();
455
714
  if (step.done) { this.push(null); return; }
456
715
  emitted += 1;
457
- this.push(cryptoField.unsealRow(key, step.value));
716
+ this.push(cryptoField.unsealRow(key, step.value, undefined, dbHandle));
458
717
  } catch (e) {
459
718
  this.destroy(e);
460
719
  }
@@ -463,9 +722,11 @@ class Query {
463
722
  }
464
723
 
465
724
  count() {
466
- var sql = "SELECT COUNT(*) AS n FROM " + this._quotedTable() + this._whereClause();
467
- var stmt = this._db.prepare(sql);
468
- var row = stmt.get.apply(stmt, this._whereParams);
725
+ var qb = sql.select(this._table, this._sqlOpts()).count("*", "n");
726
+ this._applyConditions(qb);
727
+ var built = qb.toSql();
728
+ var stmt = this._db.prepare(built.sql);
729
+ var row = stmt.get.apply(stmt, built.params);
469
730
  return row ? row.n : 0;
470
731
  }
471
732
 
@@ -477,14 +738,25 @@ class Query {
477
738
  if (withId._id === undefined || withId._id === null) {
478
739
  withId._id = generateToken(C.BYTES.bytes(16));
479
740
  }
480
- var sealed = cryptoField.sealRow(this._cryptoFieldKey(), withId);
481
- var cols = Object.keys(sealed);
482
- var placeholders = cols.map(function () { return "?"; }).join(", ");
483
- var quotedCols = cols.map(function (c) { return '"' + c + '"'; }).join(", ");
484
- var values = cols.map(function (c) { return sealed[c]; });
485
- var sql = "INSERT INTO " + this._quotedTable() + " (" + quotedCols + ") VALUES (" + placeholders + ")";
486
- var insertStmt = this._db.prepare(sql);
487
- insertStmt.run.apply(insertStmt, values);
741
+ // Residency gates read the PLAINTEXT row (the tag column must be
742
+ // inspectable even when sibling columns seal below).
743
+ _assertLocalResidency(this._cryptoFieldKey(), withId, "insert");
744
+ // Per-row-key tables (declarePerRowKey): materialize a fresh K_row
745
+ // BEFORE sealRow so sealed columns encrypt under the row-scoped key
746
+ // (vault.row: cells). rowId MUST be withId._id the same value
747
+ // b.subject.eraseHard / b.retention destroy on, so a later shred
748
+ // makes these cells undecryptable. Materialize stores the random
749
+ // row-secret AAD-sealed in the per-row-key store.
750
+ var sealOpts;
751
+ var cfKey = this._cryptoFieldKey();
752
+ if (cryptoField.hasPerRowKey(cfKey)) {
753
+ var kRow = cryptoField.materializePerRowKey(cfKey, withId._id, this._db);
754
+ sealOpts = { kRow: kRow, rowId: withId._id };
755
+ }
756
+ var sealed = cryptoField.sealRow(cfKey, withId, sealOpts);
757
+ var built = sql.insert(this._table, this._sqlOpts()).values(sealed).toSql();
758
+ var insertStmt = this._db.prepare(built.sql);
759
+ insertStmt.run.apply(insertStmt, built.params);
488
760
  // Return the original row with _id filled in (plaintext, never sealed)
489
761
  return Object.assign({}, withId);
490
762
  }
@@ -511,10 +783,23 @@ class Query {
511
783
  if (!changes || typeof changes !== "object") {
512
784
  throw new Error("update requires a changes object");
513
785
  }
514
- if (this._where.length === 0) {
786
+ if (!this._hasConditions()) {
515
787
  throw new Error("refusing unconditional update — call where(...) first");
516
788
  }
517
- var sealed = cryptoField.sealRow(this._cryptoFieldKey(), changes);
789
+ // Residency gates on the plaintext change set — an UPDATE that
790
+ // touches the residency tag (or a region-bound column) is a
791
+ // transfer and goes through the same refusal matrix as INSERT.
792
+ _assertLocalResidency(this._cryptoFieldKey(), changes, "update");
793
+ var cfKey = this._cryptoFieldKey();
794
+ // Per-row-key tables: sealed columns must re-encrypt under EACH
795
+ // affected row's own K_row, so a single set-based UPDATE can't seal
796
+ // one value across rows. Resolve the affected _id set, then seal +
797
+ // write each row under its row-scoped key. Idempotent materialize
798
+ // re-derives the existing K_row (created on INSERT).
799
+ if (cryptoField.hasPerRowKey(cfKey)) {
800
+ return this._updatePerRowKey(cfKey, changes, single);
801
+ }
802
+ var sealed = cryptoField.sealRow(cfKey, changes);
518
803
  var setKeys = Object.keys(sealed);
519
804
  if (setKeys.length === 0) {
520
805
  throw new Error("update changes object is empty");
@@ -522,27 +807,142 @@ class Query {
522
807
  setKeys.forEach(_validateField);
523
808
  var selfUpd = this;
524
809
  setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
525
- var setClause = setKeys.map(function (k) { return '"' + k + '" = ?'; }).join(", ");
526
- var setValues = setKeys.map(function (k) { return sealed[k]; });
527
-
528
- var whereSql = this._where.join(" AND ");
529
- var limit = single ? " LIMIT 1" : "";
530
- // SQLite supports LIMIT on UPDATE only when compiled with SQLITE_ENABLE_UPDATE_DELETE_LIMIT.
531
- // node:sqlite ships without that flag — emulate single-row with a sub-select on rowid.
532
- var sql;
533
- var qt = this._quotedTable();
810
+
811
+ // No engine ships a portable UPDATE ... LIMIT, so a single-row update
812
+ // resolves exactly one row then writes it. The shape is dialect-aware
813
+ // (sqlite rowid sub-select / postgres PK sub-select / mysql
814
+ // resolve-then-write _buildSingleRowWrite). A null result means the
815
+ // WHERE matched no row, so there is nothing to update (0 changes).
816
+ var built;
534
817
  if (single) {
535
- sql = "UPDATE " + qt + " SET " + setClause +
536
- " WHERE rowid = (SELECT rowid FROM " + qt + " WHERE " + whereSql + " LIMIT 1)";
818
+ built = this._buildSingleRowWrite(sealed);
819
+ if (built === null) return 0;
537
820
  } else {
538
- sql = "UPDATE " + qt + " SET " + setClause + " WHERE " + whereSql + limit;
821
+ var qb = sql.update(this._table, this._sqlOpts()).set(sealed);
822
+ this._applyConditions(qb);
823
+ built = qb.toSql();
539
824
  }
540
- var allParams = setValues.concat(this._whereParams);
541
- var updStmt = this._db.prepare(sql);
542
- var info = updStmt.run.apply(updStmt, allParams);
825
+ var updStmt = this._db.prepare(built.sql);
826
+ var info = updStmt.run.apply(updStmt, built.params);
543
827
  return info.changes;
544
828
  }
545
829
 
830
+ // The single-row-write row locator, by dialect. No engine ships
831
+ // UPDATE ... LIMIT portably (node:sqlite is built without
832
+ // SQLITE_ENABLE_UPDATE_DELETE_LIMIT), so the single-row idiom is a
833
+ // sub-SELECT that resolves exactly one row then matches it:
834
+ //
835
+ // sqlite — the implicit `rowid` system column (every non-WITHOUT-
836
+ // ROWID table has one); `WHERE "rowid" = (SELECT "rowid"
837
+ // FROM t WHERE ... LIMIT 1)`.
838
+ // postgres — the table's PRIMARY KEY (`_id`, the db.from() convention).
839
+ // Postgres accepts LIMIT in a scalar subquery, so the same
840
+ // `= (SELECT "_id" ... LIMIT 1)` shape works — and using the
841
+ // real, UNIQUE `_id` column keeps b.sql's quote-by-
842
+ // construction intact (ctid is an unquotable system column
843
+ // that would force a raw-identifier escape and is unstable
844
+ // across VACUUM).
845
+ // mysql — also the PRIMARY KEY, but MySQL refuses LIMIT in a
846
+ // subquery that directly references the same table in an
847
+ // `IN`/`=` predicate; wrapping the inner SELECT in a derived
848
+ // table (`... IN (SELECT "_id" FROM (SELECT "_id" ... LIMIT
849
+ // 1) AS _s)`) is the standard work-around.
850
+ //
851
+ // The inner SELECT is composed through b.sql (same table + conditions)
852
+ // and spliced via whereSub — passing the inner BUILDER (not concatenated
853
+ // SQL) so b.sql concatenates the sub-query's sql + params itself and the
854
+ // final statement still runs through b.sql's output validator.
855
+ _rowLocatorColumn(dialect) {
856
+ return dialect === "sqlite" ? "rowid" : this._pkColumn();
857
+ }
858
+
859
+ // The PRIMARY KEY column for single-row writes on non-sqlite dialects.
860
+ // db.from() tables key on `_id` (auto-generated when absent on insert);
861
+ // an operator running a table with a different PK overrides it via the
862
+ // `primaryKey` construction opt.
863
+ _pkColumn() {
864
+ return this._primaryKey || "_id";
865
+ }
866
+
867
+ _buildSingleRowWrite(sealed) {
868
+ if (this._dialect() === "mysql") {
869
+ // MySQL forbids referencing the UPDATE/DELETE target table in a
870
+ // subquery (error 1093), so the single-statement sub-SELECT idiom
871
+ // the other dialects use is unavailable. Resolve the one row's PK in
872
+ // a prior SELECT, then write `WHERE pk = ?` with the resolved value
873
+ // bound — every value still binds, the identifier still quotes by
874
+ // construction, and the write is a single validated statement with no
875
+ // self-referential subquery. Returns null when no row matched.
876
+ var pkVal = this._resolveSinglePk();
877
+ if (pkVal === null) return null;
878
+ return sql.update(this._table, this._sqlOpts())
879
+ .set(sealed)
880
+ .where(this._pkColumn(), pkVal)
881
+ .toSql();
882
+ }
883
+ var col = this._rowLocatorColumn(this._dialect());
884
+ var inner = sql.select(this._table, this._sqlOpts()).columns([col]);
885
+ this._applyConditions(inner);
886
+ inner.limit(1);
887
+ return sql.update(this._table, this._sqlOpts())
888
+ .set(sealed)
889
+ .whereSub(col, "=", inner)
890
+ .toSql();
891
+ }
892
+
893
+ // Resolve the PK of exactly one row matching the recorded WHERE (LIMIT
894
+ // 1). Used by the MySQL single-row write path, where a self-referential
895
+ // subquery is rejected by the engine. The SELECT is a clean, fully-bound
896
+ // b.sql statement; returns the PK value, or null when nothing matched.
897
+ _resolveSinglePk() {
898
+ var pk = this._pkColumn();
899
+ var pick = sql.select(this._table, this._sqlOpts()).columns([pk]);
900
+ this._applyConditions(pick);
901
+ pick.limit(1);
902
+ var built = pick.toSql();
903
+ var stmt = this._db.prepare(built.sql);
904
+ var row = stmt.get.apply(stmt, built.params);
905
+ if (!row) return null;
906
+ var v = row[pk];
907
+ return (v === undefined || v === null) ? null : v;
908
+ }
909
+
910
+ // Per-row-key UPDATE. Sealed columns on a declarePerRowKey table are
911
+ // K_row cells (vault.row:), so each affected row must be re-sealed
912
+ // under its OWN K_row — a single set-based UPDATE can't carry per-row
913
+ // ciphertext. Resolve the affected _id set via the WHERE, then for
914
+ // each row: materialize (idempotent) its K_row, seal the change set
915
+ // under it (derived hashes computed from plaintext as usual), and
916
+ // UPDATE that single row by _id. `single` stops after the first row.
917
+ _updatePerRowKey(cfKey, changes, single) {
918
+ var idSelect = sql.select(this._table, this._sqlOpts()).columns(["_id"]);
919
+ this._applyConditions(idSelect);
920
+ if (single) idSelect.limit(1);
921
+ var idBuilt = idSelect.toSql();
922
+ var idStmt = this._db.prepare(idBuilt.sql);
923
+ var idRows = idStmt.all.apply(idStmt, idBuilt.params);
924
+ var changed = 0;
925
+ for (var r = 0; r < idRows.length; r++) {
926
+ var rowId = idRows[r]._id;
927
+ if (rowId === undefined || rowId === null) continue;
928
+ var kRow = cryptoField.materializePerRowKey(cfKey, rowId, this._db);
929
+ var sealed = cryptoField.sealRow(cfKey, changes, { kRow: kRow, rowId: rowId });
930
+ var setKeys = Object.keys(sealed);
931
+ if (setKeys.length === 0) {
932
+ throw new Error("update changes object is empty");
933
+ }
934
+ setKeys.forEach(_validateField);
935
+ var selfUpd = this;
936
+ setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
937
+ var built = sql.update(this._table, this._sqlOpts())
938
+ .set(sealed).where("_id", rowId).toSql();
939
+ var updStmt = this._db.prepare(built.sql);
940
+ var info = updStmt.run.apply(updStmt, built.params);
941
+ changed += (info && info.changes) || 0;
942
+ }
943
+ return changed;
944
+ }
945
+
546
946
  deleteOne() {
547
947
  return this._delete(true) > 0;
548
948
  }
@@ -554,9 +954,9 @@ class Query {
554
954
  // Atomic counter increment.
555
955
  //
556
956
  // `from(table).where(filter).increment("col", 1)` emits
557
- // `UPDATE table SET col = col + ? WHERE ...` so concurrent writers
558
- // can't collide on a fetch/mutate/store sequence (which would lose
559
- // increments under racing transactions). Pass a negative delta to
957
+ // `UPDATE table SET col = COALESCE(col, 0) + ? WHERE ...` so concurrent
958
+ // writers can't collide on a fetch/mutate/store sequence (which would
959
+ // lose increments under racing transactions). Pass a negative delta to
560
960
  // decrement.
561
961
  //
562
962
  // Returns the number of rows changed (matches updateMany shape).
@@ -570,19 +970,22 @@ class Query {
570
970
  if (typeof delta !== "number" || !Number.isFinite(delta) || !Number.isInteger(delta)) {
571
971
  throw new Error("increment(column, delta): delta must be a finite integer (default 1)");
572
972
  }
573
- if (this._where.length === 0) {
973
+ if (!this._hasConditions()) {
574
974
  throw new Error("refusing unconditional increment — call where(...) first");
575
975
  }
576
- var whereSql = this._where.join(" AND ");
577
- var qt = this._quotedTable();
578
- var qc = '"' + column + '"';
579
976
  // Use COALESCE so a NULL counter starts at 0 instead of producing
580
977
  // NULL + delta = NULL silently (which would silently drop the
581
- // operation under SQLite's NULL-arithmetic rules).
582
- var sql = "UPDATE " + qt + " SET " + qc + " = COALESCE(" + qc + ", 0) + ? WHERE " + whereSql;
583
- var allParams = [delta].concat(this._whereParams);
584
- var stmt = this._db.prepare(sql);
585
- var info = stmt.run.apply(stmt, allParams);
978
+ // operation under SQLite's NULL-arithmetic rules). The quoted column
979
+ // expression is built by b.safeSql under the active dialect so the
980
+ // increment RHS references the same quoted identifier b.sql's set
981
+ // target uses (double-quote on sqlite/postgres, backtick on mysql).
982
+ var qc = safeSql.quoteIdentifier(column, this._dialect(), { allowReserved: true });
983
+ var qb = sql.update(this._table, this._sqlOpts())
984
+ .setRaw(column, "COALESCE(" + qc + ", 0) + ?", [delta]);
985
+ this._applyConditions(qb);
986
+ var built = qb.toSql();
987
+ var stmt = this._db.prepare(built.sql);
988
+ var info = stmt.run.apply(stmt, built.params);
586
989
  return info.changes;
587
990
  }
588
991
 
@@ -602,10 +1005,10 @@ class Query {
602
1005
  }
603
1006
  var sub = new WhereBuilder(this);
604
1007
  closure(sub);
605
- var built = sub.build();
606
- if (!built.sql) return this;
607
- this._where.push("(" + built.sql + ")");
608
- for (var i = 0; i < built.params.length; i++) this._whereParams.push(built.params[i]);
1008
+ if (sub._parts.length === 0) return this;
1009
+ this._pushLeaf("AND", function (pred) {
1010
+ pred.whereGroup(function (g) { sub.replay(g); });
1011
+ });
609
1012
  return this;
610
1013
  }
611
1014
 
@@ -613,36 +1016,61 @@ class Query {
613
1016
  // `.where(a).orWhere(b)` produces `WHERE (a) OR (b)` rather than
614
1017
  // `WHERE (a) AND (b)`. Accepts the same arg shapes as `.where`:
615
1018
  // object-literal map, `(field, value)`, `(field, op, value)`, or a
616
- // `(qb) => ...` closure.
1019
+ // `(qb) => ...` closure. Replays as `(prevLeaf OR newLeaf)` so the
1020
+ // grouping precedence matches the pre-b.sql `( prev OR ( new ) )` form.
617
1021
  orWhere(fieldOrObjOrFn, op, value) {
618
- if (this._where.length === 0) {
1022
+ if (this._conditions.length === 0) {
619
1023
  throw new Error("orWhere(...): no prior where(...) — start the chain with where(...)");
620
1024
  }
1025
+ var argc = arguments.length;
1026
+ var prevLeaf = this._conditions.pop();
1027
+ var orApply;
621
1028
  if (typeof fieldOrObjOrFn === "function") {
622
1029
  var sub = new WhereBuilder(this);
623
1030
  fieldOrObjOrFn(sub);
624
- var built = sub.build();
625
- if (!built.sql) return this;
626
- var prev = this._where.pop();
627
- this._where.push("(" + prev + " OR (" + built.sql + "))");
628
- for (var i = 0; i < built.params.length; i++) this._whereParams.push(built.params[i]);
629
- return this;
630
- }
631
- // For non-closure shapes, build a transient single-leaf Query and
632
- // splice it. We compile to a `WhereBuilder` for symmetry.
633
- var sub2 = new WhereBuilder(this);
634
- if (fieldOrObjOrFn !== null && typeof fieldOrObjOrFn === "object" && !Array.isArray(fieldOrObjOrFn)) {
635
- Object.keys(fieldOrObjOrFn).forEach(function (k) { sub2.eq(k, fieldOrObjOrFn[k]); });
636
- } else if (op === undefined) {
637
- sub2.eq(fieldOrObjOrFn, /* value */ arguments[1]);
1031
+ if (sub._parts.length === 0) {
1032
+ // Empty OR closure — restore the prior leaf untouched.
1033
+ this._conditions.push(prevLeaf);
1034
+ return this;
1035
+ }
1036
+ orApply = function (pred) {
1037
+ pred.orWhereGroup(function (g) { sub.replay(g); });
1038
+ };
1039
+ } else if (fieldOrObjOrFn !== null && typeof fieldOrObjOrFn === "object" &&
1040
+ !Array.isArray(fieldOrObjOrFn)) {
1041
+ // Object map all equalities OR'd as one group leaf.
1042
+ var self = this;
1043
+ var resolvedList = Object.keys(fieldOrObjOrFn).map(function (k) {
1044
+ return self._resolvePredicate(k, "=", fieldOrObjOrFn[k]);
1045
+ });
1046
+ orApply = function (pred) {
1047
+ pred.orWhereGroup(function (g) {
1048
+ for (var i = 0; i < resolvedList.length; i++) {
1049
+ self._emitPredicate(g, "AND", resolvedList[i].field, resolvedList[i].op,
1050
+ resolvedList[i].value);
1051
+ }
1052
+ });
1053
+ };
638
1054
  } else {
639
- sub2._push("AND", fieldOrObjOrFn, op, value);
1055
+ // 2-arg orWhere(field, value) is the equality shorthand; 3-arg
1056
+ // orWhere(field, op, value) carries an explicit operator. Mirror
1057
+ // .where()'s arguments.length discrimination so a 2-arg value of
1058
+ // (e.g.) the number 5 is never mistaken for an operator.
1059
+ var resolved = (argc === 2)
1060
+ ? this._resolvePredicate(fieldOrObjOrFn, "=", op)
1061
+ : this._resolvePredicate(fieldOrObjOrFn, op, value);
1062
+ var selfP = this;
1063
+ orApply = function (pred) {
1064
+ selfP._emitPredicate(pred, "OR", resolved.field, resolved.op, resolved.value);
1065
+ };
640
1066
  }
641
- var built2 = sub2.build();
642
- if (!built2.sql) return this;
643
- var prev2 = this._where.pop();
644
- this._where.push("(" + prev2 + " OR (" + built2.sql + "))");
645
- for (var j = 0; j < built2.params.length; j++) this._whereParams.push(built2.params[j]);
1067
+ // Re-push a single leaf that emits ( prevLeaf OR newLeaf ).
1068
+ this._pushLeaf("AND", function (pred) {
1069
+ pred.whereGroup(function (g) {
1070
+ prevLeaf.apply(g);
1071
+ orApply(g);
1072
+ });
1073
+ });
646
1074
  return this;
647
1075
  }
648
1076
 
@@ -668,22 +1096,24 @@ class Query {
668
1096
  }
669
1097
  if (term.length === 0) return this;
670
1098
  var match = (opts && opts.match) || "substring";
671
- // Escape the operator's term so SQL LIKE wildcards in user input
672
- // don't widen the match. Use `~` as the ESCAPE char (SQLite's
673
- // ESCAPE clause requires a single character — picking `~` rather
674
- // than `\` avoids JS-string-literal escaping headaches; `~` rarely
675
- // appears in user-supplied search terms).
676
- var escaped = String(term).replace(/[~%_]/g, function (c) { return "~" + c; });
677
- var pattern;
678
- if (match === "exact") pattern = escaped;
679
- else if (match === "prefix") pattern = escaped + "%";
680
- else if (match === "substring") pattern = "%" + escaped + "%";
681
- else throw new Error("search: opts.match must be 'substring' | 'prefix' | 'exact'");
682
- var clauses = fields.map(function (f) { return '"' + f + '" LIKE ? ESCAPE \'~\''; });
683
- var sql = "(" + clauses.join(" OR ") + ")";
684
- var params = fields.map(function () { return pattern; });
685
- this._where.push(sql);
686
- for (var i = 0; i < params.length; i++) this._whereParams.push(params[i]);
1099
+ if (match !== "exact" && match !== "prefix" && match !== "substring") {
1100
+ throw new Error("search: opts.match must be 'substring' | 'prefix' | 'exact'");
1101
+ }
1102
+ // b.sql's whereLike owns the wildcard handling end-to-end: it escapes
1103
+ // the user's `%` / `_` metacharacters with `~`, adds the LIVE wrapping
1104
+ // wildcard per mode, and emits `"field" LIKE ? ESCAPE '~'` (a
1105
+ // builder-emitted ESCAPE clause, so no raw-fragment guard refusal). An
1106
+ // OR group across every search field; the first leaf leads, the rest
1107
+ // OR-join.
1108
+ var fieldList = fields.slice();
1109
+ this._pushLeaf("AND", function (pred) {
1110
+ pred.whereGroup(function (g) {
1111
+ for (var i = 0; i < fieldList.length; i++) {
1112
+ if (i === 0) g.whereLike(fieldList[i], term, match);
1113
+ else g.orWhereLike(fieldList[i], term, match);
1114
+ }
1115
+ });
1116
+ });
687
1117
  return this;
688
1118
  }
689
1119
 
@@ -724,20 +1154,41 @@ class Query {
724
1154
  }
725
1155
 
726
1156
  _delete(single) {
727
- if (this._where.length === 0) {
1157
+ if (!this._hasConditions()) {
728
1158
  throw new Error("refusing unconditional delete — call where(...) first");
729
1159
  }
730
- var whereSql = this._where.join(" AND ");
731
- var sql;
732
- var qt = this._quotedTable();
1160
+ var built;
733
1161
  if (single) {
734
- sql = "DELETE FROM " + qt +
735
- " WHERE rowid = (SELECT rowid FROM " + qt + " WHERE " + whereSql + " LIMIT 1)";
1162
+ // No engine ships a portable DELETE ... LIMIT, so single-row delete
1163
+ // mirrors the single-row update idiom: sqlite splices a rowid
1164
+ // sub-select, postgres a PK sub-select (both via b.sql whereSub, the
1165
+ // inner builder object — b.sql concatenates the sub-query's sql +
1166
+ // params, no hand-rolled string), and mysql resolves the one PK in a
1167
+ // prior SELECT then deletes `WHERE pk = ?` (the engine forbids a
1168
+ // subquery referencing the DELETE target table). A null PK means the
1169
+ // WHERE matched nothing — 0 rows deleted.
1170
+ if (this._dialect() === "mysql") {
1171
+ var pkVal = this._resolveSinglePk();
1172
+ if (pkVal === null) return 0;
1173
+ built = sql.delete(this._table, this._sqlOpts())
1174
+ .where(this._pkColumn(), pkVal)
1175
+ .toSql();
1176
+ } else {
1177
+ var col = this._rowLocatorColumn(this._dialect());
1178
+ var inner = sql.select(this._table, this._sqlOpts()).columns([col]);
1179
+ this._applyConditions(inner);
1180
+ inner.limit(1);
1181
+ built = sql.delete(this._table, this._sqlOpts())
1182
+ .whereSub(col, "=", inner)
1183
+ .toSql();
1184
+ }
736
1185
  } else {
737
- sql = "DELETE FROM " + qt + " WHERE " + whereSql;
1186
+ var dqb = sql.delete(this._table, this._sqlOpts());
1187
+ this._applyConditions(dqb);
1188
+ built = dqb.toSql();
738
1189
  }
739
- var delStmt = this._db.prepare(sql);
740
- var info = delStmt.run.apply(delStmt, this._whereParams);
1190
+ var delStmt = this._db.prepare(built.sql);
1191
+ var info = delStmt.run.apply(delStmt, built.params);
741
1192
  return info.changes;
742
1193
  }
743
1194
  }
@@ -751,11 +1202,13 @@ class Query {
751
1202
  // `.orGte` / `.orLt` / `.orLte` / `.orIn` / `.orLike` ORs an
752
1203
  // expression. `.raw(sql, params)` AND's an arbitrary fragment.
753
1204
  //
754
- // `.build()` returns `{ sql, params }`. Empty builder `{ sql: "",
755
- // params: [] }`.
1205
+ // Each part is recorded structurally ({ joiner, kind, ... }) and replayed
1206
+ // onto a b.sql Predicate via replay(pred) — b.sql owns the quoting +
1207
+ // binding + LIKE escape + IN-list expansion. The owning Query runs the
1208
+ // column-membership gate as each part is recorded.
756
1209
  class WhereBuilder {
757
1210
  constructor(gate) {
758
- this._parts = []; // [{ joiner: "AND"|"OR", sql: "...", params: [...] }]
1211
+ this._parts = []; // [{ joiner, kind: "cmp"|"raw", ... }]
759
1212
  // The owning Query, so grouped/OR sub-expressions enforce the
760
1213
  // same column-membership gate as the top-level chain.
761
1214
  this._gate = gate || null;
@@ -766,19 +1219,17 @@ class WhereBuilder {
766
1219
  }
767
1220
  _validateField(field);
768
1221
  if (this._gate) this._gate._assertColumnMember(field, "whereGroup");
769
- var qf = '"' + field + '"';
770
1222
  if (op === "IN" || op === "NOT IN") {
771
1223
  if (!Array.isArray(value) || value.length === 0) {
772
1224
  throw new Error("WhereBuilder: " + op + " requires a non-empty array of values");
773
1225
  }
774
- var placeholders = value.map(function () { return "?"; }).join(", ");
775
- this._parts.push({ joiner: joiner, sql: qf + " " + op + " (" + placeholders + ")", params: value.slice() });
1226
+ this._parts.push({ joiner: joiner, kind: "cmp", field: field, op: op, value: value.slice() });
776
1227
  return this;
777
1228
  }
778
- if (!ALLOWED_OPS.has(op)) {
1229
+ if (!ALLOWED_OPS.has(op) && op !== "NOT IN") {
779
1230
  throw new Error("WhereBuilder: invalid operator '" + op + "'");
780
1231
  }
781
- this._parts.push({ joiner: joiner, sql: qf + " " + op + " ?", params: [value] });
1232
+ this._parts.push({ joiner: joiner, kind: "cmp", field: field, op: op, value: value });
782
1233
  return this;
783
1234
  }
784
1235
  eq(f, v) { return this._push("AND", f, "=", v); }
@@ -797,56 +1248,67 @@ class WhereBuilder {
797
1248
  orLte(f, v) { return this._push("OR", f, "<=", v); }
798
1249
  orIn(f, vs) { return this._push("OR", f, "IN", vs); }
799
1250
  orLike(f, v) { return this._push("OR", f, "LIKE", v); }
800
- raw(sql, params, opts) {
801
- if (typeof sql !== "string" || sql.length === 0) {
1251
+ raw(sql_, params, opts) {
1252
+ if (typeof sql_ !== "string" || sql_.length === 0) {
802
1253
  throw new Error("WhereBuilder.raw: sql must be a non-empty string");
803
1254
  }
804
- if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql, "WhereBuilder.raw");
805
- var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
806
- if (_countPlaceholders(sql) !== p.length) {
1255
+ var p = Array.isArray(params) ? params.slice() : (params == null ? [] : [params]);
1256
+ // Same fail-fast literal + placeholder-count contract as Query.whereRaw
1257
+ // (stable SafeSqlError code); b.sql re-guards at the terminal.
1258
+ if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql_, "WhereBuilder.raw");
1259
+ if (safeSql.countPlaceholders(sql_) !== p.length) {
807
1260
  throw new Error("WhereBuilder.raw: placeholder count mismatch");
808
1261
  }
809
- this._parts.push({ joiner: "AND", sql: "(" + sql + ")", params: p });
1262
+ this._parts.push({ joiner: "AND", kind: "raw", sql: sql_, params: p, opts: opts });
810
1263
  return this;
811
1264
  }
1265
+ // Replay the recorded parts onto a b.sql Predicate. The first part
1266
+ // leads the group (its joiner is the group's first leaf); each later
1267
+ // part AND/OR-joins per its recorded joiner. b.sql performs identifier
1268
+ // quoting, value binding, and IN-list expansion.
1269
+ replay(pred) {
1270
+ for (var i = 0; i < this._parts.length; i++) {
1271
+ _replayPart(pred, this._parts[i], this._parts[i].joiner === "OR" && i > 0);
1272
+ }
1273
+ }
812
1274
  build() {
1275
+ // Back-compat shim for any external reader that called build() to get
1276
+ // a { sql, params } pair. Replay onto a transient b.sql SELECT's
1277
+ // predicate and extract. Returns { sql: "", params: [] } when empty.
813
1278
  if (this._parts.length === 0) return { sql: "", params: [] };
814
- var sql = this._parts[0].sql;
815
- var params = this._parts[0].params.slice();
816
- for (var i = 1; i < this._parts.length; i += 1) {
817
- sql = sql + " " + this._parts[i].joiner + " " + this._parts[i].sql;
818
- for (var j = 0; j < this._parts[i].params.length; j += 1) {
819
- params.push(this._parts[i].params[j]);
820
- }
821
- }
822
- return { sql: sql, params: params };
1279
+ var self = this;
1280
+ var built = sql.select("t", { dialect: "sqlite" })
1281
+ .whereGroup(function (g) { self.replay(g); })
1282
+ .toSql();
1283
+ // Strip the "SELECT * FROM t WHERE (" prefix + trailing ")".
1284
+ var m = /WHERE \((.*)\)$/.exec(built.sql);
1285
+ return { sql: m ? m[1] : "", params: built.params };
823
1286
  }
824
1287
  }
825
1288
 
826
- // Count `?` placeholders outside string literals + comments.
827
- // Tracks SQL single-quoted, double-quoted, line-comment, and block-
828
- // comment state to avoid counting `?` characters that are part of
829
- // literal text the SQL engine never interprets as a binding marker.
830
- // Refuse raw SQL fragments that embed a single-quoted string
831
- // literal. A whereRaw / WhereBuilder.raw fragment is meant to be a
832
- // STATIC template whose every value is bound through a `?` placeholder;
833
- // an embedded `'...'` literal is the signature of operator input
834
- // concatenated into the query (CWE-89 / CWE-564 — concat into a
835
- // query builder). Double-quoted identifiers (`"col"`), line comments,
1289
+ // Refuse a raw SQL fragment that embeds a single-quoted string literal.
1290
+ // A whereRaw / WhereBuilder.raw fragment is a STATIC template whose every
1291
+ // value binds through a `?` placeholder; an embedded `'...'` literal is
1292
+ // the signature of operator input concatenated into the query builder
1293
+ // (CWE-89 / CWE-564). Double-quoted identifiers (`"col"`), line comments,
836
1294
  // and block comments are skipped. Operators with a deliberate static
837
- // literal pass `{ allowLiterals: true }`. Shares the quote/comment
838
- // scanning shape with _countPlaceholders.
839
- function _assertRawNoStringLiteral(sql, where) {
1295
+ // literal pass `{ allowLiterals: true }`. db-query runs this eagerly at
1296
+ // the chain-build boundary so the operator-facing `sql/raw-literal`
1297
+ // SafeSqlError contract is stable; b.sql's whereRaw re-guards the same
1298
+ // fragment at the terminal (b.guardSql + the emission-time validator).
1299
+ // Single linear pass, no backtracking regex; shares the scan shape with
1300
+ // b.safeSql.countPlaceholders.
1301
+ function _assertRawNoStringLiteral(rawSql, where) {
840
1302
  var i = 0;
841
- var len = sql.length;
1303
+ var len = rawSql.length;
842
1304
  while (i < len) {
843
- var ch = sql.charAt(i);
844
- var next = i + 1 < len ? sql.charAt(i + 1) : "";
1305
+ var ch = rawSql.charAt(i);
1306
+ var next = i + 1 < len ? rawSql.charAt(i + 1) : "";
845
1307
  if (ch === '"') {
846
1308
  i += 1;
847
1309
  while (i < len) {
848
- if (sql.charAt(i) === '"') {
849
- if (sql.charAt(i + 1) === '"') { i += 2; continue; }
1310
+ if (rawSql.charAt(i) === '"') {
1311
+ if (rawSql.charAt(i + 1) === '"') { i += 2; continue; }
850
1312
  i += 1; break;
851
1313
  }
852
1314
  i += 1;
@@ -854,12 +1316,12 @@ function _assertRawNoStringLiteral(sql, where) {
854
1316
  continue;
855
1317
  }
856
1318
  if (ch === "-" && next === "-") {
857
- while (i < len && sql.charAt(i) !== "\n") i += 1;
1319
+ while (i < len && rawSql.charAt(i) !== "\n") i += 1;
858
1320
  continue;
859
1321
  }
860
1322
  if (ch === "/" && next === "*") {
861
1323
  i += 2;
862
- while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
1324
+ while (i < len && !(rawSql.charAt(i) === "*" && rawSql.charAt(i + 1) === "/")) i += 1;
863
1325
  i += 2;
864
1326
  continue;
865
1327
  }
@@ -874,40 +1336,47 @@ function _assertRawNoStringLiteral(sql, where) {
874
1336
  }
875
1337
  }
876
1338
 
877
- function _countPlaceholders(sql) {
878
- var count = 0;
879
- var i = 0;
880
- var len = sql.length;
881
- while (i < len) {
882
- var ch = sql.charAt(i);
883
- var next = i + 1 < len ? sql.charAt(i + 1) : "";
884
- if (ch === "'" || ch === '"') {
885
- var quote = ch;
886
- i += 1;
887
- while (i < len) {
888
- if (sql.charAt(i) === quote) {
889
- // SQL doubles the quote char to escape it within a literal.
890
- if (sql.charAt(i + 1) === quote) { i += 2; continue; }
891
- i += 1; break;
892
- }
893
- i += 1;
894
- }
895
- continue;
896
- }
897
- if (ch === "-" && next === "-") {
898
- while (i < len && sql.charAt(i) !== "\n") i += 1;
899
- continue;
900
- }
901
- if (ch === "/" && next === "*") {
902
- i += 2;
903
- while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
904
- i += 2;
905
- continue;
906
- }
907
- if (ch === "?") count += 1;
908
- i += 1;
1339
+ // Apply one recorded WhereBuilder part onto a b.sql Predicate. `or`
1340
+ // selects the OR-joining method (after the first leaf in a group); the
1341
+ // first leaf ignores its joiner (it leads the group). NOT IN and LIKE
1342
+ // are the two ops with a behavior the bare structured Predicate does not
1343
+ // expose 1:1: NOT IN has no orWhere* form, and the WhereBuilder LIKE is a
1344
+ // caller-controlled-wildcard LIKE (the value binds verbatim — no
1345
+ // auto-escape, matching the pre-b.sql WhereBuilder semantics, distinct
1346
+ // from .search() which escapes). Both compose through the guarded raw /
1347
+ // group surface without weakening anything.
1348
+ function _replayPart(pred, part, or) {
1349
+ if (part.kind === "raw") {
1350
+ if (or) pred.orWhereRaw(part.sql, part.params, part.opts);
1351
+ else pred.whereRaw(part.sql, part.params, part.opts);
1352
+ return;
1353
+ }
1354
+ if (part.op === "LIKE") {
1355
+ // Verbatim LIKE — caller controls the wildcards (no escape clause),
1356
+ // exactly as the pre-migration WhereBuilder emitted `"f" LIKE ?`. The
1357
+ // identifier quoting follows the predicate's OWN dialect (the builder
1358
+ // it replays onto), so the LIKE column matches the surrounding query's
1359
+ // quoting on mysql (backtick) as well as sqlite/postgres (double-quote).
1360
+ var likeDialect = (pred && typeof pred._dialect === "function") ? pred._dialect() : "sqlite";
1361
+ var likeSql = safeSql.quoteIdentifier(part.field, likeDialect, { allowReserved: true }) + " LIKE ?";
1362
+ if (or) pred.orWhereRaw(likeSql, [part.value]);
1363
+ else pred.whereRaw(likeSql, [part.value]);
1364
+ return;
909
1365
  }
910
- return count;
1366
+ if (part.op === "IN") {
1367
+ if (or) pred.orWhereIn(part.field, part.value);
1368
+ else pred.whereIn(part.field, part.value);
1369
+ return;
1370
+ }
1371
+ if (part.op === "NOT IN") {
1372
+ // b.sql exposes no orWhereNotIn; emit an OR NOT-IN leaf as a
1373
+ // single-member OR group so the join precedence is preserved.
1374
+ if (or) pred.orWhereGroup(function (g) { g.whereNotIn(part.field, part.value); });
1375
+ else pred.whereNotIn(part.field, part.value);
1376
+ return;
1377
+ }
1378
+ if (or) pred.orWhereOp(part.field, part.op, part.value);
1379
+ else pred.whereOp(part.field, part.op, part.value);
911
1380
  }
912
1381
 
913
1382
  function _validateField(field) {
@@ -921,4 +1390,157 @@ function _validateField(field) {
921
1390
  }
922
1391
  }
923
1392
 
924
- module.exports = { Query: Query };
1393
+ // ---- raw-write residency gate (execRaw / prepared-statement execution) ----
1394
+ // The structured builder runs every insert/update through _assertLocalResidency.
1395
+ // The raw paths (b.db.runSql / execRaw, b.db.prepare(sql).run(...)) bypass it, so
1396
+ // a cross-border row could land straight on disk under a regulated posture. These
1397
+ // helpers extract the residency-column value from a raw INSERT / UPDATE / REPLACE
1398
+ // and run it through the SAME gate; a write to a residency table the framework
1399
+ // cannot parse fails CLOSED (refused) - a raw write never skips the check.
1400
+ var _RAW_WRITE_KEYWORD_RE = /^\s*(?:INSERT|REPLACE|UPDATE)\b/i;
1401
+ var _RAW_INSERT_RE = /^\s*(?:INSERT|REPLACE)\s+(?:OR\s+[A-Za-z]+\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]?\s*\(([^)]+)\)\s*VALUES\s*\(([\s\S]+)\)\s*;?\s*$/i;
1402
+ var _RAW_UPDATE_RE = /^\s*UPDATE\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\s+([\s\S]+?)\s*;?\s*$/i;
1403
+ var _RAW_TABLE_RE = /^\s*(?:INSERT|REPLACE)\s+(?:OR\s+[A-Za-z]+\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]?|^\s*UPDATE\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]?/i;
1404
+
1405
+ function _unquoteIdent(s) {
1406
+ s = String(s).trim();
1407
+ if (s.length >= 2 &&
1408
+ (s.charAt(0) === '"' || s.charAt(0) === "'" || s.charAt(0) === "`") &&
1409
+ s.charAt(s.length - 1) === s.charAt(0)) {
1410
+ return s.slice(1, -1);
1411
+ }
1412
+ return s;
1413
+ }
1414
+
1415
+ 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;
1421
+ }
1422
+
1423
+ // Cheap prepare-time pre-check so only writes to a residency table get wrapped.
1424
+ function _isRawWriteToResidencyTable(sql) {
1425
+ var table = _rawWriteTable(sql);
1426
+ if (!table) return false;
1427
+ return !!(cryptoField.getPerRowResidency(table) || cryptoField.getColumnResidency(table));
1428
+ }
1429
+
1430
+ function _splitTopLevelCommas(s) {
1431
+ var out = [], depth = 0, cur = "", q = null;
1432
+ for (var i = 0; i < s.length; i++) {
1433
+ var c = s.charAt(i);
1434
+ if (q) {
1435
+ cur += c;
1436
+ if (c === q) { if (s.charAt(i + 1) === q) { cur += s.charAt(++i); } else { q = null; } }
1437
+ continue;
1438
+ }
1439
+ if (c === "'" || c === '"' || c === "`") { q = c; cur += c; continue; }
1440
+ if (c === "(") { depth += 1; cur += c; continue; }
1441
+ if (c === ")") { depth -= 1; cur += c; continue; }
1442
+ if (c === "," && depth === 0) { out.push(cur); cur = ""; continue; }
1443
+ cur += c;
1444
+ }
1445
+ if (cur.trim() !== "") out.push(cur);
1446
+ return out.map(function (x) { return x.trim(); });
1447
+ }
1448
+
1449
+ // Quote/paren-aware: return the SET-clause text up to the first top-level
1450
+ // WHERE keyword that is NOT inside a string literal or parenthesised
1451
+ // subexpression. A WHERE embedded in a quoted value (SET note='x WHERE
1452
+ // y', ...) is skipped, so a residency-column assignment after it is still
1453
+ // parsed and gated. Linear scan; fixed 5-char keyword peek, no per-char slice.
1454
+ function _setClauseBeforeWhere(s) {
1455
+ var depth = 0, q = null, n = s.length;
1456
+ for (var i = 0; i < n; i++) {
1457
+ var c = s.charAt(i);
1458
+ if (q) {
1459
+ if (c === q) { if (s.charAt(i + 1) === q) { i++; } else { q = null; } }
1460
+ continue;
1461
+ }
1462
+ if (c === "'" || c === '"' || c === "\x60") { q = c; continue; }
1463
+ if (c === "(") { depth += 1; continue; }
1464
+ if (c === ")") { depth -= 1; continue; }
1465
+ if (depth === 0 && (c === " " || c === "\t" || c === "\n" || c === "\r")) {
1466
+ var j = i;
1467
+ while (j < n && /\s/.test(s.charAt(j))) j += 1;
1468
+ if (s.substr(j, 5).toLowerCase() === "where" && !/\w/.test(s.charAt(j + 5) || "")) {
1469
+ return s.slice(0, i);
1470
+ }
1471
+ }
1472
+ }
1473
+ return s;
1474
+ }
1475
+
1476
+ function _rawValue(tok, boundParams, pc) {
1477
+ tok = tok.trim();
1478
+ if (tok === "?") { return boundParams[pc.i++]; }
1479
+ if (tok.length >= 2 && (tok.charAt(0) === "'" || tok.charAt(0) === '"')) {
1480
+ var qc = tok.charAt(0);
1481
+ return tok.slice(1, -1).split(qc + qc).join(qc);
1482
+ }
1483
+ if (/^null$/i.test(tok)) return null;
1484
+ if (/^-?\d+(?:\.\d+)?$/.test(tok)) return Number(tok);
1485
+ return tok; // bare expression / named param: opaque -> fails the allowedTags check -> refused
1486
+ }
1487
+
1488
+ function _flattenRunParams(argsLike) {
1489
+ var a = Array.prototype.slice.call(argsLike || []);
1490
+ if (a.length === 1 && Array.isArray(a[0])) return a[0];
1491
+ return a;
1492
+ }
1493
+
1494
+ function _assertRawWriteResidency(sql, boundParams) {
1495
+ var table = _rawWriteTable(sql);
1496
+ if (!table) return;
1497
+ if (!cryptoField.getPerRowResidency(table) && !cryptoField.getColumnResidency(table)) return;
1498
+ boundParams = _flattenRunParams(boundParams);
1499
+
1500
+ // The INSERT/UPDATE body regexes below scan with [\s\S]+; bound the input
1501
+ // first and fail CLOSED on an over-long statement - a residency write the
1502
+ // framework cannot safely parse must be refused, never let past the gate.
1503
+ if (sql.length > 100000) {
1504
+ throw new DbQueryError("db-query/row-residency-raw-unparseable",
1505
+ "raw write to residency table '" + table + "' exceeds the parse limit (" +
1506
+ sql.length + " chars) - use b.db.from(\"" + table + "\") so residency is validated", true);
1507
+ }
1508
+
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
1511
+ if (!mi && !mu) {
1512
+ throw new DbQueryError("db-query/row-residency-raw-unparseable",
1513
+ "raw write to residency table '" + table + "' cannot be parsed to validate its " +
1514
+ "residency tag - use b.db.from(\"" + table + "\").insertOne / .updateOne so the tag is checked", true);
1515
+ }
1516
+
1517
+ var plaintextRow = {};
1518
+ var pc = { i: 0 };
1519
+ if (mi) {
1520
+ var cols = _splitTopLevelCommas(mi[2]).map(_unquoteIdent);
1521
+ var vals = _splitTopLevelCommas(mi[3]);
1522
+ if (cols.length !== vals.length) {
1523
+ throw new DbQueryError("db-query/row-residency-raw-unparseable",
1524
+ "raw insert to residency table '" + table + "' has an unmodelled VALUES shape " +
1525
+ "(multi-row / expression) - use the structured builder so residency is validated", true);
1526
+ }
1527
+ for (var ci = 0; ci < cols.length; ci++) {
1528
+ plaintextRow[cols[ci]] = _rawValue(vals[ci], boundParams, pc);
1529
+ }
1530
+ _assertLocalResidency(table, plaintextRow, "insert");
1531
+ } else {
1532
+ var assigns = _splitTopLevelCommas(_setClauseBeforeWhere(mu[2]));
1533
+ for (var ai = 0; ai < assigns.length; ai++) {
1534
+ var eq = assigns[ai].indexOf("=");
1535
+ if (eq === -1) continue;
1536
+ plaintextRow[_unquoteIdent(assigns[ai].slice(0, eq))] = _rawValue(assigns[ai].slice(eq + 1), boundParams, pc);
1537
+ }
1538
+ _assertLocalResidency(table, plaintextRow, "update");
1539
+ }
1540
+ }
1541
+
1542
+ module.exports = {
1543
+ Query: Query,
1544
+ _isRawWriteToResidencyTable: _isRawWriteToResidencyTable,
1545
+ _assertRawWriteResidency: _assertRawWriteResidency,
1546
+ };