@blamejs/core 0.13.35 → 0.13.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/cert.js +23 -5
- package/lib/db.js +119 -4
- package/lib/queue-local.js +2 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.37 (2026-05-29) — **Encrypted-mode DB refuses writes before a full tmpfs corrupts it.** In encrypted-at-rest mode the live SQLite working copy is on a tmpfs (Docker's /dev/shm defaults to 64 MiB). If that fills — an append-only audit chain and session rows grow over time — SQLite hits ENOSPC and the working copy is corrupted. Earlier releases made that corruption self-heal on the next boot by rolling back to the last encrypted snapshot, but the rollback still loses writes since the last flush. This adds the prevention side: a periodic free-space probe refuses growth writes (INSERT / UPDATE / REPLACE) with a clear db/storage-low error once free space drops below a threshold, before the tmpfs fills. DELETE and reads stay available so retention can reclaim space and the application keeps serving, and the refusal lifts automatically once free space recovers. The threshold defaults to 16 MiB of headroom and is tunable; the guard is encrypted-mode only. **Security:** *Free-space guard refuses growth writes before the tmpfs working copy fills* — `b.db` in encrypted-at-rest mode now probes free space on the tmpfs holding the working copy and, when it falls below `minFreeBytes` (default 16 MiB), refuses `INSERT` / `UPDATE` / `REPLACE` with a clear `db/storage-low` error instead of letting the write run the mount out of space and corrupt the database. `DELETE`, reads, and DDL stay available so retention can prune and the app keeps serving; the refusal clears automatically when free space recovers. The error message points at the cause (Docker's 64 MiB `/dev/shm` default) and the fix (`shm_size` / `--shm-size`, or pruning). Set `minFreeBytes` to tune the headroom, or `0` to disable. This complements the existing boot-time recovery: prevention (fail clear, keep recent writes) ahead of recovery (roll back to the last snapshot).
|
|
12
|
+
|
|
13
|
+
- v0.13.36 (2026-05-29) — **Certificate renewal trusts the sealed cert's own expiry, not the plaintext index.** The managed-certificate renewal check decided a cached cert was still fresh by reading expiresAt from the plaintext meta.json index that sits beside the sealed cert, rather than from the certificate itself. If that index drifted from — or was tampered relative to — the actual cert (a far-future expiry recorded over a certificate that is in fact near expiry), the manager would skip renewal and keep serving a cert that was about to expire or already had. Renewal now re-derives the expiry and fingerprint from the sealed certificate itself; meta.json is treated as an advisory convenience copy. A sealed cert that no longer parses is re-issued (the same recovery as an unreadable one), and a corrupt meta.json over a valid cert now loads cleanly from the cert instead of forcing a needless re-issue. The local job queue also bounds the size of a job payload it parses back from a stored row, matching the cap the dead-letter listing already used. **Fixed:** *Local job queue bounds the size of a payload parsed back from a stored row* — When the local queue leased a job or re-enqueued a repeating one, it parsed the job payload back from its stored row without an upper size bound, unlike the dead-letter listing which already capped it. A row with an oversized payload (a corrupted or tampered store) could force an unbounded parse. Both paths now cap the parse at the same 64 MiB ceiling the dead-letter path uses. **Security:** *Cert renewal re-derives expiry from the sealed certificate, not the meta.json index* — `b.cert`'s renewal short-circuit read `expiresAt` from the plaintext `meta.json` written beside each sealed cert. Because that index can drift from the actual certificate (or be altered independently of it), a far-future value over an actually-expiring cert would suppress renewal and serve a cert past — or about to pass — its validity. The renewal decision now parses the expiry and fingerprint from the sealed certificate itself on load, so `meta.json` is advisory only. A sealed cert that will not parse is treated as corrupt and re-issued; a corrupt `meta.json` over an otherwise-valid cert loads from the cert without a needless re-issue.
|
|
14
|
+
|
|
11
15
|
- v0.13.35 (2026-05-29) — **In-memory replay, idempotency, DNS, and i18n stores gain entry-count ceilings.** Several framework caches keyed on request-influenced input grew without an upper bound between their periodic sweeps, so a flood of unique keys could exhaust process memory faster than the sweep reclaimed it. Each now enforces a hard entry ceiling. The replay-protection nonce store is the security-sensitive one: rather than evict a live nonce to admit a new one — which would reopen a replay window for the evicted nonce — it purges expired entries and then fails closed at capacity, refusing the unrecordable request instead of admitting it unprotected. The idempotency, DNS, and i18n caches hold re-derivable values, so they evict the oldest entry instead (the worst case is a recomputed value or a single re-executed retry under flood). Ceilings are generous defaults that normal traffic never reaches; the nonce store and the agent idempotency in-memory backend expose options to tune them. **Fixed:** *Agent idempotency in-memory backend no longer grows without bound* — The default in-memory backend for `b.agent.idempotency` is keyed on the request-supplied idempotency key, and its garbage collector only reclaims expired rows when an operator wires a scheduler to call it — so a flood of distinct keys could grow it until the process ran out of memory. It now caps its entry count and evicts oldest-first; a dropped record just means that one key re-executes on a later retry, never a crash. A new `maxInMemoryEntries` option tunes the ceiling (default 100,000); deployments needing a hard guarantee at scale still supply a durable `store`. · *DNS resolver cache is bounded* — The positive and negative resolver caches in `b.network.dns` reclaimed an expired entry only when the same hostname was looked up again, so entries for never-requeried hostnames persisted — and hostnames reaching the resolver are request-influenced (outbound request targets, mail MX lookups). Both caches now cap their entry count and evict oldest-first; DNS simply re-resolves on the next miss. · *i18n formatter cache is bounded* — Per-instance `Intl` formatter caches in `b.i18n` are keyed on the locale plus a hash of the format options. The format-options shape is open-ended and caller-supplied, so the key space was request-influenced and uncapped. The cache now enforces an entry ceiling and evicts oldest-first — a formatter is pure-derived and re-created on the next miss. **Security:** *Replay-nonce store bounds memory and fails closed under a nonce flood* — The in-memory `b.nonceStore` backend recorded every request-supplied nonce until a periodic sweep ran, so a stream of unique nonces could exhaust memory between sweeps (a memory-amplification denial of service). It now caps its entry count. Because a replay-protection store must never evict a live nonce to make room — doing so would reopen a replay window for the evicted nonce — it instead purges expired entries inline and, if still at capacity with live nonces, fails closed: the new request is refused rather than admitted without replay protection. A new `maxEntries` option tunes the ceiling (default 1,000,000).
|
|
12
16
|
|
|
13
17
|
- v0.13.34 (2026-05-29) — **Corrupt TLS certs self-heal at boot, and graceful shutdown no longer loses the final DB flush.** Two failure-mode fixes in the same family as the encrypted-DB recovery in 0.13.33. The cert manager treated a corrupt sealed cert or key worse than a missing one: a missing file re-issues via ACME, but a corrupt one let a raw decrypt error escape out of start(), so the same bad file was read on every boot — an unrecoverable crash loop. A corrupt sealed cert/key is now treated like an absent one and re-issued, and a corrupt derived meta.json is re-derived rather than fatal; the ACME account key (which binds order history) instead fails with an actionable error rather than a raw throw. On the shutdown side, an encrypted database that failed its final re-encrypt used to delete its plaintext working copy anyway, discarding every write since the last periodic flush; it now keeps the working copy so the next boot recovers it. The shutdown orchestrator also gains a hard-deadline watchdog: when the operator delegates signal handling to it, a phase that never settles can no longer hold the process open until the supervisor SIGKILLs it (which would skip the final DB re-encrypt) — the watchdog forces a clean exit, so exit handlers still flush. The wiki production and base compose files set a stop_grace_period above that budget so a docker stop or rolling redeploy lets the re-encrypt finish. **Changed:** *Shutdown watchdog forces a clean, DB-flushing exit if a phase hangs* — The graceful-shutdown orchestrator uses soft per-phase timeouts — on expiry the underlying work keeps running — so a phase that never settles could hold the event loop open past the grace window, after which a container supervisor SIGKILLs the process and skips the final DB re-encrypt. When the operator opts into signal handling, a watchdog now forces `process.exit` `graceMs + forceExitMarginMs` after the signal; exit runs the registered handlers (the DB re-encrypt), so the last flush still happens. A new `forceExitMarginMs` option (default 5000) tunes the headroom; set the container stop grace above `graceMs + forceExitMarginMs`. · *Wiki compose sets stop_grace_period above the shutdown budget* — `examples/wiki/docker-compose.yml` and `docker-compose.prod.yml` now set `stop_grace_period: 40s`. Docker's 10s default would SIGKILL the container before the 30s shutdown budget reaches the DB re-encrypt phase, losing the final flush on a `docker stop` or rolling redeploy. The production note also reminds PaaS platforms that regenerate the compose (Coolify, Dokku, CapRover) to set the stop grace via the platform UI alongside the persistent-storage mount and `--shm-size`. **Fixed:** *A corrupt sealed TLS cert or key re-issues instead of crash-looping at boot* — `b.cert`'s start path read the sealed `cert.pem`/`key.pem` and let a raw unseal/decrypt error escape if the file was truncated or corrupt, so a managed restart read the same bad file on every boot — a crash loop, and worse handling than an absent file (which already re-issues). A corrupt sealed cert/key is now treated like a missing one: it is logged, an audit event is emitted, and the certificate is re-issued via ACME. A corrupt derived `meta.json` is likewise re-derived rather than throwing `cert/bad-meta`. · *Unreadable ACME account key fails with an actionable error, not a raw decrypt throw* — Unlike a re-issuable certificate, the ACME account key binds existing order and authorization history, so it is not silently regenerated on corruption. An unreadable `account/jwk.json.sealed` now raises `cert/account-key-unreadable` naming the file and the recovery (restore from backup, or delete to register a fresh account) instead of letting a raw decrypt/parse error escape out of start(). · *Encrypted DB keeps its working copy when the final shutdown re-encrypt fails* — `db.close()` re-encrypts the tmpfs working copy to `db.enc`, then deletes the working copy. If that final re-encrypt failed (a full `/dev/shm`, a full disk), the delete still ran, discarding every write since the last periodic flush and leaving only the older `db.enc`. The working copy is now kept whenever the re-encrypt fails, so the next boot's integrity-probed recovery picks up the latest writes (and still falls back to `db.enc` if the working copy is itself corrupt). `db.enc` is never modified by this path. **Detectors:** *Cross-artifact guard that stop_grace_period covers the shutdown budget* — A new codebase check fails if either wiki compose file omits `stop_grace_period` or sets it below the orchestrator's `graceMs` plus the watchdog margin read from `lib/app-shutdown.js`, so raising the shutdown budget without bumping the compose — or dropping the setting — cannot silently reopen the SIGKILL-before-re-encrypt data-loss window.
|
package/lib/cert.js
CHANGED
|
@@ -598,14 +598,32 @@ function create(opts) {
|
|
|
598
598
|
var meta = await storage.readMeta(certManifest.name);
|
|
599
599
|
var certBuf = await _readSealedOrReissue(certManifest.name + "/cert.pem", certManifest.name);
|
|
600
600
|
var keyBuf = await _readSealedOrReissue(certManifest.name + "/key.pem", certManifest.name);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
// Base the renewal decision on the SEALED cert's OWN notAfter, not the
|
|
602
|
+
// plaintext meta.json index. meta is a derived convenience copy; if it
|
|
603
|
+
// drifts from — or is tampered relative to — the actual cert (a far-
|
|
604
|
+
// future meta.expiresAt over an actually-expiring cert), trusting it
|
|
605
|
+
// would skip renewal and serve an expired cert. Re-derive expiry +
|
|
606
|
+
// fingerprint from the cert itself; if it won't parse, treat it as a
|
|
607
|
+
// corrupt sealed cert and re-issue (same recovery as an unreadable one).
|
|
608
|
+
var actual = null;
|
|
609
|
+
if (certBuf) {
|
|
610
|
+
try { actual = _certMeta(certBuf.toString("utf8")); }
|
|
611
|
+
catch (e) {
|
|
612
|
+
log.warn("cert: sealed cert for '" + certManifest.name + "' will not parse (" +
|
|
613
|
+
e.message + ") — re-issuing");
|
|
614
|
+
_emitAudit("cert.sealed.corrupt", "recovered",
|
|
615
|
+
{ path: certManifest.name + "/cert.pem", name: certManifest.name });
|
|
616
|
+
certBuf = null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (!forceIssue && actual && certBuf && keyBuf &&
|
|
620
|
+
actual.expiresAt > Date.now() + minDaysBeforeExpiry * C.TIME.days(1)) {
|
|
621
|
+
// Cached, and the cert's own notAfter is comfortably in the future.
|
|
604
622
|
loadedContexts[certManifest.name] = {
|
|
605
623
|
cert: certBuf.toString("utf8"),
|
|
606
624
|
key: keyBuf.toString("utf8"),
|
|
607
|
-
expiresAt:
|
|
608
|
-
fingerprintSha256:
|
|
625
|
+
expiresAt: actual.expiresAt,
|
|
626
|
+
fingerprintSha256: actual.fingerprintSha256,
|
|
609
627
|
sniNames: certManifest.domains.slice(),
|
|
610
628
|
};
|
|
611
629
|
return loadedContexts[certManifest.name];
|
package/lib/db.js
CHANGED
|
@@ -132,6 +132,16 @@ var encPath = null; // encrypted-at-rest path (null in plain mode)
|
|
|
132
132
|
var encKey = null; // DB encryption key buffer (null in plain mode)
|
|
133
133
|
var encTimer = null; // periodic encrypt interval handle
|
|
134
134
|
var atRest = null; // 'encrypted' or 'plain'
|
|
135
|
+
// Tmpfs free-space guard (encrypted mode). The working copy lives on a
|
|
136
|
+
// bounded tmpfs (Docker /dev/shm defaults to 64 MiB); if it fills, SQLite
|
|
137
|
+
// hits ENOSPC and corrupts the working copy. A periodic probe refuses
|
|
138
|
+
// growth writes (INSERT/UPDATE/REPLACE) before that happens — fail-clear
|
|
139
|
+
// instead of corrupt-then-recover. DELETE + reads stay available so
|
|
140
|
+
// retention can reclaim space and the app can keep serving.
|
|
141
|
+
var storageProbeTimer = null; // periodic free-space probe handle
|
|
142
|
+
var writesRefused = false; // true when free space < minFreeBytes
|
|
143
|
+
var minFreeBytes = 0; // refuse growth writes below this (0 = guard off)
|
|
144
|
+
var statfsProbe = null; // free-space reader (fs.statfsSync; injectable for tests)
|
|
135
145
|
var dataDir = null;
|
|
136
146
|
var initialized = false;
|
|
137
147
|
var dataResidency = null; // operator's declared region config (validated by storage backends)
|
|
@@ -737,6 +747,66 @@ function _dbEncAad(dir) {
|
|
|
737
747
|
return Buffer.from("blamejs.db-enc.v1\0" + (dir || ""), "utf8");
|
|
738
748
|
}
|
|
739
749
|
|
|
750
|
+
// Probe free space on the tmpfs holding the working copy and flip the
|
|
751
|
+
// write-refusal flag. Encrypted mode only (the bounded-tmpfs surface);
|
|
752
|
+
// guard disabled when minFreeBytes is 0. A probe failure leaves the flag
|
|
753
|
+
// unchanged — we never refuse writes on a stat error (that would be a
|
|
754
|
+
// self-inflicted outage). Growth writes (INSERT/UPDATE/REPLACE) are gated
|
|
755
|
+
// by the prepare() wrapper installed in init(); DELETE + reads always pass
|
|
756
|
+
// so retention can reclaim space and the app keeps serving.
|
|
757
|
+
function _probeStorageHeadroom() {
|
|
758
|
+
if (atRest !== "encrypted" || !minFreeBytes || !dbPath || !statfsProbe) return;
|
|
759
|
+
var free;
|
|
760
|
+
try {
|
|
761
|
+
var st = statfsProbe(nodePath.dirname(dbPath));
|
|
762
|
+
free = st.bavail * st.bsize;
|
|
763
|
+
if (!isFinite(free)) return;
|
|
764
|
+
} catch (_e) { return; }
|
|
765
|
+
if (free < minFreeBytes && !writesRefused) {
|
|
766
|
+
writesRefused = true;
|
|
767
|
+
log.error("storage low: " + free + " bytes free on the tmpfs working-copy mount (< " +
|
|
768
|
+
minFreeBytes + ") — refusing growth writes (INSERT/UPDATE/REPLACE) until space " +
|
|
769
|
+
"recovers. Raise shm_size / --shm-size, or let retention prune. DELETE + reads still serve.");
|
|
770
|
+
try {
|
|
771
|
+
audit.safeEmit({ action: "db.storage.low", outcome: "failure",
|
|
772
|
+
metadata: { freeBytes: free, minFreeBytes: minFreeBytes } });
|
|
773
|
+
} catch (_e2) { /* drop-silent — observability */ }
|
|
774
|
+
} else if (free >= minFreeBytes && writesRefused) {
|
|
775
|
+
writesRefused = false;
|
|
776
|
+
log("storage recovered: " + free + " bytes free — growth writes re-enabled");
|
|
777
|
+
try {
|
|
778
|
+
audit.safeEmit({ action: "db.storage.recovered", outcome: "success",
|
|
779
|
+
metadata: { freeBytes: free } });
|
|
780
|
+
} catch (_e3) { /* drop-silent */ }
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Install the growth-write gate on the SQLite handle: shadow prepare() so
|
|
785
|
+
// INSERT/UPDATE/REPLACE statements throw db/storage-low when the tmpfs is
|
|
786
|
+
// critically low, instead of proceeding into an ENOSPC corruption. Reads,
|
|
787
|
+
// DELETE, PRAGMA, and DDL pass through ungated. Called once in init() after
|
|
788
|
+
// schema setup so init's own writes are never gated (writesRefused is false
|
|
789
|
+
// until the first probe anyway).
|
|
790
|
+
function _installWriteGate() {
|
|
791
|
+
var rawPrepare = database.prepare.bind(database);
|
|
792
|
+
database.prepare = function (sql) {
|
|
793
|
+
var stmt = rawPrepare(sql);
|
|
794
|
+
if (/^\s*(?:INSERT|UPDATE|REPLACE)\b/i.test(sql)) {
|
|
795
|
+
var rawRun = stmt.run.bind(stmt);
|
|
796
|
+
stmt.run = function () {
|
|
797
|
+
if (writesRefused) {
|
|
798
|
+
throw _dbErr("db/storage-low",
|
|
799
|
+
"db: refusing write — the encrypted-mode working copy is on a tmpfs with less than " +
|
|
800
|
+
minFreeBytes + " bytes free (Docker /dev/shm defaults to 64 MiB). Raise shm_size / " +
|
|
801
|
+
"--shm-size, or let retention prune expired rows. DELETE and reads remain available.");
|
|
802
|
+
}
|
|
803
|
+
return rawRun.apply(stmt, arguments);
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return stmt;
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
740
810
|
function encryptToDisk() {
|
|
741
811
|
if (!encPath) return;
|
|
742
812
|
// Force WAL checkpoint so the .db file holds all committed transactions.
|
|
@@ -915,11 +985,11 @@ async function init(opts) {
|
|
|
915
985
|
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
|
|
916
986
|
|
|
917
987
|
// D-H7 — if the resolved tmpDir is NOT actually tmpfs, the
|
|
918
|
-
// plaintext DB file lives on persistent storage.
|
|
919
|
-
//
|
|
920
|
-
// resolves under /dev/shm or /run/shm as a heuristic. On other
|
|
988
|
+
// plaintext DB file lives on persistent storage. We check that tmpDir
|
|
989
|
+
// resolves under /dev/shm or /run/shm on Linux as a heuristic; on other
|
|
921
990
|
// platforms we warn that the operator must verify tmpfs binding
|
|
922
|
-
// out-of-band.
|
|
991
|
+
// out-of-band. (Free-space headroom is enforced separately via
|
|
992
|
+
// fs.statfsSync in the storage guard below.)
|
|
923
993
|
if (process.platform === "linux") {
|
|
924
994
|
var realTmp = "";
|
|
925
995
|
try { realTmp = nodeFs.realpathSync(tmpDir); } catch (_e) { /* stat best-effort */ }
|
|
@@ -941,6 +1011,21 @@ async function init(opts) {
|
|
|
941
1011
|
dbPath = nodePath.join(tmpDir, "blamejs-" + generateToken(C.BYTES.bytes(16)) + ".db");
|
|
942
1012
|
encKey = loadOrCreateDbKey(dataDir, opts.dbKeyPath);
|
|
943
1013
|
|
|
1014
|
+
// Tmpfs free-space guard. Default headroom is 16 MiB below which growth
|
|
1015
|
+
// writes are refused (fail-clear) before the working copy fills its
|
|
1016
|
+
// bounded tmpfs and corrupts. opts.minFreeBytes tunes it; 0 disables.
|
|
1017
|
+
// opts._statfsForTest injects a free-space reader for tests.
|
|
1018
|
+
if (opts.minFreeBytes !== undefined) {
|
|
1019
|
+
require("./numeric-bounds").requireNonNegativeFiniteIntIfPresent(
|
|
1020
|
+
opts.minFreeBytes, "db.init: opts.minFreeBytes", DbError, "db/bad-min-free-bytes");
|
|
1021
|
+
minFreeBytes = opts.minFreeBytes;
|
|
1022
|
+
} else {
|
|
1023
|
+
minFreeBytes = C.BYTES.mib(16);
|
|
1024
|
+
}
|
|
1025
|
+
statfsProbe = typeof opts._statfsForTest === "function"
|
|
1026
|
+
? opts._statfsForTest
|
|
1027
|
+
: (typeof nodeFs.statfsSync === "function" ? nodeFs.statfsSync : null);
|
|
1028
|
+
|
|
944
1029
|
cleanStaleTmpDbs(tmpDir);
|
|
945
1030
|
decryptToTmp();
|
|
946
1031
|
} else {
|
|
@@ -1337,6 +1422,18 @@ async function init(opts) {
|
|
|
1337
1422
|
}
|
|
1338
1423
|
}, C.TIME.minutes(5), { name: "db-periodic-encrypt" });
|
|
1339
1424
|
|
|
1425
|
+
// Tmpfs free-space guard. Install the growth-write gate now (after all
|
|
1426
|
+
// of init's own writes), then probe on a short interval so the
|
|
1427
|
+
// refuse-writes flag tracks a fast-filling tmpfs (the 5-minute encrypt
|
|
1428
|
+
// cadence is far too coarse to catch a fill in time). The guard is a
|
|
1429
|
+
// no-op when minFreeBytes is 0 or no statfs reader is available.
|
|
1430
|
+
if (minFreeBytes && statfsProbe) {
|
|
1431
|
+
_installWriteGate();
|
|
1432
|
+
_probeStorageHeadroom(); // seed the flag from current free space
|
|
1433
|
+
storageProbeTimer = safeAsync.repeating(_probeStorageHeadroom,
|
|
1434
|
+
C.TIME.seconds(10), { name: "db-storage-probe" });
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1340
1437
|
// Final encrypt on process exit. We don't try to unlink the plaintext
|
|
1341
1438
|
// here — the SQLite handle may still be open, and the OS reclaims tmpfs
|
|
1342
1439
|
// on reboot anyway. close() does the orderly shutdown.
|
|
@@ -1979,6 +2076,11 @@ function close() {
|
|
|
1979
2076
|
encTimer.stop();
|
|
1980
2077
|
encTimer = null;
|
|
1981
2078
|
}
|
|
2079
|
+
if (storageProbeTimer) {
|
|
2080
|
+
storageProbeTimer.stop();
|
|
2081
|
+
storageProbeTimer = null;
|
|
2082
|
+
}
|
|
2083
|
+
writesRefused = false;
|
|
1982
2084
|
// Drop prepared-statement cache so the underlying Statement handles
|
|
1983
2085
|
// release ahead of database.close().
|
|
1984
2086
|
_prepareCache.clear();
|
|
@@ -2503,6 +2605,7 @@ function _cascadeStep(name, ref) {
|
|
|
2503
2605
|
// Test helpers — not part of public contract
|
|
2504
2606
|
function _resetForTest() {
|
|
2505
2607
|
if (encTimer) { encTimer.stop(); encTimer = null; }
|
|
2608
|
+
if (storageProbeTimer) { storageProbeTimer.stop(); storageProbeTimer = null; }
|
|
2506
2609
|
try { if (database) database.close(); }
|
|
2507
2610
|
catch (e) { log.debug("test-reset close failed", { error: e.message }); }
|
|
2508
2611
|
database = null;
|
|
@@ -2511,10 +2614,20 @@ function _resetForTest() {
|
|
|
2511
2614
|
encKey = null;
|
|
2512
2615
|
atRest = null;
|
|
2513
2616
|
dataDir = null;
|
|
2617
|
+
minFreeBytes = 0;
|
|
2618
|
+
statfsProbe = null;
|
|
2619
|
+
writesRefused = false;
|
|
2514
2620
|
initialized = false;
|
|
2515
2621
|
cryptoField.clearForTest();
|
|
2516
2622
|
}
|
|
2517
2623
|
|
|
2624
|
+
// Test seam — force a storage-headroom probe synchronously (the production
|
|
2625
|
+
// path runs it on a 10s timer) and read the resulting refuse-writes flag.
|
|
2626
|
+
function _probeStorageForTest() {
|
|
2627
|
+
_probeStorageHeadroom();
|
|
2628
|
+
return { writesRefused: writesRefused, minFreeBytes: minFreeBytes };
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2518
2631
|
|
|
2519
2632
|
/**
|
|
2520
2633
|
* @primitive b.db.vacuumAfterErase
|
|
@@ -3165,6 +3278,8 @@ module.exports = {
|
|
|
3165
3278
|
_cascadeStep("redact", _resetRedact);
|
|
3166
3279
|
_cascadeStep("external-db", _resetExternalDb);
|
|
3167
3280
|
},
|
|
3281
|
+
// Test seam for the tmpfs free-space guard — force a probe + read the flag.
|
|
3282
|
+
_probeStorageForTest: _probeStorageForTest,
|
|
3168
3283
|
// Helper for audit.checkpoint to write the rollback-detection sidecar
|
|
3169
3284
|
_writeAuditTip: function (tip) {
|
|
3170
3285
|
if (!dataDir) return;
|
package/lib/queue-local.js
CHANGED
|
@@ -88,7 +88,7 @@ function _shapeLeasedRow(raw) {
|
|
|
88
88
|
return {
|
|
89
89
|
jobId: unsealed._id,
|
|
90
90
|
queueName: unsealed.queueName,
|
|
91
|
-
payload: unsealed.payload ? safeJson.parse(unsealed.payload) : null,
|
|
91
|
+
payload: unsealed.payload ? safeJson.parse(unsealed.payload, { maxBytes: C.BYTES.mib(64) }) : null,
|
|
92
92
|
attempts: Number(unsealed.attempts),
|
|
93
93
|
maxAttempts: Number(unsealed.maxAttempts),
|
|
94
94
|
traceId: unsealed.traceId,
|
|
@@ -270,7 +270,7 @@ function create(_config) {
|
|
|
270
270
|
var cron = scheduler.parseCron(unsealedRow.repeatCron);
|
|
271
271
|
var nextMs = scheduler.nextCronFire(cron, new Date(nowMs), unsealedRow.repeatTimezone || null);
|
|
272
272
|
await enqueue(unsealedRow.queueName,
|
|
273
|
-
unsealedRow.payload ? safeJson.parse(unsealedRow.payload) : null,
|
|
273
|
+
unsealedRow.payload ? safeJson.parse(unsealedRow.payload, { maxBytes: C.BYTES.mib(64) }) : null,
|
|
274
274
|
{
|
|
275
275
|
// availableAt is the precise next-fire ms — pass it alone.
|
|
276
276
|
// Don't also pass delaySeconds (the v0.6.22 / v0.6.23 fix
|
package/package.json
CHANGED
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:
|
|
5
|
+
"serialNumber": "urn:uuid:67acf3f3-dbcf-463e-a7d2-58115594e0d6",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-29T14:52:03.887Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.37",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.37",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.37",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.13.37",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|