fill.ts

4.7 KB · raw

1import { isVisible } from "./detect";2import { extractDomain } from "../shared/domain";34function getRegistrableDomain(hostname: string): string {5  return extractDomain(hostname) ?? hostname;6}78function checkFormAction(form: HTMLFormElement | null, expectedDomain: string): boolean {9  if (!form?.action) return true;10  try {11    const actionUrl = new URL(form.action);12    const actionDomain = getRegistrableDomain(actionUrl.hostname);13    if (actionDomain !== expectedDomain) {14      return false;15    }16  } catch {}17  return true;18}1920export function fillFields(21  usernameEl: HTMLInputElement | null,22  passwordEl: HTMLInputElement | null,23  username: string,24  password: string,25  form?: HTMLFormElement | null,26  expectedDomain?: string,27  emailEl?: HTMLInputElement | null,28  email?: string29): void {30  if (expectedDomain && form && !checkFormAction(form, expectedDomain)) {31    return;32  }33  // Fill username regardless of visibility — on SPA step-by-step login flows34  // the username input may be visually hidden but still tracked by the form.35  if (usernameEl && username) {36    setNativeValue(usernameEl, username);37  }38  if (passwordEl && isVisible(passwordEl)) {39    setNativeValue(passwordEl, password);40  }41  if (emailEl && email && isVisible(emailEl)) {42    setNativeValue(emailEl, email);43  }44}4546// Distributes a TOTP code character-by-character into an ordered array of47// single-character input boxes (the individual digit entry pattern used by48// some 2FA flows). Triggers native input+change events on each box so49// React/Vue controlled inputs update correctly.50export function fillOTPDigitBoxes(boxes: HTMLInputElement[], code: string): void {51  const digits = code.replace(/\s/g, "").split("");52  boxes.forEach((box, i) => {53    if (i < digits.length) setNativeValue(box, digits[i]!);54  });55}5657export function fillTOTPCode(58  totpEl: HTMLInputElement | null,59  code: string60): boolean {61  if (!totpEl || !code || !isVisible(totpEl)) return false;62  if (totpEl.value.trim() !== "") return false;63  setNativeValue(totpEl, code);64  return true;65}6667// React tracks input values via a synthetic event system that does not see68// direct `.value =` assignment. We must call the native HTMLInputElement69// value setter (pulled off the prototype) and then dispatch the real input70// and change events so React re-reads the new value. Without this, the71// field reads as empty to the form's controlled-input state.72export function setNativeValue(73  el: HTMLInputElement | HTMLTextAreaElement,74  value: string75): void {76  const proto =77    el instanceof HTMLTextAreaElement78      ? window.HTMLTextAreaElement.prototype79      : window.HTMLInputElement.prototype;80  const nativeValueSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;81  nativeValueSetter?.call(el, value);82  el.dispatchEvent(new Event("input", { bubbles: true }));83  el.dispatchEvent(new Event("change", { bubbles: true }));84}8586export interface CardFillFields {87  number: HTMLInputElement | null;88  expMonth: HTMLInputElement | null;89  expYear: HTMLInputElement | null;90  expiryCombo: HTMLInputElement | null;91  cvc: HTMLInputElement | null;92  name: HTMLInputElement | null;93}9495export function fillCardForm(fields: CardFillFields, card: { cardNumber: string; expMonth: string; expYear: string; cvc: string; cardholderName: string }): void {96  const raw = card.cardNumber.replace(/\s/g, '');97  if (fields.number) setNativeValue(fields.number, formatCardNumber(raw));98  if (fields.expiryCombo) {99    setNativeValue(fields.expiryCombo, `${card.expMonth}/${card.expYear.slice(-2)}`);100  } else {101    if (fields.expMonth) setNativeValue(fields.expMonth, card.expMonth);102    if (fields.expYear) setNativeValue(fields.expYear, card.expYear);103  }104  if (fields.cvc) setNativeValue(fields.cvc, card.cvc);105  if (fields.name) setNativeValue(fields.name, card.cardholderName);106}107108export function formatCardNumber(raw: string): string {109  if (/^3[47]/.test(raw)) return raw.replace(/(\d{4})(\d{6})(\d{5})/, '$1 $2 $3');110  return raw.replace(/(\d{4})(?=\d)/g, '$1 ').trim();111}112113export function detectCardNetwork(number: string): import('../shared/types').CardNetwork {114  const n = number.replace(/\s/g, '');115  if (/^4/.test(n)) return 'visa';116  if (/^5[1-5]|^2[2-7]/.test(n)) return 'mastercard';117  if (/^3[47]/.test(n)) return 'amex';118  if (/^6(?:011|5)/.test(n)) return 'discover';119  return 'unknown';120}121122export function passesLuhn(number: string): boolean {123  const digits = number.replace(/\s/g, '').split('').map(Number);124  let sum = 0, isEven = false;125  for (let i = digits.length - 1; i >= 0; i--) {126    let d = digits[i]!;127    if (isEven) { d *= 2; if (d > 9) d -= 9; }128    sum += d;129    isEven = !isEven;130  }131  return sum % 10 === 0;132}133134export function extractLast4(number: string): string {135  return number.replace(/\s/g, '').slice(-4);136}