@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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 (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,5 +1,6 @@
1
1
  // IANA RDAP bootstrap retrieval, bounded stale caching, and authority
2
2
  // selection for domain, IP, and ASN queries.
3
+ import net from 'node:net';
3
4
  import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
4
5
  const BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
5
6
  const BOOTSTRAP_STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -8,29 +9,71 @@ const BOOTSTRAP_FETCH_TIMEOUT_MS = 7000;
8
9
  const MAX_RDAP_ENDPOINT_LENGTH = 2048;
9
10
  const bootstrapCache = new Map();
10
11
  const bootstrapInflight = new Map();
12
+ function admitRdapEndpoint(value, options = {}) {
13
+ if (typeof value !== 'string'
14
+ || !value
15
+ || value !== value.trim()
16
+ || value.length > MAX_RDAP_ENDPOINT_LENGTH
17
+ || /[\u0000-\u001f\u007f]/u.test(value))
18
+ return null;
19
+ try {
20
+ const parsed = new URL(value);
21
+ if (!['http:', 'https:'].includes(parsed.protocol)
22
+ || !parsed.hostname
23
+ || parsed.username
24
+ || parsed.password
25
+ || parsed.port
26
+ || parsed.hash
27
+ || (!options.allowQuery && parsed.search))
28
+ return null;
29
+ return value;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
11
35
  function ipv4ToLong(ip) {
12
36
  return ip.split('.').reduce((accumulator, octet) => (accumulator << 8) + (parseInt(octet, 10) & 0xff), 0) >>> 0;
13
37
  }
14
38
  function ipInCidrV4(ip, cidr) {
15
- const [range, bitsString] = cidr.split('/');
16
- if (!range)
39
+ const parts = cidr.split('/');
40
+ if (parts.length > 2)
41
+ return false;
42
+ const [range, bitsString] = parts;
43
+ if (!range || net.isIP(ip) !== 4 || net.isIP(range) !== 4)
44
+ return false;
45
+ const bits = bitsString === undefined || /^\d{1,2}$/u.test(bitsString)
46
+ ? Number(bitsString ?? 32)
47
+ : -1;
48
+ if (!Number.isSafeInteger(bits) || bits < 0 || bits > 32)
17
49
  return false;
18
- const bits = bitsString !== undefined ? parseInt(bitsString, 10) : 32;
19
50
  const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
20
51
  return (ipv4ToLong(ip) & mask) === (ipv4ToLong(range) & mask);
21
52
  }
22
53
  function expandIpv6(ip) {
23
- let head = ip;
54
+ let normalized = ip;
55
+ if (normalized.includes('.')) {
56
+ const lastColon = normalized.lastIndexOf(':');
57
+ const embedded = normalized.slice(lastColon + 1);
58
+ if (lastColon < 0 || net.isIP(embedded) !== 4) {
59
+ throw new TypeError('Invalid IPv4-embedded IPv6 address.');
60
+ }
61
+ const value = ipv4ToLong(embedded);
62
+ normalized = `${normalized.slice(0, lastColon)}:${(value >>> 16).toString(16)}:${(value & 0xffff).toString(16)}`;
63
+ }
64
+ if (net.isIP(normalized) !== 6)
65
+ throw new TypeError('Invalid IPv6 address.');
66
+ let head = normalized;
24
67
  let tail = '';
25
- if (ip.includes('::')) {
26
- const [headPart = '', tailPart = ''] = ip.split('::');
68
+ if (normalized.includes('::')) {
69
+ const [headPart = '', tailPart = ''] = normalized.split('::');
27
70
  head = headPart;
28
71
  tail = tailPart;
29
72
  }
30
73
  const headParts = head ? head.split(':').filter(Boolean) : [];
31
74
  const tailParts = tail ? tail.split(':').filter(Boolean) : [];
32
75
  const missing = 8 - headParts.length - tailParts.length;
33
- const parts = ip.includes('::')
76
+ const parts = normalized.includes('::')
34
77
  ? [...headParts, ...Array(Math.max(missing, 0)).fill('0'), ...tailParts]
35
78
  : headParts;
36
79
  while (parts.length < 8)
@@ -41,13 +84,25 @@ function ipv6ToBigInt(ip) {
41
84
  return expandIpv6(ip).reduce((accumulator, part) => (accumulator << 16n) + BigInt(parseInt(part || '0', 16)), 0n);
42
85
  }
43
86
  function ipInCidrV6(ip, cidr) {
44
- const [range, bitsString] = cidr.split('/');
87
+ const parts = cidr.split('/');
88
+ if (parts.length > 2)
89
+ return false;
90
+ const [range, bitsString] = parts;
45
91
  if (!range)
46
92
  return false;
47
- const bits = bitsString !== undefined ? parseInt(bitsString, 10) : 128;
93
+ const bits = bitsString === undefined || /^\d{1,3}$/u.test(bitsString)
94
+ ? Number(bitsString ?? 128)
95
+ : -1;
96
+ if (!Number.isSafeInteger(bits) || bits < 0 || bits > 128)
97
+ return false;
48
98
  const full = (1n << 128n) - 1n;
49
99
  const mask = bits === 0 ? 0n : (full << BigInt(128 - bits)) & full;
50
- return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
100
+ try {
101
+ return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
102
+ }
103
+ catch {
104
+ return false;
105
+ }
51
106
  }
52
107
  function validBootstrap(data) {
53
108
  const record = data && typeof data === 'object' && !Array.isArray(data)
@@ -74,7 +129,12 @@ async function fetchBootstrap(kind, options = {}) {
74
129
  return inflight;
75
130
  const request = (async () => {
76
131
  try {
77
- const response = await fetchUpstream(`https://data.iana.org/rdap/${kind}.json`, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
132
+ const requestedEndpoint = `https://data.iana.org/rdap/${kind}.json`;
133
+ const response = await fetchUpstream(requestedEndpoint, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
134
+ const finalEndpoint = admitRdapEndpoint(response.finalUrl ?? requestedEndpoint);
135
+ if (finalEndpoint !== requestedEndpoint) {
136
+ throw new Error(`IANA bootstrap redirected outside its fixed source endpoint for ${kind}`);
137
+ }
78
138
  if (!response.ok) {
79
139
  throw new Error(`IANA bootstrap fetch failed for ${kind} (${response.status})`);
80
140
  }
@@ -112,10 +172,10 @@ function clearRdapBootstrapCache() {
112
172
  function uniqueRdapBases(urls) {
113
173
  const seen = new Set();
114
174
  return (Array.isArray(urls) ? urls : [])
115
- .filter((url) => typeof url === 'string'
116
- && url.length <= MAX_RDAP_ENDPOINT_LENGTH
117
- && !/[\u0000-\u001f\u007f]/.test(url)
118
- && /^https?:\/\//i.test(url))
175
+ .flatMap((url) => {
176
+ const admitted = admitRdapEndpoint(url);
177
+ return admitted ? [admitted] : [];
178
+ })
119
179
  .sort((left, right) => Number(/^http:\/\//i.test(left)) - Number(/^http:\/\//i.test(right)))
120
180
  .filter((url) => {
121
181
  const key = url.replace(/\/$/, '').toLowerCase();
@@ -170,4 +230,4 @@ async function findRdapBases(type, value) {
170
230
  }
171
231
  return [];
172
232
  }
173
- export { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, MAX_RDAP_ENDPOINT_LENGTH, clearRdapBootstrapCache, fetchBootstrap, findRdapBases, ipv4ToLong, ipv6ToBigInt, uniqueRdapBases, };
233
+ export { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, MAX_RDAP_ENDPOINT_LENGTH, admitRdapEndpoint, clearRdapBootstrapCache, fetchBootstrap, findRdapBases, ipv4ToLong, ipv6ToBigInt, uniqueRdapBases, };
@@ -1,8 +1,8 @@
1
1
  import { cached } from "./lookup-cache.mjs";
2
2
  import { findRdapBases, uniqueRdapBases, } from "./rdap-bootstrap.mjs";
3
3
  import { rdapAttempt, rdapFailure } from "./rdap-attempts.mjs";
4
- import { fetchRdapWithTimeout, } from "./rdap-transport.mjs";
5
- import { validateRdapResponse } from "./rdap-validation.mjs";
4
+ import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
5
+ import { admitRdapObjectEndpoint, validateRdapResponse, } from "./rdap-validation.mjs";
6
6
  const UPSTREAM_TIMEOUT_MS = 7_000;
7
7
  const UPSTREAM_TOTAL_DEADLINE_MS = 12_000;
8
8
  const MAX_RDAP_ENDPOINTS = 3;
@@ -24,7 +24,7 @@ function rdapPathFor(type, value) {
24
24
  * Query the bounded bootstrap candidate set while retaining every attempted
25
25
  * endpoint as separately attributed transport evidence.
26
26
  */
27
- export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapWithTimeout) {
27
+ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout) {
28
28
  const candidates = uniqueRdapBases(bases).slice(0, MAX_RDAP_ENDPOINTS);
29
29
  if (candidates.length === 0)
30
30
  return null;
@@ -38,13 +38,21 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
38
38
  const url = `${base.replace(/\/$/, '')}/${rdapPathFor(type, value)}`;
39
39
  try {
40
40
  const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' } }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
41
+ const selectedEndpoint = admitRdapObjectEndpoint(type, value, upstream.finalUrl ?? url);
42
+ if (!selectedEndpoint) {
43
+ attempts.push(rdapAttempt(url, 'invalid_response', {
44
+ status: upstream.status,
45
+ detail: 'The endpoint returned final URL provenance for an invalid or different RDAP object.',
46
+ }));
47
+ continue;
48
+ }
41
49
  if (upstream.status !== 404 && !upstream.ok) {
42
50
  const outcome = upstream.status === 429
43
51
  ? 'rate_limited'
44
52
  : upstream.status >= 500
45
53
  ? 'server_error'
46
54
  : 'client_error';
47
- attempts.push(rdapAttempt(url, outcome, {
55
+ attempts.push(rdapAttempt(selectedEndpoint, outcome, {
48
56
  status: upstream.status,
49
57
  detail: `The endpoint returned HTTP ${upstream.status}.`,
50
58
  }));
@@ -55,21 +63,21 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
55
63
  data = JSON.parse(upstream.text);
56
64
  }
57
65
  catch {
58
- attempts.push(rdapAttempt(url, 'invalid_json', {
66
+ attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
59
67
  status: upstream.status,
60
68
  detail: 'The endpoint returned invalid JSON.',
61
69
  }));
62
70
  continue;
63
71
  }
64
72
  if (upstream.status === 404) {
65
- attempts.push(rdapAttempt(url, 'not_found', {
73
+ attempts.push(rdapAttempt(selectedEndpoint, 'not_found', {
66
74
  status: upstream.status,
67
75
  detail: 'The authoritative endpoint reported no matching object.',
68
76
  selected: true,
69
77
  }));
70
78
  return {
71
- rdapServer: url,
72
- transportSecurity: /^https:\/\//i.test(url) ? 'https' : 'http',
79
+ rdapServer: selectedEndpoint,
80
+ transportSecurity: /^https:\/\//i.test(selectedEndpoint) ? 'https' : 'http',
73
81
  upstreamStatus: upstream.status,
74
82
  fetchedAt: new Date().toISOString(),
75
83
  data,
@@ -80,20 +88,20 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
80
88
  const parsed = parseRdap(type, data);
81
89
  const validation = validateRdapResponse(type, value, parsed);
82
90
  if (!validation.valid) {
83
- attempts.push(rdapAttempt(url, 'invalid_response', {
91
+ attempts.push(rdapAttempt(selectedEndpoint, 'invalid_response', {
84
92
  status: upstream.status,
85
93
  detail: validation.detail,
86
94
  }));
87
95
  continue;
88
96
  }
89
- attempts.push(rdapAttempt(url, 'success', {
97
+ attempts.push(rdapAttempt(selectedEndpoint, 'success', {
90
98
  status: upstream.status,
91
99
  detail: 'The endpoint returned the requested RDAP object.',
92
100
  selected: true,
93
101
  }));
94
102
  return {
95
- rdapServer: url,
96
- transportSecurity: /^https:\/\//i.test(url) ? 'https' : 'http',
103
+ rdapServer: selectedEndpoint,
104
+ transportSecurity: /^https:\/\//i.test(selectedEndpoint) ? 'https' : 'http',
97
105
  upstreamStatus: upstream.status,
98
106
  fetchedAt: new Date().toISOString(),
99
107
  data,