@blamejs/blamejs-shop 0.4.30 → 0.4.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/checkout.js +8 -0
- package/lib/order.js +71 -11
- package/lib/vendor/MANIFEST.json +392 -278
- package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
- package/lib/vendor/blamejs/.gitignore +6 -0
- package/lib/vendor/blamejs/CHANGELOG.md +26 -0
- package/lib/vendor/blamejs/MIGRATING.md +43 -0
- package/lib/vendor/blamejs/README.md +8 -6
- package/lib/vendor/blamejs/SECURITY.md +19 -3
- package/lib/vendor/blamejs/api-snapshot.json +2190 -664
- package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
- package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
- package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
- package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
- package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
- package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
- package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
- package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
- package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
- package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
- package/lib/vendor/blamejs/lib/api-key.js +158 -77
- package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
- package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
- package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
- package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
- package/lib/vendor/blamejs/lib/audit.js +259 -123
- package/lib/vendor/blamejs/lib/auth/oauth.js +53 -9
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
- package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +31 -5
- package/lib/vendor/blamejs/lib/backup/index.js +45 -10
- package/lib/vendor/blamejs/lib/break-glass.js +355 -147
- package/lib/vendor/blamejs/lib/cache.js +174 -105
- package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
- package/lib/vendor/blamejs/lib/cli.js +19 -14
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
- package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
- package/lib/vendor/blamejs/lib/cluster.js +119 -71
- package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
- package/lib/vendor/blamejs/lib/compliance.js +206 -4
- package/lib/vendor/blamejs/lib/consent.js +82 -29
- package/lib/vendor/blamejs/lib/constants.js +27 -11
- package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
- package/lib/vendor/blamejs/lib/db-query.js +882 -260
- package/lib/vendor/blamejs/lib/db-schema.js +228 -44
- package/lib/vendor/blamejs/lib/db.js +249 -99
- package/lib/vendor/blamejs/lib/dsr.js +385 -55
- package/lib/vendor/blamejs/lib/error-page.js +14 -1
- package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
- package/lib/vendor/blamejs/lib/external-db.js +549 -34
- package/lib/vendor/blamejs/lib/file-upload.js +52 -7
- package/lib/vendor/blamejs/lib/framework-error.js +20 -1
- package/lib/vendor/blamejs/lib/framework-files.js +73 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
- package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
- package/lib/vendor/blamejs/lib/guard-all.js +1 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
- package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
- package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
- package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
- package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
- package/lib/vendor/blamejs/lib/guard-email.js +47 -69
- package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
- package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
- package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
- package/lib/vendor/blamejs/lib/guard-html.js +53 -108
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
- package/lib/vendor/blamejs/lib/guard-image.js +46 -103
- package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
- package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
- package/lib/vendor/blamejs/lib/guard-json.js +38 -108
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
- package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
- package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
- package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
- package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
- package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
- package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
- package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
- package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
- package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
- package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
- package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
- package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
- package/lib/vendor/blamejs/lib/guard-template.js +35 -172
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-time.js +32 -154
- package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
- package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
- package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
- package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
- package/lib/vendor/blamejs/lib/http-client.js +37 -9
- package/lib/vendor/blamejs/lib/inbox.js +120 -107
- package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
- package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
- package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
- package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
- package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
- package/lib/vendor/blamejs/lib/mail-store.js +293 -154
- package/lib/vendor/blamejs/lib/mail.js +8 -4
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
- package/lib/vendor/blamejs/lib/migrations.js +108 -66
- package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
- package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
- package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
- package/lib/vendor/blamejs/lib/observability.js +124 -0
- package/lib/vendor/blamejs/lib/otel-export.js +12 -3
- package/lib/vendor/blamejs/lib/outbox.js +184 -83
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
- package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
- package/lib/vendor/blamejs/lib/queue-local.js +225 -140
- package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
- package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
- package/lib/vendor/blamejs/lib/queue.js +7 -0
- package/lib/vendor/blamejs/lib/redact.js +68 -11
- package/lib/vendor/blamejs/lib/redis-client.js +160 -31
- package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
- package/lib/vendor/blamejs/lib/retention.js +101 -40
- package/lib/vendor/blamejs/lib/router.js +212 -5
- package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
- package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
- package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
- package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
- package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
- package/lib/vendor/blamejs/lib/safe-url.js +170 -3
- package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
- package/lib/vendor/blamejs/lib/scheduler.js +35 -12
- package/lib/vendor/blamejs/lib/seeders.js +122 -74
- package/lib/vendor/blamejs/lib/session-stores.js +42 -14
- package/lib/vendor/blamejs/lib/session.js +175 -77
- package/lib/vendor/blamejs/lib/sql.js +3842 -0
- package/lib/vendor/blamejs/lib/sse.js +26 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +151 -4
- package/lib/vendor/blamejs/lib/static.js +177 -34
- package/lib/vendor/blamejs/lib/subject.js +96 -49
- package/lib/vendor/blamejs/lib/vault/index.js +3 -2
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
- package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
- package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
- package/lib/vendor/blamejs/lib/websocket.js +35 -5
- package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
- package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
- package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
- package/lib/vendor/blamejs/scripts/check-services.js +21 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +51 -0
- package/lib/vendor/blamejs/scripts/release.js +398 -38
- package/lib/vendor/blamejs/test/00-primitives.js +117 -0
- package/lib/vendor/blamejs/test/10-state.js +140 -14
- package/lib/vendor/blamejs/test/20-db.js +65 -2
- package/lib/vendor/blamejs/test/helpers/db.js +9 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
- package/lib/vendor/blamejs/test/helpers/services.js +21 -0
- package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
- package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
- package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
- package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
- package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
- package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
- package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
- package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
- package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
- package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
- package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
- package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
- package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
- package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1120 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +309 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
- package/lib/vendor/blamejs/test/smoke.js +79 -21
- package/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
- package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
- package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
- package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
- package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
- package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
- package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live MySQL proof that the b.sql data-layer migration of the six
|
|
4
|
+
* privacy/identity/production modules (consent / api-key / legal-hold /
|
|
5
|
+
* subject / retention / scheduler) emits valid MySQL — backtick
|
|
6
|
+
* identifiers, `ON DUPLICATE KEY UPDATE` upserts, the IF()-based fence
|
|
7
|
+
* rewrite the builder synthesizes for MySQL (which has no `WHERE` on
|
|
8
|
+
* upsert and no `RETURNING`), and the unsigned/BIGINT coercion at the
|
|
9
|
+
* driver boundary.
|
|
10
|
+
*
|
|
11
|
+
* MySQL is NOT a framework cluster backend (frameworkSchema /
|
|
12
|
+
* clusterStorage support postgres + sqlite only — see
|
|
13
|
+
* audit-chain-external-db.test.js). So api-key / consent never dispatch
|
|
14
|
+
* to MySQL in production; the framework table shapes here are exercised
|
|
15
|
+
* as b.sql operator-app-schema targets — proving the SAME builders that
|
|
16
|
+
* back the migrated modules emit MySQL that runs. The headline MySQL-
|
|
17
|
+
* specific surface:
|
|
18
|
+
*
|
|
19
|
+
* - consent fenced tip: the builder rewrites `conflictWhere(fence)` +
|
|
20
|
+
* `RETURNING` into per-column `IF(stored.fencingToken <=
|
|
21
|
+
* VALUES(fencingToken), VALUES(col), col)` (no WHERE / no RETURNING
|
|
22
|
+
* on MySQL). This test proves the IF-fence actually PRESERVES the old
|
|
23
|
+
* tip on a lower fencing token against a real MySQL server (the
|
|
24
|
+
* security property), and documents that the RETURNING-0-rows
|
|
25
|
+
* fenced-out signal consent.js depends on does NOT exist on MySQL.
|
|
26
|
+
* - api-key: issue/verify/rotate/revoke/purge SQL shapes (INSERT /
|
|
27
|
+
* SELECT / the lastUsedAt UPDATE / graceful+cutover
|
|
28
|
+
* rotate UPDATE / the whereGroup-OR purge SELECT+DELETE)
|
|
29
|
+
* + BIGINT-as-string -> JS-number coercion via the
|
|
30
|
+
* framework's coerceRows on the real MySQL readback.
|
|
31
|
+
* - legal-hold: place INSERT / release DELETE / whereLike prefix history.
|
|
32
|
+
* - subject: the INSERT-OR-REPLACE upsert + restrict INSERT/DELETE.
|
|
33
|
+
* - retention: hard / soft / erase NULL-set / cascade DELETE / candidate.
|
|
34
|
+
* - scheduler: the tick-claim upsert `onConflict(tickKey).doNothing()`
|
|
35
|
+
* — on MySQL this folds to `ON DUPLICATE KEY UPDATE
|
|
36
|
+
* tickKey = tickKey`, so a duplicate tickKey (the split-
|
|
37
|
+
* brain replay) affects 0 rows = the loser skips, exactly
|
|
38
|
+
* the dedup signal `_fireOnce` reads — plus the prune
|
|
39
|
+
* DELETE and the BIGINT-as-string -> JS-number coercion of
|
|
40
|
+
* the claimed-at readback.
|
|
41
|
+
*
|
|
42
|
+
* Distinct from data-layer-mysql.test.js (cache / nonce / rate-limit
|
|
43
|
+
* cluster backends). Driver: a minimal docker-exec `mysql -e` shim (no
|
|
44
|
+
* shell parse of SQL beyond the -e arg). `?` placeholders bound inline
|
|
45
|
+
* (operator-controlled values only). Tables namespaced + DROP/recreated.
|
|
46
|
+
*
|
|
47
|
+
* RUN: node scripts/test-integration.js --skip-service-check data-layer-mysql-privacy
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var execFileSync = require("node:child_process").execFileSync;
|
|
51
|
+
var fs = require("node:fs");
|
|
52
|
+
var os = require("node:os");
|
|
53
|
+
var path = require("node:path");
|
|
54
|
+
var helpers = require("../helpers");
|
|
55
|
+
var check = helpers.check;
|
|
56
|
+
var services = require("../helpers/services");
|
|
57
|
+
var cryptoField = require("../../lib/crypto-field");
|
|
58
|
+
var b = require("../../");
|
|
59
|
+
|
|
60
|
+
var CONTAINER = "blamejs-test-mysql";
|
|
61
|
+
var DB_NAME = "blamejs_test";
|
|
62
|
+
|
|
63
|
+
function _mysql(sql) {
|
|
64
|
+
var out;
|
|
65
|
+
try {
|
|
66
|
+
out = execFileSync("docker",
|
|
67
|
+
["exec", "-i", CONTAINER, "mysql", "-uroot", "-pblamejs_test_root",
|
|
68
|
+
"--batch", "--raw", DB_NAME, "-e", sql],
|
|
69
|
+
{ stdio: ["pipe", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024 }
|
|
70
|
+
).toString("utf8");
|
|
71
|
+
} catch (e) {
|
|
72
|
+
var err = new Error(e.stderr ? e.stderr.toString("utf8") : (e.message || String(e)));
|
|
73
|
+
err.cause = e;
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Inline `?` binding. Values are operator-controlled (ids / hashes /
|
|
80
|
+
// numbers / null). MySQL: backslash IS a string escape by default, so
|
|
81
|
+
// escape both backslash and single-quote.
|
|
82
|
+
function _bindParams(sql, params) {
|
|
83
|
+
var i = 0;
|
|
84
|
+
return sql.replace(/\?/g, function () {
|
|
85
|
+
if (i >= params.length) throw new Error("placeholder/param count mismatch");
|
|
86
|
+
var p = params[i++];
|
|
87
|
+
if (p === null || p === undefined) return "NULL";
|
|
88
|
+
if (typeof p === "number") return String(p);
|
|
89
|
+
if (typeof p === "boolean") return p ? "1" : "0";
|
|
90
|
+
if (Buffer.isBuffer(p)) return "x'" + p.toString("hex") + "'";
|
|
91
|
+
return "'" + String(p).replace(/\\/g, "\\\\").replace(/'/g, "''") + "'";
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse a --batch tab-separated result block. NULL prints as the literal
|
|
96
|
+
// "NULL"; numeric columns come back as STRINGS (faithful to a real driver
|
|
97
|
+
// returning BIGINT as a JS string), so the framework's coerceRows must
|
|
98
|
+
// turn them back to numbers.
|
|
99
|
+
function _parseBatch(out) {
|
|
100
|
+
var lines = out.split(/\r?\n/).filter(function (l) { return l.length > 0; });
|
|
101
|
+
if (lines.length < 1) return { rows: [] };
|
|
102
|
+
var headers = lines[0].split("\t");
|
|
103
|
+
var rows = [];
|
|
104
|
+
for (var i = 1; i < lines.length; i++) {
|
|
105
|
+
var cells = lines[i].split("\t");
|
|
106
|
+
var row = {};
|
|
107
|
+
for (var j = 0; j < headers.length; j++) {
|
|
108
|
+
var v = cells[j];
|
|
109
|
+
row[headers[j]] = (v === "NULL" || v === undefined) ? null : v;
|
|
110
|
+
}
|
|
111
|
+
rows.push(row);
|
|
112
|
+
}
|
|
113
|
+
return { rows: rows };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A MySQL external-db driver: every query() shells `mysql -e`. Writes
|
|
117
|
+
// report ROW_COUNT() (read on the SAME exec/connection so the count is
|
|
118
|
+
// faithful); reads return parsed rows.
|
|
119
|
+
function _makeDockerMysqlDriver() {
|
|
120
|
+
return {
|
|
121
|
+
connect: async function () { return { id: 1 }; },
|
|
122
|
+
query: async function (_client, sql, params) {
|
|
123
|
+
params = params || [];
|
|
124
|
+
var bound = _bindParams(sql, params);
|
|
125
|
+
var t = bound.trim();
|
|
126
|
+
if (/^(SELECT|SHOW|WITH)/i.test(t)) {
|
|
127
|
+
var sel = _parseBatch(_mysql(bound));
|
|
128
|
+
return { rows: sel.rows, rowCount: sel.rows.length };
|
|
129
|
+
}
|
|
130
|
+
var combined = _mysql(bound + ";\nSELECT ROW_COUNT() AS `__rc`;");
|
|
131
|
+
var rc = _parseBatch(combined);
|
|
132
|
+
var n = (rc.rows[0] && rc.rows[0].__rc != null) ? Number(rc.rows[0].__rc) : 0;
|
|
133
|
+
if (!Number.isFinite(n) || n < 0) n = 0; // CREATE/etc return -1
|
|
134
|
+
return { rows: [], rowCount: n };
|
|
135
|
+
},
|
|
136
|
+
close: async function () { /* no-op */ },
|
|
137
|
+
dialect: "mysql",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _resetState() {
|
|
142
|
+
try { b.cluster._resetForTest(); } catch (_e) {}
|
|
143
|
+
try { b.consent._resetForTest(); } catch (_e) {}
|
|
144
|
+
try { b.externalDb._resetForTest(); } catch (_e) {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function run() {
|
|
148
|
+
var mysqlSvc = await services.requireService("mysql");
|
|
149
|
+
if (!mysqlSvc.ok) throw new Error("mysql unreachable: " + mysqlSvc.reason);
|
|
150
|
+
|
|
151
|
+
var TABLES = [
|
|
152
|
+
"dl_myp_api_keys", "dl_myp_consent_tip",
|
|
153
|
+
"dl_myp_hold", "dl_myp_erasures", "dl_myp_restrictions",
|
|
154
|
+
"dl_myp_audit", "dl_myp_orders", "dl_myp_order_lines",
|
|
155
|
+
"dl_myp_sched_ticks",
|
|
156
|
+
];
|
|
157
|
+
_mysql(TABLES.map(function (t) { return "DROP TABLE IF EXISTS `" + t + "`;"; }).join(" "));
|
|
158
|
+
|
|
159
|
+
_resetState();
|
|
160
|
+
|
|
161
|
+
// Vault + api-key cryptoField schema, so sealRow seals ownerId/scopes/
|
|
162
|
+
// metadata and derives ownerIdHash (db.js FRAMEWORK_SCHEMA wires this
|
|
163
|
+
// for the local path).
|
|
164
|
+
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-dl-myp-"));
|
|
165
|
+
if (typeof b.vault._resetForTest === "function") b.vault._resetForTest();
|
|
166
|
+
await b.vault.init({ dataDir: dataDir, mode: "plaintext" });
|
|
167
|
+
cryptoField.registerTable("dl_myp_api_keys", {
|
|
168
|
+
sealedFields: ["ownerId", "scopes", "metadata"],
|
|
169
|
+
derivedHashes: { ownerIdHash: { from: "ownerId" } },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
var driver = _makeDockerMysqlDriver();
|
|
173
|
+
b.externalDb.init({
|
|
174
|
+
backends: { ops: { connect: driver.connect, query: driver.query, close: driver.close, dialect: "mysql" } },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// b.sql emits `?`; MySQL keeps `?` (placeholderize is passthrough for
|
|
178
|
+
// mysql), so the driver binds inline — no translation needed.
|
|
179
|
+
function _q(built) {
|
|
180
|
+
return b.externalDb.query(built.sql, built.params, { backend: "ops" });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var nowMs = Date.now();
|
|
184
|
+
|
|
185
|
+
// ====================================================================
|
|
186
|
+
// api-key SQL shapes on MySQL. The api-key registry's clusterStorage
|
|
187
|
+
// dispatch only targets postgres/sqlite, so here we drive the SAME
|
|
188
|
+
// b.sql shapes the module emits (built at { dialect: "mysql" }) directly
|
|
189
|
+
// against MySQL — proving the migrated statements are valid MySQL and
|
|
190
|
+
// the coercion round-trips. We hand-seal the row with cryptoField the
|
|
191
|
+
// same way issue() does.
|
|
192
|
+
// ====================================================================
|
|
193
|
+
var apiKeysDdl = b.sql.createTable("dl_myp_api_keys", [
|
|
194
|
+
{ name: "id", type: "text", primaryKey: true },
|
|
195
|
+
{ name: "namespace", type: "text", notNull: true },
|
|
196
|
+
{ name: "ownerId", type: "text", notNull: true },
|
|
197
|
+
{ name: "ownerIdHash", type: "text", notNull: true },
|
|
198
|
+
{ name: "secretHash", type: "text", notNull: true },
|
|
199
|
+
{ name: "secondarySecretHash", type: "text" },
|
|
200
|
+
{ name: "secondaryExpiresAt", type: "int" },
|
|
201
|
+
{ name: "scopes", type: "text" },
|
|
202
|
+
{ name: "metadata", type: "text" },
|
|
203
|
+
{ name: "createdAt", type: "int", notNull: true },
|
|
204
|
+
{ name: "expiresAt", type: "int" },
|
|
205
|
+
{ name: "revokedAt", type: "int" },
|
|
206
|
+
{ name: "lastUsedAt", type: "int" },
|
|
207
|
+
{ name: "prefix", type: "text", notNull: true },
|
|
208
|
+
], { dialect: "mysql", quoteName: true });
|
|
209
|
+
// MySQL needs a key length on a TEXT PRIMARY KEY. Patch the emitted DDL
|
|
210
|
+
// so `id` is a bounded VARCHAR PK (operator-app-schema concern, not a
|
|
211
|
+
// framework-shape concern — the framework only ships pg/sqlite DDL).
|
|
212
|
+
_mysql(apiKeysDdl.sql.replace("`id` TEXT PRIMARY KEY", "`id` VARCHAR(190) PRIMARY KEY"));
|
|
213
|
+
check("api-key: b.sql createTable DDL (MySQL backticks) ran on real MySQL", true);
|
|
214
|
+
|
|
215
|
+
var COLS = ["id", "namespace", "ownerId", "ownerIdHash", "secretHash",
|
|
216
|
+
"secondarySecretHash", "secondaryExpiresAt", "scopes", "metadata",
|
|
217
|
+
"createdAt", "expiresAt", "revokedAt", "lastUsedAt", "prefix"];
|
|
218
|
+
|
|
219
|
+
function _sealApiRow(plain) {
|
|
220
|
+
var sealed = cryptoField.sealRow("dl_myp_api_keys", plain);
|
|
221
|
+
for (var i = 0; i < COLS.length; i++) if (!(COLS[i] in sealed)) sealed[COLS[i]] = null;
|
|
222
|
+
return sealed;
|
|
223
|
+
}
|
|
224
|
+
function _insApiRow(plain) {
|
|
225
|
+
var sealed = _sealApiRow(plain);
|
|
226
|
+
var insertRow = {};
|
|
227
|
+
for (var ci = 0; ci < COLS.length; ci++) insertRow[COLS[ci]] = sealed[COLS[ci]];
|
|
228
|
+
return _q(b.sql.insert("dl_myp_api_keys", { dialect: "mysql", quoteName: true }).columns(COLS).values(insertRow).toSql());
|
|
229
|
+
}
|
|
230
|
+
var credentialHash = require("../../lib/credential-hash");
|
|
231
|
+
var bCrypto = require("../../lib/crypto");
|
|
232
|
+
var frameworkSchema = require("../../lib/framework-schema");
|
|
233
|
+
|
|
234
|
+
var idHex = bCrypto.generateToken(8);
|
|
235
|
+
var compositeId = "live:" + idHex;
|
|
236
|
+
var secretEnvelope = await credentialHash.hash(bCrypto.generateToken(16), { algo: "shake256" });
|
|
237
|
+
await _insApiRow({
|
|
238
|
+
id: compositeId, namespace: "live", ownerId: "owner-1",
|
|
239
|
+
secretHash: secretEnvelope, secondarySecretHash: null, secondaryExpiresAt: null,
|
|
240
|
+
scopes: JSON.stringify(["read:x"]), metadata: JSON.stringify({ name: "dev" }),
|
|
241
|
+
createdAt: nowMs, expiresAt: nowMs + b.constants.TIME.days(30),
|
|
242
|
+
revokedAt: null, lastUsedAt: null, prefix: "bk",
|
|
243
|
+
});
|
|
244
|
+
// ownerIdHash must be a real non-null derived hash (sealRow populated it).
|
|
245
|
+
var ownerHashRow = _parseBatch(_mysql("SELECT ownerIdHash FROM dl_myp_api_keys WHERE id = '" + compositeId + "';"));
|
|
246
|
+
check("api-key: sealRow populated a non-null derived ownerIdHash on MySQL",
|
|
247
|
+
ownerHashRow.rows.length === 1 && !!ownerHashRow.rows[0].ownerIdHash);
|
|
248
|
+
|
|
249
|
+
// SELECT-back + coercion: read the row through coerceRows so
|
|
250
|
+
// createdAt/expiresAt (TEXT-from-MySQL) become JS numbers.
|
|
251
|
+
var apiSel = await _q(b.sql.select("dl_myp_api_keys", { dialect: "mysql", quoteName: true })
|
|
252
|
+
.columns(COLS).where("id", compositeId).toSql());
|
|
253
|
+
check("api-key: SELECT round-trips the row on MySQL", apiSel.rows.length === 1);
|
|
254
|
+
var coercedApi = frameworkSchema.coerceRows(apiSel.rows);
|
|
255
|
+
check("api-key: coerceRows turns createdAt BIGINT-string -> JS number on MySQL",
|
|
256
|
+
typeof coercedApi[0].createdAt === "number" && coercedApi[0].createdAt === nowMs);
|
|
257
|
+
check("api-key: coerceRows turns expiresAt BIGINT-string -> JS number on MySQL",
|
|
258
|
+
typeof coercedApi[0].expiresAt === "number" &&
|
|
259
|
+
coercedApi[0].expiresAt === nowMs + b.constants.TIME.days(30));
|
|
260
|
+
|
|
261
|
+
// lastUsedAt UPDATE (verify's trackLastUsedAt touch).
|
|
262
|
+
var luRes = await _q(b.sql.update("dl_myp_api_keys", { dialect: "mysql", quoteName: true })
|
|
263
|
+
.set({ lastUsedAt: nowMs }).where("id", compositeId).toSql());
|
|
264
|
+
check("api-key: lastUsedAt UPDATE affected 1 row on MySQL", luRes.rowCount === 1);
|
|
265
|
+
|
|
266
|
+
// graceful rotate UPDATE — move secret to secondary slot with TTL.
|
|
267
|
+
var newHash = await credentialHash.hash(bCrypto.generateToken(16), { algo: "shake256" });
|
|
268
|
+
await _q(b.sql.update("dl_myp_api_keys", { dialect: "mysql", quoteName: true })
|
|
269
|
+
.set({ secretHash: newHash, secondarySecretHash: secretEnvelope, secondaryExpiresAt: nowMs + b.constants.TIME.days(7) })
|
|
270
|
+
.where("id", compositeId).toSql());
|
|
271
|
+
var graceRow = _parseBatch(_mysql("SELECT secondaryExpiresAt FROM dl_myp_api_keys WHERE id = '" + compositeId + "';"));
|
|
272
|
+
check("api-key: graceful rotate UPDATE persisted secondaryExpiresAt on MySQL",
|
|
273
|
+
graceRow.rows.length === 1 && /\d{6,}/.test(String(graceRow.rows[0].secondaryExpiresAt)));
|
|
274
|
+
|
|
275
|
+
// hard cutover UPDATE — clear the secondary slot.
|
|
276
|
+
await _q(b.sql.update("dl_myp_api_keys", { dialect: "mysql", quoteName: true })
|
|
277
|
+
.set({ secretHash: newHash, secondarySecretHash: null, secondaryExpiresAt: null })
|
|
278
|
+
.where("id", compositeId).toSql());
|
|
279
|
+
var cutRow = _parseBatch(_mysql("SELECT secondarySecretHash FROM dl_myp_api_keys WHERE id = '" + compositeId + "';"));
|
|
280
|
+
check("api-key: hard cutover NULLed secondarySecretHash on MySQL",
|
|
281
|
+
cutRow.rows.length === 1 && cutRow.rows[0].secondarySecretHash === null);
|
|
282
|
+
|
|
283
|
+
// revoke UPDATE (set revokedAt where revokedAt IS NULL).
|
|
284
|
+
var revRes = await _q(b.sql.update("dl_myp_api_keys", { dialect: "mysql", quoteName: true })
|
|
285
|
+
.set({ revokedAt: nowMs }).where("id", compositeId).whereNull("revokedAt").toSql());
|
|
286
|
+
check("api-key: revoke UPDATE (set where revokedAt IS NULL) affected 1 row on MySQL",
|
|
287
|
+
revRes.rowCount === 1);
|
|
288
|
+
|
|
289
|
+
// Seed an expired + a fresh key for the whereGroup-OR purge predicate.
|
|
290
|
+
await _insApiRow({
|
|
291
|
+
id: "live:purge-0", namespace: "live", ownerId: "owner-p0",
|
|
292
|
+
secretHash: await credentialHash.hash(bCrypto.generateToken(16), { algo: "shake256" }),
|
|
293
|
+
secondarySecretHash: null, secondaryExpiresAt: null, scopes: null, metadata: null,
|
|
294
|
+
createdAt: nowMs, expiresAt: 1, revokedAt: null, lastUsedAt: null, prefix: "bk", // expired
|
|
295
|
+
});
|
|
296
|
+
await _insApiRow({
|
|
297
|
+
id: "live:purge-1", namespace: "live", ownerId: "owner-p1",
|
|
298
|
+
secretHash: await credentialHash.hash(bCrypto.generateToken(16), { algo: "shake256" }),
|
|
299
|
+
secondarySecretHash: null, secondaryExpiresAt: null, scopes: null, metadata: null,
|
|
300
|
+
createdAt: nowMs, expiresAt: nowMs + b.constants.TIME.days(365), revokedAt: null, lastUsedAt: null, prefix: "bk", // fresh
|
|
301
|
+
});
|
|
302
|
+
var threshold = nowMs - b.constants.TIME.days(90);
|
|
303
|
+
function _purgeWhere(qb) {
|
|
304
|
+
return qb.where("namespace", "live").whereGroup(function (g) {
|
|
305
|
+
g.whereGroup(function (a) { a.whereNotNull("revokedAt").where("revokedAt", "<", threshold); })
|
|
306
|
+
.orWhereGroup(function (b2) { b2.whereNotNull("expiresAt").where("expiresAt", "<", threshold); });
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
var purgeSel = await _q(_purgeWhere(b.sql.select("dl_myp_api_keys", { dialect: "mysql", quoteName: true }).columns(["id"])).toSql());
|
|
310
|
+
// owner-1 (revokedAt=now, NOT < threshold), purge-0 (expiresAt=1 < threshold) → exactly 1.
|
|
311
|
+
check("api-key: whereGroup-OR purge SELECT found the 1 expired key on MySQL",
|
|
312
|
+
purgeSel.rows.length === 1 && purgeSel.rows[0].id === "live:purge-0");
|
|
313
|
+
var purgeDel = await _q(_purgeWhere(b.sql.delete("dl_myp_api_keys", { dialect: "mysql", quoteName: true })).toSql());
|
|
314
|
+
check("api-key: whereGroup-OR purge DELETE removed exactly 1 row on MySQL", purgeDel.rowCount === 1);
|
|
315
|
+
|
|
316
|
+
// ====================================================================
|
|
317
|
+
// consent fenced tip on MySQL — the headline MySQL-specific surface.
|
|
318
|
+
// The builder rewrites conflictWhere(fence) + RETURNING into per-column
|
|
319
|
+
// IF(stored.fencingToken <= VALUES(fencingToken), VALUES(col), col).
|
|
320
|
+
// Prove the IF-fence PRESERVES the old tip when a lower fencing token
|
|
321
|
+
// arrives, and document that the RETURNING 0-rows fenced-out signal
|
|
322
|
+
// consent.js uses does NOT exist here.
|
|
323
|
+
// ====================================================================
|
|
324
|
+
var consentTipDdl = b.sql.createTable("dl_myp_consent_tip", [
|
|
325
|
+
{ name: "scope", type: "text", primaryKey: true },
|
|
326
|
+
{ name: "atMonotonicCounter", type: "int", notNull: true },
|
|
327
|
+
{ name: "rowHash", type: "text" },
|
|
328
|
+
{ name: "signedAt", type: "text" },
|
|
329
|
+
{ name: "fencingToken", type: "int", notNull: true },
|
|
330
|
+
], { dialect: "mysql", quoteName: true });
|
|
331
|
+
_mysql(consentTipDdl.sql.replace("`scope` TEXT PRIMARY KEY", "`scope` VARCHAR(64) PRIMARY KEY"));
|
|
332
|
+
|
|
333
|
+
var safeSql = require("../../lib/safe-sql");
|
|
334
|
+
function _tipUpsert(counter, rowHash, signedAt, fencingToken) {
|
|
335
|
+
var tipFence = "dl_myp_consent_tip." + safeSql.quoteIdentifier("fencingToken", "mysql") +
|
|
336
|
+
" <= VALUES(" + safeSql.quoteIdentifier("fencingToken", "mysql") + ")";
|
|
337
|
+
return b.sql.upsert("dl_myp_consent_tip", { dialect: "mysql", quoteName: true })
|
|
338
|
+
.columns(["scope", "atMonotonicCounter", "rowHash", "signedAt", "fencingToken"])
|
|
339
|
+
.values({ scope: "consent", atMonotonicCounter: counter, rowHash: rowHash, signedAt: signedAt, fencingToken: fencingToken })
|
|
340
|
+
.onConflict(["scope"])
|
|
341
|
+
.doUpdateFromExcluded(["atMonotonicCounter", "rowHash", "signedAt", "fencingToken"])
|
|
342
|
+
.conflictWhere(tipFence, [])
|
|
343
|
+
.toSql();
|
|
344
|
+
}
|
|
345
|
+
// Confirm the builder produced the MySQL IF()-fence rewrite (no WHERE,
|
|
346
|
+
// no RETURNING — MySQL upsert has neither).
|
|
347
|
+
var tipSqlText = _tipUpsert(1, "h", "s", 1).sql;
|
|
348
|
+
check("consent-tip: builder rewrote the fence into MySQL IF() conditional updates",
|
|
349
|
+
/ON DUPLICATE KEY UPDATE/.test(tipSqlText) &&
|
|
350
|
+
/IF\(dl_myp_consent_tip\.`fencingToken` <= VALUES\(`fencingToken`\)/.test(tipSqlText) &&
|
|
351
|
+
!/RETURNING/.test(tipSqlText));
|
|
352
|
+
|
|
353
|
+
await _q(_tipUpsert(1, "hash-1", "1700000000001", 1)); // initial
|
|
354
|
+
await _q(_tipUpsert(2, "hash-2", "1700000000002", 2)); // higher token — advances
|
|
355
|
+
var tipAfterHigher = _parseBatch(_mysql("SELECT atMonotonicCounter, rowHash, fencingToken FROM dl_myp_consent_tip WHERE scope='consent';"));
|
|
356
|
+
check("consent-tip: higher fencingToken advanced the tip to counter 2 on MySQL",
|
|
357
|
+
tipAfterHigher.rows[0].atMonotonicCounter === "2" && tipAfterHigher.rows[0].rowHash === "hash-2");
|
|
358
|
+
|
|
359
|
+
// LOWER token (1) with a would-be newer counter/hash — the IF-fence must
|
|
360
|
+
// keep the OLD values (stored token 2 <= incoming 1 is FALSE).
|
|
361
|
+
await _q(_tipUpsert(99, "hash-evil", "1700000000099", 1));
|
|
362
|
+
var tipAfterFence = _parseBatch(_mysql("SELECT atMonotonicCounter, rowHash, fencingToken FROM dl_myp_consent_tip WHERE scope='consent';"));
|
|
363
|
+
check("consent-tip: MySQL IF-fence PRESERVED the tip against a lower fencing token " +
|
|
364
|
+
"(no hash-evil, still counter 2)",
|
|
365
|
+
tipAfterFence.rows[0].rowHash === "hash-2" &&
|
|
366
|
+
tipAfterFence.rows[0].atMonotonicCounter === "2" &&
|
|
367
|
+
tipAfterFence.rows[0].fencingToken === "2");
|
|
368
|
+
|
|
369
|
+
// EQUAL token (2) — the <= fence accepts; the tip advances.
|
|
370
|
+
await _q(_tipUpsert(3, "hash-3", "1700000000003", 2));
|
|
371
|
+
var tipAfterEqual = _parseBatch(_mysql("SELECT atMonotonicCounter, rowHash FROM dl_myp_consent_tip WHERE scope='consent';"));
|
|
372
|
+
check("consent-tip: equal fencingToken accepted by the <= IF-fence on MySQL (advanced to hash-3)",
|
|
373
|
+
tipAfterEqual.rows[0].rowHash === "hash-3" && tipAfterEqual.rows[0].atMonotonicCounter === "3");
|
|
374
|
+
|
|
375
|
+
// ====================================================================
|
|
376
|
+
// legal-hold — place INSERT / release DELETE / whereLike prefix history.
|
|
377
|
+
// ====================================================================
|
|
378
|
+
var holdDdl = b.sql.createTable("dl_myp_hold", [
|
|
379
|
+
{ name: "subjectIdHash", type: "text", primaryKey: true },
|
|
380
|
+
{ name: "placedAt", type: "int", notNull: true },
|
|
381
|
+
{ name: "placedBy", type: "text" },
|
|
382
|
+
{ name: "reason", type: "text", notNull: true },
|
|
383
|
+
{ name: "custodian", type: "text" },
|
|
384
|
+
{ name: "citation", type: "text" },
|
|
385
|
+
{ name: "retainUntil", type: "int" },
|
|
386
|
+
], { dialect: "mysql", quoteName: true });
|
|
387
|
+
_mysql(holdDdl.sql.replace("`subjectIdHash` TEXT PRIMARY KEY", "`subjectIdHash` VARCHAR(190) PRIMARY KEY"));
|
|
388
|
+
var hash = b.crypto.sha3Hash("bj-legal-hold:subject-42");
|
|
389
|
+
await _q(b.sql.insert("dl_myp_hold", { dialect: "mysql", quoteName: true })
|
|
390
|
+
.values({ subjectIdHash: hash, placedAt: nowMs, placedBy: "legal@x", reason: "SEC subpoena", custodian: "c@x", citation: "SEC-Rule-17a-4", retainUntil: null })
|
|
391
|
+
.toSql());
|
|
392
|
+
var holdSel = await _q(b.sql.select("dl_myp_hold", { dialect: "mysql", quoteName: true }).columns(["placedAt"]).where("subjectIdHash", hash).toSql());
|
|
393
|
+
check("legal-hold: place INSERT + existence SELECT round-trip on MySQL",
|
|
394
|
+
holdSel.rows.length === 1 && Number(holdSel.rows[0].placedAt) === nowMs);
|
|
395
|
+
var holdDel = await _q(b.sql.delete("dl_myp_hold", { dialect: "mysql", quoteName: true }).where("subjectIdHash", hash).toSql());
|
|
396
|
+
check("legal-hold: release DELETE affected 1 row on MySQL", holdDel.rowCount === 1);
|
|
397
|
+
|
|
398
|
+
var auditDdl = b.sql.createTable("dl_myp_audit", [
|
|
399
|
+
{ name: "recordedAt", type: "int", notNull: true },
|
|
400
|
+
{ name: "action", type: "text", notNull: true },
|
|
401
|
+
{ name: "metadata", type: "text" },
|
|
402
|
+
{ name: "outcome", type: "text" },
|
|
403
|
+
{ name: "resourceKind", type: "text" },
|
|
404
|
+
], { dialect: "mysql", quoteName: true });
|
|
405
|
+
_mysql(auditDdl.sql);
|
|
406
|
+
var seedRows = [
|
|
407
|
+
[1, "legalhold.placed", "{}", "success", "legal-hold"],
|
|
408
|
+
[2, "legalhold.released", "{}", "success", "legal-hold"],
|
|
409
|
+
[3, "auth.legalhold.x", "{}", "success", "legal-hold"], // NOT a prefix match
|
|
410
|
+
[4, "legalhold.100%done", "{}", "success", "legal-hold"], // literal % — must stay literal
|
|
411
|
+
];
|
|
412
|
+
for (var sr = 0; sr < seedRows.length; sr++) {
|
|
413
|
+
await _q(b.sql.insert("dl_myp_audit", { dialect: "mysql", quoteName: true })
|
|
414
|
+
.values({ recordedAt: seedRows[sr][0], action: seedRows[sr][1], metadata: seedRows[sr][2], outcome: seedRows[sr][3], resourceKind: seedRows[sr][4] })
|
|
415
|
+
.toSql());
|
|
416
|
+
}
|
|
417
|
+
var hist = await _q(b.sql.select("dl_myp_audit", { dialect: "mysql", quoteName: true })
|
|
418
|
+
.columns(["recordedAt", "action"])
|
|
419
|
+
.whereLike("action", "legalhold.", "prefix")
|
|
420
|
+
.where("resourceKind", "legal-hold")
|
|
421
|
+
.orderBy("recordedAt", "asc")
|
|
422
|
+
.toSql());
|
|
423
|
+
check("legal-hold: whereLike prefix selects exactly the legalhold.* rows on MySQL " +
|
|
424
|
+
"(placed/released/100%done, NOT auth.legalhold.x)",
|
|
425
|
+
hist.rows.length === 3 &&
|
|
426
|
+
hist.rows.every(function (r) { return r.action.indexOf("legalhold.") === 0; }));
|
|
427
|
+
var esc = await _q(b.sql.select("dl_myp_audit", { dialect: "mysql", quoteName: true })
|
|
428
|
+
.columns(["action"]).whereLike("action", "legalhold.100%", "prefix").toSql());
|
|
429
|
+
check("legal-hold: whereLike escapes a literal % in the term on MySQL (matches only the literal row)",
|
|
430
|
+
esc.rows.length === 1 && esc.rows[0].action === "legalhold.100%done");
|
|
431
|
+
|
|
432
|
+
// ====================================================================
|
|
433
|
+
// subject — INSERT-OR-REPLACE upsert (_markErased) + restrict INSERT/DELETE.
|
|
434
|
+
// ====================================================================
|
|
435
|
+
var erasuresDdl = b.sql.createTable("dl_myp_erasures", [
|
|
436
|
+
{ name: "subjectIdHash", type: "text", primaryKey: true },
|
|
437
|
+
{ name: "erasedAt", type: "int", notNull: true },
|
|
438
|
+
], { dialect: "mysql", quoteName: true });
|
|
439
|
+
_mysql(erasuresDdl.sql.replace("`subjectIdHash` TEXT PRIMARY KEY", "`subjectIdHash` VARCHAR(190) PRIMARY KEY"));
|
|
440
|
+
function _markErased(subjectHash, erasedAt) {
|
|
441
|
+
return b.sql.upsert("dl_myp_erasures", { dialect: "mysql", quoteName: true })
|
|
442
|
+
.values({ subjectIdHash: subjectHash, erasedAt: erasedAt })
|
|
443
|
+
.onConflict(["subjectIdHash"])
|
|
444
|
+
.doUpdateFromExcluded(["erasedAt"])
|
|
445
|
+
.toSql();
|
|
446
|
+
}
|
|
447
|
+
var shash = b.crypto.sha3Hash("bj-subject:user-99");
|
|
448
|
+
await _q(_markErased(shash, 1700000000000));
|
|
449
|
+
await _q(_markErased(shash, 1700000009999)); // re-erase refreshes timestamp, no dup-key
|
|
450
|
+
var erasureState = _parseBatch(_mysql("SELECT COUNT(*) AS c, MAX(erasedAt) AS m FROM dl_myp_erasures;"));
|
|
451
|
+
check("subject: INSERT-OR-REPLACE upsert kept ONE row (no dup-key) on MySQL",
|
|
452
|
+
erasureState.rows[0].c === "1");
|
|
453
|
+
check("subject: INSERT-OR-REPLACE refreshed erasedAt to the newest value on MySQL",
|
|
454
|
+
erasureState.rows[0].m === "1700000009999");
|
|
455
|
+
|
|
456
|
+
var restrictDdl = b.sql.createTable("dl_myp_restrictions", [
|
|
457
|
+
{ name: "subjectIdHash", type: "text", primaryKey: true },
|
|
458
|
+
{ name: "since", type: "int", notNull: true },
|
|
459
|
+
{ name: "reason", type: "text" },
|
|
460
|
+
], { dialect: "mysql", quoteName: true });
|
|
461
|
+
_mysql(restrictDdl.sql.replace("`subjectIdHash` TEXT PRIMARY KEY", "`subjectIdHash` VARCHAR(190) PRIMARY KEY"));
|
|
462
|
+
await _q(b.sql.insert("dl_myp_restrictions", { dialect: "mysql", quoteName: true })
|
|
463
|
+
.values({ subjectIdHash: shash, since: nowMs, reason: "art-18 hold" }).toSql());
|
|
464
|
+
var rPres = await _q(b.sql.select("dl_myp_restrictions", { dialect: "mysql", quoteName: true })
|
|
465
|
+
.columns(["subjectIdHash"]).where("subjectIdHash", shash).limit(1).toSql());
|
|
466
|
+
check("subject: restrict INSERT + presence SELECT round-trip on MySQL", rPres.rows.length === 1);
|
|
467
|
+
var rDel = await _q(b.sql.delete("dl_myp_restrictions", { dialect: "mysql", quoteName: true })
|
|
468
|
+
.where("subjectIdHash", shash).toSql());
|
|
469
|
+
check("subject: restrict DELETE (lift) affected 1 row on MySQL", rDel.rowCount === 1);
|
|
470
|
+
|
|
471
|
+
// ====================================================================
|
|
472
|
+
// retention — hard / soft / erase NULL-set / cascade / candidate WHERE.
|
|
473
|
+
// __erasedAt is TEXT (the `= ''` sentinel column shape).
|
|
474
|
+
// ====================================================================
|
|
475
|
+
var ordersDdl = b.sql.createTable("dl_myp_orders", [
|
|
476
|
+
{ name: "_id", type: "text", primaryKey: true },
|
|
477
|
+
{ name: "createdAt", type: "int", notNull: true },
|
|
478
|
+
{ name: "secretCol", type: "text" },
|
|
479
|
+
{ name: "secretColHash", type: "text" },
|
|
480
|
+
{ name: "softAt", type: "int" },
|
|
481
|
+
{ name: "__erasedAt", type: "text" },
|
|
482
|
+
], { dialect: "mysql", quoteName: true });
|
|
483
|
+
_mysql(ordersDdl.sql.replace("`_id` TEXT PRIMARY KEY", "`_id` VARCHAR(190) PRIMARY KEY"));
|
|
484
|
+
var linesDdl = b.sql.createTable("dl_myp_order_lines", [
|
|
485
|
+
{ name: "_id", type: "text", primaryKey: true },
|
|
486
|
+
{ name: "orderId", type: "text", notNull: true },
|
|
487
|
+
], { dialect: "mysql", quoteName: true });
|
|
488
|
+
_mysql(linesDdl.sql.replace("`_id` TEXT PRIMARY KEY", "`_id` VARCHAR(190) PRIMARY KEY"));
|
|
489
|
+
|
|
490
|
+
var oldAt = nowMs - b.constants.TIME.days(400);
|
|
491
|
+
for (var oi = 1; oi <= 4; oi++) {
|
|
492
|
+
var oid = "o-" + oi;
|
|
493
|
+
await _q(b.sql.insert("dl_myp_orders", { dialect: "mysql", quoteName: true })
|
|
494
|
+
.values({ _id: oid, createdAt: oldAt, secretCol: "secret-" + oi, secretColHash: "h-" + oi, softAt: null, __erasedAt: null })
|
|
495
|
+
.toSql());
|
|
496
|
+
await _q(b.sql.insert("dl_myp_order_lines", { dialect: "mysql", quoteName: true })
|
|
497
|
+
.values({ _id: "l-" + oi, orderId: oid }).toSql());
|
|
498
|
+
}
|
|
499
|
+
var cutoff = nowMs - b.constants.TIME.days(365);
|
|
500
|
+
var cand = await _q(b.sql.select("dl_myp_orders", { dialect: "mysql", quoteName: true })
|
|
501
|
+
.where("createdAt", "<=", cutoff)
|
|
502
|
+
.whereNull("softAt")
|
|
503
|
+
.whereGroup(function (g) { g.whereNull("__erasedAt").orWhereOp("__erasedAt", "=", ""); })
|
|
504
|
+
.limit(500)
|
|
505
|
+
.toSql());
|
|
506
|
+
check("retention: candidate whereGroup-OR WHERE selects the 4 aged rows on MySQL",
|
|
507
|
+
cand.rows.length === 4);
|
|
508
|
+
var softRes = await _q(b.sql.update("dl_myp_orders", { dialect: "mysql", quoteName: true })
|
|
509
|
+
.set("softAt", nowMs).where("_id", "o-1").toSql());
|
|
510
|
+
check("retention: soft-delete UPDATE set softAt on MySQL", softRes.rowCount === 1);
|
|
511
|
+
await _q(b.sql.update("dl_myp_orders", { dialect: "mysql", quoteName: true })
|
|
512
|
+
.set({ secretCol: null, secretColHash: null }).where("_id", "o-2").toSql());
|
|
513
|
+
var erasedRow = _parseBatch(_mysql("SELECT secretCol, secretColHash FROM dl_myp_orders WHERE _id = 'o-2';"));
|
|
514
|
+
check("retention: erase NULL-set wiped the sealed col + derived hash on MySQL",
|
|
515
|
+
erasedRow.rows[0].secretCol === null && erasedRow.rows[0].secretColHash === null);
|
|
516
|
+
var hardRes = await _q(b.sql.delete("dl_myp_orders", { dialect: "mysql", quoteName: true }).where("_id", "o-3").toSql());
|
|
517
|
+
check("retention: hard delete removed o-3 on MySQL", hardRes.rowCount === 1);
|
|
518
|
+
var cascRes = await _q(b.sql.delete("dl_myp_order_lines", { dialect: "mysql", quoteName: true }).where("orderId", "o-3").toSql());
|
|
519
|
+
check("retention: cascade DELETE removed o-3's order_lines on MySQL", cascRes.rowCount === 1);
|
|
520
|
+
var cnt = await _q(b.sql.select("dl_myp_order_lines", { dialect: "mysql", quoteName: true })
|
|
521
|
+
.count("*", "n").where("orderId", "o-4").toSql());
|
|
522
|
+
check("retention: cascade dry-run COUNT(*) returns 1 for o-4's lines on MySQL",
|
|
523
|
+
cnt.rows.length === 1 && Number(cnt.rows[0].n) === 1);
|
|
524
|
+
|
|
525
|
+
// ====================================================================
|
|
526
|
+
// scheduler — the cluster tick-claim. _fireOnce builds
|
|
527
|
+
// sql.upsert("_blamejs_scheduler_ticks", { dialect })
|
|
528
|
+
// .columns([tickKey, name, scheduledAtUnix, claimedAtUnix, claimedBy])
|
|
529
|
+
// .values({...}).onConflict(["tickKey"]).doNothing()
|
|
530
|
+
// and reads result.rowCount: a fresh tickKey wins (rowCount 1 = fire),
|
|
531
|
+
// a duplicate tickKey loses (rowCount 0 = skip, task.tickClaimLost++).
|
|
532
|
+
// On MySQL doNothing() folds to `ON DUPLICATE KEY UPDATE tickKey =
|
|
533
|
+
// tickKey`, so the PRIMARY KEY on `tickKey` makes the first INSERT land
|
|
534
|
+
// (ROW_COUNT 1) and the split-brain replay a no-op (ROW_COUNT 0) — the
|
|
535
|
+
// dedup is the DB's unique constraint, the security property a real
|
|
536
|
+
// server must enforce. The prune is delete().where("scheduledAtUnix",
|
|
537
|
+
// "<", threshold). claimedAtUnix is a BIGINT read back + coerced.
|
|
538
|
+
// ====================================================================
|
|
539
|
+
var schedTicksDdl = b.sql.createTable("dl_myp_sched_ticks", [
|
|
540
|
+
{ name: "tickKey", type: "text", primaryKey: true },
|
|
541
|
+
{ name: "name", type: "text", notNull: true },
|
|
542
|
+
{ name: "scheduledAtUnix", type: "int", notNull: true },
|
|
543
|
+
{ name: "claimedAtUnix", type: "int", notNull: true },
|
|
544
|
+
{ name: "claimedBy", type: "text" },
|
|
545
|
+
], { dialect: "mysql", quoteName: true });
|
|
546
|
+
_mysql(schedTicksDdl.sql.replace("`tickKey` TEXT PRIMARY KEY", "`tickKey` VARCHAR(190) PRIMARY KEY"));
|
|
547
|
+
|
|
548
|
+
var SCHED_COLS = ["tickKey", "name", "scheduledAtUnix", "claimedAtUnix", "claimedBy"];
|
|
549
|
+
function _claimTick(tickKey, name, scheduledAtUnix, claimedAtUnix, claimedBy) {
|
|
550
|
+
return b.sql.upsert("dl_myp_sched_ticks", { dialect: "mysql", quoteName: true })
|
|
551
|
+
.columns(SCHED_COLS)
|
|
552
|
+
.values({
|
|
553
|
+
tickKey: tickKey,
|
|
554
|
+
name: name,
|
|
555
|
+
scheduledAtUnix: scheduledAtUnix,
|
|
556
|
+
claimedAtUnix: claimedAtUnix,
|
|
557
|
+
claimedBy: claimedBy,
|
|
558
|
+
})
|
|
559
|
+
.onConflict(["tickKey"])
|
|
560
|
+
.doNothing()
|
|
561
|
+
.toSql();
|
|
562
|
+
}
|
|
563
|
+
// Confirm the builder emitted the MySQL no-op fold (no ON CONFLICT, no
|
|
564
|
+
// RETURNING — the doNothing() rewrite to `ON DUPLICATE KEY UPDATE
|
|
565
|
+
// tickKey = tickKey`).
|
|
566
|
+
var claimSqlText = _claimTick("rollup:1", "rollup", 1, 2, "node-A").sql;
|
|
567
|
+
check("scheduler: doNothing() emitted the MySQL ON DUPLICATE KEY UPDATE no-op fold",
|
|
568
|
+
/ON DUPLICATE KEY UPDATE `tickKey` = `tickKey`/.test(claimSqlText) &&
|
|
569
|
+
!/ON CONFLICT/.test(claimSqlText) && !/RETURNING/.test(claimSqlText));
|
|
570
|
+
|
|
571
|
+
var nominal = nowMs + b.constants.TIME.minutes(1);
|
|
572
|
+
var tickKey = "rollup:" + nominal;
|
|
573
|
+
// node-A claims the tick first — fresh tickKey, ROW_COUNT 1 (won).
|
|
574
|
+
var claimA = await _q(_claimTick(tickKey, "rollup", nominal, nowMs, "node-A"));
|
|
575
|
+
check("scheduler: first tick-claim INSERT won (rowCount 1 = the leader fires) on MySQL",
|
|
576
|
+
claimA.rowCount === 1);
|
|
577
|
+
// node-B races the SAME nominal tick (split-brain) — duplicate tickKey,
|
|
578
|
+
// the DUPLICATE-KEY no-op fold affects 0 rows (lost the claim, skips).
|
|
579
|
+
var claimB = await _q(_claimTick(tickKey, "rollup", nominal, nowMs + 5, "node-B"));
|
|
580
|
+
check("scheduler: racing tick-claim on the SAME tickKey lost (rowCount 0 = loser skips) on MySQL",
|
|
581
|
+
claimB.rowCount === 0);
|
|
582
|
+
// The DB holds exactly one tick row for the shared key — the PRIMARY KEY
|
|
583
|
+
// collapsed the racing INSERTs to one. claimedBy is still node-A's (the
|
|
584
|
+
// no-op fold left the winner's row untouched).
|
|
585
|
+
var tickRow = _parseBatch(_mysql(
|
|
586
|
+
"SELECT claimedBy, claimedAtUnix FROM dl_myp_sched_ticks WHERE tickKey = '" + tickKey + "';"));
|
|
587
|
+
check("scheduler: real MySQL holds exactly ONE tick row for the shared key, claimedBy=node-A",
|
|
588
|
+
tickRow.rows.length === 1 && tickRow.rows[0].claimedBy === "node-A");
|
|
589
|
+
// claimedAtUnix is a BIGINT column — coerceRows turns the string the
|
|
590
|
+
// driver returns back into a JS number (the same path the readback uses).
|
|
591
|
+
var coercedTick = frameworkSchema.coerceRows(tickRow.rows);
|
|
592
|
+
check("scheduler: coerceRows turns claimedAtUnix BIGINT-string -> JS number on MySQL",
|
|
593
|
+
typeof coercedTick[0].claimedAtUnix === "number" && coercedTick[0].claimedAtUnix === nowMs);
|
|
594
|
+
|
|
595
|
+
// A DISTINCT nominal tick is independently claimable (per-tickKey dedup,
|
|
596
|
+
// not a one-shot table lock).
|
|
597
|
+
var nominal2 = nominal + b.constants.TIME.minutes(1);
|
|
598
|
+
var claim2 = await _q(_claimTick("rollup:" + nominal2, "rollup", nominal2, nowMs + 9, "node-A"));
|
|
599
|
+
check("scheduler: a distinct second tick is independently claimable (rowCount 1) on MySQL",
|
|
600
|
+
claim2.rowCount === 1);
|
|
601
|
+
|
|
602
|
+
// pruneTickClaims: delete().where("scheduledAtUnix", "<", threshold).
|
|
603
|
+
// Seed an old tick + keep the two fresh ones; prune below a cutoff and
|
|
604
|
+
// assert only the aged row is removed.
|
|
605
|
+
await _q(_claimTick("rollup:" + (nominal - b.constants.TIME.days(30)), "rollup",
|
|
606
|
+
nominal - b.constants.TIME.days(30), nowMs, "node-A"));
|
|
607
|
+
var pruneThreshold = nominal - b.constants.TIME.days(7);
|
|
608
|
+
var pruneDel = await _q(b.sql.delete("dl_myp_sched_ticks", { dialect: "mysql", quoteName: true })
|
|
609
|
+
.where("scheduledAtUnix", "<", pruneThreshold)
|
|
610
|
+
.toSql());
|
|
611
|
+
check("scheduler: pruneTickClaims DELETE removed exactly the aged tick row on MySQL",
|
|
612
|
+
pruneDel.rowCount === 1);
|
|
613
|
+
var remaining = _parseBatch(_mysql("SELECT COUNT(*) AS n FROM dl_myp_sched_ticks;"));
|
|
614
|
+
check("scheduler: prune left the two un-aged tick rows intact on MySQL",
|
|
615
|
+
Number(remaining.rows[0].n) === 2);
|
|
616
|
+
|
|
617
|
+
// ---- teardown ----
|
|
618
|
+
await b.externalDb.shutdown();
|
|
619
|
+
_resetState();
|
|
620
|
+
_mysql(TABLES.map(function (t) { return "DROP TABLE IF EXISTS `" + t + "`;"; }).join(" "));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
module.exports = { run: run };
|
|
624
|
+
|
|
625
|
+
if (require.main === module) {
|
|
626
|
+
run().then(
|
|
627
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); process.exit(0); },
|
|
628
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
629
|
+
);
|
|
630
|
+
}
|