Start free

Browser — @hesohq/verify-wasm

The @hesohq/verify-wasm package is the verify-only surface the HESO web console uses: the Rust core compiled to WASM so receipts, chains, proofs, and policy previews all run in the browser. It never signs — no private key reaches the browser. And because it is the same core, just compiled for the browser, the verdict it returns matches the one the Python and Node SDKs return, byte for byte.

This package backs the browser-verify quickstartand the console’s own receipt viewer and policy editor. It ships as ESM (built with wasm-bindgen for the web target). One thing to know up front: you must await the default init export once before calling anything, because that call fetches and loads the .wasm file.

Verify-only — there is no signing in the browser

Every export here recomputes and checks. None of them sign. The operator key lives with the SDK and the approver key lives on the approver’s device — never in the browser. So this package can tell you a receipt is Valid at L0 or L1, but it can never mint one. For the signing surface, see @hesohq/core.

Install

Add the package with your package manager. It is ESM only — there is no CommonJS build — and it has no runtime dependencies beyond the bundled .wasm binary.

bash
pnpm add @hesohq/verify-wasm

The .wasm file must be served and reachable at runtime, since init() fetches it. If your bundler does not emit and resolve the asset for you, pass an explicit URL or the already-fetched module to init() (see below). For the App Router specifics — where to put the file and how to load it once on the client — follow the Next.js guide.

Initialization

Everything starts with the default export, init. Await it once, cache the returned promise, and from then on the named exports below run synchronously.

init()function

The wasm-bindgen entry point. Fetches and loads the .wasm module, then resolves. You must await this once before calling any other export. With no argument it fetches the default asset URL; pass a URL or an already-fetched module to override that. There is also a synchronous initSync(module) variant for when you already hold the compiled module bytes.

ts
export default function init(module_or_path?: unknown): Promise<InitOutput>

Parameters

module_or_pathunknown
Optional. A URL, Request, Response, or already-compiled module to instantiate from. Omit it to fetch the default bundled asset.

ReturnsPromise<InitOutput>

A promise resolving to InitOutput once the module is ready. The recommended pattern is to call it once and cache the promise, so concurrent callers share one instantiation.

Example

verifier.ts
import init, { verifyActionReceipt } from "@hesohq/verify-wasm" // Call init() once, cache the promise, then the named exports are synchronous.let ready: Promise<unknown> | null = nullexport function loadVerifier() {  ready ??= init()  return ready} await loadVerifier()const verdict = verifyActionReceipt(receiptBytes)

Verifying receipts

The core call. Hand it the receipt bytes and it runs the full verify order — algorithm, version, hash, operator signature, approver signature, redaction markers, trust level — and stops at the first failing gate. The trust level is re-derived from which signatures actually pass, so a receipt that claims more than its signatures support fails with a mismatch.

verifyActionReceipt()function

Verify a single Action Receipt end to end and return an ActionVerdict.

ts
function verifyActionReceipt(receiptBytes: Uint8Array): ActionVerdict

Parameters

receiptBytesUint8Arrayrequired
The serialized ActionReceipt JSON as bytes.

ReturnsActionVerdict

An ActionVerdict with two fields: verdict is "Valid" on success, or a variant name describing the first failing gate — for example "HashMismatch", "InvalidSignature:…", or "TrustLevelMismatch:embedded=L1,derived=L0". trust_level is the re-derived level ("L0" or "L1").

Example

verify.ts
import init, { verifyActionReceipt } from "@hesohq/verify-wasm" await init()const verdict = verifyActionReceipt(receiptBytes)if (verdict.verdict === "Valid") {  console.log("trust", verdict.trust_level) // "L0" | "L1"} else {  console.warn("rejected at gate:", verdict.verdict)}
Verdicts are strings, not exceptions

A failed verification does not throw — it returns an ActionVerdict whose verdict is the variant name of the gate that failed. Branch on verdict === "Valid". For the full list of variants and what triggers each, see Verdicts.

Chains and proofs

Beyond a single receipt, the package verifies whole audit chains and the transparency-log proofs. Chain checks return a ChainResult that points at the exact seq where a chain broke. Proof checks return a plain boolean.

verifyChain()function

Verify a sequence of receipts as a BLAKE3-linked hash chain — each entry must link to the one before it.

ts
function verifyChain(receiptsBytes: Uint8Array): ChainResult

Parameters

receiptsBytesUint8Arrayrequired
The serialized sequence of receipts, in order.

ReturnsChainResult

A ChainResult. On success, ok is true and length is the entry count. On failure, ok is false and seq, error, and detail locate the break.

Example

chain.ts
import init, { verifyChain } from "@hesohq/verify-wasm" await init()const result = verifyChain(receiptsBytes)if (!result.ok) {  console.error("chain broke at seq", result.seq, "—", result.error)}

verifySessionChain()function

Verify a single session's receipt chain. Same ChainResult shape as verifyChain().

ts
function verifySessionChain(receiptsBytes: Uint8Array): ChainResult

Parameters

receiptsBytesUint8Arrayrequired
The serialized receipts for one session, in order.

ReturnsChainResult

A ChainResult for the session.

verifySessionChainWithRotation()function

Verify a session chain where signing keys may rotate, against an expected producer key and an optional decision key.

ts
function verifySessionChainWithRotation(receiptsBytes: Uint8Array, producerKey: string, decisionKey?: string): ChainResult

Parameters

receiptsBytesUint8Arrayrequired
The serialized receipts for the session, in order.
producerKeystringrequired
The expected producer (operator) public key.
decisionKeystring
Optional expected decision (approver) public key.

ReturnsChainResult

A ChainResult for the session under the supplied keys.

verifyInclusion()function

Check an RFC-6962 Merkle inclusion proof: that a leaf sits at a given index in a tree of a given size under a given root.

ts
function verifyInclusion(leafValueHex: string, index: number, size: number, rootHex: string, proofHashesJson: string): boolean

Parameters

leafValueHexstringrequired
The leaf value, hex-encoded.
indexnumberrequired
The leaf's index in the tree.
sizenumberrequired
The tree size the proof was issued against.
rootHexstringrequired
The expected Merkle root, hex-encoded.
proofHashesJsonstringrequired
The proof hashes as a JSON array of hex strings.

Returnsboolean

True if the inclusion proof holds, false otherwise.

verifyConsistency()function

Check an RFC-6962 consistency proof: that a newer tree is an append-only extension of an older one.

ts
function verifyConsistency(oldSize: number, oldRootHex: string, newSize: number, newRootHex: string, proofHashesJson: string): boolean

Parameters

oldSizenumberrequired
The earlier tree size.
oldRootHexstringrequired
The earlier Merkle root, hex-encoded.
newSizenumberrequired
The later tree size.
newRootHexstringrequired
The later Merkle root, hex-encoded.
proofHashesJsonstringrequired
The proof hashes as a JSON array of hex strings.

Returnsboolean

True if the new tree is consistent with the old one, false otherwise.

Approval tokens

At L1, a human approver signs an action with their own device-held key. verifyApprovalToken checks that Ed25519 token against the action it is meant to authorize, blocks replays with a set of nonces it has already seen, and confirms the token is in scope and signed by a registered key.

verifyApprovalToken()function

Verify an approval token and return its ApprovalTokenClaims if it passes.

ts
function verifyApprovalToken(  token: Uint8Array,  actionCanonical: Uint8Array,  nowUnixSecs: bigint,  seenNonces: Uint8Array[],  requiredScope: string,  registeredKeysB64: string[],): ApprovalTokenClaims

Parameters

tokenUint8Arrayrequired
The approval token bytes.
actionCanonicalUint8Arrayrequired
The canonical bytes of the action the token authorizes — produce these with actionCanonicalBytes().
nowUnixSecsbigintrequired
The current time in Unix seconds, used to reject expired tokens.
seenNoncesUint8Array[]required
Nonces already accepted, so a replayed token is rejected.
requiredScopestringrequired
The scope the token must carry to authorize this action.
registeredKeysB64string[]required
The base64 approver public keys you accept signatures from.

ReturnsApprovalTokenClaims

An ApprovalTokenClaims with nonce, expiry_unix_secs, scope, and approver_public_key.

Example

approval.ts
import init, { verifyApprovalToken } from "@hesohq/verify-wasm" await init()const claims = verifyApprovalToken(  token,            // Uint8Array  actionCanonical,  // Uint8Array — from actionCanonicalBytes()  BigInt(Math.floor(Date.now() / 1000)),  seenNonces,       // Uint8Array[] — replay guard  "payment",        // required scope  registeredKeysB64 // string[] — approver pubkeys you trust)console.log(claims.scope, claims.approver_public_key)

Hashing and canonicalization

The same building blocks the verifier uses internally are exposed directly, so you can reproduce a receipt’s action_hash, derive canonical bytes for an approval check, or recompute a chain link. Canonicalization is RFC-8785 (JCS) with action_hash stripped before hashing. The content hash is BLAKE3.

contentHash()function

Compute the BLAKE3 content hash of an action's content JSON — the value that lands in action_hash.

ts
function contentHash(contentJson: string): string

Parameters

contentJsonstringrequired
The ActionContent as a JSON string.

Returnsstring

The BLAKE3 hash as a lowercase 64-hex string.

Example

hashing.ts
import init, { contentHash, actionCanonicalBytes, chainHashHex } from "@hesohq/verify-wasm" await init()const bytes = actionCanonicalBytes(contentJson) // RFC-8785 JCS, action_hash strippedconst hash = contentHash(contentJson)           // BLAKE3, 64-hexconst link = chainHashHex(prevChainHex, hash)   // BLAKE3(prev ++ action)

anchoredContentHash()function

Compute the anchored content hash of an action's content JSON.

ts
function anchoredContentHash(contentJson: string): string

Parameters

contentJsonstringrequired
The ActionContent as a JSON string.

Returnsstring

The anchored hash as a hex string.

actionCanonicalBytes()function

Produce the canonical (RFC-8785 JCS) bytes for an action's content — the bytes that get hashed and the input to approval-token checks.

ts
function actionCanonicalBytes(contentJson: string): Uint8Array

Parameters

contentJsonstringrequired
The ActionContent as a JSON string.

ReturnsUint8Array

The canonical bytes, with action_hash stripped before serialization.

chainHashHex()function

Compute one audit-chain link: BLAKE3 of the previous chain hash concatenated with the current action hash.

ts
function chainHashHex(prevHex: string, actionHex: string): string

Parameters

prevHexstringrequired
The previous entry’s chain hash, hex-encoded (or "genesis" for seq 0).
actionHexstringrequired
The current entry's action hash, hex-encoded.

Returnsstring

The chain hash for this entry, hex-encoded.

shortHash()function

Render a hex hash in a short, human-readable form for display.

ts
function shortHash(hex: string, prefix?: string): string

Parameters

hexstringrequired
The full hex hash.
prefixstring
Optional prefix to prepend to the shortened form.

Returnsstring

A short display string for the hash.

Policy in the browser

These five exports make the console’s policy editor live. They parse, evaluate, and describe a heso.toml policy entirely in the browser, so an author sees the decision a rule would produce — and catches a floor bypass — before anything is deployed. Because this is the same Rust engine the server runs, the in-browser verdict matches the one the cloud will enforce.

parsePolicy()function

Parse and validate a policy file. Returns nothing on success; throws on a malformed file or a floor bypass.

ts
function parsePolicy(tomlSrc: string): void

Parameters

tomlSrcstringrequired
The heso.toml policy source.

Throws

A [PARSE] error for a malformed policy, or a [FLOOR_BYPASS] error naming the offending rule id and verb if a rule tries to allow a dangerous lane without approval.

evaluatePolicy()function

Evaluate a policy against a captured action and return the decision — the live preview behind the editor.

ts
function evaluatePolicy(tomlSrc: string, actionJson: string): string

Parameters

tomlSrcstringrequired
The heso.toml policy source.
actionJsonstringrequired
The action to test, as a JSON string.

Returnsstring

A PolicyOutcome JSON string — the matched rule, its conditions, and the decision path (allow, block, redact, or require_approval).

ruleToSentence()function

Render one rule as its canonical English sentence — the same string a receipt carries as rule_display.

ts
function ruleToSentence(ruleJson: string): string

Parameters

ruleJsonstringrequired
A single policy rule, as a JSON string.

Returnsstring

The natural-language sentence for the rule, identical to the receipt’s rule_display.

Example

policy.ts
import init, { parsePolicy, evaluatePolicy, ruleToSentence } from "@hesohq/verify-wasm" await init()parsePolicy(tomlSrc) // throws "[PARSE]…" or "[FLOOR_BYPASS]…" // Live decision preview for the policy editor:const outcomeJson = evaluatePolicy(tomlSrc, actionJson) // PolicyOutcome JSONconst sentence = ruleToSentence(ruleJson)               // the receipt rule_display

policyRulesFromToml()function

Parse a policy file into its structured rules — used to render the rule list in the editor.

ts
function policyRulesFromToml(tomlSrc: string): string

Parameters

tomlSrcstringrequired
The heso.toml policy source.

Returnsstring

A JSON array of PolicyRule objects.

validateNoFloorBypass()function

Check a set of rules against the pinned floors. Returns nothing on success; throws if any rule bypasses a floor.

ts
function validateNoFloorBypass(rulesJson: string): void

Parameters

rulesJsonstringrequired
A JSON array of policy rules.

Throws

A [FLOOR_BYPASS] error naming the rule id and verb that tries to allow a dangerous lane without approval.

Both parsePolicy and validateNoFloorBypass enforce the same pinned floors the engine checks at load time, in the browser — so a policy that would be rejected on the server is rejected before it ever leaves the editor. For the full simulate-then- deploy loop, see Test & deploy.

Classes

Three small result classes are returned by the functions above. Each is a plain data carrier — read its fields, do not construct it yourself.

ClassFieldsReturned by
ActionVerdictverdict: string, trust_level: stringverifyActionReceipt()
ApprovalTokenClaimsnonce: Uint8Array, expiry_unix_secs: bigint, scope: string, approver_public_key: stringverifyApprovalToken()
ChainResultok: boolean, length?: number, error?: string, seq?: number, detail?: stringverifyChain() and the session-chain checks
Same core, every surface

The verdicts, hashes, and policy decisions this package returns are byte-identical to the Node core and the Python SDK, because all three call the same Rust engine. There is no re-implemented crypto to drift.

Next steps