mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-23 22:28:22 -05:00
Improve browser extension autofill suggestion performance (#1413)
This commit is contained in:
committed by
Leendert de Borst
parent
64d29ebcd4
commit
254f0a1212
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<messageCredentialsResponse> {
|
||||
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<messageCredentialsResponse> {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void> {
|
||||
async function handleSearchInput(searchInput: HTMLInputElement, initialCredentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user