@blamejs/core 0.14.5 → 0.14.7
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/README.md +4 -2
- package/lib/agent-event-bus.js +4 -4
- package/lib/agent-idempotency.js +6 -6
- package/lib/agent-orchestrator.js +9 -9
- package/lib/agent-posture-chain.js +10 -10
- package/lib/agent-saga.js +6 -7
- package/lib/agent-snapshot.js +8 -8
- package/lib/agent-stream.js +3 -3
- package/lib/agent-tenant.js +4 -4
- package/lib/agent-trace.js +5 -5
- package/lib/ai-disclosure.js +3 -3
- package/lib/app.js +2 -2
- package/lib/archive-read.js +1 -1
- package/lib/archive-tar-read.js +1 -1
- package/lib/archive-wrap.js +5 -5
- package/lib/audit-tools.js +65 -5
- package/lib/audit.js +2 -2
- package/lib/auth/ciba.js +1 -1
- package/lib/auth/dpop.js +1 -1
- package/lib/auth/fal.js +1 -1
- package/lib/auth/fido-mds3.js +2 -3
- package/lib/auth/jwt-external.js +2 -2
- package/lib/auth/oauth.js +9 -9
- package/lib/auth/oid4vci.js +7 -7
- package/lib/auth/oid4vp.js +1 -1
- package/lib/auth/openid-federation.js +5 -5
- package/lib/auth/passkey.js +6 -6
- package/lib/auth/saml.js +1 -1
- package/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/backup/index.js +18 -18
- package/lib/cache.js +4 -4
- package/lib/calendar.js +5 -5
- package/lib/circuit-breaker.js +1 -1
- package/lib/cms-codec.js +2 -2
- package/lib/compliance.js +14 -14
- package/lib/cra-report.js +3 -3
- package/lib/crypto-field.js +58 -21
- package/lib/crypto.js +5 -6
- package/lib/db-query.js +131 -9
- package/lib/db.js +106 -22
- package/lib/external-db.js +64 -16
- package/lib/framework-schema.js +4 -4
- package/lib/guard-list-id.js +2 -2
- package/lib/guard-list-unsubscribe.js +1 -2
- package/lib/incident-report.js +150 -0
- package/lib/mail-crypto-smime.js +1 -1
- package/lib/mail-deploy.js +3 -3
- package/lib/mail-server-managesieve.js +2 -2
- package/lib/mail-server-pop3.js +2 -2
- package/lib/mail-store.js +1 -1
- package/lib/metrics.js +8 -8
- package/lib/middleware/age-gate.js +20 -7
- package/lib/middleware/bearer-auth.js +36 -35
- package/lib/middleware/bot-guard.js +17 -5
- package/lib/middleware/cors.js +28 -12
- package/lib/middleware/csrf-protect.js +23 -15
- package/lib/middleware/daily-byte-quota.js +27 -13
- package/lib/middleware/deny-response.js +140 -0
- package/lib/middleware/dpop.js +37 -24
- package/lib/middleware/fetch-metadata.js +21 -12
- package/lib/middleware/host-allowlist.js +19 -8
- package/lib/middleware/idempotency-key.js +21 -22
- package/lib/middleware/index.js +3 -0
- package/lib/middleware/network-allowlist.js +24 -10
- package/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/middleware/rate-limit.js +22 -5
- package/lib/middleware/require-aal.js +25 -10
- package/lib/middleware/require-auth.js +32 -16
- package/lib/middleware/require-bound-key.js +49 -18
- package/lib/middleware/require-content-type.js +19 -8
- package/lib/middleware/require-methods.js +17 -7
- package/lib/middleware/require-mtls.js +27 -14
- package/lib/network-dns-resolver.js +2 -2
- package/lib/network-dns.js +1 -2
- package/lib/network-tls.js +0 -1
- package/lib/network.js +4 -4
- package/lib/outbox.js +1 -1
- package/lib/pqc-agent.js +1 -1
- package/lib/retention.js +1 -1
- package/lib/retry.js +1 -1
- package/lib/safe-archive.js +2 -2
- package/lib/safe-ical.js +2 -2
- package/lib/safe-mime.js +1 -1
- package/lib/self-update-standalone-verifier.js +1 -1
- package/lib/self-update.js +2 -2
- package/lib/static.js +1 -1
- package/lib/subject.js +2 -2
- package/lib/vault/index.js +64 -1
- package/lib/vault/rotate.js +19 -0
- package/lib/vendor-data.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.14.x
|
|
10
10
|
|
|
11
|
+
- v0.14.7 (2026-05-30) — **Storage and audit-trail hardening: queries are gated to declared columns, raw SQL refuses embedded literals, the database key is bound to its location, sealed-column lookup hashes gain a keyed mode, audit-chain purges can require dual control, and breach deadlines ship a running clock.** This release tightens the data and audit layers against a set of failure modes that were previously reachable. Database queries are now checked against the columns a table declared in its schema: a reference to an undeclared column fails closed by default instead of silently matching nothing, and the `whereRaw` escape hatch refuses an embedded string literal so values bind through placeholders. The database encryption key is sealed with its purpose, data directory, and key path as additional authenticated data, so a key file cannot be relocated to another deployment and unsealed there; an older key without that binding upgrades itself on first load. Sealed-column equality-lookup hashes can now be computed as a keyed MAC (HMAC-SHAKE256) off a per-deployment key, making the lookup hash unforgeable without that key, while the salted-SHA3 default is unchanged. Purging the tamper-evident audit chain can be placed under a two-authorizer dual-control grant so one operator cannot erase it alone, and database credential-rejection audits now record which relation the rejected credential tried to reach. Finally, breach-notification deadlines get a running clock that raises approaching and passed alerts as each regime's window elapses. One behavior change to note: the column gate defaults to reject — if a service issues queries against columns it did not declare in its schema, set `db.init({ columnGate: "warn" })` (audited, allowed) or `"off"` while the schema is reconciled. **Added:** *Column-membership gate on every query* — `b.db.from(table)` now checks each referenced column against the table's declared schema. The mode is set with `db.init({ columnGate: "reject" | "warn" | "off" })` (default `reject`), and `query.allowedColumns([...])` narrows a single query to an explicit allowlist that is always enforced. `b.db.getDeclaredColumns(table)` returns a table's declared column names (or `null` for an unknown table). This is defense in depth against typo'd or caller-influenced column names reaching the SQL layer (CWE-89). · *Keyed mode for sealed-column lookup hashes* — Equality-lookup ("derived") hashes for sealed columns can be computed as `hmac-shake256` — a keyed MAC over a per-deployment key — instead of the default `salted-sha3`. Set it per table with `cryptoField.registerTable(name, { derivedHashMode: "hmac-shake256" })` or per column with `{ from, mode: "hmac-shake256" }`. The keyed hash is unforgeable and un-correlatable without the deployment's MAC key, which raises the bar against offline lookup-table attacks on low-entropy sealed values (CWE-916). `b.vault.getDerivedHashMacKey()` exposes the 32-byte per-deployment key; it is created on first use and re-sealed across key rotation automatically. · *Dual-control gate on audit-chain purge* — `b.auditTools.purge` accepts `dualControlGrant`. When `audit_log` is placed under dual control, a verified archive and `confirm: true` are no longer sufficient: the purge additionally requires a consumed m-of-n grant whose action is bound to the purge, so a grant minted for another operation cannot be replayed and one operator cannot erase the tamper-evident chain alone (NIST SP 800-53 AU-9, separation of duties). · *Running clock for breach-notification deadlines* — `b.incident.report.createDeadlineClock({ notify, approachThresholds })` tracks open incidents and raises `deadline_approaching` and `deadline_passed` alerts as each regime's window elapses (GDPR 72h, DORA, NIS2, and the rest of the registry). Alerts are deduplicated per incident and stage, suppressed once a submission stage is acknowledged, and the clock can run on an interval or be ticked manually. **Changed:** *Queries against undeclared columns now fail closed by default* — The column gate defaults to `reject`: a query that references a column the table did not declare throws rather than silently matching nothing. A service that intentionally queries undeclared columns can set `db.init({ columnGate: "warn" })` to audit and allow, or `"off"` to disable the gate, while its schema is reconciled. Framework-declared columns (including `_id` and derived-hash columns) are always members. **Security:** *Database encryption key bound to its location* — `db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A sealed key copied to a different deployment or path no longer unseals there — the AEAD authentication fails — which prevents silent key relocation. A legacy key sealed without this binding is detected and re-sealed in the bound format on first load, with no operator action required. · *`whereRaw` refuses embedded string literals* — `whereRaw(sql, params)` and `WhereBuilder.raw(sql, params)` reject a raw fragment containing a string literal (`'...'`); values must bind through the `params` array. A static, operator-controlled literal can opt in with `{ allowLiterals: true }`. This closes a path where a value concatenated into a raw fragment would reintroduce SQL injection (CWE-89). · *Credential-rejection audits record the attempted relation* — A `db.auth.failed` audit row (SQLSTATE 28000 / 28P01 / 42501) now carries `attemptedTable`, the relation the rejected credential tried to reach, extracted defensively from the statement. Triage can scope the blast radius of a credential-abuse event without correlating back to the raw SQL log (CWE-778). **Detectors:** *Audit-purge dual-control gate* — A new check fails the build if a call to `purgeAuditChain` appears in a file that does not also route through the dual-control gate, so a future caller cannot physically delete chain rows without two-authorizer enforcement. · *Raw-SQL literal/interpolation guard* — A new check fails the build on a `whereRaw` / `.raw` call whose SQL argument is built by template interpolation or string concatenation, keeping the bound-params discipline enforceable in framework code. · *Hand-rolled lookup-hash guard* — A new check fails the build if a sealed-column lookup hash is derived from the per-deployment salt outside the canonical helper, so call sites cannot bypass the keyed-mode and per-column mode policy. · *Auth-audit attempted-relation guard* — A new check fails the build if a `db.auth.failed` audit is emitted in a file that does not name `attemptedTable`, so the forensic field cannot be dropped from a future emitter.
|
|
12
|
+
|
|
13
|
+
- v0.14.6 (2026-05-30) — **Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable.** Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed. **Added:** *Uniform `onDeny` and `problemDetails` options on every access-refusal middleware* — Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`). **Fixed:** *`b.middleware.requireBoundKey` is now callable* — The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface. · *`b.middleware.bearerAuth` accepts `requiredScopes`* — The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option. · *RFC 6750 challenge codes on API-key refusals* — `b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal. · *Corrected two documented call paths* — The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths. · *GitHub Actions pins refreshed* — `github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases. **Detectors:** *`@primitive` reachability gate* — A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above. · *Deny-path composition gate* — A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`. · *Actions and vendor currency in the release flow* — The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release.
|
|
14
|
+
|
|
11
15
|
- v0.14.5 (2026-05-30) — **Finished cleaning up the mislabeled byte-literal lint suppressions, with no API or behavior changes.** A follow-up to the byte-literal lint tightening. The remaining suppression comments that named the byte-literal check on values that are not byte sizes — JSON-RPC error codes, HTTP status codes, octet ranges, day-in-milliseconds constants — are removed, keeping their explanatory text and any correctly-named companion suppression. Every byte-literal suppression that remains is now on genuine 1024-scale byte arithmetic. Source-comment hygiene only. **Changed:** *Remaining mislabeled byte-literal suppressions removed* — The byte-literal lint was previously a check on any multiple-of-8 integer, so suppression comments naming it were scattered across non-byte values. The last of those (in a handful of files, in mixed comment formats) are now removed — their explanatory text is retained as plain comments, and any correctly-named companion suppression is kept. The only byte-literal suppressions that remain are on genuine 1024-scale byte arithmetic. No change to any exported API, error code, wire format, or runtime behavior.
|
|
12
16
|
|
|
13
17
|
- v0.14.4 (2026-05-30) — **Removed three pieces of dead code from the SAML, TLS, and JMAP surfaces; no API or behavior changes.** Cleanup of unreachable code. A reverse signature-algorithm lookup in the SAML verifier was never called — the actual verification path resolves the algorithm through the supported-signature table — so it is removed and a stale comment that referenced it is corrected. A leftover no-op placeholder in the TLS certificate re-encode path (a zero-length slice that was assigned and discarded) is removed, leaving the verbatim extension re-encode it sat next to. An unused JMAP well-known-path constant that existed only to be discarded is removed. None of this changes any exported API, error code, wire format, or runtime behavior. **Removed:** *Unreachable code in SAML, TLS, and JMAP* — Removed `_sigAlgFromUri` from the SAML module (a reverse alg lookup that was never called — the embedded XML-DSig verifier resolves the algorithm via the supported-signature table, and the redirect-binding path uses the forward `_sigAlgUrn`), a discarded zero-length-slice placeholder in the TLS certificate extension re-encode path, and an unused well-known-path constant in the JMAP server. Internal cleanup only — no change to any exported API, error code, wire format, or runtime behavior.
|
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
61
61
|
### Data layer
|
|
62
62
|
|
|
63
63
|
- **SQLite with sealed-by-default columns** — `b.db`, migrations, seeders, atomic-file writes
|
|
64
|
-
- **Chainable query builder** — atomic `.increment(col, delta)`, closure-form `.whereGroup` / top-level `.orWhere` OR composition, `.search(fields, term)` LIKE-OR with safe `%`/`_` ESCAPE handling, `.paginate(opts)` returning `{ items, total, page, totalPages }`
|
|
64
|
+
- **Chainable query builder** — atomic `.increment(col, delta)`, closure-form `.whereGroup` / top-level `.orWhere` OR composition, `.search(fields, term)` LIKE-OR with safe `%`/`_` ESCAPE handling, `.paginate(opts)` returning `{ items, total, page, totalPages }`; a column-membership gate (`db.init({ columnGate })`, default reject) fails a query closed when it names a column the table never declared, and `whereRaw` refuses an embedded string literal so values bind through placeholders
|
|
65
65
|
- **Mongo-style document-store facade** — `b.db.collection(name, opts?)` with `$set` / `$inc` / `$unset` / `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte` / `$in` / `$like`; schemaless-document opts via `overflow: "<col>"` (folds unknown fields into a JSON-text column; rewrites `WHERE` on virtual fields to `JSON_EXTRACT`), `jsonColumns: [...]` (auto-stringify on write + parse via `b.safeJson` on read), `sealedFields: { email: "emailHash" }` (co-locates a `b.cryptoField` sealed-column / derived-hash declaration so plaintext lookups auto-rewrite to hash-column lookups)
|
|
66
66
|
- **DB lifecycle** — in-memory encrypted snapshot via `b.db.snapshot()`; standalone encrypted-DB-file lifecycle (`b.db.fileLifecycle({ dataDir, vault })` — decrypt-to-tmpfs, periodic re-encrypt flush, graceful shutdown — same envelope as `b.db`, no schema/audit-chain coupling); `db.init` opt-outs `frameworkTables: false` / `auditSigning: false` and path overrides `encryptedDbPath` / `encryptedDbName` / `dbKeyPath`
|
|
67
67
|
- **External RDBMS** — bring-your-own Postgres / MySQL with pool tuning + role-aware connect + read-replica routing (`b.externalDb`); declarative role-narrowed views and Postgres row-level-security migrations (`b.db.declareView`, `b.db.declareRowPolicy`)
|
|
@@ -98,7 +98,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
98
98
|
- **At-rest envelope** — envelope-versioned PQC (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256); vault sealing (`b.crypto`, `b.vault`)
|
|
99
99
|
- **Power-on self-test** — `b.crypto.selfTest()` runs FIPS 140-3-style integrity checks: NIST FIPS 202 known-answer tests (SHA3-256/512, SHAKE256), AEAD round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests; fails closed (throws) on any mismatch
|
|
100
100
|
- **Field-level + crypto-shred** — `b.cryptoField.eraseRow`; per-column data residency tagging + per-row keys (`K_row = HKDF(K_table, rowId)`) so erasing the per-row key makes WAL / replica residuals undecryptable (`b.cryptoField.declareColumnResidency`, `b.cryptoField.declarePerRowKey`)
|
|
101
|
-
- **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`)
|
|
101
|
+
- **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`). The database encryption key is sealed the same way — bound to its purpose, data directory, and key path — so a relocated key file fails to unseal; an older unbound key upgrades itself on first load
|
|
102
|
+
- **Keyed lookup hashes** — sealed-column equality-lookup hashes default to salted SHA3-512 and can opt into a keyed `hmac-shake256` MAC off a per-deployment key (`cryptoField.registerTable({ derivedHashMode })`, `b.vault.getDerivedHashMacKey`), making the lookup hash unforgeable and un-correlatable across deployments
|
|
102
103
|
- **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
|
|
103
104
|
- **HPKE / HTTP signatures** — RFC 9180 HPKE with ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 (`b.crypto.hpke`); RFC 9421 HTTP Message Signatures with derived components and ed25519 / ML-DSA-65 (`b.crypto.httpSig`); RFC 9530 Content-Digest / Repr-Digest body-integrity fields (SHA-256 / SHA-512, legacy algorithms refused — `b.contentDigest`) to sign the digest rather than the whole body
|
|
104
105
|
- **X-Wing hybrid KEM** — `b.crypto.xwing` (draft-connolly-cfrg-xwing-kem, experimental): ML-KEM-768 + X25519 bound by SHA3-256, secure if either component holds — the conservative key-encapsulation shape for migrating off classical ECDH. `keygen` / `encapsulate` / `decapsulate` with a 1216-byte public key, 1120-byte ciphertext, and 32-byte shared secret
|
|
@@ -129,6 +130,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
129
130
|
- CSRF protection — double-submit cookie + Origin/Referer cross-check; auto-skips Authorization-header / cookieless requests, which are not CSRF-able (`b.middleware.csrfProtect`)
|
|
130
131
|
- CORS (W3C Private Network Access preflight refusal default + `allowPrivateNetwork` opt) and rate-limit are wired when configured via `middleware.cors` / `middleware.rateLimit`
|
|
131
132
|
- `Cache-Control: no-store` on every 401 from `requireAuth` / `requireAal` / `requireStepUp` per RFC 9111 §5.2.2.5
|
|
133
|
+
- Every access-refusal layer takes a uniform `problemDetails: true` for an RFC 9457 `application/problem+json` body or `onDeny(req, res, info)` to render the refusal itself — so a service can standardize one error envelope across its API without working around hardcoded bodies (`b.problemDetails`)
|
|
132
134
|
- **Additional middleware** to mount in your `routes` callback: compression, SSE, request logging, request-time DB role binding (`b.middleware.dbRoleFor`), in-process CIDR fence (`b.middleware.networkAllowlist`)
|
|
133
135
|
- **Outbound HTTP client** — HTTP/1.1 + HTTP/2 with SSRF gate (cloud-metadata IPs hard-denied; private / loopback / link-local overridable per call); scheme + userinfo + per-host destination allowlist; redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`)
|
|
134
136
|
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag, plus `verifyDenial` for NSEC / NSEC3 (RFC 5155) NXDOMAIN / NODATA proofs with iteration caps + Opt-Out handling, plus `verifyChain` to validate a full root→TLD→zone delegation chain against the pinned IANA root anchors) so a resolver client can verify both positive and negative answers instead of trusting the upstream AD bit; DANE / TLSA certificate matching (RFC 6698/7671 — `b.network.dns.dane.matchCertificate`) to pin a service's key through DNSSEC instead of a public CA; TSIG transaction signatures (RFC 8945 — `b.network.dns.tsig.sign` / `verify`) for shared-key HMAC authentication of zone transfers, dynamic updates, and query/response pairs, with constant-time MAC compare + fudge-window check (verified against dnspython); outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
package/lib/agent-event-bus.js
CHANGED
|
@@ -136,7 +136,7 @@ function _registerTopic(topics, name, topicOpts, auditImpl) {
|
|
|
136
136
|
throw new AgentEventBusError("agent-event-bus/bad-schema",
|
|
137
137
|
"registerTopic: schema required (flat key→type map)");
|
|
138
138
|
}
|
|
139
|
-
//
|
|
139
|
+
// `kind` is now captured on register so listTopics's kind
|
|
140
140
|
// filter actually matches. Prior shape never set entry.kind, so the
|
|
141
141
|
// filter at args.kind was dead. Default value derives from the
|
|
142
142
|
// dotted topic name's first segment ("mail.scan.x" → "mail"), giving
|
|
@@ -164,7 +164,7 @@ function _registerTopic(topics, name, topicOpts, auditImpl) {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
//
|
|
167
|
+
// Operators reloading a module (test runners between
|
|
168
168
|
// runs, hot-reload tools, multi-tenant onboarding flows that
|
|
169
169
|
// register-deregister topics) need a clean unregister path; without
|
|
170
170
|
// it the second register throws topic-duplicate and the operator is
|
|
@@ -183,7 +183,7 @@ function _unregisterTopic(topics, name, auditImpl) {
|
|
|
183
183
|
function _listTopics(topics, args, permissions) {
|
|
184
184
|
// Permission gate: list-topics requires no special scope by default;
|
|
185
185
|
// operator can wrap with their own permissions instance for stricter.
|
|
186
|
-
//
|
|
186
|
+
// Kind filter now matches because register captures kind.
|
|
187
187
|
var out = [];
|
|
188
188
|
topics.forEach(function (entry) {
|
|
189
189
|
if (args.kind && entry.kind !== args.kind) return;
|
|
@@ -229,7 +229,7 @@ async function _publish(topics, pubsub, name, payload, pOpts, permissions, audit
|
|
|
229
229
|
}
|
|
230
230
|
// Schema validation.
|
|
231
231
|
guardEventBusPayload.validate(payload, entry.schema);
|
|
232
|
-
//
|
|
232
|
+
// When a topic is tenant-scoped, require the publisher
|
|
233
233
|
// to declare a tenantId BEFORE the event reaches the durable bus
|
|
234
234
|
// backend. Prior shape allowed `wrapped._tenantId: null` to land on
|
|
235
235
|
// the bus, and the receive-side drop only fired AFTER persistence —
|
package/lib/agent-idempotency.js
CHANGED
|
@@ -158,7 +158,7 @@ function create(opts) {
|
|
|
158
158
|
return {
|
|
159
159
|
get: function (method, actorId, key) { return _get(store, method, actorId, key, auditImpl, ttlMs, maxResultBytes); },
|
|
160
160
|
put: function (method, actorId, key, result, putOpts) { return _put(store, method, actorId, key, result, putOpts || {}, ttlMs, maxResultBytes, fingerprintArgs, auditImpl); },
|
|
161
|
-
//
|
|
161
|
+
// putIfAbsent gates concurrent retries at the cache
|
|
162
162
|
// boundary so only one consumer runs the handler. Operator wraps:
|
|
163
163
|
// var claim = await idem.putIfAbsent(method, actor, key, args);
|
|
164
164
|
// if (claim.alreadyClaimed) return claim.result; // another retry won
|
|
@@ -173,7 +173,7 @@ function create(opts) {
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
//
|
|
176
|
+
// Atomic claim/check/run pattern. Returns one of:
|
|
177
177
|
// { alreadyClaimed: false, fingerprint } — caller runs the handler
|
|
178
178
|
// { alreadyClaimed: true, pending: true } — another in-flight claim holds the slot
|
|
179
179
|
// { alreadyClaimed: true, result: <cached> } — prior handler completed; cached result
|
|
@@ -299,7 +299,7 @@ async function _get(store, method, actorId, key, auditImpl, ttlMs, maxResultByte
|
|
|
299
299
|
throw new AgentIdempotencyError("agent-idempotency/corrupt-result",
|
|
300
300
|
"get: cached result failed to parse — " + (e && e.message ? e.message : String(e)));
|
|
301
301
|
}
|
|
302
|
-
//
|
|
302
|
+
// Atomic replayCount increment. The prior shape
|
|
303
303
|
// (read row → mutate → put row) raced two concurrent retries: each
|
|
304
304
|
// saw replayCount=N, both wrote replayCount=N+1, so the counter
|
|
305
305
|
// missed bumps and the put-with-fresh-result race-clobbered prior
|
|
@@ -439,7 +439,7 @@ function _fingerprintArgs(args) {
|
|
|
439
439
|
// unique fingerprint and defeat the args-mismatch defense. Strip
|
|
440
440
|
// _traceContext (varies per-hop, doesn't change result).
|
|
441
441
|
//
|
|
442
|
-
//
|
|
442
|
+
// DO NOT strip _postureChain. The prior shape ignored
|
|
443
443
|
// _postureChain.postureSet, so a request made under
|
|
444
444
|
// postureSet:["hipaa","pci-dss"] cached the same result that a
|
|
445
445
|
// downgrade attempt under postureSet:["pci-dss"] would replay
|
|
@@ -493,7 +493,7 @@ function _inMemoryBackend(maxEntries) {
|
|
|
493
493
|
map.set(_k(method, actorId, hash), row);
|
|
494
494
|
return Promise.resolve();
|
|
495
495
|
},
|
|
496
|
-
//
|
|
496
|
+
// Atomic insert. Map.set is synchronous so the
|
|
497
497
|
// get+set pair below is naturally race-free within the in-memory
|
|
498
498
|
// backend (V8 single-threaded). Returns true when inserted, false
|
|
499
499
|
// when the row already exists.
|
|
@@ -503,7 +503,7 @@ function _inMemoryBackend(maxEntries) {
|
|
|
503
503
|
map.set(k, row);
|
|
504
504
|
return Promise.resolve(true);
|
|
505
505
|
},
|
|
506
|
-
//
|
|
506
|
+
// Atomic replayCount increment. Operators wiring
|
|
507
507
|
// a SQL backend implement this with `UPDATE ... SET
|
|
508
508
|
// replay_count = replay_count + 1 WHERE keyHash = $1 RETURNING *`
|
|
509
509
|
// — read-modify-write race-free. In-memory backend is naturally
|
|
@@ -119,7 +119,7 @@ function _unsealRegistryRow(row) {
|
|
|
119
119
|
var DEFAULT_DRAIN_TIMEOUT_MS = C.TIME.minutes(2);
|
|
120
120
|
var STREAM_ID_RAND_BYTES = 8; // stream-id random-suffix byte length, not a size cap
|
|
121
121
|
var DEFAULT_PER_CONSUMER_STOP_MS = C.TIME.seconds(5);
|
|
122
|
-
//
|
|
122
|
+
// FNV-1a offset basis salted with the first 32 bits of
|
|
123
123
|
// SHA3-512(vault master). Attackers who don't have read access to the
|
|
124
124
|
// vault keypair can't compute the salt, so they can't engineer
|
|
125
125
|
// tenantIds that all map to one shard. Cached per-process; rotation
|
|
@@ -183,7 +183,7 @@ function create(opts) {
|
|
|
183
183
|
// operator-supplied metadata (kind / tenantId / posture / ...);
|
|
184
184
|
// every consuming process holds its own runtime map of name → agent.
|
|
185
185
|
liveAgents: new Map(),
|
|
186
|
-
//
|
|
186
|
+
// Drain quiesce wiring. Operator passes
|
|
187
187
|
// { outbox, sagaInFlightCount, pubsubFlush } via create() so the
|
|
188
188
|
// drain phase can quiesce real in-flight work, not just stop
|
|
189
189
|
// consumers. Optional — operators with no outbox / saga / pubsub
|
|
@@ -192,7 +192,7 @@ function create(opts) {
|
|
|
192
192
|
sagaInFlightCount: typeof opts.sagaInFlightCount === "function" ? opts.sagaInFlightCount : null,
|
|
193
193
|
pubsubFlush: typeof opts.pubsubFlush === "function" ? opts.pubsubFlush : null,
|
|
194
194
|
perConsumerStopMs: typeof opts.perConsumerStopMs === "number" ? opts.perConsumerStopMs : DEFAULT_PER_CONSUMER_STOP_MS,
|
|
195
|
-
//
|
|
195
|
+
// onTransition handler invalidates election cache
|
|
196
196
|
// on lease-lost / acquired / released. Operator opts out via
|
|
197
197
|
// { cacheElections: false } to always re-query b.cluster.
|
|
198
198
|
cacheElections: opts.cacheElections !== false,
|
|
@@ -205,7 +205,7 @@ function create(opts) {
|
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
//
|
|
208
|
+
// Subscribe to cluster lease transitions so cached
|
|
209
209
|
// election state can't go stale after a partition. b.cluster
|
|
210
210
|
// .onTransition fires for every lease-acquired / lease-lost / lease-
|
|
211
211
|
// released event; we invalidate the affected resource's cached
|
|
@@ -254,7 +254,7 @@ function create(opts) {
|
|
|
254
254
|
* @status stable
|
|
255
255
|
* @related b.agent.orchestrator.create
|
|
256
256
|
*
|
|
257
|
-
*
|
|
257
|
+
* Attach an in-process live agent reference to a row
|
|
258
258
|
* that already exists in the persistent registry backend. The
|
|
259
259
|
* canonical boot-phase contract: the *first* process to start a new
|
|
260
260
|
* agent calls `register()` (writes the backend row + holds the live
|
|
@@ -499,7 +499,7 @@ function _spawnSingleConsumer(ctx, agent, queue, topic, maxConcurrency) {
|
|
|
499
499
|
* // → integer in [0, 8)
|
|
500
500
|
*/
|
|
501
501
|
function _saltedFnvBasis() {
|
|
502
|
-
//
|
|
502
|
+
// Salt FNV-1a offset basis with the vault master so
|
|
503
503
|
// an attacker can't engineer tenantIds that all hash to one shard.
|
|
504
504
|
// Vault-less path (single-process tests / dev) falls back to the
|
|
505
505
|
// standard FNV offset basis; production deployments with vault
|
|
@@ -561,7 +561,7 @@ async function _elect(ctx, args) {
|
|
|
561
561
|
});
|
|
562
562
|
return elec;
|
|
563
563
|
}
|
|
564
|
-
//
|
|
564
|
+
// Cluster mode: ALWAYS query truth from b.cluster.
|
|
565
565
|
// The onTransition handler installed in create() invalidates the
|
|
566
566
|
// cache on every lease event, so a cache hit here is safe (it
|
|
567
567
|
// means no lease event has fired since the last query). But the
|
|
@@ -600,10 +600,10 @@ async function _drain(ctx, args) {
|
|
|
600
600
|
var drained = 0;
|
|
601
601
|
var startedAt = Date.now();
|
|
602
602
|
var perConsumerMs = ctx.perConsumerStopMs;
|
|
603
|
-
//
|
|
603
|
+
// Drain phases:
|
|
604
604
|
// 1. set ctx.draining so streams emit drain-markers + new task
|
|
605
605
|
// dispatches refuse (consumers re-check on every envelope).
|
|
606
|
-
// 2. stop each consumer with
|
|
606
|
+
// 2. stop each consumer with a per-consumer timeout race —
|
|
607
607
|
// one hung consumer can't block the full drain budget.
|
|
608
608
|
// 3. quiesce in-flight: poll outbox.pendingCount + sagaInFlightCount
|
|
609
609
|
// until 0 OR remaining-budget-ms elapses.
|
|
@@ -65,7 +65,7 @@ var AgentPostureChainError = defineClass("AgentPostureChainError", { alwaysPerma
|
|
|
65
65
|
|
|
66
66
|
var BUILTIN_REGIMES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// Envelope MAC vocabulary. Cross-process envelope
|
|
69
69
|
// integrity: an attacker with queue / event-bus write access who
|
|
70
70
|
// strips postureSet to [] and re-sends a saga / sub-agent envelope
|
|
71
71
|
// can bypass the downgrade refusal in _validate (which only checks
|
|
@@ -73,7 +73,7 @@ var BUILTIN_REGIMES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
|
|
|
73
73
|
// envelope bytes, computed at appendHop and verified at validate.
|
|
74
74
|
var ENVELOPE_MAC_LABEL = "blamejs.agent.postureChain/v1";
|
|
75
75
|
var ENVELOPE_MAC_KEY_BYTES = 32; // HMAC-SHA3-512 keyed bytes
|
|
76
|
-
//
|
|
76
|
+
// Hop count cap defends infinite recursion across
|
|
77
77
|
// agent delegation. 16 is the spec default; operators can lower via
|
|
78
78
|
// opts.maxHopCount but never raise (audit fan-out without a cap is a
|
|
79
79
|
// DoS class).
|
|
@@ -111,7 +111,7 @@ function _resolveMacKey() {
|
|
|
111
111
|
|
|
112
112
|
function _envelopeMacBytes(envelope) {
|
|
113
113
|
// Sign every field that downstream consumers verify off the wire,
|
|
114
|
-
// except the `_mac` field itself.
|
|
114
|
+
// except the `_mac` field itself. Also includes
|
|
115
115
|
// hopCount + chainTrail so a hostile rewriter can't roll back the
|
|
116
116
|
// trail to evade the cap.
|
|
117
117
|
var payload = {
|
|
@@ -163,8 +163,8 @@ function create(opts) {
|
|
|
163
163
|
opts.maxHopCount <= DEFAULT_MAX_HOP_COUNT
|
|
164
164
|
? Math.floor(opts.maxHopCount)
|
|
165
165
|
: DEFAULT_MAX_HOP_COUNT;
|
|
166
|
-
//
|
|
167
|
-
//
|
|
166
|
+
// Escape hatch — only single-process unit tests should opt out of
|
|
167
|
+
// envelope MAC. Production / multi-
|
|
168
168
|
// process / queue-spanning deployments leave the default on; the
|
|
169
169
|
// gate audit-emits when bypassed so the posture is visible.
|
|
170
170
|
var requireMac = opts.requireMac !== false;
|
|
@@ -257,7 +257,7 @@ function _appendHop(ctx, envelope, hopName) {
|
|
|
257
257
|
"appendHop: hopName must be a non-empty string");
|
|
258
258
|
}
|
|
259
259
|
var trail = Array.isArray(envelope.chainTrail) ? envelope.chainTrail.slice() : [];
|
|
260
|
-
//
|
|
260
|
+
// Cap enforced BEFORE the push so the hop-cap throw
|
|
261
261
|
// fires consistently regardless of whether the operator inspects
|
|
262
262
|
// trail.length first. Cap is a hard refusal (no truncation) because
|
|
263
263
|
// a silently-dropped hop loses audit provenance for the call.
|
|
@@ -279,8 +279,8 @@ function _appendHop(ctx, envelope, hopName) {
|
|
|
279
279
|
hopCount: trail.length,
|
|
280
280
|
});
|
|
281
281
|
guardPostureChain.validate(newEnvelope);
|
|
282
|
-
//
|
|
283
|
-
// ctx.requireMac=false (
|
|
282
|
+
// Sign at every hop. Verify-side enforces requireMac.
|
|
283
|
+
// ctx.requireMac=false (test escape hatch) skips
|
|
284
284
|
// the sign so a vault-less test path still works.
|
|
285
285
|
if (ctx.requireMac) {
|
|
286
286
|
try {
|
|
@@ -301,7 +301,7 @@ function _appendHop(ctx, envelope, hopName) {
|
|
|
301
301
|
|
|
302
302
|
function _validate(ctx, envelope, agentPostureSet) {
|
|
303
303
|
guardPostureChain.validate(envelope);
|
|
304
|
-
//
|
|
304
|
+
// MAC verification BEFORE any field-based decision so
|
|
305
305
|
// the wire-rewrite attack (postureSet:[] downgrade with valid SHAPE
|
|
306
306
|
// but no integrity binding) is refused. ctx.requireMac=false skips
|
|
307
307
|
// verification and emits an audit so the bypass is visible.
|
|
@@ -326,7 +326,7 @@ function _validate(ctx, envelope, agentPostureSet) {
|
|
|
326
326
|
chainTrail: envelope.chainTrail,
|
|
327
327
|
});
|
|
328
328
|
}
|
|
329
|
-
//
|
|
329
|
+
// Hop cap also enforced at validate-time. A hostile
|
|
330
330
|
// envelope might arrive with hopCount > cap if a prior hop's
|
|
331
331
|
// requireMac was off; refuse here regardless.
|
|
332
332
|
if (Array.isArray(envelope.chainTrail) && envelope.chainTrail.length > ctx.maxHopCount) {
|
package/lib/agent-saga.js
CHANGED
|
@@ -58,8 +58,7 @@
|
|
|
58
58
|
*
|
|
59
59
|
* ## No saga-level retry
|
|
60
60
|
*
|
|
61
|
-
*
|
|
62
|
-
* 2026-05-14): saga's value-add is compensation, not retry. If a
|
|
61
|
+
* Saga's value-add is compensation, not retry. If a
|
|
63
62
|
* step needs retry-with-backoff, the operator wraps `step.run`
|
|
64
63
|
* with `b.retry` inside the step body. With v0.9.22 idempotency
|
|
65
64
|
* available, internal retry inside step.run is side-effect-safe.
|
|
@@ -109,7 +108,7 @@ var SAGA_ID_RAND_BYTES = 8;
|
|
|
109
108
|
function create(config) {
|
|
110
109
|
guardSagaConfig.validate(config);
|
|
111
110
|
var auditImpl = config.audit || audit();
|
|
112
|
-
//
|
|
111
|
+
// Operator wires a stateStore for crash-safe resume.
|
|
113
112
|
// Interface: { saveStep, loadResumePoint, markCompleted, markFailed }.
|
|
114
113
|
// saveStep({sagaId, stepIndex, stepName, state, status}) commits
|
|
115
114
|
// after each step.run; loadResumePoint(sagaId) returns the resume
|
|
@@ -189,7 +188,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
|
|
|
189
188
|
});
|
|
190
189
|
await step.run(ctx, state);
|
|
191
190
|
completedSteps.push({ step: step, index: i });
|
|
192
|
-
//
|
|
191
|
+
// Checkpoint after the step.run returns. saveStep
|
|
193
192
|
// commits the post-step state so a crash before the NEXT step
|
|
194
193
|
// resumes from i+1. The audit chain records the checkpoint
|
|
195
194
|
// independently of the operator's stateStore — operator can
|
|
@@ -232,7 +231,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
|
|
|
232
231
|
message: (stepErr && stepErr.message) || String(stepErr),
|
|
233
232
|
});
|
|
234
233
|
var compensationError = null;
|
|
235
|
-
//
|
|
234
|
+
// Capture the compensation step that ACTUALLY failed,
|
|
236
235
|
// not "completedSteps[completedSteps.length-1].name" which
|
|
237
236
|
// names the last-COMPLETED step regardless of which compensation
|
|
238
237
|
// threw. CWE-209-adjacent (information disclosure via wrong
|
|
@@ -278,7 +277,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
|
|
|
278
277
|
});
|
|
279
278
|
} catch (_e) { /* drop-silent — audit already records */ }
|
|
280
279
|
}
|
|
281
|
-
//
|
|
280
|
+
// Attach cause:stepErr so the original step
|
|
282
281
|
// error stack survives. ES2022 Error.cause is the standard
|
|
283
282
|
// mechanism; the framework's defineClass-built AgentSagaError
|
|
284
283
|
// accepts cause via the third arg.
|
|
@@ -290,7 +289,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
|
|
|
290
289
|
((compensationError.message) || String(compensationError));
|
|
291
290
|
}
|
|
292
291
|
var sagaErr = new AgentSagaError("agent-saga/failed", detailMsg);
|
|
293
|
-
//
|
|
292
|
+
// ES2022 Error.cause attaches the originating
|
|
294
293
|
// stepErr so operator stack-trace tooling can walk the chain.
|
|
295
294
|
// defineClass({alwaysPermanent:true}) doesn't accept cause in
|
|
296
295
|
// its constructor signature; the property assignment after
|
package/lib/agent-snapshot.js
CHANGED
|
@@ -60,7 +60,7 @@ var vault = lazyRequire(function () { return require("./vault");
|
|
|
60
60
|
|
|
61
61
|
var AgentSnapshotError = defineClass("AgentSnapshotError", { alwaysPermanent: true });
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// Sealed envelopes start with this prefix on disk; the
|
|
64
64
|
// loader sniffs it and routes through unseal before guardSnapshotEnvelope
|
|
65
65
|
// validation. Compatible with operator backends that store the value
|
|
66
66
|
// as a string (JSON DBs, k/v stores) or wrap it in `{ value: "..." }`.
|
|
@@ -113,19 +113,19 @@ function create(opts) {
|
|
|
113
113
|
var snapshotIntervalMs = typeof policy.snapshotIntervalMs === "number" ? policy.snapshotIntervalMs : DEFAULT_SNAPSHOT_INTERVAL_MS;
|
|
114
114
|
var maxSnapshotBytes = typeof policy.maxSnapshotBytes === "number" ? policy.maxSnapshotBytes : DEFAULT_MAX_SNAPSHOT_BYTES;
|
|
115
115
|
var auditImpl = opts.audit || audit();
|
|
116
|
-
//
|
|
116
|
+
// Operator may inject `signer` (interface
|
|
117
117
|
// `{ sign(bytes) → Buffer, verify(bytes, sig, pubKey?) → boolean }`)
|
|
118
118
|
// for testing / alternate key custody. Default = b.auditSign when
|
|
119
119
|
// initialized at boot; refuses persist() with a clear error if
|
|
120
120
|
// neither is wired so secure-by-default holds.
|
|
121
121
|
var signer = opts.signer || null;
|
|
122
|
-
//
|
|
122
|
+
// Operator may inject `sealer` (interface
|
|
123
123
|
// `{ seal(plaintext, aadParts) → string, unseal(value, aadParts) → string }`)
|
|
124
124
|
// for alternate KMS integration. Default = b.vault.aad. Refused if
|
|
125
125
|
// neither is wired AND opts.allowPlaintext is not explicitly true
|
|
126
126
|
// (operator-justified dev / single-tenant deployments only).
|
|
127
127
|
var sealer = opts.sealer || null;
|
|
128
|
-
//
|
|
128
|
+
// Operator-supplied restoreHandlers walk the
|
|
129
129
|
// snapshot inFlight + idempotencyCache + orchestratorState segments
|
|
130
130
|
// and hydrate the corresponding consumer module. Map shape:
|
|
131
131
|
// { streams, sagas, outboxJobs, busSubscribers, pendingDeliveries,
|
|
@@ -300,7 +300,7 @@ async function _takeSnapshot(ctx, snapshotOpts) {
|
|
|
300
300
|
|
|
301
301
|
async function _persist(ctx, snap) {
|
|
302
302
|
guardSnapshotEnvelope.validate(snap);
|
|
303
|
-
//
|
|
303
|
+
// Sign first so a backend that mutates on put() (very
|
|
304
304
|
// common for k/v stores adding metadata) doesn't poison the signed
|
|
305
305
|
// bytes downstream readers verify.
|
|
306
306
|
var signer = _resolveSigner(ctx);
|
|
@@ -314,7 +314,7 @@ async function _persist(ctx, snap) {
|
|
|
314
314
|
// pubkey at verify time).
|
|
315
315
|
snap.sigPubKey = (typeof signer.getPublicKey === "function" && signer.getPublicKey()) || null;
|
|
316
316
|
|
|
317
|
-
//
|
|
317
|
+
// Seal the entire envelope under AAD that pins
|
|
318
318
|
// snapshotId + schemaVersion + tenantId. AAD mismatch on unseal (a
|
|
319
319
|
// copy-paste attack from one snapshotId's row into another) fails
|
|
320
320
|
// the Poly1305 tag check; tampered bytes also fail. The sealed
|
|
@@ -405,7 +405,7 @@ async function _unwrapAndVerify(ctx, raw, expectedId) {
|
|
|
405
405
|
throw new AgentSnapshotError("agent-snapshot/snapshot-id-mismatch",
|
|
406
406
|
"load: wrapper snapshotId='" + expectedId + "' does not match envelope='" + snap.snapshotId + "'");
|
|
407
407
|
}
|
|
408
|
-
//
|
|
408
|
+
// Verify the signature before returning the envelope
|
|
409
409
|
// to the caller. Restore-side trust derives from this gate. The
|
|
410
410
|
// allowPlaintext escape hatch (operator-acknowledged dev mode)
|
|
411
411
|
// also waives signature verification because there's no key custody
|
|
@@ -496,7 +496,7 @@ async function _restore(ctx, snap, restoreOpts) {
|
|
|
496
496
|
affectedStreams: (snap.inFlight && snap.inFlight.streams || []).length,
|
|
497
497
|
});
|
|
498
498
|
}
|
|
499
|
-
//
|
|
499
|
+
// Invoke operator-supplied restoreHandlers across
|
|
500
500
|
// every segment the snapshot envelope carries. Handlers are
|
|
501
501
|
// declared at create() time; the snapshot primitive owns ordering
|
|
502
502
|
// (orchestratorState first so live agents register before consumers
|
package/lib/agent-stream.js
CHANGED
|
@@ -138,7 +138,7 @@ function _makeIterator(ctx) {
|
|
|
138
138
|
var done = false;
|
|
139
139
|
var closed = false;
|
|
140
140
|
var drained = false;
|
|
141
|
-
//
|
|
141
|
+
// Track the cursor of the LAST row actually yielded
|
|
142
142
|
// to the consumer. The prior shape called cursor.lastSeenCursor()
|
|
143
143
|
// at drain-marker emit, which returned the position of the last
|
|
144
144
|
// FETCHED batch — clients resuming from that cursor SKIPPED every
|
|
@@ -167,7 +167,7 @@ function _makeIterator(ctx) {
|
|
|
167
167
|
try {
|
|
168
168
|
if (buffer.length > 0) {
|
|
169
169
|
var row = buffer.shift();
|
|
170
|
-
//
|
|
170
|
+
// Record the cursor for this yielded row so a
|
|
171
171
|
// drain that fires BETWEEN buffered yields emits a marker
|
|
172
172
|
// whose lastSeenCursor matches what the client actually
|
|
173
173
|
// received. The cursor extraction shape mirrors the
|
|
@@ -248,7 +248,7 @@ function _safeAudit(auditImpl, action, actor, metadata) {
|
|
|
248
248
|
agentAudit.safeAudit(auditImpl, action, actor, metadata);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
//
|
|
251
|
+
// Resolve the resume cursor for a row about to be
|
|
252
252
|
// yielded. Operators may attach the cursor per-row (`row._cursor` /
|
|
253
253
|
// `row.cursor`) OR rely on the cursor's own per-row tracker
|
|
254
254
|
// (`cursor.cursorForRow(row)`) — both shapes supported.
|
package/lib/agent-tenant.js
CHANGED
|
@@ -222,7 +222,7 @@ async function _unregister(ctx, tenantId, args) {
|
|
|
222
222
|
}
|
|
223
223
|
// Archive default — retain the key + metadata for retention-mandated
|
|
224
224
|
// restoration. Operator's compliance regime drives archivePolicy.
|
|
225
|
-
//
|
|
225
|
+
// Persist as a `status: "archived"` row in the same
|
|
226
226
|
// backend rather than only the process-local Map. GDPR Art. 17 +
|
|
227
227
|
// HIPAA §164.530(j) require the archived state to survive process
|
|
228
228
|
// restart (auditor pulls a deleted tenant's archival record years
|
|
@@ -373,7 +373,7 @@ function _check(ctx, actor, agentTenantId) {
|
|
|
373
373
|
|
|
374
374
|
// ---- Per-tenant derived key -----------------------------------------------
|
|
375
375
|
//
|
|
376
|
-
//
|
|
376
|
+
// `namespaceHash(label, tenantId)` is a PUBLIC function
|
|
377
377
|
// over PUBLIC inputs; an attacker who learns `tenantId` (an account id
|
|
378
378
|
// surfaced in URLs / API responses) reconstructs every per-tenant key
|
|
379
379
|
// without any secret material. The defense the docstring promises —
|
|
@@ -555,7 +555,7 @@ var TENANT_FIELD_PREFIX = "tnt-v1:";
|
|
|
555
555
|
function _tenantFieldKey(tenantId, table) {
|
|
556
556
|
// 32-byte symmetric key for XChaCha20-Poly1305. _deriveTenantKeyBytes
|
|
557
557
|
// returns the raw key bound to the vault master + tenantId + purpose
|
|
558
|
-
// — see
|
|
558
|
+
// — see the commentary above _derivedKey for the threat
|
|
559
559
|
// model that drove this away from public-input-only derivation.
|
|
560
560
|
return _deriveTenantKeyBytes(tenantId, "cryptoField:" + table);
|
|
561
561
|
}
|
|
@@ -646,7 +646,7 @@ function _unsealRowForTenant(ctx, tenantId, table, row) {
|
|
|
646
646
|
if (out[f] !== undefined && out[f] !== null) {
|
|
647
647
|
try { out[f] = _unsealField(tenantId, table, f, out[f]); }
|
|
648
648
|
catch (e) {
|
|
649
|
-
//
|
|
649
|
+
// Null-on-decrypt-failure was silent; the docstring
|
|
650
650
|
// promised "audit chain surfaces the failure" but no emit ever
|
|
651
651
|
// ran. Cross-tenant ciphertext replay / tampered row / wrong-
|
|
652
652
|
// prefix all hit this path; operator audit pipelines need the
|
package/lib/agent-trace.js
CHANGED
|
@@ -70,7 +70,7 @@ var audit = lazyRequire(function () { return require("./audit"); })
|
|
|
70
70
|
|
|
71
71
|
var AgentTraceError = defineClass("AgentTraceError", { alwaysPermanent: true });
|
|
72
72
|
|
|
73
|
-
//
|
|
73
|
+
// Once-per-process audit emit on the first tracer
|
|
74
74
|
// failure each install fires. Operators get the signal even when
|
|
75
75
|
// individual span calls are best-effort suppressed.
|
|
76
76
|
var _failureAuditEmittedFor = Object.create(null);
|
|
@@ -122,7 +122,7 @@ function create(opts) {
|
|
|
122
122
|
injectIntoEnvelope: function (envelope, span) { return _injectIntoEnvelope(opts.tracing, envelope, span); },
|
|
123
123
|
extractFromEnvelope: function (envelope) { return _extractFromEnvelope(envelope); },
|
|
124
124
|
recordResult: function (span, result, error) { return _recordResult(span, result, error, auditImpl); },
|
|
125
|
-
//
|
|
125
|
+
// `shouldSample` now takes a traceId so the same
|
|
126
126
|
// trace gets the same decision across hops. Operator-supplied
|
|
127
127
|
// traceId comes from the W3C `traceparent` header at request-
|
|
128
128
|
// entry; absent that, falls back to Math.random (start of trace).
|
|
@@ -150,7 +150,7 @@ function _startSpan(tracing, name, sopts, auditImpl) {
|
|
|
150
150
|
return tracing.startSpan(name, sopts);
|
|
151
151
|
}
|
|
152
152
|
} catch (e) {
|
|
153
|
-
//
|
|
153
|
+
// Tracer failures should not crash the agent's
|
|
154
154
|
// method call; surface the first failure to the audit chain
|
|
155
155
|
// (rate-limited) so operators get the signal.
|
|
156
156
|
_emitFirstFailureAudit(auditImpl, "startSpan", e && e.message);
|
|
@@ -206,7 +206,7 @@ function _extractFromEnvelope(envelope) {
|
|
|
206
206
|
|
|
207
207
|
function _recordResult(span, result, error, auditImpl) {
|
|
208
208
|
if (!span || typeof span !== "object") return;
|
|
209
|
-
//
|
|
209
|
+
// Surface first occurrence of each span-op failure
|
|
210
210
|
// via audit so the operator gets the signal. Subsequent failures
|
|
211
211
|
// stay silent (best-effort) per the operational spec.
|
|
212
212
|
if (error) {
|
|
@@ -228,7 +228,7 @@ function _recordResult(span, result, error, auditImpl) {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
//
|
|
231
|
+
// Deterministic sampling per W3C Trace Context §3.2.3.1.
|
|
232
232
|
// `Math.random` makes child-vs-parent sampling decisions non-coherent:
|
|
233
233
|
// a parent span sampled OUT can still have child spans sampled IN,
|
|
234
234
|
// producing orphaned spans operators can't correlate. Hashing the
|
package/lib/ai-disclosure.js
CHANGED
|
@@ -133,7 +133,7 @@ function chatbot(session, opts) {
|
|
|
133
133
|
var text = typeof opts.text === "string" && opts.text.length > 0
|
|
134
134
|
? opts.text
|
|
135
135
|
: DEFAULT_CHATBOT_TEXT;
|
|
136
|
-
//
|
|
136
|
+
// "on-request" placement gates on
|
|
137
137
|
// the operator's explicit `opts.requested: true` signal. Without
|
|
138
138
|
// it, "on-request" collapsed into "always" semantics and emitted
|
|
139
139
|
// every call. The operator wires this from an explicit user
|
|
@@ -155,7 +155,7 @@ function chatbot(session, opts) {
|
|
|
155
155
|
regulation: "Regulation (EU) 2024/1689",
|
|
156
156
|
};
|
|
157
157
|
if (shouldEmit) {
|
|
158
|
-
//
|
|
158
|
+
// Mark the session so subsequent
|
|
159
159
|
// calls with the same session under "first-message" placement
|
|
160
160
|
// see `aiDisclosureEmitted: true` and return shouldEmit=false.
|
|
161
161
|
// Without this mutation operators had to remember to flip the
|
|
@@ -385,7 +385,7 @@ function applyAll(scenario) {
|
|
|
385
385
|
"applyAll: scenario.kinds must be a non-empty array of " +
|
|
386
386
|
"\"chatbot\" / \"deepfake\" / \"emotion\"");
|
|
387
387
|
}
|
|
388
|
-
//
|
|
388
|
+
// Validate every kind +
|
|
389
389
|
// per-kind required field UP FRONT, before any emission.
|
|
390
390
|
// Previously a later-kind failure (e.g. deepfake missing
|
|
391
391
|
// contentType, unknown trailing kind) ran AFTER earlier kinds
|
package/lib/app.js
CHANGED
|
@@ -113,7 +113,7 @@ function _resolveMiddlewareOpt(value, allowDefault, name) {
|
|
|
113
113
|
if (value === false) {
|
|
114
114
|
// Operator explicitly disabled this middleware. When it's one of the
|
|
115
115
|
// security-on-by-default layers (allowDefault), leave an audit trace
|
|
116
|
-
// so the weakened posture is visible —
|
|
116
|
+
// so the weakened posture is visible — security defaults
|
|
117
117
|
// shouldn't be silently opt-out-able. Drop-silent observability sink.
|
|
118
118
|
if (allowDefault && name) {
|
|
119
119
|
try {
|
|
@@ -227,7 +227,7 @@ async function createApp(opts) {
|
|
|
227
227
|
var rateLimitOpts = _resolveMiddlewareOpt(mwConfig.rateLimit, false, "rateLimit");
|
|
228
228
|
if (rateLimitOpts) router.use(middleware.rateLimit(rateLimitOpts));
|
|
229
229
|
|
|
230
|
-
// Security middleware wired ON by default
|
|
230
|
+
// Security middleware wired ON by default. Each reads its
|
|
231
231
|
// config from opts.middleware.<name>: pass `false` to opt out (audited
|
|
232
232
|
// via _resolveMiddlewareOpt), or an object to customize — operator cookie
|
|
233
233
|
// / field names flow straight through, nothing static is baked in.
|
package/lib/archive-read.js
CHANGED
|
@@ -260,7 +260,7 @@ async function _readCentralDirectory(adapter, eocd) {
|
|
|
260
260
|
"central directory entry " + n + " carries ZIP64 sentinel sizes (unsupported — use tar for >4 GiB / >65535 entries)");
|
|
261
261
|
}
|
|
262
262
|
// ZIP names are CP437 or UTF-8 (per FLAG_UTF8_NAME bit). Decode
|
|
263
|
-
// as UTF-8 unconditionally —
|
|
263
|
+
// as UTF-8 unconditionally — a concern if operators in
|
|
264
264
|
// the wild rely on CP437; v0.12.7 ships UTF-8 only and operators
|
|
265
265
|
// with legacy CP437-only producers reach for an external decoder.
|
|
266
266
|
var name = cdBytes.slice(nameStart, nameStart + nameLen).toString("utf8");
|
package/lib/archive-tar-read.js
CHANGED
|
@@ -237,7 +237,7 @@ function tar(adapter, opts) {
|
|
|
237
237
|
}
|
|
238
238
|
var bodyStart = pos;
|
|
239
239
|
var paddedSize = Math.ceil(hdr.size / BLOCK_SIZE) * BLOCK_SIZE;
|
|
240
|
-
//
|
|
240
|
+
// Refuse truncated archives upfront.
|
|
241
241
|
// The walker advances `pos` by the declared padded block size; if
|
|
242
242
|
// the buffer ends mid-body, extract() would silently slice a
|
|
243
243
|
// partial payload (header says 11 bytes, buffer holds 8 — without
|