usertrust-verify
Zero-dependency standalone vault verifier with Merkle proof support.
Purpose
usertrust-verify is an intentionally standalone package with zero dependencies (Node built-ins only). It duplicates canonicalization, chain verification, and Merkle proof functions from usertrust by design — so it can verify vaults independently of the main SDK.
Installation
npm install usertrust-verifyCLI Usage
npx usertrust-verify .usertrustVerifies the entire vault: the SHA-256 hash chain, sequence continuity across rotated *.jsonl segments, and the head recorded in events.jsonl.meta (checked against the chain tail, so a log cut while the sidecar was left alone fails; a cut made with the sidecar rewritten to match does not, without an external anchor). The Merkle root is recomputed and printed; without external anchors it is informational only, and the verdict line reads Vault integrity: VERIFIED (UNANCHORED — internal consistency only).
Single transaction
npx usertrust-verify .usertrust --tx tx_m4k7z_a1b2c3Reads every audit segment, verifies the chain, finds the event whose data.transferId matches (a settlement_unrecoverable record is matched by the SHA-256 of the id) and prints a receipt with the transfer's status. An empty id matches nothing.
Options
| Flag | Meaning |
|---|---|
--tx <transferId> | Verify a single transaction and print its receipt |
--anchor <file|-> | Caller-fetched anchor artifact (repeatable; - reads stdin) |
--anchors <file> | Anchor history file (JSONL; repeatable) |
--anchor-url <url> | Opt-in: fetch one anchor artifact over HTTPS |
--bundle <file|-> | Auditor bundle {v, records} from usertrust anchor export-bundle |
--pubkey <pem-file> | Pinned genesis root public key (out-of-band, never read from the vault) |
--successor-pin <pem-file> | Pinned rotation-successor key (repeatable) |
--vault-id <uuid> | Expected vaultId |
--require-anchor | Strict: UNANCHORED, ANCHOR_UNVERIFIABLE or ANCHOR_STALE exits 1 |
--require-external-anchor | Strict: exit 0 only when ANCHORED_VERIFIED from an external source |
--max-anchor-age <dur> | Freshness policy, e.g. 1h, 30m (operator-claimed time) |
--max-unanchored-events <n> | Freshness policy (clock-independent; preferred) |
Giving --anchor, --anchors, --anchor-url, --bundle, --pubkey, --require-anchor or --require-external-anchor switches the CLI to anchored verification (verifyVaultWithAnchors), which reports one of ANCHORED_VERIFIED, UNANCHORED, ANCHOR_UNVERIFIABLE, ANCHOR_STALE, ANCHOR_INVALID or ANCHOR_MISMATCH. --successor-pin, --vault-id, --max-anchor-age and --max-unanchored-events take effect only alongside one of those.
The usertrust verify command in the main CLI takes the same anchor flags, plus --receipts <file> to cross-check receipts you kept against the chain.
Exit codes
| Code | Meaning |
|---|---|
0 | Verified |
1 | Verification failed (a chain, sequence or sidecar mismatch, a corrupt anchor, or a strict gate not met), or the arguments or an input file were unusable |
2 | Transaction not found (--tx mode only) |
Receipt subcommand
npx usertrust-verify receipt <file> --trust <snapshot.json> [--envelope] [--expect-id <ctx>] [--json]Verifies a signed receipt document against a pinned well-known snapshot. Exit codes: 0 verified, 1 failed, 2 unverifiable, 3 usage error.
Programmatic API
verifyVault
import { verifyVault } from "usertrust-verify";
const result = verifyVault(".usertrust");interface VaultVerificationResult {
valid: boolean;
errors: string[];
chainLength: number;
validHashes: number;
merkleRoot: string | null;
firstEvent: string | null;
lastEvent: string | null;
}verifyTransaction
import { verifyTransaction } from "usertrust-verify";
const result = verifyTransaction(".usertrust", "tx_m4k7z_a1b2c3");interface TransactionVerificationResult {
readonly found: boolean;
readonly valid: boolean;
readonly receipt: string;
readonly errors: string[];
// present only when anchor inputs were supplied
readonly anchorState?: AnchorState | undefined;
readonly anchoring?: AnchoringReport | undefined;
// when found: SETTLED, AMBIGUOUS, FAILED, DENIED, RELEASED, UNRECOVERABLE or PENDING
readonly status?: TransferStatus | undefined;
}verifyTransaction accepts an optional third argument, anchorParams?: AnchorVerifyParams (the same object verifyVaultWithAnchors takes). Without it the receipt claims chain consistency only; with it the anchor state machine runs over the whole vault first, and inclusion is checked against the signed external anchor root only when an anchor covers the transaction and trust pins the key. A transaction after the latest anchor is labelled IN UNANCHORED TAIL in the receipt, and valid: true there means chain consistency, not anchoring. An empty txId matches nothing and returns found: false; the transaction is looked up by data.transferId across every audit segment (a settlement_unrecoverable record, which stores only the hash of its id, is matched by that hash).
verifyVaultWithAnchors
import { verifyVaultWithAnchors, exitCodeForAnchored } from "usertrust-verify";
const result = verifyVaultWithAnchors(".usertrust", {
externalAnchorsRaw: [anchorsJsonl], // fetched by you, not by the verifier
trust: { rootPem: pinnedPublicKeyPem }, // pinned out-of-band, never from the vault
});
process.exit(exitCodeForAnchored(result, { requireExternalAnchor: true }));interface AnchorVerifyParams {
readonly externalAnchorsRaw?: readonly string[];
readonly trust?: { rootPem: string; successorPinsPem?: readonly string[] };
readonly witness?: { requested: boolean; ok?: boolean; error?: string };
readonly maxAnchorAgeMs?: number;
readonly maxUnanchoredEvents?: number;
readonly expectedVaultId?: string;
readonly nowMs?: number;
}
interface AnchoredVaultVerificationResult extends VaultVerificationResult {
anchorState: AnchorState;
anchoring: AnchoringReport;
}Runs verifyVault unchanged, then layers the anchor state machine on top. exitCodeFor(result) maps a plain vault result to 0 or 1; exitCodeForAnchored(result, { requireAnchor?, requireExternalAnchor? }) also returns 1 when a strict gate is not met. The anchor and receipt-rendering helpers (verifyAnchorChain, verifyAnchorSignature, renderReceipt, and the rest) are exported as well.
Re-exported Functions
All Merkle proof functions are also available:
import {
verifyChain,
buildMerkleTree,
hashLeaf,
hashInternal,
generateInclusionProof,
verifyInclusionProof,
generateConsistencyProof,
verifyConsistencyProof,
canonicalize,
GENESIS_HASH,
} from "usertrust-verify";Why Zero Dependencies?
The verifier must be trustworthy independently of the SDK it verifies. Adding dependencies would create a supply-chain attack surface and defeat the purpose of independent verification.
This package intentionally duplicates code from usertrust. Do not refactor into shared imports — the duplication is a security feature.