@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

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 (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,13 +1,13 @@
1
1
  // WHOIS response parsing. Transport, referral-chain and stable public exports
2
2
  // remain owned by the facade in whois.mts.
3
3
  import { registryDateIso } from "./registry-dates.mjs";
4
- import { analyzeWhoisChainAuthority } from "./whois-authority.mjs";
4
+ import { analyzeWhoisChainAuthority, whoisResponseMatchesDomain } from "./whois-authority.mjs";
5
5
  import { normalizeWhoisChain } from "./whois-normalization.mjs";
6
6
  import { applyWhoisCommonFormats } from "./whois-common-formats.mjs";
7
7
  import { applyWhoisDialects } from "./whois-dialects.mjs";
8
8
  import { WHOIS_FIELD_PATTERNS, normalizedWhoisContact, } from "./whois-parser-support.mjs";
9
9
  import { boundedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
10
- function parseWhoisChain(chain) {
10
+ function parseWhoisChain(chain, requestedDomain) {
11
11
  const source = normalizeWhoisChain(chain);
12
12
  const fields = {};
13
13
  const truncatedFields = new Set();
@@ -18,6 +18,8 @@ function parseWhoisChain(chain) {
18
18
  const text = hop.response;
19
19
  if (!text)
20
20
  return;
21
+ if (hopIndex > 0 && requestedDomain !== undefined && !whoisResponseMatchesDomain(text, requestedDomain))
22
+ return;
21
23
  // hopIndex 0 is always whois.iana.org, whose "domain:"/"created:"/
22
24
  // "changed:" fields describe the TLD's own root delegation record, not
23
25
  // the queried domain - e.g. "created: 1992-08-14" for .gt is Guatemala's
@@ -118,7 +120,7 @@ function parseWhoisChain(chain) {
118
120
  // Existence is decided authority-aware, not by a global "any hop said no
119
121
  // match" flag: positive registry evidence is never overridden by a later
120
122
  // registrar hop that failed, rate-limited, or returned "no match".
121
- const authority = analyzeWhoisChainAuthority(source);
123
+ const authority = analyzeWhoisChainAuthority(source, requestedDomain);
122
124
  return {
123
125
  ...fields,
124
126
  nameservers: [...nameservers],
@@ -16,38 +16,45 @@ function errorMessage(value, fallback) {
16
16
  // A referral hostname is upstream-controlled, so resolve and validate its
17
17
  // addresses before connecting. Connect to the selected address directly to
18
18
  // prevent a second DNS lookup from bypassing the public-address decision.
19
- function queryWhoisAddress(address, server, query, { port = 43, timeoutMs = 10000, totalDeadlineMs = WHOIS_HOP_DEADLINE_MS, createConnection = net.createConnection, } = {}) {
19
+ function queryWhoisAddress(address, server, query, { port = 43, timeoutMs = 10000, totalDeadlineMs = WHOIS_HOP_DEADLINE_MS, createConnection = net.createConnection, signal, } = {}) {
20
20
  return new Promise((resolve, reject) => {
21
+ signal?.throwIfAborted();
21
22
  const socket = createConnection({ host: address, port }, () => {
22
- socket.write(query + '\r\n');
23
+ if (!signal?.aborted)
24
+ socket.write(query + '\r\n');
23
25
  });
24
26
  const chunks = [];
25
27
  let totalBytes = 0;
26
28
  let settled = false;
27
29
  const deadline = setTimeout(() => {
28
- settled = true;
30
+ settle(reject, new Error(`WHOIS request to ${server} exceeded the total time limit`));
29
31
  socket.destroy();
30
- reject(new Error(`WHOIS request to ${server} exceeded the total time limit`));
31
32
  }, totalDeadlineMs);
32
33
  function settle(fn, value) {
33
34
  if (settled)
34
35
  return;
35
36
  settled = true;
36
37
  clearTimeout(deadline);
38
+ signal?.removeEventListener('abort', abort);
37
39
  fn(value);
38
40
  }
41
+ const abort = () => { settle(reject, signal.reason); socket.destroy(); };
39
42
  socket.setTimeout(Math.min(timeoutMs, totalDeadlineMs));
40
43
  socket.on('data', (chunk) => {
44
+ if (settled)
45
+ return;
41
46
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
42
47
  totalBytes += buffer.length;
43
48
  if (totalBytes > MAX_WHOIS_BYTES) {
44
- socket.destroy();
45
49
  settle(reject, new Error(`WHOIS response from ${server} exceeded ${MAX_WHOIS_BYTES} bytes`));
50
+ socket.destroy();
46
51
  return;
47
52
  }
48
53
  chunks.push(buffer);
49
54
  });
50
55
  const settleResponse = () => {
56
+ if (settled)
57
+ return;
51
58
  try {
52
59
  const responseText = new TextDecoder('utf-8', { fatal: true })
53
60
  .decode(Buffer.concat(chunks, totalBytes));
@@ -60,13 +67,17 @@ function queryWhoisAddress(address, server, query, { port = 43, timeoutMs = 1000
60
67
  socket.on('end', settleResponse);
61
68
  socket.on('close', settleResponse);
62
69
  socket.on('timeout', () => {
63
- socket.destroy();
64
70
  settle(reject, new Error(`WHOIS request to ${server} timed out`));
71
+ socket.destroy();
65
72
  });
66
73
  socket.on('error', (error) => settle(reject, error));
74
+ signal?.addEventListener('abort', abort, { once: true });
75
+ if (signal?.aborted)
76
+ abort();
67
77
  });
68
78
  }
69
- async function whoisQuery(server, query, { port = 43, timeoutMs = 10000, totalDeadlineMs = WHOIS_HOP_DEADLINE_MS, resolveAddresses = resolvePublicAddresses, queryAddress = queryWhoisAddress, now = Date.now, onAddressSelected = (_address) => { }, } = {}) {
79
+ async function whoisQuery(server, query, { port = 43, timeoutMs = 10000, totalDeadlineMs = WHOIS_HOP_DEADLINE_MS, resolveAddresses = resolvePublicAddresses, queryAddress = queryWhoisAddress, now = Date.now, onAddressSelected = (_address) => { }, signal, } = {}) {
80
+ signal?.throwIfAborted();
70
81
  const startedAt = now();
71
82
  let resolutionTimer;
72
83
  const records = await Promise.race([
@@ -75,10 +86,12 @@ async function whoisQuery(server, query, { port = 43, timeoutMs = 10000, totalDe
75
86
  resolutionTimer = setTimeout(() => reject(new Error(`WHOIS request to ${server} timed out during DNS resolution`)), totalDeadlineMs);
76
87
  }),
77
88
  ]).finally(() => clearTimeout(resolutionTimer));
89
+ signal?.throwIfAborted();
78
90
  const candidates = records.slice(0, MAX_WHOIS_ADDRESSES);
79
91
  const failures = [];
80
92
  let attempts = 0;
81
93
  for (const { address } of candidates) {
94
+ signal?.throwIfAborted();
82
95
  const remainingMs = totalDeadlineMs - (now() - startedAt);
83
96
  if (remainingMs <= 0)
84
97
  break;
@@ -88,7 +101,9 @@ async function whoisQuery(server, query, { port = 43, timeoutMs = 10000, totalDe
88
101
  port,
89
102
  timeoutMs: Math.min(timeoutMs, remainingMs),
90
103
  totalDeadlineMs: remainingMs,
104
+ ...(signal ? { signal } : {}),
91
105
  });
106
+ signal?.throwIfAborted();
92
107
  try {
93
108
  onAddressSelected(address);
94
109
  }
@@ -98,6 +113,7 @@ async function whoisQuery(server, query, { port = 43, timeoutMs = 10000, totalDe
98
113
  return response;
99
114
  }
100
115
  catch (error) {
116
+ signal?.throwIfAborted();
101
117
  failures.push(errorMessage(error, 'connection failed').slice(0, 200));
102
118
  }
103
119
  }
package/package.json CHANGED
@@ -33,37 +33,37 @@
33
33
  "bugs": {
34
34
  "url": "https://github.com/slicedearth/whoisleuth/issues"
35
35
  },
36
- "version": "2.3.0",
36
+ "version": "2.5.1",
37
37
  "dependencies": {
38
- "@peculiar/x509": "2.0.0",
38
+ "@nuintun/qrcode": "5.0.3",
39
+ "@peculiar/x509": "2.1.0",
40
+ "fast-png": "8.0.0",
41
+ "fflate": "0.8.3",
39
42
  "maxmind": "5.0.7",
40
43
  "parse5": "8.0.1",
44
+ "pdfjs-dist": "6.3.289",
45
+ "postal-mime": "3.0.0",
41
46
  "reflect-metadata": "0.2.2",
42
- "tldts": "7.4.11",
43
- "undici": "8.10.1"
47
+ "saxes": "6.0.0",
48
+ "tldts": "7.4.14",
49
+ "undici": "8.11.0"
44
50
  },
45
51
  "files": [
46
52
  "bin/**/*.mjs",
47
53
  "cli/**/*.mjs",
48
54
  "lib/**/*.mjs",
49
55
  "frontend/src/lib/**/*.js",
50
- "packages/cases/**/*.mjs",
51
- "packages/comparison/**/*.mjs",
52
- "packages/contracts/**/*.mjs",
53
- "packages/evidence/**/*.mjs",
54
- "packages/interchange/**/*.mjs",
55
- "packages/investigation/**/*.mjs",
56
- "packages/monitoring/**/*.mjs",
57
- "packages/workspace/**/*.mjs",
56
+ "packages/**/*.mjs",
57
+ "README.md",
58
58
  "docs/cli.md",
59
59
  "docs/cli-reference.md",
60
+ "docs/contextual-reviews.md",
60
61
  "DISCLOSURE",
61
62
  "LICENSE",
62
- "LICENSES/*.txt",
63
63
  "NOTICE",
64
- "README.md",
65
64
  "SECURITY.md",
66
65
  "TRADEMARKS.md",
66
+ "LICENSES/Retire.js-Apache-2.0.txt",
67
67
  "third-party-notices.txt"
68
68
  ],
69
69
  "publishConfig": {
@@ -0,0 +1,40 @@
1
+ // Deliberately small interoperability projection over reviewed MISP
2
+ // taxonomies. WHOISleuth retains its own bounded workflow vocabulary and
3
+ // derives machine tags only when an analyst-owned disposition and reason make
4
+ // the mapping unambiguous. These tags never alter collection or scoring.
5
+ const ANALYST_REVIEW_REASONS = Object.freeze([
6
+ Object.freeze({ value: '', label: 'Not recorded' }),
7
+ Object.freeze({ value: 'authorized_or_owned', label: 'Authorised or owned domain' }),
8
+ Object.freeze({ value: 'shared_infrastructure', label: 'Shared infrastructure explained the signal' }),
9
+ Object.freeze({ value: 'generic_platform_or_template', label: 'Generic platform or template' }),
10
+ Object.freeze({ value: 'parked_or_reseller', label: 'Parking or reseller page' }),
11
+ Object.freeze({ value: 'insufficient_evidence', label: 'Insufficient evidence' }),
12
+ Object.freeze({ value: 'legitimate_third_party', label: 'Legitimate third party' }),
13
+ Object.freeze({ value: 'confirmed_credential_abuse', label: 'Confirmed credential abuse' }),
14
+ Object.freeze({ value: 'confirmed_malware', label: 'Confirmed malware' }),
15
+ Object.freeze({ value: 'other_reviewed', label: 'Other reviewed reason' }),
16
+ ]);
17
+ const ANALYST_REVIEW_REASON_VALUES = new Set(ANALYST_REVIEW_REASONS.map(({ value }) => value).filter(Boolean));
18
+ const MISP_TAXONOMY_REFERENCES = Object.freeze([
19
+ Object.freeze({ namespace: 'domain-abuse', version: 2, sourceSha256: '5fcfb44742daac97061726faaaadd2ce6b7eac55306acaae3d30c3882d18f991', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/domain-abuse/machinetag.json' }),
20
+ Object.freeze({ namespace: 'estimative-language', version: 5, sourceSha256: '1010927d396f66f7cc58a229ebb8af6ad2cdc0250ad5f8ce3b1bd9aacf5ab4da', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/estimative-language/machinetag.json' }),
21
+ Object.freeze({ namespace: 'event-assessment', version: 2, sourceSha256: '39291bdd50e1b2975a93e33e396f42b398178015580a84402bc3d8d549dd721e', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/event-assessment/machinetag.json' }),
22
+ Object.freeze({ namespace: 'false-positive', version: 7, sourceSha256: 'ffc8d35ae36eb1d39f33611d1090793021f2bfe3b4b31ae41631e22cf544a2fb', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/false-positive/machinetag.json' }),
23
+ Object.freeze({ namespace: 'phishing', version: 5, sourceSha256: '8628ebe4d5935216a7b006a4d975669c4ae1a554ef8ed78ffaadd62f763d7263', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/phishing/machinetag.json' }),
24
+ ]);
25
+ function analystInteroperabilityTags(disposition, reviewReasonCode) {
26
+ const tags = new Set();
27
+ if (disposition === 'false_positive')
28
+ tags.add('false-positive:confirmed="true"');
29
+ if ((disposition === 'false_positive' || disposition === 'expected')
30
+ && ['authorized_or_owned', 'shared_infrastructure', 'generic_platform_or_template', 'parked_or_reseller', 'legitimate_third_party'].includes(String(reviewReasonCode)))
31
+ tags.add('false-positive:risk="high"');
32
+ if (reviewReasonCode === 'insufficient_evidence') {
33
+ tags.add('estimative-language:confidence-in-analytic-judgment="low"');
34
+ }
35
+ if (disposition === 'confirmed_abuse' && reviewReasonCode === 'confirmed_credential_abuse') {
36
+ tags.add('phishing:techniques="fake-website"');
37
+ }
38
+ return [...tags].sort();
39
+ }
40
+ export { ANALYST_REVIEW_REASONS, ANALYST_REVIEW_REASON_VALUES, MISP_TAXONOMY_REFERENCES, analystInteroperabilityTags, };
@@ -0,0 +1,330 @@
1
+ export const MAX_BOUNDED_JSON_DEPTH = 48;
2
+ export const MAX_BOUNDED_JSON_KEYS = 50_000;
3
+ export const MAX_BOUNDED_JSON_VALUES = 100_000;
4
+ export const MAX_BOUNDED_JSON_CONTAINER_ITEMS = 10_000;
5
+ export const UNSAFE_JSON_OBJECT_KEYS = Object.freeze(['__proto__']);
6
+ /** Aggregate work allowed by a separately enforced UTF-8 byte budget. */
7
+ export function boundedJsonLimitsForBytes(maximumBytes) {
8
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1) {
9
+ throw new TypeError('The JSON byte budget must be a positive safe integer.');
10
+ }
11
+ // A member needs at least five bytes (comma, two quotes, colon, value).
12
+ // Values need at least two bytes each, apart from a scalar root or the last
13
+ // array item. These upper bounds do not depend on a model's current fields.
14
+ // Nesting and individual-container limits remain independently bounded.
15
+ return {
16
+ maximumKeys: Math.floor(maximumBytes / 5),
17
+ maximumValues: Math.ceil(maximumBytes / 2),
18
+ // Decoded text is a lower bound on its UTF-8 JSON representation. Checking
19
+ // it before stringify also prevents an oversized string allocation.
20
+ maximumStringCodeUnits: maximumBytes,
21
+ };
22
+ }
23
+ function syntaxError() {
24
+ throw new TypeError('Artefact input is not valid JSON.');
25
+ }
26
+ export function isSafeJsonObjectKey(value) {
27
+ return !UNSAFE_JSON_OBJECT_KEYS.includes(value);
28
+ }
29
+ export function assertBoundedJsonStructure(value, label = 'JSON value', limits = {}) {
30
+ const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
31
+ const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
32
+ const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
33
+ const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
34
+ let keys = 0;
35
+ let values = 0;
36
+ let stringCodeUnits = 0;
37
+ const ancestors = new Set();
38
+ const text = (value) => {
39
+ stringCodeUnits += value.length;
40
+ if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
41
+ throw new TypeError(`${label} exceeds the aggregate text limit.`);
42
+ }
43
+ };
44
+ const visit = (current, depth) => {
45
+ values += 1;
46
+ if (values > maximumValues) {
47
+ throw new TypeError(`${label} exceeds the ${maximumValues}-value limit.`);
48
+ }
49
+ if (depth > maximumDepth) {
50
+ throw new TypeError(`${label} exceeds the ${maximumDepth}-level nesting limit.`);
51
+ }
52
+ if (typeof current === 'string') {
53
+ text(current);
54
+ return;
55
+ }
56
+ if (current === null || typeof current === 'boolean')
57
+ return;
58
+ if (typeof current === 'number') {
59
+ if (!Number.isFinite(current))
60
+ throw new TypeError(`${label} contains a non-JSON number.`);
61
+ return;
62
+ }
63
+ if (typeof current !== 'object')
64
+ throw new TypeError(`${label} contains a non-JSON value.`);
65
+ if (ancestors.has(current))
66
+ throw new TypeError(`${label} contains a cyclic object reference.`);
67
+ let prototype;
68
+ let ownKeys;
69
+ let descriptors;
70
+ try {
71
+ prototype = Object.getPrototypeOf(current);
72
+ ownKeys = Reflect.ownKeys(current);
73
+ descriptors = Object.getOwnPropertyDescriptors(current);
74
+ }
75
+ catch {
76
+ throw new TypeError(`${label} could not be inspected safely.`);
77
+ }
78
+ if (ownKeys.some((key) => typeof key !== 'string')) {
79
+ throw new TypeError(`${label} contains a symbol key.`);
80
+ }
81
+ if (ownKeys.some((key) => {
82
+ const descriptor = descriptors[String(key)];
83
+ return !descriptor || !Object.hasOwn(descriptor, 'value');
84
+ })) {
85
+ throw new TypeError(`${label} contains an accessor property.`);
86
+ }
87
+ ancestors.add(current);
88
+ if (Array.isArray(current)) {
89
+ if (prototype !== Array.prototype) {
90
+ throw new TypeError(`${label} contains an array with a custom prototype.`);
91
+ }
92
+ const length = descriptors.length?.value;
93
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximumContainerItems) {
94
+ throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
95
+ }
96
+ const indexes = ownKeys.filter((key) => key !== 'length');
97
+ if (indexes.length !== length
98
+ || indexes.some((key, index) => key !== String(index) || descriptors[key]?.enumerable !== true)) {
99
+ throw new TypeError(`${label} contains a sparse, extended, or non-enumerable array.`);
100
+ }
101
+ for (const key of indexes)
102
+ visit(descriptors[key].value, depth + 1);
103
+ }
104
+ else {
105
+ if (prototype !== Object.prototype && prototype !== null) {
106
+ throw new TypeError(`${label} contains an object with a custom prototype.`);
107
+ }
108
+ const objectKeys = ownKeys;
109
+ if (objectKeys.length > maximumContainerItems) {
110
+ throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
111
+ }
112
+ if (objectKeys.some((key) => descriptors[key]?.enumerable !== true)) {
113
+ throw new TypeError(`${label} contains a non-enumerable object property.`);
114
+ }
115
+ keys += objectKeys.length;
116
+ if (keys > maximumKeys) {
117
+ throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
118
+ }
119
+ for (const key of objectKeys) {
120
+ if (!isSafeJsonObjectKey(key))
121
+ throw new TypeError(`${label} contains an unsafe object key.`);
122
+ text(key);
123
+ visit(descriptors[key].value, depth + 1);
124
+ }
125
+ }
126
+ ancestors.delete(current);
127
+ };
128
+ visit(value, 0);
129
+ }
130
+ export function scanBoundedJson(raw, limits = {}, observeString) {
131
+ const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
132
+ const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
133
+ const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
134
+ const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
135
+ let index = 0;
136
+ let keys = 0;
137
+ let values = 0;
138
+ let stringCodeUnits = 0;
139
+ const whitespace = () => {
140
+ while (index < raw.length) {
141
+ const code = raw.charCodeAt(index);
142
+ if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
143
+ break;
144
+ index += 1;
145
+ }
146
+ };
147
+ const stringToken = () => {
148
+ const start = index;
149
+ index += 1;
150
+ while (index < raw.length) {
151
+ const character = raw[index];
152
+ if (character === '"') {
153
+ index += 1;
154
+ let value;
155
+ try {
156
+ value = JSON.parse(raw.slice(start, index));
157
+ }
158
+ catch {
159
+ syntaxError();
160
+ }
161
+ stringCodeUnits += value.length;
162
+ if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
163
+ throw new TypeError('Artefact JSON exceeds the aggregate text limit.');
164
+ }
165
+ return { value, offset: start };
166
+ }
167
+ if (character === '\\') {
168
+ index += 1;
169
+ if (index >= raw.length)
170
+ syntaxError();
171
+ if (raw[index] === 'u') {
172
+ if (!/^[a-fA-F0-9]{4}$/u.test(raw.slice(index + 1, index + 5)))
173
+ syntaxError();
174
+ index += 5;
175
+ }
176
+ else {
177
+ if (!/["\\/bfnrt]/u.test(raw[index]))
178
+ syntaxError();
179
+ index += 1;
180
+ }
181
+ continue;
182
+ }
183
+ if (character.charCodeAt(0) <= 0x1f)
184
+ syntaxError();
185
+ index += 1;
186
+ }
187
+ return syntaxError();
188
+ };
189
+ const literal = (token) => {
190
+ if (raw.slice(index, index + token.length) !== token)
191
+ syntaxError();
192
+ index += token.length;
193
+ };
194
+ const number = () => {
195
+ const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(raw.slice(index));
196
+ if (!match)
197
+ syntaxError();
198
+ if (!Number.isFinite(Number(match[0]))) {
199
+ throw new TypeError('Artefact JSON contains a non-finite number.');
200
+ }
201
+ index += match[0].length;
202
+ };
203
+ const value = (depth, propertyKey) => {
204
+ values += 1;
205
+ if (values > maximumValues) {
206
+ throw new TypeError(`Artefact JSON exceeds the ${maximumValues}-value limit.`);
207
+ }
208
+ if (depth > maximumDepth) {
209
+ throw new TypeError(`Artefact JSON exceeds the ${maximumDepth}-level nesting limit.`);
210
+ }
211
+ whitespace();
212
+ const character = raw[index];
213
+ if (character === '"') {
214
+ const parsedString = stringToken();
215
+ observeString?.({ ...parsedString, kind: 'value', ...(propertyKey === undefined ? {} : { propertyKey }) });
216
+ return;
217
+ }
218
+ if (character === '{') {
219
+ index += 1;
220
+ whitespace();
221
+ const seen = new Set();
222
+ if (raw[index] === '}') {
223
+ index += 1;
224
+ return;
225
+ }
226
+ while (index < raw.length) {
227
+ if (raw[index] !== '"')
228
+ syntaxError();
229
+ const parsedKey = stringToken();
230
+ const key = parsedKey.value;
231
+ if (seen.size >= maximumContainerItems) {
232
+ throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
233
+ }
234
+ keys += 1;
235
+ if (keys > maximumKeys) {
236
+ throw new TypeError(`Artefact JSON exceeds the ${maximumKeys}-key limit.`);
237
+ }
238
+ if (seen.has(key))
239
+ throw new TypeError('Artefact JSON contains a duplicate object key.');
240
+ if (!isSafeJsonObjectKey(key))
241
+ throw new TypeError('Artefact JSON contains an unsafe object key.');
242
+ seen.add(key);
243
+ observeString?.({ ...parsedKey, kind: 'key' });
244
+ whitespace();
245
+ if (raw[index] !== ':')
246
+ syntaxError();
247
+ index += 1;
248
+ value(depth + 1, key);
249
+ whitespace();
250
+ if (raw[index] === '}') {
251
+ index += 1;
252
+ return;
253
+ }
254
+ if (raw[index] !== ',')
255
+ syntaxError();
256
+ index += 1;
257
+ whitespace();
258
+ }
259
+ syntaxError();
260
+ }
261
+ if (character === '[') {
262
+ index += 1;
263
+ whitespace();
264
+ if (raw[index] === ']') {
265
+ index += 1;
266
+ return;
267
+ }
268
+ let items = 0;
269
+ while (index < raw.length) {
270
+ items += 1;
271
+ if (items > maximumContainerItems) {
272
+ throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
273
+ }
274
+ value(depth + 1);
275
+ whitespace();
276
+ if (raw[index] === ']') {
277
+ index += 1;
278
+ return;
279
+ }
280
+ if (raw[index] !== ',')
281
+ syntaxError();
282
+ index += 1;
283
+ whitespace();
284
+ }
285
+ syntaxError();
286
+ }
287
+ if (character === 't') {
288
+ literal('true');
289
+ return;
290
+ }
291
+ if (character === 'f') {
292
+ literal('false');
293
+ return;
294
+ }
295
+ if (character === 'n') {
296
+ literal('null');
297
+ return;
298
+ }
299
+ number();
300
+ };
301
+ whitespace();
302
+ value(0);
303
+ whitespace();
304
+ if (index !== raw.length)
305
+ syntaxError();
306
+ }
307
+ export function parseBoundedJson(raw, options) {
308
+ const label = options.label ?? 'Artefact input';
309
+ if (typeof raw !== 'string')
310
+ throw new TypeError(`${label} must be UTF-8 JSON text.`);
311
+ const bytes = new TextEncoder().encode(raw).byteLength;
312
+ if (bytes < 1 || bytes > options.maximumBytes) {
313
+ throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
314
+ }
315
+ scanBoundedJson(raw, options.limits);
316
+ try {
317
+ return JSON.parse(raw);
318
+ }
319
+ catch {
320
+ return syntaxError();
321
+ }
322
+ }
323
+ export function parseBoundedJsonObject(raw, options) {
324
+ const label = options.label ?? 'Artefact input';
325
+ const parsed = parseBoundedJson(raw, options);
326
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
327
+ throw new TypeError(`${label} must contain one JSON object.`);
328
+ }
329
+ return parsed;
330
+ }
@@ -0,0 +1,4 @@
1
+ // Browser-safe bounds shared by candidate discovery responses and the
2
+ // Discover-to-Bulk handoff. These are provenance labels, not domain names;
3
+ // they remain bounded independently of the target-domain contract.
4
+ export const MAX_CANDIDATE_SOURCE_LENGTH = 253;