# Message Integrity

Signed, append-only **message revisions** give P2P chat a verifiable history without central message storage. The materialized `Message` row in local SQLite/IDB is a cache; peers converge via inventory snapshots and revision events.

## Responsibilities

- **Revision chain** — Every create, edit, delete, moderation, or plugin mutation appends a `MessageRevision` with monotonically increasing `revision`, `prevRevisionHash`, and `headHash`.
- **Dual emit** — Outgoing mutations broadcast the legacy event (`chat-message`, `message-edited`, `message-deleted`) **and** `message-revision` so older peers keep working while integrity-aware peers prefer revisions.
- **Inventory** — Sync inventories include `{ id, ts, rc, ac, revision, headHash }`. Peers re-fetch when remote revision is newer or the same revision has a different hash (tamper detection).
- **Signing** — Human authors sign revisions with per-user Ed25519 keys. Public keys are registered on the signaling server; private keys stay in browser `localStorage`.

## Boundaries

| Layer | Owns |
| --- | --- |
| Product client (`toju-app`) | Revision construction, merge, verification, P2P broadcast, local persistence |
| Signaling server (`server`) | `PUT /api/users/me/signing-key`, `GET /api/users/:id/signing-public-key` — key directory only, no message storage |
| Electron / mobile persistence | `revision` + `headHash` on message rows; revision audit log (IDB store / SQLite meta) |

Plugin API messages may emit unsigned revisions (`plugin-edit` / `plugin-delete`) when the actor is a synthetic plugin user.

## Key types

- `Message.revision`, `Message.headHash` — materialized cache fields on the shared `Message` model.
- `MessageRevision` — wire + persistence audit record (`message-revision.models.ts`).
- `MessageRevisionType` — `create`, `author-edit`, `author-delete`, `moderate-edit`, `moderate-delete`, `plugin-edit`, `plugin-delete`.
- `ChatEvent.type: 'message-revision'` — P2P envelope carrying a full `MessageRevision`.

## Merge rules

1. Valid signed revision with higher `revision` wins over legacy timestamp edits.
2. Same `revision`, different `headHash` → treat as stale/tampered and re-fetch.
3. Unsigned revisions (no `signature`) are accepted for backward compatibility when verification is skipped.
4. Legacy peers without `revision`/`headHash` in inventory fall back to `ts` / `rc` / `ac` comparison.

## Client touchpoints

- Domain rules: `message-integrity.rules.ts`, `message-revision.builder.rules.ts`, `message-sync.rules.ts`
- Services: `MessageRevisionService`, `MessageSigningService`
- Store: `messages.effects.ts` (outgoing dual-emit), `messages-incoming.handlers.ts` (`handleMessageRevision`), `messages.helpers.ts` (inventory + merge)
- Plugins: `plugin-client-api.service.ts` emits revisions for send/edit/delete

## Server API

| Method | Path | Auth | Body / response |
| --- | --- | --- | --- |
| `PUT` | `/api/users/me/signing-key` | Bearer | `{ publicKeyJwk }` — stores Ed25519 public JWK on the user row |
| `GET` | `/api/users/:id/signing-public-key` | Public | `{ publicKeyJwk }` — used by peers to verify signatures |

Registration runs automatically after login/register via `AuthenticationService`.

## Degraded-mode behavior

- Outgoing revision signing is **best-effort**: if `Ed25519` signing fails, the client still broadcasts the legacy `chat-message` envelope (unsigned revision).
- Incoming signed revisions are accepted without cryptographic verification when the sender's public key is not yet registered on the server, so chat is not blocked during key-registration races.

## Testing

- Unit: `message-integrity.rules.spec.ts`, `message-revision.builder.rules.spec.ts`, `message-revision-signing.rules.spec.ts`, `message-sync.rules.spec.ts`, `messages-incoming.handlers.spec.ts`
- Outgoing revision wiring is covered indirectly through existing message effect tests; add focused specs when changing merge or signing behavior.
