@wingman-ai/gateway 0.3.0 → 0.3.2

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 (124) hide show
  1. package/README.md +8 -0
  2. package/dist/agent/config/agentConfig.cjs +12 -0
  3. package/dist/agent/config/agentConfig.d.ts +22 -0
  4. package/dist/agent/config/agentConfig.js +10 -1
  5. package/dist/agent/config/agentLoader.cjs +9 -0
  6. package/dist/agent/config/agentLoader.js +9 -0
  7. package/dist/agent/config/toolRegistry.cjs +17 -0
  8. package/dist/agent/config/toolRegistry.d.ts +15 -0
  9. package/dist/agent/config/toolRegistry.js +17 -0
  10. package/dist/agent/tests/agentConfig.test.cjs +6 -1
  11. package/dist/agent/tests/agentConfig.test.js +6 -1
  12. package/dist/agent/tests/browserControlHelpers.test.cjs +35 -0
  13. package/dist/agent/tests/browserControlHelpers.test.d.ts +1 -0
  14. package/dist/agent/tests/browserControlHelpers.test.js +29 -0
  15. package/dist/agent/tests/browserControlTool.test.cjs +2117 -0
  16. package/dist/agent/tests/browserControlTool.test.d.ts +1 -0
  17. package/dist/agent/tests/browserControlTool.test.js +2111 -0
  18. package/dist/agent/tests/internet_search.test.cjs +22 -28
  19. package/dist/agent/tests/internet_search.test.js +22 -28
  20. package/dist/agent/tests/toolRegistry.test.cjs +6 -0
  21. package/dist/agent/tests/toolRegistry.test.js +6 -0
  22. package/dist/agent/tools/browser_control.cjs +1282 -0
  23. package/dist/agent/tools/browser_control.d.ts +478 -0
  24. package/dist/agent/tools/browser_control.js +1242 -0
  25. package/dist/agent/tools/internet_search.cjs +9 -5
  26. package/dist/agent/tools/internet_search.js +9 -5
  27. package/dist/cli/commands/agent.cjs +16 -2
  28. package/dist/cli/commands/agent.js +16 -2
  29. package/dist/cli/commands/browser.cjs +603 -0
  30. package/dist/cli/commands/browser.d.ts +13 -0
  31. package/dist/cli/commands/browser.js +566 -0
  32. package/dist/cli/commands/gateway.cjs +18 -7
  33. package/dist/cli/commands/gateway.d.ts +5 -1
  34. package/dist/cli/commands/gateway.js +18 -7
  35. package/dist/cli/commands/init.cjs +134 -45
  36. package/dist/cli/commands/init.js +134 -45
  37. package/dist/cli/commands/skill.cjs +3 -2
  38. package/dist/cli/commands/skill.js +3 -2
  39. package/dist/cli/config/loader.cjs +15 -0
  40. package/dist/cli/config/loader.js +15 -0
  41. package/dist/cli/config/schema.cjs +51 -2
  42. package/dist/cli/config/schema.d.ts +49 -0
  43. package/dist/cli/config/schema.js +44 -1
  44. package/dist/cli/core/workspace.cjs +89 -0
  45. package/dist/cli/core/workspace.d.ts +1 -0
  46. package/dist/cli/core/workspace.js +55 -0
  47. package/dist/cli/index.cjs +53 -5
  48. package/dist/cli/index.js +53 -5
  49. package/dist/cli/types/browser.cjs +18 -0
  50. package/dist/cli/types/browser.d.ts +9 -0
  51. package/dist/cli/types/browser.js +0 -0
  52. package/dist/gateway/browserRelayServer.cjs +338 -0
  53. package/dist/gateway/browserRelayServer.d.ts +38 -0
  54. package/dist/gateway/browserRelayServer.js +301 -0
  55. package/dist/gateway/http/agents.cjs +22 -0
  56. package/dist/gateway/http/agents.js +22 -0
  57. package/dist/gateway/http/fs.cjs +57 -0
  58. package/dist/gateway/http/fs.js +58 -1
  59. package/dist/gateway/server.cjs +43 -6
  60. package/dist/gateway/server.d.ts +4 -1
  61. package/dist/gateway/server.js +36 -5
  62. package/dist/gateway/transport/websocket.cjs +45 -10
  63. package/dist/gateway/transport/websocket.d.ts +1 -0
  64. package/dist/gateway/transport/websocket.js +41 -9
  65. package/dist/gateway/types.d.ts +4 -0
  66. package/dist/tests/agents-api.test.cjs +52 -0
  67. package/dist/tests/agents-api.test.js +53 -1
  68. package/dist/tests/browser-command.test.cjs +264 -0
  69. package/dist/tests/browser-command.test.d.ts +1 -0
  70. package/dist/tests/browser-command.test.js +258 -0
  71. package/dist/tests/browser-relay-server.test.cjs +20 -0
  72. package/dist/tests/browser-relay-server.test.d.ts +1 -0
  73. package/dist/tests/browser-relay-server.test.js +14 -0
  74. package/dist/tests/cli-config-loader.test.cjs +43 -0
  75. package/dist/tests/cli-config-loader.test.js +43 -0
  76. package/dist/tests/cli-init.test.cjs +25 -2
  77. package/dist/tests/cli-init.test.js +25 -2
  78. package/dist/tests/cli-workspace-root.test.cjs +114 -0
  79. package/dist/tests/cli-workspace-root.test.d.ts +1 -0
  80. package/dist/tests/cli-workspace-root.test.js +108 -0
  81. package/dist/tests/fs-api.test.cjs +138 -0
  82. package/dist/tests/fs-api.test.d.ts +1 -0
  83. package/dist/tests/fs-api.test.js +132 -0
  84. package/dist/tests/gateway-command-workspace.test.cjs +150 -0
  85. package/dist/tests/gateway-command-workspace.test.d.ts +1 -0
  86. package/dist/tests/gateway-command-workspace.test.js +144 -0
  87. package/dist/tests/gateway-request-execution-overrides.test.cjs +42 -0
  88. package/dist/tests/gateway-request-execution-overrides.test.d.ts +1 -0
  89. package/dist/tests/gateway-request-execution-overrides.test.js +36 -0
  90. package/dist/tests/gateway.test.cjs +31 -0
  91. package/dist/tests/gateway.test.js +31 -0
  92. package/dist/tests/websocket-transport.test.cjs +31 -0
  93. package/dist/tests/websocket-transport.test.d.ts +1 -0
  94. package/dist/tests/websocket-transport.test.js +25 -0
  95. package/dist/webui/assets/index-Cwkg4DKj.css +11 -0
  96. package/dist/webui/assets/{index-0nUBsUUq.js → index-DHbfLOUR.js} +109 -107
  97. package/dist/webui/index.html +2 -2
  98. package/extensions/wingman-browser-extension/README.md +27 -0
  99. package/extensions/wingman-browser-extension/background.js +416 -0
  100. package/extensions/wingman-browser-extension/manifest.json +19 -0
  101. package/extensions/wingman-browser-extension/options.html +156 -0
  102. package/extensions/wingman-browser-extension/options.js +106 -0
  103. package/package.json +8 -8
  104. package/{.wingman → templates}/agents/README.md +2 -1
  105. package/{.wingman → templates}/agents/coding/agent.md +0 -1
  106. package/{.wingman → templates}/agents/coding-v2/agent.md +0 -1
  107. package/{.wingman → templates}/agents/game-dev/agent.md +8 -1
  108. package/{.wingman → templates}/agents/game-dev/art-generation.md +1 -0
  109. package/{.wingman → templates}/agents/main/agent.md +5 -0
  110. package/{.wingman → templates}/agents/researcher/agent.md +9 -0
  111. package/{.wingman → templates}/agents/stock-trader/agent.md +1 -0
  112. package/dist/webui/assets/index-kk7OrD-G.css +0 -11
  113. /package/{.wingman → templates}/agents/coding-v2/implementor.md +0 -0
  114. /package/{.wingman → templates}/agents/game-dev/asset-refinement.md +0 -0
  115. /package/{.wingman → templates}/agents/game-dev/planning-idea.md +0 -0
  116. /package/{.wingman → templates}/agents/game-dev/ui-specialist.md +0 -0
  117. /package/{.wingman → templates}/agents/stock-trader/chain-curator.md +0 -0
  118. /package/{.wingman → templates}/agents/stock-trader/goal-translator.md +0 -0
  119. /package/{.wingman → templates}/agents/stock-trader/guardrails-veto.md +0 -0
  120. /package/{.wingman → templates}/agents/stock-trader/path-planner.md +0 -0
  121. /package/{.wingman → templates}/agents/stock-trader/regime-analyst.md +0 -0
  122. /package/{.wingman → templates}/agents/stock-trader/risk.md +0 -0
  123. /package/{.wingman → templates}/agents/stock-trader/selection.md +0 -0
  124. /package/{.wingman → templates}/agents/stock-trader/strategy-composer.md +0 -0
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ BrowserRelayServer: ()=>BrowserRelayServer,
28
+ isLoopbackHost: ()=>isLoopbackHost
29
+ });
30
+ function _define_property(obj, key, value) {
31
+ if (key in obj) Object.defineProperty(obj, key, {
32
+ value: value,
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true
36
+ });
37
+ else obj[key] = value;
38
+ return obj;
39
+ }
40
+ const RELAY_INFO = {
41
+ product: "Wingman Browser Relay",
42
+ protocolVersion: "1.3",
43
+ userAgent: "WingmanRelay/0.2.0",
44
+ jsVersion: "wingman-relay"
45
+ };
46
+ const SESSION_OPTIONAL_METHODS = new Set([
47
+ "Browser.getVersion",
48
+ "Target.setDiscoverTargets",
49
+ "Target.setAutoAttach",
50
+ "Target.getTargets",
51
+ "Target.attachToTarget",
52
+ "Target.detachFromTarget",
53
+ "Target.activateTarget"
54
+ ]);
55
+ function isLoopbackHost(host) {
56
+ const normalized = host.trim().toLowerCase();
57
+ return "127.0.0.1" === normalized || "localhost" === normalized || "::1" === normalized;
58
+ }
59
+ function parseJsonMessage(raw) {
60
+ try {
61
+ return JSON.parse(raw);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function safeErrorMessage(error) {
67
+ return error instanceof Error ? error.message : String(error);
68
+ }
69
+ class BrowserRelayServer {
70
+ get running() {
71
+ return null !== this.server;
72
+ }
73
+ get wsEndpoint() {
74
+ return this.buildWsUrl("/cdp");
75
+ }
76
+ get healthEndpoint() {
77
+ return `http://${this.config.host}:${this.config.port}/health`;
78
+ }
79
+ buildWsUrl(pathname) {
80
+ const tokenParam = this.config.requireAuth ? `?token=${encodeURIComponent(this.config.authToken || "")}` : "";
81
+ return `ws://${this.config.host}:${this.config.port}${pathname}${tokenParam}`;
82
+ }
83
+ validateStartupConfig() {
84
+ if (!this.config.enabled) return;
85
+ if (!isLoopbackHost(this.config.host)) throw new Error(`Browser relay host must be loopback for security. Received "${this.config.host}".`);
86
+ if (this.config.requireAuth && !(this.config.authToken || "").trim()) throw new Error("Browser relay auth is enabled but no token is configured. Run `wingman browser extension pair`.");
87
+ }
88
+ start() {
89
+ if (!this.config.enabled || this.server) return;
90
+ this.validateStartupConfig();
91
+ this.server = Bun.serve({
92
+ hostname: this.config.host,
93
+ port: this.config.port,
94
+ fetch: (req, server)=>this.handleFetch(req, server),
95
+ websocket: {
96
+ open: (ws)=>this.handleOpen(ws),
97
+ message: (ws, message)=>this.handleMessage(ws, message),
98
+ close: (ws)=>this.handleClose(ws)
99
+ }
100
+ });
101
+ this.logger.info(`Browser relay started on ${this.config.host}:${this.config.port}`);
102
+ }
103
+ stop() {
104
+ if (!this.server) return;
105
+ this.server.stop();
106
+ this.server = null;
107
+ this.extensionSocket = null;
108
+ this.cdpSockets.clear();
109
+ this.latestSessionId = null;
110
+ this.sessionByTargetId.clear();
111
+ this.targetBySessionId.clear();
112
+ this.logger.info("Browser relay stopped");
113
+ }
114
+ handleFetch(req, server) {
115
+ const url = new URL(req.url);
116
+ const token = url.searchParams.get("token") || void 0;
117
+ if ("/" === url.pathname || "/health" === url.pathname) return new Response("ok");
118
+ if ("/json/version" === url.pathname) return Response.json({
119
+ Browser: RELAY_INFO.product,
120
+ "Protocol-Version": RELAY_INFO.protocolVersion,
121
+ "User-Agent": RELAY_INFO.userAgent,
122
+ "V8-Version": RELAY_INFO.jsVersion,
123
+ webSocketDebuggerUrl: this.buildWsUrl("/cdp")
124
+ });
125
+ if ("/extension" === url.pathname || "/cdp" === url.pathname) {
126
+ const kind = "/extension" === url.pathname ? "extension" : "cdp";
127
+ const upgraded = server.upgrade(req, {
128
+ data: {
129
+ kind,
130
+ token,
131
+ authenticated: false,
132
+ helloComplete: "cdp" === kind
133
+ }
134
+ });
135
+ if (!upgraded) return new Response("Relay upgrade failed", {
136
+ status: 400
137
+ });
138
+ return;
139
+ }
140
+ return new Response("Not Found", {
141
+ status: 404
142
+ });
143
+ }
144
+ authenticateSocket(socket) {
145
+ if (!this.config.requireAuth) {
146
+ socket.data.authenticated = true;
147
+ return true;
148
+ }
149
+ const configured = (this.config.authToken || "").trim();
150
+ if (!configured || socket.data.token !== configured) {
151
+ socket.close(4401, "Unauthorized");
152
+ return false;
153
+ }
154
+ socket.data.authenticated = true;
155
+ return true;
156
+ }
157
+ handleOpen(socket) {
158
+ if (!this.authenticateSocket(socket)) return;
159
+ if ("extension" === socket.data.kind) {
160
+ if (this.extensionSocket && this.extensionSocket !== socket) this.extensionSocket.close(4409, "Extension replaced");
161
+ this.extensionSocket = socket;
162
+ return;
163
+ }
164
+ this.cdpSockets.add(socket);
165
+ }
166
+ handleClose(socket) {
167
+ if ("extension" === socket.data.kind) {
168
+ if (this.extensionSocket === socket) this.extensionSocket = null;
169
+ this.latestSessionId = null;
170
+ this.sessionByTargetId.clear();
171
+ this.targetBySessionId.clear();
172
+ return;
173
+ }
174
+ this.cdpSockets.delete(socket);
175
+ }
176
+ handleMessage(socket, message) {
177
+ const raw = message.toString();
178
+ if (Buffer.byteLength(raw, "utf8") > this.config.maxMessageBytes) return void socket.close(1009, "Message too large");
179
+ const parsed = parseJsonMessage(raw);
180
+ if (!parsed || "object" != typeof parsed) return;
181
+ if ("extension" === socket.data.kind) return void this.handleExtensionMessage(socket, parsed);
182
+ this.handleCdpMessage(socket, parsed);
183
+ }
184
+ handleExtensionMessage(socket, message) {
185
+ if (!socket.data.helloComplete) {
186
+ const method = "string" == typeof message.method ? message.method : "";
187
+ const token = message.params?.token;
188
+ if ("hello" !== method) return void socket.close(4403, "Expected hello handshake");
189
+ if (this.config.requireAuth && ("string" != typeof token || token !== this.config.authToken)) return void socket.close(4401, "Invalid extension token");
190
+ socket.data.helloComplete = true;
191
+ this.sendJson(socket, {
192
+ method: "hello_ack",
193
+ params: {
194
+ ok: true
195
+ }
196
+ });
197
+ return;
198
+ }
199
+ if ("number" == typeof message.id && ("result" in message || "error" in message)) return void this.broadcastToCdp(message);
200
+ if ("forwardCDPEvent" === message.method && message.params) {
201
+ const event = message.params;
202
+ const method = "string" == typeof event.method ? event.method : "";
203
+ const params = event.params || {};
204
+ this.trackTargetSessions(method, params);
205
+ this.broadcastToCdp(event);
206
+ }
207
+ }
208
+ handleCdpMessage(socket, message) {
209
+ const id = message.id;
210
+ const method = message.method;
211
+ if ("number" != typeof id || "string" != typeof method) return;
212
+ if ("Browser.getVersion" === method) return void this.sendJson(socket, {
213
+ id,
214
+ result: {
215
+ product: RELAY_INFO.product,
216
+ protocolVersion: RELAY_INFO.protocolVersion,
217
+ userAgent: RELAY_INFO.userAgent,
218
+ jsVersion: RELAY_INFO.jsVersion,
219
+ revision: "wingman"
220
+ }
221
+ });
222
+ if ("Target.setDiscoverTargets" === method || "Target.setAutoAttach" === method) return void this.sendJson(socket, {
223
+ id,
224
+ result: {}
225
+ });
226
+ if ("Target.getTargets" === method) {
227
+ const targetInfos = Array.from(this.sessionByTargetId.keys()).map((targetId)=>({
228
+ targetId,
229
+ type: "page",
230
+ attached: true
231
+ }));
232
+ this.sendJson(socket, {
233
+ id,
234
+ result: {
235
+ targetInfos
236
+ }
237
+ });
238
+ return;
239
+ }
240
+ if ("Target.attachToTarget" === method) {
241
+ const targetId = String((message.params || {}).targetId || "");
242
+ const sessionId = this.sessionByTargetId.get(targetId) || this.latestSessionId;
243
+ if (!sessionId) return void this.sendJson(socket, {
244
+ id,
245
+ error: {
246
+ message: "No attached target is available"
247
+ }
248
+ });
249
+ this.sendJson(socket, {
250
+ id,
251
+ result: {
252
+ sessionId
253
+ }
254
+ });
255
+ return;
256
+ }
257
+ if ("Target.detachFromTarget" === method || "Target.activateTarget" === method) return void this.sendJson(socket, {
258
+ id,
259
+ result: {}
260
+ });
261
+ const extension = this.extensionSocket;
262
+ if (!extension || !extension.data.helloComplete) return void this.sendJson(socket, {
263
+ id,
264
+ error: {
265
+ message: "No extension is connected to browser relay"
266
+ }
267
+ });
268
+ const sessionId = "string" == typeof message.sessionId && message.sessionId || ("string" == typeof (message.params || {}).sessionId ? String((message.params || {}).sessionId) : void 0) || this.latestSessionId || void 0;
269
+ if (!sessionId && !SESSION_OPTIONAL_METHODS.has(method)) return void this.sendJson(socket, {
270
+ id,
271
+ error: {
272
+ message: `No active tab session for method "${method}"`
273
+ }
274
+ });
275
+ this.sendJson(extension, {
276
+ id,
277
+ method: "forwardCDPCommand",
278
+ params: {
279
+ sessionId,
280
+ method,
281
+ params: message.params || {}
282
+ }
283
+ });
284
+ }
285
+ trackTargetSessions(method, params) {
286
+ if ("Target.attachedToTarget" === method) {
287
+ const sessionId = "string" == typeof params.sessionId ? params.sessionId : null;
288
+ const targetId = "string" == typeof params.targetInfo?.targetId ? String(params.targetInfo?.targetId) : null;
289
+ if (sessionId) this.latestSessionId = sessionId;
290
+ if (sessionId && targetId) {
291
+ this.sessionByTargetId.set(targetId, sessionId);
292
+ this.targetBySessionId.set(sessionId, targetId);
293
+ }
294
+ return;
295
+ }
296
+ if ("Target.detachedFromTarget" === method) {
297
+ const sessionId = "string" == typeof params.sessionId ? params.sessionId : null;
298
+ if (!sessionId) return;
299
+ const targetId = this.targetBySessionId.get(sessionId);
300
+ if (targetId) {
301
+ this.sessionByTargetId.delete(targetId);
302
+ this.targetBySessionId.delete(sessionId);
303
+ }
304
+ if (this.latestSessionId === sessionId) this.latestSessionId = this.targetBySessionId.size > 0 ? Array.from(this.targetBySessionId.keys())[0] : null;
305
+ }
306
+ }
307
+ broadcastToCdp(payload) {
308
+ for (const socket of this.cdpSockets)this.sendJson(socket, payload);
309
+ }
310
+ sendJson(socket, payload) {
311
+ try {
312
+ socket.send(JSON.stringify(payload));
313
+ } catch (error) {
314
+ this.logger.debug(`Browser relay send failed: ${safeErrorMessage(error)}`);
315
+ }
316
+ }
317
+ constructor(config, logger){
318
+ _define_property(this, "config", void 0);
319
+ _define_property(this, "logger", void 0);
320
+ _define_property(this, "server", null);
321
+ _define_property(this, "extensionSocket", null);
322
+ _define_property(this, "cdpSockets", new Set());
323
+ _define_property(this, "latestSessionId", null);
324
+ _define_property(this, "sessionByTargetId", new Map());
325
+ _define_property(this, "targetBySessionId", new Map());
326
+ this.config = config;
327
+ this.logger = logger;
328
+ }
329
+ }
330
+ exports.BrowserRelayServer = __webpack_exports__.BrowserRelayServer;
331
+ exports.isLoopbackHost = __webpack_exports__.isLoopbackHost;
332
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
333
+ "BrowserRelayServer",
334
+ "isLoopbackHost"
335
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
336
+ Object.defineProperty(exports, '__esModule', {
337
+ value: true
338
+ });
@@ -0,0 +1,38 @@
1
+ import type { Logger } from "@/logger.js";
2
+ export interface BrowserRelayConfig {
3
+ enabled: boolean;
4
+ host: string;
5
+ port: number;
6
+ requireAuth: boolean;
7
+ authToken?: string;
8
+ maxMessageBytes: number;
9
+ }
10
+ export declare function isLoopbackHost(host: string): boolean;
11
+ export declare class BrowserRelayServer {
12
+ private readonly config;
13
+ private readonly logger;
14
+ private server;
15
+ private extensionSocket;
16
+ private cdpSockets;
17
+ private latestSessionId;
18
+ private sessionByTargetId;
19
+ private targetBySessionId;
20
+ constructor(config: BrowserRelayConfig, logger: Logger);
21
+ get running(): boolean;
22
+ get wsEndpoint(): string;
23
+ get healthEndpoint(): string;
24
+ private buildWsUrl;
25
+ private validateStartupConfig;
26
+ start(): void;
27
+ stop(): void;
28
+ private handleFetch;
29
+ private authenticateSocket;
30
+ private handleOpen;
31
+ private handleClose;
32
+ private handleMessage;
33
+ private handleExtensionMessage;
34
+ private handleCdpMessage;
35
+ private trackTargetSessions;
36
+ private broadcastToCdp;
37
+ private sendJson;
38
+ }
@@ -0,0 +1,301 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) Object.defineProperty(obj, key, {
3
+ value: value,
4
+ enumerable: true,
5
+ configurable: true,
6
+ writable: true
7
+ });
8
+ else obj[key] = value;
9
+ return obj;
10
+ }
11
+ const RELAY_INFO = {
12
+ product: "Wingman Browser Relay",
13
+ protocolVersion: "1.3",
14
+ userAgent: "WingmanRelay/0.2.0",
15
+ jsVersion: "wingman-relay"
16
+ };
17
+ const SESSION_OPTIONAL_METHODS = new Set([
18
+ "Browser.getVersion",
19
+ "Target.setDiscoverTargets",
20
+ "Target.setAutoAttach",
21
+ "Target.getTargets",
22
+ "Target.attachToTarget",
23
+ "Target.detachFromTarget",
24
+ "Target.activateTarget"
25
+ ]);
26
+ function isLoopbackHost(host) {
27
+ const normalized = host.trim().toLowerCase();
28
+ return "127.0.0.1" === normalized || "localhost" === normalized || "::1" === normalized;
29
+ }
30
+ function parseJsonMessage(raw) {
31
+ try {
32
+ return JSON.parse(raw);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function safeErrorMessage(error) {
38
+ return error instanceof Error ? error.message : String(error);
39
+ }
40
+ class BrowserRelayServer {
41
+ get running() {
42
+ return null !== this.server;
43
+ }
44
+ get wsEndpoint() {
45
+ return this.buildWsUrl("/cdp");
46
+ }
47
+ get healthEndpoint() {
48
+ return `http://${this.config.host}:${this.config.port}/health`;
49
+ }
50
+ buildWsUrl(pathname) {
51
+ const tokenParam = this.config.requireAuth ? `?token=${encodeURIComponent(this.config.authToken || "")}` : "";
52
+ return `ws://${this.config.host}:${this.config.port}${pathname}${tokenParam}`;
53
+ }
54
+ validateStartupConfig() {
55
+ if (!this.config.enabled) return;
56
+ if (!isLoopbackHost(this.config.host)) throw new Error(`Browser relay host must be loopback for security. Received "${this.config.host}".`);
57
+ if (this.config.requireAuth && !(this.config.authToken || "").trim()) throw new Error("Browser relay auth is enabled but no token is configured. Run `wingman browser extension pair`.");
58
+ }
59
+ start() {
60
+ if (!this.config.enabled || this.server) return;
61
+ this.validateStartupConfig();
62
+ this.server = Bun.serve({
63
+ hostname: this.config.host,
64
+ port: this.config.port,
65
+ fetch: (req, server)=>this.handleFetch(req, server),
66
+ websocket: {
67
+ open: (ws)=>this.handleOpen(ws),
68
+ message: (ws, message)=>this.handleMessage(ws, message),
69
+ close: (ws)=>this.handleClose(ws)
70
+ }
71
+ });
72
+ this.logger.info(`Browser relay started on ${this.config.host}:${this.config.port}`);
73
+ }
74
+ stop() {
75
+ if (!this.server) return;
76
+ this.server.stop();
77
+ this.server = null;
78
+ this.extensionSocket = null;
79
+ this.cdpSockets.clear();
80
+ this.latestSessionId = null;
81
+ this.sessionByTargetId.clear();
82
+ this.targetBySessionId.clear();
83
+ this.logger.info("Browser relay stopped");
84
+ }
85
+ handleFetch(req, server) {
86
+ const url = new URL(req.url);
87
+ const token = url.searchParams.get("token") || void 0;
88
+ if ("/" === url.pathname || "/health" === url.pathname) return new Response("ok");
89
+ if ("/json/version" === url.pathname) return Response.json({
90
+ Browser: RELAY_INFO.product,
91
+ "Protocol-Version": RELAY_INFO.protocolVersion,
92
+ "User-Agent": RELAY_INFO.userAgent,
93
+ "V8-Version": RELAY_INFO.jsVersion,
94
+ webSocketDebuggerUrl: this.buildWsUrl("/cdp")
95
+ });
96
+ if ("/extension" === url.pathname || "/cdp" === url.pathname) {
97
+ const kind = "/extension" === url.pathname ? "extension" : "cdp";
98
+ const upgraded = server.upgrade(req, {
99
+ data: {
100
+ kind,
101
+ token,
102
+ authenticated: false,
103
+ helloComplete: "cdp" === kind
104
+ }
105
+ });
106
+ if (!upgraded) return new Response("Relay upgrade failed", {
107
+ status: 400
108
+ });
109
+ return;
110
+ }
111
+ return new Response("Not Found", {
112
+ status: 404
113
+ });
114
+ }
115
+ authenticateSocket(socket) {
116
+ if (!this.config.requireAuth) {
117
+ socket.data.authenticated = true;
118
+ return true;
119
+ }
120
+ const configured = (this.config.authToken || "").trim();
121
+ if (!configured || socket.data.token !== configured) {
122
+ socket.close(4401, "Unauthorized");
123
+ return false;
124
+ }
125
+ socket.data.authenticated = true;
126
+ return true;
127
+ }
128
+ handleOpen(socket) {
129
+ if (!this.authenticateSocket(socket)) return;
130
+ if ("extension" === socket.data.kind) {
131
+ if (this.extensionSocket && this.extensionSocket !== socket) this.extensionSocket.close(4409, "Extension replaced");
132
+ this.extensionSocket = socket;
133
+ return;
134
+ }
135
+ this.cdpSockets.add(socket);
136
+ }
137
+ handleClose(socket) {
138
+ if ("extension" === socket.data.kind) {
139
+ if (this.extensionSocket === socket) this.extensionSocket = null;
140
+ this.latestSessionId = null;
141
+ this.sessionByTargetId.clear();
142
+ this.targetBySessionId.clear();
143
+ return;
144
+ }
145
+ this.cdpSockets.delete(socket);
146
+ }
147
+ handleMessage(socket, message) {
148
+ const raw = message.toString();
149
+ if (Buffer.byteLength(raw, "utf8") > this.config.maxMessageBytes) return void socket.close(1009, "Message too large");
150
+ const parsed = parseJsonMessage(raw);
151
+ if (!parsed || "object" != typeof parsed) return;
152
+ if ("extension" === socket.data.kind) return void this.handleExtensionMessage(socket, parsed);
153
+ this.handleCdpMessage(socket, parsed);
154
+ }
155
+ handleExtensionMessage(socket, message) {
156
+ if (!socket.data.helloComplete) {
157
+ const method = "string" == typeof message.method ? message.method : "";
158
+ const token = message.params?.token;
159
+ if ("hello" !== method) return void socket.close(4403, "Expected hello handshake");
160
+ if (this.config.requireAuth && ("string" != typeof token || token !== this.config.authToken)) return void socket.close(4401, "Invalid extension token");
161
+ socket.data.helloComplete = true;
162
+ this.sendJson(socket, {
163
+ method: "hello_ack",
164
+ params: {
165
+ ok: true
166
+ }
167
+ });
168
+ return;
169
+ }
170
+ if ("number" == typeof message.id && ("result" in message || "error" in message)) return void this.broadcastToCdp(message);
171
+ if ("forwardCDPEvent" === message.method && message.params) {
172
+ const event = message.params;
173
+ const method = "string" == typeof event.method ? event.method : "";
174
+ const params = event.params || {};
175
+ this.trackTargetSessions(method, params);
176
+ this.broadcastToCdp(event);
177
+ }
178
+ }
179
+ handleCdpMessage(socket, message) {
180
+ const id = message.id;
181
+ const method = message.method;
182
+ if ("number" != typeof id || "string" != typeof method) return;
183
+ if ("Browser.getVersion" === method) return void this.sendJson(socket, {
184
+ id,
185
+ result: {
186
+ product: RELAY_INFO.product,
187
+ protocolVersion: RELAY_INFO.protocolVersion,
188
+ userAgent: RELAY_INFO.userAgent,
189
+ jsVersion: RELAY_INFO.jsVersion,
190
+ revision: "wingman"
191
+ }
192
+ });
193
+ if ("Target.setDiscoverTargets" === method || "Target.setAutoAttach" === method) return void this.sendJson(socket, {
194
+ id,
195
+ result: {}
196
+ });
197
+ if ("Target.getTargets" === method) {
198
+ const targetInfos = Array.from(this.sessionByTargetId.keys()).map((targetId)=>({
199
+ targetId,
200
+ type: "page",
201
+ attached: true
202
+ }));
203
+ this.sendJson(socket, {
204
+ id,
205
+ result: {
206
+ targetInfos
207
+ }
208
+ });
209
+ return;
210
+ }
211
+ if ("Target.attachToTarget" === method) {
212
+ const targetId = String((message.params || {}).targetId || "");
213
+ const sessionId = this.sessionByTargetId.get(targetId) || this.latestSessionId;
214
+ if (!sessionId) return void this.sendJson(socket, {
215
+ id,
216
+ error: {
217
+ message: "No attached target is available"
218
+ }
219
+ });
220
+ this.sendJson(socket, {
221
+ id,
222
+ result: {
223
+ sessionId
224
+ }
225
+ });
226
+ return;
227
+ }
228
+ if ("Target.detachFromTarget" === method || "Target.activateTarget" === method) return void this.sendJson(socket, {
229
+ id,
230
+ result: {}
231
+ });
232
+ const extension = this.extensionSocket;
233
+ if (!extension || !extension.data.helloComplete) return void this.sendJson(socket, {
234
+ id,
235
+ error: {
236
+ message: "No extension is connected to browser relay"
237
+ }
238
+ });
239
+ const sessionId = "string" == typeof message.sessionId && message.sessionId || ("string" == typeof (message.params || {}).sessionId ? String((message.params || {}).sessionId) : void 0) || this.latestSessionId || void 0;
240
+ if (!sessionId && !SESSION_OPTIONAL_METHODS.has(method)) return void this.sendJson(socket, {
241
+ id,
242
+ error: {
243
+ message: `No active tab session for method "${method}"`
244
+ }
245
+ });
246
+ this.sendJson(extension, {
247
+ id,
248
+ method: "forwardCDPCommand",
249
+ params: {
250
+ sessionId,
251
+ method,
252
+ params: message.params || {}
253
+ }
254
+ });
255
+ }
256
+ trackTargetSessions(method, params) {
257
+ if ("Target.attachedToTarget" === method) {
258
+ const sessionId = "string" == typeof params.sessionId ? params.sessionId : null;
259
+ const targetId = "string" == typeof params.targetInfo?.targetId ? String(params.targetInfo?.targetId) : null;
260
+ if (sessionId) this.latestSessionId = sessionId;
261
+ if (sessionId && targetId) {
262
+ this.sessionByTargetId.set(targetId, sessionId);
263
+ this.targetBySessionId.set(sessionId, targetId);
264
+ }
265
+ return;
266
+ }
267
+ if ("Target.detachedFromTarget" === method) {
268
+ const sessionId = "string" == typeof params.sessionId ? params.sessionId : null;
269
+ if (!sessionId) return;
270
+ const targetId = this.targetBySessionId.get(sessionId);
271
+ if (targetId) {
272
+ this.sessionByTargetId.delete(targetId);
273
+ this.targetBySessionId.delete(sessionId);
274
+ }
275
+ if (this.latestSessionId === sessionId) this.latestSessionId = this.targetBySessionId.size > 0 ? Array.from(this.targetBySessionId.keys())[0] : null;
276
+ }
277
+ }
278
+ broadcastToCdp(payload) {
279
+ for (const socket of this.cdpSockets)this.sendJson(socket, payload);
280
+ }
281
+ sendJson(socket, payload) {
282
+ try {
283
+ socket.send(JSON.stringify(payload));
284
+ } catch (error) {
285
+ this.logger.debug(`Browser relay send failed: ${safeErrorMessage(error)}`);
286
+ }
287
+ }
288
+ constructor(config, logger){
289
+ _define_property(this, "config", void 0);
290
+ _define_property(this, "logger", void 0);
291
+ _define_property(this, "server", null);
292
+ _define_property(this, "extensionSocket", null);
293
+ _define_property(this, "cdpSockets", new Set());
294
+ _define_property(this, "latestSessionId", null);
295
+ _define_property(this, "sessionByTargetId", new Map());
296
+ _define_property(this, "targetBySessionId", new Map());
297
+ this.config = config;
298
+ this.logger = logger;
299
+ }
300
+ }
301
+ export { BrowserRelayServer, isLoopbackHost };