Backup
PQC-encrypted backup bundles — sealed columns + audit chain + keyring. SLH-DSA signature on every bundle, kid pinning, restore validates signature against operator-pinned public key.
The namespace wires b.backupBundle.create (encrypt + emit a bundle directory) to a pluggable storage backend, plus retention policy + audit emission. Ships with a local-filesystem backend (b.backup.diskStorage); S3 or any custom backend drops in through the same interface.
Storage backend contract:
{ async writeBundle(bundleId, sourceDir), async readBundle(bundleId, destDir), async listBundles(), // → [{ bundleId, createdAt, size }] async deleteBundle(bundleId), async hasBundle(bundleId), }
vaultKeyJson can be a string (the operator has the JSON in hand) or a function returning a string (or async returning a string) — the framework calls it each backup so a long-running app doesn't pin the vault key in memory between runs.
Bundle IDs are filesystem-safe timestamps with millisecond precision plus a 4-byte random suffix: 2026-04-27T14-00-00-123Z-a8f30b21. Colons + dots in standard ISO-8601 are replaced with dashes so the id works as a directory name on every platform (Windows reserves : for drive letters). String sort still gives chronological order.
Posture-enforced encryption: HIPAA / PCI-DSS postures refuse a pipeline created with encrypt: false. Posture-enforced residency: gdpr / uk-gdpr / dpdp / pipl-cn / lgpd-br / appi-jp / pdpa-sg refuse a destination tag that doesn't match the live DB residency unless the operator passes allowCrossBorder: true with a documented legalBasis.
b.backup.diskStorage(opts) #
{
root: string, // required; directory under which bundle dirs land
}
Local-filesystem storage backend implementing the { writeBundle, readBundle, listBundles, deleteBundle, hasBundle } contract. Bundles land as directories named by bundle id under opts.root. Newest-first ordering is enforced by reverse lexicographic sort on the timestamp-prefixed bundle id.
Operators pointing at S3 / GCS / Azure Blob / a tape gateway pass a custom backend matching the same shape; the engine never touches the filesystem directly.
var fs = require("node:fs");
var path = require("node:path");
var os = require("node:os");
var root = fs.mkdtempSync(path.join(os.tmpdir(), "backup-root-"));
var storage = b.backup.diskStorage({ root: root });
storage.name; // → "local"
typeof storage.writeBundle; // → "function"
typeof storage.listBundles; // → "function"
b.backup.create(opts) #
{
dataDir: string, // required; must exist on disk
storage: StorageBackend, // required; diskStorage() or custom
passphrase: Buffer | string, // required; KEK for per-file Argon2id wrap
files: Array<{ relativePath, kind, required }>,
vaultKeyJson: string | () => string | Promise,
retention: { keep: number }, // optional; sweep older bundles after run()
audit: boolean, // default true
scheduler: b.scheduler, // required for schedule() / scheduleTest()
flushBeforeBackup: false | () => void | Promise,
requireFlush: boolean, // default false
encrypt: boolean, // default true; refused under hipaa / pci-dss
residencyTag: string | null, // e.g. "EU"; checked against b.db.getDataResidency()
allowCrossBorder: boolean, // explicit override for residency mismatch
legalBasis: string, // recorded in audit chain when allowCrossBorder
}
Build a backup engine bound to a data directory, a storage backend, the operator's passphrase, and an include list. Returns an object with run / list / delete / read / purgeOlder / schedule / scheduleTest plus the wired storage reference.
Each run() produces a fresh bundle id (), stages encryption to a process-private tmpdir, writes through storage.writeBundle, sweeps tmpdir, then applies retention. Audit events backup.success / backup.failure / backup.retention.swept land on b.audit when opts.audit !== false.
Posture gates fire at create() time, not run() time — so a misconfigured pipeline refuses to construct rather than producing one good bundle and then failing the next.
var fs = require("node:fs");
var path = require("node:path");
var os = require("node:os");
var dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "backup-data-"));
var root = fs.mkdtempSync(path.join(os.tmpdir(), "backup-root-"));
fs.writeFileSync(path.join(dataDir, "db.enc"), Buffer.from([1, 2, 3]));
fs.writeFileSync(path.join(dataDir, "db.key.enc"), Buffer.from([4, 5, 6]));
var engine = b.backup.create({
dataDir: dataDir,
storage: b.backup.diskStorage({ root: root }),
passphrase: Buffer.from("operator backup passphrase"),
files: [
{ relativePath: "db.enc", kind: "raw", required: true },
{ relativePath: "db.key.enc", kind: "raw", required: true },
],
vaultKeyJson: '{"version":1,"kid":"k1"}',
retention: { keep: 7 },
});
typeof engine.run; // → "function"
typeof engine.list; // → "function"
typeof engine.purgeOlder; // → "function"
b.backup.verifyManifestSignature(target, opts) #
{
expectedFingerprint: string, // optional; SHA3-512 fingerprint to pin
}
Read the manifest from a restored bundle directory (or accept a pre-parsed manifest object) and verify its SLH-DSA audit-sign signature. Operator-facing wrapper around b.backupManifest.verifySignature that handles the on-disk fetch + JCS parse, so a regulator-facing restore drill is a single call.
Returns { ok, fingerprint?, reason? }. Throws BackupError only for missing / unreadable / unparseable manifests — a bad signature returns { ok: false, reason } so the caller can branch on the verdict without a try/catch.
Pass opts.expectedFingerprint to pin the signing key; the verification rejects any signature that validates against a different key, even if the math checks out. That's the kid-pinning the restore drill leans on.
var fs = require("node:fs");
var path = require("node:path");
var os = require("node:os");
var bundleDir = fs.mkdtempSync(path.join(os.tmpdir(), "verify-bundle-"));
try {
b.backup.verifyManifestSignature(bundleDir);
} catch (e) {
e.code; // → "backup/no-manifest"
}
b.backup.recommendedFiles(opts) #
{
atRest: "plain" | "encrypted", // default "encrypted"
vaultMode: "plaintext" | "wrapped", // default "wrapped"
dbName: string, // default "blamejs.db"
additionalSealed: Array, // operator-supplied sealed-file paths
}
Return the framework-default include list for a given DB at-rest mode + vault wrap mode. Operators with the standard layout pass the result straight to b.backup.create({ files }); operators with custom data files (additional sealed keys, OIDC provider material, application-specific keystores) append their own entries.
The list adapts to mode: - plain DB → the live SQLite file (default name blamejs.db) - encrypted DB → db.enc + db.key.enc (envelope + sealed DEK) - plaintext vault → vault.key - wrapped vault → vault.key.sealed
The audit-signing key is always included (sealed in wrapped mode) so a restored deployment can verify its own audit chain.
var files = b.backup.recommendedFiles({
atRest: "encrypted",
vaultMode: "wrapped",
additionalSealed: ["ca.key.sealed", "tls/privkey.pem.sealed"],
});
files[0].relativePath; // → "db.enc"
files[1].relativePath; // → "db.key.enc"
files[2].relativePath; // → "vault.key.sealed"
b.backup.runInWorker(opts) #
{
workerScript: string, // required; absolute path to the worker module
args: object, // optional; passed as workerData to the worker
timeoutMs: number, // optional; positive finite int, terminates worker on miss
}
Execute a backup or restore inside a node:worker_threads worker so the heavy-CPU Argon2id + XChaCha20-Poly1305 + SHA3-512 walk doesn't block the request loop. Returns a Promise resolving with the worker's posted message, or rejecting with the worker's error, a non-zero exit, or the operator's timeoutMs.
The worker script is supplied by the operator — responsibility for thread-safe storage adapters stays with the operator; this helper is the dispatch + lifecycle glue. The framework rejects with backup/no-worker-threads when node:worker_threads is unavailable (sandboxed runtimes, stripped Node builds).
var path = require("node:path");
b.backup.runInWorker({
workerScript: path.resolve("/does/not/exist/worker.js"),
args: { mode: "full" },
timeoutMs: 60000,
}).catch(function (err) {
// worker failed to load — error surfaces as a rejected promise
typeof err.message; // → "string"
});
b.backup.bundleAdapterStorage(opts) #
{
adapter: { writeFile, readFile, listKeys, deleteKey, hasKey },
}
Adapter-driven storage backend. Wraps the bundle directory's file tree into per-file key-value pairs routed through an operator- supplied byte-store adapter so backup bundles can land anywhere that exposes the contract: local fs (the default), tar / tar.gz folding, and S3 / MinIO / Azure / GCS objectStore adapters.
The adapter contract (small surface; an fs implementation is the default + ships in lib/backup/_adapter-fs.js):
adapter.writeFile(key, bytes): Promise
Keys are . Operators pointing at an objectStore implementation pass an adapter that routes keys to S3 paths; pointing at an HTTP-backed store, ditto.
var storage = b.backup.bundleAdapterStorage({
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups" }),
});
storage.name; // → "adapter"
typeof storage.writeBundle; // → "function"
b.backup.bundleAdapterStorage.objectStoreAdapter(client, opts?) #
{
prefix: string, // namespace every key under this prefix in the bucket
list: { maxResults: number }, // forwarded to client.list opts
}
Wraps a b.objectStore-shaped client into the { writeFile, readFile, listKeys, deleteKey, hasKey } adapter contract that bundleAdapterStorage consumes. The client must expose put(key, body) → Promise<{ size }>, get(key) → Promise, head(key) → Promise<{ size, ... }>, delete(key) → Promise, and list(prefix, opts?) → Promise<{ items: [{ key, size, ... }], truncated }> — the shape produced by b.objectStore.buildBackend({ protocol: ... }) for the local / SigV4 / GCS / Azure-Blob backends.
opts.prefix namespaces every key under a fixed root inside the bucket — operators sharing a bucket across multiple deployments pass distinct prefixes so listings stay scoped.
opts.list is the operator-tunable { maxResults, ... } pass- through forwarded to the underlying client.list call (defaults to whatever the backend's list defaults to — typically 1000).
Closes the v0.12.10 deferral: "S3 / MinIO / Azure / GCS-backed backups" promised since v0.11.2 JSDoc.
var client = b.objectStore.buildBackend({
protocol: "local",
rootDir: "/var/backups",
});
var storage = b.backup.bundleAdapterStorage({
adapter: b.backup.bundleAdapterStorage.objectStoreAdapter(client),
format: "tar.gz",
cryptoStrategy: "recipient",
recipient: pair,
});
// bundle bytes hit the object-store backend's put(); restore
// path composes through unwrap + read.gz + read.tar.
b.backup.migrate(opts) #
{
from: bundleAdapterStorage with format: "directory",
to: bundleAdapterStorage with format: "tar",
bundleId: string (single-bundle migrate; omit to migrate all),
deleteSourceOnSuccess: boolean (default false; explicit opt-in),
}
One-shot helper that walks an operator's directory-tree-format bundle (v0.12.7 layout) and writes the same content as a tar-format bundle via the v0.12.8 bundleAdapterStorage. Idempotent: re- running on an already-migrated bundle is a no-op. Source stays in place by default; operators with explicit transition windows opt into the inline replace via deleteSourceOnSuccess: true.
var from = b.backup.bundleAdapterStorage({
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v7" }),
format: "directory",
});
var to = b.backup.bundleAdapterStorage({
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: "/var/backups-v8" }),
format: "tar",
});
await b.backup.migrate({ from: from, to: to });
Last updated 2026-08-08T16:39:49.652Z by seeder.