@blamejs/blamejs-shop 0.4.69 → 0.4.71
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/admin.js +8 -6
- package/lib/asset-manifest.json +1 -1
- package/lib/security-middleware.js +65 -23
- package/lib/shipping-insurance.js +29 -4
- package/lib/sms-dispatcher.js +17 -4
- package/lib/storefront.js +10 -7
- package/lib/vendor/MANIFEST.json +319 -267
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +58 -5
- package/lib/vendor/blamejs/examples/wiki/README.md +1 -1
- package/lib/vendor/blamejs/examples/wiki/docker-compose.prod.yml +9 -8
- package/lib/vendor/blamejs/examples/wiki/docker-compose.yml +10 -9
- package/lib/vendor/blamejs/examples/wiki/env-snapshot.json +4 -3
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +33 -17
- package/lib/vendor/blamejs/examples/wiki/routes/admin.js +7 -10
- package/lib/vendor/blamejs/examples/wiki/server.js +5 -4
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +5 -0
- package/lib/vendor/blamejs/lib/a2a-tasks.js +38 -6
- package/lib/vendor/blamejs/lib/agent-event-bus.js +13 -0
- package/lib/vendor/blamejs/lib/agent-idempotency.js +5 -1
- package/lib/vendor/blamejs/lib/agent-snapshot.js +32 -2
- package/lib/vendor/blamejs/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/vendor/blamejs/lib/ai-content-detect.js +1 -3
- package/lib/vendor/blamejs/lib/ai-frontier-protocol.js +1 -1
- package/lib/vendor/blamejs/lib/ai-model-manifest.js +1 -1
- package/lib/vendor/blamejs/lib/ai-output.js +16 -7
- package/lib/vendor/blamejs/lib/api-snapshot.js +4 -1
- package/lib/vendor/blamejs/lib/app-shutdown.js +7 -1
- package/lib/vendor/blamejs/lib/archive-gz.js +9 -0
- package/lib/vendor/blamejs/lib/archive-read.js +9 -7
- package/lib/vendor/blamejs/lib/archive-tar-read.js +51 -8
- package/lib/vendor/blamejs/lib/archive.js +4 -2
- package/lib/vendor/blamejs/lib/asn1-der.js +70 -22
- package/lib/vendor/blamejs/lib/atomic-file.js +204 -2
- package/lib/vendor/blamejs/lib/audit-chain.js +54 -5
- package/lib/vendor/blamejs/lib/audit-daily-review.js +12 -2
- package/lib/vendor/blamejs/lib/audit-sign.js +2 -1
- package/lib/vendor/blamejs/lib/audit-tools.js +108 -23
- package/lib/vendor/blamejs/lib/audit.js +7 -2
- package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -2
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +1 -1
- package/lib/vendor/blamejs/lib/auth/ciba.js +43 -4
- package/lib/vendor/blamejs/lib/auth/dpop.js +6 -1
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +5 -1
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
- package/lib/vendor/blamejs/lib/auth/lockout.js +18 -2
- package/lib/vendor/blamejs/lib/auth/passkey.js +1 -1
- package/lib/vendor/blamejs/lib/auth/password.js +1 -1
- package/lib/vendor/blamejs/lib/auth/saml.js +33 -13
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +24 -4
- package/lib/vendor/blamejs/lib/auth/status-list.js +14 -2
- package/lib/vendor/blamejs/lib/auth/step-up.js +9 -1
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +21 -2
- package/lib/vendor/blamejs/lib/backup/bundle.js +7 -2
- package/lib/vendor/blamejs/lib/backup/crypto.js +23 -8
- package/lib/vendor/blamejs/lib/backup/index.js +41 -20
- package/lib/vendor/blamejs/lib/backup/manifest.js +7 -1
- package/lib/vendor/blamejs/lib/break-glass.js +41 -22
- package/lib/vendor/blamejs/lib/cbor.js +34 -11
- package/lib/vendor/blamejs/lib/cdn-cache-control.js +7 -3
- package/lib/vendor/blamejs/lib/cert.js +5 -3
- package/lib/vendor/blamejs/lib/cli.js +5 -1
- package/lib/vendor/blamejs/lib/cloud-events.js +3 -2
- package/lib/vendor/blamejs/lib/cluster-storage.js +7 -3
- package/lib/vendor/blamejs/lib/codepoint-class.js +17 -0
- package/lib/vendor/blamejs/lib/compliance-eaa.js +1 -1
- package/lib/vendor/blamejs/lib/compliance-sanctions.js +9 -7
- package/lib/vendor/blamejs/lib/compliance.js +1 -1
- package/lib/vendor/blamejs/lib/config-drift.js +22 -8
- package/lib/vendor/blamejs/lib/content-credentials.js +11 -8
- package/lib/vendor/blamejs/lib/content-digest.js +11 -4
- package/lib/vendor/blamejs/lib/cookies.js +10 -2
- package/lib/vendor/blamejs/lib/cose.js +20 -0
- package/lib/vendor/blamejs/lib/crdt.js +2 -1
- package/lib/vendor/blamejs/lib/crypto-field.js +29 -23
- package/lib/vendor/blamejs/lib/crypto.js +18 -4
- package/lib/vendor/blamejs/lib/csp.js +4 -0
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/data-act.js +27 -4
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +14 -6
- package/lib/vendor/blamejs/lib/db-query.js +69 -9
- package/lib/vendor/blamejs/lib/db.js +32 -15
- package/lib/vendor/blamejs/lib/dora.js +43 -13
- package/lib/vendor/blamejs/lib/dr-runbook.js +1 -1
- package/lib/vendor/blamejs/lib/dsa.js +2 -1
- package/lib/vendor/blamejs/lib/dsr.js +22 -8
- package/lib/vendor/blamejs/lib/early-hints.js +19 -0
- package/lib/vendor/blamejs/lib/eat.js +5 -1
- package/lib/vendor/blamejs/lib/external-db.js +60 -4
- package/lib/vendor/blamejs/lib/fda-21cfr11.js +30 -5
- package/lib/vendor/blamejs/lib/flag-providers.js +6 -2
- package/lib/vendor/blamejs/lib/forms.js +1 -1
- package/lib/vendor/blamejs/lib/gate-contract.js +46 -5
- package/lib/vendor/blamejs/lib/gdpr-ropa.js +18 -9
- package/lib/vendor/blamejs/lib/graphql-federation.js +17 -4
- package/lib/vendor/blamejs/lib/guard-all.js +2 -2
- package/lib/vendor/blamejs/lib/guard-dsn.js +1 -1
- package/lib/vendor/blamejs/lib/guard-envelope.js +1 -1
- package/lib/vendor/blamejs/lib/guard-html.js +9 -11
- package/lib/vendor/blamejs/lib/guard-imap-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-jmap.js +1 -1
- package/lib/vendor/blamejs/lib/guard-json.js +14 -6
- package/lib/vendor/blamejs/lib/guard-mail-move.js +1 -1
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-svg.js +8 -9
- package/lib/vendor/blamejs/lib/html-balance.js +7 -3
- package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +33 -12
- package/lib/vendor/blamejs/lib/http-client.js +225 -53
- package/lib/vendor/blamejs/lib/iab-tcf.js +3 -2
- package/lib/vendor/blamejs/lib/importmap-integrity.js +41 -1
- package/lib/vendor/blamejs/lib/incident-report.js +9 -6
- package/lib/vendor/blamejs/lib/json-patch.js +1 -1
- package/lib/vendor/blamejs/lib/json-path.js +24 -3
- package/lib/vendor/blamejs/lib/jtd.js +2 -2
- package/lib/vendor/blamejs/lib/legal-hold.js +24 -8
- package/lib/vendor/blamejs/lib/log.js +2 -2
- package/lib/vendor/blamejs/lib/mail-agent.js +2 -2
- package/lib/vendor/blamejs/lib/mail-arf.js +1 -1
- package/lib/vendor/blamejs/lib/mail-auth.js +3 -3
- package/lib/vendor/blamejs/lib/mail-bimi.js +16 -16
- package/lib/vendor/blamejs/lib/mail-bounce.js +3 -3
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +71 -6
- package/lib/vendor/blamejs/lib/mail-deploy.js +9 -5
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -4
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -4
- package/lib/vendor/blamejs/lib/mail-journal.js +11 -8
- package/lib/vendor/blamejs/lib/mail-mdn.js +8 -4
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -4
- package/lib/vendor/blamejs/lib/mail-scan.js +3 -5
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +4 -1
- package/lib/vendor/blamejs/lib/mail-server-registry.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-tls.js +9 -2
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -4
- package/lib/vendor/blamejs/lib/mail-store-fts.js +1 -1
- package/lib/vendor/blamejs/lib/mail.js +22 -2
- package/lib/vendor/blamejs/lib/markup-tokenizer.js +24 -0
- package/lib/vendor/blamejs/lib/mcp.js +6 -4
- package/lib/vendor/blamejs/lib/mdoc.js +26 -3
- package/lib/vendor/blamejs/lib/metrics.js +14 -3
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +10 -4
- package/lib/vendor/blamejs/lib/middleware/bot-guard.js +26 -18
- package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +5 -1
- package/lib/vendor/blamejs/lib/middleware/compression.js +9 -0
- package/lib/vendor/blamejs/lib/middleware/cors.js +32 -23
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +60 -21
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +6 -4
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +28 -4
- package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +61 -30
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +25 -16
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +24 -6
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -3
- package/lib/vendor/blamejs/lib/middleware/tus-upload.js +2 -2
- package/lib/vendor/blamejs/lib/money.js +1 -1
- package/lib/vendor/blamejs/lib/mtls-ca.js +10 -6
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
- package/lib/vendor/blamejs/lib/network-dns.js +9 -2
- package/lib/vendor/blamejs/lib/network-dnssec.js +2 -1
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +23 -5
- package/lib/vendor/blamejs/lib/network-tls.js +27 -5
- package/lib/vendor/blamejs/lib/network-tsig.js +2 -2
- package/lib/vendor/blamejs/lib/nis2-report.js +1 -1
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +1 -1
- package/lib/vendor/blamejs/lib/ntp-check.js +28 -0
- package/lib/vendor/blamejs/lib/numeric-bounds.js +9 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -2
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/vendor/blamejs/lib/object-store/gcs.js +6 -4
- package/lib/vendor/blamejs/lib/object-store/http-put.js +1 -2
- package/lib/vendor/blamejs/lib/object-store/http-request.js +30 -1
- package/lib/vendor/blamejs/lib/object-store/local.js +37 -17
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +1 -2
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +20 -4
- package/lib/vendor/blamejs/lib/outbox.js +11 -4
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +1 -1
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +21 -3
- package/lib/vendor/blamejs/lib/queue-local.js +10 -3
- package/lib/vendor/blamejs/lib/redact.js +7 -3
- package/lib/vendor/blamejs/lib/request-helpers.js +201 -23
- package/lib/vendor/blamejs/lib/resource-access-lock.js +3 -3
- package/lib/vendor/blamejs/lib/restore-bundle.js +46 -18
- package/lib/vendor/blamejs/lib/restore-rollback.js +10 -4
- package/lib/vendor/blamejs/lib/restore.js +19 -0
- package/lib/vendor/blamejs/lib/retention.js +20 -4
- package/lib/vendor/blamejs/lib/router.js +17 -4
- package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
- package/lib/vendor/blamejs/lib/safe-icap.js +1 -1
- package/lib/vendor/blamejs/lib/safe-json.js +44 -0
- package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
- package/lib/vendor/blamejs/lib/safe-vcard.js +1 -1
- package/lib/vendor/blamejs/lib/sandbox-worker.js +6 -0
- package/lib/vendor/blamejs/lib/sandbox.js +1 -1
- package/lib/vendor/blamejs/lib/scheduler.js +17 -1
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +16 -0
- package/lib/vendor/blamejs/lib/session.js +27 -3
- package/lib/vendor/blamejs/lib/sql.js +3 -3
- package/lib/vendor/blamejs/lib/static.js +65 -13
- package/lib/vendor/blamejs/lib/template.js +7 -5
- package/lib/vendor/blamejs/lib/tenant-quota.js +52 -19
- package/lib/vendor/blamejs/lib/tsa.js +5 -2
- package/lib/vendor/blamejs/lib/vault/index.js +5 -0
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +22 -26
- package/lib/vendor/blamejs/lib/vault/passphrase-source.js +8 -3
- package/lib/vendor/blamejs/lib/vault/rotate.js +13 -18
- package/lib/vendor/blamejs/lib/vault/seal-pem-file.js +4 -1
- package/lib/vendor/blamejs/lib/vc.js +1 -1
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/vendor/blamejs/lib/webhook.js +16 -1
- package/lib/vendor/blamejs/lib/websocket.js +1 -1
- package/lib/vendor/blamejs/lib/worm.js +1 -1
- package/lib/vendor/blamejs/lib/ws-client.js +57 -46
- package/lib/vendor/blamejs/lib/x509-chain.js +44 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.14.json +122 -0
- package/lib/vendor/blamejs/scripts/check-vendor-currency.js +119 -4
- package/lib/vendor/blamejs/test/00-primitives.js +5 -1
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +18 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a-tasks.test.js +42 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +47 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-gz.test.js +24 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar-hardening.test.js +160 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asn1-der.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-nofollow.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-write-excl.test.js +132 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-write-stream.test.js +181 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-chain-corrupted-anchor.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-chain-incremental-verify.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-daily-review.test.js +48 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +13 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +122 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-bot-challenge.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +5 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-lockout.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-status-list.test.js +73 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bot-guard.test.js +17 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +62 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/clear-site-data.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-storage.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +146 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-sanctions.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compression-range.test.js +115 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +17 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/cors.test.js +27 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/data-act.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +20 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-alg-kty.test.js +64 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fda-21cfr11.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +34 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-json.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +72 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-throttle-transform.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/importmap-integrity.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +20 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-header-injection.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +63 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +41 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/network-allowlist.test.js +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-range-header.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +35 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/output-header-hardening.test.js +102 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parser-verify-hardening.test.js +191 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/proto-shadow-allowlist.test.js +120 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +75 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/restore-blob-remap.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-sweep-termination.test.js +104 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-body-validation.test.js +98 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-json-stringify-for-script.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +48 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +23 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +54 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +24 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +42 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/step-up.test.js +7 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/template-escape-html.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +15 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-default-store.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/x509-chain-ca-enforcement.test.js +149 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +34 -0
- package/package.json +1 -1
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
*/
|
|
41
41
|
var nodeFs = require("node:fs");
|
|
42
42
|
var nodePath = require("node:path");
|
|
43
|
+
var nodeStream = require("node:stream");
|
|
44
|
+
var streamPromises = require("node:stream/promises");
|
|
43
45
|
var { generateToken, sha3Hash } = require("./crypto");
|
|
44
46
|
var safeJson = require("./safe-json");
|
|
45
47
|
var C = require("./constants");
|
|
@@ -81,7 +83,11 @@ class AtomicFileError extends FrameworkError {
|
|
|
81
83
|
// primitive's standard names; jitterFactor 0.5 reproduces the original
|
|
82
84
|
// `delay * (0.5 + Math.random()/2)` range of [delay/2, delay].
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
// EACCES joins the transient set: on Windows a freshly-written file is briefly
|
|
87
|
+
// locked by AV / the search indexer / a file-sync client (Dropbox, OneDrive),
|
|
88
|
+
// surfacing as EACCES (alongside EPERM/EBUSY) on the next open/rename — the same
|
|
89
|
+
// transient contention the sync _renameWithRetry already treats as retryable.
|
|
90
|
+
var TRANSIENT_FS_ERRNOS = new Set(["EBUSY", "EAGAIN", "ENFILE", "EMFILE", "EPERM", "EACCES"]);
|
|
85
91
|
|
|
86
92
|
function _isFsRetryable(e) {
|
|
87
93
|
return e != null && TRANSIENT_FS_ERRNOS.has(e.code);
|
|
@@ -485,6 +491,146 @@ function writeSync(filepath, data, opts) {
|
|
|
485
491
|
};
|
|
486
492
|
}
|
|
487
493
|
|
|
494
|
+
/**
|
|
495
|
+
* @primitive b.atomicFile.writeStream
|
|
496
|
+
* @signature b.atomicFile.writeStream(filepath, source, opts?)
|
|
497
|
+
* @since 0.15.14
|
|
498
|
+
* @status stable
|
|
499
|
+
* @related b.atomicFile.writeSync, b.atomicFile.openNoFollowSync
|
|
500
|
+
*
|
|
501
|
+
* Streaming sibling of `writeSync` for payloads too large to buffer in
|
|
502
|
+
* memory. Pipes a Readable `source` into a sibling temp file opened with
|
|
503
|
+
* `O_EXCL | O_NOFOLLOW` (the same exclusive, symlink-refusing create
|
|
504
|
+
* every atomic write uses), fsyncs, then atomically renames over
|
|
505
|
+
* `filepath` and fsyncs the parent directory. A plain
|
|
506
|
+
* `fs.createWriteStream(filepath)` instead follows a symlink an attacker
|
|
507
|
+
* pre-planted at `filepath` (CWE-59 arbitrary write) and leaves a
|
|
508
|
+
* half-written object at the canonical name if the source aborts
|
|
509
|
+
* mid-stream — this primitive does neither: the file appears at
|
|
510
|
+
* `filepath` only after the full stream has landed and synced.
|
|
511
|
+
*
|
|
512
|
+
* Enforces a byte ceiling while streaming (`maxBytes`, default 64 MiB) so
|
|
513
|
+
* an unbounded source cannot fill the disk; the partial temp is removed
|
|
514
|
+
* on overflow or any pipeline error.
|
|
515
|
+
*
|
|
516
|
+
* @opts
|
|
517
|
+
* fileMode: 0o600, // mode applied to the temp file (and thus the renamed final)
|
|
518
|
+
* maxBytes: 64 * 1024 * 1024, // refuse + clean up once the source exceeds this many bytes
|
|
519
|
+
* signal: undefined, // optional AbortSignal forwarded to the pipeline
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* await b.atomicFile.writeStream(
|
|
523
|
+
* "/var/lib/blamejs/object",
|
|
524
|
+
* incomingRequestStream,
|
|
525
|
+
* { fileMode: 0o600, maxBytes: b.C.BYTES.gib(2) }
|
|
526
|
+
* );
|
|
527
|
+
* // → { bytesWritten: 12345 }
|
|
528
|
+
*/
|
|
529
|
+
async function writeStream(filepath, source, opts) {
|
|
530
|
+
opts = Object.assign({}, DEFAULTS, opts || {});
|
|
531
|
+
if (!source || typeof source.pipe !== "function") {
|
|
532
|
+
throw new AtomicFileError(
|
|
533
|
+
"writeStream: source must be a Readable stream", "atomic-file/invalid-source");
|
|
534
|
+
}
|
|
535
|
+
var maxBytes = opts.maxBytes;
|
|
536
|
+
|
|
537
|
+
var dir = nodePath.dirname(filepath);
|
|
538
|
+
if (!nodeFs.existsSync(dir)) nodeFs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
539
|
+
|
|
540
|
+
var tmpPath = filepath + ".tmp-" + generateToken(C.BYTES.bytes(8));
|
|
541
|
+
var fd = _openExclTemp(tmpPath, opts.fileMode);
|
|
542
|
+
var fileStream = nodeFs.createWriteStream(null, { fd: fd, autoClose: false });
|
|
543
|
+
var bytesWritten = 0;
|
|
544
|
+
var renamed = false;
|
|
545
|
+
|
|
546
|
+
// Cap the stream as it flows — an unbounded source must not fill the disk.
|
|
547
|
+
var counter = new nodeStream.Transform({
|
|
548
|
+
transform: function (chunk, _enc, cb) {
|
|
549
|
+
bytesWritten += chunk.length;
|
|
550
|
+
if (typeof maxBytes === "number" && bytesWritten > maxBytes) {
|
|
551
|
+
return cb(new AtomicFileError(
|
|
552
|
+
"writeStream: source exceeds maxBytes " + maxBytes, "atomic-file/too-large"));
|
|
553
|
+
}
|
|
554
|
+
cb(null, chunk);
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
if (opts.signal) {
|
|
560
|
+
await streamPromises.pipeline(source, counter, fileStream, { signal: opts.signal });
|
|
561
|
+
} else {
|
|
562
|
+
await streamPromises.pipeline(source, counter, fileStream);
|
|
563
|
+
}
|
|
564
|
+
_fsync(fd);
|
|
565
|
+
nodeFs.closeSync(fd);
|
|
566
|
+
fd = -1;
|
|
567
|
+
_renameWithRetry(tmpPath, filepath);
|
|
568
|
+
renamed = true;
|
|
569
|
+
_fsyncDir(dir);
|
|
570
|
+
} finally {
|
|
571
|
+
if (fd >= 0) { try { nodeFs.closeSync(fd); } catch (_e) { /* already closed? */ } }
|
|
572
|
+
if (!renamed) {
|
|
573
|
+
// Source aborted, overflowed, or the rename failed — remove the temp so
|
|
574
|
+
// no half-written object survives at the canonical name.
|
|
575
|
+
try { nodeFs.unlinkSync(tmpPath); } catch (_e) { /* may not exist */ }
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return { bytesWritten: bytesWritten };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* @primitive b.atomicFile.writeExclSync
|
|
584
|
+
* @signature b.atomicFile.writeExclSync(filepath, data, opts?)
|
|
585
|
+
* @since 0.15.14
|
|
586
|
+
* @status stable
|
|
587
|
+
* @related b.atomicFile.writeSync, b.atomicFile.openNoFollowSync
|
|
588
|
+
*
|
|
589
|
+
* Exclusive, symlink-refusing write to `filepath` WITHOUT the atomic
|
|
590
|
+
* rename — for staged "write → fsync → verify → rename" flows where the
|
|
591
|
+
* caller must re-read and validate the written bytes before committing them
|
|
592
|
+
* over the live file (the vault seal/unseal round-trip re-reads the staged
|
|
593
|
+
* file and confirms it decrypts before renaming it into place). Clears any
|
|
594
|
+
* stale leftover at `filepath` first (an aborted prior run, or a planted
|
|
595
|
+
* symlink — `unlink` removes the LINK, never its target), then creates the
|
|
596
|
+
* file with `O_EXCL | O_NOFOLLOW`, so a symlink re-planted in the race
|
|
597
|
+
* window fails the open closed instead of being followed (CWE-59 / CWE-377).
|
|
598
|
+
* fsyncs the data before returning. For an ordinary write-and-replace use
|
|
599
|
+
* `writeSync`, which renames atomically; reach for this only when a
|
|
600
|
+
* verify-before-commit step sits between the write and the rename.
|
|
601
|
+
*
|
|
602
|
+
* @opts
|
|
603
|
+
* fileMode: 0o600, // mode applied to the created file
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* b.atomicFile.writeExclSync(stagingPath, bytes, { fileMode: 0o600 });
|
|
607
|
+
* // re-read + verify stagingPath, then:
|
|
608
|
+
* b.atomicFile.renameWithRetry(stagingPath, finalPath);
|
|
609
|
+
*/
|
|
610
|
+
function writeExclSync(filepath, data, opts) {
|
|
611
|
+
opts = Object.assign({}, DEFAULTS, opts || {});
|
|
612
|
+
var buf = safeBuffer.toBuffer(data, {
|
|
613
|
+
errorClass: AtomicFileError,
|
|
614
|
+
typeCode: "atomic-file/invalid-data",
|
|
615
|
+
typeMessage: "data must be Buffer, Uint8Array, or string",
|
|
616
|
+
});
|
|
617
|
+
// Clear any stale leftover so the exclusive create can proceed; unlink
|
|
618
|
+
// removes a planted symlink itself (not its target), and the O_EXCL open
|
|
619
|
+
// then fails closed if anything re-appears at the path in the race window.
|
|
620
|
+
try { nodeFs.unlinkSync(filepath); } catch (_e) { /* nothing to clear */ }
|
|
621
|
+
var fd = _openExclTemp(filepath, opts.fileMode);
|
|
622
|
+
try {
|
|
623
|
+
var pos = 0;
|
|
624
|
+
while (pos < buf.length) {
|
|
625
|
+
pos += nodeFs.writeSync(fd, buf, pos, buf.length - pos, null);
|
|
626
|
+
}
|
|
627
|
+
_fsync(fd);
|
|
628
|
+
} finally {
|
|
629
|
+
try { nodeFs.closeSync(fd); } catch (_e) { /* already closed? */ }
|
|
630
|
+
}
|
|
631
|
+
return { bytesWritten: buf.length };
|
|
632
|
+
}
|
|
633
|
+
|
|
488
634
|
/**
|
|
489
635
|
* @primitive b.atomicFile.cleanOrphans
|
|
490
636
|
* @signature b.atomicFile.cleanOrphans(filepath, opts)
|
|
@@ -730,6 +876,7 @@ function _validateMaxBytes(maxBytes) {
|
|
|
730
876
|
* expectedHash: string, // SHA3-512 the content must match (default: none)
|
|
731
877
|
* encoding: string, // decode to a string (default: return a Buffer)
|
|
732
878
|
* allowShortRead: boolean, // slice to the bytes read instead of throwing (default: false)
|
|
879
|
+
* withStat: boolean, // return { bytes, stat } — stat of the bound fd (mode/uid/gid/size/ino/nlink/mtimeMs), TOCTOU-free
|
|
733
880
|
* errorFor: Function, // (kind, detail) => Error|undefined; kinds: enoent / symlink / too-large / toctou / short-read / integrity
|
|
734
881
|
*
|
|
735
882
|
* @example
|
|
@@ -737,6 +884,11 @@ function _validateMaxBytes(maxBytes) {
|
|
|
737
884
|
* maxBytes: b.constants.BYTES.mib(1),
|
|
738
885
|
* encoding: "utf8",
|
|
739
886
|
* });
|
|
887
|
+
*
|
|
888
|
+
* // Assert mode + owner on the exact inode the bytes came from (no re-stat):
|
|
889
|
+
* var r = b.atomicFile.fdSafeReadSync("/etc/app/secret", { withStat: true });
|
|
890
|
+
* if ((r.stat.mode & 0o077) !== 0) throw new Error("secret is group/other-readable");
|
|
891
|
+
* // r.bytes is the Buffer (or string under `encoding`)
|
|
740
892
|
*/
|
|
741
893
|
function fdSafeReadSync(filepath, opts) {
|
|
742
894
|
opts = opts || {};
|
|
@@ -806,7 +958,26 @@ function fdSafeReadSync(filepath, opts) {
|
|
|
806
958
|
throw errorFor("integrity", { expected: opts.expectedHash, actual: actual });
|
|
807
959
|
}
|
|
808
960
|
}
|
|
809
|
-
|
|
961
|
+
var content = opts.encoding ? buf.toString(opts.encoding) : buf;
|
|
962
|
+
// withStat: return the fstat of the SAME bound fd alongside the bytes, so a
|
|
963
|
+
// caller that needs the mode / owner (e.g. to assert 0o600 + owned-by-me on a
|
|
964
|
+
// secrets file) reads it TOCTOU-free — the stat describes the exact inode the
|
|
965
|
+
// bytes came from, not a re-stat that an attacker could swap underneath.
|
|
966
|
+
if (opts.withStat) {
|
|
967
|
+
return {
|
|
968
|
+
bytes: content,
|
|
969
|
+
stat: {
|
|
970
|
+
mode: fstat.mode,
|
|
971
|
+
uid: fstat.uid,
|
|
972
|
+
gid: fstat.gid,
|
|
973
|
+
size: fstat.size,
|
|
974
|
+
ino: fstat.ino,
|
|
975
|
+
nlink: fstat.nlink,
|
|
976
|
+
mtimeMs: fstat.mtimeMs,
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
return content;
|
|
810
981
|
}
|
|
811
982
|
|
|
812
983
|
// Atomic-file's own reads route through fdSafeReadSync with an errorFor
|
|
@@ -1137,12 +1308,43 @@ function listDir(dir, opts) {
|
|
|
1137
1308
|
return out;
|
|
1138
1309
|
}
|
|
1139
1310
|
|
|
1311
|
+
/**
|
|
1312
|
+
* @primitive b.atomicFile.openNoFollowSync
|
|
1313
|
+
* @signature b.atomicFile.openNoFollowSync(filepath, mode?)
|
|
1314
|
+
* @since 0.15.14
|
|
1315
|
+
* @status stable
|
|
1316
|
+
* @related b.atomicFile.fdSafeReadSync, b.atomicFile.readSync
|
|
1317
|
+
*
|
|
1318
|
+
* Open a path read-only with `O_NOFOLLOW` so a symlink at the final path
|
|
1319
|
+
* component is refused (`ELOOP`) instead of followed — the streaming-read
|
|
1320
|
+
* counterpart to `fdSafeReadSync` for callers that must `fs.createReadStream`
|
|
1321
|
+
* (range serving, SRI/ETag hashing, large-object download) and cannot buffer
|
|
1322
|
+
* the whole file. Stream from the returned fd: `fs.createReadStream(path, { fd
|
|
1323
|
+
* })`. Defends a post-confinement symlink swap (CWE-22 / CWE-367) on
|
|
1324
|
+
* request-reachable static-serve and object-store read paths, where a lexical
|
|
1325
|
+
* `_assertInsideRoot` check alone leaves a swap window between the check and the
|
|
1326
|
+
* open. `O_NOFOLLOW` is POSIX-only; on platforms without it the flag is 0 (a
|
|
1327
|
+
* plain `O_RDONLY` open) — Windows symlink semantics differ and are out of
|
|
1328
|
+
* scope. Throws the raw `openSync` error (caller maps `ELOOP` / `ENOENT`).
|
|
1329
|
+
*
|
|
1330
|
+
* @example
|
|
1331
|
+
* var fd = b.atomicFile.openNoFollowSync(absPath);
|
|
1332
|
+
* var stream = fs.createReadStream(absPath, { fd: fd }); // autoClose closes fd
|
|
1333
|
+
*/
|
|
1334
|
+
function openNoFollowSync(filepath, mode) {
|
|
1335
|
+
var flags = nodeFs.constants.O_RDONLY | (nodeFs.constants.O_NOFOLLOW || 0);
|
|
1336
|
+
return nodeFs.openSync(filepath, flags, mode === undefined ? 0o600 : mode);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1140
1339
|
module.exports = {
|
|
1141
1340
|
write: write,
|
|
1142
1341
|
writeSync: writeSync,
|
|
1342
|
+
writeStream: writeStream,
|
|
1343
|
+
writeExclSync: writeExclSync,
|
|
1143
1344
|
read: read,
|
|
1144
1345
|
readSync: readSync,
|
|
1145
1346
|
fdSafeReadSync: fdSafeReadSync,
|
|
1347
|
+
openNoFollowSync: openNoFollowSync,
|
|
1146
1348
|
writeJson: writeJson,
|
|
1147
1349
|
readJson: readJson,
|
|
1148
1350
|
copy: copy,
|
|
@@ -39,6 +39,7 @@ var clusterStorage = require("./cluster-storage");
|
|
|
39
39
|
var frameworkSchema = require("./framework-schema");
|
|
40
40
|
var sql = require("./sql");
|
|
41
41
|
var safeSql = require("./safe-sql");
|
|
42
|
+
var safeBuffer = require("./safe-buffer");
|
|
42
43
|
var { sha3Hash } = require("./crypto");
|
|
43
44
|
|
|
44
45
|
// b.sql opts for the chain read SQL these primitives compose. The reader
|
|
@@ -219,6 +220,8 @@ async function getChainTip(queryOneAsync, tableName, opts) {
|
|
|
219
220
|
* maxRows: number, // stop after N rows per (sub-)chain (default: walk every row)
|
|
220
221
|
* chainKey: string, // partition column — verify each sub-chain independently
|
|
221
222
|
* maxChains: number, // max partitions to verify under chainKey (default 100000; fails closed)
|
|
223
|
+
* from: number, // single-chain only: verify rows with monotonicCounter >= from, anchored at the predecessor's rowHash (incremental verify after a known-good checkpoint)
|
|
224
|
+
* to: number, // single-chain only: verify rows with monotonicCounter <= to
|
|
222
225
|
*
|
|
223
226
|
* @example
|
|
224
227
|
* async function queryAll(sql) { return await myDriver.query(sql); }
|
|
@@ -275,7 +278,12 @@ function _walkRows(rows, tableName, startPrevHash, opts) {
|
|
|
275
278
|
|
|
276
279
|
if (opts.maxRows && i >= opts.maxRows - 1) break;
|
|
277
280
|
}
|
|
278
|
-
|
|
281
|
+
// Report the count ACTUALLY walked, not rows.length — under maxRows the walk
|
|
282
|
+
// stops early, so rows.length would over-report coverage (a caller reading
|
|
283
|
+
// rowsVerified to judge how much of the chain was checked must see the real
|
|
284
|
+
// number, not be told the whole table verified when only maxRows did).
|
|
285
|
+
var verifiedCount = opts.maxRows ? Math.min(rows.length, opts.maxRows) : rows.length;
|
|
286
|
+
return { ok: true, table: tableName, rowsVerified: verifiedCount, lastHash: prevHash };
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
async function verifyChain(queryAllAsync, tableName, opts) {
|
|
@@ -349,11 +357,30 @@ async function verifyChain(queryAllAsync, tableName, opts) {
|
|
|
349
357
|
anchor = [];
|
|
350
358
|
}
|
|
351
359
|
if (Array.isArray(anchor) && anchor.length > 0) {
|
|
352
|
-
|
|
353
|
-
|
|
360
|
+
var aHash = anchor[0].lastPurgedRowHash;
|
|
361
|
+
var aCounter = Number(anchor[0].lastPurgedCounter);
|
|
362
|
+
// A corrupted / tampered purge anchor (non-hex lastPurgedRowHash or a
|
|
363
|
+
// non-numeric lastPurgedCounter) must fail CLOSED with a clear reason.
|
|
364
|
+
// Passing a garbage prevHash into _walkRows → computeRowHash would THROW
|
|
365
|
+
// ("prevHash must be a 128-char hex"), turning a defensive verify into an
|
|
366
|
+
// uncaught exception; a NaN counter would skip nothing and surface as an
|
|
367
|
+
// opaque chain-break. Detect it here and return { ok:false }.
|
|
368
|
+
if (!safeBuffer.isHex(aHash, SHA3_512_HEX_LEN) || !isFinite(aCounter) || aCounter < 0) {
|
|
369
|
+
return { ok: false, table: tableName, rowsVerified: 0, reason: "corrupted purge anchor" };
|
|
370
|
+
}
|
|
371
|
+
prevHash = aHash;
|
|
372
|
+
skipBeforeCounter = aCounter;
|
|
354
373
|
}
|
|
355
374
|
}
|
|
356
375
|
|
|
376
|
+
// Incremental verify (b.audit.verify { from, to }): verify only rows whose
|
|
377
|
+
// monotonicCounter is in [from, to]. `from` must anchor on the rowHash of the
|
|
378
|
+
// row immediately BEFORE it, so the scoped walk chains correctly — otherwise
|
|
379
|
+
// the first in-range row's prevHash (= the predecessor's rowHash) wouldn't
|
|
380
|
+
// match ZERO_HASH and a good chain would falsely report a break.
|
|
381
|
+
var fromCounter = (opts.from != null && isFinite(Number(opts.from))) ? Number(opts.from) : null;
|
|
382
|
+
var toCounter = (opts.to != null && isFinite(Number(opts.to))) ? Number(opts.to) : null;
|
|
383
|
+
|
|
357
384
|
var rowsBuilt = sql.select(tableName, _sqlOpts())
|
|
358
385
|
.orderBy("monotonicCounter", "asc")
|
|
359
386
|
.toSql();
|
|
@@ -364,9 +391,31 @@ async function verifyChain(queryAllAsync, tableName, opts) {
|
|
|
364
391
|
// verified on SQLite without this. coerceRow makes the recompute
|
|
365
392
|
// type-stable across backends (no-op on already-numeric SQLite rows).
|
|
366
393
|
rows = frameworkSchema.coerceRows(rows);
|
|
367
|
-
|
|
394
|
+
|
|
395
|
+
// Resolve the incremental-verify anchor: the highest row strictly below
|
|
396
|
+
// `from` (derived from the already-read rows, no extra query). Raise
|
|
397
|
+
// skipBeforeCounter to it and adopt its rowHash as the chain anchor.
|
|
398
|
+
if (fromCounter != null && fromCounter > skipBeforeCounter + 1) {
|
|
399
|
+
var pred = null;
|
|
400
|
+
for (var pi = 0; pi < rows.length; pi++) {
|
|
401
|
+
var pc = Number(rows[pi].monotonicCounter);
|
|
402
|
+
if (pc < fromCounter && pc > skipBeforeCounter) pred = rows[pi]; else if (pc >= fromCounter) break;
|
|
403
|
+
}
|
|
404
|
+
if (pred) {
|
|
405
|
+
if (!safeBuffer.isHex(pred.rowHash, SHA3_512_HEX_LEN)) {
|
|
406
|
+
return { ok: false, table: tableName, rowsVerified: 0, reason: "incremental-verify anchor row has a corrupt rowHash" };
|
|
407
|
+
}
|
|
408
|
+
prevHash = pred.rowHash;
|
|
409
|
+
skipBeforeCounter = Math.max(skipBeforeCounter, Number(pred.monotonicCounter));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (skipBeforeCounter > 0 || toCounter != null) {
|
|
368
414
|
rows = rows.filter(function (r) {
|
|
369
|
-
|
|
415
|
+
var c = Number(r.monotonicCounter);
|
|
416
|
+
if (c <= skipBeforeCounter) return false;
|
|
417
|
+
if (toCounter != null && c > toCounter) return false;
|
|
418
|
+
return true;
|
|
370
419
|
});
|
|
371
420
|
}
|
|
372
421
|
|
|
@@ -83,9 +83,15 @@ function _defaultClassify(event) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function _severityAtLeast(severity, threshold) {
|
|
86
|
-
var sIdx = SEVERITY_ORDER.indexOf(severity);
|
|
87
86
|
var tIdx = SEVERITY_ORDER.indexOf(threshold);
|
|
88
|
-
if (
|
|
87
|
+
if (tIdx === -1) return false; // unknown threshold (validated at config)
|
|
88
|
+
var sIdx = SEVERITY_ORDER.indexOf(severity);
|
|
89
|
+
// An UNKNOWN event severity — e.g. a custom classify(event) returning an
|
|
90
|
+
// unexpected value — must NOT silently drop the event from the review. Fail
|
|
91
|
+
// SAFE: treat it as meeting the threshold so the operator still sees the
|
|
92
|
+
// event (and notices their classify mis-returned) rather than missing a
|
|
93
|
+
// flagged event.
|
|
94
|
+
if (sIdx === -1) return true;
|
|
89
95
|
return sIdx >= tIdx;
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -247,6 +253,10 @@ function create(opts) {
|
|
|
247
253
|
from: fromMs,
|
|
248
254
|
to: startedAt,
|
|
249
255
|
limit: queryLimit,
|
|
256
|
+
// Newest-first: if the window holds more than queryLimit events, keep
|
|
257
|
+
// the MOST RECENT (the actionable ones) — an ascending+limit query would
|
|
258
|
+
// keep the oldest and silently drop the newest from the review.
|
|
259
|
+
order: "desc",
|
|
250
260
|
});
|
|
251
261
|
} catch (e) {
|
|
252
262
|
_emit("audit.daily_review.failed", {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
* SLH-DSA-SHAKE-256f post-quantum signature for audit-chain checkpoints.
|
|
60
60
|
*/
|
|
61
61
|
var nodeFs = require("node:fs");
|
|
62
|
+
var numericBounds = require("./numeric-bounds");
|
|
62
63
|
var nodePath = require("node:path");
|
|
63
64
|
var nodeCrypto = require("node:crypto");
|
|
64
65
|
var atomicFile = require("./atomic-file");
|
|
@@ -835,7 +836,7 @@ function _normalizeTip(tip, fnLabel) {
|
|
|
835
836
|
"auditSign." + fnLabel + ": tip must be an object { counter, tipHash }");
|
|
836
837
|
}
|
|
837
838
|
var counter = tip.counter;
|
|
838
|
-
if (
|
|
839
|
+
if (!numericBounds.isNonNegativeFiniteInt(counter)) {
|
|
839
840
|
throw _err("ANCHOR_BAD_COUNTER",
|
|
840
841
|
"auditSign." + fnLabel + ": tip.counter must be a non-negative integer (got: " + counter + ")");
|
|
841
842
|
}
|
|
@@ -58,6 +58,7 @@ var nodeFs = require("node:fs");
|
|
|
58
58
|
var nodePath = require("node:path");
|
|
59
59
|
var pkg = require("../package.json");
|
|
60
60
|
var atomicFile = require("./atomic-file");
|
|
61
|
+
var C = require("./constants");
|
|
61
62
|
var auditChain = require("./audit-chain");
|
|
62
63
|
var canonicalJson = require("./canonical-json");
|
|
63
64
|
var auditSign = require("./audit-sign");
|
|
@@ -349,6 +350,12 @@ async function _defaultReadPredecessorRowHash(firstCounter) {
|
|
|
349
350
|
async function _buildBundle(args) {
|
|
350
351
|
var kind = args.kind;
|
|
351
352
|
var rows = args.rows;
|
|
353
|
+
// Verification witnesses: the rows between the purgeable slice's tip and
|
|
354
|
+
// the covering checkpoint's anchored counter. They ride the bundle so the
|
|
355
|
+
// chain can be walked up to the SIGNED atRowHash, but they are NOT part of
|
|
356
|
+
// the purgeable range (the manifest range below is computed from `rows`,
|
|
357
|
+
// and purge() only deletes [first..lastCounter]).
|
|
358
|
+
var witnessRows = args.witnessRows || [];
|
|
352
359
|
var checkpoint = args.checkpoint || null;
|
|
353
360
|
var passphrase = args.passphrase;
|
|
354
361
|
var predecessorRowHash = args.predecessorRowHash;
|
|
@@ -357,8 +364,9 @@ async function _buildBundle(args) {
|
|
|
357
364
|
var lastRow = rows[rows.length - 1];
|
|
358
365
|
var files = {};
|
|
359
366
|
|
|
360
|
-
// 1. Encrypt the rows JSONL
|
|
361
|
-
|
|
367
|
+
// 1. Encrypt the rows JSONL — purgeable slice followed by any witnesses
|
|
368
|
+
// (contiguous, ascending) so the verifier walks one unbroken chain.
|
|
369
|
+
var jsonl = rows.concat(witnessRows).map(function (r) {
|
|
362
370
|
return JSON.stringify(_rowToWireForm(r));
|
|
363
371
|
}).join("\n") + "\n";
|
|
364
372
|
var rowsEnc = await backupCrypto.encryptWithFreshSalt(jsonl, passphrase);
|
|
@@ -437,26 +445,39 @@ async function _readBundle(inDir, passphrase) {
|
|
|
437
445
|
"bundle directory does not exist: " + inDir);
|
|
438
446
|
}
|
|
439
447
|
var manifestPath = nodePath.join(inDir, "manifest.json");
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
var manifest = safeJson.parse(
|
|
448
|
+
// Capped fd-bound read (no existsSync check-then-read window): an externally-
|
|
449
|
+
// supplied bundle manifest is parsed before verification, so an oversized
|
|
450
|
+
// manifest.json would OOM the verifier before safeJson sees it. 4 MiB is far
|
|
451
|
+
// above any real manifest.
|
|
452
|
+
var manifest = safeJson.parse(atomicFile.fdSafeReadSync(manifestPath, {
|
|
453
|
+
maxBytes: C.BYTES.mib(4), encoding: "utf8",
|
|
454
|
+
errorFor: function (kind, detail) {
|
|
455
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-manifest", "manifest.json missing in " + inDir);
|
|
456
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/bad-format", "manifest.json too large (" + detail.size + " > " + detail.max + ")");
|
|
457
|
+
return new AuditToolsError("audit-tools/bad-format", "manifest.json unreadable: " + kind);
|
|
458
|
+
},
|
|
459
|
+
}), { maxBytes: C.BYTES.mib(4) });
|
|
445
460
|
if (!manifest || manifest.format !== BUNDLE_FORMAT) {
|
|
446
461
|
throw new AuditToolsError("audit-tools/bad-format",
|
|
447
462
|
"manifest.format is not " + BUNDLE_FORMAT);
|
|
448
463
|
}
|
|
449
|
-
if (!VALID_KINDS
|
|
464
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_KINDS, manifest.kind)) {
|
|
450
465
|
throw new AuditToolsError("audit-tools/bad-kind",
|
|
451
466
|
"manifest.kind must be one of " + Object.keys(VALID_KINDS).join(", "));
|
|
452
467
|
}
|
|
453
468
|
|
|
454
469
|
var rowsEncPath = nodePath.join(inDir, frameworkFiles.fileName("rowsEnc"));
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
470
|
+
// Capped fd-bound read: rows.enc is the PQC-encrypted archived audit slice
|
|
471
|
+
// (can be large); a hostile multi-GB blob would be read + decrypted in memory
|
|
472
|
+
// before the checksum check. 512 MiB ceiling bounds it (opt-tunable).
|
|
473
|
+
var rowsEnc = atomicFile.fdSafeReadSync(rowsEncPath, {
|
|
474
|
+
maxBytes: C.BYTES.mib(512),
|
|
475
|
+
errorFor: function (kind) {
|
|
476
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc missing in " + inDir);
|
|
477
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/rows-too-large", "rows.enc exceeds the bundle size cap");
|
|
478
|
+
return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc unreadable: " + kind);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
460
481
|
if (manifest.checksum && manifest.checksum.rowsSha3_512 &&
|
|
461
482
|
backupCrypto.checksum(rowsEnc) !== manifest.checksum.rowsSha3_512) {
|
|
462
483
|
throw new AuditToolsError("audit-tools/rows-checksum-mismatch",
|
|
@@ -470,11 +491,16 @@ async function _readBundle(inDir, passphrase) {
|
|
|
470
491
|
var checkpoint = null;
|
|
471
492
|
if (manifest.kind === KIND_ARCHIVE) {
|
|
472
493
|
var ckptPath = nodePath.join(inDir, frameworkFiles.fileName("checkpointEnc"));
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
494
|
+
// Capped fd-bound read: checkpoint.enc is a single PQC-encrypted
|
|
495
|
+
// audit_checkpoints row (a few KiB); 4 MiB bounds a hostile blob.
|
|
496
|
+
var ckptEnc = atomicFile.fdSafeReadSync(ckptPath, {
|
|
497
|
+
maxBytes: C.BYTES.mib(4),
|
|
498
|
+
errorFor: function (kind) {
|
|
499
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
|
|
500
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/checkpoint-too-large", "checkpoint.enc exceeds the cap");
|
|
501
|
+
return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc unreadable: " + kind);
|
|
502
|
+
},
|
|
503
|
+
});
|
|
478
504
|
if (manifest.checksum && manifest.checksum.checkpointSha3_512 &&
|
|
479
505
|
backupCrypto.checksum(ckptEnc) !== manifest.checksum.checkpointSha3_512) {
|
|
480
506
|
throw new AuditToolsError("audit-tools/checkpoint-checksum-mismatch",
|
|
@@ -562,10 +588,31 @@ async function archive(opts) {
|
|
|
562
588
|
|
|
563
589
|
var predecessorRowHash = await readPredecessorHash(firstCounter);
|
|
564
590
|
|
|
591
|
+
// The signed checkpoint is the bundle's only unforgeable anchor; it commits
|
|
592
|
+
// the row at checkpoint.atMonotonicCounter. When that counter sits BEYOND
|
|
593
|
+
// the purgeable slice's tip (the operator archived a subset older than the
|
|
594
|
+
// last checkpoint), carry the in-between rows as verification witnesses so
|
|
595
|
+
// verifyBundle can chain-walk up to the anchored row and bind atRowHash to
|
|
596
|
+
// it. Without them an attacker could pair any genuine high-counter
|
|
597
|
+
// checkpoint with a wholly fabricated slice. The witnesses are NOT purged.
|
|
598
|
+
var anchorCounter = Number(checkpoint.atMonotonicCounter);
|
|
599
|
+
var witnessRows = [];
|
|
600
|
+
if (anchorCounter > lastCounter) {
|
|
601
|
+
witnessRows = await readRows({ firstCounter: lastCounter + 1, lastCounter: anchorCounter });
|
|
602
|
+
var witnessTip = witnessRows.length ? Number(witnessRows[witnessRows.length - 1].monotonicCounter) : null;
|
|
603
|
+
if (witnessTip !== anchorCounter) {
|
|
604
|
+
throw new AuditToolsError("audit-tools/anchor-rows-missing",
|
|
605
|
+
"archive: covering checkpoint anchors counter=" + anchorCounter +
|
|
606
|
+
" but the rows up to it are not all available (read up to " + witnessTip +
|
|
607
|
+
") — cannot prove the slice chains to the signed anchor");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
565
611
|
if (returnBytes) {
|
|
566
612
|
var built = await _buildBundle({
|
|
567
613
|
kind: KIND_ARCHIVE,
|
|
568
614
|
rows: rows,
|
|
615
|
+
witnessRows: witnessRows,
|
|
569
616
|
checkpoint: checkpoint,
|
|
570
617
|
passphrase: opts.passphrase,
|
|
571
618
|
predecessorRowHash: predecessorRowHash,
|
|
@@ -582,6 +629,7 @@ async function archive(opts) {
|
|
|
582
629
|
outDir: opts.out,
|
|
583
630
|
kind: KIND_ARCHIVE,
|
|
584
631
|
rows: rows,
|
|
632
|
+
witnessRows: witnessRows,
|
|
585
633
|
checkpoint: checkpoint,
|
|
586
634
|
passphrase: opts.passphrase,
|
|
587
635
|
predecessorRowHash: predecessorRowHash,
|
|
@@ -769,26 +817,41 @@ async function verifyBundle(opts) {
|
|
|
769
817
|
};
|
|
770
818
|
}
|
|
771
819
|
|
|
772
|
-
// 2. Confirm the stored firstRowHash + lastRowHash match the
|
|
820
|
+
// 2. Confirm the stored firstRowHash + lastRowHash match the PURGEABLE
|
|
821
|
+
// slice boundary. The slice tip is the row at range.lastCounter — not
|
|
822
|
+
// necessarily the physical last row, which may be a verification witness
|
|
823
|
+
// carried past the slice so the chain can reach the signed checkpoint.
|
|
824
|
+
var lastCounterN = Number(read.manifest.range.lastCounter);
|
|
825
|
+
var sliceLastRow = null;
|
|
826
|
+
for (var ri = 0; ri < read.rows.length; ri++) {
|
|
827
|
+
if (Number(read.rows[ri].monotonicCounter) === lastCounterN) { sliceLastRow = read.rows[ri]; break; }
|
|
828
|
+
}
|
|
773
829
|
if (read.rows[0].rowHash !== read.manifest.range.firstRowHash) {
|
|
774
830
|
return {
|
|
775
831
|
ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
|
|
776
832
|
reason: "manifest.range.firstRowHash does not match first row's rowHash",
|
|
777
833
|
};
|
|
778
834
|
}
|
|
779
|
-
if (
|
|
835
|
+
if (!sliceLastRow || sliceLastRow.rowHash !== read.manifest.range.lastRowHash) {
|
|
780
836
|
return {
|
|
781
837
|
ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
|
|
782
|
-
reason: "manifest.range.lastRowHash does not match
|
|
838
|
+
reason: "manifest.range.lastRowHash does not match the slice row at lastCounter",
|
|
783
839
|
};
|
|
784
840
|
}
|
|
785
841
|
|
|
786
|
-
// 3. (archive only) verify the covering checkpoint signature
|
|
842
|
+
// 3. (archive only) verify the covering checkpoint signature AND bind its
|
|
843
|
+
// anchored rowHash to the slice. The signature alone proves only that a
|
|
844
|
+
// checkpoint exists for some (counter, rowHash); without binding atRowHash
|
|
845
|
+
// to the archived rows, any genuine high-counter checkpoint could be paired
|
|
846
|
+
// with a wholly fabricated slice. The row at checkpoint.atMonotonicCounter
|
|
847
|
+
// must be present in the bundle (slice or witness) and its rowHash must
|
|
848
|
+
// equal checkpoint.atRowHash — mirroring b.audit.verifyCheckpoints against
|
|
849
|
+
// the live table.
|
|
787
850
|
if (read.manifest.kind === KIND_ARCHIVE) {
|
|
788
851
|
if (!read.checkpoint) {
|
|
789
852
|
return { ok: false, kind: KIND_ARCHIVE, reason: "checkpoint missing from archive bundle" };
|
|
790
853
|
}
|
|
791
|
-
if (Number(read.checkpoint.atMonotonicCounter) <
|
|
854
|
+
if (Number(read.checkpoint.atMonotonicCounter) < lastCounterN) {
|
|
792
855
|
return {
|
|
793
856
|
ok: false, kind: KIND_ARCHIVE,
|
|
794
857
|
reason: "checkpoint atMonotonicCounter (" + read.checkpoint.atMonotonicCounter +
|
|
@@ -805,6 +868,28 @@ async function verifyBundle(opts) {
|
|
|
805
868
|
};
|
|
806
869
|
}
|
|
807
870
|
}
|
|
871
|
+
// Bind: the row the signature anchors must be in the bundle and match.
|
|
872
|
+
var anchorCounterN = Number(read.checkpoint.atMonotonicCounter);
|
|
873
|
+
var anchoredRow = null;
|
|
874
|
+
for (var ai = 0; ai < read.rows.length; ai++) {
|
|
875
|
+
if (Number(read.rows[ai].monotonicCounter) === anchorCounterN) { anchoredRow = read.rows[ai]; break; }
|
|
876
|
+
}
|
|
877
|
+
if (!anchoredRow) {
|
|
878
|
+
return {
|
|
879
|
+
ok: false, kind: KIND_ARCHIVE,
|
|
880
|
+
reason: "checkpoint anchors counter=" + anchorCounterN +
|
|
881
|
+
" but no such row is present in the bundle — checkpoint not bound to the archived slice",
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (anchoredRow.rowHash !== read.checkpoint.atRowHash) {
|
|
885
|
+
return {
|
|
886
|
+
ok: false, kind: KIND_ARCHIVE,
|
|
887
|
+
reason: "checkpoint atRowHash does not match the bundle row at counter=" + anchorCounterN +
|
|
888
|
+
" — the signed anchor does not bind this slice",
|
|
889
|
+
expected: read.checkpoint.atRowHash,
|
|
890
|
+
actual: anchoredRow.rowHash,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
808
893
|
}
|
|
809
894
|
|
|
810
895
|
return {
|
|
@@ -797,7 +797,12 @@ async function query(criteria) {
|
|
|
797
797
|
if (criteria.resourceKind) q = q.where({ resourceKind: criteria.resourceKind });
|
|
798
798
|
if (criteria.outcome) q = q.where({ outcome: criteria.outcome });
|
|
799
799
|
|
|
800
|
-
|
|
800
|
+
// order: "asc" (default, chronological) | "desc" (newest-first). A capped
|
|
801
|
+
// query (limit set) returns the FIRST `limit` rows in this order — so a
|
|
802
|
+
// consumer that wants the most RECENT events under a cap (e.g. the daily
|
|
803
|
+
// review) must pass order:"desc", else `limit` keeps the OLDEST and drops the
|
|
804
|
+
// newest events in the window.
|
|
805
|
+
q.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
|
|
801
806
|
if (criteria.limit != null) q.limit(criteria.limit);
|
|
802
807
|
if (criteria.offset != null) q.offset(criteria.offset);
|
|
803
808
|
|
|
@@ -834,7 +839,7 @@ async function _queryCluster(criteria) {
|
|
|
834
839
|
if (criteria.resourceKind) qb.where("resourceKind", criteria.resourceKind);
|
|
835
840
|
if (criteria.outcome) qb.where("outcome", criteria.outcome);
|
|
836
841
|
|
|
837
|
-
qb.orderBy("monotonicCounter", "asc");
|
|
842
|
+
qb.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
|
|
838
843
|
if (criteria.limit != null) qb.limit(criteria.limit);
|
|
839
844
|
if (criteria.offset != null) qb.offset(criteria.offset);
|
|
840
845
|
|
|
@@ -54,7 +54,7 @@ var SAFE_METHODS = Object.freeze({ GET: 1, HEAD: 1, OPTIONS: 1 });
|
|
|
54
54
|
function _normalizeMode(mode) {
|
|
55
55
|
if (typeof mode !== "string") return null;
|
|
56
56
|
var m = mode.toLowerCase();
|
|
57
|
-
return VALID_MODES
|
|
57
|
+
return Object.prototype.hasOwnProperty.call(VALID_MODES, m) ? m : null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function create(opts) {
|
|
@@ -164,7 +164,7 @@ function create(opts) {
|
|
|
164
164
|
if (_hasUnlockRole(req)) return next();
|
|
165
165
|
if (currentMode === "read-only") {
|
|
166
166
|
var method = (req.method || "GET").toUpperCase();
|
|
167
|
-
if (SAFE_METHODS
|
|
167
|
+
if (Object.prototype.hasOwnProperty.call(SAFE_METHODS, method)) return next();
|
|
168
168
|
_emitAudit("refused", "denied", { mode: currentMode, method: method, path: req.url });
|
|
169
169
|
_emitMetric("refused", 1, { mode: currentMode, reason: "non-safe-method" });
|
|
170
170
|
return _refuse(res, "non-safe-method-in-read-only");
|