Start free

Node core — @hesohq/core

The @hesohq/core package (the native @hesohq/node addon, built with napi-rs) is the full trust-layer surface for Node: verify, hash, canonicalize, chains, transparency proofs, redaction, and key handling.

@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.

bash
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.
verify.ts
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.

entries.ts
// 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"
Verify-only in the 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.

ts
verify(receiptBytes: Buffer | Uint8Array | string): ActionVerdict

Parameters

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

verify.ts
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.

ts
verifyWithTime(receiptBytes: Buffer | Uint8Array | string): ActionVerdictWithTime

Parameters

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

ts
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.

ts
contentHash(contentJson: string): string

Parameters

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

ts
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.

ts
anchoredContentHashJs(contentJson: string): string

Parameters

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

ts
const preAnchor = core.anchoredContentHashJs(contentJson)

actionCanonicalBytesJs()function

Return the exact RFC-8785 canonical bytes, with action_hash stripped.

ts
actionCanonicalBytesJs(contentJson: string): Buffer

Parameters

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

ts
const canonical = core.actionCanonicalBytesJs(contentJson) // Buffer (RFC-8785 / JCS)

shortHash()function

Abbreviate a long hex string for display, with an optional prefix.

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

Parameters

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

ts
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.

ts
chainHashHex(prevHex: string, actionHex: string): string

Parameters

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

ts
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.

ts
verifyChain(receiptsBytes: Buffer[] | string): ChainResult

Parameters

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

ts
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.

ts
verifySessionChainJs(receiptsBytes: Buffer[] | string): ChainResult

Parameters

receiptsBytesBuffer[] | stringrequired
The session's receipts in sequence.

ReturnsChainResult

A ChainResult, the same shape as verifyChain().

Example

ts
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.

ts
verifySessionChainWithRotationJs(receiptsBytes: Buffer[] | string, producerKey: string, decisionKey?: string): ChainResult

Parameters

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

ts
// 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.

ts
verifyAuditChain(bytes: Buffer | string): boolean

Parameters

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

ts
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.

ts
verifyInclusionJs(leafValueHex: string, index: number, size: number, rootHex: string, proofHashes: string[]): boolean

Parameters

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

ts
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.

ts
verifyConsistencyJs(oldSize: number, oldRootHex: string, newSize: number, newRootHex: string, proofHashes: string[]): boolean

Parameters

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

ts
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.

ts
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

ts
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.

ts
redactDestructiveJs(fieldsJson: string, fieldPaths: string[]): string

Parameters

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

ts
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.

ts
redactCommitJs(fieldsJson: string, fieldPaths: string[], salts: Buffer[]): RedactCommitResult

Parameters

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

ts
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,)
Keep the sidecar safe

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.

ts
keyFromSeed(seed: Buffer): OperatorKey

Parameters

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

ts
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.

ts
generateKey(): OperatorKey

ReturnsOperatorKey

A new random OperatorKey. This is native-only and is never reachable from a WASM path — the browser surface cannot mint signing keys.

Example

ts
const key = core.generateKey() // native-only; never from a wasm pathconsole.log(key.publicKeyB64())

OperatorKeyclass

An operator signing key, returned by keyFromSeed() and generateKey().

ts
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.

ts
taxonomyHash(): string

Returnsstring

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

ts
const hex = core.taxonomyHash() // hex hash of the embedded action taxonomy

Next steps