@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Pure incident context parsing and retention projection; no storage or Case mutation.
|
|
2
|
+
import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
|
|
3
|
+
import { MAX_CASE_OBJECTIVE_LENGTH } from "../contracts/case-portability.mjs";
|
|
4
|
+
import { parseCredentialFreeHttpUrl } from "../evidence/lookup-target.mjs";
|
|
5
|
+
export const MAX_CASE_INCIDENT_URL_LENGTH = 1_850;
|
|
6
|
+
function boundedContextText(value, maximum) {
|
|
7
|
+
return typeof value === 'string'
|
|
8
|
+
? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
9
|
+
: '';
|
|
10
|
+
}
|
|
11
|
+
export function normalizeCaseObjective(value) {
|
|
12
|
+
return boundedContextText(value, MAX_CASE_OBJECTIVE_LENGTH);
|
|
13
|
+
}
|
|
14
|
+
export function parseIncidentUrlContext(value) {
|
|
15
|
+
const parsed = parseCredentialFreeHttpUrl(value, MAX_CASE_INCIDENT_URL_LENGTH);
|
|
16
|
+
if (!parsed)
|
|
17
|
+
return null;
|
|
18
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
|
|
19
|
+
const registrableDomain = canonicalRegistrableDomain(hostname);
|
|
20
|
+
if (!registrableDomain)
|
|
21
|
+
return null;
|
|
22
|
+
const exactUrl = parsed.toString();
|
|
23
|
+
if (exactUrl.length > MAX_CASE_INCIDENT_URL_LENGTH)
|
|
24
|
+
return null;
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
exactUrl,
|
|
27
|
+
hostname,
|
|
28
|
+
registrableDomain,
|
|
29
|
+
originUrl: parsed.origin,
|
|
30
|
+
hasPath: parsed.pathname !== '/',
|
|
31
|
+
hasQuery: Boolean(parsed.search),
|
|
32
|
+
hasFragment: Boolean(parsed.hash),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function caseInvestigationContext(record) {
|
|
36
|
+
return record?.workflowMetadata?.investigationContext ?? null;
|
|
37
|
+
}
|
|
38
|
+
export function prepareCaseInvestigationContext(input) {
|
|
39
|
+
const objective = normalizeCaseObjective(input.objective);
|
|
40
|
+
if (!objective)
|
|
41
|
+
throw new Error('Enter the investigation objective before retaining Incident context.');
|
|
42
|
+
const parsed = parseIncidentUrlContext(input.incidentUrl);
|
|
43
|
+
if (!parsed)
|
|
44
|
+
throw new Error(`Enter one absolute HTTP(S) Incident URL of at most ${MAX_CASE_INCIDENT_URL_LENGTH} characters without credentials.`);
|
|
45
|
+
const retention = input.retainExactUrl ? 'exact' : 'origin_only';
|
|
46
|
+
const retainedUrl = input.retainExactUrl ? parsed.exactUrl : parsed.originUrl;
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
objective,
|
|
49
|
+
incidentUrl: retainedUrl,
|
|
50
|
+
urlRetention: retention,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Keep untrusted Case prose literal, including block syntax and GFM autolinks. */
|
|
2
|
+
export function escapeCaseMarkdownInline(text) {
|
|
3
|
+
return String(text)
|
|
4
|
+
.replace(/[\r\n\u2028\u2029]+/g, ' ')
|
|
5
|
+
.replace(/([\\`*_{}\[\]<>()#+!|~])/g, '\\$1')
|
|
6
|
+
.replace(/^(\s*)([-=]+)(?=\s|$)/u, '$1\\$2')
|
|
7
|
+
.replace(/^(\s*\d+)\.(?=\s)/u, '$1\\.')
|
|
8
|
+
// Break automatic links without altering the underlying evidence value.
|
|
9
|
+
.replace(/\b([a-z][a-z0-9+.-]{1,31}):(?=\/\/)/gi, '$1\\:')
|
|
10
|
+
.replace(/\bwww\.(?=\S)/gi, 'www\\.');
|
|
11
|
+
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION,
|
|
1
|
+
import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, MAX_CASES, MAX_NOTES_PER_CASE, } from "../contracts/case-portability.mjs";
|
|
2
|
+
import { DEFAULT_DISPOSITION, DEFAULT_STATUS, isValidDisposition, isValidStatus } from "./case-record-decisions.mjs";
|
|
3
|
+
import { DEFAULT_SOURCE } from "./case-record-contracts.mjs";
|
|
4
|
+
import { deterministicId, caseTimestampOrNull, normalizeNotes, normalizeReviewReasonCode, normalizeTags, objectRecord, safeId, isValidSource, } from "./case-record-core.mjs";
|
|
5
|
+
import { normalizeCase } from "./case-record-operations.mjs";
|
|
6
|
+
import { normalizeCaseObjective } from "./case-incident-context.mjs";
|
|
7
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
8
|
+
import { mergeImportedEvidenceHistory, normalizeEvidenceHistory } from "./case-evidence-model.mjs";
|
|
2
9
|
import { CASE_REPORT_SCHEMA, PUBLIC_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, } from "../contracts/case-portability.mjs";
|
|
3
10
|
import { canonicalArtifactJsonV2 } from "../evidence/artifact-integrity.mjs";
|
|
4
11
|
import { readCaseAttachments, mergeCaseAttachments } from "./case-attachment-model.mjs";
|
|
5
|
-
import {
|
|
12
|
+
import { readCaseEvidenceLinks, mergeCaseEvidenceLinks } from "./case-evidence-links.mjs";
|
|
13
|
+
import { EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
14
|
+
import { assertBoundedJsonStructure } from "../analysis/bounded-json.mjs";
|
|
6
15
|
import { inspectCaseBrandProfileIds, unionCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
|
|
7
16
|
import { caseInvestigationBranchReferences, mergeCaseInvestigationBranches, normalizeCaseInvestigationBranches, } from "./case-investigation-branch-model.mjs";
|
|
8
17
|
import { buildCaseClosureLinkContext, mergeCaseActions, mergeCaseClosureHistories, mergeCaseObservedEffectHistories, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_CASE_MANUAL_TRAIL_EVENTS, MAX_CASE_SIGHTINGS, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, } from "./case-response-model.mjs";
|
|
18
|
+
import { readCaseWorkflowFields } from "./case-workflow-migration.mjs";
|
|
19
|
+
import { emptyCaseWorkflowMetadata, mergeCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
|
|
9
20
|
export const MAX_CASE_INPUT_RECORDS = 2_000;
|
|
10
21
|
function compareCodeUnits(left, right) {
|
|
11
22
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -81,6 +92,8 @@ export function normalizeCaseStore(raw) {
|
|
|
81
92
|
function assertModernCaseShape(raw, sourceVersion) {
|
|
82
93
|
for (const item of boundedCaseList(raw).items) {
|
|
83
94
|
const itemRecord = objectRecord(item);
|
|
95
|
+
if (sourceVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION && Object.hasOwn(itemRecord, 'evidenceLinks'))
|
|
96
|
+
throw new TypeError('Evidence relationships require the current Case schema.');
|
|
84
97
|
if (sourceVersion < INCIDENT_CASE_SCHEMA_VERSION && Object.hasOwn(itemRecord, 'attachments')) {
|
|
85
98
|
throw new TypeError('Retained file references require the current Case schema; no data was changed.');
|
|
86
99
|
}
|
|
@@ -168,6 +181,8 @@ function boundedCaseList(raw) {
|
|
|
168
181
|
*/
|
|
169
182
|
function extractImportPatch(raw, importedVersion) {
|
|
170
183
|
const record = objectRecord(raw);
|
|
184
|
+
if (record.evidenceLinks !== undefined && importedVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION)
|
|
185
|
+
throw new TypeError('Evidence relationships require the current Case schema.');
|
|
171
186
|
const domain = normalizeDomain(record.domain);
|
|
172
187
|
if (!domain)
|
|
173
188
|
return null;
|
|
@@ -204,6 +219,9 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
204
219
|
[record.sightings, sightings],
|
|
205
220
|
].reduce((total, [candidates, retained]) => total
|
|
206
221
|
+ Math.max(0, (Array.isArray(candidates) ? candidates.length : 0) - retained.length), 0);
|
|
222
|
+
const workflow = readCaseWorkflowFields(record, domain, assertions, importedVersion);
|
|
223
|
+
const hasWorkflow = record.workflowMetadata !== undefined || workflow.workflowMetadata.types.length > 0
|
|
224
|
+
|| workflow.workflowMetadata.incidentTargets.length > 0 || workflow.workflowMetadata.investigationContext !== null;
|
|
207
225
|
return {
|
|
208
226
|
domain,
|
|
209
227
|
rawId: typeof record.id === 'string' ? record.id : null,
|
|
@@ -235,7 +253,9 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
235
253
|
? normalizeCaseInvestigationBranches(record.branches, normalizedFallback, branchReferences, timestampOptions)
|
|
236
254
|
: [],
|
|
237
255
|
attachments: importedVersion >= INCIDENT_CASE_SCHEMA_VERSION ? readCaseAttachments(record.attachments) : undefined,
|
|
238
|
-
|
|
256
|
+
evidenceLinks: readCaseEvidenceLinks(record.evidenceLinks),
|
|
257
|
+
tags: workflow.tags,
|
|
258
|
+
workflowMetadata: hasWorkflow ? workflow.workflowMetadata : undefined,
|
|
239
259
|
// Imported notes fall back only to the imported record's own timestamps
|
|
240
260
|
// (never "now"), so a timestamp-less note gets a stable, deterministic time
|
|
241
261
|
// and id, and re-importing the same file cannot manufacture a duplicate or a
|
|
@@ -245,16 +265,6 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
245
265
|
updatedAt: caseTimestampOrNull(record.updatedAt, importedVersion),
|
|
246
266
|
};
|
|
247
267
|
}
|
|
248
|
-
/** @param {CaseNote[]} a @param {CaseNote[]} b @returns {CaseNote[]} */
|
|
249
|
-
function unionNotes(a, b) {
|
|
250
|
-
const byId = new Map();
|
|
251
|
-
for (const note of [...a, ...b]) {
|
|
252
|
-
if (!byId.has(note.id))
|
|
253
|
-
byId.set(note.id, note);
|
|
254
|
-
}
|
|
255
|
-
const notes = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
|
|
256
|
-
return notes.slice(Math.max(0, notes.length - MAX_NOTES_PER_CASE));
|
|
257
|
-
}
|
|
258
268
|
function retainLocalAuthoredRecords(local, imported, maximum) {
|
|
259
269
|
const records = [...local];
|
|
260
270
|
const retainedIds = new Set(records.map((item) => item.id));
|
|
@@ -271,13 +281,6 @@ function retainLocalAuthoredRecords(local, imported, maximum) {
|
|
|
271
281
|
}
|
|
272
282
|
return { records, omitted };
|
|
273
283
|
}
|
|
274
|
-
// Additive, deduplicated union of two evidence histories. Identical material
|
|
275
|
-
// collapses (earliest firstCapturedAt, latest capturedAt), distinct snapshots
|
|
276
|
-
// are retained subject to the per-case bound, and an older import can never
|
|
277
|
-
// move an existing observation backwards.
|
|
278
|
-
function mergeEvidenceHistories(local, imported, caseDomain) {
|
|
279
|
-
return normalizeEvidenceHistory([...local, ...imported], { source: 'import', fallback: null, caseDomain });
|
|
280
|
-
}
|
|
281
284
|
/** @param {ImportPatch} patch @param {string} now @returns {CaseRecord} */
|
|
282
285
|
function caseFromPatch(patch, now) {
|
|
283
286
|
return {
|
|
@@ -289,6 +292,7 @@ function caseFromPatch(patch, now) {
|
|
|
289
292
|
reviewReasonCode: patch.reviewReasonCode ?? null,
|
|
290
293
|
brandProfileIds: patch.brandProfileIds,
|
|
291
294
|
tags: patch.tags,
|
|
295
|
+
workflowMetadata: patch.workflowMetadata ?? emptyCaseWorkflowMetadata(),
|
|
292
296
|
notes: patch.notes,
|
|
293
297
|
source: patch.source ?? DEFAULT_SOURCE,
|
|
294
298
|
evidenceHistory: patch.evidenceHistory,
|
|
@@ -302,6 +306,7 @@ function caseFromPatch(patch, now) {
|
|
|
302
306
|
closures: patch.closures,
|
|
303
307
|
branches: patch.branches,
|
|
304
308
|
...(patch.attachments === undefined ? {} : { attachments: patch.attachments }),
|
|
309
|
+
...(patch.evidenceLinks === undefined ? {} : { evidenceLinks: patch.evidenceLinks }),
|
|
305
310
|
createdAt: patch.createdAt || patch.updatedAt || now,
|
|
306
311
|
updatedAt: patch.updatedAt || patch.createdAt || now,
|
|
307
312
|
};
|
|
@@ -318,6 +323,8 @@ function caseFromPatch(patch, now) {
|
|
|
318
323
|
function applyImportPatch(local, patch) {
|
|
319
324
|
const importNewer = patch.updatedAt !== null && Date.parse(patch.updatedAt) > Date.parse(local.updatedAt);
|
|
320
325
|
const fallback = patch.updatedAt || local.updatedAt;
|
|
326
|
+
const noteSelection = retainLocalAuthoredRecords(local.notes, patch.notes, MAX_NOTES_PER_CASE);
|
|
327
|
+
const historySelection = mergeImportedEvidenceHistory(local.evidenceHistory, patch.evidenceHistory, local.domain);
|
|
321
328
|
const pinSelection = retainLocalAuthoredRecords(local.evidencePins, patch.evidencePins, MAX_CASE_EVIDENCE_PINS);
|
|
322
329
|
const evidencePins = normalizeCaseEvidencePins(pinSelection.records, fallback);
|
|
323
330
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
@@ -336,7 +343,9 @@ function applyImportPatch(local, patch) {
|
|
|
336
343
|
const trailSelection = retainLocalAuthoredRecords(local.manualTrail, patch.manualTrail, MAX_CASE_MANUAL_TRAIL_EVENTS);
|
|
337
344
|
const manualTrail = normalizeCaseManualTrail(trailSelection.records, fallback);
|
|
338
345
|
const attachments = mergeCaseAttachments(local.attachments, patch.attachments);
|
|
346
|
+
const evidenceLinks = mergeCaseEvidenceLinks(local.evidenceLinks, patch.evidenceLinks);
|
|
339
347
|
const authoredHistoryOmitted = patch.authoredHistoryOmitted
|
|
348
|
+
+ noteSelection.omitted
|
|
340
349
|
+ pinSelection.omitted
|
|
341
350
|
+ decisionSelection.omitted
|
|
342
351
|
+ assertionSelection.omitted
|
|
@@ -350,7 +359,7 @@ function applyImportPatch(local, patch) {
|
|
|
350
359
|
reviewReasonCode: patch.reviewReasonCode !== undefined && importNewer ? patch.reviewReasonCode : local.reviewReasonCode ?? null,
|
|
351
360
|
brandProfileIds: brandProfileReferences.ids,
|
|
352
361
|
source: patch.source !== undefined && importNewer ? patch.source : local.source,
|
|
353
|
-
evidenceHistory:
|
|
362
|
+
evidenceHistory: historySelection.records,
|
|
354
363
|
evidencePins,
|
|
355
364
|
decisions,
|
|
356
365
|
actions,
|
|
@@ -361,11 +370,15 @@ function applyImportPatch(local, patch) {
|
|
|
361
370
|
closures,
|
|
362
371
|
branches: mergeCaseInvestigationBranches(local.branches ?? [], patch.branches, fallback, branchReferences),
|
|
363
372
|
...(attachments === undefined ? {} : { attachments }),
|
|
373
|
+
...(evidenceLinks === undefined ? {} : { evidenceLinks }),
|
|
364
374
|
tags: normalizeTags([...local.tags, ...patch.tags]),
|
|
365
|
-
|
|
375
|
+
workflowMetadata: patch.workflowMetadata === undefined
|
|
376
|
+
? local.workflowMetadata ?? emptyCaseWorkflowMetadata()
|
|
377
|
+
: mergeCaseWorkflowMetadata(local.workflowMetadata ?? emptyCaseWorkflowMetadata(), patch.workflowMetadata, importNewer, local.domain),
|
|
378
|
+
notes: normalizeNotes(noteSelection.records, fallback),
|
|
366
379
|
createdAt: patch.createdAt && Date.parse(patch.createdAt) < Date.parse(local.createdAt) ? patch.createdAt : local.createdAt,
|
|
367
380
|
updatedAt: importNewer ? (patch.updatedAt ?? local.updatedAt) : local.updatedAt,
|
|
368
|
-
}, brandProfileReferencesOmitted: brandProfileReferences.omitted, authoredHistoryOmitted };
|
|
381
|
+
}, brandProfileReferencesOmitted: brandProfileReferences.omitted, authoredHistoryOmitted, evidenceHistoryOmitted: historySelection.omitted };
|
|
369
382
|
}
|
|
370
383
|
function pickFreeId(preferred, domain, used) {
|
|
371
384
|
const wanted = safeId(preferred);
|
|
@@ -478,7 +491,7 @@ function caseCollectionImportEnvelope(importedRaw) {
|
|
|
478
491
|
* reinterpreted.
|
|
479
492
|
* @param {CaseRecord[]} localCases
|
|
480
493
|
* @param {unknown} importedRaw
|
|
481
|
-
* @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number, authoredHistoryOmitted: number }}
|
|
494
|
+
* @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number, authoredHistoryOmitted: number, evidenceHistoryOmitted: number }}
|
|
482
495
|
*/
|
|
483
496
|
export function mergeCases(localCases, importedRaw) {
|
|
484
497
|
assertBoundedJsonStructure(importedRaw, 'Case import', CASE_INPUT_JSON_LIMITS);
|
|
@@ -507,6 +520,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
507
520
|
let skipped = imported.omitted;
|
|
508
521
|
let brandProfileReferencesOmitted = 0;
|
|
509
522
|
let authoredHistoryOmitted = 0;
|
|
523
|
+
let evidenceHistoryOmitted = 0;
|
|
510
524
|
const fallback = new Date(0).toISOString();
|
|
511
525
|
const importedIds = new Set();
|
|
512
526
|
for (const item of imported.items) {
|
|
@@ -539,6 +553,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
539
553
|
byId.set(existing.id, merged.record);
|
|
540
554
|
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted + merged.brandProfileReferencesOmitted;
|
|
541
555
|
authoredHistoryOmitted += merged.authoredHistoryOmitted;
|
|
556
|
+
evidenceHistoryOmitted += merged.evidenceHistoryOmitted;
|
|
542
557
|
updated += 1;
|
|
543
558
|
}
|
|
544
559
|
else if (byId.size < MAX_CASES) {
|
|
@@ -561,5 +576,6 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
561
576
|
skipped,
|
|
562
577
|
brandProfileReferencesOmitted,
|
|
563
578
|
authoredHistoryOmitted,
|
|
579
|
+
evidenceHistoryOmitted,
|
|
564
580
|
};
|
|
565
581
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { retainedCaseResponseObjects, sameCaseResponseObject } from "./case-response-object.mjs";
|
|
2
|
+
export function buildCaseIncidentCoverage(record) {
|
|
3
|
+
const objects = new Map();
|
|
4
|
+
const explicit = retainedCaseResponseObjects(record);
|
|
5
|
+
for (const target of record.workflowMetadata?.incidentTargets ?? []) {
|
|
6
|
+
const matching = explicit.filter(object => object.incidentTargetId === target.id && object.identifier === target.url);
|
|
7
|
+
for (const object of matching.length ? matching : [{ kind: 'other', identifier: target.url, incidentTargetId: target.id }])
|
|
8
|
+
objects.set(JSON.stringify(object), { target, object, targetRetained: true });
|
|
9
|
+
}
|
|
10
|
+
for (const object of explicit)
|
|
11
|
+
if (!objects.has(JSON.stringify(object)))
|
|
12
|
+
objects.set(JSON.stringify(object), {
|
|
13
|
+
object, targetRetained: false,
|
|
14
|
+
target: { id: object.incidentTargetId ?? `${object.kind}-${object.identifier}`, url: object.identifier, state: 'open', createdAt: record.createdAt ?? '', updatedAt: record.updatedAt ?? '' },
|
|
15
|
+
});
|
|
16
|
+
return [...objects.values()].map(({ target, object, targetRetained }) => {
|
|
17
|
+
const actions = (record.actions ?? []).filter(action => action.responseObjects?.some(bound => sameCaseResponseObject(bound, object)));
|
|
18
|
+
const reviews = (record.observedEffects?.reviews ?? []).filter(review => sameCaseResponseObject(review.responseObject, object));
|
|
19
|
+
const latestAt = Math.max(...reviews.map(review => Date.parse(review.observedAt)));
|
|
20
|
+
const latest = reviews.filter(review => Date.parse(review.observedAt) === latestAt);
|
|
21
|
+
return { target, targetRetained, responseObject: object,
|
|
22
|
+
hostname: object.kind === 'domain' || object.kind === 'hostname' ? object.identifier : new URL(object.identifier).hostname,
|
|
23
|
+
actionCoverage: actions.length ? 'bound' : 'unknown',
|
|
24
|
+
actionIds: actions.map(action => action.id),
|
|
25
|
+
providerEvents: (record.actions ?? []).flatMap(action => action.history.filter(event => event.applied
|
|
26
|
+
&& event.responseObjects?.some(bound => sameCaseResponseObject(bound, object))
|
|
27
|
+
&& (event.providerOutcome || event.objectOutcome || event.nextState === 'acknowledged'))
|
|
28
|
+
.map(event => ({ actionId: action.id, eventId: event.id, state: event.nextState, outcome: event.objectOutcome ?? event.providerOutcome, occurredAt: event.occurredAt, sourceClass: event.sourceClass }))),
|
|
29
|
+
observationCoverage: !reviews.length ? 'unknown' : new Set(latest.map(review => `${review.state}\u0000${review.objectOutcome ?? ''}`)).size > 1 ? 'ambiguous' : 'available',
|
|
30
|
+
reviews,
|
|
31
|
+
closures: (record.closures?.records ?? []).filter(closure => sameCaseResponseObject(closure.responseObject, object)), };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Local delivery linkage, not authentication, external acceptance or nonrepudiation.
|
|
2
|
+
import { array, enumeration, exact, HEX_DIGEST_RE, iso, text } from "../evidence/artifact-structure.mjs";
|
|
3
|
+
import { assertWorkspaceInputGraph } from "../workspace/hostile-input.mjs";
|
|
4
|
+
import { MAX_DECISION_PIN_REFERENCES, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_RECIPIENT_LENGTH, SUPPORTED_CASE_RESPONSE_PACKET_VERSIONS } from "../contracts/case-portability.mjs";
|
|
5
|
+
import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
|
|
6
|
+
import { readCaseResponseObjects, sameCaseResponseObject } from "./case-response-object.mjs";
|
|
7
|
+
import { RESPONSE_PACKET_PROFILE_IDS } from "./case-response-packet-vocabulary.mjs";
|
|
8
|
+
export const CASE_DELIVERY_RECEIPT_LIMITATION = 'This retained local receipt identifies canonical packet JSON and analyst-recorded delivery, not delivered message or PDF bytes, external receipt, factual truth, authorship authentication or provider acceptance.';
|
|
9
|
+
const id = (value, label) => {
|
|
10
|
+
const result = text(value, label, 64);
|
|
11
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/u.test(result))
|
|
12
|
+
throw new TypeError(`${label} is invalid.`);
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
const digest = (value) => {
|
|
16
|
+
if (typeof value !== 'string' || !HEX_DIGEST_RE.test(value))
|
|
17
|
+
throw new TypeError('Select an exact recorded packet digest.');
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
const packetVersion = (value) => {
|
|
21
|
+
if (!SUPPORTED_CASE_RESPONSE_PACKET_VERSIONS.some(version => version === value))
|
|
22
|
+
throw new TypeError('The recorded packet version is unsupported.');
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
const statement = (value, label, required = true) => {
|
|
26
|
+
const result = text(value, label, MAX_RESPONSE_RATIONALE_LENGTH, !required).trim();
|
|
27
|
+
if (required && !result)
|
|
28
|
+
throw new TypeError(`${label} is required.`);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
export function readCaseDeliveryPacketReceipt(value) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
assertWorkspaceInputGraph(value, 'Delivery packet receipt');
|
|
35
|
+
const row = exact(value, ['version', 'caseId', 'actionId', 'target', 'packetDigestSha256', 'packetVersion', 'packetGeneratedAt', 'recipient', 'profile', 'responseObjects'], 'Delivery packet receipt');
|
|
36
|
+
if (row.version !== 1)
|
|
37
|
+
throw new TypeError('The delivery receipt version is unsupported.');
|
|
38
|
+
const target = text(row.target, 'Receipt target', 253);
|
|
39
|
+
if (!isValidAsciiHostname(target) || target !== target.toLowerCase())
|
|
40
|
+
throw new TypeError('The delivery receipt target is invalid.');
|
|
41
|
+
iso(row.packetGeneratedAt, 'Packet generation time');
|
|
42
|
+
const recipient = text(row.recipient, 'Receipt recipient', MAX_RESPONSE_RECIPIENT_LENGTH).trim();
|
|
43
|
+
if (!recipient)
|
|
44
|
+
throw new TypeError('The receipt recipient is required.');
|
|
45
|
+
const responseObjects = readCaseResponseObjects(row.responseObjects);
|
|
46
|
+
if (!responseObjects?.length)
|
|
47
|
+
throw new TypeError('The receipt requires an explicit nonempty delivered object scope.');
|
|
48
|
+
return Object.freeze({ version: 1, caseId: id(row.caseId, 'Receipt Case'), actionId: id(row.actionId, 'Receipt action'), target,
|
|
49
|
+
packetDigestSha256: digest(row.packetDigestSha256), packetVersion: packetVersion(row.packetVersion), packetGeneratedAt: row.packetGeneratedAt,
|
|
50
|
+
recipient, profile: enumeration(row.profile, RESPONSE_PACKET_PROFILE_IDS, 'Receipt audience'), responseObjects });
|
|
51
|
+
}
|
|
52
|
+
export function readCasePacketCorrection(value) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
assertWorkspaceInputGraph(value, 'Packet correction');
|
|
56
|
+
const row = exact(value, ['version', 'purpose', 'deliveryEventId', 'packetDigestSha256', 'packetVersion', 'reason', 'previousStatement', 'correctedStatement', 'evidencePinIds', 'profile'], 'Packet correction');
|
|
57
|
+
if (row.version !== 1)
|
|
58
|
+
throw new TypeError('The correction version is unsupported.');
|
|
59
|
+
const purpose = enumeration(row.purpose, ['correction', 'retraction_request'], 'Correction purpose');
|
|
60
|
+
const pins = array(row.evidencePinIds, 'Correction evidence', MAX_DECISION_PIN_REFERENCES).map(value => id(value, 'Correction pin'));
|
|
61
|
+
if (!pins.length || new Set(pins).size !== pins.length)
|
|
62
|
+
throw new TypeError('Select distinct retained correction evidence pins.');
|
|
63
|
+
return Object.freeze({ version: 1, purpose, deliveryEventId: id(row.deliveryEventId, 'Original delivery event'),
|
|
64
|
+
packetDigestSha256: digest(row.packetDigestSha256), packetVersion: packetVersion(row.packetVersion),
|
|
65
|
+
reason: statement(row.reason, 'Correction reason'), previousStatement: statement(row.previousStatement, 'Previous statement'),
|
|
66
|
+
correctedStatement: statement(row.correctedStatement, 'Corrected statement', purpose === 'correction'),
|
|
67
|
+
evidencePinIds: Object.freeze(pins), profile: enumeration(row.profile, RESPONSE_PACKET_PROFILE_IDS, 'Correction audience') });
|
|
68
|
+
}
|
|
69
|
+
export function assertDeliveryPacketReceipt(receipt, event, actionId) {
|
|
70
|
+
if (!receipt)
|
|
71
|
+
return;
|
|
72
|
+
if (receipt.actionId !== actionId || !['authorised', 'submitted'].includes(event.previousState ?? '') || event.nextState !== 'submitted' || event.sourceClass !== 'analyst'
|
|
73
|
+
|| event.reference !== `response-packet-sha256:${receipt.packetDigestSha256}`
|
|
74
|
+
|| JSON.stringify(event.responseObjects ?? []) !== JSON.stringify(receipt.responseObjects)
|
|
75
|
+
|| (event.occurredAt && Date.parse(event.occurredAt) < Date.parse(receipt.packetGeneratedAt)))
|
|
76
|
+
throw new TypeError('The packet receipt must match its exact analyst-recorded delivery and object scope.');
|
|
77
|
+
}
|
|
78
|
+
export function correctionDelivery(actions, action, context) {
|
|
79
|
+
const correction = action.correction;
|
|
80
|
+
if (!correction)
|
|
81
|
+
return null;
|
|
82
|
+
const original = actions.find(item => item.id === action.originActionId);
|
|
83
|
+
const event = original?.history.find(item => item.id === correction.deliveryEventId);
|
|
84
|
+
const receipt = event?.packetReceipt;
|
|
85
|
+
if (!original || !event?.applied || !receipt)
|
|
86
|
+
throw new TypeError('An exact-linked correction requires the retained receipt of the explicitly selected delivery. Legacy digest-only or omitted receipts cannot authorise it.');
|
|
87
|
+
assertDeliveryPacketReceipt(receipt, event, original.id);
|
|
88
|
+
if (receipt.recipient === '[redacted]' || action.recipient === '[redacted]' || receipt.recipient !== action.recipient
|
|
89
|
+
|| original.recipient !== receipt.recipient || correction.profile !== receipt.profile
|
|
90
|
+
|| correction.packetDigestSha256 !== receipt.packetDigestSha256 || correction.packetVersion !== receipt.packetVersion
|
|
91
|
+
|| !action.responseObjects?.length || action.responseObjects.some(object => !receipt.responseObjects.some(previous => sameCaseResponseObject(previous, object))))
|
|
92
|
+
throw new TypeError('Correction recipient, version, digest, audience and object subset must match the selected delivered packet.');
|
|
93
|
+
if (context && (receipt.caseId !== context.caseId || receipt.target !== context.target || (context.profile && context.profile !== correction.profile)
|
|
94
|
+
|| (context.selectedPinIds && correction.evidencePinIds.some(pin => !context.selectedPinIds.has(pin)))))
|
|
95
|
+
throw new TypeError('Review the current Case target, audience and every retained correction pin before preparation.');
|
|
96
|
+
return { original, event, receipt };
|
|
97
|
+
}
|
|
98
|
+
/** Called only with the exact completed packet; never uses a later editable draft. */
|
|
99
|
+
export function deliveryReceiptForPacket(packet) {
|
|
100
|
+
const action = packet.escalationHistory.find(action => action.actionId === packet.actionBinding.selectedActionId);
|
|
101
|
+
if (!action || !packet.recipientRoute || packet.authorisation.status !== 'authorised' || !action.responseObjects?.length)
|
|
102
|
+
throw new TypeError('A retained exact receipt requires an authorised action-bound packet with explicit object scope.');
|
|
103
|
+
return readCaseDeliveryPacketReceipt({ version: 1, caseId: packet.case.id, actionId: action.actionId, target: packet.case.domain,
|
|
104
|
+
packetDigestSha256: packet.integrity.digestSha256, packetVersion: packet.schemaVersion, packetGeneratedAt: packet.generatedAt,
|
|
105
|
+
recipient: packet.recipientRoute.contact, profile: packet.profile.id, responseObjects: action.responseObjects });
|
|
106
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { isValidAsciiHostname } from "
|
|
2
|
-
import { INCIDENT_CASE_SCHEMA_VERSION, MAX_RESPONSE_RATIONALE_LENGTH } from "../contracts/case-portability.mjs";
|
|
3
|
-
import { enumeration,
|
|
4
|
-
|
|
1
|
+
import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
|
|
2
|
+
import { INCIDENT_CASE_SCHEMA_VERSION, MAX_RESPONSE_RATIONALE_LENGTH, } from "../contracts/case-portability.mjs";
|
|
3
|
+
import { enumeration, exactOptional, text } from "../evidence/artifact-structure.mjs";
|
|
4
|
+
import { readCaseResponseObject, sameCaseResponseObject } from "./case-response-object.mjs";
|
|
5
|
+
import { casePinHasCompleteObservation } from "./case-evidence-quality.mjs";
|
|
6
|
+
export const CASE_RECHECK_CONDITIONS = Object.freeze({
|
|
7
|
+
unknown: 'Not established',
|
|
8
|
+
comparable: 'Comparable for this question',
|
|
9
|
+
different: 'Different conditions',
|
|
10
|
+
});
|
|
5
11
|
function reference(value, label) {
|
|
6
12
|
const id = text(value, label, 64);
|
|
7
13
|
if (!/^[A-Za-z0-9_-]+$/u.test(id))
|
|
8
14
|
throw new TypeError(`${label} is invalid.`);
|
|
9
15
|
return id;
|
|
10
16
|
}
|
|
11
|
-
function contextFields(item) {
|
|
17
|
+
function contextFields(item, sourceVersion) {
|
|
12
18
|
const targetHostname = text(item.targetHostname, 'Recheck target hostname', 253);
|
|
13
19
|
if (targetHostname !== targetHostname.toLowerCase() || !isValidAsciiHostname(targetHostname)) {
|
|
14
20
|
throw new TypeError('Use one normalised hostname for the recheck target, without a path or query.');
|
|
@@ -16,8 +22,17 @@ function contextFields(item) {
|
|
|
16
22
|
const conditions = text(item.conditions, 'Recheck comparison conditions', MAX_RESPONSE_RATIONALE_LENGTH);
|
|
17
23
|
if (!conditions.trim())
|
|
18
24
|
throw new TypeError('Describe the conditions needed to compare the observations.');
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
const responseObject = readCaseResponseObject(item.responseObject, sourceVersion);
|
|
26
|
+
if (responseObject && (responseObject.kind === 'domain' || responseObject.kind === 'hostname'
|
|
27
|
+
? responseObject.identifier : new URL(responseObject.identifier).hostname) !== targetHostname) {
|
|
28
|
+
throw new TypeError('The recheck target hostname must match the exact object snapshot.');
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
targetHostname,
|
|
32
|
+
conditions,
|
|
33
|
+
baselinePinId: item.baselinePinId === null ? null : reference(item.baselinePinId, 'Recheck baseline pin'),
|
|
34
|
+
...(responseObject ? { responseObject } : {}),
|
|
35
|
+
});
|
|
21
36
|
}
|
|
22
37
|
/** Optional current-format context; historical records have no invented question. */
|
|
23
38
|
export function readCaseRecheckContext(value, sourceVersion) {
|
|
@@ -25,67 +40,140 @@ export function readCaseRecheckContext(value, sourceVersion) {
|
|
|
25
40
|
return undefined;
|
|
26
41
|
if (sourceVersion != null && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION)
|
|
27
42
|
throw new TypeError('This Case format cannot contain structured recheck questions.');
|
|
28
|
-
return contextFields(
|
|
43
|
+
return contextFields(exactOptional(value, ['targetHostname', 'baselinePinId', 'conditions'], ['responseObject'], 'Recheck question context'), sourceVersion);
|
|
29
44
|
}
|
|
30
45
|
export function readCaseRecheckAnswerContext(value, sourceVersion) {
|
|
31
46
|
if (value === undefined)
|
|
32
47
|
return undefined;
|
|
33
48
|
if (sourceVersion != null && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION)
|
|
34
49
|
throw new TypeError('This Case format cannot contain structured recheck answers.');
|
|
35
|
-
const item =
|
|
50
|
+
const item = exactOptional(value, ['targetHostname', 'baselinePinId', 'conditions', 'questionId', 'question', 'conditionsMatch'], ['responseObject'], 'Recheck answer context');
|
|
36
51
|
const question = text(item.question, 'Recheck question', MAX_RESPONSE_RATIONALE_LENGTH);
|
|
37
52
|
if (!question.trim())
|
|
38
53
|
throw new TypeError('A recheck answer requires its original question.');
|
|
39
|
-
return Object.freeze({
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
...contextFields(item, sourceVersion),
|
|
56
|
+
questionId: reference(item.questionId, 'Recheck question ID'),
|
|
40
57
|
question,
|
|
41
|
-
conditionsMatch: enumeration(item.conditionsMatch, Object.keys(CASE_RECHECK_CONDITIONS), 'Recheck condition comparison')
|
|
58
|
+
conditionsMatch: enumeration(item.conditionsMatch, Object.keys(CASE_RECHECK_CONDITIONS), 'Recheck condition comparison'),
|
|
59
|
+
});
|
|
42
60
|
}
|
|
43
61
|
export function caseRecheckQuestions(assertions) {
|
|
44
|
-
return assertions.filter(item => item.kind === 'next_step' && item.recheck && item.state === 'open');
|
|
62
|
+
return assertions.filter((item) => item.kind === 'next_step' && item.recheck && item.state === 'open');
|
|
45
63
|
}
|
|
46
64
|
export function caseRecheckAnswerContext(question, conditionsMatch) {
|
|
47
65
|
if (question.kind !== 'next_step' || !question.recheck)
|
|
48
66
|
throw new TypeError('Select a saved recheck question.');
|
|
49
|
-
return readCaseRecheckAnswerContext({
|
|
67
|
+
return readCaseRecheckAnswerContext({
|
|
68
|
+
...question.recheck,
|
|
69
|
+
questionId: question.id,
|
|
70
|
+
question: question.statement,
|
|
71
|
+
conditionsMatch,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Shared read projection; each form retains its draft and existing save coordinator. */
|
|
75
|
+
export function selectCaseRecheckQuestion(assertions, selection) {
|
|
76
|
+
const questions = caseRecheckQuestions(assertions);
|
|
77
|
+
const question = questions.find((item) => item.id === selection.questionId);
|
|
78
|
+
return {
|
|
79
|
+
questions,
|
|
80
|
+
context: question ? caseRecheckAnswerContext(question, selection.conditionsMatch) : undefined,
|
|
81
|
+
stale: Boolean(selection.questionId) && (!question || question.updatedAt !== selection.questionUpdatedAt),
|
|
82
|
+
};
|
|
50
83
|
}
|
|
51
84
|
/** A saved answer is a snapshot, not a live reference to an editable plan. */
|
|
52
85
|
export function assertCurrentRecheckQuestion(answer, assertions) {
|
|
53
|
-
const question = caseRecheckQuestions(assertions).find(item => item.id === answer.questionId);
|
|
54
|
-
if (!question ||
|
|
86
|
+
const question = caseRecheckQuestions(assertions).find((item) => item.id === answer.questionId);
|
|
87
|
+
if (!question ||
|
|
88
|
+
JSON.stringify(caseRecheckAnswerContext(question, answer.conditionsMatch)) !==
|
|
89
|
+
JSON.stringify(answer)) {
|
|
55
90
|
throw new Error('The recheck question changed or was resolved. Refresh and review its current conditions before saving this answer.');
|
|
56
91
|
}
|
|
57
92
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
93
|
+
const RECHECK_COMPARISON_MESSAGES = {
|
|
94
|
+
different_conditions: 'The comparison conditions differ.',
|
|
95
|
+
unconfirmed_conditions: 'Comparable conditions have not been confirmed.',
|
|
96
|
+
missing_baseline: 'The baseline evidence is no longer available.',
|
|
97
|
+
current_target_mismatch: 'The current evidence concerns a different or unknown hostname.',
|
|
98
|
+
incomplete_current: 'The current source does not establish a complete observation.',
|
|
99
|
+
later_observation_needed: 'A later source observation is needed; reviewing the baseline again is not a recheck.',
|
|
100
|
+
baseline_target_mismatch: 'The baseline concerns a different hostname.',
|
|
101
|
+
different_field_or_source: 'The observations use different fields or sources.',
|
|
102
|
+
missing_object_baseline: 'An object-specific baseline is required for this exact-object comparison.',
|
|
103
|
+
baseline_object_mismatch: 'The baseline does not explicitly concern this same object.',
|
|
104
|
+
current_object_mismatch: 'The current observation does not explicitly concern this same object.',
|
|
105
|
+
incomplete_object_baseline: 'The exact-object baseline must be a complete source observation.',
|
|
106
|
+
};
|
|
107
|
+
/** Admissibility depends on these reasons, not on presentation warnings. */
|
|
108
|
+
export function caseRecheckComparisonBlockers(context, pins, current) {
|
|
109
|
+
const blockers = [];
|
|
110
|
+
if (context.conditionsMatch !== 'comparable') {
|
|
111
|
+
blockers.push(context.conditionsMatch === 'different'
|
|
112
|
+
? 'different_conditions'
|
|
113
|
+
: 'unconfirmed_conditions');
|
|
114
|
+
}
|
|
115
|
+
const baseline = pins.find((pin) => pin.id === context.baselinePinId);
|
|
116
|
+
if (context.responseObject) {
|
|
117
|
+
if (!baseline)
|
|
118
|
+
blockers.push('missing_object_baseline');
|
|
119
|
+
else if (!sameCaseResponseObject(baseline.responseObject, context.responseObject))
|
|
120
|
+
blockers.push('baseline_object_mismatch');
|
|
121
|
+
if (baseline && !casePinHasCompleteObservation(baseline))
|
|
122
|
+
blockers.push('incomplete_object_baseline');
|
|
123
|
+
if (!current || !sameCaseResponseObject(current.responseObject, context.responseObject))
|
|
124
|
+
blockers.push('current_object_mismatch');
|
|
125
|
+
}
|
|
64
126
|
if (context.baselinePinId && !baseline)
|
|
65
|
-
|
|
127
|
+
blockers.push('missing_baseline');
|
|
66
128
|
if (current) {
|
|
67
129
|
if (current.observationHostname !== context.targetHostname)
|
|
68
|
-
|
|
69
|
-
if (current
|
|
70
|
-
|
|
130
|
+
blockers.push('current_target_mismatch');
|
|
131
|
+
if (!casePinHasCompleteObservation(current))
|
|
132
|
+
blockers.push('incomplete_current');
|
|
71
133
|
if (baseline) {
|
|
72
|
-
|
|
73
|
-
|
|
134
|
+
const sameObservation = baseline.id === current.id;
|
|
135
|
+
const unknownObservationTime = !baseline.observedAt || !current.observedAt;
|
|
136
|
+
const nonLaterObservation = !unknownObservationTime &&
|
|
137
|
+
Date.parse(current.observedAt) <= Date.parse(baseline.observedAt);
|
|
138
|
+
if (sameObservation || unknownObservationTime || nonLaterObservation) {
|
|
139
|
+
blockers.push('later_observation_needed');
|
|
140
|
+
}
|
|
74
141
|
if (baseline.observationHostname && baseline.observationHostname !== context.targetHostname)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
142
|
+
blockers.push('baseline_target_mismatch');
|
|
143
|
+
const differentField = Boolean(baseline.field) && current.field !== baseline.field;
|
|
144
|
+
const differentSource = baseline.source !== current.source;
|
|
145
|
+
if (differentField || differentSource)
|
|
146
|
+
blockers.push('different_field_or_source');
|
|
78
147
|
}
|
|
79
148
|
}
|
|
80
|
-
return
|
|
149
|
+
return blockers;
|
|
150
|
+
}
|
|
151
|
+
export function caseRecheckComparisonWarnings(context, pins, current) {
|
|
152
|
+
return caseRecheckComparisonBlockers(context, pins, current)
|
|
153
|
+
.map((reason) => RECHECK_COMPARISON_MESSAGES[reason]);
|
|
81
154
|
}
|
|
82
155
|
export function assertRecheckNonReproduction(state, context, completeness, pins, current, observedAt) {
|
|
83
156
|
if (state !== 'not_reproduced')
|
|
84
157
|
return;
|
|
85
|
-
const
|
|
86
|
-
const baseline = pins.find(pin => pin.id === context.baselinePinId);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
158
|
+
const blockers = caseRecheckComparisonBlockers(context, pins, current);
|
|
159
|
+
const baseline = pins.find((pin) => pin.id === context.baselinePinId);
|
|
160
|
+
let laterIndependentObservationNeeded = false;
|
|
161
|
+
if (!current && baseline) {
|
|
162
|
+
const missingObservationTime = !baseline.observedAt || !observedAt;
|
|
163
|
+
const invalidObservationTime = observedAt ? !Number.isFinite(Date.parse(observedAt)) : false;
|
|
164
|
+
const nonLaterObservation = !missingObservationTime && Date.parse(observedAt) <= Date.parse(baseline.observedAt);
|
|
165
|
+
laterIndependentObservationNeeded = missingObservationTime || invalidObservationTime || nonLaterObservation;
|
|
166
|
+
}
|
|
167
|
+
if (completeness !== 'complete' || blockers.length || laterIndependentObservationNeeded) {
|
|
90
168
|
throw new Error('Not reproduced requires a complete observation under comparable conditions. Record unavailable or describe the limited observation instead.');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export const COMPARATIVE_CASE_OBJECT_OUTCOMES = Object.freeze(['removed', 'restricted', 'suspended', 'delisted', 'transferred', 'restored']);
|
|
172
|
+
/** Technical state changes need an exact-object baseline; procedural dispute is distinct. */
|
|
173
|
+
export function assertCaseObjectObservationOutcome(outcome, context, completeness, pins, current, observedAt) {
|
|
174
|
+
if (!outcome || !COMPARATIVE_CASE_OBJECT_OUTCOMES.includes(outcome))
|
|
175
|
+
return;
|
|
176
|
+
if (!context?.responseObject)
|
|
177
|
+
throw new TypeError('An independent object state change requires a saved exact-object baseline question and comparable evidence.');
|
|
178
|
+
assertRecheckNonReproduction('not_reproduced', context, completeness, pins, current, observedAt);
|
|
91
179
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANALYST_REVIEW_REASONS } from "
|
|
1
|
+
import { ANALYST_REVIEW_REASONS } from "../analysis/analyst-taxonomy.mjs";
|
|
2
2
|
import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, } from "../contracts/case-portability.mjs";
|
|
3
3
|
import { CASE_DISPOSITIONS, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_STATUS, } from "./case-record-decisions.mjs";
|
|
4
4
|
export { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, };
|
|
@@ -4,7 +4,7 @@ import { CASE_REVIEW_REASONS, CASE_SOURCES, DEFAULT_DISPOSITION, DEFAULT_SOURCE,
|
|
|
4
4
|
import { dispositionLabel, isValidDisposition, isValidStatus, statusLabel, } from "./case-record-decisions.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
6
|
import { PUBLISHED_V2_3_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
7
|
-
import { canonicalRegistrableDomain } from "
|
|
7
|
+
import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
|
|
8
8
|
// Forward-version policy (two distinct guarantees):
|
|
9
9
|
// - A locally-stored envelope that declares a version greater than this is
|
|
10
10
|
// still read best-effort on load (known fields kept, unknown dropped), but
|