@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- 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 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- 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-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -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 +345 -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-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +26 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -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 +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- 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 +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -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/intake-report.mjs +19 -0
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -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/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../analysis/bounded-json.mjs";
|
|
2
|
+
import { canonicalArtifactJsonV2 } from "../evidence/artifact-integrity.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
|
+
import { CASE_SCHEMA_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
5
|
+
import { normalizeCaseStore } from "./case-migration-model.mjs";
|
|
6
|
+
import { serializeCaseStore } from "./case-storage-model.mjs";
|
|
7
|
+
/** Historical wire shape remains independent of current in-memory defaults. */
|
|
8
|
+
export function canonicalCaseExportProjection(cases, caseVersion) {
|
|
9
|
+
return cases.map((record) => {
|
|
10
|
+
const { workflowMetadata: _workflow, ...historical } = record;
|
|
11
|
+
const value = caseVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION ? historical : record;
|
|
12
|
+
if (caseVersion !== PUBLISHED_V2_3_CASE_SCHEMA_VERSION)
|
|
13
|
+
return value;
|
|
14
|
+
if ([...value.evidencePins, ...value.sightings].some((item) => item.observedAt === null)) {
|
|
15
|
+
throw new TypeError('The published Case 15 format requires a known observation time on each retained pin and sighting.');
|
|
16
|
+
}
|
|
17
|
+
const { title: _title, ...published } = value;
|
|
18
|
+
return {
|
|
19
|
+
...published,
|
|
20
|
+
sightings: [...value.sightings].sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt)
|
|
21
|
+
|| (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Editing never silently repairs, drops or truncates an imported record. */
|
|
26
|
+
export function readEditableCaseExport(input) {
|
|
27
|
+
const root = parseBoundedJsonObject(input, {
|
|
28
|
+
label: 'Case file', maximumBytes: MAX_EDITABLE_CASE_INPUT_BYTES,
|
|
29
|
+
limits: boundedJsonLimitsForBytes(MAX_CASE_STORE_BYTES),
|
|
30
|
+
});
|
|
31
|
+
if (Object.keys(root).some(key => !['version', 'exportedAt', 'cases'].includes(key))) {
|
|
32
|
+
throw new TypeError('Case editing requires an ordinary Case export, not a response packet or an envelope with additional fields.');
|
|
33
|
+
}
|
|
34
|
+
if (!CLI_CASE_PACK_INPUT_CASE_VERSIONS.some(version => version === root.version) || !Array.isArray(root.cases)) {
|
|
35
|
+
throw new TypeError(`Case editing reads exact schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(', ')} and writes schema ${CASE_SCHEMA_VERSION}. Re-export other supported historical formats through the ordinary import workflow first.`);
|
|
36
|
+
}
|
|
37
|
+
if (Object.hasOwn(root, 'exportedAt') && !normalizeExplicitIsoTimestamp(root.exportedAt)) {
|
|
38
|
+
throw new TypeError('The Case export time is invalid.');
|
|
39
|
+
}
|
|
40
|
+
const cases = normalizeCaseStore(root).cases;
|
|
41
|
+
const projected = canonicalCaseExportProjection(cases, root.version);
|
|
42
|
+
const byId = new Map(projected.map(record => [record.id, record]));
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
if (cases.length !== root.cases.length || root.cases.some(value => {
|
|
45
|
+
if (!value || typeof value !== 'object' || typeof value.id !== 'string' || seen.has(value.id))
|
|
46
|
+
return true;
|
|
47
|
+
seen.add(value.id);
|
|
48
|
+
return canonicalArtifactJsonV2(value) !== canonicalArtifactJsonV2(byId.get(value.id));
|
|
49
|
+
}))
|
|
50
|
+
throw new TypeError('The Case file would lose, repair or truncate retained data during normalisation. Nothing was changed.');
|
|
51
|
+
if (new TextEncoder().encode(serializeCaseStore(cases)).byteLength > MAX_CASE_STORE_BYTES) {
|
|
52
|
+
throw new TypeError('The Case file exceeds the canonical store byte budget. Select fewer Cases; no retained evidence was pruned.');
|
|
53
|
+
}
|
|
54
|
+
return cases;
|
|
55
|
+
}
|
|
@@ -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,10 +1,22 @@
|
|
|
1
|
-
import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION,
|
|
2
|
-
import {
|
|
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";
|
|
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
|
-
import {
|
|
11
|
+
import { readCaseAttachments, mergeCaseAttachments } from "./case-attachment-model.mjs";
|
|
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";
|
|
5
15
|
import { inspectCaseBrandProfileIds, unionCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
|
|
6
16
|
import { caseInvestigationBranchReferences, mergeCaseInvestigationBranches, normalizeCaseInvestigationBranches, } from "./case-investigation-branch-model.mjs";
|
|
7
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";
|
|
8
20
|
export const MAX_CASE_INPUT_RECORDS = 2_000;
|
|
9
21
|
function compareCodeUnits(left, right) {
|
|
10
22
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -32,8 +44,8 @@ function assignUniqueIds(cases) {
|
|
|
32
44
|
}
|
|
33
45
|
/**
|
|
34
46
|
* Recovers a clean, bounded store from the current versioned envelope or an
|
|
35
|
-
* internal bare array, drops malformed records,
|
|
36
|
-
*
|
|
47
|
+
* internal bare array, drops malformed records, preserves distinct current
|
|
48
|
+
* Case identities, caps to MAX_CASES by recency,
|
|
37
49
|
* and guarantees globally unique safe ids. Retired, future, accessor-bearing,
|
|
38
50
|
* sparse, or otherwise non-JSON envelopes fail before normalisation.
|
|
39
51
|
* @param {unknown} raw
|
|
@@ -53,44 +65,53 @@ export function normalizeCaseStore(raw) {
|
|
|
53
65
|
if (sourceVersion > CASE_SCHEMA_VERSION) {
|
|
54
66
|
throw new TypeError(`Case schema ${sourceVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
55
67
|
}
|
|
56
|
-
if (sourceVersion
|
|
57
|
-
|
|
68
|
+
if (sourceVersion >= PUBLISHED_V2_3_CASE_SCHEMA_VERSION) {
|
|
69
|
+
assertModernCaseShape(raw, sourceVersion);
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
|
-
const
|
|
72
|
+
const byIdentity = new Map();
|
|
61
73
|
for (const item of boundedCaseList(raw).items) {
|
|
62
74
|
const normalized = normalizeCase(item, undefined, fallback, sourceVersion ?? CASE_SCHEMA_VERSION);
|
|
63
75
|
if (!normalized)
|
|
64
76
|
continue;
|
|
65
|
-
|
|
77
|
+
// Published domain-centred stores retain their original recovery rule.
|
|
78
|
+
// Current incidents share a domain without sharing analyst decisions.
|
|
79
|
+
const key = sourceVersion !== null && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION
|
|
80
|
+
? normalized.domain : `${normalized.domain}\u0000${normalized.id}`;
|
|
81
|
+
const existing = byIdentity.get(key);
|
|
66
82
|
if (!existing || Date.parse(normalized.updatedAt) >= Date.parse(existing.updatedAt)) {
|
|
67
|
-
|
|
83
|
+
byIdentity.set(key, normalized);
|
|
68
84
|
}
|
|
69
85
|
}
|
|
70
|
-
const cases = [...
|
|
86
|
+
const cases = [...byIdentity.values()]
|
|
71
87
|
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
72
88
|
.slice(0, MAX_CASES);
|
|
73
89
|
assignUniqueIds(cases);
|
|
74
90
|
return { version: CASE_SCHEMA_VERSION, cases };
|
|
75
91
|
}
|
|
76
|
-
function
|
|
92
|
+
function assertModernCaseShape(raw, sourceVersion) {
|
|
77
93
|
for (const item of boundedCaseList(raw).items) {
|
|
78
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.');
|
|
97
|
+
if (sourceVersion < INCIDENT_CASE_SCHEMA_VERSION && Object.hasOwn(itemRecord, 'attachments')) {
|
|
98
|
+
throw new TypeError('Retained file references require the current Case schema; no data was changed.');
|
|
99
|
+
}
|
|
79
100
|
const evidenceHistory = itemRecord.evidenceHistory;
|
|
80
101
|
for (const snapshot of Array.isArray(evidenceHistory) ? evidenceHistory : []) {
|
|
81
102
|
const record = objectRecord(snapshot);
|
|
82
103
|
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'inputHostname')) {
|
|
83
|
-
throw new TypeError(`Case schema ${
|
|
104
|
+
throw new TypeError(`Case schema ${sourceVersion} evidence snapshots must declare their exact submitted hostname, including null; no data was changed.`);
|
|
84
105
|
}
|
|
85
106
|
}
|
|
86
107
|
const actions = itemRecord.actions;
|
|
87
108
|
for (const action of Array.isArray(actions) ? actions : []) {
|
|
88
109
|
const record = objectRecord(action);
|
|
89
110
|
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'routeObservedAt')) {
|
|
90
|
-
throw new TypeError(`Case schema ${
|
|
111
|
+
throw new TypeError(`Case schema ${sourceVersion} response actions must declare their route observation time, including null; no data was changed.`);
|
|
91
112
|
}
|
|
92
113
|
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'routeReviewAfter')) {
|
|
93
|
-
throw new TypeError(`Case schema ${
|
|
114
|
+
throw new TypeError(`Case schema ${sourceVersion} response actions must declare their route review deadline, including null; no data was changed.`);
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
const decisions = itemRecord.decisions;
|
|
@@ -98,7 +119,7 @@ function assertCurrentCaseShape(raw) {
|
|
|
98
119
|
const record = objectRecord(decision);
|
|
99
120
|
if (Object.keys(record).length > 0
|
|
100
121
|
&& (!Object.hasOwn(record, 'confidence') || !Object.hasOwn(record, 'confidenceBasis'))) {
|
|
101
|
-
throw new TypeError(`Case schema ${
|
|
122
|
+
throw new TypeError(`Case schema ${sourceVersion} analyst decisions must declare confidence and its basis; no data was changed.`);
|
|
102
123
|
}
|
|
103
124
|
}
|
|
104
125
|
}
|
|
@@ -160,6 +181,8 @@ function boundedCaseList(raw) {
|
|
|
160
181
|
*/
|
|
161
182
|
function extractImportPatch(raw, importedVersion) {
|
|
162
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.');
|
|
163
186
|
const domain = normalizeDomain(record.domain);
|
|
164
187
|
if (!domain)
|
|
165
188
|
return null;
|
|
@@ -168,7 +191,7 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
168
191
|
|| null;
|
|
169
192
|
const normalizedFallback = importFallback || '1970-01-01T00:00:00.000Z';
|
|
170
193
|
const timestampOptions = {
|
|
171
|
-
legacyTimestamps: importedVersion <
|
|
194
|
+
legacyTimestamps: importedVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
|
|
172
195
|
sourceVersion: importedVersion,
|
|
173
196
|
};
|
|
174
197
|
const rawEvidence = Array.isArray(record.evidenceHistory) ? record.evidenceHistory : [];
|
|
@@ -196,9 +219,13 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
196
219
|
[record.sightings, sightings],
|
|
197
220
|
].reduce((total, [candidates, retained]) => total
|
|
198
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;
|
|
199
225
|
return {
|
|
200
226
|
domain,
|
|
201
227
|
rawId: typeof record.id === 'string' ? record.id : null,
|
|
228
|
+
title: typeof record.title === 'string' && importedVersion >= INCIDENT_CASE_SCHEMA_VERSION ? normalizeCaseObjective(record.title) : undefined,
|
|
202
229
|
status: isValidStatus(record.status) ? record.status : undefined,
|
|
203
230
|
disposition: isValidDisposition(record.disposition) ? record.disposition : undefined,
|
|
204
231
|
reviewReasonCode: Object.hasOwn(record, 'reviewReasonCode')
|
|
@@ -225,7 +252,10 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
225
252
|
branches: importedVersion >= 11
|
|
226
253
|
? normalizeCaseInvestigationBranches(record.branches, normalizedFallback, branchReferences, timestampOptions)
|
|
227
254
|
: [],
|
|
228
|
-
|
|
255
|
+
attachments: importedVersion >= INCIDENT_CASE_SCHEMA_VERSION ? readCaseAttachments(record.attachments) : undefined,
|
|
256
|
+
evidenceLinks: readCaseEvidenceLinks(record.evidenceLinks),
|
|
257
|
+
tags: workflow.tags,
|
|
258
|
+
workflowMetadata: hasWorkflow ? workflow.workflowMetadata : undefined,
|
|
229
259
|
// Imported notes fall back only to the imported record's own timestamps
|
|
230
260
|
// (never "now"), so a timestamp-less note gets a stable, deterministic time
|
|
231
261
|
// and id, and re-importing the same file cannot manufacture a duplicate or a
|
|
@@ -235,16 +265,6 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
235
265
|
updatedAt: caseTimestampOrNull(record.updatedAt, importedVersion),
|
|
236
266
|
};
|
|
237
267
|
}
|
|
238
|
-
/** @param {CaseNote[]} a @param {CaseNote[]} b @returns {CaseNote[]} */
|
|
239
|
-
function unionNotes(a, b) {
|
|
240
|
-
const byId = new Map();
|
|
241
|
-
for (const note of [...a, ...b]) {
|
|
242
|
-
if (!byId.has(note.id))
|
|
243
|
-
byId.set(note.id, note);
|
|
244
|
-
}
|
|
245
|
-
const notes = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
|
|
246
|
-
return notes.slice(Math.max(0, notes.length - MAX_NOTES_PER_CASE));
|
|
247
|
-
}
|
|
248
268
|
function retainLocalAuthoredRecords(local, imported, maximum) {
|
|
249
269
|
const records = [...local];
|
|
250
270
|
const retainedIds = new Set(records.map((item) => item.id));
|
|
@@ -261,23 +281,18 @@ function retainLocalAuthoredRecords(local, imported, maximum) {
|
|
|
261
281
|
}
|
|
262
282
|
return { records, omitted };
|
|
263
283
|
}
|
|
264
|
-
// Additive, deduplicated union of two evidence histories. Identical material
|
|
265
|
-
// collapses (earliest firstCapturedAt, latest capturedAt), distinct snapshots
|
|
266
|
-
// are retained subject to the per-case bound, and an older import can never
|
|
267
|
-
// move an existing observation backwards.
|
|
268
|
-
function mergeEvidenceHistories(local, imported, caseDomain) {
|
|
269
|
-
return normalizeEvidenceHistory([...local, ...imported], { source: 'import', fallback: null, caseDomain });
|
|
270
|
-
}
|
|
271
284
|
/** @param {ImportPatch} patch @param {string} now @returns {CaseRecord} */
|
|
272
285
|
function caseFromPatch(patch, now) {
|
|
273
286
|
return {
|
|
274
287
|
id: '', // assigned by mergeCases so it can guarantee uniqueness against locals
|
|
275
288
|
domain: patch.domain,
|
|
289
|
+
title: patch.title ?? '',
|
|
276
290
|
status: patch.status ?? DEFAULT_STATUS,
|
|
277
291
|
disposition: patch.disposition ?? DEFAULT_DISPOSITION,
|
|
278
292
|
reviewReasonCode: patch.reviewReasonCode ?? null,
|
|
279
293
|
brandProfileIds: patch.brandProfileIds,
|
|
280
294
|
tags: patch.tags,
|
|
295
|
+
workflowMetadata: patch.workflowMetadata ?? emptyCaseWorkflowMetadata(),
|
|
281
296
|
notes: patch.notes,
|
|
282
297
|
source: patch.source ?? DEFAULT_SOURCE,
|
|
283
298
|
evidenceHistory: patch.evidenceHistory,
|
|
@@ -290,6 +305,8 @@ function caseFromPatch(patch, now) {
|
|
|
290
305
|
observedEffects: patch.observedEffects,
|
|
291
306
|
closures: patch.closures,
|
|
292
307
|
branches: patch.branches,
|
|
308
|
+
...(patch.attachments === undefined ? {} : { attachments: patch.attachments }),
|
|
309
|
+
...(patch.evidenceLinks === undefined ? {} : { evidenceLinks: patch.evidenceLinks }),
|
|
293
310
|
createdAt: patch.createdAt || patch.updatedAt || now,
|
|
294
311
|
updatedAt: patch.updatedAt || patch.createdAt || now,
|
|
295
312
|
};
|
|
@@ -306,6 +323,8 @@ function caseFromPatch(patch, now) {
|
|
|
306
323
|
function applyImportPatch(local, patch) {
|
|
307
324
|
const importNewer = patch.updatedAt !== null && Date.parse(patch.updatedAt) > Date.parse(local.updatedAt);
|
|
308
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);
|
|
309
328
|
const pinSelection = retainLocalAuthoredRecords(local.evidencePins, patch.evidencePins, MAX_CASE_EVIDENCE_PINS);
|
|
310
329
|
const evidencePins = normalizeCaseEvidencePins(pinSelection.records, fallback);
|
|
311
330
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
@@ -323,7 +342,10 @@ function applyImportPatch(local, patch) {
|
|
|
323
342
|
const decisions = normalizeCaseDecisions(decisionSelection.records, fallback, pinIds);
|
|
324
343
|
const trailSelection = retainLocalAuthoredRecords(local.manualTrail, patch.manualTrail, MAX_CASE_MANUAL_TRAIL_EVENTS);
|
|
325
344
|
const manualTrail = normalizeCaseManualTrail(trailSelection.records, fallback);
|
|
345
|
+
const attachments = mergeCaseAttachments(local.attachments, patch.attachments);
|
|
346
|
+
const evidenceLinks = mergeCaseEvidenceLinks(local.evidenceLinks, patch.evidenceLinks);
|
|
326
347
|
const authoredHistoryOmitted = patch.authoredHistoryOmitted
|
|
348
|
+
+ noteSelection.omitted
|
|
327
349
|
+ pinSelection.omitted
|
|
328
350
|
+ decisionSelection.omitted
|
|
329
351
|
+ assertionSelection.omitted
|
|
@@ -331,12 +353,13 @@ function applyImportPatch(local, patch) {
|
|
|
331
353
|
+ sightingSelection.omitted;
|
|
332
354
|
return { record: {
|
|
333
355
|
...local,
|
|
356
|
+
title: patch.title !== undefined && importNewer ? patch.title : local.title ?? '',
|
|
334
357
|
status: patch.status !== undefined && importNewer ? patch.status : local.status,
|
|
335
358
|
disposition: patch.disposition !== undefined && importNewer ? patch.disposition : local.disposition,
|
|
336
359
|
reviewReasonCode: patch.reviewReasonCode !== undefined && importNewer ? patch.reviewReasonCode : local.reviewReasonCode ?? null,
|
|
337
360
|
brandProfileIds: brandProfileReferences.ids,
|
|
338
361
|
source: patch.source !== undefined && importNewer ? patch.source : local.source,
|
|
339
|
-
evidenceHistory:
|
|
362
|
+
evidenceHistory: historySelection.records,
|
|
340
363
|
evidencePins,
|
|
341
364
|
decisions,
|
|
342
365
|
actions,
|
|
@@ -346,11 +369,16 @@ function applyImportPatch(local, patch) {
|
|
|
346
369
|
observedEffects,
|
|
347
370
|
closures,
|
|
348
371
|
branches: mergeCaseInvestigationBranches(local.branches ?? [], patch.branches, fallback, branchReferences),
|
|
372
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
373
|
+
...(evidenceLinks === undefined ? {} : { evidenceLinks }),
|
|
349
374
|
tags: normalizeTags([...local.tags, ...patch.tags]),
|
|
350
|
-
|
|
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),
|
|
351
379
|
createdAt: patch.createdAt && Date.parse(patch.createdAt) < Date.parse(local.createdAt) ? patch.createdAt : local.createdAt,
|
|
352
380
|
updatedAt: importNewer ? (patch.updatedAt ?? local.updatedAt) : local.updatedAt,
|
|
353
|
-
}, brandProfileReferencesOmitted: brandProfileReferences.omitted, authoredHistoryOmitted };
|
|
381
|
+
}, brandProfileReferencesOmitted: brandProfileReferences.omitted, authoredHistoryOmitted, evidenceHistoryOmitted: historySelection.omitted };
|
|
354
382
|
}
|
|
355
383
|
function pickFreeId(preferred, domain, used) {
|
|
356
384
|
const wanted = safeId(preferred);
|
|
@@ -456,14 +484,14 @@ function caseCollectionImportEnvelope(importedRaw) {
|
|
|
456
484
|
}
|
|
457
485
|
/**
|
|
458
486
|
* Merges an imported (already parsed) value into the local cases. Predictable
|
|
459
|
-
* and idempotent: unknown records are skipped,
|
|
487
|
+
* and idempotent: unknown records are skipped, matching Case identities merge without
|
|
460
488
|
* losing newer local decisions, and new ones are added until the store bound is
|
|
461
489
|
* reached. An imported envelope that declares a schema version newer than we
|
|
462
490
|
* support is rejected up front (before any local data is touched) rather than
|
|
463
491
|
* reinterpreted.
|
|
464
492
|
* @param {CaseRecord[]} localCases
|
|
465
493
|
* @param {unknown} importedRaw
|
|
466
|
-
* @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 }}
|
|
467
495
|
*/
|
|
468
496
|
export function mergeCases(localCases, importedRaw) {
|
|
469
497
|
assertBoundedJsonStructure(importedRaw, 'Case import', CASE_INPUT_JSON_LIMITS);
|
|
@@ -480,11 +508,11 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
480
508
|
|| !Array.isArray(importedEnvelope.cases)) {
|
|
481
509
|
throw new Error(`Expected a well-formed WHOISleuth Case export using schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
482
510
|
}
|
|
483
|
-
if (importedVersion
|
|
484
|
-
|
|
511
|
+
if (importedVersion !== null && importedVersion >= PUBLISHED_V2_3_CASE_SCHEMA_VERSION)
|
|
512
|
+
assertModernCaseShape(importedEnvelope, importedVersion);
|
|
485
513
|
const local = normalizeCaseStore(localCases).cases;
|
|
486
514
|
const supportedImportedVersion = importedVersion ?? 0;
|
|
487
|
-
const
|
|
515
|
+
const byId = new Map(local.map((item) => [item.id, item]));
|
|
488
516
|
const usedIds = new Set(local.map((item) => item.id));
|
|
489
517
|
let added = 0;
|
|
490
518
|
let updated = 0;
|
|
@@ -492,26 +520,47 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
492
520
|
let skipped = imported.omitted;
|
|
493
521
|
let brandProfileReferencesOmitted = 0;
|
|
494
522
|
let authoredHistoryOmitted = 0;
|
|
523
|
+
let evidenceHistoryOmitted = 0;
|
|
495
524
|
const fallback = new Date(0).toISOString();
|
|
525
|
+
const importedIds = new Set();
|
|
496
526
|
for (const item of imported.items) {
|
|
497
527
|
const patch = extractImportPatch(item, supportedImportedVersion);
|
|
498
528
|
if (!patch) {
|
|
499
529
|
skipped += 1;
|
|
500
530
|
continue;
|
|
501
531
|
}
|
|
502
|
-
const
|
|
532
|
+
const importedId = safeId(patch.rawId);
|
|
533
|
+
if (supportedImportedVersion >= INCIDENT_CASE_SCHEMA_VERSION && importedId) {
|
|
534
|
+
if (importedIds.has(importedId))
|
|
535
|
+
throw new Error('Incident Case imports cannot repeat a declared Case ID. No data was changed.');
|
|
536
|
+
importedIds.add(importedId);
|
|
537
|
+
}
|
|
538
|
+
let existing = importedId ? byId.get(importedId) : undefined;
|
|
539
|
+
if (existing && existing.domain !== patch.domain) {
|
|
540
|
+
if (supportedImportedVersion >= INCIDENT_CASE_SCHEMA_VERSION)
|
|
541
|
+
throw new Error('An imported Case ID belongs to a different domain in this workspace. No data was changed.');
|
|
542
|
+
existing = undefined;
|
|
543
|
+
}
|
|
544
|
+
if (!existing && (supportedImportedVersion < INCIDENT_CASE_SCHEMA_VERSION || !importedId)) {
|
|
545
|
+
const matchingDomain = [...byId.values()].filter(record => record.domain === patch.domain);
|
|
546
|
+
if (matchingDomain.length > 1) {
|
|
547
|
+
throw new Error(`The imported record for ${patch.domain} has no matching Case identity and several incidents exist. Import it into a separate workspace to review it without merging incidents; no data was changed.`);
|
|
548
|
+
}
|
|
549
|
+
existing = matchingDomain[0];
|
|
550
|
+
}
|
|
503
551
|
if (existing) {
|
|
504
552
|
const merged = applyImportPatch(existing, patch);
|
|
505
|
-
|
|
553
|
+
byId.set(existing.id, merged.record);
|
|
506
554
|
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted + merged.brandProfileReferencesOmitted;
|
|
507
555
|
authoredHistoryOmitted += merged.authoredHistoryOmitted;
|
|
556
|
+
evidenceHistoryOmitted += merged.evidenceHistoryOmitted;
|
|
508
557
|
updated += 1;
|
|
509
558
|
}
|
|
510
|
-
else if (
|
|
559
|
+
else if (byId.size < MAX_CASES) {
|
|
511
560
|
const record = caseFromPatch(patch, fallback);
|
|
512
561
|
record.id = pickFreeId(patch.rawId, patch.domain, usedIds);
|
|
513
562
|
usedIds.add(record.id);
|
|
514
|
-
|
|
563
|
+
byId.set(record.id, record);
|
|
515
564
|
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted;
|
|
516
565
|
authoredHistoryOmitted += patch.authoredHistoryOmitted;
|
|
517
566
|
added += 1;
|
|
@@ -521,11 +570,12 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
521
570
|
}
|
|
522
571
|
}
|
|
523
572
|
return {
|
|
524
|
-
cases: normalizeCaseStore([...
|
|
573
|
+
cases: normalizeCaseStore([...byId.values()]).cases,
|
|
525
574
|
added,
|
|
526
575
|
updated,
|
|
527
576
|
skipped,
|
|
528
577
|
brandProfileReferencesOmitted,
|
|
529
578
|
authoredHistoryOmitted,
|
|
579
|
+
evidenceHistoryOmitted,
|
|
530
580
|
};
|
|
531
581
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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, exact, text } from "../evidence/artifact-structure.mjs";
|
|
4
|
+
import { casePinHasCompleteObservation } from "./case-evidence-quality.mjs";
|
|
5
|
+
export const CASE_RECHECK_CONDITIONS = Object.freeze({
|
|
6
|
+
unknown: 'Not established',
|
|
7
|
+
comparable: 'Comparable for this question',
|
|
8
|
+
different: 'Different conditions',
|
|
9
|
+
});
|
|
10
|
+
function reference(value, label) {
|
|
11
|
+
const id = text(value, label, 64);
|
|
12
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(id))
|
|
13
|
+
throw new TypeError(`${label} is invalid.`);
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
function contextFields(item) {
|
|
17
|
+
const targetHostname = text(item.targetHostname, 'Recheck target hostname', 253);
|
|
18
|
+
if (targetHostname !== targetHostname.toLowerCase() || !isValidAsciiHostname(targetHostname)) {
|
|
19
|
+
throw new TypeError('Use one normalised hostname for the recheck target, without a path or query.');
|
|
20
|
+
}
|
|
21
|
+
const conditions = text(item.conditions, 'Recheck comparison conditions', MAX_RESPONSE_RATIONALE_LENGTH);
|
|
22
|
+
if (!conditions.trim())
|
|
23
|
+
throw new TypeError('Describe the conditions needed to compare the observations.');
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
targetHostname,
|
|
26
|
+
conditions,
|
|
27
|
+
baselinePinId: item.baselinePinId === null ? null : reference(item.baselinePinId, 'Recheck baseline pin'),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Optional current-format context; historical records have no invented question. */
|
|
31
|
+
export function readCaseRecheckContext(value, sourceVersion) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
if (sourceVersion != null && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION)
|
|
35
|
+
throw new TypeError('This Case format cannot contain structured recheck questions.');
|
|
36
|
+
return contextFields(exact(value, ['targetHostname', 'baselinePinId', 'conditions'], 'Recheck question context'));
|
|
37
|
+
}
|
|
38
|
+
export function readCaseRecheckAnswerContext(value, sourceVersion) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (sourceVersion != null && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION)
|
|
42
|
+
throw new TypeError('This Case format cannot contain structured recheck answers.');
|
|
43
|
+
const item = exact(value, ['targetHostname', 'baselinePinId', 'conditions', 'questionId', 'question', 'conditionsMatch'], 'Recheck answer context');
|
|
44
|
+
const question = text(item.question, 'Recheck question', MAX_RESPONSE_RATIONALE_LENGTH);
|
|
45
|
+
if (!question.trim())
|
|
46
|
+
throw new TypeError('A recheck answer requires its original question.');
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
...contextFields(item),
|
|
49
|
+
questionId: reference(item.questionId, 'Recheck question ID'),
|
|
50
|
+
question,
|
|
51
|
+
conditionsMatch: enumeration(item.conditionsMatch, Object.keys(CASE_RECHECK_CONDITIONS), 'Recheck condition comparison'),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function caseRecheckQuestions(assertions) {
|
|
55
|
+
return assertions.filter((item) => item.kind === 'next_step' && item.recheck && item.state === 'open');
|
|
56
|
+
}
|
|
57
|
+
export function caseRecheckAnswerContext(question, conditionsMatch) {
|
|
58
|
+
if (question.kind !== 'next_step' || !question.recheck)
|
|
59
|
+
throw new TypeError('Select a saved recheck question.');
|
|
60
|
+
return readCaseRecheckAnswerContext({
|
|
61
|
+
...question.recheck,
|
|
62
|
+
questionId: question.id,
|
|
63
|
+
question: question.statement,
|
|
64
|
+
conditionsMatch,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Shared read projection; each form retains its draft and existing save coordinator. */
|
|
68
|
+
export function selectCaseRecheckQuestion(assertions, selection) {
|
|
69
|
+
const questions = caseRecheckQuestions(assertions);
|
|
70
|
+
const question = questions.find((item) => item.id === selection.questionId);
|
|
71
|
+
return {
|
|
72
|
+
questions,
|
|
73
|
+
context: question ? caseRecheckAnswerContext(question, selection.conditionsMatch) : undefined,
|
|
74
|
+
stale: Boolean(selection.questionId) && (!question || question.updatedAt !== selection.questionUpdatedAt),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** A saved answer is a snapshot, not a live reference to an editable plan. */
|
|
78
|
+
export function assertCurrentRecheckQuestion(answer, assertions) {
|
|
79
|
+
const question = caseRecheckQuestions(assertions).find((item) => item.id === answer.questionId);
|
|
80
|
+
if (!question ||
|
|
81
|
+
JSON.stringify(caseRecheckAnswerContext(question, answer.conditionsMatch)) !==
|
|
82
|
+
JSON.stringify(answer)) {
|
|
83
|
+
throw new Error('The recheck question changed or was resolved. Refresh and review its current conditions before saving this answer.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const RECHECK_COMPARISON_MESSAGES = {
|
|
87
|
+
different_conditions: 'The comparison conditions differ.',
|
|
88
|
+
unconfirmed_conditions: 'Comparable conditions have not been confirmed.',
|
|
89
|
+
missing_baseline: 'The baseline evidence is no longer available.',
|
|
90
|
+
current_target_mismatch: 'The current evidence concerns a different or unknown hostname.',
|
|
91
|
+
incomplete_current: 'The current source does not establish a complete observation.',
|
|
92
|
+
later_observation_needed: 'A later source observation is needed; reviewing the baseline again is not a recheck.',
|
|
93
|
+
baseline_target_mismatch: 'The baseline concerns a different hostname.',
|
|
94
|
+
different_field_or_source: 'The observations use different fields or sources.',
|
|
95
|
+
};
|
|
96
|
+
/** Admissibility depends on these reasons, not on presentation warnings. */
|
|
97
|
+
export function caseRecheckComparisonBlockers(context, pins, current) {
|
|
98
|
+
const blockers = [];
|
|
99
|
+
if (context.conditionsMatch !== 'comparable') {
|
|
100
|
+
blockers.push(context.conditionsMatch === 'different'
|
|
101
|
+
? 'different_conditions'
|
|
102
|
+
: 'unconfirmed_conditions');
|
|
103
|
+
}
|
|
104
|
+
const baseline = pins.find((pin) => pin.id === context.baselinePinId);
|
|
105
|
+
if (context.baselinePinId && !baseline)
|
|
106
|
+
blockers.push('missing_baseline');
|
|
107
|
+
if (current) {
|
|
108
|
+
if (current.observationHostname !== context.targetHostname)
|
|
109
|
+
blockers.push('current_target_mismatch');
|
|
110
|
+
if (!casePinHasCompleteObservation(current))
|
|
111
|
+
blockers.push('incomplete_current');
|
|
112
|
+
if (baseline) {
|
|
113
|
+
const sameObservation = baseline.id === current.id;
|
|
114
|
+
const unknownObservationTime = !baseline.observedAt || !current.observedAt;
|
|
115
|
+
const nonLaterObservation = !unknownObservationTime &&
|
|
116
|
+
Date.parse(current.observedAt) <= Date.parse(baseline.observedAt);
|
|
117
|
+
if (sameObservation || unknownObservationTime || nonLaterObservation) {
|
|
118
|
+
blockers.push('later_observation_needed');
|
|
119
|
+
}
|
|
120
|
+
if (baseline.observationHostname && baseline.observationHostname !== context.targetHostname)
|
|
121
|
+
blockers.push('baseline_target_mismatch');
|
|
122
|
+
const differentField = Boolean(baseline.field) && current.field !== baseline.field;
|
|
123
|
+
const differentSource = baseline.source !== current.source;
|
|
124
|
+
if (differentField || differentSource)
|
|
125
|
+
blockers.push('different_field_or_source');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return blockers;
|
|
129
|
+
}
|
|
130
|
+
export function caseRecheckComparisonWarnings(context, pins, current) {
|
|
131
|
+
return caseRecheckComparisonBlockers(context, pins, current)
|
|
132
|
+
.map((reason) => RECHECK_COMPARISON_MESSAGES[reason]);
|
|
133
|
+
}
|
|
134
|
+
export function assertRecheckNonReproduction(state, context, completeness, pins, current, observedAt) {
|
|
135
|
+
if (state !== 'not_reproduced')
|
|
136
|
+
return;
|
|
137
|
+
const blockers = caseRecheckComparisonBlockers(context, pins, current);
|
|
138
|
+
const baseline = pins.find((pin) => pin.id === context.baselinePinId);
|
|
139
|
+
let laterIndependentObservationNeeded = false;
|
|
140
|
+
if (!current && baseline) {
|
|
141
|
+
const missingObservationTime = !baseline.observedAt || !observedAt;
|
|
142
|
+
const invalidObservationTime = observedAt ? !Number.isFinite(Date.parse(observedAt)) : false;
|
|
143
|
+
const nonLaterObservation = !missingObservationTime && Date.parse(observedAt) <= Date.parse(baseline.observedAt);
|
|
144
|
+
laterIndependentObservationNeeded = missingObservationTime || invalidObservationTime || nonLaterObservation;
|
|
145
|
+
}
|
|
146
|
+
if (completeness !== 'complete' || blockers.length || laterIndependentObservationNeeded) {
|
|
147
|
+
throw new Error('Not reproduced requires a complete observation under comparable conditions. Record unavailable or describe the limited observation instead.');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -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, };
|