Files
relicario/extension/src/content/detector.ts
adlee-was-taken baf6416805 feat: add credential capture with bar/toast prompts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:24:04 -04:00

104 lines
3.3 KiB
TypeScript

/// 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<HTMLInputElement>('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<HTMLInputElement>('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,
});