@slicedearth/whoisleuth-cli 2.2.0 → 2.4.0

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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,21 +1,18 @@
1
- // One bounded tokenization pass over the already-captured homepage body. The
2
- // parse5 tokenizer applies HTML tokenization rules without constructing a DOM,
3
- // so deeply nested hostile markup cannot create an unbounded element tree.
4
- // Only normalized start-tag markup and capped script indicators are retained.
5
- import { Tokenizer, TokenizerMode } from 'parse5';
1
+ // One bounded native HTML parse over the already-captured homepage body.
2
+ // Consumers share element/attribute evidence instead of reparsing reconstructed
3
+ // markup. Only bounded transient projections leave this module, never a DOM.
4
+ import { htmlTreeEvents, isHtmlElement, parseBoundedHtml, MAX_STATIC_HTML_CHARS, MAX_STATIC_HTML_TAGS, MAX_STATIC_HTML_TREE_EVENTS, } from "./bounded-html-document.mjs";
6
5
  import { MAX_CSP_META_POLICIES, MAX_RESPONSE_POLICY_HEADER_BYTES, } from "./response-policy.mjs";
6
+ import { MAX_FAVICON_BYTES, MAX_FAVICON_CANDIDATES } from "./outbound-request-bounds.mjs";
7
7
  import { MAX_PAGE_PUBLICATION_DECLARATIONS, MAX_PAGE_PUBLICATION_META_ELEMENTS, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES, } from "./homepage-metadata-contract.mjs";
8
- const MAX_STATIC_HTML_CHARS = 300_000;
9
- const MAX_STATIC_HTML_TAGS = 8_192;
10
- const MAX_STATIC_STRUCTURE_TOKENS = 4_096;
11
- const MAX_TECHNOLOGY_TAGS = 2_048;
8
+ const MAX_STATIC_STRUCTURE_TOKENS = MAX_STATIC_HTML_TREE_EVENTS;
12
9
  const MAX_TAG_LENGTH = 4_096;
13
10
  const MAX_ATTRIBUTES_PER_TAG = 128;
14
11
  const MAX_SCRIPT_ELEMENTS = 64;
15
12
  const MAX_SCRIPT_REFERENCE_LENGTH = 2_048;
16
13
  const MAX_SCRIPT_MEDIA_TYPE_LENGTH = 120;
17
- const MAX_INLINE_SCRIPT_CHARS = 32_768;
18
14
  const MAX_INLINE_SCRIPT_TOTAL_CHARS = 65_536;
15
+ const MAX_INLINE_SCRIPT_CHARS = MAX_INLINE_SCRIPT_TOTAL_CHARS;
19
16
  const MAX_STATIC_VISIBLE_TEXT_CHARS = MAX_STATIC_HTML_CHARS;
20
17
  const MAX_STATIC_FORMS = 50;
21
18
  const MAX_STATIC_INPUTS = 500;
@@ -39,29 +36,11 @@ const PAYMENT_AUTOCOMPLETE_TOKENS = new Set([
39
36
  'transaction-currency',
40
37
  'transaction-amount',
41
38
  ]);
42
- const RAW_TEXT_MODES = Object.freeze({
43
- iframe: TokenizerMode.RAWTEXT,
44
- noembed: TokenizerMode.RAWTEXT,
45
- noframes: TokenizerMode.RAWTEXT,
46
- script: TokenizerMode.SCRIPT_DATA,
47
- style: TokenizerMode.RAWTEXT,
48
- template: TokenizerMode.RAWTEXT,
49
- textarea: TokenizerMode.RCDATA,
50
- title: TokenizerMode.RCDATA,
51
- xmp: TokenizerMode.RAWTEXT,
52
- });
53
39
  const VOID_TAGS = new Set([
54
40
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta',
55
41
  'param', 'source', 'track', 'wbr',
56
42
  ]);
57
- const NON_VISIBLE_TEXT_TAGS = new Set(['script', 'style', 'template']);
58
- const HEAD_CONTEXT_TAGS = new Set([
59
- 'html', 'head', 'base', 'basefont', 'bgsound', 'link', 'meta', 'title',
60
- 'noscript', 'noframes', 'style', 'template', 'script',
61
- ]);
62
- const IMPLICIT_HEAD_TEXT_TAGS = new Set([
63
- 'title', 'noscript', 'noframes', 'style', 'template', 'script',
64
- ]);
43
+ const NON_VISIBLE_TEXT_TAGS = new Set(['head', 'script', 'style', 'template', 'noscript', 'iframe', 'noembed', 'noframes']);
65
44
  const ROBOTS_DIRECTIVES = new Set([
66
45
  'all', 'follow', 'index', 'max-image-preview', 'max-snippet', 'max-video-preview',
67
46
  'noarchive', 'nocache', 'nofollow', 'noimageindex', 'noindex', 'none', 'nositelinkssearchbox',
@@ -73,7 +52,7 @@ const JAVASCRIPT_MEDIA_TYPES = new Set([
73
52
  'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
74
53
  'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript',
75
54
  ]);
76
- function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH) {
55
+ function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH, lowercase = true) {
77
56
  const attribute = attributes
78
57
  .slice(0, MAX_ATTRIBUTES_PER_TAG)
79
58
  .find((candidate) => candidate.name.toLowerCase() === name);
@@ -84,7 +63,7 @@ function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LEN
84
63
  }
85
64
  return {
86
65
  present: true,
87
- value: attribute.value.trim().toLowerCase(),
66
+ value: lowercase ? attribute.value.trim().toLowerCase() : attribute.value.trim(),
88
67
  truncated: attributes.length > MAX_ATTRIBUTES_PER_TAG,
89
68
  };
90
69
  }
@@ -116,8 +95,8 @@ function formMethod(attributes) {
116
95
  truncated: method.truncated,
117
96
  };
118
97
  }
119
- function formAction(attributes, baseUrl) {
120
- const action = attributeValue(attributes, 'action');
98
+ function formAction(attributes, resolutionBase, documentOrigin) {
99
+ const action = attributeValue(attributes, 'action', MAX_FORM_ATTRIBUTE_LENGTH, false);
121
100
  if (!action.present && action.truncated) {
122
101
  return { relationship: 'unclassified', cleartext: false, truncated: true };
123
102
  }
@@ -127,16 +106,16 @@ function formAction(attributes, baseUrl) {
127
106
  if (action.value === null) {
128
107
  return { relationship: 'unclassified', cleartext: false, truncated: true };
129
108
  }
130
- if (!baseUrl) {
109
+ if (!resolutionBase || !documentOrigin) {
131
110
  return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
132
111
  }
133
112
  try {
134
- const parsed = new URL(action.value, baseUrl);
113
+ const parsed = new URL(action.value, resolutionBase);
135
114
  if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
136
115
  return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
137
116
  }
138
117
  return {
139
- relationship: parsed.origin === baseUrl.origin ? 'sameOrigin' : 'external',
118
+ relationship: parsed.origin === documentOrigin ? 'sameOrigin' : 'external',
140
119
  cleartext: parsed.protocol === 'http:',
141
120
  truncated: action.truncated,
142
121
  };
@@ -218,29 +197,18 @@ function stylesheetCandidate(attributes) {
218
197
  truncated: rel.truncated || href.truncated || disabled.truncated || media.truncated,
219
198
  };
220
199
  }
221
- function serializedStartTag(tagName, attributes) {
222
- let serialized = `<${tagName.toLowerCase()}`;
223
- let limitReached = attributes.length > MAX_ATTRIBUTES_PER_TAG;
224
- for (const attribute of attributes.slice(0, MAX_ATTRIBUTES_PER_TAG)) {
225
- const candidate = ` ${attribute.name.toLowerCase()}="${attribute.value.toLowerCase()}"`;
226
- if (serialized.length + candidate.length + 1 > MAX_TAG_LENGTH) {
227
- limitReached = true;
228
- break;
229
- }
230
- serialized += candidate;
231
- }
232
- return {
233
- markup: serialized.length + 1 <= MAX_TAG_LENGTH ? `${serialized}>` : null,
234
- limitReached,
235
- };
236
- }
237
200
  function analyzeStaticHtml(value, options = {}) {
238
- const supplied = typeof value === 'string' ? value : '';
239
- const inputLimitReached = supplied.length > MAX_STATIC_HTML_CHARS;
240
- const html = supplied.slice(0, MAX_STATIC_HTML_CHARS);
241
- const baseUrl = safeBaseUrl(options.baseUrl);
201
+ const parsed = parseBoundedHtml(value);
202
+ const { inputLimitReached } = parsed;
203
+ const documentUrl = safeBaseUrl(options.baseUrl);
204
+ let effectiveBaseUrl = documentUrl;
205
+ let baseHrefState = 'absent';
242
206
  const includeVisibleText = options.includeVisibleText === true;
243
- const markup = [];
207
+ const elements = [];
208
+ const iconLinks = [];
209
+ const iconCounts = [0, 0];
210
+ const tokens = [];
211
+ const elementStack = [];
244
212
  const visibleTextParts = [];
245
213
  const structureTokens = [];
246
214
  const scripts = [];
@@ -296,11 +264,10 @@ function analyzeStaticHtml(value, options = {}) {
296
264
  };
297
265
  const robotDirectives = new Set();
298
266
  const twitterCardTypes = new Set();
299
- let tokenizer;
300
267
  let activeInlineScript = null;
301
268
  let tagsExamined = 0;
302
269
  let inlineCharactersExamined = 0;
303
- let tagLimitReached = false;
270
+ let tagLimitReached = parsed.constructionLimitReached;
304
271
  let structureLimitReached = false;
305
272
  let scriptLimitReached = false;
306
273
  let inlineLimitReached = false;
@@ -308,20 +275,28 @@ function analyzeStaticHtml(value, options = {}) {
308
275
  let cspMetaLimitReached = false;
309
276
  let publicationMetaElements = 0;
310
277
  let robotDirectiveTokens = 0;
311
- let publicationTagLimitReached = false;
278
+ let publicationTagLimitReached = parsed.constructionLimitReached;
312
279
  let insideExplicitHead = false;
313
- let implicitHeadTextDepth = 0;
314
- let headScopeClosed = false;
315
- let bodyContentStarted = false;
280
+ let insideHead = false;
316
281
  let scriptElementSeen = false;
317
282
  let nonVisibleTextDepth = 0;
318
283
  let visibleTextCharacters = 0;
284
+ let titleDepth = 0;
285
+ let titleCharacters = 0;
286
+ const titleParts = [];
287
+ const pendingFormActions = [];
288
+ function appendTitle(chars) {
289
+ if (titleDepth === 0 || !chars || titleCharacters >= 201)
290
+ return;
291
+ const retained = chars.slice(0, 201 - titleCharacters);
292
+ titleParts.push(retained);
293
+ titleCharacters += retained.length;
294
+ }
319
295
  function appendInlineScript(chars) {
320
296
  if (!activeInlineScript || !chars)
321
297
  return;
322
- const perScriptRemaining = MAX_INLINE_SCRIPT_CHARS - activeInlineScript.inlineContent.length;
323
298
  const totalRemaining = MAX_INLINE_SCRIPT_TOTAL_CHARS - inlineCharactersExamined;
324
- const retainedLength = Math.max(0, Math.min(chars.length, perScriptRemaining, totalRemaining));
299
+ const retainedLength = Math.max(0, Math.min(chars.length, totalRemaining));
325
300
  if (retainedLength > 0) {
326
301
  activeInlineScript.inlineContent += chars.slice(0, retainedLength);
327
302
  inlineCharactersExamined += retainedLength;
@@ -348,32 +323,118 @@ function analyzeStaticHtml(value, options = {}) {
348
323
  if (retainedLength < chars.length)
349
324
  visibleTextLimitReached = true;
350
325
  }
351
- const handler = {
352
- onStartTag(token) {
353
- const tagName = token.tagName.toLowerCase();
354
- const rawTextMode = RAW_TEXT_MODES[tagName];
355
- if (!token.selfClosing && rawTextMode !== undefined)
356
- tokenizer.state = rawTextMode;
357
- if (!token.selfClosing && NON_VISIBLE_TEXT_TAGS.has(tagName))
358
- nonVisibleTextDepth += 1;
359
- if (tagsExamined >= MAX_STATIC_HTML_TAGS) {
360
- tagLimitReached = true;
361
- publicationTagLimitReached = true;
326
+ function appendToken(token) {
327
+ if (tokens.length >= MAX_STATIC_STRUCTURE_TOKENS)
328
+ structureLimitReached = true;
329
+ else
330
+ tokens.push(token);
331
+ }
332
+ for (const event of htmlTreeEvents(parsed.document)) {
333
+ if (event.kind === 'text') {
334
+ appendInlineScript(event.text);
335
+ appendTitle(event.text);
336
+ appendVisibleText(event.text);
337
+ if (nonVisibleTextDepth === 0)
338
+ appendToken({ kind: 'text', value: event.text });
339
+ continue;
340
+ }
341
+ const node = event.element;
342
+ const tagName = node.tagName.toLowerCase();
343
+ const htmlElement = isHtmlElement(node);
344
+ if (event.kind === 'end') {
345
+ elementStack.pop();
346
+ if (tagName === 'script')
362
347
  activeInlineScript = null;
363
- return;
348
+ if (htmlElement && tagName === 'title')
349
+ titleDepth = 0;
350
+ if (htmlElement && tagName === 'head') {
351
+ insideHead = false;
352
+ insideExplicitHead = false;
353
+ }
354
+ if (!htmlElement || !VOID_TAGS.has(tagName)) {
355
+ appendStructureToken(`/${tagName}`);
356
+ appendToken({ kind: 'end', name: tagName });
357
+ }
358
+ if (NON_VISIBLE_TEXT_TAGS.has(tagName))
359
+ nonVisibleTextDepth -= 1;
360
+ continue;
361
+ }
362
+ {
363
+ const token = { attrs: node.attrs };
364
+ if (NON_VISIBLE_TEXT_TAGS.has(tagName))
365
+ nonVisibleTextDepth += 1;
366
+ if (node.sourceCodeLocation?.startTag)
367
+ tagsExamined += 1;
368
+ appendStructureToken(htmlElement && VOID_TAGS.has(tagName) ? `${tagName}/` : tagName);
369
+ if (htmlElement && tagName === 'head') {
370
+ insideHead = true;
371
+ insideExplicitHead = Boolean(node.sourceCodeLocation?.startTag);
364
372
  }
365
- tagsExamined += 1;
366
- appendStructureToken(token.selfClosing || VOID_TAGS.has(tagName) ? `${tagName}/` : tagName);
367
- if (tagName === 'head')
368
- insideExplicitHead = true;
369
- if (tagName === 'body' || (!insideExplicitHead && !HEAD_CONTEXT_TAGS.has(tagName))) {
370
- bodyContentStarted = true;
371
- headScopeClosed = true;
373
+ const inPublicationHead = insideHead;
374
+ const attributes = [];
375
+ let attributeLength = 0;
376
+ for (const attribute of node.attrs) {
377
+ attributeLength += attribute.name.length + attribute.value.length;
378
+ if (attributes.length >= MAX_ATTRIBUTES_PER_TAG || attributeLength > MAX_TAG_LENGTH)
379
+ break;
380
+ attributes.push({ name: attribute.name, value: attribute.value });
381
+ }
382
+ const element = {
383
+ name: tagName, html: htmlElement, attributes,
384
+ attributesTruncated: attributes.length !== node.attrs.length,
385
+ parent: elementStack.at(-1) ?? null, inHead: insideHead,
386
+ };
387
+ elementStack.push(elements.length);
388
+ elements.push(element);
389
+ appendToken({ kind: 'start', element });
390
+ // Foreign drawing attributes are not HTML technology, form or resource
391
+ // inputs. Their per-element omission still reaches fingerprinting, but
392
+ // must not make unrelated HTML evidence incomplete.
393
+ if (htmlElement && element.attributesTruncated)
394
+ tagLimitReached = true;
395
+ if (!htmlElement)
396
+ continue;
397
+ if (tagName === 'link') {
398
+ const rel = node.attrs.find((attribute) => attribute.name === 'rel')?.value;
399
+ const href = node.attrs.find((attribute) => attribute.name === 'href')?.value.trim();
400
+ if (rel && rel.length <= 128 && href) {
401
+ const relations = rel.toLowerCase().split(/[\t\n\f\r ]+/u);
402
+ const priority = relations.includes('icon') ? 0
403
+ : relations.some((relation) => ['apple-touch-icon', 'apple-touch-icon-precomposed'].includes(relation)) ? 1 : null;
404
+ // Inline image bytes have their own bound; the general 4 KiB
405
+ // attribute projection must not silently discard supported icons.
406
+ const maxHref = /^data:/iu.test(href) ? MAX_FAVICON_BYTES * 3 + 128 : 2048;
407
+ if (priority !== null && iconCounts[priority] < MAX_FAVICON_CANDIDATES && href.length <= maxHref) {
408
+ iconLinks.push({ href, priority });
409
+ iconCounts[priority] += 1;
410
+ }
411
+ }
372
412
  }
373
- if (!insideExplicitHead && !headScopeClosed && !token.selfClosing && IMPLICIT_HEAD_TEXT_TAGS.has(tagName)) {
374
- implicitHeadTextDepth += 1;
413
+ // The first document base applies even when it appears in the body;
414
+ // publication metadata's separate head-only policy does not govern URLs.
415
+ if (tagName === 'base' && baseHrefState === 'absent') {
416
+ const href = attributeValue(token.attrs, 'href', MAX_FORM_ATTRIBUTE_LENGTH, false);
417
+ if (href.truncated && !href.present) {
418
+ effectiveBaseUrl = documentUrl;
419
+ baseHrefState = 'invalid';
420
+ }
421
+ else if (href.present) {
422
+ try {
423
+ const parsed = href.value !== null && documentUrl ? new URL(href.value, documentUrl) : null;
424
+ if (!parsed || !['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
425
+ throw new Error('Invalid document base URL.');
426
+ }
427
+ effectiveBaseUrl = parsed;
428
+ baseHrefState = 'valid';
429
+ }
430
+ catch {
431
+ effectiveBaseUrl = documentUrl;
432
+ baseHrefState = 'invalid';
433
+ }
434
+ }
375
435
  }
376
- const inPublicationHead = insideExplicitHead || (!headScopeClosed && !bodyContentStarted && HEAD_CONTEXT_TAGS.has(tagName));
436
+ if (tagName === 'title' && titleParts.length === 0)
437
+ titleDepth = 1;
377
438
  if (/^h[1-6]$/u.test(tagName)) {
378
439
  const key = tagName;
379
440
  publicationMetadata.headings[key] += 1;
@@ -414,10 +475,10 @@ function analyzeStaticHtml(value, options = {}) {
414
475
  if (candidate.candidate)
415
476
  publicationMetadata.renderBlockingCandidates.stylesheet += 1;
416
477
  }
417
- if (tagName === 'meta' && insideExplicitHead) {
478
+ if (tagName === 'meta' && insideHead) {
418
479
  const httpEquiv = attributeValue(token.attrs, 'http-equiv', 64);
419
480
  if (httpEquiv.value === 'content-security-policy') {
420
- const content = attributeValue(token.attrs, 'content', MAX_RESPONSE_POLICY_HEADER_BYTES);
481
+ const content = attributeValue(token.attrs, 'content', MAX_RESPONSE_POLICY_HEADER_BYTES, false);
421
482
  if (content.value === null || !content.value) {
422
483
  cspMetaLimitReached = cspMetaLimitReached || content.present || content.truncated;
423
484
  }
@@ -548,16 +609,6 @@ function analyzeStaticHtml(value, options = {}) {
548
609
  }
549
610
  }
550
611
  }
551
- if (markup.length < MAX_TECHNOLOGY_TAGS) {
552
- const serialized = serializedStartTag(tagName, token.attrs);
553
- if (serialized.limitReached)
554
- tagLimitReached = true;
555
- if (serialized.markup)
556
- markup.push(serialized.markup);
557
- }
558
- else {
559
- tagLimitReached = true;
560
- }
561
612
  if (tagName === 'form') {
562
613
  if (forms.formsObserved >= MAX_STATIC_FORMS) {
563
614
  forms.truncated = true;
@@ -566,11 +617,8 @@ function analyzeStaticHtml(value, options = {}) {
566
617
  forms.formsObserved += 1;
567
618
  const method = formMethod(token.attrs);
568
619
  forms.methods[method.value] += 1;
569
- const action = formAction(token.attrs, baseUrl);
570
- forms.actions[action.relationship] += 1;
571
- if (action.cleartext)
572
- forms.actions.cleartext += 1;
573
- if (method.truncated || action.truncated)
620
+ pendingFormActions.push(token.attrs);
621
+ if (method.truncated || token.attrs.length > MAX_ATTRIBUTES_PER_TAG)
574
622
  forms.truncated = true;
575
623
  }
576
624
  }
@@ -591,13 +639,13 @@ function analyzeStaticHtml(value, options = {}) {
591
639
  }
592
640
  if (tagName !== 'script') {
593
641
  activeInlineScript = null;
594
- return;
642
+ continue;
595
643
  }
596
644
  scriptElementSeen = true;
597
645
  if (scripts.length >= MAX_SCRIPT_ELEMENTS) {
598
646
  scriptLimitReached = true;
599
647
  activeInlineScript = null;
600
- return;
648
+ continue;
601
649
  }
602
650
  const reference = scriptReference(token.attrs);
603
651
  const script = {
@@ -607,49 +655,18 @@ function analyzeStaticHtml(value, options = {}) {
607
655
  };
608
656
  scripts.push(script);
609
657
  activeInlineScript = reference ? null : script;
610
- },
611
- onEndTag(token) {
612
- const tagName = token.tagName.toLowerCase();
613
- if (tagName === 'script')
614
- activeInlineScript = null;
615
- if (tagName === 'head') {
616
- insideExplicitHead = false;
617
- headScopeClosed = true;
618
- }
619
- if (!insideExplicitHead && IMPLICIT_HEAD_TEXT_TAGS.has(tagName) && implicitHeadTextDepth > 0) {
620
- implicitHeadTextDepth -= 1;
621
- }
622
- if (!VOID_TAGS.has(tagName))
623
- appendStructureToken(`/${tagName}`);
624
- if (NON_VISIBLE_TEXT_TAGS.has(tagName) && nonVisibleTextDepth > 0)
625
- nonVisibleTextDepth -= 1;
626
- },
627
- onCharacter(token) {
628
- if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0 && token.chars.trim()) {
629
- bodyContentStarted = true;
630
- headScopeClosed = true;
631
- }
632
- appendInlineScript(token.chars);
633
- appendVisibleText(token.chars);
634
- },
635
- onNullCharacter(token) {
636
- if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0) {
637
- bodyContentStarted = true;
638
- headScopeClosed = true;
639
- }
640
- appendInlineScript(token.chars);
641
- appendVisibleText(token.chars);
642
- },
643
- onWhitespaceCharacter(token) {
644
- appendInlineScript(token.chars);
645
- appendVisibleText(token.chars);
646
- },
647
- onComment() { },
648
- onDoctype() { },
649
- onEof() { },
650
- };
651
- tokenizer = new Tokenizer({ sourceCodeLocationInfo: false }, handler);
652
- tokenizer.write(html, true);
658
+ }
659
+ }
660
+ for (const attributes of pendingFormActions) {
661
+ const action = formAction(attributes, effectiveBaseUrl, documentUrl?.origin ?? null);
662
+ forms.actions[action.relationship] += 1;
663
+ if (action.cleartext)
664
+ forms.actions.cleartext += 1;
665
+ if (action.truncated)
666
+ forms.truncated = true;
667
+ }
668
+ if (baseHrefState === 'invalid')
669
+ forms.truncated = true;
653
670
  publicationMetadata.truncated = publicationMetadata.truncated || inputLimitReached || publicationTagLimitReached;
654
671
  publicationMetadata.documentTruncated = inputLimitReached || publicationTagLimitReached;
655
672
  if (publicationMetadata.documentTruncated) {
@@ -671,7 +688,17 @@ function analyzeStaticHtml(value, options = {}) {
671
688
  publicationMetadata.renderBlockingCandidates.total = publicationMetadata.renderBlockingCandidates.script
672
689
  + publicationMetadata.renderBlockingCandidates.stylesheet;
673
690
  return {
674
- markup: markup.join('\n'),
691
+ title: (() => {
692
+ const value = titleParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\ufffd/gu, ' ').replace(/\s+/gu, ' ').trim();
693
+ if (!value)
694
+ return null;
695
+ return value.length > 200 ? `${value.slice(0, 199)}…` : value;
696
+ })(),
697
+ effectiveBaseUrl: effectiveBaseUrl?.toString() ?? null,
698
+ baseHrefState,
699
+ elements,
700
+ iconLinks: iconLinks.sort((left, right) => left.priority - right.priority).slice(0, MAX_FAVICON_CANDIDATES),
701
+ tokens,
675
702
  visibleText: visibleTextParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\s+/gu, ' '),
676
703
  structureTokens,
677
704
  scripts,
@@ -689,4 +716,4 @@ function analyzeStaticHtml(value, options = {}) {
689
716
  inlineCharactersExamined,
690
717
  };
691
718
  }
692
- export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_PUBLICATION_META_ELEMENTS, MAX_STATIC_PUBLICATION_DECLARATIONS, MAX_STATIC_ROBOTS_DIRECTIVES, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, };
719
+ export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_PUBLICATION_META_ELEMENTS, MAX_STATIC_PUBLICATION_DECLARATIONS, MAX_STATIC_ROBOTS_DIRECTIVES, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, analyzeStaticHtml, };
@@ -290,6 +290,7 @@ function analyzeStructuredDataIdentity(input = {}) {
290
290
  }
291
291
  const truncated = input.sourceTruncated === true
292
292
  || htmlAnalysis.inputLimitReached
293
+ || htmlAnalysis.tagLimitReached
293
294
  || htmlAnalysis.scriptLimitReached
294
295
  || htmlAnalysis.inlineLimitReached
295
296
  || limitReached;
@@ -0,0 +1,100 @@
1
+ const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
2
+ export function attributeValue(element, name) {
3
+ return element.attributes.find((attribute) => attribute.name === name)?.value.toLowerCase() ?? '';
4
+ }
5
+ export function hasAttributes(element, ...names) {
6
+ return element.attributes.some((attribute) => names.includes(attribute.name));
7
+ }
8
+ function httpUrl(value, base) {
9
+ if (typeof value !== 'string' || value.length > 2048 || CONTROL_RE.test(value) || !value.trim())
10
+ return null;
11
+ try {
12
+ const url = new URL(value, base);
13
+ return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password ? url : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export function createTechnologyMarkupContext(analysis, effectiveBaseUrl, documentOrigin) {
20
+ const elements = analysis.elements.filter((element) => element.html && element.name !== 'template');
21
+ const documentUrl = httpUrl(documentOrigin);
22
+ const suppliedBase = effectiveBaseUrl ?? analysis.effectiveBaseUrl;
23
+ const base = httpUrl(suppliedBase) ?? documentUrl;
24
+ // A reserved base permits legacy minimised relative references without
25
+ // attributing an absolute external URL to an unknown document origin.
26
+ const resolutionBase = base ?? new URL('https://document.invalid/');
27
+ const references = (element, names) => (element.attributes.flatMap((attribute) => {
28
+ if (!names.includes(attribute.name))
29
+ return [];
30
+ if (attribute.value.trim().startsWith('#'))
31
+ return [];
32
+ const url = httpUrl(attribute.value, resolutionBase);
33
+ if (!url)
34
+ return [];
35
+ const relative = !/^(?:[a-z][a-z0-9+.-]*:|\/\/)/iu.test(attribute.value.trim());
36
+ const embedded = documentUrl
37
+ ? url.origin !== documentUrl.origin
38
+ : Boolean(base || suppliedBase || documentOrigin) || !relative;
39
+ return [{ url, embedded }];
40
+ }));
41
+ const resources = [];
42
+ const navigation = [];
43
+ for (const element of elements) {
44
+ switch (element.name) {
45
+ case 'script':
46
+ case 'img':
47
+ case 'iframe':
48
+ case 'frame':
49
+ case 'source':
50
+ case 'audio':
51
+ case 'embed':
52
+ resources.push(...references(element, ['src']));
53
+ break;
54
+ case 'video':
55
+ resources.push(...references(element, ['src', 'poster']));
56
+ break;
57
+ case 'link': {
58
+ const relations = attributeValue(element, 'rel').trim().split(/\s+/u);
59
+ // Existing minimised inputs retain a link reference without its rel.
60
+ // Explicit document metadata or navigation relations are not assets.
61
+ if (!hasAttributes(element, 'rel') || relations.some((relation) => ['stylesheet', 'preload', 'modulepreload', 'icon'].includes(relation))) {
62
+ resources.push(...references(element, ['href']));
63
+ }
64
+ break;
65
+ }
66
+ case 'object':
67
+ resources.push(...references(element, ['data']));
68
+ break;
69
+ case 'input':
70
+ if (attributeValue(element, 'type') === 'image')
71
+ resources.push(...references(element, ['src']));
72
+ break;
73
+ case 'a':
74
+ case 'area':
75
+ navigation.push(...references(element, ['href']));
76
+ break;
77
+ }
78
+ }
79
+ return { elements, resources, navigation };
80
+ }
81
+ export function elementMarker(matches, reviewMarkup) {
82
+ return { matches: ({ elements }) => elements.some(matches), reviewMarkup };
83
+ }
84
+ export function resourceMarker(matches, reviewPath) {
85
+ const sharedOrigin = reviewPath.startsWith('https://');
86
+ return {
87
+ matches: ({ resources }) => resources.some(({ url }) => matches(url)),
88
+ embedded: ({ resources }) => sharedOrigin || !resources.some(({ url, embedded }) => !embedded && matches(url)),
89
+ reviewMarkup: `<link href="${reviewPath}">`,
90
+ embeddedReviewMarkup: `<link href="${sharedOrigin ? reviewPath : `https://embedded.invalid${reviewPath}`}">`,
91
+ };
92
+ }
93
+ export function navigationMarker(matches, reviewPath) {
94
+ return {
95
+ matches: ({ navigation }) => navigation.some(({ url }) => matches(url)),
96
+ embedded: ({ navigation }) => !navigation.some(({ url, embedded }) => !embedded && matches(url)),
97
+ reviewMarkup: `<a href="${reviewPath}"></a>`,
98
+ embeddedReviewMarkup: `<a href="https://embedded.invalid/${reviewPath}"></a>`,
99
+ };
100
+ }