@blamejs/blamejs-shop 0.4.48 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/inventory-audits.js +5 -0
  4. package/lib/inventory-locations.js +66 -11
  5. package/lib/inventory-writeoffs.js +4 -0
  6. package/lib/vendor/MANIFEST.json +58 -46
  7. package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
  8. package/lib/vendor/blamejs/.gitignore +5 -1
  9. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  10. package/lib/vendor/blamejs/README.md +1 -1
  11. package/lib/vendor/blamejs/SECURITY.md +3 -1
  12. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  13. package/lib/vendor/blamejs/lib/bundler.js +2 -7
  14. package/lib/vendor/blamejs/lib/config-drift.js +17 -3
  15. package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
  16. package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
  17. package/lib/vendor/blamejs/lib/db-schema.js +29 -0
  18. package/lib/vendor/blamejs/lib/db.js +7 -0
  19. package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
  20. package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
  21. package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
  22. package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
  23. package/lib/vendor/blamejs/lib/mail.js +16 -9
  24. package/lib/vendor/blamejs/lib/mcp.js +28 -6
  25. package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
  26. package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
  27. package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
  28. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
  29. package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
  30. package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
  31. package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
  32. package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
  33. package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
  34. package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
  35. package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
  36. package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
  37. package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
  38. package/lib/vendor/blamejs/lib/seeders.js +33 -39
  39. package/lib/vendor/blamejs/lib/storage.js +71 -7
  40. package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
  41. package/lib/vendor/blamejs/package.json +1 -1
  42. package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
  43. package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
  44. package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
  45. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
  46. package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
  50. package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
  55. package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
  56. package/package.json +1 -1
@@ -82,7 +82,7 @@ jobs:
82
82
  # runtime deps (everything vendored under lib/vendor/) and each
83
83
  # transpilation step is a supply-chain hop. The explicit
84
84
  # name@version specifiers are the source-of-truth pin.
85
- run: npm install --no-audit --no-fund --no-save esbuild@0.28.0 postject@1.0.0-alpha.6
85
+ run: npm install --no-audit --no-fund --no-save esbuild@0.28.1 postject@1.0.0-alpha.6
86
86
 
87
87
  - name: Run framework smoke
88
88
  # NODE_OPTIONS heap bump — codebase-patterns duplicate-block
@@ -286,6 +286,139 @@ jobs:
286
286
  run: |
287
287
  gitleaks git --config=.gitleaks.toml --redact --verbose --exit-code=1
288
288
 
289
+ osv-scan:
290
+ name: OSV-Scanner (vendored-dep known CVEs)
291
+ runs-on: ubuntu-latest
292
+ steps:
293
+ - name: Checkout
294
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
295
+ with:
296
+ persist-credentials: false
297
+
298
+ - name: Set up Node 24.16.0 LTS
299
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
300
+ with:
301
+ node-version: '24.16.0'
302
+
303
+ - name: Build vendored-deps CycloneDX SBOM
304
+ # The framework ships ZERO npm runtime deps — its security-critical
305
+ # crypto (@noble/ciphers, @noble/curves, @noble/post-quantum,
306
+ # @simplewebauthn/server, peculiar-pki) is VENDORED under
307
+ # lib/vendor/, so `npm audit` / Dependabot / Socket never see it.
308
+ # check-vendor-currency.js tracks whether the vendored copies are
309
+ # up to date, but currency is not the same as "no known CVE". This
310
+ # SBOM emits each vendored library as an npm purl so a published
311
+ # advisory can be matched against the exact pinned version we ship.
312
+ run: node scripts/build-vendored-sbom.js > sbom.vendored.cdx.json
313
+
314
+ - name: Install osv-scanner
315
+ # Same posture as the gitleaks gate: pull the OSS binary straight
316
+ # from GitHub releases tracking the latest tag — a CVE-database
317
+ # front-end is a forward-looking security gate, and pinning would
318
+ # silently miss new advisories the scanner learns to surface.
319
+ # Authenticate the API call so the shared-runner-IP unauthenticated
320
+ # rate limit (60/hour) doesn't 403 the install mid-run.
321
+ env:
322
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
323
+ run: |
324
+ set -euo pipefail
325
+ OSV_VERSION=$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" \
326
+ https://api.github.com/repos/google/osv-scanner/releases/latest \
327
+ | grep -oP '"tag_name":\s*"v\K[^"]+' | head -1)
328
+ if [ -z "$OSV_VERSION" ]; then
329
+ echo "::error::Could not resolve latest osv-scanner release tag from GitHub API"
330
+ exit 1
331
+ fi
332
+ echo "[osv-scanner] resolved latest release: v${OSV_VERSION}"
333
+ curl -fsSL "https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}/osv-scanner_linux_amd64" \
334
+ -o /tmp/osv-scanner
335
+ chmod +x /tmp/osv-scanner
336
+ sudo mv /tmp/osv-scanner /usr/local/bin/osv-scanner
337
+ osv-scanner --version
338
+
339
+ - name: Scan vendored deps for known CVEs
340
+ # Exit 1 on any advisory match. A hit means a published CVE/GHSA
341
+ # affects a version we vendor — refresh the vendored copy (the
342
+ # vendor-currency job points at the upstream release) and re-run.
343
+ run: osv-scanner scan -L sbom.vendored.cdx.json
344
+
345
+ semgrep:
346
+ name: Semgrep (complementary SAST)
347
+ runs-on: ubuntu-latest
348
+ steps:
349
+ - name: Checkout
350
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
351
+ with:
352
+ persist-credentials: false
353
+
354
+ - name: Install Semgrep
355
+ # OSS CLI from PyPI tracking latest — same forward-looking posture
356
+ # as the gitleaks / osv-scanner gates (a SAST ruleset is a moving
357
+ # security target). pipx ships on the ubuntu-latest runner image
358
+ # and installs into an isolated venv, sidestepping PEP-668.
359
+ run: |
360
+ set -euo pipefail
361
+ pipx install semgrep
362
+ semgrep --version
363
+
364
+ - name: Run Semgrep (curated security packs, ERROR severity)
365
+ # Complementary to CodeQL: the Registry security-audit + javascript
366
+ # packs are a different rule set. Scoped to the shipped framework
367
+ # surface (lib/) at ERROR severity — the high-confidence subset —
368
+ # and fails the job on any match (--error). Structural-drift
369
+ # classes a generic SAST rule can't express are covered by the
370
+ # in-repo detector system (codebase-patterns.test.js). A confirmed
371
+ # false positive is silenced with an inline `// nosemgrep: <rule>`
372
+ # carrying the reason, mirroring the detector-allowlist discipline.
373
+ run: |
374
+ semgrep scan \
375
+ --config=p/security-audit \
376
+ --config=p/javascript \
377
+ --severity=ERROR \
378
+ --error \
379
+ --metrics=off \
380
+ --disable-version-check \
381
+ lib/
382
+
383
+ lint-actions:
384
+ name: actionlint (workflow lint)
385
+ runs-on: ubuntu-latest
386
+ steps:
387
+ - name: Checkout
388
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
389
+ with:
390
+ persist-credentials: false
391
+
392
+ - name: Install actionlint
393
+ # OSS binary from GitHub releases (latest), same posture as the
394
+ # gitleaks / osv-scanner gates. actionlint statically checks the
395
+ # workflow YAML for the bug class this project has tripped on — a
396
+ # step referencing a value drifted out of agreement with a pinned
397
+ # tool version, a mistyped expression, an unsafe ${{ }}
398
+ # interpolation of untrusted input into a run block.
399
+ env:
400
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
401
+ run: |
402
+ set -euo pipefail
403
+ AL_VERSION=$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" \
404
+ https://api.github.com/repos/rhysd/actionlint/releases/latest \
405
+ | grep -oP '"tag_name":\s*"v\K[^"]+' | head -1)
406
+ if [ -z "$AL_VERSION" ]; then
407
+ echo "::error::Could not resolve latest actionlint release tag from GitHub API"
408
+ exit 1
409
+ fi
410
+ echo "[actionlint] resolved latest release: v${AL_VERSION}"
411
+ curl -fsSL "https://github.com/rhysd/actionlint/releases/download/v${AL_VERSION}/actionlint_${AL_VERSION}_linux_amd64.tar.gz" \
412
+ | tar -xz -C /tmp actionlint
413
+ sudo mv /tmp/actionlint /usr/local/bin/actionlint
414
+ actionlint --version
415
+
416
+ - name: Run actionlint
417
+ # -color for a readable run log. actionlint shells out to the
418
+ # shellcheck already on the runner for `run:` blocks, so a shell
419
+ # bug inside a workflow step is caught here too.
420
+ run: actionlint -color
421
+
289
422
  wiki-source-comment-blocks:
290
423
  name: Wiki @module / @primitive comment-block convention
291
424
  runs-on: ubuntu-latest
@@ -98,8 +98,12 @@ scripts/gen-changelog.js
98
98
 
99
99
  # CycloneDX SBOM — generated by the npm-publish workflow at publish
100
100
  # time and bundled into the tarball; never committed. Local `npm sbom`
101
- # runs would otherwise leave the file lying around.
101
+ # runs would otherwise leave the file lying around. The vendored-deps
102
+ # variant is generated the same way (release time) and additionally by
103
+ # the OSV-Scanner CI job, which feeds it to the known-CVE scan and then
104
+ # discards it — never committed.
102
105
  sbom.cdx.json
106
+ sbom.vendored.cdx.json
103
107
 
104
108
  # OTel collector readback (integration-test fixture) — the collector writes
105
109
  # received OTLP batches here for the egress-redaction test to read back; the
@@ -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.9",
4
- "createdAt": "2026-06-13T22:58:01.805Z",
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
- if (!numericBounds.isPositiveFiniteInt(opts.hashLen) ||
270
- opts.hashLen < MIN_HASH_LEN || opts.hashLen > MAX_HASH_LEN) {
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 (defaults to cwd-relative)
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
- var libVendorDir = opts.libVendorDir || nodePath.join(process.cwd(), "lib", "vendor");
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
- var abs = nodePath.isAbsolute(rel) ? rel : nodePath.join(process.cwd(), rel);
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
- if (!rows[0].relrowsecurity) {
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
- return line.replace(/[ \t]+$/g, "");
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
- str = str.replace(/[ \t]+$/g, "");
525
- } else if (opts.trailingWhitespacePolicy === "reject" && /[ \t]+$/.test(str)) {
526
- throw _err("csv.trailing-whitespace", "cell has trailing whitespace");
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 re = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
804
- var m;
805
- while ((m = re.exec(pemText)) !== null) out.push(m[0]);
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