@blamejs/blamejs-shop 0.4.31 → 0.4.32
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.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +392 -278
- package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
- package/lib/vendor/blamejs/.gitignore +6 -0
- package/lib/vendor/blamejs/CHANGELOG.md +26 -0
- package/lib/vendor/blamejs/MIGRATING.md +43 -0
- package/lib/vendor/blamejs/README.md +8 -6
- package/lib/vendor/blamejs/SECURITY.md +19 -3
- package/lib/vendor/blamejs/api-snapshot.json +2190 -664
- package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
- package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
- package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
- package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
- package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
- package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
- package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
- package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
- package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
- package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
- package/lib/vendor/blamejs/lib/api-key.js +158 -77
- package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
- package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
- package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
- package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
- package/lib/vendor/blamejs/lib/audit.js +259 -123
- package/lib/vendor/blamejs/lib/auth/oauth.js +53 -9
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
- package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +31 -5
- package/lib/vendor/blamejs/lib/backup/index.js +45 -10
- package/lib/vendor/blamejs/lib/break-glass.js +355 -147
- package/lib/vendor/blamejs/lib/cache.js +174 -105
- package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
- package/lib/vendor/blamejs/lib/cli.js +19 -14
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
- package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
- package/lib/vendor/blamejs/lib/cluster.js +119 -71
- package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
- package/lib/vendor/blamejs/lib/compliance.js +206 -4
- package/lib/vendor/blamejs/lib/consent.js +82 -29
- package/lib/vendor/blamejs/lib/constants.js +27 -11
- package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
- package/lib/vendor/blamejs/lib/db-query.js +882 -260
- package/lib/vendor/blamejs/lib/db-schema.js +228 -44
- package/lib/vendor/blamejs/lib/db.js +249 -99
- package/lib/vendor/blamejs/lib/dsr.js +385 -55
- package/lib/vendor/blamejs/lib/error-page.js +14 -1
- package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
- package/lib/vendor/blamejs/lib/external-db.js +549 -34
- package/lib/vendor/blamejs/lib/file-upload.js +52 -7
- package/lib/vendor/blamejs/lib/framework-error.js +20 -1
- package/lib/vendor/blamejs/lib/framework-files.js +73 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
- package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
- package/lib/vendor/blamejs/lib/guard-all.js +1 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
- package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
- package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
- package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
- package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
- package/lib/vendor/blamejs/lib/guard-email.js +47 -69
- package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
- package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
- package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
- package/lib/vendor/blamejs/lib/guard-html.js +53 -108
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
- package/lib/vendor/blamejs/lib/guard-image.js +46 -103
- package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
- package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
- package/lib/vendor/blamejs/lib/guard-json.js +38 -108
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
- package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
- package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
- package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
- package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
- package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
- package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
- package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
- package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
- package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
- package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
- package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
- package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
- package/lib/vendor/blamejs/lib/guard-template.js +35 -172
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-time.js +32 -154
- package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
- package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
- package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
- package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
- package/lib/vendor/blamejs/lib/http-client.js +37 -9
- package/lib/vendor/blamejs/lib/inbox.js +120 -107
- package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
- package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
- package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
- package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
- package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
- package/lib/vendor/blamejs/lib/mail-store.js +293 -154
- package/lib/vendor/blamejs/lib/mail.js +8 -4
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
- package/lib/vendor/blamejs/lib/migrations.js +108 -66
- package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
- package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
- package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
- package/lib/vendor/blamejs/lib/observability.js +124 -0
- package/lib/vendor/blamejs/lib/otel-export.js +12 -3
- package/lib/vendor/blamejs/lib/outbox.js +184 -83
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
- package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
- package/lib/vendor/blamejs/lib/queue-local.js +225 -140
- package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
- package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
- package/lib/vendor/blamejs/lib/queue.js +7 -0
- package/lib/vendor/blamejs/lib/redact.js +68 -11
- package/lib/vendor/blamejs/lib/redis-client.js +160 -31
- package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
- package/lib/vendor/blamejs/lib/retention.js +101 -40
- package/lib/vendor/blamejs/lib/router.js +212 -5
- package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
- package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
- package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
- package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
- package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
- package/lib/vendor/blamejs/lib/safe-url.js +170 -3
- package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
- package/lib/vendor/blamejs/lib/scheduler.js +35 -12
- package/lib/vendor/blamejs/lib/seeders.js +122 -74
- package/lib/vendor/blamejs/lib/session-stores.js +42 -14
- package/lib/vendor/blamejs/lib/session.js +175 -77
- package/lib/vendor/blamejs/lib/sql.js +3842 -0
- package/lib/vendor/blamejs/lib/sse.js +26 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +151 -4
- package/lib/vendor/blamejs/lib/static.js +177 -34
- package/lib/vendor/blamejs/lib/subject.js +96 -49
- package/lib/vendor/blamejs/lib/vault/index.js +3 -2
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
- package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
- package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
- package/lib/vendor/blamejs/lib/websocket.js +35 -5
- package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
- package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
- package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
- package/lib/vendor/blamejs/scripts/check-services.js +21 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +51 -0
- package/lib/vendor/blamejs/scripts/release.js +398 -38
- package/lib/vendor/blamejs/test/00-primitives.js +117 -0
- package/lib/vendor/blamejs/test/10-state.js +140 -14
- package/lib/vendor/blamejs/test/20-db.js +65 -2
- package/lib/vendor/blamejs/test/helpers/db.js +9 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
- package/lib/vendor/blamejs/test/helpers/services.js +21 -0
- package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
- package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
- package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
- package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
- package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
- package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
- package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
- package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
- package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
- package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
- package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
- package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
- package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
- package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1120 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +309 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
- package/lib/vendor/blamejs/test/smoke.js +79 -21
- package/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
- package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
- package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
- package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
- package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
- package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
- package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
- 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
|
-
|
|
83
|
-
|
|
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
|
-
//
|
|
147
|
-
|
|
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
|
-
?
|
|
150
|
-
:
|
|
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
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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.
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
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.
|
|
311
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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)
|
|
380
|
-
if (this._offset !== null)
|
|
381
|
-
return
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
var
|
|
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
|
|
420
|
-
|
|
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,
|
|
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
|
|
467
|
-
|
|
468
|
-
var
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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.
|
|
786
|
+
if (!this._hasConditions()) {
|
|
515
787
|
throw new Error("refusing unconditional update — call where(...) first");
|
|
516
788
|
}
|
|
517
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
//
|
|
531
|
-
|
|
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
|
-
|
|
536
|
-
|
|
818
|
+
built = this._buildSingleRowWrite(sealed);
|
|
819
|
+
if (built === null) return 0;
|
|
537
820
|
} else {
|
|
538
|
-
|
|
821
|
+
var qb = sql.update(this._table, this._sqlOpts()).set(sealed);
|
|
822
|
+
this._applyConditions(qb);
|
|
823
|
+
built = qb.toSql();
|
|
539
824
|
}
|
|
540
|
-
var
|
|
541
|
-
var
|
|
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
|
|
558
|
-
// can't collide on a fetch/mutate/store sequence (which would
|
|
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.
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
var
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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.
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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.
|
|
1157
|
+
if (!this._hasConditions()) {
|
|
728
1158
|
throw new Error("refusing unconditional delete — call where(...) first");
|
|
729
1159
|
}
|
|
730
|
-
var
|
|
731
|
-
var sql;
|
|
732
|
-
var qt = this._quotedTable();
|
|
1160
|
+
var built;
|
|
733
1161
|
if (single) {
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
755
|
-
//
|
|
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: "
|
|
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
|
-
|
|
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,
|
|
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(
|
|
801
|
-
if (typeof
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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",
|
|
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
|
|
815
|
-
var
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
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 }`.
|
|
838
|
-
//
|
|
839
|
-
|
|
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 =
|
|
1303
|
+
var len = rawSql.length;
|
|
842
1304
|
while (i < len) {
|
|
843
|
-
var ch =
|
|
844
|
-
var next = i + 1 < len ?
|
|
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 (
|
|
849
|
-
if (
|
|
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 &&
|
|
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 && !(
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|