@flrande/browserctl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README-CN.md +1155 -0
  3. package/README.md +1155 -0
  4. package/apps/browserctl/src/commands/a11y-snapshot.ts +20 -0
  5. package/apps/browserctl/src/commands/act.ts +20 -0
  6. package/apps/browserctl/src/commands/common.test.ts +87 -0
  7. package/apps/browserctl/src/commands/common.ts +191 -0
  8. package/apps/browserctl/src/commands/console-list.ts +20 -0
  9. package/apps/browserctl/src/commands/cookie-clear.ts +18 -0
  10. package/apps/browserctl/src/commands/cookie-get.ts +18 -0
  11. package/apps/browserctl/src/commands/cookie-set.ts +22 -0
  12. package/apps/browserctl/src/commands/dialog-arm.ts +20 -0
  13. package/apps/browserctl/src/commands/dom-query-all.ts +18 -0
  14. package/apps/browserctl/src/commands/dom-query.ts +18 -0
  15. package/apps/browserctl/src/commands/download-trigger.ts +22 -0
  16. package/apps/browserctl/src/commands/download-wait.test.ts +67 -0
  17. package/apps/browserctl/src/commands/download-wait.ts +27 -0
  18. package/apps/browserctl/src/commands/element-screenshot.ts +20 -0
  19. package/apps/browserctl/src/commands/frame-list.ts +16 -0
  20. package/apps/browserctl/src/commands/frame-snapshot.ts +18 -0
  21. package/apps/browserctl/src/commands/network-wait-for.ts +100 -0
  22. package/apps/browserctl/src/commands/profile-list.ts +16 -0
  23. package/apps/browserctl/src/commands/profile-use.ts +18 -0
  24. package/apps/browserctl/src/commands/response-body.ts +24 -0
  25. package/apps/browserctl/src/commands/screenshot.ts +16 -0
  26. package/apps/browserctl/src/commands/snapshot.ts +16 -0
  27. package/apps/browserctl/src/commands/status.ts +10 -0
  28. package/apps/browserctl/src/commands/storage-get.ts +20 -0
  29. package/apps/browserctl/src/commands/storage-set.ts +22 -0
  30. package/apps/browserctl/src/commands/tab-close.ts +20 -0
  31. package/apps/browserctl/src/commands/tab-focus.ts +20 -0
  32. package/apps/browserctl/src/commands/tab-open.ts +19 -0
  33. package/apps/browserctl/src/commands/tabs.ts +13 -0
  34. package/apps/browserctl/src/commands/upload-arm.ts +26 -0
  35. package/apps/browserctl/src/daemon-client.test.ts +253 -0
  36. package/apps/browserctl/src/daemon-client.ts +632 -0
  37. package/apps/browserctl/src/e2e.test.ts +99 -0
  38. package/apps/browserctl/src/main.test.ts +215 -0
  39. package/apps/browserctl/src/main.ts +372 -0
  40. package/apps/browserctl/src/smoke.test.ts +16 -0
  41. package/apps/browserctl/src/smoke.ts +5 -0
  42. package/apps/browserd/src/bootstrap.ts +432 -0
  43. package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +275 -0
  44. package/apps/browserd/src/chrome-relay-extension-bridge.ts +506 -0
  45. package/apps/browserd/src/container.ts +1531 -0
  46. package/apps/browserd/src/main.test.ts +864 -0
  47. package/apps/browserd/src/main.ts +7 -0
  48. package/bin/browserctl.cjs +21 -0
  49. package/bin/browserd.cjs +21 -0
  50. package/extensions/chrome-relay/README.md +36 -0
  51. package/extensions/chrome-relay/background.js +1687 -0
  52. package/extensions/chrome-relay/manifest.json +15 -0
  53. package/extensions/chrome-relay/popup.html +369 -0
  54. package/extensions/chrome-relay/popup.js +972 -0
  55. package/package.json +51 -0
  56. package/packages/core/src/bootstrap.test.ts +10 -0
  57. package/packages/core/src/driver-registry.test.ts +45 -0
  58. package/packages/core/src/driver-registry.ts +22 -0
  59. package/packages/core/src/driver.ts +47 -0
  60. package/packages/core/src/index.ts +5 -0
  61. package/packages/core/src/ref-cache.test.ts +61 -0
  62. package/packages/core/src/ref-cache.ts +28 -0
  63. package/packages/core/src/session-store.test.ts +49 -0
  64. package/packages/core/src/session-store.ts +33 -0
  65. package/packages/core/src/types.ts +9 -0
  66. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +634 -0
  67. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +2206 -0
  68. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +264 -0
  69. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +521 -0
  70. package/packages/driver-chrome-relay/src/index.ts +26 -0
  71. package/packages/driver-managed/src/index.ts +22 -0
  72. package/packages/driver-managed/src/managed-driver.test.ts +59 -0
  73. package/packages/driver-managed/src/managed-driver.ts +125 -0
  74. package/packages/driver-managed/src/managed-local-driver.test.ts +506 -0
  75. package/packages/driver-managed/src/managed-local-driver.ts +2021 -0
  76. package/packages/driver-remote-cdp/src/index.ts +19 -0
  77. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +617 -0
  78. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +2042 -0
  79. package/packages/protocol/src/envelope.test.ts +25 -0
  80. package/packages/protocol/src/envelope.ts +31 -0
  81. package/packages/protocol/src/errors.test.ts +17 -0
  82. package/packages/protocol/src/errors.ts +11 -0
  83. package/packages/protocol/src/index.ts +3 -0
  84. package/packages/protocol/src/tools.ts +3 -0
  85. package/packages/transport-mcp-stdio/src/index.ts +3 -0
  86. package/packages/transport-mcp-stdio/src/sdk-server.ts +139 -0
  87. package/packages/transport-mcp-stdio/src/server.test.ts +281 -0
  88. package/packages/transport-mcp-stdio/src/server.ts +183 -0
  89. package/packages/transport-mcp-stdio/src/tool-map.ts +67 -0
  90. package/scripts/smoke.ps1 +127 -0
@@ -0,0 +1,2206 @@
1
+ import type {
2
+ BrowserDriver,
3
+ BrowserDriverScreenshot,
4
+ DriverObject
5
+ } from "../../core/src/driver";
6
+ import type { ProfileId, TargetId } from "../../core/src/types";
7
+
8
+ export type ChromeRelayConsoleEntryLocation = {
9
+ url?: string;
10
+ lineNumber?: number;
11
+ columnNumber?: number;
12
+ };
13
+
14
+ export type ChromeRelayConsoleEntry = {
15
+ type: string;
16
+ text: string;
17
+ timestamp: string;
18
+ location?: ChromeRelayConsoleEntryLocation;
19
+ };
20
+
21
+ export type ChromeRelayNetworkRequestSummary = {
22
+ requestId: string;
23
+ url: string;
24
+ method?: string;
25
+ resourceType?: string;
26
+ status?: number;
27
+ timestamp: string;
28
+ };
29
+
30
+ export type ChromeRelayNetworkResponseBody = {
31
+ body: string;
32
+ encoding: "utf8" | "base64";
33
+ };
34
+
35
+ export type ChromeRelaySnapshot = {
36
+ kind: "chrome-relay";
37
+ profile: ProfileId;
38
+ targetId: TargetId;
39
+ relayUrl: string;
40
+ hasTarget: boolean;
41
+ url?: string;
42
+ title?: string;
43
+ html?: string;
44
+ requestSummaries?: ChromeRelayNetworkRequestSummary[];
45
+ };
46
+
47
+ export type ChromeRelayScreenshot = {
48
+ kind: "chrome-relay";
49
+ profile: ProfileId;
50
+ targetId: TargetId;
51
+ relayUrl: string;
52
+ hasTarget: boolean;
53
+ mimeType?: string;
54
+ encoding?: "base64";
55
+ imageBase64?: string;
56
+ width?: number;
57
+ height?: number;
58
+ };
59
+
60
+ export type ChromeRelayTelemetryDriverExtensions = {
61
+ getConsoleEntries?(targetId: TargetId, profile?: ProfileId): ChromeRelayConsoleEntry[];
62
+ getNetworkResponseBody?(
63
+ requestId: string,
64
+ targetId: TargetId,
65
+ profile?: ProfileId
66
+ ): ChromeRelayNetworkResponseBody | undefined;
67
+ };
68
+
69
+ export type ChromeRelayLocator = {
70
+ click(): Promise<void>;
71
+ fill(value: string): Promise<void>;
72
+ type(value: string): Promise<void>;
73
+ };
74
+
75
+ export type ChromeRelayKeyboard = {
76
+ press(key: string): Promise<void>;
77
+ };
78
+
79
+ export type ChromeRelayPage = {
80
+ goto(url: string): Promise<void>;
81
+ bringToFront(): Promise<void>;
82
+ close(): Promise<void>;
83
+ url(): string;
84
+ title(): Promise<string>;
85
+ content(): Promise<string>;
86
+ screenshot?(options?: Record<string, unknown>): Promise<unknown>;
87
+ domQuery?(selector: string): Promise<unknown>;
88
+ domQueryAll?(selector: string): Promise<unknown>;
89
+ elementScreenshot?(selector: string): Promise<unknown>;
90
+ a11ySnapshot?(selector?: string): Promise<unknown>;
91
+ cookieGet?(name?: string): Promise<unknown>;
92
+ cookieSet?(cookie: { name: string; value: string; url?: string }): Promise<unknown>;
93
+ cookieClear?(name?: string): Promise<unknown>;
94
+ storageGet?(scope: string, key: string): Promise<unknown>;
95
+ storageSet?(scope: string, key: string, value: string): Promise<unknown>;
96
+ frameList?(): Promise<unknown>;
97
+ frameSnapshot?(frameId: string): Promise<unknown>;
98
+ scroll?(deltaX?: number, deltaY?: number): Promise<unknown>;
99
+ locator(selector: string): ChromeRelayLocator;
100
+ keyboard?: ChromeRelayKeyboard;
101
+ on?(eventName: string, listener: (payload: unknown) => unknown): void;
102
+ };
103
+
104
+ export type ChromeRelayBrowserContext = {
105
+ newPage(): Promise<ChromeRelayPage>;
106
+ close?(): Promise<void>;
107
+ };
108
+
109
+ export type ChromeRelayBrowser = {
110
+ contexts(): ChromeRelayBrowserContext[];
111
+ newContext?(): Promise<ChromeRelayBrowserContext>;
112
+ close(): Promise<void>;
113
+ };
114
+
115
+ export type ChromeRelayDriverRuntime = {
116
+ connectOverCDP(endpointUrl: string): Promise<ChromeRelayBrowser>;
117
+ fetchJson?(url: string): Promise<unknown>;
118
+ isConnected?(): boolean | Promise<boolean>;
119
+ };
120
+
121
+ export type ChromeRelayDriverConfig = {
122
+ relayUrl?: string;
123
+ runtime?: ChromeRelayDriverRuntime;
124
+ };
125
+
126
+ export type ChromeRelayDriverStatus = {
127
+ kind: "chrome-relay";
128
+ connected: boolean;
129
+ relayUrl: string;
130
+ };
131
+
132
+ const DEFAULT_PROFILE_ID: ProfileId = "profile:chrome-relay:default";
133
+ const DEFAULT_RELAY_URL = "http://127.0.0.1:9223";
134
+ const TELEMETRY_EVENT_LIMIT = 200;
135
+
136
+ type ChromeRelayTab = {
137
+ url: string;
138
+ page: ChromeRelayPage;
139
+ };
140
+
141
+ type ChromeRelayProfileState = {
142
+ nextTargetNumber: number;
143
+ tabs: Map<TargetId, ChromeRelayTab>;
144
+ tabOrder: TargetId[];
145
+ focusedTargetId?: TargetId;
146
+ };
147
+
148
+ type ChromeRelayTargetOperationState = {
149
+ uploadFiles: string[];
150
+ dialogArmedCount: number;
151
+ triggerCount: number;
152
+ downloadInFlight?: Promise<ChromeRelayDownloadArtifact>;
153
+ latestDownload?: ChromeRelayDownloadArtifact;
154
+ };
155
+
156
+ type ChromeRelayDownloadArtifact = {
157
+ path: string;
158
+ suggestedFilename?: string;
159
+ url?: string;
160
+ mimeType?: string;
161
+ };
162
+
163
+ type ChromeRelayTargetTelemetryState = {
164
+ consoleEntries: ChromeRelayConsoleEntry[];
165
+ requestSummaries: ChromeRelayNetworkRequestSummary[];
166
+ networkResponseBodies: Map<string, ChromeRelayNetworkResponseBody>;
167
+ nextRequestNumber: number;
168
+ };
169
+
170
+ function resolveProfileId(profile?: ProfileId): ProfileId {
171
+ return profile ?? DEFAULT_PROFILE_ID;
172
+ }
173
+
174
+ function createTargetId(profileId: ProfileId, targetNumber: number): TargetId {
175
+ return `target:chrome-relay:${profileId}:${targetNumber}`;
176
+ }
177
+
178
+ function createProfileState(): ChromeRelayProfileState {
179
+ return {
180
+ nextTargetNumber: 1,
181
+ tabs: new Map<TargetId, ChromeRelayTab>(),
182
+ tabOrder: []
183
+ };
184
+ }
185
+
186
+ function createTargetOperationState(): ChromeRelayTargetOperationState {
187
+ return {
188
+ uploadFiles: [],
189
+ dialogArmedCount: 0,
190
+ triggerCount: 0
191
+ };
192
+ }
193
+
194
+ function createTargetTelemetryState(): ChromeRelayTargetTelemetryState {
195
+ return {
196
+ consoleEntries: [],
197
+ requestSummaries: [],
198
+ networkResponseBodies: new Map<string, ChromeRelayNetworkResponseBody>(),
199
+ nextRequestNumber: 1
200
+ };
201
+ }
202
+
203
+ function trimConsoleEntries(telemetryState: ChromeRelayTargetTelemetryState): void {
204
+ if (telemetryState.consoleEntries.length <= TELEMETRY_EVENT_LIMIT) {
205
+ return;
206
+ }
207
+
208
+ telemetryState.consoleEntries.splice(
209
+ 0,
210
+ telemetryState.consoleEntries.length - TELEMETRY_EVENT_LIMIT
211
+ );
212
+ }
213
+
214
+ function trimNetworkTelemetry(telemetryState: ChromeRelayTargetTelemetryState): void {
215
+ if (telemetryState.requestSummaries.length > TELEMETRY_EVENT_LIMIT) {
216
+ const removedSummaries = telemetryState.requestSummaries.splice(
217
+ 0,
218
+ telemetryState.requestSummaries.length - TELEMETRY_EVENT_LIMIT
219
+ );
220
+ for (const removed of removedSummaries) {
221
+ telemetryState.networkResponseBodies.delete(removed.requestId);
222
+ }
223
+ }
224
+
225
+ const knownRequestIds = new Set(
226
+ telemetryState.requestSummaries.map((summary) => summary.requestId)
227
+ );
228
+ for (const requestId of telemetryState.networkResponseBodies.keys()) {
229
+ if (!knownRequestIds.has(requestId)) {
230
+ telemetryState.networkResponseBodies.delete(requestId);
231
+ }
232
+ }
233
+ }
234
+
235
+ function sanitizePathSegment(value: string): string {
236
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
237
+ }
238
+
239
+ function createDownloadPath(profileId: ProfileId, targetId: TargetId, triggerCount: number): string {
240
+ return `chrome-relay-${sanitizePathSegment(profileId)}-${sanitizePathSegment(targetId)}-${triggerCount}.bin`;
241
+ }
242
+
243
+ function createRequestId(profileId: ProfileId, targetId: TargetId, requestNumber: number): string {
244
+ return `request:chrome-relay:${encodeURIComponent(profileId)}:${encodeURIComponent(targetId)}:${requestNumber}`;
245
+ }
246
+
247
+ function parseRelayUrl(config?: ChromeRelayDriverConfig): string {
248
+ const rawRelayUrl = config?.relayUrl === undefined ? DEFAULT_RELAY_URL : config.relayUrl.trim();
249
+ if (rawRelayUrl.trim().length === 0) {
250
+ throw new Error("Invalid relayUrl: value must not be empty.");
251
+ }
252
+
253
+ try {
254
+ return new URL(rawRelayUrl).toString();
255
+ } catch {
256
+ throw new Error(`Invalid relayUrl: ${rawRelayUrl}`);
257
+ }
258
+ }
259
+
260
+ function toErrorMessage(error: unknown): string {
261
+ return error instanceof Error ? error.message : "Unexpected chrome-relay driver failure.";
262
+ }
263
+
264
+ function readActionPayloadString(payload: DriverObject | undefined, key: string): string | undefined {
265
+ const value = payload?.[key];
266
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
267
+ }
268
+
269
+ function readActionPayloadNumber(payload: DriverObject | undefined, key: string): number | undefined {
270
+ const value = payload?.[key];
271
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
272
+ }
273
+
274
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
275
+ return typeof value === "object" && value !== null && !Array.isArray(value);
276
+ }
277
+
278
+ function readObjectMethod<TValue>(value: unknown, methodName: string): TValue | undefined {
279
+ if (typeof value !== "object" || value === null) {
280
+ return undefined;
281
+ }
282
+
283
+ const method = (value as Record<string, unknown>)[methodName];
284
+ if (typeof method !== "function") {
285
+ return undefined;
286
+ }
287
+
288
+ try {
289
+ return (method as () => TValue).call(value);
290
+ } catch {
291
+ return undefined;
292
+ }
293
+ }
294
+
295
+ function getObjectMethod(
296
+ value: unknown,
297
+ methodName: string
298
+ ): ((...args: unknown[]) => unknown) | undefined {
299
+ if (typeof value !== "object" || value === null) {
300
+ return undefined;
301
+ }
302
+
303
+ const method = (value as Record<string, unknown>)[methodName];
304
+ return typeof method === "function" ? (method as (...args: unknown[]) => unknown) : undefined;
305
+ }
306
+
307
+ function readObjectStringProperty(value: unknown, key: string): string | undefined {
308
+ if (typeof value !== "object" || value === null) {
309
+ return undefined;
310
+ }
311
+
312
+ const property = (value as Record<string, unknown>)[key];
313
+ return typeof property === "string" ? property : undefined;
314
+ }
315
+
316
+ function readObjectNumberProperty(value: unknown, key: string): number | undefined {
317
+ if (typeof value !== "object" || value === null) {
318
+ return undefined;
319
+ }
320
+
321
+ const property = (value as Record<string, unknown>)[key];
322
+ return typeof property === "number" ? property : undefined;
323
+ }
324
+
325
+ async function readRuntimeConnectionStatus(
326
+ runtime: ChromeRelayDriverRuntime
327
+ ): Promise<boolean | undefined> {
328
+ if (typeof runtime.isConnected !== "function") {
329
+ return undefined;
330
+ }
331
+
332
+ try {
333
+ const connected = await runtime.isConnected();
334
+ return typeof connected === "boolean" ? connected : undefined;
335
+ } catch {
336
+ return undefined;
337
+ }
338
+ }
339
+
340
+ function readConsoleEntry(value: unknown): ChromeRelayConsoleEntry {
341
+ const locationValue = readObjectMethod<unknown>(value, "location");
342
+ const locationUrl = readObjectStringProperty(locationValue, "url");
343
+ const locationLineNumber = readObjectNumberProperty(locationValue, "lineNumber");
344
+ const locationColumnNumber = readObjectNumberProperty(locationValue, "columnNumber");
345
+
346
+ const hasLocation =
347
+ locationUrl !== undefined || locationLineNumber !== undefined || locationColumnNumber !== undefined;
348
+
349
+ return {
350
+ type: readObjectMethod<string>(value, "type") ?? "log",
351
+ text: readObjectMethod<string>(value, "text") ?? "",
352
+ timestamp: new Date().toISOString(),
353
+ ...(hasLocation
354
+ ? {
355
+ location: {
356
+ ...(locationUrl !== undefined ? { url: locationUrl } : {}),
357
+ ...(locationLineNumber !== undefined ? { lineNumber: locationLineNumber } : {}),
358
+ ...(locationColumnNumber !== undefined ? { columnNumber: locationColumnNumber } : {})
359
+ }
360
+ }
361
+ : {})
362
+ };
363
+ }
364
+
365
+ function readNetworkSummary(value: unknown, requestId: string): ChromeRelayNetworkRequestSummary {
366
+ const requestValue = readObjectMethod<unknown>(value, "request");
367
+ const method = readObjectMethod<string>(requestValue, "method");
368
+ const resourceType = readObjectMethod<string>(requestValue, "resourceType");
369
+ const status = readObjectMethod<number>(value, "status");
370
+
371
+ return {
372
+ requestId,
373
+ url: readObjectMethod<string>(value, "url") ?? "",
374
+ ...(typeof method === "string" ? { method } : {}),
375
+ ...(typeof resourceType === "string" ? { resourceType } : {}),
376
+ ...(typeof status === "number" ? { status } : {}),
377
+ timestamp: new Date().toISOString()
378
+ };
379
+ }
380
+
381
+ function toBase64(value: ArrayBuffer | Uint8Array): string {
382
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
383
+ return Buffer.from(bytes).toString("base64");
384
+ }
385
+
386
+ function parseScreenshotBase64(
387
+ value: string
388
+ ): { mimeType: string; imageBase64: string } | undefined {
389
+ const trimmedValue = value.trim();
390
+ if (trimmedValue.length === 0) {
391
+ return undefined;
392
+ }
393
+
394
+ const dataUrlMatch = /^data:([^;,]+);base64,(.+)$/i.exec(trimmedValue);
395
+ if (dataUrlMatch !== null) {
396
+ const mimeType = dataUrlMatch[1] ?? "image/png";
397
+ const imageBase64 = dataUrlMatch[2] ?? "";
398
+ if (imageBase64.length === 0) {
399
+ return undefined;
400
+ }
401
+
402
+ return {
403
+ mimeType,
404
+ imageBase64
405
+ };
406
+ }
407
+
408
+ return {
409
+ mimeType: "image/png",
410
+ imageBase64: trimmedValue
411
+ };
412
+ }
413
+
414
+ type ChromeRelayScreenshotImage = {
415
+ mimeType: string;
416
+ encoding: "base64";
417
+ imageBase64: string;
418
+ width?: number;
419
+ height?: number;
420
+ };
421
+
422
+ function toOptionalPositiveNumber(value: unknown): number | undefined {
423
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
424
+ }
425
+
426
+ function readScreenshotImage(value: unknown): ChromeRelayScreenshotImage | undefined {
427
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
428
+ return {
429
+ mimeType: "image/png",
430
+ encoding: "base64",
431
+ imageBase64: toBase64(value)
432
+ };
433
+ }
434
+
435
+ if (typeof value === "string") {
436
+ const parsed = parseScreenshotBase64(value);
437
+ if (parsed === undefined) {
438
+ return undefined;
439
+ }
440
+
441
+ return {
442
+ mimeType: parsed.mimeType,
443
+ encoding: "base64",
444
+ imageBase64: parsed.imageBase64
445
+ };
446
+ }
447
+
448
+ if (typeof value !== "object" || value === null) {
449
+ return undefined;
450
+ }
451
+
452
+ const imageBase64 = readObjectStringProperty(value, "imageBase64");
453
+ if (imageBase64 === undefined || imageBase64.trim().length === 0) {
454
+ return undefined;
455
+ }
456
+
457
+ const rawEncoding = readObjectStringProperty(value, "encoding");
458
+ if (rawEncoding !== undefined && rawEncoding !== "base64") {
459
+ return undefined;
460
+ }
461
+
462
+ const mimeType = readObjectStringProperty(value, "mimeType") ?? "image/png";
463
+ const width = toOptionalPositiveNumber(readObjectNumberProperty(value, "width"));
464
+ const height = toOptionalPositiveNumber(readObjectNumberProperty(value, "height"));
465
+
466
+ return {
467
+ mimeType,
468
+ encoding: "base64",
469
+ imageBase64: imageBase64.trim(),
470
+ ...(width !== undefined ? { width } : {}),
471
+ ...(height !== undefined ? { height } : {})
472
+ };
473
+ }
474
+
475
+ async function capturePageScreenshot(page: ChromeRelayPage): Promise<ChromeRelayScreenshotImage> {
476
+ const screenshotMethod = getObjectMethod(page, "screenshot");
477
+ if (typeof screenshotMethod !== "function") {
478
+ throw new Error("Screenshot is not supported by this driver runtime.");
479
+ }
480
+
481
+ const screenshot = await screenshotMethod.call(page, {
482
+ type: "png"
483
+ });
484
+ const parsedScreenshot = readScreenshotImage(screenshot);
485
+ if (parsedScreenshot === undefined) {
486
+ throw new Error("Unexpected screenshot payload from driver runtime.");
487
+ }
488
+
489
+ return parsedScreenshot;
490
+ }
491
+
492
+ type ChromeRelayCookie = {
493
+ name: string;
494
+ value: string;
495
+ domain?: string;
496
+ path?: string;
497
+ expires?: number;
498
+ httpOnly?: boolean;
499
+ secure?: boolean;
500
+ sameSite?: string;
501
+ url?: string;
502
+ };
503
+
504
+ function normalizeCookies(value: unknown): ChromeRelayCookie[] {
505
+ if (!Array.isArray(value)) {
506
+ return [];
507
+ }
508
+
509
+ const cookies: ChromeRelayCookie[] = [];
510
+ for (const item of value) {
511
+ if (!isObjectRecord(item)) {
512
+ continue;
513
+ }
514
+
515
+ const name = readObjectStringProperty(item, "name");
516
+ const cookieValue = readObjectStringProperty(item, "value");
517
+ if (name === undefined || cookieValue === undefined) {
518
+ continue;
519
+ }
520
+
521
+ const domain = readObjectStringProperty(item, "domain");
522
+ const path = readObjectStringProperty(item, "path");
523
+ const sameSite = readObjectStringProperty(item, "sameSite");
524
+ const url = readObjectStringProperty(item, "url");
525
+ const expiresRaw = readObjectNumberProperty(item, "expires");
526
+ const httpOnlyRaw = item.httpOnly;
527
+ const secureRaw = item.secure;
528
+
529
+ cookies.push({
530
+ name,
531
+ value: cookieValue,
532
+ ...(domain !== undefined ? { domain } : {}),
533
+ ...(path !== undefined ? { path } : {}),
534
+ ...(typeof expiresRaw === "number" && Number.isFinite(expiresRaw) ? { expires: expiresRaw } : {}),
535
+ ...(typeof httpOnlyRaw === "boolean" ? { httpOnly: httpOnlyRaw } : {}),
536
+ ...(typeof secureRaw === "boolean" ? { secure: secureRaw } : {}),
537
+ ...(sameSite !== undefined ? { sameSite } : {}),
538
+ ...(url !== undefined ? { url } : {})
539
+ });
540
+ }
541
+
542
+ return cookies;
543
+ }
544
+
545
+ function toCookieInput(cookie: ChromeRelayCookie): Record<string, unknown> | undefined {
546
+ const base: Record<string, unknown> = {
547
+ name: cookie.name,
548
+ value: cookie.value
549
+ };
550
+
551
+ if (cookie.url !== undefined) {
552
+ return {
553
+ ...base,
554
+ url: cookie.url,
555
+ ...(cookie.expires !== undefined ? { expires: cookie.expires } : {}),
556
+ ...(cookie.httpOnly !== undefined ? { httpOnly: cookie.httpOnly } : {}),
557
+ ...(cookie.secure !== undefined ? { secure: cookie.secure } : {}),
558
+ ...(cookie.sameSite !== undefined ? { sameSite: cookie.sameSite } : {})
559
+ };
560
+ }
561
+
562
+ if (cookie.domain !== undefined && cookie.path !== undefined) {
563
+ return {
564
+ ...base,
565
+ domain: cookie.domain,
566
+ path: cookie.path,
567
+ ...(cookie.expires !== undefined ? { expires: cookie.expires } : {}),
568
+ ...(cookie.httpOnly !== undefined ? { httpOnly: cookie.httpOnly } : {}),
569
+ ...(cookie.secure !== undefined ? { secure: cookie.secure } : {}),
570
+ ...(cookie.sameSite !== undefined ? { sameSite: cookie.sameSite } : {})
571
+ };
572
+ }
573
+
574
+ return undefined;
575
+ }
576
+
577
+ function parseStorageScope(scope: string | undefined): "local" | "session" | undefined {
578
+ if (scope === "local" || scope === "session") {
579
+ return scope;
580
+ }
581
+
582
+ return undefined;
583
+ }
584
+
585
+ function parseFrameIndex(frameId: string | undefined): number | undefined {
586
+ if (typeof frameId !== "string") {
587
+ return undefined;
588
+ }
589
+
590
+ const match = /^frame:(\d+)$/.exec(frameId.trim());
591
+ if (match === null) {
592
+ return undefined;
593
+ }
594
+
595
+ const parsed = Number.parseInt(match[1] ?? "", 10);
596
+ return Number.isFinite(parsed) ? parsed : undefined;
597
+ }
598
+
599
+ async function evaluateInPage<TResult>(
600
+ page: ChromeRelayPage,
601
+ callback: (arg: unknown) => unknown,
602
+ arg: unknown
603
+ ): Promise<TResult> {
604
+ const evaluateMethod = getObjectMethod(page, "evaluate");
605
+ if (typeof evaluateMethod !== "function") {
606
+ throw new Error("Driver runtime page does not support evaluate.");
607
+ }
608
+
609
+ const result = await evaluateMethod.call(page, callback, arg);
610
+ return result as TResult;
611
+ }
612
+
613
+ async function readNetworkResponseBody(
614
+ value: unknown
615
+ ): Promise<ChromeRelayNetworkResponseBody | undefined> {
616
+ const textMethod = getObjectMethod(value, "text");
617
+ if (typeof textMethod === "function") {
618
+ try {
619
+ const textResult = await textMethod.call(value);
620
+ if (typeof textResult === "string") {
621
+ return {
622
+ body: textResult,
623
+ encoding: "utf8"
624
+ };
625
+ }
626
+ } catch {
627
+ // Ignore body extraction failures from optional runtime hooks.
628
+ }
629
+ }
630
+
631
+ const bodyMethod = getObjectMethod(value, "body");
632
+ if (typeof bodyMethod !== "function") {
633
+ return undefined;
634
+ }
635
+
636
+ try {
637
+ const bodyResult = await bodyMethod.call(value);
638
+ if (typeof bodyResult === "string") {
639
+ return {
640
+ body: bodyResult,
641
+ encoding: "utf8"
642
+ };
643
+ }
644
+
645
+ if (bodyResult instanceof Uint8Array || bodyResult instanceof ArrayBuffer) {
646
+ return {
647
+ body: toBase64(bodyResult),
648
+ encoding: "base64"
649
+ };
650
+ }
651
+
652
+ return undefined;
653
+ } catch {
654
+ return undefined;
655
+ }
656
+ }
657
+
658
+ function attachTelemetryListeners(
659
+ page: ChromeRelayPage,
660
+ profileId: ProfileId,
661
+ targetId: TargetId,
662
+ telemetryState: ChromeRelayTargetTelemetryState,
663
+ operationState: ChromeRelayTargetOperationState
664
+ ): void {
665
+ if (typeof page.on !== "function") {
666
+ return;
667
+ }
668
+
669
+ page.on("console", (message) => {
670
+ telemetryState.consoleEntries.push(readConsoleEntry(message));
671
+ trimConsoleEntries(telemetryState);
672
+ });
673
+
674
+ page.on("response", async (response) => {
675
+ const requestId = createRequestId(profileId, targetId, telemetryState.nextRequestNumber);
676
+ telemetryState.nextRequestNumber += 1;
677
+ telemetryState.requestSummaries.push(readNetworkSummary(response, requestId));
678
+ trimNetworkTelemetry(telemetryState);
679
+
680
+ const body = await readNetworkResponseBody(response);
681
+ if (body !== undefined) {
682
+ telemetryState.networkResponseBodies.set(requestId, body);
683
+ trimNetworkTelemetry(telemetryState);
684
+ }
685
+ });
686
+
687
+ page.on("dialog", async (dialog) => {
688
+ if (operationState.dialogArmedCount <= 0) {
689
+ return;
690
+ }
691
+
692
+ operationState.dialogArmedCount -= 1;
693
+ const acceptMethod = getObjectMethod(dialog, "accept");
694
+ if (typeof acceptMethod === "function") {
695
+ try {
696
+ await acceptMethod.call(dialog);
697
+ return;
698
+ } catch {
699
+ // Fall through to dismiss.
700
+ }
701
+ }
702
+
703
+ const dismissMethod = getObjectMethod(dialog, "dismiss");
704
+ if (typeof dismissMethod === "function") {
705
+ try {
706
+ await dismissMethod.call(dialog);
707
+ } catch {
708
+ // Ignore dialog dismissal failures.
709
+ }
710
+ }
711
+ });
712
+ }
713
+
714
+ async function readDownloadArtifact(
715
+ value: unknown,
716
+ fallbackPath: string
717
+ ): Promise<ChromeRelayDownloadArtifact> {
718
+ let resolvedPath = fallbackPath;
719
+ const pathMethod = getObjectMethod(value, "path");
720
+ if (typeof pathMethod === "function") {
721
+ try {
722
+ const pathResult = await pathMethod.call(value);
723
+ if (typeof pathResult === "string" && pathResult.trim().length > 0) {
724
+ resolvedPath = pathResult;
725
+ }
726
+ } catch {
727
+ // Ignore path() failures and keep fallback path.
728
+ }
729
+ }
730
+
731
+ const saveAsMethod = getObjectMethod(value, "saveAs");
732
+ if (typeof saveAsMethod === "function") {
733
+ try {
734
+ await saveAsMethod.call(value, fallbackPath);
735
+ resolvedPath = fallbackPath;
736
+ } catch {
737
+ // Ignore saveAs failures and keep best-effort path.
738
+ }
739
+ }
740
+
741
+ const suggestedFilename = readObjectMethod<string>(value, "suggestedFilename");
742
+ const url = readObjectMethod<string>(value, "url");
743
+ const mimeType = readObjectMethod<string>(value, "mimeType");
744
+
745
+ return {
746
+ path: resolvedPath,
747
+ ...(typeof suggestedFilename === "string" ? { suggestedFilename } : {}),
748
+ ...(typeof url === "string" ? { url } : {}),
749
+ ...(typeof mimeType === "string" ? { mimeType } : {})
750
+ };
751
+ }
752
+
753
+ function readWebSocketDebuggerUrl(value: unknown): string | undefined {
754
+ const rawUrl = readObjectStringProperty(value, "webSocketDebuggerUrl");
755
+ if (rawUrl === undefined) {
756
+ return undefined;
757
+ }
758
+
759
+ try {
760
+ const parsed = new URL(rawUrl);
761
+ if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
762
+ return undefined;
763
+ }
764
+
765
+ return parsed.toString();
766
+ } catch {
767
+ return undefined;
768
+ }
769
+ }
770
+
771
+ async function resolveRelayEndpoint(relayUrl: string, runtime: ChromeRelayDriverRuntime): Promise<string> {
772
+ const parsedRelayUrl = new URL(relayUrl);
773
+ if (parsedRelayUrl.protocol === "ws:" || parsedRelayUrl.protocol === "wss:") {
774
+ return parsedRelayUrl.toString();
775
+ }
776
+
777
+ if (parsedRelayUrl.protocol === "http:" || parsedRelayUrl.protocol === "https:") {
778
+ if (typeof runtime.fetchJson === "function") {
779
+ const jsonVersionUrl = new URL("/json/version", parsedRelayUrl).toString();
780
+ try {
781
+ const payload = await runtime.fetchJson(jsonVersionUrl);
782
+ const debuggerUrl = readWebSocketDebuggerUrl(payload);
783
+ if (debuggerUrl !== undefined) {
784
+ return debuggerUrl;
785
+ }
786
+ } catch {
787
+ // Fall through to connect directly to relay URL.
788
+ }
789
+ }
790
+ }
791
+
792
+ return parsedRelayUrl.toString();
793
+ }
794
+
795
+ function createPlaywrightRuntime(): ChromeRelayDriverRuntime {
796
+ return {
797
+ connectOverCDP: async (endpointUrl) => {
798
+ const playwright = await import("playwright-core");
799
+ return (await playwright.chromium.connectOverCDP(endpointUrl)) as unknown as ChromeRelayBrowser;
800
+ },
801
+ fetchJson: async (url) => {
802
+ const response = await fetch(url);
803
+ if (!response.ok) {
804
+ throw new Error(`Relay metadata request failed (${response.status}) for ${url}`);
805
+ }
806
+
807
+ return response.json();
808
+ }
809
+ };
810
+ }
811
+
812
+ function cloneConsoleEntry(entry: ChromeRelayConsoleEntry): ChromeRelayConsoleEntry {
813
+ return {
814
+ ...entry,
815
+ ...(entry.location !== undefined ? { location: { ...entry.location } } : {})
816
+ };
817
+ }
818
+
819
+ function cloneRequestSummary(summary: ChromeRelayNetworkRequestSummary): ChromeRelayNetworkRequestSummary {
820
+ return { ...summary };
821
+ }
822
+
823
+ export function createChromeRelayDriver(
824
+ config: ChromeRelayDriverConfig = {}
825
+ ): BrowserDriver<ChromeRelayDriverStatus, ChromeRelaySnapshot> &
826
+ ChromeRelayTelemetryDriverExtensions &
827
+ BrowserDriverScreenshot<ChromeRelayScreenshot> {
828
+ const relayUrl = parseRelayUrl(config);
829
+ const runtime = config.runtime ?? createPlaywrightRuntime();
830
+ const profileStates = new Map<ProfileId, ChromeRelayProfileState>();
831
+ const targetOperationStates = new Map<ProfileId, Map<TargetId, ChromeRelayTargetOperationState>>();
832
+ const targetTelemetryStates = new Map<ProfileId, Map<TargetId, ChromeRelayTargetTelemetryState>>();
833
+ let browser: ChromeRelayBrowser | undefined;
834
+ let browserConnectInFlight: Promise<ChromeRelayBrowser> | undefined;
835
+ let browserContext: ChromeRelayBrowserContext | undefined;
836
+ let browserContextInFlight: Promise<ChromeRelayBrowserContext> | undefined;
837
+ let resolvedEndpointUrl: string | undefined;
838
+ let endpointResolveInFlight: Promise<string> | undefined;
839
+
840
+ async function ensureEndpointUrl(): Promise<string> {
841
+ if (resolvedEndpointUrl !== undefined) {
842
+ return resolvedEndpointUrl;
843
+ }
844
+
845
+ if (endpointResolveInFlight === undefined) {
846
+ endpointResolveInFlight = resolveRelayEndpoint(relayUrl, runtime)
847
+ .then((resolved) => {
848
+ resolvedEndpointUrl = resolved;
849
+ return resolved;
850
+ })
851
+ .finally(() => {
852
+ endpointResolveInFlight = undefined;
853
+ });
854
+ }
855
+
856
+ return endpointResolveInFlight;
857
+ }
858
+
859
+ async function ensureBrowser(): Promise<ChromeRelayBrowser> {
860
+ if (browser !== undefined) {
861
+ return browser;
862
+ }
863
+
864
+ if (browserConnectInFlight === undefined) {
865
+ browserConnectInFlight = (async () => {
866
+ const endpointUrl = await ensureEndpointUrl();
867
+ const createdBrowser = await runtime.connectOverCDP(endpointUrl);
868
+ browser = createdBrowser;
869
+ return createdBrowser;
870
+ })().finally(() => {
871
+ browserConnectInFlight = undefined;
872
+ });
873
+ }
874
+
875
+ return browserConnectInFlight;
876
+ }
877
+
878
+ async function ensureBrowserContext(): Promise<ChromeRelayBrowserContext> {
879
+ if (browserContext !== undefined) {
880
+ return browserContext;
881
+ }
882
+
883
+ if (browserContextInFlight === undefined) {
884
+ browserContextInFlight = (async () => {
885
+ const connectedBrowser = await ensureBrowser();
886
+ const existingContext = connectedBrowser.contexts()[0];
887
+ if (existingContext !== undefined) {
888
+ browserContext = existingContext;
889
+ return existingContext;
890
+ }
891
+
892
+ if (typeof connectedBrowser.newContext !== "function") {
893
+ throw new Error("No browser context available from relay CDP connection.");
894
+ }
895
+
896
+ const createdContext = await connectedBrowser.newContext();
897
+ browserContext = createdContext;
898
+ return createdContext;
899
+ })().finally(() => {
900
+ browserContextInFlight = undefined;
901
+ });
902
+ }
903
+
904
+ return browserContextInFlight;
905
+ }
906
+
907
+ function getOrCreateProfileState(profileId: ProfileId): ChromeRelayProfileState {
908
+ const existingState = profileStates.get(profileId);
909
+ if (existingState !== undefined) {
910
+ return existingState;
911
+ }
912
+
913
+ const createdState = createProfileState();
914
+ profileStates.set(profileId, createdState);
915
+ return createdState;
916
+ }
917
+
918
+ function requireTargetInProfile(
919
+ profileId: ProfileId,
920
+ targetId: TargetId
921
+ ): { profileState: ChromeRelayProfileState; tab: ChromeRelayTab } {
922
+ const profileState = profileStates.get(profileId);
923
+ const tab = profileState?.tabs.get(targetId);
924
+ if (profileState === undefined || tab === undefined) {
925
+ throw new Error(`Unknown targetId: ${targetId} (profile: ${profileId})`);
926
+ }
927
+
928
+ return { profileState, tab };
929
+ }
930
+
931
+ function getOrCreateTargetOperationState(
932
+ profileId: ProfileId,
933
+ targetId: TargetId,
934
+ validateTarget = true
935
+ ): ChromeRelayTargetOperationState {
936
+ if (validateTarget) {
937
+ requireTargetInProfile(profileId, targetId);
938
+ }
939
+
940
+ let profileOperationStates = targetOperationStates.get(profileId);
941
+ if (profileOperationStates === undefined) {
942
+ profileOperationStates = new Map<TargetId, ChromeRelayTargetOperationState>();
943
+ targetOperationStates.set(profileId, profileOperationStates);
944
+ }
945
+
946
+ const existingState = profileOperationStates.get(targetId);
947
+ if (existingState !== undefined) {
948
+ return existingState;
949
+ }
950
+
951
+ const createdState = createTargetOperationState();
952
+ profileOperationStates.set(targetId, createdState);
953
+ return createdState;
954
+ }
955
+
956
+ function getOrCreateProfileTelemetryStates(
957
+ profileId: ProfileId
958
+ ): Map<TargetId, ChromeRelayTargetTelemetryState> {
959
+ const existingStates = targetTelemetryStates.get(profileId);
960
+ if (existingStates !== undefined) {
961
+ return existingStates;
962
+ }
963
+
964
+ const createdStates = new Map<TargetId, ChromeRelayTargetTelemetryState>();
965
+ targetTelemetryStates.set(profileId, createdStates);
966
+ return createdStates;
967
+ }
968
+
969
+ function findTelemetryState(
970
+ targetId: TargetId,
971
+ profile?: ProfileId
972
+ ): ChromeRelayTargetTelemetryState | undefined {
973
+ if (profile !== undefined) {
974
+ return targetTelemetryStates.get(resolveProfileId(profile))?.get(targetId);
975
+ }
976
+
977
+ for (const profileTelemetryStates of targetTelemetryStates.values()) {
978
+ const telemetryState = profileTelemetryStates.get(targetId);
979
+ if (telemetryState !== undefined) {
980
+ return telemetryState;
981
+ }
982
+ }
983
+
984
+ return undefined;
985
+ }
986
+
987
+ function ensureDownloadInFlight(
988
+ profileId: ProfileId,
989
+ targetId: TargetId,
990
+ tab: ChromeRelayTab,
991
+ operationState: ChromeRelayTargetOperationState
992
+ ): Promise<ChromeRelayDownloadArtifact> {
993
+ if (operationState.downloadInFlight !== undefined) {
994
+ return operationState.downloadInFlight;
995
+ }
996
+
997
+ const fallbackPath = createDownloadPath(
998
+ profileId,
999
+ targetId,
1000
+ Math.max(operationState.triggerCount, 1)
1001
+ );
1002
+ const waitForEventMethod = getObjectMethod(tab.page, "waitForEvent");
1003
+ if (typeof waitForEventMethod !== "function") {
1004
+ const artifact: ChromeRelayDownloadArtifact = { path: fallbackPath };
1005
+ operationState.latestDownload = artifact;
1006
+ const resolved = Promise.resolve(artifact);
1007
+ operationState.downloadInFlight = resolved;
1008
+ return resolved;
1009
+ }
1010
+
1011
+ const inFlight = (async () => {
1012
+ const rawDownload = await waitForEventMethod.call(tab.page, "download");
1013
+ const artifact = await readDownloadArtifact(rawDownload, fallbackPath);
1014
+ operationState.latestDownload = artifact;
1015
+ return artifact;
1016
+ })().finally(() => {
1017
+ if (operationState.downloadInFlight === inFlight) {
1018
+ operationState.downloadInFlight = undefined;
1019
+ }
1020
+ });
1021
+
1022
+ operationState.downloadInFlight = inFlight;
1023
+ return inFlight;
1024
+ }
1025
+
1026
+ return {
1027
+ status: async () => {
1028
+ const runtimeConnected = await readRuntimeConnectionStatus(runtime);
1029
+ return {
1030
+ kind: "chrome-relay",
1031
+ connected: runtimeConnected ?? browser !== undefined,
1032
+ relayUrl
1033
+ };
1034
+ },
1035
+ listProfiles: async () => {
1036
+ const knownProfiles = new Set<ProfileId>([DEFAULT_PROFILE_ID]);
1037
+ for (const profileId of profileStates.keys()) {
1038
+ knownProfiles.add(profileId);
1039
+ }
1040
+
1041
+ return Array.from(knownProfiles).sort();
1042
+ },
1043
+ listTabs: async (profile) => {
1044
+ const profileId = resolveProfileId(profile);
1045
+ const profileState = profileStates.get(profileId);
1046
+ return profileState === undefined ? [] : [...profileState.tabOrder];
1047
+ },
1048
+ openTab: async (url, profile) => {
1049
+ const profileId = resolveProfileId(profile);
1050
+ const profileState = getOrCreateProfileState(profileId);
1051
+ const targetId = createTargetId(profileId, profileState.nextTargetNumber);
1052
+ const page = await (await ensureBrowserContext()).newPage();
1053
+ const telemetryState = createTargetTelemetryState();
1054
+ const operationState = getOrCreateTargetOperationState(profileId, targetId, false);
1055
+ attachTelemetryListeners(page, profileId, targetId, telemetryState, operationState);
1056
+ await page.goto(url);
1057
+
1058
+ profileState.nextTargetNumber += 1;
1059
+ profileState.tabs.set(targetId, { url, page });
1060
+ profileState.tabOrder.push(targetId);
1061
+ getOrCreateProfileTelemetryStates(profileId).set(targetId, telemetryState);
1062
+ if (profileState.focusedTargetId === undefined) {
1063
+ profileState.focusedTargetId = targetId;
1064
+ }
1065
+
1066
+ return targetId;
1067
+ },
1068
+ focusTab: async (targetId, profile) => {
1069
+ const profileId = resolveProfileId(profile);
1070
+ const { profileState, tab } = requireTargetInProfile(profileId, targetId);
1071
+ await tab.page.bringToFront();
1072
+ profileState.focusedTargetId = targetId;
1073
+ },
1074
+ closeTab: async (targetId, profile) => {
1075
+ const profileId = resolveProfileId(profile);
1076
+ const { profileState, tab } = requireTargetInProfile(profileId, targetId);
1077
+ await tab.page.close();
1078
+
1079
+ profileState.tabs.delete(targetId);
1080
+ profileState.tabOrder = profileState.tabOrder.filter((existingTargetId) => existingTargetId !== targetId);
1081
+ if (profileState.focusedTargetId === targetId) {
1082
+ profileState.focusedTargetId = profileState.tabOrder[0];
1083
+ }
1084
+
1085
+ const profileOperationStates = targetOperationStates.get(profileId);
1086
+ profileOperationStates?.delete(targetId);
1087
+ if (profileOperationStates !== undefined && profileOperationStates.size === 0) {
1088
+ targetOperationStates.delete(profileId);
1089
+ }
1090
+
1091
+ const profileTelemetryStates = targetTelemetryStates.get(profileId);
1092
+ profileTelemetryStates?.delete(targetId);
1093
+ if (profileTelemetryStates !== undefined && profileTelemetryStates.size === 0) {
1094
+ targetTelemetryStates.delete(profileId);
1095
+ }
1096
+ },
1097
+ snapshot: async (targetId, profile) => {
1098
+ const profileId = resolveProfileId(profile);
1099
+ const tab = profileStates.get(profileId)?.tabs.get(targetId);
1100
+ if (tab === undefined) {
1101
+ return {
1102
+ kind: "chrome-relay",
1103
+ profile: profileId,
1104
+ targetId,
1105
+ relayUrl,
1106
+ hasTarget: false
1107
+ };
1108
+ }
1109
+
1110
+ const requestSummaries =
1111
+ targetTelemetryStates
1112
+ .get(profileId)
1113
+ ?.get(targetId)
1114
+ ?.requestSummaries.map(cloneRequestSummary) ?? [];
1115
+
1116
+ return {
1117
+ kind: "chrome-relay",
1118
+ profile: profileId,
1119
+ targetId,
1120
+ relayUrl,
1121
+ hasTarget: true,
1122
+ url: tab.page.url(),
1123
+ title: await tab.page.title(),
1124
+ html: await tab.page.content(),
1125
+ requestSummaries
1126
+ };
1127
+ },
1128
+ screenshot: async (targetId, profile) => {
1129
+ const profileId = resolveProfileId(profile);
1130
+ const tab = profileStates.get(profileId)?.tabs.get(targetId);
1131
+ if (tab === undefined) {
1132
+ return {
1133
+ kind: "chrome-relay",
1134
+ profile: profileId,
1135
+ targetId,
1136
+ relayUrl,
1137
+ hasTarget: false
1138
+ };
1139
+ }
1140
+
1141
+ const screenshot = await capturePageScreenshot(tab.page);
1142
+ return {
1143
+ kind: "chrome-relay",
1144
+ profile: profileId,
1145
+ targetId,
1146
+ relayUrl,
1147
+ hasTarget: true,
1148
+ ...screenshot
1149
+ };
1150
+ },
1151
+ act: async (action, targetId, profile) => {
1152
+ const profileId = resolveProfileId(profile);
1153
+ const tab = profileStates.get(profileId)?.tabs.get(targetId);
1154
+ if (tab === undefined) {
1155
+ return {
1156
+ actionType: action.type,
1157
+ profile: profileId,
1158
+ targetId,
1159
+ relayUrl,
1160
+ targetKnown: false,
1161
+ ok: false,
1162
+ executed: false,
1163
+ error: `Unknown targetId: ${targetId} (profile: ${profileId})`
1164
+ };
1165
+ }
1166
+
1167
+ const payload = action.payload;
1168
+ const operationState = getOrCreateTargetOperationState(profileId, targetId);
1169
+ const baseResult = {
1170
+ actionType: action.type,
1171
+ profile: profileId,
1172
+ targetId,
1173
+ relayUrl,
1174
+ targetKnown: true
1175
+ };
1176
+
1177
+ const performAction = async (): Promise<{
1178
+ ok: boolean;
1179
+ executed: boolean;
1180
+ error?: string;
1181
+ data?: Record<string, unknown>;
1182
+ }> => {
1183
+ switch (action.type) {
1184
+ case "navigate":
1185
+ case "goto": {
1186
+ const nextUrl = readActionPayloadString(payload, "url");
1187
+ if (nextUrl === undefined) {
1188
+ return {
1189
+ ok: false,
1190
+ executed: false,
1191
+ error: "action.payload.url is required for navigate action."
1192
+ };
1193
+ }
1194
+
1195
+ await tab.page.goto(nextUrl);
1196
+ tab.url = nextUrl;
1197
+ return { ok: true, executed: true };
1198
+ }
1199
+ case "click": {
1200
+ const selector = readActionPayloadString(payload, "selector");
1201
+ if (selector === undefined) {
1202
+ return {
1203
+ ok: false,
1204
+ executed: false,
1205
+ error: "action.payload.selector is required for click action."
1206
+ };
1207
+ }
1208
+
1209
+ const locator = tab.page.locator(selector);
1210
+ if (operationState.uploadFiles.length > 0) {
1211
+ const setInputFilesMethod = getObjectMethod(locator, "setInputFiles");
1212
+ if (typeof setInputFilesMethod === "function") {
1213
+ await setInputFilesMethod.call(locator, [...operationState.uploadFiles]);
1214
+ operationState.uploadFiles = [];
1215
+ return { ok: true, executed: true };
1216
+ }
1217
+ }
1218
+
1219
+ await locator.click();
1220
+ return { ok: true, executed: true };
1221
+ }
1222
+ case "fill": {
1223
+ const selector = readActionPayloadString(payload, "selector");
1224
+ const value = readActionPayloadString(payload, "value");
1225
+ if (selector === undefined || value === undefined) {
1226
+ return {
1227
+ ok: false,
1228
+ executed: false,
1229
+ error: "action.payload.selector and action.payload.value are required for fill action."
1230
+ };
1231
+ }
1232
+
1233
+ await tab.page.locator(selector).fill(value);
1234
+ return { ok: true, executed: true };
1235
+ }
1236
+ case "type": {
1237
+ const selector = readActionPayloadString(payload, "selector");
1238
+ const text = readActionPayloadString(payload, "text");
1239
+ if (selector === undefined || text === undefined) {
1240
+ return {
1241
+ ok: false,
1242
+ executed: false,
1243
+ error: "action.payload.selector and action.payload.text are required for type action."
1244
+ };
1245
+ }
1246
+
1247
+ await tab.page.locator(selector).type(text);
1248
+ return { ok: true, executed: true };
1249
+ }
1250
+ case "press": {
1251
+ const key = readActionPayloadString(payload, "key");
1252
+ if (key === undefined || tab.page.keyboard === undefined) {
1253
+ return {
1254
+ ok: false,
1255
+ executed: false,
1256
+ error: "action.payload.key is required for press action."
1257
+ };
1258
+ }
1259
+
1260
+ await tab.page.keyboard.press(key);
1261
+ return { ok: true, executed: true };
1262
+ }
1263
+ case "scroll": {
1264
+ const deltaX = readActionPayloadNumber(payload, "deltaX") ?? 0;
1265
+ const deltaY = readActionPayloadNumber(payload, "deltaY") ?? 900;
1266
+
1267
+ const customScrollMethod = getObjectMethod(tab.page, "scroll");
1268
+ if (typeof customScrollMethod === "function") {
1269
+ const customResult = await customScrollMethod.call(tab.page, deltaX, deltaY);
1270
+ return {
1271
+ ok: true,
1272
+ executed: true,
1273
+ data: isObjectRecord(customResult)
1274
+ ? customResult
1275
+ : {
1276
+ deltaX,
1277
+ deltaY
1278
+ }
1279
+ };
1280
+ }
1281
+
1282
+ const evaluateMethod = getObjectMethod(tab.page, "evaluate");
1283
+ if (typeof evaluateMethod !== "function") {
1284
+ return {
1285
+ ok: true,
1286
+ executed: false
1287
+ };
1288
+ }
1289
+
1290
+ const scrollResult = await evaluateMethod.call(
1291
+ tab.page,
1292
+ (rawArg: unknown) => {
1293
+ const value =
1294
+ rawArg !== null && typeof rawArg === "object"
1295
+ ? (rawArg as { deltaX?: unknown; deltaY?: unknown })
1296
+ : {};
1297
+ const resolvedDeltaX =
1298
+ typeof value.deltaX === "number" && Number.isFinite(value.deltaX) ? value.deltaX : 0;
1299
+ const resolvedDeltaY =
1300
+ typeof value.deltaY === "number" && Number.isFinite(value.deltaY) ? value.deltaY : 900;
1301
+ window.scrollBy({
1302
+ left: resolvedDeltaX,
1303
+ top: resolvedDeltaY
1304
+ });
1305
+
1306
+ return {
1307
+ deltaX: resolvedDeltaX,
1308
+ deltaY: resolvedDeltaY,
1309
+ scrollX: window.scrollX,
1310
+ scrollY: window.scrollY
1311
+ };
1312
+ },
1313
+ {
1314
+ deltaX,
1315
+ deltaY
1316
+ }
1317
+ );
1318
+
1319
+ return {
1320
+ ok: true,
1321
+ executed: true,
1322
+ data: isObjectRecord(scrollResult)
1323
+ ? scrollResult
1324
+ : {
1325
+ deltaX,
1326
+ deltaY
1327
+ }
1328
+ };
1329
+ }
1330
+ case "domQuery": {
1331
+ const selector = readActionPayloadString(payload, "selector");
1332
+ if (selector === undefined) {
1333
+ return {
1334
+ ok: false,
1335
+ executed: false,
1336
+ error: "action.payload.selector is required for domQuery action."
1337
+ };
1338
+ }
1339
+
1340
+ const customDomQueryMethod = getObjectMethod(tab.page, "domQuery");
1341
+ if (typeof customDomQueryMethod === "function") {
1342
+ const customResult = await customDomQueryMethod.call(tab.page, selector);
1343
+ return {
1344
+ ok: true,
1345
+ executed: true,
1346
+ data: isObjectRecord(customResult)
1347
+ ? customResult
1348
+ : {
1349
+ selector,
1350
+ found: false
1351
+ }
1352
+ };
1353
+ }
1354
+
1355
+ const queryResult = await evaluateInPage<Record<string, unknown>>(
1356
+ tab.page,
1357
+ (rawArg) => {
1358
+ const selectorValue =
1359
+ rawArg !== null &&
1360
+ typeof rawArg === "object" &&
1361
+ typeof (rawArg as { selector?: unknown }).selector === "string"
1362
+ ? (rawArg as { selector: string }).selector
1363
+ : "";
1364
+ const element = selectorValue.length > 0 ? document.querySelector(selectorValue) : null;
1365
+ if (element === null) {
1366
+ return {
1367
+ selector: selectorValue,
1368
+ found: false
1369
+ };
1370
+ }
1371
+
1372
+ const attributes: Record<string, string> = {};
1373
+ for (const attribute of Array.from(element.attributes)) {
1374
+ attributes[attribute.name] = attribute.value;
1375
+ }
1376
+
1377
+ return {
1378
+ selector: selectorValue,
1379
+ found: true,
1380
+ node: {
1381
+ tagName: element.tagName.toLowerCase(),
1382
+ id: element.id || undefined,
1383
+ className: element.className || undefined,
1384
+ text: (element.textContent || "").trim().slice(0, 300),
1385
+ attributes
1386
+ }
1387
+ };
1388
+ },
1389
+ { selector }
1390
+ );
1391
+
1392
+ return {
1393
+ ok: true,
1394
+ executed: true,
1395
+ data: isObjectRecord(queryResult)
1396
+ ? queryResult
1397
+ : {
1398
+ selector,
1399
+ found: false
1400
+ }
1401
+ };
1402
+ }
1403
+ case "domQueryAll": {
1404
+ const selector = readActionPayloadString(payload, "selector");
1405
+ if (selector === undefined) {
1406
+ return {
1407
+ ok: false,
1408
+ executed: false,
1409
+ error: "action.payload.selector is required for domQueryAll action."
1410
+ };
1411
+ }
1412
+
1413
+ const customDomQueryAllMethod = getObjectMethod(tab.page, "domQueryAll");
1414
+ if (typeof customDomQueryAllMethod === "function") {
1415
+ const customResult = await customDomQueryAllMethod.call(tab.page, selector);
1416
+ return {
1417
+ ok: true,
1418
+ executed: true,
1419
+ data: isObjectRecord(customResult)
1420
+ ? customResult
1421
+ : {
1422
+ selector,
1423
+ count: 0,
1424
+ nodes: []
1425
+ }
1426
+ };
1427
+ }
1428
+
1429
+ const queryResult = await evaluateInPage<Record<string, unknown>>(
1430
+ tab.page,
1431
+ (rawArg) => {
1432
+ const selectorValue =
1433
+ rawArg !== null &&
1434
+ typeof rawArg === "object" &&
1435
+ typeof (rawArg as { selector?: unknown }).selector === "string"
1436
+ ? (rawArg as { selector: string }).selector
1437
+ : "";
1438
+ const elements = selectorValue.length > 0 ? document.querySelectorAll(selectorValue) : [];
1439
+ const nodes = Array.from(elements).map((element, index) => {
1440
+ const attributes: Record<string, string> = {};
1441
+ for (const attribute of Array.from(element.attributes)) {
1442
+ attributes[attribute.name] = attribute.value;
1443
+ }
1444
+
1445
+ return {
1446
+ index,
1447
+ tagName: element.tagName.toLowerCase(),
1448
+ id: element.id || undefined,
1449
+ className: element.className || undefined,
1450
+ text: (element.textContent || "").trim().slice(0, 300),
1451
+ attributes
1452
+ };
1453
+ });
1454
+
1455
+ return {
1456
+ selector: selectorValue,
1457
+ count: nodes.length,
1458
+ nodes
1459
+ };
1460
+ },
1461
+ { selector }
1462
+ );
1463
+
1464
+ return {
1465
+ ok: true,
1466
+ executed: true,
1467
+ data: isObjectRecord(queryResult)
1468
+ ? queryResult
1469
+ : {
1470
+ selector,
1471
+ count: 0,
1472
+ nodes: []
1473
+ }
1474
+ };
1475
+ }
1476
+ case "elementScreenshot": {
1477
+ const selector = readActionPayloadString(payload, "selector");
1478
+ if (selector === undefined) {
1479
+ return {
1480
+ ok: false,
1481
+ executed: false,
1482
+ error: "action.payload.selector is required for elementScreenshot action."
1483
+ };
1484
+ }
1485
+
1486
+ const customElementScreenshotMethod = getObjectMethod(tab.page, "elementScreenshot");
1487
+ if (typeof customElementScreenshotMethod === "function") {
1488
+ const customResult = await customElementScreenshotMethod.call(tab.page, selector);
1489
+ return {
1490
+ ok: true,
1491
+ executed: true,
1492
+ data: isObjectRecord(customResult)
1493
+ ? customResult
1494
+ : {
1495
+ selector,
1496
+ found: false
1497
+ }
1498
+ };
1499
+ }
1500
+
1501
+ const found = await evaluateInPage<boolean>(
1502
+ tab.page,
1503
+ (rawArg) => {
1504
+ const selectorValue =
1505
+ rawArg !== null &&
1506
+ typeof rawArg === "object" &&
1507
+ typeof (rawArg as { selector?: unknown }).selector === "string"
1508
+ ? (rawArg as { selector: string }).selector
1509
+ : "";
1510
+ return selectorValue.length > 0 && document.querySelector(selectorValue) !== null;
1511
+ },
1512
+ { selector }
1513
+ );
1514
+ if (!found) {
1515
+ return {
1516
+ ok: true,
1517
+ executed: true,
1518
+ data: {
1519
+ selector,
1520
+ found: false
1521
+ }
1522
+ };
1523
+ }
1524
+
1525
+ const locator = tab.page.locator(selector);
1526
+ const locatorScreenshotMethod = getObjectMethod(locator, "screenshot");
1527
+ if (typeof locatorScreenshotMethod !== "function") {
1528
+ return {
1529
+ ok: true,
1530
+ executed: false
1531
+ };
1532
+ }
1533
+
1534
+ const rawScreenshot = await locatorScreenshotMethod.call(locator, { type: "png" });
1535
+ const parsedScreenshot = readScreenshotImage(rawScreenshot);
1536
+ if (parsedScreenshot === undefined) {
1537
+ return {
1538
+ ok: false,
1539
+ executed: false,
1540
+ error: "Unable to parse element screenshot payload."
1541
+ };
1542
+ }
1543
+
1544
+ return {
1545
+ ok: true,
1546
+ executed: true,
1547
+ data: {
1548
+ selector,
1549
+ found: true,
1550
+ ...parsedScreenshot
1551
+ }
1552
+ };
1553
+ }
1554
+ case "a11ySnapshot": {
1555
+ const selector = readActionPayloadString(payload, "selector");
1556
+ const customA11ySnapshotMethod = getObjectMethod(tab.page, "a11ySnapshot");
1557
+ if (typeof customA11ySnapshotMethod === "function") {
1558
+ const customResult = await customA11ySnapshotMethod.call(tab.page, selector);
1559
+ return {
1560
+ ok: true,
1561
+ executed: true,
1562
+ data: isObjectRecord(customResult)
1563
+ ? customResult
1564
+ : {
1565
+ found: false
1566
+ }
1567
+ };
1568
+ }
1569
+
1570
+ const accessibilityValue =
1571
+ typeof tab.page === "object" && tab.page !== null
1572
+ ? (tab.page as Record<string, unknown>).accessibility
1573
+ : undefined;
1574
+ const accessibilitySnapshotMethod = getObjectMethod(accessibilityValue, "snapshot");
1575
+ if (typeof accessibilitySnapshotMethod === "function") {
1576
+ const snapshot = await accessibilitySnapshotMethod.call(accessibilityValue, {});
1577
+ return {
1578
+ ok: true,
1579
+ executed: true,
1580
+ data: {
1581
+ selector,
1582
+ found: true,
1583
+ snapshot
1584
+ }
1585
+ };
1586
+ }
1587
+
1588
+ const fallbackSnapshot = await evaluateInPage<Record<string, unknown>>(
1589
+ tab.page,
1590
+ (rawArg) => {
1591
+ const selectorValue =
1592
+ rawArg !== null &&
1593
+ typeof rawArg === "object" &&
1594
+ typeof (rawArg as { selector?: unknown }).selector === "string"
1595
+ ? (rawArg as { selector: string }).selector
1596
+ : "";
1597
+ const root =
1598
+ selectorValue.length > 0
1599
+ ? document.querySelector(selectorValue)
1600
+ : document.body ?? document.documentElement;
1601
+ if (root === null) {
1602
+ return {
1603
+ selector: selectorValue,
1604
+ found: false
1605
+ };
1606
+ }
1607
+
1608
+ const build = (element: Element, depth: number): Record<string, unknown> => {
1609
+ const role =
1610
+ element.getAttribute("role") ??
1611
+ (element.tagName.toLowerCase() === "a"
1612
+ ? "link"
1613
+ : element.tagName.toLowerCase() === "button"
1614
+ ? "button"
1615
+ : element.tagName.toLowerCase() === "input"
1616
+ ? "textbox"
1617
+ : "generic");
1618
+ const name =
1619
+ element.getAttribute("aria-label") ??
1620
+ element.getAttribute("alt") ??
1621
+ (element.textContent || "").trim().slice(0, 120);
1622
+ if (depth >= 5) {
1623
+ return {
1624
+ role,
1625
+ ...(name.length > 0 ? { name } : {})
1626
+ };
1627
+ }
1628
+
1629
+ const children = Array.from(element.children)
1630
+ .slice(0, 30)
1631
+ .map((child) => build(child, depth + 1));
1632
+ return {
1633
+ role,
1634
+ ...(name.length > 0 ? { name } : {}),
1635
+ ...(children.length > 0 ? { children } : {})
1636
+ };
1637
+ };
1638
+
1639
+ return {
1640
+ selector: selectorValue,
1641
+ found: true,
1642
+ snapshot: build(root, 0)
1643
+ };
1644
+ },
1645
+ { selector: selector ?? "" }
1646
+ );
1647
+
1648
+ return {
1649
+ ok: true,
1650
+ executed: true,
1651
+ data: isObjectRecord(fallbackSnapshot)
1652
+ ? fallbackSnapshot
1653
+ : {
1654
+ selector,
1655
+ found: false
1656
+ }
1657
+ };
1658
+ }
1659
+ case "cookieGet": {
1660
+ const cookieName = readActionPayloadString(payload, "name");
1661
+ const customCookieGetMethod = getObjectMethod(tab.page, "cookieGet");
1662
+ if (typeof customCookieGetMethod === "function") {
1663
+ const customResult = await customCookieGetMethod.call(tab.page, cookieName);
1664
+ return {
1665
+ ok: true,
1666
+ executed: true,
1667
+ data: isObjectRecord(customResult)
1668
+ ? customResult
1669
+ : {
1670
+ cookies: []
1671
+ }
1672
+ };
1673
+ }
1674
+
1675
+ const contextMethod = getObjectMethod(tab.page, "context");
1676
+ if (typeof contextMethod !== "function") {
1677
+ return {
1678
+ ok: true,
1679
+ executed: false
1680
+ };
1681
+ }
1682
+
1683
+ const context = await contextMethod.call(tab.page);
1684
+ const cookiesMethod = getObjectMethod(context, "cookies");
1685
+ if (typeof cookiesMethod !== "function") {
1686
+ return {
1687
+ ok: true,
1688
+ executed: false
1689
+ };
1690
+ }
1691
+
1692
+ let rawCookies: unknown;
1693
+ try {
1694
+ rawCookies = await cookiesMethod.call(context, [tab.page.url()]);
1695
+ } catch {
1696
+ rawCookies = await cookiesMethod.call(context);
1697
+ }
1698
+ const cookies = normalizeCookies(rawCookies).filter(
1699
+ (cookie) => cookieName === undefined || cookie.name === cookieName
1700
+ );
1701
+ return {
1702
+ ok: true,
1703
+ executed: true,
1704
+ data: {
1705
+ cookies,
1706
+ ...(cookieName !== undefined ? { name: cookieName } : {})
1707
+ }
1708
+ };
1709
+ }
1710
+ case "cookieSet": {
1711
+ const cookieName = readActionPayloadString(payload, "name");
1712
+ const cookieValue = readActionPayloadString(payload, "value");
1713
+ const cookieUrl = readActionPayloadString(payload, "url") ?? tab.page.url();
1714
+ if (cookieName === undefined || cookieValue === undefined) {
1715
+ return {
1716
+ ok: false,
1717
+ executed: false,
1718
+ error: "action.payload.name and action.payload.value are required for cookieSet action."
1719
+ };
1720
+ }
1721
+
1722
+ const customCookieSetMethod = getObjectMethod(tab.page, "cookieSet");
1723
+ if (typeof customCookieSetMethod === "function") {
1724
+ const customResult = await customCookieSetMethod.call(tab.page, {
1725
+ name: cookieName,
1726
+ value: cookieValue,
1727
+ url: cookieUrl
1728
+ });
1729
+ return {
1730
+ ok: true,
1731
+ executed: true,
1732
+ data: isObjectRecord(customResult)
1733
+ ? customResult
1734
+ : {
1735
+ set: true,
1736
+ cookie: {
1737
+ name: cookieName,
1738
+ value: cookieValue,
1739
+ url: cookieUrl
1740
+ }
1741
+ }
1742
+ };
1743
+ }
1744
+
1745
+ const contextMethod = getObjectMethod(tab.page, "context");
1746
+ if (typeof contextMethod !== "function") {
1747
+ return {
1748
+ ok: true,
1749
+ executed: false
1750
+ };
1751
+ }
1752
+
1753
+ const context = await contextMethod.call(tab.page);
1754
+ const addCookiesMethod = getObjectMethod(context, "addCookies");
1755
+ if (typeof addCookiesMethod !== "function") {
1756
+ return {
1757
+ ok: true,
1758
+ executed: false
1759
+ };
1760
+ }
1761
+
1762
+ await addCookiesMethod.call(context, [
1763
+ {
1764
+ name: cookieName,
1765
+ value: cookieValue,
1766
+ url: cookieUrl
1767
+ }
1768
+ ]);
1769
+
1770
+ return {
1771
+ ok: true,
1772
+ executed: true,
1773
+ data: {
1774
+ set: true,
1775
+ cookie: {
1776
+ name: cookieName,
1777
+ value: cookieValue,
1778
+ url: cookieUrl
1779
+ }
1780
+ }
1781
+ };
1782
+ }
1783
+ case "cookieClear": {
1784
+ const cookieName = readActionPayloadString(payload, "name");
1785
+ const customCookieClearMethod = getObjectMethod(tab.page, "cookieClear");
1786
+ if (typeof customCookieClearMethod === "function") {
1787
+ const customResult = await customCookieClearMethod.call(tab.page, cookieName);
1788
+ return {
1789
+ ok: true,
1790
+ executed: true,
1791
+ data: isObjectRecord(customResult)
1792
+ ? customResult
1793
+ : {
1794
+ cleared: true
1795
+ }
1796
+ };
1797
+ }
1798
+
1799
+ const contextMethod = getObjectMethod(tab.page, "context");
1800
+ if (typeof contextMethod !== "function") {
1801
+ return {
1802
+ ok: true,
1803
+ executed: false
1804
+ };
1805
+ }
1806
+
1807
+ const context = await contextMethod.call(tab.page);
1808
+ const clearCookiesMethod = getObjectMethod(context, "clearCookies");
1809
+ if (typeof clearCookiesMethod !== "function") {
1810
+ return {
1811
+ ok: true,
1812
+ executed: false
1813
+ };
1814
+ }
1815
+
1816
+ if (cookieName === undefined) {
1817
+ await clearCookiesMethod.call(context);
1818
+ return {
1819
+ ok: true,
1820
+ executed: true,
1821
+ data: {
1822
+ cleared: true,
1823
+ count: -1
1824
+ }
1825
+ };
1826
+ }
1827
+
1828
+ const cookiesMethod = getObjectMethod(context, "cookies");
1829
+ const addCookiesMethod = getObjectMethod(context, "addCookies");
1830
+ if (typeof cookiesMethod !== "function" || typeof addCookiesMethod !== "function") {
1831
+ return {
1832
+ ok: true,
1833
+ executed: false
1834
+ };
1835
+ }
1836
+
1837
+ let rawCookies: unknown;
1838
+ try {
1839
+ rawCookies = await cookiesMethod.call(context, [tab.page.url()]);
1840
+ } catch {
1841
+ rawCookies = await cookiesMethod.call(context);
1842
+ }
1843
+ const currentCookies = normalizeCookies(rawCookies);
1844
+ const keptCookieInputs = currentCookies
1845
+ .filter((cookie) => cookie.name !== cookieName)
1846
+ .map((cookie) => toCookieInput(cookie))
1847
+ .filter((cookie): cookie is Record<string, unknown> => cookie !== undefined);
1848
+ const removedCount = currentCookies.length - keptCookieInputs.length;
1849
+
1850
+ await clearCookiesMethod.call(context);
1851
+ if (keptCookieInputs.length > 0) {
1852
+ await addCookiesMethod.call(context, keptCookieInputs);
1853
+ }
1854
+
1855
+ return {
1856
+ ok: true,
1857
+ executed: true,
1858
+ data: {
1859
+ cleared: true,
1860
+ name: cookieName,
1861
+ count: removedCount
1862
+ }
1863
+ };
1864
+ }
1865
+ case "storageGet": {
1866
+ const scope = parseStorageScope(readActionPayloadString(payload, "scope"));
1867
+ const key = readActionPayloadString(payload, "key");
1868
+ if (scope === undefined || key === undefined) {
1869
+ return {
1870
+ ok: false,
1871
+ executed: false,
1872
+ error: "action.payload.scope and action.payload.key are required for storageGet action."
1873
+ };
1874
+ }
1875
+
1876
+ const customStorageGetMethod = getObjectMethod(tab.page, "storageGet");
1877
+ if (typeof customStorageGetMethod === "function") {
1878
+ const customResult = await customStorageGetMethod.call(tab.page, scope, key);
1879
+ return {
1880
+ ok: true,
1881
+ executed: true,
1882
+ data: isObjectRecord(customResult)
1883
+ ? customResult
1884
+ : {
1885
+ scope,
1886
+ key,
1887
+ exists: false
1888
+ }
1889
+ };
1890
+ }
1891
+
1892
+ const result = await evaluateInPage<Record<string, unknown>>(
1893
+ tab.page,
1894
+ (rawArg) => {
1895
+ const scopeValue =
1896
+ rawArg !== null &&
1897
+ typeof rawArg === "object" &&
1898
+ typeof (rawArg as { scope?: unknown }).scope === "string"
1899
+ ? (rawArg as { scope: string }).scope
1900
+ : "local";
1901
+ const keyValue =
1902
+ rawArg !== null &&
1903
+ typeof rawArg === "object" &&
1904
+ typeof (rawArg as { key?: unknown }).key === "string"
1905
+ ? (rawArg as { key: string }).key
1906
+ : "";
1907
+ const storage = scopeValue === "session" ? window.sessionStorage : window.localStorage;
1908
+ const value = storage.getItem(keyValue);
1909
+ return {
1910
+ scope: scopeValue,
1911
+ key: keyValue,
1912
+ exists: value !== null,
1913
+ ...(value !== null ? { value } : {})
1914
+ };
1915
+ },
1916
+ { scope, key }
1917
+ );
1918
+
1919
+ return {
1920
+ ok: true,
1921
+ executed: true,
1922
+ data: isObjectRecord(result)
1923
+ ? result
1924
+ : {
1925
+ scope,
1926
+ key,
1927
+ exists: false
1928
+ }
1929
+ };
1930
+ }
1931
+ case "storageSet": {
1932
+ const scope = parseStorageScope(readActionPayloadString(payload, "scope"));
1933
+ const key = readActionPayloadString(payload, "key");
1934
+ const value = readActionPayloadString(payload, "value");
1935
+ if (scope === undefined || key === undefined || value === undefined) {
1936
+ return {
1937
+ ok: false,
1938
+ executed: false,
1939
+ error: "action.payload.scope, action.payload.key and action.payload.value are required for storageSet action."
1940
+ };
1941
+ }
1942
+
1943
+ const customStorageSetMethod = getObjectMethod(tab.page, "storageSet");
1944
+ if (typeof customStorageSetMethod === "function") {
1945
+ const customResult = await customStorageSetMethod.call(tab.page, scope, key, value);
1946
+ return {
1947
+ ok: true,
1948
+ executed: true,
1949
+ data: isObjectRecord(customResult)
1950
+ ? customResult
1951
+ : {
1952
+ scope,
1953
+ key,
1954
+ value,
1955
+ set: true
1956
+ }
1957
+ };
1958
+ }
1959
+
1960
+ await evaluateInPage(
1961
+ tab.page,
1962
+ (rawArg) => {
1963
+ const scopeValue =
1964
+ rawArg !== null &&
1965
+ typeof rawArg === "object" &&
1966
+ typeof (rawArg as { scope?: unknown }).scope === "string"
1967
+ ? (rawArg as { scope: string }).scope
1968
+ : "local";
1969
+ const keyValue =
1970
+ rawArg !== null &&
1971
+ typeof rawArg === "object" &&
1972
+ typeof (rawArg as { key?: unknown }).key === "string"
1973
+ ? (rawArg as { key: string }).key
1974
+ : "";
1975
+ const valueText =
1976
+ rawArg !== null &&
1977
+ typeof rawArg === "object" &&
1978
+ typeof (rawArg as { value?: unknown }).value === "string"
1979
+ ? (rawArg as { value: string }).value
1980
+ : "";
1981
+ const storage = scopeValue === "session" ? window.sessionStorage : window.localStorage;
1982
+ storage.setItem(keyValue, valueText);
1983
+ },
1984
+ { scope, key, value }
1985
+ );
1986
+
1987
+ return {
1988
+ ok: true,
1989
+ executed: true,
1990
+ data: {
1991
+ scope,
1992
+ key,
1993
+ value,
1994
+ set: true
1995
+ }
1996
+ };
1997
+ }
1998
+ case "frameList": {
1999
+ const customFrameListMethod = getObjectMethod(tab.page, "frameList");
2000
+ if (typeof customFrameListMethod === "function") {
2001
+ const customResult = await customFrameListMethod.call(tab.page);
2002
+ return {
2003
+ ok: true,
2004
+ executed: true,
2005
+ data: isObjectRecord(customResult)
2006
+ ? customResult
2007
+ : {
2008
+ frames: []
2009
+ }
2010
+ };
2011
+ }
2012
+
2013
+ const framesMethod = getObjectMethod(tab.page, "frames");
2014
+ if (typeof framesMethod !== "function") {
2015
+ return {
2016
+ ok: true,
2017
+ executed: false
2018
+ };
2019
+ }
2020
+
2021
+ const rawFrames = await framesMethod.call(tab.page);
2022
+ if (!Array.isArray(rawFrames)) {
2023
+ return {
2024
+ ok: true,
2025
+ executed: true,
2026
+ data: {
2027
+ frames: []
2028
+ }
2029
+ };
2030
+ }
2031
+
2032
+ const mainFrameMethod = getObjectMethod(tab.page, "mainFrame");
2033
+ const mainFrame = typeof mainFrameMethod === "function" ? await mainFrameMethod.call(tab.page) : undefined;
2034
+ const frames = rawFrames.map((frame, index) => {
2035
+ const name = readObjectMethod<string>(frame, "name");
2036
+ const url = readObjectMethod<string>(frame, "url") ?? "";
2037
+ return {
2038
+ frameId: `frame:${index}`,
2039
+ index,
2040
+ ...(typeof name === "string" && name.length > 0 ? { name } : {}),
2041
+ url,
2042
+ isMainFrame: frame === mainFrame
2043
+ };
2044
+ });
2045
+
2046
+ return {
2047
+ ok: true,
2048
+ executed: true,
2049
+ data: {
2050
+ frames
2051
+ }
2052
+ };
2053
+ }
2054
+ case "frameSnapshot": {
2055
+ const frameId = readActionPayloadString(payload, "frameId");
2056
+ if (frameId === undefined) {
2057
+ return {
2058
+ ok: false,
2059
+ executed: false,
2060
+ error: "action.payload.frameId is required for frameSnapshot action."
2061
+ };
2062
+ }
2063
+
2064
+ const customFrameSnapshotMethod = getObjectMethod(tab.page, "frameSnapshot");
2065
+ if (typeof customFrameSnapshotMethod === "function") {
2066
+ const customResult = await customFrameSnapshotMethod.call(tab.page, frameId);
2067
+ return {
2068
+ ok: true,
2069
+ executed: true,
2070
+ data: isObjectRecord(customResult)
2071
+ ? customResult
2072
+ : {
2073
+ frameId,
2074
+ found: false
2075
+ }
2076
+ };
2077
+ }
2078
+
2079
+ const frameIndex = parseFrameIndex(frameId);
2080
+ if (frameIndex === undefined) {
2081
+ return {
2082
+ ok: false,
2083
+ executed: false,
2084
+ error: `Invalid frameId: ${frameId}`
2085
+ };
2086
+ }
2087
+
2088
+ const framesMethod = getObjectMethod(tab.page, "frames");
2089
+ if (typeof framesMethod !== "function") {
2090
+ return {
2091
+ ok: true,
2092
+ executed: false
2093
+ };
2094
+ }
2095
+
2096
+ const rawFrames = await framesMethod.call(tab.page);
2097
+ if (!Array.isArray(rawFrames) || frameIndex >= rawFrames.length) {
2098
+ return {
2099
+ ok: true,
2100
+ executed: true,
2101
+ data: {
2102
+ frameId,
2103
+ found: false
2104
+ }
2105
+ };
2106
+ }
2107
+
2108
+ const frame = rawFrames[frameIndex];
2109
+ const contentMethod = getObjectMethod(frame, "content");
2110
+ if (typeof contentMethod !== "function") {
2111
+ return {
2112
+ ok: true,
2113
+ executed: false
2114
+ };
2115
+ }
2116
+
2117
+ const mainFrameMethod = getObjectMethod(tab.page, "mainFrame");
2118
+ const mainFrame = typeof mainFrameMethod === "function" ? await mainFrameMethod.call(tab.page) : undefined;
2119
+ const frameName = readObjectMethod<string>(frame, "name");
2120
+ const frameUrl = readObjectMethod<string>(frame, "url") ?? "";
2121
+ const frameHtml = await contentMethod.call(frame);
2122
+
2123
+ return {
2124
+ ok: true,
2125
+ executed: true,
2126
+ data: {
2127
+ frameId,
2128
+ index: frameIndex,
2129
+ found: true,
2130
+ ...(typeof frameName === "string" && frameName.length > 0 ? { name: frameName } : {}),
2131
+ url: frameUrl,
2132
+ isMainFrame: frame === mainFrame,
2133
+ html: typeof frameHtml === "string" ? frameHtml : ""
2134
+ }
2135
+ };
2136
+ }
2137
+ default:
2138
+ return {
2139
+ ok: true,
2140
+ executed: false
2141
+ };
2142
+ }
2143
+ };
2144
+
2145
+ try {
2146
+ return {
2147
+ ...baseResult,
2148
+ ...(await performAction())
2149
+ };
2150
+ } catch (error) {
2151
+ return {
2152
+ ...baseResult,
2153
+ ok: false,
2154
+ executed: false,
2155
+ error: toErrorMessage(error)
2156
+ };
2157
+ }
2158
+ },
2159
+ armUpload: async (targetId, files, profile) => {
2160
+ const profileId = resolveProfileId(profile);
2161
+ const operationState = getOrCreateTargetOperationState(profileId, targetId);
2162
+ operationState.uploadFiles = [...files];
2163
+ },
2164
+ armDialog: async (targetId, profile) => {
2165
+ const profileId = resolveProfileId(profile);
2166
+ const operationState = getOrCreateTargetOperationState(profileId, targetId);
2167
+ operationState.dialogArmedCount += 1;
2168
+ },
2169
+ waitDownload: async (targetId, profile) => {
2170
+ const profileId = resolveProfileId(profile);
2171
+ const { tab } = requireTargetInProfile(profileId, targetId);
2172
+ const operationState = getOrCreateTargetOperationState(profileId, targetId);
2173
+ const download = await ensureDownloadInFlight(profileId, targetId, tab, operationState);
2174
+
2175
+ return {
2176
+ path: download.path,
2177
+ profile: profileId,
2178
+ targetId,
2179
+ relayUrl,
2180
+ uploadFiles: [...operationState.uploadFiles],
2181
+ dialogArmedCount: operationState.dialogArmedCount,
2182
+ triggerCount: operationState.triggerCount,
2183
+ ...(download.suggestedFilename !== undefined
2184
+ ? { suggestedFilename: download.suggestedFilename }
2185
+ : {}),
2186
+ ...(download.url !== undefined ? { url: download.url } : {}),
2187
+ ...(download.mimeType !== undefined ? { mimeType: download.mimeType } : {})
2188
+ };
2189
+ },
2190
+ triggerDownload: async (targetId, profile) => {
2191
+ const profileId = resolveProfileId(profile);
2192
+ const { tab } = requireTargetInProfile(profileId, targetId);
2193
+ const operationState = getOrCreateTargetOperationState(profileId, targetId);
2194
+ operationState.triggerCount += 1;
2195
+ void ensureDownloadInFlight(profileId, targetId, tab, operationState);
2196
+ },
2197
+ getConsoleEntries: (targetId, profile) => {
2198
+ const telemetryState = findTelemetryState(targetId, profile);
2199
+ return telemetryState === undefined ? [] : telemetryState.consoleEntries.map(cloneConsoleEntry);
2200
+ },
2201
+ getNetworkResponseBody: (requestId, targetId, profile) => {
2202
+ const body = findTelemetryState(targetId, profile)?.networkResponseBodies.get(requestId);
2203
+ return body === undefined ? undefined : { ...body };
2204
+ }
2205
+ };
2206
+ }