From c67d484152bad9b93ac77ff5ac23b7b22908a15e Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 12:29:31 -0400 Subject: [PATCH] feat(extension): update devices UI for new auth model - Show revoked devices in collapsible section with strikethrough styling - Fetch revoked.json via new list_revoked message + router case - Registration flow uses register_device WASM API (private keys internal) - Display revoked_by and timestamp for each revoked entry - Update setup wizard to use new register_device API Co-Authored-By: Claude Sonnet 4.6 --- extension/src/popup/components/devices.ts | 77 ++++++++++++++----- .../src/service-worker/router/popup-only.ts | 6 ++ extension/src/setup/setup.ts | 8 +- extension/src/shared/messages.ts | 7 +- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/extension/src/popup/components/devices.ts b/extension/src/popup/components/devices.ts index ee7a7fb..c10956f 100644 --- a/extension/src/popup/components/devices.ts +++ b/extension/src/popup/components/devices.ts @@ -3,6 +3,13 @@ import { setState, sendMessage, navigate, escapeHtml } from '../../shared/state'; import type { Device } from '../../shared/types'; +interface RevokedEntry { + name: string; + public_key: string; + revoked_at: number; + revoked_by: string; +} + function relativeTime(unixSec: number): string { const now = Math.floor(Date.now() / 1000); const diff = now - unixSec; @@ -36,16 +43,62 @@ export async function renderDevices(app: HTMLElement): Promise { const stored = await chrome.storage.local.get(['device_name']); const currentDeviceName: string | undefined = stored.device_name as string | undefined; - // Fetch device list - const resp = await sendMessage({ type: 'list_devices' }); - if (!resp.ok) { + // Fetch active device list and revoked list in parallel + const [devicesResp, revokedResp] = await Promise.all([ + sendMessage({ type: 'list_devices' }), + sendMessage({ type: 'list_revoked' }), + ]); + + if (!devicesResp.ok) { app.innerHTML = `

Failed to load devices

`; return; } - const devices = (resp.data as { devices: Device[] }).devices; + const devices = (devicesResp.data as { devices: Device[] }).devices; + const revokedDevices: RevokedEntry[] = revokedResp.ok + ? (revokedResp.data as { revoked: RevokedEntry[] }).revoked + : []; + const isRegistered = currentDeviceName && devices.some((d) => d.name === currentDeviceName); + const activeDevicesHtml = devices.length === 0 + ? `

No devices registered

` + : devices.map((d) => { + const isCurrentDevice = d.name === currentDeviceName; + return ` +
+
+ ${escapeHtml(d.name)}${isCurrentDevice ? ' ← you' : ''} + added ${relativeTime(d.added_at)} +
+ ${isCurrentDevice ? '' : ``} +
+ `; + }).join(''); + + const revokedSectionHtml = revokedDevices.length === 0 ? '' : ` +
+ + ${revokedDevices.length} revoked device${revokedDevices.length !== 1 ? 's' : ''} + +
+ ${revokedDevices.map((r) => ` +
+
+ + ${escapeHtml(r.name)} + + + revoked ${relativeTime(r.revoked_at)} + ${r.revoked_by !== 'unknown' ? ` by ${escapeHtml(r.revoked_by)}` : ''} + +
+
+ `).join('')} +
+
+ `; + app.innerHTML = `
@@ -58,20 +111,8 @@ export async function renderDevices(app: HTMLElement): Promise {
` : ''} - ${devices.length === 0 - ? `

No devices registered

` - : devices.map((d) => { - const isCurrentDevice = d.name === currentDeviceName; - return ` -
-
- ${escapeHtml(d.name)}${isCurrentDevice ? ' ← you' : ''} - added ${relativeTime(d.added_at)} -
- ${isCurrentDevice ? '' : ``} -
- `; - }).join('')} + ${activeDevicesHtml} + ${revokedSectionHtml}
`; diff --git a/extension/src/service-worker/router/popup-only.ts b/extension/src/service-worker/router/popup-only.ts index 5fa9367..5fd4ae1 100644 --- a/extension/src/service-worker/router/popup-only.ts +++ b/extension/src/service-worker/router/popup-only.ts @@ -346,6 +346,12 @@ export async function handle( return { ok: true, data: { devices: list } }; } + case 'list_revoked': { + if (!state.gitHost) return { ok: false, error: 'vault_locked' }; + const revoked = await devices.readRevoked(state.gitHost); + return { ok: true, data: { revoked } }; + } + case 'add_device': { if (!state.gitHost) return { ok: false, error: 'vault_locked' }; const device = { diff --git a/extension/src/setup/setup.ts b/extension/src/setup/setup.ts index 9d1fcd6..99eab89 100644 --- a/extension/src/setup/setup.ts +++ b/extension/src/setup/setup.ts @@ -1049,12 +1049,12 @@ function attachStep5(): void { try { const w = await loadWasm(); - const keypair = w.generate_device_keypair(); + // register_device keeps private keys internal — only public keys returned + const keypair = w.register_device(state.deviceName); - // 1) Save private key + name locally. + // 1) Save device name locally (private keys stay in WASM memory). await chrome.storage.local.set({ device_name: state.deviceName, - device_private_key: keypair.private_key_base64, }); // 2) Save vault config + reference image to extension storage. @@ -1086,7 +1086,7 @@ function attachStep5(): void { const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken); await addDevice(host, { name: state.deviceName, - public_key: keypair.public_key_hex, + public_key: keypair.signing_public_key, added_at: Math.floor(Date.now() / 1000), }); diff --git a/extension/src/shared/messages.ts b/extension/src/shared/messages.ts index 7408124..ccb24d6 100644 --- a/extension/src/shared/messages.ts +++ b/extension/src/shared/messages.ts @@ -41,6 +41,7 @@ export type PopupMessage = | { type: 'upload_attachment'; itemId: string; filename: string; mimeType: string; bytes: ArrayBuffer } | { type: 'download_attachment'; itemId: string; attachmentId: string } | { type: 'list_devices' } + | { type: 'list_revoked' } | { type: 'add_device'; name: string; public_key: string } | { type: 'register_this_device'; name: string } | { type: 'revoke_device'; name: string } @@ -139,6 +140,10 @@ export interface ListDevicesResponse extends Extract { data: { devices: Device[] }; } +export interface ListRevokedResponse extends Extract { + data: { revoked: Array<{ name: string; public_key: string; revoked_at: number; revoked_by: string }> }; +} + export interface ListTrashedResponse extends Extract { data: { items: Array<[ItemId, ManifestEntry]> }; } @@ -161,7 +166,7 @@ export const POPUP_ONLY_TYPES: ReadonlySet = new Set([ 'ack_autofill_origin', 'get_settings', 'update_settings', 'get_vault_settings', 'update_vault_settings', 'get_blacklist', 'remove_blacklist', 'get_active_tab_url', 'list_groups', 'upload_attachment', 'download_attachment', - 'list_devices', 'add_device', 'register_this_device', 'revoke_device', + 'list_devices', 'list_revoked', 'add_device', 'register_this_device', 'revoke_device', 'list_trashed', 'restore_item', 'purge_item', 'purge_all_trash', 'get_field_history', 'get_session_config', 'update_session_config',