@blamejs/blamejs-shop 0.4.30 → 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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/checkout.js +8 -0
- package/lib/order.js +71 -11
- 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
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live Postgres proof that the b.sql data-layer migration of the five
|
|
4
|
+
* privacy/identity modules emits SQL that actually RUNS against a real
|
|
5
|
+
* Postgres server — not just sqlite host smoke. These modules build their
|
|
6
|
+
* statements with b.sql and either dispatch through b.clusterStorage (the
|
|
7
|
+
* external-db path — api-key registry, consent fenced tip) or run the
|
|
8
|
+
* same b.sql builders at { dialect: "postgres" } against the operator's
|
|
9
|
+
* external DB. This test drives BOTH shapes end-to-end on the docker
|
|
10
|
+
* Postgres container.
|
|
11
|
+
*
|
|
12
|
+
* - api-key: issue / verify (lastUsedAt touch) / rotate (graceful +
|
|
13
|
+
* hard cutover) / revoke / whereGroup-OR purge — driven
|
|
14
|
+
* through the REAL b.apiKey.create() registry in cluster
|
|
15
|
+
* mode so the module's own clusterStorage dispatch + the
|
|
16
|
+
* BIGINT->JS-number coercion run against live Postgres.
|
|
17
|
+
* - consent: the fenced tip upsert (_upsertConsentTip shape) — INSERT
|
|
18
|
+
* ... ON CONFLICT(scope) DO UPDATE ... WHERE
|
|
19
|
+
* _blamejs_consent_tip."fencingToken" <= EXCLUDED... RETURNING.
|
|
20
|
+
* The fence MUST reject a lower fencing token (0 rows) and
|
|
21
|
+
* accept a higher one (1 row) on the real planner.
|
|
22
|
+
* - legal-hold: place INSERT / release DELETE / whereLike("action",
|
|
23
|
+
* "legalhold.", "prefix") history prefix-match with ESCAPE.
|
|
24
|
+
* - subject: the INSERT-OR-REPLACE upsert (_markErased shape) + the
|
|
25
|
+
* restrict INSERT/DELETE presence pattern.
|
|
26
|
+
* - retention: hard delete / soft-delete UPDATE / erase NULL-set /
|
|
27
|
+
* cascade DELETE / the whereGroup-OR candidate WHERE.
|
|
28
|
+
*
|
|
29
|
+
* The driver is a persistent docker-exec psql shim faithful to a real
|
|
30
|
+
* node-postgres driver's coercions (BIGINT/int8 -> JS string, so the
|
|
31
|
+
* framework's clusterStorage.coerceRows must turn it back into a number;
|
|
32
|
+
* a real NULL distinguished from empty string). SQL travels on stdin,
|
|
33
|
+
* never argv (no shell parse of SQL). Tables are namespaced + DROP/
|
|
34
|
+
* recreated in setup/teardown so concurrent integration tests don't
|
|
35
|
+
* collide.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
var spawn = require("node:child_process").spawn;
|
|
39
|
+
var execFileSync = require("node:child_process").execFileSync;
|
|
40
|
+
var fs = require("node:fs");
|
|
41
|
+
var os = require("node:os");
|
|
42
|
+
var path = require("node:path");
|
|
43
|
+
var helpers = require("../helpers");
|
|
44
|
+
var check = helpers.check;
|
|
45
|
+
var services = require("../helpers/services");
|
|
46
|
+
var cryptoField = require("../../lib/crypto-field");
|
|
47
|
+
var b = require("../../");
|
|
48
|
+
|
|
49
|
+
var CONTAINER = "blamejs-test-postgres";
|
|
50
|
+
var NULL_SENTINEL = "__BJNULL__";
|
|
51
|
+
var PSQL_ARGS = "psql -U blamejs -d blamejs_test -A " +
|
|
52
|
+
"-v ON_ERROR_STOP=0 -P null=__BJNULL__ 2>&1";
|
|
53
|
+
|
|
54
|
+
// ---- one-shot psql (setup / teardown / out-of-band assertions) ----
|
|
55
|
+
function _psql(sql) {
|
|
56
|
+
var prelude = "\\pset fieldsep '\\t'\n";
|
|
57
|
+
var out = execFileSync(
|
|
58
|
+
"docker",
|
|
59
|
+
["exec", "-i", CONTAINER, "sh", "-c",
|
|
60
|
+
"psql -U blamejs -d blamejs_test -qtA -P null=__BJNULL__ 2>&1"],
|
|
61
|
+
{ input: prelude + sql + "\n", stdio: ["pipe", "pipe", "pipe"] }
|
|
62
|
+
).toString("utf8");
|
|
63
|
+
if (/^ERROR:/m.test(out)) {
|
|
64
|
+
throw new Error("psql setup failed for [" + sql + "]:\n" + out);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- persistent-session docker-exec psql driver (faithful to pg) ----
|
|
70
|
+
var _seq = 0;
|
|
71
|
+
function _makeDockerPgDriver() {
|
|
72
|
+
return {
|
|
73
|
+
connect: function () {
|
|
74
|
+
return new Promise(function (resolve, reject) {
|
|
75
|
+
var child = spawn(
|
|
76
|
+
"docker",
|
|
77
|
+
["exec", "-i", CONTAINER, "sh", "-c",
|
|
78
|
+
PSQL_ARGS + " ; echo __BLAMEJS_PSQL_EXIT__"],
|
|
79
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
80
|
+
);
|
|
81
|
+
var client = { child: child, buf: "", pending: null, closed: false };
|
|
82
|
+
child.on("error", function (e) {
|
|
83
|
+
if (client.pending) { var p = client.pending; client.pending = null; p.reject(e); }
|
|
84
|
+
});
|
|
85
|
+
child.on("close", function () {
|
|
86
|
+
client.closed = true;
|
|
87
|
+
if (client.pending) {
|
|
88
|
+
var p = client.pending; client.pending = null;
|
|
89
|
+
p.reject(new Error("psql session closed mid-statement"));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
child.stdout.on("data", function (chunk) {
|
|
93
|
+
client.buf += chunk.toString("utf8");
|
|
94
|
+
_drain(client);
|
|
95
|
+
});
|
|
96
|
+
var primeSentinel = "__BJ_PRIME__";
|
|
97
|
+
client.pending = {
|
|
98
|
+
sentinel: primeSentinel,
|
|
99
|
+
resolve: function () { resolve(client); },
|
|
100
|
+
reject: reject,
|
|
101
|
+
};
|
|
102
|
+
client.child.stdin.write(
|
|
103
|
+
"\\pset fieldsep '\\t'\n\\pset footer off\n\\set VERBOSITY verbose\n" +
|
|
104
|
+
"\\echo " + primeSentinel + "\n");
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
query: function (client, sql, params) {
|
|
109
|
+
params = params || [];
|
|
110
|
+
var bound = _bindParams(sql, params);
|
|
111
|
+
var sentinel = "__BJ_EOR_" + (++_seq) + "__";
|
|
112
|
+
return new Promise(function (resolve, reject) {
|
|
113
|
+
if (client.closed) { reject(new Error("psql session is closed")); return; }
|
|
114
|
+
client.pending = { sentinel: sentinel, resolve: resolve, reject: reject };
|
|
115
|
+
client.child.stdin.write(bound + "\n;\n\\echo " + sentinel + "\n");
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
close: function (client) {
|
|
120
|
+
return new Promise(function (resolve) {
|
|
121
|
+
if (client.closed) { resolve(); return; }
|
|
122
|
+
try { client.child.stdin.end("\\q\n"); } catch (_e) { /* best effort */ }
|
|
123
|
+
var done = false;
|
|
124
|
+
client.child.on("close", function () { if (!done) { done = true; resolve(); } });
|
|
125
|
+
setTimeout(function () {
|
|
126
|
+
if (done) return;
|
|
127
|
+
done = true;
|
|
128
|
+
try { client.child.kill("SIGKILL"); } catch (_e) {}
|
|
129
|
+
resolve();
|
|
130
|
+
}, 2000);
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
dialect: "postgres",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function _drain(client) {
|
|
139
|
+
if (!client.pending) return;
|
|
140
|
+
var sentinel = client.pending.sentinel;
|
|
141
|
+
var marker = "\n" + sentinel + "\n";
|
|
142
|
+
var idx = client.buf.indexOf(marker);
|
|
143
|
+
var startAtZero = client.buf.indexOf(sentinel + "\n") === 0;
|
|
144
|
+
var block;
|
|
145
|
+
if (idx !== -1) {
|
|
146
|
+
block = client.buf.slice(0, idx);
|
|
147
|
+
client.buf = client.buf.slice(idx + marker.length);
|
|
148
|
+
} else if (startAtZero) {
|
|
149
|
+
block = "";
|
|
150
|
+
client.buf = client.buf.slice((sentinel + "\n").length);
|
|
151
|
+
} else {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
var p = client.pending;
|
|
155
|
+
client.pending = null;
|
|
156
|
+
var parsed;
|
|
157
|
+
try { parsed = _parseBlock(block); }
|
|
158
|
+
catch (e) { return p.reject(e); }
|
|
159
|
+
if (parsed.error) return p.reject(parsed.error);
|
|
160
|
+
p.resolve({ rows: parsed.rows, rowCount: parsed.rowCount });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Inline params: numbers raw, null -> NULL, everything else single-quote
|
|
164
|
+
// escaped. Every test value is operator-controlled.
|
|
165
|
+
function _bindParams(sql, params) {
|
|
166
|
+
return sql.replace(/\$(\d+)/g, function (_m, n) {
|
|
167
|
+
var i = Number(n) - 1;
|
|
168
|
+
if (i < 0 || i >= params.length) {
|
|
169
|
+
throw new Error("placeholder $" + n + " has no matching param");
|
|
170
|
+
}
|
|
171
|
+
var v = params[i];
|
|
172
|
+
if (v === null || v === undefined) return "NULL";
|
|
173
|
+
if (typeof v === "number") return String(v);
|
|
174
|
+
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
|
175
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var _CMD_TAG_RE = /^(INSERT|UPDATE|DELETE|MERGE|SELECT|COPY|MOVE)\b(?:\s+\d+)*\s*$/;
|
|
180
|
+
var _CTRL_TAG_RE = /^(BEGIN|COMMIT|ROLLBACK|SET|RESET|SAVEPOINT|RELEASE|START|CREATE|DROP|ALTER|GRANT|REVOKE|TRUNCATE|COMMENT|DO|CALL|VACUUM|ANALYZE|EXPLAIN|TABLE|SHOW|DISCARD|REINDEX)\b/;
|
|
181
|
+
|
|
182
|
+
function _parseBlock(block) {
|
|
183
|
+
var lines = block.split(/\r?\n/);
|
|
184
|
+
while (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
185
|
+
|
|
186
|
+
for (var i = 0; i < lines.length; i++) {
|
|
187
|
+
var em = /^ERROR:\s+([0-9A-Za-z]{5}):\s*(.*)$/.exec(lines[i]);
|
|
188
|
+
if (em) {
|
|
189
|
+
var err = new Error("Postgres " + em[1] + ": " + em[2]);
|
|
190
|
+
err.code = em[1];
|
|
191
|
+
return { error: err };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
var affected = null;
|
|
196
|
+
var dataLines = [];
|
|
197
|
+
for (var j = 0; j < lines.length; j++) {
|
|
198
|
+
var ln = lines[j];
|
|
199
|
+
if (/^(NOTICE|WARNING|DETAIL|HINT|LINE|LOCATION|CONTEXT|STATEMENT):/.test(ln)) continue;
|
|
200
|
+
var tm = _CMD_TAG_RE.exec(ln);
|
|
201
|
+
if (tm) {
|
|
202
|
+
var nums = ln.trim().split(/\s+/).slice(1).map(Number);
|
|
203
|
+
if (nums.length) affected = nums[nums.length - 1];
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (_CTRL_TAG_RE.test(ln) && ln.indexOf("\t") === -1) continue;
|
|
207
|
+
dataLines.push(ln);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var rows = [];
|
|
211
|
+
if (dataLines.length >= 1) {
|
|
212
|
+
var headers = dataLines[0].split("\t");
|
|
213
|
+
for (var k = 1; k < dataLines.length; k++) {
|
|
214
|
+
var cells = dataLines[k].split("\t");
|
|
215
|
+
var row = {};
|
|
216
|
+
for (var c = 0; c < headers.length; c++) {
|
|
217
|
+
var cell = cells[c];
|
|
218
|
+
if (cell === NULL_SENTINEL || cell === undefined) { row[headers[c]] = null; continue; }
|
|
219
|
+
// STRING by default — matches node-postgres int8-as-string default.
|
|
220
|
+
// The framework's clusterStorage.coerceRows turns the int columns
|
|
221
|
+
// back into JS numbers; leaving them strings here is the real-pg shape.
|
|
222
|
+
row[headers[c]] = cell;
|
|
223
|
+
}
|
|
224
|
+
rows.push(row);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
var rowCount = (affected !== null) ? affected : rows.length;
|
|
228
|
+
return { rows: rows, rowCount: rowCount, error: null };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Reset the framework-state singletons each backend run touches.
|
|
232
|
+
function _resetState() {
|
|
233
|
+
try { b.cluster._resetForTest(); } catch (_e) {}
|
|
234
|
+
try { b.consent._resetForTest(); } catch (_e) {}
|
|
235
|
+
try { b.externalDb._resetForTest(); } catch (_e) {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function run() {
|
|
239
|
+
var pg = await services.requireService("postgres");
|
|
240
|
+
if (!pg.ok) throw new Error("postgres unreachable: " + pg.reason);
|
|
241
|
+
|
|
242
|
+
// Namespaced tables — distinct names + the framework's own prefixed
|
|
243
|
+
// names this test owns. DROP first so a prior crash can't poison us.
|
|
244
|
+
var DROP_ALL = [
|
|
245
|
+
"_blamejs_api_keys", "_blamejs_consent_tip",
|
|
246
|
+
"_blamejs_leader", "_blamejs_cluster_state",
|
|
247
|
+
"dl_pg_hold", "dl_pg_erasures", "dl_pg_restrictions",
|
|
248
|
+
"dl_pg_audit", "dl_pg_orders", "dl_pg_order_lines",
|
|
249
|
+
].map(function (t) { return "DROP TABLE IF EXISTS " + t + " CASCADE;"; }).join("\n");
|
|
250
|
+
_psql(DROP_ALL);
|
|
251
|
+
|
|
252
|
+
_resetState();
|
|
253
|
+
|
|
254
|
+
// Init the framework vault (plaintext mode for the test) + register the
|
|
255
|
+
// api-key registry's cryptoField schema so sealRow seals ownerId/scopes/
|
|
256
|
+
// metadata and derives ownerIdHash — exactly what db.js's FRAMEWORK_SCHEMA
|
|
257
|
+
// wires for the local path. Without it the issued row's NOT-NULL
|
|
258
|
+
// ownerIdHash is null and the live INSERT rejects.
|
|
259
|
+
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-dl-pg-"));
|
|
260
|
+
if (typeof b.vault._resetForTest === "function") b.vault._resetForTest();
|
|
261
|
+
await b.vault.init({ dataDir: dataDir, mode: "plaintext" });
|
|
262
|
+
cryptoField.registerTable("_blamejs_api_keys", {
|
|
263
|
+
sealedFields: ["ownerId", "scopes", "metadata"],
|
|
264
|
+
derivedHashes: { ownerIdHash: { from: "ownerId" } },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
var driver = _makeDockerPgDriver();
|
|
268
|
+
b.externalDb.init({
|
|
269
|
+
backends: {
|
|
270
|
+
ops: {
|
|
271
|
+
connect: driver.connect, query: driver.query, close: driver.close,
|
|
272
|
+
dialect: "postgres",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Direct backend query helper. b.sql emits `?` placeholders; the external-db
|
|
278
|
+
// query path does NOT translate them (that is clusterStorage's job on the
|
|
279
|
+
// cluster dispatch). For statements we run straight against the backend
|
|
280
|
+
// (the local-builder shapes — legal-hold / subject / retention — and the
|
|
281
|
+
// raw DDL), placeholderize `?`->`$N` for Postgres exactly as clusterStorage
|
|
282
|
+
// would, so the live driver binds correctly.
|
|
283
|
+
var clusterStorage = require("../../lib/cluster-storage");
|
|
284
|
+
function _q(built) {
|
|
285
|
+
return b.externalDb.query(
|
|
286
|
+
clusterStorage.placeholderize(built.sql, "postgres"), built.params, { backend: "ops" });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ====================================================================
|
|
290
|
+
// api-key — drive the REAL b.apiKey.create() registry in CLUSTER mode
|
|
291
|
+
// so issue/verify/rotate/revoke/purge dispatch through the module's
|
|
292
|
+
// clusterStorage path to live Postgres. Coercion (BIGINT->number) is
|
|
293
|
+
// exercised by verify()/getById reading createdAt/expiresAt back.
|
|
294
|
+
// ====================================================================
|
|
295
|
+
// Create the _blamejs_api_keys table on the real server via b.sql DDL
|
|
296
|
+
// at the postgres dialect (the framework ships this schema for cluster
|
|
297
|
+
// mode; ensureSchema would also create it, but building it directly
|
|
298
|
+
// proves the b.sql createTable shape runs on Postgres too).
|
|
299
|
+
var apiKeysDdl = b.sql.createTable("_blamejs_api_keys", [
|
|
300
|
+
{ name: "id", type: "text", primaryKey: true },
|
|
301
|
+
{ name: "namespace", type: "text", notNull: true },
|
|
302
|
+
{ name: "ownerId", type: "text", notNull: true },
|
|
303
|
+
{ name: "ownerIdHash", type: "text", notNull: true },
|
|
304
|
+
{ name: "secretHash", type: "text", notNull: true },
|
|
305
|
+
{ name: "secondarySecretHash", type: "text" },
|
|
306
|
+
{ name: "secondaryExpiresAt", type: "int" },
|
|
307
|
+
{ name: "scopes", type: "text" },
|
|
308
|
+
{ name: "metadata", type: "text" },
|
|
309
|
+
{ name: "createdAt", type: "int", notNull: true },
|
|
310
|
+
{ name: "expiresAt", type: "int" },
|
|
311
|
+
{ name: "revokedAt", type: "int" },
|
|
312
|
+
{ name: "lastUsedAt", type: "int" },
|
|
313
|
+
{ name: "prefix", type: "text", notNull: true },
|
|
314
|
+
], { dialect: "postgres", quoteName: true });
|
|
315
|
+
// Run DDL through the external-db query path (DDL goes direct on the backend).
|
|
316
|
+
await _q(apiKeysDdl);
|
|
317
|
+
check("api-key: b.sql createTable DDL ran on real Postgres", true);
|
|
318
|
+
|
|
319
|
+
// Cluster mode so b.apiKey.create() dispatches to external-db "ops".
|
|
320
|
+
await b.cluster.init({
|
|
321
|
+
nodeId: "dl-pg-node", role: "leader",
|
|
322
|
+
externalDbBackend: "ops", dialect: "postgres",
|
|
323
|
+
});
|
|
324
|
+
check("api-key: cluster.init leader on real Postgres", b.cluster.isLeader() === true);
|
|
325
|
+
|
|
326
|
+
var keys = b.apiKey.create({ namespace: "live", prefix: "bk" });
|
|
327
|
+
|
|
328
|
+
var nowMs = Date.now();
|
|
329
|
+
var issued = await keys.issue({
|
|
330
|
+
ownerId: "owner-1",
|
|
331
|
+
scopes: ["read:x", "write:y"],
|
|
332
|
+
metadata: { name: "device A" },
|
|
333
|
+
expiresAt: nowMs + b.constants.TIME.days(30),
|
|
334
|
+
});
|
|
335
|
+
check("api-key: issue returns a composed key", typeof issued.key === "string" && issued.key.indexOf("bk_live_") === 0);
|
|
336
|
+
var idHex = issued.id;
|
|
337
|
+
|
|
338
|
+
// Row physically present on the server.
|
|
339
|
+
var apiRowCount = _psql("SELECT count(*) AS n FROM _blamejs_api_keys WHERE namespace = 'live';");
|
|
340
|
+
check("api-key: issued row is physically in _blamejs_api_keys on Postgres", /\b1\b/.test(apiRowCount.trim()));
|
|
341
|
+
|
|
342
|
+
// verify() — drives SELECT + the trackLastUsedAt UPDATE; the returned
|
|
343
|
+
// record's numeric columns must come back as JS numbers (coercion).
|
|
344
|
+
var rec = await keys.verify(issued.key);
|
|
345
|
+
check("api-key: verify() succeeds against live Postgres", rec !== null && rec.id === idHex);
|
|
346
|
+
// The BIGINT round-trip: createdAt/expiresAt were written as int columns;
|
|
347
|
+
// node-postgres returns them as decimal STRINGS, and the framework's
|
|
348
|
+
// coerceRows + _scrubRecord must hand back the exact JS numbers issue()
|
|
349
|
+
// wrote. Assert against issue()'s own returned values (the registry's
|
|
350
|
+
// internal clock, not the test's nowMs which can differ by a few ms).
|
|
351
|
+
check("api-key: verify() coerces createdAt BIGINT -> the exact JS number issued",
|
|
352
|
+
typeof rec.createdAt === "number" && rec.createdAt === issued.createdAt);
|
|
353
|
+
check("api-key: verify() coerces expiresAt BIGINT -> the exact JS number issued",
|
|
354
|
+
typeof rec.expiresAt === "number" && rec.expiresAt === issued.expiresAt &&
|
|
355
|
+
rec.expiresAt === nowMs + b.constants.TIME.days(30));
|
|
356
|
+
|
|
357
|
+
// lastUsedAt UPDATE took effect on the server (was NULL at issue).
|
|
358
|
+
var luRow = _psql('SELECT "lastUsedAt" FROM _blamejs_api_keys WHERE namespace = \'live\';');
|
|
359
|
+
check("api-key: trackLastUsedAt UPDATE persisted a non-null lastUsedAt",
|
|
360
|
+
!new RegExp("\\b" + NULL_SENTINEL + "\\b").test(luRow.trim()) && /\d{6,}/.test(luRow.trim()));
|
|
361
|
+
|
|
362
|
+
// rotate() graceful — moves secret into the secondary slot with a TTL.
|
|
363
|
+
var rotated = await keys.rotate(idHex, { graceful: true });
|
|
364
|
+
check("api-key: graceful rotate returns a new key + secondaryExpiresAt",
|
|
365
|
+
typeof rotated.key === "string" && typeof rotated.secondaryExpiresAt === "number");
|
|
366
|
+
// The OLD key still verifies via the secondary slot.
|
|
367
|
+
var oldStillWorks = await keys.verify(issued.key);
|
|
368
|
+
check("api-key: old secret verifies via the secondary slot after graceful rotate",
|
|
369
|
+
oldStillWorks !== null && oldStillWorks.usedSecondary === true);
|
|
370
|
+
// The NEW key verifies via the primary slot.
|
|
371
|
+
var newWorks = await keys.verify(rotated.key);
|
|
372
|
+
check("api-key: new secret verifies via the primary slot",
|
|
373
|
+
newWorks !== null && newWorks.usedSecondary === false);
|
|
374
|
+
// secondaryExpiresAt physically set on the server (int column).
|
|
375
|
+
var secRow = _psql('SELECT "secondaryExpiresAt" FROM _blamejs_api_keys WHERE namespace = \'live\';');
|
|
376
|
+
check("api-key: graceful rotate persisted secondaryExpiresAt on Postgres",
|
|
377
|
+
/\d{6,}/.test(secRow.trim()));
|
|
378
|
+
|
|
379
|
+
// rotate() hard cutover — clears the secondary slot; the old key dies.
|
|
380
|
+
var cutover = await keys.rotate(idHex, {});
|
|
381
|
+
check("api-key: hard-cutover rotate returns a key with no grace",
|
|
382
|
+
cutover.secondaryExpiresAt === null);
|
|
383
|
+
var oldDead = await keys.verify(issued.key);
|
|
384
|
+
check("api-key: after hard cutover the original secret no longer verifies", oldDead === null);
|
|
385
|
+
var cutoverWorks = await keys.verify(cutover.key);
|
|
386
|
+
check("api-key: the cutover key verifies via primary", cutoverWorks !== null);
|
|
387
|
+
var secCleared = _psql('SELECT "secondarySecretHash" FROM _blamejs_api_keys WHERE namespace = \'live\';');
|
|
388
|
+
check("api-key: hard cutover NULLed secondarySecretHash on Postgres",
|
|
389
|
+
new RegExp("\\b" + NULL_SENTINEL + "\\b").test(secCleared.trim()));
|
|
390
|
+
|
|
391
|
+
// Issue a few more keys for the whereGroup-OR purge predicate. Expired
|
|
392
|
+
// + revoked keys past the threshold should purge; a fresh key stays.
|
|
393
|
+
var oldExpired = await keys.issue({ ownerId: "owner-2", expiresAt: 1 }); // long expired
|
|
394
|
+
var freshKey = await keys.issue({ ownerId: "owner-3", expiresAt: nowMs + b.constants.TIME.days(365) });
|
|
395
|
+
var toRevoke = await keys.issue({ ownerId: "owner-4" });
|
|
396
|
+
await keys.revoke(toRevoke.id);
|
|
397
|
+
// Backdate the revoked key's revokedAt below the purge threshold so the
|
|
398
|
+
// OR-group predicate selects it. purgeAfterMs default is 90d. ownerId is
|
|
399
|
+
// a SEALED column, so key the UPDATE on the non-sealed composite `id`
|
|
400
|
+
// ("<namespace>:<idHex>"), not the plaintext ownerId (which never matches
|
|
401
|
+
// the sealed blob on disk).
|
|
402
|
+
_psql('UPDATE _blamejs_api_keys SET "revokedAt" = 1 WHERE "id" = \'live:' + toRevoke.id + '\';');
|
|
403
|
+
|
|
404
|
+
var beforePurge = Number(_psql("SELECT count(*) AS n FROM _blamejs_api_keys;").trim());
|
|
405
|
+
var purged = await keys.purgeExpired();
|
|
406
|
+
// The OR predicate matches owner-2 (expiresAt=1 < threshold) AND owner-4
|
|
407
|
+
// (revokedAt=1 < threshold) — exactly 2 rows.
|
|
408
|
+
check("api-key: whereGroup-OR purge removed the expired + old-revoked keys on Postgres",
|
|
409
|
+
purged >= 2);
|
|
410
|
+
var afterPurge = Number(_psql("SELECT count(*) AS n FROM _blamejs_api_keys;").trim());
|
|
411
|
+
check("api-key: purge physically deleted rows (count dropped)", afterPurge < beforePurge);
|
|
412
|
+
// The fresh, non-expired, non-revoked key must survive the OR-predicate.
|
|
413
|
+
var freshSurvives = await keys.verify(freshKey.key);
|
|
414
|
+
check("api-key: the fresh non-expired key survived the purge predicate", freshSurvives !== null);
|
|
415
|
+
void oldExpired;
|
|
416
|
+
|
|
417
|
+
// listForOwner — exercises the OR group (expiresAt IS NULL OR >= now) on
|
|
418
|
+
// the live planner.
|
|
419
|
+
var owned = await keys.listForOwner("owner-3");
|
|
420
|
+
check("api-key: listForOwner returns the fresh key for owner-3 on Postgres",
|
|
421
|
+
owned.length === 1 && owned[0].id === freshKey.id);
|
|
422
|
+
|
|
423
|
+
// ====================================================================
|
|
424
|
+
// consent — the fenced tip upsert. The fence is the security-critical
|
|
425
|
+
// shape: a lower fencingToken must be REJECTED (RETURNING 0 rows), a
|
|
426
|
+
// higher one ACCEPTED (1 row). Run the EXACT _upsertConsentTip SQL
|
|
427
|
+
// through the external-db path so the real Postgres planner evaluates
|
|
428
|
+
// the WHERE _blamejs_consent_tip."fencingToken" <= EXCLUDED... fence.
|
|
429
|
+
// ====================================================================
|
|
430
|
+
var consentTipDdl = b.sql.createTable("_blamejs_consent_tip", [
|
|
431
|
+
{ name: "scope", type: "text", primaryKey: true },
|
|
432
|
+
{ name: "atMonotonicCounter", type: "int", notNull: true },
|
|
433
|
+
{ name: "rowHash", type: "text" },
|
|
434
|
+
{ name: "signedAt", type: "text" },
|
|
435
|
+
{ name: "fencingToken", type: "int", notNull: true },
|
|
436
|
+
], { dialect: "postgres", quoteName: true });
|
|
437
|
+
await _q(consentTipDdl);
|
|
438
|
+
|
|
439
|
+
var safeSql = require("../../lib/safe-sql");
|
|
440
|
+
function _tipUpsertSql(counter, rowHash, signedAt, fencingToken) {
|
|
441
|
+
var tipFence = "_blamejs_consent_tip." + safeSql.quoteIdentifier("fencingToken") +
|
|
442
|
+
" <= EXCLUDED." + safeSql.quoteIdentifier("fencingToken");
|
|
443
|
+
return b.sql.upsert("_blamejs_consent_tip", { dialect: "postgres" })
|
|
444
|
+
.columns(["scope", "atMonotonicCounter", "rowHash", "signedAt", "fencingToken"])
|
|
445
|
+
.values({ scope: "consent", atMonotonicCounter: counter, rowHash: rowHash, signedAt: signedAt, fencingToken: fencingToken })
|
|
446
|
+
.onConflict(["scope"])
|
|
447
|
+
.doUpdateFromExcluded(["atMonotonicCounter", "rowHash", "signedAt", "fencingToken"])
|
|
448
|
+
.conflictWhere(tipFence, [])
|
|
449
|
+
.returning(["fencingToken"])
|
|
450
|
+
.toSql();
|
|
451
|
+
}
|
|
452
|
+
async function _runTip(counter, rowHash, signedAt, fencingToken) {
|
|
453
|
+
var built = _tipUpsertSql(counter, rowHash, signedAt, fencingToken);
|
|
454
|
+
return await _q(built);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// First insert at token 1 — RETURNING yields the row (the initial INSERT).
|
|
458
|
+
var t1 = await _runTip(1, "hash-1", "1700000000001", 1);
|
|
459
|
+
check("consent-tip: initial upsert RETURNING produced 1 row on Postgres", t1.rows.length === 1);
|
|
460
|
+
|
|
461
|
+
// Higher token (2) — fence passes, tip advances, RETURNING 1 row.
|
|
462
|
+
var t2 = await _runTip(2, "hash-2", "1700000000002", 2);
|
|
463
|
+
check("consent-tip: higher fencingToken accepted (RETURNING 1 row)", t2.rows.length === 1);
|
|
464
|
+
var tipState = _psql('SELECT "atMonotonicCounter", "fencingToken" FROM _blamejs_consent_tip WHERE scope = \'consent\';');
|
|
465
|
+
check("consent-tip: tip advanced to counter 2 / token 2 on the server",
|
|
466
|
+
/\b2\t2\b/.test(tipState.trim()) || /2\s+2/.test(tipState.trim()));
|
|
467
|
+
|
|
468
|
+
// LOWER token (1) — the FENCE must reject: WHERE stored <= EXCLUDED is
|
|
469
|
+
// false (2 <= 1 false) -> DO UPDATE skipped -> RETURNING 0 rows. This is
|
|
470
|
+
// the partitioned-old-leader defense; consent.js throws FENCED_OUT on 0.
|
|
471
|
+
var t3 = await _runTip(99, "hash-evil", "1700000000099", 1);
|
|
472
|
+
check("consent-tip: FENCE rejects a lower fencingToken (RETURNING 0 rows) on Postgres",
|
|
473
|
+
t3.rows.length === 0);
|
|
474
|
+
var tipUnchanged = _psql('SELECT "atMonotonicCounter", "rowHash" FROM _blamejs_consent_tip WHERE scope = \'consent\';');
|
|
475
|
+
check("consent-tip: a fenced-out write did NOT overwrite the tip (still hash-2/counter-2)",
|
|
476
|
+
/hash-2/.test(tipUnchanged.trim()) && !/hash-evil/.test(tipUnchanged.trim()));
|
|
477
|
+
|
|
478
|
+
// Equal token (2) — fence is <= so an equal token is accepted (idempotent
|
|
479
|
+
// re-write of the same leader's tip), RETURNING 1 row.
|
|
480
|
+
var t4 = await _runTip(3, "hash-3", "1700000000003", 2);
|
|
481
|
+
check("consent-tip: equal fencingToken accepted by <= fence (RETURNING 1 row)", t4.rows.length === 1);
|
|
482
|
+
|
|
483
|
+
// Prove the framework reads this back coerced: fencingToken is an int
|
|
484
|
+
// column -> coerceRows turns the RETURNING string into a JS number. Read
|
|
485
|
+
// through clusterStorage.execute (the real consent-verify path) which
|
|
486
|
+
// placeholderizes + coerces in cluster mode.
|
|
487
|
+
var tipReadBuilt = b.sql.select("_blamejs_consent_tip", { dialect: "postgres", quoteName: true })
|
|
488
|
+
.columns(["atMonotonicCounter", "fencingToken"])
|
|
489
|
+
.where("scope", "consent")
|
|
490
|
+
.toSql();
|
|
491
|
+
var tipRead = await clusterStorage.execute(tipReadBuilt.sql, tipReadBuilt.params);
|
|
492
|
+
check("consent-tip: clusterStorage.execute coerces fencingToken -> JS number reading back",
|
|
493
|
+
tipRead.rows.length === 1 && typeof tipRead.rows[0].fencingToken === "number" &&
|
|
494
|
+
tipRead.rows[0].fencingToken === 2);
|
|
495
|
+
|
|
496
|
+
// Prove the fence is dialect-threaded from the MODULE: rebuild the exact
|
|
497
|
+
// upsert the way consent._upsertConsentTip does for the ACTIVE dialect
|
|
498
|
+
// (clusterStorage.dialect() === "postgres" here) — guardColumn + the
|
|
499
|
+
// EXCLUDED fence — and run it through clusterStorage.execute (the module's
|
|
500
|
+
// own dispatch path). A lower token must still be fenced out (RETURNING 0
|
|
501
|
+
// rows) on the real planner, identical to what the module emits at runtime.
|
|
502
|
+
function _moduleTipUpsert(counter, rowHash, signedAt, fencingToken) {
|
|
503
|
+
var d = clusterStorage.dialect();
|
|
504
|
+
var qf = safeSql.quoteIdentifier("fencingToken", d);
|
|
505
|
+
var fence = d === "mysql"
|
|
506
|
+
? "_blamejs_consent_tip." + qf + " <= VALUES(" + qf + ")"
|
|
507
|
+
: "_blamejs_consent_tip." + qf + " <= EXCLUDED." + qf;
|
|
508
|
+
return b.sql.upsert("_blamejs_consent_tip", { dialect: d })
|
|
509
|
+
.columns(["scope", "atMonotonicCounter", "rowHash", "signedAt", "fencingToken"])
|
|
510
|
+
.values({ scope: "consent", atMonotonicCounter: counter, rowHash: rowHash, signedAt: signedAt, fencingToken: fencingToken })
|
|
511
|
+
.onConflict(["scope"])
|
|
512
|
+
.doUpdateFromExcluded(["atMonotonicCounter", "rowHash", "signedAt", "fencingToken"])
|
|
513
|
+
.conflictWhere(fence, [], { guardColumn: "fencingToken" })
|
|
514
|
+
.returning(["fencingToken"])
|
|
515
|
+
.toSql();
|
|
516
|
+
}
|
|
517
|
+
var modLower = _moduleTipUpsert(99, "hash-evil-mod", "1700000000099", 1);
|
|
518
|
+
var modLowerRes = await clusterStorage.execute(modLower.sql, modLower.params);
|
|
519
|
+
check("consent-tip: the module-shaped (clusterStorage.dialect()) fence rejects a lower token on Postgres",
|
|
520
|
+
modLowerRes.rows.length === 0);
|
|
521
|
+
var modHigher = _moduleTipUpsert(5, "hash-5-mod", "1700000000005", 5);
|
|
522
|
+
var modHigherRes = await clusterStorage.execute(modHigher.sql, modHigher.params);
|
|
523
|
+
check("consent-tip: the module-shaped fence accepts a higher token (RETURNING 1 row) on Postgres",
|
|
524
|
+
modHigherRes.rows.length === 1);
|
|
525
|
+
var modTipState = _psql('SELECT "rowHash", "fencingToken" FROM _blamejs_consent_tip WHERE scope = \'consent\';');
|
|
526
|
+
check("consent-tip: the module-shaped advance landed hash-5-mod / token 5 on the server",
|
|
527
|
+
/hash-5-mod/.test(modTipState.trim()) && !/hash-evil-mod/.test(modTipState.trim()));
|
|
528
|
+
|
|
529
|
+
// Tear down cluster mode before the local-builder shapes (legal-hold /
|
|
530
|
+
// subject / retention) which we drive directly on the backend.
|
|
531
|
+
await b.cluster.shutdown();
|
|
532
|
+
_resetState();
|
|
533
|
+
b.externalDb.init({
|
|
534
|
+
backends: { ops: { connect: driver.connect, query: driver.query, close: driver.close, dialect: "postgres" } },
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// ====================================================================
|
|
538
|
+
// legal-hold — place INSERT / release DELETE / whereLike prefix history.
|
|
539
|
+
// legal-hold's builders run against a local b.db handle in production,
|
|
540
|
+
// but the b.sql shapes must still be valid Postgres. Build them at the
|
|
541
|
+
// postgres dialect and run on the live server.
|
|
542
|
+
// ====================================================================
|
|
543
|
+
var holdDdl = b.sql.createTable("dl_pg_hold", [
|
|
544
|
+
{ name: "subjectIdHash", type: "text", primaryKey: true },
|
|
545
|
+
{ name: "placedAt", type: "int", notNull: true },
|
|
546
|
+
{ name: "placedBy", type: "text" },
|
|
547
|
+
{ name: "reason", type: "text", notNull: true },
|
|
548
|
+
{ name: "custodian", type: "text" },
|
|
549
|
+
{ name: "citation", type: "text" },
|
|
550
|
+
{ name: "retainUntil", type: "int" },
|
|
551
|
+
], { dialect: "postgres", quoteName: true });
|
|
552
|
+
await _q(holdDdl);
|
|
553
|
+
|
|
554
|
+
// place(): the SELECT existence check + the INSERT.
|
|
555
|
+
var hash = b.crypto.sha3Hash("bj-legal-hold:subject-42");
|
|
556
|
+
var placeIns = b.sql.insert("dl_pg_hold", { dialect: "postgres", quoteName: true })
|
|
557
|
+
.values({ subjectIdHash: hash, placedAt: nowMs, placedBy: "legal@x", reason: "SEC subpoena", custodian: "c@x", citation: "SEC-Rule-17a-4", retainUntil: null })
|
|
558
|
+
.toSql();
|
|
559
|
+
await _q(placeIns);
|
|
560
|
+
var holdSelBuilt = b.sql.select("dl_pg_hold", { dialect: "postgres", quoteName: true })
|
|
561
|
+
.columns(["placedAt"]).where("subjectIdHash", hash).toSql();
|
|
562
|
+
var holdSel = await _q(holdSelBuilt);
|
|
563
|
+
check("legal-hold: place INSERT + existence SELECT round-trip on Postgres",
|
|
564
|
+
holdSel.rows.length === 1 && Number(holdSel.rows[0].placedAt) === nowMs);
|
|
565
|
+
|
|
566
|
+
// release(): the DELETE keyed on subjectIdHash.
|
|
567
|
+
var holdDel = b.sql.delete("dl_pg_hold", { dialect: "postgres", quoteName: true })
|
|
568
|
+
.where("subjectIdHash", hash).toSql();
|
|
569
|
+
var holdDelRes = await _q(holdDel);
|
|
570
|
+
check("legal-hold: release DELETE affected 1 row on Postgres", holdDelRes.rowCount === 1);
|
|
571
|
+
var holdGone = _psql("SELECT count(*) AS n FROM dl_pg_hold;");
|
|
572
|
+
check("legal-hold: hold physically removed after release", /\b0\b/.test(holdGone.trim()));
|
|
573
|
+
|
|
574
|
+
// history(): whereLike("action", "legalhold.", "prefix") — the ESCAPE '~'
|
|
575
|
+
// prefix match. Seed an audit-shaped table and prove the LIKE selects
|
|
576
|
+
// exactly the legalhold.* rows, not a row whose action merely contains it.
|
|
577
|
+
var auditDdl = b.sql.createTable("dl_pg_audit", [
|
|
578
|
+
{ name: "recordedAt", type: "int", notNull: true },
|
|
579
|
+
{ name: "action", type: "text", notNull: true },
|
|
580
|
+
{ name: "metadata", type: "text" },
|
|
581
|
+
{ name: "outcome", type: "text" },
|
|
582
|
+
{ name: "resourceKind", type: "text" },
|
|
583
|
+
], { dialect: "postgres", quoteName: true });
|
|
584
|
+
await _q(auditDdl);
|
|
585
|
+
var seedRows = [
|
|
586
|
+
[1, "legalhold.placed", '{"subjectId":"subject-42"}', "success", "legal-hold"],
|
|
587
|
+
[2, "legalhold.released", '{"subjectId":"subject-42"}', "success", "legal-hold"],
|
|
588
|
+
[3, "auth.legalhold.x", '{"subjectId":"subject-42"}', "success", "legal-hold"], // NOT a prefix match
|
|
589
|
+
[4, "consent.granted", '{"subjectId":"subject-42"}', "success", "consent"],
|
|
590
|
+
];
|
|
591
|
+
for (var sr = 0; sr < seedRows.length; sr++) {
|
|
592
|
+
var ins = b.sql.insert("dl_pg_audit", { dialect: "postgres", quoteName: true })
|
|
593
|
+
.values({ recordedAt: seedRows[sr][0], action: seedRows[sr][1], metadata: seedRows[sr][2], outcome: seedRows[sr][3], resourceKind: seedRows[sr][4] })
|
|
594
|
+
.toSql();
|
|
595
|
+
await _q(ins);
|
|
596
|
+
}
|
|
597
|
+
var histBuilt = b.sql.select("dl_pg_audit", { dialect: "postgres", quoteName: true })
|
|
598
|
+
.columns(["recordedAt", "action"])
|
|
599
|
+
.whereLike("action", "legalhold.", "prefix")
|
|
600
|
+
.where("resourceKind", "legal-hold")
|
|
601
|
+
.orderBy("recordedAt", "asc")
|
|
602
|
+
.toSql();
|
|
603
|
+
var hist = await _q(histBuilt);
|
|
604
|
+
check("legal-hold: whereLike prefix selects exactly the legalhold.* rows on Postgres",
|
|
605
|
+
hist.rows.length === 2 &&
|
|
606
|
+
hist.rows[0].action === "legalhold.placed" &&
|
|
607
|
+
hist.rows[1].action === "legalhold.released");
|
|
608
|
+
check("legal-hold: prefix LIKE did NOT match the mid-string 'auth.legalhold.x'",
|
|
609
|
+
hist.rows.every(function (r) { return r.action.indexOf("legalhold.") === 0; }));
|
|
610
|
+
|
|
611
|
+
// whereLike ESCAPE: a literal underscore/percent in the term stays
|
|
612
|
+
// literal. Seed an action with a literal "%" and prove a prefix term
|
|
613
|
+
// containing "%" matches ONLY the literal, not a wildcard expansion.
|
|
614
|
+
var wlIns = b.sql.insert("dl_pg_audit", { dialect: "postgres", quoteName: true })
|
|
615
|
+
.values({ recordedAt: 5, action: "legalhold.100%done", metadata: null, outcome: "success", resourceKind: "legal-hold" })
|
|
616
|
+
.toSql();
|
|
617
|
+
await _q(wlIns);
|
|
618
|
+
var escBuilt = b.sql.select("dl_pg_audit", { dialect: "postgres", quoteName: true })
|
|
619
|
+
.columns(["action"]).whereLike("action", "legalhold.100%", "prefix").toSql();
|
|
620
|
+
var esc = await _q(escBuilt);
|
|
621
|
+
check("legal-hold: whereLike escapes a literal % in the term (matches only the literal row)",
|
|
622
|
+
esc.rows.length === 1 && esc.rows[0].action === "legalhold.100%done");
|
|
623
|
+
|
|
624
|
+
// ====================================================================
|
|
625
|
+
// subject — the INSERT-OR-REPLACE upsert (_markErased) + restrict
|
|
626
|
+
// INSERT/DELETE presence pattern.
|
|
627
|
+
// ====================================================================
|
|
628
|
+
var erasuresDdl = b.sql.createTable("dl_pg_erasures", [
|
|
629
|
+
{ name: "subjectIdHash", type: "text", primaryKey: true },
|
|
630
|
+
{ name: "erasedAt", type: "int", notNull: true },
|
|
631
|
+
], { dialect: "postgres", quoteName: true });
|
|
632
|
+
await _q(erasuresDdl);
|
|
633
|
+
|
|
634
|
+
function _markErasedSql(subjectHash, erasedAt) {
|
|
635
|
+
return b.sql.upsert("dl_pg_erasures", { dialect: "postgres", quoteName: true })
|
|
636
|
+
.values({ subjectIdHash: subjectHash, erasedAt: erasedAt })
|
|
637
|
+
.onConflict(["subjectIdHash"])
|
|
638
|
+
.doUpdateFromExcluded(["erasedAt"])
|
|
639
|
+
.toSql();
|
|
640
|
+
}
|
|
641
|
+
var shash = b.crypto.sha3Hash("bj-subject:user-99");
|
|
642
|
+
var me1 = _markErasedSql(shash, 1700000000000);
|
|
643
|
+
await _q(me1);
|
|
644
|
+
// Re-erase the same subject — INSERT-OR-REPLACE refreshes the timestamp
|
|
645
|
+
// (ON CONFLICT DO UPDATE), NOT a duplicate-key error.
|
|
646
|
+
var me2 = _markErasedSql(shash, 1700000009999);
|
|
647
|
+
var me2res = await _q(me2);
|
|
648
|
+
void me2res;
|
|
649
|
+
var erasureState = _psql('SELECT count(*) AS c, max("erasedAt") AS m FROM dl_pg_erasures;');
|
|
650
|
+
check("subject: INSERT-OR-REPLACE upsert kept ONE row (no dup-key) on Postgres",
|
|
651
|
+
/^1\t/.test(erasureState.trim()) || /\b1\b/.test(erasureState.split("\t")[0]));
|
|
652
|
+
check("subject: INSERT-OR-REPLACE refreshed erasedAt to the newest value",
|
|
653
|
+
/1700000009999/.test(erasureState.trim()));
|
|
654
|
+
|
|
655
|
+
// restrict() presence pattern: INSERT when absent, DELETE to lift.
|
|
656
|
+
var restrictDdl = b.sql.createTable("dl_pg_restrictions", [
|
|
657
|
+
{ name: "subjectIdHash", type: "text", primaryKey: true },
|
|
658
|
+
{ name: "since", type: "int", notNull: true },
|
|
659
|
+
{ name: "reason", type: "text" },
|
|
660
|
+
], { dialect: "postgres", quoteName: true });
|
|
661
|
+
await _q(restrictDdl);
|
|
662
|
+
var rIns = b.sql.insert("dl_pg_restrictions", { dialect: "postgres", quoteName: true })
|
|
663
|
+
.values({ subjectIdHash: shash, since: nowMs, reason: "art-18 hold" }).toSql();
|
|
664
|
+
await _q(rIns);
|
|
665
|
+
var rPresBuilt = b.sql.select("dl_pg_restrictions", { dialect: "postgres", quoteName: true })
|
|
666
|
+
.columns(["subjectIdHash"]).where("subjectIdHash", shash).limit(1).toSql();
|
|
667
|
+
var rPres = await _q(rPresBuilt);
|
|
668
|
+
check("subject: restrict INSERT + presence SELECT round-trip on Postgres", rPres.rows.length === 1);
|
|
669
|
+
var rDel = b.sql.delete("dl_pg_restrictions", { dialect: "postgres", quoteName: true })
|
|
670
|
+
.where("subjectIdHash", shash).toSql();
|
|
671
|
+
var rDelRes = await _q(rDel);
|
|
672
|
+
check("subject: restrict DELETE (lift) affected 1 row on Postgres", rDelRes.rowCount === 1);
|
|
673
|
+
|
|
674
|
+
// ====================================================================
|
|
675
|
+
// retention — hard delete / soft-delete UPDATE / erase NULL-set /
|
|
676
|
+
// cascade DELETE / the candidate whereGroup-OR WHERE.
|
|
677
|
+
// ====================================================================
|
|
678
|
+
// __erasedAt is declared TEXT — retention's _candidateBase compares it
|
|
679
|
+
// with `__erasedAt = ''` (the empty-string sentinel), so the operator's
|
|
680
|
+
// erasure-marker column is a string column. (A numeric __erasedAt would
|
|
681
|
+
// make `= ''` reject on Postgres with 22P02 — a known cross-dialect
|
|
682
|
+
// footgun, but retention runs against the local SQLite db handle in
|
|
683
|
+
// production, never an external DB, and SQLite is loosely typed.)
|
|
684
|
+
var ordersDdl = b.sql.createTable("dl_pg_orders", [
|
|
685
|
+
{ name: "_id", type: "text", primaryKey: true },
|
|
686
|
+
{ name: "createdAt", type: "int", notNull: true },
|
|
687
|
+
{ name: "secretCol", type: "text" },
|
|
688
|
+
{ name: "secretColHash", type: "text" },
|
|
689
|
+
{ name: "softAt", type: "int" },
|
|
690
|
+
{ name: "__erasedAt", type: "text" },
|
|
691
|
+
], { dialect: "postgres", quoteName: true });
|
|
692
|
+
await _q(ordersDdl);
|
|
693
|
+
var linesDdl = b.sql.createTable("dl_pg_order_lines", [
|
|
694
|
+
{ name: "_id", type: "text", primaryKey: true },
|
|
695
|
+
{ name: "orderId", type: "text", notNull: true },
|
|
696
|
+
], { dialect: "postgres", quoteName: true });
|
|
697
|
+
await _q(linesDdl);
|
|
698
|
+
|
|
699
|
+
var oldAt = nowMs - b.constants.TIME.days(400);
|
|
700
|
+
for (var oi = 1; oi <= 4; oi++) {
|
|
701
|
+
var oid = "o-" + oi;
|
|
702
|
+
var oins = b.sql.insert("dl_pg_orders", { dialect: "postgres", quoteName: true })
|
|
703
|
+
.values({ _id: oid, createdAt: oldAt, secretCol: "secret-" + oi, secretColHash: "h-" + oi, softAt: null, __erasedAt: null })
|
|
704
|
+
.toSql();
|
|
705
|
+
await _q(oins);
|
|
706
|
+
var lins = b.sql.insert("dl_pg_order_lines", { dialect: "postgres", quoteName: true })
|
|
707
|
+
.values({ _id: "l-" + oi, orderId: oid }).toSql();
|
|
708
|
+
await _q(lins);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The candidate WHERE: age <= cutoff AND (softAt IS NULL) AND
|
|
712
|
+
// (__erasedAt IS NULL OR __erasedAt = '') — retention's _candidateBase +
|
|
713
|
+
// whereGroup. It must select all 4 aged rows on the real planner.
|
|
714
|
+
var cutoff = nowMs - b.constants.TIME.days(365);
|
|
715
|
+
var candBuilt = b.sql.select("dl_pg_orders", { dialect: "postgres", quoteName: true })
|
|
716
|
+
.where("createdAt", "<=", cutoff)
|
|
717
|
+
.whereNull("softAt")
|
|
718
|
+
.whereGroup(function (g) { g.whereNull("__erasedAt").orWhereOp("__erasedAt", "=", ""); })
|
|
719
|
+
.limit(500)
|
|
720
|
+
.toSql();
|
|
721
|
+
var cand = await _q(candBuilt);
|
|
722
|
+
check("retention: candidate whereGroup-OR WHERE selects the 4 aged rows on Postgres",
|
|
723
|
+
cand.rowCount === 4);
|
|
724
|
+
|
|
725
|
+
// soft-delete UPDATE on o-1.
|
|
726
|
+
var softBuilt = b.sql.update("dl_pg_orders", { dialect: "postgres", quoteName: true })
|
|
727
|
+
.set("softAt", nowMs).where("_id", "o-1").toSql();
|
|
728
|
+
var softRes = await _q(softBuilt);
|
|
729
|
+
check("retention: soft-delete UPDATE set softAt on Postgres", softRes.rowCount === 1);
|
|
730
|
+
|
|
731
|
+
// erase NULL-set on o-2 (NULL the sealed col + its derived hash).
|
|
732
|
+
var eraseBuilt = b.sql.update("dl_pg_orders", { dialect: "postgres", quoteName: true })
|
|
733
|
+
.set({ secretCol: null, secretColHash: null }).where("_id", "o-2").toSql();
|
|
734
|
+
await _q(eraseBuilt);
|
|
735
|
+
var erasedCheck = _psql('SELECT "secretCol", "secretColHash" FROM dl_pg_orders WHERE _id = \'o-2\';');
|
|
736
|
+
check("retention: erase NULL-set wiped the sealed col + derived hash on Postgres",
|
|
737
|
+
new RegExp(NULL_SENTINEL + "\\t" + NULL_SENTINEL).test(erasedCheck.trim()));
|
|
738
|
+
|
|
739
|
+
// hard delete on o-3 + cascade DELETE its order_lines.
|
|
740
|
+
var hardBuilt = b.sql.delete("dl_pg_orders", { dialect: "postgres", quoteName: true })
|
|
741
|
+
.where("_id", "o-3").toSql();
|
|
742
|
+
var hardRes = await _q(hardBuilt);
|
|
743
|
+
check("retention: hard delete removed o-3 on Postgres", hardRes.rowCount === 1);
|
|
744
|
+
var cascBuilt = b.sql.delete("dl_pg_order_lines", { dialect: "postgres", quoteName: true })
|
|
745
|
+
.where("orderId", "o-3").toSql();
|
|
746
|
+
var cascRes = await _q(cascBuilt);
|
|
747
|
+
check("retention: cascade DELETE removed o-3's order_lines on Postgres", cascRes.rowCount === 1);
|
|
748
|
+
var lineLeft = _psql("SELECT count(*) AS n FROM dl_pg_order_lines WHERE \"orderId\" = 'o-3';");
|
|
749
|
+
check("retention: no orphan order_lines remain for the cascaded parent", /\b0\b/.test(lineLeft.trim()));
|
|
750
|
+
|
|
751
|
+
// dry-run cascade count via .count("*","n") — the COUNT shape b.sql emits.
|
|
752
|
+
var cntBuilt = b.sql.select("dl_pg_order_lines", { dialect: "postgres", quoteName: true })
|
|
753
|
+
.count("*", "n").where("orderId", "o-4").toSql();
|
|
754
|
+
var cnt = await _q(cntBuilt);
|
|
755
|
+
check("retention: cascade dry-run COUNT(*) returns 1 for o-4's lines on Postgres",
|
|
756
|
+
cnt.rows.length === 1 && Number(cnt.rows[0].n) === 1);
|
|
757
|
+
|
|
758
|
+
// ---- teardown ----
|
|
759
|
+
await b.externalDb.shutdown();
|
|
760
|
+
_resetState();
|
|
761
|
+
_psql(DROP_ALL);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
module.exports = { run: run };
|
|
765
|
+
|
|
766
|
+
if (require.main === module) {
|
|
767
|
+
run().then(
|
|
768
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
|
|
769
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
770
|
+
);
|
|
771
|
+
}
|