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 listPairedDevices as listPairedDevicesV3,77 revokeDeviceById as revokeDeviceByIdV3,78 upsertPairedDevice as upsertPairedDeviceV3,79} from "./paired-devices";808182838485declare const __PEACH_E2E_PASSKEYS__: boolean;86const PASSKEY_E2E_PRO_BYPASS = __PEACH_E2E_PASSKEYS__ === true;8788let nativeSyncRunnerBootstrapped = false;8990function unhexBytes(hex: string): Uint8Array {91 if (hex.length % 2 !== 0) throw new Error("invalid hex");92 const out = new Uint8Array(hex.length / 2);93 for (let i = 0; i < out.length; i++) {94 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);95 }96 return out;97}9899100101102103104105async function requirePro(106 feature: ProFeature,107 trigger: "user" | "background" = "user"108): Promise<{ allowed: boolean }> {109 if (PASSKEY_E2E_PRO_BYPASS && feature === "passkeys") return { allowed: true };110 if (await isProUser()) return { allowed: true };111112 if (trigger === "user") {113 114 chrome.runtime115 .sendMessage({ type: "PRO_GATE_TRIGGERED", payload: { feature } })116 .catch(() => {117 118 });119 } else {120 121 122 123 if (chrome.storage.session) {124 chrome.storage.session125 .set({ pendingProNotice: { feature, triggeredAt: Date.now() } })126 .catch(() => {});127 }128 }129130 return { allowed: false };131}132133134135136137138139140const FEEDBACK_ENDPOINT = "";141142143144145146const DESTRUCTIVE_MESSAGE_TYPES: ReadonlySet<string> = new Set([147 "DESTROY_VAULT",148 "CLEAR_VAULT_ENTRIES",149 "RESTORE_VAULT_SNAPSHOT",150 "RESTORE_FROM_CODEX",151]);152153154const CANARY_MIN_INTERVAL_MS = 60 * 60 * 1000; 155156const PRIVATE_IPV4_RANGES = [157 { prefix: "10." },158 { prefix: "127." },159 { prefix: "169.254." },160 { prefix: "192.168." },161];162163export function setupMessageHandler(): void {164 chrome.runtime.onMessage.addListener(165 (msg: unknown, sender, sendResponse) => {166 if (!isMessage(msg)) {167 sendResponse({ ok: false, error: "invalid_message" });168 return true;169 }170 handleMessage(msg, sender)171 .then(sendResponse)172 .catch((err) =>173 sendResponse({174 ok: false,175 error: err instanceof Error ? err.message : String(err),176 })177 );178 return true;179 }180 );181182 183 184 185 const reg = (globalThis as any).__peachRegisterMessageHandler;186 if (typeof reg === "function") {187 reg(async (m: unknown) => {188 if (!isMessage(m)) return { ok: false, error: "invalid_message" };189 try {190 return await handleMessage(m);191 } catch (err) {192 return {193 ok: false,194 error: err instanceof Error ? err.message : String(err),195 };196 }197 });198 }199 if (isNativePlatform()) {200 void bootstrapNativeSyncRunner();201 }202}203204async function bootstrapNativeSyncRunner(): Promise<void> {205 if (nativeSyncRunnerBootstrapped) return;206 nativeSyncRunnerBootstrapped = true;207 try {208 const { setSyncRunner, hydratePersistedSyncState } = await import("./sync-trigger");209 const { productionSyncRunner } = await import("./sync-runner");210 setSyncRunner(productionSyncRunner);211 await hydratePersistedSyncState();212 } catch (err) {213 nativeSyncRunnerBootstrapped = false;214 console.warn("[Peach] failed to bootstrap native sync runner:", err);215 }216}217218function isMessage(m: unknown): m is Message {219 return typeof m === "object" && m !== null && "type" in m;220}221222223224225226227228229230function attestedSenderOrigin(sender: Partial<chrome.runtime.MessageSender>): string | undefined {231 if (sender.origin) return sender.origin;232 if (sender.url) {233 try { return new URL(sender.url).origin; } catch { return undefined; }234 }235 return undefined;236}237238239240241242let restoreReady: Promise<void> = Promise.resolve();243244245export function setRestoreReady(p: Promise<unknown>): void {246 restoreReady = p.then(() => undefined, () => undefined);247}248249export async function handleMessage(250 msg: Message,251 sender: Partial<chrome.runtime.MessageSender> = {}252): Promise<MessageResponse> {253 try {254 255 256 257 258 259 260 await restoreReady;261 262 263 264 265 if (266 sender.id !== undefined &&267 sender.id !== chrome.runtime.id268 ) {269 return { ok: false, error: "invalid_sender" };270 }271272 273 274 275 276 277 278 if (DESTRUCTIVE_MESSAGE_TYPES.has(msg.type)) {279 const extensionOrigin = chrome.runtime.getURL("");280 if (!sender.url?.startsWith(extensionOrigin)) {281 return { ok: false, error: "invalid_sender" };282 }283 }284285 if (shouldRefreshAutoLock(msg.type) && vault.isUnlocked()) {286 touchAutoLock();287 }288289 switch (msg.type) {290 case "CREATE_VAULT": {291 const { password } = msg.payload as { password: string };292 293 294 295 296 if (typeof password !== "string" || password.length === 0) {297 return { ok: false, error: "invalid_payload" };298 }299 await vault.createVault(password);300 await setupAutoLock();301 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });302 onVaultUnlocked();303 return { ok: true };304 }305 case "CREATE_VAULT_FROM_ROOT_KEY": {306 const { password, rootKeyHex } = (msg.payload ?? {}) as {307 password?: unknown;308 rootKeyHex?: unknown;309 };310 if (typeof password !== "string" || password.length === 0 || typeof rootKeyHex !== "string") {311 return { ok: false, error: "invalid_payload" };312 }313314 try {315 const { vaultId } = await vault.createVaultFromRootKey(password, rootKeyHex);316 await setupAutoLock();317 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });318 onVaultUnlocked();319 return { ok: true, vaultId };320 } catch (err) {321 return { ok: false, error: err instanceof Error ? err.message : String(err) };322 }323 }324 case "UNLOCK": {325 const { password } = msg.payload as { password: string };326 const ok = await vault.unlock(password);327 if (ok) {328 329 330 const kat = await runSelfTest();331 if (!kat.ok) {332 console.error("[Peach] Crypto self-test FAILED:", kat.error);333 await vault.lock();334 return { ok: false, error: "self_test_failed" };335 }336 await setupAutoLock();337 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });338 onVaultUnlocked();339 if (isNativePlatform()) await publishInMemoryVault(vault.getEntries());340 return { ok: true };341 }342 343 344 345 const failure = vault.consumeLastUnlockFailureReason();346 if (failure) {347 return {348 ok: false,349 error: "vault_corrupted",350 canRestoreFromHistory: true,351 } as MessageResponse;352 }353 return { ok: false };354 }355 case "VALIDATE_RECOVERY_WORDS": {356 const { words } = msg.payload as { words: string[] };357 const invalid = findInvalidWords(words);358 return { ok: true, data: { valid: invalid.length === 0, invalid } };359 }360 case "GET_RECOVERY_WORDS": {361 362 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };363 364 365 366 367 368 369 370 371 if (!(await isRecoveryConfirmPending())) {372 const { password } = (msg.payload ?? {}) as { password?: string };373 if (typeof password !== "string" || !password) {374 return { ok: false, error: "password_required" };375 }376 if (!(await vault.verifyMainPassword(password))) {377 return { ok: false, error: "wrong_password" };378 }379 }380 try {381 const words = await vault.getRecoveryWords();382 return { ok: true, data: words };383 } catch (e) {384 385 return { ok: false, error: "recovery_not_setup" };386 }387 }388 case "RECOVER_WITH_WORDS": {389 390 391 const { words, newPassword } = msg.payload as {392 words: string[];393 newPassword: string;394 };395 const stored = await vault.loadSlotA();396 if (!stored) return { ok: false, error: "no_vault" };397 if (!stored.recovery_salt || !stored.recovery_wrapped_root_key) {398 return { ok: false, error: "recovery_not_setup" };399 }400 401 try {402 await vault.unwrapRootKeyForRecovery(403 stored.recovery_wrapped_root_key,404 stored.recovery_nonce ?? "",405 stored.recovery_tag ?? "",406 stored.recovery_salt,407 words408 );409 } catch {410 return { ok: false, error: "invalid_recovery_words" };411 }412 413 const newSalt = vault.generateSalt();414 const newKey = await vault.deriveKey(newPassword, newSalt);415 416 417 await vault.rewrapRootKeyForPassword(newKey, newSalt);418 419 await vault.changePasswordWithNewKey(newSalt, newKey);420 await setupAutoLock();421 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });422 return { ok: true };423 }424 case "LOCK":425 vault.lock();426 if (isNativePlatform()) await clearInMemoryVault();427 return { ok: true };428 case "IS_UNLOCKED":429 return { ok: true, data: vault.isUnlocked() };430 case "GET_VAULT_STATUS":431 return { ok: true, unlocked: vault.isUnlocked() };432 case "HAS_VAULT":433 return { ok: true, data: await vault.hasVault() };434 case "GET_ENTRIES":435 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };436 return { ok: true, data: vault.getEntries() };437 case "ADD_ENTRY": {438 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };439 const payload = msg.payload;440 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {441 return { ok: false, error: "invalid_payload" };442 }443 const entry = await vault.addEntry(payload as Parameters<typeof vault.addEntry>[0]);444 return { ok: true, data: entry };445 }446 case "BATCH_ADD_ENTRIES": {447 const payload = msg.payload as { entries?: unknown } | null | undefined;448 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {449 return { ok: false, error: "invalid_payload" };450 }451 if (!Array.isArray(payload.entries)) {452 return { ok: false, error: "invalid_payload" };453 }454 const added = await vault.batchAddEntries(payload.entries as Parameters<typeof vault.batchAddEntries>[0]);455 return { ok: true, data: added };456 }457 case "RESTORE_ENTRIES": {458 const payload = msg.payload as459 | { entries?: unknown; mode?: unknown; settings?: unknown }460 | null461 | undefined;462 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {463 return { ok: false, error: "invalid_payload" };464 }465 if (!Array.isArray(payload.entries)) {466 return { ok: false, error: "invalid_payload" };467 }468 if (payload.mode !== "replace" && payload.mode !== "merge") {469 return { ok: false, error: "invalid_payload" };470 }471 const summary = await vault.restoreEntries(472 payload.entries as Parameters<typeof vault.restoreEntries>[0],473 payload.mode474 );475 if (payload.settings && typeof payload.settings === "object" && !Array.isArray(payload.settings)) {476 await vault.updateSettings(payload.settings as vault.VaultSettings);477 }478 return { ok: true, data: summary };479 }480 case "CLEAR_VAULT_ENTRIES": {481 await vault.clearVaultEntries("REPLACE_ALL");482 return { ok: true };483 }484 case "GET_VAULT_HISTORY": {485 const snapshots = await vault.getVaultHistory();486 return { ok: true, data: snapshots };487 }488 case "RESTORE_VAULT_SNAPSHOT": {489 const { snapshotId } = msg.payload as { snapshotId: string };490 const result = await vault.restoreVaultSnapshot(snapshotId);491 if (!result.ok) return { ok: false, error: result.error };492 broadcastToContentScripts({ type: "VAULT_LOCKED" });493 return { ok: true };494 }495 case "COUNT_DUPLICATES": {496 return { ok: true, data: vault.countDuplicates() };497 }498 case "DEDUPLICATE_ENTRIES": {499 const removed = await vault.deduplicateEntries();500 return { ok: true, data: removed };501 }502 case "UPDATE_ENTRY": {503 const payload = msg.payload as { id?: unknown; patch?: unknown } | null | undefined;504 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {505 return { ok: false, error: "invalid_payload" };506 }507 const { id, patch } = payload;508 if (typeof id !== "string" || !id || !patch || typeof patch !== "object" || Array.isArray(patch)) {509 return { ok: false, error: "invalid_payload" };510 }511 512 513 514 515 const incomingCanary = (patch as { canary?: unknown }).canary;516 if (incomingCanary && typeof incomingCanary === "object") {517 const existing = vault.getEntryById(id);518 if (!existing?.canary) {519 const gate = await requirePro("canary_entry");520 if (!gate.allowed) return { ok: false, error: "pro_required" };521 }522 }523 await vault.updateEntry(id, patch as Parameters<typeof vault.updateEntry>[1]);524 return { ok: true };525 }526 case "ADD_ADDITIONAL_URL": {527 const { id, url } = msg.payload as { id: string; url: string };528 const trimmed = typeof url === "string" ? url.trim() : "";529 if (!trimmed) return { ok: false, error: "url_required" };530 const existing = vault.getEntryById(id);531 if (!existing) return { ok: false, error: "entry_not_found" };532 if (existing.type !== "login") return { ok: false, error: "login_entries_only" };533 const merged = [...(existing.additionalUrls ?? []), trimmed];534 await vault.updateEntry(id, { additionalUrls: merged });535 return { ok: true };536 }537 case "DELETE_ENTRY": {538 const { id } = msg.payload as { id: string };539 await vault.deleteEntry(id);540 return { ok: true };541 }542 case "GET_DELETED_ENTRIES":543 return { ok: true, data: vault.getDeletedEntries() };544 case "RESTORE_ENTRY": {545 const { id } = msg.payload as { id: string };546 await vault.restoreEntry(id);547 return { ok: true };548 }549 case "PURGE_ENTRY": {550 const { id } = msg.payload as { id: string };551 await vault.purgeEntry(id);552 return { ok: true };553 }554 case "PURGE_EXPIRED_TRASH": {555 const { retentionMs } = msg.payload as { retentionMs: number };556 const purged = await vault.purgeExpiredTrash(retentionMs);557 return { ok: true, data: purged };558 }559 case "SAVE_NOTE": {560 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };561 const { id, title, body } = msg.payload as { id?: string; title: string; body: string };562 const now = Date.now();563 if (id) {564 const existing = vault.getEntryById(id);565 if (!existing) return { ok: false, error: "not_found" };566 await vault.updateEntry(id, { title: title || "Untitled", notes: body, modifiedAt: now });567 return { ok: true, data: vault.getEntryById(id) };568 } else {569 570 571 572 const entry = await vault.addEntry({573 type: "note",574 title: title || "Untitled",575 notes: body,576 });577 return { ok: true, data: entry };578 }579 }580 case "DELETE_NOTE": {581 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };582 const { id } = msg.payload as { id: string };583 await vault.deleteEntry(id);584 return { ok: true };585 }586 case "GET_NOTES": {587 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };588 const notes = vault.getEntries().filter(589 (e) => e.type === "note" && !e.deletedAt590 );591 return { ok: true, data: notes } as MessageResponse;592 }593 case "SEARCH_VAULT": {594 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };595 596 597 const { query } = (msg.payload ?? {}) as { query?: unknown };598 if (typeof query !== "string") {599 return { ok: false, error: "invalid_payload" };600 }601 const q = query.toLowerCase();602 const searchResults = vault.getEntries().filter(603 (e) =>604 !e.deletedAt &&605 (e.title.toLowerCase().includes(q) ||606 e.notes?.toLowerCase().includes(q) ||607 e.username?.toLowerCase().includes(q))608 );609 return { ok: true, data: searchResults } as MessageResponse;610 }611 case "AUTOFILL_QUERY": {612 if (!vault.isUnlocked()) {613 return { ok: false, error: "vault_locked" };614 }615 let { includeAll } = msg.payload as { includeAll?: boolean };616 const { domain, kind, userGesture } = msg.payload as {617 domain: string;618 kind?: "login" | "card" | "totp";619 userGesture?: boolean;620 };621 622 623 624 625 626 627 628 if (includeAll && userGesture !== true) {629 includeAll = false;630 }631 632 633 634 635 if (kind === "card") {636 const cards = vault.getEntries().filter((e) => e.type === "card");637 return {638 ok: true,639 data: cards.map((e) => ({640 id: e.id,641 title: e.cardNickname || e.title,642 cardholderName: e.cardholderName,643 last4: e.last4,644 expMonth: e.expMonth,645 expYear: e.expYear,646 network: e.network,647 })),648 };649 }650 if (kind === "totp") {651 const matches = (652 includeAll653 ? vault.getEntries().filter((entry) => entry.type !== "note" && entry.type !== "card")654 : vault.queryDomain(domain)655 ).filter((entry) => !isPasskeyOnly(entry) && Boolean(entry.totp));656 return {657 ok: true,658 data: matches.map((e) => ({659 id: e.id,660 title: e.title,661 username: e.username,662 email: e.email,663 url: e.url,664 hasTotp: true,665 })),666 };667 }668 669 670 671 672 673 const matches = (674 includeAll675 ? vault676 .getEntries()677 .filter((entry) => entry.type !== "note" && entry.type !== "card")678 : vault.queryDomain(domain)679 ).filter((entry) => !isPasskeyOnly(entry));680 return {681 ok: true,682 data: matches.map((e) =>683 includeAll684 ? {685 id: e.id,686 title: e.title,687 688 689 690 691 692 username: e.username,693 email: e.email,694 url: e.url,695 hasPasskey: e.passkey !== undefined,696 }697 : {698 id: e.id,699 title: e.title,700 username: e.username,701 email: e.email,702 url: e.url,703 hasTotp: Boolean(e.totp),704 hasPasskey: e.passkey !== undefined,705 }706 ),707 };708 }709 case "GET_ENTRY_FOR_FILL": {710 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };711 const { id, explicitSummon } = msg.payload as {712 id: string;713 explicitSummon?: boolean;714 };715 const entry = vault.getEntryById(id);716 if (!entry) return { ok: false, error: "entry_not_found" };717 718 719 720 if (isPasskeyOnly(entry)) return { ok: false, error: "passkey_not_fillable" };721722 723 724 725 726 if (sender.tab !== undefined || sender.url !== undefined) {727 let senderDomain: string | null = null;728 try {729 const rawUrl = sender.tab?.url ?? sender.url ?? "";730 senderDomain = rawUrl ? extractDomain(new URL(rawUrl).hostname) : null;731 } catch {732 senderDomain = null;733 }734 if (!senderDomain) return { ok: false, error: "origin_unverified" };735736 const entryDomains = new Set<string>();737 const primary = extractDomain(entry.url ?? "");738 if (primary) entryDomains.add(primary);739 for (const extra of entry.additionalUrls ?? []) {740 const d = extractDomain(extra);741 if (d) entryDomains.add(d);742 }743744 745 746 747 if (explicitSummon !== true && !entryDomains.has(senderDomain)) {748 749 return { ok: false, error: "entry_not_found" };750 }751 }752 753 754 755 756 757 758 759 760 761 762 const {763 passkey: _strippedPasskey,764 totp: seed,765 totpAlgorithm,766 totpDigits,767 totpPeriod,768 passwordHistory: _ph,769 ...safeEntry770 } = entry;771 let totpCode: string | undefined;772 let totpRemaining: number | undefined;773 if (seed) {774 try {775 776 777 const generated = generateTOTP(seed, totpAlgorithm, totpDigits, totpPeriod);778 totpCode = generated.code;779 totpRemaining = generated.remaining;780 } catch {781 totpCode = undefined;782 }783 }784 return { ok: true, data: { ...safeEntry, totpCode, totpRemaining } };785 }786 787 788 789 790 791 792 case "GET_ENTRY_FOR_EDIT": {793 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };794 const { id, explicitSummon } = msg.payload as {795 id: string;796 explicitSummon?: boolean;797 };798 const entry = vault.getEntryById(id);799 if (!entry) return { ok: false, error: "entry_not_found" };800801 802 803 804 805 806 807 808 809 if (sender.tab !== undefined || sender.url !== undefined) {810 let senderDomain: string | null = null;811 try {812 const rawUrl = sender.tab?.url ?? sender.url ?? "";813 senderDomain = rawUrl ? extractDomain(new URL(rawUrl).hostname) : null;814 } catch {815 senderDomain = null;816 }817 if (senderDomain) {818 const entryDomains = new Set<string>();819 const primary = extractDomain(entry.url ?? "");820 if (primary) entryDomains.add(primary);821 for (const extra of entry.additionalUrls ?? []) {822 const d = extractDomain(extra);823 if (d) entryDomains.add(d);824 }825 if (explicitSummon !== true && !entryDomains.has(senderDomain)) {826 return { ok: false, error: "entry_not_found" }; 827 }828 }829 }830831 const { passkey: _passkey, ...safeEntry } = entry;832 return { ok: true, data: safeEntry };833 }834 case "COPY": {835 836 837 838 const { text, label, alreadyWritten } = (msg.payload ?? {}) as {839 text?: unknown;840 label?: unknown;841 alreadyWritten?: unknown;842 };843 if (typeof text !== "string" || typeof label !== "string") {844 return { ok: false, error: "invalid_payload" };845 }846 await copyToClipboard(text, label, Boolean(alreadyWritten));847 return { ok: true, clearSeconds: getEffectiveClearSeconds() };848 }849 case "CHECK_ALL_BREACHES": {850 const allEntries = vault.getEntries();851 const results: Record<string, number> = {};852 for (const entry of allEntries) {853 if (entry.type === "note") continue;854 if (!entry.password) continue;855 results[entry.id] = await vault.checkBreach(entry.password);856 857 await new Promise((r) => setTimeout(r, 200));858 }859 860 for (const [id, count] of Object.entries(results)) {861 const entry = vault.getEntryById(id);862 if (entry && (entry.breach ?? 0) !== count) {863 await vault.updateEntry(id, { breach: count > 0 ? count : undefined });864 }865 }866 return { ok: true, data: results };867 }868 case "GET_SETTINGS":869 return { ok: true, data: await vault.ensureSettingsLoaded() };870 case "UPDATE_SETTINGS": {871 const patch = msg.payload;872 if (!patch || typeof patch !== "object" || Array.isArray(patch)) {873 return { ok: false, error: "invalid_payload" };874 }875 await vault.updateSettings(patch as vault.VaultSettings);876 if (typeof (patch as Record<string, unknown>).autoLockMinutes === "number") {877 await setupAutoLock();878 if (vault.isUnlocked()) touchAutoLock();879 }880 return { ok: true };881 }882 case "GET_PENDING_PASSKEY":883 return { ok: true, data: await getPendingPasskeyRequest() };884 case "APPROVE_PASSKEY": {885 const { requestId } = msg.payload as { requestId: number };886 await resolvePendingPasskeyRequest(requestId, true);887 return { ok: true };888 }889 case "DENY_PASSKEY": {890 const { requestId } = msg.payload as { requestId: number };891 await resolvePendingPasskeyRequest(requestId, false);892 return { ok: true };893 }894 case "GET_PASSKEY_PROVIDER_STATE":895 return { ok: true, data: await getPasskeyProviderState() };896 case "SET_PASSKEY_PROVIDER_ENABLED": {897 const { enabled } = msg.payload as { enabled: boolean };898 899 900 if (enabled) {901 const gate = await requirePro("passkeys");902 if (!gate.allowed) return { ok: false, error: "pro_required" };903 }904 return { ok: true, data: await setPasskeyProviderEnabled(enabled) };905 }906 907 908 909 910 911 912 case "FIRE_CANARY_PING": {913 914 915 916 917 const { webhookUrl, entryTitle, trigger } = msg.payload as {918 webhookUrl: string;919 entryTitle: string;920 trigger: "password_copied" | "username_copied" | "autofilled" | "submitted" | "test";921 };922 if (!isAllowedCanaryWebhookUrl(webhookUrl)) {923 return { ok: false, error: "invalid_webhook_url" };924 }925 926 927 928 929 930 931 932 if (trigger !== "test") {933 const rlKey = `canary-last-fetch:${webhookUrl}:${entryTitle}`;934 const stored = await chrome.storage.local.get(rlKey);935 const last = (stored as Record<string, unknown>)[rlKey];936 if (typeof last === "number" && Date.now() - last < CANARY_MIN_INTERVAL_MS) {937 return { ok: true, data: { rateLimited: true } };938 }939 await chrome.storage.local.set({ [rlKey]: Date.now() });940 }941 const body = JSON.stringify({942 event: trigger === "test" ? "peach_canary_test" : "peach_canary_accessed",943 entry: entryTitle,944 trigger,945 timestamp: new Date().toISOString(),946 source: "peach-extension",947 });948 if (trigger === "test") {949 950 951 952 953 954 try {955 const res = await fetch(webhookUrl, {956 method: "POST",957 headers: { "Content-Type": "application/json" },958 body,959 });960 return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };961 } catch (err) {962 return { ok: false, error: err instanceof Error ? err.message : "network_error" };963 }964 }965 966 fetch(webhookUrl, {967 method: "POST",968 headers: { "Content-Type": "application/json" },969 body,970 }).catch(() => {});971 return { ok: true };972 }973 case "ACTIVATE_LICENSE": {974 const { key } = msg.payload as { key: string };975 const licenseInfo = await activateLicense(key);976 return { ok: true, data: licenseInfo };977 }978 case "GET_LICENSE": {979 const licenseInfo = await getLicenseInfo();980 return { ok: true, data: licenseInfo };981 }982 case "GET_LICENSE_KEY": {983 const key = await getLicenseKey();984 return { ok: true, data: key };985 }986 case "DEACTIVATE_LICENSE": {987 await deactivateLicense();988 return { ok: true };989 }990 case "SHOULD_PLAY_CELEBRATION": {991 const should = await shouldPlayCelebration();992 return { ok: true, data: should };993 }994 case "MARK_CELEBRATION_PLAYED": {995 await markCelebrationPlayed();996 return { ok: true };997 }998 case "GET_CODEX_VAULT_DATA": {999 1000 1001 const gate = await requirePro("codex");1002 if (!gate.allowed) return { ok: false, error: "pro_required" };1003 return await handleGetCodexVaultData();1004 }1005 case "ESTIMATE_CODEX_PAGES": {1006 const gate = await requirePro("codex");1007 if (!gate.allowed) return { ok: false, error: "pro_required" };1008 return await handleEstimateCodexPages();1009 }1010 case "RESTORE_FROM_CODEX": {1011 1012 1013 1014 const payload = msg.payload as CodexRestorePayload;1015 return await handleRestoreFromCodex(payload);1016 }1017 case "OPEN_PRO_GATE": {1018 1019 const { feature } = (msg.payload ?? {}) as { feature?: string };1020 if (chrome.storage.session) {1021 await chrome.storage.session.set({ pendingProGate: { feature: feature ?? "passkeys" } });1022 }1023 try { await chrome.action.openPopup(); } catch { }1024 return { ok: true };1025 }1026 case "OPEN_POPUP": {1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 if (chrome.storage.session) {1037 await chrome.storage.session.set({ peach_inline_unlock: true });1038 }1039 try { await chrome.action.openPopup(); } catch { }1040 return { ok: true };1041 }1042 case "GET_IDENTITIES": {1043 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1044 return { ok: true, data: vault.getIdentities() };1045 }1046 case "TOUCH_AUTO_LOCK": {1047 1048 1049 return { ok: true };1050 }1051 case "GET_ADVANCED_HEALTH_REPORT": {1052 const gate = await requirePro("advanced_health");1053 if (!gate.allowed) return { ok: false, error: "pro_required" };1054 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1055 const report = await buildAdvancedHealthReport(vault.getEntries());1056 return { ok: true, data: report };1057 }1058 case "IS_DURESS_ENABLED": {1059 const enabled = await vault.isDuressEnabled();1060 return { ok: true, data: enabled };1061 }1062 case "SETUP_DURESS": {1063 1064 1065 1066 1067 const gate = await requirePro("duress_vault");1068 if (!gate.allowed) return { ok: false, error: "pro_required" };1069 1070 1071 1072 if (!vault.isUnlocked()) return { ok: false, error: "session_expired" };1073 const { duressPassword } = msg.payload as { duressPassword: string };1074 await vault.setupDuress(duressPassword);1075 return { ok: true };1076 }1077 case "DISABLE_DURESS": {1078 await vault.disableDuress();1079 return { ok: true };1080 }1081 case "VERIFY_MAIN_PASSWORD": {1082 const { password } = msg.payload as { password: string };1083 const ok = await vault.verifyMainPassword(password);1084 return { ok: true, data: ok };1085 }1086 case "CHANGE_PASSWORD": {1087 const { currentPassword, newPassword } = msg.payload as {1088 currentPassword: string;1089 newPassword: string;1090 };1091 const changed = await vault.changePassword(currentPassword, newPassword);1092 return changed1093 ? { ok: true }1094 : { ok: false, error: "incorrect_current_password" };1095 }1096 case "GET_SITE_PASSWORD_POLICY": {1097 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });1098 if (!tab?.id) return { ok: true, data: null };1099 try {1100 const response = await chrome.tabs.sendMessage(tab.id, {1101 type: "GET_SITE_PASSWORD_POLICY",1102 });1103 return (response as MessageResponse) ?? { ok: true, data: null };1104 } catch {1105 return { ok: true, data: null };1106 }1107 }1108 case "RECORD_MANUAL_SUMMON": {1109 const { domain } = msg.payload as { domain: string };1110 const settings = vault.getSettings();1111 const existing = settings.manualSummonDomains?.[domain];1112 const now = Date.now();1113 await vault.updateSettings({1114 manualSummonDomains: {1115 ...settings.manualSummonDomains,1116 [domain]: {1117 firstSummonedAt: existing?.firstSummonedAt ?? now,1118 summonCount: (existing?.summonCount ?? 0) + 1,1119 lastSummonedAt: now,1120 },1121 },1122 });1123 return { ok: true };1124 }1125 case "DECLINE_CARD_SAVE": {1126 const { last4, expMonth, expYear } = msg.payload as { last4: string; expMonth: string; expYear: string };1127 await vault.declineCardSaveOffer(last4, expMonth, expYear);1128 return { ok: true };1129 }1130 case "IS_CARD_DECLINED": {1131 const { last4, expMonth, expYear } = msg.payload as { last4: string; expMonth: string; expYear: string };1132 const declined = await vault.isCardDeclined(last4, expMonth, expYear);1133 return { ok: true, data: declined };1134 }1135 case "INITIATE_FIREFOX_PASSKEY_CREATE": {1136 const { rpId, rpName, userName, userDisplayName } = (msg.payload ?? {}) as {1137 rpId?: string;1138 rpName?: string;1139 userName?: string;1140 userDisplayName?: string;1141 };1142 if (!rpId || !userName) return { ok: false, error: "rpid_and_username_required" };1143 return await initiateFirefoxPasskeyCreate({1144 rpId,1145 rpName: rpName ?? rpId,1146 userName,1147 userDisplayName,1148 });1149 }11501151 case "COMPLETE_FIREFOX_PASSKEY_CREATE": {1152 const payload = msg.payload as Parameters<typeof completeFirefoxPasskeyCreate>[0] | undefined;1153 if (!payload) return { ok: false, error: "missing_payload" };1154 return await completeFirefoxPasskeyCreate(payload);1155 }11561157 case "PASSKEY_OFFER_DECLINE": {1158 const { domain } = (msg.payload ?? {}) as { domain?: string };1159 if (!domain) return { ok: false, error: "domain_required" };1160 await recordPasskeyOfferDecline(domain);1161 return { ok: true };1162 }11631164 case "GET_PASSKEY_OFFER_DECLINED": {1165 const { domain } = (msg.payload ?? {}) as { domain?: string };1166 if (!domain) return { ok: false, error: "domain_required" };1167 return { ok: true, data: await isPasskeyOfferDeclined(domain) };1168 }11691170 case "PASSKEY_HAS_CREDENTIALS": {1171 const payload = msg.payload as {1172 rpId?: string;1173 allowCredentials?: Array<{ id: string; type?: string }>;1174 } | undefined;1175 if (!payload?.rpId) return { ok: false, error: "rpid_required" };1176 return await handlePasskeyHasCredentials({1177 rpId: payload.rpId,1178 allowCredentials: payload.allowCredentials ?? [],1179 }, attestedSenderOrigin(sender));1180 }11811182 case "PASSKEY_CREATE": {1183 const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyCreate>[0] | undefined;1184 if (!payload?.rpId || !payload?.userName) {1185 return { ok: false, error: "rpid_and_username_required" };1186 }1187 return await handleFirefoxPasskeyCreate(payload, attestedSenderOrigin(sender));1188 }11891190 case "PASSKEY_GET": {1191 const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyGet>[0] | undefined;1192 if (!payload?.rpId) return { ok: false, error: "rpid_required" };1193 return await handleFirefoxPasskeyGet(payload, attestedSenderOrigin(sender));1194 }11951196 case "PASSKEY_AUTH_RESPONSE": {1197 const { requestId, approved } = (msg.payload ?? {}) as {1198 requestId?: string | number;1199 approved?: boolean;1200 };1201 if (requestId === undefined || requestId === null || typeof approved !== 'boolean') {1202 return { ok: false, error: 'requestId and approved required' };1203 }1204 await resolvePendingPasskeyRequest(Number(requestId), approved);1205 return { ok: true };1206 }12071208 case "GET_PASSKEY_FOR_DOMAIN": {1209 const { domain } = (msg.payload ?? {}) as { domain?: string };1210 if (!domain) return { ok: false, error: "domain_required" };1211 1212 1213 1214 const origin = attestedSenderOrigin(sender);1215 if (!origin || !isValidRpId(domain, origin)) return { ok: true, data: null };1216 if (!vault.isUnlocked()) return { ok: true, data: null };1217 const match = vault1218 .getEntries()1219 .find((entry) => entry.passkey?.rpId === domain);1220 if (!match || !match.passkey) return { ok: true, data: null };1221 1222 1223 return {1224 ok: true,1225 data: {1226 rpId: match.passkey.rpId,1227 userName: match.passkey.userName,1228 browser: match.passkey.browser,1229 },1230 };1231 }12321233 case "DESTROY_VAULT": {1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 const preserved = await chrome.storage.local.get([1245 "peach_license",1246 "peach_pro_celebrated",1247 ]);1248 vault.lock();1249 await storageClear();1250 await vault.clearVaultHistory();1251 const toRestore: Record<string, unknown> = {};1252 if (preserved.peach_license !== undefined) toRestore.peach_license = preserved.peach_license;1253 if (preserved.peach_pro_celebrated !== undefined) toRestore.peach_pro_celebrated = preserved.peach_pro_celebrated;1254 if (Object.keys(toRestore).length > 0) {1255 await chrome.storage.local.set(toRestore);1256 }1257 if (chrome.storage.session) {1258 await chrome.storage.session.clear();1259 }1260 return { ok: true };1261 }12621263 case "SUBMIT_FEEDBACK": {1264 const { category, message, version, browser } = msg.payload as {1265 category: string;1266 message: string;1267 version: string;1268 browser: string;1269 };1270 if (!message.trim()) return { ok: false, error: "message_empty" };1271 if (!FEEDBACK_ENDPOINT) return { ok: true };1272 if (!isAllowedCanaryWebhookUrl(FEEDBACK_ENDPOINT)) {1273 return { ok: false, error: "invalid_feedback_endpoint" };1274 }1275 try {1276 const res = await fetch(FEEDBACK_ENDPOINT, {1277 method: "POST",1278 headers: { "Content-Type": "application/json" },1279 body: JSON.stringify({ category, message, version, browser, source: "peach-extension" }),1280 });1281 return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };1282 } catch (err) {1283 return { ok: false, error: err instanceof Error ? err.message : "network_error" };1284 }1285 }1286 1287 case "UNLOCK_WITH_KEY": {1288 const { masterKeyHex } = msg.payload as { masterKeyHex: string };1289 if (!isNativePlatform()) {1290 return { ok: false, error: "desktop_only_uses_password" };1291 }1292 try {1293 const result = await vault.unlockWithKey(masterKeyHex);1294 if (result.ok) {1295 onVaultUnlocked();1296 await publishInMemoryVault(result.entries);1297 }1298 return { ok: result.ok, data: result.ok ? { entries: result.entries } : undefined, error: result.ok ? undefined : result.error };1299 } catch {1300 return { ok: false, error: "invalid_key" };1301 }1302 }1303 1304 case "GET_DERIVED_KEY": {1305 if (!isNativePlatform()) {1306 return { ok: false, error: "android_only" };1307 }1308 if (!vault.isUnlocked()) {1309 return { ok: false, error: "vault_locked" };1310 }1311 return { ok: true, data: { masterKeyHex: vault.getDerivedKeyHex() } };1312 }1313 case "GET_IDENTITY_SUGGESTIONS": {1314 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1315 const entries = vault.getEntries();1316 const suggestions = buildIdentitySuggestions(entries);1317 return { ok: true, data: { suggestions } };1318 }1319 case "POPULATE_INLINE_VAULT": {1320 if (!isNativePlatform()) return { ok: false };1321 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1322 const entries = vault.getEntries();1323 await publishInMemoryVault(entries);1324 return { ok: true };1325 }1326 case "ML_INIT":1327 return { ok: true };13281329 case "ML_PREDICT_BATCH": {1330 1331 const { featuresArray } = (msg.payload ?? {}) as { featuresArray?: unknown };1332 if (!Array.isArray(featuresArray)) {1333 return { ok: false, error: "invalid_payload" };1334 }1335 return { ok: true, data: mlPredictBatch(featuresArray as MLFieldFeatures[]) };1336 }13371338 1339 1340 1341 1342 case "GET_LOCAL_IDENTITY": {1343 1344 1345 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1346 1347 1348 const masterKeyHex = vault.getRootKeyHex();1349 if (!masterKeyHex) return { ok: false, error: "vault_locked" };1350 const masterKey = unhexBytes(masterKeyHex);1351 try {1352 const identity = await buildLocalIdentity(masterKey);1353 1354 1355 if (await isProUser()) {1356 const license = await getLicenseKey();1357 if (license) (identity as { license?: string }).license = license;1358 }1359 return { ok: true, data: identity };1360 } finally {1361 masterKey.fill(0);1362 }1363 }13641365 case "GET_ADOPTION_MATERIAL": {1366 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1367 const rootKeyHex = vault.getRootKeyHex();1368 if (!rootKeyHex) return { ok: false, error: "vault_locked" };1369 const rootKeyBytes = unhexBytes(rootKeyHex);1370 try {1371 const data: { rootKeyHex: string; vaultId: string; license?: string } = {1372 rootKeyHex,1373 vaultId: await computeVaultId(rootKeyBytes),1374 };1375 if (await isProUser()) {1376 const license = await getLicenseKey();1377 if (license) data.license = license;1378 }1379 return { ok: true, data };1380 } finally {1381 rootKeyBytes.fill(0);1382 }1383 }13841385 case "COMPLETE_PAIRING": {1386 1387 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1388 const paired = (msg.payload as { paired?: unknown })?.paired;1389 if (typeof paired !== "object" || paired === null) {1390 return { ok: false, error: "invalid_payload" };1391 }1392 const candidate = paired as Record<string, unknown>;1393 if (1394 typeof candidate.deviceId !== "string" || !candidate.deviceId ||1395 typeof candidate.deviceName !== "string" || !candidate.deviceName ||1396 typeof candidate.deviceType !== "string" ||1397 typeof candidate.longTermPublicKeyHex !== "string" ||1398 !/^[0-9a-f]{64}$/.test(candidate.longTermPublicKeyHex) ||1399 (1400 candidate.signingPublicKeyHex !== undefined &&1401 (1402 typeof candidate.signingPublicKeyHex !== "string" ||1403 !/^[0-9a-f]{64}$/.test(candidate.signingPublicKeyHex)1404 )1405 ) ||1406 typeof candidate.pairedAt !== "number"1407 ) {1408 return { ok: false, error: "invalid_paired_device" };1409 }1410 await upsertPairedDeviceV3(candidate as unknown as import("../shared/sync-types").PairedDevice);1411 return { ok: true };1412 }14131414 case "LIST_PAIRED_DEVICES": {1415 const devices = await listPairedDevicesV3();1416 return { ok: true, data: { devices } };1417 }14181419 case "UNPAIR_DEVICE": {1420 const { deviceId } = (msg.payload ?? {}) as { deviceId?: string };1421 if (typeof deviceId !== "string" || !deviceId) {1422 return { ok: false, error: "invalid_payload" };1423 }1424 1425 1426 1427 await revokeDeviceByIdV3(deviceId, await getOrCreateDeviceId());1428 return { ok: true };1429 }14301431 case "RENAME_LOCAL_DEVICE": {1432 const { name } = (msg.payload ?? {}) as { name?: string };1433 if (typeof name !== "string") return { ok: false, error: "invalid_payload" };1434 await setDeviceNameV3(name);1435 return { ok: true };1436 }14371438 1439 1440 1441 1442 case "PAIR_WITH_CODE": {1443 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1444 const { code } = (msg.payload ?? {}) as { code?: string };1445 const { parsePairingCode } = await import("../shared/pairing-code");1446 if (typeof code !== "string" || parsePairingCode(code) === null) {1447 return { ok: false, error: "invalid_payload" };1448 }1449 const { startCodePairing, isCodePairingInFlight } = await import(1450 "./code-pairing-bg"1451 );1452 if (isCodePairingInFlight()) return { ok: false, error: "pairing_in_progress" };1453 startCodePairing(code);1454 return { ok: true };1455 }14561457 case "CODE_PAIR_CONFIRM": {1458 const { confirmed } = (msg.payload ?? {}) as { confirmed?: boolean };1459 const { resolveCodePairingConfirm } = await import("./code-pairing-bg");1460 resolveCodePairingConfirm(confirmed === true);1461 return { ok: true };1462 }14631464 case "START_ADOPTION_SOURCE": {1465 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1466 const {1467 isRelayAdoptionSourceInFlight,1468 startRelayAdoptionSource,1469 } = await import("./relay-adoption-source-bg");1470 if (isRelayAdoptionSourceInFlight()) {1471 return { ok: false, error: "adoption_source_in_progress" };1472 }1473 try {1474 return { ok: true, data: await startRelayAdoptionSource() };1475 } catch (error) {1476 return {1477 ok: false,1478 error: error instanceof Error ? error.message : "adoption_source_failed",1479 };1480 }1481 }14821483 case "ADOPTION_SOURCE_CONFIRM": {1484 const { password, cancel } = (msg.payload ?? {}) as {1485 password?: unknown;1486 cancel?: unknown;1487 };1488 const { confirmRelayAdoptionSource } = await import(1489 "./relay-adoption-source-bg"1490 );1491 if (cancel === true) {1492 return {1493 ok: true,1494 data: await confirmRelayAdoptionSource(null),1495 };1496 }1497 if (typeof password !== "string" || password.length === 0) {1498 return { ok: false, error: "invalid_payload" };1499 }1500 return {1501 ok: true,1502 data: await confirmRelayAdoptionSource(password),1503 };1504 }15051506 case "CANCEL_ADOPTION_SOURCE": {1507 const { cancelRelayAdoptionSource } = await import(1508 "./relay-adoption-source-bg"1509 );1510 cancelRelayAdoptionSource();1511 return { ok: true };1512 }15131514 case "TRIGGER_SYNC": {1515 const payload = (msg.payload ?? {}) as {1516 reason?: "vault_change" | "foreground" | "manual";1517 force?: boolean;1518 };1519 const reason = payload.reason ?? "foreground";15201521 const { triggerSyncIfReachable } = await import("./sync-trigger");1522 void triggerSyncIfReachable({ reason, force: payload.force === true });1523 return { ok: true };1524 }15251526 case "GET_SYNC_STATE": {1527 const { getSyncState } = await import("./sync-trigger");1528 return { ok: true, data: getSyncState() };1529 }15301531 case "GET_RELAY_SETTINGS": {1532 const { getRelaySettings } = await import("./relay-settings");1533 return { ok: true, data: await getRelaySettings() };1534 }15351536 case "SET_RELAY_SETTINGS": {1537 const payload = (msg.payload ?? {}) as {1538 relayUrl?: string;1539 lanOnly?: boolean;1540 };1541 const { getRelaySettings, setLanOnly, setRelayUrl } = await import("./relay-settings");1542 try {1543 if (payload.relayUrl !== undefined) {1544 if (typeof payload.relayUrl !== "string") {1545 return { ok: false, error: "invalid_relay_url" };1546 }1547 await setRelayUrl(payload.relayUrl);1548 }1549 if (payload.lanOnly !== undefined) {1550 if (typeof payload.lanOnly !== "boolean") {1551 return { ok: false, error: "invalid_lan_only" };1552 }1553 await setLanOnly(payload.lanOnly);1554 }1555 return { ok: true, data: await getRelaySettings() };1556 } catch {1557 return { ok: false, error: "invalid_relay_url" };1558 }1559 }15601561 case "GET_SYNC_ALERTS": {1562 const { consume } = (msg.payload ?? {}) as { consume?: boolean };1563 const alerts = await getPendingSyncAlerts();1564 1565 1566 1567 if (consume) await clearPendingSyncAlerts();1568 return { ok: true, data: alerts };1569 }15701571 case "GET_PENDING_VOUCHED_DEVICES": {1572 return { ok: true, data: await listPendingVouchedDevices() };1573 }1574 case "CONFIRM_VOUCHED_DEVICE": {1575 const { deviceId } = msg.payload as { deviceId: string };1576 const pending = await getPendingVouchedDevice(deviceId);1577 if (!pending) return { ok: false, error: "not_pending" };1578 await upsertPairedDeviceV3({1579 deviceId: pending.deviceId,1580 deviceName: pending.deviceName,1581 deviceType:1582 pending.deviceType as import("../shared/sync-types").PairedDevice["deviceType"],1583 longTermPublicKeyHex: pending.longTermPublicKeyHex,1584 signingPublicKeyHex: pending.signingPublicKeyHex,1585 wsPort: pending.wsPort,1586 pairedAt: pending.pairedAt,1587 trustSource: "vouched",1588 });1589 await removePendingVouchedDevice(deviceId);1590 return { ok: true };1591 }1592 case "REJECT_VOUCHED_DEVICE": {1593 const { deviceId } = msg.payload as { deviceId: string };1594 await removePendingVouchedDevice(deviceId);1595 return { ok: true };1596 }1597 case "ROTATE_SYNC_IDENTITY": {1598 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1599 const result = await vault.rotateSyncIdentity();1600 return result.ok ? { ok: true } : { ok: false, error: result.error };1601 }16021603 default:1604 return { ok: false, error: "unknown_message_type" };1605 }1606 } catch (err) {1607 return {1608 ok: false,1609 error: err instanceof Error ? err.message : "operation_failed",1610 };1611 }1612}16131614161516161617async function publishInMemoryVault(entries: VaultEntry[]): Promise<void> {1618 const plugin = nativePlugin<{1619 setEntries?: (args: {1620 entries: Array<{1621 id: string;1622 title: string;1623 username: string;1624 email: string;1625 password: string;1626 url: string;1627 totp: string;1628 additionalUrls: string[];1629 }>;1630 }) => Promise<void>;1631 }>("InMemoryVaultPlugin");1632 if (!plugin?.setEntries) return;1633 await plugin.setEntries({1634 entries: entries.map((e) => ({1635 id: e.id,1636 title: e.title ?? "",1637 username: e.username ?? "",1638 email: (e as VaultEntry & { email?: string }).email ?? "",1639 password: e.password ?? "",1640 url: e.url ?? "",1641 totp: (e as VaultEntry & { totp?: string }).totp ?? "",1642 additionalUrls: (e as VaultEntry & { additionalUrls?: string[] }).additionalUrls ?? [],1643 })),1644 });1645}164616471648async function clearInMemoryVault(): Promise<void> {1649 const plugin = nativePlugin<{ clearEntries?: () => Promise<void> }>("InMemoryVaultPlugin");1650 if (!plugin?.clearEntries) return;1651 await plugin.clearEntries();1652}16531654165516561657function buildIdentitySuggestions(1658 entries: VaultEntry[],1659): Array<{ email: string; username: string; count: number }> {1660 const usernameCounts = new Map<string, number>();1661 const usernameEmail = new Map<string, string>();1662 for (const entry of entries) {1663 const username = entry.username ?? "";1664 const email = (entry as VaultEntry & { email?: string }).email ?? "";1665 if (!username && !email) continue;1666 const key = username || email;1667 usernameCounts.set(key, (usernameCounts.get(key) ?? 0) + 1);1668 if (!usernameEmail.has(key)) usernameEmail.set(key, email);1669 }1670 return Array.from(usernameCounts.entries())1671 .sort((a, b) => b[1] - a[1])1672 .map(([username, count]) => ({1673 email: usernameEmail.get(username) ?? "",1674 username,1675 count,1676 }));1677}167816791680function broadcastToContentScripts(message: { type: string }): void {1681 if (!chrome.tabs?.query) return;1682 chrome.tabs.query({}, (tabs) => {1683 for (const tab of tabs) {1684 if (tab.id !== undefined) {1685 chrome.tabs.sendMessage(tab.id, message).catch(() => {1686 1687 });1688 }1689 }1690 });1691}16921693function shouldRefreshAutoLock(type: MessageType): boolean {1694 return (1695 type !== "HAS_VAULT" &&1696 type !== "IS_UNLOCKED" &&1697 type !== "GET_VAULT_STATUS" &&1698 type !== "LOCK" &&1699 type !== "UPDATE_SETTINGS" &&1700 type !== "GET_PENDING_PASSKEY" &&1701 type !== "GET_PASSKEY_PROVIDER_STATE" &&1702 type !== "DECLINE_CARD_SAVE" &&1703 type !== "IS_CARD_DECLINED" &&1704 type !== "SUBMIT_FEEDBACK"1705 );1706}17071708function isAllowedCanaryWebhookUrl(value: string): boolean {1709 try {1710 const url = new URL(value);1711 if (url.protocol !== "https:") return false;17121713 const hostname = url.hostname.toLowerCase();1714 if (1715 hostname === "localhost" ||1716 hostname.endsWith(".localhost") ||1717 isPrivateIpv4(hostname) ||1718 isPrivateIpv6(hostname)1719 ) {1720 return false;1721 }17221723 return true;1724 } catch {1725 return false;1726 }1727}17281729function isPrivateIpv4(hostname: string): boolean {1730 if (PRIVATE_IPV4_RANGES.some((range) => hostname.startsWith(range.prefix))) {1731 return true;1732 }17331734 const octets = hostname.split(".").map((part) => Number(part));1735 if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {1736 return false;1737 }17381739 return octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31;1740}17411742function isPrivateIpv6(hostname: string): boolean {1743 const normalized = hostname.replace(/^\[|\]$/g, "").toLowerCase();1744 return (1745 normalized === "::1" ||1746 normalized.startsWith("fc") ||1747 normalized.startsWith("fd") ||1748 normalized.startsWith("fe80:")1749 );1750}