@slicedearth/whoisleuth-cli 2.0.1 → 2.1.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/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +16 -9
- package/docs/cli.md +4 -2
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +46 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +2 -2
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +225 -61
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
package/package.json
CHANGED
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"bugs": {
|
|
34
34
|
"url": "https://github.com/slicedearth/whoisleuth/issues"
|
|
35
35
|
},
|
|
36
|
-
"version": "2.0
|
|
36
|
+
"version": "2.1.0",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@peculiar/x509": "2.0.0",
|
|
39
39
|
"maxmind": "5.0.7",
|
|
40
40
|
"parse5": "8.0.1",
|
|
41
41
|
"reflect-metadata": "0.2.2",
|
|
42
42
|
"tldts": "7.4.11",
|
|
43
|
-
"undici": "8.10.
|
|
43
|
+
"undici": "8.10.1"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"bin/**/*.mjs",
|
|
@@ -413,6 +413,15 @@ export function latestCaseEvidence(record) {
|
|
|
413
413
|
const history = record && Array.isArray(record.evidenceHistory) ? record.evidenceHistory : [];
|
|
414
414
|
return history.at(-1) ?? null;
|
|
415
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Returns the exact hostname from the latest retained observation when it is
|
|
418
|
+
* available. The registrable Case identity is the conservative fallback; an
|
|
419
|
+
* older hostname is never substituted for a newer observation with unknown
|
|
420
|
+
* hostname context.
|
|
421
|
+
*/
|
|
422
|
+
export function caseLookupTarget(record) {
|
|
423
|
+
return latestCaseEvidence(record)?.inputHostname || record.domain;
|
|
424
|
+
}
|
|
416
425
|
// ---------------------------------------------------------------------------
|
|
417
426
|
// Material-change comparison (pure; no Svelte, DOM, or persistence access)
|
|
418
427
|
// ---------------------------------------------------------------------------
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CASE_DISPOSITIONS, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, MAX_CASES, MAX_NOTES_PER_CASE, deterministicId, caseTimestampOrNull, normalizeCase, normalizeDomain, normalizeEvidenceHistory, normalizeNotes, normalizeReviewReasonCode, normalizeTags, objectRecord, safeId, } from "./case-record-model.mjs";
|
|
2
|
-
import { CASE_REPORT_SCHEMA, 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";
|
|
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";
|
|
@@ -51,13 +51,13 @@ export function normalizeCaseStore(raw) {
|
|
|
51
51
|
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
52
|
}
|
|
53
53
|
if (!CASE_IMPORT_VERSIONS.includes(sourceVersion) && sourceVersion < CASE_SCHEMA_VERSION) {
|
|
54
|
-
throw new TypeError(`Case schema ${sourceVersion} is not part of the
|
|
54
|
+
throw new TypeError(`Case schema ${sourceVersion} is not part of the supported compatibility boundary. Exact published schemas 12 and 13 migrate to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
55
55
|
}
|
|
56
56
|
if (sourceVersion > CASE_SCHEMA_VERSION) {
|
|
57
57
|
throw new TypeError(`Case schema ${sourceVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
58
58
|
}
|
|
59
59
|
if (sourceVersion === CASE_SCHEMA_VERSION) {
|
|
60
|
-
|
|
60
|
+
assertCurrentCaseShape(raw);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
const byDomain = new Map();
|
|
@@ -76,17 +76,23 @@ export function normalizeCaseStore(raw) {
|
|
|
76
76
|
assignUniqueIds(cases);
|
|
77
77
|
return { version: CASE_SCHEMA_VERSION, cases };
|
|
78
78
|
}
|
|
79
|
-
function
|
|
79
|
+
function assertCurrentCaseShape(raw) {
|
|
80
80
|
for (const item of boundedCaseList(raw).items) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const snapshot of evidenceHistory) {
|
|
81
|
+
const itemRecord = objectRecord(item);
|
|
82
|
+
const evidenceHistory = itemRecord.evidenceHistory;
|
|
83
|
+
for (const snapshot of Array.isArray(evidenceHistory) ? evidenceHistory : []) {
|
|
85
84
|
const record = objectRecord(snapshot);
|
|
86
85
|
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'inputHostname')) {
|
|
87
86
|
throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} evidence snapshots must declare their exact submitted hostname, including null; unreleased local Case checkpoints are not interpreted as the v2 format and no data was changed.`);
|
|
88
87
|
}
|
|
89
88
|
}
|
|
89
|
+
const actions = itemRecord.actions;
|
|
90
|
+
for (const action of Array.isArray(actions) ? actions : []) {
|
|
91
|
+
const record = objectRecord(action);
|
|
92
|
+
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'routeObservedAt')) {
|
|
93
|
+
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
|
+
}
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
/**
|
|
@@ -388,7 +394,7 @@ function caseCollectionImportEnvelope(importedRaw) {
|
|
|
388
394
|
}
|
|
389
395
|
for (const reportValue of reports) {
|
|
390
396
|
const report = objectRecord(reportValue);
|
|
391
|
-
assertOnlyEnvelopeKeys(report, rootVersion ===
|
|
397
|
+
assertOnlyEnvelopeKeys(report, rootVersion === PUBLIC_CASE_SCHEMA_VERSION ? CLI_CASE_PACK_PUBLIC_REPORT_KEYS : CLI_CASE_PACK_REPORT_KEYS, 'CLI Case-pack report');
|
|
392
398
|
if (report.schema !== CASE_REPORT_SCHEMA
|
|
393
399
|
|| !caseReportVersionMatchesCase(rootVersion, report.schemaVersion)
|
|
394
400
|
|| report.generatedAt !== root.exportedAt) {
|
|
@@ -420,7 +426,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
420
426
|
const importedVersion = parseStoreVersion(importedEnvelope);
|
|
421
427
|
if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion < CASE_SCHEMA_VERSION
|
|
422
428
|
&& !CASE_IMPORT_VERSIONS.includes(importedVersion)) {
|
|
423
|
-
throw new Error(`Case schema ${importedVersion} is not part of the
|
|
429
|
+
throw new Error(`Case schema ${importedVersion} is not part of the supported compatibility boundary. Exact published schemas 12 and 13 migrate to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
424
430
|
}
|
|
425
431
|
if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion > CASE_SCHEMA_VERSION) {
|
|
426
432
|
throw new Error(`Case schema ${importedVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
@@ -430,7 +436,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
430
436
|
throw new Error(`Expected a well-formed WHOISleuth Case export using schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
431
437
|
}
|
|
432
438
|
if (importedVersion === CASE_SCHEMA_VERSION)
|
|
433
|
-
|
|
439
|
+
assertCurrentCaseShape(importedEnvelope);
|
|
434
440
|
const local = normalizeCaseStore(localCases).cases;
|
|
435
441
|
const supportedImportedVersion = importedVersion ?? 0;
|
|
436
442
|
const byDomain = new Map(local.map((item) => [item.domain, item]));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// deliberately separate from collected evidence snapshots: a pin describes a
|
|
3
3
|
// fact selected by an analyst, a decision records analyst reasoning, and an
|
|
4
4
|
// action records a reviewed external or internal follow-up.
|
|
5
|
-
import { MAX_ASSERTION_PROVENANCE_LABELS, MAX_ASSERTION_PROVENANCE_MARKINGS, MAX_CASE_ACTION_BYTES, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTION_EVENTS_PER_CASE, MAX_CASE_ACTION_HISTORY_BYTES_PER_CASE, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_CLOSURES, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_CASE_MANUAL_TRAIL_EVENTS, MAX_CASE_OBSERVED_EFFECT_REVIEWS, MAX_CASE_SIGHTINGS, MAX_DECISION_PIN_REFERENCES, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_RECIPIENT_LENGTH, MAX_RESPONSE_REFERENCE_LENGTH, MAX_RESPONSE_VALUE_LENGTH, MAX_TRAIL_TARGET_LENGTH, } from "../contracts/case-portability.mjs";
|
|
5
|
+
import { MAX_ASSERTION_PROVENANCE_LABELS, MAX_ASSERTION_PROVENANCE_MARKINGS, MAX_CASE_ACTION_BYTES, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTION_EVENTS_PER_CASE, MAX_CASE_ACTION_HISTORY_BYTES_PER_CASE, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_CLOSURES, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_CASE_MANUAL_TRAIL_EVENTS, MAX_CASE_OBSERVED_EFFECT_REVIEWS, MAX_CASE_SIGHTINGS, MAX_DECISION_PIN_REFERENCES, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_RECIPIENT_LENGTH, MAX_RESPONSE_REFERENCE_LENGTH, MAX_RESPONSE_VALUE_LENGTH, MAX_TRAIL_TARGET_LENGTH, CASE_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
|
|
6
6
|
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
|
|
7
7
|
export { MAX_ASSERTION_PROVENANCE_LABELS, MAX_ASSERTION_PROVENANCE_MARKINGS, MAX_CASE_ACTION_BYTES, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTION_EVENTS_PER_CASE, MAX_CASE_ACTION_HISTORY_BYTES_PER_CASE, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_CLOSURES, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_CASE_MANUAL_TRAIL_EVENTS, MAX_CASE_OBSERVED_EFFECT_REVIEWS, MAX_CASE_SIGHTINGS, MAX_DECISION_PIN_REFERENCES, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_RECIPIENT_LENGTH, MAX_RESPONSE_REFERENCE_LENGTH, MAX_RESPONSE_VALUE_LENGTH, MAX_TRAIL_TARGET_LENGTH, };
|
|
8
8
|
export const CASE_EVIDENCE_RELATION_STANCES = ['supports', 'contradicts', 'unresolved'];
|
|
@@ -118,8 +118,8 @@ const SIGHTING_CATEGORIES = new Set(CASE_SIGHTING_CATEGORIES);
|
|
|
118
118
|
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
119
119
|
function currentActionNormalizationOptions(validEvidencePinIds) {
|
|
120
120
|
return validEvidencePinIds
|
|
121
|
-
? { sourceVersion:
|
|
122
|
-
: { sourceVersion:
|
|
121
|
+
? { sourceVersion: CASE_SCHEMA_VERSION, validEvidencePinIds }
|
|
122
|
+
: { sourceVersion: CASE_SCHEMA_VERSION };
|
|
123
123
|
}
|
|
124
124
|
function record(value) {
|
|
125
125
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -668,6 +668,9 @@ function normalizeAction(raw, fallback, options = {}) {
|
|
|
668
668
|
: 'internal_review',
|
|
669
669
|
recipient,
|
|
670
670
|
contactSource: text(item.contactSource, MAX_RESPONSE_LABEL_LENGTH) || 'analyst_supplied',
|
|
671
|
+
routeObservedAt: options.sourceVersion != null && options.sourceVersion < 14
|
|
672
|
+
? null
|
|
673
|
+
: optionalIso(item.routeObservedAt, options),
|
|
671
674
|
contactLimitations: limitations(item.contactLimitations),
|
|
672
675
|
dueAt: optionalIso(item.dueAt, options),
|
|
673
676
|
state: history.state,
|
|
@@ -699,7 +702,7 @@ function mergeNormalizedActions(left, right, fallback) {
|
|
|
699
702
|
history: [...left.history, ...right.history],
|
|
700
703
|
historyOmitted: Math.max(left.historyOmitted, right.historyOmitted),
|
|
701
704
|
historyLimitations: [...left.historyLimitations, ...right.historyLimitations],
|
|
702
|
-
}, fallback, { sourceVersion:
|
|
705
|
+
}, fallback, { sourceVersion: CASE_SCHEMA_VERSION });
|
|
703
706
|
}
|
|
704
707
|
function boundActionCollection(actions, fallback) {
|
|
705
708
|
const keep = new Set();
|
|
@@ -719,7 +722,7 @@ function boundActionCollection(actions, fallback) {
|
|
|
719
722
|
let bounded = actions.map((action) => {
|
|
720
723
|
const history = action.history.filter((event) => keep.has(`${action.id}\u0000${event.id}`));
|
|
721
724
|
const omitted = action.historyOmitted + action.history.length - history.length;
|
|
722
|
-
return normalizeAction({ ...action, history, historyOmitted: omitted }, fallback, { sourceVersion:
|
|
725
|
+
return normalizeAction({ ...action, history, historyOmitted: omitted }, fallback, { sourceVersion: CASE_SCHEMA_VERSION });
|
|
723
726
|
});
|
|
724
727
|
while (bytes(bounded) > MAX_CASE_ACTION_HISTORY_BYTES_PER_CASE) {
|
|
725
728
|
const candidate = bounded
|
|
@@ -734,7 +737,7 @@ function boundActionCollection(actions, fallback) {
|
|
|
734
737
|
...action,
|
|
735
738
|
history: action.history.filter((event) => event.id !== candidate.event.id),
|
|
736
739
|
historyOmitted: action.historyOmitted + 1,
|
|
737
|
-
}, fallback, { sourceVersion:
|
|
740
|
+
}, fallback, { sourceVersion: CASE_SCHEMA_VERSION }));
|
|
738
741
|
}
|
|
739
742
|
if (bytes(bounded) > MAX_CASE_ACTION_HISTORY_BYTES_PER_CASE && bounded.length > 1) {
|
|
740
743
|
const byteOmissionPattern = /^(\d+) earlier response actions? omitted by the per-Case response-history byte bound\.$/u;
|
|
@@ -859,10 +862,10 @@ export function appendCaseAction(current, raw, now) {
|
|
|
859
862
|
historyOmitted: 0,
|
|
860
863
|
createdAt: now,
|
|
861
864
|
metadataUpdatedAt: now,
|
|
862
|
-
}, now, { sourceVersion:
|
|
865
|
+
}, now, { sourceVersion: CASE_SCHEMA_VERSION });
|
|
863
866
|
if (!created)
|
|
864
867
|
throw new Error('An action requires a recipient or internal owner.');
|
|
865
|
-
return normalizeCaseActions([...current, created], now, { sourceVersion:
|
|
868
|
+
return normalizeCaseActions([...current, created], now, { sourceVersion: CASE_SCHEMA_VERSION });
|
|
866
869
|
}
|
|
867
870
|
export function appendCaseActionTransition(current, actionId, raw, now, validPinIds) {
|
|
868
871
|
const action = current.find((item) => item.id === actionId);
|
|
@@ -927,6 +930,7 @@ export function updateCaseAction(current, raw, now, validPinIds) {
|
|
|
927
930
|
type: Object.hasOwn(patch, 'type') ? patch.type : existing.type,
|
|
928
931
|
recipient: Object.hasOwn(patch, 'recipient') ? patch.recipient : existing.recipient,
|
|
929
932
|
contactSource: Object.hasOwn(patch, 'contactSource') ? patch.contactSource : existing.contactSource,
|
|
933
|
+
routeObservedAt: Object.hasOwn(patch, 'routeObservedAt') ? patch.routeObservedAt : existing.routeObservedAt,
|
|
930
934
|
contactLimitations: Object.hasOwn(patch, 'contactLimitations') ? patch.contactLimitations : existing.contactLimitations,
|
|
931
935
|
dueAt: Object.hasOwn(patch, 'dueAt') ? patch.dueAt : existing.dueAt,
|
|
932
936
|
followUpAt: Object.hasOwn(patch, 'followUpAt') ? patch.followUpAt : existing.followUpAt,
|
|
@@ -940,7 +944,7 @@ export function updateCaseAction(current, raw, now, validPinIds) {
|
|
|
940
944
|
let updated = normalizeAction(metadata, now, currentActionNormalizationOptions(validPinIds));
|
|
941
945
|
if (!updated)
|
|
942
946
|
throw new Error('An action requires a recipient or internal owner.');
|
|
943
|
-
const materialChanged = ['type', 'recipient', 'contactSource', 'contactLimitations', 'originActionId']
|
|
947
|
+
const materialChanged = ['type', 'recipient', 'contactSource', 'routeObservedAt', 'contactLimitations', 'originActionId']
|
|
944
948
|
.some((key) => Object.hasOwn(patch, key) && JSON.stringify(record(existing)[key]) !== JSON.stringify(record(updated)[key]));
|
|
945
949
|
if (materialChanged && ['submitted', 'acknowledged', 'terminal'].includes(existing.state)) {
|
|
946
950
|
throw new Error('Submitted or terminal action identity and recipient metadata cannot be rewritten; create a linked follow-on action instead.');
|