# Public proof pages & the verify badge

Veritally can turn any tenant's ledger into a **public, tamper-evident proof** —
a page anyone can open (and, at full disclosure, re-verify in their own browser)
plus an **embeddable badge** that re-checks the live chain. It's the growth loop
(every customer's badge links back) and a Business-tier trust mark.

Everything here is **keyless and slug-scoped** — the public routes never read
`api_keys`, so a proof page can't leak key material, and a tenant is public **only
if it opts in**.

## The three surfaces

| URL | Returns | Auth |
|---|---|---|
| `GET /public/proof/:slug` | JSON attestation (`valid`, `head`, `entries_checked`, adjustments; `entries[]` at full disclosure) | none |
| `GET /public/badge/:slug.svg` | shields-style SVG — green ✓ valid / red ✗ tampered / grey unverified | none |
| `GET /proof/:slug` | human page; **re-verifies in the visitor's browser** at full disclosure | none |

Try the live demo: **`/proof/demo`** (full disclosure — your browser recomputes
all 34 hashes with its own `crypto.subtle`).

## Opt-in & disclosure levels

A tenant is public only when it has a `proof_settings` row. Two levels:

- **`head`** *(default)* — publishes only the **commitment**: the valid flag, the
  chain head hash, the entry count, and the date range. **No amounts, no line
  items.** The safe choice for a real company that wants the trust mark without
  publishing its books. (A visitor trusts our attestation of the head; they can't
  recompute without the entries.)
- **`full`** — also publishes the ledger **entries**, so anyone can recompute
  `hash = SHA-256(prev_hash | entry)` for every row **in their own browser** and
  confirm the chain independently. Trustless, but it discloses the line items —
  use it for transparency pages, not a private company's real books.

## Turn it on for a customer

```sql
-- head disclosure (commitment only) — the usual choice:
INSERT INTO proof_settings (slug,tenant_id,display_name,disclosure)
  VALUES ('acme','<tenant_id>','Acme Robotics','head');

-- full disclosure (browser-verifiable, discloses line items):
INSERT INTO proof_settings (slug,tenant_id,display_name,disclosure)
  VALUES ('acme','<tenant_id>','Acme Robotics','full');
```

Run it with `wrangler d1 execute veritally --remote --command="…"`. The slug must
match `[a-z0-9_-]{1,64}`. To un-publish: `DELETE FROM proof_settings WHERE slug='acme'`.

Existing databases need the table first — apply
`migrations/0003_public_proof.sql` once (additive, safe to re-run; it also seeds
the `demo` slug). Fresh installs get the table from `schema.sql`.

## The embed

Hand a customer this snippet (swap `your-team` for their slug):

```html
<a href="https://veritally.ai/proof/your-team">
  <img src="https://veritally.ai/public/badge/your-team.svg"
       alt="Books verified by Veritally" height="20">
</a>
```

The badge is served with `Cache-Control: max-age=300`, so it reflects the live
chain within ~5 minutes. A mistyped slug renders a neutral grey "unverified"
badge (HTTP 200) rather than a broken image.

## How the browser proof can't lie

The full-disclosure page doesn't ship a hand-rolled SHA-256 to trust. It builds a
byte array with the **same model** `engine.sha256` uses (Latin-1 for ≤ U+00FF,
UTF-8 above it — so `·` in GL names and any emoji hash identically) and feeds it
to the platform's own `crypto.subtle.digest('SHA-256', …)`. `parity-test.mjs`
locks that `SHA-256(toBytes(s)) === engine.sha256(s)`, so the browser verifier
can never silently disagree with the server.

## Security notes

- **Opt-in only** — no `proof_settings` row → `404` on all three surfaces.
- **Slug-scoped, keyless** — the public path resolves a tenant by slug and never
  touches `api_keys`.
- **Tenant-isolated** — a proof reflects exactly one tenant's ledger.
- **Disclosure is a deliberate choice** — `head` never returns line items; `full`
  returns them by design. Default is `head`.
- **XSS-safe** — the display name and every ledger field are HTML-escaped in the
  page and `</script>`-escaped inside the embedded JSON (covered by `proof-test.mjs`).
