Backup Manifest

The document a restore reads before it touches a single encrypted file: which framework version wrote the bundle, when, the sealed vault key and its salt, and an entry per file with its digest.

Every path in and out of the manifest validates it. Creating, serializing and parsing all run the same check, so a manifest that cannot be validated is never written and never acted on. The failure lists every invalid field at once rather than the first.

A manifest is signed over its canonical form, with the signature held outside the bytes that are signed, so a reader can verify the document it has rather than a re-serialization of it. That is what lets a restore prove the bundle is the one this deployment wrote and not a substituted one.

b.backupManifest.validate(manifest) #

stable0.1.96

Check a manifest and answer { ok, errors }. errors names every invalid field rather than stopping at the first, so one call tells an operator everything wrong with a manifest.

It requires the format version and framework name this build writes, a non-empty framework version, an ISO-8601 createdAt, a hex vaultKeySalt, a base64 vaultKeyEnc and a files array, and aadBound to be a boolean when it is there at all. create, serialize and parse all run this, which is why an invalid manifest is neither written nor acted on.

aadBound is checked here and not only at create, because a manifest read off disk carrying aadBound: "true" would otherwise pass, lose the field on the next serialize, and be restored as though the bundle were not AAD-bound, which surfaces as a decryption failure rather than a refusal.

var b = require("@blamejs/core");
b.backupManifest.validate({ version: 1 });
// → { ok: false, errors: [ … every missing field … ] }

b.backupManifest.create(opts?) #

stable0.1.96
{
    frameworkVersion: string,   // default: the running framework version
    createdAt:        string,   // ISO-8601; default: now
    vaultKeySalt:     string,   // hex
    vaultKeyEnc:      string,   // base64
    files:            array,    // per-file entries
    metadata:         object,   // copied into the manifest
    aadBound:         boolean,  // recorded only when true
    keyScheme:        string,   // recorded only when supplied
    bundleSalt:       string,   // recorded only when supplied

  Each `files` entry needs `relativePath`, `encryptedPath`, `size`,
  `encryptedSize`, a 128-character hex `checksum` (the SHA3-512 of the
  plaintext), a hex `salt`, and a `kind` of `raw`, `vault-sealed` or
  `plaintext`. The two paths must be relative, with no `..`, no leading
  separator and no colon, so a manifest cannot direct a restore outside its
  destination or at an NTFS data stream.
}

Build a manifest object. The format version and framework name are this build's, frameworkVersion defaults to the running version and createdAt to now, so a manifest records the build that wrote it rather than what a caller claims.

vaultKeySalt and vaultKeyEnc carry the sealed vault key, and files is the per-entry list. The result is validated before it is returned, so an incomplete manifest raises backup-manifest/invalid here rather than at restore time.

metadata is copied rather than referenced. aadBound, keyScheme and bundleSalt are recorded only when supplied, which keeps a manifest from claiming a scheme the bundle does not use. aadBound must be a boolean, raising backup-manifest/bad-input otherwise: anything else was read as false, so a bundle whose entries are AAD-bound recorded that it was not and a restore would not know to apply the binding.

var b = require("@blamejs/core");
var bytes = Buffer.from("the contents");
var manifest = b.backupManifest.create({
  vaultKeySalt: "a1b2c3",
  vaultKeyEnc:  "QUJDRA==",
  files: [{
    relativePath:  "app.db",
    encryptedPath: "files/app.db.enc",
    size:          bytes.length,
    encryptedSize: bytes.length + 40,
    checksum:      b.backupCrypto.checksum(bytes),
    salt:          b.backupCrypto.newSalt(),
    kind:          "raw",
  }],
});
manifest.frameworkVersion;     // → the version that wrote it

b.backupManifest.signingPayload(manifest) #

stable0.8.44

The exact bytes a manifest's signature covers: its canonical form with the signature block left out, as JSON with a trailing newline.

A signer and a verifier both read this, so neither re-derives the payload from its own idea of the field order. The signature block is excluded because a signature cannot cover itself.

var b = require("@blamejs/core");
var payload = b.backupManifest.signingPayload(manifest);
payload.endsWith("\n");        // → true

b.backupManifest.serialize(manifest) #

stable0.1.96

Render a manifest as the JSON text written to disk, signature block included, with a trailing newline.

The manifest is validated first, so backup-manifest/invalid is raised before anything is written. A rendered manifest above the size every reader accepts raises backup-manifest/too-large naming the file count, since the fix is to split the include list across more than one backup rather than to raise a limit the readers do not share.

var b = require("@blamejs/core");
var text = b.backupManifest.serialize(manifest);
b.backupManifest.parse(text).createdAt === manifest.createdAt;   // → true

b.backupManifest.readFile(manifestPath, opts?) #

stable0.20.31
{
  errorFor: object,   // function (kind, detail) returning the error to throw
}

Read a manifest from disk and return the validated object.

The read is bounded at the manifest size limit, so a file that grew past it is refused rather than loaded, and the bytes are read through a descriptor that cannot be swapped between the check and the read.

errorFor is a function, not an error class: it is called as errorFor(kind, detail) with the reason the read failed, one of enoent, symlink, too-large, toctou, short-read or integrity. Return an error and it is thrown; return nothing and the default is. This is how a restore surfaces a manifest read under its own code, and it is the same callback readBuffer takes.

var b = require("@blamejs/core");
var manifest = b.backupManifest.readFile("./backups/2026-10-06/manifest.json");
manifest.files.length;

b.backupManifest.readBuffer(bytes, opts?) #

stable0.20.31
{
  errorFor: function,  // build the caller's own error class
}

Read a manifest a caller already holds as bytes, under the same size limit b.backupManifest.readFile applies to one on disk. A storage backend whose manifest arrives from an object store has no path to hand to readFile, and calling parse on bytes it measured itself would put a second copy of the limit in the caller.

Throws backup-manifest/bad-input when bytes is not a Buffer, and backup-manifest/too-large when the bytes exceed the limit b.backupManifest.serialize enforces on the writer. Parsing then throws backup-manifest/bad-json when the bytes are not valid JSON, and backup-manifest/invalid when they parse but do not match the v1 schema.

var manifest = b.backupManifest.readBuffer(bytes);
manifest.files.length;                            // → 3

b.backupManifest.sign(manifest) #

stable0.6.0

Sign a v1 backup manifest in place with the audit-sign keypair, attaching a detached signature block over the manifest's canonical bytes (the serialization WITHOUT the signature field, so appending it doesn't change the signed payload). Validates the manifest against the v1 schema first; throws backup-manifest/invalid on a malformed manifest, backup-manifest/no-signer when b.auditSign.init() hasn't run, and backup-manifest/sign-failed when the signer rejects the payload. For a schema-agnostic alternative see signBytes.

b.backupManifest.sign(manifest);
manifest.signature.fingerprint;   // the signing key's fingerprint

b.backupManifest.signBytes(canonicalBytes) #

stable0.15.21

Sign caller-supplied canonical bytes with the framework's audit-sign keypair, returning a detached signature block — the schema-agnostic counterpart of sign(). Where sign() is bound to the v1 manifest schema (it validate()s the whole { version, framework, files[] } shape before signing), this signs any bytes a consumer canonicalizes itself, so a bespoke backup-header / manifest format reuses the same post-quantum signing keypair + fingerprint pinning without adopting the framework schema.

canonicalBytes is a Buffer (signed verbatim) or a string (signed as UTF-8); any other type throws backup-manifest/bad-input. Returns { algorithm, publicKey, fingerprint, value, signedAt }. Requires b.auditSign.init() (throws backup-manifest/no-signer otherwise), and throws backup-manifest/sign-failed when the signer rejects the payload.

var sig = b.backupManifest.signBytes(myCanonicalHeaderBuffer);
// store sig alongside the header; later:
var v = b.backupManifest.verifyBytes(myCanonicalHeaderBuffer, sig,
  { expectedFingerprint: b.auditSign.getPublicKeyFingerprint() });
// v.ok === true

b.backupManifest.verifySignature(manifest, opts) #

stable0.6.0
{
  expectedFingerprint: string,   // trust only the key with this fingerprint
}

Verify a signed v1 backup manifest's detached signature block over its canonical bytes. Returns { ok, reason?, fingerprint? } and does not throw, so a caller decides whether a missing or mismatched signature is fatal.

The signature is trusted only under a known key. With opts.expectedFingerprint, the fingerprint computed from signature.publicKey must equal it. Without a pin, that key must be the active or a rotated b.auditSign key, and a process that has not run b.auditSign.init() gets ok: false. signature.algorithm must be one of b.auditSign.SUPPORTED_SIGNING_ALGS and must name the type of signature.publicKey. The fingerprint in the result is the one computed from the key, never the block's own fingerprint field. For a schema-agnostic alternative see verifyBytes.

var v = b.backupManifest.verifySignature(manifest,
  { expectedFingerprint: b.auditSign.getPublicKeyFingerprint() });
if (!v.ok) throw new Error("untrusted backup: " + v.reason);

b.backupManifest.verifyBytes(canonicalBytes, signatureBlock, opts?) #

stable0.15.21
{
  expectedFingerprint: string,   // trust only the key with this fingerprint
}

Verify caller-supplied canonical bytes against a detached signature block produced by signBytes() — the schema-agnostic counterpart of verifySignature(). Returns { ok, reason?, fingerprint? }, and applies the same key trust as verifySignature(): the key must match opts.expectedFingerprint, or be the active or a rotated b.auditSign key. A verifier process that does not hold the signing key passes expectedFingerprint; without it and without b.auditSign.init(), the result is ok: false.

var v = b.backupManifest.verifyBytes(headerBytes, sigBlock,
  { expectedFingerprint: trustedFingerprint });
if (!v.ok) throw new Error("untrusted header: " + v.reason);

b.backupManifest.parse(jsonStr) #

stable0.1.96

Parse manifest text and return the validated object. Accepts a string or a Buffer; anything else raises backup-manifest/bad-input.

The JSON is parsed through the bounded, prototype-safe parser, so a manifest carrying __proto__ or exceeding the size limit is refused rather than materialized: backup-manifest/bad-json for text that will not parse, backup-manifest/invalid for a document that parses but is not a manifest.

var b = require("@blamejs/core");
var manifest = b.backupManifest.parse(text);
manifest.framework;           // → the framework name that wrote it

Last updated 2026-10-12T14:21:56.879Z by seeder.