@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -1,107 +1,2 @@
1
- import { buildScoreEvidenceQuality, } from "./scoring-evidence-quality.mjs";
2
- export const OPPORTUNITY_MODEL_VERSION = 2;
3
- export const OPPORTUNITY_REVIEW_THRESHOLD = 70;
4
- const STATE_BASE = Object.freeze({
5
- available: 82,
6
- for_sale: 70,
7
- expiring: 52,
8
- registered: 22,
9
- });
10
- const STATE_LABELS = Object.freeze({
11
- available: 'available',
12
- for_sale: 'registered with a sale signal',
13
- expiring: 'registered in a lifecycle transition',
14
- registered: 'registered',
15
- });
16
- function finite(value) {
17
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
18
- }
19
- function formatAge(days) {
20
- if (days < 60)
21
- return `${Math.max(0, Math.round(days))}d old`;
22
- const years = days / 365.25;
23
- return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
24
- }
25
- function addFactor(factors, dimensions, family, label, delta) {
26
- factors.push({ family, label, delta });
27
- dimensions.set(family, (dimensions.get(family) ?? 0) + delta);
28
- }
29
- export function normalizeOpportunityModelVersion(value) {
30
- return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
31
- }
32
- // Opportunity is acquisition readiness, not price, value, eligibility, or a
33
- // prediction that a registration will lapse. It uses only bounded evidence
34
- // already collected for the current lookup.
35
- export function explainOpportunityScore(value) {
36
- if (!value || typeof value !== 'object' || Array.isArray(value))
37
- return null;
38
- const input = value;
39
- const state = input.availability ?? input.state;
40
- if (typeof state !== 'string' || !Object.hasOwn(STATE_BASE, state))
41
- return null;
42
- const factors = [];
43
- const dimensions = new Map();
44
- addFactor(factors, dimensions, 'registration', `Base readiness for “${STATE_LABELS[state] ?? state}”`, STATE_BASE[state] ?? 0);
45
- if (input.confidence === 'high')
46
- addFactor(factors, dimensions, 'registration', 'High-confidence registration decision', 8);
47
- else if (input.confidence === 'medium')
48
- addFactor(factors, dimensions, 'registration', 'Medium-confidence registration decision', 3);
49
- else if (input.confidence === 'low')
50
- addFactor(factors, dimensions, 'registration', 'Low-confidence registration decision', -8);
51
- if (state === 'for_sale')
52
- addFactor(factors, dimensions, 'listing', 'Bounded sale signal observed; listing remains unverified', 6);
53
- else if (input.activityStatus === 'parked')
54
- addFactor(factors, dimensions, 'listing', 'Parked page observed without a verified sale offer', 3);
55
- else if (state === 'registered' && input.activityStatus === 'active') {
56
- addFactor(factors, dimensions, 'listing', 'Active website suggests an in-use registration', -12);
57
- }
58
- if (input.hasPublicRegistrantContact === true) {
59
- addFactor(factors, dimensions, 'contactability', 'Public registrant contact route observed; acquisition suitability is not established', 10);
60
- }
61
- const expiresInDays = finite(input.expiresInDays);
62
- if (state === 'registered' && expiresInDays !== null && expiresInDays >= 0 && expiresInDays < 30) {
63
- addFactor(factors, dimensions, 'lifecycle', 'Published expiry is within 30 days; release is not implied', 4);
64
- }
65
- if (typeof input.privacyProtected === 'boolean') {
66
- addFactor(factors, dimensions, 'contactability', 'Registration privacy is neutral; it does not establish contactability', 0);
67
- }
68
- const ageDays = finite(input.domainAgeDays);
69
- if (ageDays !== null && ageDays >= 0) {
70
- addFactor(factors, dimensions, 'registration', `Domain age (${formatAge(ageDays)}) is context only`, 0);
71
- }
72
- const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
73
- const score = Math.max(0, Math.min(100, rawScore));
74
- const dimensionLabels = {
75
- registration: 'Registration certainty',
76
- listing: 'Listing readiness',
77
- contactability: 'Contactability',
78
- lifecycle: 'Lifecycle timing',
79
- };
80
- const observedFamilies = [...dimensions.keys()];
81
- const evidenceQuality = buildScoreEvidenceQuality(input, {
82
- expectedDepth: 'fast',
83
- observedFamilies,
84
- expectedFamilies: ['registration'],
85
- });
86
- return {
87
- modelVersion: OPPORTUNITY_MODEL_VERSION,
88
- score,
89
- rawScore,
90
- capped: score !== rawScore,
91
- factors,
92
- dimensions: [...dimensions.entries()].map(([id, contribution]) => ({ id, label: dimensionLabels[id], contribution })),
93
- evidenceQuality,
94
- };
95
- }
96
- export function computeOpportunityScore(value) {
97
- return explainOpportunityScore(value)?.score ?? null;
98
- }
99
- export function opportunityTone(score) {
100
- if (score === null)
101
- return 'neutral';
102
- if (score >= OPPORTUNITY_REVIEW_THRESHOLD)
103
- return 'good';
104
- if (score >= 40)
105
- return 'neutral';
106
- return 'warn';
107
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/opportunity-scoring.mjs";
@@ -1,5 +1,5 @@
1
1
  import { WHOISLEUTH_APPLICATION_VERSION } from "./application-version.mjs";
2
- import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "./project-metadata.mjs";
2
+ import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "../packages/analysis/project-metadata.mjs";
3
3
  export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
4
4
  export const WHOISLEUTH_USER_AGENT_VERSION = WHOISLEUTH_APPLICATION_VERSION;
5
5
  export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
@@ -64,7 +64,24 @@ function reduceDynamicValues(value) {
64
64
  .replace(/\b\d{4}-\d{1,2}-\d{1,2}(?:[t\s]\d{1,2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:z|[+-]\d{2}:?\d{2})?)?\b/gi, '<time>')
65
65
  .replace(/\b\d{10,13}\b/g, '<time>')
66
66
  .replace(/\b[a-f0-9]{16,}\b/gi, '<id>')
67
- .replace(/\b(?=[a-z0-9_-]{20,}\b)(?=[a-z0-9_-]*[a-z])(?=[a-z0-9_-]*\d)[a-z0-9_-]+\b/gi, '<id>');
67
+ .replace(/[a-z0-9_-]+/gi, (token) => {
68
+ // Visit each maximal token once. Repeated lookaheads at word boundaries
69
+ // rescan hyphenated text quadratically when a required digit is absent.
70
+ // Only hyphens are outside the former ASCII word-boundary match;
71
+ // underscores remain part of the identifier, including at either end.
72
+ let start = 0;
73
+ let end = token.length;
74
+ while (start < end && token[start] === '-')
75
+ start += 1;
76
+ while (end > start && token[end - 1] === '-')
77
+ end -= 1;
78
+ if (end - start < 20)
79
+ return token;
80
+ const identifier = token.slice(start, end);
81
+ return /[a-z]/i.test(identifier) && /\d/.test(identifier)
82
+ ? `${token.slice(0, start)}<id>${token.slice(end)}`
83
+ : token;
84
+ });
68
85
  }
69
86
  function normalizeText(value, maxLength = 512) {
70
87
  const normalized = reduceDynamicValues(String(value || ''))
@@ -1,34 +1,2 @@
1
- // Browser-safe comparison contract for the 64-bit perceptual hashes produced
2
- // by the bounded image decoder. Keeping comparison and informativeness here
3
- // prevents Node and browser consumers from drifting into different matches.
4
- export const PERCEPTUAL_HASH_HEX_RE = /^[0-9a-f]{16}$/u;
5
- export const MIN_INFORMATIVE_HASH_BITS = 10;
6
- const POPCOUNT = new Uint8Array(16);
7
- for (let index = 0; index < POPCOUNT.length; index += 1) {
8
- POPCOUNT[index] = (index & 1) + ((index >> 1) & 1) + ((index >> 2) & 1) + ((index >> 3) & 1);
9
- }
10
- export function isPerceptualHash(value) {
11
- return typeof value === 'string' && PERCEPTUAL_HASH_HEX_RE.test(value);
12
- }
13
- function popcountHex(hex) {
14
- let bits = 0;
15
- for (let index = 0; index < hex.length; index += 1) {
16
- bits += POPCOUNT[parseInt(hex.charAt(index), 16)] ?? 0;
17
- }
18
- return bits;
19
- }
20
- export function isInformativePerceptualHash(value) {
21
- if (!isPerceptualHash(value))
22
- return false;
23
- const bits = popcountHex(value);
24
- return bits >= MIN_INFORMATIVE_HASH_BITS && bits <= 64 - MIN_INFORMATIVE_HASH_BITS;
25
- }
26
- export function hammingDistanceHex(left, right) {
27
- if (!isPerceptualHash(left) || !isPerceptualHash(right))
28
- return null;
29
- let distance = 0;
30
- for (let index = 0; index < 16; index += 1) {
31
- distance += POPCOUNT[parseInt(left.charAt(index), 16) ^ parseInt(right.charAt(index), 16)] ?? 0;
32
- }
33
- return distance;
34
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/perceptual-hash-comparison.mjs";
@@ -21,11 +21,23 @@
21
21
  // case, while staying cheap (no DCT). Output is 16 lowercase hex chars;
22
22
  // similarity is the Hamming distance between two such strings.
23
23
  import * as zlib from 'node:zlib';
24
- import { hammingDistanceHex, isInformativePerceptualHash, } from "./perceptual-hash-comparison.mjs";
24
+ import { hammingDistanceHex, isInformativePerceptualHash, } from "../packages/analysis/perceptual-hash-comparison.mjs";
25
25
  // Bound the decode work on attacker-controlled image bytes. Real favicons top
26
26
  // out around 256x256; larger inputs above this ceiling remain exact-only
27
27
  // evidence rather than causing unbounded pixel allocation.
28
28
  const MAX_DIM = 1024;
29
+ // Permit two maximum-sized decode attempts (more than a normal resolution
30
+ // pyramid), while bounding aggregate inflation/filtering across a container.
31
+ // Encoded payloads may consume at most the container's byte length in total;
32
+ // ordinary non-overlapping entries never exceed that bound.
33
+ const MAX_DECODE_PIXELS = 2 * MAX_DIM * MAX_DIM;
34
+ function admitPixels(work, width, height) {
35
+ const count = width * height;
36
+ if (count > work.pixels)
37
+ return false;
38
+ work.pixels -= count;
39
+ return true;
40
+ }
29
41
  const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
30
42
  function byteAt(bytes, index) {
31
43
  return bytes[index] ?? 0;
@@ -53,7 +65,7 @@ function paethPredictor(a, b, c) {
53
65
  // { width, height, pixels } with pixels as RGBA bytes, or null for anything
54
66
  // outside that subset (or malformed input). CRCs are intentionally not
55
67
  // verified - we're computing a fuzzy hash, not validating integrity.
56
- function decodePng(buf) {
68
+ function decodePng(buf, work) {
57
69
  const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
58
70
  let width = 0;
59
71
  let height = 0;
@@ -90,7 +102,7 @@ function decodePng(buf) {
90
102
  transparency = data;
91
103
  }
92
104
  else if (type === 'IDAT') {
93
- idatParts.push(Buffer.from(data));
105
+ idatParts.push(data);
94
106
  }
95
107
  else if (type === 'IEND') {
96
108
  break;
@@ -113,6 +125,8 @@ function decodePng(buf) {
113
125
  return null;
114
126
  if (idatParts.length === 0)
115
127
  return null;
128
+ if (!admitPixels(work, width, height))
129
+ return null;
116
130
  // A valid non-interlaced 8-bit PNG decompresses to exactly one filter byte
117
131
  // plus one scanline per row. Cap the inflate at that size so a malicious
118
132
  // IDAT can't expand a sub-200KB favicon into hundreds of MB (a zip bomb)
@@ -218,7 +232,7 @@ function decodePng(buf) {
218
232
  // palettized and 24/32-bit truecolour. The stored height is doubled (it
219
233
  // includes the 1-bpp AND transparency mask). Non-alpha and legacy all-zero
220
234
  // alpha images use that mask; meaningful 32-bit alpha remains authoritative.
221
- function decodeDib(buf) {
235
+ function decodeDib(buf, work) {
222
236
  if (buf.length < 40)
223
237
  return null;
224
238
  const headerSize = buf.readUInt32LE(0);
@@ -258,6 +272,8 @@ function decodeDib(buf) {
258
272
  const maskPresent = maskStart + maskRowSize * height <= buf.length;
259
273
  if (bitCount !== 32 && !maskPresent)
260
274
  return null;
275
+ if (!admitPixels(work, width, height))
276
+ return null;
261
277
  const paletteColor = (index) => {
262
278
  if (!palette || index >= palette.count)
263
279
  return [0, 0, 0];
@@ -323,9 +339,10 @@ function decodeDib(buf) {
323
339
  // one is in a variant we don't decode (e.g. a 4-bit-palette PNG sub-entry
324
340
  // alongside a plain BMP one). A multi-size favicon.ico only needs one usable
325
341
  // entry, and any size hashes to nearly the same dHash since it downsamples.
326
- function decodeIco(buf) {
342
+ function decodeIco(buf, work) {
327
343
  const count = buf.readUInt16LE(4);
328
- if (count < 1 || 6 + count * 16 > buf.length)
344
+ const directoryEnd = 6 + count * 16;
345
+ if (count < 1 || directoryEnd > buf.length)
329
346
  return null;
330
347
  const entries = [];
331
348
  for (let i = 0; i < count; i += 1) {
@@ -334,14 +351,22 @@ function decodeIco(buf) {
334
351
  const h = buf[entry + 1] || 256;
335
352
  const size = buf.readUInt32LE(entry + 8);
336
353
  const dataOffset = buf.readUInt32LE(entry + 12);
337
- if (dataOffset + size > buf.length || size < 1)
354
+ if (dataOffset < directoryEnd || dataOffset + size > buf.length || size < 1)
338
355
  continue;
339
356
  entries.push({ area: w * h, dataOffset, size });
340
357
  }
341
358
  entries.sort((a, b) => b.area - a.area);
359
+ const visited = new Set();
342
360
  for (const { dataOffset, size } of entries) {
361
+ const identity = `${dataOffset}:${size}`;
362
+ if (visited.has(identity))
363
+ continue;
364
+ visited.add(identity);
365
+ if (size > work.encodedBytes)
366
+ return null;
367
+ work.encodedBytes -= size;
343
368
  const payload = buf.subarray(dataOffset, dataOffset + size);
344
- const image = isPng(payload) ? decodePng(payload) : decodeDib(payload);
369
+ const image = isPng(payload) ? decodePng(payload, work) : decodeDib(payload, work);
345
370
  if (image)
346
371
  return image;
347
372
  }
@@ -350,10 +375,11 @@ function decodeIco(buf) {
350
375
  function decodeImage(buf) {
351
376
  if (!Buffer.isBuffer(buf))
352
377
  return null;
378
+ const work = { encodedBytes: buf.length, pixels: MAX_DECODE_PIXELS };
353
379
  if (isPng(buf))
354
- return decodePng(buf);
380
+ return decodePng(buf, work);
355
381
  if (isIco(buf))
356
- return decodeIco(buf);
382
+ return decodeIco(buf, work);
357
383
  return null;
358
384
  }
359
385
  // Downscales to cols x rows of grayscale luma. Transparency is composited
@@ -1,29 +1,2 @@
1
- import { WHOISLEUTH_SOURCE_REPOSITORY_URL, } from "./project-metadata.mjs";
2
- import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
3
- const URI_LABEL_UNSAFE_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
4
- const URI_LABEL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/|[^\s])/u;
5
- export function isUriShapedLabel(value) {
6
- if (typeof value !== 'string')
7
- return false;
8
- const candidate = value.replace(URI_LABEL_UNSAFE_GLOBAL_RE, '').trim();
9
- return URI_LABEL_SCHEME_RE.test(candidate);
10
- }
11
- export function buildPortableGeneratorMetadata(applicationVersion) {
12
- let version = null;
13
- if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
14
- try {
15
- version = normalizeBoundedSemanticVersion(applicationVersion, 'Portable report application');
16
- }
17
- catch {
18
- version = null;
19
- }
20
- }
21
- return Object.freeze({
22
- name: 'WHOISleuth',
23
- version,
24
- projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
25
- });
26
- }
27
- export function portableGeneratorAttribution(generator) {
28
- return `Generated with ${generator.name}${generator.version ? ` ${generator.version}` : ''} · Source: ${generator.projectUrl}`;
29
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/portable-generator.mjs";
@@ -1,8 +1,2 @@
1
- // Browser-safe public project metadata shared by request identity, package
2
- // verification, canonical URLs, structured data, and public policy pages.
3
- export const WHOISLEUTH_SITE_ORIGIN = 'https://www.whoisleuth.com';
4
- export const WHOISLEUTH_PROJECT_URL = `${WHOISLEUTH_SITE_ORIGIN}/`;
5
- export const WHOISLEUTH_REQUEST_POLICY_URL = `${WHOISLEUTH_SITE_ORIGIN}/request-policy`;
6
- export const WHOISLEUTH_SOURCE_REPOSITORY_URL = 'https://github.com/slicedearth/whoisleuth';
7
- export const WHOISLEUTH_SOURCE_REPOSITORY_GIT_URL = `git+${WHOISLEUTH_SOURCE_REPOSITORY_URL}.git`;
8
- export const WHOISLEUTH_SOURCE_ISSUES_URL = `${WHOISLEUTH_SOURCE_REPOSITORY_URL}/issues`;
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/project-metadata.mjs";
@@ -61,21 +61,25 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
61
61
  }));
62
62
  continue;
63
63
  }
64
- let data;
64
+ let data = null;
65
65
  try {
66
66
  data = JSON.parse(upstream.text);
67
67
  }
68
68
  catch {
69
- attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
70
- status: upstream.status,
71
- detail: 'The endpoint returned invalid JSON.',
72
- }));
73
- continue;
69
+ // RFC 7480 section 5.3 makes the explanatory body optional for a
70
+ // negative answer. URL binding and transport/body bounds still apply.
71
+ if (upstream.status !== 404) {
72
+ attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
73
+ status: upstream.status,
74
+ detail: 'The endpoint returned invalid JSON.',
75
+ }));
76
+ continue;
77
+ }
74
78
  }
75
79
  if (upstream.status === 404) {
76
80
  attempts.push(rdapAttempt(selectedEndpoint, 'not_found', {
77
81
  status: upstream.status,
78
- detail: 'The authoritative endpoint reported no matching object.',
82
+ detail: `The authoritative endpoint reported no matching object.${data === null ? ' No usable JSON explanation was supplied.' : ''}`,
79
83
  selected: true,
80
84
  }));
81
85
  return {
@@ -1,56 +1,2 @@
1
- import { getDomain, getPublicSuffix } from 'tldts';
2
- import { isValidAsciiDomainName } from "./hostname.mjs";
3
- import { MAX_DOMAIN_NAME_LENGTH, MAX_DOMAIN_LABEL_LENGTH } from "../packages/contracts/domain-name.mjs";
4
- // Reserve the shortest generated name and its separating dot.
5
- export const MAX_DISCOVERY_SUFFIX_LENGTH = MAX_DOMAIN_NAME_LENGTH - 2;
6
- /** ICANN suffix rules for a plain ASCII hostname; private suffixes do not change identity. */
7
- export function publicSuffixForAsciiHostname(value) {
8
- if (!isValidAsciiDomainName(value, { requireDot: false, requireLowercase: true }))
9
- return null;
10
- return getPublicSuffix(value);
11
- }
12
- /** Preserve reserved single labels; compound suffixes must match the local ICANN suffix rules. */
13
- export function normalizeDiscoverySuffix(raw) {
14
- if (typeof raw !== 'string' || raw.length > MAX_DISCOVERY_SUFFIX_LENGTH + 1 || /[\u0000-\u001f\u007f]/u.test(raw))
15
- return null;
16
- const value = raw.trim().toLowerCase().replace(/^\./u, '');
17
- if (!value || value.length > MAX_DISCOVERY_SUFFIX_LENGTH)
18
- return null;
19
- if (value.length <= MAX_DOMAIN_LABEL_LENGTH && /^[a-z]{2,}$/u.test(value))
20
- return value;
21
- return value.includes('.') && publicSuffixForAsciiHostname(value) === value ? value : null;
22
- }
23
- // Returns one canonical registrable-domain identity for an already public DNS
24
- // hostname. Callers retain the original hostname separately when subdomain
25
- // context matters; authority, evidence binding and scoring use this value.
26
- export function canonicalRegistrableDomain(value) {
27
- if (typeof value !== 'string')
28
- return null;
29
- const trimmed = value.trim();
30
- if (!trimmed || /[\u0000-\u001f\u007f]/u.test(trimmed))
31
- return null;
32
- const withoutScheme = trimmed.replace(/^[a-z]+:\/\//iu, '').split(/[/?#]/u)[0] ?? '';
33
- if (withoutScheme.endsWith('..'))
34
- return null;
35
- const candidate = withoutScheme.replace(/\.$/u, '');
36
- if (!candidate)
37
- return null;
38
- let hostname;
39
- try {
40
- hostname = new URL(`https://${candidate}/`).hostname.toLowerCase().replace(/\.$/u, '');
41
- }
42
- catch {
43
- return null;
44
- }
45
- if (!isValidAsciiDomainName(hostname, { requireDot: true, requireLowercase: true }))
46
- return null;
47
- // Keep this boundary identical to classifyQuery() and the collection
48
- // runtime: registrable identity follows the ICANN Public Suffix List, while
49
- // privately operated suffixes remain ordinary host context.
50
- const domain = getDomain(hostname);
51
- return domain
52
- && isValidAsciiDomainName(domain, { requireDot: true, requireLowercase: true })
53
- && (hostname === domain || hostname.endsWith(`.${domain}`))
54
- ? domain
55
- : null;
56
- }
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')
@@ -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) {
@@ -151,4 +151,4 @@ function registryCompatibilityMatrix() {
151
151
  })))
152
152
  .sort((left, right) => (left.suffixes[0] ?? '').localeCompare(right.suffixes[0] ?? ''));
153
153
  }
154
- 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, };
@@ -2,17 +2,17 @@
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
4
  export const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
5
- const REGISTRY_CAPABILITIES_VERSION = 29;
5
+ const REGISTRY_CAPABILITIES_VERSION = 31;
6
6
  const MAX_CAPABILITY_INPUT_LENGTH = 253;
7
7
  const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
8
8
  schema: REGISTRY_STANDARDS_COVERAGE_SCHEMA,
9
9
  version: 1,
10
- verifiedAt: '2026-08-03',
10
+ verifiedAt: '2026-10-03',
11
11
  sources: Object.freeze({
12
- rootZoneVersion: '2026080200',
13
- rootZoneLastUpdatedAt: '2026-08-02T07:07:01.000Z',
14
- rootZoneTldSetSha256: 'aa0a75a9860b2cba07d7fe8172f4546d981be3674bf6764fb0d5f39a45940d25',
15
- 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',
16
16
  rdapBootstrapVersion: '1.0',
17
17
  urls: Object.freeze([
18
18
  'https://data.iana.org/TLD/tlds-alpha-by-domain.txt',
@@ -21,15 +21,15 @@ const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
21
21
  ]),
22
22
  }),
23
23
  counts: Object.freeze({
24
- activeTlds: 1438,
24
+ activeTlds: 1437,
25
25
  countryCode: 309,
26
- nonCountryCode: 1129,
27
- generic: 1111,
26
+ nonCountryCode: 1128,
27
+ generic: 1110,
28
28
  genericRestricted: 3,
29
29
  sponsored: 14,
30
30
  infrastructure: 1,
31
- rdapBootstrapServiceGroups: 590,
32
- genericAndRestrictedRdapCovered: 1114,
31
+ rdapBootstrapServiceGroups: 592,
32
+ genericAndRestrictedRdapCovered: 1113,
33
33
  sponsoredRdapCovered: 12,
34
34
  infrastructureRdapCovered: 0,
35
35
  }),
@@ -82,7 +82,7 @@ const VERSION_16_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
82
82
  ]);
83
83
  const VERSION_19_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
84
84
  'xn--mgbai9azgqp6j', 'xn--mgbayh7gpa', 'xn--mgbc0a9azcg',
85
- 'xn--mgbcpq6gpa1a', 'xn--mgbpl2fh', 'xn--wgbh1c',
85
+ 'xn--mgbpl2fh', 'xn--wgbh1c',
86
86
  ]);
87
87
  const VERSION_19_RDAP_ONLY_SUFFIXES = Object.freeze(['na', 'pn']);
88
88
  const VERSION_20_FIXTURE_SUFFIXES = Object.freeze([
@@ -117,13 +117,13 @@ const VERSION_24_PROMOTED_SUFFIXES = Object.freeze([
117
117
  'xn--90ae', 'xn--l1acc', 'xn--wgbl6a',
118
118
  ]);
119
119
  const CURRENT_UNVERIFIED_REFERRAL_SUFFIXES = Object.freeze(VERSION_23_UNVERIFIED_REFERRAL_SUFFIXES.filter((suffix) => !VERSION_24_PROMOTED_SUFFIXES.includes(suffix)));
120
- const VERSION_26_NO_RDAP_SUFFIXES = Object.freeze([
120
+ const REVIEWED_NO_RDAP_SUFFIXES = Object.freeze([
121
121
  'ac', 'ae', 'af', 'am', 'at', 'be', 'bg', 'by', 'cl', 'cn',
122
122
  'co', 'de', 'dk', 'ee', 'eu', 'gf', 'gi', 'gt', 'hk', 'hr',
123
- 'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', 'kr', 'kz', 'la',
123
+ 'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', 'kz', 'la',
124
124
  'lt', 'lu', 'lv', 'md', 'me', 'mk', 'mo', 'mq', 'mx', 'my',
125
125
  'nz', 'pk', 'pt', 'ro', 'rs', 'ru', 'sa', 'se', 'sk', 'su',
126
- 'tn', 'tr', 'us', 'vc', 'xn--2scrj9c', 'xn--3e0b707e',
126
+ 'tn', 'tr', 'us', 'vc', 'xn--2scrj9c',
127
127
  'xn--3hcrj9c', 'xn--45br5cyl', 'xn--45brj9c', 'xn--80ao21a',
128
128
  'xn--90a3ac', 'xn--90ais', 'xn--d1alf', 'xn--e1a4c',
129
129
  'xn--fiqs8s', 'xn--fiqz9s', 'xn--fpcrj9c3d', 'xn--gecrj9c',
@@ -132,7 +132,7 @@ const VERSION_26_NO_RDAP_SUFFIXES = Object.freeze([
132
132
  'xn--qxa6a', 'xn--rvc1e0am3e', 'xn--s9brj9c',
133
133
  'xn--xkc2dl3a5ee0h', 'xn--y9a3aq',
134
134
  ]);
135
- 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);
136
136
  const VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES = Object.freeze([
137
137
  'app', 'boo', 'channel', 'dad', 'day', 'dev', 'esq', 'foo', 'how', 'info',
138
138
  'ing', 'meme', 'mov', 'new', 'nexus', 'page', 'phd', 'prof', 'rsvp', 'soy',
@@ -312,6 +312,14 @@ const EXPLICIT_CAPABILITY_SEEDS = [
312
312
  documentationUrls: [`https://www.iana.org/domains/root/db/${suffix}.html`],
313
313
  limitation: RDAP_ONLY_MACHINE_SERVICE_LIMITATION,
314
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
+ },
315
323
  ...VERSION_20_FIXTURE_SUFFIXES.map((suffix) => ({
316
324
  id: `iana-cc-colon-${suffix}`,
317
325
  suffixes: [suffix],
@@ -1044,7 +1052,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
1044
1052
  {
1045
1053
  id: 'nic-bh-icann-colon', suffixes: ['bh'], registryClass: 'country-code',
1046
1054
  whoisParserProfile: 'icann-style-colon', fixtureScenarios: ['registered', 'not_found'],
1047
- rdapAccessProfile: 'no-iana-service',
1055
+ rdapAccessProfile: 'iana-bootstrap',
1048
1056
  documentationUrls: ['https://www.iana.org/domains/root/db/bh.html'],
1049
1057
  },
1050
1058
  {
@@ -1181,7 +1189,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
1181
1189
  ];
1182
1190
  function resolvedSeedRdapAccessProfile(entry) {
1183
1191
  const declared = entry.rdapAccessProfile || 'iana-bootstrap';
1184
- 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)));
1185
1193
  if (resolved.size !== 1) {
1186
1194
  throw new Error(`Registry capability ${entry.id} mixes RDAP access profiles and must be split by suffix.`);
1187
1195
  }
@@ -1215,4 +1223,4 @@ for (const capability of EXPLICIT_CAPABILITIES) {
1215
1223
  CAPABILITY_BY_SUFFIX.set(suffix, capability);
1216
1224
  }
1217
1225
  }
1218
- 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, };