@slicedearth/whoisleuth-cli 1.45.0 → 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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,49 +1,222 @@
1
1
  import { classifyQuery } from "../lib/classify.mjs";
2
- import { buildDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
3
- import { verifyDomainControlManifest } from "../lib/domain-control-manifest.mjs";
2
+ import { scanBoundedJson } from "../lib/bounded-json.mjs";
3
+ import { buildDomainControlFlightRecorder, validateDomainControlFlightRecorderDocument, } from "../lib/domain-control-flight-recorder.mjs";
4
+ import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, } from "../packages/contracts/domain-control-flight-recorder.mjs";
5
+ import { DOMAIN_CONTROL_REVIEW_SCHEMA, DOMAIN_CONTROL_REVIEW_VERSION, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, } from "../packages/contracts/domain-control-review.mjs";
6
+ import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
7
+ import { validateDomainControlReviewDocument, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
8
+ import { requireDomainName, requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
4
9
  import { buildCliLookupDocument } from "./formatters/json.mjs";
5
10
  import { buildCliDomainControlReview, } from "./domain-control-observations.mjs";
6
- import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
7
- export const CLI_DOMAIN_CONTROL_MONITOR_SCHEMA = 'whoisleuth.cli.domain-control-monitor';
8
- export const CLI_DOMAIN_CONTROL_MONITOR_VERSION = 1;
9
- export const MAX_DOMAIN_CONTROL_MONITOR_DOMAINS = 20;
10
- function previousSnapshot(input) {
11
- if (!input)
11
+ import { CliUsageError } from "./errors.mjs";
12
+ export { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION };
13
+ export const MAX_DOMAIN_CONTROL_MONITOR_DOMAINS = CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS;
14
+ function parseMonitorJson(input, label) {
15
+ const normalized = input.replace(/^\uFEFF/u, '');
16
+ if (Buffer.byteLength(input, 'utf8') > MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES) {
17
+ throw new CliUsageError(`${label} exceeds the ${MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES}-byte limit.`);
18
+ }
19
+ try {
20
+ scanBoundedJson(normalized, {
21
+ maximumDepth: MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH,
22
+ maximumKeys: MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS,
23
+ maximumValues: MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES,
24
+ maximumContainerItems: MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS,
25
+ });
26
+ return JSON.parse(normalized);
27
+ }
28
+ catch (cause) {
29
+ const detail = cause instanceof Error ? cause.message : '';
30
+ if (detail === 'Artefact input is not valid JSON.') {
31
+ throw new CliUsageError(`${label} must be valid JSON.`);
32
+ }
33
+ if (detail.startsWith('Artefact JSON ')) {
34
+ throw new CliUsageError(`${label} ${detail.slice('Artefact JSON '.length)}`);
35
+ }
36
+ throw new CliUsageError(`${label} must satisfy the bounded JSON structure contract.`);
37
+ }
38
+ }
39
+ function exactMonitorRecord(value, keys, label) {
40
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
41
+ throw new CliUsageError(`${label} must use its exact object fields.`);
42
+ }
43
+ const record = value;
44
+ const actualKeys = Object.keys(record);
45
+ if (actualKeys.length !== keys.length || actualKeys.some((key) => !keys.includes(key))) {
46
+ throw new CliUsageError(`${label} must use its exact object fields.`);
47
+ }
48
+ return record;
49
+ }
50
+ function monitorInteger(value, minimum, maximum, label) {
51
+ if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
52
+ throw new CliUsageError(`${label} must be an integer from ${minimum} to ${maximum}.`);
53
+ }
54
+ return Number(value);
55
+ }
56
+ function previousSnapshot(input, currentGeneratedAt) {
57
+ if (input === null)
12
58
  return { observations: [] };
13
- let parsed;
59
+ const parsed = parseMonitorJson(input, 'Previous monitor snapshot');
60
+ const root = exactMonitorRecord(parsed, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, 'Previous monitor snapshot');
61
+ if (root.schema !== CLI_DOMAIN_CONTROL_MONITOR_SCHEMA || root.version !== CLI_DOMAIN_CONTROL_MONITOR_VERSION) {
62
+ throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} version ${CLI_DOMAIN_CONTROL_MONITOR_VERSION}.`);
63
+ }
64
+ let previousGeneratedAt;
14
65
  try {
15
- parsed = JSON.parse(input.replace(/^\uFEFF/u, ''));
66
+ previousGeneratedAt = requireIsoTimestamp(root.generatedAt, 'Previous monitor snapshot generatedAt');
16
67
  }
17
68
  catch {
18
- throw new CliUsageError('Previous monitor snapshot must be valid JSON.');
69
+ throw new CliUsageError('Previous monitor snapshot generatedAt must be a valid ISO 8601 timestamp.');
19
70
  }
20
- const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
21
- if (root.schema !== CLI_DOMAIN_CONTROL_MONITOR_SCHEMA || root.version !== CLI_DOMAIN_CONTROL_MONITOR_VERSION || !Array.isArray(root.observations)) {
22
- throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} version ${CLI_DOMAIN_CONTROL_MONITOR_VERSION}.`);
71
+ if (Date.parse(previousGeneratedAt) >= Date.parse(currentGeneratedAt)) {
72
+ throw new CliUsageError('Previous monitor snapshot must precede the current monitor run.');
23
73
  }
24
- const validated = buildDomainControlFlightRecorder({
25
- schema: 'whoisleuth.domain-control-flight-recorder.input', version: 1, observations: root.observations, approvedWindows: [],
26
- });
27
- void validated;
28
- return { observations: root.observations };
74
+ const manifest = exactMonitorRecord(root.manifest, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, 'Previous monitor snapshot manifest');
75
+ if (typeof manifest.digestSha256 !== 'string'
76
+ || !/^sha256:[a-f0-9]{64}$/u.test(manifest.digestSha256)
77
+ || typeof manifest.expiresAt !== 'string') {
78
+ throw new CliUsageError('Previous monitor snapshot manifest is invalid.');
79
+ }
80
+ try {
81
+ requireIsoTimestamp(manifest.expiresAt, 'Previous monitor snapshot manifest expiresAt');
82
+ }
83
+ catch {
84
+ throw new CliUsageError('Previous monitor snapshot manifest expiresAt must be a valid ISO 8601 timestamp.');
85
+ }
86
+ if (Date.parse(manifest.expiresAt) <= Date.parse(previousGeneratedAt)) {
87
+ throw new CliUsageError('Previous monitor snapshot manifest must have been unexpired when the checkpoint was generated.');
88
+ }
89
+ const collection = exactMonitorRecord(root.collection, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, 'Previous monitor snapshot collection');
90
+ const requested = monitorInteger(collection.requested, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, 'Previous monitor snapshot requested count');
91
+ const succeeded = monitorInteger(collection.succeeded, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, 'Previous monitor snapshot succeeded count');
92
+ const failed = monitorInteger(collection.failed, 0, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, 'Previous monitor snapshot failed count');
93
+ if (requested !== succeeded + failed || !Array.isArray(collection.failures) || collection.failures.length !== failed) {
94
+ throw new CliUsageError('Previous monitor snapshot collection counts are inconsistent.');
95
+ }
96
+ const failureDomains = new Set();
97
+ const failureDomainOrder = [];
98
+ for (const [index, failureValue] of collection.failures.entries()) {
99
+ const failure = exactMonitorRecord(failureValue, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, `Previous monitor snapshot failure ${index + 1}`);
100
+ let failureDomain;
101
+ try {
102
+ failureDomain = requireDomainName(failure.domain, `Previous monitor snapshot failure ${index + 1} domain`);
103
+ }
104
+ catch {
105
+ throw new CliUsageError(`Previous monitor snapshot failure ${index + 1} domain is invalid.`);
106
+ }
107
+ if (failureDomain !== failure.domain
108
+ || failureDomains.has(failureDomain)
109
+ || typeof failure.error !== 'string'
110
+ || !DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES.includes(failure.error)
111
+ || failure.error.length > MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH) {
112
+ throw new CliUsageError(`Previous monitor snapshot failure ${index + 1} is invalid.`);
113
+ }
114
+ failureDomains.add(failureDomain);
115
+ failureDomainOrder.push(failureDomain);
116
+ }
117
+ if (!Array.isArray(root.observations)
118
+ || root.observations.length !== succeeded
119
+ || root.observations.length < MIN_DOMAIN_CONTROL_MONITOR_DOMAINS
120
+ || root.observations.length > MAX_DOMAIN_CONTROL_MONITOR_DOMAINS) {
121
+ throw new CliUsageError('Previous monitor snapshot observations are inconsistent with its collection summary.');
122
+ }
123
+ let review;
124
+ let flightRecorder;
125
+ try {
126
+ review = validateDomainControlReviewDocument(root.review);
127
+ }
128
+ catch {
129
+ throw new CliUsageError(`Previous monitor snapshot review must use the exact ${DOMAIN_CONTROL_REVIEW_SCHEMA} version ${DOMAIN_CONTROL_REVIEW_VERSION} contract.`);
130
+ }
131
+ try {
132
+ flightRecorder = validateDomainControlFlightRecorderDocument(root.flightRecorder);
133
+ }
134
+ catch {
135
+ throw new CliUsageError(`Previous monitor snapshot flight recorder must use the exact ${DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA} version ${DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION} contract.`);
136
+ }
137
+ if (!Array.isArray(root.limitations)
138
+ || root.limitations.length !== DOMAIN_CONTROL_MONITOR_LIMITATIONS.length
139
+ || root.limitations.some((value, index) => value !== DOMAIN_CONTROL_MONITOR_LIMITATIONS[index])) {
140
+ throw new CliUsageError('Previous monitor snapshot limitations are invalid.');
141
+ }
142
+ try {
143
+ buildDomainControlFlightRecorder({
144
+ schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
145
+ version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
146
+ observations: root.observations,
147
+ approvedWindows: [],
148
+ }, previousGeneratedAt);
149
+ }
150
+ catch {
151
+ throw new CliUsageError('Previous monitor snapshot observations must use the exact bounded flight-recorder input contract.');
152
+ }
153
+ const observations = root.observations;
154
+ const observationDomainOrder = observations.map((observation) => observation.domain);
155
+ const observationDomains = new Set(observationDomainOrder);
156
+ const requestedDomains = review.domains.slice(0, requested).map((item) => item.domain);
157
+ const expectedObservationDomains = requestedDomains.filter((domain) => !failureDomains.has(domain));
158
+ const expectedFailureDomains = requestedDomains.filter((domain) => !observationDomains.has(domain));
159
+ if (review.domains.length < requested
160
+ || observationDomains.size !== observations.length
161
+ || observationDomainOrder.some((domain, index) => domain !== expectedObservationDomains[index])
162
+ || observationDomainOrder.length !== expectedObservationDomains.length
163
+ || failureDomainOrder.some((domain, index) => domain !== expectedFailureDomains[index])
164
+ || failureDomainOrder.length !== expectedFailureDomains.length
165
+ || observations.some((observation) => observation.observedAt !== previousGeneratedAt)
166
+ || review.domains.some((item) => item.comparisons.some((comparison) => comparison.observedAt !== null && comparison.observedAt !== previousGeneratedAt))
167
+ || review.generatedAt !== previousGeneratedAt
168
+ || review.manifest.digestSha256 !== manifest.digestSha256
169
+ || review.manifest.expiresAt !== manifest.expiresAt
170
+ || [...observationDomains].some((domain) => !review.domains.some((item) => item.domain === domain))
171
+ || flightRecorder.generatedAt !== previousGeneratedAt
172
+ || flightRecorder.observationCount < observations.length
173
+ || [...observationDomains].some((domain) => !flightRecorder.domains.includes(domain))) {
174
+ throw new CliUsageError('Previous monitor snapshot embedded documents are inconsistent with its observation projection.');
175
+ }
176
+ return { observations };
177
+ }
178
+ function monitorFailureMessage() {
179
+ return DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES[0];
29
180
  }
30
181
  export async function runDomainControlMonitor(manifestInput, previousInput, options) {
31
- const generatedAt = options.now();
32
- let rawManifest;
182
+ if (!Number.isSafeInteger(options.limit)
183
+ || options.limit < MIN_DOMAIN_CONTROL_MONITOR_DOMAINS
184
+ || options.limit > MAX_DOMAIN_CONTROL_MONITOR_DOMAINS) {
185
+ throw new CliUsageError(`Domain-control monitor limit must be from ${MIN_DOMAIN_CONTROL_MONITOR_DOMAINS} to ${MAX_DOMAIN_CONTROL_MONITOR_DOMAINS}.`);
186
+ }
187
+ if (!Number.isSafeInteger(options.concurrency)
188
+ || options.concurrency < MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY
189
+ || options.concurrency > MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY) {
190
+ throw new CliUsageError(`Domain-control monitor concurrency must be from ${MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY} to ${MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY}.`);
191
+ }
192
+ let generatedAt;
193
+ try {
194
+ generatedAt = requireIsoTimestamp(options.now(), 'Domain-control monitor time');
195
+ }
196
+ catch {
197
+ throw new CliUsageError('Domain-control monitor time must be a valid ISO 8601 timestamp.');
198
+ }
199
+ const rawManifest = parseMonitorJson(manifestInput, 'Domain-control manifest');
200
+ let manifest;
33
201
  try {
34
- rawManifest = JSON.parse(manifestInput.replace(/^\uFEFF/u, ''));
202
+ manifest = verifyDomainControlManifest(rawManifest);
35
203
  }
36
204
  catch {
37
- throw new CliUsageError('Domain-control manifest must be valid JSON.');
205
+ throw new CliUsageError('Domain-control manifest must satisfy its supported bounded integrity contract.');
206
+ }
207
+ const checkedAt = Date.parse(generatedAt);
208
+ if (Date.parse(manifest.expiresAt) <= checkedAt) {
209
+ throw new CliUsageError('Domain-control monitor requires an unexpired manifest.');
38
210
  }
39
- const manifest = verifyDomainControlManifest(rawManifest);
211
+ const previous = previousSnapshot(previousInput, generatedAt);
40
212
  const entries = manifest.entries.slice(0, Math.min(options.limit, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS));
41
213
  const lookups = new Array(entries.length);
42
- const failures = [];
214
+ const failuresByIndex = new Array(entries.length);
43
215
  let cursor = 0;
44
216
  let completed = 0;
45
217
  const worker = async () => {
46
218
  while (cursor < entries.length) {
219
+ options.signal?.throwIfAborted();
47
220
  const index = cursor++;
48
221
  const entry = entries[index];
49
222
  if (!entry)
@@ -51,10 +224,14 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
51
224
  try {
52
225
  const classified = classifyQuery(entry.domain);
53
226
  const result = await options.executeLookup(classified, { fast: false, compact: false, ...(options.signal ? { signal: options.signal } : {}) });
227
+ options.signal?.throwIfAborted();
54
228
  lookups[index] = buildCliLookupDocument(entry.domain, classified, result, generatedAt, 'deep');
55
229
  }
56
- catch (cause) {
57
- failures.push({ domain: entry.domain, error: boundedCliErrorMessage(cause, 'Lookup failed') });
230
+ catch {
231
+ if (options.signal?.aborted) {
232
+ throw options.signal.reason || new DOMException('Aborted', 'AbortError');
233
+ }
234
+ failuresByIndex[index] = Object.freeze({ domain: entry.domain, error: monitorFailureMessage() });
58
235
  }
59
236
  finally {
60
237
  completed += 1;
@@ -63,17 +240,23 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
63
240
  }
64
241
  };
65
242
  await Promise.all(Array.from({ length: Math.min(options.concurrency, Math.max(entries.length, 1)) }, worker));
243
+ options.signal?.throwIfAborted();
244
+ const failures = failuresByIndex.filter((failure) => Boolean(failure));
66
245
  const successful = lookups.filter(Boolean);
67
246
  if (!successful.length)
68
247
  throw new Error('No domain-control monitor lookup completed successfully.');
69
248
  const review = buildCliDomainControlReview(JSON.stringify({
70
- schema: 'whoisleuth.cli.domain-control-review-input', version: 1, manifest, lookups: successful,
249
+ schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
250
+ version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
251
+ manifest,
252
+ lookups: successful,
71
253
  }), generatedAt);
72
- const previous = previousSnapshot(previousInput);
73
254
  const observations = review.observations;
74
255
  const flightRecorder = buildDomainControlFlightRecorder({
75
- schema: 'whoisleuth.domain-control-flight-recorder.input', version: 1,
76
- observations: [...previous.observations, ...observations], approvedWindows: [],
256
+ schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
257
+ version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
258
+ observations: [...previous.observations, ...observations],
259
+ approvedWindows: [],
77
260
  }, generatedAt);
78
261
  return Object.freeze({
79
262
  schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA,
@@ -84,11 +267,7 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
84
267
  observations,
85
268
  review: review.review,
86
269
  flightRecorder,
87
- limitations: Object.freeze([
88
- 'This is one bounded run, not a daemon. Scheduling and secure checkpoint retention remain operator-controlled.',
89
- 'The output retains compact normalised observations and errors, not raw RDAP, WHOIS, DNS, HTTP, TLS or page payloads.',
90
- 'A failed source or lookup remains incomplete and does not establish that a previously observed value disappeared.',
91
- ]),
270
+ limitations: DOMAIN_CONTROL_MONITOR_LIMITATIONS,
92
271
  });
93
272
  }
94
273
  export function formatDomainControlMonitor(document) {
@@ -1,12 +1,12 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { scanBoundedJson, } from "../lib/bounded-json.mjs";
3
+ import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
2
4
  import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, } from "../lib/domain-control-flight-recorder.mjs";
3
5
  import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
4
6
  import { CliUsageError } from "./errors.mjs";
5
7
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
6
- export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA = 'whoisleuth.cli.domain-control-review-input';
7
- export const CLI_DOMAIN_CONTROL_REVIEW_SCHEMA = 'whoisleuth.cli.domain-control-review';
8
- export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 1;
9
- export const MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES = 32 * 1024 * 1024;
8
+ export { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, };
9
+ const CLI_REVIEW_INPUT_KEY_SET = new Set(CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS);
10
10
  function record(value) {
11
11
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
12
12
  }
@@ -134,22 +134,43 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
134
134
  if (Buffer.byteLength(inputText, 'utf8') > MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES) {
135
135
  throw new CliUsageError(`Domain-control review input is limited to ${MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES} bytes.`);
136
136
  }
137
+ const normalizedInput = inputText.replace(/^\uFEFF/u, '');
138
+ try {
139
+ scanBoundedJson(normalizedInput, {
140
+ maximumDepth: MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH,
141
+ maximumKeys: MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS,
142
+ maximumValues: MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES,
143
+ });
144
+ }
145
+ catch (cause) {
146
+ const detail = cause instanceof Error ? cause.message : '';
147
+ if (detail === 'Artefact input is not valid JSON.') {
148
+ throw new CliUsageError('Domain-control review input must be valid JSON.');
149
+ }
150
+ const boundedDetail = detail.replace(/^Artefact JSON /u, '');
151
+ throw new CliUsageError(boundedDetail
152
+ ? `Domain-control review input ${boundedDetail}`
153
+ : 'Domain-control review input must be valid JSON.');
154
+ }
137
155
  let parsed;
138
156
  try {
139
- parsed = JSON.parse(inputText.replace(/^\uFEFF/u, ''));
157
+ parsed = JSON.parse(normalizedInput);
140
158
  }
141
159
  catch {
142
160
  throw new CliUsageError('Domain-control review input must be valid JSON.');
143
161
  }
144
162
  const input = record(parsed);
145
- if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA || input.version !== 1 || !Array.isArray(input.lookups)) {
146
- throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version 1.`);
163
+ if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
164
+ || input.version !== CLI_DOMAIN_CONTROL_REVIEW_VERSION
165
+ || !Array.isArray(input.lookups)) {
166
+ throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version ${CLI_DOMAIN_CONTROL_REVIEW_VERSION}.`);
147
167
  }
148
- if (Object.keys(input).some((key) => !new Set(['schema', 'version', 'manifest', 'lookups']).has(key))) {
168
+ if (Object.keys(input).some((key) => !CLI_REVIEW_INPUT_KEY_SET.has(key))) {
149
169
  throw new CliUsageError('Domain-control review input contains an unsupported field.');
150
170
  }
151
- if (input.lookups.length < 1 || input.lookups.length > 100) {
152
- throw new CliUsageError('Domain-control review requires from 1 to 100 saved Lookup documents.');
171
+ if (input.lookups.length < MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS
172
+ || input.lookups.length > MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS) {
173
+ throw new CliUsageError(`Domain-control review requires from ${MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS} to ${MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS} saved Lookup documents.`);
153
174
  }
154
175
  const manifest = verifyDomainControlManifest(input.manifest);
155
176
  const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
@@ -162,7 +183,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
162
183
  const observations = [...latest.values()].map(domainControlObservationFromSavedLookup);
163
184
  const review = reviewDomainControlManifest({
164
185
  schema: DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
165
- version: 1,
186
+ version: DOMAIN_CONTROL_REVIEW_VERSION,
166
187
  manifest,
167
188
  observations: observations.map((item) => ({
168
189
  domain: item.domain,
@@ -202,17 +223,13 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
202
223
  latestDomainObservations: observations.length,
203
224
  ignoredHistoricalLookups: lookups.length - observations.length,
204
225
  }),
205
- limitations: Object.freeze([
206
- 'The review uses only supplied saved Lookup documents and performs no request.',
207
- 'The newest supplied observation per domain is used for desired-state comparison; all supplied observations remain available to a separate flight-recorder review.',
208
- 'Raw RDAP, WHOIS, HTTP, TLS, and page payloads are not copied into this output.',
209
- ]),
226
+ limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
210
227
  });
211
228
  }
212
229
  export function buildControlReviewInput(manifest, lookupDocuments) {
213
230
  return Object.freeze({
214
231
  schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
215
- version: 1,
232
+ version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
216
233
  manifest,
217
234
  lookups: Object.freeze(lookupDocuments),
218
235
  });
package/cli/errors.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
2
+ const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
3
+ const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
2
4
  class CliUsageError extends Error {
3
5
  constructor(message) {
4
6
  super(message);
@@ -12,16 +14,19 @@ function errorMessage(error) {
12
14
  }
13
15
  function boundedCliErrorMessage(error, fallback = 'Unexpected command failure') {
14
16
  return String(errorMessage(error) || error || fallback)
15
- .replace(/[\x00-\x1f\x7f]+/g, ' ')
16
- .replace(/[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/g, '')
17
+ .replace(/[\x00-\x1f\x7f-\x9f]+/g, ' ')
18
+ .replace(CLI_DEFAULT_IGNORABLE_GLOBAL_RE, '')
17
19
  .replace(/\s+/g, ' ')
18
20
  .trim()
19
21
  .slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
20
22
  }
23
+ function hasUnsafeCliText(value) {
24
+ return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
25
+ }
21
26
  function boundedCliInputError(error, label) {
22
27
  if (error instanceof CliUsageError)
23
28
  return error;
24
29
  const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
25
30
  return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`);
26
31
  }
27
- export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, };
32
+ export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, hasUnsafeCliText, };
@@ -1,11 +1,10 @@
1
- import { createReadStream } from 'node:fs';
2
1
  import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, } from "./artifact-verify.mjs";
3
2
  import { formatArchiveInspection, inspectWorkspaceArchive, } from "./archive-inspect.mjs";
4
3
  import { MAX_SIGNING_KEY_FILE_BYTES, formatEvidenceSignatureVerification, signEvidencePackage, verifyEvidencePackageSignature, } from "./evidence-signing.mjs";
5
4
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
6
5
  import EXIT_CODES from "./exit-codes.mjs";
7
6
  import { formatJsonDocument } from "./formatters/json.mjs";
8
- import { readSavedLookupInputBounded, } from "./saved-lookup.mjs";
7
+ import { readCliTextInput } from "./input.mjs";
9
8
  export function isEvidenceCommand(args) {
10
9
  return args.action === 'inspect-archive'
11
10
  || args.action === 'sign-artifact'
@@ -22,11 +21,10 @@ async function readArtifact(source, label, dependencies) {
22
21
  try {
23
22
  return dependencies.readArtifactInput
24
23
  ? await dependencies.readArtifactInput(source)
25
- : await readSavedLookupInputBounded(source
26
- ? createReadStream(source, { highWaterMark: 64 * 1024 })
27
- : dependencies.stdin, {
28
- limit: MAX_OFFLINE_ARTIFACT_BYTES,
24
+ : await readCliTextInput(source, dependencies.stdin, {
25
+ maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES,
29
26
  label,
27
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
30
28
  });
31
29
  }
32
30
  catch (error) {
@@ -35,11 +33,15 @@ async function readArtifact(source, label, dependencies) {
35
33
  throw new CliUsageError(`Could not read ${label.toLowerCase()}: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
36
34
  }
37
35
  }
38
- async function readKey(source, label, injected) {
36
+ async function readKey(source, label, injected, dependencies) {
39
37
  try {
40
38
  return injected
41
39
  ? await injected(source)
42
- : await readSavedLookupInputBounded(createReadStream(source, { highWaterMark: MAX_SIGNING_KEY_FILE_BYTES }), { limit: MAX_SIGNING_KEY_FILE_BYTES, label });
40
+ : await readCliTextInput(source, dependencies.stdin, {
41
+ maximumBytes: MAX_SIGNING_KEY_FILE_BYTES,
42
+ label,
43
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
44
+ });
43
45
  }
44
46
  catch (error) {
45
47
  if (error instanceof CliUsageError)
@@ -50,7 +52,11 @@ async function readKey(source, label, injected) {
50
52
  async function readPassphrase(source, dependencies) {
51
53
  const passphraseText = dependencies.readPassphraseFile
52
54
  ? await dependencies.readPassphraseFile(source)
53
- : await readSavedLookupInputBounded(createReadStream(source, { highWaterMark: MAX_OFFLINE_PASSPHRASE_FILE_BYTES }), { limit: MAX_OFFLINE_PASSPHRASE_FILE_BYTES, label: 'Passphrase file' });
55
+ : await readCliTextInput(source, dependencies.stdin, {
56
+ maximumBytes: MAX_OFFLINE_PASSPHRASE_FILE_BYTES,
57
+ label: 'Passphrase file',
58
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
59
+ });
54
60
  const passphrase = passphraseText.replace(/\r?\n$/u, '');
55
61
  if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
56
62
  throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
@@ -85,7 +91,7 @@ export async function runEvidenceCommand(args, dependencies) {
85
91
  if (!input.trim()) {
86
92
  throw new CliUsageError('sign-artifact requires one reviewed artefact file or an artefact on stdin.');
87
93
  }
88
- const privateKey = await readKey(args.privateKeySource, 'Private key file', dependencies.readPrivateKeyFile);
94
+ const privateKey = await readKey(args.privateKeySource, 'Private key file', dependencies.readPrivateKeyFile, dependencies);
89
95
  const signed = await signEvidencePackage(input, privateKey, dependencies.now ? dependencies.now() : new Date().toISOString());
90
96
  dependencies.stdout.write(formatJsonDocument(signed));
91
97
  return EXIT_CODES.SUCCESS;
@@ -95,7 +101,7 @@ export async function runEvidenceCommand(args, dependencies) {
95
101
  throw new CliUsageError('verify-signature requires one signed package file or a package on stdin.');
96
102
  }
97
103
  const publicKey = args.publicKeySource
98
- ? await readKey(args.publicKeySource, 'Public key file', dependencies.readPublicKeyFile)
104
+ ? await readKey(args.publicKeySource, 'Public key file', dependencies.readPublicKeyFile, dependencies)
99
105
  : null;
100
106
  const report = await verifyEvidencePackageSignature(input, publicKey);
101
107
  if (!args.quiet) {