@slicedearth/whoisleuth-cli 2.2.0 → 2.3.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/README.md +2 -1
- package/cli/arguments.mjs +389 -1745
- package/cli/artifact-validation/case-response.mjs +55 -30
- package/cli/artifact-validation/signed-review.mjs +30 -12
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/case-pack.mjs +39 -92
- package/cli/command-argument-grammar.mjs +211 -0
- package/cli/command-reference.mjs +200 -213
- package/cli/completion.mjs +5 -1
- package/cli/discovery-observation-snapshot.mjs +16 -3
- package/cli/discovery-scan-command-runner.mjs +19 -0
- package/cli/discovery-scan.mjs +6 -0
- package/cli/domain-control-observations.mjs +1 -1
- package/cli/inline-command-families.mjs +1 -0
- package/cli/input.mjs +116 -0
- package/cli/interchange-report.mjs +14 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/mail-header-review.mjs +467 -0
- package/cli/review-command-runner.mjs +25 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +7 -1
- package/docs/cli-reference.md +8 -1
- package/docs/cli.md +12 -1
- package/lib/bounded-json.mjs +9 -6
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-intelligence.mjs +37 -3
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/domain-change-packet.mjs +42 -14
- package/lib/domain-control-flight-recorder.mjs +4 -1
- package/lib/domain-control-manifest.mjs +16 -16
- package/lib/domain-posture-parsers.mjs +114 -3
- package/lib/domain-posture.mjs +34 -3
- package/lib/evidence-export.mjs +60 -11
- package/lib/evidence-report.mjs +2 -2
- package/lib/html-signals.mjs +39 -33
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-response.mjs +5 -4
- package/lib/lookup-source-progress.mjs +27 -4
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/registry-insights.mjs +35 -22
- package/lib/risk-calibration-summary.mjs +1 -3
- package/lib/security-txt.mjs +33 -1
- package/lib/static-html-analysis.mjs +69 -13
- package/lib/website-technology.mjs +79 -11
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +1 -1
- package/packages/cases/case-migration-model.mjs +71 -22
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +10 -21
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +6 -4
- package/packages/cases/case-record-projection.mjs +218 -0
- package/packages/cases/case-report.mjs +11 -6
- package/packages/cases/case-response-model.mjs +43 -6
- package/packages/cases/case-response-packet.mjs +78 -27
- package/packages/cases/case-storage-model.mjs +6 -2
- package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
- package/packages/comparison/page-similarity.mjs +3 -1
- package/packages/contracts/case-portability.mjs +171 -51
- package/packages/contracts/cli-command-semantics.mjs +93 -0
- package/packages/contracts/risk-calibration.mjs +15 -10
- package/packages/contracts/workspace-portability.mjs +44 -7
- package/packages/evidence/domain-control-runtime.mjs +46 -6
- package/packages/interchange/external-findings-import.mjs +3 -2
- package/packages/investigation/lookup-asset-graph.mjs +35 -27
- package/packages/workspace/brand-profile-model.mjs +118 -2
- package/packages/workspace/bulk-review-model.mjs +13 -7
- package/packages/workspace/bulk-session-model.mjs +5 -4
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/relationship-observation-model.mjs +4 -1
- package/packages/workspace/website-snapshot-model.mjs +106 -8
- package/packages/workspace/workspace-archive.mjs +2 -0
- package/cli/evidence-command-arguments.mjs +0 -156
|
@@ -73,7 +73,7 @@ const JAVASCRIPT_MEDIA_TYPES = new Set([
|
|
|
73
73
|
'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
|
|
74
74
|
'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript',
|
|
75
75
|
]);
|
|
76
|
-
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH) {
|
|
76
|
+
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH, lowercase = true) {
|
|
77
77
|
const attribute = attributes
|
|
78
78
|
.slice(0, MAX_ATTRIBUTES_PER_TAG)
|
|
79
79
|
.find((candidate) => candidate.name.toLowerCase() === name);
|
|
@@ -84,7 +84,7 @@ function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LEN
|
|
|
84
84
|
}
|
|
85
85
|
return {
|
|
86
86
|
present: true,
|
|
87
|
-
value: attribute.value.trim().toLowerCase(),
|
|
87
|
+
value: lowercase ? attribute.value.trim().toLowerCase() : attribute.value.trim(),
|
|
88
88
|
truncated: attributes.length > MAX_ATTRIBUTES_PER_TAG,
|
|
89
89
|
};
|
|
90
90
|
}
|
|
@@ -116,8 +116,8 @@ function formMethod(attributes) {
|
|
|
116
116
|
truncated: method.truncated,
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
function formAction(attributes,
|
|
120
|
-
const action = attributeValue(attributes, 'action');
|
|
119
|
+
function formAction(attributes, resolutionBase, documentOrigin) {
|
|
120
|
+
const action = attributeValue(attributes, 'action', MAX_FORM_ATTRIBUTE_LENGTH, false);
|
|
121
121
|
if (!action.present && action.truncated) {
|
|
122
122
|
return { relationship: 'unclassified', cleartext: false, truncated: true };
|
|
123
123
|
}
|
|
@@ -127,16 +127,16 @@ function formAction(attributes, baseUrl) {
|
|
|
127
127
|
if (action.value === null) {
|
|
128
128
|
return { relationship: 'unclassified', cleartext: false, truncated: true };
|
|
129
129
|
}
|
|
130
|
-
if (!
|
|
130
|
+
if (!resolutionBase || !documentOrigin) {
|
|
131
131
|
return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
|
|
132
132
|
}
|
|
133
133
|
try {
|
|
134
|
-
const parsed = new URL(action.value,
|
|
134
|
+
const parsed = new URL(action.value, resolutionBase);
|
|
135
135
|
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
|
|
136
136
|
return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
|
|
137
137
|
}
|
|
138
138
|
return {
|
|
139
|
-
relationship: parsed.origin ===
|
|
139
|
+
relationship: parsed.origin === documentOrigin ? 'sameOrigin' : 'external',
|
|
140
140
|
cleartext: parsed.protocol === 'http:',
|
|
141
141
|
truncated: action.truncated,
|
|
142
142
|
};
|
|
@@ -238,7 +238,9 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
238
238
|
const supplied = typeof value === 'string' ? value : '';
|
|
239
239
|
const inputLimitReached = supplied.length > MAX_STATIC_HTML_CHARS;
|
|
240
240
|
const html = supplied.slice(0, MAX_STATIC_HTML_CHARS);
|
|
241
|
-
const
|
|
241
|
+
const documentUrl = safeBaseUrl(options.baseUrl);
|
|
242
|
+
let effectiveBaseUrl = documentUrl;
|
|
243
|
+
let baseHrefState = 'absent';
|
|
242
244
|
const includeVisibleText = options.includeVisibleText === true;
|
|
243
245
|
const markup = [];
|
|
244
246
|
const visibleTextParts = [];
|
|
@@ -316,6 +318,17 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
316
318
|
let scriptElementSeen = false;
|
|
317
319
|
let nonVisibleTextDepth = 0;
|
|
318
320
|
let visibleTextCharacters = 0;
|
|
321
|
+
let titleDepth = 0;
|
|
322
|
+
let titleCharacters = 0;
|
|
323
|
+
const titleParts = [];
|
|
324
|
+
const pendingFormActions = [];
|
|
325
|
+
function appendTitle(chars) {
|
|
326
|
+
if (titleDepth === 0 || !chars || titleCharacters >= 201)
|
|
327
|
+
return;
|
|
328
|
+
const retained = chars.slice(0, 201 - titleCharacters);
|
|
329
|
+
titleParts.push(retained);
|
|
330
|
+
titleCharacters += retained.length;
|
|
331
|
+
}
|
|
319
332
|
function appendInlineScript(chars) {
|
|
320
333
|
if (!activeInlineScript || !chars)
|
|
321
334
|
return;
|
|
@@ -374,6 +387,29 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
374
387
|
implicitHeadTextDepth += 1;
|
|
375
388
|
}
|
|
376
389
|
const inPublicationHead = insideExplicitHead || (!headScopeClosed && !bodyContentStarted && HEAD_CONTEXT_TAGS.has(tagName));
|
|
390
|
+
if (tagName === 'base' && inPublicationHead && baseHrefState === 'absent') {
|
|
391
|
+
const href = attributeValue(token.attrs, 'href', MAX_FORM_ATTRIBUTE_LENGTH, false);
|
|
392
|
+
if (href.truncated && !href.present) {
|
|
393
|
+
effectiveBaseUrl = documentUrl;
|
|
394
|
+
baseHrefState = 'invalid';
|
|
395
|
+
}
|
|
396
|
+
else if (href.present) {
|
|
397
|
+
try {
|
|
398
|
+
const parsed = href.value !== null && documentUrl ? new URL(href.value, documentUrl) : null;
|
|
399
|
+
if (!parsed || !['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
|
|
400
|
+
throw new Error('Invalid document base URL.');
|
|
401
|
+
}
|
|
402
|
+
effectiveBaseUrl = parsed;
|
|
403
|
+
baseHrefState = 'valid';
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
effectiveBaseUrl = documentUrl;
|
|
407
|
+
baseHrefState = 'invalid';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (tagName === 'title' && titleDepth === 0 && titleParts.length === 0)
|
|
412
|
+
titleDepth = 1;
|
|
377
413
|
if (/^h[1-6]$/u.test(tagName)) {
|
|
378
414
|
const key = tagName;
|
|
379
415
|
publicationMetadata.headings[key] += 1;
|
|
@@ -566,11 +602,8 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
566
602
|
forms.formsObserved += 1;
|
|
567
603
|
const method = formMethod(token.attrs);
|
|
568
604
|
forms.methods[method.value] += 1;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (action.cleartext)
|
|
572
|
-
forms.actions.cleartext += 1;
|
|
573
|
-
if (method.truncated || action.truncated)
|
|
605
|
+
pendingFormActions.push(token.attrs);
|
|
606
|
+
if (method.truncated || token.attrs.length > MAX_ATTRIBUTES_PER_TAG)
|
|
574
607
|
forms.truncated = true;
|
|
575
608
|
}
|
|
576
609
|
}
|
|
@@ -612,6 +645,8 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
612
645
|
const tagName = token.tagName.toLowerCase();
|
|
613
646
|
if (tagName === 'script')
|
|
614
647
|
activeInlineScript = null;
|
|
648
|
+
if (tagName === 'title' && titleDepth > 0)
|
|
649
|
+
titleDepth = 0;
|
|
615
650
|
if (tagName === 'head') {
|
|
616
651
|
insideExplicitHead = false;
|
|
617
652
|
headScopeClosed = true;
|
|
@@ -630,6 +665,7 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
630
665
|
headScopeClosed = true;
|
|
631
666
|
}
|
|
632
667
|
appendInlineScript(token.chars);
|
|
668
|
+
appendTitle(token.chars);
|
|
633
669
|
appendVisibleText(token.chars);
|
|
634
670
|
},
|
|
635
671
|
onNullCharacter(token) {
|
|
@@ -638,10 +674,12 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
638
674
|
headScopeClosed = true;
|
|
639
675
|
}
|
|
640
676
|
appendInlineScript(token.chars);
|
|
677
|
+
appendTitle(token.chars);
|
|
641
678
|
appendVisibleText(token.chars);
|
|
642
679
|
},
|
|
643
680
|
onWhitespaceCharacter(token) {
|
|
644
681
|
appendInlineScript(token.chars);
|
|
682
|
+
appendTitle(token.chars);
|
|
645
683
|
appendVisibleText(token.chars);
|
|
646
684
|
},
|
|
647
685
|
onComment() { },
|
|
@@ -650,6 +688,16 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
650
688
|
};
|
|
651
689
|
tokenizer = new Tokenizer({ sourceCodeLocationInfo: false }, handler);
|
|
652
690
|
tokenizer.write(html, true);
|
|
691
|
+
for (const attributes of pendingFormActions) {
|
|
692
|
+
const action = formAction(attributes, effectiveBaseUrl, documentUrl?.origin ?? null);
|
|
693
|
+
forms.actions[action.relationship] += 1;
|
|
694
|
+
if (action.cleartext)
|
|
695
|
+
forms.actions.cleartext += 1;
|
|
696
|
+
if (action.truncated)
|
|
697
|
+
forms.truncated = true;
|
|
698
|
+
}
|
|
699
|
+
if (baseHrefState === 'invalid')
|
|
700
|
+
forms.truncated = true;
|
|
653
701
|
publicationMetadata.truncated = publicationMetadata.truncated || inputLimitReached || publicationTagLimitReached;
|
|
654
702
|
publicationMetadata.documentTruncated = inputLimitReached || publicationTagLimitReached;
|
|
655
703
|
if (publicationMetadata.documentTruncated) {
|
|
@@ -671,6 +719,14 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
671
719
|
publicationMetadata.renderBlockingCandidates.total = publicationMetadata.renderBlockingCandidates.script
|
|
672
720
|
+ publicationMetadata.renderBlockingCandidates.stylesheet;
|
|
673
721
|
return {
|
|
722
|
+
title: (() => {
|
|
723
|
+
const value = titleParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\ufffd/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
724
|
+
if (!value)
|
|
725
|
+
return null;
|
|
726
|
+
return value.length > 200 ? `${value.slice(0, 200)}…` : value;
|
|
727
|
+
})(),
|
|
728
|
+
effectiveBaseUrl: effectiveBaseUrl?.toString() ?? null,
|
|
729
|
+
baseHrefState,
|
|
674
730
|
markup: markup.join('\n'),
|
|
675
731
|
visibleText: visibleTextParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\s+/gu, ' '),
|
|
676
732
|
structureTokens,
|
|
@@ -9,6 +9,23 @@ import { createObservation } from "../packages/evidence/observation.mjs";
|
|
|
9
9
|
import { MAX_STATIC_HTML_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
10
10
|
import { TECHNOLOGY_EVIDENCE_ROLE_ORDER, } from "./technology-evidence-role.mjs";
|
|
11
11
|
import { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, TECHNOLOGY_PROFILE_VERSION, } from "./lookup-child-profile-contract.mjs";
|
|
12
|
+
const TECHNOLOGY_CATEGORIES = Object.freeze([
|
|
13
|
+
'application runtime',
|
|
14
|
+
'content management',
|
|
15
|
+
'commerce',
|
|
16
|
+
'site builder',
|
|
17
|
+
'web framework',
|
|
18
|
+
'static site generator',
|
|
19
|
+
'web server',
|
|
20
|
+
'delivery platform',
|
|
21
|
+
]);
|
|
22
|
+
const TECHNOLOGY_EVIDENCE_SOURCES = Object.freeze([
|
|
23
|
+
'generator metadata',
|
|
24
|
+
'static HTML',
|
|
25
|
+
'resource origin',
|
|
26
|
+
'HTTP server header',
|
|
27
|
+
'passive response header',
|
|
28
|
+
]);
|
|
12
29
|
const MAX_TECHNOLOGY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
13
30
|
const MAX_TECHNOLOGY_TAG_LENGTH = MAX_TAG_LENGTH;
|
|
14
31
|
const MAX_RESOURCE_ORIGINS = 30;
|
|
@@ -38,6 +55,29 @@ function normalizedResourceHosts(value) {
|
|
|
38
55
|
}
|
|
39
56
|
return hosts;
|
|
40
57
|
}
|
|
58
|
+
function applicationMarkup(markup, resourceHosts, effectiveBaseUrl, documentOrigin) {
|
|
59
|
+
let resolutionBase;
|
|
60
|
+
try {
|
|
61
|
+
resolutionBase = new URL(typeof effectiveBaseUrl === 'string' ? effectiveBaseUrl : 'https://document.invalid/');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
resolutionBase = new URL('https://document.invalid/');
|
|
65
|
+
}
|
|
66
|
+
const comparedOrigin = typeof documentOrigin === 'string' && /^https?:\/\//iu.test(documentOrigin)
|
|
67
|
+
? documentOrigin
|
|
68
|
+
: null;
|
|
69
|
+
return markup.replace(/\s(src|href|poster|data)="([^"]*)"/giu, (attribute, name, value) => {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(value, resolutionBase);
|
|
72
|
+
const knownExternalHost = resourceHosts.has(parsed.hostname.toLowerCase());
|
|
73
|
+
const offOrigin = comparedOrigin ? parsed.origin !== comparedOrigin : knownExternalHost;
|
|
74
|
+
return knownExternalHost && offOrigin ? ` ${name.toLowerCase()}=""` : attribute;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return attribute;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
41
81
|
const PASSIVE_TECHNOLOGY_HEADER_NAMES = Object.freeze([
|
|
42
82
|
'cf-ray',
|
|
43
83
|
'x-drupal-cache',
|
|
@@ -106,6 +146,17 @@ function htmlEvidence(markers, description, confidence = 'high') {
|
|
|
106
146
|
matches: ({ html }) => markers.some((marker) => html.includes(marker)),
|
|
107
147
|
};
|
|
108
148
|
}
|
|
149
|
+
function resourcePathEvidence(markers, description, confidence = 'high') {
|
|
150
|
+
return {
|
|
151
|
+
source: 'static HTML',
|
|
152
|
+
description,
|
|
153
|
+
confidence,
|
|
154
|
+
matches: ({ html }) => markers.some((marker) => html.includes(marker)),
|
|
155
|
+
roleFor: ({ applicationHtml }) => markers.some((marker) => applicationHtml.includes(marker))
|
|
156
|
+
? undefined
|
|
157
|
+
: 'embedded_dependency',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
109
160
|
function resourceEvidence(hosts, description) {
|
|
110
161
|
return {
|
|
111
162
|
source: 'resource origin',
|
|
@@ -142,7 +193,10 @@ function responseHeaderEvidence(name, pattern, description, confidence = 'high')
|
|
|
142
193
|
},
|
|
143
194
|
};
|
|
144
195
|
}
|
|
145
|
-
function evidenceRole(signature, evidence) {
|
|
196
|
+
function evidenceRole(signature, evidence, context) {
|
|
197
|
+
const contextualRole = context ? evidence.roleFor?.(context) : undefined;
|
|
198
|
+
if (contextualRole)
|
|
199
|
+
return contextualRole;
|
|
146
200
|
if (evidence.role)
|
|
147
201
|
return evidence.role;
|
|
148
202
|
if (evidence.source === 'resource origin')
|
|
@@ -159,7 +213,7 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
159
213
|
id: 'wordpress', name: 'WordPress', category: 'content management',
|
|
160
214
|
evidence: [
|
|
161
215
|
generatorEvidence(/^wordpress(?:\s|$)/i, 'Generator metadata identifies WordPress.'),
|
|
162
|
-
|
|
216
|
+
resourcePathEvidence(['/wp-content/', '/wp-includes/'], 'Static resource paths use WordPress conventions.', 'medium'),
|
|
163
217
|
],
|
|
164
218
|
},
|
|
165
219
|
{
|
|
@@ -222,8 +276,9 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
222
276
|
{
|
|
223
277
|
id: 'bigcommerce', name: 'BigCommerce', category: 'commerce',
|
|
224
278
|
minimumEvidenceMatches: 2,
|
|
279
|
+
allowEmbeddedOnly: true,
|
|
225
280
|
evidence: [
|
|
226
|
-
|
|
281
|
+
resourcePathEvidence(['cdn11.bigcommerce.com/s-', 'stencil-utils'], 'Static markup contains BigCommerce storefront asset markers.', 'medium'),
|
|
227
282
|
resourcePatternEvidence(/^cdn\d+\.bigcommerce\.com$/i, 'A retained resource origin uses BigCommerce storefront delivery infrastructure.'),
|
|
228
283
|
],
|
|
229
284
|
},
|
|
@@ -421,28 +476,41 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
421
476
|
}) : null;
|
|
422
477
|
const context = {
|
|
423
478
|
html: htmlAnalysis.markup,
|
|
479
|
+
applicationHtml: '',
|
|
424
480
|
generator: boundedLowercase(input.generator, MAX_GENERATOR_INPUT),
|
|
425
481
|
httpServer: boundedLowercase(input.httpServer, MAX_SERVER_INPUT),
|
|
426
482
|
resourceHosts: normalizedResourceHosts(input.resourceOrigins),
|
|
427
483
|
responseHeaders: normalizedResponseHeaders(input.responseHeaders),
|
|
428
484
|
};
|
|
485
|
+
context.applicationHtml = applicationMarkup(context.html, context.resourceHosts, input.effectiveBaseUrl, input.documentOrigin);
|
|
429
486
|
const findings = [];
|
|
430
487
|
for (const signature of TECHNOLOGY_SIGNATURES) {
|
|
431
|
-
const matched = signature.evidence
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
488
|
+
const matched = signature.evidence
|
|
489
|
+
.filter((evidence) => evidence.matches(context))
|
|
490
|
+
.map((evidence) => ({ evidence, role: evidenceRole(signature, evidence, context) }));
|
|
491
|
+
const nonEmbeddedMatches = matched.filter((entry) => entry.role !== 'embedded_dependency');
|
|
492
|
+
const minimumEvidenceMatches = signature.minimumEvidenceMatches || 1;
|
|
493
|
+
const qualifiesForPlatform = matched.length >= minimumEvidenceMatches
|
|
494
|
+
&& (minimumEvidenceMatches === 1 || nonEmbeddedMatches.length > 0)
|
|
495
|
+
&& (!signature.requiresNonResourceEvidence || nonEmbeddedMatches.length > 0);
|
|
496
|
+
const contextualEmbeddedEvidence = matched.filter((entry) => (entry.role === 'embedded_dependency' && entry.evidence.roleFor !== undefined));
|
|
497
|
+
const selected = qualifiesForPlatform
|
|
498
|
+
? matched
|
|
499
|
+
: signature.allowEmbeddedOnly
|
|
500
|
+
? contextualEmbeddedEvidence.slice(0, 1)
|
|
501
|
+
: [];
|
|
502
|
+
if (selected.length === 0)
|
|
435
503
|
continue;
|
|
436
|
-
const matchedEvidence =
|
|
504
|
+
const matchedEvidence = selected.slice(0, MAX_EVIDENCE_PER_TECHNOLOGY).map(({ evidence, role }) => ({
|
|
437
505
|
source: evidence.source,
|
|
438
|
-
role
|
|
506
|
+
role,
|
|
439
507
|
description: evidence.description,
|
|
440
508
|
}));
|
|
441
509
|
findings.push({
|
|
442
510
|
id: signature.id,
|
|
443
511
|
name: signature.name,
|
|
444
512
|
category: signature.category,
|
|
445
|
-
confidence:
|
|
513
|
+
confidence: selected.some(({ evidence }) => evidence.confidence === 'high') ? 'high' : 'medium',
|
|
446
514
|
roles: TECHNOLOGY_EVIDENCE_ROLE_ORDER.filter((role) => matchedEvidence.some((evidence) => evidence.role === role)),
|
|
447
515
|
evidence: matchedEvidence,
|
|
448
516
|
});
|
|
@@ -493,4 +561,4 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
493
561
|
browserLibraryProfile,
|
|
494
562
|
};
|
|
495
563
|
}
|
|
496
|
-
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, PASSIVE_TECHNOLOGY_HEADER_NAMES, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, captureTechnologyResponseHeaders, };
|
|
564
|
+
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, PASSIVE_TECHNOLOGY_HEADER_NAMES, TECHNOLOGY_CATEGORIES, TECHNOLOGY_EVIDENCE_SOURCES, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, captureTechnologyResponseHeaders, };
|
|
@@ -142,7 +142,11 @@ export function normaliseRdata(type, rawValue, origin, nameMode = 'absolute') {
|
|
|
142
142
|
const family = type === 'A' ? 4 : 6;
|
|
143
143
|
if (tokens.length !== 1 || isIP(tokens[0] ?? '') !== family)
|
|
144
144
|
throw new TypeError(`${type} data must contain one valid address.`);
|
|
145
|
-
|
|
145
|
+
const supplied = tokens[0];
|
|
146
|
+
const normalised = family === 4
|
|
147
|
+
? supplied.split('.').map((part) => String(Number(part))).join('.')
|
|
148
|
+
: new URL(`http://[${supplied}]/`).hostname.slice(1, -1).toLowerCase();
|
|
149
|
+
return { value: normalised, valueTreatment: 'normalised' };
|
|
146
150
|
}
|
|
147
151
|
if (type === 'CNAME' || type === 'NS') {
|
|
148
152
|
if (tokens.length !== 1)
|
|
@@ -152,7 +156,12 @@ export function normaliseRdata(type, rawValue, origin, nameMode = 'absolute') {
|
|
|
152
156
|
if (type === 'MX') {
|
|
153
157
|
if (tokens.length !== 2)
|
|
154
158
|
throw new TypeError('MX data must contain preference and exchange.');
|
|
155
|
-
|
|
159
|
+
const preference = integerToken(tokens[0], 0, 65_535, 'MX preference');
|
|
160
|
+
const exchangeToken = tokens[1];
|
|
161
|
+
if (exchangeToken === '.' && preference !== 0)
|
|
162
|
+
throw new TypeError('A Null MX exchange must use preference 0.');
|
|
163
|
+
const exchange = exchangeToken === '.' ? '.' : host(exchangeToken, 'MX exchange');
|
|
164
|
+
return { value: `${preference} ${exchange}`, valueTreatment: 'normalised' };
|
|
156
165
|
}
|
|
157
166
|
if (type === 'CAA') {
|
|
158
167
|
if (tokens.length < 3)
|
|
@@ -380,7 +389,6 @@ function parseBindZone(zoneText, initialOrigin) {
|
|
|
380
389
|
if (tokens.length !== 2)
|
|
381
390
|
throw new TypeError('$ORIGIN requires one domain name.');
|
|
382
391
|
origin = domainName(tokens[1], origin, '$ORIGIN', 'master-file');
|
|
383
|
-
lastOwner = origin;
|
|
384
392
|
continue;
|
|
385
393
|
}
|
|
386
394
|
if (directive === '$TTL')
|
|
@@ -388,10 +396,12 @@ function parseBindZone(zoneText, initialOrigin) {
|
|
|
388
396
|
if (directive?.startsWith('$'))
|
|
389
397
|
throw new TypeError(`${directive} is deliberately unsupported.`);
|
|
390
398
|
let offset = 0;
|
|
391
|
-
const ownerToken = entry.ownerOmitted ?
|
|
392
|
-
if (!ownerToken)
|
|
399
|
+
const ownerToken = entry.ownerOmitted ? null : tokens[offset++];
|
|
400
|
+
if (!entry.ownerOmitted && !ownerToken)
|
|
393
401
|
throw new TypeError('Record owner is missing.');
|
|
394
|
-
const owner =
|
|
402
|
+
const owner = entry.ownerOmitted
|
|
403
|
+
? lastOwner
|
|
404
|
+
: ownerName(ownerToken, origin, 'record owner', 'master-file');
|
|
395
405
|
lastOwner = owner;
|
|
396
406
|
let ttl = null;
|
|
397
407
|
if (/^\d+$/u.test(tokens[offset] ?? ''))
|
package/package.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, MAX_CASES, MAX_NOTES_PER_CASE, deterministicId, caseTimestampOrNull, normalizeCase, normalizeDomain, normalizeEvidenceHistory, normalizeNotes, normalizeReviewReasonCode, normalizeTags, objectRecord, safeId, isValidDisposition, isValidSource, isValidStatus, } from "./case-record-model.mjs";
|
|
2
2
|
import { CASE_REPORT_SCHEMA, PUBLIC_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, } from "../contracts/case-portability.mjs";
|
|
3
3
|
import { canonicalArtifactJsonV2 } from "../evidence/artifact-integrity.mjs";
|
|
4
4
|
import { assertBoundedJsonStructure } from "../../lib/bounded-json.mjs";
|
|
5
5
|
import { inspectCaseBrandProfileIds, unionCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
|
|
6
6
|
import { caseInvestigationBranchReferences, mergeCaseInvestigationBranches, normalizeCaseInvestigationBranches, } from "./case-investigation-branch-model.mjs";
|
|
7
|
-
import { buildCaseClosureLinkContext, mergeCaseActions,
|
|
8
|
-
const STATUS_VALUES = new Set(CASE_STATUSES.map((item) => item.value));
|
|
9
|
-
const DISPOSITION_VALUES = new Set(CASE_DISPOSITIONS.map((item) => item.value));
|
|
10
|
-
const SOURCE_VALUES = new Set(CASE_SOURCES.map((item) => item.value));
|
|
7
|
+
import { buildCaseClosureLinkContext, mergeCaseActions, mergeCaseClosureHistories, mergeCaseObservedEffectHistories, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_CASE_MANUAL_TRAIL_EVENTS, MAX_CASE_SIGHTINGS, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, } from "./case-response-model.mjs";
|
|
11
8
|
export const MAX_CASE_INPUT_RECORDS = 2_000;
|
|
12
9
|
function compareCodeUnits(left, right) {
|
|
13
10
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -51,7 +48,7 @@ export function normalizeCaseStore(raw) {
|
|
|
51
48
|
throw new TypeError(`Unversioned Case stores are retired. Export or reset them explicitly before using Case schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
52
49
|
}
|
|
53
50
|
if (!CASE_IMPORT_VERSIONS.includes(sourceVersion) && sourceVersion < CASE_SCHEMA_VERSION) {
|
|
54
|
-
throw new TypeError(`Case schema ${sourceVersion} is not part of the supported compatibility boundary. Exact published schemas
|
|
51
|
+
throw new TypeError(`Case schema ${sourceVersion} is not part of the supported compatibility boundary. Exact published schemas ${CASE_IMPORT_VERSIONS.filter((version) => version < CASE_SCHEMA_VERSION).join(', ')} migrate to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
55
52
|
}
|
|
56
53
|
if (sourceVersion > CASE_SCHEMA_VERSION) {
|
|
57
54
|
throw new TypeError(`Case schema ${sourceVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
@@ -92,6 +89,17 @@ function assertCurrentCaseShape(raw) {
|
|
|
92
89
|
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'routeObservedAt')) {
|
|
93
90
|
throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} response actions must declare their route observation time, including null; unreleased local Case checkpoints are not interpreted as the current format and no data was changed.`);
|
|
94
91
|
}
|
|
92
|
+
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'routeReviewAfter')) {
|
|
93
|
+
throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} response actions must declare their route review deadline, including null; unreleased local Case checkpoints are not interpreted as the current format and no data was changed.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const decisions = itemRecord.decisions;
|
|
97
|
+
for (const decision of Array.isArray(decisions) ? decisions : []) {
|
|
98
|
+
const record = objectRecord(decision);
|
|
99
|
+
if (Object.keys(record).length > 0
|
|
100
|
+
&& (!Object.hasOwn(record, 'confidence') || !Object.hasOwn(record, 'confidenceBasis'))) {
|
|
101
|
+
throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} analyst decisions must declare confidence and its basis; unreleased local Case checkpoints are not interpreted as the current format and no data was changed.`);
|
|
102
|
+
}
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
}
|
|
@@ -140,9 +148,6 @@ function boundedCaseList(raw) {
|
|
|
140
148
|
* @param {string} [nowIso]
|
|
141
149
|
* @returns {CaseRecord}
|
|
142
150
|
*/
|
|
143
|
-
function importScalar(value, valid) {
|
|
144
|
-
return typeof value === 'string' && valid.has(value) ? value : undefined;
|
|
145
|
-
}
|
|
146
151
|
/**
|
|
147
152
|
* Validates one imported record into a patch. Unlike normalizeCase, absent or
|
|
148
153
|
* invalid scalar fields stay `undefined` (never defaulted) and a missing/invalid
|
|
@@ -174,6 +179,8 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
174
179
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
175
180
|
const actions = normalizeCaseActions(record.actions, normalizedFallback, { ...timestampOptions, validEvidencePinIds: pinIds });
|
|
176
181
|
const assertions = normalizeCaseAssertions(record.assertions, normalizedFallback, pinIds, timestampOptions);
|
|
182
|
+
const decisions = normalizeCaseDecisions(record.decisions, normalizedFallback, pinIds, timestampOptions);
|
|
183
|
+
const manualTrail = normalizeCaseManualTrail(record.manualTrail, normalizedFallback, timestampOptions);
|
|
177
184
|
const sightings = normalizeCaseSightings(record.sightings, normalizedFallback, pinIds, timestampOptions);
|
|
178
185
|
const observedEffects = normalizeCaseObservedEffectHistory(importedVersion >= 13 ? record.observedEffects : undefined, normalizedFallback, pinIds, new Set(sightings.map((item) => item.id)), timestampOptions);
|
|
179
186
|
const closures = normalizeCaseClosureHistory(importedVersion >= 13 ? record.closures : undefined, normalizedFallback, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
|
|
@@ -181,17 +188,26 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
181
188
|
const brandProfileReferences = importedVersion >= 12
|
|
182
189
|
? inspectCaseBrandProfileIds(record.brandProfileIds)
|
|
183
190
|
: { ids: [], omitted: 0 };
|
|
191
|
+
const authoredHistoryOmitted = [
|
|
192
|
+
[record.evidencePins, evidencePins],
|
|
193
|
+
[record.decisions, decisions],
|
|
194
|
+
[record.assertions, assertions],
|
|
195
|
+
[record.manualTrail, manualTrail],
|
|
196
|
+
[record.sightings, sightings],
|
|
197
|
+
].reduce((total, [candidates, retained]) => total
|
|
198
|
+
+ Math.max(0, (Array.isArray(candidates) ? candidates.length : 0) - retained.length), 0);
|
|
184
199
|
return {
|
|
185
200
|
domain,
|
|
186
201
|
rawId: typeof record.id === 'string' ? record.id : null,
|
|
187
|
-
status:
|
|
188
|
-
disposition:
|
|
202
|
+
status: isValidStatus(record.status) ? record.status : undefined,
|
|
203
|
+
disposition: isValidDisposition(record.disposition) ? record.disposition : undefined,
|
|
189
204
|
reviewReasonCode: Object.hasOwn(record, 'reviewReasonCode')
|
|
190
205
|
? normalizeReviewReasonCode(record.reviewReasonCode)
|
|
191
206
|
: undefined,
|
|
192
207
|
brandProfileIds: brandProfileReferences.ids,
|
|
193
208
|
brandProfileReferencesOmitted: brandProfileReferences.omitted,
|
|
194
|
-
|
|
209
|
+
authoredHistoryOmitted,
|
|
210
|
+
source: isValidSource(record.source) ? record.source : undefined,
|
|
195
211
|
evidenceHistory: normalizeEvidenceHistory(rawEvidence, {
|
|
196
212
|
source: 'import',
|
|
197
213
|
fallback: importFallback,
|
|
@@ -199,10 +215,10 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
199
215
|
caseDomain: domain,
|
|
200
216
|
}),
|
|
201
217
|
evidencePins,
|
|
202
|
-
decisions
|
|
218
|
+
decisions,
|
|
203
219
|
actions,
|
|
204
220
|
assertions,
|
|
205
|
-
manualTrail
|
|
221
|
+
manualTrail,
|
|
206
222
|
sightings,
|
|
207
223
|
observedEffects,
|
|
208
224
|
closures,
|
|
@@ -229,6 +245,22 @@ function unionNotes(a, b) {
|
|
|
229
245
|
const notes = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
|
|
230
246
|
return notes.slice(Math.max(0, notes.length - MAX_NOTES_PER_CASE));
|
|
231
247
|
}
|
|
248
|
+
function retainLocalAuthoredRecords(local, imported, maximum) {
|
|
249
|
+
const records = [...local];
|
|
250
|
+
const retainedIds = new Set(records.map((item) => item.id));
|
|
251
|
+
let omitted = 0;
|
|
252
|
+
for (const item of imported) {
|
|
253
|
+
if (retainedIds.has(item.id))
|
|
254
|
+
continue;
|
|
255
|
+
if (records.length >= maximum) {
|
|
256
|
+
omitted += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
records.push(item);
|
|
260
|
+
retainedIds.add(item.id);
|
|
261
|
+
}
|
|
262
|
+
return { records, omitted };
|
|
263
|
+
}
|
|
232
264
|
// Additive, deduplicated union of two evidence histories. Identical material
|
|
233
265
|
// collapses (earliest firstCapturedAt, latest capturedAt), distinct snapshots
|
|
234
266
|
// are retained subject to the per-case bound, and an older import can never
|
|
@@ -274,16 +306,29 @@ function caseFromPatch(patch, now) {
|
|
|
274
306
|
function applyImportPatch(local, patch) {
|
|
275
307
|
const importNewer = patch.updatedAt !== null && Date.parse(patch.updatedAt) > Date.parse(local.updatedAt);
|
|
276
308
|
const fallback = patch.updatedAt || local.updatedAt;
|
|
277
|
-
const
|
|
309
|
+
const pinSelection = retainLocalAuthoredRecords(local.evidencePins, patch.evidencePins, MAX_CASE_EVIDENCE_PINS);
|
|
310
|
+
const evidencePins = normalizeCaseEvidencePins(pinSelection.records, fallback);
|
|
278
311
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
279
312
|
const actions = mergeCaseActions(local.actions, patch.actions, fallback, pinIds);
|
|
280
|
-
const
|
|
313
|
+
const assertionSelection = retainLocalAuthoredRecords(local.assertions, patch.assertions, MAX_CASE_ASSERTIONS);
|
|
314
|
+
const assertions = normalizeCaseAssertions(assertionSelection.records, fallback, pinIds);
|
|
281
315
|
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
282
|
-
const
|
|
316
|
+
const sightingSelection = retainLocalAuthoredRecords(local.sightings, patch.sightings, MAX_CASE_SIGHTINGS);
|
|
317
|
+
const sightings = normalizeCaseSightings(sightingSelection.records, fallback, pinIds);
|
|
283
318
|
const sightingIds = new Set(sightings.map((item) => item.id));
|
|
284
319
|
const observedEffects = mergeCaseObservedEffectHistories(local.observedEffects, patch.observedEffects, fallback, pinIds, sightingIds);
|
|
285
320
|
const closures = mergeCaseClosureHistories(local.closures, patch.closures, fallback, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), buildCaseClosureLinkContext(observedEffects, actions));
|
|
286
321
|
const brandProfileReferences = unionCaseBrandProfileIds(local.brandProfileIds, patch.brandProfileIds);
|
|
322
|
+
const decisionSelection = retainLocalAuthoredRecords(local.decisions, patch.decisions, MAX_CASE_DECISIONS);
|
|
323
|
+
const decisions = normalizeCaseDecisions(decisionSelection.records, fallback, pinIds);
|
|
324
|
+
const trailSelection = retainLocalAuthoredRecords(local.manualTrail, patch.manualTrail, MAX_CASE_MANUAL_TRAIL_EVENTS);
|
|
325
|
+
const manualTrail = normalizeCaseManualTrail(trailSelection.records, fallback);
|
|
326
|
+
const authoredHistoryOmitted = patch.authoredHistoryOmitted
|
|
327
|
+
+ pinSelection.omitted
|
|
328
|
+
+ decisionSelection.omitted
|
|
329
|
+
+ assertionSelection.omitted
|
|
330
|
+
+ trailSelection.omitted
|
|
331
|
+
+ sightingSelection.omitted;
|
|
287
332
|
return { record: {
|
|
288
333
|
...local,
|
|
289
334
|
status: patch.status !== undefined && importNewer ? patch.status : local.status,
|
|
@@ -293,10 +338,10 @@ function applyImportPatch(local, patch) {
|
|
|
293
338
|
source: patch.source !== undefined && importNewer ? patch.source : local.source,
|
|
294
339
|
evidenceHistory: mergeEvidenceHistories(local.evidenceHistory, patch.evidenceHistory, local.domain),
|
|
295
340
|
evidencePins,
|
|
296
|
-
decisions
|
|
341
|
+
decisions,
|
|
297
342
|
actions,
|
|
298
343
|
assertions,
|
|
299
|
-
manualTrail
|
|
344
|
+
manualTrail,
|
|
300
345
|
sightings,
|
|
301
346
|
observedEffects,
|
|
302
347
|
closures,
|
|
@@ -305,7 +350,7 @@ function applyImportPatch(local, patch) {
|
|
|
305
350
|
notes: unionNotes(local.notes, patch.notes),
|
|
306
351
|
createdAt: patch.createdAt && Date.parse(patch.createdAt) < Date.parse(local.createdAt) ? patch.createdAt : local.createdAt,
|
|
307
352
|
updatedAt: importNewer ? (patch.updatedAt ?? local.updatedAt) : local.updatedAt,
|
|
308
|
-
}, brandProfileReferencesOmitted: brandProfileReferences.omitted };
|
|
353
|
+
}, brandProfileReferencesOmitted: brandProfileReferences.omitted, authoredHistoryOmitted };
|
|
309
354
|
}
|
|
310
355
|
function pickFreeId(preferred, domain, used) {
|
|
311
356
|
const wanted = safeId(preferred);
|
|
@@ -418,7 +463,7 @@ function caseCollectionImportEnvelope(importedRaw) {
|
|
|
418
463
|
* reinterpreted.
|
|
419
464
|
* @param {CaseRecord[]} localCases
|
|
420
465
|
* @param {unknown} importedRaw
|
|
421
|
-
* @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number }}
|
|
466
|
+
* @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number, authoredHistoryOmitted: number }}
|
|
422
467
|
*/
|
|
423
468
|
export function mergeCases(localCases, importedRaw) {
|
|
424
469
|
assertBoundedJsonStructure(importedRaw, 'Case import', CASE_INPUT_JSON_LIMITS);
|
|
@@ -426,7 +471,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
426
471
|
const importedVersion = parseStoreVersion(importedEnvelope);
|
|
427
472
|
if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion < CASE_SCHEMA_VERSION
|
|
428
473
|
&& !CASE_IMPORT_VERSIONS.includes(importedVersion)) {
|
|
429
|
-
throw new Error(`Case schema ${importedVersion} is not part of the supported compatibility boundary. Exact published schemas
|
|
474
|
+
throw new Error(`Case schema ${importedVersion} is not part of the supported compatibility boundary. Exact published schemas ${CASE_IMPORT_VERSIONS.filter((version) => version < CASE_SCHEMA_VERSION).join(', ')} migrate to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
430
475
|
}
|
|
431
476
|
if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion > CASE_SCHEMA_VERSION) {
|
|
432
477
|
throw new Error(`Case schema ${importedVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
@@ -446,6 +491,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
446
491
|
const imported = boundedCaseList(importedEnvelope);
|
|
447
492
|
let skipped = imported.omitted;
|
|
448
493
|
let brandProfileReferencesOmitted = 0;
|
|
494
|
+
let authoredHistoryOmitted = 0;
|
|
449
495
|
const fallback = new Date(0).toISOString();
|
|
450
496
|
for (const item of imported.items) {
|
|
451
497
|
const patch = extractImportPatch(item, supportedImportedVersion);
|
|
@@ -458,6 +504,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
458
504
|
const merged = applyImportPatch(existing, patch);
|
|
459
505
|
byDomain.set(patch.domain, merged.record);
|
|
460
506
|
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted + merged.brandProfileReferencesOmitted;
|
|
507
|
+
authoredHistoryOmitted += merged.authoredHistoryOmitted;
|
|
461
508
|
updated += 1;
|
|
462
509
|
}
|
|
463
510
|
else if (byDomain.size < MAX_CASES) {
|
|
@@ -466,6 +513,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
466
513
|
usedIds.add(record.id);
|
|
467
514
|
byDomain.set(patch.domain, record);
|
|
468
515
|
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted;
|
|
516
|
+
authoredHistoryOmitted += patch.authoredHistoryOmitted;
|
|
469
517
|
added += 1;
|
|
470
518
|
}
|
|
471
519
|
else {
|
|
@@ -478,5 +526,6 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
478
526
|
updated,
|
|
479
527
|
skipped,
|
|
480
528
|
brandProfileReferencesOmitted,
|
|
529
|
+
authoredHistoryOmitted,
|
|
481
530
|
};
|
|
482
531
|
}
|