123456789101112import { copyToClipboard, getEffectiveClearSeconds } from "./clipboard";13import { getPendingSyncAlerts, clearPendingSyncAlerts } from "./sync-alerts";14import { extractDomain } from "../shared/domain";15import { computeVaultId } from "../shared/sync-identity";16import {17 getPendingVouchedDevice,18 listPendingVouchedDevices,19 removePendingVouchedDevice,20} from "./pending-vouched-devices";21import { isRecoveryConfirmPending } from "./recovery-pending";22import { setupAutoLock, touchAutoLock } from "./alarms";23import { runSelfTest } from "./self-test";24import * as vault from "./vault";25import {26 handleEstimateCodexPages,27 handleGetCodexVaultData,28 handleRestoreFromCodex,29} from "./codex-handlers";30import type { CodexRestorePayload } from "../shared/codex-types";31import {32 activateLicense,33 deactivateLicense,34 getLicenseInfo,35 getLicenseKey,36 isProUser,37 markCelebrationPlayed,38 shouldPlayCelebration,39} from "./license";40import {41 completeFirefoxPasskeyCreate,42 getPasskeyProviderState,43 getPendingPasskeyRequest,44 handleFirefoxPasskeyCreate,45 handleFirefoxPasskeyGet,46 handlePasskeyHasCredentials,47 initiateFirefoxPasskeyCreate,48 isPasskeyOfferDeclined,49 onVaultUnlocked,50 recordPasskeyOfferDecline,51 resolvePendingPasskeyRequest,52 setPasskeyProviderEnabled,53} from "./passkey";54import { buildAdvancedHealthReport } from "./health";55import { storageClear } from "../shared/storage";56import { findInvalidWords } from "../shared/recovery-words";57import type {58 Message,59 MessageResponse,60 MessageType,61 ProFeature,62 VaultEntry,63} from "../shared/types";64import { isPasskeyOnly } from "../shared/types";65import { generateTOTP } from "../crypto/totp";66import { isValidRpId } from "../shared/passkey-utils";67import { isNativePlatform, nativePlugin } from "../shared/native";68import { predictBatch as mlPredictBatch } from "./ml";69import type { MLFieldFeatures } from "../shared/ml";70import {71 buildLocalIdentity,72 getOrCreateDeviceId,73 setDeviceName as setDeviceNameV3,74} from "./device-identity";75import {76 clearDeviceRevocation,77 isDeviceRevoked,78 listPairedDevices as listPairedDevicesV3,79 revokeDeviceById as revokeDeviceByIdV3,80 upsertPairedDevice as upsertPairedDeviceV3,81} from "./paired-devices";8283848586878889import {90 cancelRelayAdoptionSource,91 isRelayAdoptionSourceInFlight,92 resolveReceiverSasConfirmation,93 resolveSourceSasConfirmation,94 startRelayAdoptionReceiver,95 startRelayAdoptionSource,96} from "./relay-adoption-source-bg";97import {98 getSyncSettings,99 setRelayUrl,100 setSyncMode,101 type SyncMode,102} from "./sync-settings";103import {104 getSyncState,105 hydratePersistedSyncState,106 setSyncRunner,107 triggerSyncIfReachable,108} from "./sync-trigger";109import { productionSyncRunner } from "./sync-runner";110111112113114115declare const __PEACH_E2E_PASSKEYS__: boolean;116const PASSKEY_E2E_PRO_BYPASS = __PEACH_E2E_PASSKEYS__ === true;117118let nativeSyncRunnerBootstrapped = false;119120function unhexBytes(hex: string): Uint8Array {121 if (hex.length % 2 !== 0) throw new Error("invalid hex");122 const out = new Uint8Array(hex.length / 2);123 for (let i = 0; i < out.length; i++) {124 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);125 }126 return out;127}128129130131132133134135async function requirePro(136 feature: ProFeature,137 trigger: "user" | "background" = "user"138): Promise<{ allowed: boolean }> {139 if (PASSKEY_E2E_PRO_BYPASS && feature === "passkeys") return { allowed: true };140 if (await isProUser()) return { allowed: true };141142 if (trigger === "user") {143 144 chrome.runtime145 .sendMessage({ type: "PRO_GATE_TRIGGERED", payload: { feature } })146 .catch(() => {147 148 });149 } else {150 151 152 153 if (chrome.storage.session) {154 chrome.storage.session155 .set({ pendingProNotice: { feature, triggeredAt: Date.now() } })156 .catch(() => {});157 }158 }159160 return { allowed: false };161}162163164165166167168const DESTRUCTIVE_MESSAGE_TYPES: ReadonlySet<string> = new Set([169 "DESTROY_VAULT",170 "CLEAR_VAULT_ENTRIES",171 "RESTORE_VAULT_SNAPSHOT",172 "RESTORE_FROM_CODEX",173]);174175176const CANARY_MIN_INTERVAL_MS = 60 * 60 * 1000; 177178const PRIVATE_IPV4_RANGES = [179 { prefix: "10." },180 { prefix: "127." },181 { prefix: "169.254." },182 { prefix: "192.168." },183];184185export function setupMessageHandler(): void {186 chrome.runtime.onMessage.addListener(187 (msg: unknown, sender, sendResponse) => {188 if (!isMessage(msg)) {189 sendResponse({ ok: false, error: "invalid_message" });190 return true;191 }192 handleMessage(msg, sender)193 .then(sendResponse)194 .catch((err) =>195 sendResponse({196 ok: false,197 error: err instanceof Error ? err.message : String(err),198 })199 );200 return true;201 }202 );203204 205 206 207 const reg = (globalThis as any).__peachRegisterMessageHandler;208 if (typeof reg === "function") {209 reg(async (m: unknown) => {210 if (!isMessage(m)) return { ok: false, error: "invalid_message" };211 try {212 return await handleMessage(m);213 } catch (err) {214 return {215 ok: false,216 error: err instanceof Error ? err.message : String(err),217 };218 }219 });220 }221 if (isNativePlatform()) {222 void bootstrapNativeSyncRunner();223 }224}225226async function bootstrapNativeSyncRunner(): Promise<void> {227 if (nativeSyncRunnerBootstrapped) return;228 nativeSyncRunnerBootstrapped = true;229 try {230 setSyncRunner(productionSyncRunner);231 await hydratePersistedSyncState();232 } catch (err) {233 nativeSyncRunnerBootstrapped = false;234 console.warn("[Peach] failed to bootstrap native sync runner:", err);235 }236}237238function isMessage(m: unknown): m is Message {239 return typeof m === "object" && m !== null && "type" in m;240}241242243244245246247248249250function attestedSenderOrigin(sender: Partial<chrome.runtime.MessageSender>): string | undefined {251 if (sender.origin) return sender.origin;252 if (sender.url) {253 try { return new URL(sender.url).origin; } catch { return undefined; }254 }255 return undefined;256}257258259260261262let restoreReady: Promise<void> = Promise.resolve();263264265export function setRestoreReady(p: Promise<unknown>): void {266 restoreReady = p.then(() => undefined, () => undefined);267}268269export async function handleMessage(270 msg: Message,271 sender: Partial<chrome.runtime.MessageSender> = {}272): Promise<MessageResponse> {273 try {274 275 276 277 278 279 280 await restoreReady;281 282 283 284 285 if (286 sender.id !== undefined &&287 sender.id !== chrome.runtime.id288 ) {289 return { ok: false, error: "invalid_sender" };290 }291292 293 294 295 296 297 298 if (DESTRUCTIVE_MESSAGE_TYPES.has(msg.type)) {299 const extensionOrigin = chrome.runtime.getURL("");300 if (!sender.url?.startsWith(extensionOrigin)) {301 return { ok: false, error: "invalid_sender" };302 }303 }304305 if (shouldRefreshAutoLock(msg.type) && vault.isUnlocked()) {306 touchAutoLock();307 }308309 switch (msg.type) {310 case "CREATE_VAULT": {311 const { password } = msg.payload as { password: string };312 313 314 315 316 if (typeof password !== "string" || password.length === 0) {317 return { ok: false, error: "invalid_payload" };318 }319 await vault.createVault(password);320 await setupAutoLock();321 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });322 onVaultUnlocked();323 return { ok: true };324 }325 case "CREATE_VAULT_FROM_ROOT_KEY": {326 const { password, rootKeyHex } = (msg.payload ?? {}) as {327 password?: unknown;328 rootKeyHex?: unknown;329 };330 if (typeof password !== "string" || password.length === 0 || typeof rootKeyHex !== "string") {331 return { ok: false, error: "invalid_payload" };332 }333334 try {335 const { vaultId } = await vault.createVaultFromRootKey(password, rootKeyHex);336 await setupAutoLock();337 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });338 onVaultUnlocked();339 return { ok: true, vaultId };340 } catch (err) {341 return { ok: false, error: err instanceof Error ? err.message : String(err) };342 }343 }344 case "UNLOCK": {345 const { password } = msg.payload as { password: string };346 const ok = await vault.unlock(password);347 if (ok) {348 349 350 const kat = await runSelfTest();351 if (!kat.ok) {352 console.error("[Peach] Crypto self-test FAILED:", kat.error);353 await vault.lock();354 return { ok: false, error: "self_test_failed" };355 }356 await setupAutoLock();357 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });358 onVaultUnlocked();359 if (isNativePlatform()) await publishInMemoryVault(vault.getEntries());360 return { ok: true };361 }362 363 364 365 const failure = vault.consumeLastUnlockFailureReason();366 if (failure) {367 return {368 ok: false,369 error: "vault_corrupted",370 canRestoreFromHistory: true,371 } as MessageResponse;372 }373 return { ok: false };374 }375 case "VALIDATE_RECOVERY_WORDS": {376 const { words } = msg.payload as { words: string[] };377 const invalid = findInvalidWords(words);378 return { ok: true, data: { valid: invalid.length === 0, invalid } };379 }380 case "GET_RECOVERY_WORDS": {381 382 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };383 384 385 386 387 388 389 390 391 if (!(await isRecoveryConfirmPending())) {392 const { password } = (msg.payload ?? {}) as { password?: string };393 if (typeof password !== "string" || !password) {394 return { ok: false, error: "password_required" };395 }396 if (!(await vault.verifyMainPassword(password))) {397 return { ok: false, error: "wrong_password" };398 }399 }400 try {401 const words = await vault.getRecoveryWords();402 return { ok: true, data: words };403 } catch (e) {404 405 return { ok: false, error: "recovery_not_setup" };406 }407 }408 case "RECOVER_WITH_WORDS": {409 410 411 const { words, newPassword } = msg.payload as {412 words: string[];413 newPassword: string;414 };415 const stored = await vault.loadSlotA();416 if (!stored) return { ok: false, error: "no_vault" };417 if (!stored.recovery_salt || !stored.recovery_wrapped_root_key) {418 return { ok: false, error: "recovery_not_setup" };419 }420 421 try {422 await vault.unwrapRootKeyForRecovery(423 stored.recovery_wrapped_root_key,424 stored.recovery_nonce ?? "",425 stored.recovery_tag ?? "",426 stored.recovery_salt,427 words428 );429 } catch {430 return { ok: false, error: "invalid_recovery_words" };431 }432 433 const newSalt = vault.generateSalt();434 const newKey = await vault.deriveKey(newPassword, newSalt);435 436 437 await vault.rewrapRootKeyForPassword(newKey, newSalt);438 439 await vault.changePasswordWithNewKey(newSalt, newKey);440 await setupAutoLock();441 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });442 return { ok: true };443 }444 case "LOCK":445 vault.lock();446 if (isNativePlatform()) await clearInMemoryVault();447 return { ok: true };448 case "IS_UNLOCKED":449 return { ok: true, data: vault.isUnlocked() };450 case "GET_VAULT_STATUS":451 return { ok: true, unlocked: vault.isUnlocked() };452 case "HAS_VAULT":453 return { ok: true, data: await vault.hasVault() };454 case "GET_ENTRIES":455 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };456 return { ok: true, data: vault.getEntries() };457 case "ADD_ENTRY": {458 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };459 const payload = msg.payload;460 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {461 return { ok: false, error: "invalid_payload" };462 }463 const entry = await vault.addEntry(payload as Parameters<typeof vault.addEntry>[0]);464 return { ok: true, data: entry };465 }466 case "BATCH_ADD_ENTRIES": {467 const payload = msg.payload as { entries?: unknown } | null | undefined;468 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {469 return { ok: false, error: "invalid_payload" };470 }471 if (!Array.isArray(payload.entries)) {472 return { ok: false, error: "invalid_payload" };473 }474 const added = await vault.batchAddEntries(payload.entries as Parameters<typeof vault.batchAddEntries>[0]);475 return { ok: true, data: added };476 }477 case "RESTORE_ENTRIES": {478 const payload = msg.payload as479 | { entries?: unknown; mode?: unknown; settings?: unknown }480 | null481 | undefined;482 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {483 return { ok: false, error: "invalid_payload" };484 }485 if (!Array.isArray(payload.entries)) {486 return { ok: false, error: "invalid_payload" };487 }488 if (payload.mode !== "replace" && payload.mode !== "merge") {489 return { ok: false, error: "invalid_payload" };490 }491 const summary = await vault.restoreEntries(492 payload.entries as Parameters<typeof vault.restoreEntries>[0],493 payload.mode494 );495 if (payload.settings && typeof payload.settings === "object" && !Array.isArray(payload.settings)) {496 await vault.updateSettings(payload.settings as vault.VaultSettings);497 }498 return { ok: true, data: summary };499 }500 case "CLEAR_VAULT_ENTRIES": {501 await vault.clearVaultEntries("REPLACE_ALL");502 return { ok: true };503 }504 case "GET_VAULT_HISTORY": {505 const snapshots = await vault.getVaultHistory();506 return { ok: true, data: snapshots };507 }508 case "RESTORE_VAULT_SNAPSHOT": {509 const { snapshotId } = msg.payload as { snapshotId: string };510 const result = await vault.restoreVaultSnapshot(snapshotId);511 if (!result.ok) return { ok: false, error: result.error };512 broadcastToContentScripts({ type: "VAULT_LOCKED" });513 return { ok: true };514 }515 case "COUNT_DUPLICATES": {516 return { ok: true, data: vault.countDuplicates() };517 }518 case "DEDUPLICATE_ENTRIES": {519 const removed = await vault.deduplicateEntries();520 return { ok: true, data: removed };521 }522 case "UPDATE_ENTRY": {523 const payload = msg.payload as { id?: unknown; patch?: unknown } | null | undefined;524 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {525 return { ok: false, error: "invalid_payload" };526 }527 const { id, patch } = payload;528 if (typeof id !== "string" || !id || !patch || typeof patch !== "object" || Array.isArray(patch)) {529 return { ok: false, error: "invalid_payload" };530 }531 532 533 534 535 const incomingCanary = (patch as { canary?: unknown }).canary;536 if (incomingCanary && typeof incomingCanary === "object") {537 const existing = vault.getEntryById(id);538 if (!existing?.canary) {539 const gate = await requirePro("canary_entry");540 if (!gate.allowed) return { ok: false, error: "pro_required" };541 }542 }543 await vault.updateEntry(id, patch as Parameters<typeof vault.updateEntry>[1]);544 return { ok: true };545 }546 case "ADD_ADDITIONAL_URL": {547 const { id, url } = msg.payload as { id: string; url: string };548 const trimmed = typeof url === "string" ? url.trim() : "";549 if (!trimmed) return { ok: false, error: "url_required" };550 const existing = vault.getEntryById(id);551 if (!existing) return { ok: false, error: "entry_not_found" };552 if (existing.type !== "login") return { ok: false, error: "login_entries_only" };553 const merged = [...(existing.additionalUrls ?? []), trimmed];554 await vault.updateEntry(id, { additionalUrls: merged });555 return { ok: true };556 }557 case "DELETE_ENTRY": {558 const { id } = msg.payload as { id: string };559 await vault.deleteEntry(id);560 return { ok: true };561 }562 case "GET_DELETED_ENTRIES":563 return { ok: true, data: vault.getDeletedEntries() };564 case "RESTORE_ENTRY": {565 const { id } = msg.payload as { id: string };566 await vault.restoreEntry(id);567 return { ok: true };568 }569 case "PURGE_ENTRY": {570 const { id } = msg.payload as { id: string };571 await vault.purgeEntry(id);572 return { ok: true };573 }574 case "PURGE_EXPIRED_TRASH": {575 const { retentionMs } = msg.payload as { retentionMs: number };576 const purged = await vault.purgeExpiredTrash(retentionMs);577 return { ok: true, data: purged };578 }579 case "SAVE_NOTE": {580 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };581 const { id, title, body } = msg.payload as { id?: string; title: string; body: string };582 const now = Date.now();583 if (id) {584 const existing = vault.getEntryById(id);585 if (!existing) return { ok: false, error: "not_found" };586 await vault.updateEntry(id, { title: title || "Untitled", notes: body, modifiedAt: now });587 return { ok: true, data: vault.getEntryById(id) };588 } else {589 590 591 592 const entry = await vault.addEntry({593 type: "note",594 title: title || "Untitled",595 notes: body,596 });597 return { ok: true, data: entry };598 }599 }600 case "DELETE_NOTE": {601 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };602 const { id } = msg.payload as { id: string };603 await vault.deleteEntry(id);604 return { ok: true };605 }606 case "GET_NOTES": {607 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };608 const notes = vault.getEntries().filter(609 (e) => e.type === "note" && !e.deletedAt610 );611 return { ok: true, data: notes } as MessageResponse;612 }613 case "SEARCH_VAULT": {614 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };615 616 617 const { query } = (msg.payload ?? {}) as { query?: unknown };618 if (typeof query !== "string") {619 return { ok: false, error: "invalid_payload" };620 }621 const q = query.toLowerCase();622 const searchResults = vault.getEntries().filter(623 (e) =>624 !e.deletedAt &&625 (e.title.toLowerCase().includes(q) ||626 e.notes?.toLowerCase().includes(q) ||627 e.username?.toLowerCase().includes(q))628 );629 return { ok: true, data: searchResults } as MessageResponse;630 }631 case "AUTOFILL_QUERY": {632 if (!vault.isUnlocked()) {633 return { ok: false, error: "vault_locked" };634 }635 let { includeAll } = msg.payload as { includeAll?: boolean };636 const { domain, kind, userGesture } = msg.payload as {637 domain: string;638 kind?: "login" | "card" | "totp";639 userGesture?: boolean;640 };641 642 643 644 645 646 647 648 if (includeAll && userGesture !== true) {649 includeAll = false;650 }651 652 653 654 655 if (kind === "card") {656 const cards = vault.getEntries().filter((e) => e.type === "card");657 return {658 ok: true,659 data: cards.map((e) => ({660 id: e.id,661 title: e.cardNickname || e.title,662 cardholderName: e.cardholderName,663 last4: e.last4,664 expMonth: e.expMonth,665 expYear: e.expYear,666 network: e.network,667 })),668 };669 }670 if (kind === "totp") {671 const matches = (672 includeAll673 ? vault.getEntries().filter((entry) => entry.type !== "note" && entry.type !== "card")674 : vault.queryDomain(domain)675 ).filter((entry) => !isPasskeyOnly(entry) && Boolean(entry.totp));676 return {677 ok: true,678 data: matches.map((e) => ({679 id: e.id,680 title: e.title,681 username: e.username,682 email: e.email,683 url: e.url,684 hasTotp: true,685 })),686 };687 }688 689 690 691 692 693 const matches = (694 includeAll695 ? vault696 .getEntries()697 .filter((entry) => entry.type !== "note" && entry.type !== "card")698 : vault.queryDomain(domain)699 ).filter((entry) => !isPasskeyOnly(entry));700 return {701 ok: true,702 data: matches.map((e) =>703 includeAll704 ? {705 id: e.id,706 title: e.title,707 708 709 710 711 712 username: e.username,713 email: e.email,714 url: e.url,715 hasPasskey: e.passkey !== undefined,716 }717 : {718 id: e.id,719 title: e.title,720 username: e.username,721 email: e.email,722 url: e.url,723 hasTotp: Boolean(e.totp),724 hasPasskey: e.passkey !== undefined,725 }726 ),727 };728 }729 case "GET_ENTRY_FOR_FILL": {730 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };731 const { id, explicitSummon } = msg.payload as {732 id: string;733 explicitSummon?: boolean;734 };735 const entry = vault.getEntryById(id);736 if (!entry) return { ok: false, error: "entry_not_found" };737 738 739 740 if (isPasskeyOnly(entry)) return { ok: false, error: "passkey_not_fillable" };741742 743 744 745 746 if (sender.tab !== undefined || sender.url !== undefined) {747 let senderDomain: string | null = null;748 try {749 const rawUrl = sender.tab?.url ?? sender.url ?? "";750 senderDomain = rawUrl ? extractDomain(new URL(rawUrl).hostname) : null;751 } catch {752 senderDomain = null;753 }754 if (!senderDomain) return { ok: false, error: "origin_unverified" };755756 const entryDomains = new Set<string>();757 const primary = extractDomain(entry.url ?? "");758 if (primary) entryDomains.add(primary);759 for (const extra of entry.additionalUrls ?? []) {760 const d = extractDomain(extra);761 if (d) entryDomains.add(d);762 }763764 765 766 767 if (explicitSummon !== true && !entryDomains.has(senderDomain)) {768 769 return { ok: false, error: "entry_not_found" };770 }771 }772 773 774 775 776 777 778 779 780 781 782 const {783 passkey: _strippedPasskey,784 totp: seed,785 totpAlgorithm,786 totpDigits,787 totpPeriod,788 passwordHistory: _ph,789 ...safeEntry790 } = entry;791 let totpCode: string | undefined;792 let totpRemaining: number | undefined;793 if (seed) {794 try {795 796 797 const generated = generateTOTP(seed, totpAlgorithm, totpDigits, totpPeriod);798 totpCode = generated.code;799 totpRemaining = generated.remaining;800 } catch {801 totpCode = undefined;802 }803 }804 return { ok: true, data: { ...safeEntry, totpCode, totpRemaining } };805 }806 807 808 809 810 811 812 case "GET_ENTRY_FOR_EDIT": {813 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };814 const { id, explicitSummon } = msg.payload as {815 id: string;816 explicitSummon?: boolean;817 };818 const entry = vault.getEntryById(id);819 if (!entry) return { ok: false, error: "entry_not_found" };820821 822 823 824 825 826 827 828 829 if (sender.tab !== undefined || sender.url !== undefined) {830 let senderDomain: string | null = null;831 try {832 const rawUrl = sender.tab?.url ?? sender.url ?? "";833 senderDomain = rawUrl ? extractDomain(new URL(rawUrl).hostname) : null;834 } catch {835 senderDomain = null;836 }837 if (senderDomain) {838 const entryDomains = new Set<string>();839 const primary = extractDomain(entry.url ?? "");840 if (primary) entryDomains.add(primary);841 for (const extra of entry.additionalUrls ?? []) {842 const d = extractDomain(extra);843 if (d) entryDomains.add(d);844 }845 if (explicitSummon !== true && !entryDomains.has(senderDomain)) {846 return { ok: false, error: "entry_not_found" }; 847 }848 }849 }850851 const { passkey: _passkey, ...safeEntry } = entry;852 return { ok: true, data: safeEntry };853 }854 case "COPY": {855 856 857 858 const { text, label, alreadyWritten } = (msg.payload ?? {}) as {859 text?: unknown;860 label?: unknown;861 alreadyWritten?: unknown;862 };863 if (typeof text !== "string" || typeof label !== "string") {864 return { ok: false, error: "invalid_payload" };865 }866 await copyToClipboard(text, label, Boolean(alreadyWritten));867 return { ok: true, clearSeconds: getEffectiveClearSeconds() };868 }869 case "CHECK_ALL_BREACHES": {870 const allEntries = vault.getEntries();871 const results: Record<string, number> = {};872 for (const entry of allEntries) {873 if (entry.type === "note") continue;874 if (!entry.password) continue;875 results[entry.id] = await vault.checkBreach(entry.password);876 877 await new Promise((r) => setTimeout(r, 200));878 }879 880 for (const [id, count] of Object.entries(results)) {881 const entry = vault.getEntryById(id);882 if (entry && (entry.breach ?? 0) !== count) {883 await vault.updateEntry(id, { breach: count > 0 ? count : undefined });884 }885 }886 return { ok: true, data: results };887 }888 case "GET_SETTINGS":889 return { ok: true, data: await vault.ensureSettingsLoaded() };890 case "UPDATE_SETTINGS": {891 const patch = msg.payload;892 if (!patch || typeof patch !== "object" || Array.isArray(patch)) {893 return { ok: false, error: "invalid_payload" };894 }895 await vault.updateSettings(patch as vault.VaultSettings);896 if (typeof (patch as Record<string, unknown>).autoLockMinutes === "number") {897 await setupAutoLock();898 if (vault.isUnlocked()) touchAutoLock();899 }900 return { ok: true };901 }902 case "GET_PENDING_PASSKEY":903 return { ok: true, data: await getPendingPasskeyRequest() };904 case "APPROVE_PASSKEY": {905 const { requestId } = msg.payload as { requestId: number };906 await resolvePendingPasskeyRequest(requestId, true);907 return { ok: true };908 }909 case "DENY_PASSKEY": {910 const { requestId } = msg.payload as { requestId: number };911 await resolvePendingPasskeyRequest(requestId, false);912 return { ok: true };913 }914 case "GET_PASSKEY_PROVIDER_STATE":915 return { ok: true, data: await getPasskeyProviderState() };916 case "SET_PASSKEY_PROVIDER_ENABLED": {917 const { enabled } = msg.payload as { enabled: boolean };918 919 920 if (enabled) {921 const gate = await requirePro("passkeys");922 if (!gate.allowed) return { ok: false, error: "pro_required" };923 }924 return { ok: true, data: await setPasskeyProviderEnabled(enabled) };925 }926 927 928 929 930 931 932 case "FIRE_CANARY_PING": {933 934 935 936 937 const { webhookUrl, entryTitle, trigger } = msg.payload as {938 webhookUrl: string;939 entryTitle: string;940 trigger: "password_copied" | "username_copied" | "autofilled" | "submitted" | "test";941 };942 if (!isAllowedCanaryWebhookUrl(webhookUrl)) {943 return { ok: false, error: "invalid_webhook_url" };944 }945 946 947 948 949 950 951 952 if (trigger !== "test") {953 const rlKey = `canary-last-fetch:${webhookUrl}:${entryTitle}`;954 const stored = await chrome.storage.local.get(rlKey);955 const last = (stored as Record<string, unknown>)[rlKey];956 if (typeof last === "number" && Date.now() - last < CANARY_MIN_INTERVAL_MS) {957 return { ok: true, data: { rateLimited: true } };958 }959 await chrome.storage.local.set({ [rlKey]: Date.now() });960 }961 const body = JSON.stringify({962 event: trigger === "test" ? "peach_canary_test" : "peach_canary_accessed",963 entry: entryTitle,964 trigger,965 timestamp: new Date().toISOString(),966 source: "peach-extension",967 });968 if (trigger === "test") {969 970 971 972 973 974 try {975 const res = await fetch(webhookUrl, {976 method: "POST",977 headers: { "Content-Type": "application/json" },978 body,979 });980 return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };981 } catch (err) {982 return { ok: false, error: err instanceof Error ? err.message : "network_error" };983 }984 }985 986 fetch(webhookUrl, {987 method: "POST",988 headers: { "Content-Type": "application/json" },989 body,990 }).catch(() => {});991 return { ok: true };992 }993 case "ACTIVATE_LICENSE": {994 const { key } = msg.payload as { key: string };995 const licenseInfo = await activateLicense(key);996 return { ok: true, data: licenseInfo };997 }998 case "GET_LICENSE": {999 const licenseInfo = await getLicenseInfo();1000 return { ok: true, data: licenseInfo };1001 }1002 case "GET_LICENSE_KEY": {1003 const key = await getLicenseKey();1004 return { ok: true, data: key };1005 }1006 case "DEACTIVATE_LICENSE": {1007 await deactivateLicense();1008 return { ok: true };1009 }1010 case "SHOULD_PLAY_CELEBRATION": {1011 const should = await shouldPlayCelebration();1012 return { ok: true, data: should };1013 }1014 case "MARK_CELEBRATION_PLAYED": {1015 await markCelebrationPlayed();1016 return { ok: true };1017 }1018 case "GET_CODEX_VAULT_DATA": {1019 1020 1021 const gate = await requirePro("codex");1022 if (!gate.allowed) return { ok: false, error: "pro_required" };1023 return await handleGetCodexVaultData();1024 }1025 case "ESTIMATE_CODEX_PAGES": {1026 const gate = await requirePro("codex");1027 if (!gate.allowed) return { ok: false, error: "pro_required" };1028 return await handleEstimateCodexPages();1029 }1030 case "RESTORE_FROM_CODEX": {1031 1032 1033 1034 const payload = msg.payload as CodexRestorePayload;1035 return await handleRestoreFromCodex(payload);1036 }1037 case "OPEN_PRO_GATE": {1038 1039 const { feature } = (msg.payload ?? {}) as { feature?: string };1040 if (chrome.storage.session) {1041 await chrome.storage.session.set({ pendingProGate: { feature: feature ?? "passkeys" } });1042 }1043 try { await chrome.action.openPopup(); } catch { }1044 return { ok: true };1045 }1046 case "OPEN_POPUP": {1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 if (chrome.storage.session) {1057 await chrome.storage.session.set({ peach_inline_unlock: true });1058 }1059 try { await chrome.action.openPopup(); } catch { }1060 return { ok: true };1061 }1062 case "GET_IDENTITIES": {1063 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1064 return { ok: true, data: vault.getIdentities() };1065 }1066 case "TOUCH_AUTO_LOCK": {1067 1068 1069 return { ok: true };1070 }1071 case "GET_ADVANCED_HEALTH_REPORT": {1072 const gate = await requirePro("advanced_health");1073 if (!gate.allowed) return { ok: false, error: "pro_required" };1074 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1075 const report = await buildAdvancedHealthReport(vault.getEntries());1076 return { ok: true, data: report };1077 }1078 case "IS_DURESS_ENABLED": {1079 const enabled = await vault.isDuressEnabled();1080 return { ok: true, data: enabled };1081 }1082 case "SETUP_DURESS": {1083 1084 1085 1086 1087 const gate = await requirePro("duress_vault");1088 if (!gate.allowed) return { ok: false, error: "pro_required" };1089 1090 1091 1092 if (!vault.isUnlocked()) return { ok: false, error: "session_expired" };1093 const { duressPassword } = msg.payload as { duressPassword: string };1094 await vault.setupDuress(duressPassword);1095 return { ok: true };1096 }1097 case "DISABLE_DURESS": {1098 await vault.disableDuress();1099 return { ok: true };1100 }1101 case "VERIFY_MAIN_PASSWORD": {1102 const { password } = msg.payload as { password: string };1103 const ok = await vault.verifyMainPassword(password);1104 return { ok: true, data: ok };1105 }1106 case "CHANGE_PASSWORD": {1107 const { currentPassword, newPassword } = msg.payload as {1108 currentPassword: string;1109 newPassword: string;1110 };1111 const changed = await vault.changePassword(currentPassword, newPassword);1112 return changed1113 ? { ok: true }1114 : { ok: false, error: "incorrect_current_password" };1115 }1116 case "GET_SITE_PASSWORD_POLICY": {1117 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });1118 if (!tab?.id) return { ok: true, data: null };1119 try {1120 const response = await chrome.tabs.sendMessage(tab.id, {1121 type: "GET_SITE_PASSWORD_POLICY",1122 });1123 return (response as MessageResponse) ?? { ok: true, data: null };1124 } catch {1125 return { ok: true, data: null };1126 }1127 }1128 case "RECORD_MANUAL_SUMMON": {1129 const { domain } = msg.payload as { domain: string };1130 const settings = vault.getSettings();1131 const existing = settings.manualSummonDomains?.[domain];1132 const now = Date.now();1133 await vault.updateSettings({1134 manualSummonDomains: {1135 ...settings.manualSummonDomains,1136 [domain]: {1137 firstSummonedAt: existing?.firstSummonedAt ?? now,1138 summonCount: (existing?.summonCount ?? 0) + 1,1139 lastSummonedAt: now,1140 },1141 },1142 });1143 return { ok: true };1144 }1145 case "DECLINE_CARD_SAVE": {1146 const { last4, expMonth, expYear } = msg.payload as { last4: string; expMonth: string; expYear: string };1147 await vault.declineCardSaveOffer(last4, expMonth, expYear);1148 return { ok: true };1149 }1150 case "IS_CARD_DECLINED": {1151 const { last4, expMonth, expYear } = msg.payload as { last4: string; expMonth: string; expYear: string };1152 const declined = await vault.isCardDeclined(last4, expMonth, expYear);1153 return { ok: true, data: declined };1154 }1155 case "INITIATE_FIREFOX_PASSKEY_CREATE": {1156 const { rpId, rpName, userName, userDisplayName } = (msg.payload ?? {}) as {1157 rpId?: string;1158 rpName?: string;1159 userName?: string;1160 userDisplayName?: string;1161 };1162 if (!rpId || !userName) return { ok: false, error: "rpid_and_username_required" };1163 return await initiateFirefoxPasskeyCreate({1164 rpId,1165 rpName: rpName ?? rpId,1166 userName,1167 userDisplayName,1168 });1169 }11701171 case "COMPLETE_FIREFOX_PASSKEY_CREATE": {1172 const payload = msg.payload as Parameters<typeof completeFirefoxPasskeyCreate>[0] | undefined;1173 if (!payload) return { ok: false, error: "missing_payload" };1174 return await completeFirefoxPasskeyCreate(payload);1175 }11761177 case "PASSKEY_OFFER_DECLINE": {1178 const { domain } = (msg.payload ?? {}) as { domain?: string };1179 if (!domain) return { ok: false, error: "domain_required" };1180 await recordPasskeyOfferDecline(domain);1181 return { ok: true };1182 }11831184 case "GET_PASSKEY_OFFER_DECLINED": {1185 const { domain } = (msg.payload ?? {}) as { domain?: string };1186 if (!domain) return { ok: false, error: "domain_required" };1187 return { ok: true, data: await isPasskeyOfferDeclined(domain) };1188 }11891190 case "PASSKEY_HAS_CREDENTIALS": {1191 const payload = msg.payload as {1192 rpId?: string;1193 allowCredentials?: Array<{ id: string; type?: string }>;1194 } | undefined;1195 if (!payload?.rpId) return { ok: false, error: "rpid_required" };1196 return await handlePasskeyHasCredentials({1197 rpId: payload.rpId,1198 allowCredentials: payload.allowCredentials ?? [],1199 }, attestedSenderOrigin(sender));1200 }12011202 case "PASSKEY_CREATE": {1203 const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyCreate>[0] | undefined;1204 if (!payload?.rpId || !payload?.userName) {1205 return { ok: false, error: "rpid_and_username_required" };1206 }1207 return await handleFirefoxPasskeyCreate(payload, attestedSenderOrigin(sender));1208 }12091210 case "PASSKEY_GET": {1211 const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyGet>[0] | undefined;1212 if (!payload?.rpId) return { ok: false, error: "rpid_required" };1213 return await handleFirefoxPasskeyGet(payload, attestedSenderOrigin(sender));1214 }12151216 case "PASSKEY_AUTH_RESPONSE": {1217 const { requestId, approved } = (msg.payload ?? {}) as {1218 requestId?: string | number;1219 approved?: boolean;1220 };1221 if (requestId === undefined || requestId === null || typeof approved !== 'boolean') {1222 return { ok: false, error: 'requestId and approved required' };1223 }1224 await resolvePendingPasskeyRequest(Number(requestId), approved);1225 return { ok: true };1226 }12271228 case "GET_PASSKEY_FOR_DOMAIN": {1229 const { domain } = (msg.payload ?? {}) as { domain?: string };1230 if (!domain) return { ok: false, error: "domain_required" };1231 1232 1233 1234 const origin = attestedSenderOrigin(sender);1235 if (!origin || !isValidRpId(domain, origin)) return { ok: true, data: null };1236 if (!vault.isUnlocked()) return { ok: true, data: null };1237 const match = vault1238 .getEntries()1239 .find((entry) => entry.passkey?.rpId === domain);1240 if (!match || !match.passkey) return { ok: true, data: null };1241 1242 1243 return {1244 ok: true,1245 data: {1246 rpId: match.passkey.rpId,1247 userName: match.passkey.userName,1248 browser: match.passkey.browser,1249 },1250 };1251 }12521253 case "DESTROY_VAULT": {1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 const preserved = await chrome.storage.local.get([1265 "peach_license",1266 "peach_pro_celebrated",1267 ]);1268 vault.lock();1269 await storageClear();1270 await vault.clearVaultHistory();1271 const toRestore: Record<string, unknown> = {};1272 if (preserved.peach_license !== undefined) toRestore.peach_license = preserved.peach_license;1273 if (preserved.peach_pro_celebrated !== undefined) toRestore.peach_pro_celebrated = preserved.peach_pro_celebrated;1274 if (Object.keys(toRestore).length > 0) {1275 await chrome.storage.local.set(toRestore);1276 }1277 if (chrome.storage.session) {1278 await chrome.storage.session.clear();1279 }1280 return { ok: true };1281 }12821283 1284 case "UNLOCK_WITH_KEY": {1285 const { masterKeyHex } = msg.payload as { masterKeyHex: string };1286 if (!isNativePlatform()) {1287 return { ok: false, error: "desktop_only_uses_password" };1288 }1289 try {1290 const result = await vault.unlockWithKey(masterKeyHex);1291 if (result.ok) {1292 onVaultUnlocked();1293 await publishInMemoryVault(result.entries);1294 }1295 return { ok: result.ok, data: result.ok ? { entries: result.entries } : undefined, error: result.ok ? undefined : result.error };1296 } catch {1297 return { ok: false, error: "invalid_key" };1298 }1299 }1300 1301 case "GET_DERIVED_KEY": {1302 if (!isNativePlatform()) {1303 return { ok: false, error: "android_only" };1304 }1305 if (!vault.isUnlocked()) {1306 return { ok: false, error: "vault_locked" };1307 }1308 return { ok: true, data: { masterKeyHex: vault.getDerivedKeyHex() } };1309 }1310 case "GET_IDENTITY_SUGGESTIONS": {1311 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1312 const entries = vault.getEntries();1313 const suggestions = buildIdentitySuggestions(entries);1314 return { ok: true, data: { suggestions } };1315 }1316 case "POPULATE_INLINE_VAULT": {1317 if (!isNativePlatform()) return { ok: false };1318 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1319 const entries = vault.getEntries();1320 await publishInMemoryVault(entries);1321 return { ok: true };1322 }1323 case "ML_INIT":1324 return { ok: true };13251326 case "ML_PREDICT_BATCH": {1327 1328 const { featuresArray } = (msg.payload ?? {}) as { featuresArray?: unknown };1329 if (!Array.isArray(featuresArray)) {1330 return { ok: false, error: "invalid_payload" };1331 }1332 return { ok: true, data: mlPredictBatch(featuresArray as MLFieldFeatures[]) };1333 }13341335 1336 1337 1338 1339 case "GET_LOCAL_IDENTITY": {1340 1341 1342 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1343 1344 1345 const masterKeyHex = vault.getRootKeyHex();1346 if (!masterKeyHex) return { ok: false, error: "vault_locked" };1347 const masterKey = unhexBytes(masterKeyHex);1348 try {1349 const identity = await buildLocalIdentity(masterKey);1350 1351 1352 if (await isProUser()) {1353 const license = await getLicenseKey();1354 if (license) (identity as { license?: string }).license = license;1355 }1356 return { ok: true, data: identity };1357 } finally {1358 masterKey.fill(0);1359 }1360 }13611362 case "GET_ADOPTION_MATERIAL": {1363 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1364 const rootKeyHex = vault.getRootKeyHex();1365 if (!rootKeyHex) return { ok: false, error: "vault_locked" };1366 const rootKeyBytes = unhexBytes(rootKeyHex);1367 try {1368 const data: { rootKeyHex: string; vaultId: string; license?: string } = {1369 rootKeyHex,1370 vaultId: await computeVaultId(rootKeyBytes),1371 };1372 if (await isProUser()) {1373 const license = await getLicenseKey();1374 if (license) data.license = license;1375 }1376 return { ok: true, data };1377 } finally {1378 rootKeyBytes.fill(0);1379 }1380 }13811382 case "COMPLETE_PAIRING": {1383 1384 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1385 const paired = (msg.payload as { paired?: unknown })?.paired;1386 if (typeof paired !== "object" || paired === null) {1387 return { ok: false, error: "invalid_payload" };1388 }1389 const candidate = paired as Record<string, unknown>;1390 if (1391 typeof candidate.deviceId !== "string" || !candidate.deviceId ||1392 typeof candidate.deviceName !== "string" || !candidate.deviceName ||1393 typeof candidate.deviceType !== "string" ||1394 typeof candidate.longTermPublicKeyHex !== "string" ||1395 !/^[0-9a-f]{64}$/.test(candidate.longTermPublicKeyHex) ||1396 (1397 candidate.signingPublicKeyHex !== undefined &&1398 (1399 typeof candidate.signingPublicKeyHex !== "string" ||1400 !/^[0-9a-f]{64}$/.test(candidate.signingPublicKeyHex)1401 )1402 ) ||1403 typeof candidate.pairedAt !== "number"1404 ) {1405 return { ok: false, error: "invalid_paired_device" };1406 }1407 1408 1409 1410 1411 await clearDeviceRevocation(candidate.deviceId);1412 await upsertPairedDeviceV3(candidate as unknown as import("../shared/sync-types").PairedDevice);1413 1414 1415 1416 void triggerSyncIfReachable({ reason: "device_list_changed" }).catch(() => undefined);1417 return { ok: true };1418 }14191420 case "LIST_PAIRED_DEVICES": {1421 const devices = await listPairedDevicesV3();1422 return { ok: true, data: { devices } };1423 }14241425 case "UNPAIR_DEVICE": {1426 const { deviceId } = (msg.payload ?? {}) as { deviceId?: string };1427 if (typeof deviceId !== "string" || !deviceId) {1428 return { ok: false, error: "invalid_payload" };1429 }1430 1431 1432 1433 await revokeDeviceByIdV3(deviceId, await getOrCreateDeviceId());1434 1435 1436 void triggerSyncIfReachable({ reason: "device_list_changed" }).catch(() => undefined);1437 return { ok: true };1438 }14391440 case "RENAME_LOCAL_DEVICE": {1441 const { name } = (msg.payload ?? {}) as { name?: string };1442 if (typeof name !== "string") return { ok: false, error: "invalid_payload" };1443 await setDeviceNameV3(name);1444 return { ok: true };1445 }14461447 1448 1449 1450 1451 case "START_ADOPTION_SOURCE": {1452 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1453 if (isRelayAdoptionSourceInFlight()) {1454 return { ok: false, error: "adoption_source_in_progress" };1455 }1456 try {1457 return { ok: true, data: await startRelayAdoptionSource() };1458 } catch (error) {1459 return {1460 ok: false,1461 error: error instanceof Error ? error.message : "adoption_source_failed",1462 };1463 }1464 }14651466 1467 1468 1469 1470 case "START_ADOPTION_RECEIVER": {1471 if (isRelayAdoptionSourceInFlight()) {1472 return { ok: false, error: "adoption_receiver_in_progress" };1473 }1474 const password = (msg.payload as { password?: unknown } | undefined)?.password;1475 if (typeof password !== "string" || password.length === 0) {1476 return { ok: false, error: "invalid_payload" };1477 }1478 try {1479 return { ok: true, data: await startRelayAdoptionReceiver(password) };1480 } catch (error) {1481 return {1482 ok: false,1483 error: error instanceof Error ? error.message : "adoption_receiver_failed",1484 };1485 }1486 }14871488 1489 1490 1491 1492 case "RECEIVER_SAS_DECISION": {1493 const payload = msg.payload as { id?: unknown; ok?: unknown } | undefined;1494 if (typeof payload?.id !== "string" || typeof payload?.ok !== "boolean") {1495 return { ok: false, error: "invalid_payload" };1496 }1497 return { ok: resolveReceiverSasConfirmation(payload.id, payload.ok) };1498 }14991500 1501 1502 1503 case "SOURCE_SAS_DECISION": {1504 const payload = msg.payload as { id?: unknown; ok?: unknown } | undefined;1505 if (typeof payload?.id !== "string" || typeof payload?.ok !== "boolean") {1506 return { ok: false, error: "invalid_payload" };1507 }1508 return { ok: resolveSourceSasConfirmation(payload.id, payload.ok) };1509 }15101511 case "CANCEL_ADOPTION_SOURCE": {1512 cancelRelayAdoptionSource();1513 return { ok: true };1514 }15151516 case "TRIGGER_SYNC": {1517 const payload = (msg.payload ?? {}) as {1518 reason?: "vault_change" | "foreground" | "manual";1519 force?: boolean;1520 };1521 const reason = payload.reason ?? "foreground";15221523 void triggerSyncIfReachable({ reason, force: payload.force === true });1524 return { ok: true };1525 }15261527 case "GET_SYNC_STATE": {1528 return { ok: true, data: getSyncState() };1529 }15301531 case "GET_SYNC_SETTINGS": {1532 return { ok: true, data: await getSyncSettings() };1533 }15341535 case "SET_SYNC_SETTINGS": {1536 const payload = (msg.payload ?? {}) as {1537 mode?: unknown;1538 relayUrl?: unknown;1539 };1540 try {1541 1542 1543 if (payload.relayUrl !== undefined) {1544 if (typeof payload.relayUrl !== "string") {1545 return { ok: false, error: "invalid_relay_url" };1546 }1547 return { ok: true, data: await setRelayUrl(payload.relayUrl) };1548 }1549 if (payload.mode !== undefined) {1550 if (payload.mode !== "public" && payload.mode !== "private" && payload.mode !== "off") {1551 return { ok: false, error: "invalid_sync_mode" };1552 }1553 return { ok: true, data: await setSyncMode(payload.mode as SyncMode) };1554 }1555 return { ok: true, data: await getSyncSettings() };1556 } catch (error) {1557 return {1558 ok: false,1559 error: error instanceof Error ? error.message : "invalid_relay_url",1560 };1561 }1562 }15631564 case "GET_SYNC_ALERTS": {1565 const { consume } = (msg.payload ?? {}) as { consume?: boolean };1566 const alerts = await getPendingSyncAlerts();1567 1568 1569 1570 if (consume) await clearPendingSyncAlerts();1571 return { ok: true, data: alerts };1572 }15731574 case "GET_PENDING_VOUCHED_DEVICES": {1575 return { ok: true, data: await listPendingVouchedDevices() };1576 }1577 case "CONFIRM_VOUCHED_DEVICE": {1578 const { deviceId } = msg.payload as { deviceId: string };1579 const pending = await getPendingVouchedDevice(deviceId);1580 if (!pending) return { ok: false, error: "not_pending" };1581 1582 1583 1584 if (await isDeviceRevoked(pending.deviceId, pending.longTermPublicKeyHex)) {1585 await removePendingVouchedDevice(deviceId);1586 return { ok: false, error: "device_revoked" };1587 }1588 await upsertPairedDeviceV3({1589 deviceId: pending.deviceId,1590 deviceName: pending.deviceName,1591 deviceType:1592 pending.deviceType as import("../shared/sync-types").PairedDevice["deviceType"],1593 longTermPublicKeyHex: pending.longTermPublicKeyHex,1594 signingPublicKeyHex: pending.signingPublicKeyHex,1595 wsPort: pending.wsPort,1596 pairedAt: pending.pairedAt,1597 trustSource: "vouched",1598 });1599 await removePendingVouchedDevice(deviceId);1600 1601 1602 void triggerSyncIfReachable({ reason: "device_list_changed" }).catch(() => undefined);1603 return { ok: true };1604 }1605 case "REJECT_VOUCHED_DEVICE": {1606 const { deviceId } = msg.payload as { deviceId: string };1607 await removePendingVouchedDevice(deviceId);1608 return { ok: true };1609 }1610 case "ROTATE_SYNC_IDENTITY": {1611 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1612 const result = await vault.rotateSyncIdentity();1613 return result.ok ? { ok: true } : { ok: false, error: result.error };1614 }16151616 default:1617 return { ok: false, error: "unknown_message_type" };1618 }1619 } catch (err) {1620 return {1621 ok: false,1622 error: err instanceof Error ? err.message : "operation_failed",1623 };1624 }1625}16261627162816291630async function publishInMemoryVault(entries: VaultEntry[]): Promise<void> {1631 const plugin = nativePlugin<{1632 setEntries?: (args: {1633 entries: Array<{1634 id: string;1635 title: string;1636 username: string;1637 email: string;1638 password: string;1639 url: string;1640 totp: string;1641 additionalUrls: string[];1642 }>;1643 }) => Promise<void>;1644 }>("InMemoryVaultPlugin");1645 if (!plugin?.setEntries) return;1646 await plugin.setEntries({1647 entries: entries.map((e) => ({1648 id: e.id,1649 title: e.title ?? "",1650 username: e.username ?? "",1651 email: (e as VaultEntry & { email?: string }).email ?? "",1652 password: e.password ?? "",1653 url: e.url ?? "",1654 totp: (e as VaultEntry & { totp?: string }).totp ?? "",1655 additionalUrls: (e as VaultEntry & { additionalUrls?: string[] }).additionalUrls ?? [],1656 })),1657 });1658}165916601661async function clearInMemoryVault(): Promise<void> {1662 const plugin = nativePlugin<{ clearEntries?: () => Promise<void> }>("InMemoryVaultPlugin");1663 if (!plugin?.clearEntries) return;1664 await plugin.clearEntries();1665}16661667166816691670function buildIdentitySuggestions(1671 entries: VaultEntry[],1672): Array<{ email: string; username: string; count: number }> {1673 const usernameCounts = new Map<string, number>();1674 const usernameEmail = new Map<string, string>();1675 for (const entry of entries) {1676 const username = entry.username ?? "";1677 const email = (entry as VaultEntry & { email?: string }).email ?? "";1678 if (!username && !email) continue;1679 const key = username || email;1680 usernameCounts.set(key, (usernameCounts.get(key) ?? 0) + 1);1681 if (!usernameEmail.has(key)) usernameEmail.set(key, email);1682 }1683 return Array.from(usernameCounts.entries())1684 .sort((a, b) => b[1] - a[1])1685 .map(([username, count]) => ({1686 email: usernameEmail.get(username) ?? "",1687 username,1688 count,1689 }));1690}169116921693function broadcastToContentScripts(message: { type: string }): void {1694 if (!chrome.tabs?.query) return;1695 chrome.tabs.query({}, (tabs) => {1696 for (const tab of tabs) {1697 if (tab.id !== undefined) {1698 chrome.tabs.sendMessage(tab.id, message).catch(() => {1699 1700 });1701 }1702 }1703 });1704}17051706function shouldRefreshAutoLock(type: MessageType): boolean {1707 return (1708 type !== "HAS_VAULT" &&1709 type !== "IS_UNLOCKED" &&1710 type !== "GET_VAULT_STATUS" &&1711 type !== "LOCK" &&1712 type !== "UPDATE_SETTINGS" &&1713 type !== "GET_PENDING_PASSKEY" &&1714 type !== "GET_PASSKEY_PROVIDER_STATE" &&1715 type !== "DECLINE_CARD_SAVE" &&1716 type !== "IS_CARD_DECLINED"1717 );1718}17191720function isAllowedCanaryWebhookUrl(value: string): boolean {1721 try {1722 const url = new URL(value);1723 if (url.protocol !== "https:") return false;17241725 const hostname = url.hostname.toLowerCase();1726 if (1727 hostname === "localhost" ||1728 hostname.endsWith(".localhost") ||1729 isPrivateIpv4(hostname) ||1730 isPrivateIpv6(hostname)1731 ) {1732 return false;1733 }17341735 return true;1736 } catch {1737 return false;1738 }1739}17401741function isPrivateIpv4(hostname: string): boolean {1742 if (PRIVATE_IPV4_RANGES.some((range) => hostname.startsWith(range.prefix))) {1743 return true;1744 }17451746 const octets = hostname.split(".").map((part) => Number(part));1747 if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {1748 return false;1749 }17501751 return octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31;1752}17531754function isPrivateIpv6(hostname: string): boolean {1755 const normalized = hostname.replace(/^\[|\]$/g, "").toLowerCase();1756 return (1757 normalized === "::1" ||1758 normalized.startsWith("fc") ||1759 normalized.startsWith("fd") ||1760 normalized.startsWith("fe80:")1761 );1762}