@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
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { canonicalDomainControlRecords } from "../packages/evidence/domain-control-runtime.mjs";
|
|
2
3
|
import { requireBoundedString, requireDomainName, requireIsoTimestamp, } from "./bounded-contract-normalizers.mjs";
|
|
3
|
-
import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_FIELD_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS, DOMAIN_CONTROL_FLIGHT_RECORDER_OBSERVATION_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_ROOT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, DOMAIN_CONTROL_FLIGHT_RECORDER_WINDOW_KEYS, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_BYTES, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES, MAX_FLIGHT_RECORDER_FIELDS, MAX_FLIGHT_RECORDER_EVENTS, MAX_FLIGHT_RECORDER_INPUT_VALUES, MAX_FLIGHT_RECORDER_JSON_DEPTH, MAX_FLIGHT_RECORDER_JSON_VALUES, MAX_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_SOURCE_LENGTH, MAX_FLIGHT_RECORDER_VALUE_LENGTH, MAX_FLIGHT_RECORDER_VALUES, MAX_FLIGHT_RECORDER_WINDOWS, MAX_FLIGHT_RECORDER_WINDOW_ID_LENGTH, MAX_FLIGHT_RECORDER_WINDOW_REASON_LENGTH, MIN_FLIGHT_RECORDER_OBSERVATIONS, } from "../packages/contracts/domain-control-flight-recorder.mjs";
|
|
4
|
+
import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_FIELD_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS, DOMAIN_CONTROL_FLIGHT_RECORDER_OBSERVATION_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_ROOT_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_OBSERVATION_KEYS, PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_FIELD_KEYS, PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS, PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS, SOURCE_TIMED_FLIGHT_RECORDER_EVENT_KEYS, SOURCE_TIMED_FLIGHT_RECORDER_LIMITATIONS, FLIGHT_RECORDER_CHANGE_INTERVAL_KEYS, HISTORICAL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS, PUBLIC_MAX_FLIGHT_RECORDER_INPUT_VALUES, PUBLIC_MAX_FLIGHT_RECORDER_VALUES, PUBLIC_MAX_FLIGHT_RECORDER_VALUE_LENGTH, SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS, DOMAIN_CONTROL_FLIGHT_RECORDER_WINDOW_KEYS, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_BYTES, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES, MAX_FLIGHT_RECORDER_FIELDS, MAX_FLIGHT_RECORDER_EVENTS, MAX_FLIGHT_RECORDER_INPUT_VALUES, MAX_FLIGHT_RECORDER_JSON_DEPTH, MAX_FLIGHT_RECORDER_JSON_VALUES, MAX_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_SOURCE_LENGTH, MAX_FLIGHT_RECORDER_VALUE_LENGTH, MAX_FLIGHT_RECORDER_VALUES, MAX_FLIGHT_RECORDER_WINDOWS, MAX_FLIGHT_RECORDER_WINDOW_ID_LENGTH, MAX_FLIGHT_RECORDER_WINDOW_REASON_LENGTH, MIN_FLIGHT_RECORDER_OBSERVATIONS, } from "../packages/contracts/domain-control-flight-recorder.mjs";
|
|
4
5
|
export { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, MAX_FLIGHT_RECORDER_FIELDS, MAX_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_VALUES, MAX_FLIGHT_RECORDER_WINDOWS, };
|
|
5
6
|
const ROOT_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_KEYS);
|
|
6
7
|
const OBSERVATION_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_OBSERVATION_KEYS);
|
|
7
8
|
const FIELD_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_FIELD_KEYS);
|
|
9
|
+
const PUBLIC_OBSERVATION_KEYS = new Set(PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_OBSERVATION_KEYS);
|
|
10
|
+
const PUBLIC_FIELD_KEYS = new Set(PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_FIELD_KEYS);
|
|
8
11
|
const WINDOW_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_WINDOW_KEYS);
|
|
9
12
|
const DOCUMENT_ROOT_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_ROOT_KEYS);
|
|
10
13
|
const EVENT_KEYS = new Set(DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS);
|
|
@@ -217,22 +220,23 @@ function assertPrettyJsonByteBudget(value, maximum, label) {
|
|
|
217
220
|
throw new TypeError(`${label} must be serialisable as bounded JSON.`);
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
|
-
function snapshotFlightRecorderInput(input) {
|
|
223
|
+
function snapshotFlightRecorderInput(input, legacy) {
|
|
221
224
|
const observations = boundedDataArray(input.observations, 'Domain control flight-recorder input observations', MIN_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_OBSERVATIONS).map((raw, index) => {
|
|
222
|
-
const item = exactRecord(raw, OBSERVATION_KEYS, `observations[${index}]`);
|
|
225
|
+
const item = exactRecord(raw, legacy ? PUBLIC_OBSERVATION_KEYS : OBSERVATION_KEYS, `observations[${index}]`);
|
|
223
226
|
const fields = boundedDataArray(item.fields, `observations[${index}].fields`, 0, MAX_FLIGHT_RECORDER_FIELDS)
|
|
224
227
|
.map((fieldRaw, fieldIndex) => {
|
|
225
|
-
const field = exactRecord(fieldRaw, FIELD_KEYS, `observations[${index}].fields[${fieldIndex}]`);
|
|
228
|
+
const field = exactRecord(fieldRaw, legacy ? PUBLIC_FIELD_KEYS : FIELD_KEYS, `observations[${index}].fields[${fieldIndex}]`);
|
|
226
229
|
return Object.freeze({
|
|
227
230
|
id: field.id,
|
|
228
231
|
source: field.source,
|
|
229
232
|
state: field.state,
|
|
230
|
-
|
|
233
|
+
...(!legacy ? { observedAt: field.observedAt } : {}),
|
|
234
|
+
values: boundedDataArray(field.values, `observations[${index}].fields[${fieldIndex}].values`, 0, legacy ? PUBLIC_MAX_FLIGHT_RECORDER_INPUT_VALUES : MAX_FLIGHT_RECORDER_INPUT_VALUES),
|
|
231
235
|
});
|
|
232
236
|
});
|
|
233
237
|
return Object.freeze({
|
|
234
238
|
domain: item.domain,
|
|
235
|
-
observedAt: item.observedAt,
|
|
239
|
+
...(legacy ? { observedAt: item.observedAt } : { capturedAt: item.capturedAt }),
|
|
236
240
|
collectionDepth: item.collectionDepth,
|
|
237
241
|
fields: Object.freeze(fields),
|
|
238
242
|
});
|
|
@@ -251,7 +255,7 @@ function snapshotFlightRecorderInput(input) {
|
|
|
251
255
|
});
|
|
252
256
|
return Object.freeze({
|
|
253
257
|
schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
|
|
254
|
-
version:
|
|
258
|
+
version: input.version,
|
|
255
259
|
observations: Object.freeze(observations),
|
|
256
260
|
approvedWindows: Object.freeze(approvedWindows),
|
|
257
261
|
});
|
|
@@ -262,25 +266,41 @@ function text(value, label, maximum = 240) {
|
|
|
262
266
|
.replace(/\s+/gu, ' ')
|
|
263
267
|
.trim();
|
|
264
268
|
}
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
const RECORD_FIELDS = Object.freeze({
|
|
270
|
+
registry_nameservers: 'nameservers', whois_nameservers: 'nameservers', delegated_nameservers: 'nameservers',
|
|
271
|
+
delegation_ds: 'ds', mail_exchangers: 'mx', caa_policy: 'caa',
|
|
272
|
+
});
|
|
273
|
+
function recorderValue(value, label, legacy, field, complete = true) {
|
|
274
|
+
if (legacy)
|
|
275
|
+
return text(value, label, PUBLIC_MAX_FLIGHT_RECORDER_VALUE_LENGTH).toLowerCase();
|
|
276
|
+
if (typeof value !== 'string' || !value.trim() || value.length > MAX_FLIGHT_RECORDER_VALUE_LENGTH
|
|
277
|
+
|| /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
278
|
+
throw new TypeError(`${label} must be bounded text without control characters.`);
|
|
279
|
+
}
|
|
280
|
+
const recordField = RECORD_FIELDS[field];
|
|
281
|
+
return recordField && complete ? canonicalDomainControlRecords([value], recordField)[0] : value.trim();
|
|
282
|
+
}
|
|
283
|
+
function values(value, label, legacy, field, complete) {
|
|
284
|
+
const input = boundedDataArray(value, label, 0, legacy ? PUBLIC_MAX_FLIGHT_RECORDER_INPUT_VALUES : MAX_FLIGHT_RECORDER_INPUT_VALUES);
|
|
285
|
+
const maximum = legacy ? PUBLIC_MAX_FLIGHT_RECORDER_VALUES : MAX_FLIGHT_RECORDER_VALUES;
|
|
267
286
|
const output = new Set();
|
|
268
287
|
for (let index = 0; index < input.length; index += 1) {
|
|
269
|
-
const normalized =
|
|
288
|
+
const normalized = recorderValue(input[index], `${label}[${index}]`, legacy, field, complete);
|
|
270
289
|
if (normalized)
|
|
271
290
|
output.add(normalized);
|
|
272
|
-
if (output.size >
|
|
273
|
-
throw new TypeError(`${label} must contain at most ${
|
|
291
|
+
if (output.size > maximum) {
|
|
292
|
+
throw new TypeError(`${label} must contain at most ${maximum} unique normalized values.`);
|
|
274
293
|
}
|
|
275
294
|
}
|
|
276
295
|
return [...output].sort(ordinalCompare);
|
|
277
296
|
}
|
|
278
|
-
function observation(value, index) {
|
|
279
|
-
const item = exactRecord(value, OBSERVATION_KEYS, `observations[${index}]`);
|
|
297
|
+
function observation(value, index, legacy) {
|
|
298
|
+
const item = exactRecord(value, legacy ? PUBLIC_OBSERVATION_KEYS : OBSERVATION_KEYS, `observations[${index}]`);
|
|
299
|
+
const capturedAt = requireIsoTimestamp(legacy ? item.observedAt : item.capturedAt, `observations[${index}].${legacy ? 'observedAt' : 'capturedAt'}`);
|
|
280
300
|
const inputFields = boundedDataArray(item.fields, `observations[${index}].fields`, 0, MAX_FLIGHT_RECORDER_FIELDS);
|
|
281
301
|
const seen = new Set();
|
|
282
302
|
const fields = inputFields.map((raw, fieldIndex) => {
|
|
283
|
-
const field = exactRecord(raw, FIELD_KEYS, `observations[${index}].fields[${fieldIndex}]`);
|
|
303
|
+
const field = exactRecord(raw, legacy ? PUBLIC_FIELD_KEYS : FIELD_KEYS, `observations[${index}].fields[${fieldIndex}]`);
|
|
284
304
|
if (typeof field.id !== 'string' || !FIELD_IDS.has(field.id) || seen.has(field.id)) {
|
|
285
305
|
throw new TypeError(`observations[${index}] contains an unsupported or duplicate field.`);
|
|
286
306
|
}
|
|
@@ -288,11 +308,27 @@ function observation(value, index) {
|
|
|
288
308
|
throw new TypeError(`observations[${index}].fields[${fieldIndex}].state is unsupported.`);
|
|
289
309
|
}
|
|
290
310
|
seen.add(field.id);
|
|
311
|
+
let fieldState = field.state;
|
|
312
|
+
let fieldValues = values(field.values, `observations[${index}].fields[${fieldIndex}].values`, legacy, field.id, fieldState === 'observed');
|
|
313
|
+
const recordField = RECORD_FIELDS[field.id];
|
|
314
|
+
if (legacy && recordField) {
|
|
315
|
+
try {
|
|
316
|
+
fieldValues = canonicalDomainControlRecords(fieldValues, recordField);
|
|
317
|
+
}
|
|
318
|
+
catch (cause) {
|
|
319
|
+
if (!(cause instanceof TypeError))
|
|
320
|
+
throw cause;
|
|
321
|
+
if (fieldState === 'observed')
|
|
322
|
+
fieldState = 'partial';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
291
325
|
return Object.freeze({
|
|
292
326
|
id: field.id,
|
|
293
327
|
source: text(field.source, `observations[${index}].fields[${fieldIndex}].source`, MAX_FLIGHT_RECORDER_SOURCE_LENGTH),
|
|
294
|
-
state:
|
|
295
|
-
values: Object.freeze(
|
|
328
|
+
state: fieldState,
|
|
329
|
+
values: Object.freeze(fieldValues),
|
|
330
|
+
observedAt: legacy ? capturedAt : field.observedAt === null ? null
|
|
331
|
+
: requireIsoTimestamp(field.observedAt, `observations[${index}].fields[${fieldIndex}].observedAt`),
|
|
296
332
|
});
|
|
297
333
|
}).sort((left, right) => ordinalCompare(left.id, right.id));
|
|
298
334
|
const collectionDepth = item.collectionDepth === 'deep' || item.collectionDepth === 'fast'
|
|
@@ -304,11 +340,18 @@ function observation(value, index) {
|
|
|
304
340
|
throw new TypeError(`observations[${index}].collectionDepth is unsupported.`);
|
|
305
341
|
return Object.freeze({
|
|
306
342
|
domain: requireDomainName(item.domain, `observations[${index}].domain`),
|
|
307
|
-
|
|
343
|
+
capturedAt,
|
|
308
344
|
collectionDepth,
|
|
309
345
|
fields: Object.freeze(fields),
|
|
310
346
|
});
|
|
311
347
|
}
|
|
348
|
+
export function normalizeDomainControlFlightRecorderObservations(value, version) {
|
|
349
|
+
if (!SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS.some((supported) => supported === version)) {
|
|
350
|
+
throw new TypeError('Domain control observations use an unsupported version.');
|
|
351
|
+
}
|
|
352
|
+
return Object.freeze(boundedDataArray(value, 'observations', MIN_FLIGHT_RECORDER_OBSERVATIONS, MAX_FLIGHT_RECORDER_OBSERVATIONS)
|
|
353
|
+
.map((item, index) => observation(item, index, version === PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION)));
|
|
354
|
+
}
|
|
312
355
|
function approvedWindow(value, index) {
|
|
313
356
|
const item = exactRecord(value, WINDOW_KEYS, `approvedWindows[${index}]`);
|
|
314
357
|
const inputFields = boundedDataArray(item.fields, `approvedWindows[${index}].fields`, 1, MAX_FLIGHT_RECORDER_FIELDS);
|
|
@@ -335,109 +378,158 @@ function approvedWindow(value, index) {
|
|
|
335
378
|
function sameValues(left, right) {
|
|
336
379
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
337
380
|
}
|
|
338
|
-
function matchingWindow(windows, domain, field,
|
|
339
|
-
const time = Date.parse(observedAt);
|
|
381
|
+
function matchingWindow(windows, domain, field, after, by) {
|
|
340
382
|
return windows.find((window) => (window.domain === domain
|
|
341
383
|
&& window.fields.includes(field)
|
|
342
|
-
&& Date.parse(window.startsAt) <=
|
|
343
|
-
&&
|
|
384
|
+
&& Date.parse(window.startsAt) <= Date.parse(after)
|
|
385
|
+
&& Date.parse(by) <= Date.parse(window.endsAt))) ?? null;
|
|
386
|
+
}
|
|
387
|
+
/** Equal-capture disagreement stays incomplete, independently of input ordering. */
|
|
388
|
+
export function mergeConcurrentDomainControlFields(observations) {
|
|
389
|
+
return Object.freeze(DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS.flatMap((id) => {
|
|
390
|
+
const candidates = observations.flatMap((fields) => fields.filter((field) => field.id === id));
|
|
391
|
+
const first = candidates[0];
|
|
392
|
+
if (!first)
|
|
393
|
+
return [];
|
|
394
|
+
const identity = (field) => JSON.stringify([field.source, field.state, field.observedAt, field.values]);
|
|
395
|
+
if (candidates.every((field) => identity(field) === identity(first)))
|
|
396
|
+
return [first];
|
|
397
|
+
const retained = [...new Set(candidates.flatMap((field) => field.values))].sort(ordinalCompare);
|
|
398
|
+
const omitted = Math.max(0, retained.length - MAX_FLIGHT_RECORDER_VALUES);
|
|
399
|
+
const sources = new Set(candidates.map((field) => field.source));
|
|
400
|
+
const times = new Set(candidates.map((field) => field.observedAt));
|
|
401
|
+
return [Object.freeze({
|
|
402
|
+
id, source: `${sources.size === 1 ? first.source.slice(0, 60) : 'Multiple sources'} (conflicting observations; ${omitted} omitted)`,
|
|
403
|
+
state: 'partial', observedAt: times.size === 1 ? first.observedAt : null,
|
|
404
|
+
values: Object.freeze(retained.slice(0, MAX_FLIGHT_RECORDER_VALUES)),
|
|
405
|
+
})];
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
function observationTimeline(observations) {
|
|
409
|
+
const cohorts = new Map();
|
|
410
|
+
for (const item of observations) {
|
|
411
|
+
const key = `${item.domain}\u0000${item.capturedAt}`;
|
|
412
|
+
const cohort = cohorts.get(key) ?? [];
|
|
413
|
+
cohort.push(item);
|
|
414
|
+
cohorts.set(key, cohort);
|
|
415
|
+
}
|
|
416
|
+
return [...cohorts.values()].map((cohort) => {
|
|
417
|
+
const first = cohort[0];
|
|
418
|
+
const collectionDepth = cohort.every((item) => item.collectionDepth === first.collectionDepth)
|
|
419
|
+
? first.collectionDepth : 'unknown';
|
|
420
|
+
return Object.freeze({ ...first, collectionDepth, fields: mergeConcurrentDomainControlFields(cohort.map((item) => item.fields)) });
|
|
421
|
+
}).sort((left, right) => Date.parse(left.capturedAt) - Date.parse(right.capturedAt)
|
|
422
|
+
|| ordinalCompare(left.domain, right.domain));
|
|
344
423
|
}
|
|
345
424
|
export function buildDomainControlFlightRecorder(inputRaw, generatedAtValue = new Date().toISOString()) {
|
|
346
425
|
const inputRoot = exactRecord(inputRaw, ROOT_KEYS, 'Domain control flight-recorder input');
|
|
347
426
|
if (inputRoot.schema !== DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA
|
|
348
|
-
||
|
|
349
|
-
throw new TypeError(`Domain control flight-recorder input must use ${DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA} version ${
|
|
427
|
+
|| !SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS.some((version) => version === inputRoot.version)) {
|
|
428
|
+
throw new TypeError(`Domain control flight-recorder input must use ${DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA} supported version ${SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS.join(' or ')}.`);
|
|
350
429
|
}
|
|
351
|
-
const input = snapshotFlightRecorderInput(inputRoot);
|
|
430
|
+
const input = snapshotFlightRecorderInput(inputRoot, inputRoot.version === PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION);
|
|
352
431
|
assertPrettyJsonByteBudget(input, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_BYTES, 'Domain control flight-recorder input');
|
|
353
|
-
const inputObservations = input.observations;
|
|
354
432
|
const inputWindows = input.approvedWindows;
|
|
355
|
-
const observations =
|
|
356
|
-
.sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt)
|
|
357
|
-
|| ordinalCompare(left.domain, right.domain));
|
|
433
|
+
const observations = normalizeDomainControlFlightRecorderObservations(input.observations, Number(input.version));
|
|
358
434
|
const windows = inputWindows.map(approvedWindow);
|
|
359
435
|
if (new Set(windows.map((window) => window.id)).size !== windows.length) {
|
|
360
436
|
throw new TypeError('Approved change-window ids must be unique.');
|
|
361
437
|
}
|
|
362
438
|
const generatedAt = requireIsoTimestamp(generatedAtValue, 'generatedAt');
|
|
363
|
-
if (observations.some((item) => Date.parse(item.
|
|
364
|
-
throw new TypeError('Domain control flight-recorder
|
|
439
|
+
if (observations.some((item) => Date.parse(item.capturedAt) > Date.parse(generatedAt))) {
|
|
440
|
+
throw new TypeError('Domain control flight-recorder captures cannot be later than generatedAt.');
|
|
365
441
|
}
|
|
366
442
|
const latest = new Map();
|
|
443
|
+
const lastComplete = new Map();
|
|
444
|
+
const incompleteSince = new Map();
|
|
367
445
|
const events = [];
|
|
368
|
-
|
|
369
|
-
|
|
446
|
+
let incompleteFields = 0;
|
|
447
|
+
for (const item of observationTimeline(observations)) {
|
|
448
|
+
const fields = [...item.fields];
|
|
449
|
+
for (const [key, previous] of latest) {
|
|
450
|
+
if (key.startsWith(`${item.domain}\u0000`) && !fields.some(field => field.id === previous.field.id)) {
|
|
451
|
+
fields.push({ id: previous.field.id, source: 'Not supplied in this capture', state: 'unavailable', observedAt: null, values: [] });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
fields.sort((a, b) => ordinalCompare(a.id, b.id));
|
|
455
|
+
for (const field of fields) {
|
|
370
456
|
const key = `${item.domain}\u0000${field.id}`;
|
|
371
457
|
const previous = latest.get(key);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
field: field.id,
|
|
380
|
-
observedAt: item.observedAt,
|
|
381
|
-
kind: 'first_observation',
|
|
382
|
-
state: 'observed',
|
|
383
|
-
before: Object.freeze([]),
|
|
384
|
-
after: field.values,
|
|
385
|
-
source: field.source,
|
|
386
|
-
approvedWindow: null,
|
|
387
|
-
explanation: 'The first complete value for this source-attributed field was retained.',
|
|
388
|
-
}));
|
|
458
|
+
let baseline = lastComplete.get(key);
|
|
459
|
+
let complete = field.state === 'observed' && field.observedAt !== null
|
|
460
|
+
&& Date.parse(field.observedAt) <= Date.parse(item.capturedAt) && item.collectionDepth !== 'unknown';
|
|
461
|
+
const sameContext = previous?.field.source === field.source && previous.collectionDepth === item.collectionDepth;
|
|
462
|
+
const repeated = previous?.complete && complete && sameContext
|
|
463
|
+
&& previous.field.observedAt === field.observedAt && sameValues(previous.field.values, field.values);
|
|
464
|
+
if (repeated)
|
|
389
465
|
continue;
|
|
466
|
+
let limitation = field.observedAt === null ? 'The source observation time is unavailable.'
|
|
467
|
+
: Date.parse(field.observedAt) > Date.parse(item.capturedAt) ? 'The source observation time is later than capture.'
|
|
468
|
+
: item.collectionDepth === 'unknown' ? 'Collection conditions are unknown or conflicting.'
|
|
469
|
+
: `The source is ${field.state}.`;
|
|
470
|
+
const contextChanged = baseline && field.source !== 'Not supplied in this capture'
|
|
471
|
+
&& (baseline.field.source !== field.source || (item.collectionDepth !== 'unknown' && baseline.collectionDepth !== item.collectionDepth));
|
|
472
|
+
if (contextChanged) {
|
|
473
|
+
if (complete) {
|
|
474
|
+
complete = false;
|
|
475
|
+
limitation = 'The source identity or collection conditions changed.';
|
|
476
|
+
}
|
|
477
|
+
lastComplete.delete(key);
|
|
478
|
+
baseline = undefined;
|
|
390
479
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
? 'A complete observed value changed during an analyst-approved change window. The evidence is retained and labelled expected rather than removed.'
|
|
408
|
-
: 'Two complete source-attributed observations contain different values.',
|
|
409
|
-
}));
|
|
480
|
+
else if (complete && baseline && Date.parse(field.observedAt) <= Date.parse(baseline.field.observedAt)) {
|
|
481
|
+
complete = false;
|
|
482
|
+
limitation = 'The source observation time did not increase.';
|
|
483
|
+
}
|
|
484
|
+
latest.set(key, { field, collectionDepth: item.collectionDepth, complete });
|
|
485
|
+
let kind;
|
|
486
|
+
let explanation;
|
|
487
|
+
let before = [];
|
|
488
|
+
let approved = null;
|
|
489
|
+
let changeInterval = null;
|
|
490
|
+
if (!complete) {
|
|
491
|
+
incompleteSince.set(key, (incompleteSince.get(key) ?? 0) + 1);
|
|
492
|
+
incompleteFields += 1;
|
|
493
|
+
kind = previous?.complete ? 'collection_change' : 'incomplete_observation';
|
|
494
|
+
before = previous?.complete ? previous.field.values : [];
|
|
495
|
+
explanation = `${limitation} Retained values are incomplete; this is not evidence that a prior value disappeared or changed.`;
|
|
410
496
|
}
|
|
411
|
-
else if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
before: Object.freeze([]),
|
|
420
|
-
after: field.values,
|
|
421
|
-
source: field.source,
|
|
422
|
-
approvedWindow: null,
|
|
423
|
-
explanation: 'Collection returned a complete value after an earlier incomplete source state.',
|
|
424
|
-
}));
|
|
497
|
+
else if (baseline && !sameValues(baseline.field.values, field.values)) {
|
|
498
|
+
kind = 'observed_change';
|
|
499
|
+
before = baseline.field.values;
|
|
500
|
+
changeInterval = Object.freeze({ after: baseline.field.observedAt, by: field.observedAt, incompleteObservations: incompleteSince.get(key) ?? 0 });
|
|
501
|
+
approved = matchingWindow(windows, item.domain, field.id, changeInterval.after, changeInterval.by);
|
|
502
|
+
explanation = `Comparable complete observations contain different values. The change occurred after ${changeInterval.after} and by ${changeInterval.by}; its exact time is unknown.`
|
|
503
|
+
+ (changeInterval.incompleteObservations ? ` ${changeInterval.incompleteObservations} incomplete observation(s) occurred in this interval.` : '')
|
|
504
|
+
+ (approved ? ' The recorded approved window contains the entire interval.' : ' No recorded approved window contains the entire interval.');
|
|
425
505
|
}
|
|
426
|
-
else if (previous
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
domain: item.domain,
|
|
430
|
-
field: field.id,
|
|
431
|
-
observedAt: item.observedAt,
|
|
432
|
-
kind: 'collection_change',
|
|
433
|
-
state: 'partial',
|
|
434
|
-
before: previous.values,
|
|
435
|
-
after: Object.freeze([]),
|
|
436
|
-
source: field.source,
|
|
437
|
-
approvedWindow: null,
|
|
438
|
-
explanation: `The source became ${field.state}; this is a collection-quality change, not evidence that the prior value disappeared.`,
|
|
439
|
-
}));
|
|
506
|
+
else if (!previous) {
|
|
507
|
+
kind = 'first_observation';
|
|
508
|
+
explanation = 'The first complete source-timed value for this field was retained.';
|
|
440
509
|
}
|
|
510
|
+
else if (!previous.complete) {
|
|
511
|
+
kind = 'recovered';
|
|
512
|
+
explanation = baseline
|
|
513
|
+
? 'The recovered complete value matches the last comparable complete observation. Intervening changes during the incomplete interval remain unknown.'
|
|
514
|
+
: 'A complete source-timed value is available after an incomplete observation; this does not establish a value change.';
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
lastComplete.set(key, { field, collectionDepth: item.collectionDepth, complete });
|
|
518
|
+
incompleteSince.delete(key);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (complete) {
|
|
522
|
+
lastComplete.set(key, { field, collectionDepth: item.collectionDepth, complete });
|
|
523
|
+
incompleteSince.delete(key);
|
|
524
|
+
}
|
|
525
|
+
events.push(Object.freeze({
|
|
526
|
+
id: `${item.domain}:${field.id}:${item.capturedAt}:${events.length + 1}`,
|
|
527
|
+
domain: item.domain, field: field.id, observedAt: field.observedAt, capturedAt: item.capturedAt,
|
|
528
|
+
kind, state: complete ? 'observed' : 'partial', before: Object.freeze([...before]), after: field.values,
|
|
529
|
+
source: field.source, approvedWindow: approved ? Object.freeze({ id: approved.id, reason: approved.reason, startsAt: approved.startsAt, endsAt: approved.endsAt }) : null,
|
|
530
|
+
collectionDepth: item.collectionDepth, changeInterval,
|
|
531
|
+
explanation,
|
|
532
|
+
}));
|
|
441
533
|
}
|
|
442
534
|
}
|
|
443
535
|
const domains = [...new Set(observations.map((item) => item.domain))].sort(ordinalCompare);
|
|
@@ -457,6 +549,7 @@ export function buildDomainControlFlightRecorder(inputRaw, generatedAtValue = ne
|
|
|
457
549
|
unexpectedChanges: observedChanges.filter((event) => event.approvedWindow === null).length,
|
|
458
550
|
collectionChanges: collectionChanges.length,
|
|
459
551
|
recoveredSources: events.filter((event) => event.kind === 'recovered').length,
|
|
552
|
+
incompleteFields,
|
|
460
553
|
}),
|
|
461
554
|
limitations: DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS,
|
|
462
555
|
});
|
|
@@ -475,9 +568,15 @@ function exactRecorderInteger(value, minimum, maximum, label) {
|
|
|
475
568
|
}
|
|
476
569
|
return Number(value);
|
|
477
570
|
}
|
|
478
|
-
function exactRecorderValues(value, label) {
|
|
479
|
-
const input = boundedDataArray(value, label, 0, MAX_FLIGHT_RECORDER_VALUES);
|
|
480
|
-
const normalized = input.map((item, index) =>
|
|
571
|
+
function exactRecorderValues(value, label, legacy, field, complete) {
|
|
572
|
+
const input = boundedDataArray(value, label, 0, legacy ? PUBLIC_MAX_FLIGHT_RECORDER_VALUES : MAX_FLIGHT_RECORDER_VALUES);
|
|
573
|
+
const normalized = input.map((item, index) => {
|
|
574
|
+
const result = legacy ? exactRecorderText(item, `${label}[${index}]`, PUBLIC_MAX_FLIGHT_RECORDER_VALUE_LENGTH)
|
|
575
|
+
: recorderValue(item, `${label}[${index}]`, false, field, complete);
|
|
576
|
+
if (result !== item)
|
|
577
|
+
throw new TypeError(`${label} must use canonical values.`);
|
|
578
|
+
return result;
|
|
579
|
+
});
|
|
481
580
|
if (new Set(normalized).size !== normalized.length
|
|
482
581
|
|| normalized.some((item, index) => index > 0 && normalized[index - 1] >= item)) {
|
|
483
582
|
throw new TypeError(`${label} must contain unique canonically ordered values.`);
|
|
@@ -487,9 +586,11 @@ function exactRecorderValues(value, label) {
|
|
|
487
586
|
export function validateDomainControlFlightRecorderDocument(value) {
|
|
488
587
|
const root = exactRecord(value, DOCUMENT_ROOT_KEYS, 'Domain control flight-recorder document');
|
|
489
588
|
if (root.schema !== DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA
|
|
490
|
-
||
|
|
491
|
-
throw new TypeError(`Domain control flight-recorder document must use ${DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA} version ${
|
|
589
|
+
|| !SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS.some((version) => version === root.version)) {
|
|
590
|
+
throw new TypeError(`Domain control flight-recorder document must use ${DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA} supported version ${SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS.join(' or ')}.`);
|
|
492
591
|
}
|
|
592
|
+
const legacy = root.version === PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION;
|
|
593
|
+
const intervalVersion = root.version === DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION;
|
|
493
594
|
const generatedAt = requireIsoTimestamp(root.generatedAt, 'Domain control flight-recorder document generatedAt');
|
|
494
595
|
if (generatedAt !== root.generatedAt) {
|
|
495
596
|
throw new TypeError('Domain control flight-recorder document generatedAt must use its canonical timestamp form.');
|
|
@@ -517,10 +618,17 @@ export function validateDomainControlFlightRecorderDocument(value) {
|
|
|
517
618
|
collection_change: 0,
|
|
518
619
|
recovered: 0,
|
|
519
620
|
approved_change: 0,
|
|
621
|
+
incomplete_observation: 0,
|
|
520
622
|
};
|
|
623
|
+
const seenIds = new Set();
|
|
624
|
+
const latest = new Map();
|
|
625
|
+
const lastComplete = new Map();
|
|
626
|
+
const incompleteSince = new Map();
|
|
627
|
+
let lastCapture = -Infinity;
|
|
521
628
|
const events = eventInput.map((eventValue, index) => {
|
|
522
629
|
const label = `Domain control flight-recorder document event ${index + 1}`;
|
|
523
|
-
const event = exactRecord(eventValue,
|
|
630
|
+
const event = exactRecord(eventValue, legacy ? new Set(PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS)
|
|
631
|
+
: intervalVersion ? EVENT_KEYS : new Set(SOURCE_TIMED_FLIGHT_RECORDER_EVENT_KEYS), label);
|
|
524
632
|
const eventDomain = requireDomainName(event.domain, `${label}.domain`);
|
|
525
633
|
if (eventDomain !== event.domain || !domains.includes(eventDomain)) {
|
|
526
634
|
throw new TypeError(`${label}.domain must be present in the document domain list.`);
|
|
@@ -528,52 +636,115 @@ export function validateDomainControlFlightRecorderDocument(value) {
|
|
|
528
636
|
if (typeof event.field !== 'string' || !FIELD_IDS.has(event.field)) {
|
|
529
637
|
throw new TypeError(`${label}.field is unsupported.`);
|
|
530
638
|
}
|
|
531
|
-
const observedAt = requireIsoTimestamp(event.observedAt, `${label}.observedAt`);
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
639
|
+
const observedAt = !legacy && event.observedAt === null ? null : requireIsoTimestamp(event.observedAt, `${label}.observedAt`);
|
|
640
|
+
const capturedAt = legacy ? undefined : requireIsoTimestamp(event.capturedAt, `${label}.capturedAt`);
|
|
641
|
+
if (observedAt !== event.observedAt || (legacy && Date.parse(observedAt) > Date.parse(generatedAt))
|
|
642
|
+
|| (!legacy && (capturedAt !== event.capturedAt || Date.parse(capturedAt) > Date.parse(generatedAt)
|
|
643
|
+
|| Date.parse(capturedAt) < lastCapture)))
|
|
644
|
+
throw new TypeError(`${label} contains an invalid or unordered timestamp.`);
|
|
645
|
+
if (capturedAt)
|
|
646
|
+
lastCapture = Date.parse(capturedAt);
|
|
535
647
|
if (event.kind !== 'first_observation'
|
|
536
648
|
&& event.kind !== 'observed_change'
|
|
537
649
|
&& event.kind !== 'collection_change'
|
|
538
|
-
&& event.kind !== 'recovered'
|
|
650
|
+
&& event.kind !== 'recovered'
|
|
651
|
+
&& (legacy || event.kind !== 'incomplete_observation')) {
|
|
539
652
|
throw new TypeError(`${label}.kind is unsupported.`);
|
|
540
653
|
}
|
|
541
|
-
const
|
|
654
|
+
const kind = event.kind;
|
|
655
|
+
const expectedState = kind === 'collection_change' || kind === 'incomplete_observation' ? 'partial' : 'observed';
|
|
542
656
|
if (event.state !== expectedState)
|
|
543
657
|
throw new TypeError(`${label}.state is inconsistent.`);
|
|
544
|
-
|
|
545
|
-
|
|
658
|
+
if (!legacy && expectedState === 'observed' && (observedAt === null || Date.parse(observedAt) > Date.parse(capturedAt))) {
|
|
659
|
+
throw new TypeError(`${label} cannot claim a complete observation without a source time at or before capture.`);
|
|
660
|
+
}
|
|
661
|
+
const collectionDepth = intervalVersion ? event.collectionDepth : undefined;
|
|
662
|
+
if (intervalVersion && (collectionDepth !== 'deep' && collectionDepth !== 'fast' && collectionDepth !== 'unknown'
|
|
663
|
+
|| expectedState === 'observed' && collectionDepth === 'unknown')) {
|
|
664
|
+
throw new TypeError(`${label} has invalid collection conditions.`);
|
|
665
|
+
}
|
|
666
|
+
const before = exactRecorderValues(event.before, `${label}.before`, legacy, event.field, expectedState === 'observed');
|
|
667
|
+
const after = exactRecorderValues(event.after, `${label}.after`, legacy, event.field, expectedState === 'observed');
|
|
546
668
|
if (((event.kind === 'first_observation' || event.kind === 'recovered') && before.length)
|
|
547
|
-
|| (event.kind === 'collection_change' && after.length)) {
|
|
669
|
+
|| (legacy && event.kind === 'collection_change' && after.length)) {
|
|
548
670
|
throw new TypeError(`${label} before/after values are inconsistent.`);
|
|
549
671
|
}
|
|
672
|
+
let changeInterval = null;
|
|
673
|
+
if (intervalVersion && event.changeInterval !== null) {
|
|
674
|
+
const interval = exactRecord(event.changeInterval, new Set(FLIGHT_RECORDER_CHANGE_INTERVAL_KEYS), `${label}.changeInterval`);
|
|
675
|
+
const after = requireIsoTimestamp(interval.after, `${label}.changeInterval.after`);
|
|
676
|
+
const by = requireIsoTimestamp(interval.by, `${label}.changeInterval.by`);
|
|
677
|
+
if (kind !== 'observed_change' || after !== interval.after || by !== interval.by || by !== observedAt || Date.parse(after) >= Date.parse(by)) {
|
|
678
|
+
throw new TypeError(`${label} has an inconsistent change interval.`);
|
|
679
|
+
}
|
|
680
|
+
changeInterval = Object.freeze({ after, by, incompleteObservations: exactRecorderInteger(interval.incompleteObservations, 0, MAX_FLIGHT_RECORDER_OBSERVATIONS, `${label}.changeInterval.incompleteObservations`) });
|
|
681
|
+
}
|
|
682
|
+
if (intervalVersion && (kind === 'observed_change') !== (changeInterval !== null))
|
|
683
|
+
throw new TypeError(`${label} must retain its change interval.`);
|
|
550
684
|
let approvedWindow = null;
|
|
551
685
|
if (event.approvedWindow !== null) {
|
|
552
686
|
if (event.kind !== 'observed_change')
|
|
553
687
|
throw new TypeError(`${label}.approvedWindow is inconsistent.`);
|
|
554
|
-
const window = exactRecord(event.approvedWindow, EVENT_APPROVED_WINDOW_KEYS, `${label}.approvedWindow`);
|
|
688
|
+
const window = exactRecord(event.approvedWindow, intervalVersion ? EVENT_APPROVED_WINDOW_KEYS : new Set(HISTORICAL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS), `${label}.approvedWindow`);
|
|
689
|
+
const startsAt = intervalVersion ? requireIsoTimestamp(window.startsAt, `${label}.approvedWindow.startsAt`) : undefined;
|
|
690
|
+
const endsAt = intervalVersion ? requireIsoTimestamp(window.endsAt, `${label}.approvedWindow.endsAt`) : undefined;
|
|
691
|
+
if (intervalVersion && (!changeInterval || startsAt !== window.startsAt || endsAt !== window.endsAt
|
|
692
|
+
|| Date.parse(startsAt) > Date.parse(changeInterval.after) || Date.parse(endsAt) < Date.parse(changeInterval.by))) {
|
|
693
|
+
throw new TypeError(`${label}.approvedWindow does not contain the change interval.`);
|
|
694
|
+
}
|
|
555
695
|
approvedWindow = Object.freeze({
|
|
556
696
|
id: exactRecorderText(window.id, `${label}.approvedWindow.id`, MAX_FLIGHT_RECORDER_WINDOW_ID_LENGTH),
|
|
557
697
|
reason: exactRecorderText(window.reason, `${label}.approvedWindow.reason`, MAX_FLIGHT_RECORDER_WINDOW_REASON_LENGTH),
|
|
698
|
+
...(intervalVersion ? { startsAt: startsAt, endsAt: endsAt } : {}),
|
|
558
699
|
});
|
|
559
700
|
kindCounts.approved_change += 1;
|
|
560
701
|
}
|
|
561
|
-
kindCounts[
|
|
562
|
-
|
|
702
|
+
kindCounts[kind] += 1;
|
|
703
|
+
const result = Object.freeze({
|
|
563
704
|
id: exactRecorderText(event.id, `${label}.id`, 400),
|
|
564
705
|
domain: eventDomain,
|
|
565
706
|
field: event.field,
|
|
566
707
|
observedAt,
|
|
567
|
-
|
|
708
|
+
...(capturedAt ? { capturedAt } : {}),
|
|
709
|
+
kind,
|
|
568
710
|
state: expectedState,
|
|
569
711
|
before,
|
|
570
712
|
after,
|
|
571
713
|
source: exactRecorderText(event.source, `${label}.source`, MAX_FLIGHT_RECORDER_SOURCE_LENGTH),
|
|
572
714
|
approvedWindow,
|
|
573
|
-
explanation: exactRecorderText(event.explanation, `${label}.explanation`, MAX_FLIGHT_RECORDER_VALUE_LENGTH),
|
|
715
|
+
explanation: exactRecorderText(event.explanation, `${label}.explanation`, legacy ? PUBLIC_MAX_FLIGHT_RECORDER_VALUE_LENGTH : MAX_FLIGHT_RECORDER_VALUE_LENGTH),
|
|
716
|
+
...(intervalVersion ? { collectionDepth: collectionDepth, changeInterval } : {}),
|
|
574
717
|
});
|
|
718
|
+
if (!legacy) {
|
|
719
|
+
const key = `${eventDomain}\u0000${event.field}`;
|
|
720
|
+
const previous = intervalVersion ? lastComplete.get(key) : latest.get(key);
|
|
721
|
+
if (seenIds.has(result.id) || (kind === 'observed_change' && (!previous || previous.state !== 'observed'
|
|
722
|
+
|| previous.source !== result.source || Date.parse(previous.observedAt) >= Date.parse(result.observedAt)
|
|
723
|
+
|| !sameValues(previous.after, before) || sameValues(before, after)
|
|
724
|
+
|| intervalVersion && (previous.collectionDepth !== result.collectionDepth || !changeInterval
|
|
725
|
+
|| Date.parse(changeInterval.after) < Date.parse(previous.observedAt)
|
|
726
|
+
|| changeInterval.incompleteObservations !== (incompleteSince.get(key) ?? 0))))) {
|
|
727
|
+
throw new TypeError(`${label} has a duplicate identity or an unsupported observed-change claim.`);
|
|
728
|
+
}
|
|
729
|
+
seenIds.add(result.id);
|
|
730
|
+
latest.set(key, result);
|
|
731
|
+
if (intervalVersion) {
|
|
732
|
+
if (expectedState === 'observed') {
|
|
733
|
+
lastComplete.set(key, result);
|
|
734
|
+
incompleteSince.delete(key);
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
incompleteSince.set(key, (incompleteSince.get(key) ?? 0) + 1);
|
|
738
|
+
if (previous && result.source !== 'Not supplied in this capture'
|
|
739
|
+
&& (previous.source !== result.source || result.collectionDepth !== 'unknown' && previous.collectionDepth !== result.collectionDepth))
|
|
740
|
+
lastComplete.delete(key);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return result;
|
|
575
745
|
});
|
|
576
|
-
const
|
|
746
|
+
const summaryKeys = legacy ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS : DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS;
|
|
747
|
+
const summaryInput = exactRecord(root.summary, legacy ? new Set(summaryKeys) : SUMMARY_KEYS, 'Domain control flight-recorder document summary');
|
|
577
748
|
const expectedSummary = Object.freeze({
|
|
578
749
|
firstObservations: kindCounts.first_observation,
|
|
579
750
|
observedChanges: kindCounts.observed_change,
|
|
@@ -581,26 +752,29 @@ export function validateDomainControlFlightRecorderDocument(value) {
|
|
|
581
752
|
unexpectedChanges: kindCounts.observed_change - kindCounts.approved_change,
|
|
582
753
|
collectionChanges: kindCounts.collection_change,
|
|
583
754
|
recoveredSources: kindCounts.recovered,
|
|
755
|
+
...(!legacy ? { incompleteFields: kindCounts.collection_change + kindCounts.incomplete_observation } : {}),
|
|
584
756
|
});
|
|
585
|
-
for (const key of
|
|
757
|
+
for (const key of summaryKeys) {
|
|
586
758
|
const supplied = exactRecorderInteger(summaryInput[key], 0, MAX_FLIGHT_RECORDER_EVENTS, `Domain control flight-recorder document summary ${key}`);
|
|
587
759
|
if (supplied !== expectedSummary[key]) {
|
|
588
760
|
throw new TypeError('Domain control flight-recorder document summary is inconsistent.');
|
|
589
761
|
}
|
|
590
762
|
}
|
|
591
|
-
const
|
|
592
|
-
|
|
763
|
+
const limitations = legacy ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS
|
|
764
|
+
: intervalVersion ? DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS : SOURCE_TIMED_FLIGHT_RECORDER_LIMITATIONS;
|
|
765
|
+
const limitationsInput = boundedDataArray(root.limitations, 'Domain control flight-recorder document limitations', limitations.length, limitations.length);
|
|
766
|
+
if (limitationsInput.some((item, index) => item !== limitations[index])) {
|
|
593
767
|
throw new TypeError('Domain control flight-recorder document limitations are invalid.');
|
|
594
768
|
}
|
|
595
769
|
const document = Object.freeze({
|
|
596
770
|
schema: DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA,
|
|
597
|
-
version:
|
|
771
|
+
version: root.version,
|
|
598
772
|
generatedAt,
|
|
599
773
|
domains: Object.freeze(domains),
|
|
600
774
|
observationCount,
|
|
601
775
|
events: Object.freeze(events),
|
|
602
776
|
summary: expectedSummary,
|
|
603
|
-
limitations
|
|
777
|
+
limitations,
|
|
604
778
|
});
|
|
605
779
|
assertPrettyJsonByteBudget(document, MAX_DOMAIN_CONTROL_FLIGHT_RECORDER_OUTPUT_BYTES, 'Domain control flight-recorder document');
|
|
606
780
|
return document;
|
|
@@ -621,10 +795,13 @@ export function formatDomainControlFlightRecorder(document) {
|
|
|
621
795
|
`Unexpected changes ${document.summary.unexpectedChanges}`,
|
|
622
796
|
`Approved changes ${document.summary.approvedChanges}`,
|
|
623
797
|
`Collection changes ${document.summary.collectionChanges}`,
|
|
798
|
+
...(document.summary.incompleteFields === undefined ? [] : [`Incomplete fields ${document.summary.incompleteFields}`]),
|
|
624
799
|
'',
|
|
625
800
|
];
|
|
626
801
|
for (const event of document.events.filter((item) => item.kind !== 'first_observation')) {
|
|
627
|
-
lines.push(`${event.observedAt} ${event.domain} ${event.field.replaceAll('_', ' ')} ${event.kind.replaceAll('_', ' ')}`);
|
|
802
|
+
lines.push(`${event.observedAt ?? 'Source time unavailable'} ${event.domain} ${event.field.replaceAll('_', ' ')} ${event.kind.replaceAll('_', ' ')}`);
|
|
803
|
+
if (event.capturedAt)
|
|
804
|
+
lines.push(` Captured ${event.capturedAt}`);
|
|
628
805
|
lines.push(` ${event.explanation}`);
|
|
629
806
|
}
|
|
630
807
|
if (!document.events.some((item) => item.kind !== 'first_observation')) {
|