@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.
- package/DISCLOSURE +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +348 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- 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
|
-
|
|
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
|
|
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
|
-
:
|
|
142
|
-
responseLifecycle.latestObservedChangeAt
|
|
143
|
-
? `Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
|
|
144
|
-
:
|
|
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 {
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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 "
|
|
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',
|
|
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
|
|
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. */
|