@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,13 +1,18 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import {
|
|
4
|
-
import { parseCredentialFreeHttpUrl } from "../evidence/lookup-target.mjs";
|
|
3
|
+
import { normalizeCaseObjective, parseIncidentUrlContext, } from "./case-incident-context.mjs";
|
|
5
4
|
import { selectExistingCase } from "./case-selection.mjs";
|
|
5
|
+
import { emptyCaseWorkflowMetadata, updateCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
|
|
6
|
+
import { assertCaseResponseObject, readCaseResponseObject, readCaseResponseObjects, sameCaseResponseObject } from "./case-response-object.mjs";
|
|
7
|
+
import { correctionDelivery, readCasePacketCorrection, readCaseDeliveryPacketReceipt } from "./case-packet-correction.mjs";
|
|
8
|
+
import { readCaseWorkflowFields } from "./case-workflow-migration.mjs";
|
|
6
9
|
import { appendCaseAction, appendCaseAssertion, appendCaseClosure, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseObservedEffectReview, appendCaseSighting, buildCaseClosureLinkContext, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.mjs";
|
|
7
10
|
import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
|
|
8
|
-
import {
|
|
11
|
+
import { MAX_CASE_OBJECTIVE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
|
|
12
|
+
import { caseRecordVersionInput } from "./case-record-version-input.mjs";
|
|
9
13
|
import { readCaseAttachments } from "./case-attachment-model.mjs";
|
|
10
|
-
import {
|
|
14
|
+
import { readCaseEvidenceLinks, appendCaseEvidenceLink, withdrawCaseEvidenceLink } from "./case-evidence-links.mjs";
|
|
15
|
+
import { assertCurrentRecheckQuestion, assertRecheckNonReproduction, assertCaseObjectObservationOutcome, readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
|
|
11
16
|
import { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, } from "./case-record-decisions.mjs";
|
|
12
17
|
import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, caseTimestampOrNull, deterministicId, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeReviewReasonCode, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.mjs";
|
|
13
18
|
import { assertCaseBrandProfileIds, normalizeCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
|
|
@@ -15,84 +20,7 @@ import { normalizeEvidenceHistory, } from "./case-evidence-model.mjs";
|
|
|
15
20
|
import { appendCaseInvestigationBranch, caseInvestigationBranchReferences, normalizeCaseInvestigationBranches, updateCaseInvestigationBranch, } from "./case-investigation-branch-model.mjs";
|
|
16
21
|
export { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, };
|
|
17
22
|
export { MAX_CASE_OBJECTIVE_LENGTH };
|
|
18
|
-
export
|
|
19
|
-
export const INCIDENT_CONTEXT_STATEMENT_PREFIX = 'Investigate incident URL: ';
|
|
20
|
-
const OBJECTIVE_PREFIX = 'Objective: ';
|
|
21
|
-
const RETENTION_SEPARATOR = ' | URL retained: ';
|
|
22
|
-
function boundedContextText(value, maximum) {
|
|
23
|
-
return typeof value === 'string'
|
|
24
|
-
? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
25
|
-
: '';
|
|
26
|
-
}
|
|
27
|
-
export function normalizeCaseObjective(value) {
|
|
28
|
-
return boundedContextText(value, MAX_CASE_OBJECTIVE_LENGTH);
|
|
29
|
-
}
|
|
30
|
-
export function parseIncidentUrlContext(value) {
|
|
31
|
-
const parsed = parseCredentialFreeHttpUrl(value, MAX_CASE_INCIDENT_URL_LENGTH);
|
|
32
|
-
if (!parsed)
|
|
33
|
-
return null;
|
|
34
|
-
const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
|
|
35
|
-
const registrableDomain = canonicalRegistrableDomain(hostname);
|
|
36
|
-
if (!registrableDomain)
|
|
37
|
-
return null;
|
|
38
|
-
const exactUrl = parsed.toString();
|
|
39
|
-
if (exactUrl.length > MAX_CASE_INCIDENT_URL_LENGTH)
|
|
40
|
-
return null;
|
|
41
|
-
return Object.freeze({
|
|
42
|
-
exactUrl,
|
|
43
|
-
hostname,
|
|
44
|
-
registrableDomain,
|
|
45
|
-
originUrl: parsed.origin,
|
|
46
|
-
hasPath: parsed.pathname !== '/',
|
|
47
|
-
hasQuery: Boolean(parsed.search),
|
|
48
|
-
hasFragment: Boolean(parsed.hash),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
export function caseInvestigationContext(record) {
|
|
52
|
-
if (!record)
|
|
53
|
-
return null;
|
|
54
|
-
for (const assertion of [...record.assertions].reverse()) {
|
|
55
|
-
if (assertion.kind !== 'next_step' || assertion.state !== 'open'
|
|
56
|
-
|| !assertion.statement.startsWith(INCIDENT_CONTEXT_STATEMENT_PREFIX))
|
|
57
|
-
continue;
|
|
58
|
-
const incidentUrl = assertion.statement.slice(INCIDENT_CONTEXT_STATEMENT_PREFIX.length);
|
|
59
|
-
const parsed = parseIncidentUrlContext(incidentUrl);
|
|
60
|
-
if (!parsed || parsed.registrableDomain !== record.domain)
|
|
61
|
-
continue;
|
|
62
|
-
const rationale = assertion.rationale ?? '';
|
|
63
|
-
const separatorIndex = rationale.lastIndexOf(RETENTION_SEPARATOR);
|
|
64
|
-
const objective = separatorIndex > OBJECTIVE_PREFIX.length && rationale.startsWith(OBJECTIVE_PREFIX)
|
|
65
|
-
? normalizeCaseObjective(rationale.slice(OBJECTIVE_PREFIX.length, separatorIndex))
|
|
66
|
-
: '';
|
|
67
|
-
const retention = separatorIndex >= 0 ? rationale.slice(separatorIndex + RETENTION_SEPARATOR.length) : '';
|
|
68
|
-
if (!objective || (retention !== 'exact' && retention !== 'origin_only'))
|
|
69
|
-
continue;
|
|
70
|
-
return Object.freeze({
|
|
71
|
-
objective,
|
|
72
|
-
incidentUrl: parsed.exactUrl,
|
|
73
|
-
urlRetention: retention,
|
|
74
|
-
assertionId: assertion.id,
|
|
75
|
-
updatedAt: assertion.updatedAt,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
export function caseInvestigationContextAssertion(input) {
|
|
81
|
-
const objective = normalizeCaseObjective(input.objective);
|
|
82
|
-
if (!objective)
|
|
83
|
-
throw new Error('Enter the investigation objective before retaining Incident context.');
|
|
84
|
-
const parsed = parseIncidentUrlContext(input.incidentUrl);
|
|
85
|
-
if (!parsed)
|
|
86
|
-
throw new Error(`Enter one absolute HTTP(S) Incident URL of at most ${MAX_CASE_INCIDENT_URL_LENGTH} characters without credentials.`);
|
|
87
|
-
const retention = input.retainExactUrl ? 'exact' : 'origin_only';
|
|
88
|
-
const retainedUrl = input.retainExactUrl ? parsed.exactUrl : parsed.originUrl;
|
|
89
|
-
return Object.freeze({
|
|
90
|
-
statement: `${INCIDENT_CONTEXT_STATEMENT_PREFIX}${retainedUrl}`,
|
|
91
|
-
rationale: `${OBJECTIVE_PREFIX}${objective}${RETENTION_SEPARATOR}${retention}`,
|
|
92
|
-
retainedUrl,
|
|
93
|
-
retention,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
23
|
+
export * from "./case-incident-context.mjs";
|
|
96
24
|
// ---------------------------------------------------------------------------
|
|
97
25
|
// Case normalization
|
|
98
26
|
// ---------------------------------------------------------------------------
|
|
@@ -130,39 +58,42 @@ function normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, source
|
|
|
130
58
|
*/
|
|
131
59
|
export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
132
60
|
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
133
|
-
const
|
|
134
|
-
const domain = normalizeDomain(existing ? existing.domain :
|
|
61
|
+
const input = objectRecord(raw);
|
|
62
|
+
const domain = normalizeDomain(existing ? existing.domain : input.domain);
|
|
135
63
|
if (!domain)
|
|
136
64
|
return null;
|
|
65
|
+
const { record, timestampOptions } = caseRecordVersionInput(input, sourceVersion);
|
|
137
66
|
const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
|
|
138
67
|
const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
|
|
139
|
-
const timestampOptions = {
|
|
140
|
-
legacyTimestamps: sourceVersion != null && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
|
|
141
|
-
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
142
|
-
};
|
|
143
68
|
const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
|
|
144
69
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
145
70
|
const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
|
|
71
|
+
for (const action of actions)
|
|
72
|
+
for (const event of action.history)
|
|
73
|
+
if (event.packetReceipt
|
|
74
|
+
&& (event.packetReceipt.caseId !== record.id || event.packetReceipt.target !== domain))
|
|
75
|
+
throw new TypeError('The packet receipt belongs to a different Case or target.');
|
|
146
76
|
const assertions = normalizeCaseAssertions(record.assertions, updatedAt, pinIds, timestampOptions);
|
|
147
77
|
const sightings = normalizeCaseSightings(record.sightings, updatedAt, pinIds, timestampOptions);
|
|
148
78
|
const sightingIds = new Set(sightings.map((item) => item.id));
|
|
149
|
-
const observedEffects = normalizeCaseObservedEffectHistory(
|
|
150
|
-
const closures = normalizeCaseClosureHistory(
|
|
79
|
+
const observedEffects = normalizeCaseObservedEffectHistory(record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
|
|
80
|
+
const closures = normalizeCaseClosureHistory(record.closures, updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
|
|
151
81
|
const normalizedStatus = normalizeStatus(record.status);
|
|
152
82
|
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
153
83
|
const attachments = readCaseAttachments(record.attachments);
|
|
84
|
+
const evidenceLinks = readCaseEvidenceLinks(record.evidenceLinks);
|
|
154
85
|
return {
|
|
155
86
|
id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
|
|
156
87
|
domain,
|
|
157
|
-
title:
|
|
88
|
+
title: normalizeCaseObjective(record.title),
|
|
158
89
|
status: caseStatusRequiresClosure(normalizedStatus)
|
|
159
|
-
&& closures.records.
|
|
90
|
+
&& !closures.records.some(closure => closure.responseObject === undefined) && !closures.preV13HistoryUnavailable
|
|
160
91
|
? 'reviewing'
|
|
161
92
|
: normalizedStatus,
|
|
162
93
|
disposition: normalizeDisposition(record.disposition),
|
|
163
94
|
reviewReasonCode: normalizeReviewReasonCode(record.reviewReasonCode),
|
|
164
95
|
brandProfileIds: normalizeCaseBrandProfileIds(record.brandProfileIds),
|
|
165
|
-
|
|
96
|
+
...readCaseWorkflowFields(record, domain, assertions, sourceVersion),
|
|
166
97
|
notes: normalizeNotes(record.notes, now, sourceVersion),
|
|
167
98
|
source: normalizeSource(record.source),
|
|
168
99
|
evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
|
|
@@ -176,6 +107,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
|
176
107
|
closures,
|
|
177
108
|
branches: normalizeCaseInvestigationBranches(record.branches, updatedAt, branchReferences, timestampOptions),
|
|
178
109
|
...(attachments === undefined ? {} : { attachments }),
|
|
110
|
+
...(evidenceLinks === undefined ? {} : { evidenceLinks }),
|
|
179
111
|
createdAt,
|
|
180
112
|
updatedAt,
|
|
181
113
|
};
|
|
@@ -192,6 +124,13 @@ export function createCase(input, nowIso) {
|
|
|
192
124
|
throw new Error('A valid domain is required to open a case.');
|
|
193
125
|
const noteBody = normalizeNoteBody(input.note);
|
|
194
126
|
const source = normalizeSource(input.source);
|
|
127
|
+
const workflowMetadata = updateCaseWorkflowMetadata(emptyCaseWorkflowMetadata(), input, domain, now);
|
|
128
|
+
const scopedCase = { domain, workflowMetadata };
|
|
129
|
+
for (const raw of [input.evidencePin, ...(Array.isArray(input.evidencePins) ? input.evidencePins : []), input.observedEffectReview, input.closure])
|
|
130
|
+
assertCaseResponseObject(readCaseResponseObject(objectRecord(raw).responseObject), scopedCase);
|
|
131
|
+
for (const object of readCaseResponseObjects(objectRecord(input.action).responseObjects) ?? [])
|
|
132
|
+
assertCaseResponseObject(object, scopedCase);
|
|
133
|
+
assertCaseResponseObject(readCaseResponseObject(objectRecord(objectRecord(input.assertion).recheck).responseObject), scopedCase);
|
|
195
134
|
const evidencePins = input.evidencePins !== undefined
|
|
196
135
|
? appendCaseEvidencePins([], input.evidencePins, now)
|
|
197
136
|
: input.evidencePin !== undefined
|
|
@@ -212,13 +151,19 @@ export function createCase(input, nowIso) {
|
|
|
212
151
|
? appendCaseObservedEffectReview(normalizeCaseObservedEffectHistory(undefined, now), input.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)))
|
|
213
152
|
: normalizeCaseObservedEffectHistory(undefined, now);
|
|
214
153
|
for (const review of observedEffects.reviews) {
|
|
154
|
+
const pin = evidencePins.find(item => item.id === review.evidencePinId);
|
|
155
|
+
if (review.responseObject && pin && !sameCaseResponseObject(review.responseObject, pin.responseObject))
|
|
156
|
+
throw new TypeError('The selected observation evidence does not explicitly concern this exact object.');
|
|
157
|
+
if (review.responseObject && review.state === 'not_reproduced' && !review.recheck)
|
|
158
|
+
throw new TypeError('Object-specific non-reproduction requires a saved baseline question and comparable evidence.');
|
|
159
|
+
assertCaseObjectObservationOutcome(review.objectOutcome, review.recheck, review.completeness, evidencePins, pin, review.observedAt);
|
|
215
160
|
if (!review.recheck)
|
|
216
161
|
continue;
|
|
217
162
|
assertCurrentRecheckQuestion(review.recheck, assertions);
|
|
218
163
|
assertRecheckNonReproduction(review.state, review.recheck, review.completeness, evidencePins, evidencePins.find(pin => pin.id === review.evidencePinId), review.observedAt);
|
|
219
164
|
}
|
|
220
165
|
const closures = input.closure !== undefined
|
|
221
|
-
? appendCaseClosure(normalizeCaseClosureHistory(undefined, now), input.closure, now, observedEffects, actions)
|
|
166
|
+
? appendCaseClosure(normalizeCaseClosureHistory(undefined, now), input.closure, now, observedEffects, actions, evidencePins)
|
|
222
167
|
: normalizeCaseClosureHistory(undefined, now);
|
|
223
168
|
if (caseStatusRequiresClosure(normalizeStatus(input.status)) && input.closure === undefined) {
|
|
224
169
|
throw new Error('Opening a resolved case requires a deliberate closure reason and its linked review context.');
|
|
@@ -227,11 +172,12 @@ export function createCase(input, nowIso) {
|
|
|
227
172
|
id: makeId(),
|
|
228
173
|
domain,
|
|
229
174
|
title: normalizeCaseObjective(input.title),
|
|
230
|
-
status: input.closure !== undefined ? 'resolved' : normalizeStatus(input.status),
|
|
175
|
+
status: input.closure !== undefined && objectRecord(input.closure).responseObject === undefined ? 'resolved' : normalizeStatus(input.status),
|
|
231
176
|
disposition: normalizeDisposition(input.disposition),
|
|
232
177
|
reviewReasonCode: normalizeReviewReasonCode(input.reviewReasonCode),
|
|
233
178
|
brandProfileIds: input.brandProfileIds === undefined ? [] : assertCaseBrandProfileIds(input.brandProfileIds),
|
|
234
179
|
tags: normalizeTags(input.tags),
|
|
180
|
+
workflowMetadata,
|
|
235
181
|
notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
|
|
236
182
|
source,
|
|
237
183
|
evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
|
|
@@ -243,6 +189,7 @@ export function createCase(input, nowIso) {
|
|
|
243
189
|
decisions: input.decision !== undefined
|
|
244
190
|
? appendCaseDecision([], input.decision, now)
|
|
245
191
|
: [],
|
|
192
|
+
...(input.evidenceLink === undefined ? {} : { evidenceLinks: appendCaseEvidenceLink([], input.evidenceLink, evidencePins, now) }),
|
|
246
193
|
actions,
|
|
247
194
|
assertions,
|
|
248
195
|
manualTrail: input.trailEvent !== undefined
|
|
@@ -319,9 +266,36 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
319
266
|
const current = cases[index];
|
|
320
267
|
if (!current)
|
|
321
268
|
throw new Error('That case no longer exists.');
|
|
269
|
+
if (patch.expectedResponseContext !== undefined && patch.expectedResponseContext !== JSON.stringify(current))
|
|
270
|
+
throw new TypeError('The Case response context changed after preview. Review the current delivery, scope and evidence before creating this draft.');
|
|
271
|
+
const workflowMetadata = updateCaseWorkflowMetadata(current.workflowMetadata
|
|
272
|
+
?? readCaseWorkflowFields(current, current.domain, current.assertions).workflowMetadata, patch, current.domain, now);
|
|
273
|
+
const scopedCase = { ...current, workflowMetadata };
|
|
274
|
+
for (const raw of [patch.evidencePin, ...(Array.isArray(patch.evidencePins) ? patch.evidencePins : []), patch.observedEffectReview, patch.closure]) {
|
|
275
|
+
assertCaseResponseObject(readCaseResponseObject(objectRecord(raw).responseObject), scopedCase);
|
|
276
|
+
}
|
|
277
|
+
for (const raw of [patch.action, patch.actionUpdate, objectRecord(patch.actionUpdate).transition]) {
|
|
278
|
+
for (const object of readCaseResponseObjects(objectRecord(raw).responseObjects) ?? [])
|
|
279
|
+
assertCaseResponseObject(object, scopedCase);
|
|
280
|
+
}
|
|
281
|
+
for (const raw of [patch.assertion, patch.assertionUpdate, patch.observedEffectReview]) {
|
|
282
|
+
assertCaseResponseObject(readCaseResponseObject(objectRecord(objectRecord(raw).recheck).responseObject), scopedCase);
|
|
283
|
+
}
|
|
284
|
+
if (patch.evidenceLink !== undefined && patch.evidenceLinkWithdrawal !== undefined)
|
|
285
|
+
throw new TypeError('Record or withdraw one evidence relationship at a time.');
|
|
322
286
|
if (patch.title !== undefined && patch.expectedTitle !== undefined && patch.expectedTitle !== (current.title ?? '')) {
|
|
323
287
|
throw new Error('The incident title changed after this draft was started. Reload and review the current title before saving; your draft has not been applied.');
|
|
324
288
|
}
|
|
289
|
+
for (const [value, expected, actual, label] of [
|
|
290
|
+
[patch.status, patch.expectedStatus, current.status, 'status'],
|
|
291
|
+
[patch.disposition, patch.expectedDisposition, current.disposition, 'disposition'],
|
|
292
|
+
[patch.reviewReasonCode, patch.expectedReviewReasonCode, current.reviewReasonCode ?? null, 'review reason'],
|
|
293
|
+
[patch.tags, patch.expectedTags, current.tags, 'tags'],
|
|
294
|
+
]) {
|
|
295
|
+
if (value !== undefined && expected !== undefined && JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
296
|
+
throw new Error(`The Case ${label} changed after this edit was started. Reopen the Case to review the current value; your edit has not been applied.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
325
299
|
let notes = current.notes;
|
|
326
300
|
if (patch.note !== undefined) {
|
|
327
301
|
const body = normalizeNoteBody(patch.note);
|
|
@@ -345,6 +319,17 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
345
319
|
evidencePins = appendCaseEvidencePin(evidencePins, patch.evidencePin, now);
|
|
346
320
|
}
|
|
347
321
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
322
|
+
const correctionInput = patch.action !== undefined ? objectRecord(patch.action) : patch.actionUpdate !== undefined
|
|
323
|
+
? { ...current.actions.find(action => action.id === objectRecord(patch.actionUpdate).id), ...objectRecord(patch.actionUpdate) } : null;
|
|
324
|
+
if (correctionInput?.correction !== undefined)
|
|
325
|
+
correctionDelivery(current.actions, {
|
|
326
|
+
originActionId: typeof correctionInput.originActionId === 'string' ? correctionInput.originActionId : null,
|
|
327
|
+
recipient: typeof correctionInput.recipient === 'string' ? correctionInput.recipient : '',
|
|
328
|
+
responseObjects: readCaseResponseObjects(correctionInput.responseObjects), correction: readCasePacketCorrection(correctionInput.correction),
|
|
329
|
+
}, { caseId: current.id, target: current.domain, selectedPinIds: pinIds });
|
|
330
|
+
const transitionReceipt = readCaseDeliveryPacketReceipt(objectRecord(objectRecord(patch.actionUpdate).transition).packetReceipt);
|
|
331
|
+
if (transitionReceipt && (transitionReceipt.caseId !== current.id || transitionReceipt.target !== current.domain))
|
|
332
|
+
throw new TypeError('The packet receipt belongs to a different Case or target.');
|
|
348
333
|
let decisions = current.decisions;
|
|
349
334
|
if (patch.decision !== undefined) {
|
|
350
335
|
decisions = appendCaseDecision(current.decisions, patch.decision, now, pinIds);
|
|
@@ -356,6 +341,16 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
356
341
|
if (patch.actionUpdate !== undefined) {
|
|
357
342
|
actions = updateCaseAction(actions, patch.actionUpdate, now, pinIds);
|
|
358
343
|
}
|
|
344
|
+
if (correctionInput?.correction !== undefined || transitionReceipt) {
|
|
345
|
+
for (const original of current.actions) {
|
|
346
|
+
const retained = actions.find(action => action.id === original.id);
|
|
347
|
+
if (!retained || original.history.some(event => !retained.history.some(next => JSON.stringify(next) === JSON.stringify(event)))
|
|
348
|
+
|| (patch.action !== undefined && JSON.stringify(retained) !== JSON.stringify(original)))
|
|
349
|
+
throw new TypeError('This linked operation would omit or change historical delivery records within the Case bounds. No correction or receipt was retained.');
|
|
350
|
+
}
|
|
351
|
+
if (transitionReceipt && !actions.find(action => action.id === transitionReceipt.actionId)?.history.some(event => JSON.stringify(event.packetReceipt) === JSON.stringify(transitionReceipt)))
|
|
352
|
+
throw new TypeError('The exact delivery receipt exceeds the retained history bounds. No delivery was recorded.');
|
|
353
|
+
}
|
|
359
354
|
let assertions = current.assertions;
|
|
360
355
|
if (patch.assertion !== undefined) {
|
|
361
356
|
assertions = appendCaseAssertion(current.assertions, patch.assertion, now, pinIds);
|
|
@@ -387,20 +382,28 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
387
382
|
assertCurrentRecheckQuestion(context, assertions);
|
|
388
383
|
assertRecheckNonReproduction(review.state, context, String(review.completeness), evidencePins, evidencePins.find(pin => pin.id === review.evidencePinId), typeof review.observedAt === 'string' ? review.observedAt : null);
|
|
389
384
|
}
|
|
385
|
+
const responseObject = readCaseResponseObject(review.responseObject);
|
|
386
|
+
const pin = evidencePins.find(item => item.id === review.evidencePinId);
|
|
387
|
+
assertCaseObjectObservationOutcome(review.objectOutcome, context, String(review.completeness), evidencePins, pin, typeof review.observedAt === 'string' ? review.observedAt : null);
|
|
388
|
+
if (responseObject && pin && !sameCaseResponseObject(responseObject, pin.responseObject))
|
|
389
|
+
throw new TypeError('The selected observation evidence does not explicitly concern this exact object.');
|
|
390
|
+
if (responseObject && review.state === 'not_reproduced' && !context)
|
|
391
|
+
throw new TypeError('Object-specific non-reproduction requires a saved baseline question and comparable evidence.');
|
|
390
392
|
observedEffects = appendCaseObservedEffectReview(observedEffects, patch.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)));
|
|
391
393
|
}
|
|
392
394
|
let closures = normalizeCaseClosureHistory(current.closures, current.updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), {}, buildCaseClosureLinkContext(observedEffects, actions));
|
|
393
395
|
if (patch.closure !== undefined) {
|
|
394
|
-
closures = appendCaseClosure(closures, patch.closure, now, observedEffects, actions);
|
|
396
|
+
closures = appendCaseClosure(closures, patch.closure, now, observedEffects, actions, evidencePins);
|
|
395
397
|
}
|
|
396
398
|
if (caseStatusRequiresClosure(patch.status) && patch.closure === undefined) {
|
|
397
399
|
throw new Error('Resolve this case through the deliberate closure review so the reason and evidence state remain explicit.');
|
|
398
400
|
}
|
|
399
401
|
const record = {
|
|
400
402
|
...current,
|
|
403
|
+
workflowMetadata,
|
|
401
404
|
title: patch.title === undefined ? current.title ?? '' : normalizeCaseObjective(patch.title),
|
|
402
405
|
status: patch.closure !== undefined
|
|
403
|
-
? 'resolved'
|
|
406
|
+
? objectRecord(patch.closure).responseObject !== undefined ? current.status : 'resolved'
|
|
404
407
|
: patch.status !== undefined ? normalizeStatus(patch.status) : current.status,
|
|
405
408
|
disposition: patch.disposition !== undefined ? normalizeDisposition(patch.disposition) : current.disposition,
|
|
406
409
|
reviewReasonCode: patch.reviewReasonCode !== undefined
|
|
@@ -413,6 +416,11 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
413
416
|
source,
|
|
414
417
|
evidenceHistory,
|
|
415
418
|
evidencePins,
|
|
419
|
+
...((patch.evidenceLink !== undefined || patch.evidenceLinkWithdrawal !== undefined) ? {
|
|
420
|
+
evidenceLinks: patch.evidenceLink !== undefined
|
|
421
|
+
? appendCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLink, evidencePins, now)
|
|
422
|
+
: withdrawCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLinkWithdrawal, now),
|
|
423
|
+
} : {}),
|
|
416
424
|
decisions,
|
|
417
425
|
actions,
|
|
418
426
|
assertions,
|
|
@@ -537,26 +545,7 @@ export function recordCaseInvestigationContext(cases, id, input, nowIso) {
|
|
|
537
545
|
if (!parsed || parsed.registrableDomain !== current.domain) {
|
|
538
546
|
throw new Error(`The Incident URL must belong to the Case domain ${current.domain}.`);
|
|
539
547
|
}
|
|
540
|
-
|
|
541
|
-
const existing = caseInvestigationContext(current);
|
|
542
|
-
return updateCase(cases, id, existing
|
|
543
|
-
? {
|
|
544
|
-
assertionUpdate: {
|
|
545
|
-
id: existing.assertionId,
|
|
546
|
-
statement: context.statement,
|
|
547
|
-
rationale: context.rationale,
|
|
548
|
-
state: 'open',
|
|
549
|
-
},
|
|
550
|
-
}
|
|
551
|
-
: {
|
|
552
|
-
assertion: {
|
|
553
|
-
kind: 'next_step',
|
|
554
|
-
statement: context.statement,
|
|
555
|
-
rationale: context.rationale,
|
|
556
|
-
evidenceRelations: [],
|
|
557
|
-
state: 'open',
|
|
558
|
-
},
|
|
559
|
-
}, nowIso);
|
|
548
|
+
return updateCase(cases, id, { investigationContext: input }, nowIso);
|
|
560
549
|
}
|
|
561
550
|
export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
562
551
|
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
@@ -577,8 +566,10 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
|
577
566
|
observedAt: input.observedAt,
|
|
578
567
|
collectionDepth: input.collectionDepth,
|
|
579
568
|
observationHostname: input.observationHostname,
|
|
569
|
+
...(input.responseObject ? { responseObject: input.responseObject } : {}),
|
|
580
570
|
completeness: input.completeness,
|
|
581
|
-
truncated:
|
|
571
|
+
truncated: input.comparisonTruncated === true
|
|
572
|
+
|| (typeof input.comparisonSummary === 'string' && input.comparisonSummary.length > MAX_RESPONSE_VALUE_LENGTH),
|
|
582
573
|
limitations: input.limitations,
|
|
583
574
|
},
|
|
584
575
|
}, now);
|
|
@@ -596,6 +587,8 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
|
596
587
|
followUpAt: input.followUpAt,
|
|
597
588
|
limitations: input.limitations,
|
|
598
589
|
...(input.recheck ? { recheck: input.recheck } : {}),
|
|
590
|
+
...(input.responseObject ? { responseObject: input.responseObject } : {}),
|
|
591
|
+
...(input.objectOutcome ? { objectOutcome: input.objectOutcome } : {}),
|
|
599
592
|
},
|
|
600
593
|
}, now);
|
|
601
594
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
1
|
+
import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
2
2
|
import { caseStatusIsClosed } from "./case-record-decisions.mjs";
|
|
3
3
|
const PRESERVE = Object.freeze({
|
|
4
4
|
durable: 'preserve', internal: 'preserve', trusted: 'preserve', public: 'preserve',
|
|
@@ -86,6 +86,12 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
86
86
|
audienceExclusions: { public: { label: 'Brand Profile references', order: 2 } },
|
|
87
87
|
}),
|
|
88
88
|
tags: fieldRule('tags', PRESERVE, (record) => [...record.tags]),
|
|
89
|
+
workflowMetadata: fieldRule('workflowMetadata', PUBLIC_REDACT, (record, profile) => (record.workflowMetadata === undefined ? undefined : profile === 'public'
|
|
90
|
+
? { types: [...record.workflowMetadata.types], incidentTargets: [], investigationContext: null }
|
|
91
|
+
: structuredClone(record.workflowMetadata)), {
|
|
92
|
+
nestedSensitiveFields: ['incidentTargets', 'investigationContext'],
|
|
93
|
+
audienceExclusions: { public: { label: 'Private incident links and investigation context', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } },
|
|
94
|
+
}),
|
|
89
95
|
notes: fieldRule('notes', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public'
|
|
90
96
|
? []
|
|
91
97
|
: record.notes.map((item) => ({ ...item }))), {
|
|
@@ -96,14 +102,26 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
96
102
|
}),
|
|
97
103
|
source: preservedField('source'),
|
|
98
104
|
evidenceHistory: fieldRule('evidenceHistory', PRESERVE, (record) => structuredClone(record.evidenceHistory)),
|
|
99
|
-
evidencePins: fieldRule('evidencePins',
|
|
105
|
+
evidencePins: fieldRule('evidencePins', PUBLIC_REDACT, (record, profile) => structuredClone(record.evidencePins).map(pin => {
|
|
106
|
+
if (profile !== 'public')
|
|
107
|
+
return pin;
|
|
108
|
+
const { responseObject: _scope, infrastructureObservation: _infrastructure, ...publicPin } = pin;
|
|
109
|
+
return publicPin;
|
|
110
|
+
}), { nestedSensitiveFields: ['responseObject', 'infrastructureObservation'], audienceExclusions: { public: { label: 'Exact-object evidence scope and infrastructure observations', order: 4, sinceVersion: 18 } } }),
|
|
111
|
+
evidenceLinks: fieldRule('evidenceLinks', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? undefined : structuredClone(record.evidenceLinks)), { audienceExclusions: { public: { label: 'Analyst evidence relationships', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } } }),
|
|
100
112
|
decisions: fieldRule('decisions', PRESERVE, (record) => structuredClone(record.decisions)),
|
|
101
113
|
actions: fieldRule('actions', TRUSTED_REDACT_PUBLIC_EXCLUDE, (record, profile) => (profile === 'public'
|
|
102
114
|
? []
|
|
103
115
|
: profile === 'trusted'
|
|
104
|
-
? structuredClone(record.actions).map((item) =>
|
|
116
|
+
? structuredClone(record.actions).map((item) => {
|
|
117
|
+
const { correction: _privateCorrection, ...shared } = item;
|
|
118
|
+
return { ...shared, recipient: '[redacted]', history: shared.history.map(event => {
|
|
119
|
+
const { packetReceipt: _privateReceipt, ...sharedEvent } = event;
|
|
120
|
+
return sharedEvent;
|
|
121
|
+
}) };
|
|
122
|
+
})
|
|
105
123
|
: structuredClone(record.actions)), {
|
|
106
|
-
nestedSensitiveFields: ['recipient'],
|
|
124
|
+
nestedSensitiveFields: ['recipient', 'responseObjects', 'correction', 'packetReceipt'],
|
|
107
125
|
audienceExclusions: {
|
|
108
126
|
trusted: { label: 'Recipient values', order: 2 },
|
|
109
127
|
public: { label: 'Actions and recipient values', order: 3 },
|
|
@@ -184,6 +202,7 @@ const OUTSIDE_SCHEMA_EXCLUSIONS = Object.freeze({
|
|
|
184
202
|
});
|
|
185
203
|
function currentAudienceExclusions(audience, caseVersion) {
|
|
186
204
|
const entries = [
|
|
205
|
+
...(audience === 'trusted' && caseVersion >= 19 ? [{ label: 'Private correction content and exact delivery receipts', order: 2 }] : []),
|
|
187
206
|
...Object.values(CASE_FIELD_RULES).flatMap((rule) => (rule.audienceExclusions[audience] ? [rule.audienceExclusions[audience]] : [])),
|
|
188
207
|
OUTSIDE_SCHEMA_EXCLUSIONS[audience],
|
|
189
208
|
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, PUBLISHED_V2_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, OBJECT_RESPONSE_CASE_SCHEMA_VERSION, DELIVERY_CORRECTION_CASE_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
|
|
2
|
+
/** Adapt declared historical fields before ordinary current-record recovery. */
|
|
3
|
+
export function caseRecordVersionInput(record, sourceVersion) {
|
|
4
|
+
const declared = sourceVersion != null;
|
|
5
|
+
if (declared && sourceVersion < DELIVERY_CORRECTION_CASE_SCHEMA_VERSION && Array.isArray(record.actions)
|
|
6
|
+
&& record.actions.some(action => action && typeof action === 'object' && (action.correction !== undefined
|
|
7
|
+
|| Array.isArray(action.history) && action.history.some((event) => event?.packetReceipt !== undefined)))) {
|
|
8
|
+
throw new TypeError('Delivery receipts and corrections require Case schema 19; historical deliveries were not reinterpreted.');
|
|
9
|
+
}
|
|
10
|
+
if (declared && sourceVersion < OBJECT_RESPONSE_CASE_SCHEMA_VERSION) {
|
|
11
|
+
const items = (value) => Array.isArray(value) ? value.filter((item) => !!item && typeof item === 'object' && !Array.isArray(item)) : [];
|
|
12
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
13
|
+
const pins = items(record.evidencePins);
|
|
14
|
+
const actions = items(record.actions);
|
|
15
|
+
const reviews = items(object(record.observedEffects).reviews);
|
|
16
|
+
const closures = items(object(record.closures).records);
|
|
17
|
+
const questions = [...items(record.assertions), ...reviews].map(item => object(item.recheck));
|
|
18
|
+
const events = actions.flatMap(action => items(action.history));
|
|
19
|
+
if (pins.some(pin => pin.responseObject !== undefined || pin.infrastructureObservation !== undefined)
|
|
20
|
+
|| actions.some(action => action.responseObjects !== undefined)
|
|
21
|
+
|| events.some(event => event.responseObjects !== undefined || event.objectOutcome !== undefined)
|
|
22
|
+
|| [...reviews, ...closures, ...questions].some(item => item.responseObject !== undefined || item.objectOutcome !== undefined)
|
|
23
|
+
|| [...items(record.evidenceHistory), object(record.evidence)].some(snapshot => snapshot.hasExternalPasswordForm !== undefined)) {
|
|
24
|
+
throw new TypeError('Object response and password-form attribution fields require Case schema 18; historical evidence was not reinterpreted.');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (record.evidenceLinks !== undefined && declared && sourceVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION) {
|
|
28
|
+
throw new TypeError('Evidence relationships require the current Case schema.');
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
record: declared && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION ? {
|
|
32
|
+
...record,
|
|
33
|
+
...(sourceVersion < PUBLISHED_V2_CASE_SCHEMA_VERSION ? { observedEffects: undefined, closures: undefined } : {}),
|
|
34
|
+
title: '',
|
|
35
|
+
} : record,
|
|
36
|
+
timestampOptions: {
|
|
37
|
+
legacyTimestamps: declared && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
|
|
38
|
+
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|