diff --git a/apps/browser-extension/src/entrypoints/background.ts b/apps/browser-extension/src/entrypoints/background.ts index 0cde44cf7..6cb0d1cee 100644 --- a/apps/browser-extension/src/entrypoints/background.ts +++ b/apps/browser-extension/src/entrypoints/background.ts @@ -9,7 +9,7 @@ import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardCl import { setupContextMenus } from '@/entrypoints/background/ContextMenu'; import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handlePasskeyPopupResponse, handleGetRequestData } from '@/entrypoints/background/PasskeyHandler'; import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler'; -import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler'; +import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetFilteredCredentials, handleGetSearchCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler'; import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants'; import { EncryptionKeyDerivationParams } from "@/utils/dist/shared/models/metadata"; @@ -28,6 +28,8 @@ export default defineBackground({ onMessage('GET_ENCRYPTION_KEY_DERIVATION_PARAMS', () => handleGetEncryptionKeyDerivationParams()); onMessage('GET_VAULT', () => handleGetVault()); onMessage('GET_CREDENTIALS', () => handleGetCredentials()); + onMessage('GET_FILTERED_CREDENTIALS', ({ data }) => handleGetFilteredCredentials(data as { currentUrl: string, pageTitle: string, matchingMode?: string })); + onMessage('GET_SEARCH_CREDENTIALS', ({ data }) => handleGetSearchCredentials(data as { searchTerm: string })); onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain()); onMessage('GET_DEFAULT_IDENTITY_SETTINGS', () => handleGetDefaultIdentitySettings()); diff --git a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts index 8ad2bcc9e..4fd4e3617 100644 --- a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts @@ -4,12 +4,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { handleGetEncryptionKey } from '@/entrypoints/background/VaultMessageHandler'; -import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher'; import { PASSKEY_PROVIDER_ENABLED_KEY, PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants'; +import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher'; import { EncryptionUtility } from '@/utils/EncryptionUtility'; import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper'; import type { diff --git a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts index 5a081be45..bfe2fdf34 100644 --- a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts @@ -264,6 +264,101 @@ export async function handleGetCredentials( } } +/** + * Get credentials filtered by URL and page title for autofill performance optimization. + * Filters credentials in the background script before sending to reduce message payload size. + * Critical for large vaults (1000+ credentials) to avoid multi-second delays. + * + * @param message - Filtering parameters: currentUrl, pageTitle, matchingMode + */ +export async function handleGetFilteredCredentials( + message: { currentUrl: string, pageTitle: string, matchingMode?: string } +) : Promise { + const encryptionKey = await handleGetEncryptionKey(); + + if (!encryptionKey) { + return { success: false, error: await t('common.errors.vaultIsLocked') }; + } + + try { + const sqliteClient = await createVaultSqliteClient(); + const allCredentials = sqliteClient.getAllCredentials(); + + // Import filtering logic + const { filterCredentials, AutofillMatchingMode } = await import('@/utils/credentialMatcher/CredentialMatcher'); + + // Parse matching mode from string + let matchingMode = AutofillMatchingMode.DEFAULT; + if (message.matchingMode) { + matchingMode = message.matchingMode as typeof AutofillMatchingMode[keyof typeof AutofillMatchingMode]; + } + + // Filter credentials in background to reduce payload size (~95% reduction) + const filteredCredentials = filterCredentials( + allCredentials, + message.currentUrl, + message.pageTitle, + matchingMode + ); + + return { success: true, credentials: filteredCredentials }; + } catch (error) { + console.error('Error getting filtered credentials:', error); + return { success: false, error: await t('common.errors.unknownError') }; + } +} + +/** + * Get credentials filtered by text search query. + * Searches across entire vault (service name, username, email, URL) and returns matches. + * + * @param message - Search parameters: searchTerm + */ +export async function handleGetSearchCredentials( + message: { searchTerm: string } +) : Promise { + const encryptionKey = await handleGetEncryptionKey(); + + if (!encryptionKey) { + return { success: false, error: await t('common.errors.vaultIsLocked') }; + } + + try { + const sqliteClient = await createVaultSqliteClient(); + const allCredentials = sqliteClient.getAllCredentials(); + + // If search term is empty, return empty array + if (!message.searchTerm || message.searchTerm.trim() === '') { + return { success: true, credentials: [] }; + } + + const searchTerm = message.searchTerm.toLowerCase().trim(); + + // Filter credentials by search term across multiple fields + const searchResults = allCredentials.filter(cred => { + const searchableFields = [ + cred.ServiceName?.toLowerCase(), + cred.Username?.toLowerCase(), + cred.Alias?.Email?.toLowerCase(), + cred.ServiceUrl?.toLowerCase() + ]; + return searchableFields.some(field => field?.includes(searchTerm)); + }).sort((a, b) => { + // Sort by service name, then username + const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? ''); + if (serviceNameComparison !== 0) { + return serviceNameComparison; + } + return (a.Username ?? '').localeCompare(b.Username ?? ''); + }); + + return { success: true, credentials: searchResults }; + } catch (error) { + console.error('Error searching credentials:', error); + return { success: false, error: await t('common.errors.unknownError') }; + } +} + /** * Create an identity. */ diff --git a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts index 2564cd821..53faa464e 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts @@ -1,9 +1,9 @@ import { sendMessage } from 'webext-bridge/content-script'; -import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/CredentialMatcher'; import { fillCredential } from '@/entrypoints/contentScript/Form'; import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants'; +import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher'; import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator'; import type { Credential } from '@/utils/dist/shared/models/vault'; import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/shared/password-generator'; @@ -49,7 +49,14 @@ export function openAutofillPopup(input: HTMLInputElement, container: HTMLElemen document.addEventListener('keydown', handleEnterKey); (async () : Promise => { - const response = await sendMessage('GET_CREDENTIALS', { }, 'background') as CredentialsResponse; + // Load autofill matching mode setting to send to background for filtering + const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT; + + const response = await sendMessage('GET_FILTERED_CREDENTIALS', { + currentUrl: window.location.href, + pageTitle: document.title, + matchingMode: matchingMode + }, 'background') as CredentialsResponse; if (response.success) { await createAutofillPopup(input, response.credentials, container); @@ -182,22 +189,12 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials: credentialList.className = 'av-credential-list'; popup.appendChild(credentialList); - // Add initial credentials + // Add initial credentials (already filtered by background script for performance) if (!credentials) { credentials = []; } - // Load autofill matching mode setting - const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT; - - const filteredCredentials = filterCredentials( - credentials, - window.location.href, - document.title, - matchingMode - ); - - updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText); + updatePopupContent(credentials, credentialList, input, rootContainer, noMatchesText); // Add divider const divider = document.createElement('div'); @@ -549,62 +546,41 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai } /** - * Handle popup search input by filtering credentials based on the search term. + * Handle popup search input - searches entire vault when user types. + * When empty, shows the initially URL-filtered credentials. + * When user types, searches ALL credentials in vault (not just the pre-filtered set). + * + * @param searchInput - The search input element + * @param initialCredentials - The initially URL-filtered credentials to show when search is empty + * @param rootContainer - The root container element + * @param searchTimeout - Timeout for debouncing search + * @param credentialList - The credential list element to update + * @param input - The input field that triggered the popup + * @param noMatchesText - Text to show when no matches found */ -async function handleSearchInput(searchInput: HTMLInputElement, credentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise { +async function handleSearchInput(searchInput: HTMLInputElement, initialCredentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise { if (searchTimeout) { clearTimeout(searchTimeout); } - const searchTerm = searchInput.value.toLowerCase(); - // Ensure we have unique credentials - const uniqueCredentials = Array.from(new Map(credentials.map(cred => [cred.Id, cred])).values()); - let filteredCredentials; + const searchTerm = searchInput.value.trim(); if (searchTerm === '') { - // Load autofill matching mode setting - const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT; - - // If search is empty, use original URL-based filtering - filteredCredentials = filterCredentials( - uniqueCredentials, - window.location.href, - document.title, - matchingMode - ).sort((a, b) => { - // First compare by service name - const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? ''); - if (serviceNameComparison !== 0) { - return serviceNameComparison; - } - - // If service names are equal, compare by username/nickname - return (a.Username ?? '').localeCompare(b.Username ?? ''); - }); + // If search is empty, show the initially URL-filtered credentials + updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText); } else { - // Otherwise filter based on search term - filteredCredentials = uniqueCredentials.filter(cred => { - const searchableFields = [ - cred.ServiceName?.toLowerCase(), - cred.Username?.toLowerCase(), - cred.Alias?.Email?.toLowerCase(), - cred.ServiceUrl?.toLowerCase() - ]; - return searchableFields.some(field => field?.includes(searchTerm)); - }).sort((a, b) => { - // First compare by service name - const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? ''); - if (serviceNameComparison !== 0) { - return serviceNameComparison; - } + // Search in full vault with search term + const response = await sendMessage('GET_SEARCH_CREDENTIALS', { + searchTerm: searchTerm + }, 'background') as CredentialsResponse; - // If service names are equal, compare by username/nickname - return (a.Username ?? '').localeCompare(b.Username ?? ''); - }); + if (response.success && response.credentials) { + updatePopupContent(response.credentials, credentialList, input, rootContainer, noMatchesText); + } else { + // On error, fallback to showing initial filtered credentials + updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText); + } } - - // Update popup content with filtered results - updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText); } /** diff --git a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx index bdd714cef..e501491c2 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import { sendMessage } from 'webext-bridge/popup'; -import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher'; import Button from '@/entrypoints/popup/components/Button'; import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog'; import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner'; @@ -12,6 +11,7 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext'; import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedirect'; import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants'; +import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher'; import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator'; import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper'; import type { GetRequest, PasskeyGetCredentialResponse, PendingPasskeyGetRequest, StoredPasskeyRecord } from '@/utils/passkey/types'; diff --git a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx index 12bfe3178..3d1af529d 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import { sendMessage } from 'webext-bridge/popup'; -import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher'; import Alert from '@/entrypoints/popup/components/Alert'; import Button from '@/entrypoints/popup/components/Button'; import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog'; @@ -16,6 +15,7 @@ import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedi import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate'; import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants'; +import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher'; import type { Passkey } from '@/utils/dist/shared/models/vault'; import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator'; import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper'; diff --git a/apps/browser-extension/src/entrypoints/popup/pages/settings/AutofillSettings.tsx b/apps/browser-extension/src/entrypoints/popup/pages/settings/AutofillSettings.tsx index 312e59d58..d57d77716 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/settings/AutofillSettings.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/settings/AutofillSettings.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { AutofillMatchingMode } from '@/entrypoints/contentScript/CredentialMatcher'; import { useLoading } from '@/entrypoints/popup/context/LoadingContext'; import { @@ -10,6 +9,7 @@ import { TEMPORARY_DISABLED_SITES_KEY, AUTOFILL_MATCHING_MODE_KEY } from '@/utils/Constants'; +import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher'; import { storage, browser } from "#imports"; diff --git a/apps/browser-extension/src/entrypoints/popup/pages/settings/PasskeySettings.tsx b/apps/browser-extension/src/entrypoints/popup/pages/settings/PasskeySettings.tsx index f477d7a0d..6eaaab662 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/settings/PasskeySettings.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/settings/PasskeySettings.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher'; import { useLoading } from '@/entrypoints/popup/context/LoadingContext'; import { PASSKEY_PROVIDER_ENABLED_KEY, PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants'; +import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher'; import { storage, browser } from "#imports"; diff --git a/apps/browser-extension/src/entrypoints/contentScript/CredentialMatcher.ts b/apps/browser-extension/src/utils/credentialMatcher/CredentialMatcher.ts similarity index 100% rename from apps/browser-extension/src/entrypoints/contentScript/CredentialMatcher.ts rename to apps/browser-extension/src/utils/credentialMatcher/CredentialMatcher.ts diff --git a/apps/browser-extension/src/entrypoints/contentScript/__tests__/CredentialMatcher.test.ts b/apps/browser-extension/src/utils/credentialMatcher/__tests__/CredentialMatcher.test.ts similarity index 99% rename from apps/browser-extension/src/entrypoints/contentScript/__tests__/CredentialMatcher.test.ts rename to apps/browser-extension/src/utils/credentialMatcher/__tests__/CredentialMatcher.test.ts index 3d8ebf0ef..7d7d8b5c3 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/__tests__/CredentialMatcher.test.ts +++ b/apps/browser-extension/src/utils/credentialMatcher/__tests__/CredentialMatcher.test.ts @@ -1,9 +1,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; +import { filterCredentials } from '@/utils/credentialMatcher/CredentialMatcher'; import type { Credential } from '@/utils/dist/shared/models/vault'; -import { filterCredentials } from '../CredentialMatcher'; - describe('CredentialMatcher - Credential URL Matching', () => { let testCredentials: Credential[];