@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,7 +1,8 @@
|
|
|
1
1
|
// Independent observed effects, linked closures and lifecycle summaries.
|
|
2
2
|
import { MAX_CASE_CLOSURES, MAX_CASE_OBSERVED_EFFECT_REVIEWS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, } from "../contracts/case-portability.mjs";
|
|
3
3
|
import { latestObservationCohort, } from "../evidence/latest-observations.mjs";
|
|
4
|
-
import { readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
|
|
4
|
+
import { readCaseRecheckAnswerContext, assertRecheckNonReproduction, COMPARATIVE_CASE_OBJECT_OUTCOMES } from "./case-recheck-model.mjs";
|
|
5
|
+
import { readCaseResponseObject, readCaseResponseObjectOutcome, assertCaseObjectOutcome, sameCaseResponseObject } from "./case-response-object.mjs";
|
|
5
6
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
7
|
import { CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, } from "./case-response-records.mjs";
|
|
7
8
|
import { COMPLETENESS, SAFE_ID_RE, boundedCounter, compareCodeUnits, freshId, iso, lifecycleLimitations, limitations, optionalIso, record, safeId, text, } from "./case-response-values.mjs";
|
|
@@ -20,6 +21,16 @@ function normalizeObservedEffectReview(raw, fallback, validPinIds, validSighting
|
|
|
20
21
|
return null;
|
|
21
22
|
const createdAt = iso(item.createdAt, observedAt || fallback, options);
|
|
22
23
|
const recheck = readCaseRecheckAnswerContext(item.recheck, options.sourceVersion);
|
|
24
|
+
const responseObject = readCaseResponseObject(item.responseObject, options.sourceVersion);
|
|
25
|
+
const objectOutcome = readCaseResponseObjectOutcome(item.objectOutcome, options.sourceVersion);
|
|
26
|
+
assertCaseObjectOutcome(objectOutcome, responseObject);
|
|
27
|
+
if (objectOutcome && (item.state === 'unavailable' || item.state === 'not_checked'))
|
|
28
|
+
throw new TypeError('Unavailable or unchecked collection cannot establish an object outcome.');
|
|
29
|
+
if (objectOutcome && COMPARATIVE_CASE_OBJECT_OUTCOMES.includes(objectOutcome)
|
|
30
|
+
&& (item.completeness !== 'complete' || !recheck?.responseObject || recheck.conditionsMatch !== 'comparable'))
|
|
31
|
+
throw new TypeError('An independently observed object state change requires complete evidence and comparable exact-object conditions.');
|
|
32
|
+
if (recheck?.responseObject && !sameCaseResponseObject(recheck.responseObject, responseObject))
|
|
33
|
+
throw new TypeError('The recheck answer and observation must concern the same exact object.');
|
|
23
34
|
if (recheck && item.state === 'not_reproduced' && (item.completeness !== 'complete' || recheck.conditionsMatch !== 'comparable')) {
|
|
24
35
|
throw new TypeError('A question cannot be marked not reproduced from incomplete evidence or unconfirmed comparison conditions.');
|
|
25
36
|
}
|
|
@@ -51,6 +62,8 @@ function normalizeObservedEffectReview(raw, fallback, validPinIds, validSighting
|
|
|
51
62
|
sightingId,
|
|
52
63
|
followUpAt: optionalIso(item.followUpAt, options),
|
|
53
64
|
...(recheck ? { recheck } : {}),
|
|
65
|
+
...(responseObject ? { responseObject } : {}),
|
|
66
|
+
...(objectOutcome ? { objectOutcome } : {}),
|
|
54
67
|
createdAt,
|
|
55
68
|
};
|
|
56
69
|
}
|
|
@@ -128,6 +141,7 @@ function normalizeClosure(raw, fallback, validReviewIds, validActionIds, options
|
|
|
128
141
|
if (!summary)
|
|
129
142
|
return null;
|
|
130
143
|
const createdAt = iso(item.createdAt, fallback, options);
|
|
144
|
+
const responseObject = readCaseResponseObject(item.responseObject, options.sourceVersion);
|
|
131
145
|
const observedEffectReviewId = typeof item.observedEffectReviewId === 'string'
|
|
132
146
|
&& SAFE_ID_RE.test(item.observedEffectReviewId)
|
|
133
147
|
&& (!validReviewIds || validReviewIds.has(item.observedEffectReviewId))
|
|
@@ -150,7 +164,8 @@ function normalizeClosure(raw, fallback, validReviewIds, validActionIds, options
|
|
|
150
164
|
return null;
|
|
151
165
|
const linkedProviderEvents = actionId ? linkContext.providerResolutionEvents?.get(actionId) ?? [] : [];
|
|
152
166
|
if (linkContext.providerResolutionEvents && reason === 'provider_reported_resolution_not_independently_checked'
|
|
153
|
-
&& !linkedProviderEvents.some((event) => Date.parse(event.occurredAt) <= Date.parse(createdAt)
|
|
167
|
+
&& !linkedProviderEvents.some((event) => Date.parse(event.occurredAt) <= Date.parse(createdAt)
|
|
168
|
+
&& (responseObject ? event.responseObjects?.some(object => sameCaseResponseObject(object, responseObject)) : !event.responseObjects?.length)))
|
|
154
169
|
return null;
|
|
155
170
|
const linkLimitations = [
|
|
156
171
|
...(item.observedEffectReviewId != null && !observedEffectReviewId ? ['A malformed or dangling observed-effect review reference was omitted from this closure.'] : []),
|
|
@@ -158,6 +173,7 @@ function normalizeClosure(raw, fallback, validReviewIds, validActionIds, options
|
|
|
158
173
|
];
|
|
159
174
|
return {
|
|
160
175
|
id: safeId(item.id, 'case-closure', { reason: item.reason, summary, createdAt }),
|
|
176
|
+
...(responseObject === undefined ? {} : { responseObject }),
|
|
161
177
|
reason,
|
|
162
178
|
summary,
|
|
163
179
|
observedEffectReviewId,
|
|
@@ -174,8 +190,11 @@ export function buildCaseClosureLinkContext(observedEffects, actions) {
|
|
|
174
190
|
createdAt: review.createdAt,
|
|
175
191
|
}])),
|
|
176
192
|
providerResolutionEvents: new Map(actions.map((action) => [action.id, action.history
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
// Reconciliation can change projection, but not the identity of a retained
|
|
194
|
+
// typed receipt supporting an already-authored historical decision.
|
|
195
|
+
.filter((event) => event.providerOutcome === 'provider_reports_resolved')
|
|
196
|
+
.map((event) => ({ eventId: event.id, occurredAt: event.occurredAt,
|
|
197
|
+
...(event.responseObjects === undefined ? {} : { responseObjects: event.responseObjects }) }))])),
|
|
179
198
|
};
|
|
180
199
|
}
|
|
181
200
|
export function normalizeCaseClosureHistory(raw, fallback, validReviewIds, validActionIds, options = {}, linkContext = {}) {
|
|
@@ -224,7 +243,9 @@ export function normalizeCaseClosureHistory(raw, fallback, validReviewIds, valid
|
|
|
224
243
|
};
|
|
225
244
|
}
|
|
226
245
|
/** New-write policy only. Historical normalisation must not re-adjudicate a closure. */
|
|
227
|
-
export function caseClosureReviewBlocker(reason, review, now) {
|
|
246
|
+
export function caseClosureReviewBlocker(reason, review, now, responseObject, evidencePins = []) {
|
|
247
|
+
if (review?.responseObject && !sameCaseResponseObject(responseObject, review.responseObject))
|
|
248
|
+
return 'This independent review concerns one object. Select that object for closure; it cannot close the whole Case.';
|
|
228
249
|
if (reason !== 'independently_not_reproduced' && reason !== 'infrastructure_changed')
|
|
229
250
|
return null;
|
|
230
251
|
const expected = reason === 'independently_not_reproduced' ? 'not_reproduced' : 'changed';
|
|
@@ -233,6 +254,18 @@ export function caseClosureReviewBlocker(reason, review, now) {
|
|
|
233
254
|
: 'This closure reason requires a linked independent changed review.';
|
|
234
255
|
if (!review || review.state !== expected)
|
|
235
256
|
return invalidLink;
|
|
257
|
+
if (responseObject) {
|
|
258
|
+
if (!sameCaseResponseObject(responseObject, review.responseObject))
|
|
259
|
+
return 'Object-specific technical closure requires a linked independent review explicitly bound to this exact object; historical missing binding remains unknown.';
|
|
260
|
+
if (!review.recheck || !sameCaseResponseObject(responseObject, review.recheck.responseObject))
|
|
261
|
+
return 'Object-specific technical closure requires a complete exact-object baseline and current review under comparable conditions.';
|
|
262
|
+
try {
|
|
263
|
+
assertRecheckNonReproduction('not_reproduced', review.recheck, review.completeness, evidencePins, evidencePins.find(pin => pin.id === review.evidencePinId), review.observedAt);
|
|
264
|
+
}
|
|
265
|
+
catch (cause) {
|
|
266
|
+
return cause instanceof Error ? cause.message : 'The exact-object review cannot support technical closure.';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
236
269
|
const observedAt = normalizeExplicitIsoTimestamp(review.observedAt);
|
|
237
270
|
const createdAt = normalizeExplicitIsoTimestamp(review.createdAt);
|
|
238
271
|
const closedAt = normalizeExplicitIsoTimestamp(now);
|
|
@@ -248,7 +281,43 @@ export function caseClosureReviewBlocker(reason, review, now) {
|
|
|
248
281
|
}
|
|
249
282
|
return null;
|
|
250
283
|
}
|
|
251
|
-
|
|
284
|
+
/** Latest applicable provider cohort, never the action-wide summary or an old receipt. */
|
|
285
|
+
export function caseClosureProviderBlocker(action, responseObject, now) {
|
|
286
|
+
const blocked = 'This closure reason requires a linked typed provider-reported-resolution outcome for the latest applicable object observation.';
|
|
287
|
+
const closedAt = normalizeExplicitIsoTimestamp(now);
|
|
288
|
+
if (!action || !closedAt)
|
|
289
|
+
return blocked;
|
|
290
|
+
if (responseObject ? !action.responseObjects?.some(object => sameCaseResponseObject(object, responseObject)) : action.responseObjects?.length)
|
|
291
|
+
return blocked;
|
|
292
|
+
// A losing workflow transition is still retained evidence. Select the
|
|
293
|
+
// exact-object cohort before checking whether its receipts were applied.
|
|
294
|
+
const events = action.history.filter(event => (event.providerOutcome !== null || event.objectOutcome !== undefined)
|
|
295
|
+
&& Date.parse(event.occurredAt) <= Date.parse(closedAt)
|
|
296
|
+
&& (responseObject ? event.responseObjects?.some(object => sameCaseResponseObject(object, responseObject)) : !event.responseObjects?.length));
|
|
297
|
+
const cohort = latestObservationCohort(events, event => event.occurredAt);
|
|
298
|
+
if (!cohort.latest.length || cohort.undated.length || cohort.latest.some(event => !event.applied || event.providerOutcome !== 'provider_reports_resolved'
|
|
299
|
+
|| event.objectOutcome === 'restored' || event.objectOutcome === 'disputed')
|
|
300
|
+
|| new Set(cohort.latest.map(event => event.objectOutcome ?? null)).size !== 1)
|
|
301
|
+
return blocked;
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
export function caseClosureActionBlocker(reason, action, responseObject, now) {
|
|
305
|
+
if (action?.responseObjects?.length && (!responseObject || !action.responseObjects.some(object => sameCaseResponseObject(object, responseObject))))
|
|
306
|
+
return 'This action concerns explicitly bound objects. Select one of them for this closure; other objects remain independent.';
|
|
307
|
+
return reason === 'provider_reported_resolution_not_independently_checked' ? caseClosureProviderBlocker(action, responseObject, now) : null;
|
|
308
|
+
}
|
|
309
|
+
/** Derived presentation only: never rewrite the analyst's summary or limitations. */
|
|
310
|
+
export function caseClosureHistoryQualification(closure, actions) {
|
|
311
|
+
if (closure.reason !== 'provider_reported_resolution_not_independently_checked')
|
|
312
|
+
return null;
|
|
313
|
+
const action = actions.find(candidate => candidate.id === closure.actionId);
|
|
314
|
+
// Evaluate the retained history, not a wall-clock freshness claim. Future-dated
|
|
315
|
+
// or conflicting receipts cannot silently make an old decision authoritative.
|
|
316
|
+
const latestAt = (action?.history ?? []).reduce((latest, event) => Date.parse(event.occurredAt) > Date.parse(latest) ? event.occurredAt : latest, closure.createdAt);
|
|
317
|
+
return caseClosureProviderBlocker(action, closure.responseObject, latestAt) === null ? null
|
|
318
|
+
: 'As of the latest retained receipt, provider history does not support a new closure for this scope. This historical analyst decision is preserved; it does not establish current remediation.';
|
|
319
|
+
}
|
|
320
|
+
export function appendCaseClosure(current, raw, now, observedEffects, actions, evidencePins = []) {
|
|
252
321
|
const item = record(raw);
|
|
253
322
|
const reason = typeof item.reason === 'string' && CLOSURE_REASONS.has(item.reason)
|
|
254
323
|
? item.reason
|
|
@@ -259,16 +328,13 @@ export function appendCaseClosure(current, raw, now, observedEffects, actions) {
|
|
|
259
328
|
const action = typeof item.actionId === 'string'
|
|
260
329
|
? actions.find((candidate) => candidate.id === item.actionId) ?? null
|
|
261
330
|
: null;
|
|
262
|
-
const
|
|
331
|
+
const responseObject = readCaseResponseObject(item.responseObject);
|
|
332
|
+
const reviewBlocker = caseClosureReviewBlocker(reason, review, now, responseObject, evidencePins);
|
|
263
333
|
if (reviewBlocker)
|
|
264
334
|
throw new Error(reviewBlocker);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
&& event.providerOutcome === 'provider_reports_resolved'
|
|
269
|
-
&& Date.parse(event.occurredAt) <= Date.parse(now)))) {
|
|
270
|
-
throw new Error('This closure reason requires a linked typed provider-reported-resolution outcome.');
|
|
271
|
-
}
|
|
335
|
+
const actionBlocker = caseClosureActionBlocker(reason, action, responseObject, now);
|
|
336
|
+
if (actionBlocker)
|
|
337
|
+
throw new Error(actionBlocker);
|
|
272
338
|
const linkContext = buildCaseClosureLinkContext(observedEffects, actions);
|
|
273
339
|
const created = normalizeClosure({ ...item, id: freshId('case-closure'), createdAt: now }, now, new Set(observedEffects.reviews.map((candidate) => candidate.id)), new Set(actions.map((candidate) => candidate.id)), {}, linkContext);
|
|
274
340
|
if (!created || !reason)
|
|
@@ -314,6 +380,7 @@ export function buildCaseResponseLifecycleSummary(input) {
|
|
|
314
380
|
? 'missing'
|
|
315
381
|
: latestObservedChangeAt ? 'available' : 'ambiguous';
|
|
316
382
|
const latestClosure = [...(input.closures?.records ?? [])]
|
|
383
|
+
.filter(closure => closure.responseObject === undefined)
|
|
317
384
|
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt) || compareCodeUnits(right.id, left.id))[0] ?? null;
|
|
318
385
|
return {
|
|
319
386
|
providerOutcomeState,
|
|
@@ -3,6 +3,7 @@ import { escapeCaseMarkdownInline as escapeMarkdown } from "./case-markdown.mjs"
|
|
|
3
3
|
/** Render only the projected packet, never the source Case or private inputs. */
|
|
4
4
|
export function renderCaseResponsePacket(packet) {
|
|
5
5
|
const { profile, case: caseRecord, incident: { category, affectedParty, abusiveUrls, observedHarm, observedAt }, generatedAt: normalizedGeneratedAt, recipientRoute, escalationHistory, escalationHistoryOmitted, escalationHistoryLimitations, authorisation, readiness, selectedEvidence, artefactReferences, responseLifecycle, preflight, provenance: { limitations, evidencePinCount, decisionCount, assertionCount, observationAge: age }, integrity: { digestSha256 }, } = packet;
|
|
6
|
+
const selectedCorrection = escalationHistory.find(action => action.actionId === packet.actionBinding.selectedActionId)?.correction;
|
|
6
7
|
const lines = [
|
|
7
8
|
`# ${escapeMarkdown(profile.label)} packet`,
|
|
8
9
|
'',
|
|
@@ -14,7 +15,7 @@ export function renderCaseResponsePacket(packet) {
|
|
|
14
15
|
`**Audience:** ${escapeMarkdown(profile.audience)}`,
|
|
15
16
|
`**Suggested subject:** ${escapeMarkdown(profile.subject)}`,
|
|
16
17
|
'',
|
|
17
|
-
'## Observed harm',
|
|
18
|
+
selectedCorrection ? '## Retained incident context (not a renewed allegation)' : '## Observed harm',
|
|
18
19
|
'',
|
|
19
20
|
escapeMarkdown(observedHarm),
|
|
20
21
|
'',
|
|
@@ -48,9 +49,14 @@ export function renderCaseResponsePacket(packet) {
|
|
|
48
49
|
...(action.outcomeDetail ? [` - Outcome detail: ${escapeMarkdown(action.outcomeDetail)}`] : []),
|
|
49
50
|
...(action.originActionId ? [` - Originating action: ${escapeMarkdown(action.originActionId)}`] : []),
|
|
50
51
|
...(action.amendment ? [` - Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}`, ` - Prepared request events: ${action.amendment.requestEventIds.map(escapeMarkdown).join(', ')}`] : []),
|
|
52
|
+
...(action.correction ? [` - ${action.correction.purpose === 'correction' ? 'Correction request' : 'Retraction request'} for delivery event ${escapeMarkdown(action.correction.deliveryEventId)}; original packet v${action.correction.packetVersion} SHA-256: ${action.correction.packetDigestSha256}`,
|
|
53
|
+
` - Analyst reason: ${escapeMarkdown(action.correction.reason)}`, ` - Previous statement: ${escapeMarkdown(action.correction.previousStatement)}`,
|
|
54
|
+
` - Corrected statement: ${escapeMarkdown(action.correction.correctedStatement || 'Request to retract the previous statement')}`,
|
|
55
|
+
` - Corrected evidence pins: ${action.correction.evidencePinIds.map(escapeMarkdown).join(', ')}`] : []),
|
|
51
56
|
...action.transitions.flatMap((event) => [
|
|
52
57
|
` - ${event.occurredAt}: ${escapeMarkdown(event.previousState ?? 'none')} → ${escapeMarkdown(event.nextState)} · ${escapeMarkdown(event.sourceClass)} · ${escapeMarkdown(event.provenance)}${event.providerOutcome ? ` · ${escapeMarkdown(event.providerOutcome.replaceAll('_', ' '))}` : ''}${event.applied ? '' : ' · retained conflict'}`,
|
|
53
58
|
...(event.reference ? [` - Reference: ${escapeMarkdown(event.reference)}`] : []),
|
|
59
|
+
...(event.packetReceipt ? [` - Exact local packet receipt: v${event.packetReceipt.packetVersion} · ${event.packetReceipt.packetDigestSha256} · ${escapeMarkdown(event.packetReceipt.recipient)} · ${escapeMarkdown(event.packetReceipt.profile)} · generated ${event.packetReceipt.packetGeneratedAt}`] : []),
|
|
54
60
|
...(event.evidencePinId ? [` - Evidence pin: ${escapeMarkdown(event.evidencePinId)}`] : []),
|
|
55
61
|
...(event.evidenceRequest ? [
|
|
56
62
|
` - Requested evidence (${event.evidenceRequest.state}): ${escapeMarkdown(event.evidenceRequest.summary)}`,
|
|
@@ -86,6 +92,10 @@ export function renderCaseResponsePacket(packet) {
|
|
|
86
92
|
'',
|
|
87
93
|
'## Provider outcome and independent effect',
|
|
88
94
|
'',
|
|
95
|
+
...escalationHistory.flatMap(action => [
|
|
96
|
+
`- Action ${escapeMarkdown(action.actionId)} object scope: ${action.responseObjects?.length ? action.responseObjects.map(object => `${escapeMarkdown(object.kind)} · ${escapeMarkdown(object.identifier)}`).join('; ') : 'unknown; no exact-object coverage inferred'}`,
|
|
97
|
+
...action.transitions.filter(event => event.objectOutcome).map(event => ` - ${escapeMarkdown(event.objectOutcome)} · ${escapeMarkdown(event.sourceClass)} · ${event.occurredAt} · affected objects: ${event.responseObjects?.map(object => `${escapeMarkdown(object.kind)} · ${escapeMarkdown(object.identifier)}`).join('; ')}${event.applied ? '' : ' · retained conflict'}`),
|
|
98
|
+
]),
|
|
89
99
|
responseLifecycle.latestProviderOutcome
|
|
90
100
|
? `- Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${escapeMarkdown(responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' '))})`
|
|
91
101
|
: `- Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
|
|
@@ -93,6 +103,7 @@ export function renderCaseResponsePacket(packet) {
|
|
|
93
103
|
? `- Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
|
|
94
104
|
: `- Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
|
|
95
105
|
...(responseLifecycle.latestObservedEffect ? [`- Latest independent review: ${escapeMarkdown(responseLifecycle.latestObservedEffect.state.replaceAll('_', ' '))} · ${responseLifecycle.latestObservedEffect.observedAt} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.source)}`] : []),
|
|
106
|
+
...(responseLifecycle.latestObservedEffect?.responseObject ? [`- Reviewed object: ${escapeMarkdown(responseLifecycle.latestObservedEffect.responseObject.kind)} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.responseObject.identifier)}${responseLifecycle.latestObservedEffect.objectOutcome ? ` · ${escapeMarkdown(responseLifecycle.latestObservedEffect.objectOutcome)}` : ''}`] : []),
|
|
96
107
|
...responseLifecycle.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
|
|
97
108
|
'',
|
|
98
109
|
'## Review and provenance',
|
|
@@ -117,22 +128,29 @@ export function renderCaseResponsePacket(packet) {
|
|
|
117
128
|
...profile.followUpFields.map((item) => `- Follow-up field: ${escapeMarkdown(item)}`),
|
|
118
129
|
];
|
|
119
130
|
const markdown = `${lines.join('\n').trim()}\n`;
|
|
131
|
+
const correction = escalationHistory.find(action => action.actionId === packet.actionBinding.selectedActionId)?.correction;
|
|
120
132
|
const email = [
|
|
121
133
|
`Subject: ${profile.subject}`,
|
|
122
134
|
'',
|
|
123
135
|
'Hello,',
|
|
124
136
|
'',
|
|
125
|
-
`I am
|
|
137
|
+
correction ? `I am requesting ${correction.purpose === 'correction' ? 'a correction' : 'retraction or withdrawal'} of a previous statement involving ${caseRecord.domain}.`
|
|
138
|
+
: `I am reporting observed ${category} activity involving ${caseRecord.domain}.`,
|
|
139
|
+
...(correction ? [`Original delivery event: ${correction.deliveryEventId}`, `Original canonical packet v${correction.packetVersion} SHA-256: ${correction.packetDigestSha256}`,
|
|
140
|
+
`Analyst reason: ${correction.reason}`, `Previous statement: ${correction.previousStatement}`,
|
|
141
|
+
`Corrected statement: ${correction.correctedStatement || 'Please retract the previous statement.'}`,
|
|
142
|
+
`Corrected evidence references: ${correction.evidencePinIds.join(', ')}`, 'This new request does not establish delivery, recipient acceptance, reversal, restoration or independent recheck.', ''] : []),
|
|
126
143
|
`Affected party: ${affectedParty}`,
|
|
127
144
|
`Observed at (UTC): ${observedAt}`,
|
|
128
145
|
'',
|
|
129
|
-
'Observed harm:',
|
|
146
|
+
correction ? 'Retained incident context (not a renewed allegation):' : 'Observed harm:',
|
|
130
147
|
observedHarm,
|
|
131
148
|
'',
|
|
132
149
|
'Exact URLs:',
|
|
133
150
|
...abusiveUrls.map((url) => `- ${url}`),
|
|
134
151
|
'',
|
|
135
152
|
'Selected evidence:',
|
|
153
|
+
...escalationHistory.flatMap(action => action.responseObjects?.length ? [`Action ${action.actionId} scope: ${action.responseObjects.map(object => `${object.kind} · ${object.identifier}`).join('; ')}`] : []),
|
|
136
154
|
...(selectedEvidence.length
|
|
137
155
|
? selectedEvidence.map((item) => `- ${item.label} — ${item.source}${item.observationHostname ? ` for ${item.observationHostname}` : ''}${item.webObservationMode ? ' · selected URL' : ''}, observed ${item.observedAt ?? 'time unavailable'} (${item.completeness})`)
|
|
138
156
|
: ['- No Case evidence pin was selected.']),
|
|
@@ -148,6 +166,7 @@ export function renderCaseResponsePacket(packet) {
|
|
|
148
166
|
: []),
|
|
149
167
|
'',
|
|
150
168
|
'Please review this report under the applicable abuse and acceptable-use policies.',
|
|
169
|
+
...limitations,
|
|
151
170
|
'',
|
|
152
171
|
authorisation.status === 'authorised'
|
|
153
172
|
? 'This locally prepared packet is bound to explicit review confirmations. It was not submitted automatically and does not promise any provider outcome.'
|