@slicedearth/whoisleuth-cli 2.4.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 (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -1,31 +1,9 @@
1
1
  // Lookup diagnostics, bounded source errors and optional deep-lookup timing.
2
2
  import { OPERATION_BUDGET_ERROR_CODE } from "./operation-budget.mjs";
3
3
  import { FEATURE_DISABLED_ERROR_CODE } from "./feature-policy.mjs";
4
+ import { LOOKUP_SOURCE_STATES, lookupDiagnosticStates } from "./lookup-progress.mjs";
4
5
  const LOOKUP_DIAGNOSTICS_VERSION = 8;
5
6
  const LOOKUP_LEGACY_DIAGNOSTICS_VERSION = 7;
6
- const LOOKUP_SOURCE_STATES = Object.freeze([
7
- 'success', 'partial', 'not_found', 'skipped', 'error', 'unsupported',
8
- 'not_applicable', 'unavailable', 'rate_limited', 'complete', 'disabled', 'stale',
9
- ]);
10
- const SOURCE_STATES = new Set(LOOKUP_SOURCE_STATES);
11
- function lookupDiagnosticStates(value) {
12
- const record = (input) => input && typeof input === 'object' && !Array.isArray(input)
13
- ? input : {};
14
- const diagnostics = record(value);
15
- const candidates = {
16
- rdap: record(diagnostics.rdap).status,
17
- registrar_rdap: record(record(diagnostics.rdap).registrar).status,
18
- whois: record(diagnostics.whois).status,
19
- availability: record(diagnostics.availability).status,
20
- reverse_dns: record(diagnostics.reverseDns).status,
21
- network_context: record(diagnostics.network).status,
22
- security_txt: record(diagnostics.securityTxt).status,
23
- sslbl: record(diagnostics.sslbl).status,
24
- };
25
- return Object.freeze(Object.fromEntries(Object.entries(candidates).map(([source, state]) => [
26
- source, typeof state === 'string' && SOURCE_STATES.has(state) ? state : null,
27
- ])));
28
- }
29
7
  const LOOKUP_TIMING_VERSION = 1;
30
8
  const MAX_LOOKUP_TIMING_MS = 120_000;
31
9
  const LOOKUP_TIMING_SOURCE_ORDER = Object.freeze([
@@ -1,10 +1 @@
1
- // Browser-safe collection bounds shared by the network evidence producers,
2
- // the Lookup response trust boundary, and the display projection.
3
- export const MAX_LOOKUP_DNS_RECORDS_PER_TYPE = 16;
4
- export const MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS = 8;
5
- export const TLS_PROFILE_VERSION = 3;
6
- export const MAX_LOOKUP_TLS_CHAIN_CERTIFICATES = 8;
7
- export const MAX_LOOKUP_TLS_ALT_NAMES = 50;
8
- export const MAX_LOOKUP_TLS_NAME_VALUES = 4;
9
- export const MAX_LOOKUP_TLS_FINDINGS = 5;
10
- export const MAX_LOOKUP_TLS_CERTIFICATE_POLICIES = 16;
1
+ export * from "../packages/analysis/lookup-network-evidence-bounds.mjs";
@@ -1,7 +1,7 @@
1
1
  // Portable bounded NDJSON Lookup progress contract. Partial events are
2
2
  // presentation-only; only a validated final ordinary Lookup result may cross
3
3
  // the persistence boundary.
4
- import { scanBoundedJson, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES } from "./bounded-json.mjs";
4
+ import { scanBoundedJson, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES } from "../packages/analysis/bounded-json.mjs";
5
5
  export const LOOKUP_PROGRESS_SCHEMA = 'whoisleuth.lookup-progress';
6
6
  export const LOOKUP_PROGRESS_VERSION = 1;
7
7
  export const MAX_LOOKUP_PROGRESS_SOURCES = 16;
@@ -32,7 +32,56 @@ const SOURCE_STATES = new Set([
32
32
  'unavailable',
33
33
  'rate_limited',
34
34
  ]);
35
+ // Final diagnostic states are distinct from the provisional progress subset.
36
+ export const LOOKUP_SOURCE_STATES = Object.freeze([
37
+ 'success', 'partial', 'not_found', 'skipped', 'error', 'unsupported',
38
+ 'not_applicable', 'unavailable', 'rate_limited', 'complete', 'disabled', 'stale',
39
+ ]);
40
+ const FINAL_SOURCE_STATES = new Set(LOOKUP_SOURCE_STATES);
41
+ export function lookupDiagnosticStates(value) {
42
+ const record = (input) => input && typeof input === 'object' && !Array.isArray(input)
43
+ ? input : {};
44
+ const diagnostics = record(value);
45
+ const candidates = {
46
+ rdap: record(diagnostics.rdap).status,
47
+ registrar_rdap: record(record(diagnostics.rdap).registrar).status,
48
+ whois: record(diagnostics.whois).status,
49
+ availability: record(diagnostics.availability).status,
50
+ reverse_dns: record(diagnostics.reverseDns).status,
51
+ network_context: record(diagnostics.network).status,
52
+ security_txt: record(diagnostics.securityTxt).status,
53
+ sslbl: record(diagnostics.sslbl).status,
54
+ };
55
+ return Object.freeze(Object.fromEntries(Object.entries(candidates).map(([source, state]) => [
56
+ source, typeof state === 'string' && FINAL_SOURCE_STATES.has(state) ? state : null,
57
+ ])));
58
+ }
35
59
  const SOURCE_ID_RE = /^[a-z][a-z0-9_]{0,39}$/u;
60
+ /** The existing Lookup recipe, shared by offline plans and source progress.
61
+ * This describes admitted families, not provider health or request counts. */
62
+ export function plannedLookupSources(targetType, mode, options = {}) {
63
+ const sources = ['rdap'];
64
+ if (mode === 'fast') {
65
+ if (targetType === 'domain')
66
+ sources.push('domain_evidence');
67
+ return Object.freeze(sources);
68
+ }
69
+ sources.push('whois');
70
+ if (targetType === 'domain') {
71
+ sources.push('domain_evidence', 'registrar_rdap', 'network_context');
72
+ if (options.securityTxt)
73
+ sources.push('security_txt');
74
+ if (options.externalIntelligence)
75
+ sources.push('external_intelligence');
76
+ if (options.malwareHostIntelligence)
77
+ sources.push('malware_host_intelligence');
78
+ if (options.malwareIocIntelligence)
79
+ sources.push('malware_ioc_intelligence');
80
+ }
81
+ else if (targetType === 'ipv4' || targetType === 'ipv6')
82
+ sources.push('reverse_dns');
83
+ return Object.freeze(sources);
84
+ }
36
85
  const TEXT_ENCODER = new TextEncoder();
37
86
  function byteLength(value) {
38
87
  return TEXT_ENCODER.encode(value).byteLength;
@@ -6,12 +6,13 @@
6
6
  // relies on before any evidence is derived or rendered. Nested source records
7
7
  // stay separately attributed and additive; their own normalizers remain the
8
8
  // authority for source-specific fields.
9
- import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "./threat-intelligence-types.mjs";
10
- import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, } from "./http-evidence-bounds.mjs";
9
+ import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "../packages/analysis/threat-intelligence-types.mjs";
10
+ import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, } from "../packages/analysis/http-evidence-bounds.mjs";
11
11
  import { MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, normalizeExplicitIsoTimestamp, } from "../packages/evidence/observation.mjs";
12
- import { assertBoundedJsonStructure } from "./bounded-json.mjs";
13
- import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
12
+ import { assertBoundedJsonStructure } from "../packages/analysis/bounded-json.mjs";
13
+ import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
14
14
  import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
15
+ import { isWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
15
16
  import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
16
17
  import { MAX_SECURITY_POSTURE_FINDINGS, sanitizeLookupChildProfiles, validSecurityPostureFinding, validTlsChainCertificate, validTlsFinding, } from "./lookup-child-profile-contract.mjs";
17
18
  import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "./homepage-metadata-contract.mjs";
@@ -136,22 +137,28 @@ function normalizeThreatFinding(value, providerId) {
136
137
  }
137
138
  function normalizeThreatProvider(value, expectedDomain) {
138
139
  const input = record(value);
140
+ const identity = record(input.provider);
141
+ const providerId = boundedThreatText(identity.id, 80);
142
+ const providerDefinition = providerId && Object.hasOwn(THREAT_INTELLIGENCE_PROVIDERS, providerId)
143
+ ? THREAT_INTELLIGENCE_PROVIDERS[providerId] : null;
144
+ const withheld = (reason) => ({ provider: null, withheld: {
145
+ providerId: providerDefinition ? providerId : null,
146
+ label: providerDefinition ? `${providerDefinition.label} record` : 'Unrecognised provider record',
147
+ count: 1, reason,
148
+ } });
139
149
  if (input.schema !== THREAT_INTELLIGENCE_SCHEMA
140
150
  || input.version !== THREAT_INTELLIGENCE_CONTRACT_VERSION)
141
- return null;
151
+ return withheld('Unsupported provider format; evidence was withheld.');
142
152
  const target = record(input.target);
143
153
  if (target.type !== 'domain'
144
154
  || target.exposure !== 'registrable_domain'
145
155
  || target.value !== expectedDomain)
146
- return null;
147
- const identity = record(input.provider);
148
- const providerId = boundedThreatText(identity.id, 80);
149
- const providerDefinition = providerId ? THREAT_INTELLIGENCE_PROVIDERS[providerId] : null;
156
+ return withheld('The provider target did not match this Lookup; evidence was withheld.');
150
157
  if (!providerId || !providerDefinition)
151
- return null;
158
+ return withheld('The provider identity was not recognised; evidence was withheld.');
152
159
  const state = boundedThreatText(input.state, 32);
153
160
  if (!state || !THREAT_INTELLIGENCE_STATES.has(state))
154
- return null;
161
+ return withheld('The provider result state was not supported; evidence was withheld.');
155
162
  const observationInput = record(input.observation);
156
163
  const limitations = Array.isArray(observationInput.limitations)
157
164
  ? observationInput.limitations
@@ -177,35 +184,40 @@ function normalizeThreatProvider(value, expectedDomain) {
177
184
  ? `${omitted} invalid or over-limit provider finding${omitted === 1 ? ' was' : 's were'} omitted from this view.`
178
185
  : 'Provider findings were not a supported array and were withheld from this view.');
179
186
  }
180
- return {
181
- schema: THREAT_INTELLIGENCE_SCHEMA,
182
- version: THREAT_INTELLIGENCE_CONTRACT_VERSION,
183
- provider: {
184
- id: providerId,
185
- label: providerDefinition.label,
186
- },
187
- target: {
188
- type: 'domain',
189
- value: expectedDomain,
190
- exposure: 'registrable_domain',
191
- },
192
- state: projectionIncomplete && ['success', 'not_found'].includes(state) ? 'partial' : state,
193
- detail: boundedThreatText(input.detail),
194
- findings,
195
- observation: {
196
- observedAt: threatTimestamp(observationInput.observedAt),
197
- limitations: limitations.slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS),
198
- complete: projectionIncomplete ? false : typeof observationInput.complete === 'boolean' ? observationInput.complete : null,
199
- truncated: projectionTruncated ? true : typeof observationInput.truncated === 'boolean' ? observationInput.truncated : null,
200
- },
201
- };
187
+ return { withheld: null, provider: {
188
+ schema: THREAT_INTELLIGENCE_SCHEMA,
189
+ version: THREAT_INTELLIGENCE_CONTRACT_VERSION,
190
+ provider: {
191
+ id: providerId,
192
+ label: providerDefinition.label,
193
+ },
194
+ target: {
195
+ type: 'domain',
196
+ value: expectedDomain,
197
+ exposure: 'registrable_domain',
198
+ },
199
+ state: projectionIncomplete && ['success', 'not_found'].includes(state) ? 'partial' : state,
200
+ detail: boundedThreatText(input.detail),
201
+ findings,
202
+ observation: {
203
+ observedAt: threatTimestamp(observationInput.observedAt),
204
+ limitations: limitations.slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS),
205
+ complete: projectionIncomplete ? false : typeof observationInput.complete === 'boolean' ? observationInput.complete : null,
206
+ truncated: projectionTruncated ? true : typeof observationInput.truncated === 'boolean' ? observationInput.truncated : null,
207
+ },
208
+ } };
202
209
  }
203
210
  function projectThreatProviders(values, expectedDomain) {
204
211
  const grouped = new Map();
212
+ const withheld = [];
213
+ const unexamined = Math.max(0, values.length - MAX_THREAT_INTELLIGENCE_PROVIDERS * 2);
205
214
  for (const value of values.slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS * 2)) {
206
- const provider = normalizeThreatProvider(value, expectedDomain);
207
- if (!provider)
215
+ const projection = normalizeThreatProvider(value, expectedDomain);
216
+ if (projection.withheld) {
217
+ withheld.push(projection.withheld);
208
218
  continue;
219
+ }
220
+ const provider = projection.provider;
209
221
  const providerId = String(record(provider.provider).id || '');
210
222
  if (!providerId)
211
223
  continue;
@@ -218,20 +230,25 @@ function projectThreatProviders(values, expectedDomain) {
218
230
  grouped.set(providerId, { first: provider, signatures: new Set([signature]) });
219
231
  }
220
232
  }
221
- return [...grouped.values()].slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS).map(({ first, signatures }) => (signatures.size === 1
233
+ if (unexamined)
234
+ withheld.push({ providerId: null, label: 'Additional provider records', count: unexamined,
235
+ reason: 'The provider-record review bound was exceeded; these records were not examined.' });
236
+ const withheldIds = new Set(withheld.map((item) => item.providerId).filter(Boolean));
237
+ const providers = [...grouped.values()].slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS).map(({ first, signatures }) => (signatures.size === 1 && !withheldIds.has(String(record(first.provider).id)) && unexamined === 0
222
238
  ? first
223
239
  : {
224
240
  ...first,
225
241
  state: 'partial',
226
- detail: 'Conflicting records were returned for this provider; no conclusive provider projection is available.',
242
+ detail: 'Conflicting or unvalidated provider records prevent a conclusive provider projection.',
227
243
  findings: [],
228
244
  observation: {
229
245
  observedAt: null,
230
- limitations: ['Conflicting records for the same provider identity were withheld rather than resolved by array order.'],
246
+ limitations: ['Conflicting or unvalidated records were withheld rather than resolved by array order.'],
231
247
  complete: false,
232
248
  truncated: null,
233
249
  },
234
250
  }));
251
+ return { providers, withheld };
235
252
  }
236
253
  function optionalBoundedText(value, maxLength) {
237
254
  return value === undefined || (typeof value === 'string'
@@ -624,6 +641,7 @@ function validAvailabilityScalars(value) {
624
641
  && validOptionalNullableText(value.pageTitle, MAX_LOOKUP_RESPONSE_PAGE_TITLE_LENGTH)
625
642
  && validOptionalNullableText(value.dnssec, 40)
626
643
  && (value.deepScanComplete === undefined || typeof value.deepScanComplete === 'boolean')
644
+ && (value.webCollectionQuality === undefined || isWebCollectionQuality(value.webCollectionQuality))
627
645
  && (value.hasMx === undefined || value.hasMx === null || typeof value.hasMx === 'boolean')
628
646
  && (value.hasNullMx === undefined || value.hasNullMx === null || typeof value.hasNullMx === 'boolean')
629
647
  && (value.hasSpf === undefined || value.hasSpf === null || typeof value.hasSpf === 'boolean')
@@ -860,6 +878,7 @@ function parseCompactLookupHttpResponse(value, expectedDomain) {
860
878
  || typeof availability.confidence !== 'string'
861
879
  || !COMPACT_CONFIDENCE_LEVELS.has(availability.confidence)
862
880
  || (availability.deepScanComplete !== undefined && typeof availability.deepScanComplete !== 'boolean')
881
+ || (availability.webCollectionQuality !== undefined && !isWebCollectionQuality(availability.webCollectionQuality))
863
882
  || (availability.bulkComparison !== undefined
864
883
  && !validCompactBulkComparison(availability.bulkComparison))
865
884
  || compactPageIdentity !== null && Object.hasOwn(compactPageIdentity, 'publicationMetadata')
@@ -968,11 +987,20 @@ function createLookupViewModel(response) {
968
987
  ? canonicalRegistrableDomain(response.registrableDomain)
969
988
  ?? canonicalRegistrableDomain(availability.domain)
970
989
  : null;
971
- const providers = expectedThreatDomain
990
+ const threatProjection = expectedThreatDomain
972
991
  && rawThreatIntelligence.version === THREAT_INTELLIGENCE_ENVELOPE_VERSION
973
992
  && Array.isArray(rawThreatIntelligence.providers)
974
993
  ? projectThreatProviders(rawThreatIntelligence.providers, expectedThreatDomain)
975
- : [];
994
+ : { providers: [], withheld: Object.keys(rawThreatIntelligence).length ? [{
995
+ providerId: null,
996
+ label: 'External-intelligence data',
997
+ count: Array.isArray(rawThreatIntelligence.providers) ? rawThreatIntelligence.providers.length : null,
998
+ reason: !expectedThreatDomain ? 'External intelligence was not bound to a domain Lookup and was withheld.'
999
+ : rawThreatIntelligence.version !== THREAT_INTELLIGENCE_ENVELOPE_VERSION
1000
+ ? 'The external-intelligence envelope uses an unsupported version; its records were withheld.'
1001
+ : 'External-intelligence records were not a supported array and were withheld.',
1002
+ }] : [] };
1003
+ const { providers, withheld } = threatProjection;
976
1004
  const threatIntelligence = providers.length
977
1005
  ? { version: THREAT_INTELLIGENCE_ENVELOPE_VERSION, providers }
978
1006
  : {};
@@ -1008,6 +1036,7 @@ function createLookupViewModel(response) {
1008
1036
  sslbl,
1009
1037
  threatIntelligence,
1010
1038
  threatIntelligenceProviders: providers,
1039
+ threatIntelligenceWithheld: withheld,
1011
1040
  dnsEvidence,
1012
1041
  dnsRecords: record(dnsEvidence.records),
1013
1042
  httpEvidence,
@@ -1,11 +1,12 @@
1
1
  // Lookup result shaping after the source plan has settled. This module starts no
2
2
  // collection and preserves compact, diagnostics and enrichment response shapes.
3
+ import { withoutHttpDeliveryMetadata } from "./http-intelligence.mjs";
3
4
  import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
4
5
  import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
5
6
  import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, } from "./lookup-diagnostics.mjs";
6
7
  import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
7
8
  import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
8
- import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
9
+ import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "../packages/analysis/threat-intelligence-types.mjs";
9
10
  import { registryAccessDiagnosticFor } from "./registry-capabilities.mjs";
10
11
  import { buildRegistryInsights } from "./registry-insights.mjs";
11
12
  import { buildRegistrarStanding } from "./registrar-standing.mjs";
@@ -20,17 +21,6 @@ function withoutNestedPublicationMetadata(value) {
20
21
  delete output.publicationMetadata;
21
22
  return output;
22
23
  }
23
- function withoutNestedDeliveryMetadata(value) {
24
- if (!value || typeof value !== 'object' || Array.isArray(value))
25
- return value;
26
- const output = { ...value };
27
- if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
28
- const response = { ...output.response };
29
- delete response.deliveryMetadata;
30
- output.response = response;
31
- }
32
- return output;
33
- }
34
24
  async function buildUnifiedLookupResponse(context) {
35
25
  const { availabilityEnabled, availabilityPromise, classified, compact, dnsIntelligenceEnabled, fast, inspectCertificateWarningData, networkContextPromise, options, rdapEnabled, rdapPromise, registrarRdapPromise, reverseDnsEligible, reverseDnsPromise, securityTxtPromise, skipWhois, threatfoxIntelligencePromise, timing, urlhausIntelligencePromise, urlscanIntelligencePromise, whoisEnabled, whoisPromise, } = context;
36
26
  const [rdapResult, whoisResult, availabilityResult, reverseDnsResult, registrarRdapResult, networkContextResult, securityTxtResult, urlscanIntelligenceResult, urlhausIntelligenceResult, threatfoxIntelligenceResult] = await Promise.allSettled([
@@ -100,7 +90,7 @@ async function buildUnifiedLookupResponse(context) {
100
90
  whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
101
91
  }
102
92
  else if (Array.isArray(whoisChain)) {
103
- whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain) };
93
+ whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined) };
104
94
  }
105
95
  else {
106
96
  whois = {
@@ -154,7 +144,7 @@ async function buildUnifiedLookupResponse(context) {
154
144
  ? 'skipped'
155
145
  : whoisResult.status === 'rejected'
156
146
  ? 'error'
157
- : whoisCollectionStatus(whoisChain);
147
+ : whoisCollectionStatus(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined);
158
148
  const availabilityStatus = classified.type !== 'domain'
159
149
  ? 'not_applicable'
160
150
  : !availabilityEnabled ? 'disabled'
@@ -298,7 +288,7 @@ async function buildUnifiedLookupResponse(context) {
298
288
  availability: {
299
289
  ...compactAvailability,
300
290
  ...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
301
- ...(richHttp !== undefined ? { http: withoutNestedDeliveryMetadata(richHttp) } : {}),
291
+ ...(richHttp !== undefined ? { http: withoutHttpDeliveryMetadata(richHttp) } : {}),
302
292
  ...(bulkComparison ? { bulkComparison } : {}),
303
293
  },
304
294
  diagnostics,
@@ -3,7 +3,8 @@
3
3
  // provider payloads, and request errors. The ordinary final Lookup response
4
4
  // remains the only authoritative and persistable result.
5
5
  import { whoisCollectionStatus } from "./whois-authority.mjs";
6
- import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
6
+ import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "../packages/analysis/threat-intelligence-types.mjs";
7
+ import { plannedLookupSources } from "./lookup-progress.mjs";
7
8
  const RESULT_STATES = new Set([
8
9
  'available',
9
10
  'expiring',
@@ -38,28 +39,13 @@ function sourceTruncated(value) {
38
39
  .some(([key, field]) => field === true && (key === 'truncated' || key.endsWith('Truncated'))));
39
40
  }
40
41
  function plannedLookupProgressSources(classified, options = {}) {
41
- const sources = ['rdap', 'whois'];
42
- if (classified.type === 'domain') {
43
- sources.push('domain_evidence', 'registrar_rdap', 'network_context');
44
- if (options.securityTxt)
45
- sources.push('security_txt');
46
- if (options.externalIntelligence)
47
- sources.push('external_intelligence');
48
- if (options.malwareHostIntelligence)
49
- sources.push('malware_host_intelligence');
50
- if (options.malwareIocIntelligence)
51
- sources.push('malware_ioc_intelligence');
52
- }
53
- else if (classified.type === 'ipv4' || classified.type === 'ipv6') {
54
- sources.push('reverse_dns');
55
- }
56
- return Object.freeze(sources);
42
+ return plannedLookupSources(classified.type, 'deep', options);
57
43
  }
58
- function normalizedState(source, outcome, value) {
44
+ function normalizedState(source, outcome, value, requestedDomain, requested = true) {
59
45
  if (outcome === 'rejected')
60
46
  return 'error';
61
47
  if (value === null || value === undefined)
62
- return 'skipped';
48
+ return source === 'rdap' && requested ? 'unsupported' : 'skipped';
63
49
  if (source === 'rdap') {
64
50
  const rdap = record(value);
65
51
  if (rdap.upstreamStatus === 404)
@@ -69,7 +55,7 @@ function normalizedState(source, outcome, value) {
69
55
  return sourceTruncated(rdap) ? 'partial' : 'success';
70
56
  }
71
57
  if (source === 'whois') {
72
- const status = whoisCollectionStatus(value);
58
+ const status = whoisCollectionStatus(value, requestedDomain);
73
59
  return status === 'complete' ? 'success' : status;
74
60
  }
75
61
  if (source === 'domain_evidence') {
@@ -94,8 +80,8 @@ function normalizedState(source, outcome, value) {
94
80
  return 'skipped';
95
81
  return 'error';
96
82
  }
97
- function normalizeLookupSourceSettlement(source, outcome, value) {
98
- const state = normalizedState(source, outcome, value);
83
+ function normalizeLookupSourceSettlement(source, outcome, value, requestedDomain, requested = true) {
84
+ const state = normalizedState(source, outcome, value, requestedDomain, requested);
99
85
  const sourceRecord = record(value);
100
86
  const threatObservation = THREAT_INTELLIGENCE_SOURCES.has(source)
101
87
  ? record(sourceRecord.observation)
package/lib/lookup.mjs CHANGED
@@ -170,7 +170,7 @@ async function runUnifiedLookup(classified, options = {}) {
170
170
  ]);
171
171
  const notify = (source, outcome, value) => {
172
172
  try {
173
- options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value));
173
+ options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined, source !== 'rdap' || rdapEnabled));
174
174
  }
175
175
  catch {
176
176
  // Presentation callbacks must never change evidence collection.
@@ -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";