// Pending sync security alerts. // // Routine entry changes are intentionally silent. Only rare device-trust // events enter this queue so a reliable sync does not turn the vault into an // activity feed. // // 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`). import { capDeviceName } from "../shared/sync-limits"; export interface SyncAlert { type: "DEVICE_KEY_ROTATED" | "DEVICE_REVOKED"; /** 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 { // Final display seam: cap whatever name reached the queue (including the // `knownName ?? entry.deviceName` fallback) at the shared bound. const securityAlerts = alerts.filter(isSyncAlert).map((a) => ({ ...a, deviceName: capDeviceName(a.deviceName), })); if (securityAlerts.length === 0) return; const existing = await readAlerts(); const merged = [...existing, ...securityAlerts].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 === "DEVICE_KEY_ROTATED" || r.type === "DEVICE_REVOKED") && typeof r.entryId === "string" && typeof r.entryTitle === "string" && typeof r.deviceName === "string" && typeof r.timestamp === "number" ); }