@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,70 @@
|
|
|
1
|
+
import { array, boolean, domain, enumeration, exact, HEX_DIGEST_RE, iso, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
import { pageObservationOrigin } from "./page-behaviour.mjs";
|
|
3
|
+
import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS } from "../contracts/context-review.mjs";
|
|
4
|
+
export { STOREFRONT_INPUT_SCHEMA, STOREFRONT_INPUT_VERSION } from "../contracts/context-review.mjs";
|
|
5
|
+
export const STOREFRONT_FIELDS = [
|
|
6
|
+
{ id: 'brandNames', label: 'Displayed brand names', treatment: 'text' },
|
|
7
|
+
{ id: 'contactDomains', label: 'Published contact domains', treatment: 'domain' },
|
|
8
|
+
{ id: 'policyHashes', label: 'Policy text SHA-256 digests', treatment: 'hash' },
|
|
9
|
+
{ id: 'checkoutOrigins', label: 'Checkout destinations', treatment: 'origin' },
|
|
10
|
+
{ id: 'paymentMethods', label: 'Advertised payment methods', treatment: 'text' },
|
|
11
|
+
{ id: 'assetHashes', label: 'Distinctive asset SHA-256 digests', treatment: 'hash' },
|
|
12
|
+
];
|
|
13
|
+
export function readStorefrontObservation(raw) {
|
|
14
|
+
const row = exact(raw, ['hostname', 'observedAt', 'source', ...STOREFRONT_FIELDS.map(field => field.id)], 'Storefront observation');
|
|
15
|
+
domain(row.hostname, 'Storefront hostname');
|
|
16
|
+
iso(row.observedAt, 'Storefront observation time');
|
|
17
|
+
const fields = {};
|
|
18
|
+
let count = 0;
|
|
19
|
+
for (const field of STOREFRONT_FIELDS) {
|
|
20
|
+
if (row[field.id] === null) {
|
|
21
|
+
fields[field.id] = null;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const values = array(row[field.id], field.label, MAX_CONTEXT_RECORDS);
|
|
25
|
+
count += values.length;
|
|
26
|
+
if (count > MAX_CONTEXT_RECORDS)
|
|
27
|
+
throw new TypeError('A storefront observation exceeds the aggregate evidence-item bound.');
|
|
28
|
+
fields[field.id] = [...new Set(values.map(value => {
|
|
29
|
+
const candidate = text(value, field.label, 500);
|
|
30
|
+
if (field.treatment === 'domain')
|
|
31
|
+
domain(candidate, field.label);
|
|
32
|
+
if (field.treatment === 'hash' && !HEX_DIGEST_RE.test(candidate))
|
|
33
|
+
throw new TypeError('Storefront digests must be lowercase hexadecimal SHA-256.');
|
|
34
|
+
if (field.treatment === 'origin' && pageObservationOrigin(candidate) !== candidate)
|
|
35
|
+
throw new TypeError('Checkout observations retain HTTP(S) origins only.');
|
|
36
|
+
return candidate;
|
|
37
|
+
}))].sort();
|
|
38
|
+
}
|
|
39
|
+
return { hostname: row.hostname, observedAt: row.observedAt, source: text(row.source, 'Storefront source', 500), ...fields };
|
|
40
|
+
}
|
|
41
|
+
export function reviewStorefront(raw, reviewedAt) {
|
|
42
|
+
iso(reviewedAt, 'Review time');
|
|
43
|
+
const input = exact(raw, ['official', 'candidate', 'authorisedComparator', 'resellerStatus', 'resellerSource'], 'Storefront comparison');
|
|
44
|
+
boolean(input.authorisedComparator, 'Comparator authority');
|
|
45
|
+
if (!input.authorisedComparator)
|
|
46
|
+
throw new TypeError('Confirm that the official comparator is owned or authorised for this review.');
|
|
47
|
+
const official = readStorefrontObservation(input.official), candidate = readStorefrontObservation(input.candidate);
|
|
48
|
+
if (official.hostname === candidate.hostname)
|
|
49
|
+
throw new TypeError('Select a different candidate hostname for storefront comparison.');
|
|
50
|
+
const reseller = enumeration(input.resellerStatus, ['authorised', 'not_authorised', 'unknown'], 'Reseller status');
|
|
51
|
+
const resellerSource = input.resellerSource === null ? null : text(input.resellerSource, 'Reseller evidence', 500);
|
|
52
|
+
if (reseller !== 'unknown' && !resellerSource)
|
|
53
|
+
throw new TypeError('A reseller-status declaration needs its source or authority reference.');
|
|
54
|
+
const observations = [];
|
|
55
|
+
let missing = 0;
|
|
56
|
+
for (const field of STOREFRONT_FIELDS) {
|
|
57
|
+
const left = official[field.id], right = candidate[field.id];
|
|
58
|
+
if (left === null || right === null)
|
|
59
|
+
missing++;
|
|
60
|
+
const shared = left && right ? right.filter(value => left.includes(value)) : [];
|
|
61
|
+
observations.push({ label: field.label, state: left === null || right === null ? 'unknown' : 'observed',
|
|
62
|
+
detail: `Official: ${left === null ? 'not reviewed' : left.length ? left.join(', ') : 'no values recorded'}. Candidate: ${right === null ? 'not reviewed' : right.length ? right.join(', ') : 'no values recorded'}. ${left !== null && right !== null ? `Exact shared values: ${shared.length}.` : 'No comparison established.'}`,
|
|
63
|
+
source: `${official.hostname} · ${official.source} · ${official.observedAt} / ${candidate.hostname} · ${candidate.source}`, observedAt: candidate.observedAt, hostname: candidate.hostname });
|
|
64
|
+
}
|
|
65
|
+
observations.push({ label: 'Reseller or affiliate authority', state: reseller === 'unknown' ? 'unknown' : 'reported', detail: reseller.replaceAll('_', ' '), source: resellerSource ?? 'No authority evidence supplied', observedAt: null, hostname: candidate.hostname });
|
|
66
|
+
return { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'storefront', reviewedAt, title: 'Storefront comparison', state: missing ? 'partial' : 'reviewed',
|
|
67
|
+
summary: `${candidate.hostname} compared with the authorised reference ${official.hostname}; evidence categories missing one side: ${missing}.`, observations,
|
|
68
|
+
nextSteps: ['Check reseller, affiliate and regional-store authority before preparing a complaint.', 'Retain the source pages and distinctive assets privately; select relevant findings in the Case response packet.', 'Use the observed checkout platform and the rights or policy evidence to choose a reporting route. Do not enter payment information or submit an order.'],
|
|
69
|
+
limitations: ['This compares analyst-supplied observations. It does not collect a page, verify a merchant, submit a payment or issue a scam score.', 'Shared wording, assets and payment providers can have legitimate explanations. Digests establish byte equality only when inputs were captured consistently; missing observations do not establish absence.'] };
|
|
70
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sha256IdentityHex } from "../evidence/record-identity.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
1
3
|
/**
|
|
2
4
|
* Canonical, framework-independent analyst review identity and lifecycle.
|
|
3
5
|
*
|
|
@@ -7,7 +9,12 @@
|
|
|
7
9
|
*/
|
|
8
10
|
import { ANALYST_REVIEW_DISPOSITIONS, ANALYST_REVIEW_EVIDENCE_FAMILIES, ANALYST_REVIEW_STATE_SCHEMA, ANALYST_REVIEW_STATE_SCHEMA_VERSION, MAX_ANALYST_REVIEW_ASSOCIATIONS, MAX_ANALYST_REVIEW_HISTORY, MAX_ANALYST_REVIEW_IDENTITY_BYTES, MAX_ANALYST_REVIEW_ITEMS, MAX_ANALYST_REVIEW_RATIONALE_LENGTH, MAX_ANALYST_REVIEW_STATE_BYTES, MAX_ANALYST_REVIEW_STATE_RECORDS, } from "../contracts/analyst-review-state-contract.mjs";
|
|
9
11
|
export * from "../contracts/analyst-review-state-contract.mjs";
|
|
12
|
+
export { ANALYST_REVIEW_AGING_AFTER_DAYS, ANALYST_REVIEW_STALE_AFTER_DAYS, analystReviewAgeAt } from "../evidence/review-age.mjs";
|
|
13
|
+
export function analystReviewCanResolve(item) {
|
|
14
|
+
return item.completeness === 'complete' && (item.age === 'current' || item.age === 'aging');
|
|
15
|
+
}
|
|
10
16
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
17
|
+
const RATIONALE_INPUT_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
11
18
|
const SAFE_REFERENCE_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
12
19
|
const SAFE_SUBJECT_RE = /^review:[a-z0-9_]{1,40}:[a-f0-9]{64}$/u;
|
|
13
20
|
const SAFE_FINGERPRINT_RE = /^material:[a-f0-9]{64}$/u;
|
|
@@ -122,28 +129,24 @@ export function assertAnalystReviewInputGraph(value) {
|
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
function boundedRationale(value, label, formInput = false) {
|
|
133
|
+
const maximum = MAX_ANALYST_REVIEW_RATIONALE_LENGTH;
|
|
134
|
+
const controls = formInput ? RATIONALE_INPUT_CONTROL_RE : CONTROL_RE;
|
|
135
|
+
if (typeof value !== 'string' || value.length > maximum * 4 || controls.test(value)) {
|
|
129
136
|
throw inputError(`${label} is invalid`);
|
|
130
137
|
}
|
|
138
|
+
// Forms may contain ordinary line breaks; stored rationale remains single-line.
|
|
131
139
|
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
132
|
-
if (
|
|
140
|
+
if (!normalized || normalized.length > maximum)
|
|
133
141
|
throw inputError(`${label} is invalid`);
|
|
134
142
|
return normalized;
|
|
135
143
|
}
|
|
136
144
|
function timestamp(value, label, nullable = false) {
|
|
137
145
|
if (nullable && (value === null || value === undefined || value === ''))
|
|
138
146
|
return null;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!Number.isFinite(parsed))
|
|
143
|
-
throw inputError(`${label} must use an explicit valid date and time`);
|
|
144
|
-
const normalized = new Date(parsed).toISOString();
|
|
145
|
-
if (!/(?:Z|[+-]\d{2}:\d{2})$/u.test(value))
|
|
146
|
-
throw inputError(`${label} must use an explicit timezone`);
|
|
147
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
148
|
+
if (!normalized)
|
|
149
|
+
throw inputError(`${label} must use an explicit valid date, time and timezone`);
|
|
147
150
|
return normalized;
|
|
148
151
|
}
|
|
149
152
|
function referenceList(value, label) {
|
|
@@ -181,7 +184,7 @@ function normalizeSnapshot(raw, label) {
|
|
|
181
184
|
const normalized = {
|
|
182
185
|
reviewedFingerprint: fingerprint(value.reviewedFingerprint, `${label}.reviewedFingerprint`),
|
|
183
186
|
disposition: normalizedDisposition,
|
|
184
|
-
rationale:
|
|
187
|
+
rationale: boundedRationale(value.rationale, `${label}.rationale`),
|
|
185
188
|
reviewedAt: timestamp(value.reviewedAt, `${label}.reviewedAt`),
|
|
186
189
|
reviewDueAt: timestamp(value.reviewDueAt, `${label}.reviewDueAt`, true),
|
|
187
190
|
expiresAt: timestamp(value.expiresAt, `${label}.expiresAt`, true),
|
|
@@ -278,7 +281,7 @@ export function normalizeAnalystReviewStateStore(raw) {
|
|
|
278
281
|
records,
|
|
279
282
|
};
|
|
280
283
|
if (byteLength(JSON.stringify(store)) > MAX_ANALYST_REVIEW_STATE_BYTES) {
|
|
281
|
-
throw new Error('Analyst review state exceeds the 512 KiB
|
|
284
|
+
throw new Error('Analyst review state exceeds the 512 KiB workspace storage limit.');
|
|
282
285
|
}
|
|
283
286
|
return store;
|
|
284
287
|
}
|
|
@@ -323,75 +326,8 @@ export function canonicalAnalystReviewIdentityJson(parts) {
|
|
|
323
326
|
}
|
|
324
327
|
return source;
|
|
325
328
|
}
|
|
326
|
-
const SHA256_CONSTANTS = Object.freeze([
|
|
327
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
328
|
-
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
329
|
-
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
330
|
-
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
331
|
-
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
332
|
-
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
333
|
-
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
334
|
-
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
335
|
-
]);
|
|
336
|
-
function rotateRight(value, bits) {
|
|
337
|
-
return (value >>> bits) | (value << (32 - bits));
|
|
338
|
-
}
|
|
339
|
-
function sha256Hex(bytes) {
|
|
340
|
-
// This digest is a collision-resistant record identity, not a password,
|
|
341
|
-
// signature, MAC, or substitute for the platform cryptography used there.
|
|
342
|
-
const paddedLength = Math.ceil((bytes.byteLength + 9) / 64) * 64;
|
|
343
|
-
const padded = new Uint8Array(paddedLength);
|
|
344
|
-
padded.set(bytes);
|
|
345
|
-
padded[bytes.byteLength] = 0x80;
|
|
346
|
-
const view = new DataView(padded.buffer);
|
|
347
|
-
const bitLength = bytes.byteLength * 8;
|
|
348
|
-
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x1_0000_0000), false);
|
|
349
|
-
view.setUint32(paddedLength - 4, bitLength >>> 0, false);
|
|
350
|
-
const state = new Uint32Array([
|
|
351
|
-
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
352
|
-
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
353
|
-
]);
|
|
354
|
-
const words = new Uint32Array(64);
|
|
355
|
-
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
356
|
-
for (let index = 0; index < 16; index += 1)
|
|
357
|
-
words[index] = view.getUint32(offset + index * 4, false);
|
|
358
|
-
for (let index = 16; index < 64; index += 1) {
|
|
359
|
-
const left = words[index - 15];
|
|
360
|
-
const right = words[index - 2];
|
|
361
|
-
const sigma0 = rotateRight(left, 7) ^ rotateRight(left, 18) ^ (left >>> 3);
|
|
362
|
-
const sigma1 = rotateRight(right, 17) ^ rotateRight(right, 19) ^ (right >>> 10);
|
|
363
|
-
words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
|
|
364
|
-
}
|
|
365
|
-
let [a, b, c, d, e, f, g, h] = state;
|
|
366
|
-
for (let index = 0; index < 64; index += 1) {
|
|
367
|
-
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
368
|
-
const choice = (e & f) ^ (~e & g);
|
|
369
|
-
const first = (h + sum1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;
|
|
370
|
-
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
371
|
-
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
372
|
-
const second = (sum0 + majority) >>> 0;
|
|
373
|
-
h = g;
|
|
374
|
-
g = f;
|
|
375
|
-
f = e;
|
|
376
|
-
e = (d + first) >>> 0;
|
|
377
|
-
d = c;
|
|
378
|
-
c = b;
|
|
379
|
-
b = a;
|
|
380
|
-
a = (first + second) >>> 0;
|
|
381
|
-
}
|
|
382
|
-
state[0] = (state[0] + a) >>> 0;
|
|
383
|
-
state[1] = (state[1] + b) >>> 0;
|
|
384
|
-
state[2] = (state[2] + c) >>> 0;
|
|
385
|
-
state[3] = (state[3] + d) >>> 0;
|
|
386
|
-
state[4] = (state[4] + e) >>> 0;
|
|
387
|
-
state[5] = (state[5] + f) >>> 0;
|
|
388
|
-
state[6] = (state[6] + g) >>> 0;
|
|
389
|
-
state[7] = (state[7] + h) >>> 0;
|
|
390
|
-
}
|
|
391
|
-
return [...state].map((value) => value.toString(16).padStart(8, '0')).join('');
|
|
392
|
-
}
|
|
393
329
|
function stableHash(parts) {
|
|
394
|
-
return
|
|
330
|
+
return sha256IdentityHex(new TextEncoder().encode(canonicalAnalystReviewIdentityJson(parts)));
|
|
395
331
|
}
|
|
396
332
|
export function analystReviewSubjectKey(evidenceFamily, stableParts) {
|
|
397
333
|
if (!FAMILY_VALUES.has(evidenceFamily))
|
|
@@ -421,6 +357,11 @@ function snapshotIdentity(snapshot) {
|
|
|
421
357
|
snapshot.expiresAt,
|
|
422
358
|
]);
|
|
423
359
|
}
|
|
360
|
+
export function analystReviewDecisionIdentity(subjectKey, snapshot) {
|
|
361
|
+
if (!SAFE_SUBJECT_RE.test(subjectKey))
|
|
362
|
+
throw inputError('Review subject identity is invalid');
|
|
363
|
+
return `decision:${stableHash([subjectKey, snapshotIdentity(snapshot)])}`;
|
|
364
|
+
}
|
|
424
365
|
function mergedReviewHistory(current, other) {
|
|
425
366
|
const currentIdentity = snapshotIdentity(currentSnapshot(current));
|
|
426
367
|
const byIdentity = new Map();
|
|
@@ -445,10 +386,10 @@ export function setAnalystReviewDecision(raw, item, input) {
|
|
|
445
386
|
}
|
|
446
387
|
if (!DISPOSITION_VALUES.has(input.disposition))
|
|
447
388
|
throw new TypeError('Choose a valid Review Item disposition.');
|
|
448
|
-
if (input.disposition === 'resolved' && (item
|
|
449
|
-
throw new Error('Partial, inconclusive, or
|
|
389
|
+
if (input.disposition === 'resolved' && !analystReviewCanResolve(item)) {
|
|
390
|
+
throw new Error('Partial, inconclusive, stale or undated evidence cannot resolve a Review Item. Refresh or attach current complete evidence first.');
|
|
450
391
|
}
|
|
451
|
-
const rationale =
|
|
392
|
+
const rationale = boundedRationale(input.rationale, 'rationale', true);
|
|
452
393
|
const reviewedAt = timestamp(input.reviewedAt ?? new Date().toISOString(), 'reviewedAt');
|
|
453
394
|
const expiresAt = timestamp(input.expiresAt, 'expiresAt', true);
|
|
454
395
|
const reviewDueAt = timestamp(input.reviewDueAt, 'reviewDueAt', true);
|
|
@@ -492,7 +433,14 @@ export function analystReviewLifecycle(item, raw, nowRaw = new Date().toISOStrin
|
|
|
492
433
|
if (!decision) {
|
|
493
434
|
return { state: 'open', effectiveDisposition: 'open', decision: null, reason: 'No analyst lifecycle decision is retained.', expired: false, invalidated: false, recurred: false, reviewDue: false };
|
|
494
435
|
}
|
|
495
|
-
const now =
|
|
436
|
+
const now = normalizeExplicitIsoTimestamp(nowRaw);
|
|
437
|
+
if (!now || Date.parse(now) < Date.parse(decision.reviewedAt)) {
|
|
438
|
+
return {
|
|
439
|
+
state: 'invalidated', effectiveDisposition: 'open', decision,
|
|
440
|
+
reason: 'The review clock is unavailable or earlier than the retained decision. Its disposition cannot hide current work.',
|
|
441
|
+
expired: false, invalidated: true, recurred: decision.disposition !== 'open', reviewDue: false,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
496
444
|
const invalidated = decision.reviewedFingerprint !== item.materialFingerprint;
|
|
497
445
|
const expired = decision.expiresAt !== null && Date.parse(decision.expiresAt) <= Date.parse(now);
|
|
498
446
|
const reviewDue = decision.reviewDueAt !== null && Date.parse(decision.reviewDueAt) <= Date.parse(now);
|
|
@@ -518,10 +466,10 @@ export function analystReviewLifecycle(item, raw, nowRaw = new Date().toISOStrin
|
|
|
518
466
|
expired: false, invalidated: false, recurred: true, reviewDue: true,
|
|
519
467
|
};
|
|
520
468
|
}
|
|
521
|
-
if (decision.disposition === 'resolved' && (item
|
|
469
|
+
if (decision.disposition === 'resolved' && !analystReviewCanResolve(item)) {
|
|
522
470
|
return {
|
|
523
471
|
state: 'invalidated', effectiveDisposition: 'open', decision,
|
|
524
|
-
reason: 'The current evidence is partial, inconclusive, or
|
|
472
|
+
reason: 'The current evidence is partial, inconclusive, stale or undated, so the retained resolved decision cannot close this item.',
|
|
525
473
|
expired: false, invalidated: true, recurred: true, reviewDue,
|
|
526
474
|
};
|
|
527
475
|
}
|
|
@@ -607,7 +555,7 @@ function normalizeDevelopmentSnapshot(raw, label) {
|
|
|
607
555
|
return {
|
|
608
556
|
reviewedFingerprint: developmentFingerprint(value.reviewedFingerprint, `${label}.reviewedFingerprint`),
|
|
609
557
|
disposition: disposition(value.disposition, `${label}.disposition`),
|
|
610
|
-
rationale:
|
|
558
|
+
rationale: boundedRationale(value.rationale, `${label}.rationale`),
|
|
611
559
|
reviewedAt,
|
|
612
560
|
reviewDueAt: reviewDueAt && Date.parse(reviewDueAt) > Date.parse(reviewedAt) ? reviewDueAt : null,
|
|
613
561
|
expiresAt: expiresAt && Date.parse(expiresAt) > Date.parse(reviewedAt) ? expiresAt : null,
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// Pure Brand Profile normalization and storage model. The browser wrapper owns
|
|
2
2
|
// persistence and downloads; this module owns schema migration, semantic field
|
|
3
3
|
// bounds, import merging, and exact serialized-byte accounting.
|
|
4
|
-
import { normalizeDomain } from "../
|
|
4
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
|
+
import { canonicalDomainControlRecords, normalizeDeclaredDomainControlRecordModes, normalizeDomainControlRecordModes } from "../evidence/domain-control-runtime.mjs";
|
|
7
|
+
import { DOMAIN_CONTROL_RECORD_LIST_FIELDS } from "../contracts/domain-control-manifest.mjs";
|
|
8
|
+
import { latestObservationCohort } from "../evidence/latest-observations.mjs";
|
|
9
|
+
import { MAX_IDENTITY_DIGEST_BYTES, sha256IdentityHex } from "../evidence/record-identity.mjs";
|
|
10
|
+
import { DOMAIN_POSTURE_COMPARISON_VERSION, MAX_POSTURE_CHECKS, MAX_POSTURE_CHECK_RECORDS, MAX_POSTURE_RECORD_LENGTH, normalizeDomainPostureSourceContext, normalizeDomainPostureProfileContext } from "../evidence/domain-posture-context.mjs";
|
|
6
11
|
import { normalizeOpaqueReferenceId } from "../cases/opaque-reference-id.mjs";
|
|
7
12
|
import { normalizePageBaseline } from "./page-baseline.mjs";
|
|
8
|
-
import { isInformativePerceptualHash as isInformativeFaviconHash } from "
|
|
13
|
+
import { isInformativePerceptualHash as isInformativeFaviconHash } from "../analysis/perceptual-hash-comparison.mjs";
|
|
9
14
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
10
15
|
import { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_OFFICIAL_CHANNELS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_URL_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, MAX_RIGHTS_REFERENCES, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
11
16
|
export { BRAND_PROFILE_SCHEMA, BRAND_PROFILE_SCHEMA_VERSION, MAX_DESIRED_POSTURE_BASELINES, MAX_DESIRED_POSTURE_CHANGE_WINDOWS, MAX_DESIRED_POSTURE_OBSERVATIONS, MAX_DESIRED_POSTURE_RECORDS, MAX_DESIRED_POSTURE_SUPPRESSIONS, MAX_DKIM_SELECTOR_LENGTH, MAX_DKIM_SELECTORS, MAX_OFFICIAL_CHANNELS, MAX_PROFILES, MAX_PROFILE_DOMAIN_LENGTH, MAX_PROFILE_NAME_LENGTH, MAX_PROFILE_STORE_BYTES, MAX_PROFILE_TEXT_LENGTH, MAX_PROFILE_TLD_LENGTH, MAX_PROFILE_URL_LENGTH, MAX_PROFILE_VALUE_INPUTS, MAX_PROFILE_VALUES, MAX_PROTECTION_ATTESTATIONS, MAX_RIGHTS_REFERENCES, SUPPORTED_BRAND_PROFILE_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
@@ -118,6 +123,32 @@ export function normalizeProfileDomains(value) {
|
|
|
118
123
|
export function normalizeProfileTextValues(value) {
|
|
119
124
|
return normalizeList(value, (item) => boundedText(item));
|
|
120
125
|
}
|
|
126
|
+
export const MAX_ALLOWLIST_DRAFT_CHARACTERS = (MAX_PROFILE_DOMAIN_LENGTH + 2) * MAX_PROFILE_VALUE_INPUTS;
|
|
127
|
+
/** Admit the complete submitted list or leave the draft unchanged. */
|
|
128
|
+
export function addBrandAllowlistValues(profile, kind, current, raw) {
|
|
129
|
+
if (raw.length > MAX_ALLOWLIST_DRAFT_CHARACTERS)
|
|
130
|
+
throw new RangeError('The submitted allowlist text is too large.');
|
|
131
|
+
const inputs = raw.split(/[\n,]+/u).map((item) => item.trim()).filter(Boolean);
|
|
132
|
+
if (inputs.length > MAX_PROFILE_VALUE_INPUTS)
|
|
133
|
+
throw new RangeError(`Review at most ${MAX_PROFILE_VALUE_INPUTS} entries at a time.`);
|
|
134
|
+
const next = new Map(current.map((value) => [value.toLowerCase(), value]));
|
|
135
|
+
const trusted = new Set([...profile.officialDomains, ...profile.approvedPartnerDomains]);
|
|
136
|
+
for (const [index, input] of inputs.entries()) {
|
|
137
|
+
const value = kind === 'domains' ? normalizeProfileDomains([input])[0] : normalizeProfileTextValues([input])[0];
|
|
138
|
+
if (!value || (kind === 'registrars' && input.replace(/\s+/gu, ' ').length > MAX_PROFILE_TEXT_LENGTH)) {
|
|
139
|
+
throw new TypeError(`Allowlist entry ${index + 1} is invalid or too long. No entries were added.`);
|
|
140
|
+
}
|
|
141
|
+
if (kind === 'domains' && trusted.has(value))
|
|
142
|
+
continue;
|
|
143
|
+
if (!next.has(value.toLowerCase()))
|
|
144
|
+
next.set(value.toLowerCase(), value);
|
|
145
|
+
}
|
|
146
|
+
if (next.size > MAX_PROFILE_VALUES)
|
|
147
|
+
throw new RangeError(`This list would contain ${next.size} entries; the current profile supports ${MAX_PROFILE_VALUES}. No entries were added.`);
|
|
148
|
+
if (next.size === current.length)
|
|
149
|
+
throw new TypeError('No new entries remain after excluding existing and trusted values.');
|
|
150
|
+
return [...next.values()];
|
|
151
|
+
}
|
|
121
152
|
export function normalizeProfileTlds(value) {
|
|
122
153
|
return normalizeList(value, normalizeTld);
|
|
123
154
|
}
|
|
@@ -159,6 +190,30 @@ export function normalizeProtectionAttestations(value) {
|
|
|
159
190
|
}
|
|
160
191
|
return output;
|
|
161
192
|
}
|
|
193
|
+
/** Only submitted, validated reviews acquire a new analyst-review clock. */
|
|
194
|
+
export function reviewProtectionAttestations(existing, reviews, nowIso) {
|
|
195
|
+
const assertedAt = timestamp(nowIso, null);
|
|
196
|
+
if (!assertedAt)
|
|
197
|
+
throw new TypeError('The account-control review time is invalid.');
|
|
198
|
+
if (reviews.length > MAX_PROTECTION_ATTESTATIONS)
|
|
199
|
+
throw new RangeError('Too many account-control reviews were submitted.');
|
|
200
|
+
const next = new Map(existing.map((item) => [item.control, { ...item }]));
|
|
201
|
+
const seen = new Set();
|
|
202
|
+
for (const review of reviews) {
|
|
203
|
+
if (!PROTECTION_ATTESTATION_CONTROL_SET.has(review.control) || seen.has(review.control)
|
|
204
|
+
|| !PROTECTION_ATTESTATION_STATES.has(review.state))
|
|
205
|
+
throw new TypeError('An account-control review is invalid or repeated.');
|
|
206
|
+
const expiresAt = review.expiresAt === null ? null : timestamp(review.expiresAt, null);
|
|
207
|
+
if (review.expiresAt !== null && !expiresAt)
|
|
208
|
+
throw new TypeError('Enter a valid account-control expiry date.');
|
|
209
|
+
if (typeof review.note !== 'string' || CONTROL_RE.test(review.note)
|
|
210
|
+
|| review.note.replace(/\s+/gu, ' ').trim().length > MAX_PROFILE_TEXT_LENGTH)
|
|
211
|
+
throw new TypeError(`Account-control notes must contain at most ${MAX_PROFILE_TEXT_LENGTH} characters and no control characters.`);
|
|
212
|
+
seen.add(review.control);
|
|
213
|
+
next.set(review.control, { control: review.control, state: review.state, assertedAt, expiresAt, note: boundedText(review.note) });
|
|
214
|
+
}
|
|
215
|
+
return [...next.values()];
|
|
216
|
+
}
|
|
162
217
|
function normalizeDesiredPostureRecords(value, normalizer) {
|
|
163
218
|
if (!Array.isArray(value))
|
|
164
219
|
return [];
|
|
@@ -176,9 +231,10 @@ function normalizeDesiredPostureRecords(value, normalizer) {
|
|
|
176
231
|
}
|
|
177
232
|
function normalizeDesiredPostureObservation(value) {
|
|
178
233
|
const candidate = record(value);
|
|
179
|
-
const observedAt =
|
|
180
|
-
if (!
|
|
234
|
+
const observedAt = normalizeExplicitIsoTimestamp(candidate.observedAt) ?? '';
|
|
235
|
+
if (!Array.isArray(candidate.checks))
|
|
181
236
|
return null;
|
|
237
|
+
const context = normalizeDomainPostureProfileContext(candidate.context);
|
|
182
238
|
const checks = [];
|
|
183
239
|
const seen = new Set();
|
|
184
240
|
for (const item of candidate.checks.slice(0, 64)) {
|
|
@@ -190,28 +246,88 @@ function normalizeDesiredPostureObservation(value) {
|
|
|
190
246
|
|| !POSTURE_CHECK_STATUSES.has(check.status))
|
|
191
247
|
continue;
|
|
192
248
|
seen.add(id);
|
|
249
|
+
if (check.sourceContext !== undefined && !Array.isArray(check.records))
|
|
250
|
+
throw new TypeError('Source-qualified posture records must be an array.');
|
|
251
|
+
const inputRecords = Array.isArray(check.records) ? check.records : [];
|
|
252
|
+
const records = [];
|
|
253
|
+
for (const item of inputRecords.slice(0, MAX_POSTURE_CHECK_RECORDS)) {
|
|
254
|
+
if (typeof item === 'string' && item.length <= MAX_POSTURE_RECORD_LENGTH && !CONTROL_RE.test(item))
|
|
255
|
+
records.push(item);
|
|
256
|
+
}
|
|
257
|
+
const omitted = Math.max(0, inputRecords.length - records.length);
|
|
258
|
+
const source = normalizeDomainPostureSourceContext(check.sourceContext);
|
|
259
|
+
const sourceContext = source && omitted ? {
|
|
260
|
+
...source,
|
|
261
|
+
state: source.state === 'unavailable' ? 'unavailable' : 'partial',
|
|
262
|
+
omittedRecords: source.omittedRecords === null ? null : Math.min(Number.MAX_SAFE_INTEGER, source.omittedRecords + omitted),
|
|
263
|
+
} : source;
|
|
193
264
|
checks.push({
|
|
194
265
|
id,
|
|
195
266
|
status: check.status,
|
|
196
|
-
records
|
|
267
|
+
records,
|
|
268
|
+
...(sourceContext ? { sourceContext } : {}),
|
|
197
269
|
});
|
|
198
|
-
if (checks.length >=
|
|
270
|
+
if (checks.length >= MAX_POSTURE_CHECKS)
|
|
199
271
|
break;
|
|
200
272
|
}
|
|
201
|
-
|
|
273
|
+
const omittedChecks = Math.max(0, candidate.checks.length - checks.length)
|
|
274
|
+
+ (Number.isSafeInteger(candidate.omittedChecks) && Number(candidate.omittedChecks) > 0 ? Number(candidate.omittedChecks) : 0);
|
|
275
|
+
return checks.length ? { observedAt, ...(context ? { context } : {}), checks, ...(omittedChecks ? { omittedChecks: Math.min(Number.MAX_SAFE_INTEGER, omittedChecks) } : {}) } : null;
|
|
202
276
|
}
|
|
203
|
-
function normalizeDesiredPostureObservationHistory(value, previous) {
|
|
277
|
+
export function normalizeDesiredPostureObservationHistory(value, previous) {
|
|
204
278
|
const candidates = Array.isArray(value) ? value : previous ? [previous] : [];
|
|
205
279
|
const byTime = new Map();
|
|
206
280
|
for (const item of candidates.slice(0, MAX_DESIRED_POSTURE_OBSERVATIONS * 4)) {
|
|
207
281
|
const normalized = normalizeDesiredPostureObservation(item);
|
|
208
|
-
if (normalized)
|
|
209
|
-
|
|
282
|
+
if (!normalized)
|
|
283
|
+
continue;
|
|
284
|
+
if (new TextEncoder().encode(JSON.stringify(normalized)).byteLength > MAX_IDENTITY_DIGEST_BYTES) {
|
|
285
|
+
throw new RangeError('Record identity exceeds its byte limit.');
|
|
286
|
+
}
|
|
287
|
+
const cohort = byTime.get(normalized.observedAt) ?? [];
|
|
288
|
+
cohort.push(normalized);
|
|
289
|
+
byTime.set(normalized.observedAt, cohort);
|
|
210
290
|
}
|
|
211
|
-
return [...byTime.
|
|
212
|
-
.sort((left, right) => Date.parse(left
|
|
291
|
+
return [...byTime.entries()]
|
|
292
|
+
.sort(([left], [right]) => !left ? 1 : !right ? -1 : Date.parse(left) - Date.parse(right))
|
|
293
|
+
.flatMap(([, cohort]) => {
|
|
294
|
+
// Different capture times already distinguish observations. Digest-based
|
|
295
|
+
// deduplication and ordering remain necessary only within a tied cohort.
|
|
296
|
+
if (cohort.length === 1)
|
|
297
|
+
return cohort;
|
|
298
|
+
const byIdentity = new Map(cohort.map((observation) => [desiredPostureObservationIdentity(observation), observation]));
|
|
299
|
+
return [...byIdentity.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
300
|
+
.map(([, observation]) => observation);
|
|
301
|
+
})
|
|
213
302
|
.slice(-MAX_DESIRED_POSTURE_OBSERVATIONS);
|
|
214
303
|
}
|
|
304
|
+
export function desiredPostureObservationIdentity(observation) {
|
|
305
|
+
return sha256IdentityHex(new TextEncoder().encode(JSON.stringify(observation)));
|
|
306
|
+
}
|
|
307
|
+
export function desiredPostureObservations(baseline) {
|
|
308
|
+
return baseline.observationHistory?.length ? baseline.observationHistory : baseline.previousObservation ? [baseline.previousObservation] : [];
|
|
309
|
+
}
|
|
310
|
+
export function currentDesiredPostureObservation(baseline) {
|
|
311
|
+
const history = desiredPostureObservations(baseline);
|
|
312
|
+
const cohort = latestObservationCohort(history, (item) => item.observedAt);
|
|
313
|
+
return {
|
|
314
|
+
observation: !cohort.undated.length && cohort.latest.length === 1 ? cohort.latest[0] : null,
|
|
315
|
+
candidates: [...cohort.latest, ...cohort.undated],
|
|
316
|
+
limitation: cohort.undated.length ? 'Retained observations include an unknown capture time; no unique latest review is selected.'
|
|
317
|
+
: cohort.latest.length > 1 ? 'Distinct observations share the latest capture time; no unique latest review is selected.' : null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export function brandPostureCollectionFingerprint(profile) {
|
|
321
|
+
return sha256IdentityHex(new TextEncoder().encode(JSON.stringify([
|
|
322
|
+
profile.id, [...profile.officialDomains].sort(), profile.mailProtectionProfile,
|
|
323
|
+
[...profile.dkimSelectors].sort(), [...profile.retiredDkimSelectors].sort(),
|
|
324
|
+
])));
|
|
325
|
+
}
|
|
326
|
+
export function brandPostureObservationContext(profile, domain) {
|
|
327
|
+
if (!profile.officialDomains.includes(domain))
|
|
328
|
+
throw new TypeError('Posture observation target is not an official domain of this profile.');
|
|
329
|
+
return { version: DOMAIN_POSTURE_COMPARISON_VERSION, domain, profileId: profile.id, profileFingerprint: brandPostureCollectionFingerprint(profile) };
|
|
330
|
+
}
|
|
215
331
|
function deterministicChangeWindowId(seed) {
|
|
216
332
|
const states = [0x811c9dc5, 0x9e3779b9, 0x85ebca6b, 0xc2b2ae35];
|
|
217
333
|
const factors = [0x01000193, 0x85ebca6b, 0xc2b2ae35, 0x27d4eb2f];
|
|
@@ -270,6 +386,23 @@ function normalizeTlsSanPatterns(value) {
|
|
|
270
386
|
}
|
|
271
387
|
return [...output].sort();
|
|
272
388
|
}
|
|
389
|
+
function normalizeBaselineRecords(candidate) {
|
|
390
|
+
const records = {
|
|
391
|
+
nameservers: normalizeDesiredPostureRecords(candidate.nameservers, (entry) => normalizeDomain(entry)),
|
|
392
|
+
ds: normalizeDesiredPostureRecords(candidate.ds),
|
|
393
|
+
mx: normalizeDesiredPostureRecords(candidate.mx),
|
|
394
|
+
caa: normalizeDesiredPostureRecords(candidate.caa),
|
|
395
|
+
};
|
|
396
|
+
if (!Object.hasOwn(candidate, 'recordModes'))
|
|
397
|
+
return records;
|
|
398
|
+
const recordModes = normalizeDeclaredDomainControlRecordModes(candidate.recordModes);
|
|
399
|
+
for (const field of DOMAIN_CONTROL_RECORD_LIST_FIELDS) {
|
|
400
|
+
if (Object.hasOwn(recordModes, field))
|
|
401
|
+
records[field] = canonicalDomainControlRecords(candidate[field], field);
|
|
402
|
+
}
|
|
403
|
+
normalizeDomainControlRecordModes(recordModes, records);
|
|
404
|
+
return { ...records, recordModes };
|
|
405
|
+
}
|
|
273
406
|
export function normalizeDesiredPostureBaselines(value, officialDomains, fallbackNow = new Date().toISOString(), identityScope = '') {
|
|
274
407
|
if (!Array.isArray(value))
|
|
275
408
|
return [];
|
|
@@ -306,10 +439,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
|
|
|
306
439
|
output.push({
|
|
307
440
|
version: 1,
|
|
308
441
|
domain,
|
|
309
|
-
|
|
310
|
-
ds: normalizeDesiredPostureRecords(candidate.ds),
|
|
311
|
-
mx: normalizeDesiredPostureRecords(candidate.mx),
|
|
312
|
-
caa: normalizeDesiredPostureRecords(candidate.caa),
|
|
442
|
+
...normalizeBaselineRecords(candidate),
|
|
313
443
|
tlsIssuer: boundedText(candidate.tlsIssuer, MAX_PROFILE_TEXT_LENGTH),
|
|
314
444
|
tlsSanPatterns: normalizeTlsSanPatterns(candidate.tlsSanPatterns),
|
|
315
445
|
tlsSpkiSha256: typeof candidate.tlsSpkiSha256 === 'string' && SHA256_RE.test(candidate.tlsSpkiSha256)
|
|
@@ -329,7 +459,7 @@ export function normalizeDesiredPostureBaselines(value, officialDomains, fallbac
|
|
|
329
459
|
approvedChangeWindows: normalizeDesiredPostureChangeWindows(candidate.approvedChangeWindows, `${identityScope}\u001f${domain}`),
|
|
330
460
|
suppressions,
|
|
331
461
|
note: boundedText(candidate.note, MAX_PROFILE_TEXT_LENGTH),
|
|
332
|
-
previousObservation:
|
|
462
|
+
previousObservation: currentDesiredPostureObservation({ observationHistory, previousObservation }).observation,
|
|
333
463
|
observationHistory,
|
|
334
464
|
updatedAt: timestamp(candidate.updatedAt, fallback),
|
|
335
465
|
});
|
|
@@ -510,7 +640,7 @@ export function brandProfileStoreVersion(raw) {
|
|
|
510
640
|
}
|
|
511
641
|
/** Normalize an internal profile collection or current stored envelope. */
|
|
512
642
|
export function normalizeBrandProfileStore(raw) {
|
|
513
|
-
assertWorkspaceInputGraph(raw, 'Brand Profile store');
|
|
643
|
+
assertWorkspaceInputGraph(raw, 'Brand Profile store', { maximumBytes: MAX_PROFILE_STORE_BYTES });
|
|
514
644
|
assertWorkspaceDeclaredVersion(raw, 'Brand Profile store');
|
|
515
645
|
const sourceVersion = brandProfileStoreVersion(raw);
|
|
516
646
|
if (!Array.isArray(raw) && sourceVersion !== null
|
|
@@ -532,6 +662,10 @@ export function normalizeBrandProfileStore(raw) {
|
|
|
532
662
|
}
|
|
533
663
|
return { version: BRAND_PROFILE_SCHEMA_VERSION, profiles: [...byId.values()] };
|
|
534
664
|
}
|
|
665
|
+
/** New authored/imported records receive an identity only at the mutation boundary. */
|
|
666
|
+
export function createBrandProfileId() {
|
|
667
|
+
return crypto.randomUUID ? crypto.randomUUID() : `bp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
668
|
+
}
|
|
535
669
|
export function serializeBrandProfileStore(profiles) {
|
|
536
670
|
return JSON.stringify(assertBrandProfileStoreBudget(profiles));
|
|
537
671
|
}
|
|
@@ -546,8 +680,8 @@ export function assertBrandProfileStoreBudget(profiles) {
|
|
|
546
680
|
return store;
|
|
547
681
|
}
|
|
548
682
|
export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
549
|
-
|
|
550
|
-
assertWorkspaceInputGraph(importedRaw, 'Imported Brand Profile document');
|
|
683
|
+
const local = normalizeBrandProfileStore(localRaw).profiles;
|
|
684
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported Brand Profile document', { maximumBytes: MAX_PROFILE_STORE_BYTES });
|
|
551
685
|
assertWorkspacePortableVersion(importedRaw, BRAND_PROFILE_SCHEMA_VERSION, 'Imported Brand Profile document');
|
|
552
686
|
const imported = record(importedRaw);
|
|
553
687
|
if (imported.schema !== BRAND_PROFILE_SCHEMA) {
|
|
@@ -579,7 +713,6 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
579
713
|
if (id && name)
|
|
580
714
|
retainIdName(id, name);
|
|
581
715
|
}
|
|
582
|
-
const local = normalizeBrandProfileStore(localRaw).profiles;
|
|
583
716
|
for (const profile of local)
|
|
584
717
|
retainIdName(profile.id, profile.name);
|
|
585
718
|
const byName = new Map(local.map((profile) => [profile.name.toLowerCase(), profile]));
|
|
@@ -594,9 +727,14 @@ export function mergeBrandProfiles(localRaw, importedRaw, options = {}) {
|
|
|
594
727
|
if (rawId && rawName)
|
|
595
728
|
retainIdName(rawId, rawName);
|
|
596
729
|
const existing = rawName ? byName.get(rawName.toLowerCase()) : null;
|
|
730
|
+
const incomingUpdatedAt = timestamp(value.updatedAt, null);
|
|
731
|
+
if (existing && (!incomingUpdatedAt || incomingUpdatedAt <= existing.updatedAt)) {
|
|
732
|
+
skipped++;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
597
735
|
const profile = normalizeBrandProfile(item, {
|
|
598
736
|
existing,
|
|
599
|
-
touch:
|
|
737
|
+
touch: false,
|
|
600
738
|
nowIso: options.nowIso,
|
|
601
739
|
makeId: options.makeId,
|
|
602
740
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Optional result columns. Domain identity and actions are always available. */
|
|
2
|
+
export const BULK_RESULT_COLUMNS = Object.freeze([
|
|
3
|
+
{ id: 'registration', label: 'Registration' },
|
|
4
|
+
{ id: 'risk', label: 'Risk' },
|
|
5
|
+
{ id: 'website', label: 'Website' },
|
|
6
|
+
{ id: 'registrar', label: 'Registrar' },
|
|
7
|
+
{ id: 'mutation', label: 'Mutation' },
|
|
8
|
+
{ id: 'review', label: 'Review' },
|
|
9
|
+
{ id: 'case', label: 'Case' },
|
|
10
|
+
]);
|
|
11
|
+
export const DEFAULT_BULK_RESULT_COLUMNS = Object.freeze(BULK_RESULT_COLUMNS.map((column) => column.id));
|
|
12
|
+
export function normalizeBulkResultColumns(value) {
|
|
13
|
+
// Missing choices preserve the full presentation used by public version 1.
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return [...DEFAULT_BULK_RESULT_COLUMNS];
|
|
16
|
+
return BULK_RESULT_COLUMNS.filter((column) => value.includes(column.id)).map((column) => column.id);
|
|
17
|
+
}
|