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.
- package/.claude/skills/bdg/SKILL.md +3 -2
- package/dist/cdp/methodTarget.d.ts +92 -0
- package/dist/cdp/methodTarget.js +159 -0
- package/dist/cdp/protocol.d.ts +16 -1
- package/dist/cdp/protocol.js +21 -0
- package/dist/cdp/schema.d.ts +55 -1
- package/dist/cdp/schema.js +134 -25
- package/dist/cdp/types.d.ts +3 -1
- package/dist/commands/cdp.d.ts +38 -1
- package/dist/commands/cdp.js +201 -133
- package/dist/commands/cleanup.js +21 -4
- package/dist/commands/dom/eval.d.ts +2 -1
- package/dist/commands/dom/eval.js +6 -21
- package/dist/commands/dom/formInteraction.js +8 -4
- package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
- package/dist/commands/dom/helpers/evalResult.js +59 -0
- package/dist/commands/dom/helpers/index.d.ts +4 -4
- package/dist/commands/dom/helpers/index.js +3 -3
- package/dist/commands/dom/helpers/query.d.ts +2 -2
- package/dist/commands/dom/helpers/query.js +2 -2
- package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
- package/dist/commands/dom/helpers/screenshot.js +50 -668
- package/dist/commands/dom/screenshot.js +56 -36
- package/dist/commands/helpJson.d.ts +1 -1
- package/dist/commands/helpJson.js +3 -3
- package/dist/commands/helpTopic.js +10 -4
- package/dist/commands/network/har.js +18 -14
- package/dist/commands/optionBehaviors.js +24 -9
- package/dist/commands/shared/CommandRunner.d.ts +5 -0
- package/dist/commands/shared/CommandRunner.js +18 -3
- package/dist/commands/shared/interrupt.d.ts +40 -0
- package/dist/commands/shared/interrupt.js +73 -0
- package/dist/commands/shared/optionTypes.d.ts +3 -0
- package/dist/commands/shared/outputFile.d.ts +2 -1
- package/dist/commands/shared/outputFile.js +7 -4
- package/dist/commands/shared/startHelpers.d.ts +26 -3
- package/dist/commands/shared/startHelpers.js +145 -23
- package/dist/commands/status.js +3 -1
- package/dist/commands/stop.js +2 -1
- package/dist/commands/types.d.ts +5 -0
- package/dist/connection/cdp.js +1 -16
- package/dist/connection/chromeIdentity.d.ts +24 -5
- package/dist/connection/chromeIdentity.js +53 -22
- package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
- package/dist/connection/launcher/flagsBuilder.js +107 -23
- package/dist/connection/launcher.d.ts +35 -2
- package/dist/connection/launcher.js +99 -12
- package/dist/connection/typed-cdp.d.ts +3 -2
- package/dist/constants.d.ts +3 -5
- package/dist/constants.js +3 -5
- package/dist/daemon/SessionController.d.ts +10 -5
- package/dist/daemon/SessionController.js +15 -8
- package/dist/daemon/ipcServer.js +1 -1
- package/dist/daemon/launcher.d.ts +22 -3
- package/dist/daemon/launcher.js +45 -8
- package/dist/daemon/session/Session.d.ts +5 -1
- package/dist/daemon/session/Session.js +9 -8
- package/dist/daemon/session/TelemetryStore.d.ts +5 -0
- package/dist/daemon/session/TelemetryStore.js +4 -0
- package/dist/daemon/session/captureGate.d.ts +59 -0
- package/dist/daemon/session/captureGate.js +96 -0
- package/dist/daemon/session/chromeConnection.d.ts +16 -1
- package/dist/daemon/session/chromeConnection.js +34 -4
- package/dist/daemon/session/collectors.d.ts +15 -0
- package/dist/daemon/session/collectors.js +39 -2
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +48 -13
- package/dist/daemon/session/downloads.d.ts +32 -0
- package/dist/daemon/session/downloads.js +96 -0
- package/dist/daemon/session/interactions.d.ts +3 -2
- package/dist/daemon/session/interactions.js +7 -2
- package/dist/daemon/session/plugins.js +6 -0
- package/dist/daemon.js +18520 -17014
- package/dist/errors/CommandError.d.ts +2 -0
- package/dist/errors/issues.d.ts +1 -1
- package/dist/errors/messages.d.ts +81 -0
- package/dist/errors/messages.js +198 -6
- package/dist/index.js +1446 -1078
- package/dist/ipc/client.d.ts +20 -2
- package/dist/ipc/client.js +32 -6
- package/dist/ipc/protocol/commands.d.ts +36 -2
- package/dist/ipc/protocol/commands.js +1 -0
- package/dist/ipc/protocol/domTypes.d.ts +24 -1
- package/dist/ipc/session/queries.d.ts +3 -0
- package/dist/ipc/session/types.d.ts +5 -0
- package/dist/ipc/transport/IPCError.d.ts +9 -0
- package/dist/ipc/transport/IPCError.js +12 -0
- package/dist/ipc/transport/errors.d.ts +2 -1
- package/dist/ipc/transport/errors.js +4 -1
- package/dist/ipc/transport/index.d.ts +10 -2
- package/dist/ipc/transport/index.js +29 -4
- package/dist/runtime/dom/actionEffects.d.ts +48 -9
- package/dist/runtime/dom/actionEffects.js +269 -34
- package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
- package/dist/runtime/dom/actionEffectsScripts.js +101 -2
- package/dist/runtime/dom/captureArea.d.ts +35 -0
- package/dist/runtime/dom/captureArea.js +203 -0
- package/dist/runtime/dom/elementInfo.d.ts +13 -4
- package/dist/runtime/dom/elementInfo.js +12 -3
- package/dist/runtime/dom/evalHelpers.d.ts +24 -4
- package/dist/runtime/dom/evalHelpers.js +40 -12
- package/dist/runtime/dom/formDiscovery.d.ts +1 -1
- package/dist/runtime/dom/frames.d.ts +2 -1
- package/dist/runtime/dom/frames.js +3 -1
- package/dist/runtime/page/bdgWorld.d.ts +9 -0
- package/dist/runtime/page/bdgWorld.js +11 -0
- package/dist/runtime/page/captureEmulation.d.ts +119 -0
- package/dist/runtime/page/captureEmulation.js +189 -0
- package/dist/runtime/page/captureScroll.d.ts +24 -0
- package/dist/runtime/page/captureScroll.js +124 -0
- package/dist/runtime/page/emulation.js +6 -5
- package/dist/runtime/page/screenshot.d.ts +41 -0
- package/dist/runtime/page/screenshot.js +394 -0
- package/dist/runtime/page/userAgent.d.ts +86 -2
- package/dist/runtime/page/userAgent.js +154 -33
- package/dist/session/paths.d.ts +52 -3
- package/dist/session/paths.js +179 -7
- package/dist/session/portClaims.d.ts +0 -8
- package/dist/session/portClaims.js +1 -22
- package/dist/session/sessionList.d.ts +5 -1
- package/dist/session/sessionList.js +5 -1
- package/dist/telemetry/downloads.d.ts +127 -0
- package/dist/telemetry/downloads.js +265 -0
- package/dist/telemetry/har/builder.d.ts +12 -1
- package/dist/telemetry/har/builder.js +32 -9
- package/dist/telemetry/har/sanitize.d.ts +28 -0
- package/dist/telemetry/har/sanitize.js +184 -0
- package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
- package/dist/telemetry/har/sanitizeBody.js +541 -0
- package/dist/telemetry/har/types.d.ts +2 -0
- package/dist/telemetry/network.d.ts +4 -4
- package/dist/telemetry/network.js +38 -4
- package/dist/telemetry/networkRetention.d.ts +35 -14
- package/dist/telemetry/networkRetention.js +62 -26
- package/dist/types.d.ts +9 -14
- package/dist/ui/OutputBuilder.d.ts +3 -2
- package/dist/ui/OutputBuilder.js +4 -3
- package/dist/ui/formatters/cdp.d.ts +32 -9
- package/dist/ui/formatters/cdp.js +77 -6
- package/dist/ui/formatters/details.js +7 -15
- package/dist/ui/formatters/preview.d.ts +2 -0
- package/dist/ui/formatters/preview.js +7 -1
- package/dist/ui/formatters/sessions.d.ts +3 -2
- package/dist/ui/formatters/sessions.js +10 -3
- package/dist/ui/formatters/status.js +6 -1
- package/dist/ui/formatting.d.ts +7 -0
- package/dist/ui/formatting.js +13 -0
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/chrome.d.ts +27 -6
- package/dist/ui/messages/chrome.js +78 -12
- package/dist/ui/messages/commands.d.ts +71 -3
- package/dist/ui/messages/commands.js +98 -3
- package/dist/ui/messages/networkMessages.d.ts +50 -5
- package/dist/ui/messages/networkMessages.js +50 -6
- package/dist/ui/messages/session.d.ts +8 -0
- package/dist/ui/messages/session.js +10 -0
- package/dist/utils/async.d.ts +3 -2
- package/dist/utils/async.js +16 -3
- package/dist/utils/atomicFile.d.ts +2 -1
- package/dist/utils/atomicFile.js +5 -2
- package/dist/utils/directories.d.ts +41 -0
- package/dist/utils/directories.js +48 -0
- package/dist/utils/http.d.ts +11 -4
- package/dist/utils/http.js +5 -3
- package/package.json +18 -4
- /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
- /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
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|