@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
@@ -0,0 +1,338 @@
1
+ // Independent observed effects, linked closures and lifecycle summaries.
2
+ import { MAX_CASE_CLOSURES, MAX_CASE_OBSERVED_EFFECT_REVIEWS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, } from "../contracts/case-portability.mjs";
3
+ import { latestObservationCohort, } from "../evidence/latest-observations.mjs";
4
+ import { readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
5
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
6
+ import { CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, } from "./case-response-records.mjs";
7
+ import { COMPLETENESS, SAFE_ID_RE, boundedCounter, compareCodeUnits, freshId, iso, lifecycleLimitations, limitations, optionalIso, record, safeId, text, } from "./case-response-values.mjs";
8
+ const OBSERVED_EFFECT_STATES = new Set(CASE_OBSERVED_EFFECT_STATES);
9
+ const OBSERVED_EFFECT_SOURCE_CLASSES = new Set(CASE_OBSERVED_EFFECT_SOURCE_CLASSES);
10
+ const CLOSURE_REASONS = new Set(CASE_CLOSURE_REASONS);
11
+ function normalizeObservedEffectReview(raw, fallback, validPinIds, validSightingIds, options = {}) {
12
+ const item = record(raw);
13
+ if (typeof item.state !== 'string' || !OBSERVED_EFFECT_STATES.has(item.state))
14
+ return null;
15
+ if (typeof item.sourceClass !== 'string' || !OBSERVED_EFFECT_SOURCE_CLASSES.has(item.sourceClass))
16
+ return null;
17
+ const source = text(item.source, MAX_RESPONSE_LABEL_LENGTH);
18
+ const observedAt = optionalIso(item.observedAt, options);
19
+ if (!source || !observedAt)
20
+ return null;
21
+ const createdAt = iso(item.createdAt, observedAt || fallback, options);
22
+ const recheck = readCaseRecheckAnswerContext(item.recheck, options.sourceVersion);
23
+ if (recheck && item.state === 'not_reproduced' && (item.completeness !== 'complete' || recheck.conditionsMatch !== 'comparable')) {
24
+ throw new TypeError('A question cannot be marked not reproduced from incomplete evidence or unconfirmed comparison conditions.');
25
+ }
26
+ const evidencePinId = typeof item.evidencePinId === 'string'
27
+ && SAFE_ID_RE.test(item.evidencePinId)
28
+ && (!validPinIds || validPinIds.has(item.evidencePinId))
29
+ ? item.evidencePinId
30
+ : null;
31
+ const sightingId = typeof item.sightingId === 'string'
32
+ && SAFE_ID_RE.test(item.sightingId)
33
+ && (!validSightingIds || validSightingIds.has(item.sightingId))
34
+ ? item.sightingId
35
+ : null;
36
+ const linkLimitations = [
37
+ ...(item.evidencePinId != null && !evidencePinId ? ['A malformed or dangling evidence-pin reference was omitted from this independent review.'] : []),
38
+ ...(item.sightingId != null && !sightingId ? ['A malformed or dangling sighting reference was omitted from this independent review.'] : []),
39
+ ];
40
+ return {
41
+ id: safeId(item.id, 'effect-review', { state: item.state, source, observedAt, createdAt }),
42
+ state: item.state,
43
+ observedAt,
44
+ sourceClass: item.sourceClass,
45
+ source,
46
+ completeness: typeof item.completeness === 'string' && COMPLETENESS.has(item.completeness)
47
+ ? item.completeness
48
+ : 'unknown',
49
+ limitations: limitations([...linkLimitations, ...limitations(item.limitations)]),
50
+ evidencePinId,
51
+ sightingId,
52
+ followUpAt: optionalIso(item.followUpAt, options),
53
+ ...(recheck ? { recheck } : {}),
54
+ createdAt,
55
+ };
56
+ }
57
+ export function normalizeCaseObservedEffectHistory(raw, fallback, validPinIds, validSightingIds, options = {}) {
58
+ const root = record(raw);
59
+ const source = Array.isArray(raw) ? raw : Array.isArray(root.reviews) ? root.reviews : [];
60
+ const byId = new Map();
61
+ let invalid = 0;
62
+ let duplicateConflict = 0;
63
+ for (const item of source.slice(0, MAX_CASE_OBSERVED_EFFECT_REVIEWS * 4)) {
64
+ const normalized = normalizeObservedEffectReview(item, fallback, validPinIds, validSightingIds, options);
65
+ if (!normalized) {
66
+ invalid += 1;
67
+ continue;
68
+ }
69
+ const existing = byId.get(normalized.id);
70
+ if (!existing)
71
+ byId.set(normalized.id, normalized);
72
+ else if (JSON.stringify(normalized) !== JSON.stringify(existing)) {
73
+ duplicateConflict += 1;
74
+ if (JSON.stringify(normalized) < JSON.stringify(existing))
75
+ byId.set(normalized.id, normalized);
76
+ }
77
+ }
78
+ const all = [...byId.values()].sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt) || compareCodeUnits(left.id, right.id));
79
+ invalid += Math.max(0, source.length - MAX_CASE_OBSERVED_EFFECT_REVIEWS * 4);
80
+ const omitted = boundedCounter(root.omitted)
81
+ + invalid
82
+ + duplicateConflict
83
+ + Math.max(0, all.length - MAX_CASE_OBSERVED_EFFECT_REVIEWS);
84
+ const preV13HistoryUnavailable = options.sourceVersion != null && options.sourceVersion <= 12
85
+ ? true
86
+ : root.preV13HistoryUnavailable === true;
87
+ const retainedLimitations = limitations(root.limitations).filter((item) => !/^\d+ earlier observed-effect reviews? omitted by bounded retention\.$/u.test(item));
88
+ return {
89
+ reviews: all.slice(-MAX_CASE_OBSERVED_EFFECT_REVIEWS),
90
+ omitted,
91
+ preV13HistoryUnavailable,
92
+ limitations: lifecycleLimitations([
93
+ ...(omitted ? [`${omitted} earlier observed-effect review${omitted === 1 ? '' : 's'} omitted by bounded retention.`] : []),
94
+ ...(invalid ? [`${invalid} malformed or unlinked observed-effect review${invalid === 1 ? '' : 's'} omitted during normalisation.`] : []),
95
+ ...(duplicateConflict ? [`${duplicateConflict} conflicting observed-effect review identit${duplicateConflict === 1 ? 'y was' : 'ies were'} reconciled deterministically.`] : []),
96
+ ...(preV13HistoryUnavailable ? ['Migrated from a pre-v13 Case; earlier independent observed-effect review history is unavailable.'] : []),
97
+ 'Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.',
98
+ ...retainedLimitations,
99
+ ]),
100
+ };
101
+ }
102
+ export function appendCaseObservedEffectReview(current, raw, now, validPinIds, validSightingIds) {
103
+ const item = record(raw);
104
+ const created = normalizeObservedEffectReview({ ...item, id: freshId('effect-review'), createdAt: now }, now, validPinIds, validSightingIds);
105
+ if (!created)
106
+ throw new Error('An independent observed-effect review requires a valid state, time, source class, and source.');
107
+ return normalizeCaseObservedEffectHistory({
108
+ reviews: [...current.reviews, created],
109
+ omitted: current.omitted,
110
+ preV13HistoryUnavailable: current.preV13HistoryUnavailable,
111
+ limitations: current.limitations,
112
+ }, now, validPinIds, validSightingIds);
113
+ }
114
+ export function mergeCaseObservedEffectHistories(local, imported, fallback, validPinIds, validSightingIds) {
115
+ return normalizeCaseObservedEffectHistory({
116
+ reviews: [...local.reviews, ...imported.reviews],
117
+ omitted: Math.max(local.omitted, imported.omitted),
118
+ preV13HistoryUnavailable: local.preV13HistoryUnavailable || imported.preV13HistoryUnavailable,
119
+ limitations: [...local.limitations, ...imported.limitations],
120
+ }, fallback, validPinIds, validSightingIds);
121
+ }
122
+ function normalizeClosure(raw, fallback, validReviewIds, validActionIds, options = {}, linkContext = {}) {
123
+ const item = record(raw);
124
+ if (typeof item.reason !== 'string' || !CLOSURE_REASONS.has(item.reason))
125
+ return null;
126
+ const reason = item.reason;
127
+ const summary = text(item.summary, MAX_RESPONSE_RATIONALE_LENGTH);
128
+ if (!summary)
129
+ return null;
130
+ const createdAt = iso(item.createdAt, fallback, options);
131
+ const observedEffectReviewId = typeof item.observedEffectReviewId === 'string'
132
+ && SAFE_ID_RE.test(item.observedEffectReviewId)
133
+ && (!validReviewIds || validReviewIds.has(item.observedEffectReviewId))
134
+ ? item.observedEffectReviewId
135
+ : null;
136
+ const actionId = typeof item.actionId === 'string'
137
+ && SAFE_ID_RE.test(item.actionId)
138
+ && (!validActionIds || validActionIds.has(item.actionId))
139
+ ? item.actionId
140
+ : null;
141
+ const linkedReview = observedEffectReviewId ? linkContext.reviewEvidence?.get(observedEffectReviewId) : undefined;
142
+ const reviewPredatesClosure = linkedReview !== undefined
143
+ && Date.parse(linkedReview.observedAt) <= Date.parse(createdAt)
144
+ && Date.parse(linkedReview.createdAt) <= Date.parse(createdAt);
145
+ if (linkContext.reviewEvidence && reason === 'independently_not_reproduced'
146
+ && (!reviewPredatesClosure || linkedReview?.state !== 'not_reproduced'))
147
+ return null;
148
+ if (linkContext.reviewEvidence && reason === 'infrastructure_changed'
149
+ && (!reviewPredatesClosure || linkedReview?.state !== 'changed'))
150
+ return null;
151
+ const linkedProviderEvents = actionId ? linkContext.providerResolutionEvents?.get(actionId) ?? [] : [];
152
+ if (linkContext.providerResolutionEvents && reason === 'provider_reported_resolution_not_independently_checked'
153
+ && !linkedProviderEvents.some((event) => Date.parse(event.occurredAt) <= Date.parse(createdAt)))
154
+ return null;
155
+ const linkLimitations = [
156
+ ...(item.observedEffectReviewId != null && !observedEffectReviewId ? ['A malformed or dangling observed-effect review reference was omitted from this closure.'] : []),
157
+ ...(item.actionId != null && !actionId ? ['A malformed or dangling response-action reference was omitted from this closure.'] : []),
158
+ ];
159
+ return {
160
+ id: safeId(item.id, 'case-closure', { reason: item.reason, summary, createdAt }),
161
+ reason,
162
+ summary,
163
+ observedEffectReviewId,
164
+ actionId,
165
+ limitations: limitations([...linkLimitations, ...limitations(item.limitations)]),
166
+ createdAt,
167
+ };
168
+ }
169
+ export function buildCaseClosureLinkContext(observedEffects, actions) {
170
+ return {
171
+ reviewEvidence: new Map(observedEffects.reviews.map((review) => [review.id, {
172
+ state: review.state,
173
+ observedAt: review.observedAt,
174
+ createdAt: review.createdAt,
175
+ }])),
176
+ providerResolutionEvents: new Map(actions.map((action) => [action.id, action.history
177
+ .filter((event) => event.applied && event.providerOutcome === 'provider_reports_resolved')
178
+ .map((event) => ({ eventId: event.id, occurredAt: event.occurredAt }))])),
179
+ };
180
+ }
181
+ export function normalizeCaseClosureHistory(raw, fallback, validReviewIds, validActionIds, options = {}, linkContext = {}) {
182
+ const root = record(raw);
183
+ const source = Array.isArray(raw) ? raw : Array.isArray(root.records) ? root.records : [];
184
+ const byId = new Map();
185
+ let invalid = 0;
186
+ let duplicateConflict = 0;
187
+ for (const item of source.slice(0, MAX_CASE_CLOSURES * 4)) {
188
+ const normalized = normalizeClosure(item, fallback, validReviewIds, validActionIds, options, linkContext);
189
+ if (!normalized) {
190
+ invalid += 1;
191
+ continue;
192
+ }
193
+ const existing = byId.get(normalized.id);
194
+ if (!existing)
195
+ byId.set(normalized.id, normalized);
196
+ else if (JSON.stringify(normalized) !== JSON.stringify(existing)) {
197
+ duplicateConflict += 1;
198
+ if (JSON.stringify(normalized) < JSON.stringify(existing))
199
+ byId.set(normalized.id, normalized);
200
+ }
201
+ }
202
+ const all = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt) || compareCodeUnits(left.id, right.id));
203
+ invalid += Math.max(0, source.length - MAX_CASE_CLOSURES * 4);
204
+ const omitted = boundedCounter(root.omitted)
205
+ + invalid
206
+ + duplicateConflict
207
+ + Math.max(0, all.length - MAX_CASE_CLOSURES);
208
+ const preV13HistoryUnavailable = options.sourceVersion != null && options.sourceVersion <= 12
209
+ ? true
210
+ : root.preV13HistoryUnavailable === true;
211
+ const retainedLimitations = limitations(root.limitations).filter((item) => !/^\d+ earlier closure records? omitted by bounded retention\.$/u.test(item));
212
+ return {
213
+ records: all.slice(-MAX_CASE_CLOSURES),
214
+ omitted,
215
+ preV13HistoryUnavailable,
216
+ limitations: lifecycleLimitations([
217
+ ...(omitted ? [`${omitted} earlier closure record${omitted === 1 ? '' : 's'} omitted by bounded retention.`] : []),
218
+ ...(invalid ? [`${invalid} malformed, unsupported, or unlinked closure record${invalid === 1 ? '' : 's'} omitted during normalisation.`] : []),
219
+ ...(duplicateConflict ? [`${duplicateConflict} conflicting closure record identit${duplicateConflict === 1 ? 'y was' : 'ies were'} reconciled deterministically.`] : []),
220
+ ...(preV13HistoryUnavailable ? ['Migrated from a pre-v13 Case; earlier deliberate closure history is unavailable.'] : []),
221
+ 'Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.',
222
+ ...retainedLimitations,
223
+ ]),
224
+ };
225
+ }
226
+ /** New-write policy only. Historical normalisation must not re-adjudicate a closure. */
227
+ export function caseClosureReviewBlocker(reason, review, now) {
228
+ if (reason !== 'independently_not_reproduced' && reason !== 'infrastructure_changed')
229
+ return null;
230
+ const expected = reason === 'independently_not_reproduced' ? 'not_reproduced' : 'changed';
231
+ const invalidLink = reason === 'independently_not_reproduced'
232
+ ? 'This closure reason requires a linked independent not-reproduced review.'
233
+ : 'This closure reason requires a linked independent changed review.';
234
+ if (!review || review.state !== expected)
235
+ return invalidLink;
236
+ const observedAt = normalizeExplicitIsoTimestamp(review.observedAt);
237
+ const createdAt = normalizeExplicitIsoTimestamp(review.createdAt);
238
+ const closedAt = normalizeExplicitIsoTimestamp(now);
239
+ if (!observedAt || !createdAt || !closedAt
240
+ || Date.parse(observedAt) > Date.parse(closedAt)
241
+ || Date.parse(createdAt) > Date.parse(closedAt))
242
+ return invalidLink;
243
+ if (reason === 'independently_not_reproduced') {
244
+ if (review.completeness !== 'complete')
245
+ return 'Independent non-reproduction closure requires a complete observation. Keep this limited review and collect or record a complete recheck.';
246
+ if (review.recheck && review.recheck.conditionsMatch !== 'comparable')
247
+ return 'Independent non-reproduction closure requires comparable recheck conditions.';
248
+ }
249
+ return null;
250
+ }
251
+ export function appendCaseClosure(current, raw, now, observedEffects, actions) {
252
+ const item = record(raw);
253
+ const reason = typeof item.reason === 'string' && CLOSURE_REASONS.has(item.reason)
254
+ ? item.reason
255
+ : null;
256
+ const review = typeof item.observedEffectReviewId === 'string'
257
+ ? observedEffects.reviews.find((candidate) => candidate.id === item.observedEffectReviewId) ?? null
258
+ : null;
259
+ const action = typeof item.actionId === 'string'
260
+ ? actions.find((candidate) => candidate.id === item.actionId) ?? null
261
+ : null;
262
+ const reviewBlocker = caseClosureReviewBlocker(reason, review, now);
263
+ if (reviewBlocker)
264
+ throw new Error(reviewBlocker);
265
+ if (reason === 'provider_reported_resolution_not_independently_checked'
266
+ && (action?.providerOutcome !== 'provider_reports_resolved'
267
+ || !action.history.some((event) => event.applied
268
+ && event.providerOutcome === 'provider_reports_resolved'
269
+ && Date.parse(event.occurredAt) <= Date.parse(now)))) {
270
+ throw new Error('This closure reason requires a linked typed provider-reported-resolution outcome.');
271
+ }
272
+ const linkContext = buildCaseClosureLinkContext(observedEffects, actions);
273
+ const created = normalizeClosure({ ...item, id: freshId('case-closure'), createdAt: now }, now, new Set(observedEffects.reviews.map((candidate) => candidate.id)), new Set(actions.map((candidate) => candidate.id)), {}, linkContext);
274
+ if (!created || !reason)
275
+ throw new Error('A deliberate closure requires a typed reason and summary.');
276
+ return normalizeCaseClosureHistory({
277
+ records: [...current.records, created],
278
+ omitted: current.omitted,
279
+ preV13HistoryUnavailable: current.preV13HistoryUnavailable,
280
+ limitations: current.limitations,
281
+ }, now, new Set(observedEffects.reviews.map((candidate) => candidate.id)), new Set(actions.map((candidate) => candidate.id)), {}, linkContext);
282
+ }
283
+ export function mergeCaseClosureHistories(local, imported, fallback, validReviewIds, validActionIds, linkContext = {}) {
284
+ return normalizeCaseClosureHistory({
285
+ records: [...local.records, ...imported.records],
286
+ omitted: Math.max(local.omitted, imported.omitted),
287
+ preV13HistoryUnavailable: local.preV13HistoryUnavailable || imported.preV13HistoryUnavailable,
288
+ limitations: [...local.limitations, ...imported.limitations],
289
+ }, fallback, validReviewIds, validActionIds, {}, linkContext);
290
+ }
291
+ export function buildCaseResponseLifecycleSummary(input) {
292
+ const providerEvents = (input.actions ?? []).flatMap((action) => action.history
293
+ .filter((event) => event.providerOutcome)
294
+ .map((event) => ({ action, event })));
295
+ const providerCohort = latestObservationCohort(providerEvents, ({ event }) => event.occurredAt);
296
+ const providerEventsAtLatestTime = providerCohort.latest;
297
+ const latestProvider = providerEventsAtLatestTime.length === 1 && !providerCohort.undated.length && providerEventsAtLatestTime[0].event.applied
298
+ ? providerEventsAtLatestTime[0]
299
+ : null;
300
+ const providerOutcomeState = providerEvents.length === 0
301
+ ? 'missing'
302
+ : latestProvider ? 'available' : 'ambiguous';
303
+ const reviews = input.observedEffects?.reviews ?? [];
304
+ const observedCohort = latestObservationCohort(reviews, (review) => review.observedAt);
305
+ const latestObserved = observedCohort.latest.length === 1 && !observedCohort.undated.length
306
+ ? observedCohort.latest[0]
307
+ : null;
308
+ const changedReviews = reviews.filter((review) => review.state === 'changed');
309
+ const changedCohort = latestObservationCohort(changedReviews, (review) => review.observedAt);
310
+ const latestObservedChangeAt = changedCohort.latest.length === 1 && !changedCohort.undated.length
311
+ ? changedCohort.observedAt
312
+ : null;
313
+ const observedChangeState = changedReviews.length === 0
314
+ ? 'missing'
315
+ : latestObservedChangeAt ? 'available' : 'ambiguous';
316
+ const latestClosure = [...(input.closures?.records ?? [])]
317
+ .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt) || compareCodeUnits(right.id, left.id))[0] ?? null;
318
+ return {
319
+ providerOutcomeState,
320
+ latestProviderOutcome: latestProvider ? {
321
+ actionId: latestProvider.action.id,
322
+ eventId: latestProvider.event.id,
323
+ outcome: latestProvider.event.providerOutcome,
324
+ occurredAt: latestProvider.event.occurredAt,
325
+ reference: latestProvider.event.reference,
326
+ } : null,
327
+ observedChangeState,
328
+ latestObservedEffect: latestObserved ? {
329
+ reviewId: latestObserved.id,
330
+ state: latestObserved.state,
331
+ observedAt: latestObserved.observedAt,
332
+ sourceClass: latestObserved.sourceClass,
333
+ source: latestObserved.source,
334
+ } : null,
335
+ latestObservedChangeAt,
336
+ latestClosure,
337
+ };
338
+ }
@@ -0,0 +1,157 @@
1
+ import { RESPONSE_AUTHORISATION_CONFIRMATION_IDS, responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
2
+ import { escapeCaseMarkdownInline as escapeMarkdown } from "./case-markdown.mjs";
3
+ /** Render only the projected packet, never the source Case or private inputs. */
4
+ export function renderCaseResponsePacket(packet) {
5
+ const { profile, case: caseRecord, incident: { category, affectedParty, abusiveUrls, observedHarm, observedAt }, generatedAt: normalizedGeneratedAt, recipientRoute, escalationHistory, escalationHistoryOmitted, escalationHistoryLimitations, authorisation, readiness, selectedEvidence, artefactReferences, responseLifecycle, preflight, provenance: { limitations, evidencePinCount, decisionCount, assertionCount, observationAge: age }, integrity: { digestSha256 }, } = packet;
6
+ const lines = [
7
+ `# ${escapeMarkdown(profile.label)} packet`,
8
+ '',
9
+ `**Domain:** ${escapeMarkdown(caseRecord.domain)}`,
10
+ `**Category:** ${escapeMarkdown(category)}`,
11
+ `**Affected party:** ${escapeMarkdown(affectedParty)}`,
12
+ `**Observed at (UTC):** ${observedAt}`,
13
+ `**Generated at (UTC):** ${normalizedGeneratedAt}`,
14
+ `**Audience:** ${escapeMarkdown(profile.audience)}`,
15
+ `**Suggested subject:** ${escapeMarkdown(profile.subject)}`,
16
+ '',
17
+ '## Observed harm',
18
+ '',
19
+ escapeMarkdown(observedHarm),
20
+ '',
21
+ '## Exact abusive URLs',
22
+ '',
23
+ ...abusiveUrls.map((url) => `- ${escapeMarkdown(url)}`),
24
+ '',
25
+ '## Selected response route',
26
+ '',
27
+ ...(recipientRoute
28
+ ? [
29
+ `### ${contactLabel(recipientRoute.kind)}`,
30
+ '',
31
+ `- Case action: ${escapeMarkdown(recipientRoute.actionId)}`,
32
+ `- Contact: ${escapeMarkdown(recipientRoute.contact)}`,
33
+ `- Source: ${escapeMarkdown(recipientRoute.source)}`,
34
+ `- Route observed: ${recipientRoute.observedAt ?? 'Not provided'} (${recipientRoute.freshness})`,
35
+ `- Route review after: ${recipientRoute.reviewAfter ?? 'Not provided'}`,
36
+ `- Limitations: ${recipientRoute.limitations.length ? recipientRoute.limitations.map(escapeMarkdown).join('; ') : 'None recorded'}`,
37
+ '',
38
+ ]
39
+ : ['No profile-appropriate response route was bound to this packet.', '']),
40
+ '## Selected action lineage',
41
+ '',
42
+ ...(escalationHistory.length
43
+ ? escalationHistory.flatMap((action) => [
44
+ `- ${escapeMarkdown(action.type.replaceAll('_', ' '))} to ${escapeMarkdown(action.recipient)} · ${escapeMarkdown(action.state.replaceAll('_', ' '))} · updated ${action.updatedAt}`,
45
+ ` - Route observed: ${action.routeObservedAt ?? 'Not provided'}`,
46
+ ...(action.reference ? [` - Reference: ${escapeMarkdown(action.reference)}`] : []),
47
+ ...(action.providerOutcome ? [` - Typed provider outcome: ${escapeMarkdown(action.providerOutcome.replaceAll('_', ' '))}`] : []),
48
+ ...(action.outcomeDetail ? [` - Outcome detail: ${escapeMarkdown(action.outcomeDetail)}`] : []),
49
+ ...(action.originActionId ? [` - Originating action: ${escapeMarkdown(action.originActionId)}`] : []),
50
+ ...(action.amendment ? [` - Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}`, ` - Prepared request events: ${action.amendment.requestEventIds.map(escapeMarkdown).join(', ')}`] : []),
51
+ ...action.transitions.flatMap((event) => [
52
+ ` - ${event.occurredAt}: ${escapeMarkdown(event.previousState ?? 'none')} → ${escapeMarkdown(event.nextState)} · ${escapeMarkdown(event.sourceClass)} · ${escapeMarkdown(event.provenance)}${event.providerOutcome ? ` · ${escapeMarkdown(event.providerOutcome.replaceAll('_', ' '))}` : ''}${event.applied ? '' : ' · retained conflict'}`,
53
+ ...(event.reference ? [` - Reference: ${escapeMarkdown(event.reference)}`] : []),
54
+ ...(event.evidencePinId ? [` - Evidence pin: ${escapeMarkdown(event.evidencePinId)}`] : []),
55
+ ...(event.evidenceRequest ? [
56
+ ` - Requested evidence (${event.evidenceRequest.state}): ${escapeMarkdown(event.evidenceRequest.summary)}`,
57
+ ` - Original packet SHA-256: ${event.evidenceRequest.packetDigestSha256}; deadline: ${event.evidenceRequest.dueAt ?? 'not provided'}`,
58
+ ` - Prepared pins: ${event.evidenceRequest.evidencePinIds.map(escapeMarkdown).join(', ') || 'none'}${event.evidenceRequest.rationale ? `; ${escapeMarkdown(event.evidenceRequest.rationale)}` : ''}`,
59
+ ] : []),
60
+ ...(event.originActionId ? [` - Originating action: ${escapeMarkdown(event.originActionId)}`] : []),
61
+ ...event.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
62
+ ]),
63
+ ...(action.historyOmitted ? [` - ${action.historyOmitted} earlier transition event${action.historyOmitted === 1 ? '' : 's'} omitted by bound.`] : []),
64
+ ...action.historyLimitations.map((limitation) => ` - History limitation: ${escapeMarkdown(limitation)}`),
65
+ ])
66
+ : ['No Case action was selected.']),
67
+ ...(escalationHistoryOmitted ? [`- Unrelated Case actions excluded from packet: ${escalationHistoryOmitted}`] : []),
68
+ ...escalationHistoryLimitations.map((limitation) => `- Packet history limitation: ${escapeMarkdown(limitation)}`),
69
+ '',
70
+ '## Readiness and authorisation',
71
+ '',
72
+ `- Packet state: ${authorisation.status}`,
73
+ `- Reviewed-input SHA-256: ${authorisation.reviewedInputDigestSha256}`,
74
+ `- Supplied review digest matches: ${authorisation.digestMatches ? 'yes' : 'no'}`,
75
+ ...RESPONSE_AUTHORISATION_CONFIRMATION_IDS.map((id) => `- Confirmation ${id}: ${authorisation.confirmations[id] ? 'yes' : 'no'}`),
76
+ ...readiness.rows.flatMap((row) => [
77
+ `- ${escapeMarkdown(row.label)} [${row.state}]: ${escapeMarkdown(row.detail)}`,
78
+ ...row.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
79
+ ]),
80
+ ...authorisation.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
81
+ '',
82
+ '## Selected evidence and integrity references',
83
+ '',
84
+ ...(selectedEvidence.length ? selectedEvidence.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · ${escapeMarkdown(item.source)}${item.observationHostname ? ` · ${escapeMarkdown(item.observationHostname)}` : ''}${item.webObservationMode ? ' · selected URL' : ''} · ${item.observedAt ?? 'Observation time unavailable'} · ${escapeMarkdown(item.completeness)}`) : ['- No evidence pin was explicitly selected.']),
85
+ ...artefactReferences.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · SHA-256 ${item.digestSha256} · captured ${item.capturedAt}`),
86
+ '',
87
+ '## Provider outcome and independent effect',
88
+ '',
89
+ responseLifecycle.latestProviderOutcome
90
+ ? `- Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${escapeMarkdown(responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' '))})`
91
+ : `- Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
92
+ responseLifecycle.latestObservedChangeAt
93
+ ? `- Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
94
+ : `- Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
95
+ ...(responseLifecycle.latestObservedEffect ? [`- Latest independent review: ${escapeMarkdown(responseLifecycle.latestObservedEffect.state.replaceAll('_', ' '))} · ${responseLifecycle.latestObservedEffect.observedAt} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.source)}`] : []),
96
+ ...responseLifecycle.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
97
+ '',
98
+ '## Review and provenance',
99
+ '',
100
+ `- Preflight: ${preflight.status.replaceAll('_', ' ')} (${preflight.counts.pass} pass, ${preflight.counts.caution} caution, ${preflight.counts.block} block)`,
101
+ ...preflight.checks.map((check) => `- ${escapeMarkdown(check.label)} [${check.state}]: ${escapeMarkdown(check.detail)}`),
102
+ ...limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
103
+ `- Case evidence pins: ${evidencePinCount}`,
104
+ `- Case decision records: ${decisionCount}`,
105
+ `- Case structured assertions: ${assertionCount}`,
106
+ `- Observation-age band at export: ${age.band.replaceAll('_', ' ')}`,
107
+ `- Canonical packet SHA-256: ${digestSha256}`,
108
+ '- Digest scope: canonical sorted JSON packet excluding the integrity object',
109
+ '',
110
+ '## Audience profile',
111
+ '',
112
+ ...profile.checklist.map((item) => `- Checklist: ${escapeMarkdown(item)}`),
113
+ ...profile.includedEvidence.map((item) => `- Included: ${escapeMarkdown(item)}`),
114
+ ...profile.excludedEvidence.map((item) => `- Excluded: ${escapeMarkdown(item)}`),
115
+ ...profile.redactions.map((item) => `- Redaction to confirm: ${escapeMarkdown(item)}`),
116
+ ...profile.attachments.map((item) => `- Attachment expectation: ${escapeMarkdown(item)}`),
117
+ ...profile.followUpFields.map((item) => `- Follow-up field: ${escapeMarkdown(item)}`),
118
+ ];
119
+ const markdown = `${lines.join('\n').trim()}\n`;
120
+ const email = [
121
+ `Subject: ${profile.subject}`,
122
+ '',
123
+ 'Hello,',
124
+ '',
125
+ `I am reporting observed ${category} activity involving ${caseRecord.domain}.`,
126
+ `Affected party: ${affectedParty}`,
127
+ `Observed at (UTC): ${observedAt}`,
128
+ '',
129
+ 'Observed harm:',
130
+ observedHarm,
131
+ '',
132
+ 'Exact URLs:',
133
+ ...abusiveUrls.map((url) => `- ${url}`),
134
+ '',
135
+ 'Selected evidence:',
136
+ ...(selectedEvidence.length
137
+ ? selectedEvidence.map((item) => `- ${item.label} — ${item.source}${item.observationHostname ? ` for ${item.observationHostname}` : ''}${item.webObservationMode ? ' · selected URL' : ''}, observed ${item.observedAt ?? 'time unavailable'} (${item.completeness})`)
138
+ : ['- No Case evidence pin was selected.']),
139
+ '',
140
+ `Reviewed packet SHA-256: ${digestSha256}`,
141
+ 'Attach the reviewed packet (and any separately reviewed evidence files) through the recipient’s approved submission channel; this message does not embed or transmit attachments.',
142
+ '',
143
+ ...(responseLifecycle.latestProviderOutcome
144
+ ? [`Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' ')})`]
145
+ : []),
146
+ ...(responseLifecycle.latestObservedChangeAt
147
+ ? [`Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`]
148
+ : []),
149
+ '',
150
+ 'Please review this report under the applicable abuse and acceptable-use policies.',
151
+ '',
152
+ authorisation.status === 'authorised'
153
+ ? 'This locally prepared packet is bound to explicit review confirmations. It was not submitted automatically and does not promise any provider outcome.'
154
+ : 'This is an unauthorised local draft with cautions. It was not submitted automatically and does not promise any provider outcome.',
155
+ ].join('\n');
156
+ return { markdown, email: `${email}\n` };
157
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ // Response packet vocabulary shared by construction and independent validation.
2
+ export const RESPONSE_CONTACT_KINDS = [
3
+ 'registrar',
4
+ 'registry',
5
+ 'network_hosting',
6
+ 'security_txt',
7
+ 'application_platform',
8
+ ];
9
+ export const RESPONSE_PACKET_PROFILE_IDS = [
10
+ 'registrar',
11
+ 'registry',
12
+ 'network_hosting',
13
+ 'security_contact',
14
+ 'application_platform',
15
+ 'browser_blocklist',
16
+ 'internal_soc',
17
+ ];
18
+ export const RESPONSE_READINESS_STATES = ['complete', 'partial', 'stale', 'unavailable', 'not_provided'];
19
+ export const RESPONSE_READINESS_ROW_IDS = [
20
+ 'observed_behaviour',
21
+ 'exact_url',
22
+ 'observation_time',
23
+ 'capture_provenance',
24
+ 'infrastructure_responsibility',
25
+ 'recipient_route',
26
+ 'authority_review',
27
+ 'selected_evidence',
28
+ 'contradictions',
29
+ 'source_limitations',
30
+ ];
31
+ export const RESPONSE_AUTHORISATION_CONFIRMATION_IDS = [
32
+ 'selectedEvidence',
33
+ 'recipientScope',
34
+ 'privacyRedactions',
35
+ 'analystAuthority',
36
+ 'evidenceFreshness',
37
+ ];
38
+ export function responseContactLabel(value) {
39
+ if (value === 'network_hosting')
40
+ return 'Observed endpoint network registration';
41
+ if (value === 'security_txt')
42
+ return 'security.txt';
43
+ if (value === 'application_platform')
44
+ return 'Application platform';
45
+ if (value === 'manual')
46
+ return 'Manual submission';
47
+ return value.charAt(0).toUpperCase() + value.slice(1);
48
+ }