@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
@@ -0,0 +1,844 @@
1
+ import { StringDecoder } from 'node:string_decoder';
2
+ import { formatTerminalLookup, safeTerminalValue } from "./formatters/terminal.mjs";
3
+ import { presentTerminalOutput, terminalPresentation, wrapTerminalOutput, } from "./terminal-presentation.mjs";
4
+ const MIN_LOOKUP_BROWSER_HEIGHT = 12;
5
+ const MAX_LOOKUP_BROWSER_HEIGHT = 80;
6
+ const MAX_LOOKUP_BROWSER_PANELS = 9;
7
+ const ENTER_ALTERNATE_SCREEN = '\u001b[?1049h\u001b[?25l';
8
+ const LEAVE_ALTERNATE_SCREEN = '\u001b[?25h\u001b[?1049l';
9
+ const CLEAR_SCREEN = '\u001b[2J\u001b[H';
10
+ const ESCAPE_SEQUENCE_WAIT_MS = 40;
11
+ const MAX_PENDING_INPUT_LENGTH = 512;
12
+ const MAX_LOOKUP_BROWSER_INPUT_CHUNK_BYTES = 2_048;
13
+ const MAX_LOOKUP_BROWSER_SEARCH_BYTES = 256;
14
+ const MAX_LOOKUP_BROWSER_SEARCH_SCALARS = 80;
15
+ const MAX_LOOKUP_BROWSER_SEARCH_MATCHES = 256;
16
+ const ANSI_SEQUENCE_RE = /\u001b\[[0-?]*[ -/]*[@-~]/gu;
17
+ const COMPLETE_CSI_RE = /^\u001b\[[0-?]*[ -/]*[@-~]/u;
18
+ function boundedHeight(value) {
19
+ const height = Number(value);
20
+ if (!Number.isSafeInteger(height))
21
+ return 24;
22
+ return Math.max(MIN_LOOKUP_BROWSER_HEIGHT, Math.min(MAX_LOOKUP_BROWSER_HEIGHT, height));
23
+ }
24
+ function panelId(value, index) {
25
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '');
26
+ return normalized || `panel-${index + 1}`;
27
+ }
28
+ function terminalCharacterWidth(character) {
29
+ const codePoint = character.codePointAt(0) || 0;
30
+ if (/\p{Mark}/u.test(character)
31
+ || codePoint === 0x200d
32
+ || codePoint >= 0xfe00 && codePoint <= 0xfe0f
33
+ || codePoint >= 0xe0100 && codePoint <= 0xe01ef)
34
+ return 0;
35
+ return codePoint >= 0x1100 && (codePoint <= 0x115f
36
+ || codePoint === 0x2329
37
+ || codePoint === 0x232a
38
+ || codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f
39
+ || codePoint >= 0xac00 && codePoint <= 0xd7a3
40
+ || codePoint >= 0xf900 && codePoint <= 0xfaff
41
+ || codePoint >= 0xfe10 && codePoint <= 0xfe19
42
+ || codePoint >= 0xfe30 && codePoint <= 0xfe6f
43
+ || codePoint >= 0xff00 && codePoint <= 0xff60
44
+ || codePoint >= 0xffe0 && codePoint <= 0xffe6
45
+ || codePoint >= 0x1f300 && codePoint <= 0x1faff
46
+ || codePoint >= 0x20000 && codePoint <= 0x3fffd) ? 2 : 1;
47
+ }
48
+ function terminalDisplayWidth(value) {
49
+ return Array.from(value.replace(ANSI_SEQUENCE_RE, ''))
50
+ .reduce((total, character) => total + terminalCharacterWidth(character), 0);
51
+ }
52
+ function hardWrapTerminalLine(line, width) {
53
+ if (!line || terminalDisplayWidth(line) <= width)
54
+ return [line];
55
+ const lines = [];
56
+ let current = '';
57
+ let currentWidth = 0;
58
+ for (const character of line) {
59
+ const characterWidth = terminalCharacterWidth(character);
60
+ if (current && currentWidth + characterWidth > width) {
61
+ lines.push(current);
62
+ current = '';
63
+ currentWidth = 0;
64
+ }
65
+ current += character;
66
+ currentWidth += characterWidth;
67
+ }
68
+ if (current || !lines.length)
69
+ lines.push(current);
70
+ return lines;
71
+ }
72
+ function clipTerminalLine(line, width) {
73
+ if (terminalDisplayWidth(line) <= width)
74
+ return line;
75
+ const maximumContentWidth = Math.max(1, width - 1);
76
+ let clipped = '';
77
+ let clippedWidth = 0;
78
+ for (const character of line) {
79
+ const characterWidth = terminalCharacterWidth(character);
80
+ if (clippedWidth + characterWidth > maximumContentWidth)
81
+ break;
82
+ clipped += character;
83
+ clippedWidth += characterWidth;
84
+ }
85
+ return `${clipped}…`;
86
+ }
87
+ function wrapLookupBrowserLines(value, width) {
88
+ const softlyWrapped = wrapTerminalOutput(`${value}\n`, width).replace(/\n$/u, '').split('\n');
89
+ return softlyWrapped.flatMap((line) => hardWrapTerminalLine(line, width));
90
+ }
91
+ function buildLookupBrowserPanels(document) {
92
+ const blocks = formatTerminalLookup(document, { detail: 'verbose' })
93
+ .trimEnd()
94
+ .split(/\n\n/gu)
95
+ .slice(0, MAX_LOOKUP_BROWSER_PANELS);
96
+ return blocks.flatMap((block, index) => {
97
+ const [heading, ...lines] = block.split('\n');
98
+ if (!heading?.endsWith(':') || lines.length === 0)
99
+ return [];
100
+ const label = heading.slice(0, -1);
101
+ return [Object.freeze({
102
+ id: panelId(label, index),
103
+ label,
104
+ lines: Object.freeze(lines),
105
+ })];
106
+ });
107
+ }
108
+ function browserTabLine(panels, activeIndex, width) {
109
+ const labels = panels.map((panel, index) => index === activeIndex
110
+ ? `[${index + 1} ${panel.label}]`
111
+ : `${index + 1} ${panel.label}`);
112
+ const complete = labels.join(' ');
113
+ if (terminalDisplayWidth(complete) <= width)
114
+ return complete;
115
+ const visible = [activeIndex - 1, activeIndex, activeIndex + 1]
116
+ .filter((index) => index >= 0 && index < panels.length)
117
+ .map((index) => labels[index]);
118
+ const windowed = `${activeIndex > 1 ? '‹ ' : ''}${visible.join(' ')}${activeIndex + 2 < panels.length ? ' ›' : ''}`;
119
+ return clipTerminalLine(windowed, width);
120
+ }
121
+ function boundedTargetLine(query, width) {
122
+ return clipTerminalLine(`Target ${query}`, width);
123
+ }
124
+ function boundedBrowserDimensions(options) {
125
+ return {
126
+ width: Math.max(40, Math.min(160, Number.isSafeInteger(options.width) ? Number(options.width) : 100)),
127
+ height: boundedHeight(options.height),
128
+ };
129
+ }
130
+ function boundedSearchText(value) {
131
+ const text = typeof value === 'string' ? value : '';
132
+ if (Buffer.byteLength(text, 'utf8') > MAX_LOOKUP_BROWSER_SEARCH_BYTES
133
+ || Array.from(text).length > MAX_LOOKUP_BROWSER_SEARCH_SCALARS
134
+ || /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/u.test(text)) {
135
+ throw new RangeError(`Lookup browser search is limited to ${MAX_LOOKUP_BROWSER_SEARCH_SCALARS} characters and ${MAX_LOOKUP_BROWSER_SEARCH_BYTES} UTF-8 bytes.`);
136
+ }
137
+ return text;
138
+ }
139
+ function findLookupBrowserMatches(panels, query) {
140
+ const normalized = boundedSearchText(query).trim().toLocaleLowerCase('en-US');
141
+ if (!normalized)
142
+ return { matches: [], truncated: false };
143
+ const matches = [];
144
+ let truncated = false;
145
+ outer: for (let panelIndex = 0; panelIndex < panels.length; panelIndex += 1) {
146
+ const panel = panels[panelIndex];
147
+ const lines = [panel.label, ...panel.lines];
148
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
149
+ if (!String(lines[lineIndex] || '').toLocaleLowerCase('en-US').includes(normalized))
150
+ continue;
151
+ if (matches.length >= MAX_LOOKUP_BROWSER_SEARCH_MATCHES) {
152
+ truncated = true;
153
+ break outer;
154
+ }
155
+ matches.push(Object.freeze({ panelIndex, lineIndex: Math.max(0, lineIndex - 1) }));
156
+ }
157
+ }
158
+ return { matches, truncated };
159
+ }
160
+ function renderedPanelLineOffset(panel, lineIndex, width) {
161
+ if (lineIndex <= 0)
162
+ return 0;
163
+ return panel.lines
164
+ .slice(0, Math.min(lineIndex, panel.lines.length))
165
+ .reduce((total, line) => total + wrapLookupBrowserLines(line, width).length, 0);
166
+ }
167
+ function renderStandaloneBrowserFrame(lines, options) {
168
+ const { width, height } = boundedBrowserDimensions(options);
169
+ const clipped = lines.slice(0, height).map((line) => clipTerminalLine(line, width));
170
+ return presentTerminalOutput(`${clipped.join('\n')}\n`, {
171
+ color: options.color === true,
172
+ interactive: true,
173
+ palette: options.palette || 'auto',
174
+ width: null,
175
+ });
176
+ }
177
+ function buildLookupBrowserHelpFrame(document, options = {}) {
178
+ const query = safeTerminalValue(document?.query ?? options.query, 'unknown target');
179
+ const mode = safeTerminalValue(document?.mode ?? options.mode, 'unknown mode');
180
+ return renderStandaloneBrowserFrame([
181
+ 'WHOISleuth evidence browser help',
182
+ boundedTargetLine(query, boundedBrowserDimensions(options).width),
183
+ `Mode ${mode}`,
184
+ '',
185
+ options.collecting ? 'Collection' : 'Panels',
186
+ options.collecting
187
+ ? ' q / Ctrl-C cancel the active collection'
188
+ : ' ←/→ h/l Tab move panels · 1-9 select',
189
+ options.collecting
190
+ ? ' progress is transient and is never retained'
191
+ : ' ↑/↓ j/k PgUp/PgDn Home/End scroll',
192
+ options.collecting ? ' ? / Esc return to collection progress' : ' / search rendered panels · n/N matches',
193
+ ' ? / Esc return · q close',
194
+ ], options);
195
+ }
196
+ function buildLookupBrowserSearchFrame(document, query, notice, options = {}) {
197
+ return renderStandaloneBrowserFrame([
198
+ 'WHOISleuth evidence browser search',
199
+ boundedTargetLine(safeTerminalValue(document.query, 'unknown target'), boundedBrowserDimensions(options).width),
200
+ `Mode ${safeTerminalValue(document.mode, 'unknown mode')}`,
201
+ '',
202
+ `Search ${query || 'type a bounded term'}`,
203
+ notice || 'Search covers only the bounded text rendered in the evidence panels.',
204
+ '',
205
+ 'Enter apply · Backspace edit · Esc cancel',
206
+ ], options);
207
+ }
208
+ function buildLookupCollectionFrame(queryValue, modeValue, sources, options = {}) {
209
+ const { width, height } = boundedBrowserDimensions(options);
210
+ const query = safeTerminalValue(queryValue, 'unknown target');
211
+ const mode = modeValue === 'deep' ? 'deep' : 'fast';
212
+ const header = [
213
+ 'WHOISleuth evidence browser',
214
+ boundedTargetLine(query, width),
215
+ `Mode ${mode}`,
216
+ 'Collection in progress:',
217
+ ];
218
+ const footer = ['? help · progress is transient', 'q/Esc/Ctrl-C cancel'];
219
+ const bodyCapacity = Math.max(1, height - header.length - footer.length);
220
+ const body = mode === 'fast'
221
+ ? [
222
+ 'Registration evidence collecting',
223
+ 'Fast mode waits for the final bounded result.',
224
+ 'Per-source states are not fabricated.',
225
+ ]
226
+ : sources.flatMap((source) => {
227
+ const state = source.state === 'pending'
228
+ ? 'pending'
229
+ : `${source.state}${source.complete ? ' · complete' : ' · incomplete'}${source.truncated ? ' · truncated' : ''}`;
230
+ const limitation = source.state !== 'pending' && source.limitation
231
+ ? ` · ${source.limitation}`
232
+ : '';
233
+ return [clipTerminalLine(`${source.source.replaceAll('_', ' ')} ${state}${limitation}`, width)];
234
+ });
235
+ const visible = body.slice(0, bodyCapacity);
236
+ if (body.length > visible.length && visible.length) {
237
+ visible[visible.length - 1] = `+${body.length - visible.length + 1} source states not visible at this height`;
238
+ }
239
+ return renderStandaloneBrowserFrame([...header, ...visible, ...footer], {
240
+ ...options,
241
+ width,
242
+ height,
243
+ });
244
+ }
245
+ function buildLookupBrowserFrame(document, options = {}) {
246
+ const panels = buildLookupBrowserPanels(document);
247
+ const { width, height } = boundedBrowserDimensions(options);
248
+ if (!panels.length)
249
+ return Object.freeze({
250
+ value: `${clipTerminalLine('WHOISleuth evidence browser', width)}\n${clipTerminalLine('No bounded terminal panels were available.', width)}\n`,
251
+ scrollOffset: 0,
252
+ maximumScrollOffset: 0,
253
+ pageSize: 0,
254
+ totalBodyLines: 0,
255
+ });
256
+ const activeIndex = Math.max(0, Math.min(panels.length - 1, Number.isSafeInteger(options.activeIndex) ? Number(options.activeIndex) : 0));
257
+ const panel = panels[activeIndex];
258
+ const query = safeTerminalValue(document.query, 'unknown target');
259
+ const mode = safeTerminalValue(document.mode, 'unknown mode');
260
+ const header = [
261
+ 'WHOISleuth evidence browser',
262
+ boundedTargetLine(query, width),
263
+ `Mode ${clipTerminalLine(mode, Math.max(1, width - 8))}`,
264
+ browserTabLine(panels, activeIndex, width),
265
+ `${panel.label}:`,
266
+ ];
267
+ const footerLineCount = 2;
268
+ const body = wrapLookupBrowserLines(panel.lines.join('\n'), width);
269
+ const bodyCapacity = Math.max(1, height - header.length - footerLineCount);
270
+ const maximumScrollOffset = Math.max(0, body.length - bodyCapacity);
271
+ const requestedScrollOffset = Number.isSafeInteger(options.scrollOffset) ? Number(options.scrollOffset) : 0;
272
+ const scrollOffset = Math.max(0, Math.min(maximumScrollOffset, requestedScrollOffset));
273
+ const visibleBody = body.slice(scrollOffset, scrollOffset + bodyCapacity);
274
+ const firstVisibleLine = body.length ? scrollOffset + 1 : 0;
275
+ const lastVisibleLine = body.length ? scrollOffset + visibleBody.length : 0;
276
+ const footer = [
277
+ options.searchStatus || `View ${activeIndex + 1}/${panels.length} · lines ${firstVisibleLine}-${lastVisibleLine}/${body.length}`,
278
+ '←/→ panels · ↑/↓ scroll · / search · ? help · q close',
279
+ ].map((line) => clipTerminalLine(line, width));
280
+ const value = [...header, ...visibleBody, ...footer].slice(0, height).join('\n');
281
+ return Object.freeze({
282
+ value: presentTerminalOutput(`${value}\n`, {
283
+ color: options.color === true,
284
+ interactive: true,
285
+ palette: options.palette || 'auto',
286
+ width: null,
287
+ }),
288
+ scrollOffset,
289
+ maximumScrollOffset,
290
+ pageSize: bodyCapacity,
291
+ totalBodyLines: body.length,
292
+ });
293
+ }
294
+ function renderLookupBrowser(document, options = {}) {
295
+ return buildLookupBrowserFrame(document, options).value;
296
+ }
297
+ function canBrowseLookup(input, output, environment = process.env) {
298
+ return input?.isTTY === true
299
+ && output?.isTTY === true
300
+ && typeof input.setRawMode === 'function'
301
+ && typeof input.resume === 'function'
302
+ && typeof input.pause === 'function'
303
+ && typeof input.on === 'function'
304
+ && typeof input.off === 'function'
305
+ && typeof output.write === 'function'
306
+ && !(Number.isFinite(output.columns) && Number(output.columns) < 40)
307
+ && !(Number.isFinite(output.rows) && Number(output.rows) < MIN_LOOKUP_BROWSER_HEIGHT)
308
+ && environment.TERM !== 'dumb'
309
+ && !environment.CI;
310
+ }
311
+ function runLookupBrowserSession(options) {
312
+ const environment = options.environment || process.env;
313
+ if (!canBrowseLookup(options.input, options.output, environment)) {
314
+ throw new TypeError('Lookup browsing requires interactive terminal input and output.');
315
+ }
316
+ if (options.signal?.aborted)
317
+ return Promise.reject(new DOMException('Aborted', 'AbortError'));
318
+ if (!options.document && (!options.collect || !options.query || !options.mode)) {
319
+ throw new TypeError('Lookup browser collection requires a target, mode, and collection operation.');
320
+ }
321
+ const input = options.input;
322
+ const output = options.output;
323
+ const previousRaw = input.isRaw === true;
324
+ const wasPaused = input.isPaused?.() === true;
325
+ const collectionController = new AbortController();
326
+ const collectionSignal = options.signal
327
+ ? AbortSignal.any([options.signal, collectionController.signal])
328
+ : collectionController.signal;
329
+ const sourceOrder = [...(options.plannedSources || [])];
330
+ if (sourceOrder.length > 16
331
+ || new Set(sourceOrder).size !== sourceOrder.length
332
+ || sourceOrder.some((source) => !/^[a-z][a-z0-9_]{0,39}$/u.test(source))) {
333
+ throw new TypeError('Lookup browser planned sources are invalid or exceed their bound.');
334
+ }
335
+ const sourceStates = new Map(sourceOrder.map((source) => [source, Object.freeze({
336
+ source,
337
+ state: 'pending',
338
+ complete: false,
339
+ truncated: false,
340
+ limitation: null,
341
+ })]));
342
+ const settledSourceIds = new Set();
343
+ let document = options.document || null;
344
+ let collecting = document === null;
345
+ let panels = document ? buildLookupBrowserPanels(document) : [];
346
+ let activeIndex = 0;
347
+ let scrollOffset = 0;
348
+ let maximumScrollOffset = 0;
349
+ let pageSize = 1;
350
+ let settled = false;
351
+ let pendingInput = '';
352
+ let escapeTimer = null;
353
+ let view = 'normal';
354
+ let searchDraft = '';
355
+ let searchNotice = '';
356
+ let searchQuery = '';
357
+ let searchMatches = [];
358
+ let searchMatchIndex = 0;
359
+ let searchTruncated = false;
360
+ let renderedWidth = 100;
361
+ let inputDecoder = new StringDecoder('utf8');
362
+ return new Promise((resolve, reject) => {
363
+ const render = () => {
364
+ if (Number.isFinite(output.columns) && Number(output.columns) < 40
365
+ || Number.isFinite(output.rows) && Number(output.rows) < MIN_LOOKUP_BROWSER_HEIGHT) {
366
+ throw new RangeError(`Lookup browsing requires a terminal of at least 40 columns by ${MIN_LOOKUP_BROWSER_HEIGHT} rows.`);
367
+ }
368
+ const presentation = terminalPresentation(output, options.color !== false, environment, options.palette);
369
+ const geometry = {
370
+ width: presentation.width ?? output.columns ?? 100,
371
+ ...(output.rows !== undefined ? { height: output.rows } : {}),
372
+ color: presentation.color,
373
+ palette: presentation.palette,
374
+ };
375
+ renderedWidth = boundedBrowserDimensions(geometry).width;
376
+ let value;
377
+ if (view === 'help') {
378
+ value = buildLookupBrowserHelpFrame(document, {
379
+ ...geometry,
380
+ ...(options.query ? { query: options.query } : {}),
381
+ ...(options.mode ? { mode: options.mode } : {}),
382
+ collecting,
383
+ });
384
+ }
385
+ else if (view === 'search' && document) {
386
+ value = buildLookupBrowserSearchFrame(document, searchDraft, searchNotice, geometry);
387
+ }
388
+ else if (collecting) {
389
+ value = buildLookupCollectionFrame(options.query, options.mode || 'fast', sourceOrder.map((source) => sourceStates.get(source)), geometry);
390
+ }
391
+ else if (document) {
392
+ const searchStatus = searchQuery
393
+ ? searchMatches.length
394
+ ? `Match ${searchMatchIndex + 1}/${searchMatches.length}${searchTruncated ? '+' : ''} · ${safeTerminalValue(searchQuery, 'search')}`
395
+ : `No matches · ${safeTerminalValue(searchQuery, 'search')}`
396
+ : undefined;
397
+ const frame = buildLookupBrowserFrame(document, {
398
+ activeIndex,
399
+ scrollOffset,
400
+ ...geometry,
401
+ environment,
402
+ ...(searchStatus ? { searchStatus } : {}),
403
+ });
404
+ scrollOffset = frame.scrollOffset;
405
+ maximumScrollOffset = frame.maximumScrollOffset;
406
+ pageSize = Math.max(1, frame.pageSize);
407
+ value = frame.value;
408
+ }
409
+ else {
410
+ throw new Error('Lookup browser has no collection or final document to render.');
411
+ }
412
+ output.write(CLEAR_SCREEN);
413
+ output.write(value);
414
+ };
415
+ const redraw = () => {
416
+ try {
417
+ render();
418
+ }
419
+ catch (error) {
420
+ if (collecting && !collectionController.signal.aborted)
421
+ collectionController.abort(error);
422
+ finish(error instanceof Error ? error : new Error('Lookup browser could not redraw.'));
423
+ }
424
+ };
425
+ const cleanup = () => {
426
+ let failure = null;
427
+ const attempt = (operation) => {
428
+ try {
429
+ operation();
430
+ }
431
+ catch (error) {
432
+ failure ||= error instanceof Error ? error : new Error('Lookup browser terminal cleanup failed.');
433
+ }
434
+ };
435
+ if (escapeTimer)
436
+ clearTimeout(escapeTimer);
437
+ escapeTimer = null;
438
+ attempt(() => input.off?.('data', onData));
439
+ attempt(() => input.off?.('end', onEnd));
440
+ attempt(() => output.off?.('resize', onResize));
441
+ attempt(() => options.signal?.removeEventListener('abort', onAbort));
442
+ attempt(() => input.setRawMode?.(previousRaw));
443
+ if (wasPaused)
444
+ attempt(() => input.pause?.());
445
+ attempt(() => output.write(LEAVE_ALTERNATE_SCREEN));
446
+ return failure;
447
+ };
448
+ const finish = (error) => {
449
+ if (settled)
450
+ return;
451
+ settled = true;
452
+ const cleanupFailure = cleanup();
453
+ if (error && cleanupFailure) {
454
+ reject(new AggregateError([error, cleanupFailure], `${error.message}; terminal cleanup also failed: ${cleanupFailure.message}`));
455
+ }
456
+ else if (error || cleanupFailure)
457
+ reject(error || cleanupFailure);
458
+ else if (document)
459
+ resolve(document);
460
+ else
461
+ reject(new Error('Lookup browser closed without a final document.'));
462
+ };
463
+ const clearSearch = () => {
464
+ searchQuery = '';
465
+ searchMatches = [];
466
+ searchMatchIndex = 0;
467
+ searchTruncated = false;
468
+ };
469
+ const movePanel = (offset, preserveSearch = false) => {
470
+ if (!panels.length)
471
+ return;
472
+ activeIndex = (activeIndex + offset + panels.length) % panels.length;
473
+ scrollOffset = 0;
474
+ if (!preserveSearch)
475
+ clearSearch();
476
+ redraw();
477
+ };
478
+ const scrollBy = (offset) => {
479
+ scrollOffset = Math.max(0, Math.min(maximumScrollOffset, scrollOffset + offset));
480
+ redraw();
481
+ };
482
+ const scrollTo = (offset) => {
483
+ scrollOffset = Math.max(0, Math.min(maximumScrollOffset, offset));
484
+ redraw();
485
+ };
486
+ const cancelCollection = () => {
487
+ const error = new DOMException('Aborted', 'AbortError');
488
+ if (!collectionController.signal.aborted)
489
+ collectionController.abort(error);
490
+ finish(error);
491
+ };
492
+ const handleEscape = () => {
493
+ if (view !== 'normal') {
494
+ view = 'normal';
495
+ searchDraft = '';
496
+ searchNotice = '';
497
+ redraw();
498
+ }
499
+ else if (collecting)
500
+ cancelCollection();
501
+ else
502
+ finish();
503
+ };
504
+ const scheduleEscapeAction = () => {
505
+ if (escapeTimer)
506
+ return;
507
+ escapeTimer = setTimeout(() => {
508
+ escapeTimer = null;
509
+ pendingInput = '';
510
+ handleEscape();
511
+ }, ESCAPE_SEQUENCE_WAIT_MS);
512
+ };
513
+ const scheduleIncompleteCsiDiscard = () => {
514
+ if (escapeTimer)
515
+ return;
516
+ escapeTimer = setTimeout(() => {
517
+ escapeTimer = null;
518
+ pendingInput = '';
519
+ }, ESCAPE_SEQUENCE_WAIT_MS);
520
+ };
521
+ const applySearchMatch = () => {
522
+ const match = searchMatches[searchMatchIndex];
523
+ if (!match)
524
+ return;
525
+ activeIndex = match.panelIndex;
526
+ const panel = panels[match.panelIndex];
527
+ scrollOffset = panel ? renderedPanelLineOffset(panel, match.lineIndex, renderedWidth) : 0;
528
+ redraw();
529
+ };
530
+ const onResize = () => {
531
+ if (searchMatches.length) {
532
+ const match = searchMatches[searchMatchIndex];
533
+ const panel = match ? panels[match.panelIndex] : null;
534
+ const width = boundedBrowserDimensions(output.columns === undefined ? {} : { width: output.columns }).width;
535
+ if (match && panel)
536
+ scrollOffset = renderedPanelLineOffset(panel, match.lineIndex, width);
537
+ }
538
+ redraw();
539
+ };
540
+ const commitSearch = () => {
541
+ searchQuery = searchDraft.trim();
542
+ const result = findLookupBrowserMatches(panels, searchQuery);
543
+ searchMatches = result.matches;
544
+ searchTruncated = result.truncated;
545
+ searchMatchIndex = 0;
546
+ view = 'normal';
547
+ searchDraft = '';
548
+ searchNotice = '';
549
+ if (searchMatches.length)
550
+ applySearchMatch();
551
+ else
552
+ redraw();
553
+ };
554
+ const moveSearchMatch = (offset) => {
555
+ if (!searchMatches.length)
556
+ return;
557
+ searchMatchIndex = (searchMatchIndex + offset + searchMatches.length) % searchMatches.length;
558
+ applySearchMatch();
559
+ };
560
+ const appendSearchCharacter = (character) => {
561
+ const candidate = `${searchDraft}${character}`;
562
+ try {
563
+ searchDraft = boundedSearchText(candidate);
564
+ searchNotice = '';
565
+ }
566
+ catch (error) {
567
+ searchNotice = error instanceof Error ? error.message : 'Search input exceeds its bound.';
568
+ }
569
+ redraw();
570
+ };
571
+ const processSearchCharacter = (character) => {
572
+ if (view !== 'search')
573
+ return false;
574
+ if (character === '\r' || character === '\n') {
575
+ commitSearch();
576
+ return true;
577
+ }
578
+ if (character === '\u0008' || character === '\u007f') {
579
+ searchDraft = Array.from(searchDraft).slice(0, -1).join('');
580
+ searchNotice = '';
581
+ redraw();
582
+ return true;
583
+ }
584
+ if (!/[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/u.test(character)) {
585
+ appendSearchCharacter(character);
586
+ }
587
+ else {
588
+ searchNotice = 'Search input is not valid bounded UTF-8 text.';
589
+ redraw();
590
+ }
591
+ return true;
592
+ };
593
+ const processPendingInput = () => {
594
+ while (pendingInput && !settled) {
595
+ if (pendingInput.startsWith('\u001b')) {
596
+ if (pendingInput.length === 1) {
597
+ scheduleEscapeAction();
598
+ return;
599
+ }
600
+ if (pendingInput[1] === '[') {
601
+ const sequence = pendingInput.match(COMPLETE_CSI_RE)?.[0];
602
+ if (!sequence) {
603
+ if (pendingInput.length < MAX_PENDING_INPUT_LENGTH) {
604
+ scheduleIncompleteCsiDiscard();
605
+ return;
606
+ }
607
+ pendingInput = '';
608
+ return;
609
+ }
610
+ pendingInput = pendingInput.slice(sequence.length);
611
+ if (!collecting && view === 'normal') {
612
+ if (sequence === '\u001b[D' || sequence === '\u001b[Z')
613
+ movePanel(-1);
614
+ else if (sequence === '\u001b[C')
615
+ movePanel(1);
616
+ else if (sequence === '\u001b[A')
617
+ scrollBy(-1);
618
+ else if (sequence === '\u001b[B')
619
+ scrollBy(1);
620
+ else if (sequence === '\u001b[5~')
621
+ scrollBy(-pageSize);
622
+ else if (sequence === '\u001b[6~')
623
+ scrollBy(pageSize);
624
+ else if (sequence === '\u001b[H' || sequence === '\u001b[1~')
625
+ scrollTo(0);
626
+ else if (sequence === '\u001b[F' || sequence === '\u001b[4~')
627
+ scrollTo(maximumScrollOffset);
628
+ }
629
+ continue;
630
+ }
631
+ pendingInput = pendingInput.slice(1);
632
+ handleEscape();
633
+ continue;
634
+ }
635
+ const character = Array.from(pendingInput)[0];
636
+ pendingInput = pendingInput.slice(character.length);
637
+ if (character === '\u0003') {
638
+ if (collecting)
639
+ cancelCollection();
640
+ else
641
+ finish(new DOMException('Aborted', 'AbortError'));
642
+ return;
643
+ }
644
+ if (character === '\u0004') {
645
+ cancelCollection();
646
+ return;
647
+ }
648
+ if (processSearchCharacter(character))
649
+ continue;
650
+ if (character === '?') {
651
+ view = view === 'help' ? 'normal' : 'help';
652
+ redraw();
653
+ continue;
654
+ }
655
+ if (character === 'q' || character === 'Q') {
656
+ if (collecting)
657
+ cancelCollection();
658
+ else
659
+ finish();
660
+ return;
661
+ }
662
+ if (collecting || view === 'help')
663
+ continue;
664
+ if (character === '\r' || character === '\n') {
665
+ finish();
666
+ return;
667
+ }
668
+ if (character === '/') {
669
+ view = 'search';
670
+ searchDraft = '';
671
+ searchNotice = '';
672
+ redraw();
673
+ }
674
+ else if (character === 'n')
675
+ moveSearchMatch(1);
676
+ else if (character === 'N')
677
+ moveSearchMatch(-1);
678
+ else if (character === '\t' || character === 'l' || character === 'L')
679
+ movePanel(1);
680
+ else if (character === 'h' || character === 'H')
681
+ movePanel(-1);
682
+ else if (character === 'j' || character === 'J')
683
+ scrollBy(1);
684
+ else if (character === 'k' || character === 'K')
685
+ scrollBy(-1);
686
+ else if (/^[1-9]$/u.test(character)) {
687
+ const requested = Number(character) - 1;
688
+ if (requested < panels.length) {
689
+ activeIndex = requested;
690
+ scrollOffset = 0;
691
+ clearSearch();
692
+ redraw();
693
+ }
694
+ }
695
+ }
696
+ };
697
+ const onData = (chunk) => {
698
+ if (escapeTimer)
699
+ clearTimeout(escapeTimer);
700
+ escapeTimer = null;
701
+ let rawChunk;
702
+ if (Buffer.isBuffer(chunk)) {
703
+ if (chunk.length > MAX_LOOKUP_BROWSER_INPUT_CHUNK_BYTES) {
704
+ inputDecoder = new StringDecoder('utf8');
705
+ pendingInput = '';
706
+ if (view === 'search') {
707
+ searchNotice = `Search input is limited to ${MAX_LOOKUP_BROWSER_SEARCH_SCALARS} characters and ${MAX_LOOKUP_BROWSER_SEARCH_BYTES} UTF-8 bytes.`;
708
+ redraw();
709
+ }
710
+ return;
711
+ }
712
+ rawChunk = chunk;
713
+ }
714
+ else {
715
+ const supplied = String(chunk ?? '');
716
+ if (supplied.length > MAX_LOOKUP_BROWSER_INPUT_CHUNK_BYTES
717
+ || Buffer.byteLength(supplied, 'utf8') > MAX_LOOKUP_BROWSER_INPUT_CHUNK_BYTES) {
718
+ inputDecoder = new StringDecoder('utf8');
719
+ pendingInput = '';
720
+ if (view === 'search') {
721
+ searchNotice = `Search input is limited to ${MAX_LOOKUP_BROWSER_SEARCH_SCALARS} characters and ${MAX_LOOKUP_BROWSER_SEARCH_BYTES} UTF-8 bytes.`;
722
+ redraw();
723
+ }
724
+ return;
725
+ }
726
+ rawChunk = Buffer.from(supplied, 'utf8');
727
+ }
728
+ const value = inputDecoder.write(rawChunk);
729
+ if (value.includes('\ufffd')) {
730
+ inputDecoder = new StringDecoder('utf8');
731
+ pendingInput = '';
732
+ if (view === 'search') {
733
+ searchNotice = 'Search input must be valid UTF-8 text.';
734
+ redraw();
735
+ }
736
+ return;
737
+ }
738
+ const combinedInput = `${pendingInput}${value}`;
739
+ if (combinedInput.length > MAX_PENDING_INPUT_LENGTH) {
740
+ inputDecoder = new StringDecoder('utf8');
741
+ pendingInput = '';
742
+ if (view === 'search') {
743
+ searchNotice = `Search input is limited to ${MAX_LOOKUP_BROWSER_SEARCH_SCALARS} characters and ${MAX_LOOKUP_BROWSER_SEARCH_BYTES} UTF-8 bytes.`;
744
+ redraw();
745
+ }
746
+ return;
747
+ }
748
+ pendingInput = combinedInput;
749
+ processPendingInput();
750
+ };
751
+ const onEnd = () => cancelCollection();
752
+ const onAbort = () => {
753
+ const suppliedReason = options.signal?.reason;
754
+ const error = suppliedReason instanceof Error && suppliedReason.name !== 'AbortError'
755
+ ? suppliedReason
756
+ : new DOMException('Aborted', 'AbortError');
757
+ if (collecting && !collectionController.signal.aborted)
758
+ collectionController.abort(error);
759
+ finish(error);
760
+ };
761
+ const onSourceSettled = (settlement) => {
762
+ if (settled || !collecting)
763
+ return;
764
+ if (!sourceStates.has(settlement.source) || settledSourceIds.has(settlement.source)) {
765
+ const error = new Error('Lookup browser received an unplanned or duplicate source settlement.');
766
+ if (!collectionController.signal.aborted)
767
+ collectionController.abort(error);
768
+ finish(error);
769
+ return;
770
+ }
771
+ settledSourceIds.add(settlement.source);
772
+ const limitation = typeof settlement.fragment?.limitation === 'string'
773
+ ? safeTerminalValue(settlement.fragment.limitation, 'Source limitation unavailable')
774
+ : null;
775
+ sourceStates.set(settlement.source, Object.freeze({
776
+ source: settlement.source,
777
+ state: settlement.state,
778
+ complete: settlement.complete,
779
+ truncated: settlement.truncated,
780
+ limitation,
781
+ }));
782
+ redraw();
783
+ };
784
+ const startCollection = () => {
785
+ if (settled || !collecting || !options.collect)
786
+ return;
787
+ void new Promise((resolve) => setImmediate(resolve))
788
+ .then(() => {
789
+ if (settled || collectionSignal.aborted) {
790
+ throw collectionSignal.reason || new DOMException('Aborted', 'AbortError');
791
+ }
792
+ return options.collect({ signal: collectionSignal, onSourceSettled });
793
+ })
794
+ .then((nextDocument) => {
795
+ if (settled)
796
+ return;
797
+ if (!nextDocument || typeof nextDocument !== 'object' || Array.isArray(nextDocument)) {
798
+ throw new TypeError('Lookup collection did not return one final document.');
799
+ }
800
+ document = nextDocument;
801
+ panels = buildLookupBrowserPanels(nextDocument);
802
+ collecting = false;
803
+ view = 'normal';
804
+ activeIndex = 0;
805
+ scrollOffset = 0;
806
+ redraw();
807
+ })
808
+ .catch((error) => {
809
+ if (settled)
810
+ return;
811
+ finish(error instanceof Error ? error : new Error('Lookup collection failed.'));
812
+ });
813
+ };
814
+ try {
815
+ output.write(ENTER_ALTERNATE_SCREEN);
816
+ input.on?.('data', onData);
817
+ input.on?.('end', onEnd);
818
+ output.on?.('resize', onResize);
819
+ options.signal?.addEventListener('abort', onAbort, { once: true });
820
+ if (options.signal?.aborted) {
821
+ onAbort();
822
+ return;
823
+ }
824
+ input.setRawMode?.(true);
825
+ if (settled)
826
+ return;
827
+ render();
828
+ if (settled)
829
+ return;
830
+ input.resume?.();
831
+ startCollection();
832
+ }
833
+ catch (error) {
834
+ finish(error instanceof Error ? error : new Error('Lookup browser could not start.'));
835
+ }
836
+ });
837
+ }
838
+ function browseLookupDocument(document, options) {
839
+ return runLookupBrowserSession({ ...options, document }).then(() => undefined);
840
+ }
841
+ function browseLookupOperation(options) {
842
+ return runLookupBrowserSession(options);
843
+ }
844
+ export { MAX_LOOKUP_BROWSER_PANELS, MAX_LOOKUP_BROWSER_INPUT_CHUNK_BYTES, MAX_LOOKUP_BROWSER_SEARCH_BYTES, MAX_LOOKUP_BROWSER_SEARCH_MATCHES, MAX_LOOKUP_BROWSER_SEARCH_SCALARS, browseLookupOperation, browseLookupDocument, boundedSearchText, buildLookupBrowserHelpFrame, buildLookupBrowserFrame, buildLookupBrowserPanels, buildLookupBrowserSearchFrame, buildLookupCollectionFrame, canBrowseLookup, findLookupBrowserMatches, renderedPanelLineOffset, renderLookupBrowser, terminalDisplayWidth, };