@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.
- package/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +67 -10
- package/bin/whoisleuth.mjs +22 -9
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +67 -31
- package/cli/bulk-command-runner.mjs +16 -6
- package/cli/bulk.mjs +18 -7
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +50 -11
- package/cli/config-profile.mjs +15 -12
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-observation-snapshot.mjs +8 -14
- package/cli/discovery-scan-command-runner.mjs +18 -6
- package/cli/discovery-snapshot.mjs +8 -15
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/errors.mjs +7 -1
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +12 -4
- package/cli/formatters/terminal.mjs +4 -1
- package/cli/http.mjs +2 -1
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/lookup-reconcile.mjs +1 -1
- package/cli/mail-review.mjs +5 -5
- package/cli/manual.mjs +1 -1
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-fixture-scaffold.mjs +4 -3
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +150 -177
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +467 -74
- package/docs/cli.md +25 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +1 -1
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +43 -13
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
- package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
- package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +27 -4
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
- package/frontend/src/lib/analysis/workspace-archive.js +105 -19
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +82 -0
- package/lib/bounded-json-response.mjs +170 -0
- package/lib/bounded-json.mjs +249 -0
- package/lib/browser-library-profile.mjs +167 -38
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +5 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +6 -6
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +96 -42
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +4 -13
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/website-technology.mjs +2 -6
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
package/cli/bulk-checkpoint.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
3
3
|
import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
return await readBoundedRegularTextFile(path, {
|
|
135
|
+
maximumBytes: MAX_BULK_CHECKPOINT_BYTES,
|
|
136
|
+
label: 'Bulk checkpoint input',
|
|
137
|
+
allowSymbolicLink: true,
|
|
138
|
+
});
|
|
128
139
|
}
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
catch (error) {
|
|
141
|
+
throw boundedCliInputError(error, 'Bulk checkpoint input');
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
144
|
async function checkpointFileExists(path) {
|
|
134
145
|
try {
|
|
135
|
-
|
|
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
|
|
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()]
|
|
183
|
+
results: [...results.values()]
|
|
184
|
+
.sort((left, right) => left.index - right.index)
|
|
185
|
+
.map(({ collectionOrigin: _collectionOrigin, ...result }) => result),
|
|
172
186
|
};
|
|
173
187
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
}
|