Use this package when you verify receipts and talk to the control plane from Node. It does not capture or gate your own agent — that is the Python SDK with its decorators. In Node you verify receipts (and construct or redact them directly with @hesohq/core) and push them to the cloud.
Install
Add the package with your package manager. It targets Node 18 or newer and ships as CommonJS, so it imports cleanly from both require and ESM interop.
pnpm add @hesohq/sdkThen import the helpers and types you need from the single entry point:
import { configure, gate, assertGate, isDecisionAllowed, shortHash, wrap, pullPolicy, pushReceipt, pushReceipts, openApproval, pollApproval, waitForApproval, submitApprovalToken,} from "@hesohq/sdk"import type { ActionReceipt, DecisionPath, TrustLevel } from "@hesohq/sdk"
Every verify, sign, and redact call runs in @hesohq/core, the native Rust addon. Ed25519 and BLAKE3 are never re-implemented in JavaScript — the SDK is wiring and types around the one Rust source of truth.
Configuration
Call configure once at startup, before any cloud call. It sets the API key and endpoint the client uses for every request. See Authentication for where the key comes from.
configure()function
Set the cloud credentials once at startup. Stores a HesoCoreConfig the cloud client reads on every request. Call it before pullPolicy, pushReceipt, or any approval call.
configure(apiKey: string, endpoint: string): voidParameters
- apiKeystringrequired
- Your team’s API key, sent as
Authorization: Bearer <key>. - endpointstringrequired
- The base URL of your control plane.
Returns
Nothing. It mutates module-level config in place.
Example
import { configure } from "@hesohq/sdk" configure(process.env.HESO_API_KEY!, process.env.HESO_ENDPOINT!)
The cloud functions read the config at call time. If you invoke them before configure, the request has no key or endpoint and fails. The local gating helpers below do not need it — they verify offline.
Gating
These helpers verify a receipt locally and turn the verdict into a decision. They run entirely offline through @hesohq/core and follow the same seven-gate verify order as every other surface. minTrust defaults to "L0"; pass "L1" to require a human co-signature.
gate()function
Verify a receipt and return a structured 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 resolved verdict state (e.g. valid, hash_mismatch, trust_mismatch); 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) { 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 clause 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 provably 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 a short display form: the first eight characters, optionally namespaced with a prefix. Handy for logs and UI without re-deriving anything.
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"
The proxy
wrap returns a stand-in for any client that behaves just like the original. After each method call it checks the result for a __heso_receipt field, and if it finds one, gates it. This sets a minimum trust level across every method on the client without changing each call site.
wrap()function
Wrap a client so it gates 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 of its methods 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 } 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 call result through }, onReceipt: (method, receiptJson) => { void pushReceipt(JSON.parse(receiptJson)) },}) // Each method result is gated 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 the gate. |
onGateFail | (method, verdict) => boolean | Promise<boolean> | Called when a result fails the gate. Return true to let the result through anyway, false to suppress it. |
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. For the underlying endpoints and request shapes, see the cloud API reference.
When you push a receipt, the control plane runs the full verify order again before accepting it — the same math your gate call ran locally. A receipt that fails comes back with accepted: false and a rejectionReason.
pullPolicy()function
Fetch the active policy bundle for a team. Maps to GET /v1/teams/{teamId}/policy.
pullPolicy(teamId: string): Promise<PolicyBundle>Parameters
- teamIdstringrequired
- The team whose policy you want.
ReturnsPromise<PolicyBundle>
A PolicyBundle of { version: string; rules: PolicyRule[]; fetchedAt: string }. See policy files for the rule shape.
Example
import { pullPolicy } from "@hesohq/sdk" const bundle = await pullPolicy("team_19")console.log(bundle.version, bundle.rules.length, bundle.fetchedAt)
pushReceipt()function
Submit one receipt to the control plane. Maps to POST /v1/receipts; the server re-verifies before accepting.
pushReceipt(receipt: ActionReceipt): Promise<OutboxPushResult>Parameters
- receiptActionReceiptrequired
- The receipt to submit.
ReturnsPromise<OutboxPushResult>
An OutboxPushResult of { receiptId; accepted; rejectionReason? }. Check accepted before treating the action as recorded.
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 accepting if (!result.accepted) { console.error("rejected:", result.rejectionReason)}
pushReceipts()function
Submit a batch of receipts in one round trip. Maps to POST /v1/receipts/batch.
pushReceipts(receipts: ActionReceipt[]): Promise<OutboxPushResult[]>Parameters
- receiptsActionReceipt[]required
- The receipts to submit together.
ReturnsPromise<OutboxPushResult[]>
An OutboxPushResult[] — one result per submitted receipt, in order.
Example
import { pushReceipts } from "@hesohq/sdk" const results = await pushReceipts(pending)const rejected = results.filter((r) => !r.accepted)
openApproval()function
Open a human approval for a receipt that policy routed to a person. Maps to POST /v1/approvals.
openApproval(req: OpenApprovalRequest): Promise<OpenApprovalResult>Parameters
- reqOpenApprovalRequestrequired
{ receipt: ActionReceipt; routingHint?: string }— the receipt to approve and an optional routing hint.
ReturnsPromise<OpenApprovalResult>
An OpenApprovalResult of { approvalId; token?; expiresAt }.
Example
import { openApproval } from "@hesohq/sdk"import type { ActionReceipt } from "@hesohq/sdk" const receipt: ActionReceipt = JSON.parse(receiptBytes)const opened = await openApproval({ receipt, routingHint: "finance" })console.log(opened.approvalId, opened.expiresAt)
pollApproval()function
Read the current state of an approval once. Maps to GET /v1/approvals/{approvalId}.
pollApproval(approvalId: string): Promise<PollApprovalResult>Parameters
- approvalIdstringrequired
- The id returned by
openApproval.
ReturnsPromise<PollApprovalResult>
A PollApprovalResult of { outcome: ApprovalOutcome; approval?: Approval }. The approvalcarries the approver’s Ed25519 co-signature once a decision lands.
Example
import { pollApproval } from "@hesohq/sdk" const { outcome, approval } = await pollApproval(approvalId) if (outcome === "approved" && approval) { // approval carries the approver's Ed25519 co-signature.}
waitForApproval()function
Poll an approval until it resolves, then return the Approval. A convenience loop over pollApproval.
waitForApproval(approvalId: string, options?: { pollIntervalMs?: number; timeoutMs?: number }): Promise<Approval>Parameters
- approvalIdstringrequired
- The approval 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<Approval>
The resolved Approval.
Throws
An error when the timeout elapses before the approval resolves.
Example
import { waitForApproval } from "@hesohq/sdk" // Polls every 2 s, throws after 5 min by default.const approval = await waitForApproval(approvalId, { pollIntervalMs: 3000, timeoutMs: 120000,})
submitApprovalToken()function
Redeem the one-time token a reviewer signed with their device-held key. Maps to POST /v1/approvals/{approvalId}/token. The cloud holds no signing key — the approver’s signature is theirs alone.
submitApprovalToken(approvalId: string, token: string): Promise<Approval>Parameters
- approvalIdstringrequired
- The approval being decided.
- tokenstringrequired
- The signed approval token.
ReturnsPromise<Approval>
The completed Approval, including the approver’s co-signature.
Example
import { submitApprovalToken } from "@hesohq/sdk" // Redeem the one-time token a reviewer signed with their device-held key.const approval = await submitApprovalToken(approvalId, token)
Types
The package re-exports the wire types so you can type receipts, policies, and approvals without a second dependency. These mirror the JSON on the network exactly; for a field-by-field breakdown of the receipt envelope see the receipt schema.
Among the exports are Verb, DecisionPath, TrustLevel, ConditionOp, ActionReceipt, ActionContent, ActionDetail, PolicyOutcome, SignatureEntry, 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. |
PolicyRule | One rule from a policy file. |
Approval | A resolved human approval, including the approver’s co-signature. |
@hesohq/sdkverifies receipts and moves them to and from the cloud. It does not capture your agent’s actions: to gate, sign, and chain your own calls, use the Python SDK. A passing verdict proves the action was authorized under a known policy and the bytes are untampered — what was authorized, not whether the action succeeded downstream.
Next steps
- Need the raw crypto surface (sign, redact, keys, chain)? That is @hesohq/core.
- Verifying inside a browser instead of Node? Use @hesohq/verify-wasm.
