import type { FieldPrediction, MLFieldFeatures, } from "../shared/ml"; export type { FieldPrediction, MLFieldFeatures } from "../shared/ml"; export class ContentMLDetector { private static instance: ContentMLDetector; private initialized = false; static getInstance(): ContentMLDetector { if (!ContentMLDetector.instance) { ContentMLDetector.instance = new ContentMLDetector(); } return ContentMLDetector.instance; } async init(): Promise { if (this.initialized) return; const response = await chrome.runtime.sendMessage({ type: "ML_INIT" }); this.initialized = Boolean((response as { ok?: boolean }).ok); } extractFeatures(input: HTMLInputElement): MLFieldFeatures { const inputType = input.type.toLowerCase(); const matchScore = (text: string, patterns: RegExp[]): number => { if (!text) return 0; const matches = patterns.reduce( (count, pattern) => count + (pattern.test(text) ? 1 : 0), 0 ); return Math.min((matches / patterns.length) * 3, 1); }; const userPatterns = [ /user/i, /login/i, /usr/i, /uname/i, /account/i, /acct/i, /signin/i, /sign-in/i, /session/i, /member/i, /alias/i, ]; const emailPatterns = [/email/i, /e-mail/i, /mail/i]; const passPatterns = [ /pass/i, /password/i, /pwd/i, /secret/i, /passphrase/i, /key/i, /credential/i, ]; const features: MLFieldFeatures = { type_text: inputType === "text" ? 1 : 0, type_email: inputType === "email" ? 1 : 0, type_password: inputType === "password" ? 1 : 0, type_tel: inputType === "tel" ? 1 : 0, type_number: inputType === "number" ? 1 : 0, type_search: inputType === "search" ? 1 : 0, type_url: inputType === "url" ? 1 : 0, type_other: [ "text", "email", "password", "tel", "number", "search", "url", ].includes(inputType) ? 0 : 1, auto_username: 0, auto_email: 0, auto_current_password: 0, auto_new_password: 0, auto_one_time_code: 0, auto_off: 0, auto_other: 0, name_has_user: matchScore(input.name, userPatterns), name_has_login: matchScore(input.name, [/login/i]), name_has_email: matchScore(input.name, emailPatterns), name_has_pass: matchScore(input.name, passPatterns), name_length: input.name.length / 50, id_has_user: matchScore(input.id, userPatterns), id_has_login: matchScore(input.id, [/login/i]), id_has_email: matchScore(input.id, emailPatterns), id_has_pass: matchScore(input.id, passPatterns), id_length: input.id.length / 50, placeholder_has_user: matchScore(input.placeholder, userPatterns), placeholder_has_email: matchScore(input.placeholder, emailPatterns), placeholder_has_pass: matchScore(input.placeholder, passPatterns), placeholder_length: input.placeholder.length / 100, aria_label_has_user: matchScore( input.getAttribute("aria-label") || "", userPatterns ), aria_label_has_email: matchScore( input.getAttribute("aria-label") || "", emailPatterns ), aria_label_has_pass: matchScore( input.getAttribute("aria-label") || "", passPatterns ), aria_label_length: (input.getAttribute("aria-label") || "").length / 100, parent_is_form: 0, parent_is_div: 0, parent_is_section: 0, sibling_count: 0, has_password_sibling: 0, has_email_sibling: 0, form_has_submit: 0, form_action_has_login: 0, is_required: input.required ? 1 : 0, has_placeholder: input.placeholder ? 1 : 0, has_aria_label: input.getAttribute("aria-label") ? 1 : 0, inputmode_numeric: input.inputMode === "numeric" ? 1 : 0, }; const auto = (input.getAttribute("autocomplete") || "").toLowerCase(); if (auto.includes("username")) features.auto_username = 1; else if (auto.includes("email")) features.auto_email = 1; else if (auto.includes("current-password")) features.auto_current_password = 1; else if (auto.includes("new-password")) features.auto_new_password = 1; else if (auto.includes("one-time-code")) features.auto_one_time_code = 1; else if (auto === "off") features.auto_off = 1; else features.auto_other = 1; const parent = input.parentElement; if (parent) { const parentName = parent.tagName.toLowerCase(); features.parent_is_form = parentName === "form" ? 1 : 0; features.parent_is_div = parentName === "div" ? 1 : 0; features.parent_is_section = parentName === "section" ? 1 : 0; const siblings = Array.from(parent.children).filter( (el) => el.tagName === "INPUT" ); features.sibling_count = siblings.length / 10; for (const sibling of siblings) { if (sibling === input) continue; const siblingType = (sibling as HTMLInputElement).type; if (siblingType === "password") features.has_password_sibling = 1; if (siblingType === "email") features.has_email_sibling = 1; } } const form = input.closest("form"); if (form) { features.form_has_submit = form.querySelector( 'button[type="submit"], input[type="submit"]' ) ? 1 : 0; const action = form.getAttribute("action") || ""; features.form_action_has_login = /login|signin|auth|session/i.test(action) ? 1 : 0; } return features; } async predictBatch( inputs: HTMLInputElement[] ): Promise<(FieldPrediction | null)[]> { try { const featuresArray = inputs.map((input) => this.extractFeatures(input)); const response = await chrome.runtime.sendMessage({ type: "ML_PREDICT_BATCH", payload: { featuresArray }, }); const data = (response as { ok?: boolean; data?: unknown }).data; return (response as { ok?: boolean }).ok && Array.isArray(data) ? (data as FieldPrediction[]) : inputs.map(() => null); } catch { return inputs.map(() => null); } } } export const contentMLDetector = ContentMLDetector.getInstance();