@jobshimo/browser-link 0.28.8 → 0.29.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/dist/activity/extract.js +2 -0
- package/dist/activity/extract.js.map +1 -1
- package/dist/agent-instructions/content.js +7 -1
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/bridge/events.d.ts +1 -1
- package/dist/bridge/events.js.map +1 -1
- package/dist/cdp/support.d.ts +2 -1
- package/dist/cdp/support.js +6 -1
- package/dist/cdp/support.js.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.js +2 -0
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/config.d.ts +12 -0
- package/dist/commands/config.js +49 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js.map +1 -1
- package/dist/downloads.d.ts +127 -0
- package/dist/downloads.js +303 -0
- package/dist/downloads.js.map +1 -0
- package/dist/extension/background.js +49 -0
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/download-capture.d.ts +96 -0
- package/dist/extension/download-capture.js +131 -0
- package/dist/extension/download-capture.js.map +1 -0
- package/dist/extension/manifest.json +1 -1
- package/dist/permissions.js +6 -0
- package/dist/permissions.js.map +1 -1
- package/dist/server.js +8 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +45 -1
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +12 -1
- package/dist/tools/browser-dispatch.js +134 -0
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +18 -4
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `browser.download` support: everything the tool needs that is NOT talking
|
|
3
|
+
* to Chrome — filename policy, the download directory, the file write, the
|
|
4
|
+
* in-page fetch expression and the shaping of the result the agent sees.
|
|
5
|
+
*
|
|
6
|
+
* WHY THE BYTES COME THROUGH THE SERVER. The obvious design — tell Chrome
|
|
7
|
+
* "save downloads to <dir>, never ask" with CDP `Browser.setDownloadBehavior`
|
|
8
|
+
* — is not available to us: Chromium refuses it (and the tab-scoped
|
|
9
|
+
* `Page.setDownloadBehavior`, which delegates to the same check) for any
|
|
10
|
+
* `chrome.debugger` client, because the extension debugger host reports
|
|
11
|
+
* `MayWriteLocalFiles() == false`. And `chrome.downloads.download({saveAs:
|
|
12
|
+
* false})` does NOT suppress the dialog either: Chrome still prompts when the
|
|
13
|
+
* user enabled "Ask where to save each file". So instead the bytes are
|
|
14
|
+
* fetched IN THE PAGE (with the page's own cookies) and written here, into a
|
|
15
|
+
* directory only the human configures.
|
|
16
|
+
*
|
|
17
|
+
* Chrome's download manager never SAVES anything. In url mode it is not
|
|
18
|
+
* involved at all. In selector mode the extension uses it only to catch the
|
|
19
|
+
* download a click starts: it hooks `chrome.downloads.onDeterminingFilename`,
|
|
20
|
+
* which Chrome consults before prompting, cancels and erases that download
|
|
21
|
+
* there, and hands its URL to the same in-page fetch (see the extension's
|
|
22
|
+
* `download-capture.ts`).
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_DOWNLOAD_TIMEOUT_MS = 30000;
|
|
25
|
+
export declare const MAX_DOWNLOAD_TIMEOUT_MS = 120000;
|
|
26
|
+
/** Hard cap on one download's size. The bytes travel base64-encoded inside a
|
|
27
|
+
* CDP `Runtime.evaluate` result and then a bridge frame, so this also bounds
|
|
28
|
+
* the memory both hops can be asked to hold. */
|
|
29
|
+
export declare const MAX_DOWNLOAD_BYTES: number;
|
|
30
|
+
/** Extra bridge timeout on top of each hop's own budget (selector mode has
|
|
31
|
+
* two hops: capture, then fetch), for the round trip only — the capture and
|
|
32
|
+
* the in-page fetch enforce their budgets themselves and always answer, so
|
|
33
|
+
* this is spent only when the bridge itself stalls. */
|
|
34
|
+
export declare const DOWNLOAD_BRIDGE_SLACK_MS = 10000;
|
|
35
|
+
/**
|
|
36
|
+
* Validate a filename the AGENT asked for. Strict on purpose: the agent may
|
|
37
|
+
* name the file but never choose where it lands, so anything that could be
|
|
38
|
+
* read as a path (separators, `..`, a drive prefix) is rejected outright
|
|
39
|
+
* rather than quietly cleaned up.
|
|
40
|
+
*/
|
|
41
|
+
export declare function validateRequestedFilename(name: unknown): string;
|
|
42
|
+
/**
|
|
43
|
+
* Make a filename that came from the PAGE (Chrome's suggestion, a
|
|
44
|
+
* Content-Disposition header, a URL) safe to write: keep the last path
|
|
45
|
+
* segment, replace reserved characters, drop control characters and leading
|
|
46
|
+
* or trailing dots/spaces, and defuse Windows device names with a `_`
|
|
47
|
+
* prefix. Cleaned up rather than rejected: the page, not the agent, chose
|
|
48
|
+
* it. Returns null when nothing usable is left.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sanitizeDerivedFilename(raw: string | null | undefined): string | null;
|
|
51
|
+
/** Extract the filename from a Content-Disposition header, preferring the
|
|
52
|
+
* RFC 5987 `filename*=` form (it carries non-ASCII names correctly). */
|
|
53
|
+
export declare function filenameFromContentDisposition(header: string | null | undefined): string | null;
|
|
54
|
+
/** Last path segment of an http(s) URL, decoded. Null for `blob:`/`data:`
|
|
55
|
+
* URLs (their "path" is an opaque id or the payload itself). */
|
|
56
|
+
export declare function filenameFromUrl(url: string | null | undefined): string | null;
|
|
57
|
+
export interface FilenameSources {
|
|
58
|
+
/** Already validated by `validateRequestedFilename`. */
|
|
59
|
+
requested?: string;
|
|
60
|
+
/** Chrome's own suggestion for a captured download. */
|
|
61
|
+
suggested?: string | null;
|
|
62
|
+
contentDisposition?: string | null;
|
|
63
|
+
url?: string | null;
|
|
64
|
+
}
|
|
65
|
+
/** requested → Chrome's suggestion → Content-Disposition → URL → "download". */
|
|
66
|
+
export declare function pickFilename(sources: FilenameSources): string;
|
|
67
|
+
export declare function clampDownloadTimeoutMs(value: unknown): number;
|
|
68
|
+
/**
|
|
69
|
+
* Where downloads land: the human-configured `downloads.dir` when it is an
|
|
70
|
+
* absolute path, otherwise `<dataDir>/downloads`. config.json is untrusted
|
|
71
|
+
* (hand-edited, corrupted), so a relative or non-string value falls back to
|
|
72
|
+
* the default instead of resolving against whatever the cwd happens to be.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveDownloadsDir(cfg: {
|
|
75
|
+
downloadsDir?: unknown;
|
|
76
|
+
}, dataDir: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Write `data` as `filename` inside `dir`, creating `dir` if needed. Never
|
|
79
|
+
* overwrites: an existing name becomes `name (1).ext`, `name (2).ext`, …
|
|
80
|
+
* (the `wx` flag makes the existence check and the create one atomic step).
|
|
81
|
+
* Returns the absolute path written.
|
|
82
|
+
*/
|
|
83
|
+
export declare function writeDownloadFile(dir: string, filename: string, data: Buffer): string;
|
|
84
|
+
/** What the in-page fetch expression resolves to. */
|
|
85
|
+
export interface FetchDownloadOutcome {
|
|
86
|
+
state: 'complete' | 'interrupted' | 'timeout';
|
|
87
|
+
status?: number;
|
|
88
|
+
mime?: string | null;
|
|
89
|
+
content_disposition?: string | null;
|
|
90
|
+
final_url?: string | null;
|
|
91
|
+
/** Bytes received — the full size on `complete`, partial otherwise. */
|
|
92
|
+
bytes: number;
|
|
93
|
+
/** Present only on `complete`. */
|
|
94
|
+
data_b64?: string;
|
|
95
|
+
error?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the expression `Runtime.evaluate` runs in the page: fetch `url` with
|
|
99
|
+
* the page's credentials, stream the body (so `maxBytes` is enforced while
|
|
100
|
+
* reading, not after buffering everything), and base64 it. Every exit path
|
|
101
|
+
* resolves to a `FetchDownloadOutcome` — a timeout or an error reports how
|
|
102
|
+
* far it got rather than throwing, so the agent always gets partial info.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildFetchDownloadJs(opts: {
|
|
105
|
+
url: string;
|
|
106
|
+
timeoutMs: number;
|
|
107
|
+
maxBytes: number;
|
|
108
|
+
}): string;
|
|
109
|
+
/** The result `browser.download` returns to the agent. */
|
|
110
|
+
export interface DownloadResult {
|
|
111
|
+
ok: boolean;
|
|
112
|
+
state: 'complete' | 'interrupted' | 'timeout';
|
|
113
|
+
path: string | null;
|
|
114
|
+
filename: string | null;
|
|
115
|
+
bytes: number;
|
|
116
|
+
mime: string | null;
|
|
117
|
+
url: string | null;
|
|
118
|
+
error?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Turn the in-page fetch outcome into the agent-facing result, writing the
|
|
122
|
+
* file on `complete`. A failed write downgrades to `interrupted` with the
|
|
123
|
+
* error — the agent must never be told a file exists when it does not.
|
|
124
|
+
*/
|
|
125
|
+
export declare function shapeDownloadResult(outcome: FetchDownloadOutcome, sources: FilenameSources & {
|
|
126
|
+
url: string;
|
|
127
|
+
}, save: (filename: string, data: Buffer) => string): DownloadResult;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* `browser.download` support: everything the tool needs that is NOT talking
|
|
5
|
+
* to Chrome — filename policy, the download directory, the file write, the
|
|
6
|
+
* in-page fetch expression and the shaping of the result the agent sees.
|
|
7
|
+
*
|
|
8
|
+
* WHY THE BYTES COME THROUGH THE SERVER. The obvious design — tell Chrome
|
|
9
|
+
* "save downloads to <dir>, never ask" with CDP `Browser.setDownloadBehavior`
|
|
10
|
+
* — is not available to us: Chromium refuses it (and the tab-scoped
|
|
11
|
+
* `Page.setDownloadBehavior`, which delegates to the same check) for any
|
|
12
|
+
* `chrome.debugger` client, because the extension debugger host reports
|
|
13
|
+
* `MayWriteLocalFiles() == false`. And `chrome.downloads.download({saveAs:
|
|
14
|
+
* false})` does NOT suppress the dialog either: Chrome still prompts when the
|
|
15
|
+
* user enabled "Ask where to save each file". So instead the bytes are
|
|
16
|
+
* fetched IN THE PAGE (with the page's own cookies) and written here, into a
|
|
17
|
+
* directory only the human configures.
|
|
18
|
+
*
|
|
19
|
+
* Chrome's download manager never SAVES anything. In url mode it is not
|
|
20
|
+
* involved at all. In selector mode the extension uses it only to catch the
|
|
21
|
+
* download a click starts: it hooks `chrome.downloads.onDeterminingFilename`,
|
|
22
|
+
* which Chrome consults before prompting, cancels and erases that download
|
|
23
|
+
* there, and hands its URL to the same in-page fetch (see the extension's
|
|
24
|
+
* `download-capture.ts`).
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
27
|
+
export const MAX_DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
28
|
+
/** Hard cap on one download's size. The bytes travel base64-encoded inside a
|
|
29
|
+
* CDP `Runtime.evaluate` result and then a bridge frame, so this also bounds
|
|
30
|
+
* the memory both hops can be asked to hold. */
|
|
31
|
+
export const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
|
|
32
|
+
/** Extra bridge timeout on top of each hop's own budget (selector mode has
|
|
33
|
+
* two hops: capture, then fetch), for the round trip only — the capture and
|
|
34
|
+
* the in-page fetch enforce their budgets themselves and always answer, so
|
|
35
|
+
* this is spent only when the bridge itself stalls. */
|
|
36
|
+
export const DOWNLOAD_BRIDGE_SLACK_MS = 10_000;
|
|
37
|
+
const MAX_FILENAME_LENGTH = 200;
|
|
38
|
+
// Characters no filename may carry on any OS we run on (Windows is the
|
|
39
|
+
// strictest) — plus the two path separators.
|
|
40
|
+
// eslint-disable-next-line no-control-regex
|
|
41
|
+
const RESERVED_CHARS = /[<>:"/\\|?*\u0000-\u001f]/;
|
|
42
|
+
// eslint-disable-next-line no-control-regex
|
|
43
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
44
|
+
/**
|
|
45
|
+
* Validate a filename the AGENT asked for. Strict on purpose: the agent may
|
|
46
|
+
* name the file but never choose where it lands, so anything that could be
|
|
47
|
+
* read as a path (separators, `..`, a drive prefix) is rejected outright
|
|
48
|
+
* rather than quietly cleaned up.
|
|
49
|
+
*/
|
|
50
|
+
export function validateRequestedFilename(name) {
|
|
51
|
+
const fail = (why) => {
|
|
52
|
+
throw new Error(`browser.download: filename ${why} — pass a plain basename like "report.pdf"`);
|
|
53
|
+
};
|
|
54
|
+
if (typeof name !== 'string')
|
|
55
|
+
return fail('must be a string');
|
|
56
|
+
if (name.trim().length === 0)
|
|
57
|
+
return fail('must not be empty');
|
|
58
|
+
if (name.length > MAX_FILENAME_LENGTH)
|
|
59
|
+
return fail(`must be at most ${MAX_FILENAME_LENGTH} characters`);
|
|
60
|
+
if (name === '.' || name === '..')
|
|
61
|
+
return fail('must not be "." or ".."');
|
|
62
|
+
if (RESERVED_CHARS.test(name))
|
|
63
|
+
return fail('must not contain path separators or reserved characters');
|
|
64
|
+
if (/[.\s]$/.test(name))
|
|
65
|
+
return fail('must not end with a dot or a space');
|
|
66
|
+
if (WINDOWS_DEVICE_NAME.test(name)) {
|
|
67
|
+
return fail('must not be a Windows device name (CON, PRN, AUX, NUL, COM1-9, LPT1-9)');
|
|
68
|
+
}
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Windows device names — `NUL`, `con.txt`, `LPT1.log`: with or without an
|
|
73
|
+
* extension, any case. Writing to one on Windows hits the device (or a
|
|
74
|
+
* normalized name), so the server would report a file that does not exist.
|
|
75
|
+
*/
|
|
76
|
+
const WINDOWS_DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
77
|
+
/**
|
|
78
|
+
* Make a filename that came from the PAGE (Chrome's suggestion, a
|
|
79
|
+
* Content-Disposition header, a URL) safe to write: keep the last path
|
|
80
|
+
* segment, replace reserved characters, drop control characters and leading
|
|
81
|
+
* or trailing dots/spaces, and defuse Windows device names with a `_`
|
|
82
|
+
* prefix. Cleaned up rather than rejected: the page, not the agent, chose
|
|
83
|
+
* it. Returns null when nothing usable is left.
|
|
84
|
+
*/
|
|
85
|
+
export function sanitizeDerivedFilename(raw) {
|
|
86
|
+
if (typeof raw !== 'string')
|
|
87
|
+
return null;
|
|
88
|
+
const lastSegment = raw.split(/[/\\]/).pop() ?? '';
|
|
89
|
+
const cleaned = lastSegment
|
|
90
|
+
.replace(CONTROL_CHARS, '')
|
|
91
|
+
.replace(/[<>:"|?*]/g, '_')
|
|
92
|
+
.replace(/^[.\s]+/, '')
|
|
93
|
+
.slice(0, MAX_FILENAME_LENGTH - 1)
|
|
94
|
+
.replace(/[.\s]+$/, '');
|
|
95
|
+
if (cleaned.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
return WINDOWS_DEVICE_NAME.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
98
|
+
}
|
|
99
|
+
/** Extract the filename from a Content-Disposition header, preferring the
|
|
100
|
+
* RFC 5987 `filename*=` form (it carries non-ASCII names correctly). */
|
|
101
|
+
export function filenameFromContentDisposition(header) {
|
|
102
|
+
if (typeof header !== 'string')
|
|
103
|
+
return null;
|
|
104
|
+
const star = /filename\*\s*=\s*([^']*)'[^']*'([^;]+)/i.exec(header);
|
|
105
|
+
if (star) {
|
|
106
|
+
try {
|
|
107
|
+
return decodeURIComponent(star[2].trim().replace(/^"|"$/g, ''));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Malformed percent-encoding — fall through to the plain form.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const plain = /filename\s*=\s*("[^"]*"|[^;]+)/i.exec(header);
|
|
114
|
+
if (!plain)
|
|
115
|
+
return null;
|
|
116
|
+
const value = plain[1].trim().replace(/^"|"$/g, '');
|
|
117
|
+
return value.length > 0 ? value : null;
|
|
118
|
+
}
|
|
119
|
+
/** Last path segment of an http(s) URL, decoded. Null for `blob:`/`data:`
|
|
120
|
+
* URLs (their "path" is an opaque id or the payload itself). */
|
|
121
|
+
export function filenameFromUrl(url) {
|
|
122
|
+
if (typeof url !== 'string')
|
|
123
|
+
return null;
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = new URL(url);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
132
|
+
return null;
|
|
133
|
+
const segment = parsed.pathname.split('/').pop() ?? '';
|
|
134
|
+
if (segment.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
try {
|
|
137
|
+
return decodeURIComponent(segment);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return segment;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** requested → Chrome's suggestion → Content-Disposition → URL → "download". */
|
|
144
|
+
export function pickFilename(sources) {
|
|
145
|
+
if (sources.requested !== undefined)
|
|
146
|
+
return sources.requested;
|
|
147
|
+
return (sanitizeDerivedFilename(sources.suggested) ??
|
|
148
|
+
sanitizeDerivedFilename(filenameFromContentDisposition(sources.contentDisposition)) ??
|
|
149
|
+
sanitizeDerivedFilename(filenameFromUrl(sources.url)) ??
|
|
150
|
+
'download');
|
|
151
|
+
}
|
|
152
|
+
export function clampDownloadTimeoutMs(value) {
|
|
153
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
154
|
+
return DEFAULT_DOWNLOAD_TIMEOUT_MS;
|
|
155
|
+
}
|
|
156
|
+
return Math.min(Math.floor(value), MAX_DOWNLOAD_TIMEOUT_MS);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Where downloads land: the human-configured `downloads.dir` when it is an
|
|
160
|
+
* absolute path, otherwise `<dataDir>/downloads`. config.json is untrusted
|
|
161
|
+
* (hand-edited, corrupted), so a relative or non-string value falls back to
|
|
162
|
+
* the default instead of resolving against whatever the cwd happens to be.
|
|
163
|
+
*/
|
|
164
|
+
export function resolveDownloadsDir(cfg, dataDir) {
|
|
165
|
+
const configured = cfg.downloadsDir;
|
|
166
|
+
if (typeof configured === 'string' && configured.length > 0 && isAbsolute(configured)) {
|
|
167
|
+
return configured;
|
|
168
|
+
}
|
|
169
|
+
return join(dataDir, 'downloads');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Write `data` as `filename` inside `dir`, creating `dir` if needed. Never
|
|
173
|
+
* overwrites: an existing name becomes `name (1).ext`, `name (2).ext`, …
|
|
174
|
+
* (the `wx` flag makes the existence check and the create one atomic step).
|
|
175
|
+
* Returns the absolute path written.
|
|
176
|
+
*/
|
|
177
|
+
export function writeDownloadFile(dir, filename, data) {
|
|
178
|
+
mkdirSync(dir, { recursive: true });
|
|
179
|
+
const ext = extname(filename);
|
|
180
|
+
const stem = ext.length > 0 ? filename.slice(0, -ext.length) : filename;
|
|
181
|
+
for (let attempt = 0; attempt < 1000; attempt++) {
|
|
182
|
+
const name = attempt === 0 ? filename : `${stem} (${attempt})${ext}`;
|
|
183
|
+
const path = join(dir, name);
|
|
184
|
+
try {
|
|
185
|
+
writeFileSync(path, data, { flag: 'wx' });
|
|
186
|
+
return path;
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if (err.code !== 'EEXIST')
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`could not find a free filename for ${filename} in ${dir}`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build the expression `Runtime.evaluate` runs in the page: fetch `url` with
|
|
197
|
+
* the page's credentials, stream the body (so `maxBytes` is enforced while
|
|
198
|
+
* reading, not after buffering everything), and base64 it. Every exit path
|
|
199
|
+
* resolves to a `FetchDownloadOutcome` — a timeout or an error reports how
|
|
200
|
+
* far it got rather than throwing, so the agent always gets partial info.
|
|
201
|
+
*/
|
|
202
|
+
export function buildFetchDownloadJs(opts) {
|
|
203
|
+
const url = JSON.stringify(opts.url);
|
|
204
|
+
const timeoutMs = Math.floor(opts.timeoutMs);
|
|
205
|
+
const maxBytes = Math.floor(opts.maxBytes);
|
|
206
|
+
return `(async () => {
|
|
207
|
+
const url = ${url};
|
|
208
|
+
const timeoutMs = ${timeoutMs};
|
|
209
|
+
const maxBytes = ${maxBytes};
|
|
210
|
+
const ctrl = new AbortController();
|
|
211
|
+
let timedOut = false;
|
|
212
|
+
const timer = setTimeout(() => { timedOut = true; ctrl.abort(); }, timeoutMs);
|
|
213
|
+
const out = { state: 'interrupted', bytes: 0, mime: null, content_disposition: null, final_url: null };
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(url, { credentials: 'include', signal: ctrl.signal });
|
|
216
|
+
out.status = res.status;
|
|
217
|
+
out.mime = res.headers.get('content-type');
|
|
218
|
+
out.content_disposition = res.headers.get('content-disposition');
|
|
219
|
+
out.final_url = res.url || url;
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
out.error = 'HTTP ' + res.status;
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
const chunks = [];
|
|
225
|
+
if (res.body) {
|
|
226
|
+
const reader = res.body.getReader();
|
|
227
|
+
for (;;) {
|
|
228
|
+
const step = await reader.read();
|
|
229
|
+
if (step.done) break;
|
|
230
|
+
out.bytes += step.value.byteLength;
|
|
231
|
+
if (out.bytes > maxBytes) {
|
|
232
|
+
ctrl.abort();
|
|
233
|
+
out.error = 'download exceeds the ' + maxBytes + ' bytes cap';
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
chunks.push(step.value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
let binary = '';
|
|
240
|
+
for (const chunk of chunks) {
|
|
241
|
+
for (let i = 0; i < chunk.length; i += 0x8000) {
|
|
242
|
+
binary += String.fromCharCode.apply(null, chunk.subarray(i, i + 0x8000));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
out.data_b64 = btoa(binary);
|
|
246
|
+
out.state = 'complete';
|
|
247
|
+
return out;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (timedOut) {
|
|
250
|
+
out.state = 'timeout';
|
|
251
|
+
out.error = 'timed out after ' + timeoutMs + 'ms';
|
|
252
|
+
} else {
|
|
253
|
+
out.error = err && err.message ? err.message : String(err);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
} finally {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
})()`;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Turn the in-page fetch outcome into the agent-facing result, writing the
|
|
263
|
+
* file on `complete`. A failed write downgrades to `interrupted` with the
|
|
264
|
+
* error — the agent must never be told a file exists when it does not.
|
|
265
|
+
*/
|
|
266
|
+
export function shapeDownloadResult(outcome, sources, save) {
|
|
267
|
+
const filename = pickFilename({
|
|
268
|
+
...sources,
|
|
269
|
+
contentDisposition: outcome.content_disposition,
|
|
270
|
+
url: outcome.final_url ?? sources.url,
|
|
271
|
+
});
|
|
272
|
+
const base = {
|
|
273
|
+
filename,
|
|
274
|
+
mime: outcome.mime ?? null,
|
|
275
|
+
url: outcome.final_url ?? sources.url,
|
|
276
|
+
};
|
|
277
|
+
if (outcome.state === 'complete' && typeof outcome.data_b64 === 'string') {
|
|
278
|
+
const data = Buffer.from(outcome.data_b64, 'base64');
|
|
279
|
+
try {
|
|
280
|
+
const path = save(filename, data);
|
|
281
|
+
return { ok: true, state: 'complete', path, bytes: data.length, ...base };
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
state: 'interrupted',
|
|
287
|
+
path: null,
|
|
288
|
+
bytes: data.length,
|
|
289
|
+
...base,
|
|
290
|
+
error: `could not write the file: ${err instanceof Error ? err.message : String(err)}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
state: outcome.state === 'timeout' ? 'timeout' : 'interrupted',
|
|
297
|
+
path: null,
|
|
298
|
+
bytes: outcome.bytes,
|
|
299
|
+
...base,
|
|
300
|
+
error: outcome.error ?? 'download did not complete',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=downloads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"downloads.js","sourceRoot":"","sources":["../src/downloads.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildSnapshotJs, buildFindJs, buildClickResolveJs, buildTypeResolveJs, buildFocusJs, buildStateJs, buildDragProbeJs, } from './inpage/builders.js';
|
|
2
2
|
import { buildKeyEventSequence, resolveKey, modifiersToBitmask, MODIFIER_BITS, } from './keymap.js';
|
|
3
3
|
import { resolveSettleParams, settleSafely } from './settle.js';
|
|
4
|
+
import { captureAround, createDownloadCapture } from './download-capture.js';
|
|
4
5
|
import { runFlow, } from './flow.js';
|
|
5
6
|
import { MAX_DETACHED_FLOW_MS, appendFlowHistory, createFlowRegistry, detachedFlowKey, detachedFlowRecordsToEvict, finishedDetachedFlowRecord, flowHistoryKey, flowIdFromDetachedFlowKey, minimalDetachedFlowRecord, parseDetachedFlowRecord, parseFlowHistory, startedDetachedFlowRecord, terminatedDetachedFlowRecord, terminatedFlowHistoryEntry, toFlowStatus, toHistoryEntry, } from './flow-registry.js';
|
|
6
7
|
import { DEFAULT_IDLE_TTL_MINUTES, IDLE_TTL_STORAGE_KEY, IDLE_TTL_UPDATED_AT_STORAGE_KEY, clampIdleTtlMinutes, parseIncomingSettings, shouldAcceptIncomingSettings, shouldDisconnectForIdle, shouldScheduleIdleSweep, } from './idle-policy.js';
|
|
@@ -1895,6 +1896,52 @@ async function handleDialogRespond(tabId, msg, p, optStr) {
|
|
|
1895
1896
|
result: { accepted: accept },
|
|
1896
1897
|
};
|
|
1897
1898
|
}
|
|
1899
|
+
/** One worker-wide capture: `chrome.downloads` events are not tied to a tab,
|
|
1900
|
+
* so only one `browser.download` may wait for a download at a time. */
|
|
1901
|
+
const downloadCapture = createDownloadCapture(chrome.downloads);
|
|
1902
|
+
/** The tab's origin for `mayComeFrom`, or undefined when it has none worth
|
|
1903
|
+
* matching (opaque `null` origins, unparseable URLs). */
|
|
1904
|
+
function downloadOriginOf(url) {
|
|
1905
|
+
try {
|
|
1906
|
+
const origin = new URL(url).origin;
|
|
1907
|
+
return origin === 'null' ? undefined : origin;
|
|
1908
|
+
}
|
|
1909
|
+
catch {
|
|
1910
|
+
return undefined;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* `browser.download`'s selector mode: arm the capture, click, and wait for
|
|
1915
|
+
* the download the click starts. The capture cancels that download before
|
|
1916
|
+
* Chrome can show "Save as" (see `download-capture.ts`) and hands its URL
|
|
1917
|
+
* back; the server then fetches the bytes in the page and writes the file.
|
|
1918
|
+
* A click that starts nothing within `timeout_ms` answers `state: "timeout"`.
|
|
1919
|
+
*/
|
|
1920
|
+
async function handleDownloadCapture(tabId, msg, p) {
|
|
1921
|
+
const selector = typeof p.selector === 'string' ? p.selector : '';
|
|
1922
|
+
// No local default: the server always sends a clamped budget, and a second
|
|
1923
|
+
// copy of its default here would only drift.
|
|
1924
|
+
if (typeof p.timeout_ms !== 'number' || !(p.timeout_ms > 0)) {
|
|
1925
|
+
return { kind: 'tool.response', id: msg.id, ok: false, error: 'download: timeout_ms required' };
|
|
1926
|
+
}
|
|
1927
|
+
const timeoutMs = p.timeout_ms;
|
|
1928
|
+
// Narrow the capture to downloads that may come from this tab's origin —
|
|
1929
|
+
// `chrome.downloads` items carry no tab id (see `mayComeFrom`).
|
|
1930
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
1931
|
+
const origin = tab?.url ? downloadOriginOf(tab.url) : undefined;
|
|
1932
|
+
const outcome = await captureAround(downloadCapture, { timeoutMs, ...(origin ? { origin } : {}) },
|
|
1933
|
+
// settle_ms: 0 — the thing to wait for is the download, not DOM quiet.
|
|
1934
|
+
() => performClick(tabId, { selector, settle_ms: 0 }));
|
|
1935
|
+
if (!outcome.ok) {
|
|
1936
|
+
return { kind: 'tool.response', id: msg.id, ok: false, error: outcome.error };
|
|
1937
|
+
}
|
|
1938
|
+
return {
|
|
1939
|
+
kind: 'tool.response',
|
|
1940
|
+
id: msg.id,
|
|
1941
|
+
ok: true,
|
|
1942
|
+
result: outcome.captured ? { state: 'captured', ...outcome.captured } : { state: 'timeout' },
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1898
1945
|
async function handleSetPermission(msg, str) {
|
|
1899
1946
|
const origin = str('origin');
|
|
1900
1947
|
const name = str('name');
|
|
@@ -2059,6 +2106,8 @@ async function handleTool(state, msg) {
|
|
|
2059
2106
|
return await handleDialogRespond(tabId, msg, p, optStr);
|
|
2060
2107
|
case 'set_permission':
|
|
2061
2108
|
return await handleSetPermission(msg, str);
|
|
2109
|
+
case 'download':
|
|
2110
|
+
return await handleDownloadCapture(tabId, msg, p);
|
|
2062
2111
|
default:
|
|
2063
2112
|
return { kind: 'tool.response', id: msg.id, ok: false, error: `Unknown tool: ${msg.tool}` };
|
|
2064
2113
|
}
|