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