@wingman-ai/gateway 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +29 -111
  2. package/dist/agent/config/agentConfig.cjs +2 -0
  3. package/dist/agent/config/agentConfig.d.ts +6 -0
  4. package/dist/agent/config/agentConfig.js +2 -0
  5. package/dist/agent/config/agentLoader.cjs +21 -18
  6. package/dist/agent/config/agentLoader.js +22 -19
  7. package/dist/agent/config/mcpClientManager.cjs +48 -9
  8. package/dist/agent/config/mcpClientManager.d.ts +12 -0
  9. package/dist/agent/config/mcpClientManager.js +48 -9
  10. package/dist/agent/config/toolRegistry.cjs +19 -0
  11. package/dist/agent/config/toolRegistry.d.ts +4 -0
  12. package/dist/agent/config/toolRegistry.js +17 -1
  13. package/dist/agent/middleware/additional-messages.cjs +115 -11
  14. package/dist/agent/middleware/additional-messages.d.ts +9 -0
  15. package/dist/agent/middleware/additional-messages.js +115 -11
  16. package/dist/agent/tests/agentLoader.test.cjs +45 -0
  17. package/dist/agent/tests/agentLoader.test.js +45 -0
  18. package/dist/agent/tests/mcpClientManager.test.cjs +50 -0
  19. package/dist/agent/tests/mcpClientManager.test.js +50 -0
  20. package/dist/agent/tests/toolRegistry.test.cjs +2 -0
  21. package/dist/agent/tests/toolRegistry.test.js +2 -0
  22. package/dist/agent/tools/node_invoke.cjs +146 -0
  23. package/dist/agent/tools/node_invoke.d.ts +86 -0
  24. package/dist/agent/tools/node_invoke.js +109 -0
  25. package/dist/cli/commands/gateway.cjs +1 -1
  26. package/dist/cli/commands/gateway.js +1 -1
  27. package/dist/cli/commands/skill.cjs +12 -4
  28. package/dist/cli/commands/skill.js +12 -4
  29. package/dist/cli/config/jsonSchema.cjs +55 -0
  30. package/dist/cli/config/jsonSchema.d.ts +2 -0
  31. package/dist/cli/config/jsonSchema.js +18 -0
  32. package/dist/cli/config/loader.cjs +33 -1
  33. package/dist/cli/config/loader.js +33 -1
  34. package/dist/cli/config/schema.cjs +119 -2
  35. package/dist/cli/config/schema.d.ts +40 -0
  36. package/dist/cli/config/schema.js +119 -2
  37. package/dist/cli/core/agentInvoker.cjs +25 -4
  38. package/dist/cli/core/agentInvoker.d.ts +13 -0
  39. package/dist/cli/core/agentInvoker.js +25 -4
  40. package/dist/cli/services/skillRepository.cjs +138 -20
  41. package/dist/cli/services/skillRepository.d.ts +10 -2
  42. package/dist/cli/services/skillRepository.js +138 -20
  43. package/dist/cli/services/skillSecurityScanner.cjs +158 -0
  44. package/dist/cli/services/skillSecurityScanner.d.ts +28 -0
  45. package/dist/cli/services/skillSecurityScanner.js +121 -0
  46. package/dist/cli/services/skillService.cjs +44 -12
  47. package/dist/cli/services/skillService.d.ts +2 -0
  48. package/dist/cli/services/skillService.js +46 -14
  49. package/dist/cli/types/skill.d.ts +9 -0
  50. package/dist/gateway/http/nodes.cjs +247 -0
  51. package/dist/gateway/http/nodes.d.ts +20 -0
  52. package/dist/gateway/http/nodes.js +210 -0
  53. package/dist/gateway/node.cjs +10 -1
  54. package/dist/gateway/node.d.ts +10 -1
  55. package/dist/gateway/node.js +10 -1
  56. package/dist/gateway/server.cjs +418 -27
  57. package/dist/gateway/server.d.ts +34 -0
  58. package/dist/gateway/server.js +412 -27
  59. package/dist/gateway/types.d.ts +15 -1
  60. package/dist/gateway/validation.cjs +2 -0
  61. package/dist/gateway/validation.d.ts +4 -0
  62. package/dist/gateway/validation.js +2 -0
  63. package/dist/tests/additionalMessageMiddleware.test.cjs +92 -0
  64. package/dist/tests/additionalMessageMiddleware.test.js +92 -0
  65. package/dist/tests/cli-config-loader.test.cjs +33 -1
  66. package/dist/tests/cli-config-loader.test.js +33 -1
  67. package/dist/tests/config-json-schema.test.cjs +25 -0
  68. package/dist/tests/config-json-schema.test.d.ts +1 -0
  69. package/dist/tests/config-json-schema.test.js +19 -0
  70. package/dist/tests/gateway-http-security.test.cjs +277 -0
  71. package/dist/tests/gateway-http-security.test.d.ts +1 -0
  72. package/dist/tests/gateway-http-security.test.js +271 -0
  73. package/dist/tests/gateway-node-mode.test.cjs +174 -0
  74. package/dist/tests/gateway-node-mode.test.d.ts +1 -0
  75. package/dist/tests/gateway-node-mode.test.js +168 -0
  76. package/dist/tests/gateway-origin-policy.test.cjs +60 -0
  77. package/dist/tests/gateway-origin-policy.test.d.ts +1 -0
  78. package/dist/tests/gateway-origin-policy.test.js +54 -0
  79. package/dist/tests/gateway.test.cjs +1 -0
  80. package/dist/tests/gateway.test.js +1 -0
  81. package/dist/tests/node-tools.test.cjs +77 -0
  82. package/dist/tests/node-tools.test.d.ts +1 -0
  83. package/dist/tests/node-tools.test.js +71 -0
  84. package/dist/tests/nodes-api.test.cjs +86 -0
  85. package/dist/tests/nodes-api.test.d.ts +1 -0
  86. package/dist/tests/nodes-api.test.js +80 -0
  87. package/dist/tests/skill-repository.test.cjs +106 -0
  88. package/dist/tests/skill-repository.test.d.ts +1 -0
  89. package/dist/tests/skill-repository.test.js +100 -0
  90. package/dist/tests/skill-security-scanner.test.cjs +126 -0
  91. package/dist/tests/skill-security-scanner.test.d.ts +1 -0
  92. package/dist/tests/skill-security-scanner.test.js +120 -0
  93. package/dist/tests/uv.test.cjs +47 -0
  94. package/dist/tests/uv.test.d.ts +1 -0
  95. package/dist/tests/uv.test.js +41 -0
  96. package/dist/utils/uv.cjs +64 -0
  97. package/dist/utils/uv.d.ts +3 -0
  98. package/dist/utils/uv.js +24 -0
  99. package/dist/webui/assets/{index-DHbfLOUR.js → index-BMekSELC.js} +106 -106
  100. package/dist/webui/index.html +1 -1
  101. package/package.json +2 -1
  102. package/skills/gog/SKILL.md +36 -0
  103. package/skills/weather/SKILL.md +49 -0
@@ -27,9 +27,11 @@ var __webpack_require__ = {};
27
27
  var __webpack_exports__ = {};
28
28
  __webpack_require__.r(__webpack_exports__);
29
29
  __webpack_require__.d(__webpack_exports__, {
30
+ isLoopbackHostname: ()=>isLoopbackHostname,
31
+ resolveExecutionConfigDirOverride: ()=>resolveExecutionConfigDirOverride,
30
32
  resolveExecutionWorkspaceOverride: ()=>resolveExecutionWorkspaceOverride,
31
33
  GatewayServer: ()=>GatewayServer,
32
- resolveExecutionConfigDirOverride: ()=>resolveExecutionConfigDirOverride
34
+ isGatewayOriginAllowed: ()=>isGatewayOriginAllowed
33
35
  });
34
36
  const external_node_fs_namespaceObject = require("node:fs");
35
37
  const external_node_os_namespaceObject = require("node:os");
@@ -41,15 +43,17 @@ const agentInvoker_cjs_namespaceObject = require("../cli/core/agentInvoker.cjs")
41
43
  const outputManager_cjs_namespaceObject = require("../cli/core/outputManager.cjs");
42
44
  const sessionManager_cjs_namespaceObject = require("../cli/core/sessionManager.cjs");
43
45
  const external_logger_cjs_namespaceObject = require("../logger.cjs");
46
+ const uv_cjs_namespaceObject = require("../utils/uv.cjs");
44
47
  const discord_cjs_namespaceObject = require("./adapters/discord.cjs");
45
48
  const external_auth_cjs_namespaceObject = require("./auth.cjs");
46
- const external_browserRelayServer_cjs_namespaceObject = require("./browserRelayServer.cjs");
47
49
  const external_broadcast_cjs_namespaceObject = require("./broadcast.cjs");
50
+ const external_browserRelayServer_cjs_namespaceObject = require("./browserRelayServer.cjs");
48
51
  const index_cjs_namespaceObject = require("./discovery/index.cjs");
49
52
  const external_env_cjs_namespaceObject = require("./env.cjs");
50
53
  const registry_cjs_namespaceObject = require("./hooks/registry.cjs");
51
54
  const agents_cjs_namespaceObject = require("./http/agents.cjs");
52
55
  const fs_cjs_namespaceObject = require("./http/fs.cjs");
56
+ const nodes_cjs_namespaceObject = require("./http/nodes.cjs");
53
57
  const providers_cjs_namespaceObject = require("./http/providers.cjs");
54
58
  const routines_cjs_namespaceObject = require("./http/routines.cjs");
55
59
  const sessions_cjs_namespaceObject = require("./http/sessions.cjs");
@@ -69,19 +73,72 @@ function _define_property(obj, key, value) {
69
73
  return obj;
70
74
  }
71
75
  const API_CORS_HEADERS = {
72
- "Access-Control-Allow-Origin": "*",
73
76
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
74
77
  "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Wingman-Token, X-Wingman-Password",
75
78
  "Access-Control-Max-Age": "600"
76
79
  };
77
- function withApiCors(response) {
78
- const headers = new Headers(response.headers);
79
- for (const [key, value] of Object.entries(API_CORS_HEADERS))headers.set(key, value);
80
- return new Response(response.body, {
81
- status: response.status,
82
- statusText: response.statusText,
83
- headers
84
- });
80
+ const LOOPBACK_HOSTNAMES = new Set([
81
+ "127.0.0.1",
82
+ "localhost",
83
+ "::1"
84
+ ]);
85
+ const CLIENT_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/;
86
+ const CLIENT_TYPE_PATTERN = /^[a-zA-Z0-9._:-]{1,64}$/;
87
+ const NODE_EXECUTION_CAPABILITIES = new Set([
88
+ "system.notify",
89
+ "system.run"
90
+ ]);
91
+ const DEFAULT_NODE_REQUEST_TIMEOUT_MS = 30000;
92
+ const MIN_NODE_REQUEST_TIMEOUT_MS = 1000;
93
+ const MAX_NODE_REQUEST_TIMEOUT_MS = 120000;
94
+ const MAX_PENDING_NODE_REQUESTS = 2000;
95
+ function normalizeHostname(hostname) {
96
+ const trimmed = hostname.trim().toLowerCase();
97
+ if (!trimmed) return "";
98
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1);
99
+ return trimmed;
100
+ }
101
+ function defaultPortForProtocol(protocol) {
102
+ return "https:" === protocol ? "443" : "80";
103
+ }
104
+ function normalizeClientIdentifier(raw, pattern) {
105
+ const trimmed = raw.trim();
106
+ if (!trimmed) return null;
107
+ if (trimmed.length > 128) return null;
108
+ if (!pattern.test(trimmed)) return null;
109
+ return trimmed;
110
+ }
111
+ function includesNodeExecutionCapability(capabilities) {
112
+ if (!Array.isArray(capabilities)) return false;
113
+ for (const capability of capabilities)if ("string" == typeof capability && NODE_EXECUTION_CAPABILITIES.has(capability.trim())) return true;
114
+ return false;
115
+ }
116
+ function isLoopbackHostname(hostname) {
117
+ return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
118
+ }
119
+ function isGatewayOriginAllowed(params) {
120
+ let originUrl;
121
+ let requestUrl;
122
+ try {
123
+ originUrl = new URL(params.origin);
124
+ requestUrl = "string" == typeof params.requestUrl ? new URL(params.requestUrl) : params.requestUrl;
125
+ } catch {
126
+ return false;
127
+ }
128
+ if ("http:" !== originUrl.protocol && "https:" !== originUrl.protocol) return false;
129
+ const originHost = normalizeHostname(originUrl.hostname);
130
+ const requestHost = normalizeHostname(requestUrl.hostname);
131
+ const configHost = normalizeHostname(params.gatewayHost);
132
+ if (isLoopbackHostname(originHost) && isLoopbackHostname(requestHost)) return true;
133
+ if (!originHost || originHost !== requestHost) return false;
134
+ const originPort = originUrl.port || defaultPortForProtocol(originUrl.protocol);
135
+ const allowedPorts = new Set([
136
+ String(params.gatewayPort)
137
+ ]);
138
+ if (params.controlUiEnabled) allowedPorts.add(String(params.controlUiPort));
139
+ if ("0.0.0.0" === configHost || "::" === configHost) return allowedPorts.has(originPort);
140
+ if (originHost !== configHost) return false;
141
+ return allowedPorts.has(originPort);
85
142
  }
86
143
  function resolveExecutionWorkspaceOverride(payload) {
87
144
  const rawWorkspace = payload?.execution?.workspace;
@@ -99,9 +156,12 @@ function resolveExecutionConfigDirOverride(payload) {
99
156
  class GatewayServer {
100
157
  async start() {
101
158
  if (void 0 === globalThis.Bun) throw new Error("Gateway server requires Bun runtime. Start with `bun ./bin/wingman gateway start`.");
159
+ const proxyConfig = this.wingmanConfig.gateway?.mcpProxy;
160
+ if (proxyConfig?.enabled) (0, uv_cjs_namespaceObject.ensureUvAvailableForFeature)(proxyConfig.command || "uvx", "gateway.mcpProxy.enabled");
102
161
  this.startedAt = Date.now();
103
162
  this.internalHooks = new registry_cjs_namespaceObject.InternalHookRegistry(this.getHttpContext(), this.wingmanConfig.hooks);
104
163
  await this.internalHooks.load();
164
+ 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.");
105
165
  this.server = Bun.serve({
106
166
  port: this.config.port,
107
167
  hostname: this.config.host,
@@ -109,10 +169,21 @@ class GatewayServer {
109
169
  const url = new URL(req.url);
110
170
  if ("/health" === url.pathname) return this.handleHealthCheck();
111
171
  if ("/stats" === url.pathname) return this.handleStats();
112
- if ("/bridge/send" === url.pathname) return this.handleBridgeSend(req);
113
- if ("/bridge/poll" === url.pathname) return this.handleBridgePoll(req);
172
+ if ("/bridge/send" === url.pathname) {
173
+ const authFailure = this.requireHttpAuth(req);
174
+ if (authFailure) return authFailure;
175
+ return this.handleBridgeSend(req);
176
+ }
177
+ if ("/bridge/poll" === url.pathname) {
178
+ const authFailure = this.requireHttpAuth(req);
179
+ if (authFailure) return authFailure;
180
+ return this.handleBridgePoll(req);
181
+ }
114
182
  if ("/ws" === url.pathname) {
115
- const tailscaleUser = req.headers.get("tailscale-user-login") || req.headers.get("ts-user-login") || void 0;
183
+ if (!this.isRequestOriginAllowed(req, url)) return new Response("Forbidden origin", {
184
+ status: 403
185
+ });
186
+ const tailscaleUser = this.resolveTrustedTailscaleUser(req);
116
187
  const upgraded = server.upgrade(req, {
117
188
  data: {
118
189
  nodeId: "",
@@ -302,6 +373,18 @@ class GatewayServer {
302
373
  case "direct":
303
374
  this.handleDirect(ws, msg);
304
375
  break;
376
+ case "req:node":
377
+ this.handleNodeRequest(ws, msg);
378
+ break;
379
+ case "event:node":
380
+ this.handleNodeResponse(ws, msg);
381
+ break;
382
+ case "res":
383
+ this.handleNodeResponse(ws, msg);
384
+ break;
385
+ case "error":
386
+ this.handleNodeResponse(ws, msg);
387
+ break;
305
388
  case "ping":
306
389
  this.handlePing(ws, msg);
307
390
  break;
@@ -323,6 +406,7 @@ class GatewayServer {
323
406
  this.nodeManager.unregisterNode(nodeId);
324
407
  this.log("info", `Node disconnected: ${nodeId}`);
325
408
  }
409
+ this.cleanupPendingNodeRequestsForSocket(ws);
326
410
  this.connectedClients.delete(ws);
327
411
  this.clearSessionSubscriptions(ws);
328
412
  this.cancelSocketAgentRequests(ws);
@@ -351,8 +435,21 @@ class GatewayServer {
351
435
  ws.close();
352
436
  return;
353
437
  }
354
- ws.data.clientId = msg.client.instanceId;
355
- ws.data.clientType = msg.client.clientType;
438
+ const clientId = normalizeClientIdentifier(msg.client.instanceId, CLIENT_ID_PATTERN);
439
+ const clientType = normalizeClientIdentifier(msg.client.clientType, CLIENT_TYPE_PATTERN);
440
+ if (!clientId || !clientType) {
441
+ this.sendMessage(ws, {
442
+ type: "res",
443
+ id: msg.id,
444
+ ok: false,
445
+ payload: "invalid client info",
446
+ timestamp: Date.now()
447
+ });
448
+ ws.close();
449
+ return;
450
+ }
451
+ ws.data.clientId = clientId;
452
+ ws.data.clientType = clientType;
356
453
  ws.data.authenticated = true;
357
454
  this.connectedClients.add(ws);
358
455
  this.sendMessage(ws, {
@@ -546,7 +643,12 @@ class GatewayServer {
546
643
  sessionManager,
547
644
  terminalSessionManager: this.terminalSessionManager,
548
645
  workdir,
549
- defaultOutputDir
646
+ defaultOutputDir,
647
+ mcpProxyConfig: this.wingmanConfig.gateway?.mcpProxy,
648
+ nodeInvoker: (request)=>this.invokeNodeTool(ws, request),
649
+ nodeDefaultTargetClientId: "desktop" === ws.data.clientType ? ws.data.clientId : void 0,
650
+ nodeConnectedIdsProvider: ()=>this.listConnectedNodeIdsForRequester(ws),
651
+ nodeConnectedTargetsProvider: ()=>this.listConnectedNodeTargetsForRequester(ws)
550
652
  });
551
653
  const abortController = new AbortController();
552
654
  this.activeAgentRequests.set(msg.id, {
@@ -702,14 +804,19 @@ class GatewayServer {
702
804
  }
703
805
  handleRegister(ws, msg) {
704
806
  const payload = msg.payload;
705
- if (!this.auth.validate({
807
+ const hasSessionAuth = true === ws.data.authenticated;
808
+ const hasPayloadAuth = this.auth.validate({
706
809
  token: payload.token
707
- }, ws.data.tailscaleUser)) {
810
+ }, ws.data.tailscaleUser);
811
+ if (!hasSessionAuth && !hasPayloadAuth) {
708
812
  this.sendError(ws, "AUTH_FAILED", "Authentication failed");
709
813
  ws.close();
710
814
  return;
711
815
  }
712
- const node = this.nodeManager.registerNode(ws, payload.name, payload.capabilities, payload.sessionId, payload.agentName);
816
+ const clientId = ws.data.clientId?.trim();
817
+ const requiresNodeApproval = this.nodePairingRequired && includesNodeExecutionCapability(payload.capabilities);
818
+ if (requiresNodeApproval && (!clientId || !this.nodeApprovalStore.isEnabled(clientId))) return void this.sendError(ws, "NODE_NOT_ENABLED", "This client is not approved for node execution");
819
+ const node = this.nodeManager.registerNode(ws, payload.name, payload.capabilities, payload.sessionId, payload.agentName, clientId);
713
820
  if (!node) {
714
821
  this.sendError(ws, "MAX_NODES_REACHED", "Maximum nodes reached");
715
822
  ws.close();
@@ -726,6 +833,7 @@ class GatewayServer {
726
833
  },
727
834
  timestamp: Date.now()
728
835
  });
836
+ if (clientId) this.nodeApprovalStore.markSeen(clientId, payload.name);
729
837
  const sessionInfo = node.sessionId ? ` (session: ${node.sessionId})` : "";
730
838
  this.log("info", `Node registered: ${node.id} (${node.name})${sessionInfo}`);
731
839
  }
@@ -766,6 +874,7 @@ class GatewayServer {
766
874
  this.nodeManager.unregisterNode(nodeId);
767
875
  this.log("info", `Node unregistered: ${nodeId}`);
768
876
  }
877
+ this.cleanupPendingNodeRequestsForSocket(ws);
769
878
  }
770
879
  handleJoinGroup(ws, msg) {
771
880
  const nodeId = ws.data.nodeId;
@@ -832,6 +941,77 @@ class GatewayServer {
832
941
  const sent = this.nodeManager.sendToNode(payload.targetNodeId, directMsg);
833
942
  if (!sent) this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
834
943
  }
944
+ handleNodeRequest(ws, msg) {
945
+ if (!ws.data.authenticated) return void this.sendError(ws, "AUTH_REQUIRED", "Client is not authenticated");
946
+ if (!msg.id) return void this.sendError(ws, "INVALID_REQUEST", "Node request id is required");
947
+ const targetNodeId = "string" == typeof msg.targetNodeId ? msg.targetNodeId.trim() : "";
948
+ if (!targetNodeId) return void this.sendError(ws, "INVALID_REQUEST", "targetNodeId is required");
949
+ const target = this.nodeManager.getNode(targetNodeId);
950
+ if (!target) return void this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
951
+ if (!this.isNodeApprovedForExecution(target.clientId)) return void this.sendError(ws, "NODE_REVOKED", "Target node has been revoked");
952
+ if (this.pendingNodeRequests.has(msg.id)) return void this.sendError(ws, "DUPLICATE_REQUEST_ID", "Node request id is already in use");
953
+ if (this.pendingNodeRequests.size >= MAX_PENDING_NODE_REQUESTS) return void this.sendError(ws, "NODE_REQUEST_OVERLOADED", "Too many pending node requests");
954
+ const timeoutMs = this.resolveNodeRequestTimeout(msg.payload);
955
+ const pendingRequest = {
956
+ id: msg.id,
957
+ requester: ws,
958
+ targetNodeId,
959
+ createdAt: Date.now()
960
+ };
961
+ pendingRequest.timeoutHandle = setTimeout(()=>{
962
+ if (!this.pendingNodeRequests.has(msg.id)) return;
963
+ this.clearPendingNodeRequest(msg.id, new Error(`Node request timed out after ${timeoutMs}ms`));
964
+ this.sendMessageWithRetry(ws, {
965
+ type: "error",
966
+ id: msg.id,
967
+ payload: {
968
+ code: "NODE_TIMEOUT",
969
+ message: `Node request timed out after ${timeoutMs}ms`
970
+ },
971
+ timestamp: Date.now()
972
+ });
973
+ }, timeoutMs);
974
+ this.pendingNodeRequests.set(msg.id, pendingRequest);
975
+ const forwarded = {
976
+ type: "req:node",
977
+ id: msg.id,
978
+ clientId: ws.data.clientId || ws.data.nodeId || "gateway",
979
+ targetNodeId,
980
+ payload: msg.payload,
981
+ timestamp: Date.now()
982
+ };
983
+ const sent = this.nodeManager.sendToNode(targetNodeId, forwarded);
984
+ if (!sent) {
985
+ this.clearPendingNodeRequest(msg.id);
986
+ this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
987
+ }
988
+ }
989
+ handleNodeResponse(ws, msg) {
990
+ if (!msg.id) return;
991
+ const pending = this.pendingNodeRequests.get(msg.id);
992
+ if (!pending) return;
993
+ const sourceNodeId = ws.data.nodeId;
994
+ if (!sourceNodeId || sourceNodeId !== pending.targetNodeId) return void this.sendError(ws, "NODE_RESPONSE_REJECTED", "Only the target node may respond to this request");
995
+ const forwarded = {
996
+ ...msg,
997
+ nodeId: sourceNodeId,
998
+ timestamp: Date.now()
999
+ };
1000
+ if (pending.requester) this.sendMessageWithRetry(pending.requester, forwarded);
1001
+ if ("res" === msg.type) {
1002
+ if (false === msg.ok) pending.reject?.(new Error(this.extractNodeErrorMessage(msg)));
1003
+ else pending.resolve?.({
1004
+ nodeId: sourceNodeId,
1005
+ payload: msg.payload
1006
+ });
1007
+ this.clearPendingNodeRequest(msg.id);
1008
+ return;
1009
+ }
1010
+ if ("error" === msg.type) {
1011
+ pending.reject?.(new Error(this.extractNodeErrorMessage(msg)));
1012
+ this.clearPendingNodeRequest(msg.id);
1013
+ }
1014
+ }
835
1015
  handlePing(ws, msg) {
836
1016
  const nodeId = ws.data.nodeId;
837
1017
  if (nodeId) this.nodeManager.updatePing(nodeId);
@@ -844,6 +1024,127 @@ class GatewayServer {
844
1024
  const nodeId = ws.data.nodeId;
845
1025
  if (nodeId) this.nodeManager.updatePing(nodeId);
846
1026
  }
1027
+ cleanupPendingNodeRequestsForSocket(ws) {
1028
+ 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"));
1029
+ }
1030
+ clearPendingNodeRequest(requestId, error) {
1031
+ const pending = this.pendingNodeRequests.get(requestId);
1032
+ if (!pending) return;
1033
+ this.pendingNodeRequests.delete(requestId);
1034
+ if (pending.timeoutHandle) clearTimeout(pending.timeoutHandle);
1035
+ if (error) pending.reject?.(error);
1036
+ }
1037
+ extractNodeErrorMessage(msg) {
1038
+ const payload = msg.payload;
1039
+ if (payload && "object" == typeof payload && !Array.isArray(payload) && "string" == typeof payload.message) return String(payload.message);
1040
+ if ("string" == typeof payload && payload.trim()) return payload;
1041
+ return "Node invocation failed";
1042
+ }
1043
+ isNodeApprovedForExecution(clientId) {
1044
+ if (!this.nodePairingRequired) return true;
1045
+ const trimmed = "string" == typeof clientId ? clientId.trim() : "";
1046
+ if (!trimmed) return false;
1047
+ return this.nodeApprovalStore.isEnabled(trimmed);
1048
+ }
1049
+ listConnectedNodeIdsForRequester(requester) {
1050
+ return this.listConnectedNodeTargetsForRequester(requester).map((node)=>node.nodeId);
1051
+ }
1052
+ listConnectedNodeTargetsForRequester(requester) {
1053
+ const requesterClientId = "desktop" === requester.data.clientType ? requester.data.clientId?.trim() || "" : "";
1054
+ const nodes = this.nodeManager.getAllNodes().filter((node)=>{
1055
+ if (!this.isNodeApprovedForExecution(node.clientId)) return false;
1056
+ if (!includesNodeExecutionCapability(node.capabilities)) return false;
1057
+ return true;
1058
+ }).sort((a, b)=>b.connectedAt - a.connectedAt);
1059
+ const preferred = requesterClientId ? nodes.filter((node)=>node.clientId === requesterClientId) : [];
1060
+ const others = requesterClientId ? nodes.filter((node)=>node.clientId !== requesterClientId) : nodes;
1061
+ return [
1062
+ ...preferred,
1063
+ ...others
1064
+ ].map((node)=>({
1065
+ nodeId: node.id,
1066
+ clientId: node.clientId,
1067
+ name: node.name,
1068
+ capabilities: Array.isArray(node.capabilities) ? node.capabilities.filter((capability)=>"string" == typeof capability) : void 0
1069
+ }));
1070
+ }
1071
+ resolveNodeRequestTimeout(payload) {
1072
+ let requestedTimeout;
1073
+ if (payload && "object" == typeof payload && !Array.isArray(payload)) {
1074
+ const timeoutValue = payload.timeoutMs;
1075
+ if ("number" == typeof timeoutValue && Number.isFinite(timeoutValue)) requestedTimeout = Math.trunc(timeoutValue);
1076
+ }
1077
+ const timeout = requestedTimeout ?? DEFAULT_NODE_REQUEST_TIMEOUT_MS;
1078
+ return Math.min(Math.max(timeout, MIN_NODE_REQUEST_TIMEOUT_MS), MAX_NODE_REQUEST_TIMEOUT_MS);
1079
+ }
1080
+ resolveNodeTarget(request, defaultTargetClientId) {
1081
+ const requiredCapability = "string" == typeof request.capability ? request.capability.trim() : "";
1082
+ const targetNodeId = "string" == typeof request.targetNodeId ? request.targetNodeId.trim() : "";
1083
+ const targetClientId = ("string" == typeof request.targetClientId ? request.targetClientId.trim() : "") || defaultTargetClientId?.trim() || "";
1084
+ const canUseNode = (node)=>{
1085
+ if (!this.isNodeApprovedForExecution(node.clientId)) return false;
1086
+ if (!requiredCapability) return true;
1087
+ return Array.isArray(node.capabilities) && node.capabilities.includes(requiredCapability);
1088
+ };
1089
+ if (targetNodeId) {
1090
+ const node = this.nodeManager.getNode(targetNodeId);
1091
+ if (!node) throw new Error("Target node not found");
1092
+ if (!canUseNode(node)) throw new Error(requiredCapability ? `Target node does not support capability ${requiredCapability}` : "Target node is not available");
1093
+ return {
1094
+ nodeId: node.id
1095
+ };
1096
+ }
1097
+ let candidates = this.nodeManager.getAllNodes().filter((node)=>canUseNode(node));
1098
+ if (targetClientId) {
1099
+ candidates = candidates.filter((node)=>node.clientId === targetClientId);
1100
+ if (0 === candidates.length) throw new Error(`No available node found for client ${targetClientId}`);
1101
+ }
1102
+ if (0 === candidates.length) throw new Error(requiredCapability ? `No available node supports capability ${requiredCapability}` : "No available node found");
1103
+ candidates.sort((a, b)=>b.connectedAt - a.connectedAt);
1104
+ const selected = candidates[0];
1105
+ return {
1106
+ nodeId: selected.id
1107
+ };
1108
+ }
1109
+ async invokeNodeTool(requester, request) {
1110
+ if (!requester.data.authenticated) throw new Error("Client is not authenticated");
1111
+ const toolName = request.tool;
1112
+ if ("system.notify" !== toolName && "system.run" !== toolName) throw new Error(`Unsupported node tool: ${toolName}`);
1113
+ const timeoutMsRaw = "number" == typeof request.timeoutMs ? Math.trunc(request.timeoutMs) : DEFAULT_NODE_REQUEST_TIMEOUT_MS;
1114
+ const timeoutMs = Math.min(Math.max(timeoutMsRaw, MIN_NODE_REQUEST_TIMEOUT_MS), MAX_NODE_REQUEST_TIMEOUT_MS);
1115
+ if (this.pendingNodeRequests.size >= MAX_PENDING_NODE_REQUESTS) throw new Error("Too many pending node requests");
1116
+ const target = this.resolveNodeTarget(request, "desktop" === requester.data.clientType ? requester.data.clientId : void 0);
1117
+ let requestId = "";
1118
+ do requestId = `node-tool-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1119
+ while (this.pendingNodeRequests.has(requestId));
1120
+ return await new Promise((resolve, reject)=>{
1121
+ const pending = {
1122
+ id: requestId,
1123
+ targetNodeId: target.nodeId,
1124
+ createdAt: Date.now(),
1125
+ resolve,
1126
+ reject
1127
+ };
1128
+ pending.timeoutHandle = setTimeout(()=>{
1129
+ this.clearPendingNodeRequest(requestId, new Error(`Node invocation timed out after ${timeoutMs}ms`));
1130
+ }, timeoutMs);
1131
+ this.pendingNodeRequests.set(requestId, pending);
1132
+ const forwarded = {
1133
+ type: "req:node",
1134
+ id: requestId,
1135
+ clientId: requester.data.clientId || requester.data.nodeId || "gateway",
1136
+ targetNodeId: target.nodeId,
1137
+ payload: {
1138
+ tool: toolName,
1139
+ args: request.args || {},
1140
+ timeoutMs
1141
+ },
1142
+ timestamp: Date.now()
1143
+ };
1144
+ const sent = this.nodeManager.sendToNode(target.nodeId, forwarded);
1145
+ if (!sent) this.clearPendingNodeRequest(requestId, new Error("Target node not found"));
1146
+ });
1147
+ }
847
1148
  sendMessage(ws, message) {
848
1149
  try {
849
1150
  const result = ws.send(JSON.stringify(message));
@@ -1208,15 +1509,87 @@ class GatewayServer {
1208
1509
  break;
1209
1510
  }
1210
1511
  }
1512
+ isRequestOriginAllowed(req, url) {
1513
+ const origin = req.headers.get("origin");
1514
+ if (!origin) return true;
1515
+ return isGatewayOriginAllowed({
1516
+ origin,
1517
+ requestUrl: url,
1518
+ gatewayHost: this.config.host,
1519
+ gatewayPort: this.config.port,
1520
+ controlUiEnabled: this.controlUiEnabled,
1521
+ controlUiPort: this.controlUiPort
1522
+ });
1523
+ }
1524
+ withApiCors(req, url, response) {
1525
+ const headers = new Headers(response.headers);
1526
+ for (const [key, value] of Object.entries(API_CORS_HEADERS))headers.set(key, value);
1527
+ const existingVary = headers.get("Vary");
1528
+ if (existingVary) {
1529
+ if (!existingVary.toLowerCase().includes("origin")) headers.set("Vary", `${existingVary}, Origin`);
1530
+ } else headers.set("Vary", "Origin");
1531
+ const origin = req.headers.get("origin");
1532
+ if (origin && this.isRequestOriginAllowed(req, url)) headers.set("Access-Control-Allow-Origin", origin);
1533
+ return new Response(response.body, {
1534
+ status: response.status,
1535
+ statusText: response.statusText,
1536
+ headers
1537
+ });
1538
+ }
1539
+ resolveTrustedTailscaleUser(req) {
1540
+ if (!this.config.auth?.allowTailscale) return;
1541
+ if (!isLoopbackHostname(this.config.host)) return;
1542
+ const raw = req.headers.get("tailscale-user-login") || req.headers.get("ts-user-login") || "";
1543
+ const normalized = raw.trim();
1544
+ if (!normalized) return;
1545
+ if (normalized.length > 256) return;
1546
+ if (!/^[a-zA-Z0-9._:@+-]+$/.test(normalized)) return;
1547
+ return normalized;
1548
+ }
1549
+ resolveHttpAuthPayload(req) {
1550
+ const authorization = req.headers.get("authorization") || "";
1551
+ let bearerToken;
1552
+ if (authorization.toLowerCase().startsWith("bearer ")) {
1553
+ const value = authorization.slice(7).trim();
1554
+ bearerToken = value || void 0;
1555
+ }
1556
+ const headerToken = req.headers.get("x-wingman-token")?.trim();
1557
+ const password = req.headers.get("x-wingman-password")?.trim();
1558
+ return {
1559
+ token: headerToken || bearerToken,
1560
+ password: password || void 0
1561
+ };
1562
+ }
1563
+ requireHttpAuth(req) {
1564
+ if (!this.auth.isAuthRequired()) return null;
1565
+ const tailscaleUser = this.resolveTrustedTailscaleUser(req);
1566
+ const authPayload = this.resolveHttpAuthPayload(req);
1567
+ const allowed = this.auth.validate(authPayload, tailscaleUser);
1568
+ if (allowed) return null;
1569
+ return new Response("Unauthorized", {
1570
+ status: 401,
1571
+ headers: {
1572
+ "WWW-Authenticate": 'Bearer realm="wingman-gateway"'
1573
+ }
1574
+ });
1575
+ }
1211
1576
  async handleUiRequest(req) {
1212
1577
  const url = new URL(req.url);
1213
1578
  const ctx = this.getHttpContext();
1214
1579
  const webhookResponse = await (0, webhooks_cjs_namespaceObject.handleWebhookInvoke)(ctx, this.webhookStore, req, url);
1215
1580
  if (webhookResponse) return webhookResponse;
1216
1581
  if (url.pathname.startsWith("/api/")) {
1217
- if ("OPTIONS" === req.method) return withApiCors(new Response(null, {
1582
+ if (!this.isRequestOriginAllowed(req, url)) return this.withApiCors(req, url, new Response("Forbidden origin", {
1583
+ status: 403
1584
+ }));
1585
+ if ("OPTIONS" === req.method) return this.withApiCors(req, url, new Response(null, {
1218
1586
  status: 204
1219
1587
  }));
1588
+ const publicApiRoute = "/api/config" === url.pathname || "/api/health" === url.pathname;
1589
+ if (!publicApiRoute) {
1590
+ const authFailure = this.requireHttpAuth(req);
1591
+ if (authFailure) return this.withApiCors(req, url, authFailure);
1592
+ }
1220
1593
  if ("/api/config" === url.pathname) {
1221
1594
  const agents = this.wingmanConfig.agents?.list?.map((agent)=>({
1222
1595
  id: agent.id,
@@ -1224,7 +1597,7 @@ class GatewayServer {
1224
1597
  default: agent.default
1225
1598
  })) || [];
1226
1599
  const defaultAgentId = this.router.selectAgent();
1227
- return withApiCors(new Response(JSON.stringify({
1600
+ return this.withApiCors(req, url, new Response(JSON.stringify({
1228
1601
  gatewayHost: this.config.host,
1229
1602
  gatewayPort: this.config.port,
1230
1603
  requireAuth: this.auth.isAuthRequired(),
@@ -1239,11 +1612,11 @@ class GatewayServer {
1239
1612
  }
1240
1613
  }));
1241
1614
  }
1242
- const apiResponse = await (0, webhooks_cjs_namespaceObject.handleWebhooksApi)(ctx, this.webhookStore, req, url) || await (0, routines_cjs_namespaceObject.handleRoutinesApi)(ctx, this.routineStore, req, url) || await (0, agents_cjs_namespaceObject.handleAgentsApi)(ctx, req, url) || await (0, providers_cjs_namespaceObject.handleProvidersApi)(ctx, req, url) || await (0, voice_cjs_namespaceObject.handleVoiceApi)(ctx, req, url) || await (0, fs_cjs_namespaceObject.handleFsApi)(ctx, req, url) || await (0, sessions_cjs_namespaceObject.handleSessionsApi)(ctx, req, url);
1243
- if (apiResponse) return withApiCors(apiResponse);
1244
- if ("/api/health" === url.pathname) return withApiCors(this.handleHealthCheck());
1245
- if ("/api/stats" === url.pathname) return withApiCors(this.handleStats());
1246
- return withApiCors(new Response("Not Found", {
1615
+ const apiResponse = await (0, webhooks_cjs_namespaceObject.handleWebhooksApi)(ctx, this.webhookStore, req, url) || await (0, routines_cjs_namespaceObject.handleRoutinesApi)(ctx, this.routineStore, req, url) || await (0, nodes_cjs_namespaceObject.handleNodesApi)(ctx, this.nodeManager, this.nodeApprovalStore, req, url) || await (0, agents_cjs_namespaceObject.handleAgentsApi)(ctx, req, url) || await (0, providers_cjs_namespaceObject.handleProvidersApi)(ctx, req, url) || await (0, voice_cjs_namespaceObject.handleVoiceApi)(ctx, req, url) || await (0, fs_cjs_namespaceObject.handleFsApi)(ctx, req, url) || await (0, sessions_cjs_namespaceObject.handleSessionsApi)(ctx, req, url);
1616
+ if (apiResponse) return this.withApiCors(req, url, apiResponse);
1617
+ if ("/api/health" === url.pathname) return this.withApiCors(req, url, this.handleHealthCheck());
1618
+ if ("/api/stats" === url.pathname) return this.withApiCors(req, url, this.handleStats());
1619
+ return this.withApiCors(req, url, new Response("Not Found", {
1247
1620
  status: 404
1248
1621
  }));
1249
1622
  }
@@ -1309,6 +1682,15 @@ class GatewayServer {
1309
1682
  const validatedMessage = validation.data;
1310
1683
  if ("register" === validatedMessage.type) {
1311
1684
  const payload = validatedMessage.payload;
1685
+ const maxBridgeNodes = this.config.maxNodes || 1000;
1686
+ if (this.bridgeQueues.size >= maxBridgeNodes) return new Response(JSON.stringify({
1687
+ error: "Bridge capacity reached"
1688
+ }), {
1689
+ status: 429,
1690
+ headers: {
1691
+ "Content-Type": "application/json"
1692
+ }
1693
+ });
1312
1694
  const nodeId = this.generateNodeId();
1313
1695
  this.bridgeQueues.set(nodeId, []);
1314
1696
  const response = {
@@ -1440,6 +1822,7 @@ class GatewayServer {
1440
1822
  _define_property(this, "browserRelayServer", null);
1441
1823
  _define_property(this, "webhookStore", void 0);
1442
1824
  _define_property(this, "routineStore", void 0);
1825
+ _define_property(this, "nodeApprovalStore", void 0);
1443
1826
  _define_property(this, "internalHooks", null);
1444
1827
  _define_property(this, "discordAdapter", null);
1445
1828
  _define_property(this, "sessionSubscriptions", new Map());
@@ -1449,7 +1832,9 @@ class GatewayServer {
1449
1832
  _define_property(this, "activeSessionRequests", new Map());
1450
1833
  _define_property(this, "queuedSessionRequests", new Map());
1451
1834
  _define_property(this, "requestSessionKeys", new Map());
1835
+ _define_property(this, "pendingNodeRequests", new Map());
1452
1836
  _define_property(this, "terminalSessionManager", void 0);
1837
+ _define_property(this, "nodePairingRequired", void 0);
1453
1838
  _define_property(this, "bridgeQueues", new Map());
1454
1839
  _define_property(this, "bridgePollWaiters", new Map());
1455
1840
  this.workspace = config.workspace || process.cwd();
@@ -1459,6 +1844,7 @@ class GatewayServer {
1459
1844
  this.router = new external_router_cjs_namespaceObject.GatewayRouter(this.wingmanConfig);
1460
1845
  this.webhookStore = (0, webhooks_cjs_namespaceObject.createWebhookStore)(()=>this.resolveConfigDirPath());
1461
1846
  this.routineStore = (0, routines_cjs_namespaceObject.createRoutineStore)(()=>this.resolveConfigDirPath());
1847
+ this.nodeApprovalStore = (0, nodes_cjs_namespaceObject.createNodeApprovalStore)(()=>this.resolveConfigDirPath());
1462
1848
  const gatewayDefaults = this.wingmanConfig.gateway;
1463
1849
  const envToken = (0, external_env_cjs_namespaceObject.getGatewayTokenFromEnv)();
1464
1850
  const authFromConfig = config.auth?.mode === "token" ? {
@@ -1498,6 +1884,7 @@ class GatewayServer {
1498
1884
  const controlUi = this.wingmanConfig.gateway?.controlUi;
1499
1885
  this.controlUiEnabled = controlUi?.enabled ?? false;
1500
1886
  this.controlUiPort = controlUi?.port || 18790;
1887
+ this.nodePairingRequired = controlUi?.pairingRequired ?? true;
1501
1888
  this.controlUiSamePort = this.controlUiEnabled && this.controlUiPort === this.config.port;
1502
1889
  this.uiDistDir = this.controlUiEnabled ? this.resolveControlUiDir() : null;
1503
1890
  const relayConfig = this.wingmanConfig.browser?.relay;
@@ -1555,10 +1942,14 @@ function isFileAttachment(attachment) {
1555
1942
  return "string" == typeof attachment.textContent;
1556
1943
  }
1557
1944
  exports.GatewayServer = __webpack_exports__.GatewayServer;
1945
+ exports.isGatewayOriginAllowed = __webpack_exports__.isGatewayOriginAllowed;
1946
+ exports.isLoopbackHostname = __webpack_exports__.isLoopbackHostname;
1558
1947
  exports.resolveExecutionConfigDirOverride = __webpack_exports__.resolveExecutionConfigDirOverride;
1559
1948
  exports.resolveExecutionWorkspaceOverride = __webpack_exports__.resolveExecutionWorkspaceOverride;
1560
1949
  for(var __rspack_i in __webpack_exports__)if (-1 === [
1561
1950
  "GatewayServer",
1951
+ "isGatewayOriginAllowed",
1952
+ "isLoopbackHostname",
1562
1953
  "resolveExecutionConfigDirOverride",
1563
1954
  "resolveExecutionWorkspaceOverride"
1564
1955
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];