@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,649 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live MySQL coverage for the b.sql-migrated cluster data layer — the
|
|
4
|
+
* parts that only ran on sqlite host smoke before this file: cluster-
|
|
5
|
+
* storage coercion (MySQL BIGINT text → JS number), pubsub-cluster
|
|
6
|
+
* publish/poll/prune, the cluster vault-key-consistency upsert (MySQL's
|
|
7
|
+
* `ON DUPLICATE KEY UPDATE scope = scope` DO-NOTHING fold + the backtick-
|
|
8
|
+
* quoted camelCase readback), and external-db-migrate up/down/status +
|
|
9
|
+
* advisory lock. The audit chain is Postgres/SQLite-only (frameworkSchema
|
|
10
|
+
* refuses MySQL), so there is no MySQL audit-chain path to prove here —
|
|
11
|
+
* that surface lives in the PG file.
|
|
12
|
+
*
|
|
13
|
+
* The "driver" is a docker-exec mysql shim — every query() shells
|
|
14
|
+
* mysql --batch --raw <db> -e "<SQL>"
|
|
15
|
+
* inside the container via execFileSync (no shell parsing of SQL beyond
|
|
16
|
+
* the single -e argument). Writes follow up with `SELECT ROW_COUNT()` for
|
|
17
|
+
* affectedRows. It removes the npm-mysql-driver dep while exercising the
|
|
18
|
+
* framework's real MySQL SQL against a real 8.x server.
|
|
19
|
+
*
|
|
20
|
+
* COERCION note: MySQL --batch renders every value as text, so a BIGINT
|
|
21
|
+
* comes back as a STRING (like a streaming text protocol). The framework's
|
|
22
|
+
* coerceRow normalizes the int framework columns back to JS numbers — the
|
|
23
|
+
* property this file asserts on the real backend.
|
|
24
|
+
*
|
|
25
|
+
* RUN: node scripts/test-integration.js --skip-service-check data-layer-cluster-mysql
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var execFileSync = require("node:child_process").execFileSync;
|
|
29
|
+
var helpers = require("../helpers");
|
|
30
|
+
var check = helpers.check;
|
|
31
|
+
var services = require("../helpers/services");
|
|
32
|
+
var b = require("../../");
|
|
33
|
+
|
|
34
|
+
var CONTAINER = "blamejs-test-mysql";
|
|
35
|
+
var DB_NAME = "blamejs_cluster_test";
|
|
36
|
+
|
|
37
|
+
// ---- one-shot mysql (setup / teardown / out-of-band assertions) ----
|
|
38
|
+
function _mysqlRoot(sql, dbName) {
|
|
39
|
+
var args = ["exec", "-i", CONTAINER, "mysql", "-uroot", "-pblamejs_test_root", "--batch", "--raw"];
|
|
40
|
+
if (dbName) args.push(dbName);
|
|
41
|
+
args.push("-e", sql);
|
|
42
|
+
return execFileSync("docker", args, { stdio: ["pipe", "pipe", "pipe"] }).toString("utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- docker-exec mysql driver (faithful to a text-protocol driver) ----
|
|
46
|
+
function _makeDockerMysqlDriver() {
|
|
47
|
+
return {
|
|
48
|
+
connect: async function () { return { id: 1 }; },
|
|
49
|
+
query: async function (_client, sql, params) {
|
|
50
|
+
params = params || [];
|
|
51
|
+
var bound = _bindParams(sql, params);
|
|
52
|
+
var t = bound.trim();
|
|
53
|
+
// DML / DDL → run + read ROW_COUNT() for affectedRows (mysql2 style).
|
|
54
|
+
// ROW_COUNT() is CONNECTION-scoped, so the statement + the read must
|
|
55
|
+
// run in ONE mysql invocation (each `mysql -e` is a fresh connection);
|
|
56
|
+
// a `;`-joined multi-statement keeps them on the same connection.
|
|
57
|
+
if (/^(CREATE|ALTER|INSERT|UPDATE|DELETE|DROP|REPLACE|TRUNCATE)\b/i.test(t)) {
|
|
58
|
+
var stmt = bound.replace(/;\s*$/, "");
|
|
59
|
+
var ar = _exec(stmt + "; SELECT ROW_COUNT() AS n");
|
|
60
|
+
var parsed = _parseBatch(ar);
|
|
61
|
+
var n = parsed.rows[0] ? Number(parsed.rows[0].n) : 0;
|
|
62
|
+
// ROW_COUNT() returns -1 for statements that don't affect rows
|
|
63
|
+
// (e.g. CREATE/DROP); normalize that to 0 affected.
|
|
64
|
+
if (!isFinite(n) || n < 0) n = 0;
|
|
65
|
+
return { rows: [], affectedRows: n, rowCount: n };
|
|
66
|
+
}
|
|
67
|
+
var out = _exec(bound);
|
|
68
|
+
var parsedSel = _parseBatch(out);
|
|
69
|
+
return { rows: parsedSel.rows, rowCount: parsedSel.rows.length };
|
|
70
|
+
},
|
|
71
|
+
close: async function () { /* no-op */ },
|
|
72
|
+
dialect: "mysql",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _exec(sql) {
|
|
77
|
+
try {
|
|
78
|
+
return execFileSync("docker",
|
|
79
|
+
["exec", "-i", CONTAINER, "mysql", "-uroot", "-pblamejs_test_root",
|
|
80
|
+
"--batch", "--raw", DB_NAME, "-e", sql],
|
|
81
|
+
{ stdio: ["pipe", "pipe", "pipe"] }).toString("utf8");
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// mysql writes its error to stderr; surface it with a SQLSTATE-ish code
|
|
84
|
+
// when present so the framework's coded-error paths see something.
|
|
85
|
+
var msg = e.stderr ? e.stderr.toString("utf8") : (e.message || String(e));
|
|
86
|
+
// Strip the benign "World-writable config file ... ignored" / password
|
|
87
|
+
// warnings mysql prints to stderr so the surfaced message is the real
|
|
88
|
+
// ERROR line, not warning noise.
|
|
89
|
+
var errLine = (msg.split(/\r?\n/).filter(function (l) {
|
|
90
|
+
return /ERROR \d+/.test(l);
|
|
91
|
+
})[0]) || msg.trim();
|
|
92
|
+
var err = new Error(errLine.trim());
|
|
93
|
+
var m = /ERROR (\d+) \(([0-9A-Za-z]{5})\)/.exec(msg);
|
|
94
|
+
if (m) { err.errno = Number(m[1]); err.code = m[2]; err.sqlState = m[2]; }
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _bindParams(sql, params) {
|
|
100
|
+
var i = 0;
|
|
101
|
+
return sql.replace(/\?/g, function () {
|
|
102
|
+
if (i >= params.length) throw new Error("placeholder/param count mismatch");
|
|
103
|
+
var p = params[i++];
|
|
104
|
+
if (p === null || p === undefined) return "NULL";
|
|
105
|
+
if (typeof p === "number") return String(p);
|
|
106
|
+
if (typeof p === "boolean") return p ? "1" : "0";
|
|
107
|
+
return "'" + String(p).replace(/\\/g, "\\\\").replace(/'/g, "''") + "'";
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --batch output is tab-separated with a header row; "NULL" is the null
|
|
112
|
+
// sentinel. Every cell is text (BIGINT included) — coerceRow's job.
|
|
113
|
+
function _parseBatch(out) {
|
|
114
|
+
var lines = out.split(/\r?\n/).filter(function (l) { return l.length > 0; });
|
|
115
|
+
if (lines.length < 1) return { rows: [] };
|
|
116
|
+
var headers = lines[0].split("\t");
|
|
117
|
+
var rows = [];
|
|
118
|
+
for (var i = 1; i < lines.length; i++) {
|
|
119
|
+
var cells = lines[i].split("\t");
|
|
120
|
+
var row = {};
|
|
121
|
+
for (var j = 0; j < headers.length; j++) {
|
|
122
|
+
var v = cells[j];
|
|
123
|
+
row[headers[j]] = (v === "NULL" || v === undefined) ? null : v;
|
|
124
|
+
}
|
|
125
|
+
rows.push(row);
|
|
126
|
+
}
|
|
127
|
+
return { rows: rows };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Tables this file owns, dropped in setup + teardown.
|
|
131
|
+
var OWNED_TABLES = [
|
|
132
|
+
"_blamejs_pubsub_messages",
|
|
133
|
+
"_blamejs_cluster_state",
|
|
134
|
+
"_blamejs_leader",
|
|
135
|
+
"_blamejs_externaldb_migrations",
|
|
136
|
+
"_blamejs_externaldb_migrations_lock",
|
|
137
|
+
"_blamejs_schema_version_history",
|
|
138
|
+
"_blamejs_audit_tip",
|
|
139
|
+
"_blamejs_consent_tip",
|
|
140
|
+
"mig_demo_widgets_my",
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Empty chain-tip tables so cluster.init's rollback check takes the
|
|
144
|
+
// "no tip row → skip" branch instead of the "table missing" branch (the
|
|
145
|
+
// latter mis-handled on MySQL — see _proveChainTipSkipOnMysql). Backtick-
|
|
146
|
+
// quoted camelCase columns, matching the framework tip DDL.
|
|
147
|
+
function _ensureTipTables() {
|
|
148
|
+
_mysqlRoot(
|
|
149
|
+
"CREATE TABLE IF NOT EXISTS `_blamejs_audit_tip` (" +
|
|
150
|
+
" `scope` VARCHAR(64) PRIMARY KEY," +
|
|
151
|
+
" `atMonotonicCounter` BIGINT NOT NULL DEFAULT 0," +
|
|
152
|
+
" `rowHash` TEXT," +
|
|
153
|
+
" `signedAt` TEXT," +
|
|
154
|
+
" `fencingToken` BIGINT NOT NULL DEFAULT 0)", DB_NAME);
|
|
155
|
+
_mysqlRoot(
|
|
156
|
+
"CREATE TABLE IF NOT EXISTS `_blamejs_consent_tip` (" +
|
|
157
|
+
" `scope` VARCHAR(64) PRIMARY KEY," +
|
|
158
|
+
" `atMonotonicCounter` BIGINT NOT NULL DEFAULT 0," +
|
|
159
|
+
" `rowHash` TEXT," +
|
|
160
|
+
" `signedAt` TEXT," +
|
|
161
|
+
" `fencingToken` BIGINT NOT NULL DEFAULT 0)", DB_NAME);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _dropOwned() {
|
|
165
|
+
for (var i = 0; i < OWNED_TABLES.length; i++) {
|
|
166
|
+
try { _mysqlRoot("DROP TABLE IF EXISTS `" + OWNED_TABLES[i] + "`", DB_NAME); } catch (_e) {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Out-of-band COUNT(*) → JS number, parsed from the --batch header+value
|
|
171
|
+
// output ("n\n<count>").
|
|
172
|
+
function _countMysql(table, whereClause) {
|
|
173
|
+
var sql = "SELECT count(*) AS n FROM " + table +
|
|
174
|
+
(whereClause ? " WHERE " + whereClause : "");
|
|
175
|
+
var out = _mysqlRoot(sql, DB_NAME);
|
|
176
|
+
var parsed = _parseBatch(out);
|
|
177
|
+
return parsed.rows[0] ? Number(parsed.rows[0].n) : 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Soft findings — a recorded lib-bug surfaced live that must NOT halt the
|
|
181
|
+
// rest of the suite; printed at the end + makes the file exit non-zero.
|
|
182
|
+
var _softFindings = [];
|
|
183
|
+
function _softCheck(label, ok) {
|
|
184
|
+
if (ok) { check(label, true); return; }
|
|
185
|
+
_softFindings.push(label);
|
|
186
|
+
console.error("[SOFT-FAIL] " + label);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function run() {
|
|
190
|
+
var svc = await services.requireService("mysql");
|
|
191
|
+
if (!svc.ok) throw new Error("mysql unreachable: " + svc.reason);
|
|
192
|
+
|
|
193
|
+
_mysqlRoot("CREATE DATABASE IF NOT EXISTS " + DB_NAME);
|
|
194
|
+
_dropOwned();
|
|
195
|
+
|
|
196
|
+
var driver = _makeDockerMysqlDriver();
|
|
197
|
+
b.cluster._resetForTest();
|
|
198
|
+
b.externalDb._resetForTest();
|
|
199
|
+
b.externalDb.init({
|
|
200
|
+
backends: {
|
|
201
|
+
ops: {
|
|
202
|
+
connect: driver.connect, query: driver.query, close: driver.close,
|
|
203
|
+
dialect: "mysql",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// The cluster boot-time rollback check on the audit/consent chains is
|
|
210
|
+
// gated by a missing-table skip. On MySQL the chain-tip tables don't
|
|
211
|
+
// exist in this gates-only setup, so the check should SKIP — but the
|
|
212
|
+
// skip-regex in cluster._checkChainTipRollback only recognizes Postgres/
|
|
213
|
+
// SQLite phrasing ("no such table" / "does not exist"), NOT MySQL's
|
|
214
|
+
// "Table 'x' doesn't exist". Surface that as the first proof, then
|
|
215
|
+
// create empty tip tables so the remaining sections (which need
|
|
216
|
+
// cluster.init to complete) can run.
|
|
217
|
+
await _proveChainTipSkipOnMysql();
|
|
218
|
+
_ensureTipTables();
|
|
219
|
+
await _proveClusterStorageCoercion();
|
|
220
|
+
await _provePubsubCluster();
|
|
221
|
+
await _proveVaultKeyConsistency();
|
|
222
|
+
await _proveExternalDbMigrate();
|
|
223
|
+
await _proveRoleHardeningSkip();
|
|
224
|
+
} finally {
|
|
225
|
+
try { await b.cluster.shutdown(); } catch (_e) {}
|
|
226
|
+
b.cluster._resetForTest();
|
|
227
|
+
try { await b.externalDb.shutdown(); } catch (_e) {}
|
|
228
|
+
_dropOwned();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (_softFindings.length > 0) {
|
|
232
|
+
throw new Error("data-layer-cluster-mysql: " + _softFindings.length +
|
|
233
|
+
" live-surfaced lib bug(s):\n - " + _softFindings.join("\n - "));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ======================================================================
|
|
238
|
+
// 0. cluster boot-time chain-tip rollback check, gates-only mode, MySQL.
|
|
239
|
+
// cluster.init runs _checkChainTipRollback on the audit + consent
|
|
240
|
+
// chains; when the tip table is absent (framework state not resident on
|
|
241
|
+
// the external DB) it is DOCUMENTED to "skip silently (cluster gates-
|
|
242
|
+
// only mode)". That skip keys off the driver error text via the regex
|
|
243
|
+
// /no such table|does not exist|relation .* does not exist/i — which
|
|
244
|
+
// recognizes Postgres ("does not exist") + SQLite ("no such table") but
|
|
245
|
+
// NOT MySQL's "Table 'x' doesn't exist". So on MySQL the gates-only skip
|
|
246
|
+
// mis-fires and cluster.init throws ER_NO_SUCH_TABLE (1146/42S02). This
|
|
247
|
+
// asserts the CORRECT contract (init completes) so the MySQL-only gap
|
|
248
|
+
// surfaces; the tables are absent here on purpose.
|
|
249
|
+
// ======================================================================
|
|
250
|
+
async function _proveChainTipSkipOnMysql() {
|
|
251
|
+
// No tip tables exist (dropOwned cleared them). A gates-only cluster.init
|
|
252
|
+
// should complete by skipping the rollback check.
|
|
253
|
+
var initErr = null;
|
|
254
|
+
try {
|
|
255
|
+
await b.cluster.init({
|
|
256
|
+
nodeId: "tip-skip-node",
|
|
257
|
+
role: "leader",
|
|
258
|
+
leaseTtl: b.constants.TIME.seconds(30),
|
|
259
|
+
heartbeatInterval: b.constants.TIME.seconds(10),
|
|
260
|
+
externalDbBackend: "ops",
|
|
261
|
+
dialect: "mysql",
|
|
262
|
+
});
|
|
263
|
+
} catch (e) { initErr = e; }
|
|
264
|
+
console.error("[chain-tip-skip mysql] " +
|
|
265
|
+
(initErr ? ("code=" + (initErr.code || "") + " msg=" + (initErr.message || "").slice(0, 160))
|
|
266
|
+
: "init completed (skipped)"));
|
|
267
|
+
_softCheck("cluster (mysql): gates-only cluster.init skips the missing chain-tip " +
|
|
268
|
+
"rollback check instead of throwing ER_NO_SUCH_TABLE on the MySQL " +
|
|
269
|
+
"\"doesn't exist\" phrasing the skip-regex misses",
|
|
270
|
+
initErr === null);
|
|
271
|
+
// Reset for the next section regardless (init may have partially run).
|
|
272
|
+
try { await b.cluster.shutdown(); } catch (_e) {}
|
|
273
|
+
b.cluster._resetForTest();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ======================================================================
|
|
277
|
+
// 1. cluster-storage coercion on real MySQL. execute() routes framework
|
|
278
|
+
// SQL to the external DB in cluster mode, then coerceRows-normalizes
|
|
279
|
+
// the driver-native shape. MySQL --batch hands BIGINT back as text;
|
|
280
|
+
// assert the readback int columns are JS NUMBERS.
|
|
281
|
+
// ======================================================================
|
|
282
|
+
async function _proveClusterStorageCoercion() {
|
|
283
|
+
await b.cluster.init({
|
|
284
|
+
nodeId: "cs-node-my",
|
|
285
|
+
role: "leader",
|
|
286
|
+
leaseTtl: b.constants.TIME.seconds(30),
|
|
287
|
+
heartbeatInterval: b.constants.TIME.seconds(10),
|
|
288
|
+
externalDbBackend: "ops",
|
|
289
|
+
dialect: "mysql",
|
|
290
|
+
});
|
|
291
|
+
check("cluster-storage (mysql): cluster mode routes framework state to MySQL",
|
|
292
|
+
b.cluster.isClusterMode() === true);
|
|
293
|
+
|
|
294
|
+
// Pubsub fan-out table — MySQL DDL with backtick-quoted camelCase columns
|
|
295
|
+
// (mirrors framework-schema _pubsubMessagesDDL for mysql). MySQL preserves
|
|
296
|
+
// identifier case, so the camelCase reads back verbatim regardless of
|
|
297
|
+
// quoting; the BIGINT columns are the coercion subjects.
|
|
298
|
+
_mysqlRoot(
|
|
299
|
+
"CREATE TABLE IF NOT EXISTS `_blamejs_pubsub_messages` (" +
|
|
300
|
+
" `id` BIGINT PRIMARY KEY AUTO_INCREMENT," +
|
|
301
|
+
" `topic` TEXT NOT NULL," +
|
|
302
|
+
" `payload` TEXT NOT NULL," +
|
|
303
|
+
" `publishedAt` BIGINT NOT NULL," +
|
|
304
|
+
" `publishedBy` VARCHAR(255) NOT NULL" +
|
|
305
|
+
")", DB_NAME);
|
|
306
|
+
|
|
307
|
+
var bigAt = 1700000000000;
|
|
308
|
+
var insRes = await b.clusterStorage.execute(
|
|
309
|
+
"INSERT INTO `_blamejs_pubsub_messages` (`topic`,`payload`,`publishedAt`,`publishedBy`) " +
|
|
310
|
+
"VALUES (?, ?, ?, ?)",
|
|
311
|
+
["coerce-topic", '{"k":1}', bigAt, "cs-node-my"]);
|
|
312
|
+
check("cluster-storage (mysql): INSERT through execute() affected 1 row",
|
|
313
|
+
insRes.rowCount === 1);
|
|
314
|
+
|
|
315
|
+
var row = await b.clusterStorage.executeOne(
|
|
316
|
+
"SELECT `id`,`topic`,`payload`,`publishedAt`,`publishedBy` " +
|
|
317
|
+
"FROM `_blamejs_pubsub_messages` WHERE `publishedBy` = ?",
|
|
318
|
+
["cs-node-my"]);
|
|
319
|
+
check("cluster-storage (mysql): round-tripped the row by camelCase key",
|
|
320
|
+
row !== null && row.publishedBy === "cs-node-my" && row.topic === "coerce-topic");
|
|
321
|
+
check("cluster-storage (mysql) COERCION: BIGINT publishedAt coerced text→number",
|
|
322
|
+
typeof row.publishedAt === "number" && row.publishedAt === bigAt);
|
|
323
|
+
check("cluster-storage (mysql) COERCION: BIGINT id coerced text→number",
|
|
324
|
+
typeof row.id === "number" && row.id >= 1);
|
|
325
|
+
check("cluster-storage (mysql) COERCION: text payload left as the string it is",
|
|
326
|
+
typeof row.payload === "string" && row.payload === '{"k":1}');
|
|
327
|
+
|
|
328
|
+
await b.cluster.shutdown();
|
|
329
|
+
b.cluster._resetForTest();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ======================================================================
|
|
333
|
+
// 2. pubsub-cluster publish / poll / prune on real MySQL.
|
|
334
|
+
// ======================================================================
|
|
335
|
+
async function _provePubsubCluster() {
|
|
336
|
+
_mysqlRoot("DROP TABLE IF EXISTS `_blamejs_pubsub_messages`", DB_NAME);
|
|
337
|
+
_mysqlRoot(
|
|
338
|
+
"CREATE TABLE IF NOT EXISTS `_blamejs_pubsub_messages` (" +
|
|
339
|
+
" `id` BIGINT PRIMARY KEY AUTO_INCREMENT," +
|
|
340
|
+
" `topic` TEXT NOT NULL," +
|
|
341
|
+
" `payload` TEXT NOT NULL," +
|
|
342
|
+
" `publishedAt` BIGINT NOT NULL," +
|
|
343
|
+
" `publishedBy` VARCHAR(255) NOT NULL" +
|
|
344
|
+
")", DB_NAME);
|
|
345
|
+
|
|
346
|
+
await b.cluster.init({
|
|
347
|
+
nodeId: "node-pub-my",
|
|
348
|
+
role: "leader",
|
|
349
|
+
leaseTtl: b.constants.TIME.seconds(30),
|
|
350
|
+
heartbeatInterval: b.constants.TIME.seconds(10),
|
|
351
|
+
externalDbBackend: "ops",
|
|
352
|
+
dialect: "mysql",
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
var pubsubBackend = require("../../lib/pubsub-cluster");
|
|
356
|
+
var pubView = { currentNodeId: function () { return "node-pub-my"; } };
|
|
357
|
+
var subView = { currentNodeId: function () { return "node-sub-my"; } };
|
|
358
|
+
|
|
359
|
+
var publisher = pubsubBackend.create({
|
|
360
|
+
cluster: pubView, pollIntervalMs: 25, retentionMs: b.constants.TIME.minutes(1),
|
|
361
|
+
});
|
|
362
|
+
var subscriber = pubsubBackend.create({
|
|
363
|
+
cluster: subView, pollIntervalMs: 25, retentionMs: b.constants.TIME.minutes(1),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// pubsub-cluster builds its INSERT/SELECT/DELETE through b.sql WITHOUT a
|
|
367
|
+
// { dialect } option, so b.sql defaults to Postgres and double-quotes the
|
|
368
|
+
// identifiers ("topic", "publishedAt", …). SQLite + Postgres accept double-
|
|
369
|
+
// quoted identifiers; MySQL (no ANSI_QUOTES) reads them as STRING LITERALS,
|
|
370
|
+
// so the statement is a syntax error (ER 1064). pubsub on a MySQL cluster
|
|
371
|
+
// backend is therefore broken. Drive the real publish and record the
|
|
372
|
+
// failure as a soft finding so the remaining sections still run.
|
|
373
|
+
var pubErr = null;
|
|
374
|
+
var pr = null;
|
|
375
|
+
try { pr = await publisher.publishRemote("orders:created", { orderId: "o-7", amount: 42 }); }
|
|
376
|
+
catch (e) { pubErr = e; }
|
|
377
|
+
console.error("[pubsub-cluster mysql publish] " +
|
|
378
|
+
(pubErr ? ("code=" + (pubErr.code || "") + " msg=" + (pubErr.message || "").slice(0, 160))
|
|
379
|
+
: "ok"));
|
|
380
|
+
_softCheck("pubsub-cluster (mysql): publishRemote composes valid MySQL — " +
|
|
381
|
+
"pubsub-cluster's b.sql builders pass NO { dialect } so they emit " +
|
|
382
|
+
"Postgres double-quoted identifiers, invalid as identifiers on MySQL",
|
|
383
|
+
pubErr === null && pr && pr.remote === 1);
|
|
384
|
+
|
|
385
|
+
if (pubErr === null) {
|
|
386
|
+
var landed = _mysqlRoot(
|
|
387
|
+
"SELECT `topic`,`publishedBy` FROM `_blamejs_pubsub_messages` " +
|
|
388
|
+
"WHERE `topic` = 'orders:created'", DB_NAME);
|
|
389
|
+
check("pubsub-cluster (mysql): publish row physically present on real MySQL",
|
|
390
|
+
/orders:created/.test(landed) && /node-pub-my/.test(landed));
|
|
391
|
+
|
|
392
|
+
var received = [];
|
|
393
|
+
subscriber.start(function (topic, payload, meta) {
|
|
394
|
+
received.push({ topic: topic, payload: payload, meta: meta });
|
|
395
|
+
});
|
|
396
|
+
await helpers.waitUntil(function () { return received.length >= 1; }, {
|
|
397
|
+
timeoutMs: 15000,
|
|
398
|
+
label: "pubsub-cluster (mysql): subscriber dispatched the remote row",
|
|
399
|
+
}).catch(function () { /* retry below */ });
|
|
400
|
+
if (received.length === 0) {
|
|
401
|
+
await publisher.publishRemote("orders:created", { orderId: "o-8", amount: 99 });
|
|
402
|
+
await helpers.waitUntil(function () { return received.length >= 1; }, {
|
|
403
|
+
timeoutMs: 15000,
|
|
404
|
+
label: "pubsub-cluster (mysql): subscriber dispatched a post-prime remote row",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
var first = received[0];
|
|
408
|
+
check("pubsub-cluster (mysql): subscriber received the remote topic verbatim",
|
|
409
|
+
first.topic === "orders:created");
|
|
410
|
+
check("pubsub-cluster (mysql): meta.publishedBy is the PUBLISHER node",
|
|
411
|
+
first.meta && first.meta.publishedBy === "node-pub-my");
|
|
412
|
+
check("pubsub-cluster (mysql) COERCION: meta.publishedAt resolved to a finite number",
|
|
413
|
+
typeof first.meta.publishedAt === "number" && isFinite(first.meta.publishedAt));
|
|
414
|
+
subscriber.stop();
|
|
415
|
+
publisher.stop();
|
|
416
|
+
|
|
417
|
+
// ---- prune ----
|
|
418
|
+
_mysqlRoot("DELETE FROM `_blamejs_pubsub_messages`", DB_NAME);
|
|
419
|
+
_mysqlRoot(
|
|
420
|
+
"INSERT INTO `_blamejs_pubsub_messages` (`topic`,`payload`,`publishedAt`,`publishedBy`) " +
|
|
421
|
+
"VALUES ('expired','{}',1,'node-other'), " +
|
|
422
|
+
"('fresh','{}'," + (Date.now() + 60000) + ",'node-other')", DB_NAME);
|
|
423
|
+
var pruner = pubsubBackend.create({
|
|
424
|
+
cluster: subView, pollIntervalMs: 25, retentionMs: 1, pruneEveryMs: 1,
|
|
425
|
+
});
|
|
426
|
+
pruner.start(function () { /* no-op */ });
|
|
427
|
+
await helpers.waitUntil(function () {
|
|
428
|
+
return _countMysql("`_blamejs_pubsub_messages`", "`topic` = 'expired'") === 0;
|
|
429
|
+
}, { timeoutMs: 15000, label: "pubsub-cluster (mysql): prune DELETE removed the expired row" });
|
|
430
|
+
check("pubsub-cluster (mysql): prune removed the expired row", true);
|
|
431
|
+
check("pubsub-cluster (mysql): prune left the un-expired row intact",
|
|
432
|
+
_countMysql("`_blamejs_pubsub_messages`", "`topic` = 'fresh'") === 1);
|
|
433
|
+
pruner.stop();
|
|
434
|
+
} else {
|
|
435
|
+
try { subscriber.stop(); } catch (_e) {}
|
|
436
|
+
try { publisher.stop(); } catch (_e) {}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
await b.cluster.shutdown();
|
|
440
|
+
b.cluster._resetForTest();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ======================================================================
|
|
444
|
+
// 3. cluster vault-key-consistency upsert on real MySQL. cluster.init's
|
|
445
|
+
// _checkVaultKeyConsistency emits `INSERT ... ON DUPLICATE KEY UPDATE
|
|
446
|
+
// scope = scope` (the MySQL fold for DO NOTHING), then a backtick-quoted
|
|
447
|
+
// SELECT reading vaultKeyFp / recordedByNode / rotationEpoch back. Prove
|
|
448
|
+
// a first boot RECORDS the fingerprint, a second boot with the SAME key
|
|
449
|
+
// reads it back + AGREES (no VAULT_KEY_DRIFT), and the DO-NOTHING fold
|
|
450
|
+
// preserved the first recorder.
|
|
451
|
+
// ======================================================================
|
|
452
|
+
async function _proveVaultKeyConsistency() {
|
|
453
|
+
var fs = require("node:fs");
|
|
454
|
+
var os = require("node:os");
|
|
455
|
+
var path = require("node:path");
|
|
456
|
+
var vaultDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-vk-my-"));
|
|
457
|
+
await helpers.setupVaultOnly(vaultDir);
|
|
458
|
+
|
|
459
|
+
_mysqlRoot("DROP TABLE IF EXISTS `_blamejs_cluster_state`", DB_NAME);
|
|
460
|
+
_mysqlRoot("DROP TABLE IF EXISTS `_blamejs_leader`", DB_NAME);
|
|
461
|
+
|
|
462
|
+
await b.cluster.init({
|
|
463
|
+
nodeId: "vk-node-A-my",
|
|
464
|
+
role: "leader",
|
|
465
|
+
leaseTtl: b.constants.TIME.seconds(30),
|
|
466
|
+
heartbeatInterval: b.constants.TIME.seconds(10),
|
|
467
|
+
externalDbBackend: "ops",
|
|
468
|
+
dialect: "mysql",
|
|
469
|
+
});
|
|
470
|
+
check("vault-key (mysql): first boot completed (recorded fingerprint, no drift)", true);
|
|
471
|
+
|
|
472
|
+
var stateRow = _mysqlRoot(
|
|
473
|
+
"SELECT `vaultKeyFp`,`recordedByNode` FROM `_blamejs_cluster_state` " +
|
|
474
|
+
"WHERE `scope` = 'state'", DB_NAME);
|
|
475
|
+
check("vault-key (mysql): cluster-state row recorded by this node",
|
|
476
|
+
/vk-node-A-my/.test(stateRow));
|
|
477
|
+
check("vault-key (mysql): recorded fingerprint is a 128-hex SHA3-512 digest",
|
|
478
|
+
/\b[0-9a-f]{128}\b/.test(stateRow));
|
|
479
|
+
|
|
480
|
+
await b.cluster.shutdown();
|
|
481
|
+
b.cluster._resetForTest();
|
|
482
|
+
|
|
483
|
+
// Second boot, SAME vault key → reads back + agrees (DO-NOTHING fold).
|
|
484
|
+
var secondBootErr = null;
|
|
485
|
+
try {
|
|
486
|
+
await b.cluster.init({
|
|
487
|
+
nodeId: "vk-node-B-my",
|
|
488
|
+
role: "follower",
|
|
489
|
+
leaseTtl: b.constants.TIME.seconds(30),
|
|
490
|
+
heartbeatInterval: b.constants.TIME.seconds(10),
|
|
491
|
+
externalDbBackend: "ops",
|
|
492
|
+
dialect: "mysql",
|
|
493
|
+
});
|
|
494
|
+
} catch (e) { secondBootErr = e; }
|
|
495
|
+
check("vault-key (mysql): second boot with the SAME key did NOT throw " +
|
|
496
|
+
"VAULT_KEY_DRIFT (canonical fingerprint read back by camelCase key + matched)",
|
|
497
|
+
secondBootErr === null);
|
|
498
|
+
if (secondBootErr) {
|
|
499
|
+
check("VAULT-KEY (mysql) DETAIL: " + (secondBootErr.code || "") + " " +
|
|
500
|
+
(secondBootErr.message || String(secondBootErr)).slice(0, 200), false);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
var stillA = _mysqlRoot(
|
|
504
|
+
"SELECT `recordedByNode` FROM `_blamejs_cluster_state` WHERE `scope` = 'state'", DB_NAME);
|
|
505
|
+
check("vault-key (mysql): ON DUPLICATE KEY UPDATE scope=scope preserved the first recorder",
|
|
506
|
+
/vk-node-A-my/.test(stillA));
|
|
507
|
+
|
|
508
|
+
await b.cluster.shutdown();
|
|
509
|
+
b.cluster._resetForTest();
|
|
510
|
+
try { helpers.teardownVaultOnly(vaultDir); } catch (_e) {}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ======================================================================
|
|
514
|
+
// 4. external-db-migrate up / down / status + advisory lock on real MySQL.
|
|
515
|
+
// Same end-to-end shape as the PG file. The lock-contention recovery
|
|
516
|
+
// (INSERT-conflict → holder-naming SELECT) is the contrast point:
|
|
517
|
+
// MySQL does NOT abort the whole transaction on a duplicate-key error,
|
|
518
|
+
// so the recovery SELECT runs and the operator gets the clean
|
|
519
|
+
// "migration lock is held by <holder>" message (the PG file shows this
|
|
520
|
+
// path failing on Postgres, where the conflict aborts the txn).
|
|
521
|
+
// ======================================================================
|
|
522
|
+
async function _proveExternalDbMigrate() {
|
|
523
|
+
var fs = require("node:fs");
|
|
524
|
+
var os = require("node:os");
|
|
525
|
+
var path = require("node:path");
|
|
526
|
+
|
|
527
|
+
_mysqlRoot("DROP TABLE IF EXISTS `_blamejs_externaldb_migrations`", DB_NAME);
|
|
528
|
+
_mysqlRoot("DROP TABLE IF EXISTS `_blamejs_externaldb_migrations_lock`", DB_NAME);
|
|
529
|
+
_mysqlRoot("DROP TABLE IF EXISTS `_blamejs_schema_version_history`", DB_NAME);
|
|
530
|
+
_mysqlRoot("DROP TABLE IF EXISTS `mig_demo_widgets_my`", DB_NAME);
|
|
531
|
+
|
|
532
|
+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-mig-my-"));
|
|
533
|
+
// NOTE: the migration runner's tracking/history/lock DDL is emitted with
|
|
534
|
+
// { dialect: "postgres" } inside external-db-migrate.js, but the only
|
|
535
|
+
// dialect-specific token there is the BIGINT/INTEGER type, which MySQL
|
|
536
|
+
// accepts. The operator migration body below uses MySQL-portable SQL.
|
|
537
|
+
fs.writeFileSync(path.join(dir, "0001-create-widgets.js"),
|
|
538
|
+
"module.exports = {\n" +
|
|
539
|
+
" description: 'create widgets',\n" +
|
|
540
|
+
" up: async function (xdb) {\n" +
|
|
541
|
+
" await xdb.query('CREATE TABLE IF NOT EXISTS mig_demo_widgets_my (`id` VARCHAR(64) PRIMARY KEY, `n` BIGINT)', []);\n" +
|
|
542
|
+
" },\n" +
|
|
543
|
+
" down: async function (xdb) {\n" +
|
|
544
|
+
" await xdb.query('DROP TABLE IF EXISTS mig_demo_widgets_my', []);\n" +
|
|
545
|
+
" },\n" +
|
|
546
|
+
"};\n");
|
|
547
|
+
|
|
548
|
+
var migrate = b.externalDb.migrate.create({ dir: dir, backend: "ops", signHistory: false });
|
|
549
|
+
|
|
550
|
+
var migrateUsable = true;
|
|
551
|
+
var pre;
|
|
552
|
+
try {
|
|
553
|
+
pre = await migrate.status();
|
|
554
|
+
} catch (e) {
|
|
555
|
+
// If the runner's postgres-dialect bookkeeping DDL doesn't apply on
|
|
556
|
+
// MySQL, record that as a soft finding (the migrate runner advertises
|
|
557
|
+
// running against an externalDb backend, and MySQL is a declared
|
|
558
|
+
// externalDb dialect) and skip the rest of this section.
|
|
559
|
+
migrateUsable = false;
|
|
560
|
+
_softCheck("migrate (mysql): status() runs the runner's bookkeeping DDL on MySQL " +
|
|
561
|
+
"(got " + ((e && e.code) || "") + ": " + ((e && e.message) || String(e)).slice(0, 160) + ")",
|
|
562
|
+
false);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (migrateUsable) {
|
|
566
|
+
check("migrate (mysql): status() before up reports the migration pending",
|
|
567
|
+
pre.pending.indexOf("0001-create-widgets.js") !== -1 && pre.applied.length === 0);
|
|
568
|
+
|
|
569
|
+
var upRes = await migrate.up();
|
|
570
|
+
check("migrate (mysql): up() applied 0001-create-widgets.js",
|
|
571
|
+
upRes.applied.indexOf("0001-create-widgets.js") !== -1);
|
|
572
|
+
|
|
573
|
+
check("migrate (mysql): the migration's CREATE TABLE landed on real MySQL",
|
|
574
|
+
_countMysql("information_schema.tables",
|
|
575
|
+
"table_schema = '" + DB_NAME + "' AND table_name = 'mig_demo_widgets_my'") === 1);
|
|
576
|
+
|
|
577
|
+
var trackRow = _mysqlRoot(
|
|
578
|
+
"SELECT `name`,`description` FROM `_blamejs_externaldb_migrations` " +
|
|
579
|
+
"WHERE `name` = '0001-create-widgets.js'", DB_NAME);
|
|
580
|
+
check("migrate (mysql): tracking row recorded the applied migration",
|
|
581
|
+
/0001-create-widgets\.js/.test(trackRow));
|
|
582
|
+
|
|
583
|
+
check("migrate (mysql): advisory lock released after up() (0 lock rows remain)",
|
|
584
|
+
_countMysql("`_blamejs_externaldb_migrations_lock`", null) === 0);
|
|
585
|
+
|
|
586
|
+
var post = await migrate.status();
|
|
587
|
+
check("migrate (mysql): status() after up reports it applied, none pending",
|
|
588
|
+
post.applied.length === 1 && post.pending.length === 0);
|
|
589
|
+
|
|
590
|
+
var upAgain = await migrate.up();
|
|
591
|
+
check("migrate (mysql): re-running up() skips the already-applied migration",
|
|
592
|
+
upAgain.skipped.indexOf("0001-create-widgets.js") !== -1 && upAgain.applied.length === 0);
|
|
593
|
+
|
|
594
|
+
var downRes = await migrate.down({ steps: 1 });
|
|
595
|
+
check("migrate (mysql): down() reverted the migration",
|
|
596
|
+
downRes.reverted.indexOf("0001-create-widgets.js") !== -1);
|
|
597
|
+
check("migrate (mysql): down() DROPped the migration's table",
|
|
598
|
+
_countMysql("information_schema.tables",
|
|
599
|
+
"table_schema = '" + DB_NAME + "' AND table_name = 'mig_demo_widgets_my'") === 0);
|
|
600
|
+
|
|
601
|
+
// ---- lock contention (contrast vs PG): the holder-naming recovery
|
|
602
|
+
// SELECT runs because MySQL does not abort the txn on a dup-key. ----
|
|
603
|
+
_mysqlRoot(
|
|
604
|
+
"INSERT INTO `_blamejs_externaldb_migrations_lock` (`scope`,`lockedAt`,`lockedBy`) " +
|
|
605
|
+
"VALUES ('lock'," + Date.now() + ",'other-process@host@deadbeef')", DB_NAME);
|
|
606
|
+
var migrate2 = b.externalDb.migrate.create({ dir: dir, backend: "ops", signHistory: false });
|
|
607
|
+
var lockErr = null;
|
|
608
|
+
try { await migrate2.up(); } catch (e) { lockErr = e; }
|
|
609
|
+
var lockMsg = (lockErr && lockErr.message) || "";
|
|
610
|
+
console.error("[migrate-lock-contention mysql] code=" + ((lockErr && lockErr.code) || "") +
|
|
611
|
+
" | message=" + lockMsg.slice(0, 220));
|
|
612
|
+
check("migrate (mysql): up() threw when the advisory lock is held",
|
|
613
|
+
lockErr !== null);
|
|
614
|
+
check("migrate (mysql): lock-contention surfaces the operator-facing lock-held " +
|
|
615
|
+
"message naming the holding process (MySQL keeps the txn alive on dup-key)",
|
|
616
|
+
/lock.held|lock is held/i.test(lockMsg) && /other-process@host@deadbeef/.test(lockMsg));
|
|
617
|
+
_mysqlRoot("DELETE FROM `_blamejs_externaldb_migrations_lock`", DB_NAME);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ======================================================================
|
|
624
|
+
// 5. role-hardening on MySQL is a documented no-op (pg_roles is Postgres-
|
|
625
|
+
// only). assertRoleHardening must SKIP cleanly with empty observed
|
|
626
|
+
// lists rather than emitting MySQL-invalid SQL — the honest "no path
|
|
627
|
+
// on this dialect" behavior, asserted live.
|
|
628
|
+
// ======================================================================
|
|
629
|
+
async function _proveRoleHardeningSkip() {
|
|
630
|
+
var report = await b.externalDb.assertRoleHardening({
|
|
631
|
+
backend: "ops",
|
|
632
|
+
declaredRoles: ["app_user"],
|
|
633
|
+
mode: "throw", // even throw-mode must not raise on a non-PG dialect
|
|
634
|
+
ignoreSystem: true,
|
|
635
|
+
});
|
|
636
|
+
check("role-hardening (mysql): non-Postgres dialect skips cleanly (no observed roles)",
|
|
637
|
+
Array.isArray(report.observed) && report.observed.length === 0);
|
|
638
|
+
check("role-hardening (mysql): skip surfaces no unrecognized/missing under throw mode",
|
|
639
|
+
report.unrecognized.length === 0);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
module.exports = { run: run };
|
|
643
|
+
|
|
644
|
+
if (require.main === module) {
|
|
645
|
+
run().then(
|
|
646
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
|
|
647
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
648
|
+
);
|
|
649
|
+
}
|