browser-debugger-cli 0.15.0 → 0.16.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 (133) hide show
  1. package/.claude/skills/bdg/SKILL.md +2 -1
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +200 -133
  11. package/dist/commands/cleanup.js +18 -4
  12. package/dist/commands/dom/formInteraction.js +8 -4
  13. package/dist/commands/dom/helpers/index.d.ts +4 -4
  14. package/dist/commands/dom/helpers/index.js +3 -3
  15. package/dist/commands/dom/helpers/query.d.ts +2 -2
  16. package/dist/commands/dom/helpers/query.js +2 -2
  17. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  18. package/dist/commands/dom/helpers/screenshot.js +50 -668
  19. package/dist/commands/dom/screenshot.js +56 -36
  20. package/dist/commands/optionBehaviors.js +18 -8
  21. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  22. package/dist/commands/shared/CommandRunner.js +18 -3
  23. package/dist/commands/shared/interrupt.d.ts +40 -0
  24. package/dist/commands/shared/interrupt.js +73 -0
  25. package/dist/commands/shared/optionTypes.d.ts +2 -0
  26. package/dist/commands/shared/startHelpers.d.ts +26 -3
  27. package/dist/commands/shared/startHelpers.js +145 -23
  28. package/dist/commands/types.d.ts +5 -0
  29. package/dist/connection/cdp.js +1 -16
  30. package/dist/connection/chromeIdentity.d.ts +24 -5
  31. package/dist/connection/chromeIdentity.js +53 -22
  32. package/dist/connection/launcher.d.ts +34 -1
  33. package/dist/connection/launcher.js +98 -10
  34. package/dist/connection/typed-cdp.d.ts +3 -2
  35. package/dist/constants.d.ts +1 -1
  36. package/dist/constants.js +1 -1
  37. package/dist/daemon/SessionController.d.ts +10 -5
  38. package/dist/daemon/SessionController.js +15 -8
  39. package/dist/daemon/ipcServer.js +1 -1
  40. package/dist/daemon/launcher.d.ts +5 -0
  41. package/dist/daemon/launcher.js +8 -1
  42. package/dist/daemon/session/Session.d.ts +5 -1
  43. package/dist/daemon/session/Session.js +9 -8
  44. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  45. package/dist/daemon/session/TelemetryStore.js +4 -0
  46. package/dist/daemon/session/captureGate.d.ts +59 -0
  47. package/dist/daemon/session/captureGate.js +96 -0
  48. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  49. package/dist/daemon/session/chromeConnection.js +34 -4
  50. package/dist/daemon/session/collectors.d.ts +15 -0
  51. package/dist/daemon/session/collectors.js +39 -2
  52. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  53. package/dist/daemon/session/commandRegistry.js +46 -11
  54. package/dist/daemon/session/downloads.d.ts +32 -0
  55. package/dist/daemon/session/downloads.js +96 -0
  56. package/dist/daemon/session/interactions.d.ts +3 -2
  57. package/dist/daemon/session/interactions.js +7 -2
  58. package/dist/daemon/session/plugins.js +6 -0
  59. package/dist/daemon.js +12843 -11482
  60. package/dist/errors/CommandError.d.ts +2 -0
  61. package/dist/errors/issues.d.ts +1 -1
  62. package/dist/errors/messages.d.ts +58 -0
  63. package/dist/errors/messages.js +112 -0
  64. package/dist/index.js +999 -1020
  65. package/dist/ipc/client.d.ts +14 -1
  66. package/dist/ipc/client.js +21 -4
  67. package/dist/ipc/protocol/commands.d.ts +32 -2
  68. package/dist/ipc/protocol/commands.js +1 -0
  69. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  70. package/dist/ipc/session/queries.d.ts +3 -0
  71. package/dist/ipc/session/types.d.ts +5 -0
  72. package/dist/ipc/transport/IPCError.d.ts +9 -0
  73. package/dist/ipc/transport/IPCError.js +12 -0
  74. package/dist/ipc/transport/errors.d.ts +2 -1
  75. package/dist/ipc/transport/errors.js +4 -1
  76. package/dist/ipc/transport/index.d.ts +4 -2
  77. package/dist/ipc/transport/index.js +13 -3
  78. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  79. package/dist/runtime/dom/actionEffects.js +269 -34
  80. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  81. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  82. package/dist/runtime/dom/captureArea.d.ts +35 -0
  83. package/dist/runtime/dom/captureArea.js +203 -0
  84. package/dist/runtime/dom/elementInfo.d.ts +10 -8
  85. package/dist/runtime/dom/elementInfo.js +8 -6
  86. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  87. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  88. package/dist/runtime/page/bdgWorld.js +11 -0
  89. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  90. package/dist/runtime/page/captureEmulation.js +189 -0
  91. package/dist/runtime/page/captureScroll.d.ts +24 -0
  92. package/dist/runtime/page/captureScroll.js +124 -0
  93. package/dist/runtime/page/screenshot.d.ts +41 -0
  94. package/dist/runtime/page/screenshot.js +394 -0
  95. package/dist/session/paths.d.ts +14 -0
  96. package/dist/session/paths.js +25 -0
  97. package/dist/telemetry/downloads.d.ts +127 -0
  98. package/dist/telemetry/downloads.js +265 -0
  99. package/dist/telemetry/har/builder.js +22 -7
  100. package/dist/telemetry/har/sanitize.d.ts +7 -3
  101. package/dist/telemetry/har/sanitize.js +52 -6
  102. package/dist/telemetry/har/sanitizeBody.d.ts +47 -7
  103. package/dist/telemetry/har/sanitizeBody.js +429 -56
  104. package/dist/telemetry/har/types.d.ts +2 -0
  105. package/dist/telemetry/network.d.ts +4 -4
  106. package/dist/telemetry/network.js +38 -4
  107. package/dist/telemetry/networkRetention.d.ts +35 -14
  108. package/dist/telemetry/networkRetention.js +62 -26
  109. package/dist/types.d.ts +9 -14
  110. package/dist/ui/OutputBuilder.d.ts +3 -2
  111. package/dist/ui/OutputBuilder.js +4 -3
  112. package/dist/ui/formatters/cdp.d.ts +32 -9
  113. package/dist/ui/formatters/cdp.js +77 -6
  114. package/dist/ui/formatters/details.js +7 -15
  115. package/dist/ui/formatters/preview.d.ts +2 -0
  116. package/dist/ui/formatters/preview.js +7 -1
  117. package/dist/ui/formatters/status.js +6 -1
  118. package/dist/ui/formatting.d.ts +7 -0
  119. package/dist/ui/formatting.js +13 -0
  120. package/dist/ui/logging/logger.d.ts +1 -1
  121. package/dist/ui/messages/chrome.d.ts +13 -0
  122. package/dist/ui/messages/chrome.js +26 -0
  123. package/dist/ui/messages/commands.d.ts +71 -3
  124. package/dist/ui/messages/commands.js +98 -3
  125. package/dist/ui/messages/networkMessages.d.ts +24 -5
  126. package/dist/ui/messages/networkMessages.js +31 -8
  127. package/dist/utils/async.d.ts +3 -2
  128. package/dist/utils/async.js +16 -3
  129. package/dist/utils/http.d.ts +11 -4
  130. package/dist/utils/http.js +5 -3
  131. package/package.json +18 -4
  132. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  133. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Screenshots of the page or one element (`bdg dom screenshot`), taken in the
3
+ * daemon: measuring, the emulation changes the capture needs, the capture and
4
+ * putting the emulation back all happen here, inside one `try`/`finally`, so
5
+ * a CLI interrupted mid-capture (Ctrl-C) cannot leave the page changed. The
6
+ * CLI only writes the returned image.
7
+ *
8
+ * The image travels base64-encoded in one IPC line: Chrome sends it the same
9
+ * way over its WebSocket (100 MiB at most), well below the IPC line limit
10
+ * (`MAX_JSONL_BUFFER_SIZE`).
11
+ */
12
+ import { TypedCDPConnection } from '../../connection/typed-cdp.js';
13
+ import { captureArea, getElementBounds } from '../dom/captureArea.js';
14
+ import { withBusyPageRecovery } from '../dom/evalHelpers.js';
15
+ import { CaptureEmulation, evaluateValue, scrollPosition, } from './captureEmulation.js';
16
+ import { scrollIntoViewAgain, scrollToElement } from './captureScroll.js';
17
+ import { calculateImageTokens, calculateResizeScale, isTallPage, shouldResize, } from './screenshotResize.js';
18
+ import { createLogger } from '../../ui/logging/index.js';
19
+ import { getErrorMessage } from '../../utils/errors.js';
20
+ const log = createLogger('dom');
21
+ /** JPEG quality when none is given */
22
+ const DEFAULT_JPEG_QUALITY = 90;
23
+ /**
24
+ * Take a screenshot: of the element `backendNodeId` names, else of the page.
25
+ * Whatever emulation the capture changed is put back before this returns or
26
+ * throws, from the session's record of its emulation at that time.
27
+ *
28
+ * A page whose scripts keep it busy gets them terminated
29
+ * ({@link withBusyPageRecovery}, exit 102); the capture then ends, and the
30
+ * error is reported once its emulation is back, so the next command does not
31
+ * race the restore. A capture whose client left (Ctrl-C) is not taken once
32
+ * that is known: only the restore runs.
33
+ *
34
+ * @param cdp - Session connection
35
+ * @param params - What to capture and how
36
+ * @param sessionViewport - Reads the session's emulated viewport, if any
37
+ * @param options - When the client left, and when a busy page is checked (tests shorten it)
38
+ * @returns The image (base64) and what was captured
39
+ */
40
+ export async function takeScreenshot(cdp, params, sessionViewport, options = {}) {
41
+ const { abandoned, recovery = {} } = options;
42
+ const emulation = new CaptureEmulation(cdp, sessionViewport);
43
+ const shot = captureAndRestore(cdp, params, emulation, abandoned);
44
+ try {
45
+ return await withBusyPageRecovery(cdp, shot, recovery);
46
+ }
47
+ catch (error) {
48
+ await shot.catch((captureError) => log.debug(`Capture ended after the busy page: ${getErrorMessage(captureError)}`));
49
+ throw error;
50
+ }
51
+ }
52
+ /**
53
+ * Capture, then put back the emulation the capture changed. A failed restore
54
+ * is reported only when the capture worked; otherwise the capture's error is.
55
+ *
56
+ * @param cdp - Session connection
57
+ * @param params - What to capture and how
58
+ * @param emulation - Emulation changes of this capture
59
+ * @param abandoned - Aborted when the client left
60
+ * @returns The image (base64) and what was captured
61
+ */
62
+ async function captureAndRestore(cdp, params, emulation, abandoned) {
63
+ let shot;
64
+ try {
65
+ shot =
66
+ params.backendNodeId === undefined
67
+ ? await capturePage(cdp, params, emulation, abandoned)
68
+ : await captureElement(cdp, { backendNodeId: params.backendNodeId }, params, emulation, abandoned);
69
+ }
70
+ catch (error) {
71
+ await emulation
72
+ .restore()
73
+ .catch((restoreError) => log.debug(`Restore after a failed capture: ${getErrorMessage(restoreError)}`));
74
+ throw error;
75
+ }
76
+ await emulation.restore();
77
+ return shot;
78
+ }
79
+ /**
80
+ * JPEG quality of a capture (none for PNG).
81
+ *
82
+ * @param params - Format and requested quality
83
+ * @returns Quality, or undefined for PNG
84
+ */
85
+ function jpegQuality(params) {
86
+ return params.format === 'jpeg' ? (params.quality ?? DEFAULT_JPEG_QUALITY) : undefined;
87
+ }
88
+ /**
89
+ * The page's pixel ratio.
90
+ *
91
+ * @param cdp - Session connection
92
+ * @returns `devicePixelRatio`, 1 when the page does not answer
93
+ */
94
+ async function pixelRatio(cdp) {
95
+ const value = await evaluateValue(cdp, 'window.devicePixelRatio');
96
+ return typeof value === 'number' ? value : 1;
97
+ }
98
+ /**
99
+ * The page's layout metrics.
100
+ *
101
+ * @param cdp - Session connection
102
+ * @returns `Page.getLayoutMetrics` result
103
+ */
104
+ function layoutMetrics(cdp) {
105
+ return new TypedCDPConnection(cdp).send('Page.getLayoutMetrics', {});
106
+ }
107
+ /**
108
+ * Capture an image, unless the client left: a capture of a tall page takes
109
+ * seconds, and nobody would get it.
110
+ *
111
+ * @param cdp - Session connection
112
+ * @param params - Format, quality, clip
113
+ * @param abandoned - Aborted when the client left
114
+ * @returns The image (base64) and its size in bytes
115
+ * @throws The abort reason when the client left
116
+ */
117
+ async function captureImage(cdp, params, abandoned) {
118
+ abandoned?.throwIfAborted();
119
+ const { data } = await new TypedCDPConnection(cdp).send('Page.captureScreenshot', params);
120
+ return { image: data, size: Buffer.byteLength(data, 'base64') };
121
+ }
122
+ /**
123
+ * Decide what a page capture covers: the whole page unless it is too tall
124
+ * (then the viewport), the viewport with `--no-full-page` or `--scroll`;
125
+ * scaled down to the token budget unless `--no-resize`.
126
+ *
127
+ * @param params - Capture options
128
+ * @param contentSize - Page size
129
+ * @param viewport - Viewport size
130
+ * @returns The plan
131
+ */
132
+ function planPageCapture(params, contentSize, viewport) {
133
+ const noResize = params.noResize ?? false;
134
+ const requestedFullPage = params.fullPage ?? true;
135
+ const pageIsTooTall = !noResize && requestedFullPage && isTallPage(contentSize.width, contentSize.height);
136
+ const fullPage = params.scroll === undefined && !pageIsTooTall && requestedFullPage;
137
+ const { width, height } = fullPage ? contentSize : viewport;
138
+ const resized = shouldResize(width, height, noResize);
139
+ const scale = resized ? calculateResizeScale(width, height) : 1;
140
+ return { fullPage, width, height, scale, resized, pageIsTooTall };
141
+ }
142
+ /**
143
+ * Page coordinates of the visible area's top-left corner. A capture clip is
144
+ * in page coordinates, so a viewport capture must start at the scroll
145
+ * position, not at the page origin (which shows nothing once scrolled). Read
146
+ * after any metrics override, which can move the scroll position.
147
+ *
148
+ * @param cdp - Session connection
149
+ * @returns Scroll offset of the visual viewport in CSS pixels
150
+ */
151
+ async function visibleAreaOrigin(cdp) {
152
+ const { cssVisualViewport } = await layoutMetrics(cdp);
153
+ return { x: cssVisualViewport.pageX, y: cssVisualViewport.pageY };
154
+ }
155
+ /**
156
+ * Capture the page. Auto-resizes oversized pages by default to keep Claude
157
+ * Vision token cost bounded; falls back to viewport capture when the page is
158
+ * taller than the tall-page threshold.
159
+ *
160
+ * @param cdp - Session connection
161
+ * @param params - Capture options
162
+ * @param emulation - Emulation changes of this capture
163
+ * @param abandoned - Aborted when the client left
164
+ * @returns Image and what was captured
165
+ */
166
+ async function capturePage(cdp, params, emulation, abandoned) {
167
+ if (params.scroll)
168
+ emulation.scrolledAwayFrom(await scrollToElement(cdp, params.scroll));
169
+ const devicePixelRatio = await pixelRatio(cdp);
170
+ const { contentSize, visualViewport } = await layoutMetrics(cdp);
171
+ const viewport = { width: visualViewport.clientWidth, height: visualViewport.clientHeight };
172
+ const plan = planPageCapture(params, contentSize, viewport);
173
+ await emulation.useUnitPixelRatio(devicePixelRatio, viewport);
174
+ if (devicePixelRatio !== 1 && params.scroll)
175
+ await scrollIntoViewAgain(cdp, params.scroll);
176
+ const origin = plan.fullPage ? { x: 0, y: 0 } : await visibleAreaOrigin(cdp);
177
+ const quality = jpegQuality(params);
178
+ const { image, size } = await captureImage(cdp, {
179
+ format: params.format,
180
+ ...(quality !== undefined && { quality }),
181
+ captureBeyondViewport: plan.fullPage,
182
+ clip: { ...origin, width: plan.width, height: plan.height, scale: plan.scale },
183
+ }, abandoned);
184
+ const screenshot = pageScreenshot(params, plan, size, viewport, contentSize);
185
+ return { image, screenshot };
186
+ }
187
+ /**
188
+ * What a page capture reports.
189
+ *
190
+ * @param params - Capture options
191
+ * @param plan - How it was taken
192
+ * @param size - Image size in bytes
193
+ * @param viewport - Viewport size
194
+ * @param contentSize - Page size
195
+ * @returns The report
196
+ */
197
+ function pageScreenshot(params, plan, size, viewport, contentSize) {
198
+ const width = Math.round(plan.width * plan.scale);
199
+ const height = Math.round(plan.height * plan.scale);
200
+ const quality = jpegQuality(params);
201
+ return {
202
+ format: params.format,
203
+ width,
204
+ height,
205
+ size,
206
+ fullPage: plan.fullPage,
207
+ captureMode: plan.fullPage ? 'full_page' : 'viewport',
208
+ finalTokens: calculateImageTokens(width, height),
209
+ ...(quality !== undefined && { quality }),
210
+ ...(!plan.fullPage && { viewport }),
211
+ ...resizeReport(plan.resized, plan.width, plan.height),
212
+ ...(plan.pageIsTooTall && params.scroll === undefined && tallPageReport(contentSize)),
213
+ ...(params.scroll !== undefined && { scrolledTo: params.scroll }),
214
+ };
215
+ }
216
+ /**
217
+ * Report of an auto-resize: the size before it.
218
+ *
219
+ * @param resized - Whether the image was scaled down
220
+ * @param width - Width before (CSS px)
221
+ * @param height - Height before (CSS px)
222
+ * @returns Fields to add, none when not resized
223
+ */
224
+ function resizeReport(resized, width, height) {
225
+ if (!resized)
226
+ return {};
227
+ return {
228
+ resized: true,
229
+ originalWidth: width,
230
+ originalHeight: height,
231
+ originalTokens: calculateImageTokens(width, height),
232
+ };
233
+ }
234
+ /**
235
+ * Report of a full-page capture skipped for a page too tall to read.
236
+ *
237
+ * @param contentSize - Page size
238
+ * @returns `fullPageSkipped` and a warning
239
+ */
240
+ function tallPageReport(contentSize) {
241
+ const aspectRatio = Math.round((contentSize.height / contentSize.width) * 10) / 10;
242
+ return {
243
+ fullPageSkipped: {
244
+ reason: 'page_too_tall',
245
+ originalHeight: contentSize.height,
246
+ aspectRatio,
247
+ },
248
+ warning: `Full page capture skipped: page too tall (${aspectRatio}:1 aspect ratio). Only viewport captured.`,
249
+ };
250
+ }
251
+ /**
252
+ * Whether an area (viewport coordinates) lies inside the viewport.
253
+ *
254
+ * @param area - Area
255
+ * @param view - Viewport size
256
+ * @returns True when fully inside
257
+ */
258
+ function insideView(area, view) {
259
+ return (area.x >= 0 &&
260
+ area.y >= 0 &&
261
+ area.x + area.width <= view.width &&
262
+ area.y + area.height <= view.height);
263
+ }
264
+ /**
265
+ * Measure the area to capture and, when it fits in the viewport but is not
266
+ * in view, scroll it to the middle first (put back by the emulation's
267
+ * restore). A capture inside the viewport keeps the page as it is; one
268
+ * beyond it makes Chrome lay the page out without its scrollbar, so for an
269
+ * area larger than the viewport the scrollbars are hidden first and the
270
+ * area measured in that layout (centered content would else move by half
271
+ * the scrollbar's width).
272
+ *
273
+ * @param cdp - Session connection
274
+ * @param ref - The element
275
+ * @param padding - Extra space around the area (CSS px)
276
+ * @param emulation - Emulation changes of this capture
277
+ * @returns The measurements
278
+ */
279
+ async function measureInView(cdp, ref, padding, emulation) {
280
+ const { cssVisualViewport } = await layoutMetrics(cdp);
281
+ const view = { width: cssVisualViewport.clientWidth, height: cssVisualViewport.clientHeight };
282
+ const first = await measureArea(cdp, ref, padding);
283
+ const { bounds } = first;
284
+ if (bounds.width > view.width || bounds.height > view.height) {
285
+ await emulation.keepLayoutWithoutScrollbars(view);
286
+ return { ...(await measureArea(cdp, ref, padding)), inView: false };
287
+ }
288
+ if (insideView(bounds, view))
289
+ return { ...first, inView: true };
290
+ emulation.scrolledAwayFrom(await scrollPosition(cdp));
291
+ const dx = bounds.x + bounds.width / 2 - view.width / 2;
292
+ const dy = bounds.y + bounds.height / 2 - view.height / 2;
293
+ await evaluateValue(cdp, `window.scrollBy(${dx}, ${dy})`);
294
+ const moved = await measureArea(cdp, ref, padding);
295
+ return { ...moved, inView: insideView(moved.bounds, view) };
296
+ }
297
+ /**
298
+ * Measure an element's border box and the area to capture.
299
+ *
300
+ * @param cdp - Session connection
301
+ * @param ref - The element
302
+ * @param padding - Extra space around the area (CSS px)
303
+ * @returns Border box and area (viewport coordinates)
304
+ */
305
+ async function measureArea(cdp, ref, padding) {
306
+ const box = await getElementBounds(cdp, ref);
307
+ return { box, bounds: await captureArea(cdp, ref, box, padding) };
308
+ }
309
+ /**
310
+ * Capture a single element: its border box, grown to include content
311
+ * overflowing it ({@link captureArea}). The box model is relative to the
312
+ * viewport and the capture clip to the page, so the page scroll is added
313
+ * (the reported bounds are page coordinates, like `dom layout`'s).
314
+ *
315
+ * @param cdp - Session connection
316
+ * @param ref - The element
317
+ * @param params - Capture options
318
+ * @param emulation - Emulation changes of this capture
319
+ * @param abandoned - Aborted when the client left
320
+ * @returns Image and what was captured
321
+ */
322
+ async function captureElement(cdp, ref, params, emulation, abandoned) {
323
+ const devicePixelRatio = await pixelRatio(cdp);
324
+ const { visualViewport } = await layoutMetrics(cdp);
325
+ await emulation.useUnitPixelRatio(devicePixelRatio, {
326
+ width: visualViewport.clientWidth,
327
+ height: visualViewport.clientHeight,
328
+ });
329
+ const measured = await measureInView(cdp, ref, params.padding ?? 0, emulation);
330
+ const { cssLayoutViewport } = await layoutMetrics(cdp);
331
+ const onPage = (area) => ({
332
+ ...area,
333
+ x: area.x + cssLayoutViewport.pageX,
334
+ y: area.y + cssLayoutViewport.pageY,
335
+ });
336
+ const clip = onPage(measured.bounds);
337
+ const resized = shouldResize(clip.width, clip.height, params.noResize ?? false);
338
+ const scale = resized ? calculateResizeScale(clip.width, clip.height) : 1;
339
+ const quality = jpegQuality(params);
340
+ const { image, size } = await captureImage(cdp, {
341
+ format: params.format,
342
+ ...(quality !== undefined && { quality }),
343
+ clip: { ...clip, scale },
344
+ captureBeyondViewport: !measured.inView,
345
+ }, abandoned);
346
+ const element = {
347
+ bounds: roundBounds(onPage(measured.box)),
348
+ ...(measured.bounds !== measured.box && { captured: roundBounds(clip) }),
349
+ ...(params.padding && { padding: params.padding }),
350
+ };
351
+ return { image, screenshot: elementScreenshot(params, clip, scale, size, element) };
352
+ }
353
+ /**
354
+ * What an element capture reports.
355
+ *
356
+ * @param params - Capture options
357
+ * @param clip - Area captured (CSS px)
358
+ * @param scale - Resize scale
359
+ * @param size - Image size in bytes
360
+ * @param element - Element bounds as reported
361
+ * @returns The report
362
+ */
363
+ function elementScreenshot(params, clip, scale, size, element) {
364
+ const width = Math.round(clip.width * scale);
365
+ const height = Math.round(clip.height * scale);
366
+ const quality = jpegQuality(params);
367
+ return {
368
+ format: params.format,
369
+ width,
370
+ height,
371
+ size,
372
+ fullPage: false,
373
+ captureMode: 'element',
374
+ finalTokens: calculateImageTokens(width, height),
375
+ element,
376
+ ...(quality !== undefined && { quality }),
377
+ ...resizeReport(scale !== 1, clip.width, clip.height),
378
+ };
379
+ }
380
+ /**
381
+ * Bounds in whole pixels.
382
+ *
383
+ * @param bounds - Bounds
384
+ * @returns Rounded bounds
385
+ */
386
+ function roundBounds(bounds) {
387
+ return {
388
+ x: Math.round(bounds.x),
389
+ y: Math.round(bounds.y),
390
+ width: Math.round(bounds.width),
391
+ height: Math.round(bounds.height),
392
+ };
393
+ }
394
+ //# sourceMappingURL=screenshot.js.map
@@ -132,6 +132,20 @@ export declare function getDaemonSocketPath(): string;
132
132
  * @throws Error if the directory cannot be created
133
133
  */
134
134
  export declare function ensureSessionDir(): void;
135
+ /**
136
+ * The session's downloads directory (whether or not it exists).
137
+ *
138
+ * @returns Absolute path, `<session dir>/downloads`
139
+ */
140
+ export declare function getSessionDownloadsDir(): string;
141
+ /**
142
+ * Ensure the session's downloads directory exists (mode 0700, like the
143
+ * session directory) and return it.
144
+ *
145
+ * @returns Absolute path, `<session dir>/downloads`
146
+ * @throws Error if the directory cannot be created, or a file is in its place
147
+ */
148
+ export declare function ensureSessionDownloadsDir(): string;
135
149
  /** A session directory (or one above it) that cannot be trusted */
136
150
  export interface UntrustedSessionDir {
137
151
  /** The untrusted directory */
@@ -190,6 +190,31 @@ const GROUP_OTHER_ACCESS = 0o077;
190
190
  export function ensureSessionDir() {
191
191
  makeDirectory(getSessionDir(), PRIVATE_DIR_MODE);
192
192
  }
193
+ /** Subdirectory of a session directory that a launched Chrome downloads into */
194
+ const DOWNLOADS_DIR = 'downloads';
195
+ /**
196
+ * The session's downloads directory (whether or not it exists).
197
+ *
198
+ * @returns Absolute path, `<session dir>/downloads`
199
+ */
200
+ export function getSessionDownloadsDir() {
201
+ return path.join(getSessionDir(), DOWNLOADS_DIR);
202
+ }
203
+ /**
204
+ * Ensure the session's downloads directory exists (mode 0700, like the
205
+ * session directory) and return it.
206
+ *
207
+ * @returns Absolute path, `<session dir>/downloads`
208
+ * @throws Error if the directory cannot be created, or a file is in its place
209
+ */
210
+ export function ensureSessionDownloadsDir() {
211
+ const dir = getSessionDownloadsDir();
212
+ makeDirectory(dir, PRIVATE_DIR_MODE);
213
+ if (!fs.statSync(dir).isDirectory()) {
214
+ throw Object.assign(new Error(`${dir} is a file`), { code: 'ENOTDIR' });
215
+ }
216
+ return dir;
217
+ }
193
218
  /** Session directories need not keep group write out (umask 002 made them 0775) */
194
219
  const SESSION_DIR_TRUST = { allowGroupWrite: true };
195
220
  /**
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Download tracking: where a session's downloads go, and what became of them.
3
+ */
4
+ import type { CDPConnection } from '../connection/cdp.js';
5
+ import type { DownloadInfo } from '../ipc/protocol/domTypes.js';
6
+ /** A download the session saw begin, with Chrome's id for it */
7
+ export interface TrackedDownload extends DownloadInfo {
8
+ guid: string;
9
+ }
10
+ /**
11
+ * Where a session's downloads go: a directory bdg chose (a Chrome bdg
12
+ * launched), wherever the browser puts them (an attached Chrome), or nowhere
13
+ * (bdg's directory could not be created: refusing beats saving them to
14
+ * `~/Downloads`), with why
15
+ */
16
+ export type DownloadDestination = {
17
+ kind: 'directory';
18
+ dir: string;
19
+ } | {
20
+ kind: 'browser';
21
+ } | {
22
+ kind: 'refused';
23
+ reason: string;
24
+ };
25
+ /** Paths chosen for downloads still running, by {@link reservationKey} */
26
+ type Reservations = Set<string>;
27
+ /** Where the session keeps its downloads and what it says about them */
28
+ export interface DownloadRecord {
29
+ /** Downloads that began, oldest first, updated as they progress */
30
+ downloads: TrackedDownload[];
31
+ /** Set while downloads do not go where bdg meant them to (refused, or not redirected) */
32
+ downloadsWarning: string | undefined;
33
+ }
34
+ /**
35
+ * Track the session's downloads.
36
+ *
37
+ * In a directory, Chrome saves each download under its id (`allowAndName`),
38
+ * and the file is renamed to the suggested name once complete; the name is
39
+ * chosen when the download begins (`report (1).txt` when `report.txt` exists
40
+ * or was chosen for another), so a download still running already reports
41
+ * where it will be. In the browser's place, its own download settings stay
42
+ * and only its events are enabled. Refused downloads are canceled by Chrome
43
+ * and recorded with the reason.
44
+ *
45
+ * Chrome keeps a download behavior only while the connection that set it is
46
+ * open, so {@link attach} applies it again on another connection when the
47
+ * first one is lost. A browser-level connection also receives download
48
+ * events of other tabs (`target=_blank` links, `window.open()`), which do not
49
+ * reach a page's connection.
50
+ */
51
+ export declare class DownloadTracker {
52
+ private readonly record;
53
+ private readonly destination;
54
+ private readonly reserved;
55
+ private applied;
56
+ private unsubscribe;
57
+ /** Set once stopped: no connection is followed, nor any behavior set, afterwards */
58
+ private stopped;
59
+ /** Number of the latest attach: an earlier one still running gives way to it */
60
+ private latestAttach;
61
+ /** Attaches run one after another */
62
+ private attaching;
63
+ /**
64
+ * @param record - Session record receiving downloads and the warning
65
+ * @param destination - Where downloads should go
66
+ */
67
+ constructor(record: DownloadRecord, destination: DownloadDestination);
68
+ /**
69
+ * Apply the destination on a connection and follow its download events
70
+ * there (instead of on the previous one). When Chrome refuses it, reports
71
+ * stop claiming bdg's directory (downloads go where the browser puts them)
72
+ * and the record carries a warning.
73
+ *
74
+ * Attaches run one at a time, and the latest wins: one called meanwhile
75
+ * (the browser-level connection lost while it was being set up) makes an
76
+ * earlier one give way without following its connection or warning. After
77
+ * {@link stop}, nothing is followed and no behavior is set.
78
+ *
79
+ * @param cdp - Connection (browser-level when possible)
80
+ * @returns True when Chrome took the destination on this connection
81
+ */
82
+ attach(cdp: CDPConnection): Promise<boolean>;
83
+ /** Stop following download events */
84
+ stop(): void;
85
+ /**
86
+ * Whether an attach was superseded by a later one or by {@link stop}.
87
+ *
88
+ * @param attempt - Number of the attach
89
+ * @returns True when it must give way
90
+ */
91
+ private superseded;
92
+ /**
93
+ * Set the behavior on a connection and follow its events, unless the
94
+ * attach was superseded before or while Chrome answered.
95
+ *
96
+ * @param cdp - Connection
97
+ * @param attempt - Number of the attach
98
+ * @returns True when Chrome took the destination and the attach still stands
99
+ */
100
+ private applyOn;
101
+ /**
102
+ * Record a download that began, with the path chosen for it in bdg's directory.
103
+ *
104
+ * @param event - `Browser.downloadWillBegin` parameters
105
+ */
106
+ private begin;
107
+ }
108
+ /**
109
+ * A tracked download as commands report it, as it is now.
110
+ *
111
+ * @param download - Tracked download
112
+ * @returns Copy without Chrome's id
113
+ */
114
+ export declare function toDownloadInfo(download: TrackedDownload): DownloadInfo;
115
+ /**
116
+ * Choose a free path for a download: its suggested name, or with ` (1)`,
117
+ * ` (2)`… before the extension when a file or another running download has
118
+ * that name.
119
+ *
120
+ * @param downloadDir - Directory downloads are saved into
121
+ * @param suggestedFilename - Name the page or server suggested
122
+ * @param reserved - Paths chosen for downloads still running (the result is added)
123
+ * @returns Absolute path
124
+ */
125
+ export declare function reserveDownloadPath(downloadDir: string, suggestedFilename: string, reserved: Reservations): string;
126
+ export {};
127
+ //# sourceMappingURL=downloads.d.ts.map