@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs}
RENAMED
|
@@ -1,58 +1,44 @@
|
|
|
1
1
|
// Pure, bounded workspace archive composition. Existing typed store models remain the
|
|
2
2
|
// authority for normalization and merge semantics; this module only packages
|
|
3
3
|
// their portable contracts, verifies integrity, and previews conflicts.
|
|
4
|
-
import { buildCaseExport,
|
|
5
|
-
import { assertBrandProfileStoreBudget,
|
|
6
|
-
import { assertCampaignStoreBudget, buildCampaignExport,
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export
|
|
22
|
-
export const MAX_WORKSPACE_ARCHIVE_SECTIONS = 12;
|
|
23
|
-
export const WORKSPACE_ARCHIVE_SECTION_IDS = [
|
|
24
|
-
'cases',
|
|
25
|
-
'campaigns',
|
|
26
|
-
'brandProfiles',
|
|
27
|
-
'watchlists',
|
|
28
|
-
'shortlist',
|
|
29
|
-
'detectionRules',
|
|
30
|
-
'relationshipObservations',
|
|
31
|
-
'bulkSessions',
|
|
32
|
-
'websiteSnapshots',
|
|
33
|
-
'investigationTemplates',
|
|
34
|
-
'bulkReview',
|
|
35
|
-
'settings',
|
|
36
|
-
];
|
|
37
|
-
export function isSupportedWorkspaceArchiveVersion(value) {
|
|
38
|
-
return typeof value === 'number'
|
|
39
|
-
&& Number.isSafeInteger(value)
|
|
40
|
-
&& SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS.some((version) => version === value);
|
|
41
|
-
}
|
|
4
|
+
import { buildCaseExport, enforceStoreBudget, mergeCases, } from "../cases/case-model.mjs";
|
|
5
|
+
import { assertBrandProfileStoreBudget, buildBrandProfileExport, mergeBrandProfiles, normalizeBrandProfileId, } from "./brand-profile-model.mjs";
|
|
6
|
+
import { assertCampaignStoreBudget, buildCampaignExport, mergeCampaigns, } from "./campaign-model.mjs";
|
|
7
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
8
|
+
import { assertWorkspaceInputGraph } from "./hostile-input.mjs";
|
|
9
|
+
import { assertWatchlistStoreBudget, buildWatchlistExport, mergeWatchlistStores, } from "./watchlist-store.mjs";
|
|
10
|
+
import { assertShortlistStoreBudget, buildShortlistExport, mergeShortlistStores, } from "./shortlist-model.mjs";
|
|
11
|
+
import { assertDetectionRuleStoreBudget, buildDetectionRuleExport, mergeDetectionRules, } from "./detection-rule-model.mjs";
|
|
12
|
+
import { buildRelationshipObservationExport, mergeRelationshipObservations, } from "./relationship-observation-model.mjs";
|
|
13
|
+
import { buildBulkSessionExport, enforceBulkSessionStoreBudget, mergeBulkSessions, } from "./bulk-session-model.mjs";
|
|
14
|
+
import { buildWebsiteSnapshotExport, mergeWebsiteSnapshots, } from "./website-snapshot-model.mjs";
|
|
15
|
+
import { buildInvestigationTemplateExport, mergeInvestigationTemplates, } from "./investigation-template-model.mjs";
|
|
16
|
+
import { buildBulkReviewExport, mergeBulkReviewStores, } from "./bulk-review-model.mjs";
|
|
17
|
+
import { buildAnalystReviewStateExport, emptyAnalystReviewStateStore, mergeAnalystReviewStateStores, } from "../monitoring/analyst-review-state.mjs";
|
|
18
|
+
import { isSupportedWorkspaceArchiveVersion, MAX_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_SECTION_BYTES, MAX_WORKSPACE_ARCHIVE_SECTIONS, serialiseWorkspaceArchiveSection, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_CASE_SECTION, WORKSPACE_ARCHIVE_SCHEMA, WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_VERSION, WORKSPACE_ARCHIVE_VERSION, WORKSPACE_SETTINGS_COMPATIBILITY, WORKSPACE_SETTINGS_SCHEMA, WORKSPACE_SETTINGS_VERSION, } from "../contracts/case-portability.mjs";
|
|
19
|
+
import { ANALYST_REVIEW_STATE_COMPATIBILITY } from "../contracts/analyst-review-state.mjs";
|
|
20
|
+
import { WORKSPACE_PORTABILITY_ARCHIVE_SECTION_REFERENCES } from "../contracts/workspace-portability.mjs";
|
|
21
|
+
export { isSupportedWorkspaceArchiveVersion, MAX_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_SECTION_BYTES, MAX_WORKSPACE_ARCHIVE_SECTIONS, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_CASE_SECTION, WORKSPACE_ARCHIVE_SCHEMA, WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_VERSION, WORKSPACE_ARCHIVE_VERSION, WORKSPACE_SETTINGS_SCHEMA, WORKSPACE_SETTINGS_VERSION, };
|
|
42
22
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
43
23
|
const CHECKSUM_RE = /^sha256:[a-f0-9]{64}$/;
|
|
24
|
+
const WORKSPACE_ARCHIVE_ROOT_KEYS = Object.freeze(['schema', 'version', 'generatedAt', 'manifest', 'sections', 'limitations']);
|
|
25
|
+
const WORKSPACE_ARCHIVE_MANIFEST_KEYS = Object.freeze(['sectionCount', 'totalRecords', 'sections']);
|
|
26
|
+
const WORKSPACE_ARCHIVE_MANIFEST_ENTRY_KEYS = Object.freeze(['id', 'schema', 'version', 'recordCount', 'bytes', 'checksum']);
|
|
44
27
|
function record(value) {
|
|
45
28
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
46
29
|
}
|
|
30
|
+
function assertExactKeys(value, expected, label) {
|
|
31
|
+
const keys = Object.keys(value);
|
|
32
|
+
if (keys.length !== expected.length || keys.some((key) => !expected.includes(key))) {
|
|
33
|
+
throw new Error(`The workspace archive ${label} contains missing or undeclared fields.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
47
36
|
function byteLength(value) {
|
|
48
37
|
return new TextEncoder().encode(value).byteLength;
|
|
49
38
|
}
|
|
50
39
|
function serialize(value, message = 'The workspace archive contains data that cannot be serialised.') {
|
|
51
40
|
try {
|
|
52
|
-
|
|
53
|
-
if (typeof serialized !== 'string')
|
|
54
|
-
throw new Error(message);
|
|
55
|
-
return serialized;
|
|
41
|
+
return serialiseWorkspaceArchiveSection(value);
|
|
56
42
|
}
|
|
57
43
|
catch {
|
|
58
44
|
throw new Error(message);
|
|
@@ -62,10 +48,10 @@ function clone(value) {
|
|
|
62
48
|
return JSON.parse(serialize(value));
|
|
63
49
|
}
|
|
64
50
|
function timestamp(value, fallback = null) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return
|
|
51
|
+
const explicit = normalizeExplicitIsoTimestamp(value);
|
|
52
|
+
if (explicit)
|
|
53
|
+
return explicit;
|
|
54
|
+
return fallback;
|
|
69
55
|
}
|
|
70
56
|
function boundedText(value, maximum = 300) {
|
|
71
57
|
if (typeof value !== 'string' || value.length > maximum * 4 || CONTROL_RE.test(value))
|
|
@@ -124,6 +110,7 @@ function workspaceArchiveSections(input, now) {
|
|
|
124
110
|
websiteSnapshots: buildWebsiteSnapshotExport(input.websiteSnapshots, now),
|
|
125
111
|
investigationTemplates: buildInvestigationTemplateExport(input.investigationTemplates, now),
|
|
126
112
|
bulkReview: buildBulkReviewExport(input.bulkReview),
|
|
113
|
+
analystReviewState: buildAnalystReviewStateExport(input.analystReviewState),
|
|
127
114
|
settings: settingsDocument(input),
|
|
128
115
|
};
|
|
129
116
|
}
|
|
@@ -136,9 +123,29 @@ function objectCount(data, key) {
|
|
|
136
123
|
const nested = value ? record(value[key]) : null;
|
|
137
124
|
return nested ? Object.keys(nested).length : 0;
|
|
138
125
|
}
|
|
126
|
+
function sectionContract(sectionId) {
|
|
127
|
+
const reference = WORKSPACE_PORTABILITY_ARCHIVE_SECTION_REFERENCES.find((entry) => entry.sectionId === sectionId);
|
|
128
|
+
if (!reference)
|
|
129
|
+
throw new TypeError(`Workspace archive section ${sectionId} has no canonical contract reference.`);
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
id: reference.sectionId,
|
|
132
|
+
schema: reference.schema,
|
|
133
|
+
version: reference.version,
|
|
134
|
+
supportedVersions: reference.supportedVersions,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function compatibilitySchema(descriptor) {
|
|
138
|
+
if (!descriptor.schema)
|
|
139
|
+
throw new TypeError(`Workspace compatibility ${descriptor.id} must name a schema.`);
|
|
140
|
+
return descriptor.schema;
|
|
141
|
+
}
|
|
139
142
|
const SECTION_DEFINITIONS = [
|
|
140
143
|
{
|
|
141
|
-
id:
|
|
144
|
+
id: WORKSPACE_ARCHIVE_CASE_SECTION.id,
|
|
145
|
+
label: 'Cases',
|
|
146
|
+
schema: WORKSPACE_ARCHIVE_CASE_SECTION.schema,
|
|
147
|
+
version: WORKSPACE_ARCHIVE_CASE_SECTION.currentVersion,
|
|
148
|
+
supportedVersions: WORKSPACE_ARCHIVE_CASE_SECTION.supportedVersions,
|
|
142
149
|
count: (data) => arrayCount(data, 'cases'),
|
|
143
150
|
merge: (local, data) => {
|
|
144
151
|
const result = mergeCases(local.cases, data);
|
|
@@ -147,7 +154,7 @@ const SECTION_DEFINITIONS = [
|
|
|
147
154
|
},
|
|
148
155
|
},
|
|
149
156
|
{
|
|
150
|
-
|
|
157
|
+
...sectionContract('campaigns'), label: 'Campaigns',
|
|
151
158
|
count: (data) => arrayCount(data, 'campaigns'),
|
|
152
159
|
merge: (local, data) => {
|
|
153
160
|
const result = mergeCampaigns(local.campaigns, data);
|
|
@@ -155,7 +162,7 @@ const SECTION_DEFINITIONS = [
|
|
|
155
162
|
},
|
|
156
163
|
},
|
|
157
164
|
{
|
|
158
|
-
|
|
165
|
+
...sectionContract('brandProfiles'), label: 'Brand profiles',
|
|
159
166
|
count: (data) => arrayCount(data, 'profiles'),
|
|
160
167
|
merge: (local, data, now) => {
|
|
161
168
|
const result = mergeBrandProfiles(local.brandProfiles, data, { nowIso: now });
|
|
@@ -163,7 +170,7 @@ const SECTION_DEFINITIONS = [
|
|
|
163
170
|
},
|
|
164
171
|
},
|
|
165
172
|
{
|
|
166
|
-
|
|
173
|
+
...sectionContract('watchlists'), label: 'Watchlists',
|
|
167
174
|
count: (data) => objectCount(data, 'watchlists'),
|
|
168
175
|
merge: (local, data) => {
|
|
169
176
|
const result = mergeWatchlistStores(local.watchlists, data);
|
|
@@ -171,7 +178,7 @@ const SECTION_DEFINITIONS = [
|
|
|
171
178
|
},
|
|
172
179
|
},
|
|
173
180
|
{
|
|
174
|
-
|
|
181
|
+
...sectionContract('shortlist'), label: 'Shortlist',
|
|
175
182
|
count: (data) => arrayCount(data, 'entries'),
|
|
176
183
|
merge: (local, data) => {
|
|
177
184
|
const result = mergeShortlistStores(local.shortlist, data);
|
|
@@ -179,7 +186,7 @@ const SECTION_DEFINITIONS = [
|
|
|
179
186
|
},
|
|
180
187
|
},
|
|
181
188
|
{
|
|
182
|
-
|
|
189
|
+
...sectionContract('detectionRules'), label: 'Detection rules',
|
|
183
190
|
count: (data) => arrayCount(data, 'rules'),
|
|
184
191
|
merge: (local, data) => {
|
|
185
192
|
const result = mergeDetectionRules(local.detectionRules, data);
|
|
@@ -187,18 +194,14 @@ const SECTION_DEFINITIONS = [
|
|
|
187
194
|
},
|
|
188
195
|
},
|
|
189
196
|
{
|
|
190
|
-
|
|
197
|
+
...sectionContract('relationshipObservations'),
|
|
191
198
|
label: 'Retained relationship observations',
|
|
192
|
-
schema: RELATIONSHIP_OBSERVATION_SCHEMA,
|
|
193
|
-
version: RELATIONSHIP_OBSERVATION_SCHEMA_VERSION,
|
|
194
199
|
count: (data) => arrayCount(data, 'observations'),
|
|
195
200
|
merge: (local, data) => mergeRelationshipObservations(local.relationshipObservations, data),
|
|
196
201
|
},
|
|
197
202
|
{
|
|
198
|
-
|
|
203
|
+
...sectionContract('bulkSessions'),
|
|
199
204
|
label: 'Saved Bulk sessions',
|
|
200
|
-
schema: BULK_SESSION_SCHEMA,
|
|
201
|
-
version: BULK_SESSION_SCHEMA_VERSION,
|
|
202
205
|
count: (data) => arrayCount(data, 'sessions'),
|
|
203
206
|
merge: (local, data) => {
|
|
204
207
|
const result = mergeBulkSessions(local.bulkSessions, data);
|
|
@@ -207,31 +210,36 @@ const SECTION_DEFINITIONS = [
|
|
|
207
210
|
},
|
|
208
211
|
},
|
|
209
212
|
{
|
|
210
|
-
|
|
213
|
+
...sectionContract('websiteSnapshots'),
|
|
211
214
|
label: 'Website profile snapshots',
|
|
212
|
-
schema: WEBSITE_SNAPSHOT_SCHEMA,
|
|
213
|
-
version: WEBSITE_SNAPSHOT_SCHEMA_VERSION,
|
|
214
215
|
count: (data) => arrayCount(data, 'snapshots'),
|
|
215
216
|
merge: (local, data) => mergeWebsiteSnapshots(local.websiteSnapshots, data),
|
|
216
217
|
},
|
|
217
218
|
{
|
|
218
|
-
|
|
219
|
+
...sectionContract('investigationTemplates'),
|
|
219
220
|
label: 'Investigation templates',
|
|
220
|
-
schema: INVESTIGATION_TEMPLATE_SCHEMA,
|
|
221
|
-
version: INVESTIGATION_TEMPLATE_VERSION,
|
|
222
221
|
count: (data) => arrayCount(data, 'templates'),
|
|
223
222
|
merge: (local, data) => mergeInvestigationTemplates(local.investigationTemplates, data),
|
|
224
223
|
},
|
|
225
224
|
{
|
|
226
|
-
|
|
225
|
+
...sectionContract('bulkReview'),
|
|
227
226
|
label: 'Bulk saved views and review queue',
|
|
228
|
-
schema: BULK_REVIEW_SCHEMA,
|
|
229
|
-
version: BULK_REVIEW_SCHEMA_VERSION,
|
|
230
227
|
count: (data) => arrayCount(data, 'presets') + arrayCount(data, 'rows'),
|
|
231
228
|
merge: (local, data) => mergeBulkReviewStores(local.bulkReview, data),
|
|
232
229
|
},
|
|
233
230
|
{
|
|
234
|
-
id: '
|
|
231
|
+
id: 'analystReviewState',
|
|
232
|
+
label: 'Analyst Review Item lifecycle',
|
|
233
|
+
schema: compatibilitySchema(ANALYST_REVIEW_STATE_COMPATIBILITY),
|
|
234
|
+
version: ANALYST_REVIEW_STATE_COMPATIBILITY.currentVersion,
|
|
235
|
+
supportedVersions: ANALYST_REVIEW_STATE_COMPATIBILITY.supportedVersions,
|
|
236
|
+
count: (data) => arrayCount(data, 'records'),
|
|
237
|
+
merge: (local, data) => mergeAnalystReviewStateStores(local.analystReviewState, data),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'settings', label: 'Workspace settings', schema: compatibilitySchema(WORKSPACE_SETTINGS_COMPATIBILITY),
|
|
241
|
+
version: WORKSPACE_SETTINGS_COMPATIBILITY.currentVersion,
|
|
242
|
+
supportedVersions: WORKSPACE_SETTINGS_COMPATIBILITY.supportedVersions,
|
|
235
243
|
count: () => 1,
|
|
236
244
|
merge: null,
|
|
237
245
|
},
|
|
@@ -257,6 +265,7 @@ function normalizedInput(input) {
|
|
|
257
265
|
websiteSnapshots: Array.isArray(value.websiteSnapshots) ? value.websiteSnapshots : [],
|
|
258
266
|
investigationTemplates: Array.isArray(value.investigationTemplates) ? value.investigationTemplates : [],
|
|
259
267
|
bulkReview: record(value.bulkReview) || {},
|
|
268
|
+
analystReviewState: record(value.analystReviewState) || emptyAnalystReviewStateStore(),
|
|
260
269
|
settings: record(value.settings) || {},
|
|
261
270
|
};
|
|
262
271
|
}
|
|
@@ -270,7 +279,9 @@ function ensureArchiveBudget(value) {
|
|
|
270
279
|
}
|
|
271
280
|
/** Build one deterministic, unencrypted archive from normalized local stores. */
|
|
272
281
|
export async function buildWorkspaceArchive(input, options = {}) {
|
|
273
|
-
const now = timestamp(options.generatedAt
|
|
282
|
+
const now = timestamp(options.generatedAt ?? new Date().toISOString());
|
|
283
|
+
if (!now)
|
|
284
|
+
throw new Error('Workspace archive generation time must use an explicit timezone.');
|
|
274
285
|
const source = normalizedInput(input);
|
|
275
286
|
const sections = workspaceArchiveSections(source, now);
|
|
276
287
|
const manifestSections = [];
|
|
@@ -327,23 +338,35 @@ function manifestEntry(raw) {
|
|
|
327
338
|
}
|
|
328
339
|
/** Validate structure, section byte counts, and checksums without applying data. */
|
|
329
340
|
export async function readWorkspaceArchive(raw, options = {}) {
|
|
341
|
+
assertWorkspaceInputGraph(raw, 'Workspace archive');
|
|
330
342
|
const value = record(raw);
|
|
331
343
|
if (!value || value.schema !== WORKSPACE_ARCHIVE_SCHEMA) {
|
|
332
344
|
throw new Error('This file is not a WHOISleuth workspace archive.');
|
|
333
345
|
}
|
|
334
346
|
if (!isSupportedWorkspaceArchiveVersion(value.version)) {
|
|
347
|
+
if (typeof value.version === 'number' && Number.isSafeInteger(value.version) && value.version < WORKSPACE_ARCHIVE_VERSION) {
|
|
348
|
+
throw new Error(`Workspace archive schema ${value.version} is retired. Export it as schema ${WORKSPACE_ARCHIVE_VERSION} with the last broad-reader release before importing; no data was changed.`);
|
|
349
|
+
}
|
|
335
350
|
if (typeof value.version === 'number' && Number.isSafeInteger(value.version) && value.version > WORKSPACE_ARCHIVE_VERSION) {
|
|
336
|
-
throw new Error(`
|
|
351
|
+
throw new Error(`Workspace archive schema ${value.version} is newer than the supported schema ${WORKSPACE_ARCHIVE_VERSION}; no data was changed.`);
|
|
337
352
|
}
|
|
338
|
-
throw new Error(`Expected workspace archive schema
|
|
353
|
+
throw new Error(`Expected a well-formed workspace archive using schema ${WORKSPACE_ARCHIVE_VERSION}; no data was changed.`);
|
|
339
354
|
}
|
|
340
355
|
const sourceVersion = value.version;
|
|
356
|
+
const expectedSectionIds = sourceVersion === PUBLIC_WORKSPACE_ARCHIVE_VERSION
|
|
357
|
+
? PUBLIC_WORKSPACE_ARCHIVE_SECTION_IDS
|
|
358
|
+
: WORKSPACE_ARCHIVE_SECTION_IDS;
|
|
359
|
+
const generatedAt = timestamp(value.generatedAt);
|
|
360
|
+
if (!generatedAt)
|
|
361
|
+
throw new Error('The workspace archive generation time is invalid.');
|
|
341
362
|
const { bytes } = ensureArchiveBudget(value);
|
|
363
|
+
assertExactKeys(value, WORKSPACE_ARCHIVE_ROOT_KEYS, `version ${sourceVersion} envelope`);
|
|
342
364
|
const manifest = record(value.manifest);
|
|
343
365
|
const sectionValues = record(value.sections);
|
|
344
366
|
if (!manifest || !sectionValues || !Array.isArray(manifest.sections)) {
|
|
345
367
|
throw new Error('The workspace archive manifest is missing or malformed.');
|
|
346
368
|
}
|
|
369
|
+
assertExactKeys(manifest, WORKSPACE_ARCHIVE_MANIFEST_KEYS, 'manifest');
|
|
347
370
|
if (manifest.sections.length > MAX_WORKSPACE_ARCHIVE_SECTIONS) {
|
|
348
371
|
throw new Error(`Workspace archives are limited to ${MAX_WORKSPACE_ARCHIVE_SECTIONS} sections.`);
|
|
349
372
|
}
|
|
@@ -351,6 +374,10 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
351
374
|
const sections = [];
|
|
352
375
|
let totalRecords = 0;
|
|
353
376
|
for (const rawEntry of manifest.sections) {
|
|
377
|
+
const rawEntryRecord = record(rawEntry);
|
|
378
|
+
if (!rawEntryRecord)
|
|
379
|
+
throw new Error('The workspace archive manifest contains a malformed section entry.');
|
|
380
|
+
assertExactKeys(rawEntryRecord, WORKSPACE_ARCHIVE_MANIFEST_ENTRY_KEYS, 'manifest section entry');
|
|
354
381
|
const entry = manifestEntry(rawEntry);
|
|
355
382
|
if (!entry || seen.has(entry.id) || !Object.prototype.hasOwnProperty.call(sectionValues, entry.id)) {
|
|
356
383
|
throw new Error('The workspace archive manifest contains an invalid, duplicate, or missing section.');
|
|
@@ -363,6 +390,21 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
363
390
|
if (await checksum(data, options.cryptoProvider) !== entry.checksum) {
|
|
364
391
|
throw new Error(`${entry.id} failed its archive checksum check.`);
|
|
365
392
|
}
|
|
393
|
+
const embedded = record(data);
|
|
394
|
+
const embeddedVersion = embedded?.version;
|
|
395
|
+
const embeddedSchema = embedded && Object.prototype.hasOwnProperty.call(embedded, 'schema')
|
|
396
|
+
? embedded.schema
|
|
397
|
+
: null;
|
|
398
|
+
if (!embedded
|
|
399
|
+
|| typeof embeddedVersion !== 'number'
|
|
400
|
+
|| !Number.isSafeInteger(embeddedVersion)
|
|
401
|
+
|| embeddedVersion < 1
|
|
402
|
+
|| embeddedVersion > 1000
|
|
403
|
+
|| (embeddedSchema !== null && typeof embeddedSchema !== 'string')
|
|
404
|
+
|| embeddedVersion !== entry.version
|
|
405
|
+
|| embeddedSchema !== entry.schema) {
|
|
406
|
+
throw new Error(`${entry.id} section contract does not match its archive manifest.`);
|
|
407
|
+
}
|
|
366
408
|
const definition = DEFINITION_BY_ID.get(entry.id);
|
|
367
409
|
let status = 'ready';
|
|
368
410
|
let reason = '';
|
|
@@ -370,11 +412,13 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
370
412
|
status = 'unsupported';
|
|
371
413
|
reason = 'This app does not recognise the archive section.';
|
|
372
414
|
}
|
|
373
|
-
else if (
|
|
415
|
+
else if (!(definition.supportedVersions ?? [definition.version]).includes(entry.version) || entry.schema !== definition.schema) {
|
|
374
416
|
status = 'unsupported';
|
|
375
|
-
reason = entry.
|
|
376
|
-
?
|
|
377
|
-
:
|
|
417
|
+
reason = entry.schema !== definition.schema
|
|
418
|
+
? 'This section uses an unsupported schema contract.'
|
|
419
|
+
: entry.version > definition.version
|
|
420
|
+
? `This section uses newer schema ${entry.version}.`
|
|
421
|
+
: `This section uses retired schema ${entry.version}. Export it with the last broad-reader release before importing; no data was changed.`;
|
|
378
422
|
}
|
|
379
423
|
else if (definition.count(data) !== entry.recordCount) {
|
|
380
424
|
throw new Error(`${entry.id} does not match its manifest record count.`);
|
|
@@ -388,12 +432,30 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
388
432
|
if (manifest.sectionCount !== sections.length || manifest.totalRecords !== totalRecords) {
|
|
389
433
|
throw new Error('The workspace archive manifest totals do not match its sections.');
|
|
390
434
|
}
|
|
435
|
+
if (seen.size !== expectedSectionIds.length || expectedSectionIds.some((id) => !seen.has(id))) {
|
|
436
|
+
throw new Error(`Workspace archive schema ${sourceVersion} does not contain its exact required section set.`);
|
|
437
|
+
}
|
|
438
|
+
if (sourceVersion === PUBLIC_WORKSPACE_ARCHIVE_VERSION) {
|
|
439
|
+
const data = emptyAnalystReviewStateStore();
|
|
440
|
+
sections.push({
|
|
441
|
+
id: 'analystReviewState',
|
|
442
|
+
label: 'Analyst Review Item lifecycle',
|
|
443
|
+
schema: compatibilitySchema(ANALYST_REVIEW_STATE_COMPATIBILITY),
|
|
444
|
+
version: ANALYST_REVIEW_STATE_COMPATIBILITY.currentVersion,
|
|
445
|
+
recordCount: 0,
|
|
446
|
+
bytes: byteLength(serialize(data)),
|
|
447
|
+
checksum: await checksum(data, options.cryptoProvider),
|
|
448
|
+
status: 'ready',
|
|
449
|
+
reason: `Workspace schema ${PUBLIC_WORKSPACE_ARCHIVE_VERSION} did not contain Review Item lifecycle state. Import supplies an empty section and invents no analyst decisions.`,
|
|
450
|
+
data,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
391
453
|
sections.sort(canonicalSectionOrder);
|
|
392
454
|
return {
|
|
393
455
|
schema: WORKSPACE_ARCHIVE_SCHEMA,
|
|
394
456
|
version: WORKSPACE_ARCHIVE_VERSION,
|
|
395
457
|
sourceVersion,
|
|
396
|
-
generatedAt
|
|
458
|
+
generatedAt,
|
|
397
459
|
bytes,
|
|
398
460
|
sections,
|
|
399
461
|
limitations: Array.isArray(value.limitations)
|
|
@@ -403,14 +465,31 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
403
465
|
}
|
|
404
466
|
function settingsPreview(data, local, mergedProfiles) {
|
|
405
467
|
const value = record(data) || {};
|
|
406
|
-
const
|
|
468
|
+
const rawActiveProfileId = value.activeProfileId;
|
|
469
|
+
const intentionalClear = rawActiveProfileId === '';
|
|
470
|
+
const normalizedActiveProfileId = normalizeBrandProfileId(rawActiveProfileId);
|
|
471
|
+
const validActiveProfileId = typeof rawActiveProfileId === 'string'
|
|
472
|
+
&& rawActiveProfileId.length > 0
|
|
473
|
+
&& normalizedActiveProfileId === rawActiveProfileId;
|
|
407
474
|
const theme = normalizeTheme(value.theme);
|
|
475
|
+
const retainedActiveProfileId = normalizeBrandProfileId(local.settings?.activeProfileId) || '';
|
|
476
|
+
let activeProfileId = retainedActiveProfileId;
|
|
408
477
|
let skipped = 0;
|
|
409
|
-
|
|
410
|
-
|
|
478
|
+
let reason = '';
|
|
479
|
+
if (!intentionalClear && !validActiveProfileId) {
|
|
480
|
+
skipped = 1;
|
|
481
|
+
reason = 'The imported active Brand Profile preference is missing or malformed. Existing active-profile context is preserved.';
|
|
482
|
+
}
|
|
483
|
+
else if (validActiveProfileId && !mergedProfiles.some((profile) => profile?.id === normalizedActiveProfileId)) {
|
|
484
|
+
skipped = 1;
|
|
485
|
+
reason = 'The imported active Brand Profile preference will be skipped because its identifier is not available in the selected Profile data or the current browser. Existing active-profile context is preserved.';
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
activeProfileId = intentionalClear ? '' : normalizedActiveProfileId ?? retainedActiveProfileId;
|
|
489
|
+
}
|
|
411
490
|
const updated = Number(theme !== normalizeTheme(local.settings?.theme)
|
|
412
|
-
|| (
|
|
413
|
-
return { added: 0, updated, skipped, settings: { activeProfileId
|
|
491
|
+
|| (skipped === 0 && activeProfileId !== retainedActiveProfileId));
|
|
492
|
+
return { added: 0, updated, skipped, reason, settings: { activeProfileId, theme } };
|
|
414
493
|
}
|
|
415
494
|
/** Preview section-specific non-destructive merge outcomes without writing. */
|
|
416
495
|
export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
@@ -418,6 +497,9 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
418
497
|
const local = normalizedInput(localInput);
|
|
419
498
|
const results = [];
|
|
420
499
|
let mergedProfiles = local.brandProfiles;
|
|
500
|
+
const selectedIds = options.selectedSectionIds
|
|
501
|
+
? new Set(options.selectedSectionIds.slice(0, MAX_WORKSPACE_ARCHIVE_SECTIONS))
|
|
502
|
+
: null;
|
|
421
503
|
for (const section of archive.sections) {
|
|
422
504
|
if (section.status !== 'ready') {
|
|
423
505
|
results.push({ ...section, added: 0, updated: 0, skipped: section.recordCount, selected: false });
|
|
@@ -432,17 +514,21 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
432
514
|
: definition.merge
|
|
433
515
|
? definition.merge(local, section.data, archive.generatedAt)
|
|
434
516
|
: { added: 0, updated: 0, skipped: section.recordCount };
|
|
435
|
-
|
|
517
|
+
const selected = selectedIds ? selectedIds.has(section.id) : true;
|
|
518
|
+
if (definition.id === 'brandProfiles' && selected && result.profiles)
|
|
436
519
|
mergedProfiles = result.profiles;
|
|
437
520
|
results.push({
|
|
438
521
|
...section,
|
|
439
522
|
status: 'ready',
|
|
440
|
-
reason: ''
|
|
523
|
+
reason: definition.id === 'settings' && selected && (result.skipped ?? 0) > 0
|
|
524
|
+
? result.reason ?? 'The imported workspace preference was skipped. Existing browser-local context is preserved.'
|
|
525
|
+
: section.reason,
|
|
441
526
|
added: result.added ?? 0,
|
|
442
527
|
updated: result.updated ?? 0,
|
|
443
528
|
skipped: result.skipped ?? 0,
|
|
444
529
|
pruned: result.pruned ?? 0,
|
|
445
|
-
|
|
530
|
+
brandProfileReferencesOmitted: result.brandProfileReferencesOmitted ?? 0,
|
|
531
|
+
selected,
|
|
446
532
|
normalizedSettings: result.settings || null,
|
|
447
533
|
});
|
|
448
534
|
}
|
|
@@ -461,6 +547,31 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
461
547
|
});
|
|
462
548
|
}
|
|
463
549
|
}
|
|
550
|
+
const profileCollision = results.find((section) => section.id === 'brandProfiles'
|
|
551
|
+
&& section.status === 'blocked'
|
|
552
|
+
&& /reuses one exact identifier for different normalised profile names/iu.test(section.reason));
|
|
553
|
+
if (profileCollision) {
|
|
554
|
+
for (const dependentId of ['cases', 'settings']) {
|
|
555
|
+
const dependentIndex = results.findIndex((section) => section.id === dependentId && section.status === 'ready');
|
|
556
|
+
const dependentSection = results[dependentIndex];
|
|
557
|
+
if (dependentIndex < 0 || !dependentSection)
|
|
558
|
+
continue;
|
|
559
|
+
results[dependentIndex] = {
|
|
560
|
+
...dependentSection,
|
|
561
|
+
status: 'blocked',
|
|
562
|
+
reason: dependentId === 'cases'
|
|
563
|
+
? 'Cases were not selected because a Brand Profile identifier collision must be resolved before their opaque references can be imported safely.'
|
|
564
|
+
: 'Workspace settings were not selected because an active Brand Profile preference could otherwise bind to a different local profile that reuses the same identifier.',
|
|
565
|
+
added: 0,
|
|
566
|
+
updated: 0,
|
|
567
|
+
skipped: dependentSection.recordCount,
|
|
568
|
+
pruned: 0,
|
|
569
|
+
brandProfileReferencesOmitted: 0,
|
|
570
|
+
selected: false,
|
|
571
|
+
normalizedSettings: dependentId === 'settings' ? null : dependentSection.normalizedSettings ?? null,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
464
575
|
return {
|
|
465
576
|
...archive,
|
|
466
577
|
sections: results,
|