@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

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 (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -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/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -2,38 +2,65 @@
2
2
  // normalization, and registrar enrichment remain separate concerns in
3
3
  // lib/rdap.mts.
4
4
  import { readTextCapped, safeFetch, safeFetchDetailed } from "./safe-fetch.mjs";
5
+ import { abortable } from "./abort.mjs";
5
6
  const MAX_RDAP_BYTES = 2000000;
6
- async function fetchRdapWithTimeout(url, options, timeoutMs, dependencies = {}) {
7
- const controller = new AbortController();
8
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
9
- try {
10
- const response = await (dependencies.fetch ?? safeFetch)(url, { ...options, signal: controller.signal });
11
- const { text, truncated } = await (dependencies.readText ?? readTextCapped)(response, MAX_RDAP_BYTES, { fatalUtf8: true });
12
- if (truncated)
13
- throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
14
- return { status: response.status, ok: response.ok, text };
15
- }
16
- finally {
17
- clearTimeout(timeout);
7
+ export class RdapBodyAdmissionError extends Error {
8
+ status;
9
+ kind;
10
+ constructor(status, kind) {
11
+ super(kind === 'too_large' ? `RDAP response exceeded ${MAX_RDAP_BYTES} bytes.`
12
+ : kind === 'invalid_encoding' ? 'RDAP response contains invalid UTF-8 encoding.'
13
+ : 'RDAP response body could not be read completely.');
14
+ this.name = 'RdapBodyAdmissionError';
15
+ this.status = status;
16
+ this.kind = kind;
18
17
  }
19
18
  }
20
- async function fetchRdapDetailedWithTimeout(url, options, timeoutMs, dependencies = {}) {
19
+ async function fetchRdapTransport(url, options, timeoutMs, fetchResponse, readText) {
20
+ options.signal?.throwIfAborted();
21
21
  const controller = new AbortController();
22
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
22
23
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
24
+ let response;
25
+ const cancelBody = () => { void response?.body?.cancel().catch(() => { }); };
23
26
  try {
24
- const result = await (dependencies.fetchDetailed ?? safeFetchDetailed)(url, { ...options, signal: controller.signal });
25
- const { text, truncated } = await (dependencies.readText ?? readTextCapped)(result.response, MAX_RDAP_BYTES, { fatalUtf8: true });
27
+ const result = await abortable(async () => {
28
+ const result = await fetchResponse(url, { ...options, signal });
29
+ response = result.response;
30
+ if (signal.aborted) {
31
+ cancelBody();
32
+ signal.throwIfAborted();
33
+ }
34
+ return result;
35
+ }, signal);
36
+ let body;
37
+ try {
38
+ body = await abortable(() => readText(result.response, MAX_RDAP_BYTES, { fatalUtf8: true }), signal);
39
+ }
40
+ catch (error) {
41
+ signal.throwIfAborted();
42
+ const invalidEncoding = error instanceof TypeError && 'code' in error
43
+ && error.code === 'ERR_ENCODING_INVALID_ENCODED_DATA';
44
+ throw new RdapBodyAdmissionError(result.response.status, invalidEncoding ? 'invalid_encoding' : 'unreadable');
45
+ }
46
+ const { text, truncated } = body;
26
47
  if (truncated)
27
- throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
48
+ throw new RdapBodyAdmissionError(result.response.status, 'too_large');
28
49
  return {
29
- status: result.response.status,
30
- ok: result.response.ok,
31
- text,
32
- finalUrl: result.finalUrl,
50
+ status: result.response.status, ok: result.response.ok, text,
51
+ ...(result.finalUrl === undefined ? {} : { finalUrl: result.finalUrl }),
33
52
  };
34
53
  }
35
54
  finally {
36
55
  clearTimeout(timeout);
56
+ if (signal.aborted)
57
+ cancelBody();
37
58
  }
38
59
  }
60
+ async function fetchRdapWithTimeout(url, options, timeoutMs, dependencies = {}) {
61
+ return fetchRdapTransport(url, options, timeoutMs, async (target, request) => ({ response: await (dependencies.fetch ?? safeFetch)(target, request) }), dependencies.readText ?? readTextCapped);
62
+ }
63
+ async function fetchRdapDetailedWithTimeout(url, options, timeoutMs, dependencies = {}) {
64
+ return fetchRdapTransport(url, options, timeoutMs, dependencies.fetchDetailed ?? safeFetchDetailed, dependencies.readText ?? readTextCapped);
65
+ }
39
66
  export { fetchRdapDetailedWithTimeout, fetchRdapWithTimeout, };
@@ -1 +1,2 @@
1
- export {};
1
+ export const MAX_RDAP_STATUSES = 100;
2
+ export const MAX_RDAP_REDACTIONS = 100;
package/lib/rdap.mjs CHANGED
@@ -4,18 +4,33 @@ import { fetchRdapFromBasesWithParser, fetchRdapRecordWithParser, } from "./rdap
4
4
  import { normalizeRdapEvents, parseRdap, summarizeLifecycle, } from "./rdap-normalization.mjs";
5
5
  import { fetchRegistrarRdapRecordWithParser, selectRegistrarRdapLink, } from "./rdap-registrar.mjs";
6
6
  import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
7
- import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
7
+ import { REGISTRY_CAPABILITIES_VERSION, registryServiceAdmissionFor } from "./registry-capabilities.mjs";
8
+ // A null record does not establish domain absence. Keep the HTTP explanation
9
+ // aligned with the same admission decision used before bootstrap discovery.
10
+ function rdapUnavailableResponse(type, value) {
11
+ const admission = type === 'domain' ? registryServiceAdmissionFor(value, 'rdap') : null;
12
+ if (admission?.allowed === false) {
13
+ return {
14
+ error: 'RDAP collection was not attempted because the retained registry capability policy does not admit this service.',
15
+ source: 'retained_registry_capability_policy',
16
+ capabilityVersion: REGISTRY_CAPABILITIES_VERSION,
17
+ limitation: admission.limitation,
18
+ };
19
+ }
20
+ return { error: `No RDAP registry found for "${value}" via IANA bootstrap` };
21
+ }
8
22
  async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapDetailedWithTimeout) {
9
23
  return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream);
10
24
  }
11
25
  async function fetchRdapRecord(type, value, options = {}) {
26
+ options.signal?.throwIfAborted();
12
27
  if (type === 'domain' && registryServiceAdmissionFor(value, 'rdap')?.allowed === false) {
13
28
  return null;
14
29
  }
15
30
  const fetchRecord = options.fetchRecord ?? fetchRdapRecordWithParser;
16
- return fetchRecord(type, value, parseRdap);
31
+ return fetchRecord(type, value, parseRdap, options.signal ? { signal: options.signal } : {});
17
32
  }
18
33
  async function fetchRegistrarRdapRecord(domain, registryRecord, options = {}) {
19
34
  return fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options);
20
35
  }
21
- export { BOOTSTRAP_TTL_MS, BOOTSTRAP_STALE_TTL_MS, fetchBootstrap, clearRdapBootstrapCache, fetchRdapRecord, fetchRdapFromBases, fetchRegistrarRdapRecord, selectRegistrarRdapLink, uniqueBases, parseRdap, normalizeRdapEvents, summarizeLifecycle, };
36
+ export { BOOTSTRAP_TTL_MS, BOOTSTRAP_STALE_TTL_MS, fetchBootstrap, clearRdapBootstrapCache, fetchRdapRecord, rdapUnavailableResponse, fetchRdapFromBases, fetchRegistrarRdapRecord, selectRegistrarRdapLink, uniqueBases, parseRdap, normalizeRdapEvents, summarizeLifecycle, };
@@ -1,36 +1,2 @@
1
- import { getDomain } from 'tldts';
2
- import { isValidAsciiDomainName } from "./hostname.mjs";
3
- // Returns one canonical registrable-domain identity for an already public DNS
4
- // hostname. Callers retain the original hostname separately when subdomain
5
- // context matters; authority, evidence binding and scoring use this value.
6
- export function canonicalRegistrableDomain(value) {
7
- if (typeof value !== 'string')
8
- return null;
9
- const trimmed = value.trim();
10
- if (!trimmed || /[\u0000-\u001f\u007f]/u.test(trimmed))
11
- return null;
12
- const withoutScheme = trimmed.replace(/^[a-z]+:\/\//iu, '').split(/[/?#]/u)[0] ?? '';
13
- if (withoutScheme.endsWith('..'))
14
- return null;
15
- const candidate = withoutScheme.replace(/\.$/u, '');
16
- if (!candidate)
17
- return null;
18
- let hostname;
19
- try {
20
- hostname = new URL(`https://${candidate}/`).hostname.toLowerCase().replace(/\.$/u, '');
21
- }
22
- catch {
23
- return null;
24
- }
25
- if (!isValidAsciiDomainName(hostname, { requireDot: true, requireLowercase: true }))
26
- return null;
27
- // Keep this boundary identical to classifyQuery() and the collection
28
- // runtime: registrable identity follows the ICANN Public Suffix List, while
29
- // privately operated suffixes remain ordinary host context.
30
- const domain = getDomain(hostname);
31
- return domain
32
- && isValidAsciiDomainName(domain, { requireDot: true, requireLowercase: true })
33
- && (hostname === domain || hostname.endsWith(`.${domain}`))
34
- ? domain
35
- : null;
36
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/registrable-domain.mjs";
@@ -64,7 +64,7 @@ const SOURCE_HEALTH_STATES = new Set(REGISTRAR_STANDING_SOURCE_HEALTH_STATES);
64
64
  const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/u;
65
65
  const NOTICE_ID_RE = /^notice-[1-9]\d{0,7}$/u;
66
66
  const IANA_ID_RE = /^[1-9]\d{0,7}$/u;
67
- function normalizedRegistrarIanaId(value) {
67
+ export function normalizedRegistrarIanaId(value) {
68
68
  if (typeof value === 'number' && Number.isSafeInteger(value))
69
69
  value = String(value);
70
70
  if (typeof value !== 'string')
@@ -101,6 +101,7 @@ function validNullableTimestamp(value) {
101
101
  function validNullableDate(value) {
102
102
  return value === null || (typeof value === 'string'
103
103
  && ISO_DATE_RE.test(value)
104
+ && Number.isFinite(Date.parse(`${value}T00:00:00.000Z`))
104
105
  && new Date(`${value}T00:00:00.000Z`).toISOString().startsWith(value));
105
106
  }
106
107
  function validNullableText(value, maximum = MAX_REGISTRAR_STANDING_TEXT_LENGTH) {
@@ -5,7 +5,7 @@
5
5
  import { createHash } from 'node:crypto';
6
6
  import { REGISTRAR_STANDING_CATALOGUE } from "./generated/registrar-standing-catalogue.mjs";
7
7
  import { ICANN_COMPLIANCE_SOURCE_URL, IANA_REGISTRAR_SOURCE_URL, REGISTRAR_STANDING_CATALOGUE_SCHEMA, REGISTRAR_STANDING_CATALOGUE_VERSION, REGISTRAR_STANDING_MAX_AGE_DAYS, REGISTRAR_STANDING_MAX_FUTURE_SKEW_MS, registrarStandingOfficialSourceUrl, } from "./registrar-standing-catalogue-contract.mjs";
8
- import { deriveRegistrarStandingAssessment, MAX_REGISTRAR_COMPLIANCE_ACTIONS, REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, } from "./registrar-standing-contract.mjs";
8
+ import { deriveRegistrarStandingAssessment, normalizedRegistrarIanaId, MAX_REGISTRAR_COMPLIANCE_ACTIONS, REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, } from "./registrar-standing-contract.mjs";
9
9
  const STATUS_BY_CODE = Object.freeze({
10
10
  A: 'accredited',
11
11
  R: 'reserved',
@@ -40,6 +40,10 @@ function ageDays(value, now) {
40
40
  function sourceHealth(value, now) {
41
41
  return ageDays(value, now) <= REGISTRAR_STANDING_MAX_AGE_DAYS ? 'current' : 'stale';
42
42
  }
43
+ function complianceHealth(catalogue, now) {
44
+ return catalogue.icann.catalogueYear === now.getUTCFullYear()
45
+ ? sourceHealth(catalogue.icann.reviewedAt, now) : 'stale';
46
+ }
43
47
  function validatedCatalogue(value, now) {
44
48
  if (!value || typeof value !== 'object' || Array.isArray(value))
45
49
  return null;
@@ -157,14 +161,6 @@ function accreditationMap(catalogue) {
157
161
  }
158
162
  return result;
159
163
  }
160
- function normalizedIanaId(value) {
161
- if (typeof value === 'number' && Number.isSafeInteger(value))
162
- value = String(value);
163
- if (typeof value !== 'string')
164
- return null;
165
- const trimmed = value.trim();
166
- return /^\d{1,8}$/u.test(trimmed) && Number(trimmed) > 0 ? String(Number(trimmed)) : null;
167
- }
168
164
  function projectedActions(catalogue, ianaId) {
169
165
  const matching = catalogue.icann.notices.filter((notice) => String(notice.ianaId) === ianaId);
170
166
  const actions = matching.slice(0, MAX_REGISTRAR_COMPLIANCE_ACTIONS).map((notice) => {
@@ -183,9 +179,9 @@ export function buildRegistrarStanding(options) {
183
179
  if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
184
180
  throw new TypeError('Registrar standing time must be valid.');
185
181
  const catalogue = validatedCatalogue(options.catalogue ?? REGISTRAR_STANDING_CATALOGUE, now);
186
- const ianaId = normalizedIanaId(options.registrarIanaId);
182
+ const ianaId = normalizedRegistrarIanaId(options.registrarIanaId);
187
183
  const ianaHealth = catalogue ? sourceHealth(catalogue.iana.observedAt, now) : 'unavailable';
188
- const icannHealth = catalogue ? sourceHealth(catalogue.icann.reviewedAt, now) : 'unavailable';
184
+ const icannHealth = catalogue ? complianceHealth(catalogue, now) : 'unavailable';
189
185
  const accreditation = catalogue && ianaId
190
186
  ? accreditationMap(catalogue).get(ianaId) ?? 'unknown'
191
187
  : 'unknown';
@@ -205,7 +201,7 @@ export function buildRegistrarStanding(options) {
205
201
  ? `The reviewed ${catalogue.icann.catalogueYear} ICANN index is not a complete enforcement history and excludes third-party allegations.`
206
202
  : 'The checked-in official-source catalogue could not be validated, so registrar standing is unavailable.',
207
203
  ...(!ianaId ? ['No single numeric registrar IANA ID was available across the registration evidence, so no registrar match was attempted.'] : []),
208
- ...(combinedHealth === 'stale' ? ['The catalogue is past its review-age threshold; current absence and accreditation conclusions require a refresh.'] : []),
204
+ ...(combinedHealth === 'stale' ? ['The catalogue is outside its current-year coverage or review-age threshold; current absence and accreditation conclusions require a refresh.'] : []),
209
205
  ].slice(0, 4);
210
206
  const nextActions = hasAction ? [
211
207
  'Open the official notice and verify its dates, scope, and current outcome before acting.',
@@ -259,7 +255,7 @@ export function registrarStandingCatalogueHealth(now = new Date(), value = REGIS
259
255
  });
260
256
  const age = Math.max(ageDays(catalogue.iana.observedAt, now), ageDays(catalogue.icann.reviewedAt, now));
261
257
  return Object.freeze({
262
- state: age <= REGISTRAR_STANDING_MAX_AGE_DAYS ? 'current' : 'stale',
258
+ state: age <= REGISTRAR_STANDING_MAX_AGE_DAYS && complianceHealth(catalogue, now) === 'current' ? 'current' : 'stale',
263
259
  sourceObservedAt: [catalogue.iana.observedAt, catalogue.icann.reviewedAt]
264
260
  .sort((left, right) => Date.parse(left) - Date.parse(right))[0] ?? null,
265
261
  ageDays: age,
@@ -1,5 +1,5 @@
1
1
  // Registry capability evaluation over the immutable reviewed catalogue.
2
- import { CAPABILITY_BY_SUFFIX, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, MAX_CAPABILITY_INPUT_LENGTH, REGISTRY_CAPABILITIES_VERSION, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, VERSION_26_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, } from "./registry-capability-catalogue.mjs";
2
+ import { CAPABILITY_BY_SUFFIX, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, MAX_CAPABILITY_INPUT_LENGTH, REGISTRY_CAPABILITIES_VERSION, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, } from "./registry-capability-catalogue.mjs";
3
3
  const IANA_TLD_RECORD_PREFIX = 'https://www.iana.org/domains/root/db/';
4
4
  function documentationUrlsForSuffix(capability, suffixes) {
5
5
  if (suffixes.length !== 1 || capability.suffixes.length <= 1) {
@@ -98,7 +98,11 @@ function registryServiceAdmissionFor(value, service) {
98
98
  if (!capability || (service !== 'rdap' && service !== 'whois'))
99
99
  return null;
100
100
  if (service === 'rdap') {
101
- const allowed = capability.rdapAccessProfile === 'iana-bootstrap';
101
+ // A retained absence is a discovery hint, not an access restriction.
102
+ // Both current profiles permit bounded bootstrap discovery; unknown or
103
+ // future restriction profiles remain denied until explicitly handled.
104
+ const allowed = capability.rdapAccessProfile === 'iana-bootstrap'
105
+ || capability.rdapAccessProfile === 'no-iana-service';
102
106
  return {
103
107
  service,
104
108
  state: allowed ? 'allowed' : 'unsupported',
@@ -147,4 +151,4 @@ function registryCompatibilityMatrix() {
147
151
  })))
148
152
  .sort((left, right) => (left.suffixes[0] ?? '').localeCompare(right.suffixes[0] ?? ''));
149
153
  }
150
- export { REGISTRY_CAPABILITIES_VERSION, VERSION_26_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, registryCapabilityFor, registryCompatibilityMatrix, listRegistryCapabilities, registryAccessDiagnosticFor, registryServiceAdmissionFor, registryStandardsCoverageSnapshot, };
154
+ export { REGISTRY_CAPABILITIES_VERSION, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, registryCapabilityFor, registryCompatibilityMatrix, listRegistryCapabilities, registryAccessDiagnosticFor, registryServiceAdmissionFor, registryStandardsCoverageSnapshot, };
@@ -1,17 +1,18 @@
1
1
  // Registry compatibility metadata. IANA bootstrap and referral discovery
2
2
  // remain authoritative. Explicit query profiles may alter only the first
3
3
  // referred registry query after a fixture-backed adapter is integrated.
4
- const REGISTRY_CAPABILITIES_VERSION = 29;
4
+ export const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
5
+ const REGISTRY_CAPABILITIES_VERSION = 31;
5
6
  const MAX_CAPABILITY_INPUT_LENGTH = 253;
6
7
  const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
7
- schema: 'whoisleuth.registry-standards-coverage',
8
+ schema: REGISTRY_STANDARDS_COVERAGE_SCHEMA,
8
9
  version: 1,
9
- verifiedAt: '2026-08-03',
10
+ verifiedAt: '2026-10-03',
10
11
  sources: Object.freeze({
11
- rootZoneVersion: '2026080200',
12
- rootZoneLastUpdatedAt: '2026-08-02T07:07:01.000Z',
13
- rootZoneTldSetSha256: 'aa0a75a9860b2cba07d7fe8172f4546d981be3674bf6764fb0d5f39a45940d25',
14
- rdapBootstrapPublication: '2026-07-23T02:00:03.000Z',
12
+ rootZoneVersion: '2026100200',
13
+ rootZoneLastUpdatedAt: '2026-10-02T07:07:02.000Z',
14
+ rootZoneTldSetSha256: '71093802743b87ee52fac5dd4f662cafd0133dae5089ac6c2e34ee73cebfc836',
15
+ rdapBootstrapPublication: '2026-09-30T23:00:03.000Z',
15
16
  rdapBootstrapVersion: '1.0',
16
17
  urls: Object.freeze([
17
18
  'https://data.iana.org/TLD/tlds-alpha-by-domain.txt',
@@ -20,15 +21,15 @@ const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
20
21
  ]),
21
22
  }),
22
23
  counts: Object.freeze({
23
- activeTlds: 1438,
24
+ activeTlds: 1437,
24
25
  countryCode: 309,
25
- nonCountryCode: 1129,
26
- generic: 1111,
26
+ nonCountryCode: 1128,
27
+ generic: 1110,
27
28
  genericRestricted: 3,
28
29
  sponsored: 14,
29
30
  infrastructure: 1,
30
- rdapBootstrapServiceGroups: 590,
31
- genericAndRestrictedRdapCovered: 1114,
31
+ rdapBootstrapServiceGroups: 592,
32
+ genericAndRestrictedRdapCovered: 1113,
32
33
  sponsoredRdapCovered: 12,
33
34
  infrastructureRdapCovered: 0,
34
35
  }),
@@ -81,7 +82,7 @@ const VERSION_16_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
81
82
  ]);
82
83
  const VERSION_19_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
83
84
  'xn--mgbai9azgqp6j', 'xn--mgbayh7gpa', 'xn--mgbc0a9azcg',
84
- 'xn--mgbcpq6gpa1a', 'xn--mgbpl2fh', 'xn--wgbh1c',
85
+ 'xn--mgbpl2fh', 'xn--wgbh1c',
85
86
  ]);
86
87
  const VERSION_19_RDAP_ONLY_SUFFIXES = Object.freeze(['na', 'pn']);
87
88
  const VERSION_20_FIXTURE_SUFFIXES = Object.freeze([
@@ -116,13 +117,13 @@ const VERSION_24_PROMOTED_SUFFIXES = Object.freeze([
116
117
  'xn--90ae', 'xn--l1acc', 'xn--wgbl6a',
117
118
  ]);
118
119
  const CURRENT_UNVERIFIED_REFERRAL_SUFFIXES = Object.freeze(VERSION_23_UNVERIFIED_REFERRAL_SUFFIXES.filter((suffix) => !VERSION_24_PROMOTED_SUFFIXES.includes(suffix)));
119
- const VERSION_26_NO_RDAP_SUFFIXES = Object.freeze([
120
+ const REVIEWED_NO_RDAP_SUFFIXES = Object.freeze([
120
121
  'ac', 'ae', 'af', 'am', 'at', 'be', 'bg', 'by', 'cl', 'cn',
121
122
  'co', 'de', 'dk', 'ee', 'eu', 'gf', 'gi', 'gt', 'hk', 'hr',
122
- 'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', 'kr', 'kz', 'la',
123
+ 'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', 'kz', 'la',
123
124
  'lt', 'lu', 'lv', 'md', 'me', 'mk', 'mo', 'mq', 'mx', 'my',
124
125
  'nz', 'pk', 'pt', 'ro', 'rs', 'ru', 'sa', 'se', 'sk', 'su',
125
- 'tn', 'tr', 'us', 'vc', 'xn--2scrj9c', 'xn--3e0b707e',
126
+ 'tn', 'tr', 'us', 'vc', 'xn--2scrj9c',
126
127
  'xn--3hcrj9c', 'xn--45br5cyl', 'xn--45brj9c', 'xn--80ao21a',
127
128
  'xn--90a3ac', 'xn--90ais', 'xn--d1alf', 'xn--e1a4c',
128
129
  'xn--fiqs8s', 'xn--fiqz9s', 'xn--fpcrj9c3d', 'xn--gecrj9c',
@@ -131,7 +132,7 @@ const VERSION_26_NO_RDAP_SUFFIXES = Object.freeze([
131
132
  'xn--qxa6a', 'xn--rvc1e0am3e', 'xn--s9brj9c',
132
133
  'xn--xkc2dl3a5ee0h', 'xn--y9a3aq',
133
134
  ]);
134
- const VERSION_26_NO_RDAP_SUFFIX_SET = new Set(VERSION_26_NO_RDAP_SUFFIXES);
135
+ const REVIEWED_NO_RDAP_SUFFIX_SET = new Set(REVIEWED_NO_RDAP_SUFFIXES);
135
136
  const VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES = Object.freeze([
136
137
  'app', 'boo', 'channel', 'dad', 'day', 'dev', 'esq', 'foo', 'how', 'info',
137
138
  'ing', 'meme', 'mov', 'new', 'nexus', 'page', 'phd', 'prof', 'rsvp', 'soy',
@@ -311,6 +312,14 @@ const EXPLICIT_CAPABILITY_SEEDS = [
311
312
  documentationUrls: [`https://www.iana.org/domains/root/db/${suffix}.html`],
312
313
  limitation: RDAP_ONLY_MACHINE_SERVICE_LIMITATION,
313
314
  })),
315
+ {
316
+ id: 'no-iana-machine-service-xn--mgbcpq6gpa1a', suffixes: ['xn--mgbcpq6gpa1a'], registryClass: 'country-code',
317
+ whoisParserProfile: 'generic-colon', fixtureScenarios: [],
318
+ coverageState: 'access_documented', whoisAccessProfile: 'no-iana-service',
319
+ rdapAccessProfile: 'iana-bootstrap', verificationFiles: [],
320
+ documentationUrls: ['https://www.iana.org/domains/root/db/xn--mgbcpq6gpa1a.html'],
321
+ limitation: RDAP_ONLY_MACHINE_SERVICE_LIMITATION,
322
+ },
314
323
  ...VERSION_20_FIXTURE_SUFFIXES.map((suffix) => ({
315
324
  id: `iana-cc-colon-${suffix}`,
316
325
  suffixes: [suffix],
@@ -1043,7 +1052,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
1043
1052
  {
1044
1053
  id: 'nic-bh-icann-colon', suffixes: ['bh'], registryClass: 'country-code',
1045
1054
  whoisParserProfile: 'icann-style-colon', fixtureScenarios: ['registered', 'not_found'],
1046
- rdapAccessProfile: 'no-iana-service',
1055
+ rdapAccessProfile: 'iana-bootstrap',
1047
1056
  documentationUrls: ['https://www.iana.org/domains/root/db/bh.html'],
1048
1057
  },
1049
1058
  {
@@ -1180,7 +1189,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
1180
1189
  ];
1181
1190
  function resolvedSeedRdapAccessProfile(entry) {
1182
1191
  const declared = entry.rdapAccessProfile || 'iana-bootstrap';
1183
- const resolved = new Set(entry.suffixes.map((suffix) => (VERSION_26_NO_RDAP_SUFFIX_SET.has(suffix) ? 'no-iana-service' : declared)));
1192
+ const resolved = new Set(entry.suffixes.map((suffix) => (REVIEWED_NO_RDAP_SUFFIX_SET.has(suffix) ? 'no-iana-service' : declared)));
1184
1193
  if (resolved.size !== 1) {
1185
1194
  throw new Error(`Registry capability ${entry.id} mixes RDAP access profiles and must be split by suffix.`);
1186
1195
  }
@@ -1214,4 +1223,4 @@ for (const capability of EXPLICIT_CAPABILITIES) {
1214
1223
  CAPABILITY_BY_SUFFIX.set(suffix, capability);
1215
1224
  }
1216
1225
  }
1217
- export { REGISTRY_CAPABILITIES_VERSION, MAX_CAPABILITY_INPUT_LENGTH, VERSION_26_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, CAPABILITY_BY_SUFFIX, };
1226
+ export { REGISTRY_CAPABILITIES_VERSION, MAX_CAPABILITY_INPUT_LENGTH, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, CAPABILITY_BY_SUFFIX, };
@@ -1,190 +1,2 @@
1
- // Deterministic normalization for lifecycle dates published by RDAP and
2
- // WHOIS services. Raw upstream strings remain the provenance source; these
3
- // helpers only produce additive Date/ISO companions for comparison, storage,
4
- // and presentation.
5
- const REGISTRY_MONTHS = Object.freeze({
6
- jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
7
- jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12,
8
- });
9
- const REGISTRY_MONTH_NAMES = Object.freeze({
10
- january: 1, february: 2, march: 3, april: 4, may: 5, june: 6,
11
- july: 7, august: 8, september: 9, october: 10, november: 11, december: 12,
12
- });
13
- function utcDateFromParts(year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0) {
14
- if (![year, month, day, hour, minute, second, millisecond].every(Number.isInteger))
15
- return null;
16
- if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 1
17
- || hour < 0 || hour > 23 || minute < 0 || minute > 59
18
- || second < 0 || second > 59 || millisecond < 0 || millisecond > 999)
19
- return null;
20
- const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond));
21
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day
22
- || date.getUTCHours() !== hour || date.getUTCMinutes() !== minute
23
- || date.getUTCSeconds() !== second || date.getUTCMilliseconds() !== millisecond)
24
- return null;
25
- return date;
26
- }
27
- // Parse the explicit non-ISO formats observed across supported ccTLD WHOIS
28
- // services before considering ISO-shaped input. Runtime Date parsing treats
29
- // dotted dates ambiguously and timezone-less timestamps in the host's local
30
- // timezone, so every accepted shape is validated and normalized through UTC.
31
- function parseRegistryDate(input) {
32
- if (typeof input !== 'string')
33
- return null;
34
- // The documented .nz WHOIS examples place one space before the RFC3339
35
- // `T` separator. Canonicalize that registry-specific presentation quirk
36
- // before applying the existing strict ISO-shaped parser.
37
- const value = input.trim()
38
- .replace(/^(\d{4}-\d{2}-\d{2})[ ]+[Tt](?=\d{2}:)/, '$1T')
39
- // The .ee WHOIS service separates an otherwise ISO-shaped numeric
40
- // timezone from its clock with one space.
41
- .replace(/^(\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?)[ ]+([+-]\d{2}:?\d{2})$/, '$1$2');
42
- if (!value)
43
- return null;
44
- // DD.MM.YYYY[ HH:MM:SS] - e.g. 14.03.2024 10:46:48
45
- let match = value.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?$/);
46
- if (match) {
47
- const [, day = '', month = '', year = '', hour = '0', minute = '0', second = '0'] = match;
48
- return utcDateFromParts(+year, +month, +day, +(hour || 0), +(minute || 0), +(second || 0));
49
- }
50
- // YYYY. MM. DD. - e.g. 2006. 09. 18.
51
- match = value.match(/^(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\.?$/);
52
- if (match) {
53
- const [, year = '', month = '', day = ''] = match;
54
- return utcDateFromParts(+year, +month, +day);
55
- }
56
- // YYYY.MM.DD HH:MM:SS - used by NASK's .pl WHOIS service.
57
- match = value.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})$/);
58
- if (match) {
59
- const [, year = '', month = '', day = '', hour = '', minute = '', second = ''] = match;
60
- return utcDateFromParts(+year, +month, +day, +hour, +minute, +second);
61
- }
62
- // YYYY/MM/DD - an unambiguous year-first form used by CIRA WHOIS.
63
- match = value.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/);
64
- if (match) {
65
- const [, year = '', month = '', day = ''] = match;
66
- return utcDateFromParts(+year, +month, +day);
67
- }
68
- // YYYYMMDD with an optional time or Registro.br contact-reference suffix.
69
- // The raw value remains available as provenance while the validated date
70
- // and optional clock time are projected into the additive ISO companion.
71
- match = value.match(/^(\d{4})(\d{2})(\d{2})(?:\s+(\d{1,2}):(\d{2}):(\d{2})|\s+#[0-9]{1,20})?$/);
72
- if (match) {
73
- const [, year = '', month = '', day = '', hour = '0', minute = '0', second = '0'] = match;
74
- return utcDateFromParts(+year, +month, +day, +(hour || 0), +(minute || 0), +(second || 0));
75
- }
76
- // YYYY-Mon-DD. - e.g. 1999-Feb-16.
77
- match = value.match(/^(\d{4})-([A-Za-z]{3})-(\d{1,2})\.?$/);
78
- if (match) {
79
- const month = REGISTRY_MONTHS[(match[2] ?? '').toLowerCase()];
80
- return month ? utcDateFromParts(+(match[1] ?? ''), month, +(match[3] ?? '')) : null;
81
- }
82
- // DD-Mon-YYYY[ HH:MM:SS] - used by several ICANN-style ccTLD services.
83
- match = value.match(/^(\d{1,2})-([A-Za-z]{3})-(\d{4})(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?$/);
84
- if (match) {
85
- const month = REGISTRY_MONTHS[(match[2] ?? '').toLowerCase()];
86
- return month
87
- ? utcDateFromParts(+(match[3] ?? ''), month, +(match[1] ?? ''), +(match[4] || 0), +(match[5] || 0), +(match[6] || 0))
88
- : null;
89
- }
90
- // DD-MM-YYYY - the day-first form published by the currently profiled
91
- // registries that use bare numeric hyphen dates. A future month-first
92
- // source must receive its own marker-gated parser rather than reusing this
93
- // global shape. The four-digit final year remains distinct from ISO below.
94
- match = value.match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
95
- if (match) {
96
- const [, day = '', month = '', year = ''] = match;
97
- return utcDateFromParts(+year, +month, +day);
98
- }
99
- // YYYY-MM-DD HH:MM:SS (GMT+H:MM) - the explicit offset form published by
100
- // the Kazakhstan registry. Parentheses and the GMT marker are required so
101
- // unrelated prose is not accepted as a lifecycle timestamp.
102
- match = value.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s+\(GMT([+-])(\d{1,2}):(\d{2})\)$/i);
103
- if (match) {
104
- const [, year = '', month = '', day = '', hour = '', minute = '', second = '', sign = '', offsetHours = '', offsetMinutes = '',] = match;
105
- const local = utcDateFromParts(+year, +month, +day, +hour, +minute, +second);
106
- if (!local || +offsetHours > 23 || +offsetMinutes > 59)
107
- return null;
108
- const offsetMs = (+offsetHours * 60 + +offsetMinutes) * 60000;
109
- return new Date(local.getTime() + (sign === '+' ? -offsetMs : offsetMs));
110
- }
111
- // DD-MM-YYYY HH:MM:SS GMT+H - the Tunisian registry's bounded local-time
112
- // presentation. Accept an optional minute component but require the GMT
113
- // marker and validate the calendar and offset before normalization.
114
- match = value.match(/^(\d{1,2})-(\d{1,2})-(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+GMT([+-])(\d{1,2})(?::(\d{2}))?$/i);
115
- if (match) {
116
- const [, day = '', month = '', year = '', hour = '', minute = '', second = '', sign = '', offsetHours = '', offsetMinutes = '0',] = match;
117
- const local = utcDateFromParts(+year, +month, +day, +hour, +minute, +second);
118
- if (!local || +offsetHours > 23 || +offsetMinutes > 59)
119
- return null;
120
- const offsetMs = (+offsetHours * 60 + +offsetMinutes) * 60000;
121
- return new Date(local.getTime() + (sign === '+' ? -offsetMs : offsetMs));
122
- }
123
- // DD Mon YYYY - the compact English month form published by THNIC.
124
- match = value.match(/^(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})$/);
125
- if (match) {
126
- const month = REGISTRY_MONTHS[(match[2] ?? '').toLowerCase()];
127
- return month ? utcDateFromParts(+(match[3] ?? ''), month, +(match[1] ?? '')) : null;
128
- }
129
- // Ordinal-day Month YYYY[ at HH:MM:SS.sss] - the English form published
130
- // by the Channel Islands registry. The suffix and `at` token are required
131
- // so ordinary prose containing a month name is not accepted as lifecycle
132
- // evidence.
133
- match = value.match(/^(\d{1,2})(st|nd|rd|th)\s+([A-Za-z]{3,9})\s+(\d{4})(?:\s+at\s+(\d{1,2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?)?$/i);
134
- if (match) {
135
- const day = +(match[1] ?? '');
136
- const suffix = (match[2] ?? '').toLowerCase();
137
- const expectedSuffix = day % 100 >= 11 && day % 100 <= 13
138
- ? 'th'
139
- : ({ 1: 'st', 2: 'nd', 3: 'rd' }[day % 10] || 'th');
140
- if (suffix !== expectedSuffix)
141
- return null;
142
- const monthText = (match[3] ?? '').toLowerCase();
143
- const month = REGISTRY_MONTH_NAMES[monthText] || REGISTRY_MONTHS[monthText];
144
- const millisecond = match[8] ? Number(`${match[8]}00`.slice(0, 3)) : 0;
145
- return month
146
- ? utcDateFromParts(+(match[4] ?? ''), month, day, +(match[5] || 0), +(match[6] || 0), +(match[7] || 0), millisecond)
147
- : null;
148
- }
149
- // Ddd Mon DD YYYY[ HH:MM:SS] - the English textual form used by the
150
- // Belgian registry. Weekday text is presentation-only; the validated
151
- // calendar components determine the canonical UTC companion.
152
- match = value.match(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?$/i);
153
- if (match) {
154
- const month = REGISTRY_MONTHS[(match[1] ?? '').toLowerCase()];
155
- return month
156
- ? utcDateFromParts(+(match[3] ?? ''), month, +(match[2] ?? ''), +(match[4] || 0), +(match[5] || 0), +(match[6] || 0))
157
- : null;
158
- }
159
- // Month DD YYYY - ISNIC publishes English month names with variable
160
- // spacing and no timezone. Keep the accepted shape narrow and accept only
161
- // the twelve exact English names from a fixed table.
162
- match = value.match(/^([A-Za-z]{3,9})\s+(\d{1,2})\s+(\d{4})$/);
163
- if (match) {
164
- const month = REGISTRY_MONTH_NAMES[(match[1] ?? '').toLowerCase()];
165
- return month ? utcDateFromParts(+(match[3] ?? ''), month, +(match[2] ?? '')) : null;
166
- }
167
- // ISO 8601-shaped dates and timestamps. A missing timezone is deliberately
168
- // interpreted as UTC so Express and function deployments agree.
169
- match = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(?:([Zz])|([+-])(\d{2}):?(\d{2}))?)?$/);
170
- if (!match)
171
- return null;
172
- const [, year = '', month = '', day = '', hour = '0', minute = '0', second = '0', fraction, zulu, offsetSign, offsetHours = '0', offsetMinutes = '0',] = match;
173
- const millisecond = fraction ? Number(`${fraction}000`.slice(0, 3)) : 0;
174
- const local = utcDateFromParts(+year, +month, +day, +(hour || 0), +(minute || 0), +(second || 0), millisecond);
175
- if (!local)
176
- return null;
177
- if (!offsetSign || zulu)
178
- return local;
179
- const offsetHour = +offsetHours;
180
- const offsetMinute = +offsetMinutes;
181
- if (offsetHour > 23 || offsetMinute > 59)
182
- return null;
183
- const offsetMs = (offsetHour * 60 + offsetMinute) * 60000;
184
- return new Date(local.getTime() + (offsetSign === '+' ? -offsetMs : offsetMs));
185
- }
186
- function registryDateIso(value) {
187
- const date = parseRegistryDate(value);
188
- return date ? date.toISOString() : null;
189
- }
190
- export { parseRegistryDate, registryDateIso };
1
+ // Stable source-facing entry point; portable consumers use the evidence owner.
2
+ export { parseRegistryDate, registryDateIso } from "../packages/evidence/registry-dates.mjs";