A client-only log
This guide shows how a browser keeps a tamper-evident log of its own, such as what an app did on the device or what a user consented to; read it when the log should live on the user’s device.
Example: examples/client-only
With the safe API it takes two calls, and the log is durable, shared safely between tabs, and signed by a key that no script can export:
import { openBrowserLog, openDeviceKey, verifyReceipt } from "webtessera/browser";
const key = await openDeviceKey(origin); // generated once; non-extractable; kept in IndexedDB
const log = await openBrowserLog({ key }); // kept in IndexedDB, shared by every tab of the page
const receipt = await log.append(entry); // resolves once a signed checkpoint covers it
verifyReceipt(receipt.text, { vkey: log.vkey, data: entry }); // anyone, offline
Choices to make
-
The origin names the log, its key and its IndexedDB database. Choose one per device and per purpose (the example uses
<host>/device/<random>), and remember it: it is public. -
Several tabs write the same log through Web Locks (
log.lockScope === "origin"). Outside a secure context there are no Web Locks, andopenBrowserLogrefuses to open rather than let two tabs fork the log; choosing storage explains the way out. -
Persistence. Ask for
navigator.storage.persist(): a log the browser evicts can no longer prove what it signed (receipts already handed out still verify). -
Reading entries back:
log.entries(from?, to?)streams them in order, each checked against the leaf hash the log’s tiles hold for it, andlog.entry(index)reads one. The example’shistory.tslists the newest twenty, proving each withlog.prove(index):const { size } = await log.latestCheckpoint(); for await (const { index, data } of log.entries(size > 20n ? size - 20n : 0n)) { … }
What it guarantees, and what it does not
The log is tamper-evident: once a receipt or checkpoint has left the page, the device cannot rewrite what came before without every holder noticing. It is not tamper-proof: the device holds its own key, and could sign a different history for someone who has seen nothing yet. Witnessing closes that gap: session receipts.
The example’s Chromium tests cover the guardrails: a tampered receipt, a receipt for other data and one checked with another key all fail; two tabs appending at once make one log; the log refuses to open without Web Locks unless told otherwise. See also the safe API.
This page is generated from docs/guides/client-only-log.md
. Edit it on GitHub.