Part of TunnelMind — the intelligence layer agents call before they trust the internet. Three lenses on one signed corpus: Scry (who is attacking?) · Sigil (who can you trust?) · Tracker Data API (who is watching?). This package serves: the receipt verifier. Apache-2.0. See tunnelmind.ai/standards/receipt-format/v1.
Reference TypeScript verifier for the TunnelMind Receipt Format v1.0. Implements §4 (Verification procedure) end-to-end plus §8 (Revocation) lookups.
npm install @tunnelmindai/receipt-verifyZero runtime dependencies. Targets WebCrypto SubtleCrypto + fetch — runs on Node ≥18, Cloudflare Workers, Deno, Bun.
import { verifyReceipt } from '@tunnelmindai/receipt-verify';
const receipt = await (
await fetch('https://data.tunnelmind.ai/v1/receipt/generate?receipt=true', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event_type: 'agent.action', payload: { foo: 'bar' } }),
})
).json();
const result = await verifyReceipt(receipt);
if (result.valid) {
console.log('✓ verified', result.receipt_id, 'signed by', result.key_id);
} else {
console.error('rejected:', result.errors);
console.warn('warnings:', result.warnings);
}- Version —
receipt_version === "1.0"(verifiers MUST reject unknown majors) - Payload hash — recompute
0x + hex(SHA-256(JCS(payload))), compare topayload_hash - Signature — reconstruct the signing object (§3 step 2), JCS-canonicalize, verify Ed25519 against the embedded
signature.public_key - Key bundle resolution — fetch
https://tunnelmind.ai/.well-known/receipt-signing-key.json, confirm the resolved key'spublic_keymatches the receipt's - Attestation-strength ceiling — reject if
receipt.attestation_strengthexceeds the resolved key's declared strength - Revocation — fetch
https://tunnelmind.ai/.well-known/receipt-revocations.json:- if
signature.key_idis revoked AND the receipt was signed afterrevoked_at, reject - if
receipt_idis inrevoked_receipts, reject
- if
- Chain sanity — warn (not reject) if
chain.sequenceandprevious_receipt_hashare inconsistent
The first 3 checks are pure-crypto and require no network. Checks 4-6 fetch well-known feeds, which can be disabled or pre-loaded.
For high-throughput or air-gapped verifiers, pre-fetch the well-known feeds and pass them in:
import { verifyReceipt, fetchKeyBundle, fetchRevocations } from '@tunnelmindai/receipt-verify';
const [keys, revocations] = await Promise.all([fetchKeyBundle(), fetchRevocations()]);
const result = await verifyReceipt(receipt, { keys, revocations });For fully-offline verification (pure crypto + embedded public_key only):
const result = await verifyReceipt(receipt, {
noFetchKeys: true,
noFetchRevocations: true,
});When the issuer rotates a key, both the old and new key are live for ~24h. During that window, accept a key-bundle mismatch as a warning:
const result = await verifyReceipt(receipt, { allowKeyMismatch: true });Main entry point. Returns Promise<VerifyResult>:
interface VerifyResult {
valid: boolean;
errors: string[];
warnings: string[];
receipt_id?: string;
key_id?: string;
attestation_strength?: 'self-asserted' | 'software' | 'tee-tpm' | 'silicon-root';
}| Option | Default | Purpose |
|---|---|---|
keys |
(fetched) | Pre-fetched KeyBundle to skip the network |
noFetchKeys |
false |
Skip key-bundle resolution entirely |
keysUrl |
tunnelmind.ai/.well-known/receipt-signing-key.json |
Self-hosted issuer override |
revocations |
(fetched) | Pre-fetched RevocationFeed |
noFetchRevocations |
false |
Skip revocation check entirely |
revocationsUrl |
tunnelmind.ai/.well-known/receipt-revocations.json |
Self-hosted issuer override |
allowKeyMismatch |
false |
Demote key-mismatch error → warning |
fetcher |
globalThis.fetch |
Custom fetch (e.g., for tests) |
fetchKeyBundle, resolveKey, clearKeyCache, fetchRevocations, isKeyRevoked, isReceiptRevoked, clearRevocationCache, canonicalize, canonicalizeBytes, RECEIPT_VERSION.
Receipts are designed to be verifiable by anyone — the public key is embedded in the receipt and validated against TunnelMind's published key bundle. You don't need credentials, an account, or any prior relationship with the issuer.
That said: the key bundle and revocation feed must be retrievable from a source you trust. The defaults point to tunnelmind.ai; for receipts issued by another producer, point keysUrl / revocationsUrl at the producer's well-known endpoints.
The JCS canonicalizer in src/jcs.ts is bit-identical to the issuer-side serializers in scry-server/src/lib/receipt_v1.js and tunnelmind-data-api/api/utils/receipt-v1.js. Any change to one MUST be mirrored across all three. The frozen wire vector lives in this package's test/golden.test.js to catch silent drift.
Apache-2.0. The receipt-format spec text itself is CC-BY-4.0 (separately hosted).
@tunnelmindai/atap— ATAP receipt verifier (different format, complementary use case)@tunnelmindai/eat— EAT Profile v0.1 verifier (RFC 9711 alternative serialization of the same claim set)agent-onboarding.md— the 5-call golden path that uses these receipts end-to-end