@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.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +400 -282
- 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 +28 -0
- package/lib/vendor/blamejs/MIGRATING.md +55 -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/elevation-grant.js +6 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +66 -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 +36 -7
- 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 +210 -4
- package/lib/vendor/blamejs/lib/consent.js +82 -29
- package/lib/vendor/blamejs/lib/constants.js +27 -11
- package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
- 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 +117 -42
- 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 +47 -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 +169 -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/release-notes/v0.15.7.json +43 -0
- package/lib/vendor/blamejs/scripts/check-services.js +21 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +67 -0
- package/lib/vendor/blamejs/scripts/release.js +398 -38
- package/lib/vendor/blamejs/test/00-primitives.js +168 -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 +1196 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -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/retention-floor.test.js +59 -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 +362 -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/scheduler-watchdog-stale-settle.test.js +71 -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 +2 -2
- 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
|
@@ -52,44 +52,303 @@ var log = boot("external-db");
|
|
|
52
52
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
53
53
|
var db = lazyRequire(function () { return require("./db"); });
|
|
54
54
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
55
|
+
// b.sql composes the framework's own internal queries against a backend
|
|
56
|
+
// (e.g. the pg_roles hardening scan). Lazy because b.sql -> framework-schema
|
|
57
|
+
// -> external-db would cycle at module load; resolved when a query runs.
|
|
58
|
+
var sql = lazyRequire(function () { return require("./sql"); });
|
|
55
59
|
|
|
56
60
|
function _emitMetric(name, value, labels) {
|
|
57
61
|
try { observability().event(name, value, labels || {}); }
|
|
58
62
|
catch (_e) { /* hot-path observability sink — drop silent by design */ }
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
// Statement-class classifier for auth-failure forensics
|
|
62
|
-
// the leading keyword
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
65
|
+
// Statement-class classifier for auth-failure forensics AND the
|
|
66
|
+
// residency write gate. Reads the leading keyword — but a leading
|
|
67
|
+
// keyword is not the whole story: `WITH ... INSERT`, `EXPLAIN ANALYZE
|
|
68
|
+
// INSERT`, `CALL`/`EXECUTE`/`DO`, `COPY ... FROM`, and `REPLACE` all
|
|
69
|
+
// place rows while their leading (or only) keyword reads as harmless.
|
|
70
|
+
// _classifyStatement unwraps WITH (CTE) and EXPLAIN [ANALYZE] prefixes
|
|
71
|
+
// to the effective verb so the gate sees the real statement class; an
|
|
72
|
+
// attacker-controlled trailing fragment still can't smuggle a false
|
|
73
|
+
// class because the multi-statement form is refused upstream
|
|
74
|
+
// (_hasTrailingStatement) and an unresolvable prefix classifies
|
|
75
|
+
// UNKNOWN (fail-closed on the gate's enforced path).
|
|
76
|
+
//
|
|
77
|
+
// Skips leading whitespace plus SQL line / block comments before
|
|
78
|
+
// reading the keyword. Linear (non-backtracking) comment/whitespace
|
|
79
|
+
// skip: each iteration of the outer group consumes exactly one
|
|
80
|
+
// whitespace char, one complete block comment (star-not-slash form,
|
|
81
|
+
// never a lazy `[\s\S]*?`), or one complete line comment — disjoint by
|
|
82
|
+
// first char, so a crafted SQL string of nested `/**/` or `*/--` runs
|
|
83
|
+
// cannot backtrack polynomially (CWE-1333 ReDoS).
|
|
71
84
|
var _STATEMENT_CLASS_RE = /^(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/|--[^\n]*\n)*([A-Za-z]+)/;
|
|
72
85
|
var _STATEMENT_CLASS_MAP = Object.freeze({
|
|
73
|
-
SELECT: "SELECT",
|
|
74
|
-
|
|
86
|
+
SELECT: "SELECT", VALUES: "SELECT", TABLE: "SELECT", // allow:hand-rolled-sql — leading-keyword classifier table, not composed SQL
|
|
87
|
+
SHOW: "READ_INFO", DESCRIBE: "READ_INFO", DESC: "READ_INFO",
|
|
88
|
+
PRAGMA: "READ_INFO", USE: "READ_INFO",
|
|
89
|
+
INSERT: "DML", UPDATE: "DML", DELETE: "DML", MERGE: "DML",
|
|
90
|
+
UPSERT: "DML", REPLACE: "DML",
|
|
75
91
|
CREATE: "DDL", DROP: "DDL", ALTER: "DDL", TRUNCATE: "DDL",
|
|
76
92
|
RENAME: "DDL", COMMENT: "DDL",
|
|
77
93
|
GRANT: "DCL", REVOKE: "DCL",
|
|
78
94
|
SET: "SESSION", RESET: "SESSION",
|
|
79
95
|
BEGIN: "TX", START: "TX", COMMIT: "TX", ROLLBACK: "TX",
|
|
80
96
|
SAVEPOINT: "TX", RELEASE: "TX",
|
|
81
|
-
CALL: "ROUTINE", EXECUTE: "ROUTINE",
|
|
97
|
+
CALL: "ROUTINE", EXECUTE: "ROUTINE", DO: "ROUTINE",
|
|
82
98
|
COPY: "BULK",
|
|
83
99
|
EXPLAIN: "META", ANALYZE: "META", VACUUM: "META",
|
|
84
100
|
});
|
|
85
101
|
|
|
102
|
+
// Main-statement keywords that may follow a WITH (CTE) clause list —
|
|
103
|
+
// the verb that decides the statement's effect. `WITH src AS (...)
|
|
104
|
+
// INSERT INTO ...` is a write; classifying it by its leading keyword
|
|
105
|
+
// would label it a read and wave it past the residency write gate.
|
|
106
|
+
var _CTE_MAIN_VERBS = Object.freeze({
|
|
107
|
+
SELECT: true, VALUES: true, TABLE: true,
|
|
108
|
+
INSERT: true, UPDATE: true, DELETE: true,
|
|
109
|
+
MERGE: true, UPSERT: true, REPLACE: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// SQL identifier character classes, as char-range predicates rather
|
|
113
|
+
// than `_isIdentChar(ch)` regex literals — faster per-char in
|
|
114
|
+
// the tight statement-scan loops and keeps the trivial word-char class
|
|
115
|
+
// out of the cross-file duplicate-regex catalog.
|
|
116
|
+
function _isIdentStart(ch) {
|
|
117
|
+
return (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || ch === "_";
|
|
118
|
+
}
|
|
119
|
+
function _isIdentChar(ch) {
|
|
120
|
+
return _isIdentStart(ch) || (ch >= "0" && ch <= "9");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If sql[i] begins an opaque span — a string literal ('...' with
|
|
124
|
+
// doubled-quote escapes), a quoted identifier ("..." / `...` / [...]),
|
|
125
|
+
// a Postgres dollar-quoted body ($tag$...$tag$), or a SQL line / block
|
|
126
|
+
// comment — return the index just past its close; -1 if the span is
|
|
127
|
+
// unterminated; i unchanged if sql[i] does not begin a span. One
|
|
128
|
+
// linear scan per call, no backtracking regex (CWE-1333). Shared by
|
|
129
|
+
// every SQL leading-/effective-verb scanner below so the multi-
|
|
130
|
+
// statement, CTE, EXPLAIN, and COPY walkers all agree on what counts
|
|
131
|
+
// as data vs. structure across the Postgres / MySQL / SQLite dialects
|
|
132
|
+
// external-db targets. A doubled closing quote ('it''s') re-enters as
|
|
133
|
+
// a fresh empty span on the caller's next iteration, staying opaque.
|
|
134
|
+
function _skipOpaqueSpan(sql, i) {
|
|
135
|
+
var n = sql.length;
|
|
136
|
+
var ch = sql.charAt(i);
|
|
137
|
+
if (ch === "'" || ch === "\"" || ch === "`") {
|
|
138
|
+
var close = sql.indexOf(ch, i + 1);
|
|
139
|
+
return close === -1 ? -1 : close + 1;
|
|
140
|
+
}
|
|
141
|
+
if (ch === "[") { // SQLite / MSSQL bracket identifier
|
|
142
|
+
var rb = sql.indexOf("]", i + 1);
|
|
143
|
+
return rb === -1 ? -1 : rb + 1;
|
|
144
|
+
}
|
|
145
|
+
if (ch === "$") {
|
|
146
|
+
// $tag$ ... $tag$ dollar-quoted body; a bare $n placeholder has no
|
|
147
|
+
// second `$` after the run of word chars and is not a span.
|
|
148
|
+
var tagEnd = i + 1;
|
|
149
|
+
while (tagEnd < n && _isIdentChar(sql.charAt(tagEnd))) tagEnd += 1;
|
|
150
|
+
if (tagEnd < n && sql.charAt(tagEnd) === "$") {
|
|
151
|
+
var tag = sql.slice(i, tagEnd + 1);
|
|
152
|
+
var closeTag = sql.indexOf(tag, tagEnd + 1);
|
|
153
|
+
return closeTag === -1 ? -1 : closeTag + tag.length;
|
|
154
|
+
}
|
|
155
|
+
return i;
|
|
156
|
+
}
|
|
157
|
+
if (ch === "-" && sql.charAt(i + 1) === "-") {
|
|
158
|
+
var nl = sql.indexOf("\n", i + 2);
|
|
159
|
+
return nl === -1 ? -1 : nl + 1;
|
|
160
|
+
}
|
|
161
|
+
if (ch === "/" && sql.charAt(i + 1) === "*") {
|
|
162
|
+
var ce = sql.indexOf("*/", i + 2);
|
|
163
|
+
return ce === -1 ? -1 : ce + 2;
|
|
164
|
+
}
|
|
165
|
+
return i;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Resolve the main-statement keyword of a WITH-prefixed (CTE)
|
|
169
|
+
// statement: walk past the CTE definition list to the first top-level
|
|
170
|
+
// main verb (opaque spans skipped via _skipOpaqueSpan, parens tracked
|
|
171
|
+
// by depth). `WITH src AS (...) INSERT INTO ...` is a write; the
|
|
172
|
+
// leading keyword would label it a read and wave it past the residency
|
|
173
|
+
// write gate. Returns the uppercased verb, or null when unresolvable
|
|
174
|
+
// (unterminated span, parenthesized main statement, stray close-paren,
|
|
175
|
+
// no verb found) — the gate REFUSES unresolvable statements on its
|
|
176
|
+
// enforced path, so a parse miss fails closed.
|
|
177
|
+
function _cteMainKeyword(sql, start) {
|
|
178
|
+
var n = sql.length;
|
|
179
|
+
var depth = 0;
|
|
180
|
+
var i = start;
|
|
181
|
+
while (i < n) {
|
|
182
|
+
var ch = sql.charAt(i);
|
|
183
|
+
var skipped = _skipOpaqueSpan(sql, i);
|
|
184
|
+
if (skipped === -1) return null;
|
|
185
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
186
|
+
if (ch === "(") { depth += 1; i += 1; continue; }
|
|
187
|
+
if (ch === ")") { depth -= 1; i += 1; continue; }
|
|
188
|
+
if (_isIdentStart(ch)) {
|
|
189
|
+
var we = i + 1;
|
|
190
|
+
while (we < n && _isIdentChar(sql.charAt(we))) we += 1;
|
|
191
|
+
if (depth === 0) {
|
|
192
|
+
var word = sql.slice(i, we).toUpperCase();
|
|
193
|
+
if (_CTE_MAIN_VERBS[word] === true) return word;
|
|
194
|
+
// Top-level word that is not a main verb: CTE name, AS,
|
|
195
|
+
// RECURSIVE, [NOT] MATERIALIZED, or a SEARCH / CYCLE clause
|
|
196
|
+
// word — keep walking.
|
|
197
|
+
}
|
|
198
|
+
i = we;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
i += 1;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// EXPLAIN option words (Postgres parenthesized + legacy bare; MySQL
|
|
207
|
+
// bare). These precede the inner statement and never terminate the
|
|
208
|
+
// option list, so the verb scanner skips them; only ANALYZE flips the
|
|
209
|
+
// "this EXPLAIN actually executes the statement" bit.
|
|
210
|
+
var _EXPLAIN_OPTION_WORDS = Object.freeze({
|
|
211
|
+
ANALYZE: true, VERBOSE: true, COSTS: true, BUFFERS: true,
|
|
212
|
+
SETTINGS: true, WAL: true, TIMING: true, SUMMARY: true,
|
|
213
|
+
SERIALIZE: true, MEMORY: true, GENERIC_PLAN: true, FORMAT: true,
|
|
214
|
+
TEXT: true, JSON: true, YAML: true, XML: true, TREE: true,
|
|
215
|
+
EXTENDED: true, PARTITIONS: true,
|
|
216
|
+
ON: true, OFF: true, TRUE: true, FALSE: true,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Resolve an EXPLAIN prefix: skip the option list (a parenthesized
|
|
220
|
+
// `( ANALYZE, FORMAT JSON )` group and/or bare option words), noting
|
|
221
|
+
// whether ANALYZE is present, and return { hasAnalyze, innerStart }
|
|
222
|
+
// pointing at the wrapped statement's leading keyword. null when
|
|
223
|
+
// unresolvable (unterminated span, unbalanced option parens, no inner
|
|
224
|
+
// statement). EXPLAIN ANALYZE EXECUTES the wrapped statement, so an
|
|
225
|
+
// `EXPLAIN ANALYZE INSERT ...` is a real write the gate must see.
|
|
226
|
+
function _explainResolve(sql, start) {
|
|
227
|
+
var n = sql.length;
|
|
228
|
+
var hasAnalyze = false;
|
|
229
|
+
var i = start;
|
|
230
|
+
while (i < n) {
|
|
231
|
+
var ch = sql.charAt(i);
|
|
232
|
+
var skipped = _skipOpaqueSpan(sql, i);
|
|
233
|
+
if (skipped === -1) return null;
|
|
234
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
235
|
+
if (ch === "(") {
|
|
236
|
+
var depth = 0;
|
|
237
|
+
var j = i;
|
|
238
|
+
while (j < n) {
|
|
239
|
+
var c2 = sql.charAt(j);
|
|
240
|
+
var s2 = _skipOpaqueSpan(sql, j);
|
|
241
|
+
if (s2 === -1) return null;
|
|
242
|
+
if (s2 !== j) { j = s2; continue; }
|
|
243
|
+
if (c2 === "(") { depth += 1; j += 1; continue; }
|
|
244
|
+
if (c2 === ")") { depth -= 1; j += 1; if (depth === 0) break; continue; }
|
|
245
|
+
if (_isIdentStart(c2)) {
|
|
246
|
+
var oe = j + 1;
|
|
247
|
+
while (oe < n && _isIdentChar(sql.charAt(oe))) oe += 1;
|
|
248
|
+
if (sql.slice(j, oe).toUpperCase() === "ANALYZE") hasAnalyze = true;
|
|
249
|
+
j = oe;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
j += 1;
|
|
253
|
+
}
|
|
254
|
+
if (depth !== 0) return null;
|
|
255
|
+
i = j;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (_isIdentStart(ch)) {
|
|
259
|
+
var we = i + 1;
|
|
260
|
+
while (we < n && _isIdentChar(sql.charAt(we))) we += 1;
|
|
261
|
+
var word = sql.slice(i, we).toUpperCase();
|
|
262
|
+
if (word === "ANALYZE") { hasAnalyze = true; i = we; continue; }
|
|
263
|
+
if (_EXPLAIN_OPTION_WORDS[word] === true) { i = we; continue; }
|
|
264
|
+
return { hasAnalyze: hasAnalyze, innerStart: i };
|
|
265
|
+
}
|
|
266
|
+
i += 1;
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// COPY <target> FROM <source> LOADS rows (a write); COPY <target> /
|
|
272
|
+
// COPY (query) TO <dest> EXPORTS rows (a read). Find the first
|
|
273
|
+
// top-level FROM / TO keyword after COPY, skipping a parenthesized
|
|
274
|
+
// source query and opaque spans. FROM → true (load); TO → false
|
|
275
|
+
// (export); unresolvable or neither → true (fail-closed write).
|
|
276
|
+
function _copyLoadsRows(sql) {
|
|
277
|
+
var m = _STATEMENT_CLASS_RE.exec(sql);
|
|
278
|
+
if (!m) return true;
|
|
279
|
+
var n = sql.length;
|
|
280
|
+
var i = m.index + m[0].length;
|
|
281
|
+
while (i < n) {
|
|
282
|
+
var ch = sql.charAt(i);
|
|
283
|
+
var skipped = _skipOpaqueSpan(sql, i);
|
|
284
|
+
if (skipped === -1) return true;
|
|
285
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
286
|
+
if (ch === "(") {
|
|
287
|
+
var depth = 0;
|
|
288
|
+
var j = i;
|
|
289
|
+
while (j < n) {
|
|
290
|
+
var c2 = sql.charAt(j);
|
|
291
|
+
var s2 = _skipOpaqueSpan(sql, j);
|
|
292
|
+
if (s2 === -1) return true;
|
|
293
|
+
if (s2 !== j) { j = s2; continue; }
|
|
294
|
+
if (c2 === "(") { depth += 1; j += 1; continue; }
|
|
295
|
+
if (c2 === ")") { depth -= 1; j += 1; if (depth === 0) break; continue; }
|
|
296
|
+
j += 1;
|
|
297
|
+
}
|
|
298
|
+
i = j;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (_isIdentStart(ch)) {
|
|
302
|
+
var we = i + 1;
|
|
303
|
+
while (we < n && _isIdentChar(sql.charAt(we))) we += 1;
|
|
304
|
+
var word = sql.slice(i, we).toUpperCase();
|
|
305
|
+
if (word === "FROM") return true;
|
|
306
|
+
if (word === "TO") return false;
|
|
307
|
+
i = we;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
i += 1;
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Forensic / gate statement class. Resolves WITH (CTE) and EXPLAIN
|
|
316
|
+
// [ANALYZE] prefixes to the effective verb so a write wearing a
|
|
317
|
+
// harmless leading keyword classifies as the write it is; unresolvable
|
|
318
|
+
// prefixes classify UNKNOWN (fail-closed at the gate).
|
|
86
319
|
function _classifyStatement(sql) {
|
|
87
320
|
if (typeof sql !== "string" || sql.length === 0) return "UNKNOWN";
|
|
88
321
|
var m = _STATEMENT_CLASS_RE.exec(sql);
|
|
89
322
|
if (!m) return "UNKNOWN";
|
|
90
|
-
|
|
323
|
+
var kw = m[1].toUpperCase();
|
|
324
|
+
if (kw === "WITH") {
|
|
325
|
+
var main = _cteMainKeyword(sql, m.index + m[0].length);
|
|
326
|
+
return main === null ? "UNKNOWN" : (_STATEMENT_CLASS_MAP[main] || "OTHER");
|
|
327
|
+
}
|
|
328
|
+
if (kw === "EXPLAIN") {
|
|
329
|
+
// Plan-only EXPLAIN reads (META). EXPLAIN ANALYZE executes the
|
|
330
|
+
// wrapped statement, so its effective class is the inner one's.
|
|
331
|
+
var ex = _explainResolve(sql, m.index + m[0].length);
|
|
332
|
+
if (ex === null) return "UNKNOWN";
|
|
333
|
+
if (!ex.hasAnalyze) return "META";
|
|
334
|
+
return _classifyStatement(sql.slice(ex.innerStart));
|
|
335
|
+
}
|
|
336
|
+
return _STATEMENT_CLASS_MAP[kw] || "OTHER";
|
|
91
337
|
}
|
|
92
338
|
|
|
339
|
+
// Statement classes that place no rows on the backend, so the cross-
|
|
340
|
+
// border residency write gate lets them pass without a row tag. Every
|
|
341
|
+
// other class — DML, ROUTINE (CALL / EXECUTE / DO), a COPY ... FROM
|
|
342
|
+
// load, or an unmapped/unresolved statement — is treated as a write
|
|
343
|
+
// and must carry a residency tag on the enforced path. DDL (schema
|
|
344
|
+
// changes) and DCL (grants) move no row data across a border; the one
|
|
345
|
+
// edge they don't cover, CREATE TABLE AS SELECT / SELECT INTO, is a
|
|
346
|
+
// documented residency limitation rather than a silent bypass.
|
|
347
|
+
var _RESIDENCY_READ_CLASS = Object.freeze({
|
|
348
|
+
SELECT: true, READ_INFO: true, SESSION: true,
|
|
349
|
+
TX: true, DCL: true, DDL: true, META: true,
|
|
350
|
+
});
|
|
351
|
+
|
|
93
352
|
// ---- OpenTelemetry database-client semantic conventions ----
|
|
94
353
|
//
|
|
95
354
|
// db.* span / metric attributes on the query / transaction / read emit
|
|
@@ -507,7 +766,7 @@ function init(opts) {
|
|
|
507
766
|
return async function () {
|
|
508
767
|
var client = await cn();
|
|
509
768
|
try {
|
|
510
|
-
await qn(client, "SET application_name TO " + quotedAppName, []);
|
|
769
|
+
await qn(client, "SET application_name TO " + quotedAppName, []); // allow:hand-rolled-sql — Postgres session SET (no table; not a b.sql verb)
|
|
511
770
|
} catch (_e) {
|
|
512
771
|
// Best-effort. Real Postgres always supports SET
|
|
513
772
|
// application_name; a driver that refuses it is a shim
|
|
@@ -738,6 +997,7 @@ function _servesClassification(b, cls) {
|
|
|
738
997
|
* backend?: string, // explicit backend name; bypasses classification + role pick
|
|
739
998
|
* classification?: string, // route to first backend whose classifications include this value
|
|
740
999
|
* includeSqlInAudit?: boolean, // emit SQL text in audit metadata (off by default — may carry literal PII)
|
|
1000
|
+
* rowResidencyTag?: string, // the row's residency region tag; required for a write (DML, CALL/EXECUTE/DO, COPY ... FROM, REPLACE, or a WITH/EXPLAIN-ANALYZE wrapping one) to a residency-tagged backend under a cross-border regulated posture (pass "global"/"unrestricted" for region-neutral rows)
|
|
741
1001
|
*
|
|
742
1002
|
* @example
|
|
743
1003
|
* var res = await b.externalDb.query(
|
|
@@ -754,6 +1014,14 @@ async function query(sql, params, opts) {
|
|
|
754
1014
|
var b = _pickBackend(opts);
|
|
755
1015
|
var role = dbRoleContext.getRole();
|
|
756
1016
|
|
|
1017
|
+
// Per-row residency write gate — refuses a cross-border write before
|
|
1018
|
+
// the statement reaches the wire (see _assertRowResidency).
|
|
1019
|
+
var _resRefusal = _assertRowResidency(sql, opts, b);
|
|
1020
|
+
if (_resRefusal) {
|
|
1021
|
+
_emit("db.residency.gate.rejected", "denied", _resRefusal.metadata, _resRefusal.code);
|
|
1022
|
+
throw _err(_resRefusal.code, _resRefusal.message, true);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
757
1025
|
var t0 = Date.now();
|
|
758
1026
|
try {
|
|
759
1027
|
var result = await retryHelper.withRetry(function () {
|
|
@@ -854,6 +1122,7 @@ async function query(sql, params, opts) {
|
|
|
854
1122
|
* statementTimeoutMs?: number, // SET LOCAL statement_timeout
|
|
855
1123
|
* idleInTransactionTimeoutMs?: number, // SET LOCAL idle_in_transaction_session_timeout
|
|
856
1124
|
* deadlockRetries?: number, // retries for 40P01 / 40001 (default 3)
|
|
1125
|
+
* rowResidencyTag?: string, // residency tag applied to every statement; a per-call tx.query(sql, params, { rowResidencyTag }) overrides it for that statement
|
|
857
1126
|
*
|
|
858
1127
|
* @example
|
|
859
1128
|
* var summary = await b.externalDb.transaction(async function (tx) {
|
|
@@ -907,10 +1176,34 @@ async function transaction(fn, opts) {
|
|
|
907
1176
|
}
|
|
908
1177
|
var maxRetries = (typeof opts.deadlockRetries === "number")
|
|
909
1178
|
? Math.floor(opts.deadlockRetries) : 3; // allow:numeric-opt-Infinity
|
|
1179
|
+
// Validate the transaction-level residency tag shape at entry (the
|
|
1180
|
+
// sessionGucs / deadlockRetries discipline) so an empty-string tag
|
|
1181
|
+
// fails before BEGIN rather than at the first statement.
|
|
1182
|
+
if (opts.rowResidencyTag !== undefined && opts.rowResidencyTag !== null &&
|
|
1183
|
+
(typeof opts.rowResidencyTag !== "string" || opts.rowResidencyTag.length === 0)) {
|
|
1184
|
+
throw _err("INVALID_OPT",
|
|
1185
|
+
"transaction: opts.rowResidencyTag must be a non-empty string when supplied", true);
|
|
1186
|
+
}
|
|
910
1187
|
return await b.breaker.wrap(async function () {
|
|
911
1188
|
var client = await b.pool.acquire();
|
|
912
1189
|
var txClient = {
|
|
913
|
-
|
|
1190
|
+
// Per-statement residency gate inside the transaction: a
|
|
1191
|
+
// transaction-level opts.rowResidencyTag applies to every
|
|
1192
|
+
// statement; an optional per-call third argument overrides it
|
|
1193
|
+
// for that statement. A refusal throws into the operator's tx
|
|
1194
|
+
// body, which rolls the transaction back — no partial commit of
|
|
1195
|
+
// a cross-border write.
|
|
1196
|
+
query: function (sql, params, perCallOpts) {
|
|
1197
|
+
var effOpts = (perCallOpts && perCallOpts.rowResidencyTag !== undefined)
|
|
1198
|
+
? perCallOpts
|
|
1199
|
+
: { rowResidencyTag: opts.rowResidencyTag };
|
|
1200
|
+
var refusal = _assertRowResidency(sql, effOpts, b);
|
|
1201
|
+
if (refusal) {
|
|
1202
|
+
_emit("db.residency.gate.rejected", "denied", refusal.metadata, refusal.code);
|
|
1203
|
+
throw _err(refusal.code, refusal.message, true);
|
|
1204
|
+
}
|
|
1205
|
+
return b.query(client, sql, params || []);
|
|
1206
|
+
},
|
|
914
1207
|
};
|
|
915
1208
|
var committed = false;
|
|
916
1209
|
var attempt = 0;
|
|
@@ -921,10 +1214,10 @@ async function transaction(fn, opts) {
|
|
|
921
1214
|
try {
|
|
922
1215
|
await b.beginTx(client);
|
|
923
1216
|
if (typeof stmtTimeoutMs === "number" && isFinite(stmtTimeoutMs) && stmtTimeoutMs > 0) {
|
|
924
|
-
await b.query(client, "SET LOCAL statement_timeout = " + Math.floor(stmtTimeoutMs), []);
|
|
1217
|
+
await b.query(client, "SET LOCAL statement_timeout = " + Math.floor(stmtTimeoutMs), []); // allow:hand-rolled-sql — Postgres session SET (no table; not a b.sql verb)
|
|
925
1218
|
}
|
|
926
1219
|
if (typeof idleTimeoutMs === "number" && isFinite(idleTimeoutMs) && idleTimeoutMs > 0) {
|
|
927
|
-
await b.query(client, "SET LOCAL idle_in_transaction_session_timeout = " + Math.floor(idleTimeoutMs), []);
|
|
1220
|
+
await b.query(client, "SET LOCAL idle_in_transaction_session_timeout = " + Math.floor(idleTimeoutMs), []); // allow:hand-rolled-sql — Postgres session SET (no table; not a b.sql verb)
|
|
928
1221
|
}
|
|
929
1222
|
for (var gi = 0; gi < prebuiltGucs.length; gi++) {
|
|
930
1223
|
await b.query(client, prebuiltGucs[gi], []);
|
|
@@ -1023,7 +1316,7 @@ async function _pingBackend(b) {
|
|
|
1023
1316
|
var client = await b.pool.acquire();
|
|
1024
1317
|
try {
|
|
1025
1318
|
if (b.ping) await b.ping(client);
|
|
1026
|
-
else await b.query(client, "SELECT 1", []);
|
|
1319
|
+
else await b.query(client, "SELECT 1", []); // allow:hand-rolled-sql — fixed connectivity ping (no table / b.sql verb)
|
|
1027
1320
|
b.pool.release(client);
|
|
1028
1321
|
return { ok: true, breakerState: b.breaker.getState(), pool: b.pool.stats() };
|
|
1029
1322
|
} catch (e) {
|
|
@@ -1162,7 +1455,7 @@ function _buildSessionGucsStatements(sessionGucs) {
|
|
|
1162
1455
|
"sessionGucs['" + name + "']: value must be a string, finite number, or boolean (got " +
|
|
1163
1456
|
typeof value + ")", true);
|
|
1164
1457
|
}
|
|
1165
|
-
out.push("SET LOCAL " + qName + " = " + literal);
|
|
1458
|
+
out.push("SET LOCAL " + qName + " = " + literal); // allow:hand-rolled-sql — Postgres session GUC SET (no table; not a b.sql verb)
|
|
1166
1459
|
}
|
|
1167
1460
|
return out;
|
|
1168
1461
|
}
|
|
@@ -1229,9 +1522,17 @@ var REPLICA_UNHEALTHY_COOLDOWN_MS = C.TIME.seconds(30);
|
|
|
1229
1522
|
// - "unrestricted" tag on either side: compatible (operator
|
|
1230
1523
|
// declared no constraint).
|
|
1231
1524
|
// - Different tags: compatible only when allowCrossBorder is true.
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1525
|
+
//
|
|
1526
|
+
// The regulated-posture set itself lives on b.compliance
|
|
1527
|
+
// (CROSS_BORDER_REGULATED_POSTURES) — one vocabulary shared with the
|
|
1528
|
+
// local db-query residency gate.
|
|
1529
|
+
function _crossBorderRegulated(posture) {
|
|
1530
|
+
if (posture === null || posture === undefined) return false;
|
|
1531
|
+
try {
|
|
1532
|
+
var compliance = require("./compliance"); // allow:inline-require — defensive against optional load
|
|
1533
|
+
return compliance.isCrossBorderRegulated(posture);
|
|
1534
|
+
} catch (_e) { return false; }
|
|
1535
|
+
}
|
|
1235
1536
|
|
|
1236
1537
|
function _residencyCompatible(primaryTag, replicaTag) {
|
|
1237
1538
|
if (!primaryTag || !replicaTag) return true;
|
|
@@ -1240,6 +1541,54 @@ function _residencyCompatible(primaryTag, replicaTag) {
|
|
|
1240
1541
|
return false;
|
|
1241
1542
|
}
|
|
1242
1543
|
|
|
1544
|
+
// True when `sql` carries a non-comment, non-whitespace statement after
|
|
1545
|
+
// the first top-level semicolon — the multi-statement shape that would
|
|
1546
|
+
// let a trailing DML hide behind a harmless leading keyword. A `;`
|
|
1547
|
+
// inside any opaque span (string literal, quoted identifier, dollar-
|
|
1548
|
+
// quoted body, comment) is data, not a separator: the main scan skips
|
|
1549
|
+
// every span via _skipOpaqueSpan, so a `;` inside `$$ ... ; ... $$` or
|
|
1550
|
+
// a doubled-quote run can't be mistaken for a top-level separator (and
|
|
1551
|
+
// can't desync the scanner into missing a real one). Single linear
|
|
1552
|
+
// pass, no backtracking regex (CWE-1333).
|
|
1553
|
+
function _hasTrailingStatement(sql) {
|
|
1554
|
+
if (typeof sql !== "string") return false;
|
|
1555
|
+
var n = sql.length;
|
|
1556
|
+
var i = 0;
|
|
1557
|
+
while (i < n) {
|
|
1558
|
+
var ch = sql.charAt(i);
|
|
1559
|
+
var skipped = _skipOpaqueSpan(sql, i);
|
|
1560
|
+
if (skipped === -1) return false; // unterminated span — no top-level content beyond it
|
|
1561
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
1562
|
+
if (ch !== ";") { i += 1; continue; }
|
|
1563
|
+
// First top-level `;` decides: if everything after it is only
|
|
1564
|
+
// whitespace and SQL comments there is no second statement (a
|
|
1565
|
+
// single statement may end with `;`); any other character is one.
|
|
1566
|
+
// Comments-only skip here (not _skipOpaqueSpan) — a string / ident
|
|
1567
|
+
// after the `;` IS trailing content, so it must count, not be
|
|
1568
|
+
// skipped as a span.
|
|
1569
|
+
var j = i + 1;
|
|
1570
|
+
while (j < n) {
|
|
1571
|
+
var c = sql.charAt(j);
|
|
1572
|
+
if (c === " " || c === "\t" || c === "\r" || c === "\n") { j += 1; continue; }
|
|
1573
|
+
if (c === "/" && sql.charAt(j + 1) === "*") {
|
|
1574
|
+
var end = sql.indexOf("*/", j + 2);
|
|
1575
|
+
if (end === -1) return false; // unterminated comment — no content
|
|
1576
|
+
j = end + 2;
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
if (c === "-" && sql.charAt(j + 1) === "-") {
|
|
1580
|
+
var nl = sql.indexOf("\n", j + 2);
|
|
1581
|
+
if (nl === -1) return false; // line comment to EOF — no content
|
|
1582
|
+
j = nl + 1;
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
return false;
|
|
1588
|
+
}
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1243
1592
|
function _activePosture() {
|
|
1244
1593
|
try {
|
|
1245
1594
|
var compliance = require("./compliance"); // allow:inline-require — defensive against optional load
|
|
@@ -1247,6 +1596,115 @@ function _activePosture() {
|
|
|
1247
1596
|
} catch (_e) { return null; }
|
|
1248
1597
|
}
|
|
1249
1598
|
|
|
1599
|
+
// Per-row residency write gate (GDPR Art 44-46 / PIPL Art 38 / DPDP
|
|
1600
|
+
// §16 cross-border transfer restrictions). External-db takes raw SQL,
|
|
1601
|
+
// not row objects, so the row's residency tag travels as
|
|
1602
|
+
// `opts.rowResidencyTag` — the operator computes it from app logic
|
|
1603
|
+
// (session / declared user region), never inferred from request
|
|
1604
|
+
// metadata. Under a cross-border regulated posture, DML to a
|
|
1605
|
+
// residency-tagged backend REQUIRES the tag and refuses a mismatch;
|
|
1606
|
+
// untagged backends, unregulated postures, and non-DML statements
|
|
1607
|
+
// pass (with an advisory audit when a tag was supplied anyway).
|
|
1608
|
+
// Returns null on pass or { code, message, metadata } — the caller
|
|
1609
|
+
// throws via _err so the refusal carries permanent=true.
|
|
1610
|
+
function _assertRowResidency(sql, opts, backend) {
|
|
1611
|
+
var tag = opts && opts.rowResidencyTag;
|
|
1612
|
+
if (tag !== undefined && tag !== null &&
|
|
1613
|
+
(typeof tag !== "string" || tag.length === 0)) {
|
|
1614
|
+
return {
|
|
1615
|
+
code: "INVALID_OPT",
|
|
1616
|
+
message: "rowResidencyTag must be a non-empty string when supplied",
|
|
1617
|
+
metadata: { backend: backend.name, statementClass: _classifyStatement(sql) },
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
var backendTag = backend.residencyTag || "unrestricted";
|
|
1621
|
+
var posture = _activePosture();
|
|
1622
|
+
var regulated = _crossBorderRegulated(posture);
|
|
1623
|
+
// The gate only enforces on the cross-border-regulated + residency-
|
|
1624
|
+
// tagged-backend path. Everywhere else (unregulated posture,
|
|
1625
|
+
// unrestricted backend — including the framework's own coordination
|
|
1626
|
+
// stores) statements pass untouched; multi-statement SQL stays the
|
|
1627
|
+
// operator's business there.
|
|
1628
|
+
if (regulated && backendTag !== "unrestricted") {
|
|
1629
|
+
// A trailing statement after a top-level `;` could hide a write
|
|
1630
|
+
// behind a harmless prefix — refuse multi-statement SQL on the
|
|
1631
|
+
// enforced path so a residency-bound write cannot ride a SELECT.
|
|
1632
|
+
if (_hasTrailingStatement(sql)) {
|
|
1633
|
+
return {
|
|
1634
|
+
code: "MULTI_STATEMENT_REFUSED",
|
|
1635
|
+
message: "multi-statement SQL is not supported on the residency-gated " +
|
|
1636
|
+
"write path; pass one statement per query()",
|
|
1637
|
+
metadata: { backend: backend.name, backendTag: backendTag, posture: posture,
|
|
1638
|
+
statementClass: _classifyStatement(sql), scope: "external" },
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
var cls = _classifyStatement(sql);
|
|
1642
|
+
// Fail-closed: a statement whose effective class can't be resolved
|
|
1643
|
+
// (an unparseable WITH / EXPLAIN prefix or pathological quoting)
|
|
1644
|
+
// could be hiding a write, so refuse it rather than wave it through
|
|
1645
|
+
// as a read.
|
|
1646
|
+
if (cls === "UNKNOWN") {
|
|
1647
|
+
return {
|
|
1648
|
+
code: "STATEMENT_UNRESOLVED_REFUSED",
|
|
1649
|
+
message: "could not resolve the effective statement class on the " +
|
|
1650
|
+
"residency-gated write path; pass one plain statement per " +
|
|
1651
|
+
"query() (an unparseable WITH/EXPLAIN prefix or quoting)",
|
|
1652
|
+
metadata: { backend: backend.name, backendTag: backendTag, posture: posture,
|
|
1653
|
+
statementClass: cls, scope: "external" },
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
// The gate enforces on writes, not just DML: ROUTINE (CALL /
|
|
1657
|
+
// EXECUTE / DO), a COPY ... FROM load, and an unmapped (OTHER)
|
|
1658
|
+
// verb all place rows and must carry a tag. Recognized pure reads
|
|
1659
|
+
// (and a COPY ... TO export) place none and pass untagged.
|
|
1660
|
+
var isWrite = !(_RESIDENCY_READ_CLASS[cls] === true ||
|
|
1661
|
+
(cls === "BULK" && !_copyLoadsRows(sql)));
|
|
1662
|
+
if (!isWrite) return null;
|
|
1663
|
+
if (!tag) {
|
|
1664
|
+
return {
|
|
1665
|
+
code: "RESIDENCY_GATE_REQUIRED",
|
|
1666
|
+
message: "write to backend '" + backend.name + "' (residencyTag='" +
|
|
1667
|
+
backendTag + "') under '" + posture + "' posture requires " +
|
|
1668
|
+
"opts.rowResidencyTag. Pass { rowResidencyTag: \"" + backendTag +
|
|
1669
|
+
"\" } for rows belonging to this region, or declare per-row " +
|
|
1670
|
+
"residency via b.cryptoField.declarePerRowResidency for local tables",
|
|
1671
|
+
metadata: { backend: backend.name, backendTag: backendTag,
|
|
1672
|
+
rowTag: null, posture: posture, statementClass: cls,
|
|
1673
|
+
scope: "external" },
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
if (tag !== "global" && tag !== "unrestricted" &&
|
|
1677
|
+
!_residencyCompatible(tag, backendTag)) {
|
|
1678
|
+
return {
|
|
1679
|
+
code: "RESIDENCY_TAG_MISMATCH",
|
|
1680
|
+
message: "row residencyTag '" + tag + "' is not compatible with backend '" +
|
|
1681
|
+
backend.name + "' residencyTag '" + backendTag + "' under '" + posture +
|
|
1682
|
+
"' posture (cross-border transfer refused)",
|
|
1683
|
+
metadata: { backend: backend.name, backendTag: backendTag,
|
|
1684
|
+
rowTag: tag, posture: posture, statementClass: cls,
|
|
1685
|
+
scope: "external" },
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
if (tag) {
|
|
1691
|
+
// Unregulated posture or untagged backend with a tag supplied on a
|
|
1692
|
+
// write — pass, but surface the advisory so operators staging a
|
|
1693
|
+
// posture flip can see what WOULD be evaluated.
|
|
1694
|
+
var advisoryCls = _classifyStatement(sql);
|
|
1695
|
+
var advisoryWrite = advisoryCls !== "UNKNOWN" &&
|
|
1696
|
+
!(_RESIDENCY_READ_CLASS[advisoryCls] === true ||
|
|
1697
|
+
(advisoryCls === "BULK" && !_copyLoadsRows(sql)));
|
|
1698
|
+
if (advisoryWrite) {
|
|
1699
|
+
_emit("db.residency.gate.advisory", "info", {
|
|
1700
|
+
backend: backend.name, backendTag: backendTag, rowTag: tag,
|
|
1701
|
+
posture: posture || null, statementClass: advisoryCls, scope: "external",
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1250
1708
|
function _buildReplicas(backendName, cfg) {
|
|
1251
1709
|
if (!cfg.replicas) return null;
|
|
1252
1710
|
if (!Array.isArray(cfg.replicas) || cfg.replicas.length === 0) {
|
|
@@ -1275,7 +1733,7 @@ function _buildReplicas(backendName, cfg) {
|
|
|
1275
1733
|
var replicaTag = r.residencyTag || "unrestricted";
|
|
1276
1734
|
var allowCrossBorder = r.allowCrossBorder === true;
|
|
1277
1735
|
if (!_residencyCompatible(primaryTag, replicaTag) && !allowCrossBorder) {
|
|
1278
|
-
var underPosture =
|
|
1736
|
+
var underPosture = _crossBorderRegulated(posture);
|
|
1279
1737
|
throw _err("RESIDENCY_MISMATCH",
|
|
1280
1738
|
"backend '" + backendName + "': replica[" + i +
|
|
1281
1739
|
"] residencyTag '" + replicaTag +
|
|
@@ -1286,7 +1744,12 @@ function _buildReplicas(backendName, cfg) {
|
|
|
1286
1744
|
"documented legal basis (SCCs / BCRs / adequacy decision) to suppress.", true);
|
|
1287
1745
|
}
|
|
1288
1746
|
if (!_residencyCompatible(primaryTag, replicaTag) && allowCrossBorder) {
|
|
1289
|
-
|
|
1747
|
+
// The action name MUST stay in the registered `db.` namespace and
|
|
1748
|
+
// lowercase — the audit validator refuses "externalDb.*" (the old
|
|
1749
|
+
// name silently dropped every cross-border-allowed event through
|
|
1750
|
+
// safeEmit, leaving no audit-chain record of the operator's
|
|
1751
|
+
// conscious opt-in). Mirrors the read-path event name below.
|
|
1752
|
+
_emit("db.residency.replica.cross_border_allowed", "warning",
|
|
1290
1753
|
{ backend: backendName, replicaIndex: i,
|
|
1291
1754
|
primaryTag: primaryTag, replicaTag: replicaTag,
|
|
1292
1755
|
legalBasis: r.legalBasis || null,
|
|
@@ -1344,6 +1807,52 @@ async function _readQuery(sql, params, opts) {
|
|
|
1344
1807
|
throw _err("ALL_REPLICAS_UNHEALTHY",
|
|
1345
1808
|
"backend '" + b.name + "': all replicas unhealthy and fallback disabled", true);
|
|
1346
1809
|
}
|
|
1810
|
+
// Replica residency check — when the caller identifies the row's
|
|
1811
|
+
// region (opts.rowResidencyTag) under a regulated posture, a read
|
|
1812
|
+
// routed to an incompatible replica is refused unless the replica
|
|
1813
|
+
// was explicitly configured allowCrossBorder (which is audited).
|
|
1814
|
+
var _readPosture = _activePosture();
|
|
1815
|
+
var _tagPresent = opts.rowResidencyTag && typeof opts.rowResidencyTag === "string";
|
|
1816
|
+
// Fail CLOSED when the row's region is not identified: a regulated read to a
|
|
1817
|
+
// residency-tagged replica without opts.rowResidencyTag would otherwise route
|
|
1818
|
+
// residency-restricted rows to an arbitrary-region replica with no check at
|
|
1819
|
+
// all (symmetric with the write gate's RESIDENCY_GATE_REQUIRED).
|
|
1820
|
+
if (!_tagPresent && _crossBorderRegulated(_readPosture) &&
|
|
1821
|
+
replica.residencyTag && !replica.allowCrossBorder) {
|
|
1822
|
+
_emit("db.residency.replica.tag_required", "denied", {
|
|
1823
|
+
backend: b.name, replicaIdx: replica.index,
|
|
1824
|
+
replicaTag: replica.residencyTag, posture: _readPosture,
|
|
1825
|
+
});
|
|
1826
|
+
throw _err("REPLICA_RESIDENCY_TAG_REQUIRED",
|
|
1827
|
+
"read routed to residency-tagged replica " + replica.index + " of backend '" +
|
|
1828
|
+
b.name + "' (residencyTag='" + replica.residencyTag + "') under '" + _readPosture +
|
|
1829
|
+
"' posture without opts.rowResidencyTag - identify the row's region or set " +
|
|
1830
|
+
"allowCrossBorder on the replica (audited)", true);
|
|
1831
|
+
}
|
|
1832
|
+
if (_tagPresent) {
|
|
1833
|
+
if (_crossBorderRegulated(_readPosture) &&
|
|
1834
|
+
opts.rowResidencyTag !== "global" && opts.rowResidencyTag !== "unrestricted" &&
|
|
1835
|
+
!_residencyCompatible(opts.rowResidencyTag, replica.residencyTag)) {
|
|
1836
|
+
if (replica.allowCrossBorder) {
|
|
1837
|
+
_emit("db.residency.replica.cross_border", "warning", {
|
|
1838
|
+
backend: b.name, replicaIdx: replica.index,
|
|
1839
|
+
rowTag: opts.rowResidencyTag, replicaTag: replica.residencyTag,
|
|
1840
|
+
posture: _readPosture,
|
|
1841
|
+
});
|
|
1842
|
+
} else {
|
|
1843
|
+
_emit("db.residency.replica.incompatible", "denied", {
|
|
1844
|
+
backend: b.name, replicaIdx: replica.index,
|
|
1845
|
+
rowTag: opts.rowResidencyTag, replicaTag: replica.residencyTag,
|
|
1846
|
+
posture: _readPosture,
|
|
1847
|
+
});
|
|
1848
|
+
throw _err("REPLICA_RESIDENCY_INCOMPATIBLE",
|
|
1849
|
+
"read for row residencyTag '" + opts.rowResidencyTag + "' routed to replica " +
|
|
1850
|
+
replica.index + " of backend '" + b.name + "' (residencyTag='" +
|
|
1851
|
+
replica.residencyTag + "') under '" + _readPosture +
|
|
1852
|
+
"' posture; set allowCrossBorder on the replica to permit (audited)", true);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1347
1856
|
var role = dbRoleContext.getRole();
|
|
1348
1857
|
var t0 = Date.now();
|
|
1349
1858
|
try {
|
|
@@ -1655,22 +2164,27 @@ function _connectAs(rawConnect, query, opts) {
|
|
|
1655
2164
|
|
|
1656
2165
|
// Pre-compute the SET statements once — every fresh client runs the
|
|
1657
2166
|
// same list, so building it per-connect would burn microbenchmarks.
|
|
2167
|
+
// Postgres session-config statements (SET ROLE / search_path /
|
|
2168
|
+
// application_name / statement_timeout / GUCs). These are session-state
|
|
2169
|
+
// commands, not table DML — b.sql has no SET verb, so they stay
|
|
2170
|
+
// hand-composed (identifiers double-quoted, string values single-quote
|
|
2171
|
+
// escaped, numerics rendered after a finite-check below).
|
|
1658
2172
|
var stmts = [];
|
|
1659
2173
|
if (opts.role) {
|
|
1660
|
-
stmts.push('SET ROLE "' + opts.role + '"');
|
|
2174
|
+
stmts.push('SET ROLE "' + opts.role + '"'); // allow:hand-rolled-sql — Postgres session SET (no table; not a b.sql verb)
|
|
1661
2175
|
}
|
|
1662
2176
|
if (pathSegments) {
|
|
1663
2177
|
var pathSql = pathSegments.map(function (s) { return '"' + s + '"'; }).join(", ");
|
|
1664
|
-
stmts.push("SET search_path TO " + pathSql);
|
|
2178
|
+
stmts.push("SET search_path TO " + pathSql); // allow:hand-rolled-sql — Postgres session SET (no table; not a b.sql verb)
|
|
1665
2179
|
}
|
|
1666
2180
|
if (opts.applicationName !== undefined) {
|
|
1667
2181
|
// Single-quoted string literal — SQL-standard escape doubles embedded
|
|
1668
2182
|
// single quotes.
|
|
1669
2183
|
var an = String(opts.applicationName).replace(/'/g, "''");
|
|
1670
|
-
stmts.push("SET application_name TO '" + an + "'");
|
|
2184
|
+
stmts.push("SET application_name TO '" + an + "'"); // allow:hand-rolled-sql — Postgres session SET (no table; not a b.sql verb)
|
|
1671
2185
|
}
|
|
1672
2186
|
if (opts.statementTimeoutMs !== undefined) {
|
|
1673
|
-
stmts.push("SET statement_timeout TO " + opts.statementTimeoutMs);
|
|
2187
|
+
stmts.push("SET statement_timeout TO " + opts.statementTimeoutMs); // allow:hand-rolled-sql — Postgres session SET (no table; not a b.sql verb)
|
|
1674
2188
|
}
|
|
1675
2189
|
if (opts.gucs) {
|
|
1676
2190
|
for (var gn in opts.gucs) {
|
|
@@ -1979,11 +2493,12 @@ async function assertRoleHardening(opts) {
|
|
|
1979
2493
|
var ignoreSystem = opts.ignoreSystem !== false; // default true
|
|
1980
2494
|
var rows;
|
|
1981
2495
|
try {
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2496
|
+
// pg_roles is a Postgres system catalog (this path is Postgres-only —
|
|
2497
|
+
// guarded above), so b.sql emits the bare unquoted catalog name with a
|
|
2498
|
+
// quoted projection. No params, no `?`, so no placeholder translation.
|
|
2499
|
+
var rolesBuilt = sql().select("pg_roles", { dialect: "postgres" })
|
|
2500
|
+
.columns(["rolname"]).orderBy("rolname", "asc").toSql();
|
|
2501
|
+
var res = await query(rolesBuilt.sql, rolesBuilt.params, { backend: backendName });
|
|
1987
2502
|
rows = (res && res.rows) || [];
|
|
1988
2503
|
} catch (e) {
|
|
1989
2504
|
audit().safeEmit({
|