// Pending sync alerts — audit fix HV-1b. // // When a sync round accepts a remote entry that REPLACES an existing local // password, or soft-deletes a local entry via a remote tombstone, the user // gets no signal today. This queue records those remote mutations so the // popup can display them on next open (non-blocking — the sync itself is // not interrupted). // // Persistence: chrome.storage.local under a dedicated key so alerts survive // MV3 worker eviction between the sync round and the next popup open. The // queue is cleared after the popup acknowledges display (GET_SYNC_ALERTS // with `consume: true`). export interface SyncAlert { type: "REMOTE_PASSWORD_CHANGE" | "REMOTE_DELETE" | "DEVICE_KEY_ROTATED"; /** VaultEntry.id — a UUID string in this codebase (spec deviation: the * fix spec typed this `number`, but entry ids are `crypto.randomUUID()`). * Empty string for DEVICE_KEY_ROTATED (not entry-scoped). */ entryId: string; entryTitle: string; /** Peer device name when known; "paired device" when the merge layer * had no device context. */ deviceName: string; timestamp: number; } const SYNC_ALERTS_KEY = "peach_pending_sync_alerts_v1"; /** Bound the queue so a hostile peer cannot grow storage without limit. */ const MAX_PENDING_ALERTS = 200; export async function recordSyncAlerts(alerts: SyncAlert[]): Promise { if (alerts.length === 0) return; const existing = await readAlerts(); const merged = [...existing, ...alerts].slice(-MAX_PENDING_ALERTS); try { await chrome.storage.local.set({ [SYNC_ALERTS_KEY]: merged }); } catch { // Alert persistence must never block or fail a sync merge. } } export async function getPendingSyncAlerts(): Promise { return readAlerts(); } export async function clearPendingSyncAlerts(): Promise { try { await chrome.storage.local.remove(SYNC_ALERTS_KEY); } catch { /* non-fatal */ } } async function readAlerts(): Promise { try { const result = await chrome.storage.local.get(SYNC_ALERTS_KEY); const value = (result as Record)[SYNC_ALERTS_KEY]; if (!Array.isArray(value)) return []; return value.filter(isSyncAlert); } catch { return []; } } function isSyncAlert(x: unknown): x is SyncAlert { if (typeof x !== "object" || x === null) return false; const r = x as Record; return ( (r.type === "REMOTE_PASSWORD_CHANGE" || r.type === "REMOTE_DELETE" || r.type === "DEVICE_KEY_ROTATED") && typeof r.entryId === "string" && typeof r.entryTitle === "string" && typeof r.deviceName === "string" && typeof r.timestamp === "number" ); }