@protontech/autofill 0.0.22991789 → 0.0.33690782

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/constants/features.d.ts +1 -0
  2. package/constants/features.js +1 -0
  3. package/constants/heuristics.d.ts +5 -4
  4. package/constants/heuristics.js +5 -10
  5. package/constants/selectors.d.ts +13 -7
  6. package/constants/selectors.js +29 -17
  7. package/debug.d.ts +24 -1
  8. package/debug.js +15 -13
  9. package/dictionary/generate.js +30 -21
  10. package/dictionary/generated/dictionary.d.ts +18 -4
  11. package/dictionary/generated/dictionary.js +31 -17
  12. package/dictionary/source/dictionary.d.ts +4 -3
  13. package/dictionary/source/dictionary.js +155 -89
  14. package/dictionary/source/patterns.js +2 -2
  15. package/features/feature.d.ts +17 -0
  16. package/features/feature.js +72 -0
  17. package/features/feature.spec.d.ts +1 -0
  18. package/features/feature.spec.js +55 -0
  19. package/features/v1/abstract.field.d.ts +20401 -0
  20. package/features/v1/abstract.field.js +95 -0
  21. package/features/v1/abstract.form.d.ts +1226 -0
  22. package/features/v1/abstract.form.js +336 -0
  23. package/features/v1/field.email.d.ts +62 -0
  24. package/features/v1/field.email.js +27 -0
  25. package/features/v1/field.otp.d.ts +142 -0
  26. package/features/v1/field.otp.js +105 -0
  27. package/features/v1/field.password.d.ts +162 -0
  28. package/features/v1/field.password.js +81 -0
  29. package/features/v1/field.username-hidden.d.ts +62 -0
  30. package/features/v1/field.username-hidden.js +25 -0
  31. package/features/v1/field.username.d.ts +72 -0
  32. package/features/v1/field.username.js +31 -0
  33. package/features/v1/index.d.ts +4427 -0
  34. package/features/v1/index.js +22 -0
  35. package/features/v1/index.spec.d.ts +1 -0
  36. package/features/v1/index.spec.js +30 -0
  37. package/index.d.ts +5 -2
  38. package/index.js +10 -3
  39. package/models/perceptron/index.d.ts +2 -0
  40. package/models/perceptron/index.js +27 -0
  41. package/models/perceptron/params/email-model.json +53 -0
  42. package/models/perceptron/params/login-model.json +465 -0
  43. package/models/perceptron/params/new-password-model.json +133 -0
  44. package/models/perceptron/params/otp-model.json +117 -0
  45. package/models/perceptron/params/password-change-model.json +465 -0
  46. package/models/perceptron/params/password-model.json +133 -0
  47. package/models/perceptron/params/recovery-model.json +465 -0
  48. package/models/perceptron/params/register-model.json +465 -0
  49. package/models/perceptron/params/username-hidden-model.json +53 -0
  50. package/models/perceptron/params/username-model.json +61 -0
  51. package/models/prod_20240829/index.d.ts +2 -0
  52. package/models/prod_20240829/index.js +27 -0
  53. package/models/prod_20240829/params/email-model.json +45 -0
  54. package/models/prod_20240829/params/login-model.json +421 -0
  55. package/models/prod_20240829/params/new-password-model.json +125 -0
  56. package/models/prod_20240829/params/otp-model.json +153 -0
  57. package/models/prod_20240829/params/password-change-model.json +421 -0
  58. package/models/prod_20240829/params/password-model.json +125 -0
  59. package/models/prod_20240829/params/recovery-model.json +421 -0
  60. package/models/prod_20240829/params/register-model.json +421 -0
  61. package/models/prod_20240829/params/username-hidden-model.json +41 -0
  62. package/models/prod_20240829/params/username-model.json +45 -0
  63. package/models/random_forest/index.d.ts +2 -0
  64. package/models/random_forest/index.js +27 -0
  65. package/models/random_forest/params/email-model.json +1456 -0
  66. package/models/random_forest/params/login-model.json +4194 -0
  67. package/models/random_forest/params/new-password-model.json +1448 -0
  68. package/models/random_forest/params/otp-model.json +2004 -0
  69. package/models/random_forest/params/password-change-model.json +1422 -0
  70. package/models/random_forest/params/password-model.json +1292 -0
  71. package/models/random_forest/params/recovery-model.json +2754 -0
  72. package/models/random_forest/params/register-model.json +3678 -0
  73. package/models/random_forest/params/username-hidden-model.json +1108 -0
  74. package/models/random_forest/params/username-model.json +1052 -0
  75. package/models/v1/index.d.ts +2 -0
  76. package/models/v1/index.js +35 -0
  77. package/package.json +14 -15
  78. package/rules/v1/index.d.ts +3 -0
  79. package/rules/v1/index.js +69 -0
  80. package/rulesets.d.ts +2 -2
  81. package/rulesets.js +11 -10
  82. package/types/index.d.ts +72 -17
  83. package/types/index.js +35 -2
  84. package/utils/attributes.js +1 -1
  85. package/utils/clustering.js +18 -5
  86. package/utils/credit-card.d.ts +32 -0
  87. package/utils/credit-card.js +259 -0
  88. package/utils/credit-card.samples.spec.d.ts +1 -0
  89. package/utils/credit-card.samples.spec.js +452 -0
  90. package/utils/credit-card.spec.d.ts +1 -0
  91. package/utils/credit-card.spec.js +296 -0
  92. package/utils/dom.d.ts +3 -2
  93. package/utils/dom.js +12 -7
  94. package/utils/exclusion.d.ts +1 -0
  95. package/utils/exclusion.js +22 -10
  96. package/utils/extract.d.ts +1 -0
  97. package/utils/extract.js +26 -7
  98. package/utils/fathom.d.ts +10 -23
  99. package/utils/fathom.js +7 -12
  100. package/utils/field.d.ts +12 -4
  101. package/utils/field.js +25 -14
  102. package/utils/flags.d.ts +9 -3
  103. package/utils/flags.js +27 -9
  104. package/utils/form.d.ts +16 -5
  105. package/utils/form.js +35 -14
  106. package/utils/identity.d.ts +12 -21
  107. package/utils/identity.js +66 -41
  108. package/utils/identity.samples.spec.d.ts +1 -0
  109. package/utils/identity.samples.spec.js +28 -0
  110. package/utils/iframe.d.ts +2 -0
  111. package/utils/iframe.js +22 -0
  112. package/utils/overrides.d.ts +19 -0
  113. package/utils/overrides.js +40 -0
  114. package/utils/prepass.js +6 -4
  115. package/utils/re.d.ts +19 -4
  116. package/utils/re.js +22 -7
  117. package/utils/re.spec.d.ts +1 -0
  118. package/utils/re.spec.js +62 -0
  119. package/utils/shadow-dom.d.ts +8 -0
  120. package/utils/shadow-dom.js +53 -0
  121. package/utils/shadow-dom.spec.d.ts +1 -0
  122. package/utils/shadow-dom.spec.js +215 -0
  123. package/utils/visible.d.ts +3 -2
  124. package/utils/visible.js +42 -22
  125. package/cli.d.ts +0 -2
  126. package/cli.js +0 -128
  127. package/features/abstract.field.d.ts +0 -123
  128. package/features/abstract.field.js +0 -63
  129. package/features/abstract.form.d.ts +0 -98
  130. package/features/abstract.form.js +0 -281
  131. package/features/field.email.d.ts +0 -18
  132. package/features/field.email.js +0 -43
  133. package/features/field.otp.d.ts +0 -36
  134. package/features/field.otp.js +0 -116
  135. package/features/field.password.d.ts +0 -35
  136. package/features/field.password.js +0 -104
  137. package/features/field.username-hidden.d.ts +0 -15
  138. package/features/field.username-hidden.js +0 -40
  139. package/features/field.username.d.ts +0 -16
  140. package/features/field.username.js +0 -41
  141. package/features/form.combined.d.ts +0 -1
  142. package/features/form.combined.js +0 -6
  143. package/trainees/field.email.d.ts +0 -2
  144. package/trainees/field.email.js +0 -16
  145. package/trainees/field.identity.d.ts +0 -2
  146. package/trainees/field.identity.js +0 -9
  147. package/trainees/field.otp.d.ts +0 -2
  148. package/trainees/field.otp.js +0 -16
  149. package/trainees/field.password.current.d.ts +0 -2
  150. package/trainees/field.password.current.js +0 -16
  151. package/trainees/field.password.new.d.ts +0 -2
  152. package/trainees/field.password.new.js +0 -16
  153. package/trainees/field.username-hidden.d.ts +0 -2
  154. package/trainees/field.username-hidden.js +0 -22
  155. package/trainees/field.username.d.ts +0 -2
  156. package/trainees/field.username.js +0 -16
  157. package/trainees/form.login.d.ts +0 -2
  158. package/trainees/form.login.js +0 -16
  159. package/trainees/form.noop.d.ts +0 -1
  160. package/trainees/form.noop.js +0 -7
  161. package/trainees/form.password-change.d.ts +0 -2
  162. package/trainees/form.password-change.js +0 -16
  163. package/trainees/form.recovery.d.ts +0 -2
  164. package/trainees/form.recovery.js +0 -16
  165. package/trainees/form.register.d.ts +0 -2
  166. package/trainees/form.register.js +0 -16
  167. package/trainees/index.d.ts +0 -9
  168. package/trainees/index.js +0 -72
  169. package/trainees/results/result.email.d.ts +0 -2
  170. package/trainees/results/result.email.js +0 -17
  171. package/trainees/results/result.login.d.ts +0 -2
  172. package/trainees/results/result.login.js +0 -110
  173. package/trainees/results/result.new-password.d.ts +0 -2
  174. package/trainees/results/result.new-password.js +0 -36
  175. package/trainees/results/result.otp.d.ts +0 -2
  176. package/trainees/results/result.otp.js +0 -43
  177. package/trainees/results/result.password-change.d.ts +0 -2
  178. package/trainees/results/result.password-change.js +0 -110
  179. package/trainees/results/result.password.d.ts +0 -2
  180. package/trainees/results/result.password.js +0 -36
  181. package/trainees/results/result.recovery.d.ts +0 -2
  182. package/trainees/results/result.recovery.js +0 -110
  183. package/trainees/results/result.register.d.ts +0 -2
  184. package/trainees/results/result.register.js +0 -110
  185. package/trainees/results/result.username-hidden.d.ts +0 -2
  186. package/trainees/results/result.username-hidden.js +0 -15
  187. package/trainees/results/result.username.d.ts +0 -2
  188. package/trainees/results/result.username.js +0 -16
  189. package/utils/memoize.d.ts +0 -5
  190. package/utils/memoize.js +0 -12
package/utils/field.js CHANGED
@@ -1,14 +1,23 @@
1
- import { MIN_AREA_SUBMIT_BTN } from "../constants/heuristics";
2
- import { inputCandidateSelector, kPasswordSelector, kUsernameSelector, otpSelector } from "../constants/selectors";
1
+ import { MIN_AREA_SUBMIT_BTN } from "@protontech/autofill/constants/heuristics";
2
+ import { inputCandidateSelector, kPasswordSelector, kUsernameSelector } from "@protontech/autofill/constants/selectors";
3
3
  import { and, any, not, or } from "./combinators";
4
4
  import { getAllFieldHaystacks } from "./extract";
5
5
  import { getParentFormFnode } from "./fathom";
6
6
  import { isClassifiable, isHidden, isProcessed } from "./flags";
7
7
  import { matchEmail, matchOAuth, matchUsername } from "./re";
8
8
  import { isVisible, isVisibleField } from "./visible";
9
+ export const isInput = (el) => el.tagName === "INPUT";
10
+ export const isInputField = (el) => {
11
+ if (!isInput(el))
12
+ return false;
13
+ return el.type !== "button" && el.type !== "submit";
14
+ };
15
+ export const isSelect = (el) => el.tagName === "SELECT";
16
+ export const isTextarea = (el) => el.tagName === "TEXTAREA";
9
17
  const isActiveFieldFNode = (fnode) => {
10
- const { visible, readonly, disabled } = fnode.noteFor("field");
11
- return visible && !readonly && !disabled;
18
+ const el = fnode.element;
19
+ const { visible } = fnode.noteFor("field");
20
+ return visible && !el.readOnly && !el.disabled;
12
21
  };
13
22
  export const splitFieldsByVisibility = (els) => {
14
23
  return els.reduce((acc, el) => {
@@ -19,19 +28,21 @@ export const splitFieldsByVisibility = (els) => {
19
28
  return acc;
20
29
  }, [[], []]);
21
30
  };
22
- const fType = (types) => (fnode) => types.includes(fnode.element.type);
31
+ export const fType = (type) => (fnode) => fnode.hasType(type);
32
+ const fInput = (types) => (fnode) => types.includes(fnode.element.type);
23
33
  const fMatch = (selector) => (fnode) => fnode.element.matches(selector);
24
- const fInputMode = (inputMode) => (fnode) => fnode.element.inputMode === inputMode;
34
+ const fMode = (mode) => (fnode) => fnode.element.inputMode === mode;
25
35
  const fActive = (fnode) => isActiveFieldFNode(fnode);
26
36
  const fList = (fnode) => fnode.element.getAttribute("aria-autocomplete") === "list" || fnode.element.role === "combobox";
27
- export const maybeEmail = and(not(fList), or(fType(["email", "text"]), fInputMode("email")), fActive);
28
- export const maybePassword = and(fMatch(kPasswordSelector), fActive);
29
- export const maybeOTP = and(not(fList), fMatch(otpSelector), fActive);
30
- export const maybeUsername = and(not(fList), or(and(not(fInputMode("email")), fType(["text", "tel"])), fMatch(kUsernameSelector)), fActive);
31
- export const maybeHiddenUsername = and(not(fList), fType(["email", "text", "hidden"]), not(fActive));
32
- export const isUsernameCandidate = (el) => !el.matches('input[type="email"]') && any(matchUsername)(getAllFieldHaystacks(el));
33
- export const isEmailCandidate = (el) => el.matches('input[type="email"]') || any(matchEmail)(getAllFieldHaystacks(el));
37
+ export const maybeEmail = and(not(fList), or(fInput(["email", "text"]), fMode("email")), fActive);
38
+ export const maybePassword = and(not(fList), fMatch(kPasswordSelector), fActive);
39
+ export const maybeOTP = and(fInput(["text", "number", "tel"]), fActive, not(fList));
40
+ export const maybeUsername = and(not(fList), or(and(not(fMode("email")), fInput(["text", "tel"])), fMatch(kUsernameSelector)), fActive);
41
+ export const maybeHiddenUsername = and(not(fList), fInput(["email", "text", "hidden"]), not(fActive));
42
+ export const isUsernameCandidate = (el) => el.type === "text" || (el.type === "tel" && any(matchUsername)(getAllFieldHaystacks(el)));
43
+ export const isEmailCandidate = (el) => el.type === "email" || (el.type === "text" && any(matchEmail)(getAllFieldHaystacks(el)));
34
44
  export const isOAuthCandidate = (el) => any(matchOAuth)(getAllFieldHaystacks(el));
45
+ export const isMFACandidate = (el) => ["text", "number", "tel"].includes(el.type);
35
46
  export const isBtnCandidate = (btn) => {
36
47
  if (btn.getAttribute("type") === "submit")
37
48
  return true;
@@ -46,5 +57,5 @@ export const isProcessableField = (input) => {
46
57
  const hidden = isHidden(input);
47
58
  return (!processed || hidden) && isVisibleField(input) && isVisible(input, { opacity: false });
48
59
  };
49
- export const isClassifiableField = (fnode) => isClassifiable(fnode.element) && getParentFormFnode(fnode) !== null;
60
+ export const isClassifiableField = (fnode) => fnode.element.tagName === "INPUT" && isClassifiable(fnode.element) && getParentFormFnode(fnode) !== null;
50
61
  export const selectInputCandidates = (target = document) => Array.from(target.querySelectorAll(inputCandidateSelector)).filter(isClassifiable);
package/utils/flags.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Fnode } from "@protontech/fathom";
1
+ import type { Fnode } from "@protontech/fathom";
2
2
  export declare const isCluster: (el: HTMLElement) => boolean;
3
3
  export declare const flagCluster: (el: HTMLElement) => void;
4
4
  export declare const isHidden: (el: HTMLElement) => boolean;
@@ -14,12 +14,18 @@ export declare const isProcessed: (el: HTMLElement) => boolean;
14
14
  export declare const flagAsProcessed: (el: HTMLElement) => boolean;
15
15
  export declare const removeProcessedFlag: (el: HTMLElement) => boolean;
16
16
  export declare const isPrediction: (el: HTMLElement) => boolean;
17
- export declare const removePredictionFlag: (el: HTMLElement) => boolean;
17
+ export declare const removePredictionFlag: (el: HTMLElement) => void;
18
18
  export declare const getParentFormPrediction: (el?: HTMLElement) => HTMLElement | null;
19
+ export declare const setCachedSubType: (_el: HTMLElement, subType: string) => void;
20
+ export declare const getCachedSubType: (el: HTMLElement) => string | undefined;
21
+ export declare const matchPredictedType: (type: string) => (str: string) => boolean;
19
22
  export declare const setCachedPredictionScore: (_el: HTMLElement, type: string, score: number) => void;
20
23
  export declare const getCachedPredictionScore: (type: string) => (fnode: Fnode) => number;
21
24
  export declare const isPredictedType: (type: string) => (fnode: Fnode) => boolean;
25
+ export declare const isPredictedForm: (value: Fnode) => boolean;
26
+ export declare const isPredictedField: (value: Fnode) => boolean;
22
27
  export declare const isClassifiable: (el: HTMLElement) => boolean;
23
- export declare const removeClassifierFlags: (el: HTMLElement, options: {
28
+ export declare const removeClassifierFlags: (target: HTMLElement, options: {
24
29
  preserveIgnored: boolean;
30
+ fields?: HTMLElement[];
25
31
  }) => void;
package/utils/flags.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { FORM_CLUSTER_ATTR, kFieldSelector } from "../constants/selectors";
2
+ import { FieldType, FormType } from "../types";
3
+ import { or } from "./combinators";
2
4
  import { closestParent } from "./dom";
3
5
  export const isCluster = (el) => el.getAttribute(FORM_CLUSTER_ATTR) !== null;
4
6
  export const flagCluster = (el) => el.setAttribute(FORM_CLUSTER_ATTR, "");
@@ -18,10 +20,19 @@ export const isProcessed = (el) => el.__PP_SEEN__ === true;
18
20
  export const flagAsProcessed = (el) => (el.__PP_SEEN__ = true);
19
21
  export const removeProcessedFlag = (el) => delete el.__PP_SEEN__;
20
22
  export const isPrediction = (el) => el.__PP_TYPE__ !== undefined;
21
- export const removePredictionFlag = (el) => delete el.__PP_TYPE__;
23
+ export const removePredictionFlag = (el) => {
24
+ delete el.__PP_TYPE__;
25
+ delete el.__PP_SUBTYPE__;
26
+ };
22
27
  export const getParentFormPrediction = (el) => (el ? closestParent(el, isPrediction) : null);
23
28
  const TYPE_SEPARATOR = ",";
24
29
  const SCORE_SEPARATOR = ":";
30
+ export const setCachedSubType = (_el, subType) => {
31
+ const el = _el;
32
+ el.__PP_SUBTYPE__ = subType;
33
+ };
34
+ export const getCachedSubType = (el) => el.__PP_SUBTYPE__;
35
+ export const matchPredictedType = (type) => (str) => { var _a; return ((_a = str.split(SCORE_SEPARATOR)) === null || _a === void 0 ? void 0 : _a[0]) === type; };
25
36
  export const setCachedPredictionScore = (_el, type, score) => {
26
37
  const el = _el;
27
38
  const currentType = el.__PP_TYPE__;
@@ -31,7 +42,7 @@ export const setCachedPredictionScore = (_el, type, score) => {
31
42
  return;
32
43
  }
33
44
  const types = currentType.split(TYPE_SEPARATOR);
34
- const existingIndex = types.findIndex((pred) => pred.startsWith(type));
45
+ const existingIndex = types.findIndex(matchPredictedType(type));
35
46
  if (existingIndex !== -1)
36
47
  types[existingIndex] = flag;
37
48
  else
@@ -42,7 +53,7 @@ export const getCachedPredictionScore = (type) => (fnode) => {
42
53
  const types = fnode.element.__PP_TYPE__;
43
54
  if (!types)
44
55
  return -1;
45
- const predForType = types.split(TYPE_SEPARATOR).find((pred) => pred.startsWith(type));
56
+ const predForType = types.split(TYPE_SEPARATOR).find(matchPredictedType(type));
46
57
  if (!predForType)
47
58
  return -1;
48
59
  const [, scoreStr] = predForType.split(SCORE_SEPARATOR);
@@ -50,11 +61,18 @@ export const getCachedPredictionScore = (type) => (fnode) => {
50
61
  return Number.isFinite(score) ? score : -1;
51
62
  };
52
63
  export const isPredictedType = (type) => (fnode) => getCachedPredictionScore(type)(fnode) !== -1;
64
+ export const isPredictedForm = or(...Object.values(FormType).map((type) => isPredictedType(type)));
65
+ export const isPredictedField = or(...Object.values(FieldType).map((type) => isPredictedType(type)));
53
66
  export const isClassifiable = (el) => !(isPrediction(el) || isIgnored(el) || attrIgnored(el));
54
- export const removeClassifierFlags = (el, options) => {
55
- removeProcessedFlag(el);
56
- removePredictionFlag(el);
57
- if (!options.preserveIgnored)
58
- removeIgnoredFlag(el);
59
- el.querySelectorAll(kFieldSelector).forEach((el) => removeClassifierFlags(el, options));
67
+ export const removeClassifierFlags = (target, options) => {
68
+ var _a;
69
+ const clean = (el) => {
70
+ removeProcessedFlag(el);
71
+ removePredictionFlag(el);
72
+ if (!options.preserveIgnored)
73
+ removeIgnoredFlag(el);
74
+ };
75
+ clean(target);
76
+ target.querySelectorAll(kFieldSelector).forEach(clean);
77
+ (_a = options.fields) === null || _a === void 0 ? void 0 : _a.forEach(clean);
60
78
  };
package/utils/form.d.ts CHANGED
@@ -1,7 +1,18 @@
1
+ import type { HTMLFieldElement } from "./field";
1
2
  export declare const getFormParent: (form: HTMLElement) => HTMLElement;
2
- export type FormInputIterator = ReturnType<typeof createInputIterator>;
3
- export declare const createInputIterator: (form: HTMLElement) => {
4
- prev(input: HTMLElement): HTMLElement | null;
5
- next(input: HTMLElement): HTMLElement | null;
6
- };
3
+ export interface FormInputIterator {
4
+ prev: (el: HTMLElement, tagNameFilter?: string) => HTMLElement | null;
5
+ next: (el: HTMLElement, tagNameFilter?: string) => HTMLElement | null;
6
+ }
7
+ export declare const createInputIterator: (form: HTMLElement) => FormInputIterator;
7
8
  export declare const selectFormCandidates: (root?: Document | HTMLElement) => HTMLElement[];
9
+ type FormComplexityOptions = {
10
+ fields: HTMLFieldElement[];
11
+ visibleFields: number;
12
+ nonVisibleFields: number;
13
+ hiddenFields: number;
14
+ buttons: number;
15
+ anchors: number;
16
+ };
17
+ export declare const getFormComplexity: (form: HTMLElement, options: FormComplexityOptions) => number;
18
+ export {};
package/utils/form.js CHANGED
@@ -1,25 +1,46 @@
1
- import { MAX_FORM_FIELD_WALK_UP } from "../constants/heuristics";
2
- import { formCandidateSelector, inputCandidateSelector } from "../constants/selectors";
1
+ import { MAX_FORM_FIELD_WALK_UP } from "@protontech/autofill/constants/heuristics";
2
+ import { formCandidateSelector, kInputIteratorSelector } from "@protontech/autofill/constants/selectors";
3
3
  import { walkUpWhile } from "./dom";
4
4
  import { attrIgnored, isIgnored } from "./flags";
5
5
  import { isVisibleField } from "./visible";
6
6
  export const getFormParent = (form) => walkUpWhile(form, MAX_FORM_FIELD_WALK_UP)((el) => el.querySelectorAll(formCandidateSelector).length <= 1);
7
7
  export const createInputIterator = (form) => {
8
- const formEls = Array.from(form.querySelectorAll(inputCandidateSelector)).filter(isVisibleField);
9
- return {
10
- prev(input) {
11
- var _a;
12
- const idx = formEls.indexOf(input);
13
- return idx === -1 ? null : (_a = formEls === null || formEls === void 0 ? void 0 : formEls[idx - 1]) !== null && _a !== void 0 ? _a : null;
14
- },
15
- next(input) {
16
- var _a;
17
- const idx = formEls.indexOf(input);
18
- return idx === -1 ? null : (_a = formEls === null || formEls === void 0 ? void 0 : formEls[idx + 1]) !== null && _a !== void 0 ? _a : null;
19
- },
8
+ const formEls = Array.from(form.querySelectorAll(kInputIteratorSelector)).filter(isVisibleField);
9
+ const getAdjacent = (el, offset, tagNameFilter) => {
10
+ var _a;
11
+ const idx = formEls.indexOf(el);
12
+ const res = idx === -1 ? null : ((_a = formEls === null || formEls === void 0 ? void 0 : formEls[idx + offset]) !== null && _a !== void 0 ? _a : null);
13
+ return res && tagNameFilter && (res === null || res === void 0 ? void 0 : res.tagName) !== tagNameFilter ? getAdjacent(res, offset, tagNameFilter) : res;
20
14
  };
15
+ const iterator = {
16
+ prev: (el, tagNameFilter) => getAdjacent(el, -1, tagNameFilter),
17
+ next: (el, tagNameFilter) => getAdjacent(el, 1, tagNameFilter),
18
+ };
19
+ return iterator;
21
20
  };
22
21
  export const selectFormCandidates = (root = document) => {
23
22
  const candidates = Array.from(root.querySelectorAll(formCandidateSelector));
24
23
  return candidates.filter((form) => !isIgnored(form) && !attrIgnored(form));
25
24
  };
25
+ export const getFormComplexity = (form, options) => {
26
+ const types = new Set();
27
+ let score = 0;
28
+ score += options.visibleFields * 2;
29
+ score += options.buttons * 1;
30
+ score += options.anchors * 0.5;
31
+ score += options.nonVisibleFields * 0.25;
32
+ score += options.hiddenFields * 0.1;
33
+ for (const field of options.fields) {
34
+ const type = field.tagName === "INPUT" ? field.type : field.tagName;
35
+ if (!types.has(type)) {
36
+ score += 0.2;
37
+ types.add(type);
38
+ }
39
+ }
40
+ score += form.querySelectorAll("fieldset, [role='group'], [role='radiogroup']").length * 0.2;
41
+ for (const el of form.querySelectorAll("div, section")) {
42
+ if (el.children.length > 1)
43
+ score += 0.2;
44
+ }
45
+ return score;
46
+ };
@@ -1,24 +1,15 @@
1
- import { Fnode } from "@protontech/fathom";
2
- export declare enum IdentityFieldType {
3
- FULLNAME = 1,
4
- FIRSTNAME = 2,
5
- MIDDLENAME = 3,
6
- LASTNAME = 4,
7
- TELEPHONE = 5,
8
- ADDRESS = 6,
9
- STATE = 7,
10
- CITY = 8,
11
- ZIPCODE = 9,
12
- ORGANIZATION = 10,
13
- COUNTRY = 11,
14
- EMAIL = 12
15
- }
16
- export declare const getIdentityHaystack: (input: HTMLInputElement) => string;
17
- export declare const getIdentityFieldType: (input: HTMLInputElement) => IdentityFieldType | undefined;
18
- export declare const maybeIdentity: (fnode: Fnode) => boolean;
19
- export declare const matchesIdentityField: (input: HTMLInputElement, options: {
20
- isFormLogin: boolean;
1
+ import type { Fnode } from "@protontech/fathom";
2
+ import { IdentityFieldType } from "@protontech/autofill/types";
3
+ import type { FormClassification } from "./fathom";
4
+ type IdentityFieldMatchParams = {
5
+ form: FormClassification;
21
6
  searchField: boolean;
22
7
  type: string | null;
23
8
  visible: boolean;
24
- }) => boolean;
9
+ };
10
+ export declare const getCachedIdentitySubType: (el: HTMLElement) => IdentityFieldType | undefined;
11
+ export declare const getIdentityHaystack: (input: HTMLInputElement) => string;
12
+ export declare const getIdentityFieldType: (input: HTMLInputElement) => IdentityFieldType | undefined;
13
+ export declare const matchIdentityField: (input: HTMLInputElement, { visible }: IdentityFieldMatchParams) => boolean;
14
+ export declare const isIdentity: (fnode: Fnode) => boolean;
15
+ export {};
package/utils/identity.js CHANGED
@@ -1,63 +1,88 @@
1
- import { matchAddress, matchCity, matchCountry, matchFirstName, matchFullName, matchLastName, matchMiddleName, matchOrganization, matchState, matchTelephone, matchZipCode, } from "./re";
1
+ import { IdentityFieldType, identityFields } from "@protontech/autofill/types";
2
+ import { getAutocompletes } from "./extract";
3
+ import { getCachedSubType, setCachedSubType } from "./flags";
4
+ import { matchAddress, matchCity, matchCountry, matchFirstName, matchFullName, matchLastName, matchMiddleName, matchOrganization, matchState, matchTelephone, matchZipCode } from "./re";
2
5
  import { sanitizeStringWithSpaces } from "./text";
3
- export var IdentityFieldType;
4
- (function (IdentityFieldType) {
5
- IdentityFieldType[IdentityFieldType["FULLNAME"] = 1] = "FULLNAME";
6
- IdentityFieldType[IdentityFieldType["FIRSTNAME"] = 2] = "FIRSTNAME";
7
- IdentityFieldType[IdentityFieldType["MIDDLENAME"] = 3] = "MIDDLENAME";
8
- IdentityFieldType[IdentityFieldType["LASTNAME"] = 4] = "LASTNAME";
9
- IdentityFieldType[IdentityFieldType["TELEPHONE"] = 5] = "TELEPHONE";
10
- IdentityFieldType[IdentityFieldType["ADDRESS"] = 6] = "ADDRESS";
11
- IdentityFieldType[IdentityFieldType["STATE"] = 7] = "STATE";
12
- IdentityFieldType[IdentityFieldType["CITY"] = 8] = "CITY";
13
- IdentityFieldType[IdentityFieldType["ZIPCODE"] = 9] = "ZIPCODE";
14
- IdentityFieldType[IdentityFieldType["ORGANIZATION"] = 10] = "ORGANIZATION";
15
- IdentityFieldType[IdentityFieldType["COUNTRY"] = 11] = "COUNTRY";
16
- IdentityFieldType[IdentityFieldType["EMAIL"] = 12] = "EMAIL";
17
- })(IdentityFieldType || (IdentityFieldType = {}));
18
- const IDENTITY_RE_MAP = [
19
- [IdentityFieldType.FIRSTNAME, matchFirstName],
20
- [IdentityFieldType.MIDDLENAME, matchMiddleName],
21
- [IdentityFieldType.LASTNAME, matchLastName],
22
- [IdentityFieldType.FULLNAME, matchFullName],
23
- [IdentityFieldType.TELEPHONE, matchTelephone],
24
- [IdentityFieldType.ORGANIZATION, matchOrganization],
25
- [IdentityFieldType.CITY, matchCity],
26
- [IdentityFieldType.ZIPCODE, matchZipCode],
27
- [IdentityFieldType.STATE, matchState],
28
- [IdentityFieldType.COUNTRY, matchCountry],
29
- [IdentityFieldType.ADDRESS, matchAddress],
6
+ const guard = (options, predicate) => (field, autocompletes, haystack) => {
7
+ if (!options.number && field.getAttribute("type") === "number")
8
+ return false;
9
+ if (options.autocompletes.some((match) => autocompletes.includes(match)))
10
+ return true;
11
+ return predicate(haystack);
12
+ };
13
+ const IDENTITY_MATCHERS = [
14
+ [IdentityFieldType.FIRSTNAME, guard({ autocompletes: ["given-name"] }, matchFirstName)],
15
+ [IdentityFieldType.MIDDLENAME, guard({ autocompletes: ["additional-name"] }, matchMiddleName)],
16
+ [IdentityFieldType.LASTNAME, guard({ autocompletes: ["family-name"] }, matchLastName)],
17
+ [IdentityFieldType.FULLNAME, guard({ autocompletes: ["name"] }, matchFullName)],
18
+ [IdentityFieldType.TELEPHONE, guard({ autocompletes: ["tel", "tel-national", "tel-local"], number: true }, matchTelephone)],
19
+ [IdentityFieldType.ORGANIZATION, guard({ autocompletes: ["organization"] }, matchOrganization)],
20
+ [IdentityFieldType.CITY, guard({ autocompletes: ["address-level2"] }, matchCity)],
21
+ [IdentityFieldType.ZIPCODE, guard({ autocompletes: ["postal-code"], number: true }, matchZipCode)],
22
+ [IdentityFieldType.STATE, guard({ autocompletes: ["address-level1"] }, matchState)],
23
+ [IdentityFieldType.COUNTRY, guard({ autocompletes: ["country-name"] }, matchCountry)],
24
+ [IdentityFieldType.ADDRESS, guard({ autocompletes: ["street-address", "address-line1"] }, matchAddress)],
30
25
  ];
31
26
  const IDENTITY_ATTRIBUTES = ["autocomplete", "name", "id", "data-bhw"];
32
27
  const IDENTITY_INPUT_TYPES = ["tel", "phone", "text", "number"];
28
+ export const getCachedIdentitySubType = (el) => {
29
+ const subType = getCachedSubType(el);
30
+ if (subType && identityFields.has(subType))
31
+ return subType;
32
+ };
33
33
  export const getIdentityHaystack = (input) => {
34
34
  const attrs = IDENTITY_ATTRIBUTES.map((attr) => { var _a; return (_a = input === null || input === void 0 ? void 0 : input.getAttribute(attr)) !== null && _a !== void 0 ? _a : ""; });
35
35
  return sanitizeStringWithSpaces(attrs.join(" "));
36
36
  };
37
37
  export const getIdentityFieldType = (input) => {
38
- var _a;
38
+ var _a, _b;
39
+ const cachedSubType = getCachedIdentitySubType(input);
40
+ if (cachedSubType)
41
+ return cachedSubType;
42
+ if (getCachedSubType(input) !== undefined)
43
+ return;
44
+ const type = input.getAttribute("type");
45
+ if (type && !IDENTITY_INPUT_TYPES.includes(type))
46
+ return;
47
+ if ((_a = input.getAttribute("autocomplete")) === null || _a === void 0 ? void 0 : _a.includes("email"))
48
+ return;
39
49
  const haystack = getIdentityHaystack(input);
40
- if (haystack)
41
- return (_a = IDENTITY_RE_MAP.find(([, test]) => test(haystack))) === null || _a === void 0 ? void 0 : _a[0];
50
+ const autocompletes = getAutocompletes(input);
51
+ if (haystack) {
52
+ const identityType = (_b = IDENTITY_MATCHERS.find(([, test]) => test(input, autocompletes, haystack))) === null || _b === void 0 ? void 0 : _b[0];
53
+ if (identityType)
54
+ setCachedSubType(input, identityType);
55
+ return identityType;
56
+ }
42
57
  };
43
58
  const isAutocompleteListInput = (el) => el.getAttribute("aria-autocomplete") === "list" || el.role === "combobox";
44
- export const maybeIdentity = (fnode) => {
45
- var _a;
46
- const fieldFeatures = fnode.noteFor("field");
47
- return (_a = fieldFeatures === null || fieldFeatures === void 0 ? void 0 : fieldFeatures.identityField) !== null && _a !== void 0 ? _a : false;
59
+ export const matchIdentityField = (input, { visible }) => {
60
+ if (!visible)
61
+ return false;
62
+ if (getCachedIdentitySubType(input))
63
+ return true;
64
+ const identityType = getIdentityFieldType(input);
65
+ if (!identityType)
66
+ return false;
67
+ setCachedSubType(input, identityType);
68
+ return true;
48
69
  };
49
- export const matchesIdentityField = (input, options) => {
50
- const { visible, isFormLogin, type, searchField } = options;
51
- if ((type && !IDENTITY_INPUT_TYPES.includes(type)) || !visible || isFormLogin)
70
+ export const isIdentity = (fnode) => {
71
+ const input = fnode.element;
72
+ const { isIdentity, isCC, searchField, isFormLogin, isFormRecovery, visible } = fnode.noteFor("field");
73
+ if (!visible)
74
+ return false;
75
+ if (isCC || !isIdentity)
52
76
  return false;
53
77
  const identityType = getIdentityFieldType(input);
54
78
  if (!identityType)
55
79
  return false;
80
+ const outlierForm = isFormLogin || isFormRecovery;
81
+ if (outlierForm)
82
+ return false;
56
83
  if (isAutocompleteListInput(input))
57
84
  return [IdentityFieldType.ADDRESS, IdentityFieldType.ZIPCODE].includes(identityType);
58
- if (type === "number")
59
- return [IdentityFieldType.TELEPHONE, IdentityFieldType.ZIPCODE].includes(identityType);
60
85
  if (searchField)
61
- return [IdentityFieldType.ADDRESS, IdentityFieldType.ZIPCODE, identityType === IdentityFieldType.CITY].includes(identityType);
86
+ return [IdentityFieldType.ADDRESS, IdentityFieldType.ZIPCODE, IdentityFieldType.CITY].includes(identityType);
62
87
  return true;
63
88
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { IdentityFieldType } from "@protontech/autofill/types";
2
+ import { getIdentityFieldType } from "./identity";
3
+ const fieldTest = (type, samples) => {
4
+ test.each(samples.map((html) => ({ html, preview: html.substring(0, 42) })))("$preview", ({ html }) => {
5
+ document.body.innerHTML = html;
6
+ const field = document.querySelector("input");
7
+ expect(getIdentityFieldType(field)).toBe(type);
8
+ });
9
+ };
10
+ Object.defineProperty(HTMLElement.prototype, "innerText", {
11
+ get() {
12
+ return this.textContent;
13
+ },
14
+ });
15
+ describe("Identity field samples", () => {
16
+ describe("IdentityFieldType.FIRSTNAME", () => {
17
+ const samples = [
18
+ `<div class="sc-d9ebd515-0 hLmoFN"><label data-ignore-a11y="true" for="shipping_firstname" id="shipping_firstname-label" class="sc-94eb08bc-0 bfsAop sc-d9ebd515-1 dIGbxh">Nombre</label><input autocomplete="on" name="shipping_firstname" maxlength="50" id="shipping_firstname" type="text" aria-invalid="true" aria-required="false" aria-disabled="false" data-test="shipping_firstname" class="sc-7af5671c-1 boqXeD" value=""></div>`,
19
+ ];
20
+ fieldTest(IdentityFieldType.FIRSTNAME, samples);
21
+ });
22
+ describe.only("IdentityFieldType.TELEPHONE", () => {
23
+ const samples = [
24
+ `<div class="sc-d9ebd515-0 hLmoFN"><label data-ignore-a11y="true" for=":rp:" id=":rp:-label" class="sc-94eb08bc-0 dEnlRJ sc-d9ebd515-1 hfkVHI">Número de teléfono.</label><input name="shipping_phoneNumber" autocomplete="on" id=":rp:" placeholder="" type="tel" aria-invalid="true" aria-required="false" aria-disabled="false" aria-describedby=":rp:-help" data-test="shipping_phoneNumber" class="sc-7af5671c-2 iteJpf" value="+34 "></div>`,
25
+ ];
26
+ fieldTest(IdentityFieldType.TELEPHONE, samples);
27
+ });
28
+ });
@@ -0,0 +1,2 @@
1
+ export declare const isIFrameField: (iframe: HTMLIFrameElement) => boolean;
2
+ export declare const isEditorFrame: () => boolean;
@@ -0,0 +1,22 @@
1
+ import { kEditorSelector } from "@protontech/autofill/constants/selectors";
2
+ import { matchIFrameField } from "./re";
3
+ import { sanitizeStringWithSpaces } from "./text";
4
+ export const isIFrameField = (iframe) => {
5
+ var _a, _b, _c;
6
+ const title = (_a = iframe.getAttribute("title")) !== null && _a !== void 0 ? _a : "";
7
+ const name = (_b = iframe.getAttribute("name")) !== null && _b !== void 0 ? _b : "";
8
+ const ariaLabel = (_c = iframe.getAttribute("aria-label")) !== null && _c !== void 0 ? _c : "";
9
+ const id = iframe.id;
10
+ const src = iframe.src.substring(0, 150);
11
+ const className = iframe.className.substring(0, 150);
12
+ const haystack = sanitizeStringWithSpaces(`${title} ${name} ${ariaLabel} ${id} ${src} ${className}`);
13
+ return matchIFrameField(haystack);
14
+ };
15
+ export const isEditorFrame = () => {
16
+ var _a;
17
+ if (document.designMode === "on")
18
+ return true;
19
+ if ((_a = document.body) === null || _a === void 0 ? void 0 : _a.isContentEditable)
20
+ return true;
21
+ return document.querySelector(kEditorSelector) !== null;
22
+ };
@@ -0,0 +1,19 @@
1
+ import type { AnyRule } from "@protontech/autofill/types";
2
+ type Override = {
3
+ form: HTMLElement;
4
+ formType: string;
5
+ fields: {
6
+ field: HTMLElement;
7
+ fieldType: string;
8
+ }[];
9
+ };
10
+ export declare const OVERRIDE_FORMS: Set<HTMLElement>;
11
+ export declare const OVERRIDE_FIELDS: Set<HTMLElement>;
12
+ export declare const addFormOverride: (el: HTMLElement) => Set<HTMLElement>;
13
+ export declare const addFieldOverride: (el: HTMLElement) => Set<HTMLElement>;
14
+ export declare const clearOverrides: () => void;
15
+ export declare const getOverridableForms: () => HTMLElement[];
16
+ export declare const getOverridableFields: () => HTMLElement[];
17
+ export declare const overrides: AnyRule[];
18
+ export declare const flagOverride: ({ form, formType, fields }: Override) => void;
19
+ export {};
@@ -0,0 +1,40 @@
1
+ import { domQuery, rule, type } from "@protontech/fathom";
2
+ import { flagCluster, isPredictedField, isPredictedForm, removePredictionFlag, setCachedPredictionScore } from "./flags";
3
+ import { isShadowElement } from "./shadow-dom";
4
+ import { isVisibleField, isVisibleForm } from "./visible";
5
+ export const OVERRIDE_FORMS = new Set();
6
+ export const OVERRIDE_FIELDS = new Set();
7
+ export const addFormOverride = (el) => OVERRIDE_FORMS.add(el);
8
+ export const addFieldOverride = (el) => OVERRIDE_FIELDS.add(el);
9
+ export const clearOverrides = () => {
10
+ OVERRIDE_FORMS.clear();
11
+ OVERRIDE_FIELDS.clear();
12
+ };
13
+ export const getOverridableForms = () => Array.from(OVERRIDE_FORMS);
14
+ export const getOverridableFields = () => Array.from(OVERRIDE_FIELDS);
15
+ const matchFormOverrides = () => domQuery(getOverridableForms);
16
+ const matchFieldOverrides = () => domQuery(getOverridableFields);
17
+ const acceptFormOverride = (fnode) => isPredictedForm(fnode) && isVisibleForm(fnode.element);
18
+ const acceptFieldOverride = (fnode) => isPredictedField(fnode) && isVisibleField(fnode.element);
19
+ export const overrides = [
20
+ rule(matchFormOverrides(), type("override-form"), {}),
21
+ rule(matchFieldOverrides(), type("override-field"), {}),
22
+ rule(type("override-form").when(acceptFormOverride), type("form-candidate"), {}),
23
+ rule(type("override-field").when(acceptFieldOverride), type("field-candidate"), {}),
24
+ ];
25
+ export const flagOverride = ({ form, formType, fields }) => {
26
+ if (isVisibleForm(form)) {
27
+ removePredictionFlag(form);
28
+ setCachedPredictionScore(form, formType, 1);
29
+ if (isShadowElement(form))
30
+ addFormOverride(form);
31
+ else if (form.tagName !== "FORM")
32
+ flagCluster(form);
33
+ fields.forEach(({ field, fieldType }) => {
34
+ removePredictionFlag(field);
35
+ setCachedPredictionScore(field, fieldType, 1);
36
+ if (isShadowElement(field))
37
+ addFieldOverride(field);
38
+ });
39
+ }
40
+ };
package/utils/prepass.js CHANGED
@@ -3,6 +3,7 @@ import { excludeClusterableNodes, excludeFields, excludeForms } from "./exclusio
3
3
  import { isProcessableField, selectInputCandidates } from "./field";
4
4
  import { isProcessed, removeClassifierFlags, removeHiddenFlag } from "./flags";
5
5
  import { selectFormCandidates } from "./form";
6
+ import { getOverridableFields, getOverridableForms } from "./overrides";
6
7
  import { isVisibleForm } from "./visible";
7
8
  export const prepass = (doc = document) => {
8
9
  excludeForms(doc);
@@ -11,7 +12,8 @@ export const prepass = (doc = document) => {
11
12
  resolveFormClusters(doc);
12
13
  };
13
14
  export const shouldRunClassifier = () => {
14
- const runForForms = selectFormCandidates().reduce((runDetection, form) => {
15
+ const formCandidates = getOverridableForms().concat(selectFormCandidates());
16
+ const shouldRunForForms = formCandidates.reduce((runDetection, form) => {
15
17
  if (isProcessed(form)) {
16
18
  const unprocessedFields = selectInputCandidates(form).some(isProcessableField);
17
19
  if (unprocessedFields)
@@ -24,8 +26,8 @@ export const shouldRunClassifier = () => {
24
26
  }
25
27
  return runDetection;
26
28
  }, false);
27
- if (runForForms)
29
+ if (shouldRunForForms)
28
30
  return true;
29
- const runForFields = selectInputCandidates().some(isProcessableField);
30
- return runForFields;
31
+ const fieldCandidates = getOverridableFields().concat(selectInputCandidates());
32
+ return fieldCandidates.some(isProcessableField);
31
33
  };
package/utils/re.d.ts CHANGED
@@ -37,11 +37,14 @@ export declare const matchPasswordCurrent: (value: string) => boolean;
37
37
  export declare const matchPasswordCurrentAttr: (value: string) => boolean;
38
38
  export declare const matchPasswordOutlier: (str: string) => boolean;
39
39
  export declare const matchHidden: (str: string) => boolean;
40
- export declare const matchMfaAction: (str: string) => boolean;
41
- export declare const matchMfa: (str: string) => boolean;
42
- export declare const matchMfaAttr: (str: string) => boolean;
40
+ export declare const matchTwoFa: (value: string) => boolean;
43
41
  export declare const matchOtpAttr: (str: string) => boolean;
44
- export declare const matchOtpOutlier: (value: string) => boolean;
42
+ export declare const matchOtpOutlier: (str: string) => boolean;
43
+ export declare const matchOtpFieldOutlier: (value: string) => boolean;
44
+ export declare const matchOtpOutlierAction: (str: string) => boolean;
45
+ export declare const matchAuthenticator: (str: string) => boolean;
46
+ export declare const matchMFAAttr: (str: string) => boolean;
47
+ export declare const matchMFA: (value: string) => boolean;
45
48
  export declare const matchNewsletter: (str: string) => boolean;
46
49
  export declare const matchNewsletterAttr: (value: string) => boolean;
47
50
  export declare const matchFullName: (value: string) => boolean;
@@ -55,4 +58,16 @@ export declare const matchZipCode: (str: string) => boolean;
55
58
  export declare const matchState: (str: string) => boolean;
56
59
  export declare const matchCountry: (value: string) => boolean;
57
60
  export declare const matchAddress: (value: string) => boolean;
61
+ export declare const matchCCName: (str: string) => boolean;
62
+ export declare const matchCCFirstName: (value: string) => boolean;
63
+ export declare const matchCCLastName: (value: string) => boolean;
64
+ export declare const matchCCNumber: (str: string) => boolean;
65
+ export declare const matchCCSecurityCode: (value: string) => boolean;
66
+ export declare const matchCCV: (str: string) => boolean;
67
+ export declare const matchCCOutlier: (value: string) => boolean;
68
+ export declare const matchCCExp: (value: string) => boolean;
69
+ export declare const matchCCExpMonth: (value: string) => boolean;
70
+ export declare const matchCCExpYear: (value: string) => boolean;
71
+ export declare const matchPayment: (str: string) => boolean;
72
+ export declare const matchIFrameField: (value: string) => boolean;
58
73
  export {};