@slicedearth/whoisleuth-cli 1.32.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 (189) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
  3. package/NOTICE +76 -0
  4. package/README.md +84 -0
  5. package/TRADEMARKS.md +41 -0
  6. package/bin/whoisleuth.mjs +35 -0
  7. package/cli/archive-inspect.mjs +198 -0
  8. package/cli/arguments.mjs +1022 -0
  9. package/cli/artifact-verify.mjs +210 -0
  10. package/cli/bulk-checkpoint.mjs +218 -0
  11. package/cli/bulk-command-runner.mjs +109 -0
  12. package/cli/bulk-output.mjs +112 -0
  13. package/cli/bulk.mjs +133 -0
  14. package/cli/compare.mjs +238 -0
  15. package/cli/completion.mjs +175 -0
  16. package/cli/csv.mjs +1 -0
  17. package/cli/discover.mjs +50 -0
  18. package/cli/discovery-command-runner.mjs +36 -0
  19. package/cli/discovery-observation-snapshot.mjs +315 -0
  20. package/cli/discovery-scan-command-runner.mjs +141 -0
  21. package/cli/discovery-scan.mjs +325 -0
  22. package/cli/discovery-snapshot.mjs +133 -0
  23. package/cli/discovery-workflow.mjs +73 -0
  24. package/cli/doctor.mjs +137 -0
  25. package/cli/errors.mjs +21 -0
  26. package/cli/evidence-command-arguments.mjs +145 -0
  27. package/cli/evidence-command-runner.mjs +106 -0
  28. package/cli/evidence-signing.mjs +215 -0
  29. package/cli/exit-codes.mjs +9 -0
  30. package/cli/export-evidence.mjs +28 -0
  31. package/cli/formatters/evidence-report.mjs +3 -0
  32. package/cli/formatters/html.mjs +69 -0
  33. package/cli/formatters/json.mjs +135 -0
  34. package/cli/formatters/markdown.mjs +3 -0
  35. package/cli/formatters/terminal.mjs +658 -0
  36. package/cli/http.mjs +27 -0
  37. package/cli/lookup-command-runner.mjs +87 -0
  38. package/cli/lookup-diff.mjs +171 -0
  39. package/cli/mail-review.mjs +204 -0
  40. package/cli/manual.mjs +34 -0
  41. package/cli/network-command-runner.mjs +88 -0
  42. package/cli/output-file.mjs +97 -0
  43. package/cli/page-compare.mjs +123 -0
  44. package/cli/posture.mjs +20 -0
  45. package/cli/progress-events.mjs +53 -0
  46. package/cli/progress.mjs +56 -0
  47. package/cli/registry-access.mjs +3 -0
  48. package/cli/registry-support.mjs +120 -0
  49. package/cli/risk-calibration.mjs +294 -0
  50. package/cli/runner-types.mjs +1 -0
  51. package/cli/runner.mjs +726 -0
  52. package/cli/saved-lookup.mjs +87 -0
  53. package/cli/source-reliability.mjs +727 -0
  54. package/cli/strict-exit.mjs +55 -0
  55. package/cli/terminal-presentation.mjs +147 -0
  56. package/docs/cli.md +827 -0
  57. package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
  58. package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
  59. package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
  60. package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
  61. package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
  62. package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
  63. package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
  64. package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
  65. package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
  66. package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
  67. package/frontend/src/lib/analysis/bulk-sort.js +49 -0
  68. package/frontend/src/lib/analysis/campaign-model.js +265 -0
  69. package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
  70. package/frontend/src/lib/analysis/case-migration-model.js +248 -0
  71. package/frontend/src/lib/analysis/case-model.js +5 -0
  72. package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
  73. package/frontend/src/lib/analysis/case-record-core.js +232 -0
  74. package/frontend/src/lib/analysis/case-record-model.js +7 -0
  75. package/frontend/src/lib/analysis/case-record-operations.js +224 -0
  76. package/frontend/src/lib/analysis/case-response-model.js +640 -0
  77. package/frontend/src/lib/analysis/case-response-packet.js +573 -0
  78. package/frontend/src/lib/analysis/case-storage-model.js +93 -0
  79. package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
  80. package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
  81. package/frontend/src/lib/analysis/http-summary.js +165 -0
  82. package/frontend/src/lib/analysis/investigation-guide.js +500 -0
  83. package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
  84. package/frontend/src/lib/analysis/page-baseline.js +272 -0
  85. package/frontend/src/lib/analysis/page-similarity.js +179 -0
  86. package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
  87. package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
  88. package/frontend/src/lib/analysis/scoring.js +135 -0
  89. package/frontend/src/lib/analysis/shortlist-model.js +209 -0
  90. package/frontend/src/lib/analysis/utils.js +265 -0
  91. package/frontend/src/lib/analysis/watchlist-history.js +643 -0
  92. package/frontend/src/lib/analysis/watchlist-store.js +127 -0
  93. package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
  94. package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
  95. package/frontend/src/lib/analysis/workspace-archive.js +472 -0
  96. package/lib/abort.mjs +26 -0
  97. package/lib/abusech-auth.mjs +19 -0
  98. package/lib/availability.mjs +716 -0
  99. package/lib/bounded-contract-normalizers.mjs +61 -0
  100. package/lib/browser-library-profile.mjs +310 -0
  101. package/lib/bulk-comparison-evidence.mjs +83 -0
  102. package/lib/classify.mjs +93 -0
  103. package/lib/client-behavior-profile.mjs +83 -0
  104. package/lib/credential-surface-profile.mjs +66 -0
  105. package/lib/csv.mjs +23 -0
  106. package/lib/ct-query.mjs +27 -0
  107. package/lib/ct-search.mjs +393 -0
  108. package/lib/distributed-operation-budget.mjs +461 -0
  109. package/lib/dns-delegation-health.mjs +344 -0
  110. package/lib/dns-intelligence.mjs +739 -0
  111. package/lib/dns-mx.mjs +24 -0
  112. package/lib/dns-resolver-selection.mjs +33 -0
  113. package/lib/domain-posture-analysis.mjs +254 -0
  114. package/lib/domain-posture-parsers.mjs +358 -0
  115. package/lib/domain-posture.mjs +745 -0
  116. package/lib/error-detail.mjs +10 -0
  117. package/lib/evidence-export.mjs +431 -0
  118. package/lib/evidence-report-markdown.mjs +71 -0
  119. package/lib/evidence-report.mjs +391 -0
  120. package/lib/external-intelligence-risk.mjs +108 -0
  121. package/lib/favicon.mjs +146 -0
  122. package/lib/feature-policy.mjs +70 -0
  123. package/lib/generated/retire-browser-catalog.mjs +8724 -0
  124. package/lib/generated/unicode-confusables-17.mjs +93 -0
  125. package/lib/hostname.mjs +38 -0
  126. package/lib/html-signals.mjs +680 -0
  127. package/lib/http-intelligence.mjs +232 -0
  128. package/lib/idn-confusable-policy.mjs +86 -0
  129. package/lib/idn-confusables.mjs +465 -0
  130. package/lib/lookup-cache.mjs +130 -0
  131. package/lib/lookup-progress.mjs +291 -0
  132. package/lib/lookup-source-progress.mjs +109 -0
  133. package/lib/lookup-threat-provider-inventory.mjs +13 -0
  134. package/lib/lookup.mjs +592 -0
  135. package/lib/observation.mjs +114 -0
  136. package/lib/observed-network-context.mjs +355 -0
  137. package/lib/operation-budget.mjs +489 -0
  138. package/lib/page-fingerprints.mjs +481 -0
  139. package/lib/page-role-profile.mjs +117 -0
  140. package/lib/perceptual-hash.mjs +451 -0
  141. package/lib/rdap-attempts.mjs +34 -0
  142. package/lib/rdap-bootstrap.mjs +173 -0
  143. package/lib/rdap-capabilities.mjs +292 -0
  144. package/lib/rdap-client.mjs +125 -0
  145. package/lib/rdap-normalization.mjs +691 -0
  146. package/lib/rdap-registrar.mjs +196 -0
  147. package/lib/rdap-transport.mjs +39 -0
  148. package/lib/rdap-types.mjs +1 -0
  149. package/lib/rdap-validation.mjs +94 -0
  150. package/lib/rdap.mjs +16 -0
  151. package/lib/registry-access.mjs +32 -0
  152. package/lib/registry-capabilities.mjs +117 -0
  153. package/lib/registry-capability-catalogue.mjs +1206 -0
  154. package/lib/registry-comparison.mjs +318 -0
  155. package/lib/registry-dates.mjs +190 -0
  156. package/lib/registry-insights.mjs +325 -0
  157. package/lib/response-policy.mjs +256 -0
  158. package/lib/risk-scoring.mjs +164 -0
  159. package/lib/safe-fetch.mjs +446 -0
  160. package/lib/security-txt.mjs +359 -0
  161. package/lib/service-binding-dns.mjs +608 -0
  162. package/lib/sslbl-certificates.generated.mjs +143 -0
  163. package/lib/sslbl-intelligence.mjs +241 -0
  164. package/lib/static-html-analysis.mjs +340 -0
  165. package/lib/structured-data-identity.mjs +323 -0
  166. package/lib/threat-intelligence-contract.mjs +5 -0
  167. package/lib/threat-intelligence-definition-registry.mjs +34 -0
  168. package/lib/threat-intelligence-fixtures.mjs +82 -0
  169. package/lib/threat-intelligence-manifests.mjs +512 -0
  170. package/lib/threat-intelligence-runtime.mjs +662 -0
  171. package/lib/threat-intelligence-targets.mjs +168 -0
  172. package/lib/threat-intelligence-types.mjs +3 -0
  173. package/lib/threatfox-intelligence.mjs +307 -0
  174. package/lib/tls-intelligence.mjs +814 -0
  175. package/lib/typosquat-generator.mjs +884 -0
  176. package/lib/urlhaus-intelligence.mjs +361 -0
  177. package/lib/urlscan-intelligence.mjs +349 -0
  178. package/lib/website-security-posture.mjs +325 -0
  179. package/lib/website-technology.mjs +437 -0
  180. package/lib/whois-authority.mjs +98 -0
  181. package/lib/whois-chain.mjs +261 -0
  182. package/lib/whois-contacts.mjs +163 -0
  183. package/lib/whois-contracts.mjs +3 -0
  184. package/lib/whois-normalization.mjs +27 -0
  185. package/lib/whois-transport.mjs +100 -0
  186. package/lib/whois-values.mjs +92 -0
  187. package/lib/whois.mjs +1187 -0
  188. package/package.json +55 -0
  189. package/third-party-notices.txt +5571 -0
@@ -0,0 +1,93 @@
1
+ import { sha256ArtifactDigest } from "./artifact-integrity.js";
2
+ export const ACQUISITION_DECISION_PACKET_SCHEMA = 'whoisleuth.acquisition-decision';
3
+ export const ACQUISITION_DECISION_PACKET_VERSION = 1;
4
+ export const ACQUISITION_DECISIONS = [
5
+ 'unresolved',
6
+ 'continue_manual_review',
7
+ 'pause',
8
+ 'do_not_proceed',
9
+ ];
10
+ export const ACQUISITION_MANUAL_CHECKS = [
11
+ 'eligibility',
12
+ 'counterparty',
13
+ 'transfer',
14
+ 'continuity',
15
+ 'legal',
16
+ ];
17
+ const DECISION_SET = new Set(ACQUISITION_DECISIONS);
18
+ const CHECK_SET = new Set(ACQUISITION_MANUAL_CHECKS);
19
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
20
+ function text(value, maximum) {
21
+ return typeof value === 'string'
22
+ ? value.replace(CONTROL_CHARACTERS, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
23
+ : '';
24
+ }
25
+ function timestamp(value) {
26
+ if (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))
27
+ return null;
28
+ return new Date(value).toISOString();
29
+ }
30
+ function target(value) {
31
+ const normalized = text(value, 253).toLowerCase().replace(/\.$/u, '');
32
+ if (!normalized || !normalized.includes('.') || /[/\\\s]/u.test(normalized)) {
33
+ throw new Error('A canonical domain is required for an acquisition decision packet.');
34
+ }
35
+ return normalized;
36
+ }
37
+ function decision(value) {
38
+ return typeof value === 'string' && DECISION_SET.has(value)
39
+ ? value
40
+ : 'unresolved';
41
+ }
42
+ function checks(value) {
43
+ if (!Array.isArray(value))
44
+ return [];
45
+ const selected = new Set();
46
+ for (const item of value.slice(0, ACQUISITION_MANUAL_CHECKS.length * 2)) {
47
+ if (typeof item === 'string' && CHECK_SET.has(item))
48
+ selected.add(item);
49
+ }
50
+ return ACQUISITION_MANUAL_CHECKS.filter((item) => selected.has(item));
51
+ }
52
+ export async function buildAcquisitionDecisionPacket(input) {
53
+ const generatedAt = timestamp(input.generatedAt) ?? new Date().toISOString();
54
+ const selectedDecision = decision(input.decision);
55
+ const reviewedChecks = checks(input.reviewedChecks);
56
+ const rationale = text(input.rationale, 2_000);
57
+ const unsigned = {
58
+ schema: ACQUISITION_DECISION_PACKET_SCHEMA,
59
+ version: ACQUISITION_DECISION_PACKET_VERSION,
60
+ generatedAt,
61
+ target: target(input.target),
62
+ synthetic: input.synthetic === true,
63
+ evidenceObservedAt: timestamp(input.evidenceObservedAt),
64
+ analystReview: {
65
+ decision: selectedDecision,
66
+ rationale,
67
+ reviewedChecks,
68
+ outstandingChecks: ACQUISITION_MANUAL_CHECKS.filter((item) => !reviewedChecks.includes(item)),
69
+ state: selectedDecision === 'unresolved' || reviewedChecks.length < ACQUISITION_MANUAL_CHECKS.length
70
+ ? 'draft'
71
+ : 'reviewed',
72
+ },
73
+ evidenceReview: input.review,
74
+ limitations: [
75
+ 'This local artifact records an analyst review of bounded point-in-time evidence and does not submit, reserve, value, purchase, or transfer a domain.',
76
+ 'A reviewed state records completion of the displayed manual checklist, not the accuracy of external statements or a legal, financial, eligibility, or ownership determination.',
77
+ 'Refresh authoritative registration and policy evidence immediately before acting.',
78
+ ...(input.synthetic === true
79
+ ? ['This packet contains synthetic demonstration data and must not be used as evidence or an acquisition record.']
80
+ : []),
81
+ ],
82
+ };
83
+ const digestSha256 = await sha256ArtifactDigest(unsigned);
84
+ const document = {
85
+ ...unsigned,
86
+ integrity: { algorithm: 'SHA-256', digestSha256 },
87
+ };
88
+ return {
89
+ document,
90
+ content: `${JSON.stringify(document, null, 2)}\n`,
91
+ filename: `whoisleuth-acquisition-review-${unsigned.target}-${generatedAt.slice(0, 10)}.json`,
92
+ };
93
+ }
@@ -0,0 +1,391 @@
1
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
2
+ const REGISTERED_STATES = new Set(['registered', 'expiring', 'for_sale']);
3
+ function record(value) {
4
+ return value && typeof value === 'object' && !Array.isArray(value)
5
+ ? value
6
+ : {};
7
+ }
8
+ function text(value, maximum = 120) {
9
+ return typeof value === 'string'
10
+ ? value.replace(CONTROL_CHARACTERS, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
11
+ : '';
12
+ }
13
+ function finite(value) {
14
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
15
+ }
16
+ function records(value, maximum = 8) {
17
+ return Array.isArray(value)
18
+ ? value.slice(0, maximum).map(record).filter((item) => Object.keys(item).length > 0)
19
+ : [];
20
+ }
21
+ function strings(value, maximum = 40) {
22
+ if (!Array.isArray(value))
23
+ return [];
24
+ const unique = new Set();
25
+ for (const item of value.slice(0, maximum * 2)) {
26
+ const normalized = text(item, 160);
27
+ if (normalized)
28
+ unique.add(normalized);
29
+ if (unique.size >= maximum)
30
+ break;
31
+ }
32
+ return [...unique];
33
+ }
34
+ function availabilityItem(availability) {
35
+ const state = text(availability.state, 40).toLowerCase();
36
+ const confidence = text(availability.confidence, 24).toLowerCase();
37
+ const source = text(availability.source, 24).toLowerCase();
38
+ if (state === 'available' && source === 'rdap' && confidence === 'high') {
39
+ return {
40
+ id: 'availability',
41
+ label: 'Registry non-existence response observed',
42
+ state: 'authoritative',
43
+ detail: 'The registry RDAP service returned no record at collection time.',
44
+ provenance: 'Registry RDAP availability decision',
45
+ };
46
+ }
47
+ if (state === 'available') {
48
+ return {
49
+ id: 'availability',
50
+ label: 'No registration record reported',
51
+ state: 'review',
52
+ detail: 'The available source reported no matching registration. Confirm current eligibility and registration state through an appropriate registrar or registry.',
53
+ provenance: source === 'whois' ? 'WHOIS availability decision' : 'Availability decision',
54
+ };
55
+ }
56
+ if (REGISTERED_STATES.has(state)) {
57
+ return {
58
+ id: 'availability',
59
+ label: state === 'for_sale' ? 'Registered with a sale signal' : state === 'expiring' ? 'Registered in a lifecycle transition' : 'Registration observed',
60
+ state: state === 'registered' ? 'observed' : 'review',
61
+ detail: state === 'for_sale'
62
+ ? 'A bounded page or infrastructure signal suggested a sale listing; it is not a verified offer, price, or ownership statement.'
63
+ : state === 'expiring'
64
+ ? 'Registry status evidence indicates redemption or pending deletion; this does not establish release timing or acquisition success.'
65
+ : 'Current registry, WHOIS, or authoritative DNS evidence supports a registered state.',
66
+ provenance: source ? `${source.toUpperCase()} registration evidence` : 'Registration evidence',
67
+ };
68
+ }
69
+ return {
70
+ id: 'availability',
71
+ label: 'Registration state unresolved',
72
+ state: 'unavailable',
73
+ detail: 'No enabled authoritative registration source produced a conclusive decision.',
74
+ provenance: 'Availability source health',
75
+ };
76
+ }
77
+ function lifecycleItem(availability, lifecycle, publications) {
78
+ const stage = text(lifecycle.stage, 40).toLowerCase();
79
+ const statuses = strings(lifecycle.rawStatuses);
80
+ const expiresInDays = finite(availability.expiresInDays);
81
+ const expiry = text(availability.expiryDateIso || availability.expiryDate, 64);
82
+ const completePublication = publications.some((item) => item.state === 'complete');
83
+ if (stage === 'pending_delete' || stage === 'redemption' || stage === 'pending_transfer' || stage === 'hold') {
84
+ return {
85
+ id: 'lifecycle',
86
+ label: text(lifecycle.label, 80) || 'Lifecycle transition observed',
87
+ state: 'review',
88
+ detail: `${statuses.length ? `${statuses.join(', ')}. ` : ''}The transition can change or be reversed and does not guarantee deletion, release, or transfer.`,
89
+ provenance: 'Registry lifecycle statuses',
90
+ };
91
+ }
92
+ if (expiry) {
93
+ const timing = expiresInDays === null
94
+ ? 'A published expiry date was observed.'
95
+ : expiresInDays < 0
96
+ ? 'The published expiry date has passed; refresh authoritative evidence before drawing a conclusion.'
97
+ : expiresInDays <= 30
98
+ ? `The published expiry date is within ${Math.max(0, Math.ceil(expiresInDays))} days.`
99
+ : `The published expiry date is approximately ${Math.ceil(expiresInDays)} days away.`;
100
+ return {
101
+ id: 'lifecycle',
102
+ label: 'Lifecycle date observed',
103
+ state: expiresInDays !== null && expiresInDays <= 30 ? 'review' : 'observed',
104
+ detail: timing,
105
+ provenance: 'Normalized RDAP or WHOIS lifecycle',
106
+ };
107
+ }
108
+ return {
109
+ id: 'lifecycle',
110
+ label: completePublication ? 'Expiry date not published' : 'Lifecycle timing unavailable',
111
+ state: completePublication ? 'observed' : 'unavailable',
112
+ detail: completePublication
113
+ ? 'A usable publication was collected without a normalized expiry date. Registry policy and renewal state still require manual confirmation.'
114
+ : 'Incomplete or unavailable registry publications cannot establish an expiry or release window.',
115
+ provenance: 'Registry publication quality',
116
+ };
117
+ }
118
+ function transferItem(lifecycle, publications) {
119
+ const locks = record(lifecycle.locks);
120
+ const statuses = strings(lifecycle.rawStatuses);
121
+ const client = locks.client === true;
122
+ const server = locks.server === true;
123
+ const completePublication = publications.some((item) => item.state === 'complete');
124
+ if (client || server) {
125
+ return {
126
+ id: 'transfer',
127
+ label: 'Transfer or update constraints observed',
128
+ state: 'observed',
129
+ detail: `${client ? 'Client-side registry lock status is present. ' : ''}${server ? 'Server-side registry lock status is present.' : ''}`.trim(),
130
+ provenance: 'EPP status interpretation',
131
+ };
132
+ }
133
+ if (statuses.length || completePublication) {
134
+ return {
135
+ id: 'transfer',
136
+ label: 'No interpreted lock status observed',
137
+ state: 'review',
138
+ detail: 'The collected statuses did not contain a recognized client or server lock. This does not prove transfer eligibility or that other policy restrictions are absent.',
139
+ provenance: 'EPP status interpretation',
140
+ };
141
+ }
142
+ return {
143
+ id: 'transfer',
144
+ label: 'Transfer constraints unavailable',
145
+ state: 'unavailable',
146
+ detail: 'No complete status publication was available for transfer-policy review.',
147
+ provenance: 'Registry publication quality',
148
+ };
149
+ }
150
+ function operationsItem(activation) {
151
+ const web = record(activation.web);
152
+ const mail = record(activation.mail);
153
+ const webState = text(web.state, 40);
154
+ const mailState = text(mail.state, 40);
155
+ const webObserved = ['page_observed', 'response_observed', 'tls_only'].includes(webState);
156
+ const mailObserved = ['authenticated_mail', 'mail_auth_gap', 'mail_observed'].includes(mailState);
157
+ if (webObserved || mailObserved) {
158
+ return {
159
+ id: 'operations',
160
+ label: 'Current service dependencies observed',
161
+ state: 'observed',
162
+ detail: `${webObserved ? text(web.label, 100) || 'Web evidence was observed' : 'Web state was inconclusive'}; ${mailObserved ? text(mail.label, 100) || 'mail evidence was observed' : text(mail.label, 100) || 'mail state was inconclusive'}.`,
163
+ provenance: 'Point-in-time DNS, HTTP, TLS, and page evidence',
164
+ };
165
+ }
166
+ return {
167
+ id: 'operations',
168
+ label: 'Service continuity unresolved',
169
+ state: 'unavailable',
170
+ detail: 'The available point-in-time evidence does not establish current web or mail dependencies.',
171
+ provenance: 'Point-in-time service evidence',
172
+ };
173
+ }
174
+ function contactsItem(insights) {
175
+ const routes = records(insights.abuseRouting);
176
+ const disclosure = record(insights.contactDisclosure);
177
+ const rdap = record(disclosure.registryRdap);
178
+ const whois = record(disclosure.whois);
179
+ if (routes.length) {
180
+ return {
181
+ id: 'contacts',
182
+ label: `${routes.length} published escalation route${routes.length === 1 ? '' : 's'}`,
183
+ state: 'observed',
184
+ detail: 'Published registry or registrar contact routes can support manual verification, but reachability and suitability remain unconfirmed.',
185
+ provenance: 'Separately attributed RDAP and WHOIS contacts',
186
+ };
187
+ }
188
+ if (rdap.state || whois.state) {
189
+ return {
190
+ id: 'contacts',
191
+ label: 'No usable escalation route observed',
192
+ state: 'review',
193
+ detail: 'Disclosure, redaction, or publication state was interpreted without a usable abuse or registrar route. Use official registry or registrar support channels for manual review.',
194
+ provenance: 'RDAP and WHOIS disclosure states',
195
+ };
196
+ }
197
+ return {
198
+ id: 'contacts',
199
+ label: 'Contact routing unavailable',
200
+ state: 'unavailable',
201
+ detail: 'No usable contact publication was available from the collected sources.',
202
+ provenance: 'Registry source health',
203
+ };
204
+ }
205
+ function sourceComplete(evidence) {
206
+ return evidence.source === 'dns' && evidence.complete === true;
207
+ }
208
+ function transitionDependencies(availability, activation, dnsEvidence, dnsRecords, tlsEvidence) {
209
+ const dnsComplete = sourceComplete(dnsEvidence);
210
+ const nameservers = strings(dnsRecords.ns);
211
+ const addresses = [...strings(dnsRecords.a), ...strings(dnsRecords.aaaa)];
212
+ const aliases = strings(dnsRecords.cname);
213
+ const mx = records(dnsRecords.mx);
214
+ const spf = strings(dnsRecords.spf);
215
+ const dmarc = strings(dnsRecords.dmarc);
216
+ const web = record(activation.web);
217
+ const mail = record(activation.mail);
218
+ const tlsState = text(tlsEvidence.status, 40).toLowerCase();
219
+ const tlsObserved = tlsEvidence.source === 'tls' && !['', 'error', 'skipped', 'unsupported'].includes(tlsState);
220
+ return [
221
+ nameservers.length
222
+ ? {
223
+ id: 'nameservers',
224
+ label: `${nameservers.length} nameserver dependenc${nameservers.length === 1 ? 'y' : 'ies'} observed`,
225
+ state: 'observed',
226
+ detail: `Preserve or deliberately replace the observed delegation: ${nameservers.slice(0, 4).join(', ')}${nameservers.length > 4 ? ', …' : ''}.`,
227
+ provenance: 'Point-in-time DNS nameserver answers',
228
+ }
229
+ : {
230
+ id: 'nameservers',
231
+ label: dnsComplete ? 'No nameserver answer observed' : 'Nameserver dependencies unavailable',
232
+ state: dnsComplete ? 'review' : 'unavailable',
233
+ detail: dnsComplete
234
+ ? 'The complete resolver capture retained no nameserver answer. Confirm the authoritative delegation before any transition.'
235
+ : 'Incomplete DNS evidence cannot establish the current authoritative delegation.',
236
+ provenance: 'Point-in-time DNS nameserver source health',
237
+ },
238
+ addresses.length || aliases.length || ['page_observed', 'response_observed', 'tls_only'].includes(text(web.state, 40))
239
+ ? {
240
+ id: 'web',
241
+ label: 'Web-routing dependencies observed',
242
+ state: 'observed',
243
+ detail: `${addresses.length} address answer${addresses.length === 1 ? '' : 's'} and ${aliases.length} alias answer${aliases.length === 1 ? '' : 's'} were retained. Preserve the intended HTTP origin, redirects, and application routing before changing DNS.`,
244
+ provenance: 'Point-in-time DNS and HTTP activation evidence',
245
+ }
246
+ : {
247
+ id: 'web',
248
+ label: dnsComplete ? 'No web-routing answer observed' : 'Web-routing dependencies unavailable',
249
+ state: dnsComplete ? 'review' : 'unavailable',
250
+ detail: dnsComplete
251
+ ? 'The complete DNS capture retained no address or CNAME answer. Confirm whether a web service is expected before transition.'
252
+ : 'Incomplete DNS and activation evidence cannot establish current web routing.',
253
+ provenance: 'Point-in-time DNS and HTTP source health',
254
+ },
255
+ mx.length || spf.length || dmarc.length || ['authenticated_mail', 'mail_auth_gap', 'mail_observed'].includes(text(mail.state, 40))
256
+ ? {
257
+ id: 'mail',
258
+ label: 'Mail dependencies observed',
259
+ state: 'observed',
260
+ detail: `${mx.length} MX answer${mx.length === 1 ? '' : 's'}, ${spf.length} SPF polic${spf.length === 1 ? 'y' : 'ies'}, and ${dmarc.length} DMARC polic${dmarc.length === 1 ? 'y' : 'ies'} were retained. Preserve intended delivery and authentication before delegation changes.`,
261
+ provenance: 'Point-in-time DNS mail evidence',
262
+ }
263
+ : {
264
+ id: 'mail',
265
+ label: dnsComplete ? 'No mail dependency observed' : 'Mail dependencies unavailable',
266
+ state: dnsComplete ? 'observed' : 'unavailable',
267
+ detail: dnsComplete
268
+ ? 'The complete resolver capture retained no MX, SPF, or DMARC publication. Confirm whether the domain is intentionally mail-disabled.'
269
+ : 'Incomplete DNS evidence cannot establish current mail routing or authentication.',
270
+ provenance: 'Point-in-time DNS mail source health',
271
+ },
272
+ tlsObserved
273
+ ? {
274
+ id: 'tls',
275
+ label: 'TLS service evidence observed',
276
+ state: tlsEvidence.complete === true ? 'observed' : 'review',
277
+ detail: 'A TLS service observation exists. Plan certificate issuance, private-key custody, hostname coverage, and renewal before cutover.',
278
+ provenance: 'Point-in-time TLS evidence',
279
+ }
280
+ : {
281
+ id: 'tls',
282
+ label: 'TLS transition evidence unavailable',
283
+ state: 'unavailable',
284
+ detail: 'No usable TLS observation was available to describe certificate or hostname dependencies.',
285
+ provenance: 'TLS source health',
286
+ },
287
+ ];
288
+ }
289
+ function registryPolicyChecks(availability, lifecycle, publications) {
290
+ const domain = text(availability.domain, 253).toLowerCase().replace(/\.$/u, '');
291
+ const suffix = domain.includes('.') ? domain.slice(domain.lastIndexOf('.') + 1) : '';
292
+ const statuses = strings(lifecycle.rawStatuses);
293
+ const locks = record(lifecycle.locks);
294
+ const publicationAvailable = publications.some((item) => ['complete', 'partial'].includes(text(item.state, 24).toLowerCase()));
295
+ return [
296
+ {
297
+ id: 'policy_eligibility',
298
+ label: suffix ? `Confirm .${suffix} eligibility and registration policy` : 'Confirm registry eligibility and registration policy',
299
+ state: 'review',
300
+ detail: 'Check current registry and registrar rules for registrant eligibility, reserved names, disputes, documentation, renewal, and transfer before committing funds.',
301
+ provenance: 'Manual policy check prompted by the observed domain suffix',
302
+ },
303
+ {
304
+ id: 'policy_lifecycle',
305
+ label: statuses.length ? 'Confirm lifecycle-policy interpretation' : 'Resolve lifecycle-policy context',
306
+ state: publicationAvailable ? 'review' : 'unavailable',
307
+ detail: statuses.length
308
+ ? `The collected publication included ${statuses.slice(0, 6).join(', ')}${statuses.length > 6 ? ', …' : ''}. Confirm current registry timing and restoration or deletion rules; WHOISleuth does not predict release.`
309
+ : publicationAvailable
310
+ ? 'No recognized lifecycle status was retained. Confirm renewal, grace, redemption, restoration, and deletion rules with the current registry or registrar.'
311
+ : 'No usable registry publication was available for a current lifecycle-policy review.',
312
+ provenance: 'Separately attributed RDAP and WHOIS lifecycle publications',
313
+ },
314
+ {
315
+ id: 'policy_transfer',
316
+ label: locks.client === true || locks.server === true ? 'Confirm observed transfer constraints' : 'Confirm transfer and change-of-control requirements',
317
+ state: publicationAvailable ? 'review' : 'unavailable',
318
+ detail: locks.client === true || locks.server === true
319
+ ? 'Recognized client or server lock evidence was observed. Confirm authorization codes, lock removal, waiting periods, identity checks, and any registrar change-of-control process.'
320
+ : publicationAvailable
321
+ ? 'No recognized lock was retained, but that does not establish transfer eligibility. Confirm authorization, waiting periods, identity checks, and registrar requirements.'
322
+ : 'No usable status publication was available to describe current transfer constraints.',
323
+ provenance: 'EPP status interpretation and manual policy requirements',
324
+ },
325
+ ];
326
+ }
327
+ function summaryState(availability) {
328
+ const state = text(availability.state, 40).toLowerCase();
329
+ if (state === 'for_sale')
330
+ return 'sale_signal';
331
+ if (state === 'expiring')
332
+ return 'review_transition';
333
+ if (state === 'registered')
334
+ return 'registered';
335
+ if (state === 'available'
336
+ && text(availability.source, 24).toLowerCase() === 'rdap'
337
+ && text(availability.confidence, 24).toLowerCase() === 'high')
338
+ return 'unregistered_observation';
339
+ return 'incomplete';
340
+ }
341
+ export function buildAcquisitionDueDiligence(input) {
342
+ const availability = record(input.availability);
343
+ const insights = record(input.registryInsights);
344
+ const lifecycle = record(insights.lifecycle);
345
+ const publications = records(insights.publications);
346
+ const registration = availabilityItem(availability);
347
+ const items = [
348
+ registration,
349
+ lifecycleItem(availability, lifecycle, publications),
350
+ transferItem(lifecycle, publications),
351
+ operationsItem(record(input.activationContext)),
352
+ contactsItem(insights),
353
+ ];
354
+ const dependencies = transitionDependencies(availability, record(input.activationContext), record(input.dnsEvidence), record(input.dnsRecords), record(input.tlsEvidence));
355
+ const policyChecks = registryPolicyChecks(availability, lifecycle, publications);
356
+ const unavailable = items.filter((item) => item.state === 'unavailable').length;
357
+ const review = items.filter((item) => item.state === 'review').length;
358
+ const nextSteps = [
359
+ registration.state === 'unavailable'
360
+ ? 'Refresh authoritative registration evidence before making an acquisition decision.'
361
+ : 'Confirm current registration eligibility, price, and contractual terms with an appropriate registrar or registry.',
362
+ items[1]?.state === 'review' || items[2]?.state === 'review'
363
+ ? 'Review current registry lifecycle and transfer-policy statuses immediately before acting.'
364
+ : 'Recheck lifecycle and transfer-policy statuses at the point of action.',
365
+ items[3]?.state === 'observed'
366
+ ? 'Inventory observed web, mail, certificate, and DNS dependencies before planning a transition.'
367
+ : 'Determine whether current DNS, web, mail, and certificate services must be preserved.',
368
+ items[4]?.state === 'observed'
369
+ ? 'Validate the appropriate published contact route before sending any request.'
370
+ : 'Resolve official registry and registrar support channels manually.',
371
+ 'Review trade mark, eligibility, dispute, tax, escrow, and legal requirements outside WHOISleuth.',
372
+ ].slice(0, 6);
373
+ return {
374
+ version: 2,
375
+ label: unavailable
376
+ ? `${unavailable} due-diligence area${unavailable === 1 ? '' : 's'} unavailable`
377
+ : review
378
+ ? `${review} due-diligence area${review === 1 ? '' : 's'} require review`
379
+ : 'Collected due-diligence areas observed',
380
+ state: summaryState(availability),
381
+ items,
382
+ transitionDependencies: dependencies,
383
+ policyChecks,
384
+ nextSteps,
385
+ limitations: [
386
+ 'This workspace organizes observed evidence; it does not value a domain or establish that it can be acquired.',
387
+ 'Expiry dates, EPP statuses, sale signals, and contact routes can change after collection.',
388
+ 'WHOISleuth does not assess legal rights, registry eligibility, registrar terms, escrow, tax, or payment risk.',
389
+ ],
390
+ };
391
+ }
@@ -0,0 +1,21 @@
1
+ // Small environment-neutral integrity helper for deliberate local exports.
2
+ // Inputs must already be normalized and bounded by the owning artifact model.
3
+ export function canonicalArtifactJson(value) {
4
+ if (value === undefined)
5
+ return 'null';
6
+ if (value === null || typeof value !== 'object')
7
+ return JSON.stringify(value);
8
+ if (Array.isArray(value))
9
+ return `[${value.map(canonicalArtifactJson).join(',')}]`;
10
+ const entries = Object.entries(value)
11
+ .filter(([, entry]) => entry !== undefined)
12
+ .sort(([left], [right]) => left.localeCompare(right));
13
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalArtifactJson(entry)}`).join(',')}}`;
14
+ }
15
+ export async function sha256ArtifactDigest(value) {
16
+ const bytes = new TextEncoder().encode(canonicalArtifactJson(value));
17
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
18
+ return `sha256:${[...new Uint8Array(digest)]
19
+ .map((byte) => byte.toString(16).padStart(2, '0'))
20
+ .join('')}`;
21
+ }