@slicedearth/whoisleuth-cli 2.3.0 → 2.4.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.
Files changed (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +83 -61
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -0,0 +1,317 @@
1
+ // Independent observed effects, linked closures and lifecycle summaries.
2
+ import { MAX_CASE_CLOSURES, MAX_CASE_OBSERVED_EFFECT_REVIEWS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, } from "../contracts/case-portability.mjs";
3
+ import { latestObservationCohort, } from "../evidence/latest-observations.mjs";
4
+ import { readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
5
+ import { CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, } from "./case-response-records.mjs";
6
+ import { COMPLETENESS, SAFE_ID_RE, boundedCounter, compareCodeUnits, freshId, iso, lifecycleLimitations, limitations, optionalIso, record, safeId, text, } from "./case-response-values.mjs";
7
+ const OBSERVED_EFFECT_STATES = new Set(CASE_OBSERVED_EFFECT_STATES);
8
+ const OBSERVED_EFFECT_SOURCE_CLASSES = new Set(CASE_OBSERVED_EFFECT_SOURCE_CLASSES);
9
+ const CLOSURE_REASONS = new Set(CASE_CLOSURE_REASONS);
10
+ function normalizeObservedEffectReview(raw, fallback, validPinIds, validSightingIds, options = {}) {
11
+ const item = record(raw);
12
+ if (typeof item.state !== 'string' || !OBSERVED_EFFECT_STATES.has(item.state))
13
+ return null;
14
+ if (typeof item.sourceClass !== 'string' || !OBSERVED_EFFECT_SOURCE_CLASSES.has(item.sourceClass))
15
+ return null;
16
+ const source = text(item.source, MAX_RESPONSE_LABEL_LENGTH);
17
+ const observedAt = optionalIso(item.observedAt, options);
18
+ if (!source || !observedAt)
19
+ return null;
20
+ const createdAt = iso(item.createdAt, observedAt || fallback, options);
21
+ const recheck = readCaseRecheckAnswerContext(item.recheck, options.sourceVersion);
22
+ if (recheck && item.state === 'not_reproduced' && (item.completeness !== 'complete' || recheck.conditionsMatch !== 'comparable')) {
23
+ throw new TypeError('A question cannot be marked not reproduced from incomplete evidence or unconfirmed comparison conditions.');
24
+ }
25
+ const evidencePinId = typeof item.evidencePinId === 'string'
26
+ && SAFE_ID_RE.test(item.evidencePinId)
27
+ && (!validPinIds || validPinIds.has(item.evidencePinId))
28
+ ? item.evidencePinId
29
+ : null;
30
+ const sightingId = typeof item.sightingId === 'string'
31
+ && SAFE_ID_RE.test(item.sightingId)
32
+ && (!validSightingIds || validSightingIds.has(item.sightingId))
33
+ ? item.sightingId
34
+ : null;
35
+ const linkLimitations = [
36
+ ...(item.evidencePinId != null && !evidencePinId ? ['A malformed or dangling evidence-pin reference was omitted from this independent review.'] : []),
37
+ ...(item.sightingId != null && !sightingId ? ['A malformed or dangling sighting reference was omitted from this independent review.'] : []),
38
+ ];
39
+ return {
40
+ id: safeId(item.id, 'effect-review', { state: item.state, source, observedAt, createdAt }),
41
+ state: item.state,
42
+ observedAt,
43
+ sourceClass: item.sourceClass,
44
+ source,
45
+ completeness: typeof item.completeness === 'string' && COMPLETENESS.has(item.completeness)
46
+ ? item.completeness
47
+ : 'unknown',
48
+ limitations: limitations([...linkLimitations, ...limitations(item.limitations)]),
49
+ evidencePinId,
50
+ sightingId,
51
+ followUpAt: optionalIso(item.followUpAt, options),
52
+ ...(recheck ? { recheck } : {}),
53
+ createdAt,
54
+ };
55
+ }
56
+ export function normalizeCaseObservedEffectHistory(raw, fallback, validPinIds, validSightingIds, options = {}) {
57
+ const root = record(raw);
58
+ const source = Array.isArray(raw) ? raw : Array.isArray(root.reviews) ? root.reviews : [];
59
+ const byId = new Map();
60
+ let invalid = 0;
61
+ let duplicateConflict = 0;
62
+ for (const item of source.slice(0, MAX_CASE_OBSERVED_EFFECT_REVIEWS * 4)) {
63
+ const normalized = normalizeObservedEffectReview(item, fallback, validPinIds, validSightingIds, options);
64
+ if (!normalized) {
65
+ invalid += 1;
66
+ continue;
67
+ }
68
+ const existing = byId.get(normalized.id);
69
+ if (!existing)
70
+ byId.set(normalized.id, normalized);
71
+ else if (JSON.stringify(normalized) !== JSON.stringify(existing)) {
72
+ duplicateConflict += 1;
73
+ if (JSON.stringify(normalized) < JSON.stringify(existing))
74
+ byId.set(normalized.id, normalized);
75
+ }
76
+ }
77
+ const all = [...byId.values()].sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt) || compareCodeUnits(left.id, right.id));
78
+ invalid += Math.max(0, source.length - MAX_CASE_OBSERVED_EFFECT_REVIEWS * 4);
79
+ const omitted = boundedCounter(root.omitted)
80
+ + invalid
81
+ + duplicateConflict
82
+ + Math.max(0, all.length - MAX_CASE_OBSERVED_EFFECT_REVIEWS);
83
+ const preV13HistoryUnavailable = options.sourceVersion != null && options.sourceVersion <= 12
84
+ ? true
85
+ : root.preV13HistoryUnavailable === true;
86
+ const retainedLimitations = limitations(root.limitations).filter((item) => !/^\d+ earlier observed-effect reviews? omitted by bounded retention\.$/u.test(item));
87
+ return {
88
+ reviews: all.slice(-MAX_CASE_OBSERVED_EFFECT_REVIEWS),
89
+ omitted,
90
+ preV13HistoryUnavailable,
91
+ limitations: lifecycleLimitations([
92
+ ...(omitted ? [`${omitted} earlier observed-effect review${omitted === 1 ? '' : 's'} omitted by bounded retention.`] : []),
93
+ ...(invalid ? [`${invalid} malformed or unlinked observed-effect review${invalid === 1 ? '' : 's'} omitted during normalisation.`] : []),
94
+ ...(duplicateConflict ? [`${duplicateConflict} conflicting observed-effect review identit${duplicateConflict === 1 ? 'y was' : 'ies were'} reconciled deterministically.`] : []),
95
+ ...(preV13HistoryUnavailable ? ['Migrated from a pre-v13 Case; earlier independent observed-effect review history is unavailable.'] : []),
96
+ 'Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.',
97
+ ...retainedLimitations,
98
+ ]),
99
+ };
100
+ }
101
+ export function appendCaseObservedEffectReview(current, raw, now, validPinIds, validSightingIds) {
102
+ const item = record(raw);
103
+ const created = normalizeObservedEffectReview({ ...item, id: freshId('effect-review'), createdAt: now }, now, validPinIds, validSightingIds);
104
+ if (!created)
105
+ throw new Error('An independent observed-effect review requires a valid state, time, source class, and source.');
106
+ return normalizeCaseObservedEffectHistory({
107
+ reviews: [...current.reviews, created],
108
+ omitted: current.omitted,
109
+ preV13HistoryUnavailable: current.preV13HistoryUnavailable,
110
+ limitations: current.limitations,
111
+ }, now, validPinIds, validSightingIds);
112
+ }
113
+ export function mergeCaseObservedEffectHistories(local, imported, fallback, validPinIds, validSightingIds) {
114
+ return normalizeCaseObservedEffectHistory({
115
+ reviews: [...local.reviews, ...imported.reviews],
116
+ omitted: Math.max(local.omitted, imported.omitted),
117
+ preV13HistoryUnavailable: local.preV13HistoryUnavailable || imported.preV13HistoryUnavailable,
118
+ limitations: [...local.limitations, ...imported.limitations],
119
+ }, fallback, validPinIds, validSightingIds);
120
+ }
121
+ function normalizeClosure(raw, fallback, validReviewIds, validActionIds, options = {}, linkContext = {}) {
122
+ const item = record(raw);
123
+ if (typeof item.reason !== 'string' || !CLOSURE_REASONS.has(item.reason))
124
+ return null;
125
+ const reason = item.reason;
126
+ const summary = text(item.summary, MAX_RESPONSE_RATIONALE_LENGTH);
127
+ if (!summary)
128
+ return null;
129
+ const createdAt = iso(item.createdAt, fallback, options);
130
+ const observedEffectReviewId = typeof item.observedEffectReviewId === 'string'
131
+ && SAFE_ID_RE.test(item.observedEffectReviewId)
132
+ && (!validReviewIds || validReviewIds.has(item.observedEffectReviewId))
133
+ ? item.observedEffectReviewId
134
+ : null;
135
+ const actionId = typeof item.actionId === 'string'
136
+ && SAFE_ID_RE.test(item.actionId)
137
+ && (!validActionIds || validActionIds.has(item.actionId))
138
+ ? item.actionId
139
+ : null;
140
+ const linkedReview = observedEffectReviewId ? linkContext.reviewEvidence?.get(observedEffectReviewId) : undefined;
141
+ const reviewPredatesClosure = linkedReview !== undefined
142
+ && Date.parse(linkedReview.observedAt) <= Date.parse(createdAt)
143
+ && Date.parse(linkedReview.createdAt) <= Date.parse(createdAt);
144
+ if (linkContext.reviewEvidence && reason === 'independently_not_reproduced'
145
+ && (!reviewPredatesClosure || linkedReview?.state !== 'not_reproduced'))
146
+ return null;
147
+ if (linkContext.reviewEvidence && reason === 'infrastructure_changed'
148
+ && (!reviewPredatesClosure || linkedReview?.state !== 'changed'))
149
+ return null;
150
+ const linkedProviderEvents = actionId ? linkContext.providerResolutionEvents?.get(actionId) ?? [] : [];
151
+ if (linkContext.providerResolutionEvents && reason === 'provider_reported_resolution_not_independently_checked'
152
+ && !linkedProviderEvents.some((event) => Date.parse(event.occurredAt) <= Date.parse(createdAt)))
153
+ return null;
154
+ const linkLimitations = [
155
+ ...(item.observedEffectReviewId != null && !observedEffectReviewId ? ['A malformed or dangling observed-effect review reference was omitted from this closure.'] : []),
156
+ ...(item.actionId != null && !actionId ? ['A malformed or dangling response-action reference was omitted from this closure.'] : []),
157
+ ];
158
+ return {
159
+ id: safeId(item.id, 'case-closure', { reason: item.reason, summary, createdAt }),
160
+ reason,
161
+ summary,
162
+ observedEffectReviewId,
163
+ actionId,
164
+ limitations: limitations([...linkLimitations, ...limitations(item.limitations)]),
165
+ createdAt,
166
+ };
167
+ }
168
+ export function buildCaseClosureLinkContext(observedEffects, actions) {
169
+ return {
170
+ reviewEvidence: new Map(observedEffects.reviews.map((review) => [review.id, {
171
+ state: review.state,
172
+ observedAt: review.observedAt,
173
+ createdAt: review.createdAt,
174
+ }])),
175
+ providerResolutionEvents: new Map(actions.map((action) => [action.id, action.history
176
+ .filter((event) => event.applied && event.providerOutcome === 'provider_reports_resolved')
177
+ .map((event) => ({ eventId: event.id, occurredAt: event.occurredAt }))])),
178
+ };
179
+ }
180
+ export function normalizeCaseClosureHistory(raw, fallback, validReviewIds, validActionIds, options = {}, linkContext = {}) {
181
+ const root = record(raw);
182
+ const source = Array.isArray(raw) ? raw : Array.isArray(root.records) ? root.records : [];
183
+ const byId = new Map();
184
+ let invalid = 0;
185
+ let duplicateConflict = 0;
186
+ for (const item of source.slice(0, MAX_CASE_CLOSURES * 4)) {
187
+ const normalized = normalizeClosure(item, fallback, validReviewIds, validActionIds, options, linkContext);
188
+ if (!normalized) {
189
+ invalid += 1;
190
+ continue;
191
+ }
192
+ const existing = byId.get(normalized.id);
193
+ if (!existing)
194
+ byId.set(normalized.id, normalized);
195
+ else if (JSON.stringify(normalized) !== JSON.stringify(existing)) {
196
+ duplicateConflict += 1;
197
+ if (JSON.stringify(normalized) < JSON.stringify(existing))
198
+ byId.set(normalized.id, normalized);
199
+ }
200
+ }
201
+ const all = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt) || compareCodeUnits(left.id, right.id));
202
+ invalid += Math.max(0, source.length - MAX_CASE_CLOSURES * 4);
203
+ const omitted = boundedCounter(root.omitted)
204
+ + invalid
205
+ + duplicateConflict
206
+ + Math.max(0, all.length - MAX_CASE_CLOSURES);
207
+ const preV13HistoryUnavailable = options.sourceVersion != null && options.sourceVersion <= 12
208
+ ? true
209
+ : root.preV13HistoryUnavailable === true;
210
+ const retainedLimitations = limitations(root.limitations).filter((item) => !/^\d+ earlier closure records? omitted by bounded retention\.$/u.test(item));
211
+ return {
212
+ records: all.slice(-MAX_CASE_CLOSURES),
213
+ omitted,
214
+ preV13HistoryUnavailable,
215
+ limitations: lifecycleLimitations([
216
+ ...(omitted ? [`${omitted} earlier closure record${omitted === 1 ? '' : 's'} omitted by bounded retention.`] : []),
217
+ ...(invalid ? [`${invalid} malformed, unsupported, or unlinked closure record${invalid === 1 ? '' : 's'} omitted during normalisation.`] : []),
218
+ ...(duplicateConflict ? [`${duplicateConflict} conflicting closure record identit${duplicateConflict === 1 ? 'y was' : 'ies were'} reconciled deterministically.`] : []),
219
+ ...(preV13HistoryUnavailable ? ['Migrated from a pre-v13 Case; earlier deliberate closure history is unavailable.'] : []),
220
+ 'Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.',
221
+ ...retainedLimitations,
222
+ ]),
223
+ };
224
+ }
225
+ export function appendCaseClosure(current, raw, now, observedEffects, actions) {
226
+ const item = record(raw);
227
+ const reason = typeof item.reason === 'string' && CLOSURE_REASONS.has(item.reason)
228
+ ? item.reason
229
+ : null;
230
+ const review = typeof item.observedEffectReviewId === 'string'
231
+ ? observedEffects.reviews.find((candidate) => candidate.id === item.observedEffectReviewId) ?? null
232
+ : null;
233
+ const action = typeof item.actionId === 'string'
234
+ ? actions.find((candidate) => candidate.id === item.actionId) ?? null
235
+ : null;
236
+ if (reason === 'independently_not_reproduced'
237
+ && (review?.state !== 'not_reproduced' || Date.parse(review.observedAt) > Date.parse(now) || Date.parse(review.createdAt) > Date.parse(now))) {
238
+ throw new Error('This closure reason requires a linked independent not-reproduced review.');
239
+ }
240
+ if (reason === 'infrastructure_changed'
241
+ && (review?.state !== 'changed' || Date.parse(review.observedAt) > Date.parse(now) || Date.parse(review.createdAt) > Date.parse(now))) {
242
+ throw new Error('This closure reason requires a linked independent changed review.');
243
+ }
244
+ if (reason === 'provider_reported_resolution_not_independently_checked'
245
+ && (action?.providerOutcome !== 'provider_reports_resolved'
246
+ || !action.history.some((event) => event.applied
247
+ && event.providerOutcome === 'provider_reports_resolved'
248
+ && Date.parse(event.occurredAt) <= Date.parse(now)))) {
249
+ throw new Error('This closure reason requires a linked typed provider-reported-resolution outcome.');
250
+ }
251
+ const linkContext = buildCaseClosureLinkContext(observedEffects, actions);
252
+ 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);
253
+ if (!created || !reason)
254
+ throw new Error('A deliberate closure requires a typed reason and summary.');
255
+ return normalizeCaseClosureHistory({
256
+ records: [...current.records, created],
257
+ omitted: current.omitted,
258
+ preV13HistoryUnavailable: current.preV13HistoryUnavailable,
259
+ limitations: current.limitations,
260
+ }, now, new Set(observedEffects.reviews.map((candidate) => candidate.id)), new Set(actions.map((candidate) => candidate.id)), {}, linkContext);
261
+ }
262
+ export function mergeCaseClosureHistories(local, imported, fallback, validReviewIds, validActionIds, linkContext = {}) {
263
+ return normalizeCaseClosureHistory({
264
+ records: [...local.records, ...imported.records],
265
+ omitted: Math.max(local.omitted, imported.omitted),
266
+ preV13HistoryUnavailable: local.preV13HistoryUnavailable || imported.preV13HistoryUnavailable,
267
+ limitations: [...local.limitations, ...imported.limitations],
268
+ }, fallback, validReviewIds, validActionIds, {}, linkContext);
269
+ }
270
+ export function buildCaseResponseLifecycleSummary(input) {
271
+ const providerEvents = (input.actions ?? []).flatMap((action) => action.history
272
+ .filter((event) => event.providerOutcome)
273
+ .map((event) => ({ action, event })));
274
+ const providerCohort = latestObservationCohort(providerEvents, ({ event }) => event.occurredAt);
275
+ const providerEventsAtLatestTime = providerCohort.latest;
276
+ const latestProvider = providerEventsAtLatestTime.length === 1 && !providerCohort.undated.length && providerEventsAtLatestTime[0].event.applied
277
+ ? providerEventsAtLatestTime[0]
278
+ : null;
279
+ const providerOutcomeState = providerEvents.length === 0
280
+ ? 'missing'
281
+ : latestProvider ? 'available' : 'ambiguous';
282
+ const reviews = input.observedEffects?.reviews ?? [];
283
+ const observedCohort = latestObservationCohort(reviews, (review) => review.observedAt);
284
+ const latestObserved = observedCohort.latest.length === 1 && !observedCohort.undated.length
285
+ ? observedCohort.latest[0]
286
+ : null;
287
+ const changedReviews = reviews.filter((review) => review.state === 'changed');
288
+ const changedCohort = latestObservationCohort(changedReviews, (review) => review.observedAt);
289
+ const latestObservedChangeAt = changedCohort.latest.length === 1 && !changedCohort.undated.length
290
+ ? changedCohort.observedAt
291
+ : null;
292
+ const observedChangeState = changedReviews.length === 0
293
+ ? 'missing'
294
+ : latestObservedChangeAt ? 'available' : 'ambiguous';
295
+ const latestClosure = [...(input.closures?.records ?? [])]
296
+ .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt) || compareCodeUnits(right.id, left.id))[0] ?? null;
297
+ return {
298
+ providerOutcomeState,
299
+ latestProviderOutcome: latestProvider ? {
300
+ actionId: latestProvider.action.id,
301
+ eventId: latestProvider.event.id,
302
+ outcome: latestProvider.event.providerOutcome,
303
+ occurredAt: latestProvider.event.occurredAt,
304
+ reference: latestProvider.event.reference,
305
+ } : null,
306
+ observedChangeState,
307
+ latestObservedEffect: latestObserved ? {
308
+ reviewId: latestObserved.id,
309
+ state: latestObserved.state,
310
+ observedAt: latestObserved.observedAt,
311
+ sourceClass: latestObserved.sourceClass,
312
+ source: latestObserved.source,
313
+ } : null,
314
+ latestObservedChangeAt,
315
+ latestClosure,
316
+ };
317
+ }
@@ -0,0 +1,153 @@
1
+ import { RESPONSE_AUTHORISATION_CONFIRMATION_IDS, responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
2
+ function escapeMarkdown(value) {
3
+ return value.replace(/([\\`*_[\]<>|])/gu, '\\$1').replace(/\r?\n/gu, ' ');
4
+ }
5
+ /** Render only the projected packet, never the source Case or private inputs. */
6
+ export function renderCaseResponsePacket(packet) {
7
+ 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;
8
+ const lines = [
9
+ `# ${escapeMarkdown(profile.label)} packet`,
10
+ '',
11
+ `**Domain:** ${escapeMarkdown(caseRecord.domain)}`,
12
+ `**Category:** ${escapeMarkdown(category)}`,
13
+ `**Affected party:** ${escapeMarkdown(affectedParty)}`,
14
+ `**Observed at (UTC):** ${observedAt}`,
15
+ `**Generated at (UTC):** ${normalizedGeneratedAt}`,
16
+ `**Audience:** ${escapeMarkdown(profile.audience)}`,
17
+ `**Suggested subject:** ${escapeMarkdown(profile.subject)}`,
18
+ '',
19
+ '## Observed harm',
20
+ '',
21
+ escapeMarkdown(observedHarm),
22
+ '',
23
+ '## Exact abusive URLs',
24
+ '',
25
+ ...abusiveUrls.map((url) => `- ${escapeMarkdown(url)}`),
26
+ '',
27
+ '## Selected response route',
28
+ '',
29
+ ...(recipientRoute
30
+ ? [
31
+ `### ${contactLabel(recipientRoute.kind)}`,
32
+ '',
33
+ `- Case action: ${escapeMarkdown(recipientRoute.actionId)}`,
34
+ `- Contact: ${escapeMarkdown(recipientRoute.contact)}`,
35
+ `- Source: ${escapeMarkdown(recipientRoute.source)}`,
36
+ `- Route observed: ${recipientRoute.observedAt ?? 'Not provided'} (${recipientRoute.freshness})`,
37
+ `- Route review after: ${recipientRoute.reviewAfter ?? 'Not provided'}`,
38
+ `- Limitations: ${recipientRoute.limitations.length ? recipientRoute.limitations.map(escapeMarkdown).join('; ') : 'None recorded'}`,
39
+ '',
40
+ ]
41
+ : ['No profile-appropriate response route was bound to this packet.', '']),
42
+ '## Selected action lineage',
43
+ '',
44
+ ...(escalationHistory.length
45
+ ? escalationHistory.flatMap((action) => [
46
+ `- ${escapeMarkdown(action.type.replaceAll('_', ' '))} to ${escapeMarkdown(action.recipient)} · ${escapeMarkdown(action.state.replaceAll('_', ' '))} · updated ${action.updatedAt}`,
47
+ ` - Route observed: ${action.routeObservedAt ?? 'Not provided'}`,
48
+ ...(action.reference ? [` - Reference: ${escapeMarkdown(action.reference)}`] : []),
49
+ ...(action.providerOutcome ? [` - Typed provider outcome: ${escapeMarkdown(action.providerOutcome.replaceAll('_', ' '))}`] : []),
50
+ ...(action.outcomeDetail ? [` - Outcome detail: ${escapeMarkdown(action.outcomeDetail)}`] : []),
51
+ ...(action.originActionId ? [` - Originating action: ${escapeMarkdown(action.originActionId)}`] : []),
52
+ ...action.transitions.flatMap((event) => [
53
+ ` - ${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'}`,
54
+ ...(event.reference ? [` - Reference: ${escapeMarkdown(event.reference)}`] : []),
55
+ ...(event.evidencePinId ? [` - Evidence pin: ${escapeMarkdown(event.evidencePinId)}`] : []),
56
+ ...(event.originActionId ? [` - Originating action: ${escapeMarkdown(event.originActionId)}`] : []),
57
+ ...event.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
58
+ ]),
59
+ ...(action.historyOmitted ? [` - ${action.historyOmitted} earlier transition event${action.historyOmitted === 1 ? '' : 's'} omitted by bound.`] : []),
60
+ ...action.historyLimitations.map((limitation) => ` - History limitation: ${escapeMarkdown(limitation)}`),
61
+ ])
62
+ : ['No Case action was selected.']),
63
+ ...(escalationHistoryOmitted ? [`- Unrelated Case actions excluded from packet: ${escalationHistoryOmitted}`] : []),
64
+ ...escalationHistoryLimitations.map((limitation) => `- Packet history limitation: ${escapeMarkdown(limitation)}`),
65
+ '',
66
+ '## Readiness and authorisation',
67
+ '',
68
+ `- Packet state: ${authorisation.status}`,
69
+ `- Reviewed-input SHA-256: ${authorisation.reviewedInputDigestSha256}`,
70
+ `- Supplied review digest matches: ${authorisation.digestMatches ? 'yes' : 'no'}`,
71
+ ...RESPONSE_AUTHORISATION_CONFIRMATION_IDS.map((id) => `- Confirmation ${id}: ${authorisation.confirmations[id] ? 'yes' : 'no'}`),
72
+ ...readiness.rows.flatMap((row) => [
73
+ `- ${escapeMarkdown(row.label)} [${row.state}]: ${escapeMarkdown(row.detail)}`,
74
+ ...row.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
75
+ ]),
76
+ ...authorisation.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
77
+ '',
78
+ '## Selected evidence and integrity references',
79
+ '',
80
+ ...(selectedEvidence.length ? selectedEvidence.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · ${escapeMarkdown(item.source)}${item.observationHostname ? ` · ${escapeMarkdown(item.observationHostname)}` : ''}${item.webObservationMode ? ' · selected URL' : ''} · ${item.observedAt ?? 'Observation time unavailable'} · ${escapeMarkdown(item.completeness)}`) : ['- No evidence pin was explicitly selected.']),
81
+ ...artefactReferences.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · SHA-256 ${item.digestSha256} · captured ${item.capturedAt}`),
82
+ '',
83
+ '## Provider outcome and independent effect',
84
+ '',
85
+ responseLifecycle.latestProviderOutcome
86
+ ? `- Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${escapeMarkdown(responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' '))})`
87
+ : `- Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
88
+ responseLifecycle.latestObservedChangeAt
89
+ ? `- Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
90
+ : `- Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
91
+ ...(responseLifecycle.latestObservedEffect ? [`- Latest independent review: ${escapeMarkdown(responseLifecycle.latestObservedEffect.state.replaceAll('_', ' '))} · ${responseLifecycle.latestObservedEffect.observedAt} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.source)}`] : []),
92
+ ...responseLifecycle.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
93
+ '',
94
+ '## Review and provenance',
95
+ '',
96
+ `- Preflight: ${preflight.status.replaceAll('_', ' ')} (${preflight.counts.pass} pass, ${preflight.counts.caution} caution, ${preflight.counts.block} block)`,
97
+ ...preflight.checks.map((check) => `- ${escapeMarkdown(check.label)} [${check.state}]: ${escapeMarkdown(check.detail)}`),
98
+ ...limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
99
+ `- Case evidence pins: ${evidencePinCount}`,
100
+ `- Case decision records: ${decisionCount}`,
101
+ `- Case structured assertions: ${assertionCount}`,
102
+ `- Observation-age band at export: ${age.band.replaceAll('_', ' ')}`,
103
+ `- Canonical packet SHA-256: ${digestSha256}`,
104
+ '- Digest scope: canonical sorted JSON packet excluding the integrity object',
105
+ '',
106
+ '## Audience profile',
107
+ '',
108
+ ...profile.checklist.map((item) => `- Checklist: ${escapeMarkdown(item)}`),
109
+ ...profile.includedEvidence.map((item) => `- Included: ${escapeMarkdown(item)}`),
110
+ ...profile.excludedEvidence.map((item) => `- Excluded: ${escapeMarkdown(item)}`),
111
+ ...profile.redactions.map((item) => `- Redaction to confirm: ${escapeMarkdown(item)}`),
112
+ ...profile.attachments.map((item) => `- Attachment expectation: ${escapeMarkdown(item)}`),
113
+ ...profile.followUpFields.map((item) => `- Follow-up field: ${escapeMarkdown(item)}`),
114
+ ];
115
+ const markdown = `${lines.join('\n').trim()}\n`;
116
+ const email = [
117
+ `Subject: ${profile.subject}`,
118
+ '',
119
+ 'Hello,',
120
+ '',
121
+ `I am reporting observed ${category} activity involving ${caseRecord.domain}.`,
122
+ `Affected party: ${affectedParty}`,
123
+ `Observed at (UTC): ${observedAt}`,
124
+ '',
125
+ 'Observed harm:',
126
+ observedHarm,
127
+ '',
128
+ 'Exact URLs:',
129
+ ...abusiveUrls.map((url) => `- ${url}`),
130
+ '',
131
+ 'Selected evidence:',
132
+ ...(selectedEvidence.length
133
+ ? selectedEvidence.map((item) => `- ${item.label} — ${item.source}${item.observationHostname ? ` for ${item.observationHostname}` : ''}${item.webObservationMode ? ' · selected URL' : ''}, observed ${item.observedAt ?? 'time unavailable'} (${item.completeness})`)
134
+ : ['- No Case evidence pin was selected.']),
135
+ '',
136
+ `Reviewed packet SHA-256: ${digestSha256}`,
137
+ 'Attach the reviewed packet (and any separately reviewed evidence files) through the recipient’s approved submission channel; this message does not embed or transmit attachments.',
138
+ '',
139
+ responseLifecycle.latestProviderOutcome
140
+ ? `Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' ')})`
141
+ : `Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
142
+ responseLifecycle.latestObservedChangeAt
143
+ ? `Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
144
+ : `Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
145
+ '',
146
+ 'Please review this report under the applicable abuse and acceptable-use policies.',
147
+ '',
148
+ authorisation.status === 'authorised'
149
+ ? 'This locally prepared packet is bound to explicit review confirmations. It was not submitted automatically and does not promise any provider outcome.'
150
+ : 'This is an unauthorised local draft with cautions. It was not submitted automatically and does not promise any provider outcome.',
151
+ ].join('\n');
152
+ return { markdown, email: `${email}\n` };
153
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ // Response packet vocabulary shared by construction and independent validation.
2
+ export const RESPONSE_CONTACT_KINDS = [
3
+ 'registrar',
4
+ 'registry',
5
+ 'network_hosting',
6
+ 'security_txt',
7
+ 'application_platform',
8
+ ];
9
+ export const RESPONSE_PACKET_PROFILE_IDS = [
10
+ 'registrar',
11
+ 'registry',
12
+ 'network_hosting',
13
+ 'security_contact',
14
+ 'application_platform',
15
+ 'browser_blocklist',
16
+ 'internal_soc',
17
+ ];
18
+ export const RESPONSE_READINESS_STATES = ['complete', 'partial', 'stale', 'unavailable', 'not_provided'];
19
+ export const RESPONSE_READINESS_ROW_IDS = [
20
+ 'observed_behaviour',
21
+ 'exact_url',
22
+ 'observation_time',
23
+ 'capture_provenance',
24
+ 'infrastructure_responsibility',
25
+ 'recipient_route',
26
+ 'authority_review',
27
+ 'selected_evidence',
28
+ 'contradictions',
29
+ 'source_limitations',
30
+ ];
31
+ export const RESPONSE_AUTHORISATION_CONFIRMATION_IDS = [
32
+ 'selectedEvidence',
33
+ 'recipientScope',
34
+ 'privacyRedactions',
35
+ 'analystAuthority',
36
+ 'evidenceFreshness',
37
+ ];
38
+ export function responseContactLabel(value) {
39
+ if (value === 'network_hosting')
40
+ return 'Observed endpoint network registration';
41
+ if (value === 'security_txt')
42
+ return 'security.txt';
43
+ if (value === 'application_platform')
44
+ return 'Application platform';
45
+ if (value === 'manual')
46
+ return 'Manual submission';
47
+ return value.charAt(0).toUpperCase() + value.slice(1);
48
+ }