@slicedearth/whoisleuth-cli 2.5.1 → 2.7.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 +9 -6
- package/NOTICE +3 -2
- package/README.md +60 -67
- package/cli/artifact-validation/case-response.mjs +6 -814
- package/cli/artifact-validation/lookup-evidence.mjs +30 -3
- package/cli/artifact-validation/signed-review.mjs +2 -51
- package/cli/artifact-verify.mjs +41 -3
- package/cli/bulk-command-runner.mjs +4 -6
- package/cli/case-command.mjs +92 -16
- package/cli/case-pack.mjs +14 -3
- package/cli/ci-report.mjs +17 -5
- package/cli/command-definition.mjs +7 -6
- package/cli/discovery-scan-command-runner.mjs +4 -6
- package/cli/doctor.mjs +37 -3
- package/cli/domain-feed.mjs +61 -0
- package/cli/formatters/html.mjs +3 -0
- package/cli/formatters/json.mjs +9 -14
- package/cli/formatters/terminal-lookup.mjs +24 -1
- package/cli/history-command-definitions.mjs +1 -1
- package/cli/indicator-set-command.mjs +1 -2
- package/cli/intake-command.mjs +43 -3
- package/cli/investigation-artifacts.mjs +2 -3
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +15 -0
- package/cli/local-mmdb-review.mjs +27 -4
- package/cli/lookup-brief.mjs +11 -5
- package/cli/lookup-command-runner.mjs +21 -13
- package/cli/offline-evidence-review.mjs +51 -5
- package/cli/review-arguments.mjs +3 -2
- package/cli/review-command-definitions.mjs +23 -17
- package/cli/review-command-runner.mjs +6 -3
- package/cli/risk-calibration.mjs +6 -4
- package/cli/saved-lookup.mjs +22 -7
- package/cli/selected-input-worker.mjs +12 -2
- package/cli/source-reliability.mjs +12 -10
- package/cli/support-arguments.mjs +1 -1
- package/cli/support-command-definitions.mjs +5 -4
- package/cli/support-command-runner.mjs +9 -7
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +14 -0
- package/cli/workflow-command-definitions.mjs +27 -1
- package/cli/workflow-command-runner.mjs +32 -0
- package/docs/cli-reference.md +49 -11
- package/docs/cli.md +200 -26
- package/docs/contextual-reviews.md +38 -5
- package/docs/infrastructure-observations.md +135 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/dmarc-discovery.mjs +4 -2
- package/lib/dns-delegation-health.mjs +23 -7
- package/lib/dns-inheritance-review.mjs +14 -1
- package/lib/dns-intelligence.mjs +18 -4
- package/lib/domain-posture-analysis.mjs +53 -2
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +29 -10
- package/lib/evidence-export-privacy.mjs +5 -2
- package/lib/evidence-export.mjs +10 -4
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +17 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +178 -5
- package/lib/html-signals.mjs +6 -0
- package/lib/idn-confusables.mjs +5 -1
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-collection-receipt.mjs +52 -0
- package/lib/lookup-diagnostics.mjs +2 -12
- package/lib/lookup-progress.mjs +29 -31
- package/lib/lookup-response-contract.mjs +10 -0
- package/lib/lookup-response.mjs +15 -14
- package/lib/lookup-source-progress.mjs +4 -2
- package/lib/lookup.mjs +33 -2
- package/lib/operation-budget.mjs +4 -0
- package/lib/rdap-capabilities.mjs +43 -9
- package/lib/registry-insights.mjs +17 -6
- package/lib/service-binding-dns.mjs +19 -2
- package/lib/sslbl-certificates.generated.mjs +142 -140
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/website-enrichment.mjs +39 -23
- package/lib/whois-authority.mjs +56 -1
- package/lib/whois-chain.mjs +14 -4
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-contracts.mjs +2 -0
- package/lib/whois-normalization.mjs +4 -4
- package/package.json +9 -7
- package/packages/analysis/bounded-json.mjs +12 -2
- package/packages/analysis/risk-scoring.mjs +12 -9
- package/packages/cases/case-attachment-model.mjs +36 -1
- package/packages/cases/case-checkpoint-receipts.mjs +68 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +44 -0
- package/packages/cases/case-evidence-model.mjs +51 -7
- package/packages/cases/case-migration-model.mjs +9 -1
- package/packages/cases/case-object-coverage.mjs +119 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +35 -5
- package/packages/cases/case-record-operations.mjs +87 -9
- package/packages/cases/case-record-projection.mjs +43 -5
- package/packages/cases/case-record-version-input.mjs +23 -1
- package/packages/cases/case-report.mjs +35 -4
- package/packages/cases/case-requested-evidence.mjs +1 -1
- package/packages/cases/case-response-actions.mjs +57 -1
- package/packages/cases/case-response-model.mjs +7 -0
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +81 -14
- package/packages/cases/case-response-packet-render.mjs +22 -3
- package/packages/cases/case-response-packet-validation.mjs +880 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
- package/packages/cases/case-response-packet.mjs +32 -17
- package/packages/cases/case-response-review-inputs.mjs +66 -8
- package/packages/cases/case-type-presentation.mjs +10 -0
- package/packages/cases/case-workflow-metadata.mjs +1 -17
- package/packages/comparison/capture-context.mjs +3 -2
- package/packages/contracts/analyst-interchange.mjs +7 -5
- package/packages/contracts/analyst-review-state-contract.mjs +1 -1
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +292 -43
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-doctor.mjs +4 -0
- package/packages/contracts/cli-lookup.mjs +57 -46
- package/packages/contracts/context-review.mjs +2 -1
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/lookup-evidence.mjs +13 -2
- package/packages/contracts/message-intake.mjs +7 -1
- package/packages/contracts/registry-insights.mjs +6 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +1 -1
- package/packages/contracts/workspace-portability.mjs +86 -16
- package/packages/evidence/collection-quality.mjs +1 -1
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dmarc-policy-context.mjs +80 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-collection-receipt.mjs +140 -0
- package/packages/evidence/lookup-collection-review.mjs +74 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/external-findings-import.mjs +40 -21
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +3 -2
- package/packages/interchange/managed-indicator-set.mjs +13 -0
- package/packages/investigation/acquisition-decision-packet.mjs +78 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -2
- package/packages/investigation/context-review.mjs +3 -3
- package/packages/investigation/document-intake.mjs +9 -3
- package/packages/investigation/domain-history-review.mjs +50 -12
- package/packages/investigation/domain-transition-review.mjs +2 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +2 -1
- package/packages/investigation/investigation-manifest.mjs +33 -7
- package/packages/investigation/link-intake.mjs +38 -8
- package/packages/investigation/message-intake.mjs +55 -15
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +241 -0
- package/packages/workspace/brand-profile-model.mjs +46 -2
- package/packages/workspace/bulk-session-model.mjs +12 -0
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +10 -2
- package/packages/workspace/shortlist-model.mjs +10 -2
- package/packages/workspace/watchlist-history.mjs +31 -4
- package/packages/workspace/watchlist-store.mjs +136 -7
- package/third-party-notices.txt +30 -30
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Response packet vocabulary shared by construction and independent validation.
|
|
2
1
|
export const RESPONSE_CONTACT_KINDS = [
|
|
3
2
|
'registrar',
|
|
4
3
|
'registry',
|
|
@@ -6,6 +5,18 @@ export const RESPONSE_CONTACT_KINDS = [
|
|
|
6
5
|
'security_txt',
|
|
7
6
|
'application_platform',
|
|
8
7
|
];
|
|
8
|
+
const ACTION_CONTACT_KINDS = {
|
|
9
|
+
registrar_report: 'registrar',
|
|
10
|
+
registry_report: 'registry',
|
|
11
|
+
network_hosting_report: 'network_hosting',
|
|
12
|
+
security_contact_report: 'security_txt',
|
|
13
|
+
platform_report: 'application_platform',
|
|
14
|
+
defensive_control: 'manual',
|
|
15
|
+
internal_review: 'manual',
|
|
16
|
+
};
|
|
17
|
+
export function responseActionContactKind(type) {
|
|
18
|
+
return ACTION_CONTACT_KINDS[type];
|
|
19
|
+
}
|
|
9
20
|
export const RESPONSE_PACKET_PROFILE_IDS = [
|
|
10
21
|
'registrar',
|
|
11
22
|
'registry',
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { renderCaseResponsePacket } from "./case-response-packet-render.mjs";
|
|
2
|
-
import { responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
|
|
2
|
+
import { responseActionContactKind, responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
|
|
3
3
|
// Pure abuse-evidence packet builder. It creates local review artifacts only:
|
|
4
4
|
// no network requests, mailto links, submissions, or provider side effects.
|
|
5
5
|
import { assertPacketAmendmentSelection } from "./case-requested-evidence.mjs";
|
|
6
|
+
import { correctionDelivery, CASE_DELIVERY_RECEIPT_LIMITATION } from "./case-packet-correction.mjs";
|
|
7
|
+
import { caseSelectedEvidenceSourceLimitations } from "./case-evidence-links.mjs";
|
|
8
|
+
import { sameCaseResponseObject } from "./case-response-object.mjs";
|
|
6
9
|
import { assertBoundedJsonStructure, } from "../analysis/bounded-json.mjs";
|
|
7
10
|
import { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSE_CATEGORY_LENGTH, MAX_ABUSIVE_URLS, MAX_AFFECTED_PARTY_LENGTH, MAX_EXACT_URL_LENGTH, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_HARM_LENGTH, MAX_RESPONSE_SELECTED_EVIDENCE, SUPPORTED_CASE_RESPONSE_PACKET_VERSIONS, } from "../contracts/case-portability.mjs";
|
|
8
11
|
import { SORTED_JSON_V2, canonicalArtifactJsonV2, } from "../evidence/artifact-integrity.mjs";
|
|
@@ -199,19 +202,6 @@ function normalizeLimitations(value) {
|
|
|
199
202
|
}
|
|
200
203
|
return [...unique];
|
|
201
204
|
}
|
|
202
|
-
function actionContactKind(type) {
|
|
203
|
-
if (type === 'registrar_report')
|
|
204
|
-
return 'registrar';
|
|
205
|
-
if (type === 'registry_report')
|
|
206
|
-
return 'registry';
|
|
207
|
-
if (type === 'network_hosting_report')
|
|
208
|
-
return 'network_hosting';
|
|
209
|
-
if (type === 'security_contact_report')
|
|
210
|
-
return 'security_txt';
|
|
211
|
-
if (type === 'platform_report')
|
|
212
|
-
return 'application_platform';
|
|
213
|
-
return 'manual';
|
|
214
|
-
}
|
|
215
205
|
function selectedPacketAction(caseRecord, input) {
|
|
216
206
|
const actionId = typeof input.actionId === 'string' && /^[A-Za-z0-9_-]{1,64}$/u.test(input.actionId)
|
|
217
207
|
? input.actionId
|
|
@@ -222,6 +212,9 @@ function assertAmendmentEvidence(caseRecord, input) {
|
|
|
222
212
|
const action = selectedPacketAction(caseRecord, input);
|
|
223
213
|
const selected = new Set(Array.isArray(input.selectedEvidencePinIds) ? input.selectedEvidencePinIds : []);
|
|
224
214
|
assertPacketAmendmentSelection(caseRecord.actions, action?.id ?? null, new Set(caseRecord.evidencePins.filter(pin => selected.has(pin.id)).map(pin => pin.id)));
|
|
215
|
+
if (action?.correction)
|
|
216
|
+
correctionDelivery(caseRecord.actions, action, { caseId: caseRecord.id, target: caseRecord.domain,
|
|
217
|
+
profile: responsePacketProfile(input.profile).id, selectedPinIds: new Set(caseRecord.evidencePins.filter(pin => selected.has(pin.id)).map(pin => pin.id)) });
|
|
225
218
|
}
|
|
226
219
|
function packetActionLineage(caseRecord, input) {
|
|
227
220
|
const selected = selectedPacketAction(caseRecord, input);
|
|
@@ -258,7 +251,7 @@ function bindPacketRoute(caseRecord, input, generatedAt) {
|
|
|
258
251
|
contacts: [],
|
|
259
252
|
};
|
|
260
253
|
}
|
|
261
|
-
const kind =
|
|
254
|
+
const kind = responseActionContactKind(action.type);
|
|
262
255
|
const compatible = profile.requiredContactKind
|
|
263
256
|
? kind === profile.requiredContactKind
|
|
264
257
|
: profile.id === 'browser_blocklist'
|
|
@@ -322,6 +315,7 @@ function normalizeSelectedEvidence(caseRecord, value) {
|
|
|
322
315
|
label: text(pin.label, 80),
|
|
323
316
|
source: text(pin.source, 120),
|
|
324
317
|
...(pin.observationHostname ? { observationHostname: pin.observationHostname } : {}),
|
|
318
|
+
...(pin.responseObject ? { responseObject: structuredClone(pin.responseObject) } : {}),
|
|
325
319
|
...(pin.webObservationMode ? { webObservationMode: pin.webObservationMode } : {}),
|
|
326
320
|
observedAt: timestamp(pin.observedAt),
|
|
327
321
|
completeness: text(pin.completeness, 40),
|
|
@@ -524,7 +518,7 @@ export function buildResponsePacketProfilePreview(caseRecord, input) {
|
|
|
524
518
|
id: profile.id,
|
|
525
519
|
label: profile.label,
|
|
526
520
|
audience: profile.audience,
|
|
527
|
-
subject: `${profile.subjectPrefix}: ${caseRecord.domain} (${category})`,
|
|
521
|
+
subject: `${selectedPacketAction(caseRecord, input)?.correction ? 'Correction or retraction request' : profile.subjectPrefix}: ${caseRecord.domain} (${category})`,
|
|
528
522
|
checklist: [...profile.checklist],
|
|
529
523
|
evidenceOrder: [...profile.evidenceOrder],
|
|
530
524
|
includedEvidence: [...profile.includedEvidence],
|
|
@@ -687,6 +681,8 @@ function normalizeActionHistory(caseRecord, input) {
|
|
|
687
681
|
actionId: action.id,
|
|
688
682
|
type: text(action.type, 80),
|
|
689
683
|
...(action.amendment ? { amendment: structuredClone(action.amendment) } : {}),
|
|
684
|
+
...(action.correction ? { correction: structuredClone(action.correction) } : {}),
|
|
685
|
+
...(action.responseObjects !== undefined ? { responseObjects: structuredClone(action.responseObjects) } : {}),
|
|
690
686
|
recipient: text(action.recipient, 320),
|
|
691
687
|
contactSource: text(action.contactSource, 120),
|
|
692
688
|
routeObservedAt: timestamp(action.routeObservedAt),
|
|
@@ -701,6 +697,9 @@ function normalizeActionHistory(caseRecord, input) {
|
|
|
701
697
|
transitions: action.history.map((event) => ({
|
|
702
698
|
id: event.id,
|
|
703
699
|
...(event.evidenceRequest ? { evidenceRequest: structuredClone(event.evidenceRequest) } : {}),
|
|
700
|
+
...(event.packetReceipt ? { packetReceipt: structuredClone(event.packetReceipt) } : {}),
|
|
701
|
+
...(event.responseObjects !== undefined ? { responseObjects: structuredClone(event.responseObjects) } : {}),
|
|
702
|
+
...(event.objectOutcome ? { objectOutcome: event.objectOutcome } : {}),
|
|
704
703
|
previousState: event.previousState,
|
|
705
704
|
nextState: event.nextState,
|
|
706
705
|
occurredAt: event.occurredAt,
|
|
@@ -736,21 +735,31 @@ function normalizeActionHistory(caseRecord, input) {
|
|
|
736
735
|
}
|
|
737
736
|
function normalizeResponseLifecycle(caseRecord, actionIds, lineageComplete) {
|
|
738
737
|
const scopedIds = new Set(actionIds);
|
|
738
|
+
const objects = caseRecord.actions.filter(action => scopedIds.has(action.id)).flatMap(action => action.responseObjects ?? []);
|
|
739
739
|
const scopedRecord = {
|
|
740
740
|
...caseRecord,
|
|
741
741
|
actions: caseRecord.actions.filter((action) => scopedIds.has(action.id)),
|
|
742
|
+
observedEffects: {
|
|
743
|
+
...caseRecord.observedEffects,
|
|
744
|
+
reviews: caseRecord.observedEffects.reviews.filter(review => review.responseObject
|
|
745
|
+
&& objects.some(object => sameCaseResponseObject(object, review.responseObject))),
|
|
746
|
+
},
|
|
742
747
|
closures: {
|
|
743
748
|
...caseRecord.closures,
|
|
744
749
|
records: caseRecord.closures.records.filter((closure) => closure.actionId !== null && scopedIds.has(closure.actionId)),
|
|
745
750
|
},
|
|
746
751
|
};
|
|
747
752
|
const summary = buildCaseResponseLifecycleSummary(scopedRecord);
|
|
753
|
+
const latestReview = scopedRecord.observedEffects.reviews.find(review => review.id === summary.latestObservedEffect?.reviewId);
|
|
748
754
|
const providerOutcomeState = lineageComplete ? summary.providerOutcomeState : 'ambiguous';
|
|
749
755
|
return {
|
|
750
756
|
providerOutcomeState,
|
|
751
757
|
latestProviderOutcome: lineageComplete && summary.latestProviderOutcome ? { ...summary.latestProviderOutcome } : null,
|
|
752
758
|
observedChangeState: summary.observedChangeState,
|
|
753
|
-
latestObservedEffect: summary.latestObservedEffect ? { ...summary.latestObservedEffect
|
|
759
|
+
latestObservedEffect: summary.latestObservedEffect ? { ...summary.latestObservedEffect,
|
|
760
|
+
...(latestReview?.responseObject ? { responseObject: latestReview.responseObject } : {}),
|
|
761
|
+
...(latestReview?.objectOutcome ? { objectOutcome: latestReview.objectOutcome } : {}),
|
|
762
|
+
} : null,
|
|
754
763
|
latestObservedChangeAt: summary.latestObservedChangeAt,
|
|
755
764
|
closure: summary.latestClosure ? {
|
|
756
765
|
id: summary.latestClosure.id,
|
|
@@ -762,6 +771,8 @@ function normalizeResponseLifecycle(caseRecord, actionIds, lineageComplete) {
|
|
|
762
771
|
'Provider workflow outcomes and independently observed technical effects are separate point-in-time records.',
|
|
763
772
|
'A provider acknowledgement, terminal state, or reported resolution never becomes independently observed remediation, absence, or safety.',
|
|
764
773
|
'Times are withheld when the corresponding typed event is missing or ambiguous.',
|
|
774
|
+
...(objects.length ? ['Each explicit object remains independent; a latest review does not adjudicate every object bound to the action.'] : []),
|
|
775
|
+
...(caseRecord.observedEffects.reviews.length > scopedRecord.observedEffects.reviews.length ? ['Independent reviews without matching explicit action-object scope were excluded from this packet; their retained Case history is unchanged.'] : []),
|
|
765
776
|
...(!summary.latestObservedEffect && scopedRecord.observedEffects.reviews.length ? ['A single latest independent review cannot be selected from the retained observation times; all retained reviews remain separately attributed.'] : []),
|
|
766
777
|
...(!lineageComplete ? ['Provider-outcome time is withheld because the selected action origin lineage is incomplete.'] : []),
|
|
767
778
|
],
|
|
@@ -797,6 +808,7 @@ export function buildCaseResponseReviewInputs(caseRecord, input, generatedAt) {
|
|
|
797
808
|
return {
|
|
798
809
|
contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
|
|
799
810
|
version: CASE_RESPONSE_REVIEW_INPUTS_VERSION,
|
|
811
|
+
sourceQualifications: caseSelectedEvidenceSourceLimitations(caseRecord.evidencePins, caseRecord.evidenceLinks, selectedEvidence.map(pin => pin.id)),
|
|
800
812
|
profile: {
|
|
801
813
|
id: profile.id,
|
|
802
814
|
label: profile.label,
|
|
@@ -936,6 +948,9 @@ export async function buildCaseResponsePacket(caseRecord, input, generatedAt = n
|
|
|
936
948
|
? 'This packet is bound to explicit confirmations for the exact reviewed-input digest. It still requires deliberate manual use.'
|
|
937
949
|
: 'This packet is a local draft with cautions and is not authorised for external use by WHOISleuth.',
|
|
938
950
|
'WHOISleuth did not submit this packet or verify that any listed contact is monitored.',
|
|
951
|
+
...reviewMaterial.sourceQualifications,
|
|
952
|
+
...(selectedPacketAction(caseRecord, input)?.correction ? [CASE_DELIVERY_RECEIPT_LIMITATION,
|
|
953
|
+
'This is a new correction or retraction request. It does not undo the original delivery, establish recipient acceptance or restoration, or replace independent recheck.'] : []),
|
|
939
954
|
...(!contacts.length ? ['No escalation contact was included.'] : []),
|
|
940
955
|
...(age.refreshRecommended ? ['The selected observation is over seven days old or appears to be in the future. Refresh evidence before submission.'] : []),
|
|
941
956
|
];
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Exact, bounded validation of current and published response-review inputs.
|
|
2
2
|
import { readCaseEvidenceRequest, readCasePacketAmendment, assertEvidenceRequestEvent, assertEvidenceRequestHistory, assertPacketAmendmentSelection } from "./case-requested-evidence.mjs";
|
|
3
|
+
import { readCaseDeliveryPacketReceipt, readCasePacketCorrection, assertDeliveryPacketReceipt, correctionDelivery } from "./case-packet-correction.mjs";
|
|
4
|
+
import { CASE_SELECTED_EVIDENCE_SOURCE_LIMITATION } from "./case-evidence-links.mjs";
|
|
3
5
|
import { CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, MAX_ABUSE_CATEGORY_LENGTH, MAX_ABUSIVE_URLS, MAX_AFFECTED_PARTY_LENGTH, MAX_CASE_ACTIONS, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTION_EVENTS_PER_CASE, MAX_EXACT_URL_LENGTH, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_HARM_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_REFERENCE_LENGTH, MAX_RESPONSE_SELECTED_EVIDENCE, MAX_RESPONSE_VALUE_LENGTH, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, SUPPORTED_CASE_RESPONSE_REVIEW_INPUTS_VERSIONS, } from "../contracts/case-portability.mjs";
|
|
4
6
|
import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_ASSERTION_STATES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PIN_COMPLETENESS, CASE_PROVIDER_OUTCOMES, } from "./case-response-records.mjs";
|
|
5
|
-
import { RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, } from "./case-response-packet-vocabulary.mjs";
|
|
7
|
+
import { RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, responseActionContactKind, } from "./case-response-packet-vocabulary.mjs";
|
|
6
8
|
import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
|
|
9
|
+
import { readCaseResponseObject, readCaseResponseObjects, readCaseResponseObjectOutcome, assertCaseObjectOutcome } from "./case-response-object.mjs";
|
|
7
10
|
const CONTACT_KINDS = new Set(RESPONSE_CONTACT_KINDS);
|
|
8
11
|
const PRE_PLATFORM_CONTACT_KINDS = new Set(RESPONSE_CONTACT_KINDS.filter((kind) => kind !== 'application_platform'));
|
|
9
12
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
@@ -27,6 +30,7 @@ const CASE_RESPONSE_REVIEW_INPUT_KEYS = Object.freeze([
|
|
|
27
30
|
'escalationHistoryOmitted',
|
|
28
31
|
'escalationHistoryLimitations',
|
|
29
32
|
'responseLifecycle',
|
|
33
|
+
'sourceQualifications',
|
|
30
34
|
]);
|
|
31
35
|
const PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS = Object.freeze(CASE_RESPONSE_REVIEW_INPUT_KEYS.filter((key) => key !== 'recipientRoute' && key !== 'actionBinding'));
|
|
32
36
|
function exactReviewRecord(value, keys, label, optionalKeys = []) {
|
|
@@ -88,7 +92,7 @@ function reviewEnum(value, values, label) {
|
|
|
88
92
|
function reviewNullableEnum(value, values, label) {
|
|
89
93
|
return value === null ? null : reviewEnum(value, values, label);
|
|
90
94
|
}
|
|
91
|
-
function validateReviewLifecycle(value) {
|
|
95
|
+
function validateReviewLifecycle(value, version) {
|
|
92
96
|
const lifecycle = exactReviewRecord(value, [
|
|
93
97
|
'providerOutcomeState', 'latestProviderOutcome', 'observedChangeState',
|
|
94
98
|
'latestObservedEffect', 'latestObservedChangeAt', 'closure', 'limitations',
|
|
@@ -110,7 +114,10 @@ function validateReviewLifecycle(value) {
|
|
|
110
114
|
if (lifecycle.latestObservedEffect !== null) {
|
|
111
115
|
const effect = exactReviewRecord(lifecycle.latestObservedEffect, [
|
|
112
116
|
'reviewId', 'state', 'observedAt', 'sourceClass', 'source',
|
|
113
|
-
], 'Case-response latest observed effect');
|
|
117
|
+
], 'Case-response latest observed effect', version >= 6 ? ['responseObject', 'objectOutcome'] : []);
|
|
118
|
+
const responseObject = readCaseResponseObject(effect.responseObject);
|
|
119
|
+
const objectOutcome = readCaseResponseObjectOutcome(effect.objectOutcome);
|
|
120
|
+
assertCaseObjectOutcome(objectOutcome, responseObject);
|
|
114
121
|
reviewText(effect.reviewId, 64, 'Case-response observed-effect review id');
|
|
115
122
|
reviewEnum(effect.state, CASE_OBSERVED_EFFECT_STATES, 'Case-response observed-effect state');
|
|
116
123
|
reviewText(effect.observedAt, 64, 'Case-response observed-effect time');
|
|
@@ -135,6 +142,21 @@ function recursivelyFreezeReviewValue(value) {
|
|
|
135
142
|
}
|
|
136
143
|
return Object.freeze(value);
|
|
137
144
|
}
|
|
145
|
+
/** Compare already shape-validated route and action records without rebuilding
|
|
146
|
+
* or normalising the retained evidence. Older formats omit the review deadline. */
|
|
147
|
+
export function assertCaseResponseRecipientAction(route, actions, hasReviewAfter) {
|
|
148
|
+
if (!route)
|
|
149
|
+
return;
|
|
150
|
+
const action = actions.find(candidate => candidate.actionId === route.actionId);
|
|
151
|
+
if (!action
|
|
152
|
+
|| route.kind !== responseActionContactKind(action.type)
|
|
153
|
+
|| route.contact !== action.recipient
|
|
154
|
+
|| route.source !== (action.contactSource || 'analyst supplied')
|
|
155
|
+
|| route.observedAt !== action.routeObservedAt
|
|
156
|
+
|| (hasReviewAfter && route.reviewAfter !== action.routeReviewAfter)) {
|
|
157
|
+
throw new TypeError('Case-response recipient route must match its selected action.');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
138
160
|
export function validateCaseResponseReviewInputs(value) {
|
|
139
161
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
140
162
|
throw new TypeError('Case-response review inputs must be an exact object.');
|
|
@@ -151,7 +173,12 @@ export function validateCaseResponseReviewInputs(value) {
|
|
|
151
173
|
}
|
|
152
174
|
const hasPlatformRoutes = version >= PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION;
|
|
153
175
|
const hasActionBinding = version >= PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION;
|
|
154
|
-
const source = exactReviewRecord(value, hasActionBinding ? CASE_RESPONSE_REVIEW_INPUT_KEYS : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS, 'Case-response review inputs');
|
|
176
|
+
const source = exactReviewRecord(value, (hasActionBinding ? CASE_RESPONSE_REVIEW_INPUT_KEYS : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUT_KEYS).filter(key => version >= 7 || key !== 'sourceQualifications'), 'Case-response review inputs');
|
|
177
|
+
if (version >= 7)
|
|
178
|
+
for (const qualification of boundedReviewArray(source.sourceQualifications, 1, 'Selected evidence source qualifications')) {
|
|
179
|
+
if (qualification !== CASE_SELECTED_EVIDENCE_SOURCE_LIMITATION)
|
|
180
|
+
throw new TypeError('Unknown selected-source qualification.');
|
|
181
|
+
}
|
|
155
182
|
const profile = exactReviewRecord(source.profile, [
|
|
156
183
|
'id', 'label', 'audience', 'subject', 'checklist', 'includedEvidence',
|
|
157
184
|
'excludedEvidence', 'redactions',
|
|
@@ -236,7 +263,11 @@ export function validateCaseResponseReviewInputs(value) {
|
|
|
236
263
|
for (const candidate of boundedReviewArray(source.selectedEvidence, MAX_RESPONSE_SELECTED_EVIDENCE, 'Case-response selected evidence')) {
|
|
237
264
|
const evidence = exactReviewRecord(candidate, [
|
|
238
265
|
'id', 'label', 'source', 'observedAt', 'completeness', 'limitations',
|
|
239
|
-
], 'Case-response selected evidence item',
|
|
266
|
+
], 'Case-response selected evidence item', [
|
|
267
|
+
...(version > PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION ? ['observationHostname', 'webObservationMode'] : []),
|
|
268
|
+
...(version >= 6 ? ['responseObject'] : []),
|
|
269
|
+
]);
|
|
270
|
+
readCaseResponseObject(evidence.responseObject);
|
|
240
271
|
reviewText(evidence.id, 64, 'Case-response evidence id');
|
|
241
272
|
reviewText(evidence.label, 80, 'Case-response evidence label');
|
|
242
273
|
reviewText(evidence.source, 120, 'Case-response evidence source');
|
|
@@ -319,8 +350,11 @@ export function validateCaseResponseReviewInputs(value) {
|
|
|
319
350
|
...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
|
|
320
351
|
'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
|
|
321
352
|
'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
|
|
322
|
-
], 'Case-response escalation action', version >= 5 ? ['amendment'] : []);
|
|
353
|
+
], 'Case-response escalation action', [...(version >= 5 ? ['amendment'] : []), ...(version >= 6 ? ['responseObjects'] : []), ...(version >= 7 ? ['correction'] : [])]);
|
|
354
|
+
readCaseResponseObjects(action.responseObjects);
|
|
323
355
|
readCasePacketAmendment(action.amendment);
|
|
356
|
+
if (readCasePacketCorrection(action.correction) && action.amendment)
|
|
357
|
+
throw new TypeError('Correction and provider amendment must remain separate.');
|
|
324
358
|
for (const key of ['actionId', 'recipient', 'contactSource', 'createdAt', 'updatedAt']) {
|
|
325
359
|
reviewText(action[key], MAX_RESPONSE_VALUE_LENGTH, `Case-response action ${key}`);
|
|
326
360
|
}
|
|
@@ -346,7 +380,21 @@ export function validateCaseResponseReviewInputs(value) {
|
|
|
346
380
|
'id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance',
|
|
347
381
|
'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail',
|
|
348
382
|
'originActionId', 'applied',
|
|
349
|
-
], 'Case-response transition', version >= 5 ? ['evidenceRequest'] : []);
|
|
383
|
+
], 'Case-response transition', [...(version >= 5 ? ['evidenceRequest'] : []), ...(version >= 6 ? ['responseObjects', 'objectOutcome'] : []), ...(version >= 7 ? ['packetReceipt'] : [])]);
|
|
384
|
+
const receipt = readCaseDeliveryPacketReceipt(transition.packetReceipt);
|
|
385
|
+
assertDeliveryPacketReceipt(receipt, transition, action.actionId);
|
|
386
|
+
if (receipt && (receipt.caseId !== source.case.id || receipt.target !== source.case.domain
|
|
387
|
+
|| receipt.recipient !== action.recipient))
|
|
388
|
+
throw new TypeError('The delivery receipt must retain the same Case, target and recipient.');
|
|
389
|
+
const responseObjects = readCaseResponseObjects(transition.responseObjects);
|
|
390
|
+
const objectOutcome = readCaseResponseObjectOutcome(transition.objectOutcome);
|
|
391
|
+
if (objectOutcome) {
|
|
392
|
+
if (!responseObjects?.length || !['submitted', 'acknowledged', 'terminal'].includes(String(transition.nextState))
|
|
393
|
+
|| !['analyst', 'provider'].includes(String(transition.sourceClass))
|
|
394
|
+
|| transition.previousState === 'authorised')
|
|
395
|
+
throw new TypeError('Case-response object outcome requires an explicit affected subset after submission.');
|
|
396
|
+
responseObjects.forEach(object => assertCaseObjectOutcome(objectOutcome, object));
|
|
397
|
+
}
|
|
350
398
|
const evidenceRequest = readCaseEvidenceRequest(transition.evidenceRequest);
|
|
351
399
|
assertEvidenceRequestEvent(evidenceRequest, transition);
|
|
352
400
|
requestEvents.push({ id: transition.id, ...(evidenceRequest ? { evidenceRequest } : {}) });
|
|
@@ -371,12 +419,22 @@ export function validateCaseResponseReviewInputs(value) {
|
|
|
371
419
|
|| escalationActionIds.some((id, index) => currentLineageActionIds?.[index] !== id))) {
|
|
372
420
|
throw new TypeError('Case-response escalation history must match the selected action lineage in order.');
|
|
373
421
|
}
|
|
422
|
+
if (hasActionBinding)
|
|
423
|
+
assertCaseResponseRecipientAction(source.recipientRoute, source.escalationHistory, hasPlatformRoutes);
|
|
374
424
|
reviewCount(source.escalationHistoryOmitted, MAX_CASE_ACTIONS, 'Case-response omitted action count');
|
|
375
425
|
if (version >= 5)
|
|
376
426
|
assertPacketAmendmentSelection(source.escalationHistory.map(action => ({
|
|
377
427
|
id: action.actionId, history: action.transitions, originActionId: action.originActionId, amendment: action.amendment,
|
|
378
428
|
})), currentLineageActionIds?.[0] ?? null, new Set(source.selectedEvidence.map(pin => pin.id)));
|
|
429
|
+
if (version >= 7) {
|
|
430
|
+
const actions = source.escalationHistory.map(action => ({ ...action, id: action.actionId, history: action.transitions }));
|
|
431
|
+
const selected = actions.find(action => action.id === currentLineageActionIds?.[0]);
|
|
432
|
+
if (selected?.correction)
|
|
433
|
+
correctionDelivery(actions, selected, { caseId: source.case.id,
|
|
434
|
+
target: source.case.domain, profile: profile.id,
|
|
435
|
+
selectedPinIds: new Set(source.selectedEvidence.map(pin => pin.id)) });
|
|
436
|
+
}
|
|
379
437
|
reviewStrings(source.escalationHistoryLimitations, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH, 'Case-response escalation limitations');
|
|
380
|
-
validateReviewLifecycle(source.responseLifecycle);
|
|
438
|
+
validateReviewLifecycle(source.responseLifecycle, version);
|
|
381
439
|
return recursivelyFreezeReviewValue(structuredClone(source));
|
|
382
440
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { caseTypeRecords } from "./case-workflow-metadata.mjs";
|
|
2
|
+
/** Display text for declared Case types; admission and mutation remain separate. */
|
|
3
|
+
export function caseTypeSummary(record) {
|
|
4
|
+
const labels = caseTypeRecords(record).map((item) => item.label);
|
|
5
|
+
if (!labels.length)
|
|
6
|
+
return '';
|
|
7
|
+
if (labels.length <= 2)
|
|
8
|
+
return labels.join(' and ');
|
|
9
|
+
return `${labels.slice(0, 2).join(', ')} and ${labels.length - 2} more`;
|
|
10
|
+
}
|
|
@@ -44,14 +44,6 @@ export function caseTypeRecords(record) {
|
|
|
44
44
|
const selected = new Set(caseTypeIds(record));
|
|
45
45
|
return CASE_TYPES.filter((item) => selected.has(item.id));
|
|
46
46
|
}
|
|
47
|
-
export function caseTypeSummary(record) {
|
|
48
|
-
const labels = caseTypeRecords(record).map((item) => item.label);
|
|
49
|
-
if (!labels.length)
|
|
50
|
-
return '';
|
|
51
|
-
if (labels.length <= 2)
|
|
52
|
-
return labels.join(' and ');
|
|
53
|
-
return `${labels.slice(0, 2).join(', ')} and ${labels.length - 2} more`;
|
|
54
|
-
}
|
|
55
47
|
function encodeCaseReference(value, minimumLength) {
|
|
56
48
|
let remainder = value;
|
|
57
49
|
let encoded = '';
|
|
@@ -102,15 +94,7 @@ export function normalizeCaseIncidentTargetUrl(value) {
|
|
|
102
94
|
const normalized = parsed.toString();
|
|
103
95
|
return normalized.length <= MAX_CASE_INCIDENT_TARGET_URL_LENGTH ? normalized : null;
|
|
104
96
|
}
|
|
105
|
-
|
|
106
|
-
export function buildCaseIncidentCoverage(record) {
|
|
107
|
-
return caseIncidentTargets(record, { includeResolved: true }).map(target => ({
|
|
108
|
-
target,
|
|
109
|
-
hostname: new URL(target.url).hostname,
|
|
110
|
-
actionCoverage: 'unknown',
|
|
111
|
-
observationCoverage: 'unknown',
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
97
|
+
export { buildCaseIncidentCoverage } from "./case-object-coverage.mjs";
|
|
114
98
|
export function emptyCaseWorkflowMetadata() {
|
|
115
99
|
return { types: [], incidentTargets: [], investigationContext: null };
|
|
116
100
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { exact, text } from "../evidence/artifact-structure.mjs";
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
3
|
import { readEvidenceImageDimensions } from "../evidence/image-regions.mjs";
|
|
4
|
+
export const MAX_CAPTURE_COMPARISON_CONTEXTS = 20;
|
|
4
5
|
/** Optional declarations remain unknown for existing manifests; no inference from image dimensions. */
|
|
5
6
|
export function readCaptureConditions(raw) {
|
|
6
7
|
if (raw === undefined || raw === null)
|
|
@@ -23,8 +24,8 @@ export function readObservationLabel(raw) {
|
|
|
23
24
|
return text(raw, 'Observation label', 80);
|
|
24
25
|
}
|
|
25
26
|
export function compareObservationContexts(input) {
|
|
26
|
-
if (!Array.isArray(input) || input.length < 2 || input.length >
|
|
27
|
-
throw new TypeError(
|
|
27
|
+
if (!Array.isArray(input) || input.length < 2 || input.length > MAX_CAPTURE_COMPARISON_CONTEXTS)
|
|
28
|
+
throw new TypeError(`Compare between 2 and ${MAX_CAPTURE_COMPARISON_CONTEXTS} observation contexts.`);
|
|
28
29
|
const observations = input.map(value => {
|
|
29
30
|
const observedAt = value.observedAt === null ? null : normalizeExplicitIsoTimestamp(value.observedAt);
|
|
30
31
|
if (value.observedAt !== null && !observedAt)
|
|
@@ -33,7 +33,8 @@ export const MAIL_REPORT_CANONICALIZATION_ROUTES = Object.freeze([
|
|
|
33
33
|
export const REGISTRATION_DISCLOSURE_PLAN_SCHEMA = 'whoisleuth.registration-disclosure-plan';
|
|
34
34
|
export const REGISTRATION_DISCLOSURE_PLAN_VERSION = 2;
|
|
35
35
|
export const STATIC_PAGE_PATTERN_PACK_SCHEMA = 'whoisleuth.static-page-pattern-pack';
|
|
36
|
-
export const STATIC_PAGE_PATTERN_PACK_VERSION =
|
|
36
|
+
export const STATIC_PAGE_PATTERN_PACK_VERSION = 3;
|
|
37
|
+
export const STATIC_PAGE_PATTERN_PACK_SUPPORTED_VERSIONS = [2, STATIC_PAGE_PATTERN_PACK_VERSION];
|
|
37
38
|
export const MAX_STATIC_PAGE_PATTERN_PACK_BYTES = 256 * 1024;
|
|
38
39
|
export const WEB_CAPTURE_SUMMARY_SCHEMA = 'whoisleuth.web-capture-summary';
|
|
39
40
|
export const WEB_CAPTURE_SUMMARY_VERSION = 1;
|
|
@@ -136,11 +137,11 @@ const REGISTRATION_DISCLOSURE_COMPATIBILITY = defineSchemaCompatibility({
|
|
|
136
137
|
});
|
|
137
138
|
const STATIC_PATTERN_PACK_COMPATIBILITY = defineSchemaCompatibility({
|
|
138
139
|
id: 'export.static-page-pattern-pack', kind: 'export', schema: STATIC_PAGE_PATTERN_PACK_SCHEMA,
|
|
139
|
-
currentVersion: STATIC_PAGE_PATTERN_PACK_VERSION, supportedVersions:
|
|
140
|
-
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: '
|
|
140
|
+
currentVersion: STATIC_PAGE_PATTERN_PACK_VERSION, supportedVersions: STATIC_PAGE_PATTERN_PACK_SUPPORTED_VERSIONS,
|
|
141
|
+
acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'read_only',
|
|
141
142
|
writeSemantics: 'non_destructive_merge', byteBudget: MAX_STATIC_PAGE_PATTERN_PACK_BYTES,
|
|
142
143
|
owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
|
|
143
|
-
note: '
|
|
144
|
+
note: 'Versions 2 and 3 remain readable. Version 3 can use attributed password-form destinations; older rules retain their declared meaning. Import remains explicit and does not execute code or start collection.',
|
|
144
145
|
});
|
|
145
146
|
export function serialiseAnalystInterchangeJson(value) {
|
|
146
147
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
@@ -229,6 +230,7 @@ export const ANALYST_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(
|
|
|
229
230
|
{ descriptor: STATIC_PATTERN_PACK_COMPATIBILITY, lifecycleSchema: STATIC_PAGE_PATTERN_PACK_SCHEMA,
|
|
230
231
|
requiredKeys: ['schema', 'version', 'id', 'label', 'description', 'evidenceBoundary', 'relationship', 'confidence', 'rules'], optionalKeys: [],
|
|
231
232
|
hook: { module: 'packages/interchange/static-page-pattern-packs.mts', exportName: 'validateStaticPagePatternPack', role: 'structure_validator', runtime: 'shared' },
|
|
232
|
-
fixtures: [{ id: 'static-page-pattern-pack-v2', path: `${F}static-page-pattern-pack-v2.json`, bytes: 1_094, sha256: 'c409e527850bcb924f5be0708a58c5e2c32d98947c42150f6a79765991c0eb6f', version:
|
|
233
|
+
fixtures: [{ id: 'static-page-pattern-pack-v2', path: `${F}static-page-pattern-pack-v2.json`, bytes: 1_094, sha256: 'c409e527850bcb924f5be0708a58c5e2c32d98947c42150f6a79765991c0eb6f', version: 2 },
|
|
234
|
+
{ id: 'static-page-pattern-pack-v3', path: `${F}static-page-pattern-pack-v3.json`, bytes: 746, sha256: '5be19b621ad604de8c430191a34fcd2d624b22b8bdf9a5cb2653ef7d1b86c71a', version: 3 }] },
|
|
233
235
|
],
|
|
234
236
|
}));
|
|
@@ -59,7 +59,7 @@ export const ANALYST_REVIEW_NEXT_ACTIONS = ['review', 'refresh', 'follow_up', 'r
|
|
|
59
59
|
export const ANALYST_REVIEW_LIFECYCLE_STATES = ['open', 'expected', 'suppressed', 'resolved', 'expired', 'invalidated', 'recurred', 'orphaned'];
|
|
60
60
|
export const ANALYST_REVIEW_QUEUE_OPTIONS = [
|
|
61
61
|
{ value: 'needs_action', label: 'Needs action' },
|
|
62
|
-
{ value: 'waiting', label: 'Waiting
|
|
62
|
+
{ value: 'waiting', label: 'Waiting on' },
|
|
63
63
|
{ value: 'changed', label: 'Changed since review' },
|
|
64
64
|
{ value: 'reviewed', label: 'Reviewed' },
|
|
65
65
|
{ value: 'all', label: 'Everything' },
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
|
|
2
|
+
import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
|
|
3
|
+
import { buildExtractedLifecycleFamily } from "./extracted-domain-lifecycle.mjs";
|
|
4
|
+
import { MAX_WATCHLIST_IMPORT_BYTES } from "./workspace-portability.mjs";
|
|
5
|
+
export const CANDIDATE_WATCH_INPUT_SCHEMA = 'whoisleuth.candidate-watch-input';
|
|
6
|
+
export const CANDIDATE_WATCH_INPUT_VERSION = 1;
|
|
7
|
+
export const CANDIDATE_WATCH_INPUT_COMPATIBILITY = defineSchemaCompatibility({
|
|
8
|
+
id: 'cli.candidate-watch-input',
|
|
9
|
+
kind: 'cli_document',
|
|
10
|
+
schema: CANDIDATE_WATCH_INPUT_SCHEMA,
|
|
11
|
+
currentVersion: 1,
|
|
12
|
+
supportedVersions: [1],
|
|
13
|
+
acceptsUnversionedLegacy: false,
|
|
14
|
+
futureVersionBehavior: 'reject',
|
|
15
|
+
migration: 'exact_current_only',
|
|
16
|
+
writeSemantics: 'read_only',
|
|
17
|
+
byteBudget: MAX_WATCHLIST_IMPORT_BYTES,
|
|
18
|
+
owner: 'packages/contracts/candidate-watch-review.mts',
|
|
19
|
+
note: 'Bounded offline analyst selection and optional portable Watchlist document; no collection or schedule authority.',
|
|
20
|
+
});
|
|
21
|
+
export const CANDIDATE_WATCH_REVIEW_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(buildExtractedLifecycleFamily({
|
|
22
|
+
id: 'candidate-watch-review',
|
|
23
|
+
owner: 'packages/contracts/candidate-watch-review.mts',
|
|
24
|
+
plane: 'node',
|
|
25
|
+
projection: 'review_output',
|
|
26
|
+
retention: 'transient_report',
|
|
27
|
+
serializerModule: 'cli/formatters/json.mts',
|
|
28
|
+
serializerExportName: 'formatJsonDocument',
|
|
29
|
+
includedCategories: ['candidate-provenance', 'analyst-watch-reasons', 'review-priorities'],
|
|
30
|
+
excludedCategories: ['raw-upstream-payloads', 'credentials', 'cookies', 'query-bearing-urls'],
|
|
31
|
+
formats: [
|
|
32
|
+
{
|
|
33
|
+
descriptor: CANDIDATE_WATCH_INPUT_COMPATIBILITY,
|
|
34
|
+
lifecycleSchema: CANDIDATE_WATCH_INPUT_SCHEMA,
|
|
35
|
+
requiredKeys: ['schema', 'version', 'watchlists', 'selection'],
|
|
36
|
+
optionalKeys: [],
|
|
37
|
+
hook: {
|
|
38
|
+
module: 'cli/watchlist-review.mts',
|
|
39
|
+
exportName: 'parseCandidateWatchInput',
|
|
40
|
+
role: 'structure_validator',
|
|
41
|
+
runtime: 'node',
|
|
42
|
+
},
|
|
43
|
+
fixtures: [
|
|
44
|
+
{
|
|
45
|
+
id: 'candidate-watch-input-v1',
|
|
46
|
+
path: 'test/fixtures/brand-candidate-workflow/candidate-watch-input-v1.json',
|
|
47
|
+
bytes: 895,
|
|
48
|
+
sha256: '40a0d73e5aa3afc3b6836bb289c79cf9556dd755a5086a9666506b31468b5d3d',
|
|
49
|
+
version: 1,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}));
|