@slicedearth/whoisleuth-cli 1.43.0 → 1.47.4

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 (153) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +67 -10
  4. package/bin/whoisleuth.mjs +22 -9
  5. package/cli/arguments.mjs +256 -9
  6. package/cli/artifact-structure.mjs +1833 -0
  7. package/cli/artifact-verify.mjs +242 -49
  8. package/cli/bounded-json.mjs +1 -0
  9. package/cli/bulk-checkpoint.mjs +67 -31
  10. package/cli/bulk-command-runner.mjs +16 -6
  11. package/cli/bulk.mjs +18 -7
  12. package/cli/case-pack.mjs +819 -34
  13. package/cli/command-reference.mjs +48 -24
  14. package/cli/completion.mjs +50 -11
  15. package/cli/config-profile.mjs +15 -12
  16. package/cli/ct-event-intake.mjs +32 -2
  17. package/cli/discovery-observation-snapshot.mjs +8 -14
  18. package/cli/discovery-scan-command-runner.mjs +18 -6
  19. package/cli/discovery-snapshot.mjs +8 -15
  20. package/cli/discovery-workflow.mjs +2 -3
  21. package/cli/doctor.mjs +2 -2
  22. package/cli/domain-control-monitor.mjs +21 -15
  23. package/cli/domain-control-observations.mjs +25 -2
  24. package/cli/errors.mjs +7 -1
  25. package/cli/evidence-command-runner.mjs +17 -11
  26. package/cli/evidence-signing.mjs +73 -45
  27. package/cli/export-evidence.mjs +11 -4
  28. package/cli/formatters/html.mjs +3 -3
  29. package/cli/formatters/json.mjs +12 -4
  30. package/cli/formatters/terminal.mjs +4 -1
  31. package/cli/http.mjs +2 -1
  32. package/cli/input.mjs +57 -0
  33. package/cli/interchange-report.mjs +226 -0
  34. package/cli/investigation-manifest.mjs +16 -17
  35. package/cli/investigation-run.mjs +71 -23
  36. package/cli/local-mmdb-review.mjs +45 -14
  37. package/cli/lookup-command-runner.mjs +2 -2
  38. package/cli/lookup-diff.mjs +9 -0
  39. package/cli/lookup-reconcile.mjs +1 -1
  40. package/cli/mail-review.mjs +5 -5
  41. package/cli/manual.mjs +1 -1
  42. package/cli/network-command-runner.mjs +78 -0
  43. package/cli/offline-evidence-review.mjs +28 -8
  44. package/cli/page-compare.mjs +8 -5
  45. package/cli/registry-cohort.mjs +385 -61
  46. package/cli/registry-fixture-scaffold.mjs +4 -3
  47. package/cli/registry-support.mjs +2 -1
  48. package/cli/retained-artifact-diff.mjs +376 -0
  49. package/cli/risk-calibration.mjs +7 -5
  50. package/cli/runner.mjs +150 -177
  51. package/cli/saved-lookup.mjs +26 -3
  52. package/cli/sharing-review.mjs +12 -6
  53. package/docs/cli-reference.md +467 -74
  54. package/docs/cli.md +25 -5
  55. package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
  56. package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
  57. package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
  58. package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
  59. package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
  60. package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
  61. package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
  62. package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
  63. package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
  64. package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
  65. package/frontend/src/lib/analysis/bulk-sort.js +4 -0
  66. package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
  67. package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
  68. package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
  69. package/frontend/src/lib/analysis/case-migration-model.js +83 -31
  70. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  71. package/frontend/src/lib/analysis/case-record-core.js +1 -1
  72. package/frontend/src/lib/analysis/case-record-model.js +2 -0
  73. package/frontend/src/lib/analysis/case-record-operations.js +43 -13
  74. package/frontend/src/lib/analysis/case-report.js +39 -6
  75. package/frontend/src/lib/analysis/case-response-model.js +66 -7
  76. package/frontend/src/lib/analysis/case-response-packet.js +15 -5
  77. package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
  78. package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
  79. package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
  80. package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
  81. package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
  82. package/frontend/src/lib/analysis/domain-control-records.js +88 -0
  83. package/frontend/src/lib/analysis/external-findings-import.js +70 -6
  84. package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
  85. package/frontend/src/lib/analysis/investigation-guide.js +52 -10
  86. package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
  87. package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
  88. package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
  89. package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
  90. package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
  91. package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
  92. package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
  93. package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
  94. package/frontend/src/lib/analysis/utils.js +27 -4
  95. package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
  96. package/frontend/src/lib/analysis/visualization-models.js +63 -94
  97. package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
  98. package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
  99. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
  100. package/frontend/src/lib/analysis/workspace-archive.js +105 -19
  101. package/lib/application-version.mjs +3 -0
  102. package/lib/availability.mjs +14 -12
  103. package/lib/bounded-file.mjs +82 -0
  104. package/lib/bounded-json-response.mjs +170 -0
  105. package/lib/bounded-json.mjs +249 -0
  106. package/lib/browser-library-profile.mjs +167 -38
  107. package/lib/certificate-extension-profile.mjs +2 -1
  108. package/lib/cryptographic-assurance.mjs +155 -0
  109. package/lib/ct-search.mjs +87 -47
  110. package/lib/dns-intelligence.mjs +3 -2
  111. package/lib/dns-resolver-selection.mjs +6 -4
  112. package/lib/dnssec-chain-validation.mjs +1053 -0
  113. package/lib/dnssec-evidence-validation.mjs +10 -3
  114. package/lib/domain-change-packet.mjs +7 -5
  115. package/lib/domain-control-manifest.mjs +17 -129
  116. package/lib/domain-posture.mjs +7 -4
  117. package/lib/evidence-export.mjs +751 -78
  118. package/lib/evidence-report-markdown.mjs +3 -0
  119. package/lib/evidence-report.mjs +5 -0
  120. package/lib/favicon.mjs +8 -5
  121. package/lib/http-evidence-bounds.mjs +3 -0
  122. package/lib/http-intelligence.mjs +2 -5
  123. package/lib/interchange-fidelity-registry.mjs +122 -0
  124. package/lib/local-geoip-evidence.mjs +2 -1
  125. package/lib/lookup-cache.mjs +5 -1
  126. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  127. package/lib/lookup-response-contract.mjs +691 -3
  128. package/lib/lookup.mjs +2 -2
  129. package/lib/observation.mjs +7 -6
  130. package/lib/outbound-identity.mjs +2 -3
  131. package/lib/perceptual-hash.mjs +12 -4
  132. package/lib/portable-generator.mjs +21 -0
  133. package/lib/provider-policy-admission.mjs +9 -4
  134. package/lib/rdap-normalization.mjs +18 -5
  135. package/lib/rdap-registrar.mjs +5 -1
  136. package/lib/rdap-transport.mjs +6 -6
  137. package/lib/registry-capabilities.mjs +3 -0
  138. package/lib/registry-capability-catalogue.mjs +12 -2
  139. package/lib/registry-comparison.mjs +28 -11
  140. package/lib/risk-calibration-summary.mjs +338 -0
  141. package/lib/rpki-evidence.mjs +9 -8
  142. package/lib/safe-fetch.mjs +96 -42
  143. package/lib/security-txt.mjs +26 -3
  144. package/lib/service-binding-dns.mjs +6 -1
  145. package/lib/smtp-transport-review.mjs +784 -0
  146. package/lib/sslbl-certificates.generated.mjs +136 -134
  147. package/lib/threat-intelligence-runtime.mjs +4 -13
  148. package/lib/threat-intelligence-types.mjs +18 -1
  149. package/lib/tls-intelligence.mjs +11 -5
  150. package/lib/tlsa-evidence.mjs +37 -7
  151. package/lib/website-technology.mjs +2 -6
  152. package/package.json +1 -1
  153. package/third-party-notices.txt +95 -5817
@@ -1,10 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { open } from 'node:fs/promises';
2
+ import { lstat } from 'node:fs/promises';
3
3
  import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
4
- import { CliUsageError } from "./errors.mjs";
4
+ import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
5
+ import { boundedCliInputError, CliUsageError } from "./errors.mjs";
5
6
  import { writePrivateFile } from "./output-file.mjs";
6
7
  export const CLI_BULK_CHECKPOINT_SCHEMA = 'whoisleuth.cli.bulk-checkpoint';
7
- export const CLI_BULK_CHECKPOINT_VERSION = 1;
8
+ export const CLI_BULK_CHECKPOINT_VERSION = 2;
9
+ const SUPPORTED_BULK_CHECKPOINT_VERSIONS = new Set([1, CLI_BULK_CHECKPOINT_VERSION]);
8
10
  export const MAX_BULK_CHECKPOINT_BYTES = 16 * 1024 * 1024;
9
11
  const MAX_CHECKPOINT_JSON_DEPTH = 12;
10
12
  const MAX_CHECKPOINT_OBJECT_KEYS = 256;
@@ -51,7 +53,7 @@ function normalizeCompactResult(value) {
51
53
  return null;
52
54
  return result;
53
55
  }
54
- function normalizeCheckpointResult(value, queries, classifyQuery) {
56
+ function normalizeCheckpointResult(value, queries, classifyQuery, version = CLI_BULK_CHECKPOINT_VERSION) {
55
57
  const item = recordOrNull(value);
56
58
  const index = item?.index;
57
59
  if (!Number.isSafeInteger(index) || Number(index) < 0 || Number(index) >= queries.length)
@@ -59,10 +61,17 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
59
61
  const query = queries[Number(index)];
60
62
  if (typeof item?.query !== 'string' || item.query !== query)
61
63
  return null;
64
+ const observedAt = version >= 2
65
+ ? item.observedAt === null
66
+ ? null
67
+ : validTimestamp(item.observedAt) ? new Date(item.observedAt).toISOString() : undefined
68
+ : null;
69
+ if (observedAt === undefined)
70
+ return null;
62
71
  if (item.ok === false) {
63
72
  if (typeof item.error !== 'string' || !item.error || item.error.length > 300)
64
73
  return null;
65
- return { index: Number(index), query, ok: false, error: item.error };
74
+ return { index: Number(index), query, ok: false, error: item.error, observedAt };
66
75
  }
67
76
  const result = normalizeCompactResult(item.result);
68
77
  if (item.ok !== true || !result)
@@ -71,7 +80,7 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
71
80
  const storedClassified = recordOrNull(item.classified);
72
81
  if (storedClassified?.type !== classified.type || storedClassified.value !== classified.value)
73
82
  return null;
74
- return { index: Number(index), query, ok: true, classified, result };
83
+ return { index: Number(index), query, ok: true, classified, result, observedAt };
75
84
  }
76
85
  function parseBulkCheckpoint(text, options) {
77
86
  if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
@@ -86,8 +95,11 @@ function parseBulkCheckpoint(text, options) {
86
95
  }
87
96
  const document = recordOrNull(parsed);
88
97
  const expectedDigest = checkpointDigest(options.queries, options.deep);
89
- if (document?.schema !== CLI_BULK_CHECKPOINT_SCHEMA || document.version !== CLI_BULK_CHECKPOINT_VERSION) {
90
- throw new CliUsageError(`Bulk checkpoint must use ${CLI_BULK_CHECKPOINT_SCHEMA} version ${CLI_BULK_CHECKPOINT_VERSION}.`);
98
+ const version = typeof document?.version === 'number' && Number.isSafeInteger(document.version)
99
+ ? document.version
100
+ : 0;
101
+ if (document?.schema !== CLI_BULK_CHECKPOINT_SCHEMA || !SUPPORTED_BULK_CHECKPOINT_VERSIONS.has(version)) {
102
+ throw new CliUsageError(`Bulk checkpoint must use ${CLI_BULK_CHECKPOINT_SCHEMA} version 1 or ${CLI_BULK_CHECKPOINT_VERSION}.`);
91
103
  }
92
104
  if (document.mode !== (options.deep ? 'deep' : 'fast')
93
105
  || document.inputDigestSha256 !== expectedDigest
@@ -100,11 +112,11 @@ function parseBulkCheckpoint(text, options) {
100
112
  const results = [];
101
113
  const seen = new Set();
102
114
  for (const candidate of document.results.slice(0, options.queries.length + 1)) {
103
- const result = normalizeCheckpointResult(candidate, options.queries, options.classifyQuery);
115
+ const result = normalizeCheckpointResult(candidate, options.queries, options.classifyQuery, version);
104
116
  if (!result || seen.has(result.index))
105
117
  throw new CliUsageError('Bulk checkpoint contains an invalid or duplicate result.');
106
118
  seen.add(result.index);
107
- results.push(result);
119
+ results.push({ ...result, collectionOrigin: 'resumed_checkpoint' });
108
120
  }
109
121
  return {
110
122
  schema: CLI_BULK_CHECKPOINT_SCHEMA,
@@ -118,33 +130,32 @@ function parseBulkCheckpoint(text, options) {
118
130
  };
119
131
  }
120
132
  async function readCheckpointFile(path) {
121
- const handle = await open(path, 'r');
122
133
  try {
123
- const stats = await handle.stat();
124
- if (!stats.isFile() || stats.size > MAX_BULK_CHECKPOINT_BYTES) {
125
- throw new CliUsageError(`Bulk checkpoint input is limited to ${MAX_BULK_CHECKPOINT_BYTES} bytes.`);
126
- }
127
- return await handle.readFile({ encoding: 'utf8' });
134
+ return await readBoundedRegularTextFile(path, {
135
+ maximumBytes: MAX_BULK_CHECKPOINT_BYTES,
136
+ label: 'Bulk checkpoint input',
137
+ allowSymbolicLink: true,
138
+ });
128
139
  }
129
- finally {
130
- await handle.close();
140
+ catch (error) {
141
+ throw boundedCliInputError(error, 'Bulk checkpoint input');
131
142
  }
132
143
  }
133
144
  async function checkpointFileExists(path) {
134
145
  try {
135
- const handle = await open(path, 'r');
136
- await handle.close();
146
+ await lstat(path);
137
147
  return true;
138
148
  }
139
149
  catch (error) {
140
150
  const code = error && typeof error === 'object' && 'code' in error ? error.code : null;
141
151
  if (code === 'ENOENT')
142
152
  return false;
143
- throw error;
153
+ throw boundedCliInputError(error, 'Bulk checkpoint input');
144
154
  }
145
155
  }
146
156
  async function createBulkCheckpointWriter(options) {
147
157
  const now = options.now || (() => new Date().toISOString());
158
+ const writeCheckpoint = options.writeFile ?? writePrivateFile;
148
159
  const exists = await checkpointFileExists(options.path);
149
160
  if (options.resume && !exists)
150
161
  throw new CliUsageError('Bulk checkpoint does not exist; remove --resume to start a new checkpoint.');
@@ -155,7 +166,8 @@ async function createBulkCheckpointWriter(options) {
155
166
  : null;
156
167
  const startedAt = resumed?.startedAt || now();
157
168
  const results = new Map((resumed?.results || []).map((item) => [item.index, item]));
158
- let chain = Promise.resolve();
169
+ let activeWrite = null;
170
+ let dirty = false;
159
171
  let writeFailure = null;
160
172
  let created = resumed !== null;
161
173
  function document() {
@@ -168,19 +180,20 @@ async function createBulkCheckpointWriter(options) {
168
180
  queryCount: options.queries.length,
169
181
  startedAt,
170
182
  updatedAt,
171
- results: [...results.values()].sort((left, right) => left.index - right.index),
183
+ results: [...results.values()]
184
+ .sort((left, right) => left.index - right.index)
185
+ .map(({ collectionOrigin: _collectionOrigin, ...result }) => result),
172
186
  };
173
187
  }
174
- function scheduleWrite() {
175
- chain = chain.then(async () => {
176
- if (writeFailure)
177
- return;
188
+ async function drainWrites() {
189
+ while (dirty && !writeFailure) {
190
+ dirty = false;
178
191
  try {
179
192
  const content = `${JSON.stringify(document(), null, 2)}\n`;
180
193
  if (Buffer.byteLength(content, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
181
194
  throw new CliUsageError(`Bulk checkpoints are limited to ${MAX_BULK_CHECKPOINT_BYTES} bytes.`);
182
195
  }
183
- await writePrivateFile(options.path, content, {
196
+ await writeCheckpoint(options.path, content, {
184
197
  force: created,
185
198
  existingFileMessage: 'Bulk checkpoint already exists; use --resume or choose another path.',
186
199
  });
@@ -189,6 +202,23 @@ async function createBulkCheckpointWriter(options) {
189
202
  catch (error) {
190
203
  writeFailure = error;
191
204
  }
205
+ }
206
+ }
207
+ function scheduleWrite() {
208
+ dirty = true;
209
+ if (activeWrite || writeFailure)
210
+ return;
211
+ const pending = drainWrites();
212
+ activeWrite = pending;
213
+ void pending.then(() => {
214
+ if (activeWrite === pending)
215
+ activeWrite = null;
216
+ if (dirty && !writeFailure)
217
+ scheduleWrite();
218
+ }, (error) => {
219
+ writeFailure ||= error;
220
+ if (activeWrite === pending)
221
+ activeWrite = null;
192
222
  });
193
223
  }
194
224
  if (!resumed)
@@ -196,16 +226,22 @@ async function createBulkCheckpointWriter(options) {
196
226
  return Object.freeze({
197
227
  initialResults: Object.freeze([...results.values()].sort((left, right) => left.index - right.index)),
198
228
  record(result) {
199
- const normalized = normalizeCheckpointResult(result, options.queries, options.classifyQuery);
229
+ const observedAt = validTimestamp(result.observedAt) ? new Date(result.observedAt).toISOString() : now();
230
+ const normalized = normalizeCheckpointResult({ ...result, observedAt }, options.queries, options.classifyQuery);
200
231
  if (!normalized) {
201
232
  writeFailure ||= new CliUsageError('Bulk checkpoint result is not a bounded compact lookup result.');
202
233
  return;
203
234
  }
204
- results.set(normalized.index, normalized);
235
+ results.set(normalized.index, { ...normalized, collectionOrigin: 'current_run' });
205
236
  scheduleWrite();
206
237
  },
207
238
  async flush() {
208
- await chain;
239
+ while (activeWrite || dirty) {
240
+ if (!activeWrite)
241
+ scheduleWrite();
242
+ if (activeWrite)
243
+ await activeWrite;
244
+ }
209
245
  if (writeFailure)
210
246
  throw writeFailure;
211
247
  },
@@ -1,8 +1,7 @@
1
- import { createReadStream } from 'node:fs';
2
1
  import { classifyQuery } from "../lib/classify.mjs";
3
2
  import { runUnifiedLookup } from "../lib/lookup.mjs";
4
3
  import { createBulkCheckpointWriter } from "./bulk-checkpoint.mjs";
5
- import { MAX_BULK_INPUT_BYTES, parseBulkQueries, readTextStreamBounded, runBulkLookups, } from "./bulk.mjs";
4
+ import { MAX_BULK_INPUT_BYTES, parseBulkQueries, runBulkLookups, } from "./bulk.mjs";
6
5
  import { formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems } from "./bulk-output.mjs";
7
6
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
8
7
  import EXIT_CODES from "./exit-codes.mjs";
@@ -24,9 +23,7 @@ async function runBulkCommand(args, dependencies, context) {
24
23
  try {
25
24
  input = dependencies.readBulkInput
26
25
  ? await dependencies.readBulkInput(args.source)
27
- : await readTextStreamBounded(args.source
28
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
29
- : dependencies.stdin || process.stdin, MAX_BULK_INPUT_BYTES);
26
+ : await context.readInput(args.source, MAX_BULK_INPUT_BYTES, 'Bulk input');
30
27
  }
31
28
  catch (error) {
32
29
  if (error instanceof CliUsageError)
@@ -56,6 +53,8 @@ async function runBulkCommand(args, dependencies, context) {
56
53
  : null;
57
54
  const indicator = context.beginProgress(`Collecting 0 of ${parsed.queries.length} targets`);
58
55
  let completed = checkpoint?.initialResults.length || 0;
56
+ const resumedItems = new Map((checkpoint?.initialResults ?? []).map((item) => [item.index, item]));
57
+ const settledItems = new Map();
59
58
  if (completed)
60
59
  indicator.update(`Resumed ${completed} of ${parsed.queries.length} targets`);
61
60
  let items;
@@ -69,12 +68,23 @@ async function runBulkCommand(args, dependencies, context) {
69
68
  ...(checkpoint ? { initialResults: checkpoint.initialResults } : {}),
70
69
  ...(dependencies.signal ? { signal: dependencies.signal } : {}),
71
70
  onItemSettled: (item) => {
71
+ const observed = {
72
+ ...item,
73
+ observedAt: context.now(),
74
+ collectionOrigin: 'current_run',
75
+ };
76
+ settledItems.set(item.index, observed);
72
77
  completed += 1;
73
78
  indicator.update(`Collected ${completed} of ${parsed.queries.length} targets`);
74
79
  eventProgress.emit({ event: 'item_settled', index: item.index, ok: item.ok });
75
- checkpoint?.record(item);
80
+ checkpoint?.record(observed);
76
81
  },
77
82
  });
83
+ items = items.map((item) => resumedItems.get(item.index) ?? settledItems.get(item.index) ?? {
84
+ ...item,
85
+ observedAt: null,
86
+ collectionOrigin: 'current_run',
87
+ });
78
88
  }
79
89
  finally {
80
90
  context.endProgress();
package/cli/bulk.mjs CHANGED
@@ -3,17 +3,28 @@ import { abortable } from "../lib/abort.mjs";
3
3
  import { CliUsageError } from "./arguments.mjs";
4
4
  import { MAX_DEEP_BULK_QUERIES, MAX_FAST_BULK_QUERIES, } from "../lib/bulk-limits.mjs";
5
5
  const MAX_BULK_INPUT_BYTES = 1024 * 1024;
6
- async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES) {
6
+ async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signal) {
7
7
  if (!stream || stream.isTTY)
8
8
  return '';
9
+ const abort = () => stream.destroy?.(new DOMException('Aborted', 'AbortError'));
10
+ if (signal?.aborted)
11
+ throw new DOMException('Aborted', 'AbortError');
12
+ signal?.addEventListener('abort', abort, { once: true });
9
13
  const chunks = [];
10
14
  let total = 0;
11
- for await (const chunk of stream) {
12
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
13
- total += buffer.length;
14
- if (total > limit)
15
- throw new CliUsageError(`Bulk input is limited to ${limit} bytes.`);
16
- chunks.push(buffer);
15
+ try {
16
+ for await (const chunk of stream) {
17
+ if (signal?.aborted)
18
+ throw new DOMException('Aborted', 'AbortError');
19
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
20
+ total += buffer.length;
21
+ if (total > limit)
22
+ throw new CliUsageError(`Bulk input is limited to ${limit} bytes.`);
23
+ chunks.push(buffer);
24
+ }
25
+ }
26
+ finally {
27
+ signal?.removeEventListener('abort', abort);
17
28
  }
18
29
  return Buffer.concat(chunks).toString('utf8');
19
30
  }