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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> {
|
||||
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 = `<div class="pad"><p class="error">Failed to load devices</p></div>`;
|
||||
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
|
||||
? `<p class="muted" style="text-align:center;margin-top:32px;">No devices registered</p>`
|
||||
: devices.map((d) => {
|
||||
const isCurrentDevice = d.name === currentDeviceName;
|
||||
return `
|
||||
<div class="device-row">
|
||||
<div class="device-row__info">
|
||||
<span class="device-row__name">${escapeHtml(d.name)}${isCurrentDevice ? ' <span class="device-row__you">← you</span>' : ''}</span>
|
||||
<span class="device-row__meta">added ${relativeTime(d.added_at)}</span>
|
||||
</div>
|
||||
${isCurrentDevice ? '' : `<button class="device-row__revoke" data-revoke="${escapeHtml(d.name)}">revoke</button>`}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const revokedSectionHtml = revokedDevices.length === 0 ? '' : `
|
||||
<details class="revoked-section" style="margin-top:16px;">
|
||||
<summary class="muted" style="cursor:pointer;font-size:0.85em;">
|
||||
${revokedDevices.length} revoked device${revokedDevices.length !== 1 ? 's' : ''}
|
||||
</summary>
|
||||
<div style="margin-top:8px;">
|
||||
${revokedDevices.map((r) => `
|
||||
<div class="device-row device-row--revoked">
|
||||
<div class="device-row__info">
|
||||
<span class="device-row__name" style="text-decoration:line-through;opacity:0.5;">
|
||||
${escapeHtml(r.name)}
|
||||
</span>
|
||||
<span class="device-row__meta">
|
||||
revoked ${relativeTime(r.revoked_at)}
|
||||
${r.revoked_by !== 'unknown' ? ` by ${escapeHtml(r.revoked_by)}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="pad">
|
||||
<div class="devices-header">
|
||||
@@ -58,20 +111,8 @@ export async function renderDevices(app: HTMLElement): Promise<void> {
|
||||
<button class="btn btn-primary" id="register-btn">Register this device</button>
|
||||
</div>
|
||||
` : ''}
|
||||
${devices.length === 0
|
||||
? `<p class="muted" style="text-align:center;margin-top:32px;">No devices registered</p>`
|
||||
: devices.map((d) => {
|
||||
const isCurrentDevice = d.name === currentDeviceName;
|
||||
return `
|
||||
<div class="device-row">
|
||||
<div class="device-row__info">
|
||||
<span class="device-row__name">${escapeHtml(d.name)}${isCurrentDevice ? ' <span class="device-row__you">← you</span>' : ''}</span>
|
||||
<span class="device-row__meta">added ${relativeTime(d.added_at)}</span>
|
||||
</div>
|
||||
${isCurrentDevice ? '' : `<button class="device-row__revoke" data-revoke="${escapeHtml(d.name)}">revoke</button>`}
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
${activeDevicesHtml}
|
||||
${revokedSectionHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -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<Response, { ok: true }> {
|
||||
data: { devices: Device[] };
|
||||
}
|
||||
|
||||
export interface ListRevokedResponse extends Extract<Response, { ok: true }> {
|
||||
data: { revoked: Array<{ name: string; public_key: string; revoked_at: number; revoked_by: string }> };
|
||||
}
|
||||
|
||||
export interface ListTrashedResponse extends Extract<Response, { ok: true }> {
|
||||
data: { items: Array<[ItemId, ManifestEntry]> };
|
||||
}
|
||||
@@ -161,7 +166,7 @@ export const POPUP_ONLY_TYPES: ReadonlySet<PopupMessage['type']> = 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',
|
||||
|
||||
Reference in New Issue
Block a user