@treeship/verify
Zero-dependency cryptographic verification for Treeship receipts and certificates. Runs anywhere WASM runs.
@treeship/verify is the verification-only npm package Witness, dashboards, browser viewers, and any third-party consumer embedding Treeship verification installs. Zero dependency on @treeship/sdk, zero subprocess. Only dependency is @treeship/core-wasm (the compiled Rust core, under 170 KB gzipped).
Everything treeship verify --format json produces on the CLI, @treeship/verify produces in-process, anywhere WebAssembly and fetch are available.
Install
npm install @treeship/verifyRuntime support
@treeship/core-wasm@0.24.0 does not load under Node 22. Importing it —
directly, through @treeship/verify, or through @treeship/sdk's offline
verify methods — throws before any verification runs:
WebAssembly.Table.grow(): failed to grow table by 4Reproduced deterministically on Node 22.20.0 and 22.23.1
(darwin-arm64), on a clean npm install @treeship/verify@0.24.0. The failure is
at module load, so every export is affected: verifyReceipt,
verifyCertificate, crossVerify, verifyCapability, verifyResolution,
verifyPresentation.
The cause is that the package shipped only the wasm-pack --target bundler
build, which emits import * as wasm from "./..._bg.wasm" and needs a bundler
to resolve. A fix is in flight: dual-target build plus an exports map routing
Node to the nodejs output. It went unnoticed because the only consumer anyone
exercised was the website, which vendors and bundles the wasm — nobody
installed the tarball and imported it the way a user does.
Installing a fixed @treeship/core-wasm at top level does not work around
it. This package pins core-wasm to an exact version, so npm installs the pinned
copy nested under @treeship/verify, where it wins resolution over anything at
top level. The fix arrives as a coordinated release: v0.25.0 bumps core-wasm and
every dependent's pin together, which is what makes it reach you.
Until then, verify server-side with the CLI (treeship verify <target> --format json) or @treeship/sdk's s.verify.verify(), which shells out to the binary
and is unaffected. Browser runtimes were not tested here — this note records
only what was reproduced under Node.
Same verification semantics in every runtime. The package ships with @treeship/core-wasm pinned to an exact version (no caret), so there is no silent drift between the WASM you install and the rules this package documents.
Trust roots
Certificate-bearing checks (verifyCertificate, crossVerify, verifyCapability) take a trustRoots argument, and fail closed without it. As of the v0.10.3 trust-root audit fix, a certificate is never accepted on its embedded public key alone — that would make every certificate self-signed. You pin which issuer keys may vouch for agents; any other issuer is rejected.
trustRoots accepts the same JSON shape the CLI stores in ~/.treeship/trust_roots.json, or a plain array:
import type { TrustRootInput } from '@treeship/verify';
const trustRoots: TrustRootInput[] = [
{
key_id: 'key_8a3f...',
public_key: 'ed25519:LnR1c3Rl...', // base64url, no padding
kind: 'agent_cert',
label: 'our cert issuer',
},
];kind is one of the v0.19 split trust kinds: 'hub_checkpoint' | 'hub_org' | 'cert_issuer' | 'revoker' | 'agent_cert' | 'session_host'. (The old single ship kind is deprecated and inert — no verifier honors it.)
Omitting trustRoots is allowed by the types, but the result is a deliberate fail-closed verdict — useful only for diagnostic UIs that want to show why a certificate can't be trusted yet.
API
Four functions. Each accepts a parsed object, a JSON string, or a URL.
verifyReceipt(target)
Runs the JSON-level checks a Session Receipt carries: Merkle root recomputation, inclusion proof verification, leaf-count parity, timeline ordering, chain linkage.
import { verifyReceipt } from '@treeship/verify';
const result = await verifyReceipt('https://treeship.dev/receipt/ssn_abc');
if (result.outcome === 'structural-pass') {
console.log(`session ${result.session.id} is structurally consistent`);
console.log('note: signatures and issuer are NOT verified from a fetched receipt');
} else {
console.log(`verification failed:`, result.checks.filter(c => c.status === 'fail'));
}Returns VerifyReceiptResult:
{
outcome: 'pass' | 'structural-pass' | 'fail' | 'error';
checks: { step: string; status: 'pass' | 'fail' | 'warn'; detail: string }[];
session: {
id: string;
ship_id?: string;
schema_version?: string;
agent: string;
duration_ms?: number;
actions: number;
};
error_code?: string;
message?: string;
}A fetched receipt earns structural-pass, not pass: the Merkle structure and inclusion proofs are internally consistent, but authorship is not established (no issuer trust, no envelope signatures). Do not gate on outcome === 'pass' for URL-fetched receipts — it will not match. Signature verification on individual envelopes needs the original envelope bytes; use treeship verify <artifact-id> on the CLI against local storage for that.
verifyCertificate(target, now?, trustRoots?)
Verifies the Ed25519 signature on an Agent Certificate against an issuer key you pin via trustRoots. With now supplied (Date or RFC 3339 string), also classifies the validity window.
import { verifyCertificate } from '@treeship/verify';
const result = await verifyCertificate(cert, new Date(), trustRoots);
if (result.outcome === 'pass' && result.validity === 'valid') {
console.log(`${result.certificate.agent_name} is authorized`);
}trustRoots is required for the signature to be accepted — without it the check fails closed. Omit now (or pass undefined) for signature-only checks (validity: 'not_checked').
crossVerify(receipt, certificate, now?, trustRoots?)
Answers three questions at once: do the receipt and certificate reference the same ship, was the certificate valid at now, was every tool the session called authorized by the certificate. The ok field is the roll-up. now defaults to the current time; trustRoots is required for the certificate's signature to be accepted.
import { crossVerify } from '@treeship/verify';
const result = await crossVerify(receiptUrl, certUrl, undefined, trustRoots);
if (result.ok) {
console.log('complete trust loop verified');
} else {
console.log('ship_id:', result.ship_id_status);
console.log('cert:', result.certificate_status);
console.log('unauthorized tools:', result.unauthorized_tool_calls);
}See Cross-verification for the full semantics.
verifyCapability(card, actions, trustRoots?)
Verifies an agent_card.v1 capability card and cross-checks a set of action envelopes against it — the same check treeship verify-capability runs on the CLI (shared Rust logic). trustRoots is required for key_bound to be true; without a pinned issuer the card is reported self-asserted.
import { verifyCapability } from '@treeship/verify';
const result = await verifyCapability(card, actionEnvelopes, trustRoots);
if (result.status === 'verified') {
console.log(`${result.agent}: ${result.in_scope} in-scope actions, key-bound`);
} else if (result.status === 'self-asserted') {
console.log('card signature ok, but issuer is not in your trust roots');
} else {
console.log('violations:', result.violations);
}Returns CapabilityVerifyResult: { outcome, agent?, key_bound?, declared_tools?, in_scope?, out_of_scope?, violations?, status? } where status is 'verified' | 'self-asserted' | 'violations'.
Honest contract: this is consistency over the actions you provide, not a completeness guarantee. It cannot prove the agent took no off-card action.
Runtime compatibility
| Runtime | Status |
|---|---|
| Node.js 18+ | supported |
| Node.js 20+ | supported |
| Deno | supported |
| Browser (bundler) | supported |
| Vercel Edge | supported |
| Cloudflare Workers | supported |
| AWS Lambda (Node) | supported |
See the edge runtime guide for deployable examples on each target.
Input shapes
All four functions accept four target forms:
| Form | Example | Behavior |
|---|---|---|
| Parsed object | { type: 'treeship/...', ... } | Serialized with JSON.stringify, handed to WASM |
| JSON string | '{"type":"treeship/..."}' | Handed to WASM directly |
| URL string | 'https://treeship.dev/receipt/ssn_abc' | Fetched with global fetch, /receipt/ remapped to /v1/receipt/ |
URL object | new URL('https://...') | Same as URL string |
What this package is NOT
- Not an attestation SDK. For signing artifacts, session management, Hub push/pull, or agent registration, use
@treeship/sdkwhich shells out to thetreeshipCLI. - Not a trust anchor. Certificates are only accepted against issuer keys you pin via
trustRoots— never against the certificate's own embedded key. Deciding which issuers to trust is the caller's responsibility; the package just fails closed until you decide. - Not a drop-in for local-chain verification. Some signature verification needs the original envelope bytes, which a URL-fetched receipt does not carry. Use
treeship verify <artifact-id>on the CLI for that.
Schema versioning
@treeship/verify picks up schema versioning from the WASM core without any API surface change here. A receipt emitted with schema_version: "1" verifies under v1 rules; a pre-v0.9.0 receipt with no field verifies under legacy rules. See Schema versioning for the full story.