mecord-connect 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +47 -0
- package/LICENSE +64 -0
- package/README.md +44 -0
- package/bin/operator.mjs +10 -0
- package/package.json +58 -0
- package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
- package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
- package/runtime/app/apps/local-agent/src/cli.js +237 -0
- package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
- package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
- package/runtime/app/apps/local-agent/src/main.js +311 -0
- package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
- package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
- package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
- package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
- package/runtime/app/apps/local-agent/src/remote.js +130 -0
- package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
- package/runtime/app/apps/local-agent/src/server.js +578 -0
- package/runtime/app/package.json +4 -0
- package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
- package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
- package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
- package/runtime/app/src/capabilities/browser-cdp.js +384 -0
- package/runtime/app/src/capabilities/browser-managed.js +345 -0
- package/runtime/app/src/capabilities/docker.js +448 -0
- package/runtime/app/src/capabilities/filesystem.js +264 -0
- package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
- package/runtime/app/src/capabilities/git-write.js +321 -0
- package/runtime/app/src/capabilities/git.js +252 -0
- package/runtime/app/src/capabilities/index.js +16 -0
- package/runtime/app/src/capabilities/path-scope.js +86 -0
- package/runtime/app/src/capabilities/postgres.js +566 -0
- package/runtime/app/src/capabilities/process.js +258 -0
- package/runtime/app/src/capabilities/project-command.js +599 -0
- package/runtime/app/src/capabilities/project-transaction.js +215 -0
- package/runtime/app/src/capabilities/project.js +67 -0
- package/runtime/app/src/capabilities/system.js +40 -0
- package/runtime/app/src/capabilities/vscode.js +312 -0
- package/runtime/app/src/capabilities/windows-uia.js +331 -0
- package/runtime/app/src/core/account-device-registry.js +604 -0
- package/runtime/app/src/core/action-identity.js +44 -0
- package/runtime/app/src/core/audit.js +295 -0
- package/runtime/app/src/core/capability-policy.js +58 -0
- package/runtime/app/src/core/child-environment.js +29 -0
- package/runtime/app/src/core/device-enrollment.js +417 -0
- package/runtime/app/src/core/device-identity.js +436 -0
- package/runtime/app/src/core/device-registry.js +477 -0
- package/runtime/app/src/core/device-reset.js +178 -0
- package/runtime/app/src/core/device-routing.js +305 -0
- package/runtime/app/src/core/durable-state.js +306 -0
- package/runtime/app/src/core/errors.js +20 -0
- package/runtime/app/src/core/evidence.js +9 -0
- package/runtime/app/src/core/index.js +17 -0
- package/runtime/app/src/core/machine-state.js +160 -0
- package/runtime/app/src/core/network-authority.js +20 -0
- package/runtime/app/src/core/policy.js +67 -0
- package/runtime/app/src/core/product-identity.js +7 -0
- package/runtime/app/src/core/provenance.js +18 -0
- package/runtime/app/src/core/provider-learning.js +274 -0
- package/runtime/app/src/core/public-plugin-surface.js +25 -0
- package/runtime/app/src/core/public-restricted-data.js +105 -0
- package/runtime/app/src/core/rate-limit.js +118 -0
- package/runtime/app/src/core/relay-client.js +568 -0
- package/runtime/app/src/core/relay-delivery-store.js +512 -0
- package/runtime/app/src/core/relay-result-store.js +321 -0
- package/runtime/app/src/core/release-update.js +253 -0
- package/runtime/app/src/core/router.js +96 -0
- package/runtime/app/src/core/runtime.js +136 -0
- package/runtime/app/src/core/scoped-path-syntax.js +16 -0
- package/runtime/app/src/core/session-token.js +498 -0
- package/runtime/app/src/core/task-orchestrator.js +1148 -0
- package/runtime/app/src/core/task-store.js +517 -0
- package/runtime/app/src/core/task.js +142 -0
- package/runtime/app/src/core/trusted-executable.js +124 -0
- package/runtime/app/src/core/types.js +83 -0
- package/runtime/app/src/core/windows-path-lease.js +138 -0
- package/runtime/native/operator-windows-dpapi.exe +0 -0
- package/runtime/native/operator-windows-path-lease.exe +0 -0
- package/runtime/native/operator-windows-uia.exe +0 -0
- package/runtime/runtime-manifest.json +380 -0
- package/src/cli.mjs +345 -0
- package/src/publish-check.mjs +22 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
|
|
2
|
+
import { evidence } from '../core/evidence.js';
|
|
3
|
+
import { OperatorError } from '../core/errors.js';
|
|
4
|
+
import { CdpConnection, CdpSessionManager, } from './browser-cdp-connection.js';
|
|
5
|
+
import { inspectOopifFrames, performSemanticInteraction } from './browser-cdp-frames.js';
|
|
6
|
+
import {
|
|
7
|
+
assertLoopbackEndpoint,
|
|
8
|
+
collectDiagnostics,
|
|
9
|
+
compactTab,
|
|
10
|
+
failure,
|
|
11
|
+
inspectPage,
|
|
12
|
+
normalizeTargetSpec,
|
|
13
|
+
pageIdentity,
|
|
14
|
+
requirePageTarget,
|
|
15
|
+
requireTarget,
|
|
16
|
+
sameDestination,
|
|
17
|
+
settleAfterInteraction,
|
|
18
|
+
unwrapRuntimeValue,
|
|
19
|
+
validateNavigationUrl,
|
|
20
|
+
waitForReadyState
|
|
21
|
+
} from './browser-cdp-page.js';
|
|
22
|
+
|
|
23
|
+
const SCORE = {
|
|
24
|
+
reliability: 0.96,
|
|
25
|
+
latency: 0.97,
|
|
26
|
+
determinism: 0.97,
|
|
27
|
+
security: 0.93,
|
|
28
|
+
reversibility: 0.9,
|
|
29
|
+
informationQuality: 0.96,
|
|
30
|
+
interactionCost: 0.01
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const MAX_TABS = 100;
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export class BrowserCdpProvider {
|
|
41
|
+
name = 'browser.cdp';
|
|
42
|
+
#endpoint ;
|
|
43
|
+
#sessions = new CdpSessionManager();
|
|
44
|
+
#browserSession ;
|
|
45
|
+
|
|
46
|
+
constructor(endpoint = 'http://127.0.0.1:9222') {
|
|
47
|
+
this.#endpoint = new URL(endpoint);
|
|
48
|
+
assertLoopbackEndpoint(this.#endpoint);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
supports(action ) {
|
|
52
|
+
return ['browser.inspect', 'browser.navigate', 'browser.interact', 'browser.tab.focus', 'browser.tab.close'].includes(action.capability);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
score() { return SCORE; }
|
|
56
|
+
|
|
57
|
+
async execute(action , context = {}) {
|
|
58
|
+
const started = performance.now();
|
|
59
|
+
try {
|
|
60
|
+
throwIfAborted(context.signal);
|
|
61
|
+
if (action.capability === 'browser.inspect') return await this.#inspect(action, started, context.signal);
|
|
62
|
+
if (action.capability === 'browser.navigate') return await this.#navigate(action, started, context.signal);
|
|
63
|
+
if (action.capability === 'browser.interact') return await this.#interact(action, started, context.signal);
|
|
64
|
+
if (action.capability === 'browser.tab.focus') return await this.#focusTab(action, started, context.signal);
|
|
65
|
+
if (action.capability === 'browser.tab.close') return await this.#closeTab(action, started, context.signal);
|
|
66
|
+
throw new OperatorError('UNSUPPORTED_ACTION', action.capability);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return failure(action, this.name, started, context.signal?.aborted ? abortError() : error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close() {
|
|
73
|
+
this.#sessions.closeAll();
|
|
74
|
+
this.#browserSession?.close();
|
|
75
|
+
this.#browserSession = undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async #inspect(action , started , signal ) {
|
|
79
|
+
const tabs = await this.#listTargets(signal);
|
|
80
|
+
const targetId = typeof action.input.targetId === 'string' ? action.input.targetId : undefined;
|
|
81
|
+
if (!targetId) {
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
capability: action.capability,
|
|
85
|
+
provider: this.name,
|
|
86
|
+
output: { endpoint: this.#endpoint.origin, tabs: tabs.map(compactTab) },
|
|
87
|
+
evidence: [evidence('browser_state', 'pass', 'Browser tabs inspected through the Chrome DevTools Protocol endpoint.', { tabCount: tabs.length })],
|
|
88
|
+
durationMs: Math.round(performance.now() - started)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const target = requireTarget(tabs, targetId);
|
|
93
|
+
const session = this.#sessions.get(target);
|
|
94
|
+
throwIfAborted(signal);
|
|
95
|
+
const [mainPage, frames] = await Promise.all([
|
|
96
|
+
inspectPage(session),
|
|
97
|
+
inspectOopifFrames(session)
|
|
98
|
+
]);
|
|
99
|
+
throwIfAborted(signal);
|
|
100
|
+
const page = { ...mainPage, frames };
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
capability: action.capability,
|
|
104
|
+
provider: this.name,
|
|
105
|
+
output: { target: compactTab(target), page },
|
|
106
|
+
evidence: [
|
|
107
|
+
evidence('browser_state', 'pass', 'Semantic page state inspected through CDP across the main document, open shadow roots, same-origin frames, and bounded attached cross-origin frame targets.', { targetId, oopifFrames: frames.length }),
|
|
108
|
+
evidence('data_minimization', 'pass', 'Returned bounded accessibility/DOM summaries instead of raw page HTML.', { accessibilityNodes: page.accessibility.length, oopifFrames: frames.length })
|
|
109
|
+
],
|
|
110
|
+
durationMs: Math.round(performance.now() - started)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async #navigate(action , started , signal ) {
|
|
115
|
+
const url = validateNavigationUrl(String(action.input.url ?? ''));
|
|
116
|
+
const newTab = action.input.newTab === true;
|
|
117
|
+
const requestedTargetId = typeof action.input.targetId === 'string' ? action.input.targetId : undefined;
|
|
118
|
+
|
|
119
|
+
let target ;
|
|
120
|
+
if (newTab) {
|
|
121
|
+
target = await this.#createTarget(url, signal);
|
|
122
|
+
} else {
|
|
123
|
+
const tabs = await this.#listTargets(signal);
|
|
124
|
+
target = requestedTargetId ? requireTarget(tabs, requestedTargetId) : requirePageTarget(tabs);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const session = this.#sessions.get(target);
|
|
128
|
+
const diagnostics = await collectDiagnostics(session);
|
|
129
|
+
try {
|
|
130
|
+
await session.send('Page.enable');
|
|
131
|
+
await session.send('Runtime.enable');
|
|
132
|
+
if (!newTab) {
|
|
133
|
+
const result = await session.send('Page.navigate', { url });
|
|
134
|
+
if (typeof result.errorText === 'string' && result.errorText) {
|
|
135
|
+
throw new OperatorError('BROWSER_NAVIGATION_FAILED', result.errorText, { retryable: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await waitForReadyState(session, 10_000, signal);
|
|
139
|
+
throwIfAborted(signal);
|
|
140
|
+
const state = await pageIdentity(session);
|
|
141
|
+
if (!sameDestination(state.url, url)) {
|
|
142
|
+
throw new OperatorError('BROWSER_POSTCONDITION_FAILED', 'Browser did not reach the requested destination.', { retryable: true, details: { requested: url, actual: state.url } });
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
capability: action.capability,
|
|
147
|
+
provider: this.name,
|
|
148
|
+
output: { targetId: target.id, requestedUrl: url, ...state, diagnostics: diagnostics.snapshot() },
|
|
149
|
+
evidence: [
|
|
150
|
+
evidence('browser_navigation', 'pass', 'Browser navigation completed through CDP.', { targetId: target.id, requestedUrl: url }),
|
|
151
|
+
evidence('postcondition', 'pass', 'Active target URL matches the requested destination.', { actualUrl: state.url, title: state.title })
|
|
152
|
+
],
|
|
153
|
+
durationMs: Math.round(performance.now() - started)
|
|
154
|
+
};
|
|
155
|
+
} finally {
|
|
156
|
+
diagnostics.stop();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async #focusTab(action , started , signal ) {
|
|
161
|
+
const targetId = String(action.input.targetId ?? '');
|
|
162
|
+
if (!targetId) throw new OperatorError('INVALID_BROWSER_TARGET', 'targetId is required.');
|
|
163
|
+
const tabs = await this.#listTargets(signal);
|
|
164
|
+
const target = requireTarget(tabs, targetId);
|
|
165
|
+
const response = await fetch(new URL(`/json/activate/${encodeURIComponent(targetId)}`, this.#endpoint), {
|
|
166
|
+
redirect: 'error',
|
|
167
|
+
signal: combinedSignal(signal, 3_000)
|
|
168
|
+
});
|
|
169
|
+
if (!response.ok) throw new OperatorError('CDP_ACTIVATE_TAB_FAILED', `CDP returned HTTP ${response.status} while focusing a tab.`, { retryable: true });
|
|
170
|
+
const session = this.#sessions.get(target);
|
|
171
|
+
const result = await session.send('Runtime.evaluate', { expression: 'document.visibilityState', returnByValue: true });
|
|
172
|
+
const visibilityState = unwrapRuntimeValue(result);
|
|
173
|
+
if (visibilityState !== 'visible') {
|
|
174
|
+
throw new OperatorError('BROWSER_POSTCONDITION_FAILED', 'Target did not become visible after activation.', { retryable: true, details: { targetId, visibilityState } });
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
capability: action.capability,
|
|
179
|
+
provider: this.name,
|
|
180
|
+
output: { targetId, visibilityState },
|
|
181
|
+
evidence: [
|
|
182
|
+
evidence('browser_tab_focus', 'pass', 'Browser target activation command completed.', { targetId }),
|
|
183
|
+
evidence('postcondition', 'pass', 'Target document reports visible state after activation.', { targetId, visibilityState })
|
|
184
|
+
],
|
|
185
|
+
durationMs: Math.round(performance.now() - started)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async #closeTab(action , started , signal ) {
|
|
190
|
+
const targetId = String(action.input.targetId ?? '');
|
|
191
|
+
if (!targetId) throw new OperatorError('INVALID_BROWSER_TARGET', 'targetId is required.');
|
|
192
|
+
requireTarget(await this.#listTargets(signal), targetId);
|
|
193
|
+
const response = await fetch(new URL(`/json/close/${encodeURIComponent(targetId)}`, this.#endpoint), {
|
|
194
|
+
redirect: 'error',
|
|
195
|
+
signal: combinedSignal(signal, 3_000)
|
|
196
|
+
});
|
|
197
|
+
if (!response.ok) throw new OperatorError('CDP_CLOSE_TAB_FAILED', `CDP returned HTTP ${response.status} while closing a tab.`, { retryable: true });
|
|
198
|
+
this.#sessions.forget(targetId);
|
|
199
|
+
const remaining = await this.#listTargets(signal);
|
|
200
|
+
if (remaining.some((item) => item.id === targetId)) {
|
|
201
|
+
throw new OperatorError('BROWSER_POSTCONDITION_FAILED', 'Target still exists after close command.', { retryable: true, details: { targetId } });
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
capability: action.capability,
|
|
206
|
+
provider: this.name,
|
|
207
|
+
output: { targetId, closed: true },
|
|
208
|
+
evidence: [
|
|
209
|
+
evidence('browser_tab_close', 'pass', 'Browser target close command completed.', { targetId }),
|
|
210
|
+
evidence('postcondition', 'pass', 'Closed target is absent from browser discovery state.', { targetId })
|
|
211
|
+
],
|
|
212
|
+
durationMs: Math.round(performance.now() - started)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async #interact(action , started , signal ) {
|
|
217
|
+
const targetId = String(action.input.targetId ?? '');
|
|
218
|
+
if (!targetId) throw new OperatorError('INVALID_BROWSER_TARGET', 'targetId is required.');
|
|
219
|
+
const operation = String(action.input.operation ?? '');
|
|
220
|
+
if (!['click', 'type', 'select'].includes(operation)) throw new OperatorError('INVALID_BROWSER_OPERATION', 'operation must be click, type, or select.');
|
|
221
|
+
|
|
222
|
+
const targetSpec = normalizeTargetSpec(action.input.target);
|
|
223
|
+
if (!targetSpec.css && !targetSpec.text && !(targetSpec.role && targetSpec.name)) {
|
|
224
|
+
throw new OperatorError('INVALID_BROWSER_TARGET', 'Provide css, text, or role+name for semantic targeting.');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const tabs = await this.#listTargets(signal);
|
|
228
|
+
const target = requireTarget(tabs, targetId);
|
|
229
|
+
const session = this.#sessions.get(target);
|
|
230
|
+
const diagnostics = await collectDiagnostics(session);
|
|
231
|
+
const expectDownload = action.input.expectDownload === true;
|
|
232
|
+
let download ;
|
|
233
|
+
try {
|
|
234
|
+
await session.send('Runtime.enable');
|
|
235
|
+
if (expectDownload) {
|
|
236
|
+
if (operation !== 'click') throw new OperatorError('INVALID_DOWNLOAD_INTERACTION', 'expectDownload is only valid for click operations.');
|
|
237
|
+
download = await this.#prepareDownload(Math.min(Math.max(Number(action.input.downloadTimeoutMs ?? 30_000), 1_000), 10 * 60_000), signal);
|
|
238
|
+
}
|
|
239
|
+
const before = await pageIdentity(session);
|
|
240
|
+
const interaction = await performSemanticInteraction(session, {
|
|
241
|
+
operation,
|
|
242
|
+
target: targetSpec,
|
|
243
|
+
value: action.input.value ?? null
|
|
244
|
+
});
|
|
245
|
+
const value = interaction.value;
|
|
246
|
+
if (value.ok !== true) {
|
|
247
|
+
const message = typeof value.error === 'string' ? String(value.error) : 'Browser interaction did not complete.';
|
|
248
|
+
throw new OperatorError('BROWSER_INTERACTION_FAILED', message, { retryable: false, details: { target: targetSpec, frame: interaction.frame } });
|
|
249
|
+
}
|
|
250
|
+
await settleAfterInteraction(session, signal);
|
|
251
|
+
throwIfAborted(signal);
|
|
252
|
+
const after = await pageIdentity(session);
|
|
253
|
+
const downloadResult = download ? await download.done : undefined;
|
|
254
|
+
if (downloadResult?.state === 'canceled') {
|
|
255
|
+
throw new OperatorError('BROWSER_DOWNLOAD_CANCELED', 'Browser download was canceled.', { retryable: true, details: downloadResult });
|
|
256
|
+
}
|
|
257
|
+
const evidenceItems = [
|
|
258
|
+
evidence('browser_interaction', 'pass', `Semantic browser ${operation} executed through CDP after a unique cross-context locate preflight.`, { targetId, matched: value.matched, frame: interaction.frame }),
|
|
259
|
+
evidence('postcondition', 'pass', 'Element state was re-read after the interaction.', { after: value.after, pageUrl: after.url, frame: interaction.frame })
|
|
260
|
+
];
|
|
261
|
+
if (downloadResult) evidenceItems.push(evidence('download_complete', 'pass', 'Browser emitted a completed download event.', downloadResult));
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
capability: action.capability,
|
|
265
|
+
provider: this.name,
|
|
266
|
+
output: { targetId, operation, matched: value.matched, ...(interaction.frame ? { frame: interaction.frame } : {}), before, after, ...(downloadResult ? { download: downloadResult } : {}), diagnostics: diagnostics.snapshot() },
|
|
267
|
+
evidence: evidenceItems,
|
|
268
|
+
durationMs: Math.round(performance.now() - started)
|
|
269
|
+
};
|
|
270
|
+
} finally {
|
|
271
|
+
diagnostics.stop();
|
|
272
|
+
download?.stop();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async #browserConnection(signal ) {
|
|
277
|
+
if (this.#browserSession && !this.#browserSession.closed) return this.#browserSession;
|
|
278
|
+
const response = await fetch(new URL('/json/version', this.#endpoint), { redirect: 'error', signal: combinedSignal(signal, 3_000) });
|
|
279
|
+
if (!response.ok) throw new OperatorError('CDP_HTTP_ERROR', `CDP returned HTTP ${response.status} while discovering browser endpoint.`, { retryable: true });
|
|
280
|
+
const version = await response.json() ;
|
|
281
|
+
const ws = typeof version.webSocketDebuggerUrl === 'string' ? version.webSocketDebuggerUrl : '';
|
|
282
|
+
if (!ws) throw new OperatorError('CDP_BROWSER_TARGET_UNAVAILABLE', 'Browser does not expose a browser-level DevTools WebSocket endpoint.', { retryable: true });
|
|
283
|
+
this.#browserSession = new CdpConnection('browser', ws);
|
|
284
|
+
return this.#browserSession;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async #prepareDownload(timeoutMs , signal ) {
|
|
288
|
+
const browser = await this.#browserConnection(signal);
|
|
289
|
+
await browser.send('Browser.setDownloadBehavior', { behavior: 'default', eventsEnabled: true });
|
|
290
|
+
let activeGuid ;
|
|
291
|
+
let meta = {};
|
|
292
|
+
let settled = false;
|
|
293
|
+
let resolveDone ;
|
|
294
|
+
let rejectDone ;
|
|
295
|
+
const done = new Promise ((resolve, reject) => { resolveDone = resolve; rejectDone = reject; });
|
|
296
|
+
const onAbort = () => {
|
|
297
|
+
if (settled) return;
|
|
298
|
+
settled = true;
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
rejectDone(abortError());
|
|
301
|
+
};
|
|
302
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
303
|
+
const timer = setTimeout(() => {
|
|
304
|
+
if (!settled) {
|
|
305
|
+
settled = true;
|
|
306
|
+
signal?.removeEventListener('abort', onAbort);
|
|
307
|
+
rejectDone(new OperatorError('BROWSER_DOWNLOAD_TIMEOUT', `No completed download event arrived within ${timeoutMs}ms.`, { retryable: true }));
|
|
308
|
+
}
|
|
309
|
+
}, timeoutMs);
|
|
310
|
+
if (signal?.aborted) onAbort();
|
|
311
|
+
const offBegin = browser.on('Browser.downloadWillBegin', (params) => {
|
|
312
|
+
if (activeGuid) return;
|
|
313
|
+
activeGuid = typeof params.guid === 'string' ? params.guid : undefined;
|
|
314
|
+
meta = {
|
|
315
|
+
url: typeof params.url === 'string' ? params.url.slice(0, 2000) : undefined,
|
|
316
|
+
suggestedFilename: typeof params.suggestedFilename === 'string' ? params.suggestedFilename.slice(0, 500) : undefined
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
const offProgress = browser.on('Browser.downloadProgress', (params) => {
|
|
320
|
+
const guid = typeof params.guid === 'string' ? params.guid : '';
|
|
321
|
+
const state = typeof params.state === 'string' ? params.state : '';
|
|
322
|
+
if (!guid || (activeGuid && guid !== activeGuid) || !['completed', 'canceled'].includes(state)) return;
|
|
323
|
+
activeGuid = activeGuid ?? guid;
|
|
324
|
+
settled = true;
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
signal?.removeEventListener('abort', onAbort);
|
|
327
|
+
resolveDone({
|
|
328
|
+
guid,
|
|
329
|
+
state,
|
|
330
|
+
...meta,
|
|
331
|
+
receivedBytes: typeof params.receivedBytes === 'number' ? params.receivedBytes : undefined,
|
|
332
|
+
totalBytes: typeof params.totalBytes === 'number' ? params.totalBytes : undefined,
|
|
333
|
+
filePath: typeof params.filePath === 'string' ? params.filePath.slice(0, 2000) : undefined
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
return { done, stop: () => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); offBegin(); offProgress(); } };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async #listTargets(signal ) {
|
|
340
|
+
const response = await fetch(new URL('/json/list', this.#endpoint), { redirect: 'error', signal: combinedSignal(signal, 3_000) });
|
|
341
|
+
if (!response.ok) throw new OperatorError('CDP_HTTP_ERROR', `CDP returned HTTP ${response.status}.`, { retryable: true });
|
|
342
|
+
const raw = await response.json() ;
|
|
343
|
+
return raw.slice(0, MAX_TABS).map((tab) => ({
|
|
344
|
+
id: String(tab.id ?? ''),
|
|
345
|
+
type: typeof tab.type === 'string' ? tab.type : undefined,
|
|
346
|
+
title: typeof tab.title === 'string' ? tab.title : undefined,
|
|
347
|
+
url: typeof tab.url === 'string' ? tab.url : undefined,
|
|
348
|
+
webSocketDebuggerUrl: typeof tab.webSocketDebuggerUrl === 'string' ? tab.webSocketDebuggerUrl : undefined
|
|
349
|
+
})).filter((tab) => tab.id.length > 0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async #createTarget(url , signal ) {
|
|
353
|
+
const endpoint = new URL('/json/new', this.#endpoint);
|
|
354
|
+
endpoint.search = url;
|
|
355
|
+
const response = await fetch(endpoint, { method: 'PUT', redirect: 'error', signal: combinedSignal(signal, 4_000) });
|
|
356
|
+
if (!response.ok) throw new OperatorError('CDP_CREATE_TAB_FAILED', `CDP returned HTTP ${response.status} while creating a tab.`, { retryable: true });
|
|
357
|
+
const tab = await response.json() ;
|
|
358
|
+
const target = {
|
|
359
|
+
id: String(tab.id ?? ''),
|
|
360
|
+
type: typeof tab.type === 'string' ? tab.type : undefined,
|
|
361
|
+
title: typeof tab.title === 'string' ? tab.title : undefined,
|
|
362
|
+
url: typeof tab.url === 'string' ? tab.url : undefined,
|
|
363
|
+
webSocketDebuggerUrl: typeof tab.webSocketDebuggerUrl === 'string' ? tab.webSocketDebuggerUrl : undefined
|
|
364
|
+
};
|
|
365
|
+
if (!target.id) throw new OperatorError('CDP_CREATE_TAB_FAILED', 'Browser returned an invalid new-tab target.', { retryable: true });
|
|
366
|
+
return target;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function throwIfAborted(signal ) {
|
|
371
|
+
if (signal?.aborted) throw abortError();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function abortError() {
|
|
375
|
+
return new OperatorError('EXECUTION_ABORTED', 'Browser execution was cancelled.', { retryable: false });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function combinedSignal(signal , timeoutMs ) {
|
|
379
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
380
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Backward-compatible M0 name.
|
|
384
|
+
export class BrowserCdpInspectProvider extends BrowserCdpProvider {}
|