# Trust UX — Fingerprint Gates (V3.3) > Status: shipped in 0.4.0, GA-frozen in 4.0 — see [V3.3 plan](./archive/V3.3.md). Shade ships with a small number of **blocking** verification gates that fire automatically before the operations where MITM risk is highest. Each gate calls a handler you register on the SDK; until the user (or your handler) approves, the operation aborts with `FingerprintNotVerifiedError`. The point of the gate model is to be alert-fatigue-free: you don't see a prompt before every chat message, just before the handful of moments that genuinely matter. --- ## What the gates protect | Gate | Fires when | Default policy | |------|------------|----------------| | `first-large-file` | `Shade.upload(...)` for an unverified peer with a known size at or above the configured threshold. | Threshold `10 MiB`. Below = no gate. | | `backup-import` | `Shade.importBackup(...)` before any state is written. Handler receives the fingerprint of the identity *embedded in the backup*. | Always fires. | | `new-device-trust` | `Shade.acceptIdentityChange(...)` after a peer rotates identity. The peer's `identity_version` is bumped first so any prior verification is automatically stale. | Always fires. | | `inbox-fanout` | Reserved for V3.6 (`@shade/inbox`). Per-recipient hook is wired today so apps can register it now. | Always fires. | --- ## Registering handlers ```ts const shade = await createShade({ prekeyServer: 'https://prekeys.example.com', storage: 'sqlite:/data/shade.db', }); shade.beforeFirstLargeFile(10 * 1024 * 1024, async (ctx) => { // ctx.peerAddress, ctx.fingerprint, ctx.fileSize return await ui.confirmFingerprintModal(ctx); }); shade.beforeBackupImport(async (ctx) => { // ctx.fingerprint = fingerprint of the identity in the backup blob return await ui.confirmBackupOwner(ctx); }); shade.beforeNewDeviceTrust(async (ctx) => { // ctx.fingerprint = fingerprint of the rotated identity return await ui.confirmDeviceRotation(ctx); }); ``` Return `true` to allow the operation and persist a `'user'` verification. Return `false` (or throw) to abort with `FingerprintNotVerifiedError`. If you don't register a handler, the gate **logs a one-time warning per peer and proceeds on TOFU**, persisting a `'tofu-after-warning'` verification. This satisfies the V3.3 acceptance criterion that apps without registered gates get sane defaults instead of hard-failing — but it does mean the gate is informational, not a hard wall, in that configuration. Always register handlers in production. --- ## Manual verification The handler model assumes your app drives the OOB compare/confirm flow. If the user verifies through some other path (QR code scan, audio read-aloud, transitive trust from V3.10), call: ```ts await shade.markPeerVerified('bob'); // pin current fingerprint await shade.unmarkPeerVerified('bob'); // revoke const ok = await shade.isPeerVerified('bob'); // check status ``` `markPeerVerified` reads the peer's *current* fingerprint and pins it together with the per-peer `identity_version`. When the peer rotates (`acceptIdentityChange`), the version bumps and the saved verification goes stale automatically — `isPeerVerified` will return `false` until the user re-verifies. --- ## Tuning thresholds The `first-large-file` threshold is the only knob that's customer-tunable without code changes. The defaults are conservative: - **Default:** `10 MiB`. Big enough that ordinary chat attachments don't trigger; small enough that obvious "exfil candidates" do. - **Lower** (e.g. `1 MiB`) for high-sensitivity deployments — every document goes through the gate. - **Raise** (e.g. `100 MiB`) only for use cases where small uploads are routine and large transfers are deliberate / pre-arranged. `backup-import` and `new-device-trust` have no threshold by design — the spec mandates an irremovable minimum gate for both, since each one either trusts a fresh identity or overwrites pinned trust wholesale. --- ## React widget Use `` from `@shade/widgets` to block UI on verification status: ```tsx import { FingerprintGate } from '@shade/widgets'; ``` The default fallback shows the safety number, a "Copy OOB text" button, and an "I have verified" button that calls `Shade.markPeerVerified`. Pass a `fallback` render prop to use your own UI, or `onVerified` to react to the unverified → verified transition. `` is the existing observer-dashboard widget; it now exposes the same Copy-OOB / verify actions when an `onVerified` prop is wired. --- ## Errors `FingerprintNotVerifiedError` carries: - `peerAddress` — the address the gate was protecting. - `gate` — `'first-large-file' | 'backup-import' | 'new-device-trust' | 'inbox-fanout'`. - `code = 'SHADE_FINGERPRINT_NOT_VERIFIED'` — maps to HTTP 403. Catch it explicitly when wrapping `upload`, `importBackup`, and `acceptIdentityChange`: ```ts try { await shade.upload({ to: 'bob', input: bytes }); } catch (err) { if (err instanceof FingerprintNotVerifiedError) { showVerifyFirst(err.peerAddress); return; } throw err; } ``` --- ## Migration from 0.3.x No breaking changes: existing apps gain warning-mode gates automatically (see the no-handler note above). To upgrade to hard gates, register handlers for the operations you use. Your existing `FingerprintCompare` calls keep working; pass `onVerified` to enable the new actions.