@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -1,10 +1,9 @@
1
1
  import { RESPONSE_AUTHORISATION_CONFIRMATION_IDS, responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
2
- function escapeMarkdown(value) {
3
- return value.replace(/([\\`*_[\]<>|])/gu, '\\$1').replace(/\r?\n/gu, ' ');
4
- }
2
+ import { escapeCaseMarkdownInline as escapeMarkdown } from "./case-markdown.mjs";
5
3
  /** Render only the projected packet, never the source Case or private inputs. */
6
4
  export function renderCaseResponsePacket(packet) {
7
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 selectedCorrection = escalationHistory.find(action => action.actionId === packet.actionBinding.selectedActionId)?.correction;
8
7
  const lines = [
9
8
  `# ${escapeMarkdown(profile.label)} packet`,
10
9
  '',
@@ -16,7 +15,7 @@ export function renderCaseResponsePacket(packet) {
16
15
  `**Audience:** ${escapeMarkdown(profile.audience)}`,
17
16
  `**Suggested subject:** ${escapeMarkdown(profile.subject)}`,
18
17
  '',
19
- '## Observed harm',
18
+ selectedCorrection ? '## Retained incident context (not a renewed allegation)' : '## Observed harm',
20
19
  '',
21
20
  escapeMarkdown(observedHarm),
22
21
  '',
@@ -49,10 +48,21 @@ export function renderCaseResponsePacket(packet) {
49
48
  ...(action.providerOutcome ? [` - Typed provider outcome: ${escapeMarkdown(action.providerOutcome.replaceAll('_', ' '))}`] : []),
50
49
  ...(action.outcomeDetail ? [` - Outcome detail: ${escapeMarkdown(action.outcomeDetail)}`] : []),
51
50
  ...(action.originActionId ? [` - Originating action: ${escapeMarkdown(action.originActionId)}`] : []),
51
+ ...(action.amendment ? [` - Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}`, ` - Prepared request events: ${action.amendment.requestEventIds.map(escapeMarkdown).join(', ')}`] : []),
52
+ ...(action.correction ? [` - ${action.correction.purpose === 'correction' ? 'Correction request' : 'Retraction request'} for delivery event ${escapeMarkdown(action.correction.deliveryEventId)}; original packet v${action.correction.packetVersion} SHA-256: ${action.correction.packetDigestSha256}`,
53
+ ` - Analyst reason: ${escapeMarkdown(action.correction.reason)}`, ` - Previous statement: ${escapeMarkdown(action.correction.previousStatement)}`,
54
+ ` - Corrected statement: ${escapeMarkdown(action.correction.correctedStatement || 'Request to retract the previous statement')}`,
55
+ ` - Corrected evidence pins: ${action.correction.evidencePinIds.map(escapeMarkdown).join(', ')}`] : []),
52
56
  ...action.transitions.flatMap((event) => [
53
57
  ` - ${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'}`,
54
58
  ...(event.reference ? [` - Reference: ${escapeMarkdown(event.reference)}`] : []),
59
+ ...(event.packetReceipt ? [` - Exact local packet receipt: v${event.packetReceipt.packetVersion} · ${event.packetReceipt.packetDigestSha256} · ${escapeMarkdown(event.packetReceipt.recipient)} · ${escapeMarkdown(event.packetReceipt.profile)} · generated ${event.packetReceipt.packetGeneratedAt}`] : []),
55
60
  ...(event.evidencePinId ? [` - Evidence pin: ${escapeMarkdown(event.evidencePinId)}`] : []),
61
+ ...(event.evidenceRequest ? [
62
+ ` - Requested evidence (${event.evidenceRequest.state}): ${escapeMarkdown(event.evidenceRequest.summary)}`,
63
+ ` - Original packet SHA-256: ${event.evidenceRequest.packetDigestSha256}; deadline: ${event.evidenceRequest.dueAt ?? 'not provided'}`,
64
+ ` - Prepared pins: ${event.evidenceRequest.evidencePinIds.map(escapeMarkdown).join(', ') || 'none'}${event.evidenceRequest.rationale ? `; ${escapeMarkdown(event.evidenceRequest.rationale)}` : ''}`,
65
+ ] : []),
56
66
  ...(event.originActionId ? [` - Originating action: ${escapeMarkdown(event.originActionId)}`] : []),
57
67
  ...event.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
58
68
  ]),
@@ -82,6 +92,10 @@ export function renderCaseResponsePacket(packet) {
82
92
  '',
83
93
  '## Provider outcome and independent effect',
84
94
  '',
95
+ ...escalationHistory.flatMap(action => [
96
+ `- Action ${escapeMarkdown(action.actionId)} object scope: ${action.responseObjects?.length ? action.responseObjects.map(object => `${escapeMarkdown(object.kind)} · ${escapeMarkdown(object.identifier)}`).join('; ') : 'unknown; no exact-object coverage inferred'}`,
97
+ ...action.transitions.filter(event => event.objectOutcome).map(event => ` - ${escapeMarkdown(event.objectOutcome)} · ${escapeMarkdown(event.sourceClass)} · ${event.occurredAt} · affected objects: ${event.responseObjects?.map(object => `${escapeMarkdown(object.kind)} · ${escapeMarkdown(object.identifier)}`).join('; ')}${event.applied ? '' : ' · retained conflict'}`),
98
+ ]),
85
99
  responseLifecycle.latestProviderOutcome
86
100
  ? `- Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${escapeMarkdown(responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' '))})`
87
101
  : `- Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
@@ -89,6 +103,7 @@ export function renderCaseResponsePacket(packet) {
89
103
  ? `- Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
90
104
  : `- Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
91
105
  ...(responseLifecycle.latestObservedEffect ? [`- Latest independent review: ${escapeMarkdown(responseLifecycle.latestObservedEffect.state.replaceAll('_', ' '))} · ${responseLifecycle.latestObservedEffect.observedAt} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.source)}`] : []),
106
+ ...(responseLifecycle.latestObservedEffect?.responseObject ? [`- Reviewed object: ${escapeMarkdown(responseLifecycle.latestObservedEffect.responseObject.kind)} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.responseObject.identifier)}${responseLifecycle.latestObservedEffect.objectOutcome ? ` · ${escapeMarkdown(responseLifecycle.latestObservedEffect.objectOutcome)}` : ''}`] : []),
92
107
  ...responseLifecycle.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
93
108
  '',
94
109
  '## Review and provenance',
@@ -113,22 +128,29 @@ export function renderCaseResponsePacket(packet) {
113
128
  ...profile.followUpFields.map((item) => `- Follow-up field: ${escapeMarkdown(item)}`),
114
129
  ];
115
130
  const markdown = `${lines.join('\n').trim()}\n`;
131
+ const correction = escalationHistory.find(action => action.actionId === packet.actionBinding.selectedActionId)?.correction;
116
132
  const email = [
117
133
  `Subject: ${profile.subject}`,
118
134
  '',
119
135
  'Hello,',
120
136
  '',
121
- `I am reporting observed ${category} activity involving ${caseRecord.domain}.`,
137
+ correction ? `I am requesting ${correction.purpose === 'correction' ? 'a correction' : 'retraction or withdrawal'} of a previous statement involving ${caseRecord.domain}.`
138
+ : `I am reporting observed ${category} activity involving ${caseRecord.domain}.`,
139
+ ...(correction ? [`Original delivery event: ${correction.deliveryEventId}`, `Original canonical packet v${correction.packetVersion} SHA-256: ${correction.packetDigestSha256}`,
140
+ `Analyst reason: ${correction.reason}`, `Previous statement: ${correction.previousStatement}`,
141
+ `Corrected statement: ${correction.correctedStatement || 'Please retract the previous statement.'}`,
142
+ `Corrected evidence references: ${correction.evidencePinIds.join(', ')}`, 'This new request does not establish delivery, recipient acceptance, reversal, restoration or independent recheck.', ''] : []),
122
143
  `Affected party: ${affectedParty}`,
123
144
  `Observed at (UTC): ${observedAt}`,
124
145
  '',
125
- 'Observed harm:',
146
+ correction ? 'Retained incident context (not a renewed allegation):' : 'Observed harm:',
126
147
  observedHarm,
127
148
  '',
128
149
  'Exact URLs:',
129
150
  ...abusiveUrls.map((url) => `- ${url}`),
130
151
  '',
131
152
  'Selected evidence:',
153
+ ...escalationHistory.flatMap(action => action.responseObjects?.length ? [`Action ${action.actionId} scope: ${action.responseObjects.map(object => `${object.kind} · ${object.identifier}`).join('; ')}`] : []),
132
154
  ...(selectedEvidence.length
133
155
  ? selectedEvidence.map((item) => `- ${item.label} — ${item.source}${item.observationHostname ? ` for ${item.observationHostname}` : ''}${item.webObservationMode ? ' · selected URL' : ''}, observed ${item.observedAt ?? 'time unavailable'} (${item.completeness})`)
134
156
  : ['- No Case evidence pin was selected.']),
@@ -136,14 +158,15 @@ export function renderCaseResponsePacket(packet) {
136
158
  `Reviewed packet SHA-256: ${digestSha256}`,
137
159
  '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.',
138
160
  '',
139
- responseLifecycle.latestProviderOutcome
140
- ? `Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' ')})`
141
- : `Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
142
- responseLifecycle.latestObservedChangeAt
143
- ? `Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
144
- : `Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
161
+ ...(responseLifecycle.latestProviderOutcome
162
+ ? [`Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' ')})`]
163
+ : []),
164
+ ...(responseLifecycle.latestObservedChangeAt
165
+ ? [`Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`]
166
+ : []),
145
167
  '',
146
168
  'Please review this report under the applicable abuse and acceptable-use policies.',
169
+ ...limitations,
147
170
  '',
148
171
  authorisation.status === 'authorised'
149
172
  ? 'This locally prepared packet is bound to explicit review confirmations. It was not submitted automatically and does not promise any provider outcome.'
@@ -2,21 +2,24 @@ import { renderCaseResponsePacket } from "./case-response-packet-render.mjs";
2
2
  import { responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
3
3
  // Pure abuse-evidence packet builder. It creates local review artifacts only:
4
4
  // no network requests, mailto links, submissions, or provider side effects.
5
- import { assertBoundedJsonStructure, } from "../../lib/bounded-json.mjs";
5
+ import { assertPacketAmendmentSelection } from "./case-requested-evidence.mjs";
6
+ import { correctionDelivery, CASE_DELIVERY_RECEIPT_LIMITATION } from "./case-packet-correction.mjs";
7
+ import { caseSelectedEvidenceSourceLimitations } from "./case-evidence-links.mjs";
8
+ import { sameCaseResponseObject } from "./case-response-object.mjs";
9
+ import { assertBoundedJsonStructure, } from "../analysis/bounded-json.mjs";
6
10
  import { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSE_CATEGORY_LENGTH, MAX_ABUSIVE_URLS, MAX_AFFECTED_PARTY_LENGTH, MAX_EXACT_URL_LENGTH, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_HARM_LENGTH, MAX_RESPONSE_SELECTED_EVIDENCE, SUPPORTED_CASE_RESPONSE_PACKET_VERSIONS, } from "../contracts/case-portability.mjs";
7
11
  import { SORTED_JSON_V2, canonicalArtifactJsonV2, } from "../evidence/artifact-integrity.mjs";
8
12
  import { normalizeExplicitIsoTimestamp, } from "../evidence/observation.mjs";
9
13
  import { latestCaseEvidence, } from "./case-evidence-model.mjs";
10
14
  import {} from "./case-model.mjs";
11
- import { caseDispositionSupportsDefensiveResponse, } from "./case-record-operations.mjs";
12
- import { buildCaseActionOutcomeSummary, buildCaseResponseLifecycleSummary, } from "./case-response-model.mjs";
15
+ import { caseDispositionSupportsDefensiveResponse } from "./case-record-decisions.mjs";
16
+ import { buildCaseActionOutcomeSummary, buildCaseResponseLifecycleSummary, caseActionCompletesResponseDecision, countEvidenceLinkedCaseDecisions, } from "./case-response-model.mjs";
13
17
  import { RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_STATES, } from "./case-response-packet-vocabulary.mjs";
14
18
  import { validateCaseResponseReviewInputs, } from "./case-response-review-inputs.mjs";
15
19
  import { responseRouteFreshness, } from "./response-route-freshness.mjs";
16
20
  export * from "./case-response-packet-vocabulary.mjs";
17
21
  export { validateCaseResponseReviewInputs, } from "./case-response-review-inputs.mjs";
18
- export { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSE_CATEGORY_LENGTH, MAX_ABUSIVE_URLS, MAX_AFFECTED_PARTY_LENGTH, MAX_EXACT_URL_LENGTH, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_HARM_LENGTH, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_ROUTE_STALE_AFTER_DAYS, SUPPORTED_CASE_RESPONSE_PACKET_VERSIONS, } from "../contracts/case-portability.mjs";
19
- export { LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, } from "../contracts/case-portability.mjs";
22
+ export { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSE_CATEGORY_LENGTH, MAX_ABUSIVE_URLS, MAX_AFFECTED_PARTY_LENGTH, MAX_EXACT_URL_LENGTH, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_HARM_LENGTH, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_ROUTE_STALE_AFTER_DAYS, SUPPORTED_CASE_RESPONSE_PACKET_VERSIONS, } from "../contracts/case-portability.mjs";
20
23
  export const CASE_RESPONSE_PREFLIGHT_EVIDENCE_SCOPE = Object.freeze({
21
24
  version: 1,
22
25
  owner: 'case',
@@ -218,6 +221,14 @@ function selectedPacketAction(caseRecord, input) {
218
221
  : null;
219
222
  return actionId ? caseRecord.actions.find((action) => action.id === actionId) ?? null : null;
220
223
  }
224
+ function assertAmendmentEvidence(caseRecord, input) {
225
+ const action = selectedPacketAction(caseRecord, input);
226
+ const selected = new Set(Array.isArray(input.selectedEvidencePinIds) ? input.selectedEvidencePinIds : []);
227
+ assertPacketAmendmentSelection(caseRecord.actions, action?.id ?? null, new Set(caseRecord.evidencePins.filter(pin => selected.has(pin.id)).map(pin => pin.id)));
228
+ if (action?.correction)
229
+ correctionDelivery(caseRecord.actions, action, { caseId: caseRecord.id, target: caseRecord.domain,
230
+ profile: responsePacketProfile(input.profile).id, selectedPinIds: new Set(caseRecord.evidencePins.filter(pin => selected.has(pin.id)).map(pin => pin.id)) });
231
+ }
221
232
  function packetActionLineage(caseRecord, input) {
222
233
  const selected = selectedPacketAction(caseRecord, input);
223
234
  if (!selected)
@@ -317,6 +328,7 @@ function normalizeSelectedEvidence(caseRecord, value) {
317
328
  label: text(pin.label, 80),
318
329
  source: text(pin.source, 120),
319
330
  ...(pin.observationHostname ? { observationHostname: pin.observationHostname } : {}),
331
+ ...(pin.responseObject ? { responseObject: structuredClone(pin.responseObject) } : {}),
320
332
  ...(pin.webObservationMode ? { webObservationMode: pin.webObservationMode } : {}),
321
333
  observedAt: timestamp(pin.observedAt),
322
334
  completeness: text(pin.completeness, 40),
@@ -519,7 +531,7 @@ export function buildResponsePacketProfilePreview(caseRecord, input) {
519
531
  id: profile.id,
520
532
  label: profile.label,
521
533
  audience: profile.audience,
522
- subject: `${profile.subjectPrefix}: ${caseRecord.domain} (${category})`,
534
+ subject: `${selectedPacketAction(caseRecord, input)?.correction ? 'Correction or retraction request' : profile.subjectPrefix}: ${caseRecord.domain} (${category})`,
523
535
  checklist: [...profile.checklist],
524
536
  evidenceOrder: [...profile.evidenceOrder],
525
537
  includedEvidence: [...profile.includedEvidence],
@@ -534,6 +546,13 @@ export function buildCaseResponsePreflight(caseRecord, input, generatedAt = new
534
546
  const observedAt = timestamp(input.observedAt);
535
547
  const normalizedGeneratedAt = timestamp(generatedAt) || new Date().toISOString();
536
548
  const binding = bindPacketRoute(caseRecord, input, normalizedGeneratedAt);
549
+ let amendmentError = null;
550
+ try {
551
+ assertAmendmentEvidence(caseRecord, input);
552
+ }
553
+ catch (error) {
554
+ amendmentError = error instanceof Error ? error.message : 'Review this amendment and its retained evidence.';
555
+ }
537
556
  const urls = normalizeUrls(input.abusiveUrls);
538
557
  const selectedEvidence = normalizeSelectedEvidence(caseRecord, input.selectedEvidencePinIds);
539
558
  const requiredComplete = Boolean(text(input.category, MAX_ABUSE_CATEGORY_LENGTH)
@@ -546,10 +565,9 @@ export function buildCaseResponsePreflight(caseRecord, input, generatedAt = new
546
565
  .filter((item) => item.kind === 'contradiction' && item.state === 'open')
547
566
  .length;
548
567
  const actionSummary = buildCaseActionOutcomeSummary(caseRecord.actions, normalizedGeneratedAt);
549
- const retainedPinIds = new Set(caseRecord.evidencePins.map((pin) => pin.id));
550
- const evidenceLinkedDecisionCount = caseRecord.decisions.filter((decision) => decision.evidencePinIds.some((evidencePinId) => retainedPinIds.has(evidencePinId))).length;
568
+ const evidenceLinkedDecisionCount = countEvidenceLinkedCaseDecisions(caseRecord.decisions, caseRecord.evidencePins);
551
569
  const responseDisposition = caseDispositionSupportsDefensiveResponse(caseRecord.disposition);
552
- const reviewedActionCount = caseRecord.actions.filter((action) => ['reviewed', 'authorised', 'submitted', 'acknowledged', 'terminal'].includes(action.state)).length;
570
+ const reviewedActionCount = caseRecord.actions.filter((action) => caseActionCompletesResponseDecision(action.state)).length;
553
571
  const profile = responsePacketProfile(input.profile);
554
572
  const checks = [
555
573
  {
@@ -637,10 +655,10 @@ export function buildCaseResponsePreflight(caseRecord, input, generatedAt = new
637
655
  {
638
656
  id: 'packet_action',
639
657
  label: 'Packet action',
640
- state: binding.actionBinding.state === 'selected' ? 'pass' : profile.id === 'internal_soc' ? 'caution' : 'block',
641
- detail: binding.actionBinding.state === 'selected'
658
+ state: amendmentError ? 'block' : binding.actionBinding.state === 'selected' ? 'pass' : profile.id === 'internal_soc' ? 'caution' : 'block',
659
+ detail: amendmentError ?? (binding.actionBinding.state === 'selected'
642
660
  ? `Action ${binding.actionBinding.selectedActionId} owns this packet and its retained origin lineage.`
643
- : 'Select the retained Case action this packet prepares or documents.',
661
+ : 'Select the retained Case action this packet prepares or documents.'),
644
662
  },
645
663
  {
646
664
  id: 'action_tracking',
@@ -675,6 +693,9 @@ function normalizeActionHistory(caseRecord, input) {
675
693
  .map((action) => ({
676
694
  actionId: action.id,
677
695
  type: text(action.type, 80),
696
+ ...(action.amendment ? { amendment: structuredClone(action.amendment) } : {}),
697
+ ...(action.correction ? { correction: structuredClone(action.correction) } : {}),
698
+ ...(action.responseObjects !== undefined ? { responseObjects: structuredClone(action.responseObjects) } : {}),
678
699
  recipient: text(action.recipient, 320),
679
700
  contactSource: text(action.contactSource, 120),
680
701
  routeObservedAt: timestamp(action.routeObservedAt),
@@ -688,6 +709,10 @@ function normalizeActionHistory(caseRecord, input) {
688
709
  historyLimitations: normalizeLimitations(action.historyLimitations),
689
710
  transitions: action.history.map((event) => ({
690
711
  id: event.id,
712
+ ...(event.evidenceRequest ? { evidenceRequest: structuredClone(event.evidenceRequest) } : {}),
713
+ ...(event.packetReceipt ? { packetReceipt: structuredClone(event.packetReceipt) } : {}),
714
+ ...(event.responseObjects !== undefined ? { responseObjects: structuredClone(event.responseObjects) } : {}),
715
+ ...(event.objectOutcome ? { objectOutcome: event.objectOutcome } : {}),
691
716
  previousState: event.previousState,
692
717
  nextState: event.nextState,
693
718
  occurredAt: event.occurredAt,
@@ -723,21 +748,31 @@ function normalizeActionHistory(caseRecord, input) {
723
748
  }
724
749
  function normalizeResponseLifecycle(caseRecord, actionIds, lineageComplete) {
725
750
  const scopedIds = new Set(actionIds);
751
+ const objects = caseRecord.actions.filter(action => scopedIds.has(action.id)).flatMap(action => action.responseObjects ?? []);
726
752
  const scopedRecord = {
727
753
  ...caseRecord,
728
754
  actions: caseRecord.actions.filter((action) => scopedIds.has(action.id)),
755
+ observedEffects: {
756
+ ...caseRecord.observedEffects,
757
+ reviews: caseRecord.observedEffects.reviews.filter(review => review.responseObject
758
+ && objects.some(object => sameCaseResponseObject(object, review.responseObject))),
759
+ },
729
760
  closures: {
730
761
  ...caseRecord.closures,
731
762
  records: caseRecord.closures.records.filter((closure) => closure.actionId !== null && scopedIds.has(closure.actionId)),
732
763
  },
733
764
  };
734
765
  const summary = buildCaseResponseLifecycleSummary(scopedRecord);
766
+ const latestReview = scopedRecord.observedEffects.reviews.find(review => review.id === summary.latestObservedEffect?.reviewId);
735
767
  const providerOutcomeState = lineageComplete ? summary.providerOutcomeState : 'ambiguous';
736
768
  return {
737
769
  providerOutcomeState,
738
770
  latestProviderOutcome: lineageComplete && summary.latestProviderOutcome ? { ...summary.latestProviderOutcome } : null,
739
771
  observedChangeState: summary.observedChangeState,
740
- latestObservedEffect: summary.latestObservedEffect ? { ...summary.latestObservedEffect } : null,
772
+ latestObservedEffect: summary.latestObservedEffect ? { ...summary.latestObservedEffect,
773
+ ...(latestReview?.responseObject ? { responseObject: latestReview.responseObject } : {}),
774
+ ...(latestReview?.objectOutcome ? { objectOutcome: latestReview.objectOutcome } : {}),
775
+ } : null,
741
776
  latestObservedChangeAt: summary.latestObservedChangeAt,
742
777
  closure: summary.latestClosure ? {
743
778
  id: summary.latestClosure.id,
@@ -749,6 +784,8 @@ function normalizeResponseLifecycle(caseRecord, actionIds, lineageComplete) {
749
784
  'Provider workflow outcomes and independently observed technical effects are separate point-in-time records.',
750
785
  'A provider acknowledgement, terminal state, or reported resolution never becomes independently observed remediation, absence, or safety.',
751
786
  'Times are withheld when the corresponding typed event is missing or ambiguous.',
787
+ ...(objects.length ? ['Each explicit object remains independent; a latest review does not adjudicate every object bound to the action.'] : []),
788
+ ...(caseRecord.observedEffects.reviews.length > scopedRecord.observedEffects.reviews.length ? ['Independent reviews without matching explicit action-object scope were excluded from this packet; their retained Case history is unchanged.'] : []),
752
789
  ...(!summary.latestObservedEffect && scopedRecord.observedEffects.reviews.length ? ['A single latest independent review cannot be selected from the retained observation times; all retained reviews remain separately attributed.'] : []),
753
790
  ...(!lineageComplete ? ['Provider-outcome time is withheld because the selected action origin lineage is incomplete.'] : []),
754
791
  ],
@@ -773,6 +810,7 @@ async function sha256(value) {
773
810
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
774
811
  }
775
812
  export function buildCaseResponseReviewInputs(caseRecord, input, generatedAt) {
813
+ assertAmendmentEvidence(caseRecord, input);
776
814
  const profile = buildResponsePacketProfilePreview(caseRecord, input);
777
815
  const category = text(input.category, MAX_ABUSE_CATEGORY_LENGTH);
778
816
  const selectedEvidence = normalizeSelectedEvidence(caseRecord, input.selectedEvidencePinIds);
@@ -783,6 +821,7 @@ export function buildCaseResponseReviewInputs(caseRecord, input, generatedAt) {
783
821
  return {
784
822
  contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
785
823
  version: CASE_RESPONSE_REVIEW_INPUTS_VERSION,
824
+ sourceQualifications: caseSelectedEvidenceSourceLimitations(caseRecord.evidencePins, caseRecord.evidenceLinks, selectedEvidence.map(pin => pin.id)),
786
825
  profile: {
787
826
  id: profile.id,
788
827
  label: profile.label,
@@ -922,6 +961,9 @@ export async function buildCaseResponsePacket(caseRecord, input, generatedAt = n
922
961
  ? 'This packet is bound to explicit confirmations for the exact reviewed-input digest. It still requires deliberate manual use.'
923
962
  : 'This packet is a local draft with cautions and is not authorised for external use by WHOISleuth.',
924
963
  'WHOISleuth did not submit this packet or verify that any listed contact is monitored.',
964
+ ...reviewMaterial.sourceQualifications,
965
+ ...(selectedPacketAction(caseRecord, input)?.correction ? [CASE_DELIVERY_RECEIPT_LIMITATION,
966
+ 'This is a new correction or retraction request. It does not undo the original delivery, establish recipient acceptance or restoration, or replace independent recheck.'] : []),
925
967
  ...(!contacts.length ? ['No escalation contact was included.'] : []),
926
968
  ...(age.refreshRecommended ? ['The selected observation is over seven days old or appears to be in the future. Refresh evidence before submission.'] : []),
927
969
  ];
@@ -1,4 +1,3 @@
1
- // Framework-neutral response record types and vocabulary.
2
1
  export const CASE_EVIDENCE_RELATION_STANCES = ['supports', 'contradicts', 'unresolved'];
3
2
  export const CASE_PIN_COMPLETENESS = ['complete', 'partial', 'inconclusive', 'unknown'];
4
3
  export const CASE_TRANSITION_EXPECTATIONS = ['preserve', 'change', 'review'];
@@ -1,8 +1,12 @@
1
1
  // Exact, bounded validation of current and published response-review inputs.
2
+ import { readCaseEvidenceRequest, readCasePacketAmendment, assertEvidenceRequestEvent, assertEvidenceRequestHistory, assertPacketAmendmentSelection } from "./case-requested-evidence.mjs";
3
+ import { readCaseDeliveryPacketReceipt, readCasePacketCorrection, assertDeliveryPacketReceipt, correctionDelivery } from "./case-packet-correction.mjs";
4
+ import { CASE_SELECTED_EVIDENCE_SOURCE_LIMITATION } from "./case-evidence-links.mjs";
2
5
  import { CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, MAX_ABUSE_CATEGORY_LENGTH, MAX_ABUSIVE_URLS, MAX_AFFECTED_PARTY_LENGTH, MAX_CASE_ACTIONS, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTION_EVENTS_PER_CASE, MAX_EXACT_URL_LENGTH, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_HARM_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_REFERENCE_LENGTH, MAX_RESPONSE_SELECTED_EVIDENCE, MAX_RESPONSE_VALUE_LENGTH, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, SUPPORTED_CASE_RESPONSE_REVIEW_INPUTS_VERSIONS, } from "../contracts/case-portability.mjs";
3
6
  import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_ASSERTION_STATES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PIN_COMPLETENESS, CASE_PROVIDER_OUTCOMES, } from "./case-response-records.mjs";
4
7
  import { RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, } from "./case-response-packet-vocabulary.mjs";
5
- import { isValidAsciiHostname } from "../../lib/hostname.mjs";
8
+ import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
9
+ import { readCaseResponseObject, readCaseResponseObjects, readCaseResponseObjectOutcome, assertCaseObjectOutcome } from "./case-response-object.mjs";
6
10
  const CONTACT_KINDS = new Set(RESPONSE_CONTACT_KINDS);
7
11
  const PRE_PLATFORM_CONTACT_KINDS = new Set(RESPONSE_CONTACT_KINDS.filter((kind) => kind !== 'application_platform'));
8
12
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
@@ -26,6 +30,7 @@ const CASE_RESPONSE_REVIEW_INPUT_KEYS = Object.freeze([
26
30
  'escalationHistoryOmitted',
27
31
  'escalationHistoryLimitations',
28
32
  'responseLifecycle',
33
+ 'sourceQualifications',
29
34
  ]);
30
35
  const PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS = Object.freeze(CASE_RESPONSE_REVIEW_INPUT_KEYS.filter((key) => key !== 'recipientRoute' && key !== 'actionBinding'));
31
36
  function exactReviewRecord(value, keys, label, optionalKeys = []) {
@@ -87,7 +92,7 @@ function reviewEnum(value, values, label) {
87
92
  function reviewNullableEnum(value, values, label) {
88
93
  return value === null ? null : reviewEnum(value, values, label);
89
94
  }
90
- function validateReviewLifecycle(value) {
95
+ function validateReviewLifecycle(value, version) {
91
96
  const lifecycle = exactReviewRecord(value, [
92
97
  'providerOutcomeState', 'latestProviderOutcome', 'observedChangeState',
93
98
  'latestObservedEffect', 'latestObservedChangeAt', 'closure', 'limitations',
@@ -109,7 +114,10 @@ function validateReviewLifecycle(value) {
109
114
  if (lifecycle.latestObservedEffect !== null) {
110
115
  const effect = exactReviewRecord(lifecycle.latestObservedEffect, [
111
116
  'reviewId', 'state', 'observedAt', 'sourceClass', 'source',
112
- ], 'Case-response latest observed effect');
117
+ ], 'Case-response latest observed effect', version >= 6 ? ['responseObject', 'objectOutcome'] : []);
118
+ const responseObject = readCaseResponseObject(effect.responseObject);
119
+ const objectOutcome = readCaseResponseObjectOutcome(effect.objectOutcome);
120
+ assertCaseObjectOutcome(objectOutcome, responseObject);
113
121
  reviewText(effect.reviewId, 64, 'Case-response observed-effect review id');
114
122
  reviewEnum(effect.state, CASE_OBSERVED_EFFECT_STATES, 'Case-response observed-effect state');
115
123
  reviewText(effect.observedAt, 64, 'Case-response observed-effect time');
@@ -150,7 +158,12 @@ export function validateCaseResponseReviewInputs(value) {
150
158
  }
151
159
  const hasPlatformRoutes = version >= PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION;
152
160
  const hasActionBinding = version >= PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION;
153
- const source = exactReviewRecord(value, hasActionBinding ? CASE_RESPONSE_REVIEW_INPUT_KEYS : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS, 'Case-response review inputs');
161
+ const source = exactReviewRecord(value, (hasActionBinding ? CASE_RESPONSE_REVIEW_INPUT_KEYS : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS).filter(key => version >= 7 || key !== 'sourceQualifications'), 'Case-response review inputs');
162
+ if (version >= 7)
163
+ for (const qualification of boundedReviewArray(source.sourceQualifications, 1, 'Selected evidence source qualifications')) {
164
+ if (qualification !== CASE_SELECTED_EVIDENCE_SOURCE_LIMITATION)
165
+ throw new TypeError('Unknown selected-source qualification.');
166
+ }
154
167
  const profile = exactReviewRecord(source.profile, [
155
168
  'id', 'label', 'audience', 'subject', 'checklist', 'includedEvidence',
156
169
  'excludedEvidence', 'redactions',
@@ -235,7 +248,11 @@ export function validateCaseResponseReviewInputs(value) {
235
248
  for (const candidate of boundedReviewArray(source.selectedEvidence, MAX_RESPONSE_SELECTED_EVIDENCE, 'Case-response selected evidence')) {
236
249
  const evidence = exactReviewRecord(candidate, [
237
250
  'id', 'label', 'source', 'observedAt', 'completeness', 'limitations',
238
- ], 'Case-response selected evidence item', version > PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION ? ['observationHostname', 'webObservationMode'] : []);
251
+ ], 'Case-response selected evidence item', [
252
+ ...(version > PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION ? ['observationHostname', 'webObservationMode'] : []),
253
+ ...(version >= 6 ? ['responseObject'] : []),
254
+ ]);
255
+ readCaseResponseObject(evidence.responseObject);
239
256
  reviewText(evidence.id, 64, 'Case-response evidence id');
240
257
  reviewText(evidence.label, 80, 'Case-response evidence label');
241
258
  reviewText(evidence.source, 120, 'Case-response evidence source');
@@ -318,7 +335,11 @@ export function validateCaseResponseReviewInputs(value) {
318
335
  ...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
319
336
  'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
320
337
  'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
321
- ], 'Case-response escalation action');
338
+ ], 'Case-response escalation action', [...(version >= 5 ? ['amendment'] : []), ...(version >= 6 ? ['responseObjects'] : []), ...(version >= 7 ? ['correction'] : [])]);
339
+ readCaseResponseObjects(action.responseObjects);
340
+ readCasePacketAmendment(action.amendment);
341
+ if (readCasePacketCorrection(action.correction) && action.amendment)
342
+ throw new TypeError('Correction and provider amendment must remain separate.');
322
343
  for (const key of ['actionId', 'recipient', 'contactSource', 'createdAt', 'updatedAt']) {
323
344
  reviewText(action[key], MAX_RESPONSE_VALUE_LENGTH, `Case-response action ${key}`);
324
345
  }
@@ -338,12 +359,30 @@ export function validateCaseResponseReviewInputs(value) {
338
359
  reviewNullableEnum(action.providerOutcome, CASE_PROVIDER_OUTCOMES, 'Case-response action provider outcome');
339
360
  reviewCount(action.historyOmitted, MAX_CASE_ACTION_EVENTS_PER_CASE, 'Case-response omitted transition count');
340
361
  reviewStrings(action.historyLimitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response action history limitations');
362
+ const requestEvents = [];
341
363
  for (const candidateTransition of boundedReviewArray(action.transitions, MAX_CASE_ACTION_EVENTS_PER_ACTION, 'Case-response transitions')) {
342
364
  const transition = exactReviewRecord(candidateTransition, [
343
365
  'id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance',
344
366
  'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail',
345
367
  'originActionId', 'applied',
346
- ], 'Case-response transition');
368
+ ], 'Case-response transition', [...(version >= 5 ? ['evidenceRequest'] : []), ...(version >= 6 ? ['responseObjects', 'objectOutcome'] : []), ...(version >= 7 ? ['packetReceipt'] : [])]);
369
+ const receipt = readCaseDeliveryPacketReceipt(transition.packetReceipt);
370
+ assertDeliveryPacketReceipt(receipt, transition, action.actionId);
371
+ if (receipt && (receipt.caseId !== source.case.id || receipt.target !== source.case.domain
372
+ || receipt.recipient !== action.recipient))
373
+ throw new TypeError('The delivery receipt must retain the same Case, target and recipient.');
374
+ const responseObjects = readCaseResponseObjects(transition.responseObjects);
375
+ const objectOutcome = readCaseResponseObjectOutcome(transition.objectOutcome);
376
+ if (objectOutcome) {
377
+ if (!responseObjects?.length || !['submitted', 'acknowledged', 'terminal'].includes(String(transition.nextState))
378
+ || !['analyst', 'provider'].includes(String(transition.sourceClass))
379
+ || transition.previousState === 'authorised')
380
+ throw new TypeError('Case-response object outcome requires an explicit affected subset after submission.');
381
+ responseObjects.forEach(object => assertCaseObjectOutcome(objectOutcome, object));
382
+ }
383
+ const evidenceRequest = readCaseEvidenceRequest(transition.evidenceRequest);
384
+ assertEvidenceRequestEvent(evidenceRequest, transition);
385
+ requestEvents.push({ id: transition.id, ...(evidenceRequest ? { evidenceRequest } : {}) });
347
386
  for (const key of ['id', 'occurredAt', 'provenance']) {
348
387
  reviewText(transition[key], MAX_RESPONSE_VALUE_LENGTH, `Case-response transition ${key}`);
349
388
  }
@@ -358,6 +397,7 @@ export function validateCaseResponseReviewInputs(value) {
358
397
  throw new TypeError('Case-response transition applied state is invalid.');
359
398
  reviewStrings(transition.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response transition limitations');
360
399
  }
400
+ assertEvidenceRequestHistory(requestEvents, Number(action.historyOmitted) > 0);
361
401
  }
362
402
  if (currentLineageActionIds
363
403
  && (currentLineageActionIds.length !== escalationActionIds.length
@@ -365,7 +405,19 @@ export function validateCaseResponseReviewInputs(value) {
365
405
  throw new TypeError('Case-response escalation history must match the selected action lineage in order.');
366
406
  }
367
407
  reviewCount(source.escalationHistoryOmitted, MAX_CASE_ACTIONS, 'Case-response omitted action count');
408
+ if (version >= 5)
409
+ assertPacketAmendmentSelection(source.escalationHistory.map(action => ({
410
+ id: action.actionId, history: action.transitions, originActionId: action.originActionId, amendment: action.amendment,
411
+ })), currentLineageActionIds?.[0] ?? null, new Set(source.selectedEvidence.map(pin => pin.id)));
412
+ if (version >= 7) {
413
+ const actions = source.escalationHistory.map(action => ({ ...action, id: action.actionId, history: action.transitions }));
414
+ const selected = actions.find(action => action.id === currentLineageActionIds?.[0]);
415
+ if (selected?.correction)
416
+ correctionDelivery(actions, selected, { caseId: source.case.id,
417
+ target: source.case.domain, profile: profile.id,
418
+ selectedPinIds: new Set(source.selectedEvidence.map(pin => pin.id)) });
419
+ }
368
420
  reviewStrings(source.escalationHistoryLimitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response escalation limitations');
369
- validateReviewLifecycle(source.responseLifecycle);
421
+ validateReviewLifecycle(source.responseLifecycle, version);
370
422
  return recursivelyFreezeReviewValue(structuredClone(source));
371
423
  }
@@ -1,4 +1,4 @@
1
- import { CASE_SCHEMA_VERSION, MAX_CASE_STORE_BYTES, } from "./case-record-model.mjs";
1
+ import { CASE_SCHEMA_VERSION, MAX_CASE_STORE_BYTES } from "../contracts/case-portability.mjs";
2
2
  import { normalizeCaseStore } from "./case-migration-model.mjs";
3
3
  import { projectCaseForDurableWrite } from "./case-record-projection.mjs";
4
4
  /** The exact versioned string persisted by the browser storage adapter. */