123456789101112import { copyToClipboard } from "./clipboard";13import { setupAutoLock, touchAutoLock } from "./alarms";14import * as vault from "./vault";15import {16 activateLicense,17 deactivateLicense,18 getLicenseInfo,19 getLicenseKey,20 isProUser,21 markCelebrationPlayed,22 shouldPlayCelebration,23} from "./license";24import {25 cancelFirefoxPasskeyCreate,26 completeFirefoxPasskeyCreate,27 getPasskeyProviderState,28 getPendingPasskeyRequest,29 handleFirefoxPasskeyCreate,30 handleFirefoxPasskeyGet,31 handlePasskeyHasCredentials,32 initiateFirefoxPasskeyCreate,33 isPasskeyOfferDeclined,34 recordPasskeyOfferDecline,35 resolvePendingPasskeyRequest,36 setPasskeyProviderEnabled,37} from "./passkey";38import { buildAdvancedHealthReport, resolveResetUrl } from "./health";39import { Capacitor } from "@capacitor/core";40import { storageClear } from "../shared/storage";41import type {42 Message,43 MessageResponse,44 MessageType,45 ProFeature,46} from "../shared/types";47import { isPasskeyOnly } from "../shared/types";48import type { SyncStatus } from "../shared/sync-types";4950515253545556async function requirePro(57 feature: ProFeature,58 trigger: "user" | "background" = "user"59): Promise<{ allowed: boolean }> {60 if (await isProUser()) return { allowed: true };6162 if (trigger === "user") {63 64 if (!Capacitor.isNativePlatform()) {65 chrome.runtime66 .sendMessage({ type: "PRO_GATE_TRIGGERED", payload: { feature } })67 .catch(() => {68 69 });70 }71 } else {72 73 74 75 if (!Capacitor.isNativePlatform() && chrome.storage.session) {76 chrome.storage.session77 .set({ pendingProNotice: { feature, triggeredAt: Date.now() } })78 .catch(() => {});79 }80 }8182 return { allowed: false };83}8485const PRIVATE_IPV4_RANGES = [86 { prefix: "10." },87 { prefix: "127." },88 { prefix: "169.254." },89 { prefix: "192.168." },90];919293949596const FEEDBACK_ENDPOINT = "";9798export function setupMessageHandler(): void {99 chrome.runtime.onMessage.addListener(100 (msg: unknown, sender, sendResponse) => {101 if (!isMessage(msg)) {102 sendResponse({ ok: false, error: "invalid message" });103 return true;104 }105 handleMessage(msg, sender)106 .then(sendResponse)107 .catch((err) =>108 sendResponse({109 ok: false,110 error: err instanceof Error ? err.message : String(err),111 })112 );113 return true;114 }115 );116}117118function isMessage(m: unknown): m is Message {119 return typeof m === "object" && m !== null && "type" in m;120}121122export async function handleMessage(123 msg: Message,124 sender: Partial<chrome.runtime.MessageSender> = {}125): Promise<MessageResponse> {126 try {127 128 129 130 131 if (132 !Capacitor.isNativePlatform() &&133 sender.id !== undefined &&134 sender.id !== chrome.runtime.id135 ) {136 return { ok: false, error: "invalid sender" };137 }138139 if (shouldRefreshAutoLock(msg.type) && vault.isUnlocked()) {140 touchAutoLock();141 }142143 switch (msg.type) {144 case "CREATE_VAULT": {145 const { password } = msg.payload as { password: string };146 await vault.createVault(password);147 await setupAutoLock();148 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });149 return { ok: true };150 }151 case "UNLOCK": {152 const { password } = msg.payload as { password: string };153 const ok = await vault.unlock(password);154 if (ok) {155 await setupAutoLock();156 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });157 }158 return { ok };159 }160 case "LOCK":161 vault.lock();162 return { ok: true };163 case "IS_UNLOCKED":164 return { ok: true, data: vault.isUnlocked() };165 case "GET_VAULT_STATUS":166 return { ok: true, unlocked: vault.isUnlocked() };167 case "HAS_VAULT":168 return { ok: true, data: await vault.hasVault() };169 case "GET_ENTRIES":170 return { ok: true, data: vault.getEntries() };171 case "ADD_ENTRY": {172 if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };173 const entry = await vault.addEntry(msg.payload as any);174 return { ok: true, data: entry };175 }176 case "BATCH_ADD_ENTRIES": {177 const { entries } = msg.payload as { entries: any[] };178 const added = await vault.batchAddEntries(entries);179 return { ok: true, data: added };180 }181 case "RESTORE_ENTRIES": {182 const { entries, mode, settings } = msg.payload as {183 entries: any[];184 mode: "replace" | "merge";185 settings?: vault.VaultSettings;186 };187 const summary = await vault.restoreEntries(entries, mode);188 if (settings && typeof settings === "object") {189 await vault.updateSettings(settings);190 }191 return { ok: true, data: summary };192 }193 case "CLEAR_VAULT_ENTRIES": {194 await vault.clearVaultEntries("REPLACE_ALL");195 return { ok: true };196 }197 case "LIST_VAULT_SNAPSHOTS": {198 const snapshots = await vault.listSnapshots();199 return { ok: true, data: snapshots };200 }201 case "RESTORE_VAULT_SNAPSHOT": {202 const { index, password } = msg.payload as { index: number; password: string };203 const ok = await vault.restoreFromSnapshot(index, password);204 if (ok) broadcastToContentScripts({ type: "VAULT_UNLOCKED" });205 return { ok };206 }207 case "COUNT_DUPLICATES": {208 return { ok: true, data: vault.countDuplicates() };209 }210 case "DEDUPLICATE_ENTRIES": {211 const removed = await vault.deduplicateEntries();212 return { ok: true, data: removed };213 }214 case "UPDATE_ENTRY": {215 const { id, patch } = msg.payload as any;216 await vault.updateEntry(id, patch);217 return { ok: true };218 }219 case "ADD_ADDITIONAL_URL": {220 const { id, url } = msg.payload as { id: string; url: string };221 const trimmed = typeof url === "string" ? url.trim() : "";222 if (!trimmed) return { ok: false, error: "url required" };223 const existing = vault.getEntryById(id);224 if (!existing) return { ok: false, error: "entry not found" };225 if (existing.type !== "login") return { ok: false, error: "login entries only" };226 const merged = [...(existing.additionalUrls ?? []), trimmed];227 await vault.updateEntry(id, { additionalUrls: merged });228 return { ok: true };229 }230 case "DELETE_ENTRY": {231 const { id } = msg.payload as { id: string };232 await vault.deleteEntry(id);233 return { ok: true };234 }235 case "GET_DELETED_ENTRIES":236 return { ok: true, data: vault.getDeletedEntries() };237 case "RESTORE_ENTRY": {238 const { id } = msg.payload as { id: string };239 await vault.restoreEntry(id);240 return { ok: true };241 }242 case "PURGE_ENTRY": {243 const { id } = msg.payload as { id: string };244 await vault.purgeEntry(id);245 return { ok: true };246 }247 case "PURGE_EXPIRED_TRASH": {248 const { retentionMs } = msg.payload as { retentionMs: number };249 const purged = await vault.purgeExpiredTrash(retentionMs);250 return { ok: true, data: purged };251 }252 case "AUTOFILL_QUERY": {253 if (!vault.isUnlocked()) {254 return { ok: false, error: "VAULT_LOCKED" };255 }256 const { domain, includeAll, kind } = msg.payload as {257 domain: string;258 includeAll?: boolean;259 kind?: "login" | "card";260 };261 262 263 264 265 if (kind === "card") {266 const cards = vault.getEntries().filter((e) => e.type === "card");267 return {268 ok: true,269 data: cards.map((e) => ({270 id: e.id,271 title: e.cardNickname || e.title,272 cardholderName: e.cardholderName,273 last4: e.last4,274 expMonth: e.expMonth,275 expYear: e.expYear,276 network: e.network,277 })),278 };279 }280 281 282 283 284 285 const matches = (286 includeAll287 ? vault288 .getEntries()289 .filter((entry) => entry.type !== "note" && entry.type !== "card")290 : vault.queryDomain(domain)291 ).filter((entry) => !isPasskeyOnly(entry));292 return {293 ok: true,294 data: matches.map((e) =>295 includeAll296 ? {297 id: e.id,298 title: e.title,299 hasPasskey: e.passkey !== undefined,300 }301 : {302 id: e.id,303 title: e.title,304 username: e.username,305 url: e.url,306 hasTotp: Boolean(e.totp),307 hasPasskey: e.passkey !== undefined,308 }309 ),310 };311 }312 case "GET_ENTRY_FOR_FILL": {313 if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };314 const { id } = msg.payload as { id: string };315 const entry = vault.getEntryById(id);316 if (!entry) return { ok: false, error: "entry not found" };317 318 319 320 if (isPasskeyOnly(entry)) return { ok: false, error: "passkey not fillable" };321 322 323 324 const { passkey: _strippedPasskey, ...safeEntry } = entry;325 return { ok: true, data: safeEntry };326 }327 case "COPY": {328 const { text, label, alreadyWritten } = msg.payload as {329 text: string;330 label: string;331 alreadyWritten?: boolean;332 };333 await copyToClipboard(text, label, Boolean(alreadyWritten));334 return { ok: true };335 }336 case "CHECK_BREACH": {337 const { password } = msg.payload as { password: string };338 const count = await vault.checkBreach(password);339 return { ok: true, data: count };340 }341 case "CHECK_ALL_BREACHES": {342 const allEntries = vault.getEntries();343 const results: Record<string, number> = {};344 for (const entry of allEntries) {345 if (entry.type === "note") continue;346 if (!entry.password) continue;347 results[entry.id] = await vault.checkBreach(entry.password);348 349 await new Promise((r) => setTimeout(r, 200));350 }351 352 for (const [id, count] of Object.entries(results)) {353 const entry = vault.getEntryById(id);354 if (entry && (entry.breach ?? 0) !== count) {355 await vault.updateEntry(id, { breach: count > 0 ? count : undefined });356 }357 }358 return { ok: true, data: results };359 }360 case "GET_SETTINGS":361 return { ok: true, data: await vault.ensureSettingsLoaded() };362 case "UPDATE_SETTINGS": {363 const patch = msg.payload as Record<string, unknown>;364 await vault.updateSettings(patch as any);365 if (typeof patch.autoLockMinutes === "number") {366 await setupAutoLock();367 if (vault.isUnlocked()) touchAutoLock();368 }369 return { ok: true };370 }371 case "GET_PENDING_PASSKEY":372 return { ok: true, data: await getPendingPasskeyRequest() };373 case "APPROVE_PASSKEY": {374 const { requestId } = msg.payload as { requestId: number };375 await resolvePendingPasskeyRequest(requestId, true);376 return { ok: true };377 }378 case "DENY_PASSKEY": {379 const { requestId } = msg.payload as { requestId: number };380 await resolvePendingPasskeyRequest(requestId, false);381 return { ok: true };382 }383 case "GET_PASSKEYS":384 return {385 ok: true,386 data: vault.getEntries().filter((entry) => entry.passkey !== undefined),387 };388 case "DELETE_PASSKEY": {389 const { id } = msg.payload as { id: string };390 await vault.deleteEntry(id);391 return { ok: true };392 }393 case "GET_PASSKEY_PROVIDER_STATE":394 return { ok: true, data: await getPasskeyProviderState() };395 case "SET_PASSKEY_PROVIDER_ENABLED": {396 const { enabled } = msg.payload as { enabled: boolean };397 398 399 if (enabled) {400 const gate = await requirePro("passkeys");401 if (!gate.allowed) return { ok: false, error: "pro_required" };402 }403 return { ok: true, data: await setPasskeyProviderEnabled(enabled) };404 }405 case "FIRE_CANARY_PING": {406 const { webhookUrl, entryTitle, trigger } = msg.payload as {407 webhookUrl: string;408 entryTitle: string;409 trigger: "password_copied" | "username_copied" | "autofilled" | "submitted" | "test";410 };411 if (!isAllowedCanaryWebhookUrl(webhookUrl)) {412 return { ok: false, error: "invalid webhook url" };413 }414 const body = JSON.stringify({415 event: trigger === "test" ? "peach_canary_test" : "peach_canary_accessed",416 entry: entryTitle,417 trigger,418 timestamp: new Date().toISOString(),419 source: "peach-extension",420 });421 if (trigger === "test") {422 423 424 425 426 427 try {428 const res = await fetch(webhookUrl, {429 method: "POST",430 headers: { "Content-Type": "application/json" },431 body,432 });433 return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };434 } catch (err) {435 return { ok: false, error: err instanceof Error ? err.message : "network error" };436 }437 }438 439 fetch(webhookUrl, {440 method: "POST",441 headers: { "Content-Type": "application/json" },442 body,443 }).catch(() => {});444 return { ok: true };445 }446 case "ACTIVATE_LICENSE": {447 const { key } = msg.payload as { key: string };448 const licenseInfo = await activateLicense(key);449 return { ok: true, data: licenseInfo };450 }451 case "GET_LICENSE": {452 const licenseInfo = await getLicenseInfo();453 return { ok: true, data: licenseInfo };454 }455 case "GET_LICENSE_KEY": {456 const key = await getLicenseKey();457 return { ok: true, data: key };458 }459 case "DEACTIVATE_LICENSE": {460 await deactivateLicense();461 return { ok: true };462 }463 case "SHOULD_PLAY_CELEBRATION": {464 const should = await shouldPlayCelebration();465 return { ok: true, data: should };466 }467 case "MARK_CELEBRATION_PLAYED": {468 await markCelebrationPlayed();469 return { ok: true };470 }471 case "OPEN_PRO_GATE": {472 473 const { feature } = (msg.payload ?? {}) as { feature?: string };474 if (chrome.storage.session) {475 await chrome.storage.session.set({ pendingProGate: { feature: feature ?? "passkeys" } });476 }477 try { await chrome.action.openPopup(); } catch { }478 return { ok: true };479 }480 case "GET_IDENTITIES": {481 if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };482 return { ok: true, data: vault.getIdentities() };483 }484 case "GET_ADVANCED_HEALTH_REPORT": {485 const gate = await requirePro("advanced_health");486 if (!gate.allowed) return { ok: false, error: "pro_required" };487 if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };488 const report = await buildAdvancedHealthReport(vault.getEntries());489 return { ok: true, data: report };490 }491 case "INITIATE_PASSWORD_CHANGE": {492 const gate = await requirePro("smart_password_change");493 if (!gate.allowed) return { ok: false, error: "pro_required" };494 if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };495 const { id } = msg.payload as { id: string };496 const entry = vault.getEntryById(id);497 if (!entry) return { ok: false, error: "entry not found" };498 const url = resolveResetUrl(entry);499 if (!url) return { ok: false, error: "no reset url" };500 try {501 await chrome.tabs.create({ url });502 return { ok: true, data: { url } };503 } catch (err) {504 return {505 ok: false,506 error: err instanceof Error ? err.message : "failed to open tab",507 };508 }509 }510 case "IS_DURESS_ENABLED": {511 const enabled = await vault.isDuressEnabled();512 return { ok: true, data: enabled };513 }514 case "SETUP_DURESS": {515 const { duressPassword } = msg.payload as { duressPassword: string };516 await vault.setupDuress(duressPassword);517 return { ok: true };518 }519 case "DISABLE_DURESS": {520 await vault.disableDuress();521 return { ok: true };522 }523 case "VERIFY_MAIN_PASSWORD": {524 const { password } = msg.payload as { password: string };525 const ok = await vault.verifyMainPassword(password);526 return { ok: true, data: ok };527 }528 case "CHANGE_PASSWORD": {529 const { currentPassword, newPassword } = msg.payload as {530 currentPassword: string;531 newPassword: string;532 };533 const changed = await vault.changePassword(currentPassword, newPassword);534 return changed535 ? { ok: true }536 : { ok: false, error: "incorrect current password" };537 }538 case "GET_SITE_PASSWORD_POLICY": {539 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });540 if (!tab?.id) return { ok: true, data: null };541 try {542 const response = await chrome.tabs.sendMessage(tab.id, {543 type: "GET_SITE_PASSWORD_POLICY",544 });545 return (response as MessageResponse) ?? { ok: true, data: null };546 } catch {547 return { ok: true, data: null };548 }549 }550 case "GET_SYNC_STATE": {551 const { getPairedDevices, getTrustedNetworks } = await import('./sync-storage');552 const { isOnTailscale } = await import('./sync-network');553 const { isProUser } = await import('./license');554 const pairedDevices = await getPairedDevices();555 const trustedNetworks = await getTrustedNetworks();556 const tailscale = await isOnTailscale();557 const pro = await isProUser();558 559 let status: SyncStatus = "offline";560 if (tailscale || trustedNetworks.some((n: { isTailscale?: boolean }) => !n.isTailscale)) {561 if (pairedDevices.length === 0) {562 status = "no_peers";563 } else {564 status = "synced";565 }566 } else {567 status = "not_trusted";568 }569 570 return { ok: true, data: { pairedDevices, trustedNetworks, status, isPro: pro } };571 }572 case "TRIGGER_SYNC": {573 const gate = await requirePro("sync");574 if (!gate.allowed) return { ok: false, error: "pro_required" };575 return { ok: true };576 }577 case "START_PAIRING": {578 const gate = await requirePro("sync");579 if (!gate.allowed) return { ok: false, error: "pro_required" };580 return { ok: true };581 }582 case "CANCEL_PAIRING": {583 return { ok: true };584 }585 case "ACCEPT_PAIRING": {586 const gate = await requirePro("sync");587 if (!gate.allowed) return { ok: false, error: "pro_required" };588 return { ok: true };589 }590 case "REMOVE_PAIRED_DEVICE": {591 const { deviceId } = msg.payload as { deviceId: string };592 const { removePairedDevice } = await import('./sync-storage');593 await removePairedDevice(deviceId);594 return { ok: true };595 }596 case "REMOVE_TRUSTED_NETWORK": {597 const { ssid, bssid, isTailscale } = msg.payload as any;598 const { removeTrustedNetwork, getTrustedNetworks } = await import('./sync-storage');599 const networks = await getTrustedNetworks();600 const network = networks.find((n: { ssid?: string; bssid?: string; isTailscale?: boolean }) => 601 (isTailscale ? n.isTailscale : n.ssid === ssid) && 602 (!bssid || n.bssid === bssid)603 );604 if (network) {605 await removeTrustedNetwork(network.addedAt);606 }607 return { ok: true };608 }609 case "MARK_NETWORK_TRUSTED": {610 const { addTrustedNetwork } = await import('./sync-storage');611 const { getCurrentNetwork } = await import('./sync-network');612 const network = await getCurrentNetwork();613 await addTrustedNetwork({614 ssid: network.ssid,615 bssid: network.bssid,616 isTailscale: network.isTailscale,617 addedAt: Date.now(),618 });619 return { ok: true };620 }621 case "RECORD_MANUAL_SUMMON": {622 const { domain } = msg.payload as { domain: string };623 const settings = vault.getSettings();624 const existing = settings.manualSummonDomains?.[domain];625 const now = Date.now();626 await vault.updateSettings({627 manualSummonDomains: {628 ...settings.manualSummonDomains,629 [domain]: {630 firstSummonedAt: existing?.firstSummonedAt ?? now,631 summonCount: (existing?.summonCount ?? 0) + 1,632 lastSummonedAt: now,633 },634 },635 });636 return { ok: true };637 }638 case "DECLINE_CARD_SAVE": {639 const { last4, expMonth, expYear } = msg.payload as { last4: string; expMonth: string; expYear: string };640 await vault.declineCardSaveOffer(last4, expMonth, expYear);641 return { ok: true };642 }643 case "IS_CARD_DECLINED": {644 const { last4, expMonth, expYear } = msg.payload as { last4: string; expMonth: string; expYear: string };645 const declined = await vault.isCardDeclined(last4, expMonth, expYear);646 return { ok: true, data: declined };647 }648 case "INITIATE_FIREFOX_PASSKEY_CREATE": {649 const { rpId, rpName, userName, userDisplayName } = (msg.payload ?? {}) as {650 rpId?: string;651 rpName?: string;652 userName?: string;653 userDisplayName?: string;654 };655 if (!rpId || !userName) return { ok: false, error: "rpId and userName required" };656 return await initiateFirefoxPasskeyCreate({657 rpId,658 rpName: rpName ?? rpId,659 userName,660 userDisplayName,661 });662 }663664 case "COMPLETE_FIREFOX_PASSKEY_CREATE": {665 const payload = msg.payload as Parameters<typeof completeFirefoxPasskeyCreate>[0] | undefined;666 if (!payload) return { ok: false, error: "missing payload" };667 return await completeFirefoxPasskeyCreate(payload);668 }669670 case "PASSKEY_OFFER_DECLINE": {671 const { domain } = (msg.payload ?? {}) as { domain?: string };672 if (!domain) return { ok: false, error: "domain required" };673 await recordPasskeyOfferDecline(domain);674 return { ok: true };675 }676677 case "GET_PASSKEY_OFFER_DECLINED": {678 const { domain } = (msg.payload ?? {}) as { domain?: string };679 if (!domain) return { ok: false, error: "domain required" };680 return { ok: true, data: await isPasskeyOfferDeclined(domain) };681 }682683 case "PASSKEY_HAS_CREDENTIALS": {684 const payload = msg.payload as {685 rpId?: string;686 allowCredentials?: Array<{ id: string; type?: string }>;687 } | undefined;688 if (!payload?.rpId) return { ok: false, error: "rpId required" };689 return await handlePasskeyHasCredentials({690 rpId: payload.rpId,691 allowCredentials: payload.allowCredentials ?? [],692 });693 }694695 case "PASSKEY_CREATE": {696 const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyCreate>[0] | undefined;697 if (!payload?.rpId || !payload?.userName) {698 return { ok: false, error: "rpId and userName required" };699 }700 return await handleFirefoxPasskeyCreate(payload);701 }702703 case "PASSKEY_GET": {704 const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyGet>[0] | undefined;705 if (!payload?.rpId) return { ok: false, error: "rpId required" };706 return await handleFirefoxPasskeyGet(payload);707 }708709 case "GET_PASSKEY_FOR_DOMAIN": {710 const { domain } = (msg.payload ?? {}) as { domain?: string };711 if (!domain) return { ok: false, error: "domain required" };712 if (!vault.isUnlocked()) return { ok: true, data: null };713 const match = vault714 .getEntries()715 .find((entry) => entry.passkey?.rpId === domain);716 if (!match || !match.passkey) return { ok: true, data: null };717 718 719 return {720 ok: true,721 data: {722 rpId: match.passkey.rpId,723 userName: match.passkey.userName,724 browser: match.passkey.browser,725 },726 };727 }728729 case "DESTROY_VAULT": {730 731 732 733 734 735 vault.lock();736 await storageClear();737 if (!Capacitor.isNativePlatform() && chrome.storage.session) {738 await chrome.storage.session.clear();739 }740 return { ok: true };741 }742743 case "SUBMIT_FEEDBACK": {744 const { category, message, version, browser } = msg.payload as {745 category: string;746 message: string;747 version: string;748 browser: string;749 };750 if (!message.trim()) return { ok: false, error: "message is empty" };751 if (!FEEDBACK_ENDPOINT) return { ok: true };752 if (!isAllowedCanaryWebhookUrl(FEEDBACK_ENDPOINT)) {753 return { ok: false, error: "invalid feedback endpoint" };754 }755 try {756 const res = await fetch(FEEDBACK_ENDPOINT, {757 method: "POST",758 headers: { "Content-Type": "application/json" },759 body: JSON.stringify({ category, message, version, browser, source: "peach-extension" }),760 });761 return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };762 } catch (err) {763 return { ok: false, error: err instanceof Error ? err.message : "network error" };764 }765 }766 default:767 return { ok: false, error: "unknown message type" };768 }769 } catch (err) {770 return {771 ok: false,772 error: err instanceof Error ? err.message : "operation failed",773 };774 }775}776777778function broadcastToContentScripts(message: { type: string }): void {779 if (Capacitor.isNativePlatform() || !chrome.tabs?.query) return;780 chrome.tabs.query({}, (tabs) => {781 for (const tab of tabs) {782 if (tab.id !== undefined) {783 chrome.tabs.sendMessage(tab.id, message).catch(() => {784 785 });786 }787 }788 });789}790791function shouldRefreshAutoLock(type: MessageType): boolean {792 return (793 type !== "HAS_VAULT" &&794 type !== "IS_UNLOCKED" &&795 type !== "GET_VAULT_STATUS" &&796 type !== "LOCK" &&797 type !== "UPDATE_SETTINGS" &&798 type !== "GET_PENDING_PASSKEY" &&799 type !== "GET_PASSKEY_PROVIDER_STATE" &&800 type !== "DECLINE_CARD_SAVE" &&801 type !== "IS_CARD_DECLINED" &&802 type !== "SUBMIT_FEEDBACK"803 );804}805806function isAllowedCanaryWebhookUrl(value: string): boolean {807 try {808 const url = new URL(value);809 if (url.protocol !== "https:") return false;810811 const hostname = url.hostname.toLowerCase();812 if (813 hostname === "localhost" ||814 hostname.endsWith(".localhost") ||815 isPrivateIpv4(hostname) ||816 isPrivateIpv6(hostname)817 ) {818 return false;819 }820821 return true;822 } catch {823 return false;824 }825}826827function isPrivateIpv4(hostname: string): boolean {828 if (PRIVATE_IPV4_RANGES.some((range) => hostname.startsWith(range.prefix))) {829 return true;830 }831832 const octets = hostname.split(".").map((part) => Number(part));833 if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {834 return false;835 }836837 return octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31;838}839840function isPrivateIpv6(hostname: string): boolean {841 const normalized = hostname.replace(/^\[|\]$/g, "").toLowerCase();842 return (843 normalized === "::1" ||844 normalized.startsWith("fc") ||845 normalized.startsWith("fd") ||846 normalized.startsWith("fe80:")847 );848}