@wingman-ai/gateway 0.4.0 → 0.4.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 +29 -111
- package/dist/agent/config/agentConfig.cjs +2 -0
- package/dist/agent/config/agentConfig.d.ts +6 -0
- package/dist/agent/config/agentConfig.js +2 -0
- package/dist/agent/config/agentLoader.cjs +21 -18
- package/dist/agent/config/agentLoader.js +22 -19
- package/dist/agent/config/toolRegistry.cjs +19 -0
- package/dist/agent/config/toolRegistry.d.ts +4 -0
- package/dist/agent/config/toolRegistry.js +17 -1
- package/dist/agent/middleware/additional-messages.cjs +115 -11
- package/dist/agent/middleware/additional-messages.d.ts +9 -0
- package/dist/agent/middleware/additional-messages.js +115 -11
- package/dist/agent/tests/agentLoader.test.cjs +45 -0
- package/dist/agent/tests/agentLoader.test.js +45 -0
- package/dist/agent/tests/toolRegistry.test.cjs +2 -0
- package/dist/agent/tests/toolRegistry.test.js +2 -0
- package/dist/agent/tools/node_invoke.cjs +146 -0
- package/dist/agent/tools/node_invoke.d.ts +86 -0
- package/dist/agent/tools/node_invoke.js +109 -0
- package/dist/cli/commands/gateway.cjs +1 -1
- package/dist/cli/commands/gateway.js +1 -1
- package/dist/cli/commands/init.cjs +135 -1
- package/dist/cli/commands/init.js +136 -2
- package/dist/cli/commands/skill.cjs +7 -3
- package/dist/cli/commands/skill.js +7 -3
- package/dist/cli/config/loader.cjs +7 -3
- package/dist/cli/config/loader.js +7 -3
- package/dist/cli/config/schema.cjs +27 -9
- package/dist/cli/config/schema.d.ts +18 -4
- package/dist/cli/config/schema.js +23 -8
- package/dist/cli/core/agentInvoker.cjs +70 -14
- package/dist/cli/core/agentInvoker.d.ts +10 -0
- package/dist/cli/core/agentInvoker.js +70 -14
- package/dist/cli/services/skillRepository.cjs +155 -69
- package/dist/cli/services/skillRepository.d.ts +7 -2
- package/dist/cli/services/skillRepository.js +155 -69
- package/dist/cli/services/skillService.cjs +93 -26
- package/dist/cli/services/skillService.d.ts +7 -0
- package/dist/cli/services/skillService.js +96 -29
- package/dist/cli/types/skill.d.ts +8 -3
- package/dist/gateway/http/nodes.cjs +247 -0
- package/dist/gateway/http/nodes.d.ts +20 -0
- package/dist/gateway/http/nodes.js +210 -0
- package/dist/gateway/node.cjs +10 -1
- package/dist/gateway/node.d.ts +10 -1
- package/dist/gateway/node.js +10 -1
- package/dist/gateway/server.cjs +414 -27
- package/dist/gateway/server.d.ts +34 -0
- package/dist/gateway/server.js +408 -27
- package/dist/gateway/types.d.ts +6 -1
- package/dist/gateway/validation.cjs +2 -0
- package/dist/gateway/validation.d.ts +4 -0
- package/dist/gateway/validation.js +2 -0
- package/dist/skills/activation.cjs +92 -0
- package/dist/skills/activation.d.ts +12 -0
- package/dist/skills/activation.js +58 -0
- package/dist/skills/bin-requirements.cjs +63 -0
- package/dist/skills/bin-requirements.d.ts +3 -0
- package/dist/skills/bin-requirements.js +26 -0
- package/dist/skills/metadata.cjs +141 -0
- package/dist/skills/metadata.d.ts +29 -0
- package/dist/skills/metadata.js +104 -0
- package/dist/skills/overlay.cjs +75 -0
- package/dist/skills/overlay.d.ts +2 -0
- package/dist/skills/overlay.js +38 -0
- package/dist/tests/additionalMessageMiddleware.test.cjs +92 -0
- package/dist/tests/additionalMessageMiddleware.test.js +92 -0
- package/dist/tests/cli-config-loader.test.cjs +7 -3
- package/dist/tests/cli-config-loader.test.js +7 -3
- package/dist/tests/cli-init.test.cjs +54 -0
- package/dist/tests/cli-init.test.js +54 -0
- package/dist/tests/config-json-schema.test.cjs +12 -0
- package/dist/tests/config-json-schema.test.js +12 -0
- package/dist/tests/gateway-http-security.test.cjs +277 -0
- package/dist/tests/gateway-http-security.test.d.ts +1 -0
- package/dist/tests/gateway-http-security.test.js +271 -0
- package/dist/tests/gateway-node-mode.test.cjs +174 -0
- package/dist/tests/gateway-node-mode.test.d.ts +1 -0
- package/dist/tests/gateway-node-mode.test.js +168 -0
- package/dist/tests/gateway-origin-policy.test.cjs +60 -0
- package/dist/tests/gateway-origin-policy.test.d.ts +1 -0
- package/dist/tests/gateway-origin-policy.test.js +54 -0
- package/dist/tests/gateway.test.cjs +1 -0
- package/dist/tests/gateway.test.js +1 -0
- package/dist/tests/node-tools.test.cjs +77 -0
- package/dist/tests/node-tools.test.d.ts +1 -0
- package/dist/tests/node-tools.test.js +71 -0
- package/dist/tests/nodes-api.test.cjs +86 -0
- package/dist/tests/nodes-api.test.d.ts +1 -0
- package/dist/tests/nodes-api.test.js +80 -0
- package/dist/tests/skill-activation.test.cjs +86 -0
- package/dist/tests/skill-activation.test.d.ts +1 -0
- package/dist/tests/skill-activation.test.js +80 -0
- package/dist/tests/skill-metadata.test.cjs +119 -0
- package/dist/tests/skill-metadata.test.d.ts +1 -0
- package/dist/tests/skill-metadata.test.js +113 -0
- package/dist/tests/skill-repository.test.cjs +363 -0
- package/dist/tests/skill-repository.test.js +363 -0
- package/dist/webui/assets/{index-DHbfLOUR.js → index-BMekSELC.js} +106 -106
- package/dist/webui/index.html +1 -1
- package/package.json +4 -4
- package/skills/gog/SKILL.md +1 -1
- package/skills/weather/SKILL.md +1 -1
- package/skills/ui-registry/SKILL.md +0 -35
package/dist/gateway/server.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { GatewayAuth } from "./auth.js";
|
|
2
2
|
import type { AgentRequestPayload, GatewayConfig } from "./types.js";
|
|
3
|
+
export declare function isLoopbackHostname(hostname: string): boolean;
|
|
4
|
+
export declare function isGatewayOriginAllowed(params: {
|
|
5
|
+
origin: string;
|
|
6
|
+
requestUrl: string | URL;
|
|
7
|
+
gatewayHost: string;
|
|
8
|
+
gatewayPort: number;
|
|
9
|
+
controlUiEnabled: boolean;
|
|
10
|
+
controlUiPort: number;
|
|
11
|
+
}): boolean;
|
|
3
12
|
export declare function resolveExecutionWorkspaceOverride(payload: AgentRequestPayload | undefined): string | null;
|
|
4
13
|
export declare function resolveExecutionConfigDirOverride(payload: AgentRequestPayload | undefined): string | null;
|
|
5
14
|
/**
|
|
@@ -31,6 +40,7 @@ export declare class GatewayServer {
|
|
|
31
40
|
private browserRelayServer;
|
|
32
41
|
private webhookStore;
|
|
33
42
|
private routineStore;
|
|
43
|
+
private nodeApprovalStore;
|
|
34
44
|
private internalHooks;
|
|
35
45
|
private discordAdapter;
|
|
36
46
|
private sessionSubscriptions;
|
|
@@ -40,7 +50,9 @@ export declare class GatewayServer {
|
|
|
40
50
|
private activeSessionRequests;
|
|
41
51
|
private queuedSessionRequests;
|
|
42
52
|
private requestSessionKeys;
|
|
53
|
+
private pendingNodeRequests;
|
|
43
54
|
private terminalSessionManager;
|
|
55
|
+
private nodePairingRequired;
|
|
44
56
|
private bridgeQueues;
|
|
45
57
|
private bridgePollWaiters;
|
|
46
58
|
constructor(config?: Partial<GatewayConfig>);
|
|
@@ -120,6 +132,14 @@ export declare class GatewayServer {
|
|
|
120
132
|
* Handle direct message
|
|
121
133
|
*/
|
|
122
134
|
private handleDirect;
|
|
135
|
+
/**
|
|
136
|
+
* Handle node tool invocation request
|
|
137
|
+
*/
|
|
138
|
+
private handleNodeRequest;
|
|
139
|
+
/**
|
|
140
|
+
* Route node stream/response messages back to the original requester
|
|
141
|
+
*/
|
|
142
|
+
private handleNodeResponse;
|
|
123
143
|
/**
|
|
124
144
|
* Handle ping message
|
|
125
145
|
*/
|
|
@@ -128,6 +148,15 @@ export declare class GatewayServer {
|
|
|
128
148
|
* Handle pong message
|
|
129
149
|
*/
|
|
130
150
|
private handlePong;
|
|
151
|
+
private cleanupPendingNodeRequestsForSocket;
|
|
152
|
+
private clearPendingNodeRequest;
|
|
153
|
+
private extractNodeErrorMessage;
|
|
154
|
+
private isNodeApprovedForExecution;
|
|
155
|
+
private listConnectedNodeIdsForRequester;
|
|
156
|
+
private listConnectedNodeTargetsForRequester;
|
|
157
|
+
private resolveNodeRequestTimeout;
|
|
158
|
+
private resolveNodeTarget;
|
|
159
|
+
private invokeNodeTool;
|
|
131
160
|
/**
|
|
132
161
|
* Send a message to a WebSocket
|
|
133
162
|
*/
|
|
@@ -179,6 +208,11 @@ export declare class GatewayServer {
|
|
|
179
208
|
* Log a message
|
|
180
209
|
*/
|
|
181
210
|
private log;
|
|
211
|
+
private isRequestOriginAllowed;
|
|
212
|
+
private withApiCors;
|
|
213
|
+
private resolveTrustedTailscaleUser;
|
|
214
|
+
private resolveHttpAuthPayload;
|
|
215
|
+
private requireHttpAuth;
|
|
182
216
|
private handleUiRequest;
|
|
183
217
|
/**
|
|
184
218
|
* Get the auth instance
|
package/dist/gateway/server.js
CHANGED
|
@@ -11,13 +11,14 @@ import { createLogger } from "../logger.js";
|
|
|
11
11
|
import { ensureUvAvailableForFeature } from "../utils/uv.js";
|
|
12
12
|
import { DiscordGatewayAdapter } from "./adapters/discord.js";
|
|
13
13
|
import { GatewayAuth } from "./auth.js";
|
|
14
|
-
import { BrowserRelayServer } from "./browserRelayServer.js";
|
|
15
14
|
import { BroadcastGroupManager } from "./broadcast.js";
|
|
15
|
+
import { BrowserRelayServer } from "./browserRelayServer.js";
|
|
16
16
|
import { MDNSDiscoveryService, TailscaleDiscoveryService } from "./discovery/index.js";
|
|
17
17
|
import { getGatewayTokenFromEnv } from "./env.js";
|
|
18
18
|
import { InternalHookRegistry } from "./hooks/registry.js";
|
|
19
19
|
import { handleAgentsApi } from "./http/agents.js";
|
|
20
20
|
import { handleFsApi } from "./http/fs.js";
|
|
21
|
+
import { createNodeApprovalStore, handleNodesApi } from "./http/nodes.js";
|
|
21
22
|
import { handleProvidersApi } from "./http/providers.js";
|
|
22
23
|
import { createRoutineStore, handleRoutinesApi } from "./http/routines.js";
|
|
23
24
|
import { handleSessionsApi } from "./http/sessions.js";
|
|
@@ -37,19 +38,72 @@ function _define_property(obj, key, value) {
|
|
|
37
38
|
return obj;
|
|
38
39
|
}
|
|
39
40
|
const API_CORS_HEADERS = {
|
|
40
|
-
"Access-Control-Allow-Origin": "*",
|
|
41
41
|
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
42
42
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Wingman-Token, X-Wingman-Password",
|
|
43
43
|
"Access-Control-Max-Age": "600"
|
|
44
44
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
const LOOPBACK_HOSTNAMES = new Set([
|
|
46
|
+
"127.0.0.1",
|
|
47
|
+
"localhost",
|
|
48
|
+
"::1"
|
|
49
|
+
]);
|
|
50
|
+
const CLIENT_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/;
|
|
51
|
+
const CLIENT_TYPE_PATTERN = /^[a-zA-Z0-9._:-]{1,64}$/;
|
|
52
|
+
const NODE_EXECUTION_CAPABILITIES = new Set([
|
|
53
|
+
"system.notify",
|
|
54
|
+
"system.run"
|
|
55
|
+
]);
|
|
56
|
+
const DEFAULT_NODE_REQUEST_TIMEOUT_MS = 30000;
|
|
57
|
+
const MIN_NODE_REQUEST_TIMEOUT_MS = 1000;
|
|
58
|
+
const MAX_NODE_REQUEST_TIMEOUT_MS = 120000;
|
|
59
|
+
const MAX_PENDING_NODE_REQUESTS = 2000;
|
|
60
|
+
function normalizeHostname(hostname) {
|
|
61
|
+
const trimmed = hostname.trim().toLowerCase();
|
|
62
|
+
if (!trimmed) return "";
|
|
63
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1);
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
function defaultPortForProtocol(protocol) {
|
|
67
|
+
return "https:" === protocol ? "443" : "80";
|
|
68
|
+
}
|
|
69
|
+
function normalizeClientIdentifier(raw, pattern) {
|
|
70
|
+
const trimmed = raw.trim();
|
|
71
|
+
if (!trimmed) return null;
|
|
72
|
+
if (trimmed.length > 128) return null;
|
|
73
|
+
if (!pattern.test(trimmed)) return null;
|
|
74
|
+
return trimmed;
|
|
75
|
+
}
|
|
76
|
+
function includesNodeExecutionCapability(capabilities) {
|
|
77
|
+
if (!Array.isArray(capabilities)) return false;
|
|
78
|
+
for (const capability of capabilities)if ("string" == typeof capability && NODE_EXECUTION_CAPABILITIES.has(capability.trim())) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function isLoopbackHostname(hostname) {
|
|
82
|
+
return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
|
|
83
|
+
}
|
|
84
|
+
function isGatewayOriginAllowed(params) {
|
|
85
|
+
let originUrl;
|
|
86
|
+
let requestUrl;
|
|
87
|
+
try {
|
|
88
|
+
originUrl = new URL(params.origin);
|
|
89
|
+
requestUrl = "string" == typeof params.requestUrl ? new URL(params.requestUrl) : params.requestUrl;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if ("http:" !== originUrl.protocol && "https:" !== originUrl.protocol) return false;
|
|
94
|
+
const originHost = normalizeHostname(originUrl.hostname);
|
|
95
|
+
const requestHost = normalizeHostname(requestUrl.hostname);
|
|
96
|
+
const configHost = normalizeHostname(params.gatewayHost);
|
|
97
|
+
if (isLoopbackHostname(originHost) && isLoopbackHostname(requestHost)) return true;
|
|
98
|
+
if (!originHost || originHost !== requestHost) return false;
|
|
99
|
+
const originPort = originUrl.port || defaultPortForProtocol(originUrl.protocol);
|
|
100
|
+
const allowedPorts = new Set([
|
|
101
|
+
String(params.gatewayPort)
|
|
102
|
+
]);
|
|
103
|
+
if (params.controlUiEnabled) allowedPorts.add(String(params.controlUiPort));
|
|
104
|
+
if ("0.0.0.0" === configHost || "::" === configHost) return allowedPorts.has(originPort);
|
|
105
|
+
if (originHost !== configHost) return false;
|
|
106
|
+
return allowedPorts.has(originPort);
|
|
53
107
|
}
|
|
54
108
|
function resolveExecutionWorkspaceOverride(payload) {
|
|
55
109
|
const rawWorkspace = payload?.execution?.workspace;
|
|
@@ -72,6 +126,7 @@ class GatewayServer {
|
|
|
72
126
|
this.startedAt = Date.now();
|
|
73
127
|
this.internalHooks = new InternalHookRegistry(this.getHttpContext(), this.wingmanConfig.hooks);
|
|
74
128
|
await this.internalHooks.load();
|
|
129
|
+
if (this.config.auth?.allowTailscale && !isLoopbackHostname(this.config.host)) this.log("warn", "Tailscale header-based auth bypass is disabled on non-loopback gateway hosts; use token/password auth instead.");
|
|
75
130
|
this.server = Bun.serve({
|
|
76
131
|
port: this.config.port,
|
|
77
132
|
hostname: this.config.host,
|
|
@@ -79,10 +134,21 @@ class GatewayServer {
|
|
|
79
134
|
const url = new URL(req.url);
|
|
80
135
|
if ("/health" === url.pathname) return this.handleHealthCheck();
|
|
81
136
|
if ("/stats" === url.pathname) return this.handleStats();
|
|
82
|
-
if ("/bridge/send" === url.pathname)
|
|
83
|
-
|
|
137
|
+
if ("/bridge/send" === url.pathname) {
|
|
138
|
+
const authFailure = this.requireHttpAuth(req);
|
|
139
|
+
if (authFailure) return authFailure;
|
|
140
|
+
return this.handleBridgeSend(req);
|
|
141
|
+
}
|
|
142
|
+
if ("/bridge/poll" === url.pathname) {
|
|
143
|
+
const authFailure = this.requireHttpAuth(req);
|
|
144
|
+
if (authFailure) return authFailure;
|
|
145
|
+
return this.handleBridgePoll(req);
|
|
146
|
+
}
|
|
84
147
|
if ("/ws" === url.pathname) {
|
|
85
|
-
|
|
148
|
+
if (!this.isRequestOriginAllowed(req, url)) return new Response("Forbidden origin", {
|
|
149
|
+
status: 403
|
|
150
|
+
});
|
|
151
|
+
const tailscaleUser = this.resolveTrustedTailscaleUser(req);
|
|
86
152
|
const upgraded = server.upgrade(req, {
|
|
87
153
|
data: {
|
|
88
154
|
nodeId: "",
|
|
@@ -272,6 +338,18 @@ class GatewayServer {
|
|
|
272
338
|
case "direct":
|
|
273
339
|
this.handleDirect(ws, msg);
|
|
274
340
|
break;
|
|
341
|
+
case "req:node":
|
|
342
|
+
this.handleNodeRequest(ws, msg);
|
|
343
|
+
break;
|
|
344
|
+
case "event:node":
|
|
345
|
+
this.handleNodeResponse(ws, msg);
|
|
346
|
+
break;
|
|
347
|
+
case "res":
|
|
348
|
+
this.handleNodeResponse(ws, msg);
|
|
349
|
+
break;
|
|
350
|
+
case "error":
|
|
351
|
+
this.handleNodeResponse(ws, msg);
|
|
352
|
+
break;
|
|
275
353
|
case "ping":
|
|
276
354
|
this.handlePing(ws, msg);
|
|
277
355
|
break;
|
|
@@ -293,6 +371,7 @@ class GatewayServer {
|
|
|
293
371
|
this.nodeManager.unregisterNode(nodeId);
|
|
294
372
|
this.log("info", `Node disconnected: ${nodeId}`);
|
|
295
373
|
}
|
|
374
|
+
this.cleanupPendingNodeRequestsForSocket(ws);
|
|
296
375
|
this.connectedClients.delete(ws);
|
|
297
376
|
this.clearSessionSubscriptions(ws);
|
|
298
377
|
this.cancelSocketAgentRequests(ws);
|
|
@@ -321,8 +400,21 @@ class GatewayServer {
|
|
|
321
400
|
ws.close();
|
|
322
401
|
return;
|
|
323
402
|
}
|
|
324
|
-
|
|
325
|
-
|
|
403
|
+
const clientId = normalizeClientIdentifier(msg.client.instanceId, CLIENT_ID_PATTERN);
|
|
404
|
+
const clientType = normalizeClientIdentifier(msg.client.clientType, CLIENT_TYPE_PATTERN);
|
|
405
|
+
if (!clientId || !clientType) {
|
|
406
|
+
this.sendMessage(ws, {
|
|
407
|
+
type: "res",
|
|
408
|
+
id: msg.id,
|
|
409
|
+
ok: false,
|
|
410
|
+
payload: "invalid client info",
|
|
411
|
+
timestamp: Date.now()
|
|
412
|
+
});
|
|
413
|
+
ws.close();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
ws.data.clientId = clientId;
|
|
417
|
+
ws.data.clientType = clientType;
|
|
326
418
|
ws.data.authenticated = true;
|
|
327
419
|
this.connectedClients.add(ws);
|
|
328
420
|
this.sendMessage(ws, {
|
|
@@ -517,7 +609,11 @@ class GatewayServer {
|
|
|
517
609
|
terminalSessionManager: this.terminalSessionManager,
|
|
518
610
|
workdir,
|
|
519
611
|
defaultOutputDir,
|
|
520
|
-
mcpProxyConfig: this.wingmanConfig.gateway?.mcpProxy
|
|
612
|
+
mcpProxyConfig: this.wingmanConfig.gateway?.mcpProxy,
|
|
613
|
+
nodeInvoker: (request)=>this.invokeNodeTool(ws, request),
|
|
614
|
+
nodeDefaultTargetClientId: "desktop" === ws.data.clientType ? ws.data.clientId : void 0,
|
|
615
|
+
nodeConnectedIdsProvider: ()=>this.listConnectedNodeIdsForRequester(ws),
|
|
616
|
+
nodeConnectedTargetsProvider: ()=>this.listConnectedNodeTargetsForRequester(ws)
|
|
521
617
|
});
|
|
522
618
|
const abortController = new AbortController();
|
|
523
619
|
this.activeAgentRequests.set(msg.id, {
|
|
@@ -673,14 +769,19 @@ class GatewayServer {
|
|
|
673
769
|
}
|
|
674
770
|
handleRegister(ws, msg) {
|
|
675
771
|
const payload = msg.payload;
|
|
676
|
-
|
|
772
|
+
const hasSessionAuth = true === ws.data.authenticated;
|
|
773
|
+
const hasPayloadAuth = this.auth.validate({
|
|
677
774
|
token: payload.token
|
|
678
|
-
}, ws.data.tailscaleUser)
|
|
775
|
+
}, ws.data.tailscaleUser);
|
|
776
|
+
if (!hasSessionAuth && !hasPayloadAuth) {
|
|
679
777
|
this.sendError(ws, "AUTH_FAILED", "Authentication failed");
|
|
680
778
|
ws.close();
|
|
681
779
|
return;
|
|
682
780
|
}
|
|
683
|
-
const
|
|
781
|
+
const clientId = ws.data.clientId?.trim();
|
|
782
|
+
const requiresNodeApproval = this.nodePairingRequired && includesNodeExecutionCapability(payload.capabilities);
|
|
783
|
+
if (requiresNodeApproval && (!clientId || !this.nodeApprovalStore.isEnabled(clientId))) return void this.sendError(ws, "NODE_NOT_ENABLED", "This client is not approved for node execution");
|
|
784
|
+
const node = this.nodeManager.registerNode(ws, payload.name, payload.capabilities, payload.sessionId, payload.agentName, clientId);
|
|
684
785
|
if (!node) {
|
|
685
786
|
this.sendError(ws, "MAX_NODES_REACHED", "Maximum nodes reached");
|
|
686
787
|
ws.close();
|
|
@@ -697,6 +798,7 @@ class GatewayServer {
|
|
|
697
798
|
},
|
|
698
799
|
timestamp: Date.now()
|
|
699
800
|
});
|
|
801
|
+
if (clientId) this.nodeApprovalStore.markSeen(clientId, payload.name);
|
|
700
802
|
const sessionInfo = node.sessionId ? ` (session: ${node.sessionId})` : "";
|
|
701
803
|
this.log("info", `Node registered: ${node.id} (${node.name})${sessionInfo}`);
|
|
702
804
|
}
|
|
@@ -737,6 +839,7 @@ class GatewayServer {
|
|
|
737
839
|
this.nodeManager.unregisterNode(nodeId);
|
|
738
840
|
this.log("info", `Node unregistered: ${nodeId}`);
|
|
739
841
|
}
|
|
842
|
+
this.cleanupPendingNodeRequestsForSocket(ws);
|
|
740
843
|
}
|
|
741
844
|
handleJoinGroup(ws, msg) {
|
|
742
845
|
const nodeId = ws.data.nodeId;
|
|
@@ -803,6 +906,77 @@ class GatewayServer {
|
|
|
803
906
|
const sent = this.nodeManager.sendToNode(payload.targetNodeId, directMsg);
|
|
804
907
|
if (!sent) this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
|
|
805
908
|
}
|
|
909
|
+
handleNodeRequest(ws, msg) {
|
|
910
|
+
if (!ws.data.authenticated) return void this.sendError(ws, "AUTH_REQUIRED", "Client is not authenticated");
|
|
911
|
+
if (!msg.id) return void this.sendError(ws, "INVALID_REQUEST", "Node request id is required");
|
|
912
|
+
const targetNodeId = "string" == typeof msg.targetNodeId ? msg.targetNodeId.trim() : "";
|
|
913
|
+
if (!targetNodeId) return void this.sendError(ws, "INVALID_REQUEST", "targetNodeId is required");
|
|
914
|
+
const target = this.nodeManager.getNode(targetNodeId);
|
|
915
|
+
if (!target) return void this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
|
|
916
|
+
if (!this.isNodeApprovedForExecution(target.clientId)) return void this.sendError(ws, "NODE_REVOKED", "Target node has been revoked");
|
|
917
|
+
if (this.pendingNodeRequests.has(msg.id)) return void this.sendError(ws, "DUPLICATE_REQUEST_ID", "Node request id is already in use");
|
|
918
|
+
if (this.pendingNodeRequests.size >= MAX_PENDING_NODE_REQUESTS) return void this.sendError(ws, "NODE_REQUEST_OVERLOADED", "Too many pending node requests");
|
|
919
|
+
const timeoutMs = this.resolveNodeRequestTimeout(msg.payload);
|
|
920
|
+
const pendingRequest = {
|
|
921
|
+
id: msg.id,
|
|
922
|
+
requester: ws,
|
|
923
|
+
targetNodeId,
|
|
924
|
+
createdAt: Date.now()
|
|
925
|
+
};
|
|
926
|
+
pendingRequest.timeoutHandle = setTimeout(()=>{
|
|
927
|
+
if (!this.pendingNodeRequests.has(msg.id)) return;
|
|
928
|
+
this.clearPendingNodeRequest(msg.id, new Error(`Node request timed out after ${timeoutMs}ms`));
|
|
929
|
+
this.sendMessageWithRetry(ws, {
|
|
930
|
+
type: "error",
|
|
931
|
+
id: msg.id,
|
|
932
|
+
payload: {
|
|
933
|
+
code: "NODE_TIMEOUT",
|
|
934
|
+
message: `Node request timed out after ${timeoutMs}ms`
|
|
935
|
+
},
|
|
936
|
+
timestamp: Date.now()
|
|
937
|
+
});
|
|
938
|
+
}, timeoutMs);
|
|
939
|
+
this.pendingNodeRequests.set(msg.id, pendingRequest);
|
|
940
|
+
const forwarded = {
|
|
941
|
+
type: "req:node",
|
|
942
|
+
id: msg.id,
|
|
943
|
+
clientId: ws.data.clientId || ws.data.nodeId || "gateway",
|
|
944
|
+
targetNodeId,
|
|
945
|
+
payload: msg.payload,
|
|
946
|
+
timestamp: Date.now()
|
|
947
|
+
};
|
|
948
|
+
const sent = this.nodeManager.sendToNode(targetNodeId, forwarded);
|
|
949
|
+
if (!sent) {
|
|
950
|
+
this.clearPendingNodeRequest(msg.id);
|
|
951
|
+
this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
handleNodeResponse(ws, msg) {
|
|
955
|
+
if (!msg.id) return;
|
|
956
|
+
const pending = this.pendingNodeRequests.get(msg.id);
|
|
957
|
+
if (!pending) return;
|
|
958
|
+
const sourceNodeId = ws.data.nodeId;
|
|
959
|
+
if (!sourceNodeId || sourceNodeId !== pending.targetNodeId) return void this.sendError(ws, "NODE_RESPONSE_REJECTED", "Only the target node may respond to this request");
|
|
960
|
+
const forwarded = {
|
|
961
|
+
...msg,
|
|
962
|
+
nodeId: sourceNodeId,
|
|
963
|
+
timestamp: Date.now()
|
|
964
|
+
};
|
|
965
|
+
if (pending.requester) this.sendMessageWithRetry(pending.requester, forwarded);
|
|
966
|
+
if ("res" === msg.type) {
|
|
967
|
+
if (false === msg.ok) pending.reject?.(new Error(this.extractNodeErrorMessage(msg)));
|
|
968
|
+
else pending.resolve?.({
|
|
969
|
+
nodeId: sourceNodeId,
|
|
970
|
+
payload: msg.payload
|
|
971
|
+
});
|
|
972
|
+
this.clearPendingNodeRequest(msg.id);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if ("error" === msg.type) {
|
|
976
|
+
pending.reject?.(new Error(this.extractNodeErrorMessage(msg)));
|
|
977
|
+
this.clearPendingNodeRequest(msg.id);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
806
980
|
handlePing(ws, msg) {
|
|
807
981
|
const nodeId = ws.data.nodeId;
|
|
808
982
|
if (nodeId) this.nodeManager.updatePing(nodeId);
|
|
@@ -815,6 +989,127 @@ class GatewayServer {
|
|
|
815
989
|
const nodeId = ws.data.nodeId;
|
|
816
990
|
if (nodeId) this.nodeManager.updatePing(nodeId);
|
|
817
991
|
}
|
|
992
|
+
cleanupPendingNodeRequestsForSocket(ws) {
|
|
993
|
+
for (const [requestId, pending] of this.pendingNodeRequests)if (pending.requester === ws || pending.targetNodeId === ws.data.nodeId) this.clearPendingNodeRequest(requestId, new Error("Node request cancelled due to disconnect"));
|
|
994
|
+
}
|
|
995
|
+
clearPendingNodeRequest(requestId, error) {
|
|
996
|
+
const pending = this.pendingNodeRequests.get(requestId);
|
|
997
|
+
if (!pending) return;
|
|
998
|
+
this.pendingNodeRequests.delete(requestId);
|
|
999
|
+
if (pending.timeoutHandle) clearTimeout(pending.timeoutHandle);
|
|
1000
|
+
if (error) pending.reject?.(error);
|
|
1001
|
+
}
|
|
1002
|
+
extractNodeErrorMessage(msg) {
|
|
1003
|
+
const payload = msg.payload;
|
|
1004
|
+
if (payload && "object" == typeof payload && !Array.isArray(payload) && "string" == typeof payload.message) return String(payload.message);
|
|
1005
|
+
if ("string" == typeof payload && payload.trim()) return payload;
|
|
1006
|
+
return "Node invocation failed";
|
|
1007
|
+
}
|
|
1008
|
+
isNodeApprovedForExecution(clientId) {
|
|
1009
|
+
if (!this.nodePairingRequired) return true;
|
|
1010
|
+
const trimmed = "string" == typeof clientId ? clientId.trim() : "";
|
|
1011
|
+
if (!trimmed) return false;
|
|
1012
|
+
return this.nodeApprovalStore.isEnabled(trimmed);
|
|
1013
|
+
}
|
|
1014
|
+
listConnectedNodeIdsForRequester(requester) {
|
|
1015
|
+
return this.listConnectedNodeTargetsForRequester(requester).map((node)=>node.nodeId);
|
|
1016
|
+
}
|
|
1017
|
+
listConnectedNodeTargetsForRequester(requester) {
|
|
1018
|
+
const requesterClientId = "desktop" === requester.data.clientType ? requester.data.clientId?.trim() || "" : "";
|
|
1019
|
+
const nodes = this.nodeManager.getAllNodes().filter((node)=>{
|
|
1020
|
+
if (!this.isNodeApprovedForExecution(node.clientId)) return false;
|
|
1021
|
+
if (!includesNodeExecutionCapability(node.capabilities)) return false;
|
|
1022
|
+
return true;
|
|
1023
|
+
}).sort((a, b)=>b.connectedAt - a.connectedAt);
|
|
1024
|
+
const preferred = requesterClientId ? nodes.filter((node)=>node.clientId === requesterClientId) : [];
|
|
1025
|
+
const others = requesterClientId ? nodes.filter((node)=>node.clientId !== requesterClientId) : nodes;
|
|
1026
|
+
return [
|
|
1027
|
+
...preferred,
|
|
1028
|
+
...others
|
|
1029
|
+
].map((node)=>({
|
|
1030
|
+
nodeId: node.id,
|
|
1031
|
+
clientId: node.clientId,
|
|
1032
|
+
name: node.name,
|
|
1033
|
+
capabilities: Array.isArray(node.capabilities) ? node.capabilities.filter((capability)=>"string" == typeof capability) : void 0
|
|
1034
|
+
}));
|
|
1035
|
+
}
|
|
1036
|
+
resolveNodeRequestTimeout(payload) {
|
|
1037
|
+
let requestedTimeout;
|
|
1038
|
+
if (payload && "object" == typeof payload && !Array.isArray(payload)) {
|
|
1039
|
+
const timeoutValue = payload.timeoutMs;
|
|
1040
|
+
if ("number" == typeof timeoutValue && Number.isFinite(timeoutValue)) requestedTimeout = Math.trunc(timeoutValue);
|
|
1041
|
+
}
|
|
1042
|
+
const timeout = requestedTimeout ?? DEFAULT_NODE_REQUEST_TIMEOUT_MS;
|
|
1043
|
+
return Math.min(Math.max(timeout, MIN_NODE_REQUEST_TIMEOUT_MS), MAX_NODE_REQUEST_TIMEOUT_MS);
|
|
1044
|
+
}
|
|
1045
|
+
resolveNodeTarget(request, defaultTargetClientId) {
|
|
1046
|
+
const requiredCapability = "string" == typeof request.capability ? request.capability.trim() : "";
|
|
1047
|
+
const targetNodeId = "string" == typeof request.targetNodeId ? request.targetNodeId.trim() : "";
|
|
1048
|
+
const targetClientId = ("string" == typeof request.targetClientId ? request.targetClientId.trim() : "") || defaultTargetClientId?.trim() || "";
|
|
1049
|
+
const canUseNode = (node)=>{
|
|
1050
|
+
if (!this.isNodeApprovedForExecution(node.clientId)) return false;
|
|
1051
|
+
if (!requiredCapability) return true;
|
|
1052
|
+
return Array.isArray(node.capabilities) && node.capabilities.includes(requiredCapability);
|
|
1053
|
+
};
|
|
1054
|
+
if (targetNodeId) {
|
|
1055
|
+
const node = this.nodeManager.getNode(targetNodeId);
|
|
1056
|
+
if (!node) throw new Error("Target node not found");
|
|
1057
|
+
if (!canUseNode(node)) throw new Error(requiredCapability ? `Target node does not support capability ${requiredCapability}` : "Target node is not available");
|
|
1058
|
+
return {
|
|
1059
|
+
nodeId: node.id
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
let candidates = this.nodeManager.getAllNodes().filter((node)=>canUseNode(node));
|
|
1063
|
+
if (targetClientId) {
|
|
1064
|
+
candidates = candidates.filter((node)=>node.clientId === targetClientId);
|
|
1065
|
+
if (0 === candidates.length) throw new Error(`No available node found for client ${targetClientId}`);
|
|
1066
|
+
}
|
|
1067
|
+
if (0 === candidates.length) throw new Error(requiredCapability ? `No available node supports capability ${requiredCapability}` : "No available node found");
|
|
1068
|
+
candidates.sort((a, b)=>b.connectedAt - a.connectedAt);
|
|
1069
|
+
const selected = candidates[0];
|
|
1070
|
+
return {
|
|
1071
|
+
nodeId: selected.id
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
async invokeNodeTool(requester, request) {
|
|
1075
|
+
if (!requester.data.authenticated) throw new Error("Client is not authenticated");
|
|
1076
|
+
const toolName = request.tool;
|
|
1077
|
+
if ("system.notify" !== toolName && "system.run" !== toolName) throw new Error(`Unsupported node tool: ${toolName}`);
|
|
1078
|
+
const timeoutMsRaw = "number" == typeof request.timeoutMs ? Math.trunc(request.timeoutMs) : DEFAULT_NODE_REQUEST_TIMEOUT_MS;
|
|
1079
|
+
const timeoutMs = Math.min(Math.max(timeoutMsRaw, MIN_NODE_REQUEST_TIMEOUT_MS), MAX_NODE_REQUEST_TIMEOUT_MS);
|
|
1080
|
+
if (this.pendingNodeRequests.size >= MAX_PENDING_NODE_REQUESTS) throw new Error("Too many pending node requests");
|
|
1081
|
+
const target = this.resolveNodeTarget(request, "desktop" === requester.data.clientType ? requester.data.clientId : void 0);
|
|
1082
|
+
let requestId = "";
|
|
1083
|
+
do requestId = `node-tool-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1084
|
+
while (this.pendingNodeRequests.has(requestId));
|
|
1085
|
+
return await new Promise((resolve, reject)=>{
|
|
1086
|
+
const pending = {
|
|
1087
|
+
id: requestId,
|
|
1088
|
+
targetNodeId: target.nodeId,
|
|
1089
|
+
createdAt: Date.now(),
|
|
1090
|
+
resolve,
|
|
1091
|
+
reject
|
|
1092
|
+
};
|
|
1093
|
+
pending.timeoutHandle = setTimeout(()=>{
|
|
1094
|
+
this.clearPendingNodeRequest(requestId, new Error(`Node invocation timed out after ${timeoutMs}ms`));
|
|
1095
|
+
}, timeoutMs);
|
|
1096
|
+
this.pendingNodeRequests.set(requestId, pending);
|
|
1097
|
+
const forwarded = {
|
|
1098
|
+
type: "req:node",
|
|
1099
|
+
id: requestId,
|
|
1100
|
+
clientId: requester.data.clientId || requester.data.nodeId || "gateway",
|
|
1101
|
+
targetNodeId: target.nodeId,
|
|
1102
|
+
payload: {
|
|
1103
|
+
tool: toolName,
|
|
1104
|
+
args: request.args || {},
|
|
1105
|
+
timeoutMs
|
|
1106
|
+
},
|
|
1107
|
+
timestamp: Date.now()
|
|
1108
|
+
};
|
|
1109
|
+
const sent = this.nodeManager.sendToNode(target.nodeId, forwarded);
|
|
1110
|
+
if (!sent) this.clearPendingNodeRequest(requestId, new Error("Target node not found"));
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
818
1113
|
sendMessage(ws, message) {
|
|
819
1114
|
try {
|
|
820
1115
|
const result = ws.send(JSON.stringify(message));
|
|
@@ -1179,15 +1474,87 @@ class GatewayServer {
|
|
|
1179
1474
|
break;
|
|
1180
1475
|
}
|
|
1181
1476
|
}
|
|
1477
|
+
isRequestOriginAllowed(req, url) {
|
|
1478
|
+
const origin = req.headers.get("origin");
|
|
1479
|
+
if (!origin) return true;
|
|
1480
|
+
return isGatewayOriginAllowed({
|
|
1481
|
+
origin,
|
|
1482
|
+
requestUrl: url,
|
|
1483
|
+
gatewayHost: this.config.host,
|
|
1484
|
+
gatewayPort: this.config.port,
|
|
1485
|
+
controlUiEnabled: this.controlUiEnabled,
|
|
1486
|
+
controlUiPort: this.controlUiPort
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
withApiCors(req, url, response) {
|
|
1490
|
+
const headers = new Headers(response.headers);
|
|
1491
|
+
for (const [key, value] of Object.entries(API_CORS_HEADERS))headers.set(key, value);
|
|
1492
|
+
const existingVary = headers.get("Vary");
|
|
1493
|
+
if (existingVary) {
|
|
1494
|
+
if (!existingVary.toLowerCase().includes("origin")) headers.set("Vary", `${existingVary}, Origin`);
|
|
1495
|
+
} else headers.set("Vary", "Origin");
|
|
1496
|
+
const origin = req.headers.get("origin");
|
|
1497
|
+
if (origin && this.isRequestOriginAllowed(req, url)) headers.set("Access-Control-Allow-Origin", origin);
|
|
1498
|
+
return new Response(response.body, {
|
|
1499
|
+
status: response.status,
|
|
1500
|
+
statusText: response.statusText,
|
|
1501
|
+
headers
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
resolveTrustedTailscaleUser(req) {
|
|
1505
|
+
if (!this.config.auth?.allowTailscale) return;
|
|
1506
|
+
if (!isLoopbackHostname(this.config.host)) return;
|
|
1507
|
+
const raw = req.headers.get("tailscale-user-login") || req.headers.get("ts-user-login") || "";
|
|
1508
|
+
const normalized = raw.trim();
|
|
1509
|
+
if (!normalized) return;
|
|
1510
|
+
if (normalized.length > 256) return;
|
|
1511
|
+
if (!/^[a-zA-Z0-9._:@+-]+$/.test(normalized)) return;
|
|
1512
|
+
return normalized;
|
|
1513
|
+
}
|
|
1514
|
+
resolveHttpAuthPayload(req) {
|
|
1515
|
+
const authorization = req.headers.get("authorization") || "";
|
|
1516
|
+
let bearerToken;
|
|
1517
|
+
if (authorization.toLowerCase().startsWith("bearer ")) {
|
|
1518
|
+
const value = authorization.slice(7).trim();
|
|
1519
|
+
bearerToken = value || void 0;
|
|
1520
|
+
}
|
|
1521
|
+
const headerToken = req.headers.get("x-wingman-token")?.trim();
|
|
1522
|
+
const password = req.headers.get("x-wingman-password")?.trim();
|
|
1523
|
+
return {
|
|
1524
|
+
token: headerToken || bearerToken,
|
|
1525
|
+
password: password || void 0
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
requireHttpAuth(req) {
|
|
1529
|
+
if (!this.auth.isAuthRequired()) return null;
|
|
1530
|
+
const tailscaleUser = this.resolveTrustedTailscaleUser(req);
|
|
1531
|
+
const authPayload = this.resolveHttpAuthPayload(req);
|
|
1532
|
+
const allowed = this.auth.validate(authPayload, tailscaleUser);
|
|
1533
|
+
if (allowed) return null;
|
|
1534
|
+
return new Response("Unauthorized", {
|
|
1535
|
+
status: 401,
|
|
1536
|
+
headers: {
|
|
1537
|
+
"WWW-Authenticate": 'Bearer realm="wingman-gateway"'
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1182
1541
|
async handleUiRequest(req) {
|
|
1183
1542
|
const url = new URL(req.url);
|
|
1184
1543
|
const ctx = this.getHttpContext();
|
|
1185
1544
|
const webhookResponse = await handleWebhookInvoke(ctx, this.webhookStore, req, url);
|
|
1186
1545
|
if (webhookResponse) return webhookResponse;
|
|
1187
1546
|
if (url.pathname.startsWith("/api/")) {
|
|
1188
|
-
if (
|
|
1547
|
+
if (!this.isRequestOriginAllowed(req, url)) return this.withApiCors(req, url, new Response("Forbidden origin", {
|
|
1548
|
+
status: 403
|
|
1549
|
+
}));
|
|
1550
|
+
if ("OPTIONS" === req.method) return this.withApiCors(req, url, new Response(null, {
|
|
1189
1551
|
status: 204
|
|
1190
1552
|
}));
|
|
1553
|
+
const publicApiRoute = "/api/config" === url.pathname || "/api/health" === url.pathname;
|
|
1554
|
+
if (!publicApiRoute) {
|
|
1555
|
+
const authFailure = this.requireHttpAuth(req);
|
|
1556
|
+
if (authFailure) return this.withApiCors(req, url, authFailure);
|
|
1557
|
+
}
|
|
1191
1558
|
if ("/api/config" === url.pathname) {
|
|
1192
1559
|
const agents = this.wingmanConfig.agents?.list?.map((agent)=>({
|
|
1193
1560
|
id: agent.id,
|
|
@@ -1195,7 +1562,7 @@ class GatewayServer {
|
|
|
1195
1562
|
default: agent.default
|
|
1196
1563
|
})) || [];
|
|
1197
1564
|
const defaultAgentId = this.router.selectAgent();
|
|
1198
|
-
return withApiCors(new Response(JSON.stringify({
|
|
1565
|
+
return this.withApiCors(req, url, new Response(JSON.stringify({
|
|
1199
1566
|
gatewayHost: this.config.host,
|
|
1200
1567
|
gatewayPort: this.config.port,
|
|
1201
1568
|
requireAuth: this.auth.isAuthRequired(),
|
|
@@ -1210,11 +1577,11 @@ class GatewayServer {
|
|
|
1210
1577
|
}
|
|
1211
1578
|
}));
|
|
1212
1579
|
}
|
|
1213
|
-
const apiResponse = await handleWebhooksApi(ctx, this.webhookStore, req, url) || await handleRoutinesApi(ctx, this.routineStore, req, url) || await handleAgentsApi(ctx, req, url) || await handleProvidersApi(ctx, req, url) || await handleVoiceApi(ctx, req, url) || await handleFsApi(ctx, req, url) || await handleSessionsApi(ctx, req, url);
|
|
1214
|
-
if (apiResponse) return withApiCors(apiResponse);
|
|
1215
|
-
if ("/api/health" === url.pathname) return withApiCors(this.handleHealthCheck());
|
|
1216
|
-
if ("/api/stats" === url.pathname) return withApiCors(this.handleStats());
|
|
1217
|
-
return withApiCors(new Response("Not Found", {
|
|
1580
|
+
const apiResponse = await handleWebhooksApi(ctx, this.webhookStore, req, url) || await handleRoutinesApi(ctx, this.routineStore, req, url) || await handleNodesApi(ctx, this.nodeManager, this.nodeApprovalStore, req, url) || await handleAgentsApi(ctx, req, url) || await handleProvidersApi(ctx, req, url) || await handleVoiceApi(ctx, req, url) || await handleFsApi(ctx, req, url) || await handleSessionsApi(ctx, req, url);
|
|
1581
|
+
if (apiResponse) return this.withApiCors(req, url, apiResponse);
|
|
1582
|
+
if ("/api/health" === url.pathname) return this.withApiCors(req, url, this.handleHealthCheck());
|
|
1583
|
+
if ("/api/stats" === url.pathname) return this.withApiCors(req, url, this.handleStats());
|
|
1584
|
+
return this.withApiCors(req, url, new Response("Not Found", {
|
|
1218
1585
|
status: 404
|
|
1219
1586
|
}));
|
|
1220
1587
|
}
|
|
@@ -1280,6 +1647,15 @@ class GatewayServer {
|
|
|
1280
1647
|
const validatedMessage = validation.data;
|
|
1281
1648
|
if ("register" === validatedMessage.type) {
|
|
1282
1649
|
const payload = validatedMessage.payload;
|
|
1650
|
+
const maxBridgeNodes = this.config.maxNodes || 1000;
|
|
1651
|
+
if (this.bridgeQueues.size >= maxBridgeNodes) return new Response(JSON.stringify({
|
|
1652
|
+
error: "Bridge capacity reached"
|
|
1653
|
+
}), {
|
|
1654
|
+
status: 429,
|
|
1655
|
+
headers: {
|
|
1656
|
+
"Content-Type": "application/json"
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1283
1659
|
const nodeId = this.generateNodeId();
|
|
1284
1660
|
this.bridgeQueues.set(nodeId, []);
|
|
1285
1661
|
const response = {
|
|
@@ -1411,6 +1787,7 @@ class GatewayServer {
|
|
|
1411
1787
|
_define_property(this, "browserRelayServer", null);
|
|
1412
1788
|
_define_property(this, "webhookStore", void 0);
|
|
1413
1789
|
_define_property(this, "routineStore", void 0);
|
|
1790
|
+
_define_property(this, "nodeApprovalStore", void 0);
|
|
1414
1791
|
_define_property(this, "internalHooks", null);
|
|
1415
1792
|
_define_property(this, "discordAdapter", null);
|
|
1416
1793
|
_define_property(this, "sessionSubscriptions", new Map());
|
|
@@ -1420,7 +1797,9 @@ class GatewayServer {
|
|
|
1420
1797
|
_define_property(this, "activeSessionRequests", new Map());
|
|
1421
1798
|
_define_property(this, "queuedSessionRequests", new Map());
|
|
1422
1799
|
_define_property(this, "requestSessionKeys", new Map());
|
|
1800
|
+
_define_property(this, "pendingNodeRequests", new Map());
|
|
1423
1801
|
_define_property(this, "terminalSessionManager", void 0);
|
|
1802
|
+
_define_property(this, "nodePairingRequired", void 0);
|
|
1424
1803
|
_define_property(this, "bridgeQueues", new Map());
|
|
1425
1804
|
_define_property(this, "bridgePollWaiters", new Map());
|
|
1426
1805
|
this.workspace = config.workspace || process.cwd();
|
|
@@ -1430,6 +1809,7 @@ class GatewayServer {
|
|
|
1430
1809
|
this.router = new GatewayRouter(this.wingmanConfig);
|
|
1431
1810
|
this.webhookStore = createWebhookStore(()=>this.resolveConfigDirPath());
|
|
1432
1811
|
this.routineStore = createRoutineStore(()=>this.resolveConfigDirPath());
|
|
1812
|
+
this.nodeApprovalStore = createNodeApprovalStore(()=>this.resolveConfigDirPath());
|
|
1433
1813
|
const gatewayDefaults = this.wingmanConfig.gateway;
|
|
1434
1814
|
const envToken = getGatewayTokenFromEnv();
|
|
1435
1815
|
const authFromConfig = config.auth?.mode === "token" ? {
|
|
@@ -1469,6 +1849,7 @@ class GatewayServer {
|
|
|
1469
1849
|
const controlUi = this.wingmanConfig.gateway?.controlUi;
|
|
1470
1850
|
this.controlUiEnabled = controlUi?.enabled ?? false;
|
|
1471
1851
|
this.controlUiPort = controlUi?.port || 18790;
|
|
1852
|
+
this.nodePairingRequired = controlUi?.pairingRequired ?? true;
|
|
1472
1853
|
this.controlUiSamePort = this.controlUiEnabled && this.controlUiPort === this.config.port;
|
|
1473
1854
|
this.uiDistDir = this.controlUiEnabled ? this.resolveControlUiDir() : null;
|
|
1474
1855
|
const relayConfig = this.wingmanConfig.browser?.relay;
|
|
@@ -1525,4 +1906,4 @@ function isFileAttachment(attachment) {
|
|
|
1525
1906
|
if ("file" === attachment.kind) return true;
|
|
1526
1907
|
return "string" == typeof attachment.textContent;
|
|
1527
1908
|
}
|
|
1528
|
-
export { GatewayServer, resolveExecutionConfigDirOverride, resolveExecutionWorkspaceOverride };
|
|
1909
|
+
export { GatewayServer, isGatewayOriginAllowed, isLoopbackHostname, resolveExecutionConfigDirOverride, resolveExecutionWorkspaceOverride };
|