/// Content script entry point. /// /// Detects login forms on the page by finding password fields and their /// associated username inputs. Injects small icons into detected fields /// and sets up a fill listener to receive credentials from the service worker. import { setupFillListener } from './fill'; import { injectFieldIcons } from './icon'; import { hookForms } from './capture'; /// Find password fields on the page and detect their associated username inputs. function detectLoginForms(): Array<{ password: HTMLInputElement; username: HTMLInputElement | null }> { const passwordFields = document.querySelectorAll('input[type="password"]'); const forms: Array<{ password: HTMLInputElement; username: HTMLInputElement | null }> = []; for (const pwField of passwordFields) { // Skip hidden or very small fields (likely honeypots). if (pwField.offsetWidth < 20 || pwField.offsetHeight < 10) continue; const username = findUsernameField(pwField); forms.push({ password: pwField, username }); } return forms; } /// Find the most likely username field associated with a password field. /// /// Priority: /// 1. autocomplete="username" in the same form /// 2. autocomplete="email" in the same form /// 3. type="email" in the same form /// 4. name/id matching /user|email|login|account/i in the same form /// 5. Nearest preceding visible text input (sibling or DOM-adjacent) function findUsernameField(pwField: HTMLInputElement): HTMLInputElement | null { const form = pwField.closest('form'); const scope = form ?? document; const inputs = scope.querySelectorAll('input'); // 1. autocomplete="username" for (const input of inputs) { if (input === pwField) continue; if (input.autocomplete === 'username') return input; } // 2. autocomplete="email" for (const input of inputs) { if (input === pwField) continue; if (input.autocomplete === 'email') return input; } // 3. type="email" for (const input of inputs) { if (input === pwField) continue; if (input.type === 'email') return input; } // 4. name/id matching common patterns const pattern = /user|email|login|account/i; for (const input of inputs) { if (input === pwField) continue; if (input.type === 'hidden' || input.type === 'password') continue; if (pattern.test(input.name) || pattern.test(input.id)) return input; } // 5. Nearest preceding visible text input const allInputs = Array.from(inputs); const pwIndex = allInputs.indexOf(pwField); for (let i = pwIndex - 1; i >= 0; i--) { const input = allInputs[i]; if (input.type === 'hidden' || input.type === 'password' || input.type === 'submit') continue; if (input.offsetWidth > 0 && input.offsetHeight > 0) return input; } return null; } /// Scan the page for login forms and inject icons. function scan(): void { const forms = detectLoginForms(); for (const { password, username } of forms) { injectFieldIcons(password, username); } hookForms(); } // --- Initialization --- // Set up the fill listener (receives credentials from service worker). setupFillListener(); // Initial scan. scan(); // Watch for DOM changes (SPA navigation, dynamically loaded forms). const observer = new MutationObserver(() => { scan(); }); observer.observe(document.body, { childList: true, subtree: true, });