SDK reference /TypeScript — @hesohq/sdk

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 with assertGate.
  • Wrap a client. Use wrap to 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 / assertGate do it. In a browser, use the @hesohq/verify-wasm verifier.
  • Push a receipt to the cloud. pushReceipt mirrors one receipt to the control plane, which re-verifies it before storing.
What a receipt proves

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.

bash
npm install @hesohq/sdk

# Gating and minting also need the native addon (an optional dependency).
# Verify-only callers can skip it.
npm install @hesohq/node

Everything is on one entry point. Import the helpers and types you need:

gate.tsts
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"
Crypto lives in the core

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.

ts
init(options?: InitOptions): HesoRuntimeConfig

Parameters

options.projectRootstring
Directory holding heso.toml. Defaults to $HESO_PROJECT_ROOT, then the nearest ancestor with a heso.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

startup.tsts
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" })
The native addon mints

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.

agent.tsts
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. Throws BlockedError / SuspendedError on a refusal (blocking mode); returns the gated Action on 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).
gate-manual.tsts
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.

finalize.tsts
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 cloud holds no signing key

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.

ts
configure(apiKey: string, endpoint: string): void

Parameters

apiKeystringrequired
Your org’s API key, sent as the x-api-key header. 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

startup.tsts
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.

ts
gate(receiptBytes: Buffer | string, minTrust?: TrustLevel = "L0"): GateResult

Parameters

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

gate.tsts
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.

ts
assertGate(receiptBytes: Buffer | string, minTrust?: TrustLevel = "L0"): void

Parameters

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

fulfill.tsts
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.

ts
isDecisionAllowed(receipt: ActionReceipt, allowedPaths: DecisionPath[]): boolean

Parameters

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

decision.tsts
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.

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

Parameters

hexstringrequired
A hex string such as an action_hash or 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

display.tsts
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.

ts
wrap<T extends object>(client: T, options?: WrapOptions): T

Parameters

clientTrequired
The object to wrap. Each method is intercepted; results without a __heso_receipt pass through unchanged.
optionsWrapOptions
Per-wrap configuration; documented below.

ReturnsT

A proxy of the same type T as the wrapped client.

Example

wrap.tsts
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:

FieldTypeBehaviour
minTrustTrustLevelThe 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.

The server re-verifies

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.

ts
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

policy.tsts
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.

ts
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_hash it 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

push.tsts
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.

ts
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

batch.tsts
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}.

ts
pollApproval(actionHash: string): Promise<ApprovalView>

Parameters

actionHashstringrequired
The action_hash of the suspended action.

ReturnsPromise<ApprovalView>

An ApprovalView of { approvalId; actionHash; status: "pending" | "approved" | "rejected"; threshold; approvedCount; resolvedAt }.

Example

poll.tsts
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.

ts
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

wait.tsts
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.

ts
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

parts.tsts
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.

ts
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 QuorumParts from getQuorumParts: 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

quorum.tsts
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.

ts
submitApprovalToken(actionHash: string, input: SubmitTokenInput): Promise<ApprovalView>

Parameters

actionHashstringrequired
The action being approved.
inputSubmitTokenInputrequired
{ tokenB64; actionContent; requiredScope?; decision?; reason?; … }: the base64 token, the receipt content it signs over, and the human’s decision.

ReturnsPromise<ApprovalView>

The updated ApprovalView.

Example

submit.tsts
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",
})
Client approvals (delegation)

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.

TypeWhat it is
ActionReceiptThe signed envelope — see receipt schema.
DecisionPathallow, block, redact, or require_approval.
TrustLevelL0 (operator-signed) or L1 (human co-signed).
VerbThe action verb captured for a gated call — see actions & verbs.
ConditionOpA policy condition operator — see conditions & operators.
MultiApprovalThe k-of-n quorum block on a quorum L1 receipt — threshold, roster, and per-approver records.
PolicyRuleOne rule from a policy file.
ApprovalA resolved human approval, including the approver’s co-signature.

Next steps