Skip to content

Relay Trust UI

dazeb edited this page Sep 17, 2026 · 2 revisions

Relay Trust UI

Relay Trust UI is the renderer-side gate between a relay peer key becoming visible and the app treating that peer as trusted. The reusable core in this slice is the pure helper relay-trust.ts, which converts the persisted trust value plus the current peer fingerprint into a TrustState. That state drives the Settings → Relay confirmation card, while persistence remains in settings.relay.trustedFingerprint.

Responsibilities

  • Encode the pairing-trust decision: show nothing, ask for confirmation, trust silently, or warn about a changed key.
  • Keep the trust rule independent from the settings sheet, Electron APIs, and WebSocket/relay transport.
  • Persist a user’s trust choice across sessions in settings.relay.trustedFingerprint.
  • Ensure a changed peer key is never auto-trusted; it requires an explicit re-trust.

Trust State Model

relay-trust.ts exports:

export type TrustState = 'none' | 'confirm' | 'trusted' | 'mismatch'

export function trustState(
  trusted: string | undefined,
  peerFingerprint: string | null
): TrustState
State Condition UI behavior
none peerFingerprint is absent/null Show no card; not yet paired with a peer.
confirm A peer fingerprint exists, but no persisted trust exists Ask the user to confirm this fingerprint.
trusted Persisted trust exactly matches the current peer fingerprint Connect silently; no card.
mismatch Current peer fingerprint differs from persisted trust Hard warning; require explicit re-trust, never auto-trust.

Decision Flow

flowchart TD
  A["trustState(trusted, peerFingerprint)"] --> B{"peerFingerprint present?"}
  B -- "no / null" --> C["'none'<br/>no card"]
  B -- "yes" --> D{"trusted present?"}
  D -- "no / undefined" --> E["'confirm'<br/>first-pairing confirmation"]
  D -- "yes" --> F{"trusted === peerFingerprint?"}
  F -- "yes" --> G["'trusted'<br/>silent connect, no card"]
  F -- "no" --> H["'mismatch'<br/>hard warning, explicit re-trust"]
Loading

The first branch checks whether a peer key exists at all. If there is no peer fingerprint, the helper returns none even when this machine previously trusted a fingerprint. The second branch treats missing persisted trust as a first pairing and returns confirm. Only an exact string match produces trusted; any non-matching persisted value produces mismatch.

Call Chain

  1. The relay client exposes pairing data through RelayPairing: peerPub, peerLogin, and selfId. connect() returns this pairing.
  2. The peer’s display fingerprint comes from relayFingerprint(peerPubB64). It validates the peer public key, takes the first 16 bytes of SHA-256, and renders them as 8 space-separated lowercase hex pairs for human eyeballing.
  3. The renderer-side Settings → Relay surface reads settings.relay.trustedFingerprint.
  4. It calls trustState(trustedFingerprint, peerFingerprint) to derive the UI state.
  5. The view maps the result:
    • none: render no trust card.
    • confirm: render the first-pairing confirmation card.
    • trusted: render nothing and connect silently.
    • mismatch: render a hard warning and require explicit re-trust.
  6. When the user confirms or re-trusts, the hosting UI/persistence layer is responsible for writing the fingerprint into settings.relay.trustedFingerprint. The helper itself does not persist anything.

Key State and Persistence

  • Input trusted: string | undefined is the persisted trust value. undefined means “never trusted on this machine.”
  • Input peerFingerprint: string | null is the fingerprint reported by the current connect. null means “no peer key yet.”
  • Output TrustState is derived, not stored by this module.
  • Persisted state lives under settings.relay.trustedFingerprint.
  • UI state such as card visibility and warning severity should be derived from TrustState rather than duplicated as separate booleans.

Main Files

  • src/renderer/src/components/relay-trust.ts — pure trust-state type and decision function.
  • src/renderer/src/components/relay-trust.test.ts — unit coverage for the four outcomes without browser or window.termsprawl mocks.
  • src/renderer/src/components/AppSettingsPanel.tsx — the app-wide settings sheet; its Connections navigation group contains the Relay surface.
  • src/core/relay-client.ts — relay pairing data, fingerprint formatting, and the relay transport seam that feeds the peer key into the UI.
  • src/renderer/src/components/AGENTS.md — component conventions, including the in-app confirmation overlay rule.

Boundary Conditions

  • trustState is pure and Electron-free. It does not read settings, open sockets, format fingerprints, or render JSX.
  • If peerFingerprint is falsy, including null or an empty string, the result is none.
  • If trusted is falsy, including undefined or an empty string, the result is confirm when a peer fingerprint exists.
  • Trust comparison is exact string equality. Any fingerprint formatting change must be coordinated with persisted values or treated as a migration/reset case.
  • A mismatch must never silently update the persisted fingerprint. The user must explicitly re-trust the new key.
  • Fingerprint validation belongs to relayFingerprint, not trustState. That formatter rejects invalid base64-looking input and decoded keys that are not 32 bytes.
  • New confirmation UI must follow the .confirm-overlay in-app pattern; window.confirm must not be used because Electron silently no-ops it.

Extension Points

  • Add or restyle the Settings → Relay card by switching on TrustState without changing the decision helper.
  • Wire “trust” and “re-trust” actions to the settings persistence layer so they update settings.relay.trustedFingerprint.
  • Add a “forget trust” or reset action by clearing the persisted fingerprint, which naturally returns the next pairing to confirm.
  • Support key rotation by treating mismatch as an explicit user decision that overwrites the old fingerprint only after confirmation.
  • If new trust states are introduced, update the TrustState union, the decision function, the renderer branch table, and the unit tests together.
  • If fingerprint formatting changes, consider a migration or compatibility layer because trustState compares raw strings.

Test Coverage

relay-trust.test.ts verifies the core safety rules:

  • No peer fingerprint produces none, even with a previously trusted key.
  • First pairing with no persisted trust produces confirm.
  • A matching persisted fingerprint produces trusted.
  • A changed peer fingerprint produces mismatch.

Sources: src/renderer/src/components/relay-trust.ts; src/renderer/src/components/relay-trust.test.ts; src/core/relay-client.ts; src/core/relay-client.ts; src/renderer/src/components/AGENTS.md; src/renderer/src/components/AGENTS.md; src/renderer/src/components/AGENTS.md.

termsprawl

App Shell & Platform Foundations

Canvas, Nodes & Renderer State

Terminals & Session Continuity

Persistence, Projects & Files

Agent Runtime & Tooling

Chat Nodes & Model Providers

Git & Source Control

Embedded Browser Nodes

Server Edition

Relay & Remote Access

Integrations & Secondary Surfaces

Settings, Updates & Maintenance

Clone this wiki locally