@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,832 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live PostgreSQL coverage for the migrated b.sql data layer that today
|
|
4
|
+
* is only exercised against single-node SQLite host smoke:
|
|
5
|
+
*
|
|
6
|
+
* lib/audit.js — record() → chain-writer, checkpoint(),
|
|
7
|
+
* _upsertAuditTip() fencing-token guard, verify(),
|
|
8
|
+
* verifyCheckpoints()
|
|
9
|
+
* lib/audit-tools.js — exportSlice / archive / verifyBundle / purge
|
|
10
|
+
* lib/chain-writer.js — _insertRow / counter primer / tip read on a real
|
|
11
|
+
* backend
|
|
12
|
+
* lib/break-glass.js — policy.set/get/list + grant + unsealRow consume,
|
|
13
|
+
* all routed through clusterStorage to live Postgres
|
|
14
|
+
* lib/crypto-field.js — a K_row (vault.row:) sealed cell stored as TEXT in
|
|
15
|
+
* Postgres and read back, proving the typed codec
|
|
16
|
+
* (Buffer/object) survives a real round-trip
|
|
17
|
+
*
|
|
18
|
+
* The driver is a docker-exec psql shim that replicates a real
|
|
19
|
+
* node-postgres driver's coercions: BIGINT (int8) → JS STRING, BYTEA →
|
|
20
|
+
* Node Buffer, and unquoted identifiers folded to lowercase. The
|
|
21
|
+
* framework's own clusterStorage.coerceRows (frameworkSchema.COLUMN_TYPES)
|
|
22
|
+
* then normalizes those back to numbers / Buffers — this test asserts that
|
|
23
|
+
* normalization end-to-end against the live server, NOT a hand-coerced
|
|
24
|
+
* fake.
|
|
25
|
+
*
|
|
26
|
+
* Flow: setupTestDb (vault + local SQLite + cryptoField schema
|
|
27
|
+
* registration for the framework tables) → frameworkSchema.ensureSchema on
|
|
28
|
+
* Postgres → cluster.init (leader) flips the framework into cluster mode so
|
|
29
|
+
* every audit / break-glass / consent write dispatches to the external
|
|
30
|
+
* Postgres backend through the SAME b.sql + clusterStorage path operators
|
|
31
|
+
* run in production.
|
|
32
|
+
*
|
|
33
|
+
* Tables are namespaced under the default _blamejs_ prefix; setup drops and
|
|
34
|
+
* recreates them so a re-run is clean and concurrent integration tests in
|
|
35
|
+
* other databases don't collide (this test owns blamejs_test).
|
|
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
|
+
|
|
44
|
+
var helpers = require("../helpers");
|
|
45
|
+
var check = helpers.check;
|
|
46
|
+
var services = require("../helpers/services");
|
|
47
|
+
var setupTestDb = require("../helpers/db").setupTestDb;
|
|
48
|
+
var teardownTestDb = require("../helpers/db").teardownTestDb;
|
|
49
|
+
var b = require("../../");
|
|
50
|
+
|
|
51
|
+
var CONTAINER = "blamejs-test-postgres";
|
|
52
|
+
var NULL_SENTINEL = "__BJNULL__";
|
|
53
|
+
var PSQL_ARGS = "psql -U blamejs -d blamejs_test -A " +
|
|
54
|
+
"-v ON_ERROR_STOP=0 -P null=__BJNULL__ 2>&1";
|
|
55
|
+
|
|
56
|
+
// ---- one-shot psql (setup / teardown / out-of-band assertions) ----
|
|
57
|
+
function _psql(sql) {
|
|
58
|
+
var prelude = "\\pset fieldsep '\\t'\n";
|
|
59
|
+
var out = execFileSync(
|
|
60
|
+
"docker",
|
|
61
|
+
["exec", "-i", CONTAINER, "sh", "-c",
|
|
62
|
+
"psql -U blamejs -d blamejs_test -qtA -P null=__BJNULL__ 2>&1"],
|
|
63
|
+
{ input: prelude + sql + "\n", stdio: ["pipe", "pipe", "pipe"] }
|
|
64
|
+
).toString("utf8");
|
|
65
|
+
if (/^ERROR:/m.test(out)) {
|
|
66
|
+
throw new Error("psql setup failed for [" + sql + "]:\n" + out);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- persistent-session docker-exec psql driver (faithful to node-postgres) ----
|
|
72
|
+
var _seq = 0;
|
|
73
|
+
function _makeDockerPgDriver() {
|
|
74
|
+
return {
|
|
75
|
+
connect: function () {
|
|
76
|
+
return new Promise(function (resolve, reject) {
|
|
77
|
+
var child = spawn(
|
|
78
|
+
"docker",
|
|
79
|
+
["exec", "-i", CONTAINER, "sh", "-c",
|
|
80
|
+
PSQL_ARGS + " ; echo __BLAMEJS_PSQL_EXIT__"],
|
|
81
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
82
|
+
);
|
|
83
|
+
var client = { child: child, buf: "", pending: null, closed: false };
|
|
84
|
+
child.on("error", function (e) {
|
|
85
|
+
if (client.pending) { var p = client.pending; client.pending = null; p.reject(e); }
|
|
86
|
+
});
|
|
87
|
+
child.on("close", function () {
|
|
88
|
+
client.closed = true;
|
|
89
|
+
if (client.pending) {
|
|
90
|
+
var p = client.pending; client.pending = null;
|
|
91
|
+
p.reject(new Error("psql session closed mid-statement"));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.stdout.on("data", function (chunk) {
|
|
95
|
+
client.buf += chunk.toString("utf8");
|
|
96
|
+
_drain(client);
|
|
97
|
+
});
|
|
98
|
+
var primeSentinel = "__BJ_PRIME__";
|
|
99
|
+
client.pending = {
|
|
100
|
+
sentinel: primeSentinel,
|
|
101
|
+
resolve: function () { resolve(client); },
|
|
102
|
+
reject: reject,
|
|
103
|
+
};
|
|
104
|
+
client.child.stdin.write(
|
|
105
|
+
"\\pset fieldsep '\\t'\n\\pset footer off\n\\set VERBOSITY verbose\n" +
|
|
106
|
+
"SET bytea_output = 'hex';\n" +
|
|
107
|
+
"\\echo " + primeSentinel + "\n");
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
query: function (client, sql, params) {
|
|
112
|
+
params = params || [];
|
|
113
|
+
if (process.env.BJ_TRACE_SQL === "1") { try { process.stderr.write("[SQL] " + sql + "\n"); } catch (_e) {} }
|
|
114
|
+
var bound = _bindParams(sql, params);
|
|
115
|
+
var sentinel = "__BJ_EOR_" + (++_seq) + "__";
|
|
116
|
+
return new Promise(function (resolve, reject) {
|
|
117
|
+
if (client.closed) { reject(new Error("psql session is closed")); return; }
|
|
118
|
+
client.pending = { sentinel: sentinel, resolve: resolve, reject: reject };
|
|
119
|
+
client.child.stdin.write(bound + "\n;\n\\echo " + sentinel + "\n");
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
close: function (client) {
|
|
124
|
+
return new Promise(function (resolve) {
|
|
125
|
+
if (client.closed) { resolve(); return; }
|
|
126
|
+
try { client.child.stdin.end("\\q\n"); } catch (_e) { /* best effort */ }
|
|
127
|
+
var done = false;
|
|
128
|
+
client.child.on("close", function () { if (!done) { done = true; resolve(); } });
|
|
129
|
+
setTimeout(function () {
|
|
130
|
+
if (done) return;
|
|
131
|
+
done = true;
|
|
132
|
+
try { client.child.kill("SIGKILL"); } catch (_e) {}
|
|
133
|
+
resolve();
|
|
134
|
+
}, 2000);
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
dialect: "postgres",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _drain(client) {
|
|
143
|
+
if (!client.pending) return;
|
|
144
|
+
var sentinel = client.pending.sentinel;
|
|
145
|
+
var marker = "\n" + sentinel + "\n";
|
|
146
|
+
var idx = client.buf.indexOf(marker);
|
|
147
|
+
var startAtZero = client.buf.indexOf(sentinel + "\n") === 0;
|
|
148
|
+
var block;
|
|
149
|
+
if (idx !== -1) {
|
|
150
|
+
block = client.buf.slice(0, idx);
|
|
151
|
+
client.buf = client.buf.slice(idx + marker.length);
|
|
152
|
+
} else if (startAtZero) {
|
|
153
|
+
block = "";
|
|
154
|
+
client.buf = client.buf.slice((sentinel + "\n").length);
|
|
155
|
+
} else {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
var p = client.pending;
|
|
159
|
+
client.pending = null;
|
|
160
|
+
var parsed;
|
|
161
|
+
try { parsed = _parseBlock(block); }
|
|
162
|
+
catch (e) { return p.reject(e); }
|
|
163
|
+
if (parsed.error) return p.reject(parsed.error);
|
|
164
|
+
p.resolve({ rows: parsed.rows, rowCount: parsed.rowCount });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Inline params: Buffers as bytea hex literals (byte-faithful round-trip),
|
|
168
|
+
// numbers raw, booleans TRUE/FALSE, everything else single-quote-escaped.
|
|
169
|
+
var BYTEA_LITERAL_PREFIX = "'" + "\\" + "x";
|
|
170
|
+
function _bindParams(sql, params) {
|
|
171
|
+
return sql.replace(/\$(\d+)/g, function (_m, n) {
|
|
172
|
+
var i = Number(n) - 1;
|
|
173
|
+
if (i < 0 || i >= params.length) {
|
|
174
|
+
throw new Error("placeholder $" + n + " has no matching param");
|
|
175
|
+
}
|
|
176
|
+
var v = params[i];
|
|
177
|
+
if (v === null || v === undefined) return "NULL";
|
|
178
|
+
if (Buffer.isBuffer(v)) return BYTEA_LITERAL_PREFIX + v.toString("hex") + "'::bytea";
|
|
179
|
+
if (typeof v === "number") return String(v);
|
|
180
|
+
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
|
181
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var _CMD_TAG_RE = /^(INSERT|UPDATE|DELETE|MERGE|SELECT|COPY|MOVE)\b(?:\s+\d+)*\s*$/;
|
|
186
|
+
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)\b/;
|
|
187
|
+
|
|
188
|
+
// Columns the framework's audit / break-glass tables store as BYTEA — a real
|
|
189
|
+
// pg driver returns these as Buffers. clusterStorage.coerceRows then keeps
|
|
190
|
+
// them Buffers (idempotent), so the driver MUST hand Buffers up to match.
|
|
191
|
+
var _BYTEA_COLUMNS = { nonce: true, signature: true };
|
|
192
|
+
|
|
193
|
+
function _parseBlock(block) {
|
|
194
|
+
var lines = block.split(/\r?\n/);
|
|
195
|
+
while (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
196
|
+
|
|
197
|
+
for (var i = 0; i < lines.length; i++) {
|
|
198
|
+
var em = /^ERROR:\s+([0-9A-Za-z]{5}):\s*(.*)$/.exec(lines[i]);
|
|
199
|
+
if (em) {
|
|
200
|
+
var err = new Error("Postgres " + em[1] + ": " + em[2]);
|
|
201
|
+
err.code = em[1];
|
|
202
|
+
return { error: err };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
var affected = null;
|
|
207
|
+
var dataLines = [];
|
|
208
|
+
for (var j = 0; j < lines.length; j++) {
|
|
209
|
+
var ln = lines[j];
|
|
210
|
+
if (/^(NOTICE|WARNING|DETAIL|HINT|LINE|LOCATION|CONTEXT|STATEMENT):/.test(ln)) continue;
|
|
211
|
+
var tm = _CMD_TAG_RE.exec(ln);
|
|
212
|
+
if (tm) {
|
|
213
|
+
var nums = ln.trim().split(/\s+/).slice(1).map(Number);
|
|
214
|
+
if (nums.length) affected = nums[nums.length - 1];
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (_CTRL_TAG_RE.test(ln) && ln.indexOf("\t") === -1) continue;
|
|
218
|
+
dataLines.push(ln);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var rows = [];
|
|
222
|
+
if (dataLines.length >= 1) {
|
|
223
|
+
// Header row keyed AS POSTGRES REPORTS IT — lowercase for unquoted
|
|
224
|
+
// identifiers, case-preserving for the double-quoted camelCase columns
|
|
225
|
+
// the framework's DDL created. Kept verbatim, exactly as node-postgres
|
|
226
|
+
// would key the row object.
|
|
227
|
+
var headers = dataLines[0].split("\t");
|
|
228
|
+
for (var k = 1; k < dataLines.length; k++) {
|
|
229
|
+
var cells = dataLines[k].split("\t");
|
|
230
|
+
var row = {};
|
|
231
|
+
for (var c = 0; c < headers.length; c++) {
|
|
232
|
+
var cell = cells[c];
|
|
233
|
+
var hdr = headers[c];
|
|
234
|
+
if (cell === NULL_SENTINEL || cell === undefined) { row[hdr] = null; continue; }
|
|
235
|
+
if (_BYTEA_COLUMNS[hdr] === true) {
|
|
236
|
+
var hex = cell.charAt(0) === "\\" && cell.charAt(1) === "x"
|
|
237
|
+
? cell.slice(2) : cell;
|
|
238
|
+
row[hdr] = Buffer.from(hex, "hex");
|
|
239
|
+
} else {
|
|
240
|
+
// STRING — including BIGINT columns (node-postgres int8 default).
|
|
241
|
+
row[hdr] = cell;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
rows.push(row);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
var rowCount = (affected !== null) ? affected : rows.length;
|
|
248
|
+
return { rows: rows, rowCount: rowCount, error: null };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Drop every framework table this test touches so a re-run is clean.
|
|
252
|
+
var FRAMEWORK_TABLES = [
|
|
253
|
+
"_blamejs_audit_log", "_blamejs_consent_log", "_blamejs_audit_checkpoints",
|
|
254
|
+
"_blamejs_audit_tip", "_blamejs_consent_tip", "_blamejs_audit_purge_anchor",
|
|
255
|
+
"_blamejs_scheduler_ticks", "_blamejs_rate_limit_counters",
|
|
256
|
+
"_blamejs_pubsub_messages", "_blamejs_api_encrypt_nonces", "_blamejs_api_keys",
|
|
257
|
+
"_blamejs_sessions", "_blamejs_jobs", "_blamejs_cache", "_blamejs_cache_tags",
|
|
258
|
+
"_blamejs_seeders", "_blamejs_seeders_lock", "_blamejs_break_glass_policies",
|
|
259
|
+
"_blamejs_break_glass_grants", "_blamejs_leader", "_blamejs_cluster_state",
|
|
260
|
+
// app-side table for break-glass + K_row storage round-trips
|
|
261
|
+
"patients",
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
function _dropFrameworkTables() {
|
|
265
|
+
_psql(FRAMEWORK_TABLES.map(function (t) {
|
|
266
|
+
return "DROP TABLE IF EXISTS " + t + " CASCADE;";
|
|
267
|
+
}).join("\n"));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function run() {
|
|
271
|
+
var pg = await services.requireService("postgres");
|
|
272
|
+
if (!pg.ok) throw new Error("postgres unreachable: " + pg.reason);
|
|
273
|
+
|
|
274
|
+
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-audit-pg-"));
|
|
275
|
+
var driver = _makeDockerPgDriver();
|
|
276
|
+
var driverClient = null;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
_dropFrameworkTables();
|
|
280
|
+
|
|
281
|
+
// ---- Boot the framework: vault + local SQLite + cryptoField schema
|
|
282
|
+
// registration for the framework tables (audit_log /
|
|
283
|
+
// break_glass_* sealed-column declarations come from db.init's
|
|
284
|
+
// FRAMEWORK_SCHEMA). The "patients" app table is glass-locked +
|
|
285
|
+
// sealed for the break-glass + crypto-field portions. ----
|
|
286
|
+
await setupTestDb(tmpDir, [
|
|
287
|
+
{
|
|
288
|
+
name: "patients",
|
|
289
|
+
columns: {
|
|
290
|
+
_id: "TEXT PRIMARY KEY",
|
|
291
|
+
mrn: "TEXT",
|
|
292
|
+
ssn: "TEXT",
|
|
293
|
+
residency: "TEXT",
|
|
294
|
+
notes: "TEXT",
|
|
295
|
+
},
|
|
296
|
+
sealedFields: ["ssn", "notes"],
|
|
297
|
+
},
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
// External backend + cluster mode. cluster.init flips
|
|
301
|
+
// cluster.isClusterMode() true so clusterStorage routes the audit /
|
|
302
|
+
// break-glass / consent SQL to the external Postgres backend.
|
|
303
|
+
b.externalDb.init({
|
|
304
|
+
backends: {
|
|
305
|
+
ops: {
|
|
306
|
+
connect: driver.connect, query: driver.query, close: driver.close,
|
|
307
|
+
dialect: "postgres",
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
await b.frameworkSchema.ensureSchema({ externalDbBackend: "ops", dialect: "postgres" });
|
|
312
|
+
check("ensureSchema created _blamejs_audit_log + break-glass tables on real Postgres",
|
|
313
|
+
/\b1\b/.test(_psql(
|
|
314
|
+
"SELECT count(*) AS n FROM information_schema.tables " +
|
|
315
|
+
"WHERE table_name = '_blamejs_break_glass_grants';").trim()));
|
|
316
|
+
|
|
317
|
+
await b.cluster.init({
|
|
318
|
+
nodeId: "audit-stack-node",
|
|
319
|
+
role: "leader",
|
|
320
|
+
externalDbBackend: "ops",
|
|
321
|
+
dialect: "postgres",
|
|
322
|
+
});
|
|
323
|
+
check("cluster.init acquired leadership on real Postgres (gates every chain append)",
|
|
324
|
+
b.cluster.isLeader() === true);
|
|
325
|
+
check("framework is in cluster mode → framework SQL routes to external Postgres",
|
|
326
|
+
b.clusterStorage.tableName("audit_log") === "_blamejs_audit_log");
|
|
327
|
+
|
|
328
|
+
// A dedicated driver session for out-of-band readback assertions
|
|
329
|
+
// keyed exactly as node-postgres keys rows.
|
|
330
|
+
driverClient = await driver.connect();
|
|
331
|
+
async function liveQueryAll(sql, params) {
|
|
332
|
+
var r = await driver.query(driverClient, sql, params || []);
|
|
333
|
+
return r.rows;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await _testAuditRecordAndChain(liveQueryAll);
|
|
337
|
+
await _testCheckpointAndFence(liveQueryAll);
|
|
338
|
+
await _testCoercionFidelity(liveQueryAll);
|
|
339
|
+
await _testAuditToolsBundleAndPurge(tmpDir);
|
|
340
|
+
await _testBreakGlass();
|
|
341
|
+
await _testCryptoFieldKRowRoundTrip(liveQueryAll);
|
|
342
|
+
await _testTamperDetection();
|
|
343
|
+
|
|
344
|
+
} finally {
|
|
345
|
+
try { if (driverClient) await driver.close(driverClient); } catch (_e) {}
|
|
346
|
+
try { await b.cluster.shutdown(); } catch (_e) {}
|
|
347
|
+
try { await b.externalDb.shutdown(); } catch (_e) {}
|
|
348
|
+
try { await teardownTestDb(tmpDir); } catch (_e) {}
|
|
349
|
+
try { _dropFrameworkTables(); } catch (_e) {}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ====================================================================
|
|
354
|
+
// 1. audit.record() → chain-writer._insertRow on real Postgres. Drives
|
|
355
|
+
// the FULL primitive: counter primer (MAX(monotonicCounter)), tip read,
|
|
356
|
+
// cryptoField seal, null-fill, computeRowHash, INSERT with prevHash /
|
|
357
|
+
// rowHash / nonce / fencingToken, then verify() (the reader b.audit
|
|
358
|
+
// ships). A correct framework verifies ok:true — the CORE proof.
|
|
359
|
+
// ====================================================================
|
|
360
|
+
async function _testAuditRecordAndChain(liveQueryAll) {
|
|
361
|
+
var events = [
|
|
362
|
+
{ action: "system.boot", outcome: "success" },
|
|
363
|
+
{ action: "auth.login.success", outcome: "success",
|
|
364
|
+
actor: { userId: "u-1", ip: "10.0.0.7" } },
|
|
365
|
+
{ action: "consent.granted", outcome: "success",
|
|
366
|
+
actor: { userId: "u-2" },
|
|
367
|
+
resource: { kind: "purpose", id: "marketing" },
|
|
368
|
+
metadata: { region: "eu" } },
|
|
369
|
+
{ action: "system.shutdown", outcome: "success" },
|
|
370
|
+
];
|
|
371
|
+
var appended = [];
|
|
372
|
+
for (var i = 0; i < events.length; i++) {
|
|
373
|
+
appended.push(await b.audit.record(events[i]));
|
|
374
|
+
}
|
|
375
|
+
check("audit.record returned a monotonic counter per row (1..4)",
|
|
376
|
+
appended[0].monotonicCounter === 1 && appended[3].monotonicCounter === 4);
|
|
377
|
+
|
|
378
|
+
var count = _psql("SELECT count(*) AS n FROM _blamejs_audit_log;");
|
|
379
|
+
check("audit.record landed 4 rows in _blamejs_audit_log on real Postgres",
|
|
380
|
+
/\b4\b/.test(count.trim()));
|
|
381
|
+
|
|
382
|
+
// The reader b.audit.verify uses, against the live table.
|
|
383
|
+
var v = await b.audit.verify({});
|
|
384
|
+
check("audit.verify walks the live Postgres chain and returns ok:true " +
|
|
385
|
+
"(a valid chain on the operator's external DB must verify)", v.ok === true);
|
|
386
|
+
check("audit.verify counted every stored row (rowsVerified === 4)",
|
|
387
|
+
v.ok === true && v.rowsVerified === 4);
|
|
388
|
+
if (!v.ok) {
|
|
389
|
+
check("AUDIT-VERIFY DETAIL: verify reports '" + v.reason + "' at row " +
|
|
390
|
+
v.breakAt + " on an untampered live chain", false);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// The second row's prevHash must equal the first row's rowHash — chain
|
|
394
|
+
// linkage actually persisted, not a per-row island.
|
|
395
|
+
var linked = await liveQueryAll(
|
|
396
|
+
'SELECT "monotonicCounter", "prevHash", "rowHash" FROM _blamejs_audit_log ' +
|
|
397
|
+
'ORDER BY "monotonicCounter" ASC', []);
|
|
398
|
+
check("chain links across rows on Postgres (row2.prevHash === row1.rowHash)",
|
|
399
|
+
linked.length === 4 && linked[1].prevHash === linked[0].rowHash);
|
|
400
|
+
|
|
401
|
+
// Counter primer correctness: a brand-new chain-writer (fresh in-process
|
|
402
|
+
// state via record after flush) must read MAX(monotonicCounter) from the
|
|
403
|
+
// live table and continue, not restart at 1. Reset the audit module's
|
|
404
|
+
// chain-writer in-process counter and append once more.
|
|
405
|
+
b.audit._resetForTest();
|
|
406
|
+
// _resetForTest tore down cluster wiring's audit ties but cluster mode
|
|
407
|
+
// and externalDb remain; the counter primer re-reads MAX from Postgres.
|
|
408
|
+
var more = await b.audit.record({ action: "system.boot", outcome: "success" });
|
|
409
|
+
check("counter primer read MAX(monotonicCounter) from live Postgres on a " +
|
|
410
|
+
"fresh chain-writer (continued at 5, did not restart at 1)",
|
|
411
|
+
more.monotonicCounter === 5);
|
|
412
|
+
var count2 = _psql("SELECT count(*) AS n FROM _blamejs_audit_log;");
|
|
413
|
+
check("5 audit rows now present after primer-continued append",
|
|
414
|
+
/\b5\b/.test(count2.trim()));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ====================================================================
|
|
418
|
+
// 2. audit.checkpoint() → _insertCheckpoint + _upsertAuditTip fencing
|
|
419
|
+
// guard on real Postgres. The tip UPSERT's WHERE clause
|
|
420
|
+
// (storedToken <= EXCLUDED.token) is the canonical fencing-token guard;
|
|
421
|
+
// a strictly-lower incoming token must be FENCED_OUT.
|
|
422
|
+
// ====================================================================
|
|
423
|
+
async function _testCheckpointAndFence(liveQueryAll) {
|
|
424
|
+
var ck = await b.audit.checkpoint({});
|
|
425
|
+
check("audit.checkpoint anchored the live chain tip", ck && ck.atMonotonicCounter === 5);
|
|
426
|
+
|
|
427
|
+
var ckCount = _psql("SELECT count(*) AS n FROM _blamejs_audit_checkpoints;");
|
|
428
|
+
check("checkpoint row landed in _blamejs_audit_checkpoints on Postgres",
|
|
429
|
+
/\b1\b/.test(ckCount.trim()));
|
|
430
|
+
|
|
431
|
+
var tip = await liveQueryAll(
|
|
432
|
+
'SELECT "scope", "atMonotonicCounter", "fencingToken" FROM _blamejs_audit_tip ' +
|
|
433
|
+
"WHERE scope = 'audit'", []);
|
|
434
|
+
check("_upsertAuditTip wrote the single audit-tip row on Postgres",
|
|
435
|
+
tip.length === 1 && tip[0].scope === "audit");
|
|
436
|
+
check("audit-tip atMonotonicCounter coerced BIGINT→number and matches the chain tip",
|
|
437
|
+
Number(tip[0].atMonotonicCounter) === 5);
|
|
438
|
+
|
|
439
|
+
// verifyCheckpoints walks the live checkpoints + confirms the anchored
|
|
440
|
+
// row still has its rowHash (ML-DSA signature verify + row match).
|
|
441
|
+
var vc = await b.audit.verifyCheckpoints();
|
|
442
|
+
check("audit.verifyCheckpoints returns ok:true against the live Postgres checkpoint",
|
|
443
|
+
vc.ok === true && vc.checkpointsVerified === 1);
|
|
444
|
+
if (!vc.ok) {
|
|
445
|
+
check("VERIFY-CHECKPOINTS DETAIL: '" + vc.reason + "' at " + vc.breakAt, false);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Fencing-token guard. The audit-tip currently holds the leader's
|
|
449
|
+
// fencingToken. Directly UPSERT a HIGHER token (accepted) then attempt a
|
|
450
|
+
// LOWER token (must be fenced out → 0 RETURNING rows). We exercise the
|
|
451
|
+
// exact b.sql UPSERT shape the framework emits via a raw psql round-trip
|
|
452
|
+
// so the DB-level guard is what's tested, not application state.
|
|
453
|
+
var curTok = Number(tip[0].fencingToken);
|
|
454
|
+
// Higher token accepted: storedToken <= EXCLUDED.token.
|
|
455
|
+
var higher = _psql(
|
|
456
|
+
'INSERT INTO _blamejs_audit_tip ("scope","atMonotonicCounter","rowHash","signedAt","fencingToken") ' +
|
|
457
|
+
"VALUES ('audit', 5, 'h', 's', " + (curTok + 10) + ") " +
|
|
458
|
+
'ON CONFLICT ("scope") DO UPDATE SET "fencingToken" = EXCLUDED."fencingToken" ' +
|
|
459
|
+
'WHERE _blamejs_audit_tip."fencingToken" <= EXCLUDED."fencingToken" ' +
|
|
460
|
+
'RETURNING "fencingToken";');
|
|
461
|
+
check("fencing guard ACCEPTS a higher incoming token (RETURNING produced a row)",
|
|
462
|
+
new RegExp("\\b" + (curTok + 10) + "\\b").test(higher));
|
|
463
|
+
// Lower token rejected: WHERE storedToken(curTok+10) <= EXCLUDED(curTok+1) is false.
|
|
464
|
+
var lower = _psql(
|
|
465
|
+
'INSERT INTO _blamejs_audit_tip ("scope","atMonotonicCounter","rowHash","signedAt","fencingToken") ' +
|
|
466
|
+
"VALUES ('audit', 5, 'h2', 's2', " + (curTok + 1) + ") " +
|
|
467
|
+
'ON CONFLICT ("scope") DO UPDATE SET "fencingToken" = EXCLUDED."fencingToken" ' +
|
|
468
|
+
'WHERE _blamejs_audit_tip."fencingToken" <= EXCLUDED."fencingToken" ' +
|
|
469
|
+
'RETURNING "fencingToken";');
|
|
470
|
+
// The DO UPDATE ... WHERE that filters out the row yields no RETURNING
|
|
471
|
+
// output (no data line; only the "INSERT 0 0" tag).
|
|
472
|
+
check("fencing guard REJECTS a strictly-lower incoming token (0 RETURNING rows → FENCED_OUT)",
|
|
473
|
+
lower.indexOf(String(curTok + 1)) === -1);
|
|
474
|
+
var stillHigh = _psql('SELECT "fencingToken" FROM _blamejs_audit_tip WHERE scope=\'audit\';');
|
|
475
|
+
check("stored fencingToken stayed at the higher value (lower token did not overwrite)",
|
|
476
|
+
new RegExp("\\b" + (curTok + 10) + "\\b").test(stillHigh.trim()));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ====================================================================
|
|
480
|
+
// 3. Coercion fidelity on the live readback: the framework reader expects
|
|
481
|
+
// camelCase keys + number counters + Buffer nonces, but Postgres hands
|
|
482
|
+
// BIGINT back as a STRING and BYTEA as a Buffer. clusterStorage.coerceRows
|
|
483
|
+
// must normalize. We read THROUGH clusterStorage (the framework path) and
|
|
484
|
+
// assert the normalized JS shape — not the raw driver shape.
|
|
485
|
+
// ====================================================================
|
|
486
|
+
async function _testCoercionFidelity() {
|
|
487
|
+
// Compose via b.sql so the camelCase column is double-quoted (Postgres
|
|
488
|
+
// folds an unquoted identifier to lowercase). Bare logical table name —
|
|
489
|
+
// clusterStorage rewrites audit_log → _blamejs_audit_log + coerces the row.
|
|
490
|
+
var built = require("../../lib/sql").select("audit_log", { dialect: "sqlite" })
|
|
491
|
+
.orderBy("monotonicCounter", "asc")
|
|
492
|
+
.limit(1)
|
|
493
|
+
.toSql();
|
|
494
|
+
var rows = await b.clusterStorage.executeAll(built.sql, built.params);
|
|
495
|
+
check("clusterStorage.executeAll read an audit row back from live Postgres",
|
|
496
|
+
rows.length === 1);
|
|
497
|
+
var row = rows[0];
|
|
498
|
+
check("coercion: monotonicCounter is a JS number after clusterStorage.coerceRows " +
|
|
499
|
+
"(node-postgres handed it back as a BIGINT string)",
|
|
500
|
+
typeof row.monotonicCounter === "number" && row.monotonicCounter === 1);
|
|
501
|
+
check("coercion: recordedAt BIGINT coerced to a JS number",
|
|
502
|
+
typeof row.recordedAt === "number");
|
|
503
|
+
check("coercion: nonce BYTEA coerced to a Node Buffer",
|
|
504
|
+
Buffer.isBuffer(row.nonce));
|
|
505
|
+
check("coercion: rowHash stays a string under the camelCase key the reader uses",
|
|
506
|
+
typeof row.rowHash === "string" && row.rowHash.length > 0);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ====================================================================
|
|
510
|
+
// 4. audit-tools exportSlice → verifyBundle round-trip reading from the
|
|
511
|
+
// live Postgres audit_log via the DEFAULT clusterStorage readers, then
|
|
512
|
+
// archive (needs a covering checkpoint) → verifyBundle, then the purge
|
|
513
|
+
// monotonic gate + the live anchor UPSERT through clusterStorage.
|
|
514
|
+
// ====================================================================
|
|
515
|
+
async function _testAuditToolsBundleAndPurge(tmpDir) {
|
|
516
|
+
var pass = Buffer.from("audit-bundle-passphrase-not-secret-1234567890", "utf8");
|
|
517
|
+
|
|
518
|
+
// exportSlice reads rows from the live Postgres audit_log (default
|
|
519
|
+
// _defaultReadRows → clusterStorage.executeAll) and writes an encrypted
|
|
520
|
+
// bundle to disk. audit-tools refuses an existing out dir — pass a fresh
|
|
521
|
+
// (non-existent) path under tmpDir.
|
|
522
|
+
var exDir = path.join(tmpDir, "export-bundle");
|
|
523
|
+
var ex = await b.auditTools.exportSlice({ out: exDir, passphrase: pass });
|
|
524
|
+
check("audit-tools.exportSlice read the live Postgres chain + wrote a bundle " +
|
|
525
|
+
"(rowCount === 5)", ex.rowCount === 5);
|
|
526
|
+
|
|
527
|
+
var exVerify = await b.auditTools.verifyBundle({ in: exDir, passphrase: pass });
|
|
528
|
+
check("audit-tools.verifyBundle round-trips the exported live-Postgres slice " +
|
|
529
|
+
"(ok:true, walks the prevHash→rowHash chain)",
|
|
530
|
+
exVerify.ok === true && exVerify.rowsVerified === 5);
|
|
531
|
+
if (!exVerify.ok) {
|
|
532
|
+
check("EXPORT-VERIFY DETAIL: '" + exVerify.reason + "'", false);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// archive needs a covering checkpoint (we wrote one at counter 5) and a
|
|
536
|
+
// `before` boundary newer than every row. recordedAt is Date.now()-based,
|
|
537
|
+
// so a `before` of now+1h covers all rows.
|
|
538
|
+
var arDir = path.join(tmpDir, "archive-bundle");
|
|
539
|
+
var ar = await b.auditTools.archive({
|
|
540
|
+
out: arDir,
|
|
541
|
+
before: Date.now() + b.constants.TIME.hours(1),
|
|
542
|
+
passphrase: pass,
|
|
543
|
+
});
|
|
544
|
+
check("audit-tools.archive bundled every live-Postgres row under a covering " +
|
|
545
|
+
"checkpoint (rowCount === 5)", ar.rowCount === 5);
|
|
546
|
+
|
|
547
|
+
var arVerify = await b.auditTools.verifyBundle({ in: arDir, passphrase: pass });
|
|
548
|
+
check("audit-tools.verifyBundle confirms the archive chain + checkpoint signature " +
|
|
549
|
+
"over live-Postgres rows (ok:true)", arVerify.ok === true);
|
|
550
|
+
check("archive bundle is kind 'archive' (carries the off-chain checkpoint anchor)",
|
|
551
|
+
arVerify.kind === "archive");
|
|
552
|
+
|
|
553
|
+
// purge monotonic gate. Drive the real purge() flow but inject a no-op
|
|
554
|
+
// apply so we exercise the verifyBundle + monotonic-anchor logic against
|
|
555
|
+
// the live archive WITHOUT physically deleting the chain mid-test (the
|
|
556
|
+
// local purgeAuditChain path is single-node-only). Then separately
|
|
557
|
+
// exercise the LIVE anchor UPSERT through clusterStorage so the
|
|
558
|
+
// _blamejs_audit_purge_anchor table's b.sql UPSERT is proven on Postgres.
|
|
559
|
+
var applied = null;
|
|
560
|
+
var purgeRes = await b.auditTools.purge({
|
|
561
|
+
confirm: true,
|
|
562
|
+
archive: arDir,
|
|
563
|
+
passphrase: pass,
|
|
564
|
+
readAnchor: function () { return Promise.resolve(null); }, // origin: first purge
|
|
565
|
+
apply: function (args) {
|
|
566
|
+
applied = args;
|
|
567
|
+
return Promise.resolve({
|
|
568
|
+
rowsDeleted: ar.rowCount, checkpointsDeleted: 0,
|
|
569
|
+
archiveBundleId: args.archiveBundleId,
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
check("audit-tools.purge verified the archive + passed the monotonic gate " +
|
|
574
|
+
"(firstCounter===1 from origin) and reported rowsDeleted",
|
|
575
|
+
purgeRes.purged === true && purgeRes.rowsDeleted === 5 &&
|
|
576
|
+
applied && Number(applied.lastPurgedCounter) === 5);
|
|
577
|
+
|
|
578
|
+
// Now prove the live anchor UPSERT (the only piece of purge's default
|
|
579
|
+
// apply that targets the external DB via clusterStorage) actually runs on
|
|
580
|
+
// Postgres: run _defaultApplyPurge's anchor write shape directly.
|
|
581
|
+
await b.clusterStorage.execute(
|
|
582
|
+
'INSERT INTO _blamejs_audit_purge_anchor ' +
|
|
583
|
+
'("scope","lastPurgedCounter","lastPurgedRowHash","archiveBundleId","purgedAt") ' +
|
|
584
|
+
"VALUES ('audit', ?, ?, ?, ?) " +
|
|
585
|
+
'ON CONFLICT ("scope") DO UPDATE SET ' +
|
|
586
|
+
'"lastPurgedCounter" = EXCLUDED."lastPurgedCounter", ' +
|
|
587
|
+
'"lastPurgedRowHash" = EXCLUDED."lastPurgedRowHash", ' +
|
|
588
|
+
'"archiveBundleId" = EXCLUDED."archiveBundleId", ' +
|
|
589
|
+
'"purgedAt" = EXCLUDED."purgedAt"',
|
|
590
|
+
[5, "anchor-hash", "bundle-1", Date.now()]);
|
|
591
|
+
var anchorReadBack = await b.clusterStorage.executeOne(
|
|
592
|
+
'SELECT "lastPurgedCounter", "lastPurgedRowHash" FROM _blamejs_audit_purge_anchor WHERE "scope" = ?',
|
|
593
|
+
["audit"]);
|
|
594
|
+
check("purge anchor UPSERT through b.sql + clusterStorage landed on Postgres + " +
|
|
595
|
+
"coerced lastPurgedCounter BIGINT→number",
|
|
596
|
+
anchorReadBack && anchorReadBack.lastPurgedCounter === 5 &&
|
|
597
|
+
anchorReadBack.lastPurgedRowHash === "anchor-hash");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ====================================================================
|
|
601
|
+
// 5. break-glass policy + grant + unsealRow consume — the WHOLE flow
|
|
602
|
+
// routed through clusterStorage to live Postgres: policy UPSERT (sealed),
|
|
603
|
+
// policy.get/list (unseal + decode), grant (TOTP factor verify → sealed
|
|
604
|
+
// grant row INSERT, with the issuedToActorHash derived hash NOT NULL
|
|
605
|
+
// populated), then unsealRow (grant fetch + atomic rowsConsumed++ +
|
|
606
|
+
// glass-locked column unseal of a real app row stored on Postgres).
|
|
607
|
+
// ====================================================================
|
|
608
|
+
async function _testBreakGlass() {
|
|
609
|
+
b.breakGlass.init({ trustProxy: false });
|
|
610
|
+
|
|
611
|
+
// The glass-locked app table is an OPERATOR table — the framework does
|
|
612
|
+
// not own its DDL. In cluster mode break-glass reads it from the external
|
|
613
|
+
// Postgres, so create it there with the same column shape db.init
|
|
614
|
+
// registered for cryptoField sealing.
|
|
615
|
+
_psql('CREATE TABLE IF NOT EXISTS patients (' +
|
|
616
|
+
'"_id" TEXT PRIMARY KEY, "mrn" TEXT, "ssn" TEXT, ' +
|
|
617
|
+
'"residency" TEXT, "notes" TEXT);');
|
|
618
|
+
|
|
619
|
+
// Seed a glass-locked app row in Postgres via the framework's own write
|
|
620
|
+
// path (clusterStorage) so the SSN column is cryptoField-sealed on disk.
|
|
621
|
+
var patient = b.cryptoField.sealRow("patients", {
|
|
622
|
+
_id: "patient-001", mrn: "MRN-1", ssn: "123-45-6789",
|
|
623
|
+
residency: "eu", notes: "high blood pressure",
|
|
624
|
+
});
|
|
625
|
+
await b.clusterStorage.execute(
|
|
626
|
+
'INSERT INTO patients ("_id","mrn","ssn","residency","notes") VALUES (?,?,?,?,?)',
|
|
627
|
+
[patient._id, patient.mrn, patient.ssn, patient.residency, patient.notes]);
|
|
628
|
+
var sealedOnDisk = _psql("SELECT ssn FROM patients WHERE _id = 'patient-001';");
|
|
629
|
+
check("break-glass: glass-locked ssn is stored SEALED (vault:-prefixed) on Postgres",
|
|
630
|
+
/vault[:.]/.test(sealedOnDisk.trim()));
|
|
631
|
+
|
|
632
|
+
// policy.set → UPSERT into _blamejs_break_glass_policies (sealed columns).
|
|
633
|
+
var setRes = await b.breakGlass.policy.set("patients", {
|
|
634
|
+
columns: ["ssn", "notes"],
|
|
635
|
+
factors: ["totp"],
|
|
636
|
+
grantTtl: b.constants.TIME.minutes(15),
|
|
637
|
+
maxRowsPerGrant: 1,
|
|
638
|
+
reasonMinLength: 12,
|
|
639
|
+
pinIp: false,
|
|
640
|
+
sessionPin: false,
|
|
641
|
+
});
|
|
642
|
+
check("break-glass: policy.set UPSERT landed on Postgres", setRes.applied === true);
|
|
643
|
+
var polCount = _psql("SELECT count(*) AS n FROM _blamejs_break_glass_policies;");
|
|
644
|
+
check("break-glass: one policy row physically present on Postgres",
|
|
645
|
+
/\b1\b/.test(polCount.trim()));
|
|
646
|
+
|
|
647
|
+
// policy.get round-trips the sealed/encoded policy from Postgres.
|
|
648
|
+
var got = await b.breakGlass.policy.get("patients");
|
|
649
|
+
check("break-glass: policy.get reads + unseals the Postgres policy row",
|
|
650
|
+
got && got.table === "patients" &&
|
|
651
|
+
got.columns.length === 2 && got.columns.indexOf("ssn") !== -1);
|
|
652
|
+
check("break-glass: policy numeric fields coerced (grantTtl is a number)",
|
|
653
|
+
typeof got.grantTtl === "number" && got.grantTtl > 0);
|
|
654
|
+
|
|
655
|
+
var listed = await b.breakGlass.policy.list();
|
|
656
|
+
check("break-glass: policy.list enumerates the glass-locked table from Postgres",
|
|
657
|
+
listed.length === 1 && listed[0].table === "patients");
|
|
658
|
+
|
|
659
|
+
// grant — mint a TOTP-backed grant. Generate a real TOTP secret + a code
|
|
660
|
+
// valid at a fixed clock the verifier is threaded with.
|
|
661
|
+
var totpSecret = b.auth.totp.generateSecret();
|
|
662
|
+
var nowMs = Date.now();
|
|
663
|
+
var code = b.auth.totp.generate(totpSecret, { now: nowMs });
|
|
664
|
+
var req = {
|
|
665
|
+
user: { id: "dr-house", scopes: [] },
|
|
666
|
+
socket: { remoteAddress: "127.0.0.1" },
|
|
667
|
+
headers: { "user-agent": "test-agent" },
|
|
668
|
+
method: "POST",
|
|
669
|
+
url: "/admin/break-glass",
|
|
670
|
+
};
|
|
671
|
+
var handle = await b.breakGlass.grant({
|
|
672
|
+
req: req,
|
|
673
|
+
table: "patients",
|
|
674
|
+
columns: ["ssn"],
|
|
675
|
+
reason: "ER admit verifying identity for patient-001",
|
|
676
|
+
factor: { type: "totp", secret: totpSecret, code: code, now: nowMs },
|
|
677
|
+
});
|
|
678
|
+
check("break-glass: grant minted after live TOTP verify (handle has an id)",
|
|
679
|
+
handle && typeof handle.id === "string" && handle.id.indexOf("bg-") === 0);
|
|
680
|
+
|
|
681
|
+
var grantCount = _psql("SELECT count(*) AS n FROM _blamejs_break_glass_grants;");
|
|
682
|
+
check("break-glass: grant row physically landed on Postgres", /\b1\b/.test(grantCount.trim()));
|
|
683
|
+
// The grants DDL declares issuedToActorHash TEXT NOT NULL; the cryptoField
|
|
684
|
+
// derived hash must have populated it, or the INSERT would have failed the
|
|
685
|
+
// NOT NULL constraint on Postgres (strict) — assert it's non-null.
|
|
686
|
+
var hashCell = _psql('SELECT "issuedToActorHash" FROM _blamejs_break_glass_grants LIMIT 1;');
|
|
687
|
+
check("break-glass: issuedToActorHash NOT-NULL derived column populated on Postgres",
|
|
688
|
+
hashCell.trim().length > 0 && !/__BJNULL__/.test(hashCell));
|
|
689
|
+
|
|
690
|
+
// unsealRow consume — fetch grant from Postgres, atomic rowsConsumed++,
|
|
691
|
+
// unseal the glass-locked ssn of the real Postgres-stored row.
|
|
692
|
+
var unsealed = await b.breakGlass.unsealRow(handle, "patients", "patient-001");
|
|
693
|
+
check("break-glass: unsealRow returned the decrypted glass-locked ssn",
|
|
694
|
+
unsealed && unsealed.ssn === "123-45-6789");
|
|
695
|
+
|
|
696
|
+
var consumed = _psql('SELECT "rowsConsumed" FROM _blamejs_break_glass_grants LIMIT 1;');
|
|
697
|
+
check("break-glass: atomic rowsConsumed++ persisted on Postgres (now 1)",
|
|
698
|
+
/\b1\b/.test(consumed.trim()));
|
|
699
|
+
|
|
700
|
+
// Second consume on a maxRowsPerGrant:1 grant must be refused (exhausted).
|
|
701
|
+
var exhaustedErr = null;
|
|
702
|
+
try { await b.breakGlass.unsealRow(handle, "patients", "patient-001"); }
|
|
703
|
+
catch (e) { exhaustedErr = e; }
|
|
704
|
+
check("break-glass: second unseal refused — grant exhausted (row-by-row auth on Postgres)",
|
|
705
|
+
exhaustedErr && /exhausted/i.test((exhaustedErr.code || "") + (exhaustedErr.message || "")));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ====================================================================
|
|
709
|
+
// 6. crypto-field K_row (vault.row:) sealed cell stored on Postgres + read
|
|
710
|
+
// back, proving the typed codec (Buffer / object / string) survives a
|
|
711
|
+
// real TEXT round-trip + clusterStorage coercion. The wrapped row-secret
|
|
712
|
+
// lives in the LOCAL per-row-keys registry (its by-design home); the
|
|
713
|
+
// sealed CELL is what lands on Postgres.
|
|
714
|
+
// ====================================================================
|
|
715
|
+
async function _testCryptoFieldKRowRoundTrip(liveQueryAll) {
|
|
716
|
+
b.cryptoField.declarePerRowKey("krow_demo", { keySize: 32 });
|
|
717
|
+
b.cryptoField.registerTable("krow_demo", { sealedFields: ["secret", "blobCol", "objCol"] });
|
|
718
|
+
|
|
719
|
+
// b.db itself is the local-db handle (exposes .prepare()); the per-row-key
|
|
720
|
+
// registry (_blamejs_per_row_keys) is a LOCAL-only table by design — the
|
|
721
|
+
// wrapped row-secret never leaves the framework's own db, while the sealed
|
|
722
|
+
// CELL is what lands on Postgres.
|
|
723
|
+
var dbHandle = b.db;
|
|
724
|
+
check("crypto-field: local db handle (b.db) exposes .prepare() for the per-row-key registry",
|
|
725
|
+
typeof dbHandle.prepare === "function");
|
|
726
|
+
|
|
727
|
+
var rowId = "krow-row-1";
|
|
728
|
+
var kRow = b.cryptoField.materializePerRowKey("krow_demo", rowId, dbHandle);
|
|
729
|
+
check("crypto-field: materializePerRowKey produced a 32-byte K_row",
|
|
730
|
+
Buffer.isBuffer(kRow) && kRow.length === 32);
|
|
731
|
+
|
|
732
|
+
var origBuf = Buffer.from([0, 1, 2, 250, 251, 255]);
|
|
733
|
+
var origObj = { kind: "phi", level: 9 };
|
|
734
|
+
var sealed = b.cryptoField.sealRow("krow_demo",
|
|
735
|
+
{ _id: rowId, secret: "top-secret-string", blobCol: origBuf, objCol: origObj },
|
|
736
|
+
{ kRow: kRow, rowId: rowId });
|
|
737
|
+
check("crypto-field: sealRow under K_row emitted vault.row: cells",
|
|
738
|
+
b.cryptoField.isRowSealed(sealed.secret) &&
|
|
739
|
+
b.cryptoField.isRowSealed(sealed.blobCol) &&
|
|
740
|
+
b.cryptoField.isRowSealed(sealed.objCol));
|
|
741
|
+
|
|
742
|
+
// Store the sealed cells on Postgres as TEXT, then read back.
|
|
743
|
+
_psql('CREATE TABLE IF NOT EXISTS krow_demo (' +
|
|
744
|
+
'"_id" TEXT PRIMARY KEY, "secret" TEXT, "blobCol" TEXT, "objCol" TEXT);');
|
|
745
|
+
await b.clusterStorage.execute(
|
|
746
|
+
'INSERT INTO krow_demo ("_id","secret","blobCol","objCol") VALUES (?,?,?,?)',
|
|
747
|
+
[rowId, sealed.secret, sealed.blobCol, sealed.objCol]);
|
|
748
|
+
|
|
749
|
+
var stored = (await liveQueryAll(
|
|
750
|
+
'SELECT "_id","secret","blobCol","objCol" FROM krow_demo WHERE "_id" = $1', [rowId]))[0];
|
|
751
|
+
check("crypto-field: vault.row: cells survived the Postgres TEXT round-trip intact",
|
|
752
|
+
stored.secret === sealed.secret && stored.blobCol === sealed.blobCol &&
|
|
753
|
+
stored.objCol === sealed.objCol);
|
|
754
|
+
|
|
755
|
+
// Unseal the read-back row under K_row — the typed codec must restore the
|
|
756
|
+
// ORIGINAL types (string / Buffer / object), proving no String() mangling
|
|
757
|
+
// across the real backend round-trip.
|
|
758
|
+
var unsealed = b.cryptoField.sealRow ? _unsealKRow("krow_demo", stored, kRow, rowId) : null;
|
|
759
|
+
check("crypto-field: K_row unseal restored the string value byte-for-byte",
|
|
760
|
+
unsealed.secret === "top-secret-string");
|
|
761
|
+
check("crypto-field: K_row unseal restored the Buffer value byte-for-byte " +
|
|
762
|
+
"(typed codec, NOT String()-mangled)",
|
|
763
|
+
Buffer.isBuffer(unsealed.blobCol) && unsealed.blobCol.equals(origBuf));
|
|
764
|
+
check("crypto-field: K_row unseal restored the object value (typed codec)",
|
|
765
|
+
unsealed.objCol && unsealed.objCol.kind === "phi" && unsealed.objCol.level === 9);
|
|
766
|
+
|
|
767
|
+
_psql("DROP TABLE IF EXISTS krow_demo;");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Unseal vault.row: cells under a known K_row without going through the
|
|
771
|
+
// dbHandle fetch path (we already hold kRow). Mirrors the framework's
|
|
772
|
+
// K_row decrypt: decryptPacked under the (table,rowId,column,schemaVersion)
|
|
773
|
+
// AAD + the typed-codec decode. Uses only exported framework crypto so the
|
|
774
|
+
// AAD bytes match the seal side.
|
|
775
|
+
function _unsealKRow(table, row, kRow, rowId) {
|
|
776
|
+
var vaultAad = require("../../lib/vault-aad");
|
|
777
|
+
var crypto = require("../../lib/crypto");
|
|
778
|
+
var ROW_PREFIX = require("../../lib/constants").ROW_PREFIX;
|
|
779
|
+
var out = Object.assign({}, row);
|
|
780
|
+
var cols = ["secret", "blobCol", "objCol"];
|
|
781
|
+
for (var i = 0; i < cols.length; i++) {
|
|
782
|
+
var col = cols[i];
|
|
783
|
+
var v = row[col];
|
|
784
|
+
if (typeof v !== "string" || v.indexOf(ROW_PREFIX) !== 0) continue;
|
|
785
|
+
var aad = vaultAad.canonicalizeAad(vaultAad.buildColumnAad({
|
|
786
|
+
table: table, rowId: rowId, column: col, schemaVersion: "1",
|
|
787
|
+
}));
|
|
788
|
+
var packed = Buffer.from(v.slice(ROW_PREFIX.length), "base64");
|
|
789
|
+
var plain = crypto.decryptPacked(packed, kRow, aad).toString("utf8");
|
|
790
|
+
out[col] = _decodeTyped(plain);
|
|
791
|
+
}
|
|
792
|
+
return out;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Mirror of crypto-field's _decodeTyped for the test-side K_row unseal.
|
|
796
|
+
var TYPED_SENTINEL = String.fromCharCode(0) + "bjsv1:";
|
|
797
|
+
function _decodeTyped(str) {
|
|
798
|
+
if (typeof str !== "string" || str.indexOf(TYPED_SENTINEL) !== 0) return str;
|
|
799
|
+
var body = str.slice(TYPED_SENTINEL.length);
|
|
800
|
+
var tag = body.slice(0, 2);
|
|
801
|
+
var payload = body.slice(2);
|
|
802
|
+
if (tag === "B:") return Buffer.from(payload, "base64");
|
|
803
|
+
if (tag === "J:") return JSON.parse(payload);
|
|
804
|
+
if (tag === "S:") return payload;
|
|
805
|
+
return str;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ====================================================================
|
|
809
|
+
// 7. Tamper detection on the live chain — drop the WORM triggers (privileged
|
|
810
|
+
// DB-write attacker), mutate a hashed column, confirm verify reports
|
|
811
|
+
// ok:false. Meaningful only because the clean chain verified ok:true.
|
|
812
|
+
// ====================================================================
|
|
813
|
+
async function _testTamperDetection() {
|
|
814
|
+
_psql([
|
|
815
|
+
"DROP TRIGGER IF EXISTS no_update__blamejs_audit_log ON _blamejs_audit_log;",
|
|
816
|
+
"DROP TRIGGER IF EXISTS no_delete__blamejs_audit_log ON _blamejs_audit_log;",
|
|
817
|
+
].join("\n"));
|
|
818
|
+
_psql("UPDATE _blamejs_audit_log SET action = 'auth.login.tampered' " +
|
|
819
|
+
'WHERE "monotonicCounter" = 2;');
|
|
820
|
+
var v = await b.audit.verify({});
|
|
821
|
+
check("audit.verify returns ok:false after a hashed column is tampered on Postgres",
|
|
822
|
+
v.ok === false);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
module.exports = { run: run };
|
|
826
|
+
|
|
827
|
+
if (require.main === module) {
|
|
828
|
+
run().then(
|
|
829
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
|
|
830
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
831
|
+
);
|
|
832
|
+
}
|