@blamejs/blamejs-shop 0.4.49 → 0.4.50
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +58 -46
- package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
- package/lib/vendor/blamejs/.gitignore +5 -1
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -1
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/lib/bundler.js +2 -7
- package/lib/vendor/blamejs/lib/config-drift.js +17 -3
- package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
- package/lib/vendor/blamejs/lib/db-schema.js +29 -0
- package/lib/vendor/blamejs/lib/db.js +7 -0
- package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
- package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
- package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
- package/lib/vendor/blamejs/lib/mail.js +16 -9
- package/lib/vendor/blamejs/lib/mcp.js +28 -6
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
- package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
- package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
- package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
- package/lib/vendor/blamejs/lib/seeders.js +33 -39
- package/lib/vendor/blamejs/lib/storage.js +71 -7
- package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
- package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
- package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
- package/package.json +1 -1
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.11 (2026-06-14) — **Replaces a family of quadratic-time regexes that hostile input could use to stall a worker with linear scans, refuses a relocatable sealed-cell downgrade on the read side, fails closed when enabling row-level security behind a non-native driver, and scans the vendored crypto for known CVEs on every build.** Several text-handling primitives stripped trailing whitespace or extracted a mail address with a regex whose backtracking is quadratic in the input length on adversarial strings — a request body, a YAML document, a CSV cell, or a From header crafted as a long run of spaces could pin a worker's CPU. Each is now a linear character scan with identical output. The HTML-content check the MCP tool surface applies gained the vbscript: and data:text/html vectors it was missing. On the data-at-rest side, an AAD-bound (or per-row-key) column now refuses a plain, unbound vault cell on read — a relocatable envelope an attacker with write access could copy in from another row defeats the cross-row binding, so the field is nulled rather than surfaced; operators mid-migration opt back in with registerTable({ allowPlainMigration: true }). declareRowPolicy now treats row-level-security as enabled only on a value that unambiguously means true, so a non-native Postgres driver that returns the string "f" can no longer be read as "already on" and silently skip the ENABLE that protects the table's rows. Finally, because the framework's crypto is vendored rather than installed, npm audit and Dependabot never see it: every build now matches the vendored versions against the OSV vulnerability database, with a complementary Semgrep pass and workflow-file static analysis alongside. **Added:** *b.safeBuffer.indexAfterOpenTag(html, tagName)* — A linear helper that returns the offset just past a `<tag ...>` opening tag (case-insensitive), or -1 when absent or unterminated — the insertion point a response rewriter uses to splice content after <body> or <head> without a regex. It replaces the O(n^2) html.match(/<body[^>]*>/i) shape and is stricter than it: a real tag boundary is required after the name, so <bodyfoo> is not mistaken for <body>. **Security:** *Linear-time replacements across a family of quadratic regexes (ReDoS class)* — Several primitives located or stripped text with a regex whose backtracking is quadratic in V8 on adversarial input (CWE-1333): b.safeBuffer and the safe-env / safe-yaml / guard-csv parsers stripped trailing horizontal whitespace with /[ \t]+$/; b.mail extracted the address from a `Name <addr>` header with /<([^>]+)>/; the bot-disclosure and speculation-rules response middleware found the <body> insertion point with /<body[^>]*>/i; and the BIMI certificate-chain splitter walked PEM blocks with a lazy /BEGIN[\s\S]*?END/ scan. A crafted field — a long run of spaces, an unterminated bracket, a body carrying many <body starts with no closing >, a chain of BEGIN markers — could drive a worker's CPU to seconds of work. Each is now a linear scan: a shared b.safeBuffer.stripTrailingHspace (backward char walk), b.safeBuffer.indexAfterOpenTag (forward indexOf walk for the tag insertion point), a forward indexOf for address extraction, and an indexOf walk for the PEM split. Output is byte-identical (the tag-find is stricter — it no longer mistakes <bodyfoo> for <body>), and 400K-character adversarial inputs that took 8–85 seconds now complete in under 2 ms. · *MCP HTML-content check covers vbscript: and data:text/html* — The dangerous-markup check applied to text/html tool content matched <script>/<iframe>/<object>/<embed> and javascript: URLs but not the vbscript: scheme or data:text/html payloads. Both are now refused; data: URLs carrying non-HTML media (data:image/png and similar) are unaffected. · *AAD-bound columns refuse a plain sealed cell on read* — b.cryptoField.unsealRow now refuses a plain, unbound vault: envelope found on an AAD-bound (or per-row-key) column and nulls the field instead of returning it. A plain envelope carries no per-cell binding, so a writer who could place one — copied from anywhere under the same vault root — would otherwise relocate a value across rows or columns and defeat the copy-protection the AAD binding advertises. Operators migrating pre-AAD rows up to bound ciphertext opt into a bounded acceptance window with registerTable({ allowPlainMigration: true }) and clear it when migration completes. · *Row-level-security enablement fails closed on non-native drivers* — b.db.declareRowPolicy read pg_class.relrowsecurity to skip a redundant ENABLE ROW LEVEL SECURITY, but tested it with a bare truthiness check. A native pg driver returns a JS boolean; a proxy or ORM may return the string "f" for a disabled table — and "f" is truthy, so the check read it as already-enabled and silently skipped the ENABLE, leaving every row in the table unprotected while the migration reported success. RLS now counts as enabled only on a value that unambiguously means true (true, 1, or "t"/"true"/"1"/"on"/"yes"); every other shape re-issues ENABLE, a harmless no-op on an already-enabled table. · *Vendored-crypto CVE scanning, complementary SAST, and workflow static analysis in CI* — The framework ships zero npm runtime dependencies — its crypto (the noble suite, the WebAuthn server, the PKI layer) is vendored under lib/vendor/, where npm audit, Dependabot, and Socket cannot see it. Every build now generates a CycloneDX SBOM of the vendored tree (each library carrying an npm purl) and runs it through OSV-Scanner, matching the exact pinned version against the OSV vulnerability database; a published CVE or GHSA affecting a vendored version fails the build so the copy is refreshed before merge. A Semgrep pass (registry security-audit + javascript packs at ERROR severity) runs alongside CodeQL as complementary SAST, and actionlint statically checks the workflow files. All three install the OSS tool from its upstream release, matching the existing secret-scan gate's posture. **Detectors:** *Quadratic trailing-whitespace and tag-find regex detectors* — Two codebase-pattern detectors refuse reintroduction of the quadratic shapes: the /[ \t]+$/ trailing-whitespace strip (as .replace, .test, or via the named TRAILING_HSPACE_RE export) outside the linear helper that owns it, and the str.match(/<tag[^>]*>/) document-tag find that the response middleware must route through b.safeBuffer.indexAfterOpenTag. Each is proven to fire on the removed shape and stay silent on the linear replacement, so the ReDoS class cannot creep back into a new parser, guard, or response rewriter.
|
|
12
|
+
|
|
13
|
+
- v0.15.10 (2026-06-13) — **Makes S3 Object-Lock version erasure reachable through the object store, and pins the build toolchain's native binary to a reviewed hash.** The object store gains the versioned-delete surface its S3 Object Lock support always needed for real erasure. An unversioned delete on a versioning-enabled (Object-Lock) bucket only writes a delete-marker — the data version survives — so the framework's own delete could report success while a record protected for compliance, or one a data subject asked to erase, stayed on disk. b.objectStore / b.storage now carry a versionId: put and saveRaw return the version they created, deleteFile(key, { versionId, bypassGovernanceRetention }) targets a specific version (refused — not silently delete-markered — when it is under an active retention), and listVersions enumerates versions and delete-markers so an erasure workflow can find them. Backends with no version surface (the filesystem backend, and the current Azure and GCS adapters) refuse a versioned delete loudly rather than silently dropping the current object. Separately, the build toolchain's native bundler binary is now verified against a reviewed SHA-256 pin so a tampered or drifted binary is caught before it bundles the framework. **Added:** *Versioned object delete + listVersions for S3 Object-Lock erasure* — b.storage.deleteFile and the b.objectStore sigv4 backend now accept opts.versionId to erase a specific object version, and opts.bypassGovernanceRetention to lift a GOVERNANCE-mode retention for callers with the permission (COMPLIANCE stays immutable to everyone). b.storage.saveRaw and the backend put now return the versionId they created on a versioning-enabled bucket, and a new b.storage.listVersions(prefix) / backend listVersions enumerates every version and delete-marker (key, versionId, isLatest, deleteMarker, size, lastModified, etag) so a right-to-erasure or crypto-shred workflow can target prior versions. On a backend with no version surface, listVersions throws VERSIONS_UNSUPPORTED and a versioned delete throws VERSIONID_UNSUPPORTED rather than silently acting on the current object. · *b.localDb.thin reaches SQLite resource-limit parity (limits option)* — b.localDb.thin now opens its node:sqlite handle with the same parse-time statement-size cap as b.db and the CLI — a SQL statement over 1 MiB is rejected at parse time, the SQLITE_LIMIT_LENGTH floor that guards prepare()/exec() of raw SQL against an attacker-influenced megaquery (SQLite's default is 1 GB). The cap is on by default; a new limits option (e.g. { sqlLength: 2 * 1024 * 1024 } or other SQLITE_LIMIT_* keys) lets an operator raise or extend it. Previously the thin opener had no limits plumbing, so a consumer on that path could not reach parity with the rest of the framework's SQLite surface. **Fixed:** *S3 Object-Lock version erasure is reachable through the framework delete path* — On a versioning-enabled (Object-Lock) bucket, an unversioned DELETE only writes a delete-marker — the protected data version survives untouched — yet the framework's delete had no versionId surface, so it issued the unversioned form and reported success while the bytes the lock protects remained. A retention or legal hold could therefore look enforced to the framework caller while the operation WORM actually blocks was unreachable. The delete path now targets the exact version: deleting a version under a COMPLIANCE retention is refused (it throws, even with bypassGovernanceRetention), a no-retention version erases cleanly, and the enforcement is proven end-to-end against MinIO through the framework's own API. · *b.configDrift.verifyVendorIntegrity is now working-directory-independent* — The vendored-dependency integrity check resolved each manifest file path against process.cwd(), so it only worked when run from the application root. Run from anywhere else it read-failed every entry (reporting ok:false), and under a crafted working directory that happened to contain a clean vendor tree it could hash a different tree than the one actually loaded. It now resolves each file under the framework's own vendor directory by default — the tree loaded at runtime — and honors an explicit libVendorDir for verifying a deployed tree elsewhere, so the result no longer depends on where the process was started. **Security:** *Build toolchain native binary pinned to a reviewed hash* — The native bundler binary the build toolchain runs (esbuild's per-platform compiler, a development dependency that never ships in the runtime) is now verified against a SHA-256 pin captured by diffing the published package tarballs and hashing the binary. The build gate fails if the on-disk binary does not match the reviewed hash for its (version, platform); for a version that has not been reviewed it notes the gap and skips rather than trusting an unverified binary. A cross-artifact check keeps the version in agreement across package.json, the CI install step, and the hash map, so the gate can never quietly test a version that was never diffed — closing a real drift where CI had been installing an older patch than package.json declared. The reviewed diff is benign: version strings plus an installer size-bound and error-message hardening, no new install hooks, files, or network paths, and no runtime-dependency impact. **Detectors:** *Object-store erasure guard, esbuild-pin agreement, + structural re-anchoring of the lint detectors* — A new guard locks the object-store delete path to the versioned-erasure contract: b.storage.deleteFile must thread versionId to the backend, so it can never silently revert to the WORM-blind unversioned delete. A second guard enforces that the esbuild build-tool version agrees across package.json, the CI install step, and the binary-hash map, so a future bump can't update one and leave the gate testing an unreviewed version. Separately, the framework's internal codebase-pattern lint detectors were re-anchored from fixed character spans to structural code boundaries, so they keep matching the code they guard as those functions grow rather than aging out of range; reviving them surfaced a few internal validation and transaction sites that now route through shared helpers (a required positive-integer-with-range validator and an async transaction wrapper) instead of hand-rolling the check. No public API change.
|
|
14
|
+
|
|
11
15
|
- v0.15.9 (2026-06-13) — **Raises the Node floor to 24.16, adds SQLite parse-time resource caps, retries Windows rename locks on every atomic write and download, and ships a one-call secure logout that wipes client-side state.** This release moves the engines floor to the current Node 24 LTS patch level and adds three hardening primitives. node:sqlite handles now construct with SQLITE_LIMIT_* caps: a statement over 1 MiB is rejected at parse time (a DoS floor on the raw-SQL surface, complementary to the existing row-count gate) and ATTACH DATABASE is denied. Every final temp-to-destination rename — the file written by an atomic write, a downloaded file, a sealed vault key, a rotated log, an extracted archive entry — now routes through a single retry that rides out a transient Windows lock (antivirus, the search indexer, or a file-sync client briefly holding the destination), instead of surfacing the lock as a hard failure; the retry, previously hand-rolled and unreachable, is now the reusable b.atomicFile.renameWithRetry. And b.session.logout destroys a session and tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data header and expires the session cookie before destroying the row, the secure-default logout that previously had to be assembled by hand. **Added:** *b.session.logout — one-call secure logout* — `b.session.logout(res, token, opts?)` destroys the server-side session AND tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data response header (cookies + storage + cache + execution contexts by default) and expires the session cookie, then destroys the session row. `b.session.destroy` alone is a store operation with no response object, so it could not wipe the browser's cached pages, storage, or a stale tab still holding the now-revoked cookie — that wiring previously had to be mounted by hand. Pass `cookieName` to match a non-default cookie and `types` to choose the Clear-Site-Data directives. **Changed:** *Node engines floor raised to >=24.16.0* — The minimum supported Node is now 24.16.0 (the current Node 24 LTS patch level), up from 24.14.1. This is an LTS-currency bump — there are no Node CVE fixes between 24.14.1 and 24.16.0 (24.14.1 already carried the CVE-2026-21713 HMAC fix); it keeps the framework on the latest patched LTS and makes the node:sqlite resource-cap hardening below available everywhere. Pre-1.0, operators upgrade across the floor; Node 26 continues to satisfy it. **Fixed:** *b.watcher canonicalizes its root on Windows* — `b.watcher.create` now resolves its `root` to the real long path before watching. On Windows a root with an 8.3 short-name component (the system temp directory commonly resolves to one) made the native recursive backend deliver long-name event paths that no longer prefix-matched the watched root, which could abort the process under a strict libuv fs-event assertion. The watcher now canonicalizes the root (expanding short names and resolving symlinks), so events match the watched directory on Windows. **Security:** *SQLite parse-time statement-size cap* — Every node:sqlite database the framework opens — the main db handle and the CLI's handle — now constructs with a SQLITE_LIMIT_LENGTH cap: a SQL statement over 1 MiB is rejected at parse time. Because the query builder parameterizes every value, the size cap guards the raw-SQL surface (`b.db.runSql`) against an attacker-influenced megaquery the parser would otherwise process (SQLite's default is 1 GB); it is a parse-time DoS floor complementary to the existing row-count gate. Legitimate framework and operator statements are far under the cap. · *Windows rename-lock retry on every atomic rename and download* — On Windows a freshly-written file's destination is briefly held by antivirus, the search indexer, or a file-sync client (Dropbox, OneDrive), surfacing as a transient EPERM / EACCES / EBUSY on rename even though the temp file is fine. `b.atomicFile.writeSync` already retried this, but `b.httpClient.downloadStream` did not — a download into a cloud-synced or AV-scanned directory could fail hard on the lock. The retry is now the reusable `b.atomicFile.renameWithRetry`, and every final temp-to-destination rename in the framework routes through it: downloads, sealed vault keys, CA key/cert writes, log rotation, archive extraction, config-drift sidecars, the self-update binary swap, and restore/rollback moves. A non-transient error still throws immediately; POSIX renames are unaffected. **Detectors:** *Rename-retry, SQLite-limits, and Clear-Site-Data guards* — Three recurrence detectors ship with the fixes: a bare `nodeFs.renameSync` final rename that doesn't route through `atomicFile.renameWithRetry`; a main `DatabaseSync` handle constructed without the SQLITE_LIMIT_LENGTH `limits`; and a hand-rolled Clear-Site-Data header value that skips the shared RFC 9527 builder.
|
|
12
16
|
|
|
13
17
|
- v0.15.8 (2026-06-13) — **Redacts OTLP log-sink attributes to close a secret/PII egress the span fix missed, adds EU DSA and China PIPL cross-border compliance record-builders, ships an SSDF producer self-attestation with every release, and makes the published tarball reproducible.** This release closes a telemetry egress hole, adds two compliance record-builder namespaces, and hardens the release supply chain. The OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's meta attributes and the resource attributes to the collector unredacted — a log line carrying a bearer token, password, or API key reached the wire verbatim (CWE-532). The 0.15.4 fix wired the telemetry redactor into the span and metric exporters but the log sinks were missed; both now run record and resource attributes through the same redactor before serialization. New b.dsa builds the EU Digital Services Act (Reg 2022/2065) records an intermediary or platform must keep — Art. 16 notice-and-action, Art. 17 statement of reasons, and the Art. 15 / 24(3) transparency report. New b.pipl builds the China PIPL cross-border transfer records — an Art. 38/40 assessment that determines whether a CAC security assessment is mandatory (CIIO, important data, or the volume / sensitive-PI thresholds), and an Art. 40 security-assessment certificate. On the supply-chain side, every release now ships ssdf-attestation.json, a machine-readable NIST SP 800-218 / OMB M-22-18 producer self-attestation mapping each secure-development practice to its implementing control, and the published tarball is now packed with SOURCE_DATE_EPOCH so an operator can rebuild it byte-for-byte from the release commit. **Added:** *b.dsa — EU Digital Services Act compliance record-builders* — `b.dsa` builds the dated, frozen records the EU Digital Services Act (Regulation (EU) 2022/2065) requires an online intermediary or platform to keep: `b.dsa.noticeAndAction` (Art. 16) records a notice against a piece of content and computes the action-due window; `b.dsa.statementOfReasons` (Art. 17) records a moderation decision with its legal or contractual ground (exactly one is required), the facts, whether it was automated, and the redress routes offered; `b.dsa.transparencyReport` (Art. 15 / 24(3)) aggregates the period counts into a report with the next-due date. The builders perform no network I/O and emit a best-effort audit event; they map to the `dsa` compliance posture. · *b.pipl — China PIPL cross-border transfer record-builders* — `b.pipl.sccFilingAssessment` builds a PIPL Art. 38/40/55 cross-border transfer assessment and determines the lawful mechanism: it forces a CAC security assessment (over a self-selected standard contract or certification) when the exporter is a critical-information-infrastructure operator, exports important data, handles personal information of more than 1,000,000 individuals, or crosses the cumulative volume / sensitive-PI thresholds. `b.pipl.securityAssessmentCertificate` records an Art. 40 security-assessment self-declaration with a 3-year validity clock. Both return frozen dated records and map to the `pipl-cn` posture. · *SSDF producer self-attestation shipped with every release* — Every release now attaches `ssdf-attestation.json` — a machine-readable NIST SP 800-218 (SSDF v1.1) / OMB M-22-18 producer self-attestation. It maps each secure-software-development practice to its implementing control in the tree (SLSA L3 provenance, SSH-signed tags, vendored zero-runtime-dep supply chain, OSV-Scanner gating, coordinated disclosure) and is deterministic from the release commit. Its sha256 is a subject of the SLSA L3 provenance, so verifying the provenance verifies the attestation has not been tampered with. Downstream consumers who require SSDF supplier-compliance evidence can download it from the release page. **Security:** *OTLP log sinks redact record and resource attributes before export* — `b.logStream`'s OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's `meta` attributes and the sink's resource attributes to the collector UNREDACTED, so a log line whose meta carried a bearer token, password, or API key — or a credential placed in a resource attribute — reached the OTLP wire verbatim (CWE-532). The 0.15.4 change baked the telemetry redactor into the span and metric exporters but its detector was anchored on the span/metric encoder function names, leaving the log sinks uncovered. Both log sinks now run record and resource attributes through `b.observability.redactAttrs` before serialization, the same egress contract the span and metric exporters already hold. · *Reproducible published tarball (SOURCE_DATE_EPOCH)* — The release workflow now exports `SOURCE_DATE_EPOCH` (derived from the tagged commit's author date) before `npm pack`, so the mtime stamped into every tar header is deterministic. An operator can re-pack the package from the same commit and match the published tarball's sha256 byte-for-byte, strengthening the source-to-artifact verification path alongside the existing SLSA L3 provenance and PQC signatures. **Detectors:** *otlp-log-sink-encodes-attrs-without-redactor* — Fires when an OTLP log-sink encoder hands a raw `record.meta` or resource-attribute map to serialization without routing it through `observability.redactAttrs` — the class the span/metric detector could not see because the log sinks carry the OTLP-logs schema function names.
|
|
@@ -65,7 +65,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
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`); an opt-in `requireTls` transport posture refuses a non-TLS backend at boot, and query / transaction / read traces carry OpenTelemetry `db.*` attributes. The framework's own data layer — the signed audit chain, cluster leadership and lease fencing, sessions, break-glass, and the local queue / cache / scheduler — is composed through the dialect-aware `b.sql` builder (every identifier quoted by construction, every value bound as a placeholder, dialect-correct SQLite / Postgres / MySQL output), so the framework's tables run on a Postgres or MySQL backend, not only local SQLite; `b.guardSql` validates result rows against NUL bytes, quote-jump sequences, and per-column / total-size boundaries
|
|
68
|
-
- **Object store** — S3 / R2 / B2 / GCS / Azure with multipart upload + SSE + bucket-ops (create / delete / list / lifecycle / CORS); S3 Object Lock + per-object retention + legal hold for write-once-read-many compliance workloads (`b.storage`, `b.objectStore`)
|
|
68
|
+
- **Object store** — S3 / R2 / B2 / GCS / Azure with multipart upload + SSE + bucket-ops (create / delete / list / lifecycle / CORS); S3 Object Lock + per-object retention + legal hold for write-once-read-many compliance workloads, with versioned delete + `listVersions` for right-to-erasure / crypto-shred against an Object-Lock bucket (`b.storage`, `b.objectStore`)
|
|
69
69
|
- **Queues + cache** — durable queue with priority + cron + flows on local SQLite, shared Redis, OR AWS SQS via SigV4 + AWSJsonProtocol_1.0 (`b.queue`, `b.jobs`) — the local backend can target an operator-supplied database / table / schema; cluster-shared cache (`b.cache`)
|
|
70
70
|
### Identity & access
|
|
71
71
|
|
|
@@ -204,10 +204,11 @@ What blamejs defends against, by design:
|
|
|
204
204
|
- **Inline-script injection** — strict CSP default (`script-src 'self' 'nonce-...'`) blocks anything an XSS payload could ship.
|
|
205
205
|
- **Algorithm-substitution attacks** — every encrypted blob carries a 4-byte algorithm-ID header; `b.crypto.decrypt` dispatches on the header bytes, not on a guess at the active default. An attacker swapping a weaker algorithm into the envelope fails the AEAD tag check.
|
|
206
206
|
- **Supply-chain compromise via npm transitive deps** — zero npm runtime dependencies. Every external library is vendored under `lib/vendor/` with a manifest pinning version + license + provenance. Build reproducibility is verified via the GHCR image's SLSA provenance attestation (see DEPLOY.md → "Release verification").
|
|
207
|
+
- **A published CVE in a vendored library going unnoticed** — because the crypto is vendored rather than installed, `npm audit` / Dependabot / Socket never see it. Every PR builds a CycloneDX SBOM of `lib/vendor/` (each library carries an npm purl) and runs it through OSV-Scanner, matching the exact pinned version against the OSV vulnerability database (CVE + GHSA). A match fails the build — the vendored copy is refreshed before merge. This runs alongside CodeQL (semantic SAST), a complementary Semgrep pass (registry security-audit + javascript packs at ERROR severity), gitleaks (secret scan over full history), and actionlint (workflow static analysis).
|
|
207
208
|
- **Replay of API requests** — `apiEncrypt` middleware nonce-stores + replay-windows the `_ek` field; old session keys can't be reused.
|
|
208
209
|
- **Error-detail leakage on an encrypted channel** — when `apiEncrypt` is active, terminal error responses (the error page, schema-validation refusals, RFC 9457 problem+json, deny-middleware bodies) are sealed in the same `{ _ct }` envelope as successes via `req.apiEncryptEncode`, so error detail (paths, field names, refusal reasons) doesn't ship in plaintext while the surrounding traffic is encrypted. Errors raised before a session exists (Bearer 401, handshake reject, replay refusal) stay plaintext so the caller can read them; the client opts into reading a non-2xx with `b.httpClient.encrypted({ responseMode: "passthrough" })` instead of throwing on the shape.
|
|
209
210
|
- **Server-Side Request Forgery on outbound calls** — `b.ssrfGuard` resolves the hostname of every `b.httpClient.request({ url })` and refuses any IP in private / loopback / link-local / cloud-metadata / reserved ranges (incl. AWS / GCP / Azure metadata at 169.254.169.254). Wired default-on; operators on internal-mesh deployments override the loopback / private / link-local / reserved classes per call via `allowInternal: true | CIDR[]`. Cloud-metadata IPs are an unconditional hard-deny — no `allowInternal` value bypasses them, because metadata services leak instance credentials and a blanket override would let any compromised request exfiltrate them. Webhook delivery, OAuth, mail HTTP transports, object-store, and notify all inherit the gate.
|
|
210
|
-
- **Cross-row sealed-data smuggling** — `b.vault.aad.seal(plaintext, { table, rowId, column, schemaVersion })` binds the AEAD authentication tag to the column's identity tuple. A copy-paste of a sealed value between rows, a schema-version replay, or a table-mismatch substitution surfaces as a refused decrypt rather than silent disclosure. `reseal(value, fromAad, toAad)` re-binds during schema migrations after authenticating the source.
|
|
211
|
+
- **Cross-row sealed-data smuggling** — `b.vault.aad.seal(plaintext, { table, rowId, column, schemaVersion })` binds the AEAD authentication tag to the column's identity tuple. A copy-paste of a sealed value between rows, a schema-version replay, or a table-mismatch substitution surfaces as a refused decrypt rather than silent disclosure. `reseal(value, fromAad, toAad)` re-binds during schema migrations after authenticating the source. On the read side, `b.cryptoField.unsealRow` refuses a PLAIN (unbound) `vault:` cell found on an AAD-bound (or per-row-key) column — a relocatable envelope an attacker could copy in from anywhere under the same vault root would otherwise defeat the binding — and nulls the field rather than surfacing it. Operators migrating pre-AAD rows opt into a bounded acceptance window with `registerTable({ allowPlainMigration: true })`, cleared once migration completes.
|
|
211
212
|
- **Tampered vendored library between releases** — `b.configDrift.verifyVendorIntegrity({ manifestPath })` runs at boot and compares every artifact under `lib/vendor/*` against its SHA-256 in `MANIFEST.json`. Mismatches abort start with an audit row under the `vendor` namespace (`vendor.integrity.tampered`); successful verification audits as `vendor.integrity.verified`. Operators wire this into the boot sequence ahead of opening any listener.
|
|
212
213
|
- **Adversarial input crashing a parser / validator** — every `lib/safe-*.js` and `lib/guard-*.js` primitive (including nested `lib/parsers/safe-*.js`) is fuzzed with coverage-guided libFuzzer harnesses via jazzer.js. ClusterFuzzLite runs every PR (300s budget per target) + a daily batch (1800s + 600s coverage); OSS-Fuzz runs the same harnesses continuously on Google's infrastructure once the upstream submission lands (project config under `oss-fuzz/projects/blamejs/`). Findings ship with minimized reproducers + persist in the regression corpus across runs. The coverage gate in `test/layer-0-primitives/codebase-patterns.test.js` refuses any future parser primitive that lands without a matching `fuzz/<name>.fuzz.js` harness (or an audited `FUZZ_NOT_REQUIRED` allowlist entry with reason).
|
|
213
214
|
- **Honeytoken trip detection** — `b.honeytoken` issues canary credentials (fake API key shapes, fake admin URLs, fake row IDs) registered with the framework but never handed to a real client. Any positive lookup against the registry — in a request, log search, or DB query — emits a `honeytoken.tripped` audit row regardless of where the request landed. Operators wire alerting against the audit-chain stream; the framework refuses the request silently in production and never confirms the token was a honeypot.
|
|
@@ -369,6 +370,7 @@ This is the minimum-viable security posture for a production deployment. The fra
|
|
|
369
370
|
- [ ] For endpoints returning aggregate statistics over personal data (counts / sums / means / histograms): add calibrated noise with `b.ai.dp` and gate spend with `b.ai.dp.budget({ scope, epsilon, delta })` — use `type: "laplace"` (snapping mechanism) or `type: "gaussian"` (discrete Gaussian), NEVER a hand-rolled `Math.random`-based noise generator: a naive double-precision Laplace sampler lets an attacker distinguish neighbouring datasets from a single output (Mironov 2012), silently breaking the guarantee. Track the per-scope ε/δ budget so repeated queries can't be averaged to cancel the noise
|
|
370
371
|
- [ ] For data with a TTL (GDPR Art. 17, PCI 3.1, retention windows): declare retention rules via `b.retention.create({ db, audit }).declare({ name, table, ageField, ttlMs, action: "erase" })` and run on a `b.scheduler` cadence; honour legal-hold via `legalHoldField`
|
|
371
372
|
- [ ] For write-once-read-many object archives (SEC 17a-4, FINRA, HIPAA-shaped retention): create the bucket with `b.objectStore.bucketOps.create(name, { objectLockEnabled: true })` (Object Lock can ONLY be flipped at create time), apply a default retention via `setObjectLockConfiguration(name, { mode: "COMPLIANCE", years })`, and pin individual objects with `setObjectRetention(name, key, { mode, retainUntil })` or `setObjectLegalHold(name, key, "ON")` — `COMPLIANCE` cannot be shortened or bypassed by anyone (including root); pick deliberately
|
|
373
|
+
- [ ] To erase a record on an Object-Lock (versioning-enabled) bucket — right-to-erasure or crypto-shred — target the VERSION: a plain `b.storage.deleteFile(key)` only writes a delete-marker and the protected data version survives. Capture the `versionId` from the write (`saveRaw`/`put` return it) or enumerate it with `b.storage.listVersions(prefix)`, then `b.storage.deleteFile(key, { versionId })`; a version under an active retention is refused (the call throws), and `bypassGovernanceRetention: true` lifts a GOVERNANCE-mode hold for an authorized caller but never a `COMPLIANCE` one
|
|
372
374
|
- [ ] At boot, before any outbound socket opens: call `b.network.bootFromEnv({ env: process.env, audit: b.audit })` so operator-supplied NTP / DNS / proxy / DPI-trust / TCP socket settings (`BLAMEJS_NTP_*`, `BLAMEJS_DNS_*`, `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`, `BLAMEJS_EXTRA_CA_CERTS`, `BLAMEJS_SOCKET_*`) apply uniformly
|
|
373
375
|
- [ ] If you ship spans/metrics to an OTLP collector through a custom exporter (rather than the framework's `b.otelExport` / `b.logStream`, which already do this): run every span / metric / resource attribute **value** through `b.observability.redactAttrs(attrs)` before serialization. Telemetry is a first-class egress sink — an attribute value holding a bearer token, password, or PII is otherwise shipped to the collector verbatim (CWE-532). `redactAttrs` composes `b.redact.redact`, drops any attribute whose redactor throws (fail toward dropping), and honours an operator override installed via `b.observability.setRedactor`
|
|
374
376
|
- [ ] If the deployment sits behind a deep-packet-inspection proxy with its own re-signing CA: install the CA via `b.network.tls.addCa("/path/to/corp-ca.pem", { label: "corp-mitm" })` and pass `allowDpiTrust: true` to `b.security.assertProduction` — every CA addition audits with subject + fingerprint so a forensic review can reconstruct the trust path
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.15.
|
|
4
|
-
"createdAt": "2026-06-
|
|
3
|
+
"frameworkVersion": "0.15.11",
|
|
4
|
+
"createdAt": "2026-06-14T05:18:13.280Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -48328,6 +48328,10 @@
|
|
|
48328
48328
|
"type": "function",
|
|
48329
48329
|
"arity": 1
|
|
48330
48330
|
},
|
|
48331
|
+
"indexAfterOpenTag": {
|
|
48332
|
+
"type": "function",
|
|
48333
|
+
"arity": 2
|
|
48334
|
+
},
|
|
48331
48335
|
"isHex": {
|
|
48332
48336
|
"type": "function",
|
|
48333
48337
|
"arity": 2
|
|
@@ -51211,6 +51215,10 @@
|
|
|
51211
51215
|
"type": "function",
|
|
51212
51216
|
"arity": 0
|
|
51213
51217
|
},
|
|
51218
|
+
"listVersions": {
|
|
51219
|
+
"type": "function",
|
|
51220
|
+
"arity": 2
|
|
51221
|
+
},
|
|
51214
51222
|
"presignedDownloadUrl": {
|
|
51215
51223
|
"type": "function",
|
|
51216
51224
|
"arity": 2
|
|
@@ -266,13 +266,8 @@ function create(opts) {
|
|
|
266
266
|
var hashOn = opts.hash !== false;
|
|
267
267
|
var hashLen = DEFAULT_HASH_LEN;
|
|
268
268
|
if (opts.hashLen !== undefined) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
throw new BundlerError("bundler/bad-hash-len",
|
|
272
|
-
"bundler.create: opts.hashLen must be a positive finite integer " +
|
|
273
|
-
"between " + MIN_HASH_LEN + " and " + MAX_HASH_LEN +
|
|
274
|
-
"; got " + numericBounds.shape(opts.hashLen));
|
|
275
|
-
}
|
|
269
|
+
numericBounds.requirePositiveFiniteInt(opts.hashLen, "bundler.create: opts.hashLen",
|
|
270
|
+
BundlerError, "bundler/bad-hash-len", { min: MIN_HASH_LEN, max: MAX_HASH_LEN });
|
|
276
271
|
hashLen = opts.hashLen;
|
|
277
272
|
}
|
|
278
273
|
var log = opts.log || null;
|
|
@@ -350,7 +350,7 @@ function create(opts) {
|
|
|
350
350
|
* instead of a silent zero-files-checked pass.
|
|
351
351
|
*
|
|
352
352
|
* @opts
|
|
353
|
-
* libVendorDir: string, // absolute path to lib/vendor
|
|
353
|
+
* libVendorDir: string, // absolute path to the lib/vendor tree to verify; default: the framework's own (cwd-independent). Per-file manifest paths resolve under this directory.
|
|
354
354
|
* manifestPath: string, // absolute path to MANIFEST.json (defaults under libVendorDir)
|
|
355
355
|
*
|
|
356
356
|
* @example
|
|
@@ -367,7 +367,14 @@ function create(opts) {
|
|
|
367
367
|
*/
|
|
368
368
|
function verifyVendorIntegrity(opts) {
|
|
369
369
|
opts = opts || {};
|
|
370
|
-
|
|
370
|
+
// Default to the framework's OWN vendor directory (this module lives in
|
|
371
|
+
// lib/, so __dirname/vendor is lib/vendor) — the tree actually loaded at
|
|
372
|
+
// runtime. The previous cwd-relative default made the check cwd-dependent:
|
|
373
|
+
// run from another directory it read-failed every entry, and under a crafted
|
|
374
|
+
// cwd that happened to hold a clean vendor tree it could hash a DIFFERENT
|
|
375
|
+
// tree than the one loaded. Operators verifying a deployed tree elsewhere
|
|
376
|
+
// pass libVendorDir explicitly; per-file resolution honors it below.
|
|
377
|
+
var libVendorDir = opts.libVendorDir || nodePath.join(__dirname, "vendor");
|
|
371
378
|
var manifestPath = opts.manifestPath || nodePath.join(libVendorDir, "MANIFEST.json");
|
|
372
379
|
var raw;
|
|
373
380
|
try { raw = nodeFs.readFileSync(manifestPath, "utf8"); }
|
|
@@ -390,7 +397,14 @@ function verifyVendorIntegrity(opts) {
|
|
|
390
397
|
var rel = files[kind];
|
|
391
398
|
var expected = hashes[kind];
|
|
392
399
|
if (typeof rel !== "string" || typeof expected !== "string") return;
|
|
393
|
-
|
|
400
|
+
// Manifest paths are stored repo-root-relative (e.g. "lib/vendor/x.cjs").
|
|
401
|
+
// Resolve each one UNDER libVendorDir (the tree being verified), not
|
|
402
|
+
// process.cwd(), so the check hashes the actual loaded files regardless
|
|
403
|
+
// of the working directory. Strip the leading lib/vendor/ so the join
|
|
404
|
+
// doesn't double it; a manifest that already stored a vendor-relative
|
|
405
|
+
// path resolves the same way.
|
|
406
|
+
var relInVendor = rel.replace(/^lib[\\/]+vendor[\\/]+/, "");
|
|
407
|
+
var abs = nodePath.isAbsolute(rel) ? rel : nodePath.join(libVendorDir, relInVendor);
|
|
394
408
|
var actual;
|
|
395
409
|
try {
|
|
396
410
|
var bytes = nodeFs.readFileSync(abs);
|
|
@@ -331,6 +331,15 @@ function isRowSealed(value) {
|
|
|
331
331
|
* // threaded into AAD. Default "1". Bump
|
|
332
332
|
* // when the column layout changes to
|
|
333
333
|
* // invalidate all prior ciphertext.
|
|
334
|
+
* allowPlainMigration: boolean, // default false. On an aad / per-row-key
|
|
335
|
+
* // table the read path refuses a PLAIN
|
|
336
|
+
* // (unbound) vault: cell — a relocatable
|
|
337
|
+
* // envelope an attacker could copy in from
|
|
338
|
+
* // another row defeats the AAD copy-
|
|
339
|
+
* // protection, so it is nulled, not surfaced.
|
|
340
|
+
* // Set true ONLY for the bounded window while
|
|
341
|
+
* // migrating pre-AAD rows up to AAD-bound
|
|
342
|
+
* // ciphertext; clear it once migration ends.
|
|
334
343
|
*
|
|
335
344
|
* @example
|
|
336
345
|
* b.cryptoField.registerTable("patients", {
|
|
@@ -403,6 +412,13 @@ function registerTable(name, opts) {
|
|
|
403
412
|
rowIdField: rowIdField,
|
|
404
413
|
schemaVersion: schemaVersion,
|
|
405
414
|
derivedHashMode: derivedHashMode,
|
|
415
|
+
// allowPlainMigration — read-side downgrade window. On an aad / per-row-key
|
|
416
|
+
// table the read path refuses a plain `vault:` cell (no AAD = relocatable,
|
|
417
|
+
// which would defeat the cross-row/cross-column copy-protection the AAD
|
|
418
|
+
// binding advertises). Operators with genuine pre-AAD rows opt into a
|
|
419
|
+
// bounded lazy-migration window with { allowPlainMigration: true }; a
|
|
420
|
+
// re-seal (sealRow) then re-emits the cell AAD-bound. Default closed.
|
|
421
|
+
allowPlainMigration: opts.allowPlainMigration === true,
|
|
406
422
|
};
|
|
407
423
|
}
|
|
408
424
|
|
|
@@ -1240,6 +1256,20 @@ function unsealRow(table, row, actor, dbHandle) {
|
|
|
1240
1256
|
unsealed = _decodeTyped(vaultAad.unseal(out[field],
|
|
1241
1257
|
_aadParts(s, table, field, out)));
|
|
1242
1258
|
} else if (typeof out[field] === "string" && out[field].startsWith(VAULT_PREFIX)) {
|
|
1259
|
+
// A plain `vault:` cell (no AAD) on an AAD-bound / per-row-key table
|
|
1260
|
+
// is a downgrade: plain vault.seal carries no AAD, so a DB-write
|
|
1261
|
+
// attacker could relocate such a cell from another row/column into
|
|
1262
|
+
// this one and the read would silently accept it — defeating the
|
|
1263
|
+
// cross-row/cross-column copy-protection the AAD binding advertises.
|
|
1264
|
+
// Refuse (throw -> catch nulls the field + audits) unless the table
|
|
1265
|
+
// opted into the documented pre-AAD lazy-migration window.
|
|
1266
|
+
if ((s.aad || perRowKeyTables[table]) && !s.allowPlainMigration) {
|
|
1267
|
+
throw new CryptoFieldError("crypto-field/aad-downgrade-refused",
|
|
1268
|
+
"unsealRow: '" + table + "'.'" + field + "' is AAD-bound but the stored " +
|
|
1269
|
+
"cell is a plain (unbound) vault envelope — refusing a relocatable-seal " +
|
|
1270
|
+
"downgrade (set registerTable({ allowPlainMigration: true }) for a " +
|
|
1271
|
+
"documented pre-AAD migration window)");
|
|
1272
|
+
}
|
|
1243
1273
|
unsealed = _decodeTyped(vault.unseal(out[field]));
|
|
1244
1274
|
} else {
|
|
1245
1275
|
// Not a sealed value — pass through.
|
|
@@ -73,6 +73,18 @@ function _err(code, message) {
|
|
|
73
73
|
return new DeclareRowPolicyError(code, message);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// _isRlsEnabled — fail-closed coercion of pg_class.relrowsecurity. Native pg
|
|
77
|
+
// drivers return a JS boolean; a proxy / ORM / non-native driver may return
|
|
78
|
+
// "t"/"f", "true"/"false", or 1/0. RLS counts as enabled ONLY on a value that
|
|
79
|
+
// unambiguously means true; every other shape (including the truthy string
|
|
80
|
+
// "f") reads as not-enabled, so ENABLE is (re-)issued rather than silently
|
|
81
|
+
// skipped — a skipped ENABLE would leave the table's rows unprotected.
|
|
82
|
+
function _isRlsEnabled(v) {
|
|
83
|
+
if (v === true || v === 1) return true;
|
|
84
|
+
if (typeof v === "string") return /^(t|true|1|on|yes)$/i.test(v.trim());
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
function _validateIdent(where, value) {
|
|
77
89
|
try {
|
|
78
90
|
safeSql.validateIdentifier(value, { allowReserved: true });
|
|
@@ -227,7 +239,14 @@ function declareRowPolicy(opts) {
|
|
|
227
239
|
"source table '" + spec.schema + "." + spec.table +
|
|
228
240
|
"' not found (does it exist? does the migration role have visibility?)");
|
|
229
241
|
}
|
|
230
|
-
|
|
242
|
+
// relrowsecurity is a Postgres boolean. Native pg drivers return true/false,
|
|
243
|
+
// but a proxy / ORM / non-native driver may hand back "t"/"f", "true"/"false",
|
|
244
|
+
// or 1/0. The string "f" is TRUTHY, so a bare `!rows[0].relrowsecurity` would
|
|
245
|
+
// read "f" as "already enabled" and silently SKIP ENABLE — leaving the table's
|
|
246
|
+
// rows unprotected while the migration reports success. Treat RLS as enabled
|
|
247
|
+
// ONLY on a value that unambiguously means true; anything else fails closed and
|
|
248
|
+
// (re-)issues ENABLE, which is a harmless no-op on an already-enabled table.
|
|
249
|
+
if (!_isRlsEnabled(rows[0].relrowsecurity)) {
|
|
231
250
|
var enableStmt = sql.enableRowLevelSecurity(tableRef, { dialect: "postgres" });
|
|
232
251
|
await xdb.query(enableStmt.sql, enableStmt.params);
|
|
233
252
|
}
|
|
@@ -98,6 +98,34 @@ function runInTransaction(db, fn, opts) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// runInTransactionAsync — the async sibling of runInTransaction. SQLite
|
|
102
|
+
// transactions are synchronous at the wire, but the body between BEGIN and
|
|
103
|
+
// COMMIT may await (a seeder's run(), a per-row re-seal that reads off the
|
|
104
|
+
// handle): await fn() before COMMIT so the transaction wraps the whole
|
|
105
|
+
// awaited body, and ROLLBACK on rejection. Same opts.lockMode /
|
|
106
|
+
// opts.onRollbackFail contract as the sync form.
|
|
107
|
+
async function runInTransactionAsync(db, fn, opts) {
|
|
108
|
+
if (typeof fn !== "function") {
|
|
109
|
+
throw new TypeError("dbSchema.runInTransactionAsync: fn must be a function");
|
|
110
|
+
}
|
|
111
|
+
opts = opts || {};
|
|
112
|
+
var beginSql = opts.lockMode ? "BEGIN " + opts.lockMode : "BEGIN";
|
|
113
|
+
runSqlOnHandle(db, beginSql);
|
|
114
|
+
try {
|
|
115
|
+
var result = await fn();
|
|
116
|
+
runSqlOnHandle(db, "COMMIT");
|
|
117
|
+
return result;
|
|
118
|
+
} catch (e) {
|
|
119
|
+
try { runSqlOnHandle(db, "ROLLBACK"); }
|
|
120
|
+
catch (rollbackErr) {
|
|
121
|
+
if (typeof opts.onRollbackFail === "function") {
|
|
122
|
+
try { opts.onRollbackFail(rollbackErr); } catch (_e) { /* nested handler must not bubble */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
// ---- Internal migrations table ----
|
|
102
130
|
|
|
103
131
|
// Logical name; the physical name + configured prefix resolve through
|
|
@@ -611,6 +639,7 @@ module.exports = {
|
|
|
611
639
|
runSql: runSql,
|
|
612
640
|
runSqlOnHandle: runSqlOnHandle,
|
|
613
641
|
runInTransaction: runInTransaction,
|
|
642
|
+
runInTransactionAsync: runInTransactionAsync,
|
|
614
643
|
// Shared data-layer dialect resolution — composed by migrations.js +
|
|
615
644
|
// seeders.js so the handle-dialect / b.sql-opts / key-text-type logic
|
|
616
645
|
// lives in exactly one place.
|
|
@@ -1402,6 +1402,13 @@ async function init(opts) {
|
|
|
1402
1402
|
aad: t.aad,
|
|
1403
1403
|
rowIdField: t.rowIdField,
|
|
1404
1404
|
schemaVersion: t.schemaVersion,
|
|
1405
|
+
// The read-side pre-AAD migration opt-in must pass through too —
|
|
1406
|
+
// otherwise a schema declaring { aad: true, allowPlainMigration: true }
|
|
1407
|
+
// registers with the default (false) via this declarative db.init path,
|
|
1408
|
+
// and legacy plain vault: cells are nulled on read despite the operator
|
|
1409
|
+
// opting into the migration window. registerTable defaults this to false,
|
|
1410
|
+
// so non-migrating tables are unaffected.
|
|
1411
|
+
allowPlainMigration: t.allowPlainMigration,
|
|
1405
1412
|
});
|
|
1406
1413
|
tableMetadata[t.name] = {
|
|
1407
1414
|
primaryKey: _normalizePk(t),
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
var codepointClass = require("./codepoint-class");
|
|
58
58
|
var csv = require("./csv");
|
|
59
|
+
var safeBuffer = require("./safe-buffer");
|
|
59
60
|
var C = require("./constants");
|
|
60
61
|
var lazyRequire = require("./lazy-require");
|
|
61
62
|
var numericBounds = require("./numeric-bounds");
|
|
@@ -432,7 +433,9 @@ function _stripIssues(text, opts) {
|
|
|
432
433
|
out = out.replace(ZW_RE_G, "");
|
|
433
434
|
if (opts.trailingWhitespacePolicy === "trim") {
|
|
434
435
|
out = out.split("\n").map(function (line) {
|
|
435
|
-
|
|
436
|
+
// Linear per-line trailing-whitespace trim — .replace(/[ \t]+$/) is
|
|
437
|
+
// O(n^2) in V8 on adversarial input (untrusted CSV here).
|
|
438
|
+
return safeBuffer.stripTrailingHspace(line);
|
|
436
439
|
}).join("\n");
|
|
437
440
|
}
|
|
438
441
|
return out;
|
|
@@ -521,9 +524,15 @@ function escapeCell(value, opts) {
|
|
|
521
524
|
if (opts.bidiCharPolicy === "strip") str = str.replace(BIDI_RE_G, "");
|
|
522
525
|
|
|
523
526
|
if (opts.trailingWhitespacePolicy === "trim") {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
+
// Linear strip — .replace(/[ \t]+$/) is O(n^2) on adversarial untrusted CSV.
|
|
528
|
+
str = safeBuffer.stripTrailingHspace(str);
|
|
529
|
+
} else if (opts.trailingWhitespacePolicy === "reject") {
|
|
530
|
+
// Linear "ends in space/tab?" check — /[ \t]+$/.test is ALSO O(n^2) (the
|
|
531
|
+
// engine scans from every offset when there is no trailing run).
|
|
532
|
+
var lastCode = str.length > 0 ? str.charCodeAt(str.length - 1) : 0;
|
|
533
|
+
if (lastCode === 0x20 || lastCode === 0x09) {
|
|
534
|
+
throw _err("csv.trailing-whitespace", "cell has trailing whitespace");
|
|
535
|
+
}
|
|
527
536
|
}
|
|
528
537
|
|
|
529
538
|
if (typeof value === "number" &&
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* schemaSql: string, // required CREATE TABLE / INDEX script
|
|
45
45
|
* recovery: "refuse" | "rename-and-recreate", // default: "refuse"
|
|
46
46
|
* pragmas: object, // optional extra PRAGMA overrides
|
|
47
|
+
* limits: object, // node:sqlite SQLITE_LIMIT_* caps; default { sqlLength: 1 MiB } (parity with b.db / CLI)
|
|
47
48
|
* audit: boolean, // default: true
|
|
48
49
|
* }) -> { db, prepare, run, query, close, file }
|
|
49
50
|
*
|
|
@@ -55,12 +56,20 @@
|
|
|
55
56
|
|
|
56
57
|
var nodeFs = require("node:fs");
|
|
57
58
|
var nodePath = require("node:path");
|
|
59
|
+
var C = require("./constants");
|
|
58
60
|
var lazyRequire = require("./lazy-require");
|
|
59
61
|
var validateOpts = require("./validate-opts");
|
|
60
62
|
var safeSql = require("./safe-sql");
|
|
61
63
|
var { LocalDbThinError } = require("./framework-error");
|
|
62
64
|
var atomicFile = require("./atomic-file");
|
|
63
65
|
|
|
66
|
+
// Default parse-time statement-size cap, matching b.db and the CLI opener
|
|
67
|
+
// (the v0.15.9 node:sqlite SQLITE_LIMIT_LENGTH floor). prepare()/exec() on the
|
|
68
|
+
// thin path parse operator/application SQL, so the same cap guards it against
|
|
69
|
+
// an attacker-influenced megaquery the parser would otherwise chew (SQLite's
|
|
70
|
+
// default is 1 GB). Operators raise/relax it via opts.limits.
|
|
71
|
+
var _DEFAULT_SQL_LENGTH = C.BYTES.mib(1);
|
|
72
|
+
|
|
64
73
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
65
74
|
|
|
66
75
|
// LRU prepared-statement cache cap — same magnitude as lib/db.js's full
|
|
@@ -99,6 +108,19 @@ function _validateOpts(opts) {
|
|
|
99
108
|
throw new LocalDbThinError("localdb-thin/bad-pragmas",
|
|
100
109
|
"localDb.thin: pragmas must be an object mapping pragma name -> value");
|
|
101
110
|
}
|
|
111
|
+
if (opts.limits !== undefined &&
|
|
112
|
+
(typeof opts.limits !== "object" || opts.limits === null || Array.isArray(opts.limits))) {
|
|
113
|
+
throw new LocalDbThinError("localdb-thin/bad-limits",
|
|
114
|
+
"localDb.thin: limits must be an object of node:sqlite SQLITE_LIMIT_* caps " +
|
|
115
|
+
"(e.g. { sqlLength: 1048576 })");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Merge operator-supplied limits over the framework default (sqlLength cap),
|
|
120
|
+
// so the thin path reaches parity with b.db / the CLI opener while letting an
|
|
121
|
+
// operator raise the cap or add SQLITE_LIMIT_* keys (e.g. attach: 0).
|
|
122
|
+
function _resolveLimits(opts) {
|
|
123
|
+
return Object.assign({ sqlLength: _DEFAULT_SQL_LENGTH }, opts.limits || {});
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
function _runPragmas(database, extra) {
|
|
@@ -175,7 +197,7 @@ function thin(opts) {
|
|
|
175
197
|
var renamedTo = null;
|
|
176
198
|
|
|
177
199
|
function _attemptOpen() {
|
|
178
|
-
var db = new DatabaseSync(file);
|
|
200
|
+
var db = new DatabaseSync(file, { limits: _resolveLimits(opts) });
|
|
179
201
|
_runPragmas(db, opts.pragmas);
|
|
180
202
|
if (!_integrityOk(db)) {
|
|
181
203
|
try { db.close(); } catch (_e) { /* best-effort */ }
|
|
@@ -799,10 +799,23 @@ async function fetchAndVerifyMark(opts) {
|
|
|
799
799
|
|
|
800
800
|
function _splitPemChain(pemText) {
|
|
801
801
|
if (typeof pemText !== "string") return [];
|
|
802
|
+
// Linear indexOf walk over non-overlapping BEGIN..END blocks — NOT
|
|
803
|
+
// /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g. The
|
|
804
|
+
// lazy-quantifier scan re-walks from every BEGIN marker that has no
|
|
805
|
+
// matching END, which is superlinear on a chain of BEGIN-only markers;
|
|
806
|
+
// the indexOf walk advances monotonically and never re-scans.
|
|
807
|
+
var BEGIN = "-----BEGIN CERTIFICATE-----";
|
|
808
|
+
var END = "-----END CERTIFICATE-----";
|
|
802
809
|
var out = [];
|
|
803
|
-
var
|
|
804
|
-
|
|
805
|
-
|
|
810
|
+
var from = 0;
|
|
811
|
+
for (;;) {
|
|
812
|
+
var b = pemText.indexOf(BEGIN, from);
|
|
813
|
+
if (b === -1) break;
|
|
814
|
+
var e = pemText.indexOf(END, b + BEGIN.length);
|
|
815
|
+
if (e === -1) break; // unterminated final block — no further certs
|
|
816
|
+
out.push(pemText.slice(b, e + END.length));
|
|
817
|
+
from = e + END.length;
|
|
818
|
+
}
|
|
806
819
|
return out;
|
|
807
820
|
}
|
|
808
821
|
|
|
@@ -161,11 +161,8 @@ function create(opts) {
|
|
|
161
161
|
|
|
162
162
|
validateOpts.requireNonEmptyString(opts.host, "mail.scan.create.host",
|
|
163
163
|
MailScanError, "mail-scan/bad-host");
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"mail.scan.create.port must be a positive integer in [1,65535]; got " +
|
|
167
|
-
numericBounds.shape(opts.port));
|
|
168
|
-
}
|
|
164
|
+
numericBounds.requirePositiveFiniteInt(opts.port, "mail.scan.create.port",
|
|
165
|
+
MailScanError, "mail-scan/bad-port", { max: 65535 }); // TCP port-number range cap
|
|
169
166
|
var protocol = opts.protocol || DEFAULT_PROTOCOL;
|
|
170
167
|
if (!ALLOWED_PROTOCOLS[protocol]) {
|
|
171
168
|
throw new MailScanError("mail-scan/bad-protocol",
|
|
@@ -313,10 +313,9 @@ function _normalizeRecipientList(value, label) {
|
|
|
313
313
|
label + "[" + i + "] contains forbidden control characters", true);
|
|
314
314
|
}
|
|
315
315
|
// Accept "Name <email@addr>" form too — extract the angle-bracket
|
|
316
|
-
// address for validation; preserve the full
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (!_isValidEmail(addr.trim())) {
|
|
316
|
+
// address for validation (linear, via _extractAddr); preserve the full
|
|
317
|
+
// string in the message.
|
|
318
|
+
if (!_isValidEmail(_extractAddr(arr[i]))) {
|
|
320
319
|
throw new MailError("mail/invalid-recipient",
|
|
321
320
|
label + " '" + arr[i] + "' is not a valid email address", true);
|
|
322
321
|
}
|
|
@@ -423,9 +422,7 @@ function _validateMessage(message) {
|
|
|
423
422
|
throw new MailError("mail/invalid-from",
|
|
424
423
|
"message.from contains forbidden control characters", true);
|
|
425
424
|
}
|
|
426
|
-
|
|
427
|
-
var fromAddr = fromBracket ? fromBracket[1] : message.from;
|
|
428
|
-
if (!_isValidEmail(fromAddr.trim())) {
|
|
425
|
+
if (!_isValidEmail(_extractAddr(message.from))) {
|
|
429
426
|
throw new MailError("mail/invalid-from",
|
|
430
427
|
"message.from '" + message.from + "' is not a valid email address", true);
|
|
431
428
|
}
|
|
@@ -557,8 +554,18 @@ function _mergeMessage(defaults, message) {
|
|
|
557
554
|
|
|
558
555
|
function _extractAddr(s) {
|
|
559
556
|
if (s === undefined || s === null) return s;
|
|
560
|
-
|
|
561
|
-
|
|
557
|
+
s = String(s);
|
|
558
|
+
// Linear angle-bracket extraction — NOT s.match(/<([^>]+)>/), which is O(n^2)
|
|
559
|
+
// in V8 on a long run of '<' with no '>' (the engine retries the greedy
|
|
560
|
+
// [^>]+ from every '<' offset; 200K '<' ~ 11s). Recipient/from addresses on
|
|
561
|
+
// b.mail.send can be caller/request-supplied, so this is a reachable DoS.
|
|
562
|
+
// Mirrors the regex: the chars between the first '<' and the next '>'.
|
|
563
|
+
var lt = s.indexOf("<");
|
|
564
|
+
if (lt !== -1) {
|
|
565
|
+
var gt = s.indexOf(">", lt + 1);
|
|
566
|
+
if (gt > lt + 1) return s.slice(lt + 1, gt).trim();
|
|
567
|
+
}
|
|
568
|
+
return s.trim();
|
|
562
569
|
}
|
|
563
570
|
|
|
564
571
|
function _toArray(v) {
|
|
@@ -435,7 +435,28 @@ var PROMPT_INJECTION_MARKERS = [
|
|
|
435
435
|
"</?(?:assistant|system|user|tool)>",
|
|
436
436
|
];
|
|
437
437
|
var INJECTION_RE = new RegExp(PROMPT_INJECTION_MARKERS.join("|"), "i"); // allow:dynamic-regex — composed from the const PROMPT_INJECTION_MARKERS list above; not operator-supplied input
|
|
438
|
-
|
|
438
|
+
// vbscript: and data:text/html are dangerous URL schemes the guard claims to
|
|
439
|
+
// neutralize but the original alternation omitted (CodeQL js/incomplete-url-
|
|
440
|
+
// scheme-check). data: is scoped to text/html so a benign data:image/png isn't
|
|
441
|
+
// over-redacted. The alternation stays linear (no nested quantifier).
|
|
442
|
+
var DANGEROUS_HTML_RE = /<script\b|<iframe\b|<object\b|<embed\b|javascript:|vbscript:|data:\s*text\/html/i;
|
|
443
|
+
|
|
444
|
+
// Global variants for sanitize-mode redaction. A non-global .replace removes
|
|
445
|
+
// only the LEFTMOST match, so on `data:text/html,<script>alert(1)</script>`
|
|
446
|
+
// the leftmost `data:text/html` would be stripped and the executable
|
|
447
|
+
// `<script>` left behind — sanitize mode returning runnable HTML. _redactAll
|
|
448
|
+
// replaces EVERY dangerous token, repeating to a fixpoint in case removing one
|
|
449
|
+
// abuts two halves into a new one. Input byte-length is bounded by the caller,
|
|
450
|
+
// and [REDACTED] introduces no dangerous token, so the loop terminates quickly.
|
|
451
|
+
var INJECTION_RE_G = new RegExp(PROMPT_INJECTION_MARKERS.join("|"), "gi"); // allow:dynamic-regex — same const-composed source as INJECTION_RE
|
|
452
|
+
var DANGEROUS_HTML_RE_G = /<script\b|<iframe\b|<object\b|<embed\b|javascript:|vbscript:|data:\s*text\/html/gi;
|
|
453
|
+
|
|
454
|
+
function _redactAll(text, globalRe) {
|
|
455
|
+
var out = text;
|
|
456
|
+
var prev;
|
|
457
|
+
do { prev = out; out = out.replace(globalRe, "[REDACTED]"); } while (out !== prev);
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
439
460
|
|
|
440
461
|
function _toolResultSanitize(result, opts) {
|
|
441
462
|
opts = opts || {};
|
|
@@ -475,14 +496,15 @@ function _toolResultSanitize(result, opts) {
|
|
|
475
496
|
if (INJECTION_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
|
|
476
497
|
issues.push({ kind: "prompt-injection", index: i });
|
|
477
498
|
if (posture === "sanitize") {
|
|
478
|
-
// Strip
|
|
479
|
-
//
|
|
480
|
-
t
|
|
499
|
+
// Strip EVERY injection marker — operators wanting structural
|
|
500
|
+
// redaction wire their own classifier. Global fixpoint redact so a
|
|
501
|
+
// second marker after the first isn't left behind.
|
|
502
|
+
t = _redactAll(t, INJECTION_RE_G);
|
|
481
503
|
}
|
|
482
504
|
}
|
|
483
505
|
if (DANGEROUS_HTML_RE.test(regexInput)) { // allow:regex-no-length-cap regexInput byteLength bounded above
|
|
484
506
|
issues.push({ kind: "dangerous-html", index: i });
|
|
485
|
-
if (posture === "sanitize") t = t
|
|
507
|
+
if (posture === "sanitize") t = _redactAll(t, DANGEROUS_HTML_RE_G);
|
|
486
508
|
}
|
|
487
509
|
cleaned.push({ type: "text", text: t });
|
|
488
510
|
} else if (block.type === "image" || block.type === "resource_link" || block.type === "audio") {
|
|
@@ -922,7 +944,7 @@ function _elicitationGuard(opts) {
|
|
|
922
944
|
}
|
|
923
945
|
if (posture === "sanitize") {
|
|
924
946
|
return Object.assign({}, elicitRequest, {
|
|
925
|
-
message: message
|
|
947
|
+
message: _redactAll(message, INJECTION_RE_G),
|
|
926
948
|
});
|
|
927
949
|
}
|
|
928
950
|
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
var defineClass = require("../framework-error").defineClass;
|
|
30
30
|
var lazyRequire = require("../lazy-require");
|
|
31
31
|
var validateOpts = require("../validate-opts");
|
|
32
|
+
var safeBuffer = require("../safe-buffer");
|
|
32
33
|
|
|
33
34
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
34
35
|
|
|
@@ -140,11 +141,12 @@ function create(opts) {
|
|
|
140
141
|
if (typeof ct !== "string" || ct.indexOf("text/html") === -1) return chunk;
|
|
141
142
|
var body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") :
|
|
142
143
|
(typeof chunk === "string" ? chunk : "");
|
|
143
|
-
// Inject after <body> opening tag if present, else
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// Inject after the <body> opening tag if present, else prepend.
|
|
145
|
+
// Linear tag-find — NOT body.match(/<body[^>]*>/i), which is O(n^2)
|
|
146
|
+
// in V8 on a body carrying many `<body` starts with no closing `>`
|
|
147
|
+
// (rendered user content can produce exactly that).
|
|
148
|
+
var idx = safeBuffer.indexAfterOpenTag(body, "body");
|
|
149
|
+
if (idx !== -1) {
|
|
148
150
|
body = body.slice(0, idx) + "\n" + bannerHtml + "\n" + body.slice(idx);
|
|
149
151
|
} else {
|
|
150
152
|
body = bannerHtml + "\n" + body;
|