TypeScript — @hesohq/sdk
The HESO SDK for Node. It gates and signs your agent’s actions in-process, and verifies receipts and talks to the cloud. There is no crypto in TypeScript: minting and verifying bind to @hesohq/core and the native @hesohq/node addon, so a verdict here matches one from Python or the browser exactly.
Use this package whether your agent runs on Node or you only need to verify receipts there. The Python SDK has more capture surface (more framework adapters, decorators); on Node the same one Rust core does the work.
Common tasks
The four things most callers reach for. Each links to the full reference entry below.
- Gate an action. Verify a receipt and branch on the result with
gate, or guard a code path that throws on failure withassertGate. - Wrap a client. Use
wrapto verify the receipt on every method result of a client that attaches one. To gate an LLM’s tool calls, use the framework adapters instead. - Verify a receipt. In Node,
gate/assertGatedo it. In a browser, use the @hesohq/verify-wasm verifier. - Push a receipt to the cloud.
pushReceiptmirrors one receipt to the control plane, which re-verifies it before storing.
A receipt proves you authorized an action under your policy — and at L1 that a person co-signed it with a device-held key. It does not prove the action succeeded downstream.
Install
It targets Node 18 or newer and ships as CommonJS, so it imports from both require and ESM interop.
npm install @hesohq/sdk
# Gating and minting also need the native addon (an optional dependency).
# Verify-only callers can skip it.
npm install @hesohq/nodeEverything is on one entry point. Import the helpers and types you need:
import {
// engine runtime + cloud config
init,
currentConfig,
configure,
// capture core + framework adapters
engine,
aiSdk,
mastra,
finalizeL1,
finalizeQuorum,
BlockedError,
SuspendedError,
// verify a receipt
gate,
assertGate,
isDecisionAllowed,
shortHash,
wrap,
// cloud client
pullPolicy,
pushReceipt,
pushReceipts,
pollApproval,
waitForApproval,
getL1Parts,
getQuorumParts,
submitApprovalToken,
} from "@hesohq/sdk"
import type { ActionReceipt, DecisionPath, TrustLevel } from "@hesohq/sdk"Every verify, sign, and redact call runs in the native Rust addon: @hesohq/core for verification and @hesohq/node for minting. Ed25519 and BLAKE3 are never re-implemented in JavaScript. Minting is loaded lazily, so a verify-only deployment never needs the native addon.
Gating your agent
Gating captures each action before it runs, checks it against your policy, signs the verdict into a receipt, and chains it into the audit log. Call init() once at startup, then gate tool calls through a framework adapter or the engine core.
init()function
Resolve and install the engine runtime config: the project root holding heso.toml and heso-local-data/, the default workflow/account stamped on each action, an optional pinned clock, and the blocking/observe-only switch. Resolution order for each setting: explicit option, then environment (HESO_*), then discovery, then default.
init(options?: InitOptions): HesoRuntimeConfigParameters
- options.projectRootstring
- Directory holding
heso.toml. Defaults to$HESO_PROJECT_ROOT, then the nearest ancestor with aheso.toml, then the cwd. - options.workflowstring= "default"
- Workflow id stamped on actions (env
HESO_WORKFLOW). - options.accountstring= "default"
- Account/tenant stamped on actions (env
HESO_ACCOUNT). - options.clockOverridestring
- An RFC-3339 instant pinned into every action for reproducible runs (env
HESO_CLOCK). - options.blockingboolean= true
- When true a blocked/suspended action throws; false is observe-only (env
HESO_BLOCKING).
ReturnsHesoRuntimeConfig
The resolved HesoRuntimeConfig. Read it back any time with currentConfig().
Example
import { init } from "@hesohq/sdk"
// Call once at startup, before you gate anything. It finds heso.toml and
// heso-local-data/ (operator key, audit log) from the project root and stamps a
// default workflow/account on every captured action.
init({ workflow: "vendor-payouts", account: "acct_19" })Capture and signing run through @hesohq/node. It is an optional dependency loaded lazily the first time you mint, so verify-only code never pulls it in. If it is missing, the first gated action throws a BridgeError telling you to install it.
Framework adapters
The Vercel AI SDK and Mastra adapters are thin wrappers over the capture core, reached via the ./adapters/* subpath. They gate a tool’s execute before it runs: a blocked or suspended action throws, which the framework turns into a tool-error the model sees, so the body never fires ungated.
import { streamText, tool } from "ai"
import { init } from "@hesohq/sdk"
// Reached via the ./adapters/* subpath export.
import { gateTool, gateTools } from "@hesohq/sdk/adapters/ai-sdk"
init()
await streamText({
model,
tools: {
// Gated as a tool_call before execute() runs. A blocked action throws,
// which the AI SDK turns into a tool-error the model sees; the body never fires.
search: gateTool("search", tool({ inputSchema, execute })),
// ...or gate a whole record at once:
...gateTools({ sendEmail, refund }),
},
})aiSdk.gateTool(name, tool, opts?) wraps one tool; aiSdk.gateTools(record, opts?) wraps a whole tools record. The Mastra adapter (mastra) mirrors the same shape for Mastra’s tool interface. Both are namespaced on the package root and also importable from @hesohq/sdk/adapters/ai-sdk and @hesohq/sdk/adapters/mastra.
The capture core
Every adapter is a thin wrapper over engine (the namespace exported from the capture core). Use it directly to gate a call the adapters do not cover:
engine.gate(toolName, input, opts?)— capture, evaluate, and enforce. ThrowsBlockedError/SuspendedErroron a refusal (blocking mode); returns the gatedActionon allow.engine.evaluate(toolName, input, opts?)— capture and evaluate without enforcing; returns{ outcome, action }so you can map the decision onto your own verdict shape.engine.recordResult(action, output)— bind a finished call’s result to a follow-up receipt (best-effort evidence; never throws).
import { init, engine, SuspendedError } from "@hesohq/sdk"
init()
try {
// engine.gate(toolName, input, opts) captures, evaluates, and enforces. It
// mints a signed receipt via @hesohq/node and throws on a refusal.
const action = engine.gate("transfer_funds", { amountUsd: 4200, payee: "Globex LLC" }, { verb: "payment" })
const result = await transferFunds({ amountUsd: 4200, payee: "Globex LLC" })
engine.recordResult(action, result) // bind the result to a follow-up receipt (best-effort)
} catch (err) {
if (err instanceof SuspendedError) {
// Routed to a human. err.actionHash keys the out-of-band finalize (below).
}
}Two-phase approval
When policy routes an action to a human, engine.gate throws SuspendedError carrying the action_hash; it cannot block the tool call waiting for a person. Phase two happens out of band: once the approval resolves, fetch the relayed parts and assemble the L1 receipt with finalizeL1. A rejected or escalated decision throws ApprovalRejectedError and mints no L1.
import { waitForApproval, finalizeL1 } from "@hesohq/sdk"
// actionHash came from the SuspendedError thrown at gate time. This is the
// out-of-band second phase, never inside the tool call itself.
const resolved = await waitForApproval(actionHash)
if (resolved.status === "approved" && resolved.parts) {
// Assemble and mirror the L1 (human co-signed) receipt off the byte-exact
// suspended content the operator stamped. A rejected decision mints no L1.
const l1 = await finalizeL1(resolved.parts.suspendedContent, resolved.parts)
}For a k-of-n quorum, use getQuorumParts + finalizeQuorum instead; both are documented in the cloud client. The result is still an L1 receipt (it carries a multi_approval block), not a higher level.
The approver co-signs in their browser with a per-device key; the cloud relays that detached co-signature and holds no signing key. finalizeL1 re-mints the receipt and verifies it locally before pushing it. If the operator key rotates between the suspend and the finalize, finalizeL1 fails closed and re-suspends under the new key rather than minting under a stale one.
Cloud configuration
Call configure once at startup, before any cloud call. It sets the API key and endpoint the cloud client uses for every request. See Authentication for where the key comes from. This is separate from init(), which configures the local minting engine; local gating and verification need no configure and no network.
configure()function
Set the cloud credentials once at startup. Stores config the cloud client reads on every request. Call it before pullPolicy, pushReceipt, or any approval call.
configure(apiKey: string, endpoint: string): voidParameters
- apiKeystringrequired
- Your org’s API key, sent as the
x-api-keyheader. The org is resolved from it server-side. - endpointstringrequired
- The base URL of your control plane.
Returns
Nothing. It stores module-level config in place.
Example
import { configure } from "@hesohq/sdk"
configure(process.env.HESO_API_KEY!, process.env.HESO_ENDPOINT!)Verifying
These helpers verify a receipt locally and turn the verdict into a decision. They run offline through @hesohq/core and follow the same verify gate as every other surface. minTrust defaults to "L0"; pass "L1" to require a human co-signature.
gate()function
Verify a receipt and return a result. Never throws on a failed verdict — it reports it. Use this when you want to branch on the outcome.
gate(receiptBytes: Buffer | string, minTrust?: TrustLevel = "L0"): GateResultParameters
- receiptBytesBuffer | stringrequired
- The receipt JSON, as a string or buffer.
- minTrustTrustLevel= "L0"
- The minimum re-derived trust level required to allow.
ReturnsGateResult
A GateResult of { allowed: boolean; trustLevel: TrustLevel | null; verdict: string }. verdict is the engine verdict tag (e.g. Valid, HashMismatch); trustLevel is null when verification fails before trust is re-derived.
Example
import { gate } from "@hesohq/sdk"
import { readFile } from "node:fs/promises"
const bytes = await readFile("receipt.json", "utf8")
const result = gate(bytes, "L1")
if (!result.allowed) {
// result.verdict is the engine tag, e.g. "HashMismatch"
console.error("rejected:", result.verdict, result.trustLevel)
}assertGate()function
The throwing form of gate. Returns normally when the receipt verifies and meets minTrust; throws otherwise. Use it as a guard before a sensitive code path.
assertGate(receiptBytes: Buffer | string, minTrust?: TrustLevel = "L0"): voidParameters
- receiptBytesBuffer | stringrequired
- The receipt JSON, as a string or buffer.
- minTrustTrustLevel= "L0"
- The minimum trust level required to pass.
Returns
Nothing on success; throws when the receipt is not allowed.
Throws
An error carrying the resolved verdict when verification fails or the trust level is below minTrust.
Example
import { assertGate } from "@hesohq/sdk"
// Throws unless the receipt verifies AND meets the minimum trust level.
assertGate(receiptBytes, "L1")
// Past this line the action is authorized and human-approved.
fulfillOrder(order)isDecisionAllowed()function
Check whether a receipt’s recorded decision path is one you accept. This reads the policy outcome already embedded in the receipt; it does not re-verify signatures. Pair it with gate when authenticity matters.
isDecisionAllowed(receipt: ActionReceipt, allowedPaths: DecisionPath[]): booleanParameters
- receiptActionReceiptrequired
- A parsed receipt object.
- allowedPathsDecisionPath[]required
- The decision paths you treat as acceptable, e.g.
["allow", "redact"].
Returnsboolean
true when the receipt’s decision path is in allowedPaths.
Example
import { isDecisionAllowed } from "@hesohq/sdk"
import type { ActionReceipt } from "@hesohq/sdk"
const receipt: ActionReceipt = JSON.parse(receiptBytes)
if (isDecisionAllowed(receipt, ["allow", "redact"])) {
// The policy let this action through (possibly after redaction).
}shortHash()function
Render a long hex hash in short form: the first eight characters, optionally namespaced with a prefix. For logs and UI.
shortHash(hex: string, prefix?: string): stringParameters
- hexstringrequired
- A hex string such as an
action_hashor a chain hash. - prefixstring
- An optional label rendered as
prefix:first8.
Returnsstring
The short form: first8, or prefix:first8 when a prefix is given.
Example
import { shortHash } from "@hesohq/sdk"
shortHash("9f2c4e7a1b08d3f6…") // "9f2c4e7a"
shortHash("9f2c4e7a1b08d3f6…", "action") // "action:9f2c4e7a"Wrapping a client
wrap returns a stand-in for any client that behaves like the original. After each method call it checks the result for a __heso_receipt field, and if it finds one, verifies it against minTrust. This enforces a trust floor across every method of a client whose responses carry receipts. It is distinct from engine.gate, which captures and mints your own actions.
wrap()function
Wrap a client so it verifies every method result carrying a __heso_receipt. The returned object has the same type as the original, so you can swap it in with no other changes.
wrap<T extends object>(client: T, options?: WrapOptions): TParameters
- clientTrequired
- The object to wrap. Each method is intercepted; results without a
__heso_receiptpass through unchanged. - optionsWrapOptions
- Per-wrap configuration; documented below.
ReturnsT
A proxy of the same type T as the wrapped client.
Example
import { wrap, configure, pushReceipt } from "@hesohq/sdk"
configure(process.env.HESO_API_KEY!, process.env.HESO_ENDPOINT!)
const gated = wrap(paymentsClient, {
minTrust: "L1",
onGateFail: (method, verdict) => {
console.error(`${method} blocked by gate: ${verdict}`)
return false // do not let the result through
},
onReceipt: (method, receiptJson) => {
void pushReceipt(JSON.parse(receiptJson))
},
})
// Each method result is checked by reading its __heso_receipt field.
await gated.createTransfer({ amountUsd: 4200, payee: "Globex LLC" })WrapOptions controls the trust floor and the two hooks fired around gating:
| Field | Type | Behaviour |
|---|---|---|
minTrust | TrustLevel | The minimum trust level a result’s receipt must reach to pass. Default "L0". |
onGateFail | (method, verdict) => boolean | Promise<boolean> | Called when a result fails the gate. Return true to let it through anyway, false to throw. |
onReceipt | (method, receiptJson) => void | Promise<void> | Called with the raw receipt JSON for every gated result. A natural place to push it to the cloud. |
Cloud client
These functions talk to the control plane over HTTP. They all read the config set by configure, so call that first. The org is resolved from your api-key — there is no team id in any path. Approvals are keyed by the action_hash they gate. For the endpoints and request shapes, see the cloud API reference.
When you push a receipt, the control plane recomputes the content hash and rejects a tampered or malformed body before mirroring it. The response status is appended, duplicate (idempotent re-push), or quota_exceeded (the monthly mirror cap; your local chain is unaffected).
pullPolicy()function
Pull the org’s current pinned policy bundle. Maps to GET /v1/policy/pull.
pullPolicy(): Promise<PolicyPullResult>ReturnsPromise<PolicyPullResult>
A PolicyPullResult of { status: "policy" | "up_to_date"; policyId: string; policyHash: string; toml: string }. An empty toml is the fail-safe deny pin: the engine routes every action to a human when the org has no resolvable policy. See policy files for the format.
Example
import { pullPolicy } from "@hesohq/sdk"
// The org is resolved from your api-key — there is no team id.
const bundle = await pullPolicy()
console.log(bundle.status, bundle.policyId, bundle.toml.length)pushReceipt()function
Push one receipt to the cloud outbox. Maps to POST /v1/receipts; the server re-verifies before mirroring.
pushReceipt(receipt: ActionReceipt, supersedesActionHash?: string): Promise<ReceiptPushResult>Parameters
- receiptActionReceiptrequired
- The signed receipt to mirror.
- supersedesActionHashstring
- Set only when finalizing an approval: the freshly assembled L1 supersedes the suspended L0 entry’s
action_hashit was built from.
ReturnsPromise<ReceiptPushResult>
A ReceiptPushResult of { status: "appended" | "duplicate" | "quota_exceeded"; entryHash: string; seq: number }. entryHash echoes the receipt’s own action_hash.
Example
import { pushReceipt } from "@hesohq/sdk"
import type { ActionReceipt } from "@hesohq/sdk"
const receipt: ActionReceipt = JSON.parse(receiptBytes)
const result = await pushReceipt(receipt) // server re-verifies before mirroring
if (result.status === "quota_exceeded") {
console.warn("monthly mirror cap hit — local chain is unaffected")
}pushReceipts()function
Push a batch of receipts. There is no server-side batch route — this loops over POST /v1/receipts in order.
pushReceipts(receipts: ActionReceipt[]): Promise<ReceiptPushResult[]>Parameters
- receiptsActionReceipt[]required
- The receipts to push, in order.
ReturnsPromise<ReceiptPushResult[]>
A ReceiptPushResult[]: one result per receipt, in order.
Example
import { pushReceipts } from "@hesohq/sdk"
// Loops POST /v1/receipts in order (there is no server-side batch route).
const results = await pushReceipts(pending)
const appended = results.filter((r) => r.status === "appended")pollApproval()function
Read the current state of an approval once, keyed by the action it gates. Maps to GET /v1/approvals/{action_hash}.
pollApproval(actionHash: string): Promise<ApprovalView>Parameters
- actionHashstringrequired
- The
action_hashof the suspended action.
ReturnsPromise<ApprovalView>
An ApprovalView of { approvalId; actionHash; status: "pending" | "approved" | "rejected"; threshold; approvedCount; resolvedAt }.
Example
import { pollApproval } from "@hesohq/sdk"
// Approvals are keyed by the action_hash they gate, not an opaque approval id.
const view = await pollApproval(actionHash)
if (view.status === "approved") {
console.log(`${view.approvedCount}/${view.threshold} approvers signed`)
}waitForApproval()function
Poll an approval until it resolves, then return it. On approved it makes one extra call to getL1Parts so you can finalize without re-polling.
waitForApproval(actionHash: string, options?: { pollIntervalMs?: number; timeoutMs?: number }): Promise<ResolvedApproval>Parameters
- actionHashstringrequired
- The action to wait on.
- options.pollIntervalMsnumber= 2000
- How often to poll, in milliseconds.
- options.timeoutMsnumber= 300000
- How long to wait before giving up, in milliseconds.
ReturnsPromise<ResolvedApproval>
A ResolvedApproval: the ApprovalView plus parts (the L1 relay parts on approved; null on rejected).
Throws
An error when the timeout elapses before the approval resolves.
Example
import { waitForApproval } from "@hesohq/sdk"
// Polls every 2s, throws after 5min by default. On "approved" it fetches the L1
// relay parts once so you can finalize without re-polling.
const resolved = await waitForApproval(actionHash, {
pollIntervalMs: 3000,
timeoutMs: 120000,
})getL1Parts()function
Fetch the parts the cloud relays for finalizing an approved gate into an L1 receipt. Reads GET /v1/approvals/{action_hash}/assembly and takes the single approver leg.
getL1Parts(actionHash: string): Promise<L1Parts>Parameters
- actionHashstringrequired
- The approved action’s
action_hash.
ReturnsPromise<L1Parts>
An L1Parts of { record; approverPubkeyB64; coSigB64; suspendedContent }: the approver’s verbatim signed record and co-signature plus the byte-exact suspended content the L1 is reassembled from. Pass it to finalizeL1.
Example
import { getL1Parts } from "@hesohq/sdk"
// The approver's verbatim signed record + co-signature + the byte-exact
// suspended content the L1 is reassembled from. Fetch once, after "approved".
const parts = await getL1Parts(actionHash)finalizeQuorum() / getQuorumParts()function
The k-of-n sibling of finalizeL1. getQuorumParts(actionHash) fetches every approver leg plus the threshold, roster, and suspended content; finalizeQuorum assembles, re-verifies, and pushes the L1 receipt with a multi_approval block. A single non-approval anywhere throws ApprovalRejectedError.
finalizeQuorum(suspendedContent: ActionContent, relayedParts: QuorumParts, options?): Promise<ActionReceipt>Parameters
- suspendedContentActionContentrequired
- The operator-stamped suspended content the legs were co-signed against (read it off
parts.suspendedContent). - relayedPartsQuorumPartsrequired
- The
QuorumPartsfromgetQuorumParts: the approver legs, threshold, and roster. - options{ keyPassphrase?; loadedOperatorPubkeyB64?; onKeyRotation? }
- Optional passphrase and key-rotation recovery hooks.
ReturnsPromise<ActionReceipt>
The signed quorum ActionReceipt (L1 with a multi_approval block), already mirror-pushed.
Example
import { getQuorumParts, finalizeQuorum } from "@hesohq/sdk"
// A k-of-n gate. getQuorumParts returns every approver leg plus the threshold,
// roster, and suspended content; finalizeQuorum assembles the L1 receipt from them.
const parts = await getQuorumParts(actionHash)
const receipt = await finalizeQuorum(parts.suspendedContent, parts)submitApprovalToken()function
Submit a human-signed approval token for an open approval, keyed by the action it gates. Maps to POST /v1/approvals/{action_hash}/submit-token. The cloud verifies the token against the org’s registered approver keys and records it as a vote.
submitApprovalToken(actionHash: string, input: SubmitTokenInput): Promise<ApprovalView>Parameters
- actionHashstringrequired
- The action being approved.
- inputSubmitTokenInputrequired
{ tokenB64; actionContent; requiredScope?; decision?; reason?; … }: the base64 token, the receiptcontentit signs over, and the human’s decision.
ReturnsPromise<ApprovalView>
The updated ApprovalView.
Example
import { submitApprovalToken } from "@hesohq/sdk"
// Redeem the token a reviewer signed with their device-held key. Keyed by the
// action_hash; the cloud verifies it against the org's registered approver keys.
const view = await submitApprovalToken(actionHash, {
tokenB64,
actionContent: receipt.content,
decision: "approved",
})To let your customer co-sign from their own browser, the package also exports signDelegation and mintDelegationEnvelope— operator delegation-envelope minting (no crypto in TS; you inject an OperatorSigner). See Let your customers approve.
Wire types
The package re-exports the wire types so you can type receipts, policies, and approvals without a second dependency. They mirror the JSON on the network exactly; for a field-by-field breakdown of the receipt envelope see the receipt schema. Exported types include Verb, DecisionPath, TrustLevel, ConditionOp, ActionReceipt, ActionContent, ActionDetail, PolicyOutcome, SignatureEntry, MultiApproval, PolicyRule, and Approval, among others.
| Type | What it is |
|---|---|
ActionReceipt | The signed envelope — see receipt schema. |
DecisionPath | allow, block, redact, or require_approval. |
TrustLevel | L0 (operator-signed) or L1 (human co-signed). |
Verb | The action verb captured for a gated call — see actions & verbs. |
ConditionOp | A policy condition operator — see conditions & operators. |
MultiApproval | The k-of-n quorum block on a quorum L1 receipt — threshold, roster, and per-approver records. |
PolicyRule | One rule from a policy file. |
Approval | A resolved human approval, including the approver’s co-signature. |