ml.ts

6.4 KB · raw

1import type { FieldPrediction, FieldType, MLFieldFeatures } from '../shared/ml';23// Output classes in the order the weight rows are written below.4const CLASSES: FieldType[] = ['password', 'email', 'username', 'totp', 'none'];56// Feature vector order — must stay in sync with ContentMLDetector.extractFeatures().7export const ML_FEATURE_KEYS: readonly (keyof MLFieldFeatures)[] = [8  // input type (8)9  'type_text', 'type_email', 'type_password', 'type_tel', 'type_number',10  'type_search', 'type_url', 'type_other',11  // autocomplete (7)12  'auto_username', 'auto_email', 'auto_current_password', 'auto_new_password',13  'auto_one_time_code', 'auto_off', 'auto_other',14  // name attribute (5)15  'name_has_user', 'name_has_login', 'name_has_email', 'name_has_pass', 'name_length',16  // id attribute (5)17  'id_has_user', 'id_has_login', 'id_has_email', 'id_has_pass', 'id_length',18  // placeholder (4)19  'placeholder_has_user', 'placeholder_has_email', 'placeholder_has_pass', 'placeholder_length',20  // aria-label (4)21  'aria_label_has_user', 'aria_label_has_email', 'aria_label_has_pass', 'aria_label_length',22  // DOM context (3)23  'parent_is_form', 'parent_is_div', 'parent_is_section',24  // siblings (3)25  'sibling_count', 'has_password_sibling', 'has_email_sibling',26  // form (2)27  'form_has_submit', 'form_action_has_login',28  // misc (3)29  'is_required', 'has_placeholder', 'has_aria_label',30  // input mode (1)31  'inputmode_numeric',32  // total: 4533];3435// Biases — prior log-odds for each class.36// Login fields are rarer than generic inputs, so we start skeptical.37const B: number[] = [38  -1.5, // password39  -1.5, // email40  -2.0, // username41  -3.0, // totp42   0.0, // none43];4445// W[class][feature] — one row per output class, 45 weights each.46// Groups within each row follow the FEATURES order above.47const W: number[][] = [48  // ── password ─────────────────────────────────────────────────────────────49  [50    // type51     0.0,  -1.0,  8.0, -0.5, -0.5, -2.0, -2.0, -1.0,52    // auto53    -1.0,  -1.0,  8.0,  7.0, -1.0,  0.5,  0.0,54    // name55    -0.5,   0.0, -0.5,  5.0,  0.0,56    // id57    -0.5,   0.0, -0.5,  5.0,  0.0,58    // placeholder59    -0.5,  -0.5,  4.0,  0.0,60    // aria-label61    -0.5,  -0.5,  4.0,  0.0,62    // parent63     0.3,   0.0,  0.0,64    // siblings65     0.2,  -0.5,  0.5,66    // form67     0.5,   0.5,68    // misc69     0.3,   0.2,  0.2,70    // inputmode71    -0.5,72  ],73  // ── email ─────────────────────────────────────────────────────────────────74  [75    // type76     0.3,   6.0, -2.0, -0.5, -1.0, -2.0, -1.0, -1.0,77    // auto78     1.0,   7.0, -2.0, -2.0, -2.0,  0.0,  0.0,79    // name80     0.0,   0.5,  4.5, -1.0,  0.0,81    // id82     0.0,   0.5,  4.5, -1.0,  0.0,83    // placeholder84     0.0,   4.0, -1.0,  0.0,85    // aria-label86     0.0,   4.0, -1.0,  0.0,87    // parent88     0.3,   0.0,  0.0,89    // siblings90     0.2,   0.5, -0.3,91    // form92     0.5,   0.5,93    // misc94     0.3,   0.2,  0.2,95    // inputmode96    -1.0,97  ],98  // ── username ──────────────────────────────────────────────────────────────99  [100    // type101     1.5,   0.5, -2.0, -0.5, -1.0, -2.0, -1.0, -1.0,102    // auto103     7.0,   1.0, -2.0, -2.0, -2.0,  0.0,  0.0,104    // name105     4.0,   3.5,  0.5, -1.0,  0.0,106    // id107     4.0,   3.5,  0.5, -1.0,  0.0,108    // placeholder109     3.5,   0.5, -0.5,  0.0,110    // aria-label111     3.5,   0.5, -0.5,  0.0,112    // parent113     0.3,   0.0,  0.0,114    // siblings115     0.2,   2.0,  0.5,116    // form117     0.5,   1.0,118    // misc119     0.3,   0.2,  0.2,120    // inputmode121    -1.0,122  ],123  // ── totp ──────────────────────────────────────────────────────────────────124  [125    // type126     0.0,  -1.0, -2.0,  2.0,  3.0, -2.0, -2.0, -1.0,127    // auto128    -2.0,  -2.0, -2.0, -2.0,  9.0,  0.5,  0.0,129    // name130    -0.5,  -0.5, -0.5, -0.5,  0.0,131    // id132    -0.5,  -0.5, -0.5, -0.5,  0.0,133    // placeholder134    -0.5,  -0.5, -0.5,  0.0,135    // aria-label136    -0.5,  -0.5, -0.5,  0.0,137    // parent138     0.2,   0.0,  0.0,139    // siblings140     0.2,  -0.3, -0.3,141    // form142     0.3,   0.0,143    // misc144     0.2,   0.1,  0.1,145    // inputmode146     4.0,147  ],148  // ── none ──────────────────────────────────────────────────────────────────149  [150    // type151     0.5,  -2.0, -3.0,  1.0,  1.5,  4.0,  4.0,  2.0,152    // auto153    -2.0,  -2.0, -3.0, -3.0, -3.0,  0.5,  0.2,154    // name155    -1.0,  -1.0, -1.0, -1.0,  0.1,156    // id157    -1.0,  -1.0, -1.0, -1.0,  0.1,158    // placeholder159    -0.5,  -0.5, -0.5,  0.0,160    // aria-label161    -0.5,  -0.5, -0.5,  0.0,162    // parent163    -0.5,   0.0,  0.0,164    // siblings165    -0.3,  -1.0, -0.5,166    // form167    -0.5,  -1.0,168    // misc169    -0.3,   0.0,  0.0,170    // inputmode171     1.0,172  ],173];174175function softmax(logits: number[]): number[] {176  const max = Math.max(...logits);177  const exps = logits.map(x => Math.exp(x - max));178  const sum = exps.reduce((a, b) => a + b, 0);179  return exps.map(x => x / sum);180}181182function predict(features: MLFieldFeatures): FieldPrediction {183  const vec = ML_FEATURE_KEYS.map(k => features[k]);184  const logits = W.map((row, ci) =>185    B[ci] + row.reduce((s, w, fi) => s + w * vec[fi], 0)186  );187  const probs = softmax(logits);188  const maxIdx = probs.indexOf(Math.max(...probs));189  return {190    type: CLASSES[maxIdx],191    confidence: probs[maxIdx],192    probabilities: Object.fromEntries(193      CLASSES.map((t, i) => [t, probs[i]])194    ) as Record<FieldType, number>,195  };196}197198export function isValidMLFeatureBatch(value: unknown, maxFields: number): value is MLFieldFeatures[] {199  return Array.isArray(value) && value.length <= maxFields && value.every((item) =>200    typeof item === "object" && item !== null && ML_FEATURE_KEYS.every((key) =>201      typeof (item as Record<string, unknown>)[key] === "number" &&202      Number.isFinite((item as Record<string, unknown>)[key])203    )204  );205}206207export function predictBatch(featuresArray: MLFieldFeatures[]): FieldPrediction[] {208  return featuresArray.map(predict);209}