# TrusteeClear verification bundle, version 2

This document specifies the bundle TrusteeClear writes for an artifact (an accounting, a notice, a court form or a
packet), so that anyone who receives one can check it without an account. The machine-readable schema is
[`tc-manifest-2.schema.json`](https://trusteeclear.com/schemas/tc-manifest-2.schema.json).

## What a bundle proves, and what it does not

A bundle lets a reader check four things, each separately, each with its own result:

1. **The artifact's bytes** are the bytes the manifest describes (SHA-256).
2. **Each quotation** is the passage of its source that it claims to be. A reader can check this only when the bundle
   carries that source's text.
3. **The approval**, when there is one, covers this version of the artifact and not an earlier one.
4. **The receipt chain's structure** has no gap and starts where it says.

It does **not** prove:

- **Authenticity.** A reader who holds a bundle can change it and recompute every digest inside it. Whether TrusteeClear
  issued it is a separate question, answered by a trust root (below), not by the bundle's own digests.
- **That the record is complete or legally correct.** It shows what was recorded and approved, not that nothing is
  missing or that the law was applied correctly.
- **That a statute is quoted correctly unless its text is included.** A quotation whose source text the bundle does not
  carry cannot be checked, and a verifier says so.

No single "verified" result exists. Each check reports pass, fail, missing or not checked, and a reader weighs them.

## Canonical form and digests

- **Canonical form:** the manifest, and every JSON file a bundle carries, are in RFC 8785 canonical form (the JSON
  Canonicalization Scheme):
  - object keys sort by UTF-16 code units;
  - strings and numbers serialise as ECMAScript's `JSON.stringify` does;
  - there is no whitespace;
  - a non-finite number or a lone surrogate is refused.
- **Encoding:** text is UTF-8.
- **Digests:** lowercase hexadecimal SHA-256 of the exact bytes.
- **The bundle file:** the bundle document itself is written in canonical form, so the same bundle always has the same
  bytes.
- **The manifest's fingerprint:** the SHA-256 of the manifest's canonical text. It is the value to quote when referring
  to a bundle.

## The bundle document

```json
{ "format": "tc-bundle", "version": 2, "manifest": { ... }, "files": [ { "path", "media_type", "encoding", "data" } ] }
```

- **`files`:** each path appears once:
  - `artifact.json`, `artifact.txt` or `artifact.pdf` for the artifact;
  - `sources/N.txt` for a source's text.
- **`encoding`:** `utf8` (the data is the text) or `base64` (the data is standard base64 of the bytes).
- **Supported media types:**
  - `application/json`, whose data is its RFC 8785 canonical text;
  - `text/plain`;
  - `application/pdf`.

## The manifest

| Field | Meaning |
| --- | --- |
| `format`, `version` | `tc-manifest`, `2`. |
| `id`, `issued_at`, `issuer` | The manifest's identifier, when it was written, and who wrote it (`https://trusteeclear.com`). |
| `canonical_form`, `digest` | `rfc8785`, `sha-256`. |
| `artifact` | See [The artifact](#the-artifact). |
| `review` | See [Review](#review). |
| `sources` | Each source the artifact relies on: its identifier, kind (`document` or `statute`), version, the recorded digest of its file when there is one, and the path and digest of its text when the bundle carries it. |
| `quotes` | Each quotation: the source, the passage (UTF-16 offsets `start` to `end` into the source's text), the words **as the artifact states them**, and their digest. |
| `statutes` | The statute versions the artifact was issued under: code, effective date, when TrusteeClear's statute record was validated, and whether that record was attested. |
| `chain` | The receipt this approval minted, and the matter's receipt links, each by its content hash and its predecessor's, from the first to this one. Hashes only, no content. |
| `checks` | Checks already run, with their results: by the database (for example `receipt_integrity_v1`, which recomputes the receipt against the record) or by the writer (for example `quotes_match_sources`). A reader runs its own. |
| `omissions` | What the bundle leaves out, and why (for example, the matter's documents stay with the firm). |
| `trust_roots` | How to establish authenticity. See [Trust roots](#trust-roots). |

### The artifact

| Field | Meaning |
| --- | --- |
| `kind` | `accounting`, `court_form`, `notice` or `packet`. |
| `ref` | The record's identifier. |
| `version` | The artifact's version. |
| `path` | The file in the bundle. |
| `media_type` | The file's media type. |
| `sha256` | The digest of that file's bytes. |
| `bytes` | The file's length in bytes. |
| `legacy_sha256` | The content hash in TrusteeClear's database, when the artifact has one. See [Version 1](#version-1-and-the-legacy-content-hash). |

### Review

- **`class`** is one of:
  - `information_only`;
  - `ai_self_help`;
  - `staff_prepared`;
  - `attorney_reviewed`;
  - `independent_legal_services`.
- **`approval` is present only when `class` is `attorney_reviewed`**, and it is refused for every other class. It
  carries:
  - the version and database content hash the approval covered;
  - when it was given;
  - the attestation's identifier and version;
  - the reviewer's credential as TrusteeClear verified it: jurisdiction, Bar number, licence status, when and how it
    was verified.
- **A self-help organiser, or anything a firm staff member prepared, never carries an approval**, so it can never look
  attorney-approved.

## Trust roots

A bundle's own digests show consistency, not origin. Version 2 defines one trust root, and names what it relies on.

- **`receipt_lookup`:** `https://trusteeclear.com/api/verify-receipt`. Given the artifact's `legacy_sha256`, it answers
  whether TrusteeClear's database holds a receipt for that hash: its type, when, the statute versions and the chain
  position. A bundle's `legacy_sha256` is only a claim: anyone can copy a real receipt's hash from a printed receipt.
  So a verifier also sends `artifactSha256`, the SHA-256 of the artifact's bytes it holds (after `artifact_bytes`
  passes), and the lookup answers `content`: `match` when the content that receipt covers has exactly those bytes,
  `differs` when it has other bytes, and `not_comparable` when the record has moved on to another version. Only
  `match` makes `authenticity` pass. The lookup receives the two hashes, never the file. It relies on TrusteeClear's
  server. A reader who does not want to rely on it can instead keep the bundle and compare it later.
- **Signatures:** none in version 2, and the schema has no signature field. A verifier reports a bundle that carries a
  `signature` as an unsupported signature: it sets the signature aside, checks the rest, and never counts it.
- **External time anchors:** planned and not yet active. Until they are, a bundle names none.

## Version 1 and the legacy content hash

Version 1 is the receipt row TrusteeClear's database mints inside the same transaction that records a firm attorney's
approval.
- **It stays valid,** and the receipt lookup still answers for it.
- **Its content hash uses the database's own canonical form.** That form orders object keys by the database's locale
  collation and keeps a number's written scale (`1.50` stays `1.50`). An independent program cannot reproduce it
  reliably, so a verifier checks `legacy_sha256` only online, through the receipt lookup, and never recomputes it.
- **The bundle's own `artifact.sha256`** is over the RFC 8785 bytes the bundle carries, and any reader can recompute
  it.

## The verifier

TrusteeClear publishes a verifier that runs every check on the reader's own device and sends nothing anywhere:
- [`tc-verify.mjs`](https://trusteeclear.com/verifier/tc-verify.mjs), under the MIT licence, with no dependencies;
- the same checks on [`/verify`](https://trusteeclear.com/verify), in a browser.

In Node 20 or later, run `node tc-verify.mjs bundle.json` (add `--json` for the report as JSON). It exits 1 when a check
fails or the bundle is refused, and 0 otherwise. In a browser or in Node, `import { verifyBundle } from "./tc-verify.mjs"`
and `await verifyBundle(textOrBytes)`. The same file checks an export package (below), which it recognises by its
`format`: `node tc-verify.mjs package.json` exits 1 when the package does not match its manifest, and
`await verifyPackage(textOrBytes)` returns the report.

It is generated from the same modules TrusteeClear's writer uses, and a test holds the published file to them. Its own
tests use bundles written by a separate implementation, so a mistake the writer and the verifier shared would not pass.

Each named check reports one result: `pass`, `fail`, `missing`, `not_checked`, `not_applicable` or `unsupported`. Each
result comes with a stable reason code.

| Check | What it checks |
| --- | --- |
| `format` | The bundle follows version 2. An unknown version is `unsupported`, never read as version 2. |
| `artifact_bytes` | The artifact's bytes match its SHA-256 and length. A modified artifact fails here. |
| `artifact_canonical` | A JSON artifact is in RFC 8785 canonical form. |
| `sources` | Each source's text matches its digest. |
| `quotes` | Each quotation is the passage it names. A quotation whose words were changed and its digests recomputed still fails here. A quotation whose source text the bundle does not carry is `missing`. |
| `quotes_in_artifact` | The artifact carries each quotation as recorded (JSON and text artifacts). |
| `approval_current` | An approval covers this version and content. A stale approval fails. |
| `chain` | The receipt chain runs from the matter's first receipt to this one, link by link. An incomplete chain fails. This checks structure only. |
| `authenticity` | Who issued the bundle. Offline this is always `not_checked`. It passes only through the receipt lookup, which the reader chooses to run, which sends the artifact's `legacy_sha256` and the SHA-256 of the bytes in hand, and which relies on TrusteeClear's server: it passes when the content the receipt covers has exactly those bytes, fails when the receipt is missing or covers other bytes, and is `not_checked` when the record has moved on or the bytes failed their own check. A signature is `unsupported`. |

## Share links

A firm's staff can share one version of a bundle by a link. The link is pinned to that version and its bytes.
- **The token** is `s2.<deliverable id>.<version>.<the bundle's SHA-256, first 16 hex>.<secret>`. It names the payload,
  its version and its hash. TrusteeClear keeps only the token's SHA-256, and shows the link once, to the person who
  made it.
- **The bundle** is kept exactly as it was shared. A later change to the document never reaches the link: the sharer's
  list says when the document has moved on, and a new version needs a new link. A bundle is shared only when it is this
  document's current version and its artifact is the stored content. It may claim an approval only when that version
  was approved and its receipt covers it.
- **The recipient** opens `https://trusteeclear.com/verify#share=<token>`. The token stays in the address's fragment, and
  reaches the server only in the body of one request to `/api/share`. That request is anonymous, is not cached and sends
  no referrer. The page checks that the bundle's digest and version are the ones the token names, then runs every check
  above on the recipient's device.
- **A link stops opening** when it expires (1 to 365 days) or is revoked with a reason, when the firm's account is no
  longer active, or when the person who shared it no longer holds a staff role for that Trust or is screened from it.
  The page names which of these it is (invalid, revoked, expired or unavailable), without saying anything about the
  document.

## Export packages

An export package carries a matter's, a firm's or a person's own records in one JSON document, beside the version 1
exports, which are unchanged. It is a format of its own, not a bundle: its `format` is `tc-package`.
- **The document** is `{"format": "tc-package", "version": 2, "manifest": …, "files": […]}` in RFC 8785 canonical form.
  Each file is `{"path", "media_type", "encoding", "data"}`, where `encoding` is `utf8` (the text itself) or `base64`
  (standard, padded).
- **The manifest** (`format` `tc-package-manifest`, `version` 2) names the `kind` (`matter`, `firm` or `consumer`), when
  and by whom the package was made, its `scope` (the firm and the matter), the version 1 export it stands beside (`v1`),
  and three lists:
  - `records`: one entry for each record set, with its `path` under `records/`, the rows it carries (`count`), the rows
    that exist (`total`, with `truncated` when fewer are carried), and the SHA-256 and length of the set's RFC 8785
    bytes;
  - `files`: each document (`documents/<id>/<name>`, its stored bytes) and each approved artifact
    (`artifacts/<id>/v<version>.json`, its RFC 8785 content), with its SHA-256 and length, and the digest TrusteeClear
    recorded for it where it has one (`recorded_sha256`);
  - `omissions`: what the package does not carry, and why: `not_clean` (the document has not passed its virus scan),
    `over_package_bound` (past the package's bound) or `unreadable` (the stored copy could not be read).
- **A reader checks a package** file by file: every file the manifest names is present once, with the named SHA-256 and
  length; every record set has the rows its count says; and the package holds nothing the manifest does not name. A
  verifier reports each problem by its code: `too_large`, `not_json`, `not_a_package`, `unsupported_version`,
  `malformed_manifest` (including a path outside the package's three folders, or a path named twice), `file_missing`,
  `file_unlisted`, `file_duplicated`, `file_bytes_differ`, `file_length_differs` and `record_count_differs`.
- **What it does not show:** a package's own digests never show who made it. TrusteeClear records each package's SHA-256
  and its manifest's SHA-256 when it makes one, and shows the package's SHA-256 beside its download link; compare them.
- **Bounds:** a package is at most 48 MiB, with at most 1,000 files, 64 record sets and 5,000 omissions. The documents it
  carries total at most 32 MiB; any past that are listed as omitted, never cut.
- **Where it is kept:** privately, for seven days, behind a download link that lasts five minutes. Its record, the
  digests and counts but never the content, stays after the package is removed.

## Chain health

A matter's receipts form a hash chain: each receipt names the content hash of the receipt before it, and the matter's
first receipt names none. TrusteeClear reads the chain three ways, and each check has its own result: `pass`, `fail` or
`not_checked`. There is no single result for the chain.

| Check | What it checks |
| --- | --- |
| `recompute` | The database recomputes every receipt from its sources: the attestation it cites, the artifact it covers (unless that artifact has moved on to a newer version), and the receipt before it. |
| `structure` | The chain as a whole: it starts at the matter's first receipt, each link follows the one before it, no receipt appears twice or out of order, and it ends at the head the database recorded as receipts were written. A chain that lost its start or its tail fails, each for its own reason. |
| `external_anchor` | A commitment published outside TrusteeClear covers the chain's link at a receipt. None is active: until TrusteeClear chooses a provider, this is `not_checked`. |

- **A receipt never changes once written.** It is removed only with its matter, and its removal leaves a tombstone:
  the receipt's position, its hash and the hash before it, when it was removed and why. The tombstone never keeps the
  artifact's content, the statutes or the field hashes. The walk reads a tombstone as a link, so a removal is
  disclosed, never a gap.
- **An external anchor proves that a commitment existed at a time.** It does not prove that the records are complete
  or legally correct. The commitment would be one SHA-256 over every matter's chain head: a Merkle tree hash in the form
  of RFC 9162, where a leaf is `0x00` followed by `tc-chain-head`, the matter, the receipt and its hash (one per line),
  and a node is `0x01` followed by its two children. The root names no matter and no receipt. Each matter keeps its
  leaf and its inclusion path.
- **Nothing is submitted anywhere yet.** A commitment moves from prepared (`not_submitted`) to sent (`pending`, or
  `unavailable` when the provider could not be reached, kept and sent again) to confirmed (`anchored`, with its proof).
  The provider, the privacy of what is published and who operates it are decided before anything is sent.

## Resource bounds

A verifier refuses a bundle that is:
- larger than 25 MiB;
- carrying more than 200 files, 200 sources, 500 quotations, 1,000 statutes, 10,000 chain links, 100 checks or 100
  omissions;
- carrying a text field longer than 20,000 characters.

It refuses before hashing anything.

## Compatibility

- **Versions:** a manifest or bundle whose `version` is not `2` is refused as unsupported, and never read as version 2.
- **Fields:** an unknown field is refused, and never ignored. A later version will say what it adds.
