Getting started /Quickstart: Verify in the browser

Quickstart: Verify in the browser

You can check any HESO Action Receipt offline, in a browser or in CI, with @hesohq/verify-wasm. Verification runs locally. Nothing about it needs HESO, an account, or a network call.

A receipt is a signed JSON record of one action and the policy verdict that gated it. To verify it you re-run the same checks the signer ran. This page shows you how to do that in a few lines.

Install

bash
npm install @hesohq/verify-wasm

The package is ESM and works in the browser and in Node.

Verify a receipt

Call init() once to load the WASM module, then pass verifyActionReceipt the raw receipt bytes — encode the receipt JSON with a TextEncoder. It returns a verdict with a verdict field and a trust_level.

verify.tsts
import init, { verifyActionReceipt } from "@hesohq/verify-wasm"

await init() // load the wasm once, up front

// receipt is the Action Receipt JSON, exactly as you received it.
// The verifier takes the raw receipt bytes, not the object.
const bytes = new TextEncoder().encode(JSON.stringify(receipt))
const result = verifyActionReceipt(bytes)

console.log(result.verdict)      // "Valid"
console.log(result.trust_level)  // "L0" or "L1"

A verdict of Valid means the receipt passed every check. trust_level is L0 for an operator-signed receipt, or L1 when a human approver also co-signed it. It is only meaningful when the verdict is Valid.

Detect tampering

This is the whole point of the verifier. The receipt’s content is bound to a BLAKE3 hash, and that hash is signed. Edit any field in content and the recomputed hash no longer matches, so the verdict flips.

tamper.tsts
// Change any field inside content...
const tampered = {
  ...receipt,
  content: { ...receipt.content, amount: 999999 },
}

const bytes = new TextEncoder().encode(JSON.stringify(tampered))
const result = verifyActionReceipt(bytes)

console.log(result.verdict)  // "HashMismatch" — the receipt no longer matches its hash

A changed field gives you HashMismatch (the content no longer matches its hash). A broken or swapped signature gives you InvalidSignature. Either way, the receipt is rejected. You cannot edit a receipt and keep it valid without the signing key.

Why this works

Verification re-runs the same cryptography the signer used. It recomputes the BLAKE3 hash of the content and re-checks the Ed25519 signatures against it. None of this needs a server, so the verdict you get locally is the same one anyone else gets.

The verifier walks a fixed set of gates in order and stops at the first one that fails. The verdict names that gate: Malformed for a receipt it cannot parse, WrongAlgorithm for an algorithm it does not handle, HashMismatch for altered content, InvalidSignature for a signature that does not verify, and TrustLevelMismatch for a receipt that claims more than its signatures support. Some carry a :detail suffix, e.g. InvalidSignature:operator.

The trust_level is re-derived from the signatures actually present, not read as a claim. An operator signature alone is L0; an operator signature plus a human approver co-signature is L1. For the full gate order see Offline verification, and for every verdict and what it means see the Verdicts reference.

What a valid receipt proves

A Valid verdict proves the action was authorized under a known policy, and at L1 that a person approved it. It records what was authorized, not whether the action succeeded downstream.

If you would rather not write code, there is a hosted verifier at /verify that checks a receipt in the browser, and a heso-verify-cli for verifying receipt bundles from the command line.

Next steps