tn-proto is an attested-logging SDK: every call writes a signed, hash-chained,
field-encrypted record. The browser build is a single self-contained ES module
with the Rust/WASM core inlined — no separate .wasm to fetch.
Every example on this page runs live in your browser against the real bundle. Edit any
block and hit Run.
Import the bundle as a namespace and call tn.init(). The WASM core is
inlined and initializes synchronously inside that call — there is no separate
initWasm() step and no async font of .wasm to await.
On first run in an origin a fresh ceremony is minted (device Ed25519 key, group publisher state,
a tn.yaml manifest); later calls reload the same identity.
Import it straight from the CDN — one self-contained file, served by Cloudflare, no separate
.wasm to host:
import * as tn from "https://tn-proto.org/cdn/tn-proto.browser.mjs";
On npm the same surface is @cyaxios/tn-proto/browser.
These examples use a fresh in-memory keystore (memoryStorageAdapter()) so
re-running never collides with a previous run. Section 4 covers persistent storage.
There are five write verbs. info / warning /
error / debug carry a severity and respect the
level threshold; log is severity-less and always emits (use it for audit
facts). Each takes a dotted event type and a fields object. Fields are encrypted at rest;
a few envelope basics (timestamp, event type, level, DID, sequence, id) stay in the clear.
readRaw() returns { envelope, plaintext } per entry.
The envelope is the audit-grade record: an Ed25519 signature over the
row_hash, a prev_hash linking it to the entry before it,
and the per-group ciphertext. plaintext is your decrypted fields, keyed by group.
This is the receipt.
Tn.setLevel("warning") drops anything below WARNING — except
log, which is severity-less by design.
setContext stamps fields onto every subsequent event;
scope overlays fields for one block only.
The browser bundle ships attested envelopes one way: an HTTP handler. Pass
http to init and every signed envelope is
POSTed (batched, retried on 5xx, drained on page unload) to your endpoint — a witness, an
ingest worker, or a hosted vault. The body is the exact canonical bytes that were attested,
so server-side signature and chain checks run against precisely what the client emitted.
From the browser, the HTTP handler is the transport: it ships attested envelopes to a server you control. Account-connect and wallet-sync to a hosted vault are the Node and CLI surface.
This runs entirely in-page: we pass a local fetch stand-in so you can
inspect the exact bytes that would go over the wire. Swap in a real URL and it POSTs for real.
A keystore is just a bag of paths → bytes. To move an identity to another device, snapshot the storage adapter, ship the bytes, and replay them into a fresh adapter. The reopened client has the same DID and can decrypt the same log. This is the browser-native backup/restore — no server required.
The runtime never touches storage directly — it calls a small synchronous adapter for every
read/write. You choose where keys live by choosing the adapter. The keystore that gets written is
ten files: the device private seed, the group publisher state + your self-kit, an index-master
HMAC key, the tn.yaml manifest, and the log.
memoryStorageAdapter(): keys live only in the JS heap, gone on reload.
Best for tests, throwaway sessions, or "ship every envelope to a server and keep nothing local."
localStorageStorageAdapter(): synchronous, persists per-origin (~5 MB quota).
The default for Tn.init() with no storage option.
Bytes are base64-encoded into string slots. Tradeoff: readable by any script on the origin and any
XSS — fine for low-stakes attested logs, not for high-value long-lived secrets.
A keyPrefix lets independent identities coexist on one origin (think
"two devices," or per-account separation). Quota overflow throws
LocalStorageQuotaError.
Because the adapter is just a nine-method synchronous interface, you can back it with whatever you
like. The runtime requires the calls to be synchronous, so a truly async store (IndexedDB,
OPFS) needs an in-memory write-through cache that you flush asynchronously yourself. The pattern
below wraps memoryStorageAdapter() and intercepts write
to also encrypt-and-persist each slot. This is the seam for passphrase-derived encryption: derive a
key with crypto.subtle (PBKDF2 → AES-GCM) and wrap the bytes before they leave the page.
The whole identity reduces to one 32-byte Ed25519 seed at
.../keys/local.private. Hold that seed safely (the Node/CLI side also wraps it
as a BIP-39 recovery phrase and pushes it to the hosted vault) and you can reconstruct the same DID anywhere with
createFreshCeremony(storage, { devicePrivateBytes: seed }). The example
proves the rebuilt ceremony has the identical DID.
Safe-where guidance: keep the seed in memory or a custom encrypted-at-rest adapter, never plain localStorage for a high-value identity. localStorage is fine for disposable per-session logging identities; for anything durable, encrypt the seed under a passphrase or push it to the hosted vault (Node/CLI) and treat the recovery phrase as the cold backup.