import { detectForms } from './utils/FormDetector';
import { Credential } from './types/Credential';
type CredentialResponse = {
status: 'OK' | 'LOCKED';
credentials?: Credential[];
}
const placeholderBase64 = 'UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp5IraLASCWUA0eb/0s56RrLtCnYfLPiBshdXWMx8j1Ez65f169iA4xUDBTEV6ylMQeCIj2b7RngGi7gKZ9WjKdSoy9R8JcgOmjCMlDmLG20KhNo/i/Dc/Ah5GAvGfm8kfniV3AkR6fxN6eKwjDc6xrDgSfS48G5uGV6WzQt24YAVlLSK9BMwndzfHnePK1KFchFrL7O3ulB8cGNCeomu4o+l0SrS/JKblJ4WTzj0DAD++lCUEouSfgRKdiV2TiYCD+H+l3tANKSPQFPQuzi7rbvxqGeRmXB9kDwURaoSTTpYjA9REMUi9uA6aV7PWtBNXgUzMLowYMZeos6Xvyhb34GmufswMHA5ZyYpxzjTphOak4ZjNOiz8aScO5ygiTx99SqwX/uL+HSeVOSraHw8IymrMwm+jLxqN8BS8dGcItLlm/ioulqH2j4V8glDgSut+ExkxiD7m8TGPrrjCQNJbRDzpOFsyCyfBZupvp8QjGKW2KGziSZeIWes4aTB9tRmeEBhnUrmTDZQuXcc67Fg82KHrSfaeeOEq6jjuUjQ8wUnzM4Zz3dhrwSyslVz/WvnKqYkr4V/TTXPFF5EjF4rM1bHZ8bK63EfTnK41+n3n4gEFoYP4mXkNH0hntnYcdTqiE7Gn+q0BpRRxnkpBSZlA6Wa70jpW0FGqkw5e591A5/H+OV+60WAo+4Mi+NlsKrvLZ9EiVaPnoEFZlJQx1fA777AJ2MjXJ4KSsrWDWJi1lE8yPs8V6XvcC0chDTYt8456sKXAagCZyY+fzQriFMaddXyKQdG8qBqcdYjAsiIcjzaRFBBoOK9sU+sFY7N6B6+xtrlu3c37rQKkI3O2EoiJOris54EjJ5OFuumA0M6riNUuBf/MEPFBVx1JRcUEs+upEBsCnwYski7FT3TTqHrx7v5AjgFN97xhPTkmVpu6sxRnWBi1fxIRp8eWZeFM6mUcGgVk1WeVb1yhdV9hoMo2TsNEPE0tHo/wvuSJSzbZo7wibeXM9v/rRfKcx7X93rfiXVnyQ9f/5CaAQ4lxedPp/6uzLtOS4FyL0bCNeZ6L5w+AiuyWCTDFIYaUzhwfG+/YTQpWyeZCdQIKzhV+3GeXI2cxoP0ER/DlOKymf1gm+zRU3sqf1lBVQ0y+mK/Awl9bS3uaaQmI0FUyUwHUKP7PKuXnO+LcwDv4OfPT6hph8smc1EtMe5ib/apar/qZ9dyaEaElALJ1KKxnHziuvVl8atk1fINSQh7OtXDyqbPw9o/nGIpTnv5iFmwmWJLis2oyEgPkJqyx0vYI8rjkVEzKc8eQavAJBYSpjMwM193Swt+yJyjvaGYWPnqExxKiNarpB2WSO7soCAZXhS1uEYHryrK47BH6W1dRiruqT0xpLih3MXiwU3VDwAAAA==';
/**
* Check if the current theme is dark.
*/
function isDarkMode(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
* Listen for input field focus
*/
document.addEventListener('focusin', (e) => {
const target = e.target as HTMLInputElement;
if (target.tagName === 'INPUT') {
showCredentialPopup(target);
}
});
/**
* Show credential popup
*/
function showCredentialPopup(input: HTMLInputElement) : void {
const forms = detectForms();
if (!forms.length) return;
// Request credentials from background script
chrome.runtime.sendMessage({ type: 'GET_CREDENTIALS_FOR_URL', url: window.location.href }, (response: CredentialResponse) => {
switch (response.status) {
case 'OK':
if (response.credentials?.length) {
createPopup(input, response.credentials);
}
break;
case 'LOCKED':
createStatusPopup(input, 'AliasVault is locked.');
break;
}
});
}
/**
* Filter credentials based on current URL and page context
*/
function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string): Credential[] {
// If less than 5 entries, return all
if (credentials.length <= 5) {
return credentials;
}
const urlObject = new URL(currentUrl);
const baseUrl = `${urlObject.protocol}//${urlObject.hostname}`;
// 1. Exact URL match
let filtered = credentials.filter(cred =>
cred.ServiceUrl?.toLowerCase() === currentUrl.toLowerCase()
);
// 2. Base URL match with fuzzy domain comparison if no exact matches
if (filtered.length === 0) {
filtered = credentials.filter(cred => {
if (!cred.ServiceUrl) return false;
try {
const credUrlObject = new URL(cred.ServiceUrl);
const currentUrlObject = new URL(baseUrl);
// Extract root domains by splitting on dots and taking last two parts
const credDomainParts = credUrlObject.hostname.toLowerCase().split('.');
const currentDomainParts = currentUrlObject.hostname.toLowerCase().split('.');
// Get root domain (last two parts, e.g., 'dumpert.nl')
const credRootDomain = credDomainParts.slice(-2).join('.');
const currentRootDomain = currentDomainParts.slice(-2).join('.');
// Compare protocols and root domains
return credUrlObject.protocol === currentUrlObject.protocol &&
credRootDomain === currentRootDomain;
} catch {
return false;
}
});
}
// 3. Page title word match if still no matches
if (filtered.length === 0) {
const titleWords = pageTitle.toLowerCase().split(/\s+/);
filtered = credentials.filter(cred =>
titleWords.some(word =>
cred.ServiceName.toLowerCase().includes(word)
)
);
}
return filtered;
}
/**
* Create auto-fill popup
*/
function createPopup(input: HTMLInputElement, credentials: Credential[]) : void {
// Remove existing popup if any
removeExistingPopup();
const popup = document.createElement('div');
popup.id = 'aliasvault-credential-popup';
// Get input width
const inputWidth = input.offsetWidth;
// Set popup width to match input width, with min/max constraints
const popupWidth = Math.max(250, Math.min(960, inputWidth));
popup.style.cssText = `
position: absolute;
z-index: 999999;
background: ${isDarkMode() ? '#1f2937' : 'white'};
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
border-radius: 4px;
box-shadow: 0 2px 4px ${isDarkMode() ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.2)'};
padding: 8px 0;
width: ${popupWidth}px;
color: ${isDarkMode() ? '#f8f9fa' : '#000000'};
`;
// Filter credentials based on current page context
const filteredCredentials = filterCredentials(
credentials,
window.location.href,
document.title
);
// Add credentials to popup if any matches found
if (filteredCredentials.length > 0) {
filteredCredentials.forEach(cred => {
const item = document.createElement('div');
item.style.cssText = `
padding: 8px 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
`;
const imgElement = document.createElement('img');
imgElement.style.width = '16px';
imgElement.style.height = '16px';
// Handle base64 image data
if (cred.Logo) {
try {
const base64Logo = base64Encode(cred.Logo);
imgElement.src = `data:image/x-icon;base64,${base64Logo}`;
} catch (error) {
console.error('Error setting logo:', error);
imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`;
}
} else {
imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`;
}
item.appendChild(imgElement);
item.appendChild(document.createTextNode(cred.Username));
item.addEventListener('mouseenter', () => {
item.style.backgroundColor = isDarkMode() ? '#374151' : '#f0f0f0';
});
item.addEventListener('mouseleave', () => {
item.style.backgroundColor = 'transparent';
});
item.addEventListener('click', () => {
fillCredential(cred);
removeExistingPopup();
});
popup.appendChild(item);
});
} else {
// Show "no matches found" message
const noMatches = document.createElement('div');
noMatches.style.cssText = `
padding: 8px 16px;
color: ${isDarkMode() ? '#9ca3af' : '#6b7280'};
font-style: italic;
`;
noMatches.textContent = 'No matches found';
popup.appendChild(noMatches);
}
// Add divider
const divider = document.createElement('div');
divider.style.cssText = `
height: 1px;
background: ${isDarkMode() ? '#374151' : '#e5e7eb'};
margin: 8px 0;
`;
popup.appendChild(divider);
// Add action buttons container
const actionContainer = document.createElement('div');
actionContainer.style.cssText = `
display: flex;
gap: 8px;
padding: 8px 16px;
`;
// Create New button
const createButton = document.createElement('button');
createButton.style.cssText = `
flex: 1;
padding: 6px 12px;
border-radius: 4px;
background: ${isDarkMode() ? '#374151' : '#f3f4f6'};
color: ${isDarkMode() ? '#e5e7eb' : '#374151'};
font-size: 14px;
cursor: pointer;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
`;
createButton.innerHTML = `
Create New
`;
createButton.addEventListener('click', () => {
chrome.runtime.sendMessage({ type: 'OPEN_NEW_CREDENTIAL' });
removeExistingPopup();
});
// Search button
const searchButton = document.createElement('button');
searchButton.style.cssText = createButton.style.cssText;
searchButton.innerHTML = `
Search
`;
searchButton.addEventListener('click', () => {
// Placeholder for future search functionality
removeExistingPopup();
});
actionContainer.appendChild(createButton);
actionContainer.appendChild(searchButton);
popup.appendChild(actionContainer);
/**
* Add click outside handler
* @param event
*/
const handleClickOutside = (event: MouseEvent) : void => {
if (!popup.contains(event.target as Node)) {
removeExistingPopup();
document.removeEventListener('mousedown', handleClickOutside);
}
};
setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside);
}, 100);
// Position popup below input
const rect = input.getBoundingClientRect();
popup.style.top = `${rect.bottom + window.scrollY + 2}px`;
popup.style.left = `${rect.left + window.scrollX}px`;
document.body.appendChild(popup);
}
/**
* Create status popup. TODO: refactor to use same popup basic structure for all popup types.
*/
function createStatusPopup(input: HTMLInputElement, message: string): void {
// Remove existing popup if any
removeExistingPopup();
const popup = document.createElement('div');
popup.id = 'aliasvault-credential-popup';
// Get input width
const inputWidth = input.offsetWidth;
// Set popup width to match input width, with min/max constraints
const popupWidth = Math.max(250, Math.min(960, inputWidth));
popup.style.cssText = `
position: absolute;
z-index: 999999;
background: ${isDarkMode() ? '#1f2937' : 'white'};
border: 1px solid ${isDarkMode() ? '#374151' : '#ccc'};
border-radius: 4px;
box-shadow: 0 2px 4px ${isDarkMode() ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.2)'};
padding: 12px 16px;
width: ${popupWidth}px;
color: ${isDarkMode() ? '#f8f9fa' : '#000000'};
cursor: pointer;
transition: background-color 0.2s;
`;
// Add hover effect to the entire popup
popup.addEventListener('mouseenter', () => {
popup.style.backgroundColor = isDarkMode() ? '#374151' : '#f0f0f0';
});
popup.addEventListener('mouseleave', () => {
popup.style.backgroundColor = isDarkMode() ? '#1f2937' : 'white';
});
// Create container for message and button
const container = document.createElement('div');
container.style.cssText = `
display: flex;
align-items: center;
position: relative;
`;
// Add message
const messageElement = document.createElement('div');
messageElement.style.cssText = `
color: ${isDarkMode() ? '#d1d5db' : '#666'};
font-size: 14px;
padding-right: 32px;
`;
messageElement.textContent = message;
container.appendChild(messageElement);
// Add unlock button with SVG icon
const button = document.createElement('button');
button.title = 'Unlock AliasVault';
button.style.cssText = `
position: absolute;
right: 0;
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #0066cc;
border-radius: 4px;
`;
button.innerHTML = `
`;
// Make the whole container clickable to open the popup.
container.addEventListener('click', () => {
chrome.runtime.sendMessage({ type: 'OPEN_POPUP' });
removeExistingPopup();
});
container.appendChild(button);
popup.appendChild(container);
// Position popup below input
const rect = input.getBoundingClientRect();
popup.style.top = `${rect.bottom + window.scrollY + 2}px`;
popup.style.left = `${rect.left + window.scrollX}px`;
/**
* Add event listener to document to close popup when clicking outside.
*/
const handleClickOutside = (event: MouseEvent): void => {
if (!popup.contains(event.target as Node)) {
removeExistingPopup();
document.removeEventListener('mousedown', handleClickOutside);
}
};
setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside);
}, 100);
document.body.appendChild(popup);
}
/**
* Remove existing popup
*/
function removeExistingPopup() : void {
const existing = document.getElementById('aliasvault-credential-popup');
if (existing) {
existing.remove();
}
}
/**
* Fill credential
*/
function fillCredential(credential: Credential) : void {
const forms = detectForms();
if (!forms.length) return;
const form = forms[0];
if (form.usernameField) {
form.usernameField.value = credential.Username;
triggerInputEvents(form.usernameField);
}
if (form.passwordField) {
form.passwordField.value = credential.Password;
triggerInputEvents(form.passwordField);
}
}
/**
* Trigger input events
*/
function triggerInputEvents(element: HTMLInputElement) : void {
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
/**
* Base64 encode binary data.
*/
function base64Encode(buffer: Uint8Array): string | null {
if (!buffer || typeof buffer !== 'object') {
return null;
}
try {
// Convert object to array of numbers
const byteArray = Object.values(buffer);
// Convert to binary string
const binary = String.fromCharCode.apply(null, byteArray as number[]);
// Use btoa to encode binary string to base64
return btoa(binary);
} catch (error) {
console.error('Error encoding to base64:', error);
return null;
}
}