@byokit/openclaw 0.5.0 → 0.6.1

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 (59) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +174 -2
  3. package/dist/auth-store.js +1 -1
  4. package/dist/bridge.d.ts +10 -2
  5. package/dist/bridge.js +61 -14
  6. package/dist/browser/broker.d.ts +63 -0
  7. package/dist/browser/broker.js +990 -0
  8. package/dist/browser/host.d.ts +98 -0
  9. package/dist/browser/host.js +945 -0
  10. package/dist/browser/resume.d.ts +10 -0
  11. package/dist/browser/resume.js +23 -0
  12. package/dist/browser/store.d.ts +25 -0
  13. package/dist/browser/store.js +151 -0
  14. package/dist/browser/verify.d.ts +17 -0
  15. package/dist/browser/verify.js +38 -0
  16. package/dist/browser.d.ts +212 -0
  17. package/dist/browser.js +4 -0
  18. package/dist/config.d.ts +28 -0
  19. package/dist/config.js +130 -0
  20. package/dist/day-usage.d.ts +76 -0
  21. package/dist/day-usage.js +104 -0
  22. package/dist/device.d.ts +32 -2
  23. package/dist/device.js +224 -4
  24. package/dist/engine-patches.d.ts +36 -0
  25. package/dist/engine-patches.js +320 -0
  26. package/dist/engine-status.d.ts +13 -0
  27. package/dist/engine-status.js +31 -0
  28. package/dist/engine.d.ts +13 -1
  29. package/dist/engine.js +99 -52
  30. package/dist/index.d.ts +6 -1
  31. package/dist/index.js +2 -0
  32. package/dist/kit.d.ts +29 -3
  33. package/dist/kit.js +324 -13
  34. package/dist/link.d.ts +5 -2
  35. package/dist/link.js +333 -4
  36. package/dist/routes.d.ts +19 -8
  37. package/dist/routes.js +44 -11
  38. package/dist/routes.json +3775 -356
  39. package/dist/runs.js +8 -2
  40. package/dist/signin.d.ts +2 -1
  41. package/dist/signin.js +103 -7
  42. package/dist/testing/browser.d.ts +50 -0
  43. package/dist/testing/browser.js +166 -0
  44. package/dist/testing/index.d.ts +1 -0
  45. package/dist/testing/index.js +1 -0
  46. package/dist/testing/model-stub.d.ts +1 -0
  47. package/dist/testing/model-stub.js +3 -1
  48. package/dist/types.d.ts +50 -3
  49. package/dist/usage-boots.d.ts +8 -0
  50. package/dist/usage-boots.js +26 -0
  51. package/dist/usage.d.ts +51 -0
  52. package/dist/usage.js +61 -0
  53. package/dist/words.json +42 -0
  54. package/engine/OPENCLAW-LICENSE +24 -0
  55. package/engine/patches.json +59 -0
  56. package/engine/patches.schema.json +41 -0
  57. package/package.json +12 -4
  58. package/plugin/index.js +78 -5
  59. package/plugin/usage.js +125 -0
@@ -0,0 +1,990 @@
1
+ // Internal W1/W2 seam. The pipe is the only Chromium debugging transport; agents never own it.
2
+ import { spawn } from 'node:child_process';
3
+ import { once } from 'node:events';
4
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
5
+ import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { createServer } from 'node:http';
8
+ import { isAbsolute, join } from 'node:path';
9
+ import { StringDecoder } from 'node:string_decoder';
10
+ import { WebSocket, WebSocketServer } from 'ws';
11
+ const heldError = { code: -32000, message: 'byokit: browser fenced' };
12
+ const refusedError = { code: -32001, message: 'byokit: command refused' };
13
+ const boundedMs = 5000;
14
+ const structural = new Set(['Target.attachedToTarget', 'Target.detachedFromTarget', 'Target.targetCreated',
15
+ 'Target.targetDestroyed', 'Target.targetInfoChanged', 'Target.targetCrashed', 'Page.frameAttached',
16
+ 'Page.frameDetached', 'Page.frameNavigated', 'Page.navigatedWithinDocument', 'Page.frameStartedLoading',
17
+ 'Page.frameStoppedLoading', 'Page.lifecycleEvent', 'Page.domContentEventFired', 'Page.loadEventFired',
18
+ 'Runtime.executionContextCreated', 'Runtime.executionContextDestroyed', 'Runtime.executionContextsCleared',
19
+ 'Inspector.detached', 'Inspector.targetCrashed']);
20
+ const equal = (a, b) => {
21
+ const aa = Buffer.from(a), bb = Buffer.from(b);
22
+ return aa.length === bb.length && timingSafeEqual(aa, bb);
23
+ };
24
+ const failure = () => new Error('byokit: browser command failed');
25
+ /** Chromium refused to start without its sandbox (e.g. unprivileged user namespaces disabled). The broker never runs it unsandboxed. */
26
+ export class BrowserSandboxUnavailable extends Error {
27
+ reason = 'sandbox-unavailable';
28
+ constructor() { super('byokit: browser sandbox unavailable'); }
29
+ }
30
+ const origin = (url) => { try {
31
+ return new URL(url).origin;
32
+ }
33
+ catch {
34
+ return 'null';
35
+ } };
36
+ const exactOrigin = (value) => origin(value) !== 'null' && origin(value) === value;
37
+ const secureOrigin = (value) => {
38
+ try {
39
+ const u = new URL(value);
40
+ return u.protocol === 'https:' || ['127.0.0.1', '[::1]', 'localhost'].includes(u.hostname);
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ };
46
+ // Only the newest frame/state is useful to a slow viewer; bounded memory, never replayed to an agent.
47
+ function latest() {
48
+ let value, ended = false, wake;
49
+ return {
50
+ push(item) { if (!ended) {
51
+ value = item;
52
+ wake?.();
53
+ } },
54
+ end() { ended = true; value = undefined; wake?.(); },
55
+ async *[Symbol.asyncIterator]() {
56
+ while (!ended) {
57
+ if (value === undefined)
58
+ await new Promise(resolve => { wake = resolve; });
59
+ if (ended)
60
+ break;
61
+ if (value !== undefined) {
62
+ const item = value;
63
+ value = undefined;
64
+ yield item;
65
+ }
66
+ }
67
+ },
68
+ };
69
+ }
70
+ // JPEG segment lengths include their own two-byte length field. Read the encoded SOF,
71
+ // not CDP's unscaled viewport metadata (thumbnails deliberately differ from that viewport).
72
+ function jpegSize(bytes) {
73
+ if (bytes[0] !== 0xff || bytes[1] !== 0xd8)
74
+ return;
75
+ let at = 2;
76
+ while (at + 3 < bytes.length) {
77
+ if (bytes[at++] !== 0xff)
78
+ return;
79
+ while (bytes[at] === 0xff)
80
+ at++;
81
+ const marker = bytes[at++];
82
+ if (marker === 0xda || marker === 0xd9)
83
+ return;
84
+ if (marker === 0x01 || marker >= 0xd0 && marker <= 0xd7)
85
+ continue;
86
+ const length = (bytes[at] << 8) | bytes[at + 1];
87
+ if (length < 2 || at + length > bytes.length)
88
+ return;
89
+ if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) {
90
+ if (length < 8)
91
+ return;
92
+ const h = (bytes[at + 3] << 8) | bytes[at + 4], w = (bytes[at + 5] << 8) | bytes[at + 6];
93
+ return w > 0 && h > 0 ? { w, h } : undefined;
94
+ }
95
+ at += length;
96
+ }
97
+ return;
98
+ }
99
+ const controlMethods = new Set(['Target.attachToTarget', 'Target.detachFromTarget', 'Page.enable',
100
+ 'Page.startScreencast', 'Page.stopScreencast', 'Page.screencastFrameAck', 'Page.getNavigationHistory',
101
+ 'Page.navigateToHistoryEntry', 'Page.reload', 'Input.dispatchMouseEvent', 'Input.dispatchKeyEvent', 'Input.insertText']);
102
+ function validControl(method, p) {
103
+ // A method allowlist alone is insufficient: Page.reload can carry a scriptToEvaluateOnLoad.
104
+ if (method === 'Page.reload')
105
+ return Object.keys(p).every(k => k === 'ignoreCache') && (p.ignoreCache === undefined || typeof p.ignoreCache === 'boolean');
106
+ if (['Page.enable', 'Page.stopScreencast', 'Page.getNavigationHistory'].includes(method))
107
+ return Object.keys(p).length === 0;
108
+ if (method === 'Page.navigateToHistoryEntry')
109
+ return Object.keys(p).every(k => k === 'entryId') && Number.isSafeInteger(p.entryId);
110
+ if (method === 'Page.screencastFrameAck')
111
+ return Object.keys(p).every(k => k === 'sessionId') && Number.isSafeInteger(p.sessionId) && p.sessionId >= 0;
112
+ if (method === 'Page.startScreencast')
113
+ return Object.keys(p).every(k => ['format', 'quality', 'maxWidth', 'maxHeight', 'everyNthFrame'].includes(k))
114
+ && (p.format === undefined || p.format === 'jpeg')
115
+ && (p.quality === undefined || Number.isInteger(p.quality) && p.quality >= 0 && p.quality <= 100)
116
+ && ['maxWidth', 'maxHeight'].every(k => p[k] === undefined || Number.isInteger(p[k]) && p[k] >= 1 && p[k] <= 2048)
117
+ && (p.everyNthFrame === undefined || Number.isInteger(p.everyNthFrame) && p.everyNthFrame >= 1 && p.everyNthFrame <= 100);
118
+ if (method === 'Target.attachToTarget')
119
+ return Object.keys(p).every(k => ['targetId', 'flatten'].includes(k)) && typeof p.targetId === 'string';
120
+ if (method === 'Target.detachFromTarget')
121
+ return Object.keys(p).every(k => k === 'sessionId') && typeof p.sessionId === 'string';
122
+ if (method === 'Input.insertText')
123
+ return typeof p.text === 'string' && p.text.length <= 65536 && Object.keys(p).every(k => k === 'text');
124
+ if (method === 'Input.dispatchMouseEvent')
125
+ return ['mouseMoved', 'mousePressed', 'mouseReleased', 'mouseWheel'].includes(p.type)
126
+ && Number.isFinite(p.x) && Number.isFinite(p.y) && Math.abs(p.x) <= 100000 && Math.abs(p.y) <= 100000
127
+ && (p.button === undefined || ['left', 'right', 'middle', 'none'].includes(p.button))
128
+ && (p.deltaX === undefined || Number.isFinite(p.deltaX)) && (p.deltaY === undefined || Number.isFinite(p.deltaY))
129
+ && Object.keys(p).every(k => ['type', 'x', 'y', 'button', 'clickCount', 'deltaX', 'deltaY'].includes(k));
130
+ if (method === 'Input.dispatchKeyEvent')
131
+ return ['keyDown', 'keyUp'].includes(p.type)
132
+ && typeof p.key === 'string' && p.key.length <= 256 && (p.code === undefined || typeof p.code === 'string' && p.code.length <= 256)
133
+ && (p.modifiers === undefined || Number.isInteger(p.modifiers) && p.modifiers >= 0 && p.modifiers <= 15)
134
+ && Object.keys(p).every(k => ['type', 'key', 'code', 'modifiers'].includes(k));
135
+ return true;
136
+ }
137
+ export async function launchBroker(o) {
138
+ if (!isAbsolute(o.executablePath) || o.executablePath.includes('\0'))
139
+ throw failure();
140
+ await mkdir(join(o.profileDir, 'Default'), { recursive: true, mode: 0o700 });
141
+ await chmod(o.profileDir, 0o700);
142
+ await writeFile(join(o.profileDir, 'Default', 'Preferences'), JSON.stringify({
143
+ credentials_enable_service: false, profile: { password_manager_enabled: false },
144
+ autofill: { profile_enabled: false, credit_card_enabled: false },
145
+ }), { mode: 0o600 });
146
+ // Chromium's singleton socket lives under TMPDIR: an arbitrary app profile path can exceed AF_UNIX's limit.
147
+ const privateTemp = await mkdtemp(join(tmpdir(), 'bk-'));
148
+ const chrome = spawn(o.executablePath, ['--headless=new', `--user-data-dir=${o.profileDir}`,
149
+ '--remote-debugging-pipe', '--disable-features=BackForwardCache', '--disable-background-networking',
150
+ '--disable-component-update', '--disable-sync', '--no-first-run', '--no-default-browser-check', 'about:blank'], {
151
+ // No inherited HOME, login, proxy, DISPLAY or debugging port. Chromium's profile is app-owned.
152
+ env: { HOME: o.profileDir, TMPDIR: privateTemp },
153
+ stdio: ['ignore', 'ignore', 'pipe', 'pipe', 'pipe', 'ignore', 'ignore', 'ignore', 'ignore', 'ignore'], // never inherit the caller's fd9 job lock
154
+ });
155
+ // Startup stderr stays in memory only to classify a sandbox refusal; it is never logged, returned or kept.
156
+ let startup = '';
157
+ const stderr = chrome.stderr;
158
+ const collect = (b) => { if (startup.length < 16_384)
159
+ startup += b.toString(); };
160
+ stderr.on('data', collect);
161
+ const writer = chrome.stdio[3], reader = chrome.stdio[4];
162
+ const token = randomBytes(16).toString('hex');
163
+ // Chromium's canonical browser path: the pinned engine strips /devtools/browser/<id> to find /json/* discovery.
164
+ const browserPath = `/devtools/browser/${randomBytes(16).toString('hex')}`;
165
+ const pending = new Map();
166
+ const clients = new Set();
167
+ const owners = new Map();
168
+ const sessionTargets = new Map();
169
+ const targets = new Map();
170
+ const held = new Set(), hidden = new Set(), hostOnly = new Set();
171
+ const privateRoots = new Map();
172
+ const openings = new Set();
173
+ const destroyed = new Map();
174
+ const listeners = new Set();
175
+ let sequence = 0, port = 0, buffer = '', fenced = false, drained = false, poisoned = false, closed = false;
176
+ let lastAgent;
177
+ let fenceJob, closingPrivate;
178
+ const decoder = new StringDecoder('utf8');
179
+ let binding = null, highestEpoch = 0, activePrivate;
180
+ const confirmed = new Set(), privateOrigins = new Map();
181
+ const mainFrames = new Map(), navigationEpoch = new Map();
182
+ const viewers = new Set();
183
+ let controller;
184
+ function matches(l) {
185
+ return !!binding && !!l && l.epoch === binding.epoch && typeof l.nonce === 'string' && equal(binding.nonce, l.nonce);
186
+ }
187
+ function stateOf(id) {
188
+ if (!id || !held.has(id))
189
+ return undefined;
190
+ const current = privateOrigins.get(id) ?? 'null';
191
+ return { origin: current, secure: secureOrigin(current), offOrigin: !binding
192
+ || !(current === binding.origin || binding.knownIdps.includes(current) || confirmed.has(current)) };
193
+ }
194
+ function publishPrivate() {
195
+ const state = stateOf(activePrivate);
196
+ if (state)
197
+ for (const v of viewers)
198
+ if (v.control && !v.closed)
199
+ v.states.push(state);
200
+ }
201
+ async function trackPrivate(id) {
202
+ const { sessionId } = await send('Target.attachToTarget', { targetId: id, flatten: true });
203
+ sessionTargets.set(sessionId, id);
204
+ await send('Page.enable', {}, sessionId);
205
+ const epoch = navigationEpoch.get(id);
206
+ const tree = await send('Page.getFrameTree', {}, sessionId);
207
+ if (tree.frameTree?.frame?.id && held.has(id) && navigationEpoch.get(id) === epoch) {
208
+ mainFrames.set(id, tree.frameTree.frame.id);
209
+ privateOrigins.set(id, origin(tree.frameTree.frame.url));
210
+ }
211
+ return sessionId;
212
+ }
213
+ function send(method, params = {}, sessionId, agent = false) {
214
+ if (closed || poisoned)
215
+ return Promise.reject(failure());
216
+ return new Promise((resolve, reject) => {
217
+ const id = ++sequence;
218
+ const timer = setTimeout(() => {
219
+ pending.delete(id);
220
+ // Unknown commands (including host target creation/close) may still be running. Never release them.
221
+ poisoned = true;
222
+ fenced = true;
223
+ reject(failure());
224
+ }, boundedMs);
225
+ pending.set(id, { agent, resolve, reject, timer });
226
+ writer.write(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }) + '\0', error => {
227
+ if (error)
228
+ abortPipe();
229
+ });
230
+ });
231
+ }
232
+ function abortPipe() {
233
+ poisoned = true;
234
+ fenced = true;
235
+ for (const p of pending.values()) {
236
+ clearTimeout(p.timer);
237
+ p.reject(failure());
238
+ }
239
+ pending.clear();
240
+ for (const c of clients)
241
+ c.ws.terminate();
242
+ for (const v of viewers)
243
+ v.close();
244
+ }
245
+ chrome.once('error', abortPipe);
246
+ chrome.once('exit', code => { abortPipe(); o.onExit(code); });
247
+ reader.once('end', abortPipe);
248
+ reader.once('error', abortPipe);
249
+ writer.once('error', abortPipe);
250
+ reader.on('data', (data) => {
251
+ buffer += decoder.write(data);
252
+ let end;
253
+ while ((end = buffer.indexOf('\0')) !== -1) {
254
+ const raw = buffer.slice(0, end);
255
+ buffer = buffer.slice(end + 1);
256
+ try {
257
+ const message = JSON.parse(raw);
258
+ // Replies resume async forwarding. Give that continuation its turn before the next event,
259
+ // even when Chromium puts both in one pipe read (Playwright installs context listeners on reply).
260
+ if (message.id === undefined)
261
+ queueMicrotask(() => { try {
262
+ route(message);
263
+ }
264
+ catch {
265
+ abortPipe();
266
+ } });
267
+ else
268
+ route(message);
269
+ }
270
+ catch {
271
+ abortPipe();
272
+ }
273
+ }
274
+ });
275
+ function own(c, sid, target) {
276
+ owners.set(sid, c);
277
+ c.sessions.add(sid);
278
+ if (target)
279
+ sessionTargets.set(sid, target);
280
+ }
281
+ function output(c, m) {
282
+ if (c.ws.readyState === WebSocket.OPEN)
283
+ c.ws.send(JSON.stringify(m));
284
+ }
285
+ function route(m) {
286
+ if (m.id !== undefined) {
287
+ const p = pending.get(m.id);
288
+ if (!p)
289
+ return;
290
+ pending.delete(m.id);
291
+ clearTimeout(p.timer);
292
+ if (m.error)
293
+ p.reject(failure());
294
+ else
295
+ p.resolve(m.result ?? {});
296
+ return;
297
+ }
298
+ const info = m.params?.targetInfo;
299
+ if (info) {
300
+ targets.set(info.targetId, info);
301
+ if (info.openerId && hidden.has(info.openerId) && !hidden.has(info.targetId)) {
302
+ held.add(info.targetId);
303
+ hidden.add(info.targetId);
304
+ privateOrigins.set(info.targetId, 'null');
305
+ privateRoots.set(info.targetId, privateRoots.get(info.openerId) ?? info.openerId);
306
+ if (hostOnly.has(info.openerId))
307
+ hostOnly.add(info.targetId);
308
+ else if (info.type === 'page')
309
+ activePrivate = info.targetId;
310
+ // A late child of a closed private target is still private. Never forward it after release.
311
+ if (!fenced)
312
+ abortPipe();
313
+ void trackPrivate(info.targetId).then(publishPrivate, () => { poisoned = true; fenced = true; });
314
+ }
315
+ }
316
+ if (m.method === 'Target.targetDestroyed') {
317
+ const id = m.params?.targetId;
318
+ targets.delete(id);
319
+ held.delete(id);
320
+ privateOrigins.delete(id);
321
+ if (activePrivate === id)
322
+ activePrivate = [...held].find(t => !hostOnly.has(t) && targets.get(t)?.type === 'page');
323
+ publishPrivate();
324
+ for (const notify of destroyed.get(id) ?? [])
325
+ notify();
326
+ destroyed.delete(id);
327
+ }
328
+ const privateTarget = sessionTargets.get(m.sessionId ?? '');
329
+ if (privateTarget && held.has(privateTarget)) {
330
+ if (m.method === 'Page.frameStartedLoading' && m.params?.frameId === (mainFrames.get(privateTarget) ?? privateTarget)) {
331
+ navigationEpoch.set(privateTarget, (navigationEpoch.get(privateTarget) ?? 0) + 1);
332
+ privateOrigins.set(privateTarget, 'null');
333
+ publishPrivate();
334
+ }
335
+ if (m.method === 'Page.frameNavigated' && !m.params?.frame?.parentId) {
336
+ mainFrames.set(privateTarget, m.params.frame.id);
337
+ navigationEpoch.set(privateTarget, (navigationEpoch.get(privateTarget) ?? 0) + 1);
338
+ privateOrigins.set(privateTarget, origin(m.params.frame.url));
339
+ publishPrivate();
340
+ }
341
+ if (m.method === 'Page.frameStoppedLoading' && m.params?.frameId === (mainFrames.get(privateTarget) ?? privateTarget)) {
342
+ // A denied download has no frameNavigated. Read the committed main frame after it stops;
343
+ // never re-enable input from a query overtaken by another navigation.
344
+ const epoch = navigationEpoch.get(privateTarget);
345
+ void send('Page.getFrameTree', {}, m.sessionId).then(tree => {
346
+ if (held.has(privateTarget) && navigationEpoch.get(privateTarget) === epoch) {
347
+ privateOrigins.set(privateTarget, origin(tree.frameTree.frame.url));
348
+ publishPrivate();
349
+ }
350
+ }, () => { });
351
+ }
352
+ }
353
+ for (const listener of listeners)
354
+ listener(m);
355
+ const c = m.sessionId ? owners.get(m.sessionId) : undefined;
356
+ if (!c)
357
+ return;
358
+ const target = info?.targetId ?? m.params?.targetId ?? sessionTargets.get(m.sessionId);
359
+ // No raw nested target channel: inspect the envelope before learning any child session.
360
+ if (!c.control && (hidden.has(target) || (fenced && (!structural.has(m.method ?? '')
361
+ || ![...c.sessions].some(sid => sessionTargets.get(sid) === target)))))
362
+ return;
363
+ if (c.control && (controller !== c || !held.has(target) || hostOnly.has(target) || !['Page.screencastFrame', ...structural].includes(m.method ?? '')))
364
+ return;
365
+ if (m.method === 'Target.attachedToTarget')
366
+ own(c, m.params.sessionId, info?.targetId);
367
+ if (m.method === 'Target.detachedFromTarget') {
368
+ const sid = m.params.sessionId;
369
+ owners.delete(sid);
370
+ c.sessions.delete(sid);
371
+ sessionTargets.delete(sid);
372
+ }
373
+ output(c, { ...m, sessionId: m.sessionId === c.base ? undefined : m.sessionId });
374
+ }
375
+ function detach(c) {
376
+ if (c.detaching)
377
+ return c.detaching;
378
+ clients.delete(c);
379
+ if (controller === c)
380
+ controller = undefined;
381
+ const sessions = [...c.sessions].reverse();
382
+ c.sessions.clear();
383
+ for (const sid of sessions) {
384
+ owners.delete(sid);
385
+ sessionTargets.delete(sid);
386
+ }
387
+ c.detaching = (async () => {
388
+ // Detaching the browser target severs its auto-attach/interception as well as child sessions.
389
+ if (!closed && !poisoned && c.base)
390
+ await send('Target.detachFromTarget', { sessionId: c.base }, undefined, true);
391
+ })();
392
+ return c.detaching;
393
+ }
394
+ function guarded(c, m) {
395
+ if (!Number.isSafeInteger(m.id) || typeof m.method !== 'string')
396
+ return false;
397
+ if (m.sessionId && owners.get(m.sessionId) !== c)
398
+ return false;
399
+ if (m.params?.targetId && held.has(m.params.targetId))
400
+ return false;
401
+ if (['Browser.close', 'Browser.crash'].includes(m.method))
402
+ return false;
403
+ // Legacy nested CDP must not bypass session ownership, target checks or command denial.
404
+ if (m.method === 'Target.sendMessageToTarget') {
405
+ if (owners.get(m.params?.sessionId) !== c || typeof m.params?.message !== 'string')
406
+ return false;
407
+ try {
408
+ return guarded(c, JSON.parse(m.params.message));
409
+ }
410
+ catch {
411
+ return false;
412
+ }
413
+ }
414
+ if (m.method === 'Target.detachFromTarget' && m.params?.sessionId && owners.get(m.params.sessionId) !== c)
415
+ return false;
416
+ return true;
417
+ }
418
+ async function command(c, m) {
419
+ const reply = (value) => output(c, { id: m.id, sessionId: m.sessionId, ...value });
420
+ if (m.method === 'Byokit.claimTakeover') {
421
+ if (c.page || poisoned || !fenced || !drained || !activePrivate || !matches(m.params)
422
+ || controller && controller !== c)
423
+ return reply({ error: refusedError });
424
+ c.control = true;
425
+ controller = c;
426
+ return reply({ result: { targetId: activePrivate } });
427
+ }
428
+ if (poisoned || fenced && !c.control)
429
+ return reply({ error: heldError });
430
+ if (c.control) {
431
+ const target = m.params?.targetId ?? sessionTargets.get(m.sessionId ?? c.base);
432
+ if (!binding || controller !== c || !controlMethods.has(m.method ?? '') || !held.has(target) || hostOnly.has(target)
433
+ || !validControl(m.method, m.params ?? {})
434
+ || (m.method.startsWith('Input.') || ['Page.reload', 'Page.navigateToHistoryEntry'].includes(m.method)) && stateOf(target)?.offOrigin
435
+ || m.sessionId && owners.get(m.sessionId) !== c)
436
+ return reply({ error: refusedError });
437
+ }
438
+ else if (!guarded(c, m))
439
+ return reply({ error: refusedError });
440
+ const target = m.params?.targetId ?? sessionTargets.get(m.sessionId ?? c.base);
441
+ if (target && !held.has(target))
442
+ lastAgent = target;
443
+ try {
444
+ // Flattened child sessions keep ownership explicit even for auto-attached workers/iframes.
445
+ const params = m.method === 'Target.attachToTarget' || m.method === 'Target.setAutoAttach'
446
+ ? { ...m.params, flatten: true }
447
+ : m.method === 'Browser.setDownloadBehavior' ? { ...m.params, behavior: 'deny', eventsEnabled: false }
448
+ : m.method === 'Page.setDownloadBehavior' ? { ...m.params, behavior: 'deny' } : m.params;
449
+ const result = await send(m.method, params, m.sessionId ?? c.base, !c.control);
450
+ if (result.sessionId && m.method.startsWith('Target.attachTo'))
451
+ own(c, result.sessionId, target);
452
+ if (poisoned || fenced && !c.control)
453
+ return reply({ error: heldError });
454
+ if (m.method === 'Target.getTargets')
455
+ result.targetInfos = (result.targetInfos ?? []).filter((t) => !held.has(t.targetId));
456
+ if (m.method === 'Target.getTargetInfo' && held.has(result.targetInfo?.targetId))
457
+ return reply({ error: refusedError });
458
+ reply({ result });
459
+ }
460
+ catch {
461
+ reply({ error: fenced ? heldError : refusedError });
462
+ }
463
+ }
464
+ function authorized(req, url) {
465
+ return req.headers.origin === undefined && req.headers.host === `127.0.0.1:${port}`
466
+ && equal(token, url.searchParams.get('token') ?? '');
467
+ }
468
+ const server = createServer(async (req, res) => {
469
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
470
+ if (!authorized(req, url)) {
471
+ res.writeHead(401);
472
+ res.end();
473
+ return;
474
+ }
475
+ const json = (value) => { res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value)); };
476
+ const description = (t) => ({ id: t.targetId, type: t.type, title: t.title, url: t.url, description: '',
477
+ webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/page/${t.targetId}?token=${token}` });
478
+ const match = url.pathname.match(/^\/json\/(list|new|activate|close)(?:\/([^/]+))?\/?$/);
479
+ if (fenced || poisoned) {
480
+ res.writeHead(409);
481
+ res.end('byokit: browser fenced');
482
+ return;
483
+ }
484
+ try {
485
+ if (/^\/json\/version\/?$/.test(url.pathname)) {
486
+ const v = await send('Browser.getVersion', {}, undefined, true);
487
+ if (fenced)
488
+ throw failure();
489
+ json({ Browser: v.product, 'Protocol-Version': v.protocolVersion, 'User-Agent': v.userAgent,
490
+ webSocketDebuggerUrl: `ws://127.0.0.1:${port}${browserPath}?token=${token}` });
491
+ return;
492
+ }
493
+ if (!match) {
494
+ res.writeHead(404);
495
+ res.end();
496
+ return;
497
+ }
498
+ const action = match[1], id = match[2];
499
+ if (id && held.has(id)) {
500
+ res.writeHead(404);
501
+ res.end();
502
+ return;
503
+ }
504
+ if (action === 'list') {
505
+ const result = await send('Target.getTargets', {}, undefined, true);
506
+ if (fenced)
507
+ throw failure();
508
+ json(result.targetInfos.filter((t) => t.type === 'page' && !held.has(t.targetId)).map(description));
509
+ return;
510
+ }
511
+ if (action === 'new') {
512
+ const raw = url.search.slice(1).split('&').find(part => !part.startsWith('token=')) ?? '';
513
+ const { targetId } = await send('Target.createTarget', { url: decodeURIComponent(raw.replace(/^url=/, '')) || 'about:blank' }, undefined, true);
514
+ const result = await send('Target.getTargetInfo', { targetId }, undefined, true);
515
+ if (fenced)
516
+ throw failure();
517
+ lastAgent = targetId;
518
+ json(description(result.targetInfo));
519
+ return;
520
+ }
521
+ if (!id || !targets.has(id)) {
522
+ res.writeHead(404);
523
+ res.end();
524
+ return;
525
+ }
526
+ await send(action === 'activate' ? 'Target.activateTarget' : 'Target.closeTarget', { targetId: id }, undefined, true);
527
+ if (fenced)
528
+ throw failure();
529
+ res.writeHead(200);
530
+ res.end(action === 'activate' ? 'Target activated' : 'Target is closing');
531
+ }
532
+ catch {
533
+ res.writeHead(fenced ? 409 : 500);
534
+ res.end('byokit: browser command failed');
535
+ }
536
+ });
537
+ const sockets = new WebSocketServer({ noServer: true, maxPayload: 16 * 1024 * 1024 });
538
+ server.on('upgrade', (req, socket, head) => {
539
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
540
+ const page = url.pathname.match(/^\/devtools\/page\/([^/]+)$/)?.[1];
541
+ if (!authorized(req, url) || (!page && url.pathname !== browserPath) || (page && held.has(page))) {
542
+ socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
543
+ return;
544
+ }
545
+ if (poisoned || fenced && page) {
546
+ socket.end('HTTP/1.1 409 Conflict\r\nConnection: close\r\n\r\n');
547
+ return;
548
+ }
549
+ sockets.handleUpgrade(req, socket, head, ws => {
550
+ const c = { ws, base: '', sessions: new Set(), page };
551
+ clients.add(c);
552
+ const ready = send(page ? 'Target.attachToTarget' : 'Target.attachToBrowserTarget', page ? { targetId: page, flatten: true } : {}, undefined, !fenced)
553
+ .then(async (result) => {
554
+ own(c, result.sessionId, page);
555
+ c.base = result.sessionId;
556
+ if (!clients.has(c)) {
557
+ ws.terminate();
558
+ await detach(c);
559
+ }
560
+ }, () => ws.terminate());
561
+ ws.on('message', async (data) => {
562
+ await ready;
563
+ if (!c.base || !clients.has(c) || ws.readyState !== WebSocket.OPEN)
564
+ return;
565
+ try {
566
+ await command(c, JSON.parse(data.toString()));
567
+ }
568
+ catch {
569
+ ws.close(1008, 'invalid command');
570
+ }
571
+ });
572
+ ws.once('close', () => { void ready.then(() => detach(c)).catch(() => { poisoned = true; fenced = true; }); });
573
+ });
574
+ });
575
+ function waitDestroyed(id) {
576
+ if (!held.has(id))
577
+ return Promise.resolve();
578
+ return new Promise((resolve, reject) => {
579
+ const done = () => { clearTimeout(timer); resolve(); };
580
+ const timer = setTimeout(() => { destroyed.get(id)?.delete(done); poisoned = true; reject(failure()); }, boundedMs);
581
+ const list = destroyed.get(id) ?? new Set();
582
+ list.add(done);
583
+ destroyed.set(id, list);
584
+ });
585
+ }
586
+ function closePrivate() {
587
+ if (closingPrivate)
588
+ return closingPrivate;
589
+ if (!fenced)
590
+ return Promise.reject(failure());
591
+ binding = null;
592
+ confirmed.clear();
593
+ controller = undefined;
594
+ for (const v of viewers)
595
+ if (v.control)
596
+ v.close();
597
+ for (const c of clients)
598
+ if (c.control)
599
+ c.ws.terminate();
600
+ closingPrivate = (async () => {
601
+ // A timed-out probe may still be opening its blank target. It cannot cross the release barrier.
602
+ await Promise.all([...openings]);
603
+ while (held.size) {
604
+ await Promise.all([...held].map(async (id) => {
605
+ const gone = waitDestroyed(id);
606
+ // Observe both promises, including failure when closeTarget is refused or its result is unknown.
607
+ await Promise.all([send('Target.closeTarget', { targetId: id }), gone]);
608
+ }));
609
+ }
610
+ })();
611
+ void closingPrivate.then(() => { closingPrivate = undefined; }, () => { closingPrivate = undefined; poisoned = true; fenced = true; });
612
+ return closingPrivate;
613
+ }
614
+ function openPrivate(url, hostOnlyTab = false) {
615
+ if (!fenced || !drained || poisoned || closingPrivate)
616
+ return Promise.reject(failure());
617
+ const work = (async () => {
618
+ // Register about:blank before any site can run, navigate or open a popup.
619
+ const { targetId } = await send('Target.createTarget', { url: 'about:blank' });
620
+ held.add(targetId);
621
+ hidden.add(targetId);
622
+ privateOrigins.set(targetId, 'null');
623
+ privateRoots.set(targetId, targetId);
624
+ if (hostOnlyTab)
625
+ hostOnly.add(targetId);
626
+ else
627
+ activePrivate = targetId;
628
+ const sessionId = await trackPrivate(targetId);
629
+ await send('Page.navigate', { url }, sessionId);
630
+ return targetId;
631
+ })();
632
+ openings.add(work);
633
+ void work.then(() => openings.delete(work), () => { openings.delete(work); poisoned = true; fenced = true; });
634
+ return work;
635
+ }
636
+ async function close() {
637
+ if (closed)
638
+ return;
639
+ fenced = true;
640
+ for (const c of clients)
641
+ c.ws.terminate();
642
+ for (const v of viewers)
643
+ v.close();
644
+ sockets.close();
645
+ server.closeAllConnections();
646
+ await new Promise(resolve => server.close(() => resolve()));
647
+ // A failed spawn has no PID and emits close/error, never exit. Do not await a nonexistent process.
648
+ if (chrome.pid === undefined) {
649
+ closed = true;
650
+ abortPipe();
651
+ await rm(privateTemp, { recursive: true, force: true });
652
+ return;
653
+ }
654
+ // Closing our pipe-only child never acts on another browser or a shared profile.
655
+ if (chrome.exitCode === null && chrome.signalCode === null)
656
+ chrome.kill('SIGTERM');
657
+ await new Promise(resolve => {
658
+ if (chrome.exitCode !== null || chrome.signalCode !== null)
659
+ return resolve();
660
+ const timer = setTimeout(() => { chrome.kill('SIGKILL'); }, boundedMs);
661
+ chrome.once('exit', () => { clearTimeout(timer); resolve(); });
662
+ });
663
+ closed = true;
664
+ abortPipe();
665
+ await rm(privateTemp, { recursive: true, force: true });
666
+ }
667
+ try {
668
+ await send('Browser.getVersion');
669
+ await send('Browser.setDownloadBehavior', { behavior: 'deny', eventsEnabled: false });
670
+ await send('Target.setDiscoverTargets', { discover: true });
671
+ const result = await send('Target.getTargets');
672
+ for (const t of result.targetInfos)
673
+ targets.set(t.targetId, t);
674
+ lastAgent = result.targetInfos.find(t => t.type === 'page')?.targetId;
675
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', () => resolve()); });
676
+ port = server.address().port;
677
+ }
678
+ catch {
679
+ await close();
680
+ if (!stderr.closed)
681
+ await Promise.race([once(stderr, 'close'), new Promise(resolve => setTimeout(resolve, 1000))]);
682
+ const sandbox = startup.includes('No usable sandbox');
683
+ startup = '';
684
+ throw sandbox ? new BrowserSandboxUnavailable() : failure();
685
+ }
686
+ stderr.off('data', collect);
687
+ startup = '';
688
+ stderr.resume(); // keep draining, keep nothing
689
+ return {
690
+ bindLease(l) {
691
+ if (l === null) {
692
+ binding = null;
693
+ confirmed.clear();
694
+ controller = undefined;
695
+ for (const v of viewers)
696
+ if (v.control)
697
+ v.close();
698
+ for (const c of clients)
699
+ if (c.control)
700
+ c.ws.terminate();
701
+ publishPrivate();
702
+ return;
703
+ }
704
+ if (!fenced || !drained || poisoned || closingPrivate || held.size || !Number.isSafeInteger(l.epoch) || l.epoch <= highestEpoch
705
+ || !/^[A-Za-z0-9_-]{22,64}$/.test(l.nonce) || !exactOrigin(l.origin) || !l.knownIdps.every(exactOrigin))
706
+ throw failure();
707
+ binding = { ...l, knownIdps: [...l.knownIdps] };
708
+ highestEpoch = l.epoch;
709
+ confirmed.clear();
710
+ },
711
+ confirmOrigin(l, current) {
712
+ if (!matches(l) || !exactOrigin(current) || stateOf(activePrivate)?.origin !== current)
713
+ return false;
714
+ confirmed.add(current);
715
+ publishPrivate();
716
+ return true;
717
+ },
718
+ privateState: () => stateOf(activePrivate),
719
+ async clearSite(origins) {
720
+ if (fenced || held.size || poisoned || !origins.every(exactOrigin))
721
+ throw failure();
722
+ if (!lastAgent)
723
+ throw failure();
724
+ // Storage.clearDataForOrigin is a page-domain command in the pinned Chromium, not a root command.
725
+ const { sessionId } = await send('Target.attachToTarget', { targetId: lastAgent, flatten: true });
726
+ try {
727
+ for (const current of origins)
728
+ await send('Storage.clearDataForOrigin', { origin: current, storageTypes: 'all' }, sessionId);
729
+ }
730
+ finally {
731
+ await send('Target.detachFromTarget', { sessionId });
732
+ }
733
+ },
734
+ endpoint: () => ({ cdpUrl: `ws://127.0.0.1:${port}${browserPath}?token=${token}` }),
735
+ async fence(on) {
736
+ if (!on) {
737
+ if (poisoned || closed || held.size || openings.size || fenceJob || closingPrivate)
738
+ throw failure();
739
+ if (!fenced)
740
+ return;
741
+ if (!drained)
742
+ throw failure();
743
+ fenced = false;
744
+ drained = false;
745
+ return;
746
+ }
747
+ if (fenceJob)
748
+ return fenceJob;
749
+ if (fenced && drained) {
750
+ if (poisoned)
751
+ throw failure();
752
+ return;
753
+ }
754
+ fenced = true;
755
+ for (const v of viewers)
756
+ if (!v.control)
757
+ v.close();
758
+ let deadlineTimer;
759
+ const barrier = (async () => {
760
+ const until = Date.now() + boundedMs;
761
+ while ([...pending.values()].some(p => p.agent)) {
762
+ if (Date.now() >= until || poisoned) {
763
+ poisoned = true;
764
+ throw failure();
765
+ }
766
+ await new Promise(resolve => setTimeout(resolve, 5));
767
+ }
768
+ await Promise.all([...clients].map(async (c) => { c.ws.terminate(); await detach(c); }));
769
+ if (poisoned)
770
+ throw failure();
771
+ drained = true;
772
+ })();
773
+ fenceJob = Promise.race([barrier, new Promise((_resolve, reject) => {
774
+ deadlineTimer = setTimeout(() => { poisoned = true; fenced = true; reject(failure()); }, boundedMs);
775
+ })]);
776
+ try {
777
+ await fenceJob;
778
+ }
779
+ finally {
780
+ clearTimeout(deadlineTimer);
781
+ fenceJob = undefined;
782
+ }
783
+ },
784
+ agentTab: () => lastAgent,
785
+ async originOf(targetId) {
786
+ const result = await send('Target.getTargetInfo', { targetId });
787
+ try {
788
+ return new URL(result.targetInfo.url).origin;
789
+ }
790
+ catch {
791
+ return 'null';
792
+ }
793
+ },
794
+ openPrivate, closePrivate,
795
+ async probe(url, verify, timeoutMs) {
796
+ let timer, active = true, status = 0;
797
+ let sid;
798
+ let loaded;
799
+ const load = new Promise(resolve => { loaded = resolve; });
800
+ const observe = (m) => {
801
+ if (m.sessionId !== sid)
802
+ return;
803
+ const target = sessionTargets.get(sid ?? '');
804
+ if (m.method === 'Network.responseReceived' && m.params?.type === 'Document'
805
+ && m.params.frameId === mainFrames.get(target ?? ''))
806
+ status = m.params.response.status;
807
+ if (m.method === 'Page.loadEventFired')
808
+ loaded();
809
+ };
810
+ listeners.add(observe);
811
+ // Retain the opening promise even when the verifier deadline wins: cleanup must see its target.
812
+ const opening = openPrivate('about:blank', true);
813
+ const work = (async () => {
814
+ const targetId = await opening;
815
+ if (!active)
816
+ return 'timeout';
817
+ const result = await send('Target.attachToTarget', { targetId, flatten: true });
818
+ sid = result.sessionId;
819
+ sessionTargets.set(sid, targetId);
820
+ await send('Network.enable', {}, sid);
821
+ await send('Page.enable', {}, sid);
822
+ if (!active)
823
+ return 'timeout';
824
+ await send('Page.navigate', { url }, sid);
825
+ await load;
826
+ if (!active)
827
+ return 'timeout';
828
+ const info = await send('Target.getTargetInfo', { targetId });
829
+ const ok = await verify({ url: info.targetInfo.url, status, async exists(selector) {
830
+ if (!active)
831
+ throw failure();
832
+ const doc = await send('DOM.getDocument', {}, sid);
833
+ if (!active)
834
+ throw failure();
835
+ const found = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector }, sid);
836
+ return found.nodeId !== 0;
837
+ } });
838
+ return ok ? 'ok' : 'fail';
839
+ })();
840
+ try {
841
+ return await Promise.race([work, new Promise(resolve => { timer = setTimeout(() => { active = false; loaded(); resolve('timeout'); }, Math.max(1, timeoutMs)); })]);
842
+ }
843
+ catch {
844
+ return 'fail';
845
+ }
846
+ finally {
847
+ active = false;
848
+ loaded();
849
+ clearTimeout(timer);
850
+ listeners.delete(observe);
851
+ const targetId = await opening;
852
+ while ([...held].some(id => privateRoots.get(id) === targetId)) {
853
+ await Promise.all([...held].filter(id => privateRoots.get(id) === targetId).map(id => {
854
+ const gone = waitDestroyed(id);
855
+ return Promise.all([send('Target.closeTarget', { targetId: id }), gone]);
856
+ }));
857
+ }
858
+ }
859
+ },
860
+ attachViewer(o) {
861
+ const requestedWidth = o.maxWidth ?? 1280;
862
+ if (!Number.isFinite(requestedWidth) || requestedWidth < 1)
863
+ throw failure();
864
+ const maxWidth = Math.min(Math.floor(requestedWidth), 1280);
865
+ const control = !!o.lease;
866
+ if (poisoned || closed || control && (!fenced || !drained || !activePrivate || !matches(o.lease) || controller)
867
+ || !control && (fenced || !lastAgent))
868
+ throw failure();
869
+ let seq = 0;
870
+ const v = { control, scaleX: 1, scaleY: 1, hasFrame: false, closed: false, frames: latest(), states: latest(), close() {
871
+ if (v.closed)
872
+ return;
873
+ v.closed = true;
874
+ viewers.delete(v);
875
+ listeners.delete(frame);
876
+ listeners.delete(change);
877
+ v.frames.end();
878
+ v.states.end();
879
+ if (controller === v)
880
+ controller = undefined;
881
+ if (v.sid && !closed && !poisoned)
882
+ void send('Target.detachFromTarget', { sessionId: v.sid }).catch(() => { });
883
+ } };
884
+ if (control)
885
+ controller = v;
886
+ viewers.add(v);
887
+ const allowed = () => !v.closed && !poisoned && (control ? controller === v && !!binding && matches(o.lease) : !fenced);
888
+ const target = () => control ? activePrivate : lastAgent;
889
+ const frame = (m) => {
890
+ if (!allowed() || control && v.target !== activePrivate || m.sessionId !== v.sid || m.method !== 'Page.screencastFrame')
891
+ return;
892
+ // Frames are private memory only, ack even when the consumer is slow.
893
+ void send('Page.screencastFrameAck', { sessionId: m.params.sessionId }, v.sid).catch(() => v.close());
894
+ const jpeg = new Uint8Array(Buffer.from(m.params.data, 'base64'));
895
+ const size = jpegSize(jpeg);
896
+ if (!size || size.w > maxWidth || size.h > 960)
897
+ return v.close();
898
+ v.scaleX = m.params.metadata.deviceWidth / size.w;
899
+ v.scaleY = m.params.metadata.deviceHeight / size.h;
900
+ v.hasFrame = true;
901
+ v.frames.push({ seq: ++seq, ...size, at: Date.now(), jpeg });
902
+ };
903
+ listeners.add(frame);
904
+ const attach = async () => {
905
+ const id = target();
906
+ if (!allowed() || !id)
907
+ return v.close();
908
+ if (v.target === id && v.sid)
909
+ return;
910
+ if (v.sid)
911
+ await send('Target.detachFromTarget', { sessionId: v.sid });
912
+ const { sessionId } = await send('Target.attachToTarget', { targetId: id, flatten: true });
913
+ if (!allowed()) {
914
+ await send('Target.detachFromTarget', { sessionId });
915
+ return;
916
+ }
917
+ v.target = id;
918
+ v.sid = sessionId;
919
+ v.hasFrame = false;
920
+ sessionTargets.set(sessionId, id);
921
+ await send('Page.enable', {}, sessionId);
922
+ await send('Page.startScreencast', { format: 'jpeg', quality: 70, maxWidth, maxHeight: 960 }, sessionId);
923
+ const state = stateOf(id);
924
+ if (control && state)
925
+ v.states.push(state);
926
+ };
927
+ const ready = attach().catch(() => v.close());
928
+ const change = (m) => {
929
+ if (control && ['Target.targetCreated', 'Target.targetDestroyed'].includes(m.method ?? ''))
930
+ void ready.then(attach).catch(() => v.close());
931
+ };
932
+ listeners.add(change);
933
+ const closeViewer = () => { listeners.delete(change); v.close(); };
934
+ return { frames: v.frames, states: v.states, close: closeViewer, input(i) {
935
+ if (!control || !allowed() || !v.hasFrame || v.target !== activePrivate || !v.target || !held.has(v.target) || stateOf(v.target)?.offOrigin || !v.sid)
936
+ return;
937
+ let method, params;
938
+ if (i.kind === 'text') {
939
+ method = 'Input.insertText';
940
+ params = { text: i.text };
941
+ }
942
+ else if (i.kind === 'key') {
943
+ method = 'Input.dispatchKeyEvent';
944
+ params = { type: i.type === 'down' ? 'keyDown' : i.type === 'up' ? 'keyUp' : '', key: i.key, code: i.code, modifiers: i.modifiers };
945
+ }
946
+ else if (i.kind === 'pointer') {
947
+ method = 'Input.dispatchMouseEvent';
948
+ params = { type: { move: 'mouseMoved', down: 'mousePressed', up: 'mouseReleased', wheel: 'mouseWheel' }[i.type],
949
+ x: i.x * v.scaleX, y: i.y * v.scaleY, button: i.button, ...(i.type === 'down' || i.type === 'up' ? { clickCount: 1 } : {}),
950
+ ...(i.type === 'wheel' ? { deltaX: i.dx ?? 0, deltaY: i.dy ?? 0 } : {}) };
951
+ }
952
+ else if (i.kind === 'nav') {
953
+ if (i.action === 'reload') {
954
+ method = 'Page.reload';
955
+ params = {};
956
+ }
957
+ else {
958
+ if (!['back', 'forward'].includes(i.action))
959
+ return;
960
+ void send('Page.getNavigationHistory', {}, v.sid).then(history => {
961
+ if (!allowed() || stateOf(v.target)?.offOrigin)
962
+ return;
963
+ const entry = history.entries[history.currentIndex + (i.action === 'back' ? -1 : 1)];
964
+ if (entry)
965
+ return send('Page.navigateToHistoryEntry', { entryId: entry.id }, v.sid);
966
+ }).catch(() => v.close());
967
+ return;
968
+ }
969
+ }
970
+ else
971
+ return;
972
+ if (!validControl(method, params))
973
+ return;
974
+ void send(method, params, v.sid).catch(() => v.close());
975
+ } };
976
+ },
977
+ async navigateAgent(url) {
978
+ if (fenced || !lastAgent || held.size)
979
+ throw failure();
980
+ const { sessionId } = await send('Target.attachToTarget', { targetId: lastAgent, flatten: true });
981
+ try {
982
+ await send(url === 'reload' ? 'Page.reload' : 'Page.navigate', url === 'reload' ? {} : { url }, sessionId);
983
+ }
984
+ finally {
985
+ await send('Target.detachFromTarget', { sessionId });
986
+ }
987
+ },
988
+ close,
989
+ };
990
+ }