@hesohq/core is the same Rust core the rest of HESO uses, exposed to Node as a native addon. A verdict it returns is byte-identical to the one the Python SDK or the browser verifier would return for the same receipt — there is no re-implementation of crypto in JavaScript. If you are gating an agent or talking to the cloud, reach for the higher-level @hesohq/sdk, which wraps this package; use @hesohq/core directly when you need raw verify, hashing, chain, transparency, redaction, or key primitives.
Install
Add the package with your Node package manager. It ships prebuilt native binaries, so there is no native toolchain to set up for the supported platforms.
pnpm add @hesohq/core- Requires Node 18 or newer.
- The package is CommonJS — import it with require (or an interop default import).
- @hesohq/core is the same package as @hesohq/node; they are two names for the one native addon.
const core = require("@hesohq/core") const verdict = core.verify(receiptBytes)console.log(verdict.verdict, verdict.trustLevel)
The package also has a browser entry point. Importing @hesohq/core/browser re-exports @hesohq/verify-wasm, the verify-only WASM build — so one dependency covers both the native Node path and the browser path.
// Node native addon — verify, sign, redact, keysimport * as core from "@hesohq/core" // Browser-only re-export of @hesohq/verify-wasm (verify-only)import init from "@hesohq/core/browser"
The native addon does signing, redaction, and key generation. The /browser re-export is verify-only: it can check a receipt anywhere, but it never holds or generates a signing key.
Verifying receipts
Both functions run the full seven-gate verify order and re-derive the trust level from the signatures that actually pass. They never panic: a structurally broken receipt resolves to a Malformed:… verdict rather than throwing.
verify()function
Verify one Action Receipt and return its verdict and re-derived trust level.
verify(receiptBytes: Buffer | Uint8Array | string): ActionVerdictParameters
- receiptBytesBuffer | Uint8Array | stringrequired
- The receipt to verify, as raw bytes or a JSON string.
ReturnsActionVerdict
An ActionVerdict with verdict: string (e.g. "valid", "hash_mismatch", "invalid_signature", or "Malformed:…") and trustLevel: string ("L0" or "L1"). A receipt claiming higher trust than its signatures support resolves to trust_mismatch.
Example
const fs = require("node:fs")const core = require("@hesohq/core") const bytes = fs.readFileSync("receipt.json")const { verdict, trustLevel } = core.verify(bytes) if (verdict !== "valid") { throw new Error(`receipt rejected: ${verdict}`)}console.log("trust:", trustLevel) // "L0" or "L1"
verifyWithTime()function
Like verify(), but also reports whether the receipt carries a trusted time anchor.
verifyWithTime(receiptBytes: Buffer | Uint8Array | string): ActionVerdictWithTimeParameters
- receiptBytesBuffer | Uint8Array | stringrequired
- The receipt to verify, as raw bytes or a JSON string.
ReturnsActionVerdictWithTime
An ActionVerdictWithTime: the same verdict and trustLevel as verify(), plus timeStatus — either "NoTrustedTime" or "AnchoredRfc3161:<gen_time>" when an RFC-3161 timestamp is present and valid.
Example
const { verdict, trustLevel, timeStatus } = core.verifyWithTime(bytes) // timeStatus is "NoTrustedTime" or "AnchoredRfc3161:<gen_time>"console.log(verdict, trustLevel, timeStatus)
Hashing and canonicalization
These are the primitives behind a receipt’s action_hash. Canonicalization is RFC-8785 (JCS) with action_hash stripped before hashing; the content hash is BLAKE3. Use them to recompute a hash yourself, or to derive the exact canonical bytes an approval token signs over.
contentHash()function
Compute the BLAKE3 action_hash over a receipt's canonical content bytes.
contentHash(contentJson: string): stringParameters
- contentJsonstringrequired
- The receipt content as a JSON string.
Returnsstring
The BLAKE3 hash as lowercase 64-hex — the value that belongs in content.action_hash.
Example
const contentJson = JSON.stringify(receipt.content)const hash = core.contentHash(contentJson)// hash === receipt.content.action_hash
anchoredContentHashJs()function
Compute the pre-anchor BLAKE3 hash, excluding any time_anchor field.
anchoredContentHashJs(contentJson: string): stringParameters
- contentJsonstringrequired
- The receipt content as a JSON string.
Returnsstring
The pre-anchor BLAKE3 hash — computed with time_anchor excluded, so a later timestamp can be bound without changing the hash.
Example
const preAnchor = core.anchoredContentHashJs(contentJson)actionCanonicalBytesJs()function
Return the exact RFC-8785 canonical bytes, with action_hash stripped.
actionCanonicalBytesJs(contentJson: string): BufferParameters
- contentJsonstringrequired
- The receipt content as a JSON string.
ReturnsBuffer
A Buffer of the RFC-8785 (JCS) canonical bytes with action_hash removed — the same bytes the signature and an approval token are computed over.
Example
const canonical = core.actionCanonicalBytesJs(contentJson) // Buffer (RFC-8785 / JCS)shortHash()function
Abbreviate a long hex string for display, with an optional prefix.
shortHash(hex: string, prefix?: string): stringParameters
- hexstringrequired
- The full hex string to abbreviate.
- prefixstring
- An optional label to prepend, e.g. "ed25519:".
Returnsstring
A short, ellipsised form of the hash for display — never use the abbreviated value for verification.
Example
core.shortHash("9f2c1d…e1c0") // "9f2c…e1c0"core.shortHash(operatorPubB64, "ed25519:") // "ed25519:uP3…b1"
chainHashHex()function
Compute the domain-separated BLAKE3 chain link between two entries.
chainHashHex(prevHex: string, actionHex: string): stringParameters
- prevHexstringrequired
- The previous entry’s
chainHash, or the literal"genesis"for seq 0. - actionHexstringrequired
- This entry’s
action_hash.
Returnsstring
The next link in the audit chain — a domain-separated BLAKE3 of the previous chain hash and this action hash.
Example
const chainHash = core.chainHashHex(prevChainHex, actionHashHex)// for seq 0, prevChainHex is the literal "genesis"
Chains
A chain ties each receipt to the one before it, so altering any earlier receipt breaks every downstream link. These functions verify a sequence end to end and tell you exactly where it broke. See the audit chain for the model.
verifyChain()function
Verify a sequence of receipts as a linked, tamper-evident chain.
verifyChain(receiptsBytes: Buffer[] | string): ChainResultParameters
- receiptsBytesBuffer[] | stringrequired
- The receipts in sequence, as an array of byte buffers or a JSON string.
ReturnsChainResult
A ChainResult: ok: boolean, and on success length. On failure it carries error, the failing seq, and a human-readable detail.
Example
const result = core.verifyChain(receiptsBytes) // Buffer[] or stringif (!result.ok) { console.error("chain broke at seq", result.seq, result.detail)}
verifySessionChainJs()function
Verify a single session's receipt chain.
verifySessionChainJs(receiptsBytes: Buffer[] | string): ChainResultParameters
- receiptsBytesBuffer[] | stringrequired
- The session's receipts in sequence.
ReturnsChainResult
A ChainResult, the same shape as verifyChain().
Example
const result = core.verifySessionChainJs(receiptsBytes)console.log(result.ok, result.length)
verifySessionChainWithRotationJs()function
Verify a session chain that may rotate keys, pinned to a genesis producer key.
verifySessionChainWithRotationJs(receiptsBytes: Buffer[] | string, producerKey: string, decisionKey?: string): ChainResultParameters
- receiptsBytesBuffer[] | stringrequired
- The session's receipts in sequence.
- producerKeystringrequired
- The base64 genesis producer key — a trust-on-first-use (TOFU) pin.
- decisionKeystring
- An optional base64 decision key.
ReturnsChainResult
A ChainResult. Verification is anchored to the genesis producerKey, so a key rotation mid-session does not weaken the pin.
Example
// producerKey is the base64 genesis producer key (a TOFU pin)const result = core.verifySessionChainWithRotationJs( receiptsBytes, producerKeyB64, decisionKeyB64, // optional)
verifyAuditChain()function
Verify a BLAKE3 audit chain from its JSONL bytes.
verifyAuditChain(bytes: Buffer | string): booleanParameters
- bytesBuffer | stringrequired
- The audit log as JSONL — one AuditEntry per line.
Returnsboolean
true if every chainHash links correctly back through the log; false if any link is broken.
Example
const fs = require("node:fs")const jsonl = fs.readFileSync("audit.jsonl")const ok = core.verifyAuditChain(jsonl) // boolean
Transparency proofs
HESO’s transparency log is an RFC-6962 Merkle tree over the receipt log (SHA-256). These two functions check the two proofs that tree supports: that a leaf is included, and that an older tree is a consistent prefix of a newer one.
verifyInclusionJs()function
Check an RFC-6962 inclusion proof: that a leaf sits at an index in a tree of a given size.
verifyInclusionJs(leafValueHex: string, index: number, size: number, rootHex: string, proofHashes: string[]): booleanParameters
- leafValueHexstringrequired
- The leaf value, as hex.
- indexnumberrequired
- The leaf's zero-based position in the tree.
- sizenumberrequired
- The total number of leaves in the tree.
- rootHexstringrequired
- The expected Merkle root, as hex.
- proofHashesstring[]required
- The inclusion proof — the sibling hashes along the path to the root.
Returnsboolean
true if the proof reconstructs rootHex; otherwise false.
Example
const ok = core.verifyInclusionJs( leafValueHex, index, size, rootHex, proofHashes, // string[])
verifyConsistencyJs()function
Check an RFC-6962 consistency proof: that an old tree is a prefix of a new one.
verifyConsistencyJs(oldSize: number, oldRootHex: string, newSize: number, newRootHex: string, proofHashes: string[]): booleanParameters
- oldSizenumberrequired
- The leaf count of the older tree.
- oldRootHexstringrequired
- The Merkle root of the older tree, as hex.
- newSizenumberrequired
- The leaf count of the newer tree.
- newRootHexstringrequired
- The Merkle root of the newer tree, as hex.
- proofHashesstring[]required
- The consistency proof hashes between the two roots.
Returnsboolean
true if the older tree is an append-only prefix of the newer one — nothing was rewritten or removed; otherwise false.
Example
const ok = core.verifyConsistencyJs( oldSize, oldRootHex, newSize, newRootHex, proofHashes,)
Approval tokens
At L1, an approval is carried by an Ed25519 token the approver signs with their own device-held key. This function verifies that token against the action it approves. It checks the scope, expiry, replay nonce, and that the approver’s key is one you registered.
verifyApprovalToken()function
Verify an Ed25519 approval token against an action and return its claims.
verifyApprovalToken( token: Buffer, actionCanonical: Buffer, nowUnixSecs: bigint, seenNonces: Buffer[], requiredScope: string, registeredKeysB64: string[],): ApprovalTokenClaims
Parameters
- tokenBufferrequired
- The approval token bytes.
- actionCanonicalBufferrequired
- The action’s canonical bytes — from actionCanonicalBytesJs() — that the token must bind to.
- nowUnixSecsbigintrequired
- The current time in Unix seconds, used to check the token's expiry.
- seenNoncesBuffer[]required
- Nonces already accepted, so a replayed token is rejected.
- requiredScopestringrequired
- The scope the token must carry, e.g. the action verb it authorizes.
- registeredKeysB64string[]required
- The base64 approver public keys you accept signatures from.
ReturnsApprovalTokenClaims
An ApprovalTokenClaims object: nonce: Buffer, expiryUnixSecs: bigint, scope: string, and approverPublicKey: string.
Throws
A napi Error with a bracketed [CODE] prefix on any failure — an expired token, a wrong scope, a replayed nonce, or a key that is not in registeredKeysB64.
Example
const claims = core.verifyApprovalToken( token, // Buffer actionCanonical, // Buffer (from actionCanonicalBytesJs) BigInt(Math.floor(Date.now() / 1000)), seenNonces, // Buffer[] — replay guard "payment", // requiredScope registeredKeysB64, // string[] of approver public keys)console.log(claims.scope, claims.approverPublicKey)
Redaction
Redaction keeps sensitive field values off our servers before a receipt is signed. There are two modes: destructive, which removes a value outright, and commit-and-reveal, which replaces it with a hash commitment you can later open with a salt. Both leave a marker so the redaction is provable.
redactDestructiveJs()function
Remove the values at the given field paths, leaving well-formed markers behind.
redactDestructiveJs(fieldsJson: string, fieldPaths: string[]): stringParameters
- fieldsJsonstringrequired
- The action fields as a JSON string.
- fieldPathsstring[]required
- Dotted field paths to redact, e.g. "action.fields.member_id".
Returnsstring
The modified fields JSON with the targeted values removed. Each marker carries an empty commitment string in destructive mode — never omitted.
Example
const out = core.redactDestructiveJs( fieldsJson, ["action.fields.member_id"],)// values removed; markers carry an EMPTY commitment string
redactCommitJs()function
Replace field values with BLAKE3 commitments, returning the fields, record, and sidecar.
redactCommitJs(fieldsJson: string, fieldPaths: string[], salts: Buffer[]): RedactCommitResultParameters
- fieldsJsonstringrequired
- The action fields as a JSON string.
- fieldPathsstring[]required
- Dotted field paths to redact.
- saltsBuffer[]required
- One 32-byte Buffer per field path, used to compute each commitment.
ReturnsRedactCommitResult
A RedactCommitResult: fields (the redacted fields JSON), redactionRecord (the record embedded in the receipt, with a Merkle root), and sidecar (the values and salts to keep off-server so you can reveal later).
Example
const crypto = require("node:crypto")const salts = [crypto.randomBytes(32)] // one 32-byte Buffer per field const { fields, redactionRecord, sidecar } = core.redactCommitJs( fieldsJson, ["action.fields.member_id"], salts,)
The commit-and-reveal sidecar holds the original values and salts. Store it where you control it — without it you cannot reveal a committed field, and anyone who has it can.
Keys
Operator keys are Ed25519. Use keyFromSeed when you want the same key every time from a seed you control; use generateKey for a fresh random key. Both return an OperatorKey.
keyFromSeed()function
Derive a deterministic operator key from a seed — the preferred entry point.
keyFromSeed(seed: Buffer): OperatorKeyParameters
- seedBufferrequired
- A 32-byte seed; the same seed always yields the same key.
ReturnsOperatorKey
An OperatorKey. This path is deterministic and does not touch the OS random number generator.
Example
const seed = Buffer.alloc(32, 7) // 32-byte seedconst key = core.keyFromSeed(seed)console.log(key.publicKeyB64())
generateKey()function
Generate a fresh random operator key.
generateKey(): OperatorKeyReturnsOperatorKey
A new random OperatorKey. This is native-only and is never reachable from a WASM path — the browser surface cannot mint signing keys.
Example
const key = core.generateKey() // native-only; never from a wasm pathconsole.log(key.publicKeyB64())
OperatorKeyclass
An operator signing key, returned by keyFromSeed() and generateKey().
class OperatorKey { publicKeyB64(): string}
Returns
publicKeyB64() returns the operator’s public key as base64 — the value that appears as agent_identity in a receipt and in its operator SignatureEntry.
Misc
taxonomyHash()function
Return a hash of the embedded action taxonomy.
taxonomyHash(): stringReturnsstring
The hex hash of the action taxonomy compiled into this build — useful for confirming two surfaces agree on the same verb and field definitions.
Example
const hex = core.taxonomyHash() // hex hash of the embedded action taxonomy