sync-alerts.ts

2.5 KB · raw

1// Pending sync security alerts.2//3// Routine entry changes are intentionally silent. Only rare device-trust4// events enter this queue so a reliable sync does not turn the vault into an5// activity feed.6//7// Persistence: chrome.storage.local under a dedicated key so alerts survive8// MV3 worker eviction between the sync round and the next popup open. The9// queue is cleared after the popup acknowledges display (GET_SYNC_ALERTS10// with `consume: true`).1112export interface SyncAlert {13  type: "DEVICE_KEY_ROTATED" | "DEVICE_REVOKED";14  /** VaultEntry.id — a UUID string in this codebase (spec deviation: the15   *  fix spec typed this `number`, but entry ids are `crypto.randomUUID()`).16   *  Empty string for DEVICE_KEY_ROTATED (not entry-scoped). */17  entryId: string;18  entryTitle: string;19  /** Peer device name when known; "paired device" when the merge layer20   *  had no device context. */21  deviceName: string;22  timestamp: number;23}2425const SYNC_ALERTS_KEY = "peach_pending_sync_alerts_v1";26/** Bound the queue so a hostile peer cannot grow storage without limit. */27const MAX_PENDING_ALERTS = 200;2829export async function recordSyncAlerts(alerts: SyncAlert[]): Promise<void> {30  const securityAlerts = alerts.filter(isSyncAlert);31  if (securityAlerts.length === 0) return;32  const existing = await readAlerts();33  const merged = [...existing, ...securityAlerts].slice(-MAX_PENDING_ALERTS);34  try {35    await chrome.storage.local.set({ [SYNC_ALERTS_KEY]: merged });36  } catch {37    // Alert persistence must never block or fail a sync merge.38  }39}4041export async function getPendingSyncAlerts(): Promise<SyncAlert[]> {42  return readAlerts();43}4445export async function clearPendingSyncAlerts(): Promise<void> {46  try {47    await chrome.storage.local.remove(SYNC_ALERTS_KEY);48  } catch {49    /* non-fatal */50  }51}5253async function readAlerts(): Promise<SyncAlert[]> {54  try {55    const result = await chrome.storage.local.get(SYNC_ALERTS_KEY);56    const value = (result as Record<string, unknown>)[SYNC_ALERTS_KEY];57    if (!Array.isArray(value)) return [];58    return value.filter(isSyncAlert);59  } catch {60    return [];61  }62}6364function isSyncAlert(x: unknown): x is SyncAlert {65  if (typeof x !== "object" || x === null) return false;66  const r = x as Record<string, unknown>;67  return (68    (r.type === "DEVICE_KEY_ROTATED" ||69      r.type === "DEVICE_REVOKED") &&70    typeof r.entryId === "string" &&71    typeof r.entryTitle === "string" &&72    typeof r.deviceName === "string" &&73    typeof r.timestamp === "number"74  );75}