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) #

stable0.11.2
{
  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) #

stable0.4.0hipaapci-dssgdprsoc2dora
{
  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 (-<8 hex>), 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) #

stable0.7.30hipaapci-dsssoc2
{
  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) #

stable0.4.0
{
  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) #

stable0.8.41
{
  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) #

stable0.12.7hipaapci-dssgdprsoc2
{
  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 adapter.readFile(key): Promise adapter.listKeys(prefix): Promise adapter.deleteKey(key): Promise adapter.hasKey(key): 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?) #

stable0.12.13
{
  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) #

stable0.12.8
{
  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.