@blamejs/blamejs-shop 0.4.31 → 0.4.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +392 -278
- package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
- package/lib/vendor/blamejs/.gitignore +6 -0
- package/lib/vendor/blamejs/CHANGELOG.md +26 -0
- package/lib/vendor/blamejs/MIGRATING.md +43 -0
- package/lib/vendor/blamejs/README.md +8 -6
- package/lib/vendor/blamejs/SECURITY.md +19 -3
- package/lib/vendor/blamejs/api-snapshot.json +2190 -664
- package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
- package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
- package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
- package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
- package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
- package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
- package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
- package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
- package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
- package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
- package/lib/vendor/blamejs/lib/api-key.js +158 -77
- package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
- package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
- package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
- package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
- package/lib/vendor/blamejs/lib/audit.js +259 -123
- package/lib/vendor/blamejs/lib/auth/oauth.js +53 -9
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
- package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +31 -5
- package/lib/vendor/blamejs/lib/backup/index.js +45 -10
- package/lib/vendor/blamejs/lib/break-glass.js +355 -147
- package/lib/vendor/blamejs/lib/cache.js +174 -105
- package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
- package/lib/vendor/blamejs/lib/cli.js +19 -14
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
- package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
- package/lib/vendor/blamejs/lib/cluster.js +119 -71
- package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
- package/lib/vendor/blamejs/lib/compliance.js +206 -4
- package/lib/vendor/blamejs/lib/consent.js +82 -29
- package/lib/vendor/blamejs/lib/constants.js +27 -11
- package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
- package/lib/vendor/blamejs/lib/db-query.js +882 -260
- package/lib/vendor/blamejs/lib/db-schema.js +228 -44
- package/lib/vendor/blamejs/lib/db.js +249 -99
- package/lib/vendor/blamejs/lib/dsr.js +385 -55
- package/lib/vendor/blamejs/lib/error-page.js +14 -1
- package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
- package/lib/vendor/blamejs/lib/external-db.js +549 -34
- package/lib/vendor/blamejs/lib/file-upload.js +52 -7
- package/lib/vendor/blamejs/lib/framework-error.js +20 -1
- package/lib/vendor/blamejs/lib/framework-files.js +73 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
- package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
- package/lib/vendor/blamejs/lib/guard-all.js +1 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
- package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
- package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
- package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
- package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
- package/lib/vendor/blamejs/lib/guard-email.js +47 -69
- package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
- package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
- package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
- package/lib/vendor/blamejs/lib/guard-html.js +53 -108
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
- package/lib/vendor/blamejs/lib/guard-image.js +46 -103
- package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
- package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
- package/lib/vendor/blamejs/lib/guard-json.js +38 -108
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
- package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
- package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
- package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
- package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
- package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
- package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
- package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
- package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
- package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
- package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
- package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
- package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
- package/lib/vendor/blamejs/lib/guard-template.js +35 -172
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-time.js +32 -154
- package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
- package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
- package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
- package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
- package/lib/vendor/blamejs/lib/http-client.js +37 -9
- package/lib/vendor/blamejs/lib/inbox.js +120 -107
- package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
- package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
- package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
- package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
- package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
- package/lib/vendor/blamejs/lib/mail-store.js +293 -154
- package/lib/vendor/blamejs/lib/mail.js +8 -4
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
- package/lib/vendor/blamejs/lib/migrations.js +108 -66
- package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
- package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
- package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
- package/lib/vendor/blamejs/lib/observability.js +124 -0
- package/lib/vendor/blamejs/lib/otel-export.js +12 -3
- package/lib/vendor/blamejs/lib/outbox.js +184 -83
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
- package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
- package/lib/vendor/blamejs/lib/queue-local.js +225 -140
- package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
- package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
- package/lib/vendor/blamejs/lib/queue.js +7 -0
- package/lib/vendor/blamejs/lib/redact.js +68 -11
- package/lib/vendor/blamejs/lib/redis-client.js +160 -31
- package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
- package/lib/vendor/blamejs/lib/retention.js +101 -40
- package/lib/vendor/blamejs/lib/router.js +212 -5
- package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
- package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
- package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
- package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
- package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
- package/lib/vendor/blamejs/lib/safe-url.js +170 -3
- package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
- package/lib/vendor/blamejs/lib/scheduler.js +35 -12
- package/lib/vendor/blamejs/lib/seeders.js +122 -74
- package/lib/vendor/blamejs/lib/session-stores.js +42 -14
- package/lib/vendor/blamejs/lib/session.js +175 -77
- package/lib/vendor/blamejs/lib/sql.js +3842 -0
- package/lib/vendor/blamejs/lib/sse.js +26 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +151 -4
- package/lib/vendor/blamejs/lib/static.js +177 -34
- package/lib/vendor/blamejs/lib/subject.js +96 -49
- package/lib/vendor/blamejs/lib/vault/index.js +3 -2
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
- package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
- package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
- package/lib/vendor/blamejs/lib/websocket.js +35 -5
- package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
- package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
- package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
- package/lib/vendor/blamejs/scripts/check-services.js +21 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +51 -0
- package/lib/vendor/blamejs/scripts/release.js +398 -38
- package/lib/vendor/blamejs/test/00-primitives.js +117 -0
- package/lib/vendor/blamejs/test/10-state.js +140 -14
- package/lib/vendor/blamejs/test/20-db.js +65 -2
- package/lib/vendor/blamejs/test/helpers/db.js +9 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
- package/lib/vendor/blamejs/test/helpers/services.js +21 -0
- package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
- package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
- package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
- package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
- package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
- package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
- package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
- package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
- package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
- package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
- package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
- package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
- package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
- package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1120 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +309 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
- package/lib/vendor/blamejs/test/smoke.js +79 -21
- package/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
- package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
- package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
- package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
- package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
- package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
- package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live SQS queue-backend round-trip — exercises lib/queue-sqs.js against
|
|
4
|
+
* LocalStack's SQS API (AWSJsonProtocol_1.0) over TLS.
|
|
5
|
+
*
|
|
6
|
+
* What this proves (real, end-to-end against a live SQS server):
|
|
7
|
+
* - The adapter's SendMessage wire shape is correct: X-Amz-Target
|
|
8
|
+
* AmazonSQS.SendMessage, Content-Type application/x-amz-json-1.0, a
|
|
9
|
+
* { QueueUrl, MessageBody } body — and the message actually lands in
|
|
10
|
+
* the queue (size() rises, ReceiveMessage returns it).
|
|
11
|
+
* - The sealed-envelope round-trip: enqueue seals the job row through
|
|
12
|
+
* cryptoField.sealRow("_blamejs_jobs", ...) before SendMessage; lease
|
|
13
|
+
* unseals it on receive. A structured payload round-trips
|
|
14
|
+
* field-identical through the SQS wire (the message body in transit is
|
|
15
|
+
* the sealed envelope, not the cleartext payload).
|
|
16
|
+
* - ReceiveMessage surfaces a ReceiptHandle + MessageId, and complete()
|
|
17
|
+
* (DeleteMessage by that handle) removes the message so it does NOT
|
|
18
|
+
* redeliver — the queue drains.
|
|
19
|
+
* - The visibility-timeout / redelivery path: a leased-but-not-deleted
|
|
20
|
+
* message becomes invisible for VisibilityTimeout, and fail()
|
|
21
|
+
* (ChangeMessageVisibility VisibilityTimeout=0) makes it immediately
|
|
22
|
+
* visible again so another consumer re-leases it.
|
|
23
|
+
* - size() (GetQueueAttributes ApproximateNumberOfMessages) and purge()
|
|
24
|
+
* (PurgeQueue) hit the wire and reflect the queue state.
|
|
25
|
+
* - The endpoint override (cfg.endpoint / cfg.queueUrlByName) is
|
|
26
|
+
* honoured — requests reach 127.0.0.1:4566, not
|
|
27
|
+
* sqs.<region>.amazonaws.com.
|
|
28
|
+
*
|
|
29
|
+
* Scope honesty — what this does NOT prove:
|
|
30
|
+
* LocalStack accepts the test credentials and does NOT verify the SigV4
|
|
31
|
+
* signature (it ignores the secret key). So this proves the SQS wire /
|
|
32
|
+
* marshalling + the send/receive/delete/ack + visibility-timeout flow,
|
|
33
|
+
* NOT signature correctness. (Signature correctness against a server
|
|
34
|
+
* that DOES verify SigV4 is covered by object-store-sigv4.test.js
|
|
35
|
+
* against MinIO.)
|
|
36
|
+
*
|
|
37
|
+
* No security bypass: TLS to LocalStack trusts the test CA via
|
|
38
|
+
* NODE_EXTRA_CA_CERTS (exported by scripts/test-integration.js).
|
|
39
|
+
* rejectUnauthorized stays on; allowInternal:true only permits the
|
|
40
|
+
* loopback host, it does not disable verification. The job payload is
|
|
41
|
+
* sealed at rest in the queue (vault key + framework crypto stack) — the
|
|
42
|
+
* test registers the seal table and inits the vault exactly as a queue
|
|
43
|
+
* node would.
|
|
44
|
+
*
|
|
45
|
+
* To run:
|
|
46
|
+
* docker compose -f docker-compose.test.yml up -d --wait
|
|
47
|
+
* node scripts/test-integration.js --skip-service-check queue-sqs
|
|
48
|
+
*/
|
|
49
|
+
var fs = require("node:fs");
|
|
50
|
+
var os = require("node:os");
|
|
51
|
+
var path = require("node:path");
|
|
52
|
+
var nodeCrypto = require("node:crypto");
|
|
53
|
+
var helpers = require("../helpers");
|
|
54
|
+
var check = helpers.check;
|
|
55
|
+
var services = require("../helpers/services");
|
|
56
|
+
var b = require("../../");
|
|
57
|
+
|
|
58
|
+
var queueSqs = require("../../lib/queue-sqs");
|
|
59
|
+
var cryptoField = require("../../lib/crypto-field");
|
|
60
|
+
var sigv4 = require("../../lib/object-store/sigv4");
|
|
61
|
+
var httpClient = require("../../lib/http-client");
|
|
62
|
+
var safeUrl = require("../../lib/safe-url");
|
|
63
|
+
|
|
64
|
+
var REGION = "us-east-1";
|
|
65
|
+
var ACCESS = "test";
|
|
66
|
+
var SECRET = "test";
|
|
67
|
+
|
|
68
|
+
// ---- raw SigV4-signed SQS control-plane helper ----
|
|
69
|
+
// The adapter intentionally has no CreateQueue / DeleteQueue (queue
|
|
70
|
+
// lifecycle is operator/IaC territory), so the test stands the queue up
|
|
71
|
+
// and tears it down with its own signed AWSJsonProtocol_1.0 calls — the
|
|
72
|
+
// same signer the adapter uses. Returns the parsed JSON body; throws on a
|
|
73
|
+
// non-2xx so the caller sees the AWS exception text.
|
|
74
|
+
function _sqsCall(endpoint, action, payload) {
|
|
75
|
+
var body = Buffer.from(JSON.stringify(payload || {}), "utf8");
|
|
76
|
+
var payloadHash = nodeCrypto.createHash("sha256").update(body).digest("hex");
|
|
77
|
+
var signed = sigv4.signRequest({
|
|
78
|
+
method: "POST",
|
|
79
|
+
url: endpoint,
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "application/x-amz-json-1.0",
|
|
82
|
+
"X-Amz-Target": "AmazonSQS." + action,
|
|
83
|
+
},
|
|
84
|
+
payloadHash: payloadHash,
|
|
85
|
+
region: REGION,
|
|
86
|
+
service: "sqs",
|
|
87
|
+
accessKeyId: ACCESS,
|
|
88
|
+
secretAccessKey: SECRET,
|
|
89
|
+
allowedProtocols: safeUrl.ALLOW_HTTP_TLS,
|
|
90
|
+
});
|
|
91
|
+
return httpClient.request({
|
|
92
|
+
method: "POST",
|
|
93
|
+
url: endpoint,
|
|
94
|
+
headers: signed.headers,
|
|
95
|
+
body: body,
|
|
96
|
+
allowInternal: true,
|
|
97
|
+
allowedProtocols: safeUrl.ALLOW_HTTP_TLS,
|
|
98
|
+
}).then(function (res) {
|
|
99
|
+
var text = Buffer.isBuffer(res.body) ? res.body.toString("utf8")
|
|
100
|
+
: (res.body || "").toString();
|
|
101
|
+
return text.length ? JSON.parse(text) : {};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function run() {
|
|
106
|
+
var ls = await services.requireService("localstack");
|
|
107
|
+
if (!ls.ok) throw new Error("localstack unreachable: " + ls.reason);
|
|
108
|
+
|
|
109
|
+
var endpoint = services.URLS.localstack; // https://127.0.0.1:4566
|
|
110
|
+
|
|
111
|
+
// ---- 0) framework-side seal wiring ----
|
|
112
|
+
// The SQS adapter seals the job row through cryptoField.sealRow(
|
|
113
|
+
// "_blamejs_jobs", ...) before SendMessage and unsealRow()s it on
|
|
114
|
+
// receive. Without registering the table + a live vault key, sealRow is
|
|
115
|
+
// a no-op pass-through and the round-trip wouldn't actually exercise the
|
|
116
|
+
// seal/unseal envelope — so init the vault + register the table exactly
|
|
117
|
+
// as a standalone SQS queue node would (queue.init() calls
|
|
118
|
+
// _ensureSealTable() for the same reason).
|
|
119
|
+
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-queue-sqs-"));
|
|
120
|
+
if (typeof b.vault._resetForTest === "function") b.vault._resetForTest();
|
|
121
|
+
await b.vault.init({ dataDir: dataDir, mode: "plaintext" });
|
|
122
|
+
cryptoField.registerTable("_blamejs_jobs", { sealedFields: ["payload", "lastError"] });
|
|
123
|
+
|
|
124
|
+
// ---- 1) create a unique queue out-of-band ----
|
|
125
|
+
// LocalStack's default AWS account is 000000000000; CreateQueue echoes
|
|
126
|
+
// an AWS-shaped advertised URL (sqs.us-east-1.localhost.localstack.cloud),
|
|
127
|
+
// but the adapter routes every action to cfg.endpoint and only carries
|
|
128
|
+
// the QueueUrl in the JSON body — LocalStack accepts the path-style
|
|
129
|
+
// endpoint+accountId+name URL the adapter's own resolver synthesizes.
|
|
130
|
+
var ACCOUNT_ID = "000000000000";
|
|
131
|
+
var queueName = "blamejs-sqs-test-" + Date.now() + "-" + Math.floor(Math.random() * 1e6);
|
|
132
|
+
var created = await _sqsCall(endpoint, "CreateQueue", { QueueName: queueName });
|
|
133
|
+
var advertisedUrl = created && created.QueueUrl;
|
|
134
|
+
check("CreateQueue: returned a QueueUrl", typeof advertisedUrl === "string" && advertisedUrl.length > 0);
|
|
135
|
+
check("CreateQueue: QueueUrl carries the queue name", advertisedUrl.indexOf(queueName) !== -1);
|
|
136
|
+
|
|
137
|
+
// ---- 2) build the adapter pointed at LocalStack ----
|
|
138
|
+
// No queueUrlByName override: this exercises the adapter's built-in
|
|
139
|
+
// endpoint + accountId + name URL synthesis (the single-account default
|
|
140
|
+
// path), so the test proves that path too — not just a hand-fed resolver.
|
|
141
|
+
var q = queueSqs.create({
|
|
142
|
+
endpoint: endpoint,
|
|
143
|
+
region: REGION,
|
|
144
|
+
accountId: ACCOUNT_ID,
|
|
145
|
+
accessKeyId: ACCESS,
|
|
146
|
+
secretAccessKey: SECRET,
|
|
147
|
+
allowInternal: true,
|
|
148
|
+
allowedProtocols: safeUrl.ALLOW_HTTP_TLS,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// endpoint override honoured — the synthesized queue URL points at the
|
|
152
|
+
// LocalStack host, not the AWS regional host.
|
|
153
|
+
check("endpoint override honoured (queue URL is the LocalStack host, not sqs.<region>.amazonaws.com)",
|
|
154
|
+
q._queueUrl(queueName).indexOf("127.0.0.1:4566") !== -1 &&
|
|
155
|
+
q._queueUrl(queueName).indexOf("amazonaws.com") === -1);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// ---- 3) enqueue → receive round-trip + sealed-payload fidelity ----
|
|
159
|
+
var Q = queueName;
|
|
160
|
+
var payload = {
|
|
161
|
+
kind: "report.generate",
|
|
162
|
+
orderId: "ord-" + Math.floor(Math.random() * 1e9),
|
|
163
|
+
nested: { tags: ["alpha", "beta"], count: 3, flag: true },
|
|
164
|
+
unicode: "café — naïve — 日本語",
|
|
165
|
+
amount: 1234.56,
|
|
166
|
+
};
|
|
167
|
+
var sentId = await q.enqueue(Q, payload, { traceId: "t-sqs-1" });
|
|
168
|
+
check("enqueue: SendMessage returned a message id", typeof sentId === "string" && sentId.length > 0);
|
|
169
|
+
|
|
170
|
+
// size() reflects the just-enqueued message (ApproximateNumberOfMessages
|
|
171
|
+
// is eventually-consistent on real SQS; LocalStack is prompt — poll).
|
|
172
|
+
var sz1 = await helpers.waitUntil(async function () {
|
|
173
|
+
var n = await q.size(Q);
|
|
174
|
+
return n >= 1 ? n : false;
|
|
175
|
+
}, { timeoutMs: 8000, label: "queue-sqs: size() reflects the enqueued message" });
|
|
176
|
+
check("size: >= 1 after enqueue", sz1 >= 1);
|
|
177
|
+
|
|
178
|
+
// Receive it. The body on the wire is the sealed envelope; lease()
|
|
179
|
+
// unseals it back to the structured payload.
|
|
180
|
+
var leased = await helpers.waitUntil(async function () {
|
|
181
|
+
var rv = await q.lease(Q, { maxRows: 5, waitTimeSec: 1, visibilityTimeoutSec: 30 });
|
|
182
|
+
return rv.length >= 1 ? rv : false;
|
|
183
|
+
}, { timeoutMs: 10000, label: "queue-sqs: ReceiveMessage returns the enqueued job" });
|
|
184
|
+
check("lease: ReceiveMessage returned exactly one job", leased.length === 1);
|
|
185
|
+
|
|
186
|
+
var job = leased[0];
|
|
187
|
+
check("lease: surfaces a ReceiptHandle (the only key for delete/visibility)",
|
|
188
|
+
typeof job.receiptHandle === "string" && job.receiptHandle.length > 0);
|
|
189
|
+
check("lease: surfaces the SQS MessageId",
|
|
190
|
+
typeof job.sqsMessageId === "string" && job.sqsMessageId.length > 0);
|
|
191
|
+
check("lease: jobId present (the framework job id, not the SQS id)",
|
|
192
|
+
typeof job.jobId === "string" && job.jobId.length > 0);
|
|
193
|
+
check("lease: queueName round-tripped through the sealed row",
|
|
194
|
+
job.queueName === Q);
|
|
195
|
+
|
|
196
|
+
// The headline proof: structured payload survives the seal → SQS wire
|
|
197
|
+
// → unseal round-trip byte/field-identical (deep equality).
|
|
198
|
+
check("lease: payload round-trips field-identical through seal + SQS wire",
|
|
199
|
+
JSON.stringify(job.payload) === JSON.stringify(payload));
|
|
200
|
+
check("lease: nested object fields intact",
|
|
201
|
+
job.payload && job.payload.nested &&
|
|
202
|
+
job.payload.nested.count === 3 && job.payload.nested.flag === true &&
|
|
203
|
+
job.payload.nested.tags.length === 2 &&
|
|
204
|
+
job.payload.nested.tags[0] === "alpha" && job.payload.nested.tags[1] === "beta");
|
|
205
|
+
check("lease: unicode payload byte-identical (no mojibake on the wire)",
|
|
206
|
+
job.payload.unicode === "café — naïve — 日本語");
|
|
207
|
+
check("lease: numeric field preserved (not stringified)",
|
|
208
|
+
job.payload.amount === 1234.56 && job.payload.orderId === payload.orderId);
|
|
209
|
+
|
|
210
|
+
// ---- 4) ack (DeleteMessage) → does NOT redeliver, queue drains ----
|
|
211
|
+
var compRv = await q.complete(Q, job.jobId, { receiptHandle: job.receiptHandle });
|
|
212
|
+
check("complete: DeleteMessage returned true", compRv === true);
|
|
213
|
+
|
|
214
|
+
// After delete, the message must not come back. Long-poll a couple of
|
|
215
|
+
// times with a fresh (short) visibility window; nothing should appear.
|
|
216
|
+
var redelivered = await q.lease(Q, { maxRows: 5, waitTimeSec: 2, visibilityTimeoutSec: 5 });
|
|
217
|
+
check("complete: deleted message does NOT redeliver (queue drained)",
|
|
218
|
+
redelivered.length === 0);
|
|
219
|
+
|
|
220
|
+
// NOTE on size() semantics: ApproximateNumberOfMessages counts only
|
|
221
|
+
// VISIBLE messages — a leased-but-not-deleted message is in-flight
|
|
222
|
+
// (NotVisible) and reports 0 here. So 0 after complete confirms the
|
|
223
|
+
// message is neither visible nor in-flight. (waitUntil treats a falsy
|
|
224
|
+
// return as "not ready", so the predicate returns true on success,
|
|
225
|
+
// not the numeric 0.)
|
|
226
|
+
var szDrained = await helpers.waitUntil(async function () {
|
|
227
|
+
var n = await q.size(Q);
|
|
228
|
+
return n === 0 ? true : false;
|
|
229
|
+
}, { timeoutMs: 8000, label: "queue-sqs: size() back to 0 after complete" });
|
|
230
|
+
check("size: 0 after complete (no in-flight, no visible)", szDrained === true);
|
|
231
|
+
|
|
232
|
+
// ---- 5) visibility-timeout / redelivery path via fail() ----
|
|
233
|
+
// Enqueue, lease with a long visibility timeout (so it would NOT come
|
|
234
|
+
// back on its own within the test window), then fail() — which sets
|
|
235
|
+
// VisibilityTimeout=0 and makes it immediately visible to the next
|
|
236
|
+
// consumer. Proves ChangeMessageVisibility re-delivery, the SQS-native
|
|
237
|
+
// retry the adapter relies on (server-side RedrivePolicy decides DLQ).
|
|
238
|
+
var redeliverPayload = { step: "retry-me", n: 7 };
|
|
239
|
+
await q.enqueue(Q, redeliverPayload, { traceId: "t-sqs-redeliver" });
|
|
240
|
+
|
|
241
|
+
var firstLease = await helpers.waitUntil(async function () {
|
|
242
|
+
var rv = await q.lease(Q, { maxRows: 1, waitTimeSec: 1, visibilityTimeoutSec: 300 });
|
|
243
|
+
return rv.length >= 1 ? rv : false;
|
|
244
|
+
}, { timeoutMs: 10000, label: "queue-sqs: redelivery job first lease" });
|
|
245
|
+
check("redelivery: first lease grabbed the job under a 300s visibility window",
|
|
246
|
+
firstLease.length === 1 && firstLease[0].payload &&
|
|
247
|
+
firstLease[0].payload.step === "retry-me");
|
|
248
|
+
|
|
249
|
+
// While the 300s visibility window is in effect, a second lease sees
|
|
250
|
+
// nothing — the message is in-flight (invisible).
|
|
251
|
+
var whileInflight = await q.lease(Q, { maxRows: 1, waitTimeSec: 1, visibilityTimeoutSec: 5 });
|
|
252
|
+
check("redelivery: message invisible to a second consumer during the visibility window",
|
|
253
|
+
whileInflight.length === 0);
|
|
254
|
+
|
|
255
|
+
// fail() → VisibilityTimeout=0 → immediately visible again.
|
|
256
|
+
var failRv = await q.fail(Q, firstLease[0].jobId, { receiptHandle: firstLease[0].receiptHandle });
|
|
257
|
+
check("fail: ChangeMessageVisibility(0) returned true", failRv === true);
|
|
258
|
+
|
|
259
|
+
var reLease = await helpers.waitUntil(async function () {
|
|
260
|
+
var rv = await q.lease(Q, { maxRows: 1, waitTimeSec: 2, visibilityTimeoutSec: 30 });
|
|
261
|
+
return rv.length >= 1 ? rv : false;
|
|
262
|
+
}, { timeoutMs: 12000, label: "queue-sqs: fail() re-delivered the job to a new consumer" });
|
|
263
|
+
check("redelivery: fail() made the message visible again (re-leased)",
|
|
264
|
+
reLease.length === 1 && reLease[0].payload &&
|
|
265
|
+
reLease[0].payload.step === "retry-me" && reLease[0].payload.n === 7);
|
|
266
|
+
check("redelivery: re-leased message carries a fresh receipt handle",
|
|
267
|
+
typeof reLease[0].receiptHandle === "string" &&
|
|
268
|
+
reLease[0].receiptHandle.length > 0);
|
|
269
|
+
|
|
270
|
+
// Clean it up so the purge leg starts from a known state.
|
|
271
|
+
await q.complete(Q, reLease[0].jobId, { receiptHandle: reLease[0].receiptHandle });
|
|
272
|
+
|
|
273
|
+
// ---- 6) extendLease (ChangeMessageVisibility to a longer window) ----
|
|
274
|
+
await q.enqueue(Q, { step: "extend-me" }, {});
|
|
275
|
+
var extLease = await helpers.waitUntil(async function () {
|
|
276
|
+
var rv = await q.lease(Q, { maxRows: 1, waitTimeSec: 1, visibilityTimeoutSec: 30 });
|
|
277
|
+
return rv.length >= 1 ? rv : false;
|
|
278
|
+
}, { timeoutMs: 10000, label: "queue-sqs: extendLease job leased" });
|
|
279
|
+
check("extendLease: job leased", extLease.length === 1);
|
|
280
|
+
var extRv = await q.extendLease(Q, extLease[0].jobId, {
|
|
281
|
+
receiptHandle: extLease[0].receiptHandle, visibilityTimeoutSec: 120,
|
|
282
|
+
});
|
|
283
|
+
check("extendLease: ChangeMessageVisibility(120) returned true", extRv === true);
|
|
284
|
+
await q.complete(Q, extLease[0].jobId, { receiptHandle: extLease[0].receiptHandle });
|
|
285
|
+
|
|
286
|
+
// ---- 7) purge clears the queue ----
|
|
287
|
+
// Re-fill, confirm size, purge, confirm drained. (PurgeQueue is
|
|
288
|
+
// 60s-rate-limited server-side; this test calls it once.)
|
|
289
|
+
await q.enqueue(Q, { p: 1 }, {});
|
|
290
|
+
await q.enqueue(Q, { p: 2 }, {});
|
|
291
|
+
await q.enqueue(Q, { p: 3 }, {});
|
|
292
|
+
var preSize = await helpers.waitUntil(async function () {
|
|
293
|
+
var n = await q.size(Q);
|
|
294
|
+
return n >= 3 ? n : false;
|
|
295
|
+
}, { timeoutMs: 8000, label: "queue-sqs: size() reflects 3 enqueued before purge" });
|
|
296
|
+
check("purge: pre-purge size >= 3", preSize >= 3);
|
|
297
|
+
|
|
298
|
+
var purgeRv = await q.purge(Q);
|
|
299
|
+
check("purge: PurgeQueue returned (count 0 — SQS doesn't report a count)", purgeRv === 0);
|
|
300
|
+
|
|
301
|
+
var postPurge = await helpers.waitUntil(async function () {
|
|
302
|
+
var n = await q.size(Q);
|
|
303
|
+
return n === 0 ? true : false;
|
|
304
|
+
}, { timeoutMs: 12000, label: "queue-sqs: size() back to 0 after PurgeQueue" });
|
|
305
|
+
check("purge: post-purge size 0", postPurge === true);
|
|
306
|
+
|
|
307
|
+
} finally {
|
|
308
|
+
// Tear the queue down (control-plane, out-of-band like creation).
|
|
309
|
+
try { await _sqsCall(endpoint, "DeleteQueue", { QueueUrl: advertisedUrl }); } catch (_e) {}
|
|
310
|
+
try { fs.rmSync(dataDir, { recursive: true, force: true }); } catch (_e) {}
|
|
311
|
+
if (typeof b.vault._resetForTest === "function") b.vault._resetForTest();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = { run: run };
|
|
316
|
+
|
|
317
|
+
if (require.main === module) {
|
|
318
|
+
run().then(
|
|
319
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
|
|
320
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
321
|
+
);
|
|
322
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live reconnect / failover proof for lib/redis-client.js against a REAL
|
|
4
|
+
* redis dropped mid-connection via toxiproxy.
|
|
5
|
+
*
|
|
6
|
+
* The framework redis client points at toxiproxy's proxied redis
|
|
7
|
+
* (127.0.0.1:16379, upstream redis:6379). A working connection is
|
|
8
|
+
* established (SET/GET round-trips), then the proxy is DISABLED through
|
|
9
|
+
* the toxiproxy HTTP API (127.0.0.1:8474) so redis appears DOWN — every
|
|
10
|
+
* established connection is closed and new dials are refused. The test
|
|
11
|
+
* asserts:
|
|
12
|
+
*
|
|
13
|
+
* 1. An in-flight command issued during the outage REJECTS (with a
|
|
14
|
+
* transport / timeout error code) rather than hanging the caller
|
|
15
|
+
* forever — no caller wedge.
|
|
16
|
+
* 2. The client schedules a reconnect (the _state() reconnect machinery
|
|
17
|
+
* advances) — the lost socket drives the reconnect loop.
|
|
18
|
+
* 3. No reconnect storm: one outage schedules at most ONE pending timer
|
|
19
|
+
* at a time (the single-flight guard) — a socket failure surfacing
|
|
20
|
+
* as BOTH `error` and `close` must not stack two reconnects and burn
|
|
21
|
+
* the budget at 2x.
|
|
22
|
+
* 4. Re-enabling the proxy RECOVERS the client: it reconnects on its own
|
|
23
|
+
* and a subsequent SET/GET round-trips, with the backoff counter +
|
|
24
|
+
* give-up latch reset.
|
|
25
|
+
*
|
|
26
|
+
* Recovery is polled with helpers.waitUntil — no fixed sleep. Fault is
|
|
27
|
+
* injected over the toxiproxy HTTP control plane, not by killing the real
|
|
28
|
+
* redis (which other workflows may share); the upstream redis is never
|
|
29
|
+
* touched.
|
|
30
|
+
*
|
|
31
|
+
* Run:
|
|
32
|
+
* node scripts/test-integration.js --skip-service-check redis-reconnect-toxiproxy
|
|
33
|
+
*/
|
|
34
|
+
var http = require("node:http");
|
|
35
|
+
var helpers = require("../helpers");
|
|
36
|
+
var check = helpers.check;
|
|
37
|
+
var services = require("../helpers/services");
|
|
38
|
+
var redisClient = require("../../lib/redis-client");
|
|
39
|
+
|
|
40
|
+
// toxiproxy control plane. The proxy named "redis" listens on :16379 and
|
|
41
|
+
// forwards to upstream redis:6379; flipping its `enabled` flag is the
|
|
42
|
+
// outage switch (disabled = existing conns closed + new dials refused).
|
|
43
|
+
var TOXIPROXY_API = services.URLS.toxiproxy; // http://127.0.0.1:8474
|
|
44
|
+
var PROXY_NAME = "redis";
|
|
45
|
+
var PROXIED_REDIS = services.URLS.toxiproxyRedis; // redis://127.0.0.1:16379
|
|
46
|
+
|
|
47
|
+
// Minimal toxiproxy HTTP client over node:http — POST a JSON body to the
|
|
48
|
+
// proxy endpoint to flip `enabled`. Returns the parsed proxy object.
|
|
49
|
+
function _toxiproxyRequest(method, urlPath, bodyObj) {
|
|
50
|
+
return new Promise(function (resolve, reject) {
|
|
51
|
+
var u = new URL(TOXIPROXY_API + urlPath);
|
|
52
|
+
var payload = bodyObj === undefined ? null : Buffer.from(JSON.stringify(bodyObj), "utf8");
|
|
53
|
+
var req = http.request({
|
|
54
|
+
host: u.hostname,
|
|
55
|
+
port: u.port,
|
|
56
|
+
path: u.pathname + (u.search || ""),
|
|
57
|
+
method: method,
|
|
58
|
+
headers: payload
|
|
59
|
+
? { "content-type": "application/json", "content-length": payload.length }
|
|
60
|
+
: {},
|
|
61
|
+
}, function (res) {
|
|
62
|
+
var chunks = [];
|
|
63
|
+
res.on("data", function (c) { chunks.push(c); });
|
|
64
|
+
res.on("end", function () {
|
|
65
|
+
var raw = Buffer.concat(chunks).toString("utf8");
|
|
66
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
67
|
+
return reject(new Error("toxiproxy " + method + " " + urlPath +
|
|
68
|
+
" -> HTTP " + res.statusCode + ": " + raw));
|
|
69
|
+
}
|
|
70
|
+
var parsed = null;
|
|
71
|
+
try { parsed = raw ? JSON.parse(raw) : null; } catch (_e) { parsed = raw; }
|
|
72
|
+
resolve(parsed);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
req.on("error", reject);
|
|
76
|
+
if (payload) req.write(payload);
|
|
77
|
+
req.end();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _setProxyEnabled(enabled) {
|
|
82
|
+
return _toxiproxyRequest("POST", "/proxies/" + PROXY_NAME, { enabled: !!enabled });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function run() {
|
|
86
|
+
// Probe both the proxied-redis port AND the toxiproxy control plane —
|
|
87
|
+
// either being down means we can't run the fault-injection proof.
|
|
88
|
+
var svcProxy = await services.requireService("toxiproxyRedis");
|
|
89
|
+
var svcApi = await services.requireService("toxiproxy");
|
|
90
|
+
if (!svcProxy.ok || !svcApi.ok) {
|
|
91
|
+
console.log(" [redis-reconnect] toxiproxy unreachable — skipping " +
|
|
92
|
+
"(" + (svcProxy.ok ? "" : svcProxy.reason + " ") + (svcApi.ok ? "" : svcApi.reason) + ")");
|
|
93
|
+
console.log(" [redis-reconnect] bring up: docker compose -f docker-compose.test.yml up -d --wait");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Always leave the proxy enabled when we exit, even on a mid-test
|
|
98
|
+
// failure — a left-disabled proxy would poison every later redis test.
|
|
99
|
+
var restored = false;
|
|
100
|
+
async function _restoreProxy() {
|
|
101
|
+
if (restored) return;
|
|
102
|
+
restored = true;
|
|
103
|
+
try { await _setProxyEnabled(true); } catch (_e) { /* best-effort */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var c = null;
|
|
107
|
+
try {
|
|
108
|
+
// Start from a known-good proxy state.
|
|
109
|
+
await _setProxyEnabled(true);
|
|
110
|
+
|
|
111
|
+
// Client through the proxy. A generous reconnect budget (the outage
|
|
112
|
+
// window spans several backoff steps) + short connect/command timeouts
|
|
113
|
+
// so an in-flight op against a dead proxy settles fast instead of
|
|
114
|
+
// sitting on the full default. db 15 isolates test data.
|
|
115
|
+
c = redisClient.create({
|
|
116
|
+
url: PROXIED_REDIS + "/15",
|
|
117
|
+
connectTimeoutMs: 1000,
|
|
118
|
+
commandTimeoutMs: 1500,
|
|
119
|
+
maxReconnectAttempts: 50,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---- 1. working connection ----
|
|
123
|
+
await c.connect();
|
|
124
|
+
check("connect: established through toxiproxy", c.isOpen());
|
|
125
|
+
|
|
126
|
+
var key = "blamejs:test:reconnect:" + Date.now();
|
|
127
|
+
var setRv = await c.command("SET", key, "before-outage");
|
|
128
|
+
check("pre-outage SET: returns OK",
|
|
129
|
+
setRv === "OK" || (Buffer.isBuffer(setRv) && setRv.toString() === "OK"));
|
|
130
|
+
var getRv = await c.command("GET", key);
|
|
131
|
+
check("pre-outage GET: round-trips",
|
|
132
|
+
Buffer.isBuffer(getRv) && getRv.toString() === "before-outage");
|
|
133
|
+
|
|
134
|
+
var stateBefore = c._state();
|
|
135
|
+
check("pre-outage _state: connected, no reconnect pending, attempt 0",
|
|
136
|
+
stateBefore.connected === true &&
|
|
137
|
+
stateBefore.reconnectPending === false &&
|
|
138
|
+
stateBefore.reconnect === 0);
|
|
139
|
+
|
|
140
|
+
// ---- 2. outage: disable the proxy. Existing conns are closed; new
|
|
141
|
+
// dials are refused. This drives _teardownSocket → drain
|
|
142
|
+
// pending + schedule reconnect. ----
|
|
143
|
+
await _setProxyEnabled(false);
|
|
144
|
+
|
|
145
|
+
// An in-flight op issued DURING the outage must reject, not wedge.
|
|
146
|
+
// Whether the socket-drop reaches the client before or after this
|
|
147
|
+
// write, the command settles with an error (drained pending, write
|
|
148
|
+
// failure, queued-timeout, or command-timeout) — never an unsettled
|
|
149
|
+
// await. Bound the whole thing well under any hang with waitUntil so
|
|
150
|
+
// a regression that DOES wedge fails the gate as a timeout instead of
|
|
151
|
+
// hanging the suite.
|
|
152
|
+
var inflightErr = null;
|
|
153
|
+
var inflightResolved = false;
|
|
154
|
+
var inflightSettled = false;
|
|
155
|
+
c.command("GET", key).then(
|
|
156
|
+
function () { inflightResolved = true; inflightSettled = true; },
|
|
157
|
+
function (e) { inflightErr = e; inflightSettled = true; }
|
|
158
|
+
);
|
|
159
|
+
await helpers.waitUntil(function () { return inflightSettled; }, {
|
|
160
|
+
timeoutMs: 8000,
|
|
161
|
+
label: "redis-reconnect: in-flight op settles during outage (no caller wedge)",
|
|
162
|
+
});
|
|
163
|
+
check("in-flight op during outage SETTLES (no wedge)", inflightSettled === true);
|
|
164
|
+
check("in-flight op during outage REJECTS with a transport/timeout error",
|
|
165
|
+
inflightResolved === false &&
|
|
166
|
+
inflightErr !== null && typeof inflightErr.code === "string");
|
|
167
|
+
|
|
168
|
+
// ---- 3. the lost socket scheduled a reconnect. The client is now
|
|
169
|
+
// disconnected and the reconnect machinery is engaged
|
|
170
|
+
// (a timer pending and/or attempts advancing). ----
|
|
171
|
+
var reconnecting = await helpers.waitUntil(function () {
|
|
172
|
+
var s = c._state();
|
|
173
|
+
// Engaged = no longer connected AND (a backoff timer is pending OR a
|
|
174
|
+
// dial is mid-flight OR at least one attempt has been counted).
|
|
175
|
+
if (s.connected) return false;
|
|
176
|
+
return (s.reconnectPending || s.connecting || s.reconnect > 0) ? s : false;
|
|
177
|
+
}, {
|
|
178
|
+
timeoutMs: 8000,
|
|
179
|
+
label: "redis-reconnect: client schedules a reconnect after the socket drop",
|
|
180
|
+
});
|
|
181
|
+
check("outage: client is disconnected", reconnecting.connected === false);
|
|
182
|
+
check("outage: a reconnect is engaged (pending timer / dialing / attempt counted)",
|
|
183
|
+
reconnecting.reconnectPending === true ||
|
|
184
|
+
reconnecting.connecting === true ||
|
|
185
|
+
reconnecting.reconnect > 0);
|
|
186
|
+
check("outage: did NOT give up (budget not exhausted mid-outage)",
|
|
187
|
+
reconnecting.gaveUp === false);
|
|
188
|
+
|
|
189
|
+
// ---- 4. no reconnect storm. Single-flight means: at any instant at
|
|
190
|
+
// most ONE backoff timer is pending, and the attempt counter
|
|
191
|
+
// advances monotonically by the dial cadence — it must not
|
|
192
|
+
// leap by 2 from a single socket failure surfacing as both
|
|
193
|
+
// `error` and `close`. The first retry backoff is 100ms, so
|
|
194
|
+
// sampling at 20ms (provably below the minimum inter-dial gap)
|
|
195
|
+
// means at most ONE dial can complete per sample window; a
|
|
196
|
+
// per-sample attempt delta of 2 would prove two reconnects
|
|
197
|
+
// fired for one window — a storm / double-schedule. We sample
|
|
198
|
+
// across ~2.4s, which spans the early small-backoff steps
|
|
199
|
+
// (100/200/400/800ms) where stacking would show first. ----
|
|
200
|
+
var maxDelta = 0;
|
|
201
|
+
var prevAttempt = c._state().reconnect;
|
|
202
|
+
var sawNonBoolPending = false; // (structurally a boolean — timer
|
|
203
|
+
// !== null — but assert it: a count
|
|
204
|
+
// here would mean stacked timers)
|
|
205
|
+
for (var i = 0; i < 120; i++) {
|
|
206
|
+
var s = c._state();
|
|
207
|
+
var delta = s.reconnect - prevAttempt;
|
|
208
|
+
if (delta > maxDelta) maxDelta = delta;
|
|
209
|
+
prevAttempt = s.reconnect;
|
|
210
|
+
if (s.reconnectPending !== true && s.reconnectPending !== false) sawNonBoolPending = true;
|
|
211
|
+
await helpers.passiveObserve(20, "redis-reconnect storm-watch sample " + i);
|
|
212
|
+
}
|
|
213
|
+
check("no reconnect storm: attempts advance by <= 1 per 20ms sample (single-flight, no double-schedule)",
|
|
214
|
+
maxDelta <= 1);
|
|
215
|
+
check("no reconnect storm: reconnectPending is always a single boolean (one timer max)",
|
|
216
|
+
sawNonBoolPending === false);
|
|
217
|
+
|
|
218
|
+
// ---- 5. recovery: re-enable the proxy. The next scheduled dial
|
|
219
|
+
// succeeds and the client reconnects on its own. ----
|
|
220
|
+
await _setProxyEnabled(true);
|
|
221
|
+
|
|
222
|
+
// Poll a SYNCHRONOUS predicate (_state().connected) for the reconnect.
|
|
223
|
+
// Awaiting c.command() inside the predicate would backlog the command
|
|
224
|
+
// behind the client's reconnect timer — and that timer is unref'd by
|
|
225
|
+
// design (a backoff window must not by itself keep the loop alive), so
|
|
226
|
+
// with no other ref'd handle the loop could drain mid-await and the
|
|
227
|
+
// process would exit before the reconnect fires. waitUntil's own 25ms
|
|
228
|
+
// poll timer IS ref'd, so a sync predicate keeps the loop alive on each
|
|
229
|
+
// tick and lets the unref'd reconnect timer fire — the same role a
|
|
230
|
+
// real app's listening server socket plays in production.
|
|
231
|
+
var recovered = await helpers.waitUntil(function () {
|
|
232
|
+
return c._state().connected ? c._state() : false;
|
|
233
|
+
}, {
|
|
234
|
+
timeoutMs: 20000,
|
|
235
|
+
label: "redis-reconnect: client reconnects on its own after proxy re-enabled",
|
|
236
|
+
});
|
|
237
|
+
check("recovery: client reconnected after outage", recovered.connected === true);
|
|
238
|
+
check("recovery: client reports open again", c.isOpen());
|
|
239
|
+
|
|
240
|
+
// Now that the socket is back, a fresh SET/GET must round-trip.
|
|
241
|
+
var setAfter = await c.command("SET", key, "after-recovery");
|
|
242
|
+
check("recovery: post-reconnect SET returns OK",
|
|
243
|
+
setAfter === "OK" || (Buffer.isBuffer(setAfter) && setAfter.toString() === "OK"));
|
|
244
|
+
|
|
245
|
+
var getAfter = await c.command("GET", key);
|
|
246
|
+
check("recovery: GET round-trips the post-recovery value",
|
|
247
|
+
Buffer.isBuffer(getAfter) && getAfter.toString() === "after-recovery");
|
|
248
|
+
|
|
249
|
+
var stateAfter = c._state();
|
|
250
|
+
check("recovery: reconnect counter reset to 0 on successful reconnect",
|
|
251
|
+
stateAfter.reconnect === 0);
|
|
252
|
+
check("recovery: give-up latch cleared", stateAfter.gaveUp === false);
|
|
253
|
+
check("recovery: no reconnect timer left pending", stateAfter.reconnectPending === false);
|
|
254
|
+
|
|
255
|
+
// ---- 6. the recovered connection survives a SECOND independent
|
|
256
|
+
// outage — proves the reconnect path is reusable, not a
|
|
257
|
+
// one-shot, and the budget genuinely reset. ----
|
|
258
|
+
await _setProxyEnabled(false);
|
|
259
|
+
await helpers.waitUntil(function () {
|
|
260
|
+
var s = c._state();
|
|
261
|
+
return (!s.connected && (s.reconnectPending || s.connecting || s.reconnect > 0)) ? s : false;
|
|
262
|
+
}, {
|
|
263
|
+
timeoutMs: 8000,
|
|
264
|
+
label: "redis-reconnect: second outage re-engages the reconnect loop",
|
|
265
|
+
});
|
|
266
|
+
check("second outage: reconnect loop re-engaged (path is reusable)", !c._state().connected);
|
|
267
|
+
|
|
268
|
+
await _setProxyEnabled(true);
|
|
269
|
+
// Sync predicate again (see the phase-5 note on unref'd reconnect timers).
|
|
270
|
+
await helpers.waitUntil(function () {
|
|
271
|
+
return c._state().connected ? c._state() : false;
|
|
272
|
+
}, {
|
|
273
|
+
timeoutMs: 20000,
|
|
274
|
+
label: "redis-reconnect: second recovery — client reconnects again",
|
|
275
|
+
});
|
|
276
|
+
check("second recovery: client reconnected", c.isOpen());
|
|
277
|
+
var getAfter2 = await c.command("GET", key);
|
|
278
|
+
check("second recovery: GET round-trips the prior value (path is reusable)",
|
|
279
|
+
Buffer.isBuffer(getAfter2) && getAfter2.toString() === "after-recovery");
|
|
280
|
+
|
|
281
|
+
// ---- cleanup ----
|
|
282
|
+
await c.command("DEL", key);
|
|
283
|
+
} finally {
|
|
284
|
+
await _restoreProxy();
|
|
285
|
+
if (c) { try { await c.close(); } catch (_e) {} }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = { run: run };
|
|
290
|
+
|
|
291
|
+
if (require.main === module) {
|
|
292
|
+
run().then(
|
|
293
|
+
// No process.exit(0) on success: an immediate exit after console.log
|
|
294
|
+
// can drop the buffered banner when stdout is a pipe (the integration
|
|
295
|
+
// runner pipes it). close() + unref'd timers let the loop drain on its
|
|
296
|
+
// own, so the line flushes. The failure path still hard-exits 1.
|
|
297
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
|
|
298
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
299
|
+
);
|
|
300
|
+
}
|