From 3372358b31401ac73919215ac065befa41987fed Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 26 Apr 2026 15:32:28 -0400 Subject: [PATCH] =?UTF-8?q?docs(spec):=20Plan=201C-=CE=B3=E2=82=82=20?= =?UTF-8?q?=E2=80=94=20device=20registration=20+=20trash=20+=20field=20his?= =?UTF-8?q?tory=20+=20attachment=20caps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...26-relicario-extension-1c-gamma2-design.md | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-26-relicario-extension-1c-gamma2-design.md diff --git a/docs/superpowers/specs/2026-04-26-relicario-extension-1c-gamma2-design.md b/docs/superpowers/specs/2026-04-26-relicario-extension-1c-gamma2-design.md new file mode 100644 index 0000000..399327a --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-relicario-extension-1c-gamma2-design.md @@ -0,0 +1,395 @@ +# 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`: + +```rust +/// 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`: + +```typescript +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): + +```typescript +export async function readDevices(gitHost: GitHost): Promise; +export async function writeDevices(gitHost: GitHost, devices: Device[], message: string): Promise; +export async function addDevice(gitHost: GitHost, device: Device): Promise; +export async function revokeDevice(gitHost: GitHost, name: string): Promise; +``` + +Reads/writes `.relicario/devices.json` in the vault repo. + +**`extension/src/service-worker/vault.ts`** — new functions: + +```typescript +export async function listTrashed(manifest: Manifest): ManifestEntry[]; +export async function restoreItem(gitHost: GitHost, session: SessionHandle, itemId: string): Promise; +export async function purgeItem(gitHost: GitHost, itemId: string): Promise; +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 referenced` +2. List files in `attachments/` directory → `Set 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:** + +```typescript +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 bindings** — `generate_device_keypair`, `get_field_history` +2. **Shared types** — `Device`, `FieldHistory*`, message types +3. **SW devices** — `devices.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 screen** — `trash.ts` + styles + tests +7. **Popup devices screen** — `devices.ts` + styles + tests +8. **Popup field history screen** — `field-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.