1export interface FieldScore {2 element: HTMLInputElement;3 score: number;4 fieldType: "username" | "password" | "email" | "totp" | "unknown";5 factors: {6 autocompleteMatch: number;7 typeMatch: number;8 namePattern: number;9 idPattern: number;10 placeholderPattern: number;11 ariaLabelPattern: number;12 siblingContext: number;13 };14}1516export interface FormFields {17 username: HTMLInputElement | null;18 password: HTMLInputElement | null;19 email: HTMLInputElement | null;20 totp: HTMLInputElement | null;21}2223const USERNAME_PATTERNS = [24 /user/i,25 /login/i,26 /usr/i,27 /uname/i,28 /account/i,29 /acct/i,30 /signin/i,31 /sign-in/i,32 /session/i,33 /member/i,34 /alias/i,35];3637const EMAIL_PATTERNS = [/email/i, /e-mail/i, /mail/i];3839const PASSWORD_PATTERNS = [40 /pass/i,41 /password/i,42 /pwd/i,43 /secret/i,44 /passphrase/i,45 /key/i,46 /credential/i,47];4849const TOTP_PATTERNS = [50 /totp/i,51 /otp/i,52 /2fa/i,53 /mfa/i,54 /two-factor/i,55 /twofactor/i,56 /authenticat/i,57 /verification/i,58 /code/i,59 /pin/i,60 /token/i,61];6263const AUTOCOMPLETE_VALUES: Record<64 "username" | "email" | "password" | "totp",65 string[]66> = {67 username: ["username", "login", "user"],68 email: ["email", "username"],69 password: ["current-password", "password", "new-password"],70 totp: ["one-time-code", "totp"],71};7273function matchesPattern(value: string, patterns: RegExp[]): boolean {74 return patterns.some((pattern) => pattern.test(value));75}7677function getPatternsForType(78 type: "username" | "password" | "email" | "totp"79): RegExp[] {80 switch (type) {81 case "username":82 return USERNAME_PATTERNS;83 case "password":84 return PASSWORD_PATTERNS;85 case "email":86 return EMAIL_PATTERNS;87 case "totp":88 return TOTP_PATTERNS;89 }90}9192function getLabelText(labelledById: string): string {93 const label = document.getElementById(labelledById);94 return label?.textContent || "";95}9697function analyzeSiblingContext(input: HTMLInputElement, patterns: RegExp[]): number {98 const parent = input.parentElement;99 if (!parent) return 0;100101 for (const sibling of Array.from(parent.children)) {102 if (sibling === input) continue;103 if (matchesPattern(sibling.textContent || "", patterns)) return 5;104 }105106 const grandparent = parent.parentElement;107 if (!grandparent) return 0;108 for (const sibling of Array.from(grandparent.children)) {109 if (sibling.contains(input)) continue;110 if (matchesPattern(sibling.textContent || "", patterns)) return 5;111 }112113 return 0;114}115116function calculateFieldScore(117 input: HTMLInputElement,118 type: "username" | "password" | "email" | "totp"119): number {120 const patterns = getPatternsForType(type);121 let score = 0;122 const autocomplete = input.getAttribute("autocomplete") || "";123 const expected = AUTOCOMPLETE_VALUES[type] || [];124125 if (expected.some((value) => autocomplete.includes(value))) score += 10;126 if (type === "password" && input.type === "password") score += 8;127 if (type === "email" && input.type === "email") score += 8;128 if (type === "totp" && (input.type === "tel" || input.inputMode === "numeric")) {129 score += 6;130 }131132 const name = input.name || "";133 const id = input.id || "";134 const placeholder = input.placeholder || "";135 const ariaLabel = input.getAttribute("aria-label") || "";136 const ariaLabelledBy = input.getAttribute("aria-labelledby") || "";137138 if (matchesPattern(name, patterns)) score += 6;139 if (matchesPattern(id, patterns)) score += 6;140 if (matchesPattern(placeholder, patterns)) score += 4;141 if (matchesPattern(ariaLabel, patterns)) score += 4;142 if (ariaLabelledBy && matchesPattern(getLabelText(ariaLabelledBy), patterns)) {143 score += 4;144 }145146 score += analyzeSiblingContext(input, patterns);147 return score;148}149150export function scoreInputField(input: HTMLInputElement): FieldScore {151 const types: Array<"username" | "password" | "email" | "totp"> = [152 "username",153 "password",154 "email",155 "totp",156 ];157158 const scores = types.map((type) => {159 const factors = {160 autocompleteMatch: 0,161 typeMatch: 0,162 namePattern: 0,163 idPattern: 0,164 placeholderPattern: 0,165 ariaLabelPattern: 0,166 siblingContext: 0,167 };168 const autocomplete = input.getAttribute("autocomplete") || "";169 const patterns = getPatternsForType(type);170 const name = input.name || "";171 const id = input.id || "";172 const placeholder = input.placeholder || "";173 const ariaLabel = input.getAttribute("aria-label") || "";174 if (AUTOCOMPLETE_VALUES[type]?.some((value) => autocomplete.includes(value))) {175 factors.autocompleteMatch = 10;176 }177 if (type === "password" && input.type === "password") factors.typeMatch = 8;178 if (type === "email" && input.type === "email") factors.typeMatch = 8;179 if (type === "totp" && (input.type === "tel" || input.inputMode === "numeric")) {180 factors.typeMatch = 6;181 }182 if (matchesPattern(name, patterns)) factors.namePattern = 6;183 if (matchesPattern(id, patterns)) factors.idPattern = 6;184 if (matchesPattern(placeholder, patterns)) factors.placeholderPattern = 4;185 if (matchesPattern(ariaLabel, patterns)) factors.ariaLabelPattern = 4;186 factors.siblingContext = analyzeSiblingContext(input, patterns);187188 return {189 type,190 score: calculateFieldScore(input, type),191 factors,192 };193 });194195 scores.sort((a, b) => b.score - a.score);196 const best = scores[0];197 return {198 element: input,199 score: best.score,200 fieldType: best.score > 5 ? best.type : "unknown",201 factors: best.factors,202 };203}204205export function identifyLoginFields(form: HTMLFormElement | null): FormFields {206 const inputs = Array.from(207 (form || document).querySelectorAll<HTMLInputElement>(208 'input:not([type="hidden"]):not([type="submit"]):not([type="button"])'209 )210 );211212 const scoredFields = inputs.map(scoreInputField).sort((a, b) => b.score - a.score);213 const result: FormFields = {214 username: null,215 password: null,216 email: null,217 totp: null,218 };219 const used = new Set<HTMLInputElement>();220221 for (const field of scoredFields) {222 if (used.has(field.element) || field.score < 3) continue;223 switch (field.fieldType) {224 case "username":225 if (!result.username) {226 result.username = field.element;227 used.add(field.element);228 }229 break;230 case "password":231 if (!result.password) {232 result.password = field.element;233 used.add(field.element);234 }235 break;236 case "email":237 if (!result.email && !result.username) {238 result.email = field.element;239 used.add(field.element);240 }241 break;242 case "totp":243 if (!result.totp) {244 result.totp = field.element;245 used.add(field.element);246 }247 break;248 }249 }250251 return result;252}