@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1

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 (236) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -2,12 +2,15 @@
2
2
  // by deep availability analysis. This module does not perform network work; it
3
3
  // converts the shared safe-fetch trace and selected response metadata into an
4
4
  // additive evidence object suitable for API responses and exports.
5
- import { createObservation } from "./observation.mjs";
5
+ import { createObservation } from "../packages/evidence/observation.mjs";
6
6
  import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, MAX_HTTP_PROVENANCE_URL, } from "./http-evidence-bounds.mjs";
7
+ import { HTTP_DELIVERY_LIMITATIONS, HTTP_DELIVERY_METADATA_VERSION, MAX_HTTP_CACHE_SECONDS, MAX_HTTP_DELIVERY_HEADER_BYTES, MAX_HTTP_DELIVERY_TOKENS, } from "./homepage-metadata-contract.mjs";
7
8
  const MAX_HTTP_REDIRECTS = MAX_HTTP_EVIDENCE_REDIRECTS;
8
9
  const MAX_HTTP_HEADER_LENGTH = 1024;
10
+ const MAX_HTTP_DELIVERY_HEADER_LENGTH = MAX_HTTP_DELIVERY_HEADER_BYTES;
9
11
  const MAX_CAPTURED_BODY_BYTES = 5 * 1024 * 1024;
10
12
  const OBSERVED_HEADER_MARKER = 'observed';
13
+ const HTTP_TOKEN_RE = /^[!#$%&'*+.^_`|~0-9a-z-]+$/u;
11
14
  const SECURITY_HEADERS = Object.freeze({
12
15
  'strict-transport-security': 'strictTransportSecurity',
13
16
  'content-security-policy': 'contentSecurityPolicy',
@@ -57,6 +60,331 @@ function headerValue(headers, name, maxLength = MAX_HTTP_HEADER_LENGTH) {
57
60
  return null;
58
61
  return boundedString(headers.get(name), maxLength);
59
62
  }
63
+ function deliveryHeader(headers, name, maximum = MAX_HTTP_DELIVERY_HEADER_LENGTH) {
64
+ if (!headers || typeof headers.get !== 'function')
65
+ return { state: 'not_observed', value: null };
66
+ let raw;
67
+ try {
68
+ raw = headers.get(name);
69
+ }
70
+ catch {
71
+ return { state: 'malformed', value: null };
72
+ }
73
+ if (raw === null)
74
+ return { state: 'not_observed', value: null };
75
+ if (typeof raw !== 'string' || /[\u0000-\u001f\u007f-\u009f]/u.test(raw))
76
+ return { state: 'malformed', value: null };
77
+ if (raw.length > maximum || Buffer.byteLength(raw, 'utf8') > maximum)
78
+ return { state: 'partial', value: null };
79
+ const value = raw.trim();
80
+ return value ? { state: 'observed', value } : { state: 'malformed', value: null };
81
+ }
82
+ function parseContentEncoding(headers) {
83
+ const header = deliveryHeader(headers, 'content-encoding');
84
+ if (header.state !== 'observed' || !header.value) {
85
+ return { status: header.state, codings: [], encoded: null, unknownCodingCount: 0 };
86
+ }
87
+ const supplied = header.value.split(',').map((item) => item.trim().toLowerCase());
88
+ const malformed = supplied.some((item) => !item || !HTTP_TOKEN_RE.test(item));
89
+ const truncated = supplied.length > MAX_HTTP_DELIVERY_TOKENS;
90
+ if (malformed)
91
+ return { status: 'malformed', codings: [], encoded: null, unknownCodingCount: 0 };
92
+ if (truncated)
93
+ return { status: 'partial', codings: [], encoded: null, unknownCodingCount: 0 };
94
+ const unknownCodingCount = supplied.filter((coding) => !['gzip', 'br', 'deflate', 'zstd', 'identity'].includes(coding)).length;
95
+ const codings = [...new Set(supplied.map((coding) => (['gzip', 'br', 'deflate', 'zstd', 'identity'].includes(coding) ? coding : 'other')))].sort();
96
+ if (codings.includes('identity') && codings.length > 1) {
97
+ return { status: 'malformed', codings: [], encoded: null, unknownCodingCount: 0 };
98
+ }
99
+ return {
100
+ status: 'observed',
101
+ codings,
102
+ encoded: codings.some((coding) => coding !== 'identity'),
103
+ unknownCodingCount,
104
+ };
105
+ }
106
+ function splitCacheControl(value) {
107
+ const directives = [];
108
+ let current = '';
109
+ let quoted = false;
110
+ let escaped = false;
111
+ for (const character of value) {
112
+ if (escaped) {
113
+ current += character;
114
+ escaped = false;
115
+ continue;
116
+ }
117
+ if (quoted && character === '\\') {
118
+ current += character;
119
+ escaped = true;
120
+ continue;
121
+ }
122
+ if (character === '"') {
123
+ quoted = !quoted;
124
+ current += character;
125
+ continue;
126
+ }
127
+ if (character === ',' && !quoted) {
128
+ directives.push(current.trim());
129
+ current = '';
130
+ continue;
131
+ }
132
+ current += character;
133
+ }
134
+ directives.push(current.trim());
135
+ return {
136
+ directives: directives.slice(0, MAX_HTTP_DELIVERY_TOKENS),
137
+ truncated: directives.length > MAX_HTTP_DELIVERY_TOKENS,
138
+ malformed: quoted || escaped || directives.some((item) => !item),
139
+ };
140
+ }
141
+ function cacheSeconds(value, allowQuoted = true) {
142
+ if (value === null)
143
+ return null;
144
+ const quoted = value.startsWith('"') && value.endsWith('"');
145
+ if (quoted && !allowQuoted)
146
+ return null;
147
+ const unquoted = quoted ? value.slice(1, -1) : value;
148
+ if (!/^\d+$/u.test(unquoted))
149
+ return null;
150
+ const parsed = Number(unquoted);
151
+ return Number.isSafeInteger(parsed) && parsed >= 0 && parsed <= MAX_HTTP_CACHE_SECONDS ? parsed : null;
152
+ }
153
+ function validCacheFieldNameList(value) {
154
+ if (value === null)
155
+ return true;
156
+ if (value.length < 3 || value[0] !== '"' || value.at(-1) !== '"')
157
+ return false;
158
+ const fields = value.slice(1, -1).split(',').map((field) => field.trim());
159
+ return fields.length > 0 && fields.every((field) => Boolean(field) && HTTP_TOKEN_RE.test(field));
160
+ }
161
+ function headerValidity(header, validator) {
162
+ if (header.state === 'not_observed')
163
+ return { present: false, valid: null };
164
+ if (header.state === 'partial')
165
+ return { present: true, valid: null };
166
+ return { present: true, valid: header.state === 'observed' && Boolean(header.value && validator(header.value)) };
167
+ }
168
+ function validHttpDate(value) {
169
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
170
+ const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
171
+ const longWeekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
172
+ const time = '([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d)';
173
+ const imfFixdate = value.match(new RegExp(`^(${weekdays.join('|')}), (0[1-9]|[12]\\d|3[01]) (${months.join('|')}) (\\d{4}) ${time} GMT$`, 'u'));
174
+ const rfc850 = value.match(new RegExp(`^(${longWeekdays.join('|')}), (0[1-9]|[12]\\d|3[01])-(${months.join('|')})-(\\d{2}) ${time} GMT$`, 'u'));
175
+ const asctime = value.match(new RegExp(`^(${weekdays.join('|')}) (${months.join('|')}) ( {2}[1-9]| [12]\\d| 3[01]) ${time} (\\d{4})$`, 'u'));
176
+ let weekday;
177
+ let day;
178
+ let month;
179
+ let year;
180
+ let hour;
181
+ let minute;
182
+ let second;
183
+ let expectedWeekdays;
184
+ if (imfFixdate) {
185
+ weekday = imfFixdate[1];
186
+ day = Number(imfFixdate[2]);
187
+ month = months.indexOf(imfFixdate[3]);
188
+ year = Number(imfFixdate[4]);
189
+ hour = Number(imfFixdate[5]);
190
+ minute = Number(imfFixdate[6]);
191
+ second = Number(imfFixdate[7]);
192
+ expectedWeekdays = weekdays;
193
+ }
194
+ else if (rfc850) {
195
+ weekday = rfc850[1];
196
+ day = Number(rfc850[2]);
197
+ month = months.indexOf(rfc850[3]);
198
+ const shortYear = Number(rfc850[4]);
199
+ year = shortYear >= 70 ? 1900 + shortYear : 2000 + shortYear;
200
+ hour = Number(rfc850[5]);
201
+ minute = Number(rfc850[6]);
202
+ second = Number(rfc850[7]);
203
+ expectedWeekdays = longWeekdays;
204
+ }
205
+ else if (asctime) {
206
+ weekday = asctime[1];
207
+ month = months.indexOf(asctime[2]);
208
+ day = Number(asctime[3].trim());
209
+ hour = Number(asctime[4]);
210
+ minute = Number(asctime[5]);
211
+ second = Number(asctime[6]);
212
+ year = Number(asctime[7]);
213
+ expectedWeekdays = weekdays;
214
+ }
215
+ else
216
+ return false;
217
+ if (year < 1601 || month < 0)
218
+ return false;
219
+ const parsed = new Date(0);
220
+ parsed.setUTCFullYear(year, month, day);
221
+ parsed.setUTCHours(hour, minute, second, 0);
222
+ return parsed.getUTCFullYear() === year
223
+ && parsed.getUTCMonth() === month
224
+ && parsed.getUTCDate() === day
225
+ && expectedWeekdays[parsed.getUTCDay()] === weekday;
226
+ }
227
+ function parseCachePolicy(headers) {
228
+ const cacheControl = deliveryHeader(headers, 'cache-control');
229
+ const age = deliveryHeader(headers, 'age', 64);
230
+ const etagHeader = deliveryHeader(headers, 'etag');
231
+ const lastModifiedHeader = deliveryHeader(headers, 'last-modified');
232
+ const expiresHeader = deliveryHeader(headers, 'expires');
233
+ const parsed = cacheControl.state === 'observed' && cacheControl.value
234
+ ? splitCacheControl(cacheControl.value)
235
+ : { directives: [], truncated: false, malformed: false };
236
+ const flags = {
237
+ noStore: false,
238
+ noCache: false,
239
+ mustRevalidate: false,
240
+ public: false,
241
+ private: false,
242
+ immutable: false,
243
+ };
244
+ let maxAgeSeconds = null;
245
+ let sMaxAgeSeconds = null;
246
+ let maxAgeSeen = false;
247
+ let sMaxAgeSeen = false;
248
+ let maxAgeMalformed = false;
249
+ let sMaxAgeMalformed = false;
250
+ let unknownDirectiveCount = 0;
251
+ let malformed = parsed.malformed;
252
+ for (const directive of parsed.truncated ? [] : parsed.directives) {
253
+ const separator = directive.indexOf('=');
254
+ const name = (separator === -1 ? directive : directive.slice(0, separator)).trim().toLowerCase();
255
+ const value = separator === -1 ? null : directive.slice(separator + 1).trim();
256
+ if (!HTTP_TOKEN_RE.test(name)) {
257
+ malformed = true;
258
+ continue;
259
+ }
260
+ if (name === 'no-store') {
261
+ flags.noStore = true;
262
+ if (value !== null)
263
+ malformed = true;
264
+ }
265
+ else if (name === 'no-cache') {
266
+ flags.noCache = true;
267
+ if (!validCacheFieldNameList(value))
268
+ malformed = true;
269
+ }
270
+ else if (name === 'must-revalidate') {
271
+ flags.mustRevalidate = true;
272
+ if (value !== null)
273
+ malformed = true;
274
+ }
275
+ else if (name === 'public') {
276
+ flags.public = true;
277
+ if (value !== null)
278
+ malformed = true;
279
+ }
280
+ else if (name === 'private') {
281
+ flags.private = true;
282
+ if (!validCacheFieldNameList(value))
283
+ malformed = true;
284
+ }
285
+ else if (name === 'immutable') {
286
+ flags.immutable = true;
287
+ if (value !== null)
288
+ malformed = true;
289
+ }
290
+ else if (name === 'max-age') {
291
+ const parsedSeconds = cacheSeconds(value);
292
+ if (parsedSeconds === null || maxAgeSeen && parsedSeconds !== maxAgeSeconds) {
293
+ malformed = true;
294
+ maxAgeMalformed = true;
295
+ maxAgeSeconds = null;
296
+ }
297
+ else if (!maxAgeMalformed)
298
+ maxAgeSeconds = parsedSeconds;
299
+ maxAgeSeen = true;
300
+ }
301
+ else if (name === 's-maxage') {
302
+ const parsedSeconds = cacheSeconds(value);
303
+ if (parsedSeconds === null || sMaxAgeSeen && parsedSeconds !== sMaxAgeSeconds) {
304
+ malformed = true;
305
+ sMaxAgeMalformed = true;
306
+ sMaxAgeSeconds = null;
307
+ }
308
+ else if (!sMaxAgeMalformed)
309
+ sMaxAgeSeconds = parsedSeconds;
310
+ sMaxAgeSeen = true;
311
+ }
312
+ else
313
+ unknownDirectiveCount += 1;
314
+ }
315
+ if (parsed.truncated) {
316
+ flags.noStore = false;
317
+ flags.noCache = false;
318
+ flags.mustRevalidate = false;
319
+ flags.public = false;
320
+ flags.private = false;
321
+ flags.immutable = false;
322
+ maxAgeSeconds = null;
323
+ sMaxAgeSeconds = null;
324
+ maxAgeSeen = false;
325
+ sMaxAgeSeen = false;
326
+ unknownDirectiveCount = 0;
327
+ }
328
+ const ageSeconds = age.state === 'observed' ? cacheSeconds(age.value, false) : null;
329
+ if (age.state === 'observed' && ageSeconds === null)
330
+ malformed = true;
331
+ const etag = headerValidity(etagHeader, (value) => /^(?:W\/)?"[\x21\x23-\x7e]*"$/u.test(value));
332
+ const lastModified = headerValidity(lastModifiedHeader, validHttpDate);
333
+ const expires = headerValidity(expiresHeader, validHttpDate);
334
+ malformed = malformed || [etag, lastModified, expires].some((item) => item.present && item.valid === false);
335
+ const headersObserved = [cacheControl, age, etagHeader, lastModifiedHeader, expiresHeader]
336
+ .some((header) => header.state !== 'not_observed');
337
+ const partial = parsed.truncated || [cacheControl, age, etagHeader, lastModifiedHeader, expiresHeader]
338
+ .some((header) => header.state === 'partial');
339
+ malformed = malformed || [cacheControl, age, etagHeader, lastModifiedHeader, expiresHeader]
340
+ .some((header) => header.state === 'malformed');
341
+ const status = partial
342
+ ? 'partial'
343
+ : malformed
344
+ ? 'malformed'
345
+ : headersObserved ? 'observed' : 'not_observed';
346
+ return {
347
+ value: {
348
+ status,
349
+ ...flags,
350
+ maxAgeSeconds,
351
+ sMaxAgeSeconds,
352
+ ageSeconds,
353
+ maxAgePresent: maxAgeSeen,
354
+ sMaxAgePresent: sMaxAgeSeen,
355
+ agePresent: age.state !== 'not_observed',
356
+ unknownDirectiveCount,
357
+ etag,
358
+ lastModified,
359
+ expires,
360
+ },
361
+ malformed,
362
+ truncated: partial,
363
+ };
364
+ }
365
+ function buildDeliveryMetadata(headers) {
366
+ const contentEncoding = parseContentEncoding(headers);
367
+ const cacheResult = parseCachePolicy(headers);
368
+ const cachePolicy = cacheResult.value;
369
+ const incomplete = [contentEncoding.status, cachePolicy.status].some((status) => ['partial', 'malformed'].includes(status));
370
+ const truncated = contentEncoding.status === 'partial' || cacheResult.truncated;
371
+ const limitations = [
372
+ HTTP_DELIVERY_LIMITATIONS.scope,
373
+ ];
374
+ if (truncated)
375
+ limitations.push(HTTP_DELIVERY_LIMITATIONS.bounds);
376
+ if (contentEncoding.status === 'malformed' || cacheResult.malformed)
377
+ limitations.push(HTTP_DELIVERY_LIMITATIONS.malformed);
378
+ return {
379
+ version: HTTP_DELIVERY_METADATA_VERSION,
380
+ status: incomplete ? 'partial' : 'success',
381
+ complete: !incomplete,
382
+ truncated,
383
+ limitations,
384
+ contentEncoding,
385
+ cachePolicy,
386
+ };
387
+ }
60
388
  function declaredContentLength(headers) {
61
389
  const raw = headerValue(headers, 'content-length', 32);
62
390
  if (!raw || !/^\d+$/.test(raw))
@@ -178,6 +506,7 @@ function buildHttpObservation(detail, options = {}) {
178
506
  bytes: capturedBodyBytes,
179
507
  } : null,
180
508
  securityHeaders: securityHeaders(response && response.headers),
509
+ deliveryMetadata: buildDeliveryMetadata(response && response.headers),
181
510
  },
182
511
  };
183
512
  }
@@ -227,4 +556,4 @@ function skippedHttpObservation() {
227
556
  response: null,
228
557
  };
229
558
  }
230
- export { MAX_HTTP_ERROR_LENGTH, MAX_HTTP_PROVENANCE_URL, MAX_HTTP_REDIRECTS, MAX_HTTP_ATTEMPTS, normalizeProvenanceUrl, buildHttpObservation, failedHttpObservation, skippedHttpObservation, };
559
+ export { MAX_HTTP_ERROR_LENGTH, MAX_HTTP_PROVENANCE_URL, MAX_HTTP_REDIRECTS, MAX_HTTP_ATTEMPTS, HTTP_DELIVERY_METADATA_VERSION, MAX_HTTP_DELIVERY_HEADER_LENGTH, MAX_HTTP_DELIVERY_TOKENS, normalizeProvenanceUrl, buildHttpObservation, failedHttpObservation, skippedHttpObservation, };
@@ -1,9 +1,24 @@
1
+ import { DOMAIN_CONTROL_MANIFEST_COMPATIBILITY } from "../packages/contracts/domain-control-manifest.mjs";
2
+ import { CLI_CASE_PACK_COMPATIBILITY, ENCRYPTED_WORKSPACE_ARCHIVE_COMPATIBILITY, WORKSPACE_ARCHIVE_COMPATIBILITY, } from "../packages/contracts/case-portability.mjs";
3
+ import { LOOKUP_CLAIM_PASSPORT_COMPATIBILITY } from "../packages/contracts/investigation-portability.mjs";
4
+ import { BRAND_PROFILE_EXPORT_COMPATIBILITY } from "../packages/contracts/workspace-portability.mjs";
5
+ import { LOOKUP_EVIDENCE_COMPATIBILITY } from "./evidence-export.mjs";
1
6
  const exact = Object.freeze(['domain_identity', 'dns_control_expectations', 'certificate_control_expectations', 'registrar_control_expectations', 'renewal_review']);
7
+ function compatibilityReference(descriptor) {
8
+ if (!descriptor.schema || descriptor.futureVersionBehavior !== 'reject') {
9
+ throw new TypeError(`Interchange compatibility ${descriptor.id} must name a schema and reject future versions.`);
10
+ }
11
+ return Object.freeze({
12
+ compatibilityEntryId: descriptor.id,
13
+ schema: descriptor.schema,
14
+ versions: descriptor.supportedVersions,
15
+ futureVersionBehaviour: 'reject',
16
+ });
17
+ }
2
18
  export const INTERCHANGE_ARTIFACT_CONTRACTS = Object.freeze([
3
19
  Object.freeze({
4
20
  id: 'lookup_claim_passport',
5
- schema: 'whoisleuth.lookup-claim-passport',
6
- versions: Object.freeze([1]),
21
+ ...compatibilityReference(LOOKUP_CLAIM_PASSPORT_COMPATIBILITY),
7
22
  versionField: 'version',
8
23
  nestedSchemaPath: Object.freeze([]),
9
24
  browser: Object.freeze({ import: 'unsupported', export: 'supported' }),
@@ -12,12 +27,10 @@ export const INTERCHANGE_ARTIFACT_CONTRACTS = Object.freeze([
12
27
  requiredAssurance: 'whole_integrity',
13
28
  preservedFieldGroups: Object.freeze(['target_identity', 'claim_identity', 'evidence_requirement_ids', 'source_states', 'observation_times', 'model_versions', 'limitations']),
14
29
  excludedFieldGroups: Object.freeze(['raw_registry_payloads', 'contacts', 'page_values', 'request_paths', 'credentials', 'browser_local_records', 'signer_authentication']),
15
- futureVersionBehaviour: 'reject',
16
30
  }),
17
31
  Object.freeze({
18
32
  id: 'lookup_evidence',
19
- schema: LOOKUP_EVIDENCE_SCHEMA,
20
- versions: Object.freeze([...SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS]),
33
+ ...compatibilityReference(LOOKUP_EVIDENCE_COMPATIBILITY),
21
34
  versionField: 'schemaVersion',
22
35
  nestedSchemaPath: Object.freeze([]),
23
36
  browser: Object.freeze({ import: 'supported', export: 'supported' }),
@@ -26,26 +39,22 @@ export const INTERCHANGE_ARTIFACT_CONTRACTS = Object.freeze([
26
39
  requiredAssurance: 'structure',
27
40
  preservedFieldGroups: Object.freeze(['query_context', 'generator_metadata', 'source_health', 'normalised_registration_evidence', 'bounded_replay_facts', 'observation_times', 'limitations']),
28
41
  excludedFieldGroups: Object.freeze(['browser_local_records', 'case_context', 'brand_profiles', 'credentials', 'session_material', 'request_headers', 'cookies', 'signer_authentication', 'raw_payload_rendering_during_replay']),
29
- futureVersionBehaviour: 'reject',
30
42
  }),
31
43
  Object.freeze({
32
44
  id: 'domain_control_passport',
33
- schema: 'whoisleuth.domain-control-manifest',
34
- versions: Object.freeze([1, 2]),
45
+ ...compatibilityReference(DOMAIN_CONTROL_MANIFEST_COMPATIBILITY),
35
46
  versionField: 'version',
36
47
  nestedSchemaPath: Object.freeze([]),
37
48
  browser: Object.freeze({ import: 'supported', export: 'supported' }),
38
49
  cli: Object.freeze({ read: 'supported', write: 'supported', verify: 'supported' }),
39
- fidelity: 'semantic_exact_after_normalisation',
50
+ fidelity: 'normalised_merge',
40
51
  requiredAssurance: 'whole_integrity',
41
52
  preservedFieldGroups: exact,
42
53
  excludedFieldGroups: Object.freeze(['profile_identity', 'brand_context', 'contacts', 'notes', 'observations', 'planning_context', 'suppression_context']),
43
- futureVersionBehaviour: 'reject',
44
54
  }),
45
55
  Object.freeze({
46
56
  id: 'brand_profiles',
47
- schema: 'whoisleuth.brand-profiles',
48
- versions: Object.freeze([2, 3, 4, 5, 6]),
57
+ ...compatibilityReference(BRAND_PROFILE_EXPORT_COMPATIBILITY),
49
58
  versionField: 'version',
50
59
  nestedSchemaPath: Object.freeze([]),
51
60
  browser: Object.freeze({ import: 'supported', export: 'supported' }),
@@ -54,26 +63,22 @@ export const INTERCHANGE_ARTIFACT_CONTRACTS = Object.freeze([
54
63
  requiredAssurance: 'structure',
55
64
  preservedFieldGroups: Object.freeze(['profile_identity', 'brand_context', 'domain_scope', 'mail_posture', 'desired_posture', 'reviewed_planning', 'retained_observations']),
56
65
  excludedFieldGroups: Object.freeze(['login_sessions', 'credentials', 'raw_upstream_payloads']),
57
- futureVersionBehaviour: 'reject',
58
66
  }),
59
67
  Object.freeze({
60
68
  id: 'workspace',
61
- schema: 'whoisleuth.workspace-archive',
62
- versions: Object.freeze([1, 2, 3, 4, 5]),
69
+ ...compatibilityReference(WORKSPACE_ARCHIVE_COMPATIBILITY),
63
70
  versionField: 'version',
64
71
  nestedSchemaPath: Object.freeze([]),
65
72
  browser: Object.freeze({ import: 'supported', export: 'supported' }),
66
73
  cli: Object.freeze({ read: 'verification_only', write: 'unsupported', verify: 'supported' }),
67
74
  fidelity: 'normalised_merge',
68
- requiredAssurance: 'whole_integrity',
69
- preservedFieldGroups: Object.freeze(['supported_workspace_sections', 'section_versions', 'section_checksums', 'merge_metadata']),
70
- excludedFieldGroups: Object.freeze(['login_sessions', 'credentials', 'hosted_monitor_keys', 'raw_upstream_payloads', 'tab_state']),
71
- futureVersionBehaviour: 'reject',
75
+ requiredAssurance: 'applicable_integrity',
76
+ preservedFieldGroups: Object.freeze(['supported_workspace_sections', 'section_versions', 'section_checksums', 'merge_metadata', 'analyst_review_lifecycle']),
77
+ excludedFieldGroups: Object.freeze(['login_sessions', 'credentials', 'hosted_monitor_keys', 'raw_upstream_payloads', 'tab_state', 'certificate_transparency_history']),
72
78
  }),
73
79
  Object.freeze({
74
80
  id: 'encrypted_workspace',
75
- schema: 'whoisleuth.encrypted-workspace-archive',
76
- versions: Object.freeze([1]),
81
+ ...compatibilityReference(ENCRYPTED_WORKSPACE_ARCHIVE_COMPATIBILITY),
77
82
  versionField: 'version',
78
83
  nestedSchemaPath: Object.freeze([]),
79
84
  browser: Object.freeze({ import: 'supported', export: 'supported' }),
@@ -82,12 +87,10 @@ export const INTERCHANGE_ARTIFACT_CONTRACTS = Object.freeze([
82
87
  requiredAssurance: 'authenticated_whole_integrity',
83
88
  preservedFieldGroups: Object.freeze(['encrypted_workspace_envelope', 'authenticated_ciphertext', 'inner_workspace_after_decryption']),
84
89
  excludedFieldGroups: Object.freeze(['passphrase', 'derived_key']),
85
- futureVersionBehaviour: 'reject',
86
90
  }),
87
91
  Object.freeze({
88
92
  id: 'case_pack',
89
- schema: 'whoisleuth.cli.case-pack',
90
- versions: Object.freeze([1, 2]),
93
+ ...compatibilityReference(CLI_CASE_PACK_COMPATIBILITY),
91
94
  versionField: 'version',
92
95
  nestedSchemaPath: Object.freeze(['packet']),
93
96
  browser: Object.freeze({ import: 'supported', export: 'unsupported' }),
@@ -96,10 +99,10 @@ export const INTERCHANGE_ARTIFACT_CONTRACTS = Object.freeze([
96
99
  requiredAssurance: 'whole_integrity',
97
100
  preservedFieldGroups: Object.freeze(['redacted_case_collection', 'case_schema_version', 'trusted_internal_case_brand_profile_references']),
98
101
  excludedFieldGroups: Object.freeze(['package_reports', 'package_redaction_manifest', 'package_integrity_envelope', 'audience_excluded_case_fields', 'public_audience_case_brand_profile_references']),
99
- futureVersionBehaviour: 'reject',
100
102
  }),
101
103
  Object.freeze({
102
104
  id: 'legacy_desired_baseline',
105
+ compatibilityEntryId: null,
103
106
  schema: 'whoisleuth.desired-posture-baseline',
104
107
  versions: Object.freeze([1]),
105
108
  versionField: 'schemaVersion',
@@ -119,4 +122,3 @@ export function interchangeContractFor(id) {
119
122
  throw new TypeError('Interchange contract is not registered.');
120
123
  return contract;
121
124
  }
122
- import { LOOKUP_EVIDENCE_SCHEMA, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "./evidence-export.mjs";
@@ -18,10 +18,12 @@ const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
18
18
  // RDAP/WHOIS response, before the next sweep has a chance to run; repeated
19
19
  // or concurrent large scans compound that. `Map` preserves insertion
20
20
  // order, so the oldest entry is always `store.keys().next().value` -
21
- // simple FIFO eviction, not a full LRU (re-caching an existing key doesn't
22
- // move it), which is a fine fit for a cache whose primary job is
23
- // deduplicating near-simultaneous repeat lookups within a few minutes, not
24
- // long-term hit-rate optimization.
21
+ // simple FIFO eviction by expiry, not a full LRU (a cache *hit* doesn't
22
+ // move an entry). Re-caching a key does move it, because setCached deletes
23
+ // the key before re-inserting it, which keeps insertion order aligned with
24
+ // the refreshed expiresAt. That is a fine fit for a cache whose primary job
25
+ // is deduplicating near-simultaneous repeat lookups within a few minutes,
26
+ // not long-term hit-rate optimization.
25
27
  const MAX_ENTRIES = 3000;
26
28
  // MAX_ENTRIES alone bounds count, not size - it doesn't stop a hostile or
27
29
  // compromised registry from serving a near-maximum-size response (RDAP:
package/lib/lookup.mjs CHANGED
@@ -21,6 +21,7 @@ import { lookupUrlhausDomain } from "./urlhaus-intelligence.mjs";
21
21
  import { lookupThreatfoxDomain } from "./threatfox-intelligence.mjs";
22
22
  import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
23
23
  import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
24
+ import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
24
25
  import { buildRegistryInsights } from "./registry-insights.mjs";
25
26
  import { inspectSslblCertificate } from "./sslbl-intelligence.mjs";
26
27
  import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "./lookup-source-progress.mjs";
@@ -67,6 +68,24 @@ function boundedSourceDetail(err, fallback) {
67
68
  .trim()
68
69
  .slice(0, 240) || fallback;
69
70
  }
71
+ function withoutNestedPublicationMetadata(value) {
72
+ if (!value || typeof value !== 'object' || Array.isArray(value))
73
+ return value;
74
+ const output = { ...value };
75
+ delete output.publicationMetadata;
76
+ return output;
77
+ }
78
+ function withoutNestedDeliveryMetadata(value) {
79
+ if (!value || typeof value !== 'object' || Array.isArray(value))
80
+ return value;
81
+ const output = { ...value };
82
+ if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
83
+ const response = { ...output.response };
84
+ delete response.deliveryMetadata;
85
+ output.response = response;
86
+ }
87
+ return output;
88
+ }
70
89
  function boundedTimingMs(value) {
71
90
  if (!Number.isFinite(value))
72
91
  return 0;
@@ -167,6 +186,8 @@ async function runUnifiedLookup(classified, options = {}) {
167
186
  includeExtendedDnsContext: !compact,
168
187
  includeInheritedCaa: !fast && !compact,
169
188
  includeCredentialSurfaceProfile: !fast && !compact,
189
+ includePublicationMetadata: !fast && !compact,
190
+ includeDeliveryMetadata: !fast && !compact,
170
191
  includeStructuredDataIdentity: !fast && !compact,
171
192
  includeTechnologyProfile: !fast,
172
193
  includeSecurityPosture: !compact,
@@ -528,10 +549,12 @@ async function runUnifiedLookup(classified, options = {}) {
528
549
  const bulkComparison = !fast
529
550
  ? buildBulkComparisonEvidence(availability)
530
551
  : null;
531
- const { credentialSurfaceProfile: _credentialSurfaceProfile, structuredDataIdentity: _structuredDataIdentity, technologyProfile: _technologyProfile, pageRoleProfile: _pageRoleProfile, clientBehaviorProfile: _clientBehaviorProfile, securityPosture: _securityPosture, ...compactAvailability } = availability;
552
+ const { credentialSurfaceProfile: _credentialSurfaceProfile, structuredDataIdentity: _structuredDataIdentity, technologyProfile: _technologyProfile, pageRoleProfile: _pageRoleProfile, clientBehaviorProfile: _clientBehaviorProfile, securityPosture: _securityPosture, pageIdentity: richPageIdentity, http: richHttp, ...compactAvailability } = availability;
532
553
  return {
533
554
  availability: {
534
555
  ...compactAvailability,
556
+ ...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
557
+ ...(richHttp !== undefined ? { http: withoutNestedDeliveryMetadata(richHttp) } : {}),
535
558
  ...(bulkComparison ? { bulkComparison } : {}),
536
559
  },
537
560
  diagnostics,
@@ -555,7 +578,7 @@ async function runUnifiedLookup(classified, options = {}) {
555
578
  : createThreatIntelligenceResult(THREATFOX_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-IOC intelligence could not be completed.' }));
556
579
  }
557
580
  const threatIntelligence = threatIntelligenceProviders.length
558
- ? { version: 1, providers: threatIntelligenceProviders }
581
+ ? { version: THREAT_INTELLIGENCE_ENVELOPE_VERSION, providers: threatIntelligenceProviders }
559
582
  : null;
560
583
  const registrarRdapParsed = registrarRdap
561
584
  && typeof registrarRdap.parsed === 'object'
@@ -4,7 +4,7 @@
4
4
  // evidence. It performs one logical IP RDAP enrichment through the existing
5
5
  // cached, SSRF-safe RDAP boundary and never participates in domain availability.
6
6
  import * as net from 'node:net';
7
- import { createObservation } from "./observation.mjs";
7
+ import { createObservation } from "../packages/evidence/observation.mjs";
8
8
  import { fetchRdapRecord } from "./rdap.mjs";
9
9
  import { isPrivateAddress } from "./safe-fetch.mjs";
10
10
  const OBSERVED_NETWORK_CONTEXT_VERSION = 1;
@@ -13,8 +13,8 @@ const MAX_FORM_FINGERPRINTS = 50;
13
13
  const MAX_FORM_CONTROLS = 500;
14
14
  const MAX_RESOURCE_HOSTS = 30;
15
15
  const MAX_IDENTIFIER_VALUES = 30;
16
- const CONTROL_RE = /[\u0000-\u001f\u007f]/g;
17
- const HAS_CONTROL_RE = /[\u0000-\u001f\u007f]/;
16
+ const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
17
+ const HAS_CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
18
18
  const COMMENT_RE = /<!--[\s\S]*?(?:-->|$)/g;
19
19
  const RAW_BODY_RE = /<(script|style|noscript|textarea|template)\b([^>]*)>[\s\S]*?(?:<\/\1\s*>|$)/gi;
20
20
  const MARKUP_TOKEN_RE = /<\/?[A-Za-z][A-Za-z0-9:-]*\b[^>]*>|[^<]+/g;
@@ -2,7 +2,7 @@
2
2
  // captured by Deep Lookup. The classifier emits fixed labels and evidence
3
3
  // descriptions only; it does not retain page text, arbitrary attributes, URLs,
4
4
  // or matched markup, and it never treats a role as proof of intent or safety.
5
- import { createObservation } from "./observation.mjs";
5
+ import { createObservation } from "../packages/evidence/observation.mjs";
6
6
  import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
7
7
  const PAGE_ROLE_PROFILE_VERSION = 1;
8
8
  const MAX_PAGE_ROLE_FINDINGS = 4;
@@ -1,5 +1,13 @@
1
1
  import { WHOISLEUTH_SOURCE_REPOSITORY_URL, } from "./project-metadata.mjs";
2
2
  import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
3
+ const URI_LABEL_UNSAFE_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
4
+ const URI_LABEL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/|[^\s])/u;
5
+ export function isUriShapedLabel(value) {
6
+ if (typeof value !== 'string')
7
+ return false;
8
+ const candidate = value.replace(URI_LABEL_UNSAFE_GLOBAL_RE, '').trim();
9
+ return URI_LABEL_SCHEME_RE.test(candidate);
10
+ }
3
11
  export function buildPortableGeneratorMetadata(applicationVersion) {
4
12
  let version = null;
5
13
  if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
@@ -1,6 +1,6 @@
1
1
  // Browser-safe public project metadata shared by request identity, package
2
2
  // verification, canonical URLs, structured data, and public policy pages.
3
- export const WHOISLEUTH_SITE_ORIGIN = 'https://whoisleuth.com';
3
+ export const WHOISLEUTH_SITE_ORIGIN = 'https://www.whoisleuth.com';
4
4
  export const WHOISLEUTH_PROJECT_URL = `${WHOISLEUTH_SITE_ORIGIN}/`;
5
5
  export const WHOISLEUTH_REQUEST_POLICY_URL = `${WHOISLEUTH_SITE_ORIGIN}/request-policy`;
6
6
  export const WHOISLEUTH_SOURCE_REPOSITORY_URL = 'https://github.com/slicedearth/whoisleuth';
@@ -1,3 +1,4 @@
1
+ import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
1
2
  export const PROVIDER_POLICY_MAX_REVIEW_AGE_DAYS = 180;
2
3
  export const PROVIDER_POLICY_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
3
4
  export const DEPLOYMENT_PURPOSES = ['personal', 'internal', 'commercial'];
@@ -8,7 +9,8 @@ function deploymentPurpose(value) {
8
9
  : null;
9
10
  }
10
11
  export function providerPolicyReviewAgeDays(reviewedAtValue, now) {
11
- const reviewedAt = typeof reviewedAtValue === 'string' ? Date.parse(reviewedAtValue) : Number.NaN;
12
+ const normalizedReviewedAt = normalizeLegacyIsoTimestamp(reviewedAtValue);
13
+ const reviewedAt = normalizedReviewedAt ? Date.parse(normalizedReviewedAt) : Number.NaN;
12
14
  if (!Number.isFinite(reviewedAt) || !Number.isFinite(now) || reviewedAt > now + PROVIDER_POLICY_MAX_FUTURE_SKEW_MS) {
13
15
  return null;
14
16
  }