Quickstart: TypeScript
Gate decisions and verify receipts from Node with @hesohq/sdk, in a few lines. The verifier and the gate run in-process. No subprocess, no re-implementing the crypto in JavaScript.
Install
Add the SDK to a Node project (Node 18 or later). @hesohq/sdk binds to the native @hesohq/node addon for signing and hashing.
npm install @hesohq/sdk @hesohq/nodeTo verify receipts in a browser instead, use @hesohq/verify-wasm. It is the same Rust core compiled to WebAssembly, so the verdict is byte-identical.
Verify a receipt
This is the most common Node job: you have an Action Receipt and you want to know if it is genuine. Call init() once to load the WASM module, then pass verifyActionReceipt the raw receipt bytes — encode the receipt JSON with a TextEncoder. It recomputes the hash and re-checks the signatures, then returns a verdict and a re-derived trust level.
import init, { verifyActionReceipt } from "@hesohq/verify-wasm"
import { readFileSync } from "node:fs"
await init() // load the wasm once, up front
const receipt = JSON.parse(readFileSync("receipt.json", "utf8"))
// 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"
if (result.verdict === "Valid") {
proceed()
}A "Valid" verdict means the receipt passed every gate: the algorithm and version are recognized, the recomputed hash matches, and the signatures verify. The trust_level is "L0" (operator-signed) or "L1"(operator plus a human approver’s co-signature), re-derived from the signatures that actually pass. It is only meaningful when the verdict is "Valid".
A tampered receipt
Change any signed field and the recomputed hash stops matching the signature. The verdict names the first gate that failed.
// Change one byte of a signed receipt, then verify it.
receipt.content.action.amount_usd = 1
const bytes = new TextEncoder().encode(JSON.stringify(receipt))
const result = verifyActionReceipt(bytes)
console.log(result.verdict) // "HashMismatch" — the bytes no longer match the signatureThe verdict is one of:
Valid— passed every gate.HashMismatch— a signed field was changed after signing.InvalidSignature— a signature did not verify (e.g.InvalidSignature:operator).WrongAlgorithm— the receipt uses an algorithm this build does not support.TrustLevelMismatch— the receipt claims a higher trust level than its signatures support.Malformed— the receipt could not be parsed.
Some verdicts carry a :detail suffix naming the exact thing that failed. Trust is never read off the wire: a receipt that claims L1 but carries only an operator signature verifies as TrustLevelMismatch, not L1. Read Offline verification for the full gate order.
Gate an action
The other half of the job is deciding whether an action is allowed before you run it. gate runs an action through your active policy and returns one of four decisions: allow, block, redact, or require_approval.
import { gate } from "@hesohq/sdk"
const action = {
verb: "payment",
amount_usd: 4200,
recipient: "acct_993",
}
const decision = gate(action)
if (decision === "allow") {
applyTransfer(action)
} else {
// "block" | "redact" | "require_approval"
console.warn("not allowed:", decision)
}When you would rather halt than branch, use assertGate. It throws unless the decision is allow, so the line after it only runs on authorized actions.
import { assertGate } from "@hesohq/sdk"
// Throws unless policy returns "allow". Past this line, the action is authorized.
assertGate({ verb: "payment", amount_usd: 4200, recipient: "acct_993" })
applyTransfer()Call init() once at startup to load your policy and operator key before the agent runs. See Policy & decisions for how each decision is chosen, and Trust levels for the L0/L1 distinction.
Gate an LLM client
wrap returns a guarded copy of a client. Each call is gated as an llm_call, signed into a receipt, and blocked if policy says so — without touching your call sites.
import { wrap } from "@hesohq/sdk"
import Anthropic from "@anthropic-ai/sdk"
// Every messages.create call is gated as an llm_call and signed into a receipt.
const client = wrap(new Anthropic())
const out = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "draft the refund email" }],
})To mirror receipts to the cloud and pull your deployed policy, the cloud client is a one-liner. The server re-verifies every receipt before mirroring it.
import { pushReceipt, pullPolicy } from "@hesohq/sdk"
await pushReceipt(process.env.HESO_API_KEY!, receipt) // server re-verifies, then mirrors
const policy = await pullPolicy(process.env.HESO_API_KEY!)The full surface — wrap options, the cloud client, approvals, and the wire types — lives in the TypeScript SDK reference.
A valid receipt proves the operator authorized this action under a known policy, and at L1 that a person approved it with a device-held key. It records what was authorized, not whether the action succeeded downstream.