@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.
- package/README.md +8 -0
- package/dist/agent/config/agentConfig.cjs +12 -0
- package/dist/agent/config/agentConfig.d.ts +22 -0
- package/dist/agent/config/agentConfig.js +10 -1
- package/dist/agent/config/agentLoader.cjs +9 -0
- package/dist/agent/config/agentLoader.js +9 -0
- package/dist/agent/config/toolRegistry.cjs +17 -0
- package/dist/agent/config/toolRegistry.d.ts +15 -0
- package/dist/agent/config/toolRegistry.js +17 -0
- package/dist/agent/tests/agentConfig.test.cjs +6 -1
- package/dist/agent/tests/agentConfig.test.js +6 -1
- package/dist/agent/tests/browserControlHelpers.test.cjs +35 -0
- package/dist/agent/tests/browserControlHelpers.test.d.ts +1 -0
- package/dist/agent/tests/browserControlHelpers.test.js +29 -0
- package/dist/agent/tests/browserControlTool.test.cjs +2117 -0
- package/dist/agent/tests/browserControlTool.test.d.ts +1 -0
- package/dist/agent/tests/browserControlTool.test.js +2111 -0
- package/dist/agent/tests/internet_search.test.cjs +22 -28
- package/dist/agent/tests/internet_search.test.js +22 -28
- package/dist/agent/tests/toolRegistry.test.cjs +6 -0
- package/dist/agent/tests/toolRegistry.test.js +6 -0
- package/dist/agent/tools/browser_control.cjs +1282 -0
- package/dist/agent/tools/browser_control.d.ts +478 -0
- package/dist/agent/tools/browser_control.js +1242 -0
- package/dist/agent/tools/internet_search.cjs +9 -5
- package/dist/agent/tools/internet_search.js +9 -5
- package/dist/cli/commands/agent.cjs +16 -2
- package/dist/cli/commands/agent.js +16 -2
- package/dist/cli/commands/browser.cjs +603 -0
- package/dist/cli/commands/browser.d.ts +13 -0
- package/dist/cli/commands/browser.js +566 -0
- package/dist/cli/commands/gateway.cjs +18 -7
- package/dist/cli/commands/gateway.d.ts +5 -1
- package/dist/cli/commands/gateway.js +18 -7
- package/dist/cli/commands/init.cjs +134 -45
- package/dist/cli/commands/init.js +134 -45
- package/dist/cli/commands/skill.cjs +3 -2
- package/dist/cli/commands/skill.js +3 -2
- package/dist/cli/config/loader.cjs +15 -0
- package/dist/cli/config/loader.js +15 -0
- package/dist/cli/config/schema.cjs +51 -2
- package/dist/cli/config/schema.d.ts +49 -0
- package/dist/cli/config/schema.js +44 -1
- package/dist/cli/core/workspace.cjs +89 -0
- package/dist/cli/core/workspace.d.ts +1 -0
- package/dist/cli/core/workspace.js +55 -0
- package/dist/cli/index.cjs +53 -5
- package/dist/cli/index.js +53 -5
- package/dist/cli/types/browser.cjs +18 -0
- package/dist/cli/types/browser.d.ts +9 -0
- package/dist/cli/types/browser.js +0 -0
- package/dist/gateway/browserRelayServer.cjs +338 -0
- package/dist/gateway/browserRelayServer.d.ts +38 -0
- package/dist/gateway/browserRelayServer.js +301 -0
- package/dist/gateway/http/agents.cjs +22 -0
- package/dist/gateway/http/agents.js +22 -0
- package/dist/gateway/http/fs.cjs +57 -0
- package/dist/gateway/http/fs.js +58 -1
- package/dist/gateway/server.cjs +43 -6
- package/dist/gateway/server.d.ts +4 -1
- package/dist/gateway/server.js +36 -5
- package/dist/gateway/transport/websocket.cjs +45 -10
- package/dist/gateway/transport/websocket.d.ts +1 -0
- package/dist/gateway/transport/websocket.js +41 -9
- package/dist/gateway/types.d.ts +4 -0
- package/dist/tests/agents-api.test.cjs +52 -0
- package/dist/tests/agents-api.test.js +53 -1
- package/dist/tests/browser-command.test.cjs +264 -0
- package/dist/tests/browser-command.test.d.ts +1 -0
- package/dist/tests/browser-command.test.js +258 -0
- package/dist/tests/browser-relay-server.test.cjs +20 -0
- package/dist/tests/browser-relay-server.test.d.ts +1 -0
- package/dist/tests/browser-relay-server.test.js +14 -0
- package/dist/tests/cli-config-loader.test.cjs +43 -0
- package/dist/tests/cli-config-loader.test.js +43 -0
- package/dist/tests/cli-init.test.cjs +25 -2
- package/dist/tests/cli-init.test.js +25 -2
- package/dist/tests/cli-workspace-root.test.cjs +114 -0
- package/dist/tests/cli-workspace-root.test.d.ts +1 -0
- package/dist/tests/cli-workspace-root.test.js +108 -0
- package/dist/tests/fs-api.test.cjs +138 -0
- package/dist/tests/fs-api.test.d.ts +1 -0
- package/dist/tests/fs-api.test.js +132 -0
- package/dist/tests/gateway-command-workspace.test.cjs +150 -0
- package/dist/tests/gateway-command-workspace.test.d.ts +1 -0
- package/dist/tests/gateway-command-workspace.test.js +144 -0
- package/dist/tests/gateway-request-execution-overrides.test.cjs +42 -0
- package/dist/tests/gateway-request-execution-overrides.test.d.ts +1 -0
- package/dist/tests/gateway-request-execution-overrides.test.js +36 -0
- package/dist/tests/gateway.test.cjs +31 -0
- package/dist/tests/gateway.test.js +31 -0
- package/dist/tests/websocket-transport.test.cjs +31 -0
- package/dist/tests/websocket-transport.test.d.ts +1 -0
- package/dist/tests/websocket-transport.test.js +25 -0
- package/dist/webui/assets/index-Cwkg4DKj.css +11 -0
- package/dist/webui/assets/{index-0nUBsUUq.js → index-DHbfLOUR.js} +109 -107
- package/dist/webui/index.html +2 -2
- package/extensions/wingman-browser-extension/README.md +27 -0
- package/extensions/wingman-browser-extension/background.js +416 -0
- package/extensions/wingman-browser-extension/manifest.json +19 -0
- package/extensions/wingman-browser-extension/options.html +156 -0
- package/extensions/wingman-browser-extension/options.js +106 -0
- package/package.json +8 -8
- package/{.wingman → templates}/agents/README.md +2 -1
- package/{.wingman → templates}/agents/coding/agent.md +0 -1
- package/{.wingman → templates}/agents/coding-v2/agent.md +0 -1
- package/{.wingman → templates}/agents/game-dev/agent.md +8 -1
- package/{.wingman → templates}/agents/game-dev/art-generation.md +1 -0
- package/{.wingman → templates}/agents/main/agent.md +5 -0
- package/{.wingman → templates}/agents/researcher/agent.md +9 -0
- package/{.wingman → templates}/agents/stock-trader/agent.md +1 -0
- package/dist/webui/assets/index-kk7OrD-G.css +0 -11
- /package/{.wingman → templates}/agents/coding-v2/implementor.md +0 -0
- /package/{.wingman → templates}/agents/game-dev/asset-refinement.md +0 -0
- /package/{.wingman → templates}/agents/game-dev/planning-idea.md +0 -0
- /package/{.wingman → templates}/agents/game-dev/ui-specialist.md +0 -0
- /package/{.wingman → templates}/agents/stock-trader/chain-curator.md +0 -0
- /package/{.wingman → templates}/agents/stock-trader/goal-translator.md +0 -0
- /package/{.wingman → templates}/agents/stock-trader/guardrails-veto.md +0 -0
- /package/{.wingman → templates}/agents/stock-trader/path-planner.md +0 -0
- /package/{.wingman → templates}/agents/stock-trader/regime-analyst.md +0 -0
- /package/{.wingman → templates}/agents/stock-trader/risk.md +0 -0
- /package/{.wingman → templates}/agents/stock-trader/selection.md +0 -0
- /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 };
|