@blamejs/blamejs-shop 0.4.68 → 0.4.70
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/giftcards.js +29 -5
- package/lib/security-middleware.js +65 -23
- 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
|
@@ -108,6 +108,80 @@ async function run() {
|
|
|
108
108
|
quotedWhereRefusedCode === "db-query/row-residency-local-mismatch");
|
|
109
109
|
check("the quoted-WHERE update did not move the row cross-border",
|
|
110
110
|
(b.db.from("residents").where({ _id: "raw-eu" }).first() || {}).dataRegion === "eu-west-1");
|
|
111
|
+
|
|
112
|
+
// RAW PATH 5: a LEADING SQL COMMENT must not hide the write keyword from the
|
|
113
|
+
// ^-anchored gate. A "/* hint */ INSERT ..." or "-- hint\nUPDATE ..." that
|
|
114
|
+
// the gate fails to recognize as a write skips residency enforcement
|
|
115
|
+
// entirely — a cross-border-write bypass. Strip leading comments before the
|
|
116
|
+
// write-detection so the gate still fires.
|
|
117
|
+
var blockCommentRefusedCode = codeOf(function () {
|
|
118
|
+
b.db.runSql(
|
|
119
|
+
"/* region hint */ INSERT INTO residents (_id, name, dataRegion) VALUES ('raw-bc', 'x', 'us-east-1')");
|
|
120
|
+
});
|
|
121
|
+
check("leading block-comment write is recognized + refused (no comment bypass)",
|
|
122
|
+
blockCommentRefusedCode === "db-query/row-residency-local-mismatch");
|
|
123
|
+
check("leading block-comment cross-border row did not persist",
|
|
124
|
+
b.db.from("residents").where({ _id: "raw-bc" }).first() === null);
|
|
125
|
+
|
|
126
|
+
var lineCommentRefusedCode = codeOf(function () {
|
|
127
|
+
b.db.runSql(
|
|
128
|
+
"-- region hint\nUPDATE residents SET dataRegion='us-east-1' WHERE _id='raw-eu'");
|
|
129
|
+
});
|
|
130
|
+
check("leading line-comment write is recognized + refused (no comment bypass)",
|
|
131
|
+
lineCommentRefusedCode === "db-query/row-residency-local-mismatch");
|
|
132
|
+
check("the leading line-comment update did not move the row cross-border",
|
|
133
|
+
(b.db.from("residents").where({ _id: "raw-eu" }).first() || {}).dataRegion === "eu-west-1");
|
|
134
|
+
|
|
135
|
+
// Stacked comment + whitespace before the keyword is also stripped.
|
|
136
|
+
var stackedRefusedCode = codeOf(function () {
|
|
137
|
+
b.db.runSql(
|
|
138
|
+
"/* a */ -- b\n /* c */ INSERT INTO residents (_id, name, dataRegion) VALUES ('raw-st', 'x', 'us-east-1')");
|
|
139
|
+
});
|
|
140
|
+
check("stacked leading comments are stripped + the write is refused",
|
|
141
|
+
stackedRefusedCode === "db-query/row-residency-local-mismatch");
|
|
142
|
+
|
|
143
|
+
// An in-region leading-comment write still persists (no over-rejection).
|
|
144
|
+
b.db.runSql(
|
|
145
|
+
"/* in-region */ INSERT INTO residents (_id, name, dataRegion) VALUES ('raw-bc-ok', 'z', 'eu-west-1')");
|
|
146
|
+
check("in-region leading-comment write still persists (gate does not over-reject)",
|
|
147
|
+
(b.db.from("residents").where({ _id: "raw-bc-ok" }).first() || {}).dataRegion === "eu-west-1");
|
|
148
|
+
|
|
149
|
+
// RAW PATH 6: a writable-CTE write hides its effective verb behind a `WITH`
|
|
150
|
+
// prefix, so the ^-anchored write-keyword detector misses it — node:sqlite
|
|
151
|
+
// executes `WITH c AS (...) INSERT INTO residents ...` / `WITH ... UPDATE
|
|
152
|
+
// residents SET ...` and would persist a cross-border row UNGATED. The gate
|
|
153
|
+
// now recognizes the residency-table write target behind the CTE prefix and
|
|
154
|
+
// FAILS CLOSED (can't parse a CTE body) — directing the operator to the
|
|
155
|
+
// structured builder. (Sibling of RAW PATH 5.)
|
|
156
|
+
var cteInsertCode = codeOf(function () {
|
|
157
|
+
b.db.runSql(
|
|
158
|
+
"WITH src AS (SELECT 'us-east-1' AS r) INSERT INTO residents (_id, name, dataRegion) SELECT 'cte-i', 'x', r FROM src");
|
|
159
|
+
});
|
|
160
|
+
check("writable-CTE INSERT to a residency table is refused (no CTE-prefix bypass)",
|
|
161
|
+
cteInsertCode === "db-query/row-residency-raw-unparseable");
|
|
162
|
+
check("writable-CTE INSERT cross-border row did not persist",
|
|
163
|
+
b.db.from("residents").where({ _id: "cte-i" }).first() === null);
|
|
164
|
+
|
|
165
|
+
var cteUpdateCode = codeOf(function () {
|
|
166
|
+
b.db.prepare(
|
|
167
|
+
"WITH RECURSIVE t AS (SELECT 1) UPDATE residents SET dataRegion = 'us-east-1' WHERE _id = 'raw-eu'").run();
|
|
168
|
+
});
|
|
169
|
+
check("WITH RECURSIVE UPDATE to a residency table is refused (no CTE-prefix bypass)",
|
|
170
|
+
cteUpdateCode === "db-query/row-residency-raw-unparseable");
|
|
171
|
+
check("the writable-CTE update did not move the row cross-border",
|
|
172
|
+
(b.db.from("residents").where({ _id: "raw-eu" }).first() || {}).dataRegion === "eu-west-1");
|
|
173
|
+
|
|
174
|
+
// A read-only CTE (WITH ... SELECT) is NOT a write → not gated (no over-reject).
|
|
175
|
+
var cteReadErr = codeOf(function () {
|
|
176
|
+
b.db.runSql("WITH c AS (SELECT _id FROM residents WHERE dataRegion = 'eu-west-1') SELECT _id FROM c");
|
|
177
|
+
});
|
|
178
|
+
check("read-only WITH...SELECT is not gated (no residency error)", cteReadErr === null);
|
|
179
|
+
|
|
180
|
+
// A writable-CTE write to a NON-residency table is not over-rejected.
|
|
181
|
+
b.db.runSql("CREATE TABLE IF NOT EXISTS notes (_id TEXT PRIMARY KEY, body TEXT)");
|
|
182
|
+
b.db.runSql("WITH s AS (SELECT 'hi' AS b) INSERT INTO notes (_id, body) SELECT 'n1', b FROM s");
|
|
183
|
+
check("writable-CTE write to a NON-residency table still persists (no over-reject)",
|
|
184
|
+
(b.db.from("notes").where({ _id: "n1" }).first() || {}).body === "hi");
|
|
111
185
|
} finally {
|
|
112
186
|
b.compliance.clear();
|
|
113
187
|
b.cryptoField.clearResidencyForTest();
|
|
@@ -16,8 +16,17 @@ function testSurface() {
|
|
|
16
16
|
check("dora.create is a function", typeof b.dora.create === "function");
|
|
17
17
|
check("dora.MAJOR_INCIDENT_THRESHOLDS exposed",
|
|
18
18
|
typeof b.dora.MAJOR_INCIDENT_THRESHOLDS === "object");
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// RTS (EU) 2024/1772 Art. 5: initial notification within 4h of classifying
|
|
20
|
+
// the incident as major; the 24h-from-awareness outer bound is exposed
|
|
21
|
+
// separately. (Was asserted as 24h, contradicting the framework's own
|
|
22
|
+
// incident-report DORA regime which correctly uses 4h.)
|
|
23
|
+
check("dora.INITIAL_REPORT_DEADLINE_MS = 4h (operative from classification)",
|
|
24
|
+
b.dora.INITIAL_REPORT_DEADLINE_MS === b.constants.TIME.hours(4));
|
|
25
|
+
check("dora.INITIAL_REPORT_OUTER_DEADLINE_MS = 24h (outer, from awareness)",
|
|
26
|
+
b.dora.INITIAL_REPORT_OUTER_DEADLINE_MS === b.constants.TIME.hours(24));
|
|
27
|
+
check("dora agrees with incident-report REGIME_DEADLINES.dora",
|
|
28
|
+
b.dora.INITIAL_REPORT_DEADLINE_MS === require("../../lib/incident-report").REGIME_DEADLINES.dora.initial &&
|
|
29
|
+
b.dora.INTERMEDIATE_REPORT_DEADLINE_MS === require("../../lib/incident-report").REGIME_DEADLINES.dora.intermediate);
|
|
21
30
|
check("frameworkError.DoraError exposed",
|
|
22
31
|
typeof b.frameworkError.DoraError === "function");
|
|
23
32
|
}
|
|
@@ -32,8 +41,10 @@ function testClassifyMajor() {
|
|
|
32
41
|
});
|
|
33
42
|
check("classify with critical severity → major",
|
|
34
43
|
rv.classification === "major" && rv.mustReport === true);
|
|
35
|
-
check("major incident →
|
|
36
|
-
rv.mustReportInitialByMs === b.constants.TIME.hours(
|
|
44
|
+
check("major incident → 4h initial-report deadline (from classification)",
|
|
45
|
+
rv.mustReportInitialByMs === b.constants.TIME.hours(4));
|
|
46
|
+
check("major incident → 24h outer initial deadline (from awareness)",
|
|
47
|
+
rv.mustReportInitialOuterByMs === b.constants.TIME.hours(24));
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
function testClassifySignificant() {
|
|
@@ -91,8 +102,11 @@ function testReportInitial() {
|
|
|
91
102
|
check("report initial: returns RTS-shaped record with reportedAt set",
|
|
92
103
|
record && record.incidentId === "INC-2026-0001" &&
|
|
93
104
|
typeof record.reportedAt === "number");
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
// The intermediate deadline anchors on the INITIAL report's SUBMISSION time
|
|
106
|
+
// (record.reportedAt), not detectedAt — a report filed late must not get a
|
|
107
|
+
// deadline measured from detection (RTS 2024/1772 reporting timeline).
|
|
108
|
+
check("report initial: nextStageDueAt = reportedAt + 72h (submission-anchored)",
|
|
109
|
+
record.nextStageDueAt === record.reportedAt + b.constants.TIME.hours(72));
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
function testReportFinal() {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.auth.dpop.verify — alg/kty cross-check (alg-confusion family,
|
|
4
|
+
* CVE-2026-22817 class).
|
|
5
|
+
*
|
|
6
|
+
* The DPoP proof embeds its own public key (header.jwk) and declares the
|
|
7
|
+
* signing alg (header.alg). Every other JWS verifier in the framework calls
|
|
8
|
+
* jwtExternal._assertAlgKtyMatch(alg, jwk) before handing the self-asserted
|
|
9
|
+
* key to node:crypto; the DPoP verifier omitted it. A proof declaring
|
|
10
|
+
* alg:"ES256" while embedding an RSA jwk must be refused with a clean
|
|
11
|
+
* alg/kty mismatch — not run through node:crypto with mismatched params.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var helpers = require("../helpers");
|
|
15
|
+
var b = helpers.b;
|
|
16
|
+
var check = helpers.check;
|
|
17
|
+
var nodeCrypto = require("crypto");
|
|
18
|
+
|
|
19
|
+
function _b64url(buf) {
|
|
20
|
+
return Buffer.from(buf).toString("base64")
|
|
21
|
+
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function run() {
|
|
25
|
+
var rsa = nodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
|
|
26
|
+
var rsaJwk = rsa.publicKey.export({ format: "jwk" }); // { kty:"RSA", n, e }
|
|
27
|
+
|
|
28
|
+
// Hand-craft a proof: header.alg="ES256" but header.jwk is the RSA key,
|
|
29
|
+
// signed (RS256) with the matching RSA private key. The embedded jwk and
|
|
30
|
+
// the declared alg disagree on key type.
|
|
31
|
+
var header = { typ: "dpop+jwt", alg: "ES256", jwk: rsaJwk };
|
|
32
|
+
var payload = { htm: "POST", htu: "https://api.example.com/r",
|
|
33
|
+
jti: "jti-" + rsaJwk.n.slice(0, 8), iat: Math.floor(Date.now() / 1000) };
|
|
34
|
+
var signingInput = _b64url(JSON.stringify(header)) + "." + _b64url(JSON.stringify(payload));
|
|
35
|
+
var sig = nodeCrypto.sign("sha256", Buffer.from(signingInput, "ascii"), rsa.privateKey);
|
|
36
|
+
var proof = signingInput + "." + _b64url(sig);
|
|
37
|
+
|
|
38
|
+
var threw = null;
|
|
39
|
+
try {
|
|
40
|
+
await b.auth.dpop.verify(proof, { htm: "POST", htu: "https://api.example.com/r" });
|
|
41
|
+
} catch (e) { threw = e; }
|
|
42
|
+
check("dpop.verify: alg/kty mismatch (ES256 alg, RSA jwk) is refused with alg-kty-mismatch",
|
|
43
|
+
threw && (threw.code === "auth-jwt-external/alg-kty-mismatch" ||
|
|
44
|
+
threw.code === "auth-jwt-external/alg-crv-mismatch"));
|
|
45
|
+
check("dpop.verify: it is NOT mis-reported as a plain invalid-signature",
|
|
46
|
+
threw && threw.code !== "auth-dpop/invalid-signature");
|
|
47
|
+
|
|
48
|
+
// Control: a well-formed ES256 proof (alg matches the EC jwk) still verifies.
|
|
49
|
+
var ec = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "prime256v1" });
|
|
50
|
+
var goodProof = await b.auth.dpop.buildProof({
|
|
51
|
+
htm: "POST", htu: "https://api.example.com/r", privateKey: ec.privateKey,
|
|
52
|
+
});
|
|
53
|
+
var rv = await b.auth.dpop.verify(goodProof, { htm: "POST", htu: "https://api.example.com/r" });
|
|
54
|
+
check("dpop.verify: matching ES256/EC proof still verifies", rv && rv.header.jwk.kty === "EC");
|
|
55
|
+
|
|
56
|
+
console.log("OK — dpop alg/kty cross-check (" + helpers.getChecks() + " checks)");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { run: run };
|
|
60
|
+
|
|
61
|
+
if (require.main === module) {
|
|
62
|
+
run().then(function () { process.exit(0); })
|
|
63
|
+
.catch(function (err) { process.exitCode = 1; throw err; });
|
|
64
|
+
}
|
|
@@ -923,6 +923,37 @@ async function testDbStoreUpgradePath() {
|
|
|
923
923
|
}
|
|
924
924
|
}
|
|
925
925
|
|
|
926
|
+
async function testDbStoreFindsLegacyKeyedMacRows() {
|
|
927
|
+
// A ticket written BEFORE the keyed-MAC default flip stored the LEGACY
|
|
928
|
+
// salted-sha3 digest in subject_email_hash (non-NULL, so the NULL-only
|
|
929
|
+
// backfill never migrates it). list-by-subject must still FIND it via the
|
|
930
|
+
// dual-read (keyed-MAC + legacy candidates) — otherwise an Art.17 erasure
|
|
931
|
+
// purge (list → delete) skips the subject's prior PII-bearing tickets.
|
|
932
|
+
var cryptoField = require("../../lib/crypto-field");
|
|
933
|
+
var tmpDir = _tmp();
|
|
934
|
+
await setupTestDb(tmpDir);
|
|
935
|
+
try {
|
|
936
|
+
var h = _dbDsr();
|
|
937
|
+
var t = await h.dsr.submit({
|
|
938
|
+
type: "access", subject: { email: "alice@example.com" }, reason: "pre-flip ticket",
|
|
939
|
+
});
|
|
940
|
+
// Simulate a pre-flip row: overwrite the keyed-MAC hash with the legacy
|
|
941
|
+
// salted-sha3 digest the old default would have stored.
|
|
942
|
+
var d = cryptoField.computeDerived("dsr_tickets", "subject_email", "alice@example.com");
|
|
943
|
+
check("dsr legacy: a legacy digest distinct from the keyed-MAC exists",
|
|
944
|
+
d && d.legacyValue && d.legacyValue !== d.value);
|
|
945
|
+
b.db.prepare("UPDATE dsr_tickets SET subject_email_hash = $h WHERE id = $id")
|
|
946
|
+
.run({ $h: d.legacyValue, $id: t.id });
|
|
947
|
+
// RED before the dual-read fix: the keyed-MAC single-value equality never
|
|
948
|
+
// matches the legacy digest, so this returns 0 rows.
|
|
949
|
+
var found = await h.store.list({ subject: { email: "alice@example.com" } });
|
|
950
|
+
check("dsr legacy: list-by-subject finds the pre-flip legacy-hashed ticket",
|
|
951
|
+
found.some(function (x) { return x.id === t.id; }));
|
|
952
|
+
} finally {
|
|
953
|
+
await teardownTestDb(tmpDir);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
926
957
|
// ---- AAD_ROTATION descriptor + reseal ----
|
|
927
958
|
|
|
928
959
|
function testAadRotationDescriptor() {
|
|
@@ -1027,6 +1058,7 @@ async function testResealValidationAndStore() {
|
|
|
1027
1058
|
await testDbStoreSealsAtRest();
|
|
1028
1059
|
await testDbStoreErasurePurgesPriorTickets();
|
|
1029
1060
|
await testDbStoreUpgradePath();
|
|
1061
|
+
await testDbStoreFindsLegacyKeyedMacRows();
|
|
1030
1062
|
// AAD_ROTATION descriptor + reseal
|
|
1031
1063
|
testAadRotationDescriptor();
|
|
1032
1064
|
await testResealValidationAndStore();
|
|
@@ -136,16 +136,59 @@ function testNonModificationBypassesShape() {
|
|
|
136
136
|
check("read-shape audit bypasses before/after requirement", ok === true);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function testOffListModificationVerbsRequireShape() {
|
|
140
|
+
// §11.10(e) must fail closed: a modifying verb NOT on the legacy denylist
|
|
141
|
+
// (anonymize / revoke / overwrite / merge / withdraw_consent / restrict)
|
|
142
|
+
// must still require before/after — previously it bypassed the check.
|
|
143
|
+
var mods = ["subject.anonymize", "consent.revoke", "subject.overwrite",
|
|
144
|
+
"db.merge", "subject.withdraw_consent", "subject.restrict"];
|
|
145
|
+
for (var i = 0; i < mods.length; i += 1) {
|
|
146
|
+
var r = b.fda21cfr11.checkGxpAudit({
|
|
147
|
+
action: mods[i], recordedAt: Date.now(), actorUserId: "u",
|
|
148
|
+
});
|
|
149
|
+
check("checkGxpAudit requires §11.10(e) shape for " + mods[i], r.ok === false);
|
|
150
|
+
}
|
|
151
|
+
// A genuinely complete modification row (before/after/reason) passes.
|
|
152
|
+
var full = b.fda21cfr11.checkGxpAudit({
|
|
153
|
+
action: "subject.anonymize", recordedAt: Date.now(), actorUserId: "u",
|
|
154
|
+
reason: "GDPR Art.17", metadata: { before: { name: "Alice" }, after: { name: null } },
|
|
155
|
+
});
|
|
156
|
+
check("checkGxpAudit accepts a complete anonymize row", full.ok === true);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function testSignatureStrippedRefusedWhenVerifierWired() {
|
|
160
|
+
// #B0 — with verifyWith wired, a record whose signature is null/empty must
|
|
161
|
+
// NOT verify on recordHash alone (recordHash is self-consistency, not
|
|
162
|
+
// authentication). Accepting it is alg:none-style signature stripping.
|
|
163
|
+
var nc = require("node:crypto");
|
|
164
|
+
var key = nc.randomBytes(32);
|
|
165
|
+
var sign = function (buf) { return nc.createHmac("sha256", key).update(buf).digest(); };
|
|
166
|
+
var verify = function (buf, sig) { try { return nc.timingSafeEqual(sign(buf), sig); } catch (_e) { return false; } };
|
|
167
|
+
var fda = b.fda21cfr11.posture({ audit: _fakeAudit(), interceptAudit: false, signWith: sign, verifyWith: verify });
|
|
168
|
+
var rec = fda.electronicSignature.create({
|
|
169
|
+
printedName: "Jane Doe, M.D.", signatureMeaning: "approval",
|
|
170
|
+
predicateRule: "21 CFR 312.62", boundRecord: Buffer.from("trial-data"),
|
|
171
|
+
});
|
|
172
|
+
check("FDA properly-signed record verifies",
|
|
173
|
+
fda.electronicSignature.verify(rec, Buffer.from("trial-data")).ok === true);
|
|
174
|
+
var stripped = Object.assign({}, rec, { signature: null });
|
|
175
|
+
var v = fda.electronicSignature.verify(stripped, Buffer.from("trial-data"));
|
|
176
|
+
check("FDA signature-stripped record refused when verifier wired",
|
|
177
|
+
v.ok === false && v.reason === "signature-required");
|
|
178
|
+
}
|
|
179
|
+
|
|
139
180
|
async function run() {
|
|
140
181
|
testSurface();
|
|
141
182
|
testSignatureCreate();
|
|
142
183
|
testSignatureBadMeaning();
|
|
143
184
|
testSignatureMissingPredicate();
|
|
185
|
+
testSignatureStrippedRefusedWhenVerifierWired();
|
|
144
186
|
testAssertGxpAuditOk();
|
|
145
187
|
testAssertGxpAuditMissingBefore();
|
|
146
188
|
testAssertGxpAuditMetadataAsString();
|
|
147
189
|
testCheckGxpAuditMissingActor();
|
|
148
190
|
testNonModificationBypassesShape();
|
|
191
|
+
testOffListModificationVerbsRequireShape();
|
|
149
192
|
}
|
|
150
193
|
|
|
151
194
|
module.exports = { run: run };
|
|
@@ -185,12 +185,42 @@ async function testExactMembershipNoBypass() {
|
|
|
185
185
|
plain.next === true);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
function testMethodsEmptyThrows() {
|
|
189
|
+
var threw = null;
|
|
190
|
+
try { b.middleware.fetchMetadata({ methods: [] }); } catch (e) { threw = e; }
|
|
191
|
+
check("fetchMetadata({ methods: [] }) refused at config time (no silent pass-through)",
|
|
192
|
+
threw && /non-empty array/.test(threw.message || ""));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function testLayeredStricterGateRuns() {
|
|
196
|
+
// A lenient app-level mount must NOT silently disable a STRICTER sub-route
|
|
197
|
+
// mount sharing the request — each instance gates independently.
|
|
198
|
+
var lenient = b.middleware.fetchMetadata({}); // allowSameSite default true
|
|
199
|
+
var strict = b.middleware.fetchMetadata({ allowSameSite: false });
|
|
200
|
+
var req = _post({ "sec-fetch-site": "same-site", "sec-fetch-mode": "cors", "sec-fetch-dest": "empty" });
|
|
201
|
+
var r1 = await _run(lenient, req, _bodyRes());
|
|
202
|
+
check("lenient app-level gate passes the same-site request", r1.next === true);
|
|
203
|
+
// The SAME req now carries the lenient gate's flag; the stricter instance
|
|
204
|
+
// must still evaluate (previously a shared boolean made it a no-op).
|
|
205
|
+
var r2 = await _run(strict, req, _bodyRes());
|
|
206
|
+
check("stricter sub-route gate still refuses (not disabled by the earlier mount)",
|
|
207
|
+
r2.next === false && r2.status === 403);
|
|
208
|
+
// The SAME stricter instance run twice on a request IS idempotent.
|
|
209
|
+
var req2 = _post({ "sec-fetch-site": "same-site", "sec-fetch-mode": "cors", "sec-fetch-dest": "empty" });
|
|
210
|
+
var s1 = await _run(strict, req2, _bodyRes());
|
|
211
|
+
var s2 = await _run(strict, req2, _bodyRes());
|
|
212
|
+
check("same instance is idempotent (first refuses, second no-ops to next)",
|
|
213
|
+
s1.next === false && s2.next === true);
|
|
214
|
+
}
|
|
215
|
+
|
|
188
216
|
async function run() {
|
|
189
217
|
await testDefaultUnchanged();
|
|
190
218
|
await testDeniedDestWebIdentity();
|
|
191
219
|
await testStorageAccessGate();
|
|
192
220
|
testStrictDestThrows();
|
|
193
221
|
await testExactMembershipNoBypass();
|
|
222
|
+
testMethodsEmptyThrows();
|
|
223
|
+
await testLayeredStricterGateRuns();
|
|
194
224
|
}
|
|
195
225
|
|
|
196
226
|
module.exports = { run: run };
|
|
@@ -16,6 +16,14 @@ async function run() {
|
|
|
16
16
|
check("probes _entities", b.graphqlFederation.queryProbesSdl("{ _entities(representations:[...]) { ... } }") === true);
|
|
17
17
|
check("clean query", b.graphqlFederation.queryProbesSdl("{ user { name } }") === false);
|
|
18
18
|
check("non-string query", b.graphqlFederation.queryProbesSdl(null) === false);
|
|
19
|
+
// Aliased _service with NO leading whitespace must still probe true — the
|
|
20
|
+
// prefix-char-class form let `query{x:_service{sdl}}` slip the SDL gate.
|
|
21
|
+
check("probes aliased _service (no space)", b.graphqlFederation.queryProbesSdl("query{x:_service{sdl}}") === true);
|
|
22
|
+
check("probes single-char-alias _service", b.graphqlFederation.queryProbesSdl("{s:_service{sdl}}") === true);
|
|
23
|
+
check("probes aliased _entities (no space)", b.graphqlFederation.queryProbesSdl("{a:_entities(r:[]){__typename}}") === true);
|
|
24
|
+
// \b still excludes substring field names.
|
|
25
|
+
check("does NOT probe my_service substring", b.graphqlFederation.queryProbesSdl("query{my_service{x}}") === false);
|
|
26
|
+
check("does NOT probe userservice substring", b.graphqlFederation.queryProbesSdl("query{userservice{x}}") === false);
|
|
19
27
|
|
|
20
28
|
// ---- guardSdl opts validation ----
|
|
21
29
|
var threw = null;
|
|
@@ -37,6 +45,31 @@ async function run() {
|
|
|
37
45
|
nonceHeader: "x-my-nonce",
|
|
38
46
|
});
|
|
39
47
|
check("guardSdl: accepts custom nonceHeader", typeof withNonce === "function");
|
|
48
|
+
|
|
49
|
+
// ---- guardSdl drives: a batched ARRAY body whose SECOND operation probes
|
|
50
|
+
// _service must be refused (401) without a router token, not forwarded. ----
|
|
51
|
+
function _drive(mw, body) {
|
|
52
|
+
return new Promise(function (resolve) {
|
|
53
|
+
var out = { status: null, nextCalled: false };
|
|
54
|
+
var res = {
|
|
55
|
+
statusCode: 200,
|
|
56
|
+
setHeader: function () {}, getHeader: function () { return undefined; },
|
|
57
|
+
writeHead: function (s) { out.status = s; },
|
|
58
|
+
end: function () { if (out.status === null) out.status = res.statusCode; resolve(out); },
|
|
59
|
+
};
|
|
60
|
+
mw({ method: "POST", headers: {}, body: body, on: function () {} }, res,
|
|
61
|
+
function () { out.nextCalled = true; resolve(out); });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
var guard = b.graphqlFederation.guardSdl({ routerToken: "a".repeat(64) });
|
|
65
|
+
var batched = await _drive(guard, [{ query: "{ ok }" }, { query: "{ _service { sdl } }" }]);
|
|
66
|
+
check("guardSdl: batched-array _service probe is refused (401), not forwarded",
|
|
67
|
+
batched.status === 401 && batched.nextCalled === false);
|
|
68
|
+
var aliased = await _drive(guard, { query: "query{x:_service{sdl}}" });
|
|
69
|
+
check("guardSdl: aliased _service probe is refused (401)",
|
|
70
|
+
aliased.status === 401 && aliased.nextCalled === false);
|
|
71
|
+
var clean = await _drive(guard, { query: "{ user { name } }" });
|
|
72
|
+
check("guardSdl: a clean query is forwarded (next)", clean.nextCalled === true);
|
|
40
73
|
}
|
|
41
74
|
|
|
42
75
|
module.exports = { run: run };
|
|
@@ -124,6 +124,14 @@ function testGuardHtmlUrlSchemes() {
|
|
|
124
124
|
check("entity-encoded javascript: detected after entity decode",
|
|
125
125
|
rvEnc.issues.some(function (issue) { return issue.kind === "dangerous-url-scheme"; }));
|
|
126
126
|
|
|
127
|
+
// #B7 — NO-semicolon decimal entity `javascript:` (106='j', terminates at
|
|
128
|
+
// the non-digit 'a') is browser-decoded to javascript:. A semicolon-required
|
|
129
|
+
// decoder let this bypass the scheme allowlist as clean.
|
|
130
|
+
var rvNoSemi = b.guardHtml.validate(
|
|
131
|
+
'<a href="javascript:alert(1)">x</a>', { profile: "balanced" });
|
|
132
|
+
check("no-semicolon entity javascript: detected",
|
|
133
|
+
rvNoSemi.issues.some(function (issue) { return issue.kind === "dangerous-url-scheme"; }));
|
|
134
|
+
|
|
127
135
|
// Image-context data URL allowed under balanced when allowImageData true.
|
|
128
136
|
var rvImg = b.guardHtml.validate(
|
|
129
137
|
'<img src="data:image/png;base64,iVBORw0KG" alt="x">',
|
|
@@ -281,6 +289,31 @@ function testGuardHtmlByteCaps() {
|
|
|
281
289
|
|
|
282
290
|
// ---- Sanitize round-trip ----
|
|
283
291
|
|
|
292
|
+
function testGuardHtmlCommentEndDifferential() {
|
|
293
|
+
// mXSS comment-parser differential: the WHATWG HTML parser closes a comment
|
|
294
|
+
// at "--!>" (comment-end-bang) and ABRUPTLY at "<!-->" / "<!--->". A
|
|
295
|
+
// tokenizer honouring only "-->" treats the trailing <img onerror> as part
|
|
296
|
+
// of an inert comment; the browser parses it as a LIVE element. With the
|
|
297
|
+
// comment boundary fixed, the <img> is a real token the sanitizer disarms
|
|
298
|
+
// and validate flags — even in the permissive (allowComments) profile.
|
|
299
|
+
var payloads = [
|
|
300
|
+
"<!-- a --!><img src=x onerror=alert(1)>",
|
|
301
|
+
"<!--><img src=x onerror=alert(1)>-->",
|
|
302
|
+
"<!---><img src=x onerror=alert(1)>",
|
|
303
|
+
];
|
|
304
|
+
for (var i = 0; i < payloads.length; i += 1) {
|
|
305
|
+
var p = payloads[i];
|
|
306
|
+
var s = b.guardHtml.sanitize(p, { profile: "permissive" });
|
|
307
|
+
check("comment-differential: sanitize strips smuggled onerror (" + i + ")",
|
|
308
|
+
s.indexOf("onerror=alert") === -1);
|
|
309
|
+
check("comment-differential: sanitize is not a verbatim pass-through (" + i + ")",
|
|
310
|
+
s !== p);
|
|
311
|
+
var v = b.guardHtml.validate(p, { profile: "permissive" });
|
|
312
|
+
check("comment-differential: validate flags the smuggled element (" + i + ")",
|
|
313
|
+
v.ok === false);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
284
317
|
function testGuardHtmlSanitizeBasic() {
|
|
285
318
|
var clean = b.guardHtml.sanitize("<p>hello <b>world</b></p>",
|
|
286
319
|
{ profile: "strict" });
|
|
@@ -404,6 +437,7 @@ async function run() {
|
|
|
404
437
|
testGuardHtmlControlChar();
|
|
405
438
|
testGuardHtmlSizeCaps();
|
|
406
439
|
testGuardHtmlByteCaps();
|
|
440
|
+
testGuardHtmlCommentEndDifferential();
|
|
407
441
|
testGuardHtmlSanitizeBasic();
|
|
408
442
|
testGuardHtmlEscape();
|
|
409
443
|
testGuardHtmlBadProfile();
|
|
@@ -203,6 +203,17 @@ function testGuardJsonStringLengthCap() {
|
|
|
203
203
|
rv.issues.some(function (i) { return i.kind === "string-too-long"; }));
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
function testGuardJsonStringLengthByteCap() {
|
|
207
|
+
// maxStringLength is a per-string BYTE cap (strict 8 KiB). A multibyte string
|
|
208
|
+
// whose UTF-16 code-unit count is UNDER the cap but whose UTF-8 byte length
|
|
209
|
+
// EXCEEDS it must still be refused — value.length (code units) under-enforces.
|
|
210
|
+
// 4100 'é' = 4100 code units (< 8192) but 8200 UTF-8 bytes (> 8192).
|
|
211
|
+
var multibyte = "é".repeat(4100);
|
|
212
|
+
var rv = b.guardJson.validate('{"k":"' + multibyte + '"}', { profile: "strict" });
|
|
213
|
+
check("per-string maxStringLength measured in UTF-8 bytes (multibyte not under-enforced)",
|
|
214
|
+
rv.issues.some(function (i) { return i.kind === "string-too-long"; }));
|
|
215
|
+
}
|
|
216
|
+
|
|
206
217
|
function testGuardJsonByteCap() {
|
|
207
218
|
// maxBytes is a BYTE cap — multibyte input must be measured by UTF-8
|
|
208
219
|
// byte length, not UTF-16 code-unit count (.length). "é" is one code
|
|
@@ -356,6 +367,7 @@ async function run() {
|
|
|
356
367
|
testGuardJsonKeyCountCap();
|
|
357
368
|
testGuardJsonArrayLengthCap();
|
|
358
369
|
testGuardJsonStringLengthCap();
|
|
370
|
+
testGuardJsonStringLengthByteCap();
|
|
359
371
|
testGuardJsonByteCap();
|
|
360
372
|
testGuardJsonNumericPrecision();
|
|
361
373
|
testGuardJsonTopLevelKeyAllowlist();
|
|
@@ -396,8 +396,80 @@ function testSurface() {
|
|
|
396
396
|
b.httpClient.ALLOWED_DOWNLOAD_HASH_ALGS.indexOf("sha3-512") !== -1);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
async function _drainStream(s) {
|
|
400
|
+
return await new Promise(function (resolve, reject) {
|
|
401
|
+
var chunks = [];
|
|
402
|
+
s.on("data", function (c) { chunks.push(c); });
|
|
403
|
+
s.on("end", function () { resolve(Buffer.concat(chunks)); });
|
|
404
|
+
s.on("error", reject);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function testBandwidthThrottle() {
|
|
409
|
+
// #342: maxBytesPerSec paces a download with real backpressure. The token
|
|
410
|
+
// bucket starts full (a 1s burst = RATE bytes), so a transfer LARGER than the
|
|
411
|
+
// burst is paced: SIZE 20000 @ RATE 10000 → ~10000 free + ~10000 at 10KB/s ≈ 1s.
|
|
412
|
+
var SIZE = 20000, RATE = 10000;
|
|
413
|
+
var payload = Buffer.alloc(SIZE, 0x61);
|
|
414
|
+
await _withServer(function (req, res) {
|
|
415
|
+
res.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": String(SIZE) });
|
|
416
|
+
res.end(payload);
|
|
417
|
+
}, async function (baseUrl) {
|
|
418
|
+
var started = Date.now();
|
|
419
|
+
var resp = await b.httpClient.request({
|
|
420
|
+
url: baseUrl + "/big", method: "GET", responseMode: "stream",
|
|
421
|
+
maxBytesPerSec: RATE, allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
422
|
+
});
|
|
423
|
+
var got = await _drainStream(resp.body);
|
|
424
|
+
var elapsed = Date.now() - started;
|
|
425
|
+
check("throttle: received all bytes intact", got.length === SIZE && got.equals(payload));
|
|
426
|
+
check("throttle: paced (elapsed >= 700ms for 20KB @ 10KB/s)", elapsed >= 700);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function testDownloadTransformInterpose() {
|
|
431
|
+
// #342: downloadTransform (factory form) interposes on the response stream.
|
|
432
|
+
var nodeStream = require("stream");
|
|
433
|
+
var payload = Buffer.from("transform me", "utf8");
|
|
434
|
+
await _withServer(function (req, res) {
|
|
435
|
+
res.writeHead(200, { "Content-Length": String(payload.length) });
|
|
436
|
+
res.end(payload);
|
|
437
|
+
}, async function (baseUrl) {
|
|
438
|
+
var resp = await b.httpClient.request({
|
|
439
|
+
url: baseUrl + "/x", method: "GET", responseMode: "stream",
|
|
440
|
+
downloadTransform: function () {
|
|
441
|
+
return new nodeStream.Transform({
|
|
442
|
+
transform: function (chunk, _e, cb) { cb(null, Buffer.from(chunk.toString("utf8").toUpperCase())); },
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
446
|
+
});
|
|
447
|
+
var got = await _drainStream(resp.body);
|
|
448
|
+
check("downloadTransform: response is transformed (uppercased)", got.toString("utf8") === "TRANSFORM ME");
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function testThrottleTransformValidation() {
|
|
453
|
+
function _rejects(label, opts) {
|
|
454
|
+
return b.httpClient.request(opts).then(
|
|
455
|
+
function () { check(label + " (did not reject)", false); },
|
|
456
|
+
function (e) { check(label, e && /maxBytesPerSec|Transform/i.test(e.message || "")); });
|
|
457
|
+
}
|
|
458
|
+
await _rejects("maxBytesPerSec: non-number refused",
|
|
459
|
+
{ url: "https://x.example/", maxBytesPerSec: "fast" });
|
|
460
|
+
await _rejects("maxBytesPerSec: <= 0 refused",
|
|
461
|
+
{ url: "https://x.example/", maxBytesPerSec: 0 });
|
|
462
|
+
await _rejects("downloadTransform: non-Transform refused",
|
|
463
|
+
{ url: "https://x.example/", downloadTransform: 123 });
|
|
464
|
+
await _rejects("uploadTransform: non-Transform refused",
|
|
465
|
+
{ url: "https://x.example/", uploadTransform: { not: "a stream" } });
|
|
466
|
+
}
|
|
467
|
+
|
|
399
468
|
async function run() {
|
|
400
469
|
testSurface();
|
|
470
|
+
await testBandwidthThrottle();
|
|
471
|
+
await testDownloadTransformInterpose();
|
|
472
|
+
await testThrottleTransformValidation();
|
|
401
473
|
await testDownloadHappyPath();
|
|
402
474
|
await testDownloadExpectedHashMatch();
|
|
403
475
|
await testDownloadHashMismatch();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.httpClient — download/upload transform correctness outside stream mode.
|
|
4
|
+
*
|
|
5
|
+
* - A downloadTransform must apply in the default "buffer" responseMode, not
|
|
6
|
+
* only when responseMode is "stream": the buffered body is the transformed
|
|
7
|
+
* bytes.
|
|
8
|
+
* - A size-changing uploadTransform on a Buffer body must NOT leave a stale
|
|
9
|
+
* Content-Length: the request is framed (chunked) on the transformed bytes
|
|
10
|
+
* so the server receives the whole body, not a truncated prefix.
|
|
11
|
+
*
|
|
12
|
+
* No live network — a local http.Server on a random port.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var http = require("http");
|
|
16
|
+
var nodeStream = require("stream");
|
|
17
|
+
|
|
18
|
+
var helpers = require("../helpers");
|
|
19
|
+
var b = helpers.b;
|
|
20
|
+
var check = helpers.check;
|
|
21
|
+
|
|
22
|
+
async function _withServer(handler, fn) {
|
|
23
|
+
var server = http.createServer(handler);
|
|
24
|
+
var port = await b.testing.listenOnRandomPort(server, "127.0.0.1");
|
|
25
|
+
try { return await fn("http://127.0.0.1:" + port); }
|
|
26
|
+
finally { await new Promise(function (resolve) { server.close(function () { resolve(); }); }); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _upperTransform() {
|
|
30
|
+
return new nodeStream.Transform({
|
|
31
|
+
transform: function (chunk, _enc, cb) { cb(null, Buffer.from(chunk.toString("utf8").toUpperCase())); },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Appends a fixed suffix on flush — the transformed body is strictly larger
|
|
36
|
+
// than the source, so a stale Content-Length would truncate it.
|
|
37
|
+
function _appendTransform() {
|
|
38
|
+
return new nodeStream.Transform({
|
|
39
|
+
transform: function (chunk, _enc, cb) { cb(null, chunk); },
|
|
40
|
+
flush: function (cb) { this.push(Buffer.from("-XFORM")); cb(); },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function testDownloadTransformAppliesInBufferMode() {
|
|
45
|
+
await _withServer(function (req, res) {
|
|
46
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
47
|
+
res.end("hello world");
|
|
48
|
+
}, async function (base) {
|
|
49
|
+
var r = await b.httpClient.request({
|
|
50
|
+
url: base + "/", allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
51
|
+
downloadTransform: _upperTransform, // default responseMode = "buffer"
|
|
52
|
+
});
|
|
53
|
+
check("buffer-mode downloadTransform applied to the buffered body",
|
|
54
|
+
r.body.toString("utf8") === "HELLO WORLD");
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function testUploadTransformSizeChangeNotTruncated() {
|
|
59
|
+
var received = null;
|
|
60
|
+
await _withServer(function (req, res) {
|
|
61
|
+
var chunks = [];
|
|
62
|
+
req.on("data", function (c) { chunks.push(c); });
|
|
63
|
+
req.on("end", function () {
|
|
64
|
+
received = Buffer.concat(chunks).toString("utf8");
|
|
65
|
+
res.writeHead(200); res.end("ok");
|
|
66
|
+
});
|
|
67
|
+
}, async function (base) {
|
|
68
|
+
await b.httpClient.request({
|
|
69
|
+
url: base + "/", method: "POST", body: Buffer.from("payload"),
|
|
70
|
+
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true,
|
|
71
|
+
uploadTransform: _appendTransform, // grows the body past the Buffer length
|
|
72
|
+
});
|
|
73
|
+
check("size-changing uploadTransform delivers the whole transformed body",
|
|
74
|
+
received === "payload-XFORM");
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function run() {
|
|
79
|
+
await testDownloadTransformAppliesInBufferMode();
|
|
80
|
+
await testUploadTransformSizeChangeNotTruncated();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { run: run };
|
|
84
|
+
|
|
85
|
+
if (require.main === module) {
|
|
86
|
+
run().then(function () { console.log("OK http-client-throttle-transform — " + helpers.getChecks() + " checks"); })
|
|
87
|
+
.catch(function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); });
|
|
88
|
+
}
|