tn-proto — Browser SDK

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.

1Getting started

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.

hello.attested-log.mjs

2Logging & reading backruns in browser

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.

log-verbs.mjs

The receipt — full envelope with signature & chain hash

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.

receipt.mjs

Levels, context, and scopes

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.

levels-and-context.mjs

3Shipping to a server / vault & restoring

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.

HTTP shipping — the real transport

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.

http-ship.mjs

Restore on another "device"

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.

backup-restore.mjs

4Key storage, the JS way

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.

In-memory — ephemeral, safest

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

memory-keystore.mjs

localStorage — persistent across reloads

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.

localstorage-keystore.mjs

Custom adapter — IndexedDB, OPFS, encrypted-at-rest, anything

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.

custom-adapter.mjs

Recovery: identity = a 32-byte seed

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.

seed-recovery.mjs