@blamejs/blamejs-shop 0.4.31 → 0.4.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +400 -282
- package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
- package/lib/vendor/blamejs/.gitignore +6 -0
- package/lib/vendor/blamejs/CHANGELOG.md +28 -0
- package/lib/vendor/blamejs/MIGRATING.md +55 -0
- package/lib/vendor/blamejs/README.md +8 -6
- package/lib/vendor/blamejs/SECURITY.md +19 -3
- package/lib/vendor/blamejs/api-snapshot.json +2190 -664
- package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
- package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
- package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
- package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
- package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
- package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
- package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
- package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
- package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
- package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
- package/lib/vendor/blamejs/lib/api-key.js +158 -77
- package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
- package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
- package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
- package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
- package/lib/vendor/blamejs/lib/audit.js +259 -123
- package/lib/vendor/blamejs/lib/auth/elevation-grant.js +6 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +66 -9
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
- package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +36 -7
- package/lib/vendor/blamejs/lib/backup/index.js +45 -10
- package/lib/vendor/blamejs/lib/break-glass.js +355 -147
- package/lib/vendor/blamejs/lib/cache.js +174 -105
- package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
- package/lib/vendor/blamejs/lib/cli.js +19 -14
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
- package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
- package/lib/vendor/blamejs/lib/cluster.js +119 -71
- package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
- package/lib/vendor/blamejs/lib/compliance.js +210 -4
- package/lib/vendor/blamejs/lib/consent.js +82 -29
- package/lib/vendor/blamejs/lib/constants.js +27 -11
- package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
- package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
- package/lib/vendor/blamejs/lib/db-query.js +882 -260
- package/lib/vendor/blamejs/lib/db-schema.js +228 -44
- package/lib/vendor/blamejs/lib/db.js +249 -99
- package/lib/vendor/blamejs/lib/dsr.js +385 -55
- package/lib/vendor/blamejs/lib/error-page.js +14 -1
- package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
- package/lib/vendor/blamejs/lib/external-db.js +549 -34
- package/lib/vendor/blamejs/lib/file-upload.js +52 -7
- package/lib/vendor/blamejs/lib/framework-error.js +20 -1
- package/lib/vendor/blamejs/lib/framework-files.js +73 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
- package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
- package/lib/vendor/blamejs/lib/guard-all.js +1 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
- package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
- package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
- package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
- package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
- package/lib/vendor/blamejs/lib/guard-email.js +47 -69
- package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
- package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
- package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
- package/lib/vendor/blamejs/lib/guard-html.js +53 -108
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
- package/lib/vendor/blamejs/lib/guard-image.js +46 -103
- package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
- package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
- package/lib/vendor/blamejs/lib/guard-json.js +38 -108
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
- package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
- package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
- package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
- package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
- package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
- package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
- package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
- package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
- package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
- package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
- package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
- package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
- package/lib/vendor/blamejs/lib/guard-template.js +35 -172
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-time.js +32 -154
- package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
- package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
- package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
- package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
- package/lib/vendor/blamejs/lib/http-client.js +37 -9
- package/lib/vendor/blamejs/lib/inbox.js +120 -107
- package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
- package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
- package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
- package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
- package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
- package/lib/vendor/blamejs/lib/mail-store.js +293 -154
- package/lib/vendor/blamejs/lib/mail.js +8 -4
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
- package/lib/vendor/blamejs/lib/migrations.js +108 -66
- package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
- package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
- package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
- package/lib/vendor/blamejs/lib/observability.js +124 -0
- package/lib/vendor/blamejs/lib/otel-export.js +12 -3
- package/lib/vendor/blamejs/lib/outbox.js +184 -83
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
- package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
- package/lib/vendor/blamejs/lib/queue-local.js +225 -140
- package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
- package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
- package/lib/vendor/blamejs/lib/queue.js +7 -0
- package/lib/vendor/blamejs/lib/redact.js +68 -11
- package/lib/vendor/blamejs/lib/redis-client.js +160 -31
- package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
- package/lib/vendor/blamejs/lib/retention.js +117 -42
- package/lib/vendor/blamejs/lib/router.js +212 -5
- package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
- package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
- package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
- package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
- package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
- package/lib/vendor/blamejs/lib/safe-url.js +170 -3
- package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
- package/lib/vendor/blamejs/lib/scheduler.js +47 -12
- package/lib/vendor/blamejs/lib/seeders.js +122 -74
- package/lib/vendor/blamejs/lib/session-stores.js +42 -14
- package/lib/vendor/blamejs/lib/session.js +175 -77
- package/lib/vendor/blamejs/lib/sql.js +3842 -0
- package/lib/vendor/blamejs/lib/sse.js +26 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +169 -4
- package/lib/vendor/blamejs/lib/static.js +177 -34
- package/lib/vendor/blamejs/lib/subject.js +96 -49
- package/lib/vendor/blamejs/lib/vault/index.js +3 -2
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
- package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
- package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
- package/lib/vendor/blamejs/lib/websocket.js +35 -5
- package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
- package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
- package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
- package/lib/vendor/blamejs/release-notes/v0.15.7.json +43 -0
- package/lib/vendor/blamejs/scripts/check-services.js +21 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +67 -0
- package/lib/vendor/blamejs/scripts/release.js +398 -38
- package/lib/vendor/blamejs/test/00-primitives.js +168 -0
- package/lib/vendor/blamejs/test/10-state.js +140 -14
- package/lib/vendor/blamejs/test/20-db.js +65 -2
- package/lib/vendor/blamejs/test/helpers/db.js +9 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
- package/lib/vendor/blamejs/test/helpers/services.js +21 -0
- package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
- package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
- package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
- package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
- package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
- package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
- package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
- package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
- package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
- package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
- package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
- package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
- package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
- package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1196 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +362 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
- package/lib/vendor/blamejs/test/smoke.js +79 -21
- package/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
- package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
- package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
- package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
- package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
- package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
- package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live distributed-correctness proof against a REAL Postgres backend —
|
|
4
|
+
* not the single-process fakes the smoke tests use. Two things the
|
|
5
|
+
* framework advertises only hold if a real database enforces them:
|
|
6
|
+
*
|
|
7
|
+
* 1. EXACTLY-ONCE (b.scheduler, cluster mode): every fire INSERTs a
|
|
8
|
+
* row into _blamejs_scheduler_ticks keyed on the composite tickKey
|
|
9
|
+
* `name:scheduledAtUnix` with `ON CONFLICT (tickKey) DO NOTHING`.
|
|
10
|
+
* When two nodes race the SAME nominal tick, the PRIMARY KEY makes
|
|
11
|
+
* exactly one INSERT land — the loser's INSERT affects 0 rows and
|
|
12
|
+
* the scheduler skips its fire. The dedup is the DB's unique
|
|
13
|
+
* constraint, so this is only a real proof against a real DB.
|
|
14
|
+
*
|
|
15
|
+
* 2. FENCING (b.cluster fencing token): a stale leader holding a lower
|
|
16
|
+
* fencing token must be refused at the database layer when it
|
|
17
|
+
* attempts a fenced write after a newer leader took over. The
|
|
18
|
+
* canonical fenced write is the audit-tip upsert
|
|
19
|
+
* (`... ON CONFLICT (scope) DO UPDATE ... WHERE
|
|
20
|
+
* _blamejs_audit_tip.fencingToken <= EXCLUDED.fencingToken
|
|
21
|
+
* RETURNING fencingToken`, lib/audit.js _upsertAuditTip): the WHERE
|
|
22
|
+
* clause is the monotonic-non-decreasing guard. cluster-provider-
|
|
23
|
+
* mysql.test.js proves the lease-fencing-token issuance on MySQL;
|
|
24
|
+
* this extends both the issuance AND the tip CHECK to Postgres and
|
|
25
|
+
* ties the stale token to a real refused fenced operation.
|
|
26
|
+
*
|
|
27
|
+
* The "driver" is a docker-exec psql shim — a persistent
|
|
28
|
+
* docker exec -i blamejs-test-postgres psql -U blamejs -d blamejs_test ...
|
|
29
|
+
* subprocess per client (SQL fed over stdin, never argv — no shell
|
|
30
|
+
* parsing of SQL). It removes any npm pg-driver dep while exercising the
|
|
31
|
+
* framework's real Postgres SQL: the scheduler tick-claim, the cluster-
|
|
32
|
+
* provider-db lease/fencing-token SQL, and the audit-tip fencing guard.
|
|
33
|
+
*
|
|
34
|
+
* RUN: node scripts/test-integration.js --skip-service-check distributed-scheduler-fencing-pg
|
|
35
|
+
*
|
|
36
|
+
* STATUS: the EXACTLY-ONCE proof passes live; the FENCING proof exposes a
|
|
37
|
+
* live bug in lib/cluster-provider-db.js. Postgres folds unquoted column
|
|
38
|
+
* identifiers to lower case, so a real driver returns the leader row's
|
|
39
|
+
* columns as `nodeid` / `leaseid` / `fencingtoken` / `expiresat`, but the
|
|
40
|
+
* provider reads `row.nodeId` / `row.leaseId` / ... (camelCase). Every such
|
|
41
|
+
* read resolves to `undefined`: acquireLease's `row.nodeId !== nodeId` guard
|
|
42
|
+
* is always true so it returns null (the leader can never acquire), and
|
|
43
|
+
* currentLeader reports a phantom leader with `nodeId: undefined` and a NaN
|
|
44
|
+
* lease expiry. The columns are unaffected on MySQL (it preserves alias
|
|
45
|
+
* case), which is why cluster-provider-mysql.test.js passes. The fix is to
|
|
46
|
+
* double-quote the identifiers in the _blamejs_leader DDL and in every
|
|
47
|
+
* SELECT / RETURNING in lib/cluster-provider-db.js (or normalize row keys
|
|
48
|
+
* case-insensitively before the camelCase reads). This test asserts the
|
|
49
|
+
* CORRECT contract so it fails until that is fixed.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
var spawn = require("node:child_process").spawn;
|
|
53
|
+
var execFileSync = require("node:child_process").execFileSync;
|
|
54
|
+
var helpers = require("../helpers");
|
|
55
|
+
var check = helpers.check;
|
|
56
|
+
var services = require("../helpers/services");
|
|
57
|
+
var b = require("../../");
|
|
58
|
+
|
|
59
|
+
var CONTAINER = "blamejs-test-postgres";
|
|
60
|
+
|
|
61
|
+
// ---- one-shot psql (setup / teardown / out-of-band assertions) ----
|
|
62
|
+
// Shell-free SQL: the statement travels on stdin, never in argv.
|
|
63
|
+
function _psql(sql) {
|
|
64
|
+
var prelude = "\\pset fieldsep '\\t'\n";
|
|
65
|
+
var out = execFileSync(
|
|
66
|
+
"docker",
|
|
67
|
+
["exec", "-i", CONTAINER, "sh", "-c",
|
|
68
|
+
"psql -U blamejs -d blamejs_test -qtA -P null=__BJNULL__ 2>&1"],
|
|
69
|
+
{ input: prelude + sql + "\n", stdio: ["pipe", "pipe", "pipe"] }
|
|
70
|
+
).toString("utf8");
|
|
71
|
+
if (/^ERROR:/m.test(out)) {
|
|
72
|
+
throw new Error("psql setup failed for [" + sql + "]:\n" + out);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- persistent-session docker-exec psql driver ----
|
|
78
|
+
// Each client is a long-lived psql subprocess. Statements are written to
|
|
79
|
+
// its stdin terminated by an `\echo <sentinel>`; the driver reads merged
|
|
80
|
+
// stdout until the sentinel line, then parses the block. SQLSTATE-coded
|
|
81
|
+
// ERROR lines throw an Error carrying `.code`.
|
|
82
|
+
var PSQL_ARGS = "psql -U blamejs -d blamejs_test -A " +
|
|
83
|
+
"-v ON_ERROR_STOP=0 -P null=__BJNULL__ 2>&1";
|
|
84
|
+
var NULL_SENTINEL = "__BJNULL__";
|
|
85
|
+
var _seq = 0;
|
|
86
|
+
|
|
87
|
+
function _makeDockerPgDriver() {
|
|
88
|
+
return {
|
|
89
|
+
connect: function () {
|
|
90
|
+
return new Promise(function (resolve, reject) {
|
|
91
|
+
var child = spawn(
|
|
92
|
+
"docker",
|
|
93
|
+
["exec", "-i", CONTAINER, "sh", "-c",
|
|
94
|
+
PSQL_ARGS + " ; echo __BLAMEJS_PSQL_EXIT__"],
|
|
95
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
96
|
+
);
|
|
97
|
+
var client = {
|
|
98
|
+
child: child,
|
|
99
|
+
buf: "",
|
|
100
|
+
pending: null,
|
|
101
|
+
closed: false,
|
|
102
|
+
exitErr: null,
|
|
103
|
+
};
|
|
104
|
+
child.on("error", function (e) {
|
|
105
|
+
client.exitErr = e;
|
|
106
|
+
if (client.pending) { var p = client.pending; client.pending = null; p.reject(e); }
|
|
107
|
+
});
|
|
108
|
+
child.on("close", function () {
|
|
109
|
+
client.closed = true;
|
|
110
|
+
if (client.pending) {
|
|
111
|
+
var p = client.pending; client.pending = null;
|
|
112
|
+
p.reject(new Error("psql session closed mid-statement"));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
child.stdout.on("data", function (chunk) {
|
|
116
|
+
client.buf += chunk.toString("utf8");
|
|
117
|
+
_drain(client);
|
|
118
|
+
});
|
|
119
|
+
var primeSentinel = "__BJ_PRIME__";
|
|
120
|
+
client.pending = {
|
|
121
|
+
sentinel: primeSentinel,
|
|
122
|
+
resolve: function () { resolve(client); },
|
|
123
|
+
reject: reject,
|
|
124
|
+
};
|
|
125
|
+
client.child.stdin.write(
|
|
126
|
+
"\\pset fieldsep '\\t'\n\\pset footer off\n\\set VERBOSITY verbose\n" +
|
|
127
|
+
"\\echo " + primeSentinel + "\n");
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
query: function (client, sql, params) {
|
|
132
|
+
params = params || [];
|
|
133
|
+
var bound = _bindParams(sql, params);
|
|
134
|
+
var sentinel = "__BJ_EOR_" + (++_seq) + "__";
|
|
135
|
+
return new Promise(function (resolve, reject) {
|
|
136
|
+
if (client.closed) { reject(new Error("psql session is closed")); return; }
|
|
137
|
+
client.pending = { sentinel: sentinel, resolve: resolve, reject: reject };
|
|
138
|
+
client.child.stdin.write(bound + "\n;\n\\echo " + sentinel + "\n");
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
close: function (client) {
|
|
143
|
+
return new Promise(function (resolve) {
|
|
144
|
+
if (client.closed) { resolve(); return; }
|
|
145
|
+
try { client.child.stdin.end("\\q\n"); } catch (_e) { /* best effort */ }
|
|
146
|
+
var done = false;
|
|
147
|
+
client.child.on("close", function () { if (!done) { done = true; resolve(); } });
|
|
148
|
+
setTimeout(function () {
|
|
149
|
+
if (done) return;
|
|
150
|
+
done = true;
|
|
151
|
+
try { client.child.kill("SIGKILL"); } catch (_e) {}
|
|
152
|
+
resolve();
|
|
153
|
+
}, 2000);
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
dialect: "postgres",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _drain(client) {
|
|
162
|
+
if (!client.pending) return;
|
|
163
|
+
var sentinel = client.pending.sentinel;
|
|
164
|
+
var marker = "\n" + sentinel + "\n";
|
|
165
|
+
var idx = client.buf.indexOf(marker);
|
|
166
|
+
var startAtZero = client.buf.indexOf(sentinel + "\n") === 0;
|
|
167
|
+
var block;
|
|
168
|
+
if (idx !== -1) {
|
|
169
|
+
block = client.buf.slice(0, idx);
|
|
170
|
+
client.buf = client.buf.slice(idx + marker.length);
|
|
171
|
+
} else if (startAtZero) {
|
|
172
|
+
block = "";
|
|
173
|
+
client.buf = client.buf.slice((sentinel + "\n").length);
|
|
174
|
+
} else {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
var p = client.pending;
|
|
178
|
+
client.pending = null;
|
|
179
|
+
var parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = _parseBlock(block);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
return p.reject(e);
|
|
184
|
+
}
|
|
185
|
+
if (parsed.error) return p.reject(parsed.error);
|
|
186
|
+
p.resolve({ rows: parsed.rows, rowCount: parsed.rowCount });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Substitute Postgres $1/$2 placeholders with quoted literals. Every test
|
|
190
|
+
// value is operator-controlled (ids / nodeIds / tokens / numbers / null).
|
|
191
|
+
function _bindParams(sql, params) {
|
|
192
|
+
return sql.replace(/\$(\d+)/g, function (_m, n) {
|
|
193
|
+
var i = Number(n) - 1;
|
|
194
|
+
if (i < 0 || i >= params.length) {
|
|
195
|
+
throw new Error("placeholder $" + n + " has no matching param");
|
|
196
|
+
}
|
|
197
|
+
var v = params[i];
|
|
198
|
+
if (v === null || v === undefined) return "NULL";
|
|
199
|
+
if (typeof v === "number") return String(v);
|
|
200
|
+
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
|
201
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
var _CMD_TAG_RE = /^(INSERT|UPDATE|DELETE|MERGE|SELECT|COPY|MOVE)\b(?:\s+\d+)*\s*$/;
|
|
206
|
+
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/;
|
|
207
|
+
|
|
208
|
+
function _parseBlock(block) {
|
|
209
|
+
var lines = block.split(/\r?\n/);
|
|
210
|
+
while (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
211
|
+
|
|
212
|
+
for (var i = 0; i < lines.length; i++) {
|
|
213
|
+
var em = /^ERROR:\s+([0-9A-Za-z]{5}):\s*(.*)$/.exec(lines[i]);
|
|
214
|
+
if (em) {
|
|
215
|
+
var err = new Error("Postgres " + em[1] + ": " + em[2]);
|
|
216
|
+
err.code = em[1];
|
|
217
|
+
return { error: err };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var affected = null;
|
|
222
|
+
var dataLines = [];
|
|
223
|
+
for (var j = 0; j < lines.length; j++) {
|
|
224
|
+
var ln = lines[j];
|
|
225
|
+
if (/^(NOTICE|WARNING|DETAIL|HINT|LINE|LOCATION|CONTEXT|STATEMENT):/.test(ln)) continue;
|
|
226
|
+
var tm = _CMD_TAG_RE.exec(ln);
|
|
227
|
+
if (tm) {
|
|
228
|
+
var nums = ln.trim().split(/\s+/).slice(1).map(Number);
|
|
229
|
+
if (nums.length) affected = nums[nums.length - 1];
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (_CTRL_TAG_RE.test(ln) && ln.indexOf("\t") === -1) continue;
|
|
233
|
+
dataLines.push(ln);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
var rows = [];
|
|
237
|
+
if (dataLines.length >= 1) {
|
|
238
|
+
var headers = dataLines[0].split("\t");
|
|
239
|
+
for (var k = 1; k < dataLines.length; k++) {
|
|
240
|
+
var cells = dataLines[k].split("\t");
|
|
241
|
+
var row = {};
|
|
242
|
+
for (var c = 0; c < headers.length; c++) {
|
|
243
|
+
var cell = cells[c];
|
|
244
|
+
row[headers[c]] = (cell === NULL_SENTINEL || cell === undefined) ? null : cell;
|
|
245
|
+
}
|
|
246
|
+
rows.push(row);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
var rowCount = (affected !== null) ? affected : rows.length;
|
|
250
|
+
return { rows: rows, rowCount: rowCount, error: null };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// _blamejs_scheduler_ticks DDL (mirrors framework-schema _schedulerTicksDDL
|
|
254
|
+
// for the postgres dialect). The scheduler INSERTs through cluster-storage
|
|
255
|
+
// against this table when cluster mode is wired.
|
|
256
|
+
// Columns are double-quoted so Postgres preserves the camelCase exactly as
|
|
257
|
+
// framework-schema._schedulerTicksDDL emits them (quote-by-construction via
|
|
258
|
+
// safeSql.quoteIdentifier). The b.sql tick-claim INSERT quotes its columns,
|
|
259
|
+
// so an unquoted DDL here would create lowercased columns the quoted INSERT
|
|
260
|
+
// cannot find.
|
|
261
|
+
var SCHED_TICKS_DDL =
|
|
262
|
+
"CREATE TABLE IF NOT EXISTS _blamejs_scheduler_ticks (" +
|
|
263
|
+
' "tickKey" TEXT PRIMARY KEY,' +
|
|
264
|
+
' "name" TEXT NOT NULL,' +
|
|
265
|
+
' "scheduledAtUnix" BIGINT NOT NULL,' +
|
|
266
|
+
' "claimedAtUnix" BIGINT NOT NULL,' +
|
|
267
|
+
' "claimedBy" TEXT' +
|
|
268
|
+
")";
|
|
269
|
+
|
|
270
|
+
// _blamejs_audit_tip DDL (mirrors framework-schema _auditTipDDL for the
|
|
271
|
+
// postgres dialect) — the single-row coordination table whose
|
|
272
|
+
// fencingToken-monotonic WHERE clause is the canonical DB-layer fence.
|
|
273
|
+
var AUDIT_TIP_DDL =
|
|
274
|
+
"CREATE TABLE IF NOT EXISTS _blamejs_audit_tip (" +
|
|
275
|
+
' "scope" TEXT PRIMARY KEY,' +
|
|
276
|
+
' "atMonotonicCounter" BIGINT NOT NULL,' +
|
|
277
|
+
' "rowHash" TEXT,' +
|
|
278
|
+
' "signedAt" TEXT,' +
|
|
279
|
+
' "fencingToken" BIGINT NOT NULL DEFAULT 0,' +
|
|
280
|
+
" CHECK (\"scope\" = 'audit')" +
|
|
281
|
+
")";
|
|
282
|
+
|
|
283
|
+
async function run() {
|
|
284
|
+
var pg = await services.requireService("postgres");
|
|
285
|
+
if (!pg.ok) throw new Error("postgres unreachable: " + pg.reason);
|
|
286
|
+
|
|
287
|
+
// ---- fresh schema ----
|
|
288
|
+
_psql([
|
|
289
|
+
"DROP TABLE IF EXISTS _blamejs_scheduler_ticks;",
|
|
290
|
+
"DROP TABLE IF EXISTS _blamejs_audit_tip;",
|
|
291
|
+
"DROP TABLE IF EXISTS _blamejs_leader;",
|
|
292
|
+
"DROP TABLE IF EXISTS _blamejs_cluster_state;",
|
|
293
|
+
].join("\n"));
|
|
294
|
+
|
|
295
|
+
var driver = _makeDockerPgDriver();
|
|
296
|
+
b.cluster._resetForTest();
|
|
297
|
+
b.externalDb._resetForTest();
|
|
298
|
+
b.externalDb.init({
|
|
299
|
+
backends: {
|
|
300
|
+
ops: {
|
|
301
|
+
connect: driver.connect, query: driver.query, close: driver.close,
|
|
302
|
+
dialect: "postgres",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await _proveExactlyOnce();
|
|
309
|
+
await _proveFencing();
|
|
310
|
+
} finally {
|
|
311
|
+
// Best-effort teardown: shut cluster + externalDb, drop tables.
|
|
312
|
+
try { await b.cluster.shutdown(); } catch (_e) {}
|
|
313
|
+
b.cluster._resetForTest();
|
|
314
|
+
try { await b.externalDb.shutdown(); } catch (_e) {}
|
|
315
|
+
_psql([
|
|
316
|
+
"DROP TABLE IF EXISTS _blamejs_scheduler_ticks;",
|
|
317
|
+
"DROP TABLE IF EXISTS _blamejs_audit_tip;",
|
|
318
|
+
"DROP TABLE IF EXISTS _blamejs_leader;",
|
|
319
|
+
"DROP TABLE IF EXISTS _blamejs_cluster_state;",
|
|
320
|
+
].join("\n"));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ======================================================================
|
|
325
|
+
// 1. EXACTLY-ONCE — two racing nodes claim the SAME scheduled tick; the
|
|
326
|
+
// real PRIMARY KEY on tickKey lets exactly one win, the loser is
|
|
327
|
+
// rejected by the unique constraint (not by a hand-inserted row), and
|
|
328
|
+
// the job fires ONCE.
|
|
329
|
+
// ======================================================================
|
|
330
|
+
async function _proveExactlyOnce() {
|
|
331
|
+
_psql(SCHED_TICKS_DDL);
|
|
332
|
+
|
|
333
|
+
// Wire cluster.init against the real PG provider so cluster.isClusterMode()
|
|
334
|
+
// is true: the scheduler's tick-claim INSERT then routes through
|
|
335
|
+
// clusterStorage → externalDb → real Postgres. The provider's
|
|
336
|
+
// ensureSchema creates _blamejs_leader; we created the ticks table above.
|
|
337
|
+
//
|
|
338
|
+
// NOTE: the scheduler instances below use their OWN cluster views for the
|
|
339
|
+
// leader gate, so this proof does not depend on cluster.isLeader() — it
|
|
340
|
+
// depends only on isClusterMode() routing framework state to PG, which is
|
|
341
|
+
// true regardless of lease state. (cluster.isLeader() is in fact false
|
|
342
|
+
// here on Postgres — see the FENCING section for that bug.)
|
|
343
|
+
await b.cluster.init({
|
|
344
|
+
nodeId: "sched-node",
|
|
345
|
+
role: "leader",
|
|
346
|
+
leaseTtl: b.constants.TIME.seconds(30),
|
|
347
|
+
heartbeatInterval: b.constants.TIME.seconds(10),
|
|
348
|
+
externalDbBackend: "ops",
|
|
349
|
+
dialect: "postgres",
|
|
350
|
+
});
|
|
351
|
+
check("exactly-once: cluster is in cluster mode (state routes to PG)",
|
|
352
|
+
b.cluster.isClusterMode() === true);
|
|
353
|
+
|
|
354
|
+
// Two scheduler instances modelling two cluster nodes that both believe
|
|
355
|
+
// they're leader (the split-brain window the tick-claim defends). Each
|
|
356
|
+
// declares the SAME task name + same interval so a shared nominal
|
|
357
|
+
// scheduled time produces the SAME tickKey on both. We drive _fireOnce
|
|
358
|
+
// on both for the SAME nominal tick by pinning each task's nextRun to a
|
|
359
|
+
// shared instant before firing.
|
|
360
|
+
var firedByA = 0;
|
|
361
|
+
var firedByB = 0;
|
|
362
|
+
|
|
363
|
+
var clusterAView = {
|
|
364
|
+
isLeader: function () { return true; },
|
|
365
|
+
currentNodeId: function () { return "node-A"; },
|
|
366
|
+
};
|
|
367
|
+
var clusterBView = {
|
|
368
|
+
isLeader: function () { return true; },
|
|
369
|
+
currentNodeId: function () { return "node-B"; },
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
var schedA = b.scheduler.create({ cluster: clusterAView, audit: false });
|
|
373
|
+
var schedB = b.scheduler.create({ cluster: clusterBView, audit: false });
|
|
374
|
+
|
|
375
|
+
var taskA = schedA.schedule({
|
|
376
|
+
name: "rollup", every: b.constants.TIME.minutes(1),
|
|
377
|
+
run: async function () { firedByA++; },
|
|
378
|
+
});
|
|
379
|
+
var taskB = schedB.schedule({
|
|
380
|
+
name: "rollup", every: b.constants.TIME.minutes(1),
|
|
381
|
+
run: async function () { firedByB++; },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Pin both tasks to the SAME nominal scheduled instant so both compute
|
|
385
|
+
// the identical tickKey ("rollup:<sharedNominal>"). _fireOnce reads
|
|
386
|
+
// task.nextRun as the nominal run for the tick.
|
|
387
|
+
var sharedNominal = Date.now() + b.constants.TIME.minutes(1);
|
|
388
|
+
taskA.nextRun = sharedNominal;
|
|
389
|
+
taskB.nextRun = sharedNominal;
|
|
390
|
+
|
|
391
|
+
// Fire BOTH for the same tick concurrently. The two clusterStorage
|
|
392
|
+
// INSERTs hit the real PG PRIMARY KEY at once; exactly one lands.
|
|
393
|
+
schedA._fireOnce("rollup");
|
|
394
|
+
schedB._fireOnce("rollup");
|
|
395
|
+
|
|
396
|
+
// Wait until the tick-claim race has resolved on BOTH instances: the
|
|
397
|
+
// winner's fires===1 and the loser's tickClaimLost===1. Polling on the
|
|
398
|
+
// observable counters avoids a fixed sleep.
|
|
399
|
+
await helpers.waitUntil(function () {
|
|
400
|
+
var a = schedA.list()[0];
|
|
401
|
+
var bb = schedB.list()[0];
|
|
402
|
+
var aResolved = (a.fires === 1) || (a.tickClaimLost === 1);
|
|
403
|
+
var bResolved = (bb.fires === 1) || (bb.tickClaimLost === 1);
|
|
404
|
+
return aResolved && bResolved;
|
|
405
|
+
}, { timeoutMs: 15000, label: "exactly-once: both nodes' tick-claim race resolved" });
|
|
406
|
+
|
|
407
|
+
var aState = schedA.list()[0];
|
|
408
|
+
var bState = schedB.list()[0];
|
|
409
|
+
|
|
410
|
+
// Exactly one of the two nodes won the claim.
|
|
411
|
+
var winners = aState.fires + bState.fires;
|
|
412
|
+
var losers = aState.tickClaimLost + bState.tickClaimLost;
|
|
413
|
+
check("exactly-once: exactly ONE node won the tick-claim (fires totals 1)",
|
|
414
|
+
winners === 1);
|
|
415
|
+
check("exactly-once: exactly ONE node lost (tickClaimLost totals 1)",
|
|
416
|
+
losers === 1);
|
|
417
|
+
|
|
418
|
+
// The run-side effect fired exactly once across both nodes — the
|
|
419
|
+
// real proof the job did not double-execute.
|
|
420
|
+
check("exactly-once: the job's run() executed exactly ONCE across both nodes",
|
|
421
|
+
(firedByA + firedByB) === 1);
|
|
422
|
+
|
|
423
|
+
// The DB holds exactly one tick row for the shared tickKey — the
|
|
424
|
+
// PRIMARY KEY collapsed the racing INSERTs to one.
|
|
425
|
+
var tickRows = _psql(
|
|
426
|
+
"SELECT count(*) AS n FROM _blamejs_scheduler_ticks " +
|
|
427
|
+
"WHERE \"tickKey\" = 'rollup:" + sharedNominal + "';");
|
|
428
|
+
check("exactly-once: real PG holds exactly ONE tick row for the shared key",
|
|
429
|
+
/^1$/m.test(tickRows.trim()));
|
|
430
|
+
|
|
431
|
+
// The surviving row's claimedBy is the winner's nodeId — confirms the
|
|
432
|
+
// winner is the one whose INSERT actually landed, not a coincidence.
|
|
433
|
+
var winnerNode = _psql(
|
|
434
|
+
"SELECT \"claimedBy\" FROM _blamejs_scheduler_ticks " +
|
|
435
|
+
"WHERE \"tickKey\" = 'rollup:" + sharedNominal + "';").trim();
|
|
436
|
+
var expectedWinner = aState.fires === 1 ? "node-A" : "node-B";
|
|
437
|
+
check("exactly-once: surviving tick row's claimedBy is the node that fired",
|
|
438
|
+
winnerNode === expectedWinner);
|
|
439
|
+
|
|
440
|
+
// Control: a SECOND distinct nominal tick is independently claimable —
|
|
441
|
+
// proves the dedup is per-tickKey, not a one-shot table lock.
|
|
442
|
+
var secondNominal = sharedNominal + b.constants.TIME.minutes(1);
|
|
443
|
+
taskA.nextRun = secondNominal;
|
|
444
|
+
schedA._fireOnce("rollup");
|
|
445
|
+
await helpers.waitUntil(function () {
|
|
446
|
+
return schedA.list()[0].fires === 2;
|
|
447
|
+
}, { timeoutMs: 15000, label: "exactly-once: a second distinct tick is claimable" });
|
|
448
|
+
check("exactly-once: a distinct second tick fires (per-tickKey dedup, not a table lock)",
|
|
449
|
+
firedByA === 2 || (firedByA + firedByB) === 2);
|
|
450
|
+
|
|
451
|
+
await schedA.stop();
|
|
452
|
+
await schedB.stop();
|
|
453
|
+
|
|
454
|
+
// Tear down cluster wiring so the fencing section starts from a clean
|
|
455
|
+
// single source of truth (it re-uses _blamejs_leader through two
|
|
456
|
+
// direct provider instances, not cluster.init).
|
|
457
|
+
await b.cluster.shutdown();
|
|
458
|
+
b.cluster._resetForTest();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ======================================================================
|
|
462
|
+
// 2. FENCING — extend the MySQL lease-fencing pattern to Postgres AND
|
|
463
|
+
// tie a stale fencing token to a real refused fenced write.
|
|
464
|
+
//
|
|
465
|
+
// Two cluster-provider-db instances on the real PG _blamejs_leader
|
|
466
|
+
// row. Node-A acquires (token 1); a takeover by Node-B after A's lease
|
|
467
|
+
// expires bumps the token to 2. The canonical fenced operation — the
|
|
468
|
+
// audit-tip upsert with `WHERE stored.fencingToken <= EXCLUDED` — is
|
|
469
|
+
// issued by Node-B (token 2) and lands. Then the STALE Node-A (token
|
|
470
|
+
// 1) attempts the same fenced upsert: the real PG WHERE clause REFUSES
|
|
471
|
+
// it (RETURNING 0 rows). The stale write does not land; the tip still
|
|
472
|
+
// carries Node-B's token-2 row.
|
|
473
|
+
// ======================================================================
|
|
474
|
+
async function _proveFencing() {
|
|
475
|
+
_psql(AUDIT_TIP_DDL);
|
|
476
|
+
// Fresh leader table for an isolated fencing sequence. The prior
|
|
477
|
+
// _proveExactlyOnce acquired+released leadership (correctly leaving a
|
|
478
|
+
// released row at fencingToken 1), and the fencing token is monotonic
|
|
479
|
+
// across leadership changes BY DESIGN — so without a reset, A's first
|
|
480
|
+
// acquire here would correctly bump to 2. Drop the row so this proof
|
|
481
|
+
// asserts the 1 -> 2 progression from a clean origin.
|
|
482
|
+
_psql("DROP TABLE IF EXISTS _blamejs_leader;");
|
|
483
|
+
|
|
484
|
+
var providerFactory = require("../../lib/cluster-provider-db");
|
|
485
|
+
var pA = providerFactory.create({ externalDbBackend: "ops", dialect: "postgres" });
|
|
486
|
+
var pB = providerFactory.create({ externalDbBackend: "ops", dialect: "postgres" });
|
|
487
|
+
|
|
488
|
+
await pA.ensureSchema();
|
|
489
|
+
check("fencing: ensureSchema runs against real postgres", true);
|
|
490
|
+
|
|
491
|
+
// Node-A acquires the lease — first acquire issues fencing token 1.
|
|
492
|
+
var leaseA = await pA.acquireLease("node-A", b.constants.TIME.seconds(30));
|
|
493
|
+
check("fencing (PG): A acquired the lease", leaseA !== null);
|
|
494
|
+
check("fencing (PG): A's fencingToken = 1", leaseA.fencingToken === 1);
|
|
495
|
+
check("fencing (PG): A is the leader nodeId", leaseA.nodeId === "node-A");
|
|
496
|
+
|
|
497
|
+
// While A holds a live lease, B is blocked by the real ON CONFLICT
|
|
498
|
+
// WHERE expiresAt < now() guard.
|
|
499
|
+
var leaseBblocked = await pB.acquireLease("node-B", b.constants.TIME.seconds(30));
|
|
500
|
+
check("fencing (PG): B blocked while A holds a live lease", leaseBblocked === null);
|
|
501
|
+
|
|
502
|
+
// A performs a FENCED write at token 1 — the audit-tip upsert. First
|
|
503
|
+
// write: no prior tip row, so the INSERT lands and RETURNING is non-empty.
|
|
504
|
+
var aTipOk = await _fencedAuditTipUpsert(1, "node-A-row-hash-1", leaseA.fencingToken);
|
|
505
|
+
check("fencing (PG): A's fenced audit-tip write at token 1 landed", aTipOk === true);
|
|
506
|
+
var tipAfterA = _psql(
|
|
507
|
+
"SELECT \"fencingToken\", \"rowHash\" FROM _blamejs_audit_tip WHERE \"scope\" = 'audit';");
|
|
508
|
+
check("fencing (PG): tip row reflects A's token-1 write",
|
|
509
|
+
/\b1\b/.test(tipAfterA) && /node-A-row-hash-1/.test(tipAfterA));
|
|
510
|
+
|
|
511
|
+
// A's lease expires; B takes over. Use a short TTL on a fresh acquire so
|
|
512
|
+
// the takeover bumps the fencing token to 2 via the real
|
|
513
|
+
// `fencingToken + 1` ON CONFLICT path. (A releases first so B's takeover
|
|
514
|
+
// is deterministic rather than waiting on wall-clock expiry of the 30s
|
|
515
|
+
// lease above.)
|
|
516
|
+
await pA.releaseLease(leaseA);
|
|
517
|
+
var leaseB = await helpers.waitUntil(async function () {
|
|
518
|
+
return await pB.acquireLease("node-B", b.constants.TIME.seconds(30));
|
|
519
|
+
}, { timeoutMs: 15000, label: "fencing (PG): B takes over after A releases" });
|
|
520
|
+
check("fencing (PG): B took over the lease", leaseB !== null);
|
|
521
|
+
check("fencing (PG): takeover bumped fencingToken to 2",
|
|
522
|
+
leaseB.fencingToken === 2);
|
|
523
|
+
check("fencing (PG): leader is now node-B", leaseB.nodeId === "node-B");
|
|
524
|
+
|
|
525
|
+
// B (the NEW leader, token 2) performs a fenced write — accepted because
|
|
526
|
+
// 2 >= stored 1.
|
|
527
|
+
var bTipOk = await _fencedAuditTipUpsert(2, "node-B-row-hash-2", leaseB.fencingToken);
|
|
528
|
+
check("fencing (PG): B's fenced audit-tip write at token 2 landed (2 >= 1)",
|
|
529
|
+
bTipOk === true);
|
|
530
|
+
var tipAfterB = _psql(
|
|
531
|
+
"SELECT \"fencingToken\", \"rowHash\" FROM _blamejs_audit_tip WHERE \"scope\" = 'audit';");
|
|
532
|
+
check("fencing (PG): tip row now carries B's token-2 write",
|
|
533
|
+
/\b2\b/.test(tipAfterB) && /node-B-row-hash-2/.test(tipAfterB));
|
|
534
|
+
|
|
535
|
+
// The STALE leader A (still holding token 1) attempts a fenced write.
|
|
536
|
+
// The real PG `WHERE _blamejs_audit_tip.fencingToken <= EXCLUDED`
|
|
537
|
+
// clause refuses it: stored token is 2, incoming is 1, 2 <= 1 is false,
|
|
538
|
+
// 0 rows affected → fenced out. This is the split-brain old-leader
|
|
539
|
+
// write the fencing token exists to stop.
|
|
540
|
+
var staleAccepted = await _fencedAuditTipUpsert(99, "node-A-STALE-row-hash", leaseA.fencingToken);
|
|
541
|
+
check("fencing (PG): STALE A's fenced write at token 1 was REFUSED by the DB",
|
|
542
|
+
staleAccepted === false);
|
|
543
|
+
|
|
544
|
+
// The stale write did NOT land — the tip still carries B's token-2 row,
|
|
545
|
+
// not A's stale hash/counter. This is the real side-effect assertion:
|
|
546
|
+
// the partitioned old leader could not corrupt the chain head.
|
|
547
|
+
var tipAfterStale = _psql(
|
|
548
|
+
"SELECT \"atMonotonicCounter\", \"fencingToken\", \"rowHash\" FROM _blamejs_audit_tip " +
|
|
549
|
+
"WHERE \"scope\" = 'audit';");
|
|
550
|
+
check("fencing (PG): stale write did NOT overwrite the tip (token still 2)",
|
|
551
|
+
/\b2\b/.test(tipAfterStale));
|
|
552
|
+
check("fencing (PG): tip rowHash is still B's, NOT A's stale hash",
|
|
553
|
+
/node-B-row-hash-2/.test(tipAfterStale) &&
|
|
554
|
+
!/node-A-STALE-row-hash/.test(tipAfterStale));
|
|
555
|
+
check("fencing (PG): tip counter is B's (2), not A's stale (99)",
|
|
556
|
+
/^2\b/.test(tipAfterStale.trim()) || /\b2\t/.test(tipAfterStale));
|
|
557
|
+
|
|
558
|
+
// Same-token rewrite is permitted (the guard is <=, not <): B re-writes
|
|
559
|
+
// at token 2 with a new counter — confirms the guard fences only
|
|
560
|
+
// STRICTLY-lower tokens, matching _upsertAuditTip's documented contract.
|
|
561
|
+
var bRewriteOk = await _fencedAuditTipUpsert(3, "node-B-row-hash-3", leaseB.fencingToken);
|
|
562
|
+
check("fencing (PG): same-token (2) re-write is accepted (guard is <=, not <)",
|
|
563
|
+
bRewriteOk === true);
|
|
564
|
+
var tipAfterRewrite = _psql(
|
|
565
|
+
"SELECT \"atMonotonicCounter\", \"rowHash\" FROM _blamejs_audit_tip WHERE \"scope\" = 'audit';");
|
|
566
|
+
check("fencing (PG): same-token re-write advanced the counter to 3",
|
|
567
|
+
/^3\b/.test(tipAfterRewrite.trim()) || /\b3\t/.test(tipAfterRewrite) ||
|
|
568
|
+
/node-B-row-hash-3/.test(tipAfterRewrite));
|
|
569
|
+
|
|
570
|
+
await pB.releaseLease(leaseB);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Issue the canonical fenced audit-tip upsert against the real PG backend,
|
|
574
|
+
// byte-for-byte the SQL shape from lib/audit.js _upsertAuditTip (with $N
|
|
575
|
+
// placeholders for the postgres dialect). Returns true if the write landed
|
|
576
|
+
// (RETURNING produced a row), false if the DB fenced it out (0 rows).
|
|
577
|
+
async function _fencedAuditTipUpsert(counter, rowHash, fencingToken) {
|
|
578
|
+
var result = await b.externalDb.query(
|
|
579
|
+
"INSERT INTO _blamejs_audit_tip " +
|
|
580
|
+
" (\"scope\", \"atMonotonicCounter\", \"rowHash\", \"signedAt\", \"fencingToken\") " +
|
|
581
|
+
"VALUES ('audit', $1, $2, $3, $4) " +
|
|
582
|
+
"ON CONFLICT (\"scope\") DO UPDATE SET " +
|
|
583
|
+
" \"atMonotonicCounter\" = EXCLUDED.\"atMonotonicCounter\", " +
|
|
584
|
+
" \"rowHash\" = EXCLUDED.\"rowHash\", " +
|
|
585
|
+
" \"signedAt\" = EXCLUDED.\"signedAt\", " +
|
|
586
|
+
" \"fencingToken\" = EXCLUDED.\"fencingToken\" " +
|
|
587
|
+
"WHERE _blamejs_audit_tip.\"fencingToken\" <= EXCLUDED.\"fencingToken\" " +
|
|
588
|
+
"RETURNING \"fencingToken\"",
|
|
589
|
+
[counter, rowHash, String(Date.now()), fencingToken],
|
|
590
|
+
{ backend: "ops" }
|
|
591
|
+
);
|
|
592
|
+
return !!(result.rows && result.rows.length > 0);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
module.exports = { run: run };
|
|
596
|
+
|
|
597
|
+
if (require.main === module) {
|
|
598
|
+
run().then(
|
|
599
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
|
|
600
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
601
|
+
);
|
|
602
|
+
}
|