1import { extractDomain } from "../shared/domain";23export interface DetectedField {4 type: "username" | "password" | "email";5 el: HTMLInputElement;6}789const USERNAME_PATTERNS = /\b(user(name)?|e?mail(box)?|login|account|identifier|signin|sign.?in|credential)\b/i;10const PASSWORD_PATTERNS = /pass|passwd|pwd/i;111213const SKIP_INPUT_TYPES = new Set([14 "submit", "button", "reset", "image",15 "checkbox", "radio", "hidden", "file",16 "range", "color", "date", "time",17 "datetime-local", "month", "week",18]);1920function findLabelText(field: HTMLInputElement): string {21 const ariaLabel = field.getAttribute("aria-label") ?? "";22 if (ariaLabel) return ariaLabel.toLowerCase();23 const id = field.id;24 if (id) {25 const label = document.querySelector<HTMLLabelElement>(`label[for="${id}"]`);26 if (label) return label.textContent?.toLowerCase() ?? "";27 }28 const wrappingLabel = field.closest("label");29 return wrappingLabel?.textContent?.toLowerCase() ?? "";30}3132export function querySelectorAllDeep<T extends Element>(33 root: Document | ShadowRoot | Element,34 selector: string35): T[] {36 const results: T[] = [];37 const walk = (node: Document | ShadowRoot | Element) => {38 if (node instanceof Element) {39 results.push(...(node.querySelectorAll<T>(selector)));40 const shadowRoot = node.shadowRoot;41 if (shadowRoot) {42 shadowRoot.querySelectorAll<T>("*").forEach(walk);43 }44 }45 };46 root.querySelectorAll<T>("*").forEach(walk);47 return results;48}4950export function getDeepActiveInput(): HTMLInputElement | null {51 let current: Document | Element | null = document.activeElement;52 while (current) {53 if (current instanceof HTMLInputElement) return current;54 if (current instanceof Element && current.shadowRoot) {55 current = current.shadowRoot.activeElement;56 } else {57 break;58 }59 }60 return null;61}6263export function isLoginForm(fields: DetectedField[]): boolean {64 const hasPassword = fields.some((f) => f.type === "password");65 const hasUsernameOrEmail = fields.some((f) => f.type === "username" || f.type === "email");66 if (hasPassword && hasUsernameOrEmail) return true;67 const singlePassword = fields.filter((f) => f.type === "password");68 if (singlePassword.length === 1 && singlePassword[0].el.autocomplete === "current-password") {69 return true;70 }71 return false;72}7374export function isSignupForm(fields: DetectedField[]): boolean {75 const passwords = fields.filter((f) => f.type === "password");76 if (passwords.length >= 2) return true;77 if (passwords.some((f) => f.el.autocomplete === "new-password")) return true;78 if (79 passwords.some((f) => {80 const id = (f.el.id + f.el.name).toLowerCase();81 return /new|confirm/i.test(id);82 })83 ) {84 return true;85 }86 return false;87}8889export function detectLoginFields(root: Document | ShadowRoot = document): DetectedField[] {90 const inputs = Array.from(querySelectorAllDeep<HTMLInputElement>(root, "input"));91 const fields: DetectedField[] = [];9293 for (const el of inputs) {94 if (!isVisible(el)) continue;95 const type = el.type.toLowerCase();96 97 if (SKIP_INPUT_TYPES.has(type)) continue;98 const ac = el.autocomplete;99 const name = (el.name + el.id + el.placeholder).toLowerCase();100101 if (ac === "current-password" || ac === "new-password" || type === "password") {102 fields.push({ type: "password", el });103 continue;104 }105106 if (ac === "username" || ac === "email" || type === "email") {107 fields.push({ type: "username", el });108 continue;109 }110111 if (USERNAME_PATTERNS.test(name)) {112 fields.push({ type: "username", el });113 continue;114 }115116 const labelText = findLabelText(el);117 if (!fields.some((f) => f.el === el) && USERNAME_PATTERNS.test(labelText)) {118 fields.push({ type: "username", el });119 }120 }121122 123 124 125 const hasUsername = fields.some((f) => f.type === "username" || f.type === "email");126 const pwField = fields.find((f) => f.type === "password");127 if (!hasUsername && pwField) {128 const allInputs = Array.from(inputs);129 const pwIndex = allInputs.indexOf(pwField.el);130 for (let i = 0; i < pwIndex; i++) {131 const candidate = allInputs[i];132 const candidateType = candidate.type.toLowerCase();133 if (134 (candidateType === "text" || candidateType === "email" || candidateType === "") &&135 isVisible(candidate) &&136 !SKIP_INPUT_TYPES.has(candidateType) &&137 !fields.some((f) => f.el === candidate)138 ) {139 fields.push({ type: "username", el: candidate });140 break; 141 }142 }143 }144145 return fields;146}147148export function isVisible(el: HTMLElement): boolean {149 const s = getComputedStyle(el);150 const r = el.getBoundingClientRect();151 return (152 s.display !== "none" &&153 s.visibility !== "hidden" &&154 parseFloat(s.opacity) > 0 &&155 r.width > 0 &&156 r.height > 0157 );158}159160export function getPageDomain(): string {161 return extractDomain(location.hostname) ?? location.hostname;162}