@flrande/browserctl 0.1.0-dev.7.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README-CN.md +66 -0
  3. package/README.md +66 -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-CN.md +38 -0
  51. package/extensions/chrome-relay/README.md +38 -0
  52. package/extensions/chrome-relay/background.js +1687 -0
  53. package/extensions/chrome-relay/manifest.json +15 -0
  54. package/extensions/chrome-relay/popup.html +369 -0
  55. package/extensions/chrome-relay/popup.js +972 -0
  56. package/package.json +51 -0
  57. package/packages/core/src/bootstrap.test.ts +10 -0
  58. package/packages/core/src/driver-registry.test.ts +45 -0
  59. package/packages/core/src/driver-registry.ts +22 -0
  60. package/packages/core/src/driver.ts +47 -0
  61. package/packages/core/src/index.ts +5 -0
  62. package/packages/core/src/ref-cache.test.ts +61 -0
  63. package/packages/core/src/ref-cache.ts +28 -0
  64. package/packages/core/src/session-store.test.ts +49 -0
  65. package/packages/core/src/session-store.ts +33 -0
  66. package/packages/core/src/types.ts +9 -0
  67. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +634 -0
  68. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +2206 -0
  69. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +264 -0
  70. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +521 -0
  71. package/packages/driver-chrome-relay/src/index.ts +26 -0
  72. package/packages/driver-managed/src/index.ts +22 -0
  73. package/packages/driver-managed/src/managed-driver.test.ts +59 -0
  74. package/packages/driver-managed/src/managed-driver.ts +125 -0
  75. package/packages/driver-managed/src/managed-local-driver.test.ts +506 -0
  76. package/packages/driver-managed/src/managed-local-driver.ts +2021 -0
  77. package/packages/driver-remote-cdp/src/index.ts +19 -0
  78. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +617 -0
  79. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +2042 -0
  80. package/packages/protocol/src/envelope.test.ts +25 -0
  81. package/packages/protocol/src/envelope.ts +31 -0
  82. package/packages/protocol/src/errors.test.ts +17 -0
  83. package/packages/protocol/src/errors.ts +11 -0
  84. package/packages/protocol/src/index.ts +3 -0
  85. package/packages/protocol/src/tools.ts +3 -0
  86. package/packages/transport-mcp-stdio/src/index.ts +3 -0
  87. package/packages/transport-mcp-stdio/src/sdk-server.ts +139 -0
  88. package/packages/transport-mcp-stdio/src/server.test.ts +281 -0
  89. package/packages/transport-mcp-stdio/src/server.ts +183 -0
  90. package/packages/transport-mcp-stdio/src/tool-map.ts +67 -0
  91. package/scripts/smoke.ps1 +127 -0
@@ -0,0 +1,253 @@
1
+ import { createServer, type AddressInfo } from "node:net";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
5
+
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+
8
+ import { DAEMON_STARTUP_ARGUMENT } from "./commands/common";
9
+ import { callDaemonTool, getDaemonStatus, stopDaemon } from "./daemon-client";
10
+
11
+ type CapturedRequest = {
12
+ id: string;
13
+ name: string;
14
+ traceId: string;
15
+ arguments: Record<string, unknown>;
16
+ };
17
+
18
+ const ORIGINAL_DAEMON_PORT = process.env.BROWSERCTL_DAEMON_PORT;
19
+ const ORIGINAL_AUTH_TOKEN = process.env.BROWSERCTL_AUTH_TOKEN;
20
+ const TEST_PID_PORTS = new Set<number>();
21
+
22
+ function resolveRuntimeDirForTests(): string {
23
+ const currentFile = fileURLToPath(import.meta.url);
24
+ return resolve(dirname(currentFile), "../../../.browserctl-runtime");
25
+ }
26
+
27
+ function resolvePidFileForTests(port: number): string {
28
+ return join(resolveRuntimeDirForTests(), `daemon-${port}.pid`);
29
+ }
30
+
31
+ function writePidRecordForTests(port: number, record: number | { pid: number; authToken?: string }): void {
32
+ mkdirSync(resolveRuntimeDirForTests(), { recursive: true });
33
+ writeFileSync(
34
+ resolvePidFileForTests(port),
35
+ typeof record === "number" ? String(record) : JSON.stringify(record),
36
+ { encoding: "utf8" }
37
+ );
38
+ TEST_PID_PORTS.add(port);
39
+ }
40
+
41
+ afterEach(() => {
42
+ if (ORIGINAL_DAEMON_PORT === undefined) {
43
+ delete process.env.BROWSERCTL_DAEMON_PORT;
44
+ } else {
45
+ process.env.BROWSERCTL_DAEMON_PORT = ORIGINAL_DAEMON_PORT;
46
+ }
47
+
48
+ if (ORIGINAL_AUTH_TOKEN === undefined) {
49
+ delete process.env.BROWSERCTL_AUTH_TOKEN;
50
+ } else {
51
+ process.env.BROWSERCTL_AUTH_TOKEN = ORIGINAL_AUTH_TOKEN;
52
+ }
53
+
54
+ for (const port of TEST_PID_PORTS) {
55
+ rmSync(resolvePidFileForTests(port), { force: true });
56
+ }
57
+ TEST_PID_PORTS.clear();
58
+ vi.restoreAllMocks();
59
+ });
60
+
61
+ async function withDaemonHarness(
62
+ run: (state: { port: number; requests: CapturedRequest[] }) => Promise<void>
63
+ ): Promise<void> {
64
+ const requests: CapturedRequest[] = [];
65
+ const server = createServer((socket) => {
66
+ socket.setEncoding("utf8");
67
+ let buffer = "";
68
+
69
+ socket.on("data", (chunk: string) => {
70
+ buffer += chunk;
71
+
72
+ let lineBreakIndex = buffer.indexOf("\n");
73
+ while (lineBreakIndex >= 0) {
74
+ const line = buffer.slice(0, lineBreakIndex).trim();
75
+ buffer = buffer.slice(lineBreakIndex + 1);
76
+
77
+ if (line.length === 0) {
78
+ lineBreakIndex = buffer.indexOf("\n");
79
+ continue;
80
+ }
81
+
82
+ const request = JSON.parse(line) as CapturedRequest;
83
+ requests.push(request);
84
+ socket.write(
85
+ `${JSON.stringify({
86
+ id: request.id,
87
+ ok: true,
88
+ traceId: request.traceId,
89
+ sessionId:
90
+ typeof request.arguments.sessionId === "string"
91
+ ? request.arguments.sessionId
92
+ : "cli:test",
93
+ data: {
94
+ ok: true
95
+ }
96
+ })}\n`
97
+ );
98
+
99
+ lineBreakIndex = buffer.indexOf("\n");
100
+ }
101
+ });
102
+ });
103
+
104
+ await new Promise<void>((resolve, reject) => {
105
+ server.once("error", reject);
106
+ server.listen(0, "127.0.0.1", () => {
107
+ server.off("error", reject);
108
+ resolve();
109
+ });
110
+ });
111
+
112
+ const address = server.address() as AddressInfo;
113
+
114
+ try {
115
+ await run({ port: address.port, requests });
116
+ } finally {
117
+ await new Promise<void>((resolve, reject) => {
118
+ server.close((error) => {
119
+ if (error !== undefined) {
120
+ reject(error);
121
+ return;
122
+ }
123
+
124
+ resolve();
125
+ });
126
+ });
127
+ }
128
+ }
129
+
130
+ describe("daemon client auth token forwarding", () => {
131
+ it("includes env token in daemon status probes", async () => {
132
+ await withDaemonHarness(async ({ port, requests }) => {
133
+ process.env.BROWSERCTL_DAEMON_PORT = String(port);
134
+ process.env.BROWSERCTL_AUTH_TOKEN = "env-token";
135
+
136
+ const status = await getDaemonStatus();
137
+
138
+ expect(status.running).toBe(true);
139
+ expect(requests).toHaveLength(1);
140
+ expect(requests[0]).toMatchObject({
141
+ name: "browser.status",
142
+ arguments: {
143
+ sessionId: "cli:daemon-status",
144
+ authToken: "env-token"
145
+ }
146
+ });
147
+ });
148
+ });
149
+
150
+ it("includes env token in tool calls when not explicitly provided", async () => {
151
+ await withDaemonHarness(async ({ port, requests }) => {
152
+ process.env.BROWSERCTL_DAEMON_PORT = String(port);
153
+ process.env.BROWSERCTL_AUTH_TOKEN = "env-token";
154
+
155
+ await callDaemonTool("browser.tab.list", {
156
+ sessionId: "cli:test"
157
+ });
158
+
159
+ expect(requests).toHaveLength(1);
160
+ expect(requests[0]).toMatchObject({
161
+ name: "browser.tab.list",
162
+ arguments: {
163
+ sessionId: "cli:test",
164
+ authToken: "env-token"
165
+ }
166
+ });
167
+ });
168
+ });
169
+
170
+ it("preserves explicit authToken in tool calls", async () => {
171
+ await withDaemonHarness(async ({ port, requests }) => {
172
+ process.env.BROWSERCTL_DAEMON_PORT = String(port);
173
+ process.env.BROWSERCTL_AUTH_TOKEN = "env-token";
174
+
175
+ await callDaemonTool("browser.tab.list", {
176
+ sessionId: "cli:test",
177
+ authToken: "cli-token"
178
+ });
179
+
180
+ expect(requests).toHaveLength(1);
181
+ expect(requests[0]).toMatchObject({
182
+ name: "browser.tab.list",
183
+ arguments: {
184
+ sessionId: "cli:test",
185
+ authToken: "cli-token"
186
+ }
187
+ });
188
+ });
189
+ });
190
+
191
+ it("does not forward internal daemon startup metadata as tool arguments", async () => {
192
+ await withDaemonHarness(async ({ port, requests }) => {
193
+ process.env.BROWSERCTL_DAEMON_PORT = String(port);
194
+
195
+ await callDaemonTool("browser.tab.list", {
196
+ sessionId: "cli:test",
197
+ [DAEMON_STARTUP_ARGUMENT]: {
198
+ managedLocal: {
199
+ browserName: "chromium",
200
+ channel: "msedge"
201
+ }
202
+ }
203
+ });
204
+
205
+ expect(requests).toHaveLength(1);
206
+ expect(requests[0].arguments).toMatchObject({
207
+ sessionId: "cli:test"
208
+ });
209
+ expect(requests[0].arguments).not.toHaveProperty(DAEMON_STARTUP_ARGUMENT);
210
+ });
211
+ });
212
+
213
+ it("reuses persisted daemon auth token when env token is missing", async () => {
214
+ await withDaemonHarness(async ({ port, requests }) => {
215
+ process.env.BROWSERCTL_DAEMON_PORT = String(port);
216
+ delete process.env.BROWSERCTL_AUTH_TOKEN;
217
+ writePidRecordForTests(port, {
218
+ pid: 12345,
219
+ authToken: "persisted-token"
220
+ });
221
+
222
+ const status = await getDaemonStatus();
223
+
224
+ expect(status.running).toBe(true);
225
+ expect(requests).toHaveLength(1);
226
+ expect(requests[0]).toMatchObject({
227
+ name: "browser.status",
228
+ arguments: {
229
+ sessionId: "cli:daemon-status",
230
+ authToken: "persisted-token"
231
+ }
232
+ });
233
+ });
234
+ });
235
+
236
+ it("avoids killing stale pid records when daemon cannot be verified", async () => {
237
+ const unreachablePort = 45999;
238
+ writePidRecordForTests(unreachablePort, {
239
+ pid: 70001,
240
+ authToken: "persisted-token"
241
+ });
242
+
243
+ const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true);
244
+ const stopResult = await stopDaemon(unreachablePort);
245
+
246
+ expect(stopResult).toMatchObject({
247
+ stopped: false,
248
+ port: unreachablePort,
249
+ pid: 70001
250
+ });
251
+ expect(killSpy).not.toHaveBeenCalled();
252
+ });
253
+ });