@slicedearth/whoisleuth-cli 2.3.0 → 2.4.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/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
import { renderCaseResponsePacket } from "./case-response-packet-render.mjs";
|
|
2
|
+
import { responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
|
|
1
3
|
// Pure abuse-evidence packet builder. It creates local review artifacts only:
|
|
2
4
|
// no network requests, mailto links, submissions, or provider side effects.
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { canonicalArtifactJsonV2,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { assertBoundedJsonStructure, } from "../../lib/bounded-json.mjs";
|
|
6
|
+
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
|
+
import { SORTED_JSON_V2, canonicalArtifactJsonV2, } from "../evidence/artifact-integrity.mjs";
|
|
8
|
+
import { normalizeExplicitIsoTimestamp, } from "../evidence/observation.mjs";
|
|
9
|
+
import { latestCaseEvidence, } from "./case-evidence-model.mjs";
|
|
10
|
+
import {} from "./case-model.mjs";
|
|
11
|
+
import { caseDispositionSupportsDefensiveResponse, } from "./case-record-operations.mjs";
|
|
12
|
+
import { buildCaseActionOutcomeSummary, buildCaseResponseLifecycleSummary, } from "./case-response-model.mjs";
|
|
13
|
+
import { RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_STATES, } from "./case-response-packet-vocabulary.mjs";
|
|
14
|
+
import { validateCaseResponseReviewInputs, } from "./case-response-review-inputs.mjs";
|
|
15
|
+
import { responseRouteFreshness, } from "./response-route-freshness.mjs";
|
|
16
|
+
export * from "./case-response-packet-vocabulary.mjs";
|
|
17
|
+
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";
|
|
9
20
|
export const CASE_RESPONSE_PREFLIGHT_EVIDENCE_SCOPE = Object.freeze({
|
|
10
21
|
version: 1,
|
|
11
22
|
owner: 'case',
|
|
@@ -19,44 +30,8 @@ export const CASE_RESPONSE_PREFLIGHT_EVIDENCE_SCOPE = Object.freeze({
|
|
|
19
30
|
'case_actions',
|
|
20
31
|
]),
|
|
21
32
|
lookupDecisionFacts: 'unavailable',
|
|
22
|
-
limitation: 'Lookup Decision Facts are transient and are not copied into
|
|
33
|
+
limitation: 'Lookup Decision Facts are transient and are not copied into saved Cases. Case-response preflight evaluates only explicit case-owned records and analyst-entered incident context; it does not reconstruct Decision Facts from weaker saved fields.',
|
|
23
34
|
});
|
|
24
|
-
export const RESPONSE_CONTACT_KINDS = [
|
|
25
|
-
'registrar',
|
|
26
|
-
'registry',
|
|
27
|
-
'network_hosting',
|
|
28
|
-
'security_txt',
|
|
29
|
-
'application_platform',
|
|
30
|
-
];
|
|
31
|
-
export const RESPONSE_PACKET_PROFILE_IDS = [
|
|
32
|
-
'registrar',
|
|
33
|
-
'registry',
|
|
34
|
-
'network_hosting',
|
|
35
|
-
'security_contact',
|
|
36
|
-
'application_platform',
|
|
37
|
-
'browser_blocklist',
|
|
38
|
-
'internal_soc',
|
|
39
|
-
];
|
|
40
|
-
export const RESPONSE_READINESS_STATES = ['complete', 'partial', 'stale', 'unavailable', 'not_provided'];
|
|
41
|
-
export const RESPONSE_READINESS_ROW_IDS = [
|
|
42
|
-
'observed_behaviour',
|
|
43
|
-
'exact_url',
|
|
44
|
-
'observation_time',
|
|
45
|
-
'capture_provenance',
|
|
46
|
-
'infrastructure_responsibility',
|
|
47
|
-
'recipient_route',
|
|
48
|
-
'authority_review',
|
|
49
|
-
'selected_evidence',
|
|
50
|
-
'contradictions',
|
|
51
|
-
'source_limitations',
|
|
52
|
-
];
|
|
53
|
-
export const RESPONSE_AUTHORISATION_CONFIRMATION_IDS = [
|
|
54
|
-
'selectedEvidence',
|
|
55
|
-
'recipientScope',
|
|
56
|
-
'privacyRedactions',
|
|
57
|
-
'analystAuthority',
|
|
58
|
-
'evidenceFreshness',
|
|
59
|
-
];
|
|
60
35
|
export const RESPONSE_PACKET_PROFILES = Object.freeze([
|
|
61
36
|
{
|
|
62
37
|
id: 'registrar',
|
|
@@ -157,11 +132,7 @@ export const RESPONSE_PACKET_PROFILES = Object.freeze([
|
|
|
157
132
|
followUpFields: ['Internal owner', 'Due date', 'Decision', 'Control or escalation outcome'],
|
|
158
133
|
},
|
|
159
134
|
]);
|
|
160
|
-
const CONTACT_KINDS = new Set(RESPONSE_CONTACT_KINDS);
|
|
161
135
|
const RESPONSE_PROFILE_IDS = new Set(RESPONSE_PACKET_PROFILE_IDS);
|
|
162
|
-
const PRE_PLATFORM_CONTACT_KINDS = new Set(RESPONSE_CONTACT_KINDS.filter((kind) => kind !== 'application_platform'));
|
|
163
|
-
const PRE_PLATFORM_PROFILE_IDS = new Set(RESPONSE_PACKET_PROFILE_IDS.filter((id) => id !== 'application_platform'));
|
|
164
|
-
const PRE_PLATFORM_ACTION_TYPES = CASE_ACTION_TYPES.filter((type) => type !== 'platform_report');
|
|
165
136
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
166
137
|
const CONTROL_REPLACE_RE = /[\u0000-\u001f\u007f]+/gu;
|
|
167
138
|
function text(value, maximum) {
|
|
@@ -173,10 +144,7 @@ function compareCodeUnits(left, right) {
|
|
|
173
144
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
174
145
|
}
|
|
175
146
|
function timestamp(value) {
|
|
176
|
-
|
|
177
|
-
return null;
|
|
178
|
-
const parsed = Date.parse(value);
|
|
179
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
147
|
+
return normalizeExplicitIsoTimestamp(value);
|
|
180
148
|
}
|
|
181
149
|
function normalizeExactUrl(value, rejectOverlong) {
|
|
182
150
|
if (typeof value !== 'string' || CONTROL_RE.test(value))
|
|
@@ -244,17 +212,6 @@ function actionContactKind(type) {
|
|
|
244
212
|
return 'application_platform';
|
|
245
213
|
return 'manual';
|
|
246
214
|
}
|
|
247
|
-
function routeFreshness(observedAt, reviewAfter, generatedAt) {
|
|
248
|
-
if (!observedAt)
|
|
249
|
-
return 'unknown';
|
|
250
|
-
const routeAge = Date.parse(generatedAt) - Date.parse(observedAt);
|
|
251
|
-
const reviewExpired = reviewAfter !== null && Date.parse(generatedAt) >= Date.parse(reviewAfter);
|
|
252
|
-
return routeAge < -MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS
|
|
253
|
-
|| reviewExpired
|
|
254
|
-
|| (reviewAfter === null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000)
|
|
255
|
-
? 'stale'
|
|
256
|
-
: 'current';
|
|
257
|
-
}
|
|
258
215
|
function selectedPacketAction(caseRecord, input) {
|
|
259
216
|
const actionId = typeof input.actionId === 'string' && /^[A-Za-z0-9_-]{1,64}$/u.test(input.actionId)
|
|
260
217
|
? input.actionId
|
|
@@ -321,7 +278,7 @@ function bindPacketRoute(caseRecord, input, generatedAt) {
|
|
|
321
278
|
source: text(action.contactSource, 120) || 'analyst supplied',
|
|
322
279
|
observedAt,
|
|
323
280
|
reviewAfter,
|
|
324
|
-
freshness:
|
|
281
|
+
freshness: responseRouteFreshness(observedAt, reviewAfter, generatedAt),
|
|
325
282
|
limitations: normalizeLimitations(action.contactLimitations),
|
|
326
283
|
};
|
|
327
284
|
const contacts = kind === 'manual' ? [] : [{
|
|
@@ -359,7 +316,9 @@ function normalizeSelectedEvidence(caseRecord, value) {
|
|
|
359
316
|
id: pin.id,
|
|
360
317
|
label: text(pin.label, 80),
|
|
361
318
|
source: text(pin.source, 120),
|
|
362
|
-
|
|
319
|
+
...(pin.observationHostname ? { observationHostname: pin.observationHostname } : {}),
|
|
320
|
+
...(pin.webObservationMode ? { webObservationMode: pin.webObservationMode } : {}),
|
|
321
|
+
observedAt: timestamp(pin.observedAt),
|
|
363
322
|
completeness: text(pin.completeness, 40),
|
|
364
323
|
limitations: normalizeLimitations(pin.limitations),
|
|
365
324
|
}));
|
|
@@ -392,10 +351,10 @@ function readinessOverride(value, key) {
|
|
|
392
351
|
limitations: normalizeLimitations(item.limitations),
|
|
393
352
|
};
|
|
394
353
|
}
|
|
395
|
-
function buildResponseReadiness(
|
|
354
|
+
function buildResponseReadiness(input, generatedAt, contacts, recipientRoute, selectedEvidence, contradictions) {
|
|
396
355
|
const profile = responsePacketProfile(input.profile);
|
|
397
356
|
const urls = normalizeUrls(input.abusiveUrls);
|
|
398
|
-
const observedAt = timestamp(input.observedAt)
|
|
357
|
+
const observedAt = timestamp(input.observedAt);
|
|
399
358
|
const age = observedAt ? observationAge(observedAt, generatedAt) : null;
|
|
400
359
|
const infrastructure = readinessOverride(input.readiness, 'infrastructureResponsibility');
|
|
401
360
|
const authority = readinessOverride(input.readiness, 'authorityReview');
|
|
@@ -426,7 +385,10 @@ function buildResponseReadiness(caseRecord, input, generatedAt, contacts, recipi
|
|
|
426
385
|
{
|
|
427
386
|
id: 'capture_provenance', label: 'Capture provenance',
|
|
428
387
|
state: !selectedEvidence.length ? 'unavailable' : selectedEvidence.every((item) => item.source && item.observedAt) ? 'complete' : 'partial',
|
|
429
|
-
detail: selectedEvidence.length ? '
|
|
388
|
+
detail: !selectedEvidence.length ? 'No selected evidence is available for capture-provenance review.'
|
|
389
|
+
: selectedEvidence.every((item) => item.source && item.observedAt)
|
|
390
|
+
? 'Selected evidence retains source and observation metadata.'
|
|
391
|
+
: 'Selected evidence is retained, but some source or observation-time metadata is unavailable.',
|
|
430
392
|
requiredForAuthorisation: true,
|
|
431
393
|
limitations: selectedEvidence.flatMap((item) => item.limitations).slice(0, 8),
|
|
432
394
|
},
|
|
@@ -496,7 +458,7 @@ export function buildCaseResponseReadiness(caseRecord, input, generatedAt = new
|
|
|
496
458
|
const normalizedGeneratedAt = timestamp(generatedAt) || new Date().toISOString();
|
|
497
459
|
const binding = bindPacketRoute(caseRecord, input, normalizedGeneratedAt);
|
|
498
460
|
const selectedEvidence = normalizeSelectedEvidence(caseRecord, input.selectedEvidencePinIds);
|
|
499
|
-
return buildResponseReadiness(
|
|
461
|
+
return buildResponseReadiness(input, normalizedGeneratedAt, binding.contacts, binding.recipientRoute, selectedEvidence, normalizeContradictions(caseRecord));
|
|
500
462
|
}
|
|
501
463
|
function normalizeArtefactReferences(value) {
|
|
502
464
|
if (!Array.isArray(value))
|
|
@@ -543,7 +505,7 @@ export function buildResponsePacketProfilePreview(caseRecord, input) {
|
|
|
543
505
|
const binding = bindPacketRoute(caseRecord, input, caseRecord.updatedAt);
|
|
544
506
|
const missingEvidence = [
|
|
545
507
|
...(!normalizeUrls(input.abusiveUrls).length ? ['At least one exact HTTP(S) URL'] : []),
|
|
546
|
-
...(!timestamp(input.observedAt)
|
|
508
|
+
...(!timestamp(input.observedAt) ? ['Observation time'] : []),
|
|
547
509
|
...(!normalizeSelectedEvidence(caseRecord, input.selectedEvidencePinIds).length ? ['Explicitly selected evidence pin'] : []),
|
|
548
510
|
...(profile.id !== 'internal_soc' && binding.actionBinding.state !== 'selected'
|
|
549
511
|
? ['Selected retained Case action']
|
|
@@ -569,8 +531,7 @@ export function buildResponsePacketProfilePreview(caseRecord, input) {
|
|
|
569
531
|
};
|
|
570
532
|
}
|
|
571
533
|
export function buildCaseResponsePreflight(caseRecord, input, generatedAt = new Date().toISOString()) {
|
|
572
|
-
const
|
|
573
|
-
const observedAt = timestamp(input.observedAt) || latestEvidence?.capturedAt || null;
|
|
534
|
+
const observedAt = timestamp(input.observedAt);
|
|
574
535
|
const normalizedGeneratedAt = timestamp(generatedAt) || new Date().toISOString();
|
|
575
536
|
const binding = bindPacketRoute(caseRecord, input, normalizedGeneratedAt);
|
|
576
537
|
const urls = normalizeUrls(input.abusiveUrls);
|
|
@@ -788,6 +749,7 @@ function normalizeResponseLifecycle(caseRecord, actionIds, lineageComplete) {
|
|
|
788
749
|
'Provider workflow outcomes and independently observed technical effects are separate point-in-time records.',
|
|
789
750
|
'A provider acknowledgement, terminal state, or reported resolution never becomes independently observed remediation, absence, or safety.',
|
|
790
751
|
'Times are withheld when the corresponding typed event is missing or ambiguous.',
|
|
752
|
+
...(!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.'] : []),
|
|
791
753
|
...(!lineageComplete ? ['Provider-outcome time is withheld because the selected action origin lineage is incomplete.'] : []),
|
|
792
754
|
],
|
|
793
755
|
};
|
|
@@ -816,7 +778,7 @@ export function buildCaseResponseReviewInputs(caseRecord, input, generatedAt) {
|
|
|
816
778
|
const selectedEvidence = normalizeSelectedEvidence(caseRecord, input.selectedEvidencePinIds);
|
|
817
779
|
const contradictions = normalizeContradictions(caseRecord);
|
|
818
780
|
const binding = bindPacketRoute(caseRecord, input, generatedAt);
|
|
819
|
-
const readiness = buildResponseReadiness(
|
|
781
|
+
const readiness = buildResponseReadiness(input, generatedAt, binding.contacts, binding.recipientRoute, selectedEvidence, contradictions);
|
|
820
782
|
const escalation = normalizeActionHistory(caseRecord, input);
|
|
821
783
|
return {
|
|
822
784
|
contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
|
|
@@ -843,7 +805,7 @@ export function buildCaseResponseReviewInputs(caseRecord, input, generatedAt) {
|
|
|
843
805
|
affectedParty: text(input.affectedParty, MAX_AFFECTED_PARTY_LENGTH),
|
|
844
806
|
abusiveUrls: normalizeUrls(input.abusiveUrls, true),
|
|
845
807
|
observedHarm: text(input.observedHarm, MAX_RESPONSE_HARM_LENGTH),
|
|
846
|
-
observedAt: timestamp(input.observedAt)
|
|
808
|
+
observedAt: timestamp(input.observedAt),
|
|
847
809
|
},
|
|
848
810
|
contacts: binding.contacts,
|
|
849
811
|
recipientRoute: binding.recipientRoute,
|
|
@@ -858,359 +820,6 @@ export function buildCaseResponseReviewInputs(caseRecord, input, generatedAt) {
|
|
|
858
820
|
responseLifecycle: normalizeResponseLifecycle(caseRecord, escalation.actionBinding.lineageActionIds, escalation.lineageComplete),
|
|
859
821
|
};
|
|
860
822
|
}
|
|
861
|
-
const CASE_RESPONSE_REVIEW_INPUT_KEYS = Object.freeze([
|
|
862
|
-
'contract',
|
|
863
|
-
'version',
|
|
864
|
-
'profile',
|
|
865
|
-
'case',
|
|
866
|
-
'incident',
|
|
867
|
-
'contacts',
|
|
868
|
-
'recipientRoute',
|
|
869
|
-
'actionBinding',
|
|
870
|
-
'selectedEvidence',
|
|
871
|
-
'contradictions',
|
|
872
|
-
'readiness',
|
|
873
|
-
'artefactReferences',
|
|
874
|
-
'escalationHistory',
|
|
875
|
-
'escalationHistoryOmitted',
|
|
876
|
-
'escalationHistoryLimitations',
|
|
877
|
-
'responseLifecycle',
|
|
878
|
-
]);
|
|
879
|
-
const PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS = Object.freeze(CASE_RESPONSE_REVIEW_INPUT_KEYS.filter((key) => key !== 'recipientRoute' && key !== 'actionBinding'));
|
|
880
|
-
function exactReviewRecord(value, keys, label) {
|
|
881
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
882
|
-
throw new TypeError(`${label} must be an exact object.`);
|
|
883
|
-
}
|
|
884
|
-
const ownKeys = Reflect.ownKeys(value);
|
|
885
|
-
if (ownKeys.length !== keys.length
|
|
886
|
-
|| ownKeys.some((key) => typeof key !== 'string' || !keys.includes(key))) {
|
|
887
|
-
throw new TypeError(`${label} contains an undeclared field.`);
|
|
888
|
-
}
|
|
889
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
890
|
-
if (keys.some((key) => !descriptors[key] || !Object.hasOwn(descriptors[key], 'value'))) {
|
|
891
|
-
throw new TypeError(`${label} must not contain accessors.`);
|
|
892
|
-
}
|
|
893
|
-
return value;
|
|
894
|
-
}
|
|
895
|
-
function boundedReviewArray(value, maximum, label) {
|
|
896
|
-
if (!Array.isArray(value) || value.length > maximum) {
|
|
897
|
-
throw new TypeError(`${label} exceeds its declared array bound.`);
|
|
898
|
-
}
|
|
899
|
-
const ownKeys = Reflect.ownKeys(value).filter((key) => key !== 'length');
|
|
900
|
-
if (ownKeys.length !== value.length
|
|
901
|
-
|| ownKeys.some((key, index) => key !== String(index))) {
|
|
902
|
-
throw new TypeError(`${label} must be a dense ordinary array.`);
|
|
903
|
-
}
|
|
904
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
905
|
-
if (ownKeys.some((key) => typeof key !== 'string' || !Object.hasOwn(descriptors[key], 'value'))) {
|
|
906
|
-
throw new TypeError(`${label} must not contain accessors.`);
|
|
907
|
-
}
|
|
908
|
-
return value;
|
|
909
|
-
}
|
|
910
|
-
function reviewText(value, maximum, label, nullable = false) {
|
|
911
|
-
if (nullable && value === null)
|
|
912
|
-
return null;
|
|
913
|
-
if (typeof value !== 'string' || value.length > maximum || CONTROL_RE.test(value)) {
|
|
914
|
-
throw new TypeError(`${label} is not a bounded text value.`);
|
|
915
|
-
}
|
|
916
|
-
return value;
|
|
917
|
-
}
|
|
918
|
-
function reviewStrings(value, maximum, itemMaximum, label) {
|
|
919
|
-
const items = boundedReviewArray(value, maximum, label);
|
|
920
|
-
for (const item of items)
|
|
921
|
-
reviewText(item, itemMaximum, `${label} item`);
|
|
922
|
-
return items;
|
|
923
|
-
}
|
|
924
|
-
function reviewCount(value, maximum, label) {
|
|
925
|
-
if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
|
926
|
-
throw new TypeError(`${label} is not a bounded count.`);
|
|
927
|
-
}
|
|
928
|
-
return value;
|
|
929
|
-
}
|
|
930
|
-
function reviewEnum(value, values, label) {
|
|
931
|
-
if (typeof value !== 'string' || !values.includes(value)) {
|
|
932
|
-
throw new TypeError(`${label} contains an unsupported value.`);
|
|
933
|
-
}
|
|
934
|
-
return value;
|
|
935
|
-
}
|
|
936
|
-
function reviewNullableEnum(value, values, label) {
|
|
937
|
-
return value === null ? null : reviewEnum(value, values, label);
|
|
938
|
-
}
|
|
939
|
-
function validateReviewLifecycle(value) {
|
|
940
|
-
const lifecycle = exactReviewRecord(value, [
|
|
941
|
-
'providerOutcomeState', 'latestProviderOutcome', 'observedChangeState',
|
|
942
|
-
'latestObservedEffect', 'latestObservedChangeAt', 'closure', 'limitations',
|
|
943
|
-
], 'Case-response review lifecycle');
|
|
944
|
-
reviewEnum(lifecycle.providerOutcomeState, ['available', 'missing', 'ambiguous'], 'Case-response provider-outcome state');
|
|
945
|
-
reviewEnum(lifecycle.observedChangeState, ['available', 'missing', 'ambiguous'], 'Case-response observed-change state');
|
|
946
|
-
reviewText(lifecycle.latestObservedChangeAt, 64, 'Case-response latest observed change', true);
|
|
947
|
-
reviewStrings(lifecycle.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response lifecycle limitations');
|
|
948
|
-
if (lifecycle.latestProviderOutcome !== null) {
|
|
949
|
-
const outcome = exactReviewRecord(lifecycle.latestProviderOutcome, [
|
|
950
|
-
'actionId', 'eventId', 'outcome', 'occurredAt', 'reference',
|
|
951
|
-
], 'Case-response latest provider outcome');
|
|
952
|
-
reviewText(outcome.actionId, 64, 'Case-response provider action id');
|
|
953
|
-
reviewText(outcome.eventId, 64, 'Case-response provider event id');
|
|
954
|
-
reviewEnum(outcome.outcome, CASE_PROVIDER_OUTCOMES, 'Case-response provider outcome');
|
|
955
|
-
reviewText(outcome.occurredAt, 64, 'Case-response provider outcome time');
|
|
956
|
-
reviewText(outcome.reference, MAX_RESPONSE_REFERENCE_LENGTH, 'Case-response provider reference', true);
|
|
957
|
-
}
|
|
958
|
-
if (lifecycle.latestObservedEffect !== null) {
|
|
959
|
-
const effect = exactReviewRecord(lifecycle.latestObservedEffect, [
|
|
960
|
-
'reviewId', 'state', 'observedAt', 'sourceClass', 'source',
|
|
961
|
-
], 'Case-response latest observed effect');
|
|
962
|
-
reviewText(effect.reviewId, 64, 'Case-response observed-effect review id');
|
|
963
|
-
reviewEnum(effect.state, CASE_OBSERVED_EFFECT_STATES, 'Case-response observed-effect state');
|
|
964
|
-
reviewText(effect.observedAt, 64, 'Case-response observed-effect time');
|
|
965
|
-
reviewEnum(effect.sourceClass, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, 'Case-response observed-effect source class');
|
|
966
|
-
reviewText(effect.source, MAX_RESPONSE_VALUE_LENGTH, 'Case-response observed-effect source');
|
|
967
|
-
}
|
|
968
|
-
if (lifecycle.closure !== null) {
|
|
969
|
-
const closure = exactReviewRecord(lifecycle.closure, [
|
|
970
|
-
'id', 'reason', 'createdAt', 'limitations',
|
|
971
|
-
], 'Case-response latest closure');
|
|
972
|
-
reviewText(closure.id, 64, 'Case-response closure id');
|
|
973
|
-
reviewEnum(closure.reason, CASE_CLOSURE_REASONS, 'Case-response closure reason');
|
|
974
|
-
reviewText(closure.createdAt, 64, 'Case-response closure time');
|
|
975
|
-
reviewStrings(closure.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response closure limitations');
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
function recursivelyFreezeReviewValue(value) {
|
|
979
|
-
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
980
|
-
return value;
|
|
981
|
-
for (const child of Object.values(value)) {
|
|
982
|
-
recursivelyFreezeReviewValue(child);
|
|
983
|
-
}
|
|
984
|
-
return Object.freeze(value);
|
|
985
|
-
}
|
|
986
|
-
export function validateCaseResponseReviewInputs(value) {
|
|
987
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
988
|
-
throw new TypeError('Case-response review inputs must be an exact object.');
|
|
989
|
-
}
|
|
990
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
991
|
-
if (!descriptors.contract || !Object.hasOwn(descriptors.contract, 'value')
|
|
992
|
-
|| !descriptors.version || !Object.hasOwn(descriptors.version, 'value')) {
|
|
993
|
-
throw new TypeError('Case-response review inputs must not contain accessors.');
|
|
994
|
-
}
|
|
995
|
-
const version = descriptors.version.value;
|
|
996
|
-
const current = version === CASE_RESPONSE_REVIEW_INPUTS_VERSION;
|
|
997
|
-
const hasActionBinding = version === LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION || current;
|
|
998
|
-
const publishedV2 = version === PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION;
|
|
999
|
-
if (descriptors.contract.value !== CASE_RESPONSE_REVIEW_INPUTS_SCHEMA || (!current && !hasActionBinding && !publishedV2)) {
|
|
1000
|
-
throw new TypeError('Case-response review inputs contain an unsupported version, shape, or bound.');
|
|
1001
|
-
}
|
|
1002
|
-
const source = exactReviewRecord(value, hasActionBinding ? CASE_RESPONSE_REVIEW_INPUT_KEYS : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS, 'Case-response review inputs');
|
|
1003
|
-
const profile = exactReviewRecord(source.profile, [
|
|
1004
|
-
'id', 'label', 'audience', 'subject', 'checklist', 'includedEvidence',
|
|
1005
|
-
'excludedEvidence', 'redactions',
|
|
1006
|
-
], 'Case-response review profile');
|
|
1007
|
-
const supportedProfileIds = current ? RESPONSE_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS;
|
|
1008
|
-
if (typeof profile.id !== 'string' || !supportedProfileIds.has(profile.id)) {
|
|
1009
|
-
throw new TypeError('Case-response review profile is unsupported.');
|
|
1010
|
-
}
|
|
1011
|
-
for (const key of ['label', 'audience', 'subject']) {
|
|
1012
|
-
reviewText(profile[key], 500, `Case-response profile ${key}`);
|
|
1013
|
-
}
|
|
1014
|
-
for (const key of ['checklist', 'includedEvidence', 'excludedEvidence', 'redactions']) {
|
|
1015
|
-
reviewStrings(profile[key], 32, 500, `Case-response profile ${key}`);
|
|
1016
|
-
}
|
|
1017
|
-
const caseRecord = exactReviewRecord(source.case, [
|
|
1018
|
-
'id', 'domain', 'status', 'disposition', 'updatedAt',
|
|
1019
|
-
], 'Case-response review Case');
|
|
1020
|
-
reviewText(caseRecord.id, 64, 'Case-response Case id');
|
|
1021
|
-
reviewText(caseRecord.domain, 253, 'Case-response Case domain');
|
|
1022
|
-
reviewText(caseRecord.status, 64, 'Case-response Case status');
|
|
1023
|
-
reviewText(caseRecord.disposition, 64, 'Case-response Case disposition');
|
|
1024
|
-
reviewText(caseRecord.updatedAt, 64, 'Case-response Case update time');
|
|
1025
|
-
const incident = exactReviewRecord(source.incident, [
|
|
1026
|
-
'category', 'affectedParty', 'abusiveUrls', 'observedHarm', 'observedAt',
|
|
1027
|
-
], 'Case-response review incident');
|
|
1028
|
-
reviewText(incident.category, MAX_ABUSE_CATEGORY_LENGTH, 'Case-response category');
|
|
1029
|
-
reviewText(incident.affectedParty, MAX_AFFECTED_PARTY_LENGTH, 'Case-response affected party');
|
|
1030
|
-
reviewStrings(incident.abusiveUrls, MAX_ABUSIVE_URLS, MAX_EXACT_URL_LENGTH, 'Case-response abusive URLs');
|
|
1031
|
-
reviewText(incident.observedHarm, MAX_RESPONSE_HARM_LENGTH, 'Case-response observed harm');
|
|
1032
|
-
reviewText(incident.observedAt, 64, 'Case-response observation time', true);
|
|
1033
|
-
for (const candidate of boundedReviewArray(source.contacts, MAX_RESPONSE_CONTACTS, 'Case-response contacts')) {
|
|
1034
|
-
const contact = exactReviewRecord(candidate, [
|
|
1035
|
-
'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
1036
|
-
], 'Case-response contact');
|
|
1037
|
-
const supportedContactKinds = current ? CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS;
|
|
1038
|
-
if (typeof contact.kind !== 'string' || !supportedContactKinds.has(contact.kind)
|
|
1039
|
-
|| typeof contact.freshness !== 'string'
|
|
1040
|
-
|| !['current', 'stale', 'unknown'].includes(contact.freshness)) {
|
|
1041
|
-
throw new TypeError('Case-response contact contains an unsupported enum.');
|
|
1042
|
-
}
|
|
1043
|
-
reviewText(contact.contact, 320, 'Case-response contact value');
|
|
1044
|
-
reviewText(contact.source, 120, 'Case-response contact source');
|
|
1045
|
-
reviewText(contact.observedAt, 64, 'Case-response contact observation time', true);
|
|
1046
|
-
if (current)
|
|
1047
|
-
reviewText(contact.reviewAfter, 64, 'Case-response contact review deadline', true);
|
|
1048
|
-
reviewStrings(contact.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response contact limitations');
|
|
1049
|
-
}
|
|
1050
|
-
if (hasActionBinding && source.recipientRoute !== null) {
|
|
1051
|
-
const route = exactReviewRecord(source.recipientRoute, [
|
|
1052
|
-
'actionId', 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
1053
|
-
], 'Case-response recipient route');
|
|
1054
|
-
reviewText(route.actionId, 64, 'Case-response recipient action id');
|
|
1055
|
-
reviewEnum(route.kind, [...(current ? RESPONSE_CONTACT_KINDS : [...PRE_PLATFORM_CONTACT_KINDS]), 'manual'], 'Case-response recipient kind');
|
|
1056
|
-
reviewText(route.contact, 320, 'Case-response recipient value');
|
|
1057
|
-
reviewText(route.source, 120, 'Case-response recipient source');
|
|
1058
|
-
reviewText(route.observedAt, 64, 'Case-response recipient observation time', true);
|
|
1059
|
-
if (current)
|
|
1060
|
-
reviewText(route.reviewAfter, 64, 'Case-response recipient review deadline', true);
|
|
1061
|
-
reviewEnum(route.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
|
|
1062
|
-
reviewStrings(route.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response recipient limitations');
|
|
1063
|
-
}
|
|
1064
|
-
let currentLineageActionIds = null;
|
|
1065
|
-
if (hasActionBinding) {
|
|
1066
|
-
const actionBinding = exactReviewRecord(source.actionBinding, [
|
|
1067
|
-
'state', 'selectedActionId', 'lineageActionIds', 'limitations',
|
|
1068
|
-
], 'Case-response action binding');
|
|
1069
|
-
const bindingState = reviewEnum(actionBinding.state, ['selected', 'not_selected'], 'Case-response action binding state');
|
|
1070
|
-
const selectedActionId = reviewText(actionBinding.selectedActionId, 64, 'Case-response selected action id', true);
|
|
1071
|
-
const lineageActionIds = reviewStrings(actionBinding.lineageActionIds, MAX_RESPONSE_ACTION_HISTORY, 64, 'Case-response action lineage');
|
|
1072
|
-
currentLineageActionIds = lineageActionIds;
|
|
1073
|
-
if ((bindingState === 'selected') !== Boolean(selectedActionId)
|
|
1074
|
-
|| (selectedActionId !== null && lineageActionIds[0] !== selectedActionId)
|
|
1075
|
-
|| new Set(lineageActionIds).size !== lineageActionIds.length) {
|
|
1076
|
-
throw new TypeError('Case-response action binding is inconsistent.');
|
|
1077
|
-
}
|
|
1078
|
-
reviewStrings(actionBinding.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response action-binding limitations');
|
|
1079
|
-
if (source.recipientRoute !== null
|
|
1080
|
-
&& source.recipientRoute.actionId !== selectedActionId) {
|
|
1081
|
-
throw new TypeError('Case-response recipient route is not bound to the selected action.');
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
for (const candidate of boundedReviewArray(source.selectedEvidence, MAX_RESPONSE_SELECTED_EVIDENCE, 'Case-response selected evidence')) {
|
|
1085
|
-
const evidence = exactReviewRecord(candidate, [
|
|
1086
|
-
'id', 'label', 'source', 'observedAt', 'completeness', 'limitations',
|
|
1087
|
-
], 'Case-response selected evidence item');
|
|
1088
|
-
reviewText(evidence.id, 64, 'Case-response evidence id');
|
|
1089
|
-
reviewText(evidence.label, 80, 'Case-response evidence label');
|
|
1090
|
-
reviewText(evidence.source, 120, 'Case-response evidence source');
|
|
1091
|
-
reviewText(evidence.observedAt, 64, 'Case-response evidence observation time');
|
|
1092
|
-
reviewEnum(evidence.completeness, CASE_PIN_COMPLETENESS, 'Case-response evidence completeness');
|
|
1093
|
-
reviewStrings(evidence.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response evidence limitations');
|
|
1094
|
-
}
|
|
1095
|
-
for (const candidate of boundedReviewArray(source.contradictions, MAX_RESPONSE_CONTRADICTIONS, 'Case-response contradictions')) {
|
|
1096
|
-
const contradiction = exactReviewRecord(candidate, [
|
|
1097
|
-
'id', 'statement', 'state', 'limitations',
|
|
1098
|
-
], 'Case-response contradiction');
|
|
1099
|
-
reviewText(contradiction.id, 64, 'Case-response contradiction id');
|
|
1100
|
-
reviewText(contradiction.statement, 2_000, 'Case-response contradiction statement');
|
|
1101
|
-
reviewEnum(contradiction.state, CASE_ASSERTION_STATES, 'Case-response contradiction state');
|
|
1102
|
-
reviewStrings(contradiction.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response contradiction limitations');
|
|
1103
|
-
}
|
|
1104
|
-
const readiness = exactReviewRecord(source.readiness, [
|
|
1105
|
-
'profileId', 'rows', 'counts', 'limitations',
|
|
1106
|
-
], 'Case-response readiness');
|
|
1107
|
-
if (typeof readiness.profileId !== 'string' || !supportedProfileIds.has(readiness.profileId)) {
|
|
1108
|
-
throw new TypeError('Case-response readiness profile is unsupported.');
|
|
1109
|
-
}
|
|
1110
|
-
const readinessRows = boundedReviewArray(readiness.rows, RESPONSE_READINESS_ROW_IDS.length, 'Case-response readiness rows');
|
|
1111
|
-
const readinessIds = new Set();
|
|
1112
|
-
const readinessCounts = Object.fromEntries(RESPONSE_READINESS_STATES.map((state) => [state, 0]));
|
|
1113
|
-
for (const candidate of readinessRows) {
|
|
1114
|
-
const row = exactReviewRecord(candidate, [
|
|
1115
|
-
'id', 'label', 'state', 'detail', 'requiredForAuthorisation', 'limitations',
|
|
1116
|
-
], 'Case-response readiness row');
|
|
1117
|
-
if (typeof row.id !== 'string'
|
|
1118
|
-
|| !RESPONSE_READINESS_ROW_IDS.includes(row.id)
|
|
1119
|
-
|| readinessIds.has(row.id)
|
|
1120
|
-
|| typeof row.state !== 'string'
|
|
1121
|
-
|| !RESPONSE_READINESS_STATES.includes(row.state)
|
|
1122
|
-
|| typeof row.requiredForAuthorisation !== 'boolean') {
|
|
1123
|
-
throw new TypeError('Case-response readiness row is unsupported or duplicated.');
|
|
1124
|
-
}
|
|
1125
|
-
readinessIds.add(row.id);
|
|
1126
|
-
readinessCounts[row.state] += 1;
|
|
1127
|
-
reviewText(row.label, 120, 'Case-response readiness label');
|
|
1128
|
-
reviewText(row.detail, 500, 'Case-response readiness detail');
|
|
1129
|
-
reviewStrings(row.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response readiness limitations');
|
|
1130
|
-
}
|
|
1131
|
-
const counts = exactReviewRecord(readiness.counts, RESPONSE_READINESS_STATES, 'Case-response readiness counts');
|
|
1132
|
-
for (const state of RESPONSE_READINESS_STATES) {
|
|
1133
|
-
if (reviewCount(counts[state], RESPONSE_READINESS_ROW_IDS.length, `Case-response readiness ${state} count`) !== readinessCounts[state]) {
|
|
1134
|
-
throw new TypeError('Case-response readiness counts do not match the exact rows.');
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
reviewStrings(readiness.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response readiness limitations');
|
|
1138
|
-
for (const candidate of boundedReviewArray(source.artefactReferences, MAX_RESPONSE_ARTEFACT_REFERENCES, 'Case-response artefact references')) {
|
|
1139
|
-
const reference = exactReviewRecord(candidate, [
|
|
1140
|
-
'id', 'label', 'mediaType', 'capturedAt', 'source', 'digestSha256',
|
|
1141
|
-
'byteLength', 'limitations',
|
|
1142
|
-
], 'Case-response artefact reference');
|
|
1143
|
-
reviewText(reference.id, 64, 'Case-response artefact id');
|
|
1144
|
-
reviewText(reference.label, 120, 'Case-response artefact label');
|
|
1145
|
-
reviewText(reference.mediaType, 120, 'Case-response artefact media type');
|
|
1146
|
-
reviewText(reference.capturedAt, 64, 'Case-response artefact capture time');
|
|
1147
|
-
reviewText(reference.source, 120, 'Case-response artefact source');
|
|
1148
|
-
if (typeof reference.digestSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(reference.digestSha256)) {
|
|
1149
|
-
throw new TypeError('Case-response artefact digest is invalid.');
|
|
1150
|
-
}
|
|
1151
|
-
if (reference.byteLength !== null)
|
|
1152
|
-
reviewCount(reference.byteLength, 100 * 1024 * 1024, 'Case-response artefact byte length');
|
|
1153
|
-
reviewStrings(reference.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response artefact limitations');
|
|
1154
|
-
}
|
|
1155
|
-
const escalationActionIds = [];
|
|
1156
|
-
for (const candidate of boundedReviewArray(source.escalationHistory, MAX_RESPONSE_ACTION_HISTORY, 'Case-response escalation history')) {
|
|
1157
|
-
const action = exactReviewRecord(candidate, [
|
|
1158
|
-
'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
|
|
1159
|
-
...(hasActionBinding ? ['routeObservedAt'] : []),
|
|
1160
|
-
...(current ? ['routeReviewAfter'] : []),
|
|
1161
|
-
'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
|
|
1162
|
-
'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
|
|
1163
|
-
], 'Case-response escalation action');
|
|
1164
|
-
for (const key of ['actionId', 'recipient', 'contactSource', 'createdAt', 'updatedAt']) {
|
|
1165
|
-
reviewText(action[key], MAX_RESPONSE_VALUE_LENGTH, `Case-response action ${key}`);
|
|
1166
|
-
}
|
|
1167
|
-
if (escalationActionIds.includes(action.actionId)) {
|
|
1168
|
-
throw new TypeError('Case-response escalation action identities must be unique.');
|
|
1169
|
-
}
|
|
1170
|
-
escalationActionIds.push(action.actionId);
|
|
1171
|
-
if (hasActionBinding)
|
|
1172
|
-
reviewText(action.routeObservedAt, 64, 'Case-response action route observation time', true);
|
|
1173
|
-
if (current)
|
|
1174
|
-
reviewText(action.routeReviewAfter, 64, 'Case-response action route review deadline', true);
|
|
1175
|
-
reviewEnum(action.type, current ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
|
|
1176
|
-
reviewEnum(action.state, CASE_ACTION_STATES, 'Case-response action state');
|
|
1177
|
-
for (const key of ['reference', 'outcomeDetail', 'originActionId']) {
|
|
1178
|
-
reviewText(action[key], MAX_RESPONSE_VALUE_LENGTH, `Case-response action ${key}`, true);
|
|
1179
|
-
}
|
|
1180
|
-
reviewNullableEnum(action.providerOutcome, CASE_PROVIDER_OUTCOMES, 'Case-response action provider outcome');
|
|
1181
|
-
reviewCount(action.historyOmitted, MAX_CASE_ACTION_EVENTS_PER_CASE, 'Case-response omitted transition count');
|
|
1182
|
-
reviewStrings(action.historyLimitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response action history limitations');
|
|
1183
|
-
for (const candidateTransition of boundedReviewArray(action.transitions, MAX_CASE_ACTION_EVENTS_PER_ACTION, 'Case-response transitions')) {
|
|
1184
|
-
const transition = exactReviewRecord(candidateTransition, [
|
|
1185
|
-
'id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance',
|
|
1186
|
-
'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail',
|
|
1187
|
-
'originActionId', 'applied',
|
|
1188
|
-
], 'Case-response transition');
|
|
1189
|
-
for (const key of ['id', 'occurredAt', 'provenance']) {
|
|
1190
|
-
reviewText(transition[key], MAX_RESPONSE_VALUE_LENGTH, `Case-response transition ${key}`);
|
|
1191
|
-
}
|
|
1192
|
-
reviewNullableEnum(transition.previousState, CASE_ACTION_STATES, 'Case-response previous action state');
|
|
1193
|
-
reviewEnum(transition.nextState, CASE_ACTION_STATES, 'Case-response next action state');
|
|
1194
|
-
reviewEnum(transition.sourceClass, CASE_ACTION_EVENT_SOURCE_CLASSES, 'Case-response transition source class');
|
|
1195
|
-
for (const key of ['reference', 'evidencePinId', 'outcomeDetail', 'originActionId']) {
|
|
1196
|
-
reviewText(transition[key], MAX_RESPONSE_VALUE_LENGTH, `Case-response transition ${key}`, true);
|
|
1197
|
-
}
|
|
1198
|
-
reviewNullableEnum(transition.providerOutcome, CASE_PROVIDER_OUTCOMES, 'Case-response transition provider outcome');
|
|
1199
|
-
if (typeof transition.applied !== 'boolean')
|
|
1200
|
-
throw new TypeError('Case-response transition applied state is invalid.');
|
|
1201
|
-
reviewStrings(transition.limitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response transition limitations');
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
if (currentLineageActionIds
|
|
1205
|
-
&& (currentLineageActionIds.length !== escalationActionIds.length
|
|
1206
|
-
|| escalationActionIds.some((id, index) => currentLineageActionIds?.[index] !== id))) {
|
|
1207
|
-
throw new TypeError('Case-response escalation history must match the selected action lineage in order.');
|
|
1208
|
-
}
|
|
1209
|
-
reviewCount(source.escalationHistoryOmitted, MAX_CASE_ACTIONS, 'Case-response omitted action count');
|
|
1210
|
-
reviewStrings(source.escalationHistoryLimitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response escalation limitations');
|
|
1211
|
-
validateReviewLifecycle(source.responseLifecycle);
|
|
1212
|
-
return recursivelyFreezeReviewValue(structuredClone(source));
|
|
1213
|
-
}
|
|
1214
823
|
export async function buildCaseResponseReviewDigest(caseRecord, input, generatedAt = new Date().toISOString()) {
|
|
1215
824
|
const normalizedGeneratedAt = timestamp(generatedAt) || new Date().toISOString();
|
|
1216
825
|
return sha256(canonicalArtifactJsonV2(validateCaseResponseReviewInputs(buildCaseResponseReviewInputs(caseRecord, input, normalizedGeneratedAt))));
|
|
@@ -1280,27 +889,13 @@ export async function verifyCaseResponsePacketIntegrity(packet) {
|
|
|
1280
889
|
}
|
|
1281
890
|
return integrity.digestSha256 === await sha256(canonicalArtifactJsonV2(unsigned));
|
|
1282
891
|
}
|
|
1283
|
-
function escapeMarkdown(value) {
|
|
1284
|
-
return value.replace(/([\\`*_[\]<>|])/gu, '\\$1').replace(/\r?\n/gu, ' ');
|
|
1285
|
-
}
|
|
1286
|
-
function contactLabel(value) {
|
|
1287
|
-
if (value === 'network_hosting')
|
|
1288
|
-
return 'Observed endpoint network registration';
|
|
1289
|
-
if (value === 'security_txt')
|
|
1290
|
-
return 'security.txt';
|
|
1291
|
-
if (value === 'application_platform')
|
|
1292
|
-
return 'Application platform';
|
|
1293
|
-
if (value === 'manual')
|
|
1294
|
-
return 'Manual submission';
|
|
1295
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1296
|
-
}
|
|
1297
892
|
export async function buildCaseResponsePacket(caseRecord, input, generatedAt = new Date().toISOString()) {
|
|
1298
893
|
const category = text(input.category, MAX_ABUSE_CATEGORY_LENGTH);
|
|
1299
894
|
const affectedParty = text(input.affectedParty, MAX_AFFECTED_PARTY_LENGTH);
|
|
1300
895
|
const abusiveUrls = normalizeUrls(input.abusiveUrls, true);
|
|
1301
896
|
const observedHarm = text(input.observedHarm, MAX_RESPONSE_HARM_LENGTH);
|
|
1302
|
-
const latestEvidence = caseRecord
|
|
1303
|
-
const observedAt = timestamp(input.observedAt)
|
|
897
|
+
const latestEvidence = latestCaseEvidence(caseRecord);
|
|
898
|
+
const observedAt = timestamp(input.observedAt);
|
|
1304
899
|
if (!category || !affectedParty || !abusiveUrls.length || !observedHarm || !observedAt) {
|
|
1305
900
|
throw new Error('Category, affected party, at least one exact HTTP(S) URL, observed harm, and an observation time are required.');
|
|
1306
901
|
}
|
|
@@ -1395,151 +990,7 @@ export async function buildCaseResponsePacket(caseRecord, input, generatedAt = n
|
|
|
1395
990
|
digestSha256,
|
|
1396
991
|
},
|
|
1397
992
|
};
|
|
1398
|
-
|
|
1399
|
-
`# ${escapeMarkdown(profile.label)} packet`,
|
|
1400
|
-
'',
|
|
1401
|
-
`**Domain:** ${escapeMarkdown(caseRecord.domain)}`,
|
|
1402
|
-
`**Category:** ${escapeMarkdown(category)}`,
|
|
1403
|
-
`**Affected party:** ${escapeMarkdown(affectedParty)}`,
|
|
1404
|
-
`**Observed at (UTC):** ${observedAt}`,
|
|
1405
|
-
`**Generated at (UTC):** ${normalizedGeneratedAt}`,
|
|
1406
|
-
`**Audience:** ${escapeMarkdown(profile.audience)}`,
|
|
1407
|
-
`**Suggested subject:** ${escapeMarkdown(profile.subject)}`,
|
|
1408
|
-
'',
|
|
1409
|
-
'## Observed harm',
|
|
1410
|
-
'',
|
|
1411
|
-
escapeMarkdown(observedHarm),
|
|
1412
|
-
'',
|
|
1413
|
-
'## Exact abusive URLs',
|
|
1414
|
-
'',
|
|
1415
|
-
...abusiveUrls.map((url) => `- ${escapeMarkdown(url)}`),
|
|
1416
|
-
'',
|
|
1417
|
-
'## Selected response route',
|
|
1418
|
-
'',
|
|
1419
|
-
...(recipientRoute
|
|
1420
|
-
? [
|
|
1421
|
-
`### ${contactLabel(recipientRoute.kind)}`,
|
|
1422
|
-
'',
|
|
1423
|
-
`- Case action: ${escapeMarkdown(recipientRoute.actionId)}`,
|
|
1424
|
-
`- Contact: ${escapeMarkdown(recipientRoute.contact)}`,
|
|
1425
|
-
`- Source: ${escapeMarkdown(recipientRoute.source)}`,
|
|
1426
|
-
`- Route observed: ${recipientRoute.observedAt ?? 'Not provided'} (${recipientRoute.freshness})`,
|
|
1427
|
-
`- Route review after: ${recipientRoute.reviewAfter ?? 'Not provided'}`,
|
|
1428
|
-
`- Limitations: ${recipientRoute.limitations.length ? recipientRoute.limitations.map(escapeMarkdown).join('; ') : 'None recorded'}`,
|
|
1429
|
-
'',
|
|
1430
|
-
]
|
|
1431
|
-
: ['No profile-appropriate response route was bound to this packet.', '']),
|
|
1432
|
-
'## Selected action lineage',
|
|
1433
|
-
'',
|
|
1434
|
-
...(escalationHistory.length
|
|
1435
|
-
? escalationHistory.flatMap((action) => [
|
|
1436
|
-
`- ${escapeMarkdown(action.type.replaceAll('_', ' '))} to ${escapeMarkdown(action.recipient)} · ${escapeMarkdown(action.state.replaceAll('_', ' '))} · updated ${action.updatedAt}`,
|
|
1437
|
-
` - Route observed: ${action.routeObservedAt ?? 'Not provided'}`,
|
|
1438
|
-
...(action.reference ? [` - Reference: ${escapeMarkdown(action.reference)}`] : []),
|
|
1439
|
-
...(action.providerOutcome ? [` - Typed provider outcome: ${escapeMarkdown(action.providerOutcome.replaceAll('_', ' '))}`] : []),
|
|
1440
|
-
...(action.outcomeDetail ? [` - Outcome detail: ${escapeMarkdown(action.outcomeDetail)}`] : []),
|
|
1441
|
-
...(action.originActionId ? [` - Originating action: ${escapeMarkdown(action.originActionId)}`] : []),
|
|
1442
|
-
...action.transitions.flatMap((event) => [
|
|
1443
|
-
` - ${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'}`,
|
|
1444
|
-
...(event.reference ? [` - Reference: ${escapeMarkdown(event.reference)}`] : []),
|
|
1445
|
-
...(event.evidencePinId ? [` - Evidence pin: ${escapeMarkdown(event.evidencePinId)}`] : []),
|
|
1446
|
-
...(event.originActionId ? [` - Originating action: ${escapeMarkdown(event.originActionId)}`] : []),
|
|
1447
|
-
...event.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
|
|
1448
|
-
]),
|
|
1449
|
-
...(action.historyOmitted ? [` - ${action.historyOmitted} earlier transition event${action.historyOmitted === 1 ? '' : 's'} omitted by bound.`] : []),
|
|
1450
|
-
...action.historyLimitations.map((limitation) => ` - History limitation: ${escapeMarkdown(limitation)}`),
|
|
1451
|
-
])
|
|
1452
|
-
: ['No Case action was selected.']),
|
|
1453
|
-
...(escalationHistoryOmitted ? [`- Unrelated Case actions excluded from packet: ${escalationHistoryOmitted}`] : []),
|
|
1454
|
-
...escalationHistoryLimitations.map((limitation) => `- Packet history limitation: ${escapeMarkdown(limitation)}`),
|
|
1455
|
-
'',
|
|
1456
|
-
'## Readiness and authorisation',
|
|
1457
|
-
'',
|
|
1458
|
-
`- Packet state: ${authorisation.status}`,
|
|
1459
|
-
`- Reviewed-input SHA-256: ${authorisation.reviewedInputDigestSha256}`,
|
|
1460
|
-
`- Supplied review digest matches: ${authorisation.digestMatches ? 'yes' : 'no'}`,
|
|
1461
|
-
...RESPONSE_AUTHORISATION_CONFIRMATION_IDS.map((id) => `- Confirmation ${id}: ${authorisation.confirmations[id] ? 'yes' : 'no'}`),
|
|
1462
|
-
...readiness.rows.flatMap((row) => [
|
|
1463
|
-
`- ${escapeMarkdown(row.label)} [${row.state}]: ${escapeMarkdown(row.detail)}`,
|
|
1464
|
-
...row.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
|
|
1465
|
-
]),
|
|
1466
|
-
...authorisation.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
|
|
1467
|
-
'',
|
|
1468
|
-
'## Selected evidence and integrity references',
|
|
1469
|
-
'',
|
|
1470
|
-
...(selectedEvidence.length ? selectedEvidence.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · ${escapeMarkdown(item.source)} · ${item.observedAt} · ${escapeMarkdown(item.completeness)}`) : ['- No evidence pin was explicitly selected.']),
|
|
1471
|
-
...artefactReferences.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · SHA-256 ${item.digestSha256} · captured ${item.capturedAt}`),
|
|
1472
|
-
'',
|
|
1473
|
-
'## Provider outcome and independent effect',
|
|
1474
|
-
'',
|
|
1475
|
-
responseLifecycle.latestProviderOutcome
|
|
1476
|
-
? `- Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${escapeMarkdown(responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' '))})`
|
|
1477
|
-
: `- Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
|
|
1478
|
-
responseLifecycle.latestObservedChangeAt
|
|
1479
|
-
? `- Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
|
|
1480
|
-
: `- Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
|
|
1481
|
-
...(responseLifecycle.latestObservedEffect ? [`- Latest independent review: ${escapeMarkdown(responseLifecycle.latestObservedEffect.state.replaceAll('_', ' '))} · ${responseLifecycle.latestObservedEffect.observedAt} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.source)}`] : []),
|
|
1482
|
-
...responseLifecycle.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
|
|
1483
|
-
'',
|
|
1484
|
-
'## Review and provenance',
|
|
1485
|
-
'',
|
|
1486
|
-
`- Preflight: ${preflight.status.replaceAll('_', ' ')} (${preflight.counts.pass} pass, ${preflight.counts.caution} caution, ${preflight.counts.block} block)`,
|
|
1487
|
-
...preflight.checks.map((check) => `- ${escapeMarkdown(check.label)} [${check.state}]: ${escapeMarkdown(check.detail)}`),
|
|
1488
|
-
...limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
|
|
1489
|
-
`- Case evidence pins: ${caseRecord.evidencePins.length}`,
|
|
1490
|
-
`- Case decision records: ${caseRecord.decisions.length}`,
|
|
1491
|
-
`- Case structured assertions: ${caseRecord.assertions.length}`,
|
|
1492
|
-
`- Observation-age band at export: ${age.band.replaceAll('_', ' ')}`,
|
|
1493
|
-
`- Canonical packet SHA-256: ${digestSha256}`,
|
|
1494
|
-
'- Digest scope: canonical sorted JSON packet excluding the integrity object',
|
|
1495
|
-
'',
|
|
1496
|
-
'## Audience profile',
|
|
1497
|
-
'',
|
|
1498
|
-
...profile.checklist.map((item) => `- Checklist: ${escapeMarkdown(item)}`),
|
|
1499
|
-
...profile.includedEvidence.map((item) => `- Included: ${escapeMarkdown(item)}`),
|
|
1500
|
-
...profile.excludedEvidence.map((item) => `- Excluded: ${escapeMarkdown(item)}`),
|
|
1501
|
-
...profile.redactions.map((item) => `- Redaction to confirm: ${escapeMarkdown(item)}`),
|
|
1502
|
-
...profile.attachments.map((item) => `- Attachment expectation: ${escapeMarkdown(item)}`),
|
|
1503
|
-
...profile.followUpFields.map((item) => `- Follow-up field: ${escapeMarkdown(item)}`),
|
|
1504
|
-
];
|
|
1505
|
-
const markdown = `${lines.join('\n').trim()}\n`;
|
|
1506
|
-
const email = [
|
|
1507
|
-
`Subject: ${profile.subject}`,
|
|
1508
|
-
'',
|
|
1509
|
-
'Hello,',
|
|
1510
|
-
'',
|
|
1511
|
-
`I am reporting observed ${category} activity involving ${caseRecord.domain}.`,
|
|
1512
|
-
`Affected party: ${affectedParty}`,
|
|
1513
|
-
`Observed at (UTC): ${observedAt}`,
|
|
1514
|
-
'',
|
|
1515
|
-
'Observed harm:',
|
|
1516
|
-
observedHarm,
|
|
1517
|
-
'',
|
|
1518
|
-
'Exact URLs:',
|
|
1519
|
-
...abusiveUrls.map((url) => `- ${url}`),
|
|
1520
|
-
'',
|
|
1521
|
-
'Selected evidence:',
|
|
1522
|
-
...(selectedEvidence.length
|
|
1523
|
-
? selectedEvidence.map((item) => `- ${item.label} — ${item.source}, observed ${item.observedAt} (${item.completeness})`)
|
|
1524
|
-
: ['- No Case evidence pin was selected.']),
|
|
1525
|
-
'',
|
|
1526
|
-
`Reviewed packet SHA-256: ${digestSha256}`,
|
|
1527
|
-
'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.',
|
|
1528
|
-
'',
|
|
1529
|
-
responseLifecycle.latestProviderOutcome
|
|
1530
|
-
? `Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' ')})`
|
|
1531
|
-
: `Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
|
|
1532
|
-
responseLifecycle.latestObservedChangeAt
|
|
1533
|
-
? `Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
|
|
1534
|
-
: `Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
|
|
1535
|
-
'',
|
|
1536
|
-
'Please review this report under the applicable abuse and acceptable-use policies.',
|
|
1537
|
-
'',
|
|
1538
|
-
authorisation.status === 'authorised'
|
|
1539
|
-
? 'This locally prepared packet is bound to explicit review confirmations. It was not submitted automatically and does not promise any provider outcome.'
|
|
1540
|
-
: 'This is an unauthorised local draft with cautions. It was not submitted automatically and does not promise any provider outcome.',
|
|
1541
|
-
].join('\n');
|
|
1542
|
-
return { json, markdown, email: `${email}\n` };
|
|
993
|
+
return { json, ...renderCaseResponsePacket(json) };
|
|
1543
994
|
}
|
|
1544
995
|
export function caseResponsePacketFilename(domain, format, generatedAt) {
|
|
1545
996
|
const safeDomain = domain.toLowerCase().replace(/[^a-z0-9.-]+/gu, '-').replace(/^-+|-+$/gu, '').slice(0, 80) || 'case';
|