@blamejs/core 0.13.32 → 0.13.33

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.13.x
10
10
 
11
+ - v0.13.33 (2026-05-28) — **Encrypted-mode DB recovers from a corrupt tmpfs working copy instead of crash-looping.** In encrypted-at-rest mode the live SQLite copy is decrypted into a tmpfs working file and re-encrypted to db.enc periodically. If that working copy was corrupted (an unclean shutdown, or a full tmpfs — Docker's /dev/shm defaults to 64 MiB), the boot path trusted it because its mtime was newer than db.enc, so db.init failed its integrity gate with "database disk image is malformed" identically on every boot — an unrecoverable crash loop. db now integrity-probes the newer working copy before trusting it: if it is unreadable, the working copy is discarded and db.enc (the last-good encrypted snapshot) is re-decrypted, so the next boot self-heals. db.enc is never modified by this path, and a genuinely-corrupt db.enc still fails loudly rather than wiping data. The boot error on an unreadable database is now actionable (it names the tmpfs-size cause and the recovery). The wiki production compose also gains the storage settings encrypted mode needs. **Fixed:** *Corrupt tmpfs working copy no longer causes a boot crash loop (encrypted-at-rest mode)* — `db.init`'s crash-recovery path preferred a newer tmpfs working copy over `db.enc` unconditionally. When that copy was corrupt (truncated by an unclean shutdown or a full `/dev/shm`), every boot trusted it and failed the integrity gate the same way — an unrecoverable loop. The newer working copy is now integrity-probed (`PRAGMA quick_check`); if it is unreadable it is discarded and `db.enc` — the last-good encrypted snapshot — is re-decrypted, so boot self-heals. `db.enc` is never modified, so this only ever rolls back to the persistent copy; if `db.enc` is also corrupt, boot still fails loudly (no silent data loss). A regression test pins the recovery. · *Actionable boot error when the database is unreadable* — When SQLite reports a database too corrupt to even run an integrity check, the boot error now names the likely cause and recovery instead of surfacing the raw "database disk image is malformed": in encrypted mode it points at the tmpfs working copy and the most common operational cause (Docker's 64 MiB `/dev/shm` default — raise it via `shm_size` / `--shm-size`), or restoring `db.enc` / the DB file from backup. · *Wiki production compose ships the storage encrypted mode needs* — `examples/wiki/docker-compose.prod.yml` now sets `shm_size: '512m'` (so the encrypted-mode tmpfs working copy has headroom above Docker's 64 MiB default) and mounts a persistent `wiki-data` volume at `/data` (so `db.enc` + sealed keys survive container recreate, host reboot, and image redeploys, and give a restore point). A note flags that PaaS platforms which regenerate the compose on deploy (Coolify, Dokku, CapRover, …) must set both via the platform UI — a persistent-storage mount for `/data` and a `--shm-size 512m` custom option.
12
+
11
13
  - v0.13.32 (2026-05-28) — **`b.auditDailyReview` enforces notify under the sox-404 posture; compliance doc corrections.** b.auditDailyReview documented `sox-404` (SOX §404 ICFR — the internal-controls regime this primitive serves) as one of the postures that make a `notify` callback mandatory at construction, but the enforcement set used only `sox`, so pinning `posture: "sox-404"` without a notify channel was silently accepted. `sox-404` is now in the mandatory-notify set, so the advertised guarantee holds (a regression test pins it). The rest are documentation corrections with no behavior change: b.compliance.posturesByDomain / posturesByJurisdiction examples showed small fixed arrays where the functions return every matching posture (the catalog has grown); b.dataAct's surface list named two methods that do not exist (the real surface is declareProduct / recordUserAccess / shareWithThirdParty / recordSwitchRequest, with gatekeeper refusal folded into shareWithThirdParty); b.secCyber.eightKArtifact's documented return key `audit` is actually `deadlineBusinessDays`; and b.compliance.aiAct.transparency's helper summary named `cspMetaTag` / a `watermark({ kind })` argument that are really `metaTags` / `watermark({ mediaKind })`. **Fixed:** *`b.auditDailyReview` requires a notify channel under the `sox-404` posture* — The docs listed `sox-404` among the postures that make a `notify` callback mandatory at create-time, but the enforcement set contained only `sox` — so `posture: "sox-404"` without `notify` was accepted instead of refused. `sox-404` (SOX §404 ICFR) is now in the mandatory-notify set, matching the documented guarantee; constructing without a notify channel under it throws `auditDailyReview/notify-required-under-posture`. · *`b.compliance` jurisdiction/domain lister examples no longer enumerate a stale fixed set* — `posturesByDomain` and `posturesByJurisdiction` return every posture matching the domain/jurisdiction, but their `@example`s showed small fixed arrays from before the posture catalog grew. The examples now show a representative prefix with `...` and note they return the full matching set. · *`b.dataAct` surface list matches the real methods* — The module surface listed `userAccessible(...)` and `refuseGatekeeper(...)`, neither of which exists. The real surface is `declareProduct` / `recordUserAccess` / `shareWithThirdParty` / `recordSwitchRequest`, and DMA-gatekeeper refusal (Art 32 §1) is enforced inside `shareWithThirdParty`. The doc now reflects that. · *`b.secCyber.eightKArtifact` documented return shape corrected* — The signature line showed `{ artifact, deadline, audit }`; the function returns `{ artifact, deadline, deadlineBusinessDays }` (there is no `audit` key). The doc now matches. · *`b.compliance.aiAct.transparency` helper names corrected* — The helper summary named a `cspMetaTag(...)` function and a `watermark({ kind })` argument; the real names are `metaTags(...)` and `watermark({ mediaKind })`. Calling the documented names threw. Also corrected: a `b.aiAdverseDecision` illustration showed an ECOA `statutoryDeadlines` shape that didn't match the regime's actual deadlines.
12
14
 
13
15
  - v0.13.31 (2026-05-28) — **Circuit-breaker onStateChange callback now fires; mcp / vault-aad doc corrections.** b.circuitBreaker documented an `onStateChange` callback (both an option and an `onStateChange(handler)` registration method) plus a state-change payload, but the callback was never invoked — only an observability event fired. The callback is now implemented: it fires on every transition with `{ name, from, to, at }`, the registration method works, and a non-function handler is rejected at construction. The same primitive's docs are corrected to name the real accessor (`getState()`, not `state()`) and drop a never-read `audit` option. Plus two doc-only corrections: b.mcp.toolResult.sanitize described composing b.guardHtml / b.ai.input.classify (it uses built-in detection) and documented a `classifyInput` option it never read; and b.vault.aad's prose said HKDF-SHAKE256 where the derivation is SHAKE256 (the AEAD AAD-binding itself is unchanged and sound). **Fixed:** *`b.circuitBreaker` onStateChange callback is invoked on every transition* — The `onStateChange` option and the `onStateChange(handler)` registration method are now wired: each registered handler is called with `{ name, from, to, at }` on every state transition (closed→open, open→half, half→closed/open), alongside the existing `breaker.state.change` observability event. A non-function `onStateChange` is rejected at construction. Previously the documented callback never fired. The docs are also corrected to name the real state accessor `getState()` (there is a `state` property, so `state()` was never a method) and to drop a never-read `audit` option. · *`b.mcp.toolResult.sanitize` documents its actual detection and options* — The prose said the sanitizer composes `b.guardHtml`'s strict profile and `b.ai.input.classify`; it uses built-in dangerous-HTML and prompt-injection-marker detection. The `@opts` also listed a `classifyInput` override the function never read. The prose now describes the built-in detection and the unwired `classifyInput` option is removed. The fail-closed refusal behavior (default `posture: "refuse"`) is unchanged. · *`b.vault.aad` derivation named correctly (SHAKE256)* — The module prose described the per-binding key derivation as HKDF-SHAKE256; it is SHAKE256 over the vault root concatenated with the binding inputs (no HKDF extract/expand). The AEAD AAD-binding to (table, row, column, schema version) — the file's actual security guarantee — is unchanged and sound; only the KDF name in the doc was wrong.
package/lib/db.js CHANGED
@@ -672,13 +672,30 @@ function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
672
672
  function decryptToTmp() {
673
673
  if (!encPath || !nodeFs.existsSync(encPath)) return;
674
674
  // If a plaintext file already exists in tmpfs from a prior process, prefer
675
- // the newer mtime (crash recovery — operator's most recent state wins).
675
+ // the newer mtime (crash recovery — operator's most recent state wins)
676
+ // but ONLY if it is a readable SQLite file. A working copy corrupted by
677
+ // an unclean shutdown or a full tmpfs (e.g. Docker's 64 MiB /dev/shm
678
+ // default overflowing) would otherwise be kept on every boot, and
679
+ // db.init's integrity gate would fail identically forever — an
680
+ // unrecoverable crash loop. When the newer plaintext fails a fast
681
+ // integrity probe, discard it and fall through to re-decrypt the
682
+ // last-good db.enc snapshot. db.enc itself is never modified here, so
683
+ // this only ever rolls back to the persistent encrypted copy; if THAT
684
+ // is also corrupt, db.init still fails loudly (no silent data loss).
676
685
  if (nodeFs.existsSync(dbPath)) {
677
686
  var plainStat = nodeFs.statSync(dbPath);
678
687
  var encStat = nodeFs.statSync(encPath);
679
688
  if (plainStat.mtimeMs > encStat.mtimeMs && plainStat.size > 0) {
680
- log("plaintext is newer than encrypted — keeping plaintext (crash recovery)");
681
- return;
689
+ if (_tmpWorkingCopyIsHealthy(dbPath)) {
690
+ log("plaintext is newer than encrypted — keeping plaintext (crash recovery)");
691
+ return;
692
+ }
693
+ log("newer tmpfs working copy failed its integrity probe (corrupt — likely an " +
694
+ "unclean shutdown or a full /dev/shm); discarding it and re-decrypting from " +
695
+ "db.enc (auto-recovery to the last-good encrypted snapshot)");
696
+ try { nodeFs.unlinkSync(dbPath); } catch (_e) { /* fall through to overwrite */ }
697
+ try { nodeFs.unlinkSync(dbPath + "-wal"); } catch (_e) { /* may not exist */ }
698
+ try { nodeFs.unlinkSync(dbPath + "-shm"); } catch (_e) { /* may not exist */ }
682
699
  }
683
700
  }
684
701
  var packed = nodeFs.readFileSync(encPath);
@@ -696,6 +713,26 @@ function decryptToTmp() {
696
713
  }
697
714
  }
698
715
 
716
+ // Fast "is this a usable SQLite file" probe for the crash-recovery path.
717
+ // Opens the candidate working copy and runs PRAGMA quick_check(1) (far
718
+ // cheaper than full integrity_check — header + page-structure sanity,
719
+ // enough to catch a "database disk image is malformed" / truncated /
720
+ // non-DB file). Any throw (malformed image, not-a-DB) or non-"ok" result
721
+ // is unhealthy. The probe handle is always closed so it never holds the
722
+ // tmpfs file open against the subsequent real open.
723
+ function _tmpWorkingCopyIsHealthy(p) {
724
+ var probe = null;
725
+ try {
726
+ probe = new DatabaseSync(p);
727
+ var rows = probe.prepare("PRAGMA quick_check(1)").all();
728
+ return rows.length >= 1 && rows[0] && rows[0].quick_check === "ok";
729
+ } catch (_e) {
730
+ return false;
731
+ } finally {
732
+ if (probe) { try { probe.close(); } catch (_e2) { /* already gone */ } }
733
+ }
734
+ }
735
+
699
736
  function _dbEncAad(dir) {
700
737
  return Buffer.from("blamejs.db-enc.v1\0" + (dir || ""), "utf8");
701
738
  }
@@ -961,7 +998,25 @@ async function init(opts) {
961
998
  // the freshly-decrypted-into-tmpfs file (<1 second on a typical
962
999
  // multi-MB DB) and the result is "ok" or a list of issues.
963
1000
  if (opts.skipBootIntegrityCheck !== true) {
964
- var ic = database.prepare("PRAGMA integrity_check").all();
1001
+ var ic;
1002
+ try {
1003
+ ic = database.prepare("PRAGMA integrity_check").all();
1004
+ } catch (corruptErr) {
1005
+ // SQLite throws "database disk image is malformed" / "file is not a
1006
+ // database" when the file is too corrupt to even run the check.
1007
+ // Translate the raw native error into an actionable one — the most
1008
+ // common operational cause in encrypted mode is a too-small tmpfs.
1009
+ throw new DbError("db/integrity-check-failed",
1010
+ "database is corrupt at boot — SQLite: " +
1011
+ ((corruptErr && corruptErr.message) || String(corruptErr)) + ". " +
1012
+ (atRest === "encrypted"
1013
+ ? "Encrypted mode runs the live DB as a tmpfs working copy (" + dbPath +
1014
+ "); a recurring failure here usually means the tmpfs is too small " +
1015
+ "(Docker's /dev/shm defaults to 64 MiB — raise it via shm_size / " +
1016
+ "--shm-size), or db.enc itself is corrupt (restore <dataDir>/db.enc " +
1017
+ "from backup)."
1018
+ : "Restore the database file (" + dbPath + ") from backup."));
1019
+ }
965
1020
  var icIssues = ic.map(function (r) { return r && r.integrity_check; })
966
1021
  .filter(function (s) { return s && s !== "ok"; });
967
1022
  if (icIssues.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.32",
3
+ "version": "0.13.33",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:14f840f1-29a7-4656-93d0-e5fa6ece6ae2",
5
+ "serialNumber": "urn:uuid:b63cfcae-7595-4c46-9fd5-8d4a36ad0b85",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-29T02:13:04.485Z",
8
+ "timestamp": "2026-05-29T04:39:40.351Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.13.32",
22
+ "bom-ref": "@blamejs/core@0.13.33",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.13.32",
25
+ "version": "0.13.33",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.13.32",
29
+ "purl": "pkg:npm/%40blamejs/core@0.13.33",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.13.32",
57
+ "ref": "@blamejs/core@0.13.33",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]