Files
relicario/docs/superpowers/specs/2026-04-26-relicario-extension-1c-gamma2-design.md
adlee-was-taken 3372358b31 docs(spec): Plan 1C-γ₂ — device registration + trash + field history + attachment caps
Four features completing Plan 1C: device ed25519 keypair registration
during setup wizard, device management UI, trash view with restore/purge
(including orphan blob cleanup), per-item field history view, and
per-attachment size cap setting in vault settings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-26 15:32:28 -04:00

16 KiB

Plan 1C-γ₂: Device registration + Trash + Field history + Attachment caps — design

Date: 2026-04-26 Scope: Add device registration during setup, device management UI, trash view with restore/purge (including orphan blob cleanup), per-item field history view, and a single attachment-cap setting in vault settings.

Goal

The Rust core already supports soft-delete/restore (Item::soft_delete, Item::restore, Item::is_trashed), field history capture (auto-tracked for Password/Concealed/Totp fields in Item::field_history), and attachment caps (VaultSettings::attachment_caps). The CLI has device management via ed25519 keypairs (device add/list/revoke). What's missing is the extension surface: a way to register the extension as a device, view/revoke devices, browse and act on trashed items, view password history, and configure the attachment size limit.

γ₂ completes Plan 1C by exposing these already-implemented core capabilities in the extension UI.

Non-goals

  • Commit signing with device key — keypair is generated and stored for future use, but no operations are signed yet.
  • Bulk trash operations (select-all, empty-selected) — single-item restore + "empty all" only.
  • Field history editing/deletion — view-only.
  • Manual orphan blob purge button — orphans are cleaned automatically when emptying trash.
  • Exposing all four attachment caps — only per_attachment_max_bytes is user-configurable; others use sensible defaults.

Visual identity

Device name step in setup wizard

After passphrase + reference image, a new step appears:

Name this device

This helps you identify which devices have access to your vault.

┌─────────────────────────────────┐
│ Chrome on Linux                 │
└─────────────────────────────────┘

              [ continue ]
  • Auto-suggested default: "{browser} on {platform}" (e.g., "Chrome on Linux", "Firefox on macOS")
  • User can edit the name or accept the default
  • "Continue" generates keypair, stores private key locally, commits pubkey to devices.json

Device management screen

Entry point: "Devices" link in popup navigation (gear icon row alongside Settings).

← back                    devices

┌─────────────────────────────────┐
│ Chrome on Linux          ← you │
│ added 3d ago                    │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Firefox on MacBook       revoke │
│ added 2w ago                    │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ CLI                      revoke │
│ added 1mo ago                   │
└─────────────────────────────────┘
  • "← you" badge on current device (matched via device_name in chrome.storage.local)
  • Current device row has no revoke button (can't revoke self)
  • Revoke shows confirm: "Revoke {name}? This device will no longer be authorized."
  • Commits "device: revoke {name}" on confirm

Unregistered device banner: If device_private_key is missing from local storage but vault exists, show:

⚠ This device is not registered
[ Register this device ]

Trash screen

Entry point: "Trash" link in popup navigation.

← back                      trash

3 items · oldest auto-purges in 45d

┌─────────────────────────────────┐
│ 🔑 Old Bank Login               │
│ trashed 2d ago          restore │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 📝 Temp Note                    │
│ trashed 5d ago          restore │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 💳 Expired Card                 │
│ trashed 12d ago         restore │
└─────────────────────────────────┘

        [ empty trash ]
  • List from manifest where trashed_at != null, sorted newest-trashed first
  • Type icon + title per row (same as main item list)
  • "restore" clears trashed_at, updates manifest, commits
  • Header shows count + days until oldest item auto-purges (based on trash_retention)
  • "empty trash" confirms: "Permanently delete 3 items? This cannot be undone."
  • Empty trash also scans for orphan blobs (attachments not referenced by any item) and deletes them
  • Single commit for the whole operation: "trash: purge N items + M orphan blobs"
  • Empty state: "Trash is empty"

Field history screen

Entry point: "View history" link on item detail (only shown if field_history is non-empty).

← back to item         password history

GitHub Login

┌─────────────────────────────────┐
│ ••••••••••••           current  │
│ set 2d ago               [ 📋 ] │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ ••••••••••••                    │
│ changed 3w ago           [ 📋 ] │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ ••••••••••••                    │
│ changed 2mo ago          [ 📋 ] │
└─────────────────────────────────┘
  • Shows history for all tracked fields (Password, Concealed, Totp)
  • Current value comes from the item's field itself (not field_history), marked "current", timestamp = item's modified
  • Historical values come from field_history entries
  • Values masked by default; click row to reveal
  • Copy button per entry
  • Sorted newest-first (current always first)
  • If multiple tracked fields exist, group by field name with section headers

Attachment caps in vault settings

New section after "autofill origins" in vault settings:

attachments

max file size     [ 10 MB ▾ ]
  • Dropdown with presets: 5 MB, 10 MB (default), 25 MB, 50 MB
  • Updates vault_settings.attachment_caps.per_attachment_max_bytes
  • Other caps remain at defaults: per_item_max_count: 20, per_vault_soft_cap_bytes: 100MB, per_vault_hard_cap_bytes: 500MB

Architecture

Layer 1: WASM bindings

New exports in relicario-wasm:

/// Generate an ed25519 keypair for device registration.
/// Returns JSON: { "public_key_hex": "...", "private_key_base64": "..." }
#[wasm_bindgen]
pub fn generate_device_keypair() -> String;

/// Extract field history from a decrypted item.
/// Returns JSON array of { field_id, field_name, current_value, entries: [{ value, changed_at }] }
/// where current_value is the field's present value (for the "current" row in UI)
/// and entries are historical values from field_history.
#[wasm_bindgen]
pub fn get_field_history(item_json: &str) -> String;

The ed25519-dalek crate is already a dependency (used by CLI). WASM feature gate may be needed.

Layer 2: Shared types

extension/src/shared/types.ts:

export interface Device {
  name: string;
  public_key: string;  // hex-encoded ed25519 pubkey
  added_at: number;    // unix timestamp
}

export interface FieldHistoryEntry {
  value: string;
  changed_at: number;
}

export interface FieldHistory {
  field_id: string;
  field_name: string;
  current_value: string;   // present value of the field
  entries: FieldHistoryEntry[];  // historical values
}

extension/src/shared/messages.ts — new message types:

Message Direction Purpose
list_devices popup → SW Get Device[] from devices.json
add_device popup → SW Register new device (name, pubkey)
revoke_device popup → SW Remove device by name
list_trashed popup → SW Get manifest entries where trashed_at != null
restore_item popup → SW Clear trashed_at on item, update manifest
purge_item popup → SW Permanently delete single trashed item
purge_all_trash popup → SW Delete all trashed items + orphan blobs
get_field_history popup → SW Get history for an item

Layer 3: Service worker

extension/src/service-worker/devices.ts (NEW):

export async function readDevices(gitHost: GitHost): Promise<Device[]>;
export async function writeDevices(gitHost: GitHost, devices: Device[], message: string): Promise<void>;
export async function addDevice(gitHost: GitHost, device: Device): Promise<void>;
export async function revokeDevice(gitHost: GitHost, name: string): Promise<void>;

Reads/writes .relicario/devices.json in the vault repo.

extension/src/service-worker/vault.ts — new functions:

export async function listTrashed(manifest: Manifest): ManifestEntry[];
export async function restoreItem(gitHost: GitHost, session: SessionHandle, itemId: string): Promise<void>;
export async function purgeItem(gitHost: GitHost, itemId: string): Promise<void>;
export async function purgeAllTrash(gitHost: GitHost, session: SessionHandle, manifest: Manifest): Promise<{ itemCount: number, orphanCount: number }>;

Orphan blob scan algorithm:

  1. Collect all AttachmentRef.id values from all non-trashed items → Set<string> referenced
  2. List files in attachments/ directory → Set<string> existing
  3. Orphans = existing - referenced
  4. Delete each orphan blob file
  5. Return count for commit message

Router handlers in popup-only.ts:

  • All 8 message types get handlers
  • Standard sender check (popup-only)
  • Return { ok: true, data: ... } or { ok: false, error: '...', detail: '...' }

Layer 4: Popup

New screens:

  • extension/src/popup/components/trash.ts — trash list view
  • extension/src/popup/components/devices.ts — device management view
  • extension/src/popup/components/field-history.ts — per-item history view

Modified screens:

  • extension/src/popup/components/setup-wizard.ts — add device name step after reference image
  • extension/src/popup/components/settings-vault.ts — add attachment caps section
  • extension/src/popup/components/item-detail.ts — add "View history" link if history exists
  • extension/src/popup/popup.ts — add navigation targets for trash, devices, field-history

Navigation state:

type Screen = 
  | 'unlock' | 'setup' | 'list' | 'detail' | 'form' | 'settings' | 'vault-settings'
  | 'trash' | 'devices' | 'field-history';  // ← new

interface State {
  // existing fields...
  historyItemId?: string;  // for field-history screen
}

Device name step flow:

  1. After reference image step, show device name input
  2. On continue:
    • Call WASM generate_device_keypair(){ public_key_hex, private_key_base64 }
    • Store in chrome.storage.local: device_name, device_private_key
    • Send add_device message to SW with name + pubkey
    • SW writes to devices.json, commits
  3. Proceed to vault creation/unlock

Unregistered device detection:

On unlock success, popup checks:

  • If device_private_key missing from local storage AND vault has devices.json with entries → show "not registered" banner
  • Banner click triggers device registration flow (same as setup, but without passphrase/image steps)

Testing strategy

Unit tests (vitest + happy-dom)

Service worker tests:

  • devices.test.ts: add/list/revoke, duplicate name rejection, JSON format
  • trash.test.ts: listTrashed filter, restoreItem clears timestamp, purgeItem deletes files, orphan scan logic

Popup tests:

  • trash.test.ts: renders trashed items, restore button, empty trash confirm
  • devices.test.ts: renders device list, "you" indicator, revoke confirm
  • field-history.test.ts: renders entries, mask/reveal toggle, copy button
  • setup-wizard.test.ts: device name step appears, defaults correctly

Router tests:

  • Extend router.test.ts with cases for all 8 new message types
  • Sender check verification (reject non-popup callers)

Manual browser test matrix

# Test Chrome Firefox
1 Setup wizard shows device name step
2 Device name defaults to "Chrome on Linux" (or similar)
3 Device list shows "← you" on current device
4 Revoke other device works, confirms
5 Trash item from detail view
6 Trash view shows trashed items
7 Restore from trash returns item to list
8 Empty trash purges items + orphan blobs
9 Field history shows after password edit
10 History values masked, click to reveal
11 Attachment cap dropdown in vault settings
12 Cap change persists across unlock cycles

File changes

Rust (WASM)

File Change
crates/relicario-wasm/src/lib.rs Add generate_device_keypair, get_field_history
crates/relicario-wasm/Cargo.toml Ensure ed25519-dalek features for WASM

Extension — shared

File Change
extension/src/shared/types.ts Add Device, FieldHistoryEntry, FieldHistory
extension/src/shared/messages.ts Add 8 message types

Extension — service worker

File Change
extension/src/service-worker/devices.ts NEW — device CRUD
extension/src/service-worker/vault.ts Add trash/restore/purge functions
extension/src/service-worker/router/popup-only.ts Add 8 handlers
extension/src/service-worker/__tests__/devices.test.ts NEW
extension/src/service-worker/__tests__/trash.test.ts NEW
extension/src/service-worker/router/__tests__/router.test.ts Extend with new handlers

Extension — popup

File Change
extension/src/popup/components/trash.ts NEW
extension/src/popup/components/devices.ts NEW
extension/src/popup/components/field-history.ts NEW
extension/src/popup/components/setup-wizard.ts Add device name step
extension/src/popup/components/settings-vault.ts Add attachment caps section
extension/src/popup/components/item-detail.ts Add "View history" link
extension/src/popup/popup.ts Add navigation targets
extension/src/popup/styles.css New styles for trash, devices, history
extension/src/popup/components/__tests__/trash.test.ts NEW
extension/src/popup/components/__tests__/devices.test.ts NEW
extension/src/popup/components/__tests__/field-history.test.ts NEW

Sequencing

Bottom-up by layer, with setup wizard changes near the end:

  1. WASM bindingsgenerate_device_keypair, get_field_history
  2. Shared typesDevice, FieldHistory*, message types
  3. SW devicesdevices.ts + handlers + tests
  4. SW trash — trash functions in vault.ts + handlers + tests
  5. SW field history — handler (uses WASM binding) + tests
  6. Popup trash screentrash.ts + styles + tests
  7. Popup devices screendevices.ts + styles + tests
  8. Popup field history screenfield-history.ts + tests
  9. Popup item-detail — "View history" link
  10. Popup vault-settings — attachment caps section
  11. Popup navigation — wire trash + devices entry points
  12. Setup wizard — device name step (atomic, riskiest change last)
  13. Manual browser testing — Chrome + Firefox matrix

Commit strategy

Direct to main per project convention. Each task = one commit. Do NOT push.

Tag plan-1c-gamma2-complete after all tasks pass + manual tests verified.