browser-debugger-cli 0.14.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 (167) hide show
  1. package/.claude/skills/bdg/SKILL.md +3 -2
  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 +201 -133
  11. package/dist/commands/cleanup.js +21 -4
  12. package/dist/commands/dom/eval.d.ts +2 -1
  13. package/dist/commands/dom/eval.js +6 -21
  14. package/dist/commands/dom/formInteraction.js +8 -4
  15. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  16. package/dist/commands/dom/helpers/evalResult.js +59 -0
  17. package/dist/commands/dom/helpers/index.d.ts +4 -4
  18. package/dist/commands/dom/helpers/index.js +3 -3
  19. package/dist/commands/dom/helpers/query.d.ts +2 -2
  20. package/dist/commands/dom/helpers/query.js +2 -2
  21. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  22. package/dist/commands/dom/helpers/screenshot.js +50 -668
  23. package/dist/commands/dom/screenshot.js +56 -36
  24. package/dist/commands/helpJson.d.ts +1 -1
  25. package/dist/commands/helpJson.js +3 -3
  26. package/dist/commands/helpTopic.js +10 -4
  27. package/dist/commands/network/har.js +18 -14
  28. package/dist/commands/optionBehaviors.js +24 -9
  29. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  30. package/dist/commands/shared/CommandRunner.js +18 -3
  31. package/dist/commands/shared/interrupt.d.ts +40 -0
  32. package/dist/commands/shared/interrupt.js +73 -0
  33. package/dist/commands/shared/optionTypes.d.ts +3 -0
  34. package/dist/commands/shared/outputFile.d.ts +2 -1
  35. package/dist/commands/shared/outputFile.js +7 -4
  36. package/dist/commands/shared/startHelpers.d.ts +26 -3
  37. package/dist/commands/shared/startHelpers.js +145 -23
  38. package/dist/commands/status.js +3 -1
  39. package/dist/commands/stop.js +2 -1
  40. package/dist/commands/types.d.ts +5 -0
  41. package/dist/connection/cdp.js +1 -16
  42. package/dist/connection/chromeIdentity.d.ts +24 -5
  43. package/dist/connection/chromeIdentity.js +53 -22
  44. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  45. package/dist/connection/launcher/flagsBuilder.js +107 -23
  46. package/dist/connection/launcher.d.ts +35 -2
  47. package/dist/connection/launcher.js +99 -12
  48. package/dist/connection/typed-cdp.d.ts +3 -2
  49. package/dist/constants.d.ts +3 -5
  50. package/dist/constants.js +3 -5
  51. package/dist/daemon/SessionController.d.ts +10 -5
  52. package/dist/daemon/SessionController.js +15 -8
  53. package/dist/daemon/ipcServer.js +1 -1
  54. package/dist/daemon/launcher.d.ts +22 -3
  55. package/dist/daemon/launcher.js +45 -8
  56. package/dist/daemon/session/Session.d.ts +5 -1
  57. package/dist/daemon/session/Session.js +9 -8
  58. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  59. package/dist/daemon/session/TelemetryStore.js +4 -0
  60. package/dist/daemon/session/captureGate.d.ts +59 -0
  61. package/dist/daemon/session/captureGate.js +96 -0
  62. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  63. package/dist/daemon/session/chromeConnection.js +34 -4
  64. package/dist/daemon/session/collectors.d.ts +15 -0
  65. package/dist/daemon/session/collectors.js +39 -2
  66. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  67. package/dist/daemon/session/commandRegistry.js +48 -13
  68. package/dist/daemon/session/downloads.d.ts +32 -0
  69. package/dist/daemon/session/downloads.js +96 -0
  70. package/dist/daemon/session/interactions.d.ts +3 -2
  71. package/dist/daemon/session/interactions.js +7 -2
  72. package/dist/daemon/session/plugins.js +6 -0
  73. package/dist/daemon.js +18520 -17014
  74. package/dist/errors/CommandError.d.ts +2 -0
  75. package/dist/errors/issues.d.ts +1 -1
  76. package/dist/errors/messages.d.ts +81 -0
  77. package/dist/errors/messages.js +198 -6
  78. package/dist/index.js +1446 -1078
  79. package/dist/ipc/client.d.ts +20 -2
  80. package/dist/ipc/client.js +32 -6
  81. package/dist/ipc/protocol/commands.d.ts +36 -2
  82. package/dist/ipc/protocol/commands.js +1 -0
  83. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  84. package/dist/ipc/session/queries.d.ts +3 -0
  85. package/dist/ipc/session/types.d.ts +5 -0
  86. package/dist/ipc/transport/IPCError.d.ts +9 -0
  87. package/dist/ipc/transport/IPCError.js +12 -0
  88. package/dist/ipc/transport/errors.d.ts +2 -1
  89. package/dist/ipc/transport/errors.js +4 -1
  90. package/dist/ipc/transport/index.d.ts +10 -2
  91. package/dist/ipc/transport/index.js +29 -4
  92. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  93. package/dist/runtime/dom/actionEffects.js +269 -34
  94. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  95. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  96. package/dist/runtime/dom/captureArea.d.ts +35 -0
  97. package/dist/runtime/dom/captureArea.js +203 -0
  98. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  99. package/dist/runtime/dom/elementInfo.js +12 -3
  100. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  101. package/dist/runtime/dom/evalHelpers.js +40 -12
  102. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  103. package/dist/runtime/dom/frames.d.ts +2 -1
  104. package/dist/runtime/dom/frames.js +3 -1
  105. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  106. package/dist/runtime/page/bdgWorld.js +11 -0
  107. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  108. package/dist/runtime/page/captureEmulation.js +189 -0
  109. package/dist/runtime/page/captureScroll.d.ts +24 -0
  110. package/dist/runtime/page/captureScroll.js +124 -0
  111. package/dist/runtime/page/emulation.js +6 -5
  112. package/dist/runtime/page/screenshot.d.ts +41 -0
  113. package/dist/runtime/page/screenshot.js +394 -0
  114. package/dist/runtime/page/userAgent.d.ts +86 -2
  115. package/dist/runtime/page/userAgent.js +154 -33
  116. package/dist/session/paths.d.ts +52 -3
  117. package/dist/session/paths.js +179 -7
  118. package/dist/session/portClaims.d.ts +0 -8
  119. package/dist/session/portClaims.js +1 -22
  120. package/dist/session/sessionList.d.ts +5 -1
  121. package/dist/session/sessionList.js +5 -1
  122. package/dist/telemetry/downloads.d.ts +127 -0
  123. package/dist/telemetry/downloads.js +265 -0
  124. package/dist/telemetry/har/builder.d.ts +12 -1
  125. package/dist/telemetry/har/builder.js +32 -9
  126. package/dist/telemetry/har/sanitize.d.ts +28 -0
  127. package/dist/telemetry/har/sanitize.js +184 -0
  128. package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
  129. package/dist/telemetry/har/sanitizeBody.js +541 -0
  130. package/dist/telemetry/har/types.d.ts +2 -0
  131. package/dist/telemetry/network.d.ts +4 -4
  132. package/dist/telemetry/network.js +38 -4
  133. package/dist/telemetry/networkRetention.d.ts +35 -14
  134. package/dist/telemetry/networkRetention.js +62 -26
  135. package/dist/types.d.ts +9 -14
  136. package/dist/ui/OutputBuilder.d.ts +3 -2
  137. package/dist/ui/OutputBuilder.js +4 -3
  138. package/dist/ui/formatters/cdp.d.ts +32 -9
  139. package/dist/ui/formatters/cdp.js +77 -6
  140. package/dist/ui/formatters/details.js +7 -15
  141. package/dist/ui/formatters/preview.d.ts +2 -0
  142. package/dist/ui/formatters/preview.js +7 -1
  143. package/dist/ui/formatters/sessions.d.ts +3 -2
  144. package/dist/ui/formatters/sessions.js +10 -3
  145. package/dist/ui/formatters/status.js +6 -1
  146. package/dist/ui/formatting.d.ts +7 -0
  147. package/dist/ui/formatting.js +13 -0
  148. package/dist/ui/logging/logger.d.ts +1 -1
  149. package/dist/ui/messages/chrome.d.ts +27 -6
  150. package/dist/ui/messages/chrome.js +78 -12
  151. package/dist/ui/messages/commands.d.ts +71 -3
  152. package/dist/ui/messages/commands.js +98 -3
  153. package/dist/ui/messages/networkMessages.d.ts +50 -5
  154. package/dist/ui/messages/networkMessages.js +50 -6
  155. package/dist/ui/messages/session.d.ts +8 -0
  156. package/dist/ui/messages/session.js +10 -0
  157. package/dist/utils/async.d.ts +3 -2
  158. package/dist/utils/async.js +16 -3
  159. package/dist/utils/atomicFile.d.ts +2 -1
  160. package/dist/utils/atomicFile.js +5 -2
  161. package/dist/utils/directories.d.ts +41 -0
  162. package/dist/utils/directories.js +48 -0
  163. package/dist/utils/http.d.ts +11 -4
  164. package/dist/utils/http.js +5 -3
  165. package/package.json +18 -4
  166. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  167. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -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
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Download tracking: where a session's downloads go, and what became of them.
3
+ */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { createLogger } from '../ui/logging/index.js';
7
+ import { downloadsNotRedirectedWarning } from '../ui/messages/commands.js';
8
+ import { getErrorMessage } from '../utils/errors.js';
9
+ const log = createLogger('downloads');
10
+ /** Name given to a download whose suggested name is not a usable file name */
11
+ const FALLBACK_FILE_NAME = 'download';
12
+ /**
13
+ * Track the session's downloads.
14
+ *
15
+ * In a directory, Chrome saves each download under its id (`allowAndName`),
16
+ * and the file is renamed to the suggested name once complete; the name is
17
+ * chosen when the download begins (`report (1).txt` when `report.txt` exists
18
+ * or was chosen for another), so a download still running already reports
19
+ * where it will be. In the browser's place, its own download settings stay
20
+ * and only its events are enabled. Refused downloads are canceled by Chrome
21
+ * and recorded with the reason.
22
+ *
23
+ * Chrome keeps a download behavior only while the connection that set it is
24
+ * open, so {@link attach} applies it again on another connection when the
25
+ * first one is lost. A browser-level connection also receives download
26
+ * events of other tabs (`target=_blank` links, `window.open()`), which do not
27
+ * reach a page's connection.
28
+ */
29
+ export class DownloadTracker {
30
+ record;
31
+ destination;
32
+ reserved = new Set();
33
+ applied = { kind: 'browser' };
34
+ unsubscribe = () => undefined;
35
+ /** Set once stopped: no connection is followed, nor any behavior set, afterwards */
36
+ stopped = false;
37
+ /** Number of the latest attach: an earlier one still running gives way to it */
38
+ latestAttach = 0;
39
+ /** Attaches run one after another */
40
+ attaching = Promise.resolve();
41
+ /**
42
+ * @param record - Session record receiving downloads and the warning
43
+ * @param destination - Where downloads should go
44
+ */
45
+ constructor(record, destination) {
46
+ this.record = record;
47
+ this.destination = destination;
48
+ if (destination.kind === 'refused')
49
+ record.downloadsWarning = destination.reason;
50
+ }
51
+ /**
52
+ * Apply the destination on a connection and follow its download events
53
+ * there (instead of on the previous one). When Chrome refuses it, reports
54
+ * stop claiming bdg's directory (downloads go where the browser puts them)
55
+ * and the record carries a warning.
56
+ *
57
+ * Attaches run one at a time, and the latest wins: one called meanwhile
58
+ * (the browser-level connection lost while it was being set up) makes an
59
+ * earlier one give way without following its connection or warning. After
60
+ * {@link stop}, nothing is followed and no behavior is set.
61
+ *
62
+ * @param cdp - Connection (browser-level when possible)
63
+ * @returns True when Chrome took the destination on this connection
64
+ */
65
+ attach(cdp) {
66
+ const attempt = ++this.latestAttach;
67
+ const run = this.attaching.then(() => this.applyOn(cdp, attempt));
68
+ this.attaching = run.catch(() => undefined);
69
+ return run;
70
+ }
71
+ /** Stop following download events */
72
+ stop() {
73
+ this.stopped = true;
74
+ this.unsubscribe();
75
+ }
76
+ /**
77
+ * Whether an attach was superseded by a later one or by {@link stop}.
78
+ *
79
+ * @param attempt - Number of the attach
80
+ * @returns True when it must give way
81
+ */
82
+ superseded(attempt) {
83
+ return this.stopped || attempt !== this.latestAttach;
84
+ }
85
+ /**
86
+ * Set the behavior on a connection and follow its events, unless the
87
+ * attach was superseded before or while Chrome answered.
88
+ *
89
+ * @param cdp - Connection
90
+ * @param attempt - Number of the attach
91
+ * @returns True when Chrome took the destination and the attach still stands
92
+ */
93
+ async applyOn(cdp, attempt) {
94
+ if (this.superseded(attempt))
95
+ return false;
96
+ this.unsubscribe();
97
+ const error = await setDownloadBehavior(cdp, this.destination);
98
+ if (this.superseded(attempt))
99
+ return false;
100
+ this.applied = error === undefined ? this.destination : { kind: 'browser' };
101
+ if (this.destination.kind === 'directory') {
102
+ this.record.downloadsWarning =
103
+ error === undefined ? undefined : downloadsNotRedirectedWarning(error);
104
+ }
105
+ const handlers = [
106
+ cdp.on('Browser.downloadWillBegin', (event) => this.begin(event)),
107
+ cdp.on('Browser.downloadProgress', (progress) => {
108
+ const download = this.record.downloads.findLast((entry) => entry.guid === progress.guid);
109
+ if (download?.state !== 'inProgress')
110
+ return;
111
+ updateDownload(download, progress, this.applied, this.reserved);
112
+ }),
113
+ ];
114
+ this.unsubscribe = () => handlers.forEach((remove) => remove());
115
+ return error === undefined;
116
+ }
117
+ /**
118
+ * Record a download that began, with the path chosen for it in bdg's directory.
119
+ *
120
+ * @param event - `Browser.downloadWillBegin` parameters
121
+ */
122
+ begin({ guid, url, suggestedFilename }) {
123
+ const dir = this.applied.kind === 'directory' ? this.applied.dir : undefined;
124
+ const target = dir && reserveDownloadPath(dir, suggestedFilename, this.reserved);
125
+ this.record.downloads.push({
126
+ guid,
127
+ url,
128
+ suggestedFilename,
129
+ state: 'inProgress',
130
+ ...(target && { path: target }),
131
+ });
132
+ }
133
+ }
134
+ /**
135
+ * A tracked download as commands report it, as it is now.
136
+ *
137
+ * @param download - Tracked download
138
+ * @returns Copy without Chrome's id
139
+ */
140
+ export function toDownloadInfo(download) {
141
+ const { url, suggestedFilename, path: file, state, bytes, reason } = download;
142
+ return {
143
+ url,
144
+ suggestedFilename,
145
+ ...(file !== undefined && { path: file }),
146
+ state,
147
+ ...(bytes !== undefined && { bytes }),
148
+ ...(reason !== undefined && { reason }),
149
+ };
150
+ }
151
+ /**
152
+ * Set the browser's download behavior for the destination and enable
153
+ * download events.
154
+ *
155
+ * @param cdp - CDP connection
156
+ * @param destination - Where downloads go
157
+ * @returns Why Chrome refused it, or undefined when it took it
158
+ */
159
+ async function setDownloadBehavior(cdp, destination) {
160
+ const behavior = destination.kind === 'directory'
161
+ ? { behavior: 'allowAndName', downloadPath: destination.dir }
162
+ : { behavior: destination.kind === 'refused' ? 'deny' : 'default' };
163
+ try {
164
+ await cdp.send('Browser.setDownloadBehavior', { ...behavior, eventsEnabled: true });
165
+ return undefined;
166
+ }
167
+ catch (error) {
168
+ log.info(`Download behavior not set: ${getErrorMessage(error)}`);
169
+ return getErrorMessage(error);
170
+ }
171
+ }
172
+ /**
173
+ * Apply a progress event: bytes so far, then the final state; a completed
174
+ * download in bdg's directory is renamed from its id to its chosen name.
175
+ *
176
+ * @param download - Download being updated
177
+ * @param progress - Progress event
178
+ * @param destination - Where downloads go
179
+ * @param reserved - Paths chosen for downloads still running
180
+ */
181
+ function updateDownload(download, progress, destination, reserved) {
182
+ download.bytes = progress.receivedBytes;
183
+ if (progress.state === 'inProgress')
184
+ return;
185
+ if (download.path)
186
+ reserved.delete(reservationKey(download.path));
187
+ if (progress.state === 'canceled') {
188
+ delete download.path;
189
+ if (destination.kind === 'refused')
190
+ download.reason = destination.reason;
191
+ }
192
+ else if (destination.kind === 'directory') {
193
+ download.path = saveUnderChosenName(destination.dir, download, reserved);
194
+ }
195
+ else if (progress.filePath) {
196
+ download.path = progress.filePath;
197
+ }
198
+ download.state = progress.state;
199
+ }
200
+ /**
201
+ * Rename a completed download from its id to the name chosen for it (another
202
+ * one when a file took that name meanwhile).
203
+ *
204
+ * @param downloadDir - Directory downloads are saved into
205
+ * @param download - Completed download
206
+ * @param reserved - Paths chosen for downloads still running
207
+ * @returns Path of the file: the chosen one, or its id's when renaming failed
208
+ */
209
+ function saveUnderChosenName(downloadDir, download, reserved) {
210
+ const saved = path.join(downloadDir, download.guid);
211
+ const chosen = download.path && !fs.existsSync(download.path)
212
+ ? download.path
213
+ : reserveDownloadPath(downloadDir, download.suggestedFilename, reserved);
214
+ reserved.delete(reservationKey(chosen));
215
+ try {
216
+ fs.renameSync(saved, chosen);
217
+ return chosen;
218
+ }
219
+ catch (error) {
220
+ log.debug(`Download ${download.guid} kept under its id: ${getErrorMessage(error)}`);
221
+ return saved;
222
+ }
223
+ }
224
+ /**
225
+ * Key of a reserved path: lowercased, so `Report.txt` and `report.txt` do not
226
+ * both get chosen on a case-insensitive file system (macOS, Windows).
227
+ *
228
+ * @param file - Path
229
+ * @returns Key
230
+ */
231
+ function reservationKey(file) {
232
+ return file.toLowerCase();
233
+ }
234
+ /**
235
+ * Choose a free path for a download: its suggested name, or with ` (1)`,
236
+ * ` (2)`… before the extension when a file or another running download has
237
+ * that name.
238
+ *
239
+ * @param downloadDir - Directory downloads are saved into
240
+ * @param suggestedFilename - Name the page or server suggested
241
+ * @param reserved - Paths chosen for downloads still running (the result is added)
242
+ * @returns Absolute path
243
+ */
244
+ export function reserveDownloadPath(downloadDir, suggestedFilename, reserved) {
245
+ const name = safeFileName(suggestedFilename);
246
+ const { name: stem, ext } = path.parse(name);
247
+ for (let copy = 0;; copy++) {
248
+ const candidate = path.join(downloadDir, copy === 0 ? name : `${stem} (${copy})${ext}`);
249
+ if (reserved.has(reservationKey(candidate)) || fs.existsSync(candidate))
250
+ continue;
251
+ reserved.add(reservationKey(candidate));
252
+ return candidate;
253
+ }
254
+ }
255
+ /**
256
+ * A suggested file name reduced to a name in the download directory.
257
+ *
258
+ * @param suggestedFilename - Name the page or server suggested
259
+ * @returns Its last path segment, or {@link FALLBACK_FILE_NAME} when that is empty, `.` or `..`
260
+ */
261
+ function safeFileName(suggestedFilename) {
262
+ const name = path.basename(suggestedFilename.replaceAll('\\', '/'));
263
+ return name === '' || name === '.' || name === '..' ? FALLBACK_FILE_NAME : name;
264
+ }
265
+ //# sourceMappingURL=downloads.js.map
@@ -18,11 +18,22 @@ export interface HARMetadata {
18
18
  /** Target title (optional) */
19
19
  targetTitle?: string;
20
20
  }
21
+ /**
22
+ * Options for HAR generation.
23
+ */
24
+ export interface HAROptions {
25
+ /** Keep credentials as captured instead of redacting them (see sanitize.ts) */
26
+ includeSensitive?: boolean;
27
+ }
21
28
  /**
22
29
  * Build HAR 1.2 format from network telemetry data.
23
30
  *
31
+ * Credentials are redacted (`log.comment` says so) unless
32
+ * `options.includeSensitive` is set.
33
+ *
24
34
  * @param requests - Array of network requests collected during session
25
35
  * @param metadata - Metadata for HAR creator/browser info
36
+ * @param options - Whether to keep credentials
26
37
  * @returns Complete HAR object
27
38
  *
28
39
  * @remarks
@@ -39,5 +50,5 @@ export interface HARMetadata {
39
50
  * fs.writeFileSync('capture.har', JSON.stringify(har, null, 2));
40
51
  * ```
41
52
  */
42
- export declare function buildHAR(requests: NetworkRequest[], metadata: HARMetadata): HAR;
53
+ export declare function buildHAR(requests: NetworkRequest[], metadata: HARMetadata, options?: HAROptions): HAR;
43
54
  //# sourceMappingURL=builder.d.ts.map
@@ -2,7 +2,9 @@
2
2
  * HAR (HTTP Archive) builder for transforming network telemetry to HAR 1.2 format.
3
3
  */
4
4
  import { createRequire } from 'node:module';
5
+ import { sanitizeEntry } from './sanitize.js';
5
6
  import { skippedBodyReason } from '../networkRetention.js';
7
+ import { harSanitizedComment } from '../../ui/messages/networkMessages.js';
6
8
  /**
7
9
  * Loads Node builtins on first use: importing `node:http` in an ES module
8
10
  * reads all its exports, which loads undici and zlib (about 8 ms of every CLI
@@ -14,8 +16,12 @@ const DEFAULT_HTTP_VERSION = 'HTTP/1.1';
14
16
  /**
15
17
  * Build HAR 1.2 format from network telemetry data.
16
18
  *
19
+ * Credentials are redacted (`log.comment` says so) unless
20
+ * `options.includeSensitive` is set.
21
+ *
17
22
  * @param requests - Array of network requests collected during session
18
23
  * @param metadata - Metadata for HAR creator/browser info
24
+ * @param options - Whether to keep credentials
19
25
  * @returns Complete HAR object
20
26
  *
21
27
  * @remarks
@@ -32,8 +38,9 @@ const DEFAULT_HTTP_VERSION = 'HTTP/1.1';
32
38
  * fs.writeFileSync('capture.har', JSON.stringify(har, null, 2));
33
39
  * ```
34
40
  */
35
- export function buildHAR(requests, metadata) {
36
- const entries = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
41
+ export function buildHAR(requests, metadata, options = {}) {
42
+ const built = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
43
+ const entries = options.includeSensitive ? built : built.map(sanitizeEntry);
37
44
  const log = {
38
45
  version: '1.2',
39
46
  creator: {
@@ -42,6 +49,7 @@ export function buildHAR(requests, metadata) {
42
49
  comment: 'Browser Debugger CLI - https://github.com/szymdzum/browser-debugger-cli',
43
50
  },
44
51
  entries,
52
+ ...(!options.includeSensitive && { comment: harSanitizedComment() }),
45
53
  };
46
54
  if (metadata.chromeVersion) {
47
55
  log.browser = {
@@ -93,6 +101,7 @@ function buildWebSocketMessage(frame) {
93
101
  time: frame.timestamp / 1000,
94
102
  opcode: frame.opcode,
95
103
  data: frame.payloadData,
104
+ ...(frame.truncatedFrom !== undefined && { _truncatedFrom: frame.truncatedFrom }),
96
105
  };
97
106
  }
98
107
  /**
@@ -111,7 +120,7 @@ function buildRequest(req) {
111
120
  headers: convertHeaders(req.requestHeaders),
112
121
  queryString: extractQueryParams(url),
113
122
  headersSize: estimateRequestHeadersSize(req.method, req.url, req.requestHeaders),
114
- bodySize: req.requestBody ? Buffer.byteLength(req.requestBody, 'utf-8') : 0,
123
+ bodySize: requestBodySize(req),
115
124
  };
116
125
  const postData = buildPostData(req);
117
126
  if (postData) {
@@ -181,7 +190,21 @@ function contentLength(req) {
181
190
  return Number.isInteger(bytes) && bytes >= 0 ? bytes : undefined;
182
191
  }
183
192
  /**
184
- * Build POST data object if request has body.
193
+ * Size of the request body: unknown (-1) when it was evicted at the body budget.
194
+ *
195
+ * @param req - Network request data
196
+ * @returns Body size in bytes, or -1
197
+ */
198
+ function requestBodySize(req) {
199
+ if (!req.requestBody)
200
+ return 0;
201
+ if (skippedBodyReason(req.requestBody) !== undefined)
202
+ return -1;
203
+ return Buffer.byteLength(req.requestBody, 'utf-8');
204
+ }
205
+ /**
206
+ * Build POST data object if request has body. A body bdg did not keep is
207
+ * exported without `text` and with the reason as `comment`.
185
208
  *
186
209
  * @param req - Network request data
187
210
  * @returns POST data object or undefined
@@ -189,11 +212,11 @@ function contentLength(req) {
189
212
  function buildPostData(req) {
190
213
  if (!req.requestBody)
191
214
  return undefined;
192
- const contentType = getHeader(req.requestHeaders, 'content-type') ?? 'text/plain';
193
- return {
194
- mimeType: contentType,
195
- text: req.requestBody,
196
- };
215
+ const mimeType = getHeader(req.requestHeaders, 'content-type') ?? 'text/plain';
216
+ const skipped = skippedBodyReason(req.requestBody);
217
+ if (skipped !== undefined)
218
+ return { mimeType, comment: `Body not captured: ${skipped}` };
219
+ return { mimeType, text: req.requestBody };
197
220
  }
198
221
  /**
199
222
  * Duration between two CDP timing offsets (ms), or -1 if either is unknown.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Credential redaction for HAR exports.
3
+ *
4
+ * HAR files are made to be shared (bug reports, tickets), so `bdg network har`
5
+ * redacts credentials by default, as Chrome DevTools does since Chrome 130.
6
+ * Chrome's sanitized export drops the `Cookie`, `Set-Cookie` and
7
+ * `Authorization` headers and empties the `cookies` arrays; bdg instead keeps
8
+ * every header, cookie and parameter name (and cookie attributes such as
9
+ * `httpOnly`) with the value `[redacted]`, so the export still shows that a
10
+ * request was authenticated and which cookies were set. It also covers API
11
+ * key, token and session headers, credential query parameters in URLs
12
+ * (`?code=`, `?access_token=`), and credential fields of request and response
13
+ * bodies and of WebSocket text messages (sanitizeBody.ts), editing only
14
+ * those values. `headersSize`, `bodySize` and `content.size` stay those of the
15
+ * captured request. Base64 bodies are decoded when their type is generic,
16
+ * JSON, form or event-stream, and binary WebSocket messages always; those
17
+ * that are not UTF-8 text, and other binary bodies, are kept. A body or
18
+ * message the sanitizer fails on is replaced whole by {@link REDACTED}.
19
+ */
20
+ import type { Entry } from './types.js';
21
+ /**
22
+ * Redact the credentials of a HAR entry.
23
+ *
24
+ * @param entry - Entry built from the captured request
25
+ * @returns Copy of the entry with credential values replaced by {@link REDACTED}
26
+ */
27
+ export declare function sanitizeEntry(entry: Entry): Entry;
28
+ //# sourceMappingURL=sanitize.d.ts.map