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