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 { setState, sendMessage, navigate, escapeHtml } from '../../shared/state';
|
||||||
import type { Device } from '../../shared/types';
|
import type { Device } from '../../shared/types';
|
||||||
|
|
||||||
|
interface RevokedEntry {
|
||||||
|
name: string;
|
||||||
|
public_key: string;
|
||||||
|
revoked_at: number;
|
||||||
|
revoked_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
function relativeTime(unixSec: number): string {
|
function relativeTime(unixSec: number): string {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const diff = now - unixSec;
|
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 stored = await chrome.storage.local.get(['device_name']);
|
||||||
const currentDeviceName: string | undefined = stored.device_name as string | undefined;
|
const currentDeviceName: string | undefined = stored.device_name as string | undefined;
|
||||||
|
|
||||||
// Fetch device list
|
// Fetch active device list and revoked list in parallel
|
||||||
const resp = await sendMessage({ type: 'list_devices' });
|
const [devicesResp, revokedResp] = await Promise.all([
|
||||||
if (!resp.ok) {
|
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>`;
|
app.innerHTML = `<div class="pad"><p class="error">Failed to load devices</p></div>`;
|
||||||
return;
|
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 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 = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="devices-header">
|
<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>
|
<button class="btn btn-primary" id="register-btn">Register this device</button>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
${devices.length === 0
|
${activeDevicesHtml}
|
||||||
? `<p class="muted" style="text-align:center;margin-top:32px;">No devices registered</p>`
|
${revokedSectionHtml}
|
||||||
: 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('')}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -346,6 +346,12 @@ export async function handle(
|
|||||||
return { ok: true, data: { devices: list } };
|
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': {
|
case 'add_device': {
|
||||||
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
if (!state.gitHost) return { ok: false, error: 'vault_locked' };
|
||||||
const device = {
|
const device = {
|
||||||
|
|||||||
@@ -1049,12 +1049,12 @@ function attachStep5(): void {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const w = await loadWasm();
|
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({
|
await chrome.storage.local.set({
|
||||||
device_name: state.deviceName,
|
device_name: state.deviceName,
|
||||||
device_private_key: keypair.private_key_base64,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2) Save vault config + reference image to extension storage.
|
// 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);
|
const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);
|
||||||
await addDevice(host, {
|
await addDevice(host, {
|
||||||
name: state.deviceName,
|
name: state.deviceName,
|
||||||
public_key: keypair.public_key_hex,
|
public_key: keypair.signing_public_key,
|
||||||
added_at: Math.floor(Date.now() / 1000),
|
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: 'upload_attachment'; itemId: string; filename: string; mimeType: string; bytes: ArrayBuffer }
|
||||||
| { type: 'download_attachment'; itemId: string; attachmentId: string }
|
| { type: 'download_attachment'; itemId: string; attachmentId: string }
|
||||||
| { type: 'list_devices' }
|
| { type: 'list_devices' }
|
||||||
|
| { type: 'list_revoked' }
|
||||||
| { type: 'add_device'; name: string; public_key: string }
|
| { type: 'add_device'; name: string; public_key: string }
|
||||||
| { type: 'register_this_device'; name: string }
|
| { type: 'register_this_device'; name: string }
|
||||||
| { type: 'revoke_device'; name: string }
|
| { type: 'revoke_device'; name: string }
|
||||||
@@ -139,6 +140,10 @@ export interface ListDevicesResponse extends Extract<Response, { ok: true }> {
|
|||||||
data: { devices: Device[] };
|
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 }> {
|
export interface ListTrashedResponse extends Extract<Response, { ok: true }> {
|
||||||
data: { items: Array<[ItemId, ManifestEntry]> };
|
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',
|
'ack_autofill_origin', 'get_settings', 'update_settings',
|
||||||
'get_vault_settings', 'update_vault_settings', 'get_blacklist',
|
'get_vault_settings', 'update_vault_settings', 'get_blacklist',
|
||||||
'remove_blacklist', 'get_active_tab_url', 'list_groups', 'upload_attachment', 'download_attachment',
|
'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',
|
'list_trashed', 'restore_item', 'purge_item', 'purge_all_trash',
|
||||||
'get_field_history',
|
'get_field_history',
|
||||||
'get_session_config', 'update_session_config',
|
'get_session_config', 'update_session_config',
|
||||||
|
|||||||
Reference in New Issue
Block a user