stable

vault-head

An authenticated {id → version} manifest that detects a store withholding or rewinding records — the one attack per-envelope authentication cannot see.
Subpath@noy-db/hub/vault-head
FactorywithVaultHead()
Layerwith-commit

What it does

A record's envelope authenticates itself. Its identity — collection, id, tier, author — and its version are bound into the AEAD, so an untrusted store cannot relocate a record, re-tier it, re-author it, splice another body into it, or present it at a version it was not sealed at. The sync merge rejects an envelope that fails to authenticate before committing it.

None of that can see absence.

A store serving a genuine, unmodified v1 when v7 exists is serving a real record this client really wrote — nothing about the bytes is wrong. A store serving nothing is indistinguishable from a record that never existed. You cannot detect a missing thing by examining the things present.

withVaultHead() supplies the missing external knowledge: an authenticated {id → version} manifest the client writes and the store cannot forge.

Why it is opt-in

Identity binding costs nothing to coordinate and closes alteration, so it is unconditional. The head costs a write per commit and needs anti-entropy. On a single-device offline vault it defends against nothing — there is no second party to withhold anything — so making it mandatory would tax every user for a multi-writer property.

That split is what lets the security boundary be stated as a narrower true claim rather than a concession:

A remote store — or a by-peer peer — cannot alter, relocate, re-tier, re-author, splice or re-version any record it serves. Without withVaultHead(), it can still withhold or omit.

Prerequisites

  • A store that advertises capabilities.casAtomic — the head is written with a compare-and-set. Without it a sweep reports 'unverifiable' rather than a false clean.
  • A multi-writer or remote deployment for it to be worth enabling — on a single-device offline vault there is no second party to withhold anything.
  • Nothing else: the head travels as ordinary records over the same store contract, so no adapter needs new methods to support it.

Using it

import { createNoydb } from '@noy-db/hub'
import { withVaultHead, verifyVaultHead } from '@noy-db/hub/vault-head'

const head = withVaultHead()
const db = await createNoydb({ vaultHeadStrategy: head, store, user, secret })
const vault = await db.openVault('acme')

// …writes proceed normally; the head records each one…

const { adapter, getDEK } = vault._introspectState()
const result = await verifyVaultHead(head, adapter, 'acme', getDEK, 'invoices')

The verdict is three-way, and the middle value is the point

result.verdict  // 'verified' | 'unverifiable' | 'tampered'
result.because  // why it could not conclude — non-empty only when 'unverifiable'
verdictmeaning
'verified'the head held expectations, every one was met, and the store can serialize head writes
'unverifiable'the sweep could not reach a conclusion — see because
'tampered'at least one discrepancy. Positive evidence, so it wins over any unverifiable reason present at the same time

Collapsing the middle value is wrong in a different direction each way. Folded into "clean" it hides withholding, which is the one thing this service exists to catch. Folded into "tampered" it cries wolf on a vault that is merely unexamined.

That is not hypothetical: this service's own first bug was a head that recorded nothing and swept perfectly clean — the degraded state rendered identically to a healthy one. Assert on the count of records checked, not merely on the absence of an error.

Why a sweep may be unverifiable

becausemeaning
'no-expectations'the head holds nothing for this collection — a fresh vault, a head switched on after the fact, or a restore from a snapshot (_head is _-prefixed, so loadAll excludes it and a restored vault has no head to compare against)
'store-cannot-cas'the store does not advertise capabilities.casAtomic, so two writers racing on one bucket can silently lose an entry — and a lost entry is a record the sweep stops expecting, which is a false clean
Capability honesty is an integrity property here, not a lost-update one. The head is written with a compare-and-set. A store that under-reports casAtomic degrades the very manifest that exists to detect that store — the attack disables its own detector.

Discrepancies

if (result.verdict === 'tampered') {
  for (const d of result.discrepancies) {
    // d.expected — what the client last committed
    // d.actual   — what the store serves now, or null when it serves nothing
  }
}
kindmeaning
'withheld'the store serves nothing for a record we wrote — suppression
'rolled-back'it serves an authentic older version in place of the current one — the case no amount of per-envelope authentication can see

Limits & gotchas

  • Verification is client-side and opt-in. A store cannot be made to prove faithful service; the head only lets a client that opted in notice. In a relay or multi-writer deployment, the party that must enable it is the party the relay does not control.
  • Keyring writes are not covered. _keyring files are not records under the head, so a replayed genuine roster is outside what this detects.
  • A restore from a snapshot starts with no expectations and reports 'unverifiable' until the head reaccumulates — correct, not a fault.