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.
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.
pnpm add @hesohq/verify-wasmThe .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.
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
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.
function verifyActionReceipt(receiptBytes: Uint8Array): ActionVerdictParameters
- 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
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)}
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.
function verifyChain(receiptsBytes: Uint8Array): ChainResultParameters
- 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
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().
function verifySessionChain(receiptsBytes: Uint8Array): ChainResultParameters
- 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.
function verifySessionChainWithRotation(receiptsBytes: Uint8Array, producerKey: string, decisionKey?: string): ChainResultParameters
- 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.
function verifyInclusion(leafValueHex: string, index: number, size: number, rootHex: string, proofHashesJson: string): booleanParameters
- 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.
function verifyConsistency(oldSize: number, oldRootHex: string, newSize: number, newRootHex: string, proofHashesJson: string): booleanParameters
- 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.
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
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.
function contentHash(contentJson: string): stringParameters
- contentJsonstringrequired
- The ActionContent as a JSON string.
Returnsstring
The BLAKE3 hash as a lowercase 64-hex string.
Example
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.
function anchoredContentHash(contentJson: string): stringParameters
- 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.
function actionCanonicalBytes(contentJson: string): Uint8ArrayParameters
- 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.
function chainHashHex(prevHex: string, actionHex: string): stringParameters
- 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.
function shortHash(hex: string, prefix?: string): stringParameters
- 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.
function parsePolicy(tomlSrc: string): voidParameters
- 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.
function evaluatePolicy(tomlSrc: string, actionJson: string): stringParameters
- 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.
function ruleToSentence(ruleJson: string): stringParameters
- 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
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.
function policyRulesFromToml(tomlSrc: string): stringParameters
- 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.
function validateNoFloorBypass(rulesJson: string): voidParameters
- 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.
| Class | Fields | Returned by |
|---|---|---|
ActionVerdict | verdict: string, trust_level: string | verifyActionReceipt() |
ApprovalTokenClaims | nonce: Uint8Array, expiry_unix_secs: bigint, scope: string, approver_public_key: string | verifyApprovalToken() |
ChainResult | ok: boolean, length?: number, error?: string, seq?: number, detail?: string | verifyChain() and the session-chain checks |
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.
