@blamejs/core 0.15.12 → 0.15.14
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/index.js +2 -0
- package/lib/a2a-tasks.js +45 -29
- package/lib/acme.js +6 -5
- package/lib/agent-event-bus.js +18 -4
- package/lib/agent-idempotency.js +7 -6
- package/lib/agent-orchestrator.js +2 -5
- package/lib/agent-saga.js +3 -5
- package/lib/agent-snapshot.js +32 -2
- package/lib/agent-tenant.js +2 -5
- package/lib/ai-adverse-decision.js +2 -15
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-capability.js +1 -6
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-dp.js +1 -5
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-input.js +2 -2
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/ai-pref.js +3 -8
- package/lib/ai-quota.js +3 -14
- package/lib/api-key.js +37 -28
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-adapters.js +2 -4
- package/lib/archive-entry-policy.js +32 -0
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +14 -24
- package/lib/archive-tar-read.js +56 -24
- package/lib/archive.js +6 -12
- package/lib/arg-parser.js +7 -6
- package/lib/asn1-der.js +70 -22
- package/lib/asyncapi-traits.js +2 -6
- package/lib/atomic-file.js +312 -33
- package/lib/audit-chain.js +183 -54
- package/lib/audit-daily-review.js +36 -16
- package/lib/audit-emit.js +82 -0
- package/lib/audit-sign.js +258 -0
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +91 -2
- package/lib/auth/access-lock.js +7 -29
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +43 -4
- package/lib/auth/dpop.js +29 -36
- package/lib/auth/fido-mds3.js +14 -16
- package/lib/auth/jwt-external.js +26 -8
- package/lib/auth/jwt.js +15 -17
- package/lib/auth/lockout.js +24 -27
- package/lib/auth/oauth.js +67 -45
- package/lib/auth/oid4vci.js +55 -32
- package/lib/auth/oid4vp.js +3 -2
- package/lib/auth/openid-federation.js +6 -6
- package/lib/auth/passkey.js +4 -4
- package/lib/auth/password.js +8 -2
- package/lib/auth/saml.js +70 -40
- package/lib/auth/sd-jwt-vc-holder.js +2 -14
- package/lib/auth/sd-jwt-vc-issuer.js +2 -14
- package/lib/auth/sd-jwt-vc.js +25 -5
- package/lib/auth/status-list.js +21 -9
- package/lib/auth/step-up.js +15 -13
- package/lib/auth-bot-challenge.js +27 -39
- package/lib/backup/bundle.js +18 -20
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +55 -67
- package/lib/backup/manifest.js +7 -1
- package/lib/bounded-map.js +112 -1
- package/lib/breach-deadline.js +1 -11
- package/lib/break-glass.js +41 -22
- package/lib/cache.js +7 -18
- package/lib/cbor.js +36 -12
- package/lib/cdn-cache-control.js +15 -12
- package/lib/cert.js +8 -13
- package/lib/chain-writer.js +162 -47
- package/lib/cli.js +10 -2
- package/lib/client-hints.js +7 -9
- package/lib/cloud-events.js +43 -33
- package/lib/cluster-storage.js +9 -41
- package/lib/cms-codec.js +2 -1
- package/lib/codepoint-class.js +84 -1
- package/lib/compliance-ai-act-logging.js +1 -6
- package/lib/compliance-ai-act-transparency.js +2 -4
- package/lib/compliance-eaa.js +2 -12
- package/lib/compliance-sanctions-fetcher.js +21 -28
- package/lib/compliance-sanctions.js +11 -21
- package/lib/compliance.js +3 -10
- package/lib/config-drift.js +24 -20
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +21 -12
- package/lib/cookies.js +15 -13
- package/lib/cose.js +39 -11
- package/lib/cra-report.js +1 -11
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +34 -33
- package/lib/crypto.js +138 -7
- package/lib/csp.js +239 -3
- package/lib/daemon.js +46 -42
- package/lib/data-act.js +46 -13
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +75 -49
- package/lib/db.js +66 -27
- package/lib/dbsc.js +5 -6
- package/lib/ddl-change-control.js +18 -14
- package/lib/deprecate.js +4 -5
- package/lib/did.js +6 -2
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +30 -20
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db-migrate.js +21 -22
- package/lib/external-db.js +74 -30
- package/lib/fda-21cfr11.js +42 -13
- package/lib/fdx.js +22 -18
- package/lib/file-upload.js +80 -66
- package/lib/flag-evaluation-context.js +5 -8
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/framework-error.js +12 -0
- package/lib/framework-schema.js +19 -0
- package/lib/fsm.js +7 -12
- package/lib/gate-contract.js +911 -39
- package/lib/gdpr-ropa.js +22 -22
- package/lib/graphql-federation.js +18 -5
- package/lib/guard-agent-registry.js +2 -1
- package/lib/guard-all.js +3 -2
- package/lib/guard-archive.js +9 -30
- package/lib/guard-auth.js +23 -80
- package/lib/guard-cidr.js +20 -96
- package/lib/guard-csv.js +135 -196
- package/lib/guard-domain.js +23 -106
- package/lib/guard-dsn.js +17 -14
- package/lib/guard-email.js +46 -53
- package/lib/guard-envelope.js +2 -2
- package/lib/guard-event-bus-topic.js +2 -1
- package/lib/guard-filename.js +12 -60
- package/lib/guard-graphql.js +28 -75
- package/lib/guard-html-wcag.js +15 -2
- package/lib/guard-html.js +74 -128
- package/lib/guard-idempotency-key.js +2 -1
- package/lib/guard-image.js +280 -77
- package/lib/guard-imap-command.js +9 -10
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +101 -109
- package/lib/guard-jsonpath.js +20 -88
- package/lib/guard-jwt.js +32 -114
- package/lib/guard-list-id.js +2 -7
- package/lib/guard-list-unsubscribe.js +2 -7
- package/lib/guard-mail-compose.js +5 -6
- package/lib/guard-mail-move.js +3 -2
- package/lib/guard-mail-query.js +5 -3
- package/lib/guard-mail-sieve.js +6 -7
- package/lib/guard-managesieve-command.js +6 -5
- package/lib/guard-markdown.js +76 -110
- package/lib/guard-message-id.js +5 -6
- package/lib/guard-mime.js +20 -99
- package/lib/guard-oauth.js +19 -73
- package/lib/guard-pdf.js +65 -72
- package/lib/guard-pop3-command.js +13 -14
- package/lib/guard-posture-chain.js +2 -1
- package/lib/guard-regex.js +24 -99
- package/lib/guard-saga-config.js +2 -1
- package/lib/guard-shell.js +22 -87
- package/lib/guard-smtp-command.js +9 -12
- package/lib/guard-sql.js +15 -13
- package/lib/guard-stream-args.js +2 -1
- package/lib/guard-svg.js +103 -149
- package/lib/guard-template.js +23 -80
- package/lib/guard-tenant-id.js +2 -1
- package/lib/guard-text.js +592 -0
- package/lib/guard-time.js +27 -95
- package/lib/guard-uuid.js +21 -93
- package/lib/guard-xml.js +76 -106
- package/lib/guard-yaml.js +24 -60
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cache.js +5 -12
- package/lib/http-client-cookie-jar.js +35 -16
- package/lib/http-client.js +233 -74
- package/lib/http-message-signature.js +3 -2
- package/lib/i18n-messageformat.js +5 -7
- package/lib/i18n.js +83 -26
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/inbox.js +8 -8
- package/lib/incident-report.js +12 -27
- package/lib/ip-utils.js +49 -6
- package/lib/jobs.js +3 -2
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/keychain.js +6 -18
- package/lib/legal-hold.js +30 -23
- package/lib/log-stream-cloudwatch.js +44 -112
- package/lib/log-stream-otlp-grpc.js +17 -14
- package/lib/log-stream-otlp.js +16 -80
- package/lib/log-stream-webhook.js +20 -92
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arc-sign.js +8 -3
- package/lib/mail-arf.js +4 -3
- package/lib/mail-auth.js +43 -69
- package/lib/mail-bimi.js +35 -55
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-pgp.js +3 -2
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-dav.js +8 -35
- package/lib/mail-deploy.js +15 -14
- package/lib/mail-dkim.js +19 -38
- package/lib/mail-greylist.js +17 -30
- package/lib/mail-helo.js +4 -7
- package/lib/mail-journal.js +13 -9
- package/lib/mail-mdn.js +12 -7
- package/lib/mail-rbl.js +7 -12
- package/lib/mail-scan.js +5 -7
- package/lib/mail-send-deliver.js +33 -20
- package/lib/mail-server-imap.js +32 -87
- package/lib/mail-server-jmap.js +39 -52
- package/lib/mail-server-managesieve.js +29 -83
- package/lib/mail-server-mx.js +33 -74
- package/lib/mail-server-net.js +177 -0
- package/lib/mail-server-pop3.js +30 -83
- package/lib/mail-server-rate-limit.js +30 -6
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-submission.js +34 -73
- package/lib/mail-server-tls.js +98 -2
- package/lib/mail-spam-score.js +9 -12
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail-store.js +3 -2
- package/lib/mail.js +28 -13
- package/lib/markup-escape.js +31 -0
- package/lib/markup-tokenizer.js +78 -0
- package/lib/mcp-tool-registry.js +26 -23
- package/lib/mcp.js +7 -5
- package/lib/mdoc.js +36 -14
- package/lib/metrics.js +46 -33
- package/lib/middleware/age-gate.js +3 -23
- package/lib/middleware/api-encrypt.js +13 -24
- package/lib/middleware/assetlinks.js +2 -7
- package/lib/middleware/bearer-auth.js +2 -1
- package/lib/middleware/body-parser.js +41 -52
- package/lib/middleware/bot-disclose.js +7 -10
- package/lib/middleware/bot-guard.js +28 -28
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +73 -23
- package/lib/middleware/daily-byte-quota.js +12 -30
- package/lib/middleware/deny-response.js +19 -1
- package/lib/middleware/fetch-metadata.js +35 -4
- package/lib/middleware/idempotency-key.js +4 -20
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +45 -44
- package/lib/middleware/scim-server.js +5 -3
- package/lib/middleware/security-headers.js +33 -7
- package/lib/middleware/security-txt.js +2 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +14 -29
- package/lib/middleware/web-app-manifest.js +2 -7
- package/lib/migrations.js +4 -13
- package/lib/mime-parse.js +34 -17
- package/lib/module-loader.js +44 -0
- package/lib/money.js +105 -0
- package/lib/mtls-ca.js +125 -41
- package/lib/network-byte-quota.js +4 -18
- package/lib/network-dane.js +8 -6
- package/lib/network-dns-resolver.js +98 -7
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +18 -17
- package/lib/network-heartbeat.js +3 -2
- package/lib/network-smtp-policy.js +52 -46
- package/lib/network-tls.js +67 -47
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +2 -12
- package/lib/nist-crosswalk.js +1 -1
- package/lib/nonce-store.js +81 -3
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +31 -8
- package/lib/object-store/azure-blob-bucket-ops.js +2 -6
- package/lib/object-store/azure-blob.js +6 -47
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +14 -75
- package/lib/object-store/http-put.js +2 -7
- package/lib/object-store/http-request.js +157 -0
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4-bucket-ops.js +2 -1
- package/lib/object-store/sigv4.js +10 -79
- package/lib/observability-otlp-exporter.js +29 -29
- package/lib/observability.js +95 -0
- package/lib/openapi-paths-builder.js +7 -3
- package/lib/otel-export.js +7 -10
- package/lib/outbox.js +54 -41
- package/lib/pagination.js +11 -15
- package/lib/parsers/safe-env.js +5 -4
- package/lib/parsers/safe-ini.js +10 -4
- package/lib/parsers/safe-toml.js +11 -9
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/parsers/safe-yaml.js +28 -9
- package/lib/pick.js +63 -5
- package/lib/pipl-cn.js +69 -59
- package/lib/privacy-pass.js +8 -6
- package/lib/problem-details.js +2 -5
- package/lib/pubsub.js +4 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +9 -4
- package/lib/render.js +4 -2
- package/lib/request-helpers.js +334 -29
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +24 -22
- package/lib/retention.js +23 -9
- package/lib/router.js +17 -4
- package/lib/safe-async.js +457 -0
- package/lib/safe-buffer.js +137 -6
- package/lib/safe-decompress.js +2 -1
- package/lib/safe-dns.js +2 -1
- package/lib/safe-ical.js +14 -28
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +51 -8
- package/lib/safe-jsonpath.js +6 -5
- package/lib/safe-mime.js +25 -29
- package/lib/safe-redirect.js +2 -5
- package/lib/safe-schema.js +7 -6
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-sql.js +126 -0
- package/lib/safe-vcard.js +13 -27
- package/lib/sandbox-worker.js +15 -2
- package/lib/sandbox.js +4 -3
- package/lib/scheduler.js +22 -13
- package/lib/sec-cyber.js +82 -37
- package/lib/seeders.js +4 -11
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/self-update.js +92 -69
- package/lib/session-device-binding.js +112 -66
- package/lib/session.js +219 -7
- package/lib/sql.js +228 -81
- package/lib/sse.js +6 -10
- package/lib/static.js +201 -111
- package/lib/storage.js +4 -2
- package/lib/structured-fields.js +275 -16
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +53 -38
- package/lib/tsa.js +16 -17
- package/lib/validate-opts.js +154 -8
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +34 -49
- package/lib/vault-aad.js +3 -2
- package/lib/vc.js +3 -8
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/vex.js +35 -13
- package/lib/web-push-vapid.js +23 -20
- package/lib/webhook-dispatcher.js +706 -0
- package/lib/webhook.js +59 -19
- package/lib/websocket-channels.js +9 -7
- package/lib/websocket.js +8 -4
- package/lib/worm.js +3 -4
- package/lib/ws-client.js +65 -56
- package/lib/x509-chain.js +44 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/audit-sign.js
CHANGED
|
@@ -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");
|
|
@@ -190,6 +191,33 @@ function _computeFingerprint(publicKeyPem) {
|
|
|
190
191
|
return sha3Hash(publicKeyPem);
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
// ---- Store-independent chain anchor (the checkpoint protocol lifted off the
|
|
195
|
+
// framework audit_log / audit_checkpoints store so a consumer can anchor THEIR
|
|
196
|
+
// OWN hash chain) ----
|
|
197
|
+
|
|
198
|
+
// Domain-separation magic for a consumer anchor's signed bytes. DISTINCT from
|
|
199
|
+
// the framework audit-checkpoint format on purpose: a checkpoint signature can
|
|
200
|
+
// never be replayed as an anchor (or vice versa), and the framework
|
|
201
|
+
// audit_checkpoints wire format stays byte-stable (b.audit.checkpoint is
|
|
202
|
+
// untouched). Changing this invalidates every prior anchor.
|
|
203
|
+
var ANCHOR_FORMAT = "blamejs-chain-anchor-v1";
|
|
204
|
+
|
|
205
|
+
// Build the canonical signed bytes for one anchor. Fixed multi-line layout (no
|
|
206
|
+
// JSON serializer quirks). prevTipHash IS part of the signed payload, so the
|
|
207
|
+
// link between consecutive anchors is tamper-evident — an attacker cannot
|
|
208
|
+
// rewrite the linkage without breaking the signature. `format` lets a consumer
|
|
209
|
+
// domain-separate their own anchors from any other anchored chain.
|
|
210
|
+
function anchorPayload(counter, tipHash, prevTipHash, createdAt, format) {
|
|
211
|
+
return Buffer.from(
|
|
212
|
+
(format || ANCHOR_FORMAT) + "\n" +
|
|
213
|
+
String(counter) + "\n" +
|
|
214
|
+
tipHash + "\n" +
|
|
215
|
+
(prevTipHash || "") + "\n" +
|
|
216
|
+
String(createdAt),
|
|
217
|
+
"utf8"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
193
221
|
// ---- Passphrase sourcing (delegates to lib/passphrase-source.js with
|
|
194
222
|
// audit-signing-specific env var names) ----
|
|
195
223
|
|
|
@@ -798,6 +826,233 @@ async function rotateSigningKey(rotOpts) {
|
|
|
798
826
|
};
|
|
799
827
|
}
|
|
800
828
|
|
|
829
|
+
// Defensive coercion of an operator-supplied tip into the canonical fields.
|
|
830
|
+
// Config-time tier: throws a typed AuditSignError on a malformed tip so the
|
|
831
|
+
// consumer catches the bug at the call site, not as an opaque "signature
|
|
832
|
+
// failed" later. prevTipHash is OPTIONAL (absent for the genesis anchor).
|
|
833
|
+
function _normalizeTip(tip, fnLabel) {
|
|
834
|
+
if (!tip || typeof tip !== "object") {
|
|
835
|
+
throw _err("ANCHOR_BAD_TIP",
|
|
836
|
+
"auditSign." + fnLabel + ": tip must be an object { counter, tipHash }");
|
|
837
|
+
}
|
|
838
|
+
var counter = tip.counter;
|
|
839
|
+
if (!numericBounds.isNonNegativeFiniteInt(counter)) {
|
|
840
|
+
throw _err("ANCHOR_BAD_COUNTER",
|
|
841
|
+
"auditSign." + fnLabel + ": tip.counter must be a non-negative integer (got: " + counter + ")");
|
|
842
|
+
}
|
|
843
|
+
if (typeof tip.tipHash !== "string" || tip.tipHash.length === 0) {
|
|
844
|
+
throw _err("ANCHOR_BAD_TIPHASH",
|
|
845
|
+
"auditSign." + fnLabel + ": tip.tipHash must be a non-empty string");
|
|
846
|
+
}
|
|
847
|
+
if (tip.prevTipHash != null && typeof tip.prevTipHash !== "string") {
|
|
848
|
+
throw _err("ANCHOR_BAD_PREV",
|
|
849
|
+
"auditSign." + fnLabel + ": tip.prevTipHash, when present, must be a string");
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
counter: counter,
|
|
853
|
+
tipHash: tip.tipHash,
|
|
854
|
+
prevTipHash: tip.prevTipHash != null ? tip.prevTipHash : null,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* @primitive b.auditSign.anchor
|
|
860
|
+
* @signature b.auditSign.anchor(tip, opts?)
|
|
861
|
+
* @since 0.15.13
|
|
862
|
+
* @status stable
|
|
863
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, sox-404
|
|
864
|
+
* @related b.auditSign.verifyAnchor, b.auditSign.verifyAnchorChain, b.audit.checkpoint
|
|
865
|
+
*
|
|
866
|
+
* Sign a hash-chain tip with the in-memory PQC key, returning a
|
|
867
|
+
* self-describing anchor object the consumer persists in THEIR OWN store. This
|
|
868
|
+
* is the `b.audit.checkpoint()` protocol lifted off the framework `audit_log` /
|
|
869
|
+
* `audit_checkpoints` tables: a consumer running their own append-only chain
|
|
870
|
+
* anchors its tip the same tamper-evident way, with no framework table, no
|
|
871
|
+
* `clusterStorage`, and no leader requirement. A full-chain rewrite that
|
|
872
|
+
* recomputes every row hash still cannot forge the signature without the
|
|
873
|
+
* audit-signing private key, so a later `verifyAnchorChain` detects it.
|
|
874
|
+
*
|
|
875
|
+
* `tip.prevTipHash` (optional) is bound into the signed bytes, so truncation /
|
|
876
|
+
* reorder of a stored anchor sequence is caught by the signature, not just a
|
|
877
|
+
* plaintext compare. `opts.format` domain-separates a consumer's anchors
|
|
878
|
+
* (default `"blamejs-chain-anchor-v1"`).
|
|
879
|
+
*
|
|
880
|
+
* Verification resolves the public key from the recorded fingerprint via the
|
|
881
|
+
* key-history file under the `init({ dataDir })` directory, so it is bound to
|
|
882
|
+
* that key store (not fully store-free) — keep the history with the anchors.
|
|
883
|
+
*
|
|
884
|
+
* Throws `AuditSignError` (`ANCHOR_BAD_TIP` / `ANCHOR_BAD_COUNTER` /
|
|
885
|
+
* `ANCHOR_BAD_TIPHASH` / `ANCHOR_BAD_PREV`) on a malformed tip;
|
|
886
|
+
* `audit-sign/not-initialized` when `init()` has not been awaited.
|
|
887
|
+
*
|
|
888
|
+
* @opts
|
|
889
|
+
* format: string, // default "blamejs-chain-anchor-v1" — domain-separation magic in the signed payload
|
|
890
|
+
* createdAt: number, // default Date.now() — the anchor timestamp (also signed)
|
|
891
|
+
*
|
|
892
|
+
* @example
|
|
893
|
+
* await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
|
|
894
|
+
* var a = b.auditSign.anchor({ counter: 42, tipHash: "9f4e", prevTipHash: "1b7d" },
|
|
895
|
+
* { format: "my-app-ledger-v1" });
|
|
896
|
+
* // → { format, counter, tipHash, prevTipHash, createdAt, algorithm,
|
|
897
|
+
* // publicKeyFingerprint, signature }
|
|
898
|
+
*/
|
|
899
|
+
function anchor(tip, opts) {
|
|
900
|
+
_requireInit();
|
|
901
|
+
opts = opts || {};
|
|
902
|
+
var t = _normalizeTip(tip, "anchor");
|
|
903
|
+
var format = (typeof opts.format === "string" && opts.format.length > 0) ? opts.format : ANCHOR_FORMAT;
|
|
904
|
+
var createdAt = (typeof opts.createdAt === "number" && isFinite(opts.createdAt)) ? opts.createdAt : Date.now();
|
|
905
|
+
var payload = anchorPayload(t.counter, t.tipHash, t.prevTipHash, createdAt, format);
|
|
906
|
+
var sigBuf = sign(payload);
|
|
907
|
+
return {
|
|
908
|
+
format: format,
|
|
909
|
+
counter: t.counter,
|
|
910
|
+
tipHash: t.tipHash,
|
|
911
|
+
prevTipHash: t.prevTipHash,
|
|
912
|
+
createdAt: createdAt,
|
|
913
|
+
algorithm: keys.algorithm,
|
|
914
|
+
publicKeyFingerprint: keys.fingerprint,
|
|
915
|
+
signature: sigBuf.toString("hex"),
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Resolve + verify ONE anchor's signature. Returns { ok, reason? }. Never
|
|
920
|
+
// throws on a forgery / unknown key (defensive-reader tier) — the caller
|
|
921
|
+
// branches on ok.
|
|
922
|
+
function _verifyOneAnchor(a) {
|
|
923
|
+
if (!a || typeof a !== "object") return { ok: false, reason: "anchor is not an object" };
|
|
924
|
+
if (typeof a.tipHash !== "string" || typeof a.signature !== "string" ||
|
|
925
|
+
typeof a.publicKeyFingerprint !== "string") {
|
|
926
|
+
return { ok: false, reason: "anchor missing tipHash / signature / publicKeyFingerprint" };
|
|
927
|
+
}
|
|
928
|
+
if (typeof a.counter !== "number" || !isFinite(a.counter)) {
|
|
929
|
+
return { ok: false, reason: "anchor counter is not a finite number" };
|
|
930
|
+
}
|
|
931
|
+
var pub = getPublicKeyByFingerprint(a.publicKeyFingerprint);
|
|
932
|
+
if (!pub) {
|
|
933
|
+
return { ok: false, reason: "no audit-signing key on record for this anchor's fingerprint" };
|
|
934
|
+
}
|
|
935
|
+
var format = (typeof a.format === "string" && a.format.length > 0) ? a.format : ANCHOR_FORMAT;
|
|
936
|
+
// prevTipHash is part of the signed payload — rebuild it the same way anchor()
|
|
937
|
+
// did (absent / null → "") so the bytes are byte-identical on a clean anchor.
|
|
938
|
+
var prevTipHash = (a.prevTipHash != null && typeof a.prevTipHash === "string") ? a.prevTipHash : "";
|
|
939
|
+
var payload = anchorPayload(Number(a.counter), a.tipHash, prevTipHash, Number(a.createdAt), format);
|
|
940
|
+
// Buffer.from(hex) silently truncates on non-hex / odd-length; reject when
|
|
941
|
+
// the parsed bytes don't round-trip to the original lowercased hex.
|
|
942
|
+
var sigBuf = Buffer.from(a.signature, "hex");
|
|
943
|
+
if (sigBuf.toString("hex") !== String(a.signature).toLowerCase()) {
|
|
944
|
+
return { ok: false, reason: "anchor signature is not valid hex" };
|
|
945
|
+
}
|
|
946
|
+
if (!verify(payload, sigBuf, pub)) {
|
|
947
|
+
return { ok: false, reason: "post-quantum signature failed" };
|
|
948
|
+
}
|
|
949
|
+
return { ok: true };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* @primitive b.auditSign.verifyAnchor
|
|
954
|
+
* @signature b.auditSign.verifyAnchor(anchor)
|
|
955
|
+
* @since 0.15.13
|
|
956
|
+
* @status stable
|
|
957
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, sox-404
|
|
958
|
+
* @related b.auditSign.anchor, b.auditSign.verifyAnchorChain
|
|
959
|
+
*
|
|
960
|
+
* Verify a single anchor produced by `b.auditSign.anchor`. Resolves the public
|
|
961
|
+
* key by the anchor's recorded fingerprint (live key or a rotated-out key from
|
|
962
|
+
* the unsealed history), rebuilds the canonical payload, and checks the
|
|
963
|
+
* post-quantum signature. Returns `{ ok: true }` when valid, or
|
|
964
|
+
* `{ ok: false, reason }` for a forgery, an unknown signing key, malformed hex,
|
|
965
|
+
* or a missing field. Never throws on adversarial content.
|
|
966
|
+
*
|
|
967
|
+
* @example
|
|
968
|
+
* var a = b.auditSign.anchor({ counter: 1, tipHash: "ab12" });
|
|
969
|
+
* b.auditSign.verifyAnchor(a); // → { ok: true }
|
|
970
|
+
*/
|
|
971
|
+
function verifyAnchor(a) {
|
|
972
|
+
_requireInit();
|
|
973
|
+
return _verifyOneAnchor(a);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* @primitive b.auditSign.verifyAnchorChain
|
|
978
|
+
* @signature b.auditSign.verifyAnchorChain(anchors, opts?)
|
|
979
|
+
* @since 0.15.13
|
|
980
|
+
* @status stable
|
|
981
|
+
* @compliance hipaa, pci-dss, gdpr, soc2, sox-404
|
|
982
|
+
* @related b.auditSign.anchor, b.auditSign.verifyAnchor, b.audit.verifyCheckpoints
|
|
983
|
+
*
|
|
984
|
+
* Walk an ordered array of anchors (oldest first) and verify each one's
|
|
985
|
+
* signature AND that the sequence is internally consistent: counters strictly
|
|
986
|
+
* increase, and each anchor's `prevTipHash` equals the previous anchor's
|
|
987
|
+
* `tipHash`. This catches the two attacks a single-anchor check cannot — a
|
|
988
|
+
* stored-anchor truncation / reorder (link break) and a full-chain rewrite
|
|
989
|
+
* (signature break). Returns `{ ok: true, anchorsVerified }`, or
|
|
990
|
+
* `{ ok: false, anchorsVerified, breakAt, reason }` at the first break.
|
|
991
|
+
*
|
|
992
|
+
* `requireLinkage` (default true) makes a non-genesis anchor that omits
|
|
993
|
+
* `prevTipHash` a break, so an attacker can't drop the link to bypass the
|
|
994
|
+
* check. Pass `requireLinkage: false` for unlinked anchors.
|
|
995
|
+
*
|
|
996
|
+
* @opts
|
|
997
|
+
* requireLinkage: boolean, // default true — every non-genesis anchor must carry a matching prevTipHash
|
|
998
|
+
*
|
|
999
|
+
* @example
|
|
1000
|
+
* var a1 = b.auditSign.anchor({ counter: 1, tipHash: "h1" });
|
|
1001
|
+
* var a2 = b.auditSign.anchor({ counter: 2, tipHash: "h2", prevTipHash: "h1" });
|
|
1002
|
+
* b.auditSign.verifyAnchorChain([a1, a2]); // → { ok: true, anchorsVerified: 2 }
|
|
1003
|
+
*/
|
|
1004
|
+
function verifyAnchorChain(anchors, opts) {
|
|
1005
|
+
_requireInit();
|
|
1006
|
+
opts = opts || {};
|
|
1007
|
+
var requireLinkage = opts.requireLinkage !== false;
|
|
1008
|
+
if (!Array.isArray(anchors)) {
|
|
1009
|
+
return { ok: false, anchorsVerified: 0, breakAt: 0, reason: "anchors must be an array" };
|
|
1010
|
+
}
|
|
1011
|
+
if (anchors.length === 0) return { ok: true, anchorsVerified: 0 };
|
|
1012
|
+
|
|
1013
|
+
var prev = null;
|
|
1014
|
+
for (var i = 0; i < anchors.length; i += 1) {
|
|
1015
|
+
var a = anchors[i];
|
|
1016
|
+
var sigResult = _verifyOneAnchor(a);
|
|
1017
|
+
if (!sigResult.ok) {
|
|
1018
|
+
return { ok: false, anchorsVerified: i, breakAt: i, reason: sigResult.reason };
|
|
1019
|
+
}
|
|
1020
|
+
if (prev === null) {
|
|
1021
|
+
// Genesis: a prevTipHash on the FIRST anchor means a predecessor was
|
|
1022
|
+
// dropped — fail closed.
|
|
1023
|
+
if (a.prevTipHash != null) {
|
|
1024
|
+
return {
|
|
1025
|
+
ok: false, anchorsVerified: i, breakAt: i,
|
|
1026
|
+
reason: "non-genesis anchor missing predecessor (prevTipHash set on first anchor)",
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
if (!(Number(a.counter) > Number(prev.counter))) {
|
|
1031
|
+
return {
|
|
1032
|
+
ok: false, anchorsVerified: i, breakAt: i,
|
|
1033
|
+
reason: "anchor counter not strictly increasing",
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (requireLinkage) {
|
|
1037
|
+
if (a.prevTipHash == null) {
|
|
1038
|
+
return {
|
|
1039
|
+
ok: false, anchorsVerified: i, breakAt: i,
|
|
1040
|
+
reason: "non-genesis anchor missing prevTipHash linkage",
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
if (a.prevTipHash !== prev.tipHash) {
|
|
1044
|
+
return {
|
|
1045
|
+
ok: false, anchorsVerified: i, breakAt: i,
|
|
1046
|
+
reason: "anchor prevTipHash does not match the previous tipHash (truncation / reorder)",
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
prev = a;
|
|
1052
|
+
}
|
|
1053
|
+
return { ok: true, anchorsVerified: anchors.length };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
801
1056
|
function _resetForTest() {
|
|
802
1057
|
keys = null;
|
|
803
1058
|
initialized = false;
|
|
@@ -810,6 +1065,9 @@ module.exports = {
|
|
|
810
1065
|
init: init,
|
|
811
1066
|
sign: sign,
|
|
812
1067
|
verify: verify,
|
|
1068
|
+
anchor: anchor,
|
|
1069
|
+
verifyAnchor: verifyAnchor,
|
|
1070
|
+
verifyAnchorChain: verifyAnchorChain,
|
|
813
1071
|
rotateSigningKey: rotateSigningKey,
|
|
814
1072
|
reSignAll: reSignAll,
|
|
815
1073
|
getPublicKey: getPublicKey,
|
package/lib/audit-tools.js
CHANGED
|
@@ -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 {
|
package/lib/audit.js
CHANGED
|
@@ -424,6 +424,13 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
424
424
|
"bootgates", // b.bootGates (bootgates.passed / bootgates.failed / bootgates.onfail_threw — boot-invariant runner)
|
|
425
425
|
"metrics", // b.metrics.snapshot.shadowRegistry (metrics.shadow.cardinality_dropped — namespaced metrics export)
|
|
426
426
|
"jose", // b.jose.jwe.experimental (jose.jwe.experimental.encrypt / .decrypt — ML-KEM-JWE pre-IANA)
|
|
427
|
+
"ai", // b.ai.adverseDecision (ai.adverse_decision.* — FCRA adverse-action decisioning)
|
|
428
|
+
"breach", // b.breachDeadline (breach.report.* — breach-notification deadline clock)
|
|
429
|
+
"cra", // b.craReport (cra.report.* — EU Cyber Resilience Act conformity)
|
|
430
|
+
"gdpr", // b.gdprRopa (gdpr.ropa.* — GDPR Art. 30 Records of Processing Activities)
|
|
431
|
+
"incident", // b.incidentReport (incident.report.* — incident lifecycle)
|
|
432
|
+
"middleware", // b.middleware.ageGate / dailyByteQuota (middleware.age_gate.* / middleware.daily_byte_quota.*)
|
|
433
|
+
"nis2", // b.nis2Report (nis2.report.* — NIS2 Directive incident reporting)
|
|
427
434
|
];
|
|
428
435
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
429
436
|
|
|
@@ -790,7 +797,12 @@ async function query(criteria) {
|
|
|
790
797
|
if (criteria.resourceKind) q = q.where({ resourceKind: criteria.resourceKind });
|
|
791
798
|
if (criteria.outcome) q = q.where({ outcome: criteria.outcome });
|
|
792
799
|
|
|
793
|
-
|
|
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");
|
|
794
806
|
if (criteria.limit != null) q.limit(criteria.limit);
|
|
795
807
|
if (criteria.offset != null) q.offset(criteria.offset);
|
|
796
808
|
|
|
@@ -827,7 +839,7 @@ async function _queryCluster(criteria) {
|
|
|
827
839
|
if (criteria.resourceKind) qb.where("resourceKind", criteria.resourceKind);
|
|
828
840
|
if (criteria.outcome) qb.where("outcome", criteria.outcome);
|
|
829
841
|
|
|
830
|
-
qb.orderBy("monotonicCounter", "asc");
|
|
842
|
+
qb.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
|
|
831
843
|
if (criteria.limit != null) qb.limit(criteria.limit);
|
|
832
844
|
if (criteria.offset != null) qb.offset(criteria.offset);
|
|
833
845
|
|
|
@@ -944,6 +956,13 @@ async function checkpoint(opts) {
|
|
|
944
956
|
cluster.requireLeader();
|
|
945
957
|
opts = opts || {};
|
|
946
958
|
|
|
959
|
+
// Bind this checkpoint to the database it reads the tip from. checkpoint()
|
|
960
|
+
// spans async boundaries (tip read → sign → insert); a fire-and-forget call
|
|
961
|
+
// launched by db.close() can have its insert resume AFTER the database it
|
|
962
|
+
// read closed and a fresh one opened, anchoring the old tip into the wrong
|
|
963
|
+
// database. Capture the live db generation now and re-check before the write.
|
|
964
|
+
var dbGenAtEntry = db()._dbGeneration();
|
|
965
|
+
|
|
947
966
|
var tipReadBuilt = sql.select("audit_log", _sqlOpts())
|
|
948
967
|
.columns(["_id", "monotonicCounter", "rowHash"])
|
|
949
968
|
.orderBy("monotonicCounter", "desc")
|
|
@@ -972,6 +991,12 @@ async function checkpoint(opts) {
|
|
|
972
991
|
var signature = auditSign.sign(payload);
|
|
973
992
|
var pubFp = auditSign.getPublicKeyFingerprint();
|
|
974
993
|
|
|
994
|
+
// Fail closed if the database changed under us between the tip read and here
|
|
995
|
+
// (e.g. a close()-launched checkpoint resuming after a fresh db opened). The
|
|
996
|
+
// tip we signed belongs to a database that is gone; anchoring it into the
|
|
997
|
+
// current one would forge a checkpoint. Write nothing.
|
|
998
|
+
if (db()._dbGeneration() !== dbGenAtEntry) return null;
|
|
999
|
+
|
|
975
1000
|
var ckptId = generateToken(TRACE_ID_BYTES);
|
|
976
1001
|
var fencingToken = cluster.fencingToken();
|
|
977
1002
|
await _insertCheckpoint(
|
|
@@ -1445,6 +1470,69 @@ function safeEmit(event) {
|
|
|
1445
1470
|
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
1446
1471
|
}
|
|
1447
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* @primitive b.audit.namespaced
|
|
1475
|
+
* @signature b.audit.namespaced(prefix, opts?)
|
|
1476
|
+
* @since 0.15.13
|
|
1477
|
+
* @status stable
|
|
1478
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
1479
|
+
* @related b.audit.safeEmit, b.audit.emit, b.observability.namespaced
|
|
1480
|
+
*
|
|
1481
|
+
* Build a drop-silent emitter bound to one action namespace — the shape every
|
|
1482
|
+
* framework primitive hand-rolled as a private `_emitAudit(action, outcome,
|
|
1483
|
+
* metadata)` closure (or inline) (`if (!on) return; try { safeEmit({ action:
|
|
1484
|
+
* "ns." + action, outcome, metadata }); } catch {}`). The returned function
|
|
1485
|
+
* prefixes `action` with `prefix + "."`, fills `metadata` with `{}` when
|
|
1486
|
+
* omitted, and routes through `safeEmit` (so the same redaction + outcome
|
|
1487
|
+
* normalization applies).
|
|
1488
|
+
*
|
|
1489
|
+
* Every caller drives the SAME 4-argument emitter `(action, outcome, metadata,
|
|
1490
|
+
* extra?)`: `extra` is an object whose fields are merged onto the event, which
|
|
1491
|
+
* carries the only per-emit variations seen across the framework — `actor`
|
|
1492
|
+
* (constant `{ type: "system" }` for an unattended worker, or a per-request
|
|
1493
|
+
* `ctx.actor`) and `resource`. So a hand-rolled emitter with extra event fields
|
|
1494
|
+
* is never an exception — pass them through `extra`. `opts` is the gate flag for
|
|
1495
|
+
* the common case OR `{ audit, sink }`, where `sink` emits to an
|
|
1496
|
+
* operator-supplied audit object instead of the framework chain (the emitter is
|
|
1497
|
+
* a no-op if that sink has no `safeEmit`, matching the hand-rolled sink guard).
|
|
1498
|
+
*
|
|
1499
|
+
* A falsy `prefix` (`null` / `""`) builds the no-namespace variant: `action`
|
|
1500
|
+
* passes through verbatim (no `prefix + "."`). This serves the primitives whose
|
|
1501
|
+
* audit actions are already fully-qualified at the call site (`emitAudit(
|
|
1502
|
+
* "system.outbox.started", …)`) — the same gated drop-silent passthrough,
|
|
1503
|
+
* without re-homing the qualifier.
|
|
1504
|
+
*
|
|
1505
|
+
* @opts
|
|
1506
|
+
* audit: boolean, // false disables the emitter (default on); passing a bare boolean === { audit }
|
|
1507
|
+
* sink: object, // alternate audit target with a .safeEmit(event) (defaults to b.audit)
|
|
1508
|
+
*
|
|
1509
|
+
* @example
|
|
1510
|
+
* var emitAudit = b.audit.namespaced("gdpr.ropa", opts.audit);
|
|
1511
|
+
* emitAudit("activity_added", "success", { activityId: id });
|
|
1512
|
+
* // → safeEmit({ action: "gdpr.ropa.activity_added", outcome: "success",
|
|
1513
|
+
* // metadata: { activityId: id } })
|
|
1514
|
+
*
|
|
1515
|
+
* var emitGate = b.audit.namespaced("guardSql.gate");
|
|
1516
|
+
* emitGate("refused", "denied", { route: r }, { actor: ctx.actor }); // per-call actor
|
|
1517
|
+
*/
|
|
1518
|
+
function namespaced(prefix, opts) {
|
|
1519
|
+
// Back-compat: a bare boolean/undefined is the gate; an object carries the
|
|
1520
|
+
// gate plus the sink axis.
|
|
1521
|
+
var cfg = (opts && typeof opts === "object") ? opts : { audit: opts };
|
|
1522
|
+
var on = cfg.audit !== false;
|
|
1523
|
+
return function (action, outcome, metadata, extra) {
|
|
1524
|
+
if (!on) return;
|
|
1525
|
+
// module.exports.safeEmit (late-bound) so a test that stubs b.audit.safeEmit
|
|
1526
|
+
// still observes the emit; cfg.sink routes to an operator-supplied audit
|
|
1527
|
+
// object (no-op if it lacks safeEmit, matching the hand-rolled sink guard).
|
|
1528
|
+
var sink = cfg.sink || module.exports;
|
|
1529
|
+
if (!sink || typeof sink.safeEmit !== "function") return;
|
|
1530
|
+
var evt = { action: prefix ? prefix + "." + action : action, outcome: outcome, metadata: metadata || {} };
|
|
1531
|
+
if (extra) { for (var k in extra) { if (Object.prototype.hasOwnProperty.call(extra, k)) evt[k] = extra[k]; } }
|
|
1532
|
+
try { sink.safeEmit(evt); } catch (_e) { /* drop-silent — audit is best-effort */ }
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1448
1536
|
/**
|
|
1449
1537
|
* @primitive b.audit.flush
|
|
1450
1538
|
* @signature b.audit.flush()
|
|
@@ -1814,6 +1902,7 @@ module.exports = {
|
|
|
1814
1902
|
useStore: useStore,
|
|
1815
1903
|
emit: emit,
|
|
1816
1904
|
safeEmit: safeEmit,
|
|
1905
|
+
namespaced: namespaced,
|
|
1817
1906
|
applyPosture: applyPosture,
|
|
1818
1907
|
activePosture: activePosture,
|
|
1819
1908
|
bindActor: bindActor,
|