@protontech/autofill 0.0.22991789 → 0.0.33835493

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 (176) 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 +18 -0
  16. package/features/feature.js +79 -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 +4428 -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 +7 -2
  38. package/index.js +11 -3
  39. package/models/perceptron/index.d.ts +2 -0
  40. package/models/perceptron/index.js +42 -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/random_forest/index.d.ts +2 -0
  52. package/models/random_forest/index.js +42 -0
  53. package/models/random_forest/params/email-model.json +1456 -0
  54. package/models/random_forest/params/login-model.json +4194 -0
  55. package/models/random_forest/params/new-password-model.json +1448 -0
  56. package/models/random_forest/params/otp-model.json +2004 -0
  57. package/models/random_forest/params/password-change-model.json +1422 -0
  58. package/models/random_forest/params/password-model.json +1292 -0
  59. package/models/random_forest/params/recovery-model.json +2754 -0
  60. package/models/random_forest/params/register-model.json +3678 -0
  61. package/models/random_forest/params/username-hidden-model.json +1108 -0
  62. package/models/random_forest/params/username-model.json +1052 -0
  63. package/package.json +17 -15
  64. package/rules/v1/index.d.ts +4 -0
  65. package/rules/v1/index.js +66 -0
  66. package/rulesets.d.ts +9 -2
  67. package/rulesets.js +14 -9
  68. package/types/index.d.ts +70 -17
  69. package/types/index.js +34 -1
  70. package/utils/attributes.js +1 -1
  71. package/utils/clustering.js +18 -5
  72. package/utils/credit-card.d.ts +32 -0
  73. package/utils/credit-card.js +259 -0
  74. package/utils/credit-card.samples.spec.d.ts +1 -0
  75. package/utils/credit-card.samples.spec.js +452 -0
  76. package/utils/credit-card.spec.d.ts +1 -0
  77. package/utils/credit-card.spec.js +296 -0
  78. package/utils/dom.d.ts +3 -2
  79. package/utils/dom.js +12 -7
  80. package/utils/exclusion.d.ts +1 -0
  81. package/utils/exclusion.js +22 -10
  82. package/utils/extract.d.ts +1 -0
  83. package/utils/extract.js +26 -7
  84. package/utils/fathom.d.ts +10 -23
  85. package/utils/fathom.js +7 -12
  86. package/utils/field.d.ts +12 -4
  87. package/utils/field.js +25 -14
  88. package/utils/flags.d.ts +9 -3
  89. package/utils/flags.js +27 -9
  90. package/utils/form.d.ts +16 -5
  91. package/utils/form.js +35 -14
  92. package/utils/identity.d.ts +12 -21
  93. package/utils/identity.js +66 -41
  94. package/utils/identity.samples.spec.d.ts +1 -0
  95. package/utils/identity.samples.spec.js +28 -0
  96. package/utils/iframe.d.ts +2 -0
  97. package/utils/iframe.js +22 -0
  98. package/utils/overrides.d.ts +19 -0
  99. package/utils/overrides.js +40 -0
  100. package/utils/prepass.js +6 -4
  101. package/utils/re.d.ts +19 -4
  102. package/utils/re.js +22 -7
  103. package/utils/re.spec.d.ts +1 -0
  104. package/utils/re.spec.js +62 -0
  105. package/utils/shadow-dom.d.ts +8 -0
  106. package/utils/shadow-dom.js +53 -0
  107. package/utils/shadow-dom.spec.d.ts +1 -0
  108. package/utils/shadow-dom.spec.js +215 -0
  109. package/utils/visible.d.ts +3 -2
  110. package/utils/visible.js +42 -22
  111. package/cli.d.ts +0 -2
  112. package/cli.js +0 -128
  113. package/features/abstract.field.d.ts +0 -123
  114. package/features/abstract.field.js +0 -63
  115. package/features/abstract.form.d.ts +0 -98
  116. package/features/abstract.form.js +0 -281
  117. package/features/field.email.d.ts +0 -18
  118. package/features/field.email.js +0 -43
  119. package/features/field.otp.d.ts +0 -36
  120. package/features/field.otp.js +0 -116
  121. package/features/field.password.d.ts +0 -35
  122. package/features/field.password.js +0 -104
  123. package/features/field.username-hidden.d.ts +0 -15
  124. package/features/field.username-hidden.js +0 -40
  125. package/features/field.username.d.ts +0 -16
  126. package/features/field.username.js +0 -41
  127. package/features/form.combined.d.ts +0 -1
  128. package/features/form.combined.js +0 -6
  129. package/trainees/field.email.d.ts +0 -2
  130. package/trainees/field.email.js +0 -16
  131. package/trainees/field.identity.d.ts +0 -2
  132. package/trainees/field.identity.js +0 -9
  133. package/trainees/field.otp.d.ts +0 -2
  134. package/trainees/field.otp.js +0 -16
  135. package/trainees/field.password.current.d.ts +0 -2
  136. package/trainees/field.password.current.js +0 -16
  137. package/trainees/field.password.new.d.ts +0 -2
  138. package/trainees/field.password.new.js +0 -16
  139. package/trainees/field.username-hidden.d.ts +0 -2
  140. package/trainees/field.username-hidden.js +0 -22
  141. package/trainees/field.username.d.ts +0 -2
  142. package/trainees/field.username.js +0 -16
  143. package/trainees/form.login.d.ts +0 -2
  144. package/trainees/form.login.js +0 -16
  145. package/trainees/form.noop.d.ts +0 -1
  146. package/trainees/form.noop.js +0 -7
  147. package/trainees/form.password-change.d.ts +0 -2
  148. package/trainees/form.password-change.js +0 -16
  149. package/trainees/form.recovery.d.ts +0 -2
  150. package/trainees/form.recovery.js +0 -16
  151. package/trainees/form.register.d.ts +0 -2
  152. package/trainees/form.register.js +0 -16
  153. package/trainees/index.d.ts +0 -9
  154. package/trainees/index.js +0 -72
  155. package/trainees/results/result.email.d.ts +0 -2
  156. package/trainees/results/result.email.js +0 -17
  157. package/trainees/results/result.login.d.ts +0 -2
  158. package/trainees/results/result.login.js +0 -110
  159. package/trainees/results/result.new-password.d.ts +0 -2
  160. package/trainees/results/result.new-password.js +0 -36
  161. package/trainees/results/result.otp.d.ts +0 -2
  162. package/trainees/results/result.otp.js +0 -43
  163. package/trainees/results/result.password-change.d.ts +0 -2
  164. package/trainees/results/result.password-change.js +0 -110
  165. package/trainees/results/result.password.d.ts +0 -2
  166. package/trainees/results/result.password.js +0 -36
  167. package/trainees/results/result.recovery.d.ts +0 -2
  168. package/trainees/results/result.recovery.js +0 -110
  169. package/trainees/results/result.register.d.ts +0 -2
  170. package/trainees/results/result.register.js +0 -110
  171. package/trainees/results/result.username-hidden.d.ts +0 -2
  172. package/trainees/results/result.username-hidden.js +0 -15
  173. package/trainees/results/result.username.d.ts +0 -2
  174. package/trainees/results/result.username.js +0 -16
  175. package/utils/memoize.d.ts +0 -5
  176. package/utils/memoize.js +0 -12
package/package.json CHANGED
@@ -1,29 +1,31 @@
1
1
  {
2
2
  "name": "@protontech/autofill",
3
- "version": "0.0.22991789",
3
+ "version": "0.0.33835493",
4
4
  "private": false,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
5
8
  "dependencies": {
6
- "@protontech/fathom": "0.0.22991789",
7
- "@types/node": "^22.13.4",
8
- "@types/regexgen": "^1.3.3",
9
- "commander": "^13.1.0",
10
- "prettier": "^3.5.1",
11
- "regexgen": "^1.3.0"
9
+ "@protontech/fathom": "0.0.33835493",
10
+ "@protontech/ml-inference": "0.0.33835493"
12
11
  },
13
12
  "devDependencies": {
14
- "@rollup/plugin-commonjs": "^28.0.2",
15
- "@rollup/plugin-node-resolve": "^16.0.0",
16
- "@rollup/plugin-terser": "^0.4.4",
17
- "@rollup/plugin-typescript": "^12.1.2",
18
13
  "@types/jest": "^29.5.14",
19
- "jest": "^29.7.0",
20
- "rollup": "^4.34.6",
21
- "rollup-plugin-dts": "^6.0.2",
14
+ "@types/node": "^22.13.4",
15
+ "@types/regexgen": "^1.3.3",
16
+ "jest-cli": "^30.2.0",
17
+ "jest-environment-jsdom": "^30.2.0",
18
+ "jest-junit": "^16.0.0",
19
+ "prettier": "^3.5.1",
20
+ "regexgen": "^1.3.0",
21
+ "ts-node": "^10.9.2",
22
+ "tsconfig-paths": "^4.2.0",
22
23
  "tslib": "^2.8.1"
23
24
  },
24
25
  "license": "MPL-2.0",
25
26
  "author": "Edvin Candon <edvin.candon@proton.ch>",
26
27
  "contributors": [
27
28
  "Guðmundur Heimisson <gudmundur.heimisson@proton.ch>"
28
- ]
29
+ ],
30
+ "type": "module"
29
31
  }
@@ -0,0 +1,4 @@
1
+ import type { FeatureProvider } from "@protontech/autofill/features/v1";
2
+ import type { AnyRule, DetectionClass } from "@protontech/autofill/types";
3
+ export declare const getBaseRules: (featureProvider: FeatureProvider) => readonly [import("@protontech/fathom").OutwardRule | import("@protontech/fathom").InwardRule, import("@protontech/fathom").OutwardRule | import("@protontech/fathom").InwardRule, ...(import("@protontech/fathom").OutwardRule | import("@protontech/fathom").InwardRule)[], import("@protontech/fathom").OutwardRule | import("@protontech/fathom").InwardRule];
4
+ export declare const getRulesProvider: (featureProvider: FeatureProvider) => Record<DetectionClass, readonly AnyRule[]>;
@@ -0,0 +1,66 @@
1
+ import { FEATURE_SEPARATOR } from "@protontech/autofill/constants/features";
2
+ import { formCandidateSelector } from "@protontech/autofill/constants/selectors";
3
+ import { computeFeatures } from "@protontech/autofill/features/feature";
4
+ import { FieldType, FormType } from "@protontech/autofill/types";
5
+ import { isCCInputField, isCCSelectField } from "@protontech/autofill/utils/credit-card";
6
+ import { featureScore, isNoopForm, outRuleWithCache, processFieldEffect, processFormEffect, withFnodeEl } from "@protontech/autofill/utils/fathom";
7
+ import { isClassifiableField, maybeEmail, maybeHiddenUsername, maybeOTP, maybePassword, maybeUsername } from "@protontech/autofill/utils/field";
8
+ import { isClassifiable } from "@protontech/autofill/utils/flags";
9
+ import { isIdentity } from "@protontech/autofill/utils/identity";
10
+ import { overrides } from "@protontech/autofill/utils/overrides";
11
+ import { isVisibleForm } from "@protontech/autofill/utils/visible";
12
+ import { dom, out, rule, type } from "@protontech/fathom";
13
+ const getRulesForFieldClass = (klass, featureComputer) => [
14
+ rule(type(`${klass}-field`), type(klass), {}),
15
+ ...featureComputer.features.map((feat) => rule(type(klass), featureScore(`${klass}-field`, feat), { name: `${klass}${FEATURE_SEPARATOR}${feat.name}` })),
16
+ ...outRuleWithCache("field-candidate", klass),
17
+ ];
18
+ const getRulesForFormClass = (klass, featureComputer) => [
19
+ rule(type("form"), type(klass), {}),
20
+ ...featureComputer.features.map((feat) => rule(type(klass), featureScore("form", feat), { name: `${klass}${FEATURE_SEPARATOR}${feat.name}` })),
21
+ ...outRuleWithCache("form-candidate", klass),
22
+ ];
23
+ const noopRules = [
24
+ rule(type("form").when(isNoopForm), type(FormType.NOOP), {}),
25
+ ...outRuleWithCache("form-candidate", FormType.NOOP, () => () => true),
26
+ ];
27
+ const identityRules = [rule(type("field").when(isIdentity), type("identity-field"), {}), rule(type("identity-field"), type(FieldType.IDENTITY), {}), ...outRuleWithCache("field-candidate", FieldType.IDENTITY, () => () => true)];
28
+ const ccRules = [
29
+ rule(type("field").when(isCCInputField), type("cc-field"), {}),
30
+ rule(type("field-candidate").when(isCCSelectField), type("cc-field"), {}),
31
+ rule(type("cc-field"), type(FieldType.CREDIT_CARD), {}),
32
+ ...outRuleWithCache("field-candidate", FieldType.CREDIT_CARD, () => () => true),
33
+ ];
34
+ export const getBaseRules = (featureProvider) => [
35
+ rule(dom(formCandidateSelector), type("form-candidate"), {}),
36
+ rule(dom("input"), type("field-candidate"), {}),
37
+ ...overrides,
38
+ rule(type("form-candidate").when(withFnodeEl(isClassifiable)), type("form-element"), {}),
39
+ rule(type("form-element").when(withFnodeEl(isVisibleForm)), type("form").note((fnode) => computeFeatures(featureProvider.form, fnode)), {}),
40
+ rule(type("form-element"), out("form").through(processFormEffect), {}),
41
+ ...noopRules,
42
+ rule(type("field-candidate").when(isClassifiableField), type("field").note((fnode) => computeFeatures(featureProvider.field, fnode)), {}),
43
+ rule(type("field").when(maybeUsername), type("username-field").note((fnode) => computeFeatures(featureProvider.username, fnode)), {}),
44
+ rule(type("field").when(maybeHiddenUsername), type("username-hidden-field").note((fnode) => computeFeatures(featureProvider["username-hidden"], fnode)), {}),
45
+ rule(type("field").when(maybeEmail), type("email-field").note((fnode) => computeFeatures(featureProvider.email, fnode)), {}),
46
+ rule(type("field").when(maybePassword), type("password-field").note((fnode) => computeFeatures(featureProvider.password, fnode)), {}),
47
+ rule(type("field").when(maybePassword), type("new-password-field").note((fnode) => computeFeatures(featureProvider["new-password"], fnode)), {}),
48
+ rule(type("field").when(maybeOTP), type("otp-field").note((fnode) => computeFeatures(featureProvider.otp, fnode)), {}),
49
+ ...identityRules,
50
+ ...ccRules,
51
+ rule(type("field"), out("field").through(processFieldEffect), {}),
52
+ ];
53
+ export const getRulesProvider = (featureProvider) => {
54
+ return {
55
+ email: getRulesForFieldClass("email", featureProvider.email),
56
+ otp: getRulesForFieldClass("otp", featureProvider.otp),
57
+ password: getRulesForFieldClass("password", featureProvider.password),
58
+ "new-password": getRulesForFieldClass("new-password", featureProvider["new-password"]),
59
+ "username-hidden": getRulesForFieldClass("username-hidden", featureProvider["username-hidden"]),
60
+ username: getRulesForFieldClass("username", featureProvider.username),
61
+ login: getRulesForFormClass("login", featureProvider.login),
62
+ "password-change": getRulesForFormClass("password-change", featureProvider["password-change"]),
63
+ recovery: getRulesForFormClass("recovery", featureProvider.recovery),
64
+ register: getRulesForFormClass("register", featureProvider.register),
65
+ };
66
+ };
package/rulesets.d.ts CHANGED
@@ -1,2 +1,9 @@
1
- declare const trainees: Map<string, {}>;
2
- export { trainees };
1
+ import type { Ruleset } from "@protontech/fathom";
2
+ import type { ModelName, ModelProvider } from "./types";
3
+ type RulesetRegistry<T extends ModelName = string> = {
4
+ providers: Record<T, ModelProvider>;
5
+ make: (key: T) => Ruleset;
6
+ };
7
+ export declare const rulesetMaker: (modelProvider: ModelProvider) => Ruleset;
8
+ export declare const createRulesetRegistry: <T extends ModelName>(providers: Record<T, ModelProvider>) => RulesetRegistry<T>;
9
+ export {};
package/rulesets.js CHANGED
@@ -1,10 +1,15 @@
1
- import definitions, { rulesetMaker } from "./trainees";
2
- const trainees = new Map();
3
- definitions.forEach((definition) => {
4
- trainees.set(definition.name, {
5
- coeffs: new Map(definition.coeffs),
6
- viewportSize: { width: 1200, height: 860 },
7
- rulesetMaker,
8
- });
1
+ import { ruleset } from "@protontech/fathom";
2
+ import { getBaseRules, getRulesProvider } from "./rules/v1";
3
+ import { detectionClasses } from "./types";
4
+ import { prepass } from "./utils/prepass";
5
+ export const rulesetMaker = (modelProvider) => {
6
+ const featureProvider = Object.fromEntries(Object.keys(modelProvider).map((klass) => [klass, modelProvider[klass].featureComputer]));
7
+ const rulesProvider = getRulesProvider(featureProvider);
8
+ const rules = [...getBaseRules(featureProvider), ...detectionClasses.flatMap((klass) => rulesProvider[klass])];
9
+ const models = new Map(detectionClasses.map((klass) => [klass, modelProvider[klass].model]));
10
+ return ruleset(rules, models, prepass);
11
+ };
12
+ export const createRulesetRegistry = (providers) => ({
13
+ providers,
14
+ make: (key) => rulesetMaker(providers[key]),
9
15
  });
10
- export { trainees };
package/types/index.d.ts CHANGED
@@ -1,22 +1,10 @@
1
- import { rule, ruleset } from "@protontech/fathom";
1
+ import type { Fnode, rule } from "@protontech/fathom";
2
+ import type { Features, Inferrer } from "@protontech/ml-inference";
2
3
  export type AnyRule = ReturnType<typeof rule>;
3
- export type Ruleset = ReturnType<typeof ruleset>;
4
- export type BoundRuleset = ReturnType<Ruleset["against"]>;
5
4
  export type Coeff = [string, number];
6
- export type Bias = [string, number];
7
- export type RulesetAggregation = {
8
- rules: AnyRule[];
9
- coeffs: Coeff[];
10
- biases: Bias[];
11
- };
12
- export type TrainingResults = {
5
+ export type PerceptronParams = {
13
6
  coeffs: Coeff[];
14
7
  bias: number;
15
- cutoff: number;
16
- };
17
- export type Trainee = TrainingResults & {
18
- name: string;
19
- getRules: () => AnyRule[];
20
8
  };
21
9
  export declare enum FormType {
22
10
  LOGIN = "login",
@@ -32,7 +20,72 @@ export declare enum FieldType {
32
20
  PASSWORD_CURRENT = "password",
33
21
  PASSWORD_NEW = "new-password",
34
22
  USERNAME = "username",
35
- USERNAME_HIDDEN = "username-hidden"
23
+ USERNAME_HIDDEN = "username-hidden",
24
+ CREDIT_CARD = "credit-card"
25
+ }
26
+ export declare enum CCFieldType {
27
+ NAME = "cc:name",
28
+ FIRSTNAME = "cc:firstname",
29
+ LASTNAME = "cc:lastname",
30
+ NUMBER = "cc:number",
31
+ CSC = "cc:cvc",
32
+ EXP = "cc:exp",
33
+ EXP_YEAR = "cc:exp-year",
34
+ EXP_MONTH = "cc:exp-month"
35
+ }
36
+ export declare enum IdentityFieldType {
37
+ FULLNAME = "id:fullname",
38
+ FIRSTNAME = "id:firstname",
39
+ MIDDLENAME = "id:middlname",
40
+ LASTNAME = "id:lastname",
41
+ TELEPHONE = "id:tel",
42
+ ADDRESS = "id:address",
43
+ STATE = "id:state",
44
+ CITY = "id:city",
45
+ ZIPCODE = "id:zipcode",
46
+ ORGANIZATION = "id:org",
47
+ COUNTRY = "id:country",
48
+ EMAIL = "id:email"
36
49
  }
37
- export declare const formTypes: FormType[];
38
50
  export declare const fieldTypes: FieldType[];
51
+ export declare const formTypes: FormType[];
52
+ export declare const ccFields: Set<string>;
53
+ export declare const identityFields: Set<string>;
54
+ export declare const fieldClasses: readonly ["email", "otp", "password", "new-password", "username", "username-hidden"];
55
+ export declare const formClasses: readonly ["login", "password-change", "recovery", "register"];
56
+ export declare const detectionClasses: readonly ["email", "otp", "password", "new-password", "username", "username-hidden", "login", "password-change", "recovery", "register"];
57
+ export declare const abstractDetectionClasses: readonly ["field", "form"];
58
+ export type FormClass = (typeof formClasses)[number];
59
+ export type FieldClass = (typeof fieldClasses)[number];
60
+ export type DetectionClass = (typeof detectionClasses)[number];
61
+ export type HeuristicDetectionClass = (typeof abstractDetectionClasses)[number];
62
+ export type AbstractFeatures<T = unknown> = Record<string, AbstractFeature<T>>;
63
+ export interface AbstractFeature<T = unknown> extends Feature<string, T, AbstractFeatures> {
64
+ }
65
+ export type InferParentComputeType<P extends AbstractFeatures> = {
66
+ [K in keyof P as P[K]["name"]]: ReturnType<P[K]["compute"]>;
67
+ };
68
+ export type InferFeatureName<F extends AbstractFeature> = F extends Feature<infer N, unknown, AbstractFeatures> ? N : never;
69
+ export type InferFeatureReturnType<F extends AbstractFeature> = F extends Feature<string, infer R, AbstractFeatures> ? R : never;
70
+ export type Feature<N extends string, R, P extends AbstractFeatures> = {
71
+ name: N;
72
+ parents: P;
73
+ compute(parents: InferParentComputeType<P>, fnode: Fnode): R;
74
+ };
75
+ export type ComputedFeatures<F extends AbstractFeatures> = {
76
+ [K in keyof F as F[K]["name"]]: ReturnType<F[K]["compute"]>;
77
+ };
78
+ export interface FeatureComputer<F extends AbstractFeatures = AbstractFeatures> {
79
+ compute(fnode: Fnode): ComputedFeatures<F>;
80
+ features: AbstractFeature[];
81
+ }
82
+ export type ModelProviderRegistry = Record<ModelName, ModelProvider>;
83
+ export type ModelName = string;
84
+ export type ModelProvider = {
85
+ [Klass in DetectionClass | HeuristicDetectionClass]: Klass extends HeuristicDetectionClass ? {
86
+ featureComputer: FeatureComputer;
87
+ } : {
88
+ featureComputer: FeatureComputer;
89
+ model: Inferrer<Features, number>;
90
+ };
91
+ };
package/types/index.js CHANGED
@@ -15,6 +15,39 @@ export var FieldType;
15
15
  FieldType["PASSWORD_NEW"] = "new-password";
16
16
  FieldType["USERNAME"] = "username";
17
17
  FieldType["USERNAME_HIDDEN"] = "username-hidden";
18
+ FieldType["CREDIT_CARD"] = "credit-card";
18
19
  })(FieldType || (FieldType = {}));
19
- export const formTypes = Object.values(FormType);
20
+ export var CCFieldType;
21
+ (function (CCFieldType) {
22
+ CCFieldType["NAME"] = "cc:name";
23
+ CCFieldType["FIRSTNAME"] = "cc:firstname";
24
+ CCFieldType["LASTNAME"] = "cc:lastname";
25
+ CCFieldType["NUMBER"] = "cc:number";
26
+ CCFieldType["CSC"] = "cc:cvc";
27
+ CCFieldType["EXP"] = "cc:exp";
28
+ CCFieldType["EXP_YEAR"] = "cc:exp-year";
29
+ CCFieldType["EXP_MONTH"] = "cc:exp-month";
30
+ })(CCFieldType || (CCFieldType = {}));
31
+ export var IdentityFieldType;
32
+ (function (IdentityFieldType) {
33
+ IdentityFieldType["FULLNAME"] = "id:fullname";
34
+ IdentityFieldType["FIRSTNAME"] = "id:firstname";
35
+ IdentityFieldType["MIDDLENAME"] = "id:middlname";
36
+ IdentityFieldType["LASTNAME"] = "id:lastname";
37
+ IdentityFieldType["TELEPHONE"] = "id:tel";
38
+ IdentityFieldType["ADDRESS"] = "id:address";
39
+ IdentityFieldType["STATE"] = "id:state";
40
+ IdentityFieldType["CITY"] = "id:city";
41
+ IdentityFieldType["ZIPCODE"] = "id:zipcode";
42
+ IdentityFieldType["ORGANIZATION"] = "id:org";
43
+ IdentityFieldType["COUNTRY"] = "id:country";
44
+ IdentityFieldType["EMAIL"] = "id:email";
45
+ })(IdentityFieldType || (IdentityFieldType = {}));
20
46
  export const fieldTypes = Object.values(FieldType);
47
+ export const formTypes = Object.values(FormType);
48
+ export const ccFields = new Set(Object.values(CCFieldType));
49
+ export const identityFields = new Set(Object.values(IdentityFieldType));
50
+ export const fieldClasses = ["email", "otp", "password", "new-password", "username", "username-hidden"];
51
+ export const formClasses = ["login", "password-change", "recovery", "register"];
52
+ export const detectionClasses = [...fieldClasses, ...formClasses];
53
+ export const abstractDetectionClasses = ["field", "form"];
@@ -1,5 +1,5 @@
1
1
  import { sanitizeStringWithSpaces } from "./text";
2
- export const TEXT_ATTRIBUTES = ["title", "label", "aria-label", "aria-labelledby", "aria-describedby", "placeholder", "autocomplete", "legend"];
2
+ export const TEXT_ATTRIBUTES = ["title", "aria-label", "aria-labelledby", "aria-describedby", "placeholder", "legend", "label"];
3
3
  export const EL_ATTRIBUTES = ["id", "class", "role", "jsaction", "ng-controller", "data-bind", "ng-model", "v-model", "v-bind", "data-testid", "href"];
4
4
  export const FORM_ATTRIBUTES = [EL_ATTRIBUTES, "name", "action"].flat();
5
5
  export const FIELD_ATTRIBUTES = [EL_ATTRIBUTES, "name", "inputmode"].flat();
@@ -1,10 +1,12 @@
1
1
  import { clusters as clustering } from "@protontech/fathom";
2
- import { buttonSelector, inputCandidateSelector, kButtonSubmitSelector, kDomGroupSelector, kFieldSelector } from "../constants/selectors";
2
+ import { inputCandidateSelector, kButtonSelector, kButtonSubmitSelector, kDomClusterSelector, kFieldSelector } from "../constants/selectors";
3
3
  import { findStackedParents, getCommonAncestor, getRectMinDistance, pruneNested, uniqueNodes, walkUpWhile } from "./dom";
4
4
  import { isBtnCandidate } from "./field";
5
5
  import { isVisibleField } from "./visible";
6
+ import { isTopFrame } from "./exclusion";
6
7
  import { flagCluster, isClassifiable } from "./flags";
7
8
  import { selectFormCandidates } from "./form";
9
+ import { isIFrameField } from "./iframe";
8
10
  const { clusters } = clustering;
9
11
  const CLUSTER_MAX_X_DIST = 50;
10
12
  const CLUSTER_MAX_Y_DIST = 275;
@@ -42,7 +44,13 @@ const compare = (elA, elB) => {
42
44
  };
43
45
  const handleSingletonCluster = (cluster) => {
44
46
  const node = cluster[0];
45
- return walkUpWhile(node, 5)((_, candidate) => candidate === node || candidate.querySelectorAll(buttonSelector).length === 0);
47
+ return walkUpWhile(node, 5)((parent, candidate) => {
48
+ if (parent.tagName === "HTML")
49
+ return false;
50
+ if (candidate === node)
51
+ return true;
52
+ return candidate.querySelectorAll(kButtonSelector).length === 0;
53
+ });
46
54
  };
47
55
  export const resolveFormClusters = (doc) => {
48
56
  const forms = selectFormCandidates(doc);
@@ -52,11 +60,12 @@ export const resolveFormClusters = (doc) => {
52
60
  const inputs = fieldsOfInterest.filter((field) => field.matches(inputCandidateSelector));
53
61
  if (inputs.length === 0 || inputs.length > CLUSTER_MAX_ELEMENTS)
54
62
  return [];
55
- const domGroups = Array.from(doc.querySelectorAll(kDomGroupSelector)).filter((el) => el !== document.body);
63
+ const domGroups = Array.from(doc.querySelectorAll(kDomClusterSelector)).filter((el) => el !== document.body);
56
64
  const positionedEls = findStackedParents(inputs, 20);
57
65
  const groups = pruneNested(domGroups.filter((el) => !positionedEls.some((stack) => el.contains(stack))).concat(positionedEls));
58
66
  const buttons = clusterable(Array.from(document.querySelectorAll(kButtonSubmitSelector)).filter(isBtnCandidate));
59
- const candidates = uniqueNodes(fieldsOfInterest, buttons);
67
+ const iframes = clusterable(Array.from(doc.querySelectorAll("iframe")).filter(isIFrameField));
68
+ const candidates = uniqueNodes(fieldsOfInterest, buttons, iframes);
60
69
  if (candidates.length > CLUSTER_MAX_ELEMENTS)
61
70
  return [];
62
71
  const groupByInput = new WeakMap(candidates.map((el) => [el, groups.find((group) => group.contains(el))]));
@@ -73,7 +82,11 @@ export const resolveFormClusters = (doc) => {
73
82
  });
74
83
  const ancestors = theClusters
75
84
  .map((cluster) => (cluster.length === 1 ? handleSingletonCluster(cluster) : cluster.reduce(getCommonAncestor)))
76
- .filter((ancestor) => document.body !== ancestor && ancestor.querySelectorAll(inputCandidateSelector).length > 0);
85
+ .filter((ancestor) => {
86
+ if (isTopFrame() && document.body === ancestor)
87
+ return false;
88
+ return ancestor.querySelectorAll(inputCandidateSelector).length > 0;
89
+ });
77
90
  const result = pruneNested(ancestors);
78
91
  result.forEach(flagCluster);
79
92
  context.cache = new WeakMap();
@@ -0,0 +1,32 @@
1
+ import { CCFieldType } from "@protontech/autofill/types";
2
+ import type { Fnode } from "@protontech/fathom";
3
+ export type CCFieldElement = HTMLInputElement | HTMLSelectElement;
4
+ export type CCExpirationMonthFormat = {
5
+ padding: boolean;
6
+ };
7
+ export type CCExpirationYearFormat = {
8
+ fullYear: boolean;
9
+ };
10
+ export type CCExpirationFormat = {
11
+ separator: string;
12
+ fullYear: boolean;
13
+ monthFirst: boolean;
14
+ };
15
+ type CCFieldMatchParams = {
16
+ visible: boolean;
17
+ };
18
+ export declare const CC_ATTRIBUTES: string[];
19
+ export declare const CC_INPUT_TYPES: string[];
20
+ export declare const getExpirationFormat: (field: HTMLElement, allowFallback?: boolean) => CCExpirationFormat | undefined;
21
+ export declare const formatExpirationDate: (month: string, year: string, { fullYear, separator, monthFirst }: CCExpirationFormat) => string;
22
+ export declare const getInputExpirationMonthFormat: (input: HTMLInputElement) => CCExpirationMonthFormat;
23
+ export declare const getInputExpirationYearFormat: (input: HTMLInputElement) => CCExpirationYearFormat;
24
+ export declare const getSelectExpirationYearFormat: (select: HTMLSelectElement) => CCExpirationYearFormat | undefined;
25
+ export declare const getSelectExpirationMonthFormat: (select: HTMLSelectElement) => CCExpirationMonthFormat | undefined;
26
+ export declare const getCCHaystack: (field: HTMLElement) => string;
27
+ export declare const getCachedCCSubtype: (el: HTMLElement) => CCFieldType | undefined;
28
+ export declare const getCCFieldType: (field: CCFieldElement) => CCFieldType | undefined;
29
+ export declare const matchCCFieldCandidate: (input: HTMLInputElement, { visible }: CCFieldMatchParams) => boolean;
30
+ export declare const isCCInputField: (fnode: Fnode) => boolean;
31
+ export declare const isCCSelectField: (fnode: Fnode) => boolean;
32
+ export {};
@@ -0,0 +1,259 @@
1
+ import { ccFields, CCFieldType } from "@protontech/autofill/types";
2
+ import { not, or } from "./combinators";
3
+ import { getLabelFor } from "./dom";
4
+ import { getAutocompletes } from "./extract";
5
+ import { getCachedSubType, isClassifiable, setCachedSubType } from "./flags";
6
+ import { matchCCExp, matchCCExpMonth, matchCCExpYear, matchCCFirstName, matchCCLastName, matchCCName, matchCCNumber, matchCCOutlier, matchCCSecurityCode, matchCCV } from "./re";
7
+ import { sanitizeString, sanitizeStringWithSpaces } from "./text";
8
+ import { isVisible } from "./visible";
9
+ export const CC_ATTRIBUTES = ["autocomplete", "name", "id", "class", "form", "aria-label", "aria-labelledby", "placeholder", "data-testid", "data-stripe", "data-recurly", "data-encrypted-name"];
10
+ export const CC_INPUT_TYPES = ["tel", "phone", "text", "number", "password"];
11
+ const CC_EXP_YEAR_FORMAT = ["YYYY", "AAAA", "YY", "AA"];
12
+ const CC_EXP_MONTH_FORMAT = ["MM", "LL"];
13
+ const CC_EXP_FULL_RE = /(mmyy|mmaa|yymm|aamm)/;
14
+ const CC_EXP_SEPARATOR = ["", "/", "-"];
15
+ const CC_EXP_DEFAULT_FORMAT = { separator: "/", fullYear: false, monthFirst: true };
16
+ const CC_EXP_FORMATS = [
17
+ { fullYear: true, monthFirst: true },
18
+ { fullYear: false, monthFirst: true },
19
+ { fullYear: true, monthFirst: false },
20
+ { fullYear: false, monthFirst: false },
21
+ ];
22
+ const MAX_LENGTH_FORMAT_MAP = {
23
+ 4: { separator: "", fullYear: false, monthFirst: true },
24
+ 5: { separator: "/", fullYear: false, monthFirst: true },
25
+ 6: { separator: "", fullYear: true, monthFirst: true },
26
+ 7: { separator: "/", fullYear: true, monthFirst: true },
27
+ };
28
+ const YEAR_OPTION = (new Date().getFullYear() + 1).toString();
29
+ const generateExpirationString = ({ fullYear, separator, monthFirst }) => {
30
+ const month = "01";
31
+ const year = fullYear ? "2025" : "25";
32
+ return monthFirst ? `${month}${separator}${year}` : `${year}${separator}${month}`;
33
+ };
34
+ const getExpirationFormatFromPattern = (pattern) => {
35
+ try {
36
+ const regex = new RegExp(`^${pattern}$`);
37
+ for (const format of CC_EXP_FORMATS) {
38
+ for (const separator of CC_EXP_SEPARATOR) {
39
+ const testString = generateExpirationString({ ...format, separator });
40
+ if (regex.test(testString)) {
41
+ return {
42
+ separator,
43
+ fullYear: format.fullYear,
44
+ monthFirst: format.monthFirst,
45
+ };
46
+ }
47
+ }
48
+ }
49
+ }
50
+ catch (_a) {
51
+ return undefined;
52
+ }
53
+ };
54
+ const getCCFormatHaystack = (input) => {
55
+ var _a;
56
+ const { placeholder, className, id, name } = input;
57
+ const ariaLabel = input.getAttribute("aria-label");
58
+ const label = (_a = getLabelFor(input)) === null || _a === void 0 ? void 0 : _a.innerText;
59
+ return [className, id, name, ariaLabel, placeholder.replace(/\s/g, ""), label === null || label === void 0 ? void 0 : label.replace(/\s/g, "")].filter(Boolean).join(" ").toUpperCase();
60
+ };
61
+ const getExpirationFormatFromMaxLength = (maxLength) => MAX_LENGTH_FORMAT_MAP[maxLength];
62
+ const getExpirationFormatFromAttributes = (input) => {
63
+ const haystack = getCCFormatHaystack(input);
64
+ for (const separator of CC_EXP_SEPARATOR) {
65
+ for (const year of CC_EXP_YEAR_FORMAT) {
66
+ for (const monthFirst of [true, false]) {
67
+ if (haystack.includes(monthFirst ? `MM${separator}${year}` : `${year}${separator}MM`)) {
68
+ return {
69
+ separator,
70
+ fullYear: year.length === 4,
71
+ monthFirst,
72
+ };
73
+ }
74
+ }
75
+ }
76
+ }
77
+ };
78
+ export const getExpirationFormat = (field, allowFallback = true) => {
79
+ if (field instanceof HTMLInputElement) {
80
+ const validMaxLength = field.maxLength >= 4;
81
+ const validPattern = field.pattern && (field.maxLength === -1 || validMaxLength);
82
+ return (getExpirationFormatFromAttributes(field) ||
83
+ (validPattern && getExpirationFormatFromPattern(field.pattern)) ||
84
+ (validMaxLength && getExpirationFormatFromMaxLength(field.maxLength)) ||
85
+ (allowFallback ? CC_EXP_DEFAULT_FORMAT : undefined));
86
+ }
87
+ };
88
+ export const formatExpirationDate = (month, year, { fullYear, separator, monthFirst }) => {
89
+ const formattedYear = fullYear ? year.slice(0, 4) : year.slice(-2);
90
+ const formattedMonth = month.padStart(2, "0");
91
+ const components = monthFirst ? [formattedMonth, formattedYear] : [formattedYear, formattedMonth];
92
+ return components.join(separator);
93
+ };
94
+ const getSelectOptions = (el) => Array.from(el.options)
95
+ .map((opt) => opt.value)
96
+ .filter(Boolean);
97
+ export const getInputExpirationMonthFormat = (input) => {
98
+ const haystack = getCCFormatHaystack(input);
99
+ if (CC_EXP_MONTH_FORMAT.some((pattern) => haystack.includes(pattern)))
100
+ return { padding: true };
101
+ return { padding: input.minLength === 2 || input.maxLength === 2 };
102
+ };
103
+ export const getInputExpirationYearFormat = (input) => {
104
+ if (input.minLength === 4)
105
+ return { fullYear: true };
106
+ const haystack = getCCFormatHaystack(input);
107
+ for (const year of CC_EXP_YEAR_FORMAT) {
108
+ if (haystack.includes(year))
109
+ return { fullYear: year.length === 4 };
110
+ }
111
+ return { fullYear: false };
112
+ };
113
+ export const getSelectExpirationYearFormat = (select) => {
114
+ const options = getSelectOptions(select);
115
+ if (options.some((value) => /^\d{4}$/.test(value)))
116
+ return { fullYear: true };
117
+ if (options.some((value) => /^\d{2}$/.test(value)))
118
+ return { fullYear: false };
119
+ };
120
+ export const getSelectExpirationMonthFormat = (select) => {
121
+ const options = getSelectOptions(select);
122
+ if (options.some((value) => /^0[1-9]$/.test(value)))
123
+ return { padding: true };
124
+ if (options.some((value) => /^([1-9]|1[0-2])$/.test(value)))
125
+ return { padding: false };
126
+ };
127
+ const AUTOCOMPLETE_OUTLIERS = ["given-name", "additional-name", "family-name", "name"];
128
+ const guard = (options, predicate) => (field, autocompletes, haystack) => {
129
+ if (!options.password && field.getAttribute("type") === "password")
130
+ return false;
131
+ if (autocompletes.includes(options.autocomplete))
132
+ return true;
133
+ if (autocompletes.some((autocomplete) => AUTOCOMPLETE_OUTLIERS.includes(autocomplete)))
134
+ return false;
135
+ return predicate(field, autocompletes, haystack) && not(matchCCOutlier)(haystack);
136
+ };
137
+ const notCCIdentityOutlier = (autocompletes) => {
138
+ if (autocompletes.includes("billing"))
139
+ return false;
140
+ if (autocompletes.includes("shipping"))
141
+ return false;
142
+ return true;
143
+ };
144
+ const isCCFirstName = (_, autocompletes, haystack) => matchCCFirstName(haystack) && notCCIdentityOutlier(autocompletes);
145
+ const isCCLastName = (_, autocompletes, haystack) => matchCCLastName(haystack) && notCCIdentityOutlier(autocompletes);
146
+ const isCCName = (_, autocomplete, haystack) => matchCCName(haystack) && notCCIdentityOutlier(autocomplete);
147
+ const isCCSecurityCode = (_, _auto, haystack) => or(matchCCV, matchCCSecurityCode)(haystack);
148
+ const isCCNumber = (_, __, haystack) => matchCCNumber(haystack);
149
+ const isCCExp = (field, _, haystack) => field instanceof HTMLInputElement && matchCCExp(haystack);
150
+ const isCCExpMonth = (field, autocompletes, haystack) => {
151
+ if (autocompletes.includes("cc-exp"))
152
+ return false;
153
+ if (CC_EXP_FULL_RE.test(haystack))
154
+ return false;
155
+ if (matchCCExpMonth(haystack))
156
+ return true;
157
+ if (field instanceof HTMLSelectElement && matchCCExp(haystack)) {
158
+ return field.options.length >= 12 && field.options.length <= 14;
159
+ }
160
+ return false;
161
+ };
162
+ const isCCExpYear = (field, autocompletes, haystack) => {
163
+ if (autocompletes.includes("cc-exp"))
164
+ return false;
165
+ if (CC_EXP_FULL_RE.test(haystack))
166
+ return false;
167
+ if (matchCCExpYear(haystack))
168
+ return true;
169
+ if (field instanceof HTMLSelectElement && matchCCExp(haystack)) {
170
+ for (const option of field.options) {
171
+ if (option.innerText === YEAR_OPTION)
172
+ return true;
173
+ if (option.value === YEAR_OPTION)
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ };
179
+ const CC_MATCHERS = [
180
+ [CCFieldType.FIRSTNAME, guard({ autocomplete: "cc-given-name" }, isCCFirstName)],
181
+ [CCFieldType.LASTNAME, guard({ autocomplete: "cc-family-name" }, isCCLastName)],
182
+ [CCFieldType.NAME, guard({ autocomplete: "cc-name" }, isCCName)],
183
+ [CCFieldType.EXP_YEAR, guard({ autocomplete: "cc-exp-year" }, isCCExpYear)],
184
+ [CCFieldType.EXP_MONTH, guard({ autocomplete: "cc-exp-month" }, isCCExpMonth)],
185
+ [CCFieldType.EXP, guard({ autocomplete: "cc-exp" }, isCCExp)],
186
+ [CCFieldType.CSC, guard({ autocomplete: "cc-csc", password: true }, isCCSecurityCode)],
187
+ [CCFieldType.NUMBER, guard({ autocomplete: "cc-number", password: true }, isCCNumber)],
188
+ ];
189
+ export const getCCHaystack = (field) => {
190
+ const attrs = CC_ATTRIBUTES.map((attr) => { var _a; return (_a = field === null || field === void 0 ? void 0 : field.getAttribute(attr)) !== null && _a !== void 0 ? _a : ""; });
191
+ const labelEl = getLabelFor(field);
192
+ const label = sanitizeString((() => {
193
+ if (!labelEl)
194
+ return "";
195
+ if (labelEl.innerText)
196
+ return labelEl.innerText;
197
+ if (labelEl.childElementCount === 0)
198
+ return "";
199
+ return Array.from(labelEl.children)
200
+ .map((el) => { var _a; return (_a = el === null || el === void 0 ? void 0 : el.innerText) !== null && _a !== void 0 ? _a : ""; })
201
+ .join(" ");
202
+ })());
203
+ return sanitizeStringWithSpaces(attrs.join(" ")) + " " + label;
204
+ };
205
+ export const getCachedCCSubtype = (el) => {
206
+ const subType = getCachedSubType(el);
207
+ if (subType && ccFields.has(subType))
208
+ return subType;
209
+ };
210
+ export const getCCFieldType = (field) => {
211
+ var _a;
212
+ const cachedSubType = getCachedCCSubtype(field);
213
+ if (cachedSubType)
214
+ return cachedSubType;
215
+ const type = field.getAttribute("type");
216
+ if (field.tagName === "INPUT" && type && !CC_INPUT_TYPES.includes(type))
217
+ return;
218
+ const haystack = getCCHaystack(field);
219
+ const autocompletes = getAutocompletes(field);
220
+ if (haystack) {
221
+ const ccType = (_a = CC_MATCHERS.find(([, test]) => test(field, autocompletes, haystack))) === null || _a === void 0 ? void 0 : _a[0];
222
+ if (ccType)
223
+ setCachedSubType(field, ccType);
224
+ return ccType;
225
+ }
226
+ };
227
+ export const matchCCFieldCandidate = (input, { visible }) => {
228
+ if (getCachedCCSubtype(input))
229
+ return true;
230
+ if (!visible)
231
+ return false;
232
+ const ccType = getCCFieldType(input);
233
+ if (ccType)
234
+ setCachedSubType(input, ccType);
235
+ return ccType !== undefined;
236
+ };
237
+ export const isCCInputField = (fnode) => {
238
+ const { isCC, visible } = fnode.noteFor("field");
239
+ if (!visible)
240
+ return false;
241
+ return isCC;
242
+ };
243
+ export const isCCSelectField = (fnode) => {
244
+ const select = fnode.element;
245
+ if (!(select instanceof HTMLSelectElement))
246
+ return false;
247
+ if (getCachedCCSubtype(select))
248
+ return true;
249
+ const visible = isVisible(select, { opacity: false });
250
+ if (!visible)
251
+ return false;
252
+ const isProcessable = isClassifiable(select);
253
+ if (!isProcessable)
254
+ return false;
255
+ const ccType = getCCFieldType(select);
256
+ if (ccType)
257
+ setCachedSubType(select, ccType);
258
+ return ccType !== undefined;
259
+ };