@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,369 +0,0 @@
1
- import { scaleBand, scaleLinear, scalePoint } from 'd3-scale';
2
- import { boundedVisualizationId as boundedId, boundedVisualizationNumber as boundedNumber, boundedVisualizationText as boundedText, validVisualizationDate as validDate, } from "./visualization-bounds.js";
3
- export { FORCE_GRAPH_LINK_KINDS, MAX_FORCE_GRAPH_LINKS, MAX_FORCE_GRAPH_NODES, projectBoundedForceGraph, } from "./visualization-force-graph.js";
4
- export { MAX_VISUAL_MATRIX_COLUMNS, MAX_VISUAL_MATRIX_ROWS, projectEvidenceMatrix, } from "./visualization-matrix.js";
5
- export { MAX_COLLECTION_TIMING_SOURCES, MAX_LIFECYCLE_EVENTS, MAX_TREND_POINTS, projectCollectionTiming, projectLifecycleEvents, projectTrendPoints, } from "./visualization-time-series.js";
6
- export const MAX_REDIRECT_NODES = 9;
7
- export const MAX_TRIAGE_PLOT_POINTS = 300;
8
- export const WATCHLIST_ACTIVITY_DAYS = 28;
9
- export const MAX_SCORE_FACTORS = 16;
10
- export const MAX_COVERAGE_BAR_GROUPS = 18;
11
- export const MAX_MONITOR_TIMELINE_EVENTS = 12;
12
- export const MAX_MONITOR_TIMELINE_LANES = 6;
13
- function boundedScore(value) {
14
- if (value === null || value === undefined || value === '')
15
- return null;
16
- const score = Number(value);
17
- return Number.isFinite(score) ? Math.max(0, Math.min(100, score)) : null;
18
- }
19
- function redirectLabel(value) {
20
- const text = boundedText(value, 320);
21
- try {
22
- const parsed = new URL(text);
23
- const path = parsed.pathname === '/' ? '' : parsed.pathname;
24
- return `${parsed.hostname}${path}`.slice(0, 72);
25
- }
26
- catch {
27
- return text.slice(0, 72);
28
- }
29
- }
30
- export function projectRedirectPath(rawRedirects) {
31
- const redirects = (Array.isArray(rawRedirects) ? rawRedirects : [])
32
- .slice(0, MAX_REDIRECT_NODES - 1)
33
- .map((redirect) => ({
34
- status: boundedText(redirect?.status, 12) || '—',
35
- from: redirectLabel(redirect?.from),
36
- to: redirectLabel(redirect?.to),
37
- queryOmitted: Boolean(redirect?.queryOmitted),
38
- }))
39
- .filter((redirect) => redirect.from && redirect.to);
40
- const rawCount = Array.isArray(rawRedirects) ? rawRedirects.length : 0;
41
- if (!redirects.length) {
42
- return { width: 900, height: 150, nodes: [], edges: [], truncated: rawCount > 0 };
43
- }
44
- const firstRedirect = redirects[0];
45
- if (!firstRedirect) {
46
- return { width: 900, height: 150, nodes: [], edges: [], truncated: rawCount > 0 };
47
- }
48
- const nodes = [
49
- { id: 'redirect-start', label: firstRedirect.from, status: 'start', queryOmitted: false },
50
- ...redirects.map((redirect, index) => ({
51
- id: `redirect-${index + 1}`,
52
- label: redirect.to,
53
- status: redirect.status,
54
- queryOmitted: redirect.queryOmitted,
55
- })),
56
- ];
57
- const x = scalePoint().domain(nodes.map((node) => node.id)).range([62, 838]).padding(0.35);
58
- const projected = nodes.map((node) => ({ ...node, x: x(node.id) ?? 450, y: 70 }));
59
- const edges = redirects.flatMap((redirect, index) => {
60
- const from = projected[index];
61
- const to = projected[index + 1];
62
- return from && to ? [{
63
- id: `redirect-edge-${index}`,
64
- fromX: from.x,
65
- toX: to.x,
66
- y: 70,
67
- status: redirect.status,
68
- }] : [];
69
- });
70
- return {
71
- width: 900,
72
- height: 150,
73
- nodes: projected,
74
- edges,
75
- truncated: rawCount > redirects.length,
76
- };
77
- }
78
- function deterministicSample(items, limit) {
79
- const boundedLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
80
- if (boundedLimit === 0)
81
- return [];
82
- if (items.length <= boundedLimit)
83
- return items;
84
- if (boundedLimit === 1)
85
- return items.slice(0, 1);
86
- return Array.from({ length: boundedLimit }, (_, index) => {
87
- const sourceIndex = Math.round(index * (items.length - 1) / (boundedLimit - 1));
88
- return items[sourceIndex];
89
- }).filter((item) => item !== undefined);
90
- }
91
- export function projectTriagePoints(rawPoints) {
92
- const eligible = (Array.isArray(rawPoints) ? rawPoints : [])
93
- .map((point) => ({
94
- domain: boundedText(point?.domain, 253).toLowerCase(),
95
- risk: boundedScore(point?.risk),
96
- opportunity: boundedScore(point?.opportunity),
97
- availability: boundedText(point?.availability, 30).toLowerCase() || 'unknown',
98
- trusted: Boolean(point?.trusted),
99
- }))
100
- .filter((point) => point.domain && point.risk !== null && point.opportunity !== null)
101
- .sort((a, b) => a.domain.localeCompare(b.domain));
102
- const sampled = deterministicSample(eligible, MAX_TRIAGE_PLOT_POINTS);
103
- const quadrants = eligible.reduce((counts, point) => {
104
- if (point.risk >= 50) {
105
- if (point.opportunity >= 50)
106
- counts.priorityReview += 1;
107
- else
108
- counts.riskLedReview += 1;
109
- }
110
- else if (point.opportunity >= 50) {
111
- counts.availableReview += 1;
112
- }
113
- else {
114
- counts.lowerScores += 1;
115
- }
116
- return counts;
117
- }, {
118
- availableReview: 0,
119
- priorityReview: 0,
120
- lowerScores: 0,
121
- riskLedReview: 0,
122
- });
123
- const x = scaleLinear().domain([0, 100]).range([58, 842]).clamp(true);
124
- const y = scaleLinear().domain([0, 100]).range([308, 28]).clamp(true);
125
- const points = sampled.map((point) => ({
126
- ...point,
127
- risk: point.risk,
128
- opportunity: point.opportunity,
129
- x: x(point.risk),
130
- y: y(point.opportunity),
131
- tone: point.trusted
132
- ? 'trusted'
133
- : point.availability === 'available'
134
- ? 'available'
135
- : point.availability === 'error'
136
- ? 'error'
137
- : 'registered',
138
- }));
139
- return {
140
- width: 900,
141
- height: 360,
142
- points,
143
- eligibleCount: eligible.length,
144
- omittedCount: Math.max(0, (Array.isArray(rawPoints) ? rawPoints.length : 0) - eligible.length),
145
- sampled: eligible.length > sampled.length,
146
- quadrants,
147
- };
148
- }
149
- function utcDayKey(milliseconds) {
150
- return new Date(milliseconds).toISOString().slice(0, 10);
151
- }
152
- export function projectWatchlistActivity(rawEvents) {
153
- const valid = (Array.isArray(rawEvents) ? rawEvents : [])
154
- .map((event) => {
155
- const date = validDate(event?.checkedAt);
156
- if (!date)
157
- return null;
158
- const changeCount = Math.max(0, Math.min(10_000, Math.trunc(Number(event.changeCount) || 0)));
159
- const resultCount = Math.max(0, Math.min(10_000, Math.trunc(Number(event.resultCount) || 0)));
160
- const conclusiveCount = Math.max(0, Math.min(resultCount, Math.trunc(Number(event.conclusiveCount) || 0)));
161
- return { ...date, changeCount, resultCount, conclusiveCount };
162
- })
163
- .filter((event) => Boolean(event));
164
- if (!valid.length) {
165
- return { width: 620, height: 190, days: [], maxChanges: 0, totalChecks: 0, totalChanges: 0 };
166
- }
167
- const latest = Math.max(...valid.map((event) => event.milliseconds));
168
- const latestDay = Date.UTC(new Date(latest).getUTCFullYear(), new Date(latest).getUTCMonth(), new Date(latest).getUTCDate());
169
- const start = latestDay - (WATCHLIST_ACTIVITY_DAYS - 1) * 86_400_000;
170
- const windowEnd = latestDay + 86_400_000;
171
- const windowEvents = valid.filter((event) => event.milliseconds >= start && event.milliseconds < windowEnd);
172
- const byDay = new Map();
173
- for (const event of windowEvents) {
174
- const key = utcDayKey(event.milliseconds);
175
- const current = byDay.get(key) ?? { checks: 0, changes: 0, results: 0, conclusive: 0 };
176
- current.checks += 1;
177
- current.changes += event.changeCount;
178
- current.results += event.resultCount;
179
- current.conclusive += event.conclusiveCount;
180
- byDay.set(key, current);
181
- }
182
- const weekScale = scaleBand().domain([0, 1, 2, 3]).range([72, 580]).padding(0.12);
183
- const dayScale = scaleBand().domain([0, 1, 2, 3, 4, 5, 6]).range([26, 165]).padding(0.12);
184
- const dayWidth = weekScale.bandwidth();
185
- const dayHeight = dayScale.bandwidth();
186
- const days = Array.from({ length: WATCHLIST_ACTIVITY_DAYS }, (_, index) => {
187
- const milliseconds = start + index * 86_400_000;
188
- const key = utcDayKey(milliseconds);
189
- const activity = byDay.get(key) ?? { checks: 0, changes: 0, results: 0, conclusive: 0 };
190
- return {
191
- date: key,
192
- label: new Date(milliseconds).toLocaleDateString('en-AU', { day: 'numeric', month: 'short', timeZone: 'UTC' }),
193
- week: Math.floor(index / 7),
194
- weekday: index % 7,
195
- x: weekScale(Math.floor(index / 7)) ?? 0,
196
- y: dayScale(index % 7) ?? 0,
197
- width: dayWidth,
198
- height: dayHeight,
199
- ...activity,
200
- };
201
- });
202
- return {
203
- width: 620,
204
- height: 190,
205
- days,
206
- maxChanges: Math.max(0, ...days.map((day) => day.changes)),
207
- totalChecks: windowEvents.length,
208
- totalChanges: windowEvents.reduce((sum, event) => sum + event.changeCount, 0),
209
- };
210
- }
211
- export function projectScoreFactors(rawFactors) {
212
- const candidates = (Array.isArray(rawFactors) ? rawFactors : [])
213
- .map((factor, index) => ({
214
- id: `${boundedId(factor?.label) || 'factor'}-${index}`,
215
- label: boundedText(factor?.label, 56) || `Factor ${index + 1}`,
216
- delta: boundedNumber(factor?.delta, -100, 100),
217
- }));
218
- const factors = candidates.slice(0, MAX_SCORE_FACTORS);
219
- const maximum = Math.max(1, ...factors.map((factor) => Math.abs(factor.delta)));
220
- const x = scaleLinear().domain([-maximum, maximum]).range([220, 860]).clamp(true);
221
- const zeroX = x(0);
222
- const rowHeight = 28;
223
- return {
224
- width: 900,
225
- height: Math.max(74, 42 + factors.length * rowHeight),
226
- zeroX,
227
- maximum,
228
- factors: factors.map((factor, index) => {
229
- const valueX = x(factor.delta);
230
- return {
231
- ...factor,
232
- x: Math.min(zeroX, valueX),
233
- width: factor.delta === 0 ? 0 : Math.max(2, Math.abs(valueX - zeroX)),
234
- y: 24 + index * rowHeight,
235
- };
236
- }),
237
- truncated: candidates.length > factors.length,
238
- };
239
- }
240
- export function projectCoverageBars(rawGroups) {
241
- const candidates = (Array.isArray(rawGroups) ? rawGroups : [])
242
- .map((group, index) => {
243
- const values = {
244
- protected: Math.trunc(boundedNumber(group?.protected, 0, 100_000)),
245
- registered: Math.trunc(boundedNumber(group?.registered, 0, 100_000)),
246
- available: Math.trunc(boundedNumber(group?.available, 0, 100_000)),
247
- unknown: Math.trunc(boundedNumber(group?.unknown, 0, 100_000)),
248
- };
249
- return {
250
- id: boundedId(group?.id) || `group-${index}`,
251
- label: boundedText(group?.label, 56) || `Group ${index + 1}`,
252
- ...values,
253
- total: Object.values(values).reduce((sum, value) => sum + value, 0),
254
- };
255
- })
256
- .filter((group) => group.total > 0)
257
- .sort((a, b) => b.total - a.total || a.label.localeCompare(b.label));
258
- const groups = candidates.slice(0, MAX_COVERAGE_BAR_GROUPS);
259
- const maximum = Math.max(1, ...groups.map((group) => group.total));
260
- const x = scaleLinear().domain([0, maximum]).range([210, 860]).clamp(true);
261
- const rowHeight = 31;
262
- return {
263
- width: 900,
264
- height: Math.max(76, 40 + groups.length * rowHeight),
265
- groups: groups.map((group, index) => {
266
- let offset = 0;
267
- const segments = ['protected', 'registered', 'available', 'unknown'].map((state) => {
268
- const value = group[state];
269
- const start = offset;
270
- offset += value;
271
- return {
272
- state,
273
- value,
274
- x: x(start),
275
- width: Math.max(value ? 1 : 0, x(offset) - x(start)),
276
- };
277
- });
278
- return { ...group, y: 20 + index * rowHeight, segments };
279
- }),
280
- truncated: candidates.length > groups.length,
281
- };
282
- }
283
- export function projectMonitorTimeline(rawEvents) {
284
- const candidates = (Array.isArray(rawEvents) ? rawEvents : [])
285
- .map((event, index) => {
286
- const date = validDate(event?.checkedAt);
287
- const id = boundedId(event?.id) || `event-${index}`;
288
- if (!date)
289
- return null;
290
- return {
291
- id,
292
- checkedAt: date.text,
293
- milliseconds: date.milliseconds,
294
- mode: boundedText(event?.mode, 20).toLowerCase() || 'saved',
295
- groups: (Array.isArray(event.groups) ? event.groups : [])
296
- .map((group) => ({
297
- key: boundedId(group?.key),
298
- label: boundedText(group?.label, 40),
299
- changeCount: Math.trunc(boundedNumber(group?.changeCount, 0, 10_000)),
300
- }))
301
- .filter((group) => group.key && group.label),
302
- };
303
- })
304
- .filter((event) => Boolean(event))
305
- .sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id));
306
- const events = candidates.slice(-MAX_MONITOR_TIMELINE_EVENTS);
307
- const laneMap = new Map();
308
- for (const event of events) {
309
- for (const group of event.groups) {
310
- if (!laneMap.has(group.key) && laneMap.size < MAX_MONITOR_TIMELINE_LANES) {
311
- laneMap.set(group.key, group.label);
312
- }
313
- }
314
- }
315
- const lanes = [...laneMap].map(([key, label]) => ({ key, label }));
316
- const x = scaleBand().domain(events.map((event) => event.id)).range([170, 865]).padding(0.18);
317
- const y = scaleBand().domain(lanes.map((lane) => lane.key)).range([42, 42 + lanes.length * 40]).padding(0.18);
318
- const maxChanges = Math.max(1, ...events.flatMap((event) => event.groups.map((group) => group.changeCount)));
319
- return {
320
- width: 900,
321
- height: Math.max(110, 64 + lanes.length * 40),
322
- maxChanges,
323
- events: events.map((event) => ({
324
- ...event,
325
- x: x(event.id) ?? 170,
326
- width: x.bandwidth(),
327
- cells: lanes.map((lane) => ({
328
- key: lane.key,
329
- label: lane.label,
330
- count: event.groups.find((group) => group.key === lane.key)?.changeCount ?? 0,
331
- y: y(lane.key) ?? 42,
332
- height: y.bandwidth(),
333
- })),
334
- })),
335
- lanes: lanes.map((lane) => ({ ...lane, y: y(lane.key) ?? 42, height: y.bandwidth() })),
336
- truncated: candidates.length > events.length
337
- || events.some((event) => event.groups.some((group) => !laneMap.has(group.key))),
338
- };
339
- }
340
- export function projectCertificateValidity(input) {
341
- const validFrom = validDate(input.validFrom);
342
- const validTo = validDate(input.validTo);
343
- const observed = validDate(input.observedAt);
344
- if (!validFrom || !validTo || validTo.milliseconds <= validFrom.milliseconds) {
345
- return { available: false, width: 900, height: 110 };
346
- }
347
- const observedMilliseconds = observed?.milliseconds ?? validFrom.milliseconds;
348
- const span = validTo.milliseconds - validFrom.milliseconds;
349
- const padding = Math.max(86_400_000, span * 0.08);
350
- const x = scaleLinear()
351
- .domain([validFrom.milliseconds - padding, validTo.milliseconds + padding])
352
- .range([72, 828])
353
- .clamp(true);
354
- return {
355
- available: true,
356
- width: 900,
357
- height: 110,
358
- validFrom: validFrom.text,
359
- validTo: validTo.text,
360
- observedAt: observed?.text ?? '',
361
- hasObservation: Boolean(observed),
362
- fromX: x(validFrom.milliseconds),
363
- toX: x(validTo.milliseconds),
364
- observedX: x(observedMilliseconds),
365
- observedWithinValidity: observedMilliseconds >= validFrom.milliseconds
366
- && observedMilliseconds <= validTo.milliseconds,
367
- elapsedPercent: Math.max(0, Math.min(100, ((observedMilliseconds - validFrom.milliseconds) / span) * 100)),
368
- };
369
- }
@@ -1,144 +0,0 @@
1
- import { scaleLinear, scalePoint } from 'd3-scale';
2
- import { boundedVisualizationId as boundedId, boundedVisualizationNumber as boundedNumber, boundedVisualizationText as boundedText, validVisualizationDate as validDate, } from "./visualization-bounds.js";
3
- export const MAX_LIFECYCLE_EVENTS = 8;
4
- export const MAX_COLLECTION_TIMING_SOURCES = 16;
5
- export const MAX_TREND_POINTS = 24;
6
- export function projectLifecycleEvents(rawEvents) {
7
- const seen = new Set();
8
- const candidates = (Array.isArray(rawEvents) ? rawEvents : [])
9
- .map((event) => {
10
- const id = boundedId(event?.id);
11
- const label = boundedText(event?.label, 40);
12
- const date = validDate(event?.date);
13
- if (!id || !label || !date)
14
- return null;
15
- const kind = event.kind === 'certificate' || event.kind === 'observation'
16
- ? event.kind
17
- : 'registry';
18
- return {
19
- id,
20
- label,
21
- date: date.text,
22
- milliseconds: date.milliseconds,
23
- detail: boundedText(event.detail, 100),
24
- kind,
25
- };
26
- })
27
- .filter((event) => Boolean(event))
28
- .sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id))
29
- .filter((event) => {
30
- if (seen.has(event.id))
31
- return false;
32
- seen.add(event.id);
33
- return true;
34
- });
35
- const accepted = candidates.slice(0, MAX_LIFECYCLE_EVENTS);
36
- const x = scalePoint()
37
- .domain(accepted.map((event) => event.id))
38
- .range([70, 830])
39
- .padding(accepted.length > 1 ? 0.35 : 0.5);
40
- const laneY = [48, 158, 27];
41
- const events = accepted.map((event, index) => ({
42
- ...event,
43
- x: x(event.id) ?? 450,
44
- labelY: laneY[index % laneY.length] ?? 102,
45
- anchor: index === 0 ? 'start' : index === accepted.length - 1 ? 'end' : 'middle',
46
- }));
47
- return {
48
- width: 900,
49
- height: 205,
50
- axisY: 102,
51
- events,
52
- truncated: candidates.length > accepted.length,
53
- };
54
- }
55
- export function projectCollectionTiming(rawSources, rawTotalMs) {
56
- const candidates = (Array.isArray(rawSources) ? rawSources : [])
57
- .map((source, index) => {
58
- const id = boundedId(source?.source) || `source-${index}`;
59
- const durationMs = boundedNumber(source?.durationMs, 0, 300_000);
60
- const completedAfterMs = boundedNumber(source?.completedAfterMs, 0, 300_000);
61
- return {
62
- id,
63
- label: boundedText(source?.source, 48) || `Source ${index + 1}`,
64
- outcome: boundedText(source?.outcome, 24).toLowerCase() || 'unknown',
65
- durationMs,
66
- completedAfterMs,
67
- startedAfterMs: Math.max(0, completedAfterMs - durationMs),
68
- };
69
- })
70
- .sort((a, b) => a.completedAfterMs - b.completedAfterMs || a.id.localeCompare(b.id));
71
- const sources = candidates.slice(0, MAX_COLLECTION_TIMING_SOURCES);
72
- const longestSource = [...sources]
73
- .sort((a, b) => b.durationMs - a.durationMs
74
- || b.completedAfterMs - a.completedAfterMs
75
- || a.id.localeCompare(b.id))[0] ?? null;
76
- const lastSettledSource = sources.at(-1) ?? null;
77
- const requestErrorCount = sources.filter((source) => source.outcome === 'rejected').length;
78
- const totalMs = Math.max(1, boundedNumber(rawTotalMs, 0, 300_000), ...sources.map((source) => source.completedAfterMs));
79
- const x = scaleLinear().domain([0, totalMs]).range([190, 860]).clamp(true);
80
- const rowHeight = 30;
81
- return {
82
- width: 900,
83
- height: Math.max(92, 50 + sources.length * rowHeight),
84
- totalMs,
85
- longestSource,
86
- lastSettledSource,
87
- requestErrorCount,
88
- ticks: x.ticks(5).map((value) => ({ value, x: x(value) })),
89
- sources: sources.map((source, index) => ({
90
- ...source,
91
- x: x(source.startedAfterMs),
92
- width: Math.max(3, x(source.completedAfterMs) - x(source.startedAfterMs)),
93
- y: 30 + index * rowHeight,
94
- })),
95
- truncated: candidates.length > sources.length,
96
- };
97
- }
98
- export function projectTrendPoints(rawPoints) {
99
- const seen = new Set();
100
- const candidates = (Array.isArray(rawPoints) ? rawPoints : [])
101
- .map((point, index) => {
102
- const date = validDate(point?.date);
103
- const id = boundedId(point?.id) || `point-${index}`;
104
- if (!date || seen.has(id))
105
- return null;
106
- seen.add(id);
107
- return {
108
- id,
109
- date: date.text,
110
- milliseconds: date.milliseconds,
111
- total: Math.trunc(boundedNumber(point?.total, 0, 100_000)),
112
- added: Math.trunc(boundedNumber(point?.added, 0, 100_000)),
113
- partial: Boolean(point?.partial),
114
- };
115
- })
116
- .filter((point) => Boolean(point))
117
- .sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id));
118
- const points = candidates.slice(-MAX_TREND_POINTS);
119
- const x = scalePoint().domain(points.map((point) => point.id)).range([64, 850]).padding(0.45);
120
- const maximum = Math.max(1, ...points.map((point) => point.total));
121
- const y = scaleLinear().domain([0, maximum]).range([190, 24]).nice().clamp(true);
122
- const first = points[0] ?? null;
123
- const latest = points.at(-1) ?? null;
124
- return {
125
- width: 900,
126
- height: 225,
127
- maximum,
128
- ticks: y.ticks(4).map((value) => ({ value, y: y(value) })),
129
- points: points.map((point) => ({
130
- ...point,
131
- x: x(point.id) ?? 450,
132
- y: y(point.total),
133
- addedY: y(point.added),
134
- })),
135
- summary: {
136
- firstTotal: first?.total ?? 0,
137
- latestTotal: latest?.total ?? 0,
138
- peakTotal: points.reduce((peak, point) => Math.max(peak, point.total), 0),
139
- newlyObserved: points.reduce((total, point) => total + point.added, 0),
140
- partialChecks: points.filter((point) => point.partial).length,
141
- },
142
- truncated: candidates.length > points.length,
143
- };
144
- }
@@ -1,157 +0,0 @@
1
- // Environment-neutral browser/server JSON response boundary. It enforces a
2
- // declared and streamed byte ceiling before parsing and can apply one deadline
3
- // across both the fetch and body-read phases.
4
- export const SMALL_JSON_RESPONSE_BYTES = 64 * 1024;
5
- export const STANDARD_JSON_RESPONSE_BYTES = 2 * 1024 * 1024;
6
- export const LARGE_JSON_RESPONSE_BYTES = 8 * 1024 * 1024;
7
- export const DEFAULT_JSON_RESPONSE_TIMEOUT_MS = 40_000;
8
- const MAX_JSON_RESPONSE_TIMEOUT_MS = 120_000;
9
- export class BoundedJsonResponseError extends Error {
10
- code;
11
- constructor(code, message) {
12
- super(message);
13
- this.name = 'BoundedJsonResponseError';
14
- this.code = code;
15
- }
16
- }
17
- async function raceWithAbort(promise, signal, onLateResolution) {
18
- if (signal.aborted)
19
- throw signal.reason ?? new Error('Request aborted.');
20
- return new Promise((resolve, reject) => {
21
- let aborted = false;
22
- const onAbort = () => {
23
- aborted = true;
24
- reject(signal.reason ?? new Error('Request aborted.'));
25
- };
26
- signal.addEventListener('abort', onAbort, { once: true });
27
- void promise.then((value) => {
28
- signal.removeEventListener('abort', onAbort);
29
- if (aborted) {
30
- void Promise.resolve(onLateResolution?.(value)).catch(() => { });
31
- return;
32
- }
33
- resolve(value);
34
- }, (cause) => {
35
- signal.removeEventListener('abort', onAbort);
36
- reject(cause);
37
- });
38
- });
39
- }
40
- function boundedPositiveInteger(value, fallback, maximum) {
41
- return typeof value === 'number' && Number.isFinite(value)
42
- ? Math.max(1, Math.min(maximum, Math.round(value)))
43
- : fallback;
44
- }
45
- export async function readJsonResponseCapped(response, maximumBytes = STANDARD_JSON_RESPONSE_BYTES, signal) {
46
- const maxBytes = boundedPositiveInteger(maximumBytes, STANDARD_JSON_RESPONSE_BYTES, LARGE_JSON_RESPONSE_BYTES);
47
- const declared = response.headers.get('content-length');
48
- if (declared && /^\d+$/u.test(declared)) {
49
- const bytes = Number(declared);
50
- if (!Number.isSafeInteger(bytes) || bytes > maxBytes) {
51
- await response.body?.cancel().catch(() => { });
52
- throw new BoundedJsonResponseError('response_too_large', `JSON response exceeded ${maxBytes} bytes.`);
53
- }
54
- }
55
- if (!response.body)
56
- return null;
57
- const reader = response.body.getReader();
58
- const cancelOnAbort = () => { void reader.cancel(signal?.reason).catch(() => { }); };
59
- signal?.addEventListener('abort', cancelOnAbort, { once: true });
60
- const chunks = [];
61
- let total = 0;
62
- try {
63
- while (true) {
64
- const read = reader.read();
65
- const { done, value } = signal ? await raceWithAbort(read, signal) : await read;
66
- if (done)
67
- break;
68
- if (!value)
69
- continue;
70
- total += value.byteLength;
71
- if (total > maxBytes) {
72
- await reader.cancel().catch(() => { });
73
- throw new BoundedJsonResponseError('response_too_large', `JSON response exceeded ${maxBytes} bytes.`);
74
- }
75
- chunks.push(value);
76
- }
77
- }
78
- finally {
79
- signal?.removeEventListener('abort', cancelOnAbort);
80
- reader.releaseLock();
81
- }
82
- const bytes = new Uint8Array(total);
83
- let offset = 0;
84
- for (const chunk of chunks) {
85
- bytes.set(chunk, offset);
86
- offset += chunk.byteLength;
87
- }
88
- try {
89
- return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
90
- }
91
- catch {
92
- throw new BoundedJsonResponseError('invalid_json', 'Response body was not valid UTF-8 JSON.');
93
- }
94
- }
95
- export async function requestJsonCapped(input, init = {}, options = {}) {
96
- const timeoutMs = boundedPositiveInteger(options.timeoutMs, DEFAULT_JSON_RESPONSE_TIMEOUT_MS, MAX_JSON_RESPONSE_TIMEOUT_MS);
97
- const controller = new AbortController();
98
- const upstreamSignal = init.signal;
99
- const forwardAbort = () => controller.abort(upstreamSignal?.reason);
100
- if (upstreamSignal?.aborted)
101
- forwardAbort();
102
- else
103
- upstreamSignal?.addEventListener('abort', forwardAbort, { once: true });
104
- const timeoutReason = Object.freeze({ type: 'bounded-json-timeout' });
105
- const timeout = setTimeout(() => controller.abort(timeoutReason), timeoutMs);
106
- try {
107
- if (controller.signal.aborted) {
108
- throw new BoundedJsonResponseError('aborted', 'Request was cancelled.');
109
- }
110
- const fetchRequest = (options.fetchImpl ?? fetch)(input, {
111
- ...init,
112
- signal: controller.signal,
113
- });
114
- const response = await raceWithAbort(fetchRequest, controller.signal, async (lateResponse) => lateResponse.body?.cancel().catch(() => { }));
115
- const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
116
- const plausibleJson = !contentType
117
- || contentType.includes('application/json')
118
- || contentType.includes('+json');
119
- if (options.allowNonJsonErrorResponse !== false && !response.ok && !plausibleJson) {
120
- await response.body?.cancel().catch(() => { });
121
- return { response, body: null };
122
- }
123
- let body;
124
- try {
125
- body = await readJsonResponseCapped(response, options.maximumBytes, controller.signal);
126
- }
127
- catch (cause) {
128
- // Reverse proxies and hosting adapters can replace an upstream JSON
129
- // error with a bounded HTML or plain-text response. Preserve the HTTP
130
- // status so callers can apply retry and generic error handling, while
131
- // continuing to require valid JSON for successful responses.
132
- if (options.allowNonJsonErrorResponse !== false
133
- && !response.ok
134
- && cause instanceof BoundedJsonResponseError
135
- && cause.code === 'invalid_json') {
136
- body = null;
137
- }
138
- else {
139
- throw cause;
140
- }
141
- }
142
- return { response, body };
143
- }
144
- catch (cause) {
145
- if (controller.signal.aborted) {
146
- const timedOut = controller.signal.reason === timeoutReason;
147
- throw new BoundedJsonResponseError(timedOut ? 'timeout' : 'aborted', timedOut ? `Request timed out after ${timeoutMs} ms.` : 'Request was cancelled.');
148
- }
149
- if (cause instanceof BoundedJsonResponseError)
150
- throw cause;
151
- throw cause;
152
- }
153
- finally {
154
- clearTimeout(timeout);
155
- upstreamSignal?.removeEventListener('abort', forwardAbort);
156
- }
157
- }