@lobu/gateway 3.0.8 → 3.0.12

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 (219) hide show
  1. package/dist/api/platform.d.ts.map +1 -1
  2. package/dist/api/platform.js +8 -26
  3. package/dist/api/platform.js.map +1 -1
  4. package/dist/auth/mcp/proxy.d.ts +14 -0
  5. package/dist/auth/mcp/proxy.d.ts.map +1 -1
  6. package/dist/auth/mcp/proxy.js +149 -13
  7. package/dist/auth/mcp/proxy.js.map +1 -1
  8. package/dist/cli/gateway.d.ts.map +1 -1
  9. package/dist/cli/gateway.js +29 -0
  10. package/dist/cli/gateway.js.map +1 -1
  11. package/dist/cli/index.js +2 -2
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/connections/chat-instance-manager.d.ts.map +1 -1
  14. package/dist/connections/chat-instance-manager.js +2 -1
  15. package/dist/connections/chat-instance-manager.js.map +1 -1
  16. package/dist/connections/interaction-bridge.d.ts +9 -2
  17. package/dist/connections/interaction-bridge.d.ts.map +1 -1
  18. package/dist/connections/interaction-bridge.js +132 -230
  19. package/dist/connections/interaction-bridge.js.map +1 -1
  20. package/dist/connections/message-handler-bridge.d.ts.map +1 -1
  21. package/dist/connections/message-handler-bridge.js +44 -26
  22. package/dist/connections/message-handler-bridge.js.map +1 -1
  23. package/dist/interactions.d.ts +9 -43
  24. package/dist/interactions.d.ts.map +1 -1
  25. package/dist/interactions.js +10 -52
  26. package/dist/interactions.js.map +1 -1
  27. package/dist/orchestration/base-deployment-manager.js +7 -7
  28. package/dist/orchestration/base-deployment-manager.js.map +1 -1
  29. package/dist/platform/unified-thread-consumer.d.ts.map +1 -1
  30. package/dist/platform/unified-thread-consumer.js +38 -34
  31. package/dist/platform/unified-thread-consumer.js.map +1 -1
  32. package/dist/routes/public/agent.d.ts +4 -0
  33. package/dist/routes/public/agent.d.ts.map +1 -1
  34. package/dist/routes/public/agent.js +21 -0
  35. package/dist/routes/public/agent.js.map +1 -1
  36. package/dist/services/core-services.d.ts.map +1 -1
  37. package/dist/services/core-services.js +4 -0
  38. package/dist/services/core-services.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/__tests__/agent-config-routes.test.ts +0 -254
  41. package/src/__tests__/agent-history-routes.test.ts +0 -72
  42. package/src/__tests__/agent-routes.test.ts +0 -68
  43. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  44. package/src/__tests__/agent-settings-store.test.ts +0 -323
  45. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  46. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  47. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  48. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  49. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  50. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  51. package/src/__tests__/config-request-store.test.ts +0 -127
  52. package/src/__tests__/connection-routes.test.ts +0 -144
  53. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  54. package/src/__tests__/docker-deployment.test.ts +0 -1211
  55. package/src/__tests__/embedded-deployment.test.ts +0 -342
  56. package/src/__tests__/grant-store.test.ts +0 -148
  57. package/src/__tests__/http-proxy.test.ts +0 -281
  58. package/src/__tests__/instruction-service.test.ts +0 -37
  59. package/src/__tests__/link-buttons.test.ts +0 -112
  60. package/src/__tests__/lobu.test.ts +0 -32
  61. package/src/__tests__/mcp-config-service.test.ts +0 -347
  62. package/src/__tests__/mcp-proxy.test.ts +0 -694
  63. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  64. package/src/__tests__/model-selection.test.ts +0 -172
  65. package/src/__tests__/oauth-templates.test.ts +0 -39
  66. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  67. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  68. package/src/__tests__/provider-inheritance.test.ts +0 -212
  69. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  70. package/src/__tests__/routes/interactions.test.ts +0 -121
  71. package/src/__tests__/secret-proxy.test.ts +0 -85
  72. package/src/__tests__/session-manager.test.ts +0 -572
  73. package/src/__tests__/setup.ts +0 -133
  74. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  75. package/src/__tests__/slack-routes.test.ts +0 -161
  76. package/src/__tests__/system-config-resolver.test.ts +0 -75
  77. package/src/__tests__/system-message-limiter.test.ts +0 -89
  78. package/src/__tests__/system-skills-service.test.ts +0 -362
  79. package/src/__tests__/transcription-service.test.ts +0 -222
  80. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  81. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  82. package/src/__tests__/worker-job-router.test.ts +0 -722
  83. package/src/api/index.ts +0 -1
  84. package/src/api/platform.ts +0 -292
  85. package/src/api/response-renderer.ts +0 -157
  86. package/src/auth/agent-metadata-store.ts +0 -168
  87. package/src/auth/api-auth-middleware.ts +0 -69
  88. package/src/auth/api-key-provider-module.ts +0 -213
  89. package/src/auth/base-provider-module.ts +0 -201
  90. package/src/auth/bedrock/provider-module.ts +0 -110
  91. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  92. package/src/auth/chatgpt/device-code-client.ts +0 -218
  93. package/src/auth/chatgpt/index.ts +0 -1
  94. package/src/auth/claude/oauth-module.ts +0 -280
  95. package/src/auth/cli/token-service.ts +0 -249
  96. package/src/auth/external/client.ts +0 -560
  97. package/src/auth/external/device-code-client.ts +0 -235
  98. package/src/auth/mcp/config-service.ts +0 -420
  99. package/src/auth/mcp/proxy.ts +0 -1086
  100. package/src/auth/mcp/string-substitution.ts +0 -17
  101. package/src/auth/mcp/tool-cache.ts +0 -90
  102. package/src/auth/oauth/base-client.ts +0 -267
  103. package/src/auth/oauth/client.ts +0 -153
  104. package/src/auth/oauth/credentials.ts +0 -7
  105. package/src/auth/oauth/providers.ts +0 -69
  106. package/src/auth/oauth/state-store.ts +0 -150
  107. package/src/auth/oauth-templates.ts +0 -179
  108. package/src/auth/provider-catalog.ts +0 -220
  109. package/src/auth/provider-model-options.ts +0 -41
  110. package/src/auth/settings/agent-settings-store.ts +0 -565
  111. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  112. package/src/auth/settings/index.ts +0 -12
  113. package/src/auth/settings/model-preference-store.ts +0 -52
  114. package/src/auth/settings/model-selection.ts +0 -135
  115. package/src/auth/settings/resolved-settings-view.ts +0 -298
  116. package/src/auth/settings/template-utils.ts +0 -44
  117. package/src/auth/settings/token-service.ts +0 -88
  118. package/src/auth/system-env-store.ts +0 -98
  119. package/src/auth/user-agents-store.ts +0 -68
  120. package/src/channels/binding-service.ts +0 -214
  121. package/src/channels/index.ts +0 -4
  122. package/src/cli/gateway.ts +0 -1312
  123. package/src/cli/index.ts +0 -74
  124. package/src/commands/built-in-commands.ts +0 -80
  125. package/src/commands/command-dispatcher.ts +0 -94
  126. package/src/commands/command-reply-adapters.ts +0 -27
  127. package/src/config/file-loader.ts +0 -618
  128. package/src/config/index.ts +0 -588
  129. package/src/config/network-allowlist.ts +0 -71
  130. package/src/connections/chat-instance-manager.ts +0 -1284
  131. package/src/connections/chat-response-bridge.ts +0 -618
  132. package/src/connections/index.ts +0 -7
  133. package/src/connections/interaction-bridge.ts +0 -831
  134. package/src/connections/message-handler-bridge.ts +0 -415
  135. package/src/connections/platform-auth-methods.ts +0 -15
  136. package/src/connections/types.ts +0 -84
  137. package/src/gateway/connection-manager.ts +0 -291
  138. package/src/gateway/index.ts +0 -698
  139. package/src/gateway/job-router.ts +0 -201
  140. package/src/gateway-main.ts +0 -200
  141. package/src/index.ts +0 -41
  142. package/src/infrastructure/queue/index.ts +0 -12
  143. package/src/infrastructure/queue/queue-producer.ts +0 -148
  144. package/src/infrastructure/queue/redis-queue.ts +0 -361
  145. package/src/infrastructure/queue/types.ts +0 -133
  146. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  147. package/src/interactions/config-request-store.ts +0 -198
  148. package/src/interactions.ts +0 -363
  149. package/src/lobu.ts +0 -311
  150. package/src/metrics/prometheus.ts +0 -159
  151. package/src/modules/module-system.ts +0 -179
  152. package/src/orchestration/base-deployment-manager.ts +0 -900
  153. package/src/orchestration/deployment-utils.ts +0 -98
  154. package/src/orchestration/impl/docker-deployment.ts +0 -620
  155. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  156. package/src/orchestration/impl/index.ts +0 -8
  157. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  158. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  159. package/src/orchestration/impl/k8s/index.ts +0 -1
  160. package/src/orchestration/index.ts +0 -333
  161. package/src/orchestration/message-consumer.ts +0 -584
  162. package/src/orchestration/scheduled-wakeup.ts +0 -704
  163. package/src/permissions/approval-policy.ts +0 -36
  164. package/src/permissions/grant-store.ts +0 -219
  165. package/src/platform/file-handler.ts +0 -66
  166. package/src/platform/link-buttons.ts +0 -57
  167. package/src/platform/renderer-utils.ts +0 -44
  168. package/src/platform/response-renderer.ts +0 -84
  169. package/src/platform/unified-thread-consumer.ts +0 -187
  170. package/src/platform.ts +0 -318
  171. package/src/proxy/http-proxy.ts +0 -752
  172. package/src/proxy/proxy-manager.ts +0 -81
  173. package/src/proxy/secret-proxy.ts +0 -402
  174. package/src/proxy/token-refresh-job.ts +0 -143
  175. package/src/routes/internal/audio.ts +0 -141
  176. package/src/routes/internal/device-auth.ts +0 -652
  177. package/src/routes/internal/files.ts +0 -226
  178. package/src/routes/internal/history.ts +0 -69
  179. package/src/routes/internal/images.ts +0 -127
  180. package/src/routes/internal/interactions.ts +0 -84
  181. package/src/routes/internal/middleware.ts +0 -23
  182. package/src/routes/internal/schedule.ts +0 -226
  183. package/src/routes/internal/types.ts +0 -22
  184. package/src/routes/openapi-auto.ts +0 -239
  185. package/src/routes/public/agent-access.ts +0 -23
  186. package/src/routes/public/agent-config.ts +0 -675
  187. package/src/routes/public/agent-history.ts +0 -422
  188. package/src/routes/public/agent-schedules.ts +0 -296
  189. package/src/routes/public/agent.ts +0 -1086
  190. package/src/routes/public/agents.ts +0 -373
  191. package/src/routes/public/channels.ts +0 -191
  192. package/src/routes/public/cli-auth.ts +0 -896
  193. package/src/routes/public/connections.ts +0 -574
  194. package/src/routes/public/landing.ts +0 -16
  195. package/src/routes/public/oauth.ts +0 -147
  196. package/src/routes/public/settings-auth.ts +0 -104
  197. package/src/routes/public/slack.ts +0 -173
  198. package/src/routes/shared/agent-ownership.ts +0 -101
  199. package/src/routes/shared/token-verifier.ts +0 -34
  200. package/src/services/bedrock-model-catalog.ts +0 -217
  201. package/src/services/bedrock-openai-service.ts +0 -658
  202. package/src/services/core-services.ts +0 -1072
  203. package/src/services/image-generation-service.ts +0 -257
  204. package/src/services/instruction-service.ts +0 -318
  205. package/src/services/mcp-registry.ts +0 -94
  206. package/src/services/platform-helpers.ts +0 -287
  207. package/src/services/session-manager.ts +0 -262
  208. package/src/services/settings-resolver.ts +0 -74
  209. package/src/services/system-config-resolver.ts +0 -89
  210. package/src/services/system-skills-service.ts +0 -229
  211. package/src/services/transcription-service.ts +0 -684
  212. package/src/session.ts +0 -110
  213. package/src/spaces/index.ts +0 -1
  214. package/src/spaces/space-resolver.ts +0 -17
  215. package/src/stores/in-memory-agent-store.ts +0 -403
  216. package/src/stores/redis-agent-store.ts +0 -279
  217. package/src/utils/public-url.ts +0 -44
  218. package/src/utils/rate-limiter.ts +0 -94
  219. package/tsconfig.json +0 -33
@@ -1,752 +0,0 @@
1
- import crypto from "node:crypto";
2
- import type { LookupAddress } from "node:dns";
3
- import * as dns from "node:dns/promises";
4
- import * as http from "node:http";
5
- import * as net from "node:net";
6
- import { URL } from "node:url";
7
- import type { WorkerTokenData } from "@lobu/core";
8
- import { createLogger, verifyWorkerToken } from "@lobu/core";
9
- import {
10
- isUnrestrictedMode,
11
- loadAllowedDomains,
12
- loadDisallowedDomains,
13
- } from "../config/network-allowlist";
14
- import type { GrantStore } from "../permissions/grant-store";
15
-
16
- const logger = createLogger("http-proxy");
17
-
18
- interface ResolvedNetworkConfig {
19
- allowedDomains: string[];
20
- deniedDomains: string[];
21
- }
22
-
23
- interface TargetResolutionResult {
24
- ok: boolean;
25
- resolvedIp?: string;
26
- statusCode?: number;
27
- clientMessage?: string;
28
- reason?: string;
29
- }
30
-
31
- const blockedIpv4Ranges: ReadonlyArray<readonly [string, number]> = [
32
- ["0.0.0.0", 8],
33
- ["10.0.0.0", 8],
34
- ["100.64.0.0", 10],
35
- ["127.0.0.0", 8],
36
- ["169.254.0.0", 16],
37
- ["172.16.0.0", 12],
38
- ["192.168.0.0", 16],
39
- ["198.18.0.0", 15],
40
- ["224.0.0.0", 4],
41
- ["240.0.0.0", 4],
42
- ];
43
-
44
- const blockedIpv6Ranges: ReadonlyArray<readonly [string, number]> = [
45
- ["fc00::", 7],
46
- ["fe80::", 10],
47
- ["ff00::", 8],
48
- ];
49
-
50
- const blockedIpv4List = new net.BlockList();
51
- for (const [address, prefix] of blockedIpv4Ranges) {
52
- blockedIpv4List.addSubnet(address, prefix, "ipv4");
53
- }
54
-
55
- const blockedIpv6List = new net.BlockList();
56
- blockedIpv6List.addAddress("::", "ipv6");
57
- blockedIpv6List.addAddress("::1", "ipv6");
58
- for (const [address, prefix] of blockedIpv6Ranges) {
59
- blockedIpv6List.addSubnet(address, prefix, "ipv6");
60
- }
61
-
62
- // Cache for global defaults (used when no deployment identified)
63
- let globalConfig: ResolvedNetworkConfig | null = null;
64
-
65
- // Module-level grant store reference for domain grant checks
66
- let proxyGrantStore: GrantStore | null = null;
67
-
68
- /**
69
- * Set the grant store for the HTTP proxy to check domain grants.
70
- * Called during gateway initialization.
71
- */
72
- export function setProxyGrantStore(store: GrantStore): void {
73
- proxyGrantStore = store;
74
- }
75
-
76
- /**
77
- * Get global network config (lazy loaded)
78
- */
79
- function getGlobalConfig(): ResolvedNetworkConfig {
80
- if (!globalConfig) {
81
- globalConfig = {
82
- allowedDomains: loadAllowedDomains(),
83
- deniedDomains: loadDisallowedDomains(),
84
- };
85
- }
86
- return globalConfig;
87
- }
88
-
89
- /**
90
- * Unified domain access check: global config → grant store.
91
- *
92
- * 1. If denied by global blocklist → block
93
- * 2. If allowed by global allowlist → check grantStore.isDenied() → allow/block
94
- * 3. If not in global list → check grantStore.hasGrant() → allow/block
95
- */
96
- async function checkDomainAccess(
97
- hostname: string,
98
- agentId: string | undefined
99
- ): Promise<boolean> {
100
- const global = getGlobalConfig();
101
-
102
- // Global blocklist always takes precedence
103
- if (
104
- global.deniedDomains.length > 0 &&
105
- matchesDomainPattern(hostname, global.deniedDomains)
106
- ) {
107
- return false;
108
- }
109
-
110
- // Check if globally allowed (unrestricted or in allowlist)
111
- const globallyAllowed = isHostnameAllowed(
112
- hostname,
113
- global.allowedDomains,
114
- global.deniedDomains
115
- );
116
-
117
- if (globallyAllowed) {
118
- // Even if globally allowed, a per-agent deny grant can override
119
- if (proxyGrantStore && agentId) {
120
- const denied = await proxyGrantStore.isDenied(agentId, hostname);
121
- if (denied) {
122
- logger.debug(`Domain ${hostname} denied via grant (agent: ${agentId})`);
123
- return false;
124
- }
125
- }
126
- return true;
127
- }
128
-
129
- // Not globally allowed — check grant store for per-agent access
130
- if (proxyGrantStore && agentId) {
131
- const granted = await proxyGrantStore.hasGrant(agentId, hostname);
132
- if (granted) {
133
- logger.debug(`Domain ${hostname} allowed via grant (agent: ${agentId})`);
134
- return true;
135
- }
136
- }
137
-
138
- return false;
139
- }
140
-
141
- interface ProxyCredentials {
142
- deploymentName: string;
143
- token: string;
144
- }
145
-
146
- function parseMappedIpv4Address(ip: string): string | null {
147
- const normalized = ip.toLowerCase();
148
- if (!normalized.startsWith("::ffff:")) {
149
- return null;
150
- }
151
-
152
- const mapped = normalized.substring("::ffff:".length);
153
- return net.isIP(mapped) === 4 ? mapped : null;
154
- }
155
-
156
- function parseMappedIpv4HexAddress(ip: string): string | null {
157
- const normalized = ip.toLowerCase();
158
- if (!normalized.startsWith("::ffff:")) {
159
- return null;
160
- }
161
-
162
- const mapped = normalized.substring("::ffff:".length);
163
- if (mapped.includes(".")) {
164
- return null;
165
- }
166
-
167
- const parts = mapped.split(":");
168
- if (parts.length !== 2) {
169
- return null;
170
- }
171
-
172
- const high = Number.parseInt(parts[0] || "", 16);
173
- const low = Number.parseInt(parts[1] || "", 16);
174
- if (
175
- Number.isNaN(high) ||
176
- Number.isNaN(low) ||
177
- high < 0 ||
178
- high > 0xffff ||
179
- low < 0 ||
180
- low > 0xffff
181
- ) {
182
- return null;
183
- }
184
-
185
- return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
186
- }
187
-
188
- function isBlockedIpAddress(ip: string): boolean {
189
- const ipv6WithoutZone = ip.split("%", 1)[0] || ip;
190
- const mappedIpv4 =
191
- parseMappedIpv4Address(ipv6WithoutZone) ||
192
- parseMappedIpv4HexAddress(ipv6WithoutZone);
193
- if (mappedIpv4) {
194
- return blockedIpv4List.check(mappedIpv4, "ipv4");
195
- }
196
-
197
- const family = net.isIP(ipv6WithoutZone);
198
- if (family === 4) {
199
- return blockedIpv4List.check(ipv6WithoutZone, "ipv4");
200
- }
201
- if (family === 6) {
202
- return blockedIpv6List.check(ipv6WithoutZone, "ipv6");
203
- }
204
- return false;
205
- }
206
-
207
- export const __testOnly = {
208
- isBlockedIpAddress,
209
- };
210
-
211
- async function resolveAndValidateTarget(
212
- hostname: string
213
- ): Promise<TargetResolutionResult> {
214
- const ipFamily = net.isIP(hostname);
215
- if (ipFamily !== 0) {
216
- if (isBlockedIpAddress(hostname)) {
217
- return {
218
- ok: false,
219
- statusCode: 403,
220
- clientMessage: `403 Forbidden - Target IP not allowed: ${hostname}`,
221
- reason: `target is local/private IP (${hostname})`,
222
- };
223
- }
224
- return { ok: true, resolvedIp: hostname };
225
- }
226
-
227
- let addresses: LookupAddress[];
228
- try {
229
- addresses = await dns.lookup(hostname, { all: true, verbatim: true });
230
- } catch (error) {
231
- const message = error instanceof Error ? error.message : "unknown error";
232
- return {
233
- ok: false,
234
- statusCode: 502,
235
- clientMessage: `Bad Gateway: Could not resolve target host ${hostname}`,
236
- reason: `DNS lookup failed for ${hostname}: ${message}`,
237
- };
238
- }
239
-
240
- if (addresses.length === 0) {
241
- return {
242
- ok: false,
243
- statusCode: 502,
244
- clientMessage: `Bad Gateway: No DNS results for ${hostname}`,
245
- reason: `DNS lookup returned no addresses for ${hostname}`,
246
- };
247
- }
248
-
249
- const blockedAddress = addresses.find((addr) =>
250
- isBlockedIpAddress(addr.address)
251
- );
252
- if (blockedAddress) {
253
- return {
254
- ok: false,
255
- statusCode: 403,
256
- clientMessage: `403 Forbidden - Target resolves to local/private IP: ${hostname}`,
257
- reason: `${hostname} resolved to blocked IP ${blockedAddress.address}`,
258
- };
259
- }
260
-
261
- return { ok: true, resolvedIp: addresses[0]?.address };
262
- }
263
-
264
- /**
265
- * Extract deployment name and token from Proxy-Authorization Basic auth header.
266
- * Workers send: HTTP_PROXY=http://<deploymentName>:<token>@gateway:8118
267
- * This creates a Basic auth header with username=deploymentName, password=token
268
- */
269
- function extractProxyCredentials(
270
- req: http.IncomingMessage
271
- ): ProxyCredentials | null {
272
- const authHeader = req.headers["proxy-authorization"];
273
- if (!authHeader || typeof authHeader !== "string") {
274
- return null;
275
- }
276
-
277
- // Parse Basic auth: "Basic base64(username:password)"
278
- const match = authHeader.match(/^Basic\s+(.+)$/i);
279
- if (!match || !match[1]) {
280
- return null;
281
- }
282
-
283
- try {
284
- const decoded = Buffer.from(match[1], "base64").toString("utf-8");
285
- const colonIndex = decoded.indexOf(":");
286
- if (colonIndex === -1) {
287
- return null;
288
- }
289
- const deploymentName = decoded.substring(0, colonIndex);
290
- const token = decoded.substring(colonIndex + 1);
291
- if (!deploymentName || !token) {
292
- return null;
293
- }
294
- return { deploymentName, token };
295
- } catch {
296
- return null;
297
- }
298
- }
299
-
300
- interface ValidatedProxy {
301
- deploymentName: string;
302
- tokenData: WorkerTokenData;
303
- }
304
-
305
- /**
306
- * Validate proxy authentication by verifying the encrypted worker token
307
- * and cross-checking the claimed deployment name.
308
- */
309
- function validateProxyAuth(req: http.IncomingMessage): ValidatedProxy | null {
310
- const creds = extractProxyCredentials(req);
311
- if (!creds) {
312
- return null;
313
- }
314
-
315
- const tokenData = verifyWorkerToken(creds.token);
316
- if (!tokenData) {
317
- logger.warn(
318
- `Proxy auth failed: invalid token (claimed deployment: ${creds.deploymentName})`
319
- );
320
- return null;
321
- }
322
-
323
- const deploymentMatch =
324
- tokenData.deploymentName.length === creds.deploymentName.length &&
325
- crypto.timingSafeEqual(
326
- Buffer.from(tokenData.deploymentName),
327
- Buffer.from(creds.deploymentName)
328
- );
329
- if (!deploymentMatch) {
330
- logger.warn(
331
- `Proxy auth failed: deployment mismatch (claimed: ${creds.deploymentName}, token: ${tokenData.deploymentName})`
332
- );
333
- return null;
334
- }
335
-
336
- return { deploymentName: creds.deploymentName, tokenData };
337
- }
338
-
339
- /**
340
- * Check if a hostname matches any domain patterns
341
- * Supports exact matches and wildcard patterns (.example.com matches *.example.com)
342
- */
343
- function matchesDomainPattern(hostname: string, patterns: string[]): boolean {
344
- const lowerHostname = hostname.toLowerCase();
345
-
346
- for (const pattern of patterns) {
347
- const lowerPattern = pattern.toLowerCase();
348
-
349
- if (lowerPattern.startsWith(".")) {
350
- // Wildcard pattern: .example.com matches *.example.com
351
- const domain = lowerPattern.substring(1);
352
- if (lowerHostname === domain || lowerHostname.endsWith(`.${domain}`)) {
353
- return true;
354
- }
355
- } else if (lowerPattern === lowerHostname) {
356
- // Exact match
357
- return true;
358
- }
359
- }
360
-
361
- return false;
362
- }
363
-
364
- /**
365
- * Check if a hostname is allowed based on allowlist/blocklist configuration.
366
- * Rules:
367
- * - deniedDomains are checked first (take precedence)
368
- * - allowedDomains are checked second
369
- * - If allowedDomains contains "*", unrestricted mode is enabled
370
- * - If allowedDomains is empty, complete isolation (deny all)
371
- */
372
- function isHostnameAllowed(
373
- hostname: string,
374
- allowedDomains: string[],
375
- deniedDomains: string[]
376
- ): boolean {
377
- // Unrestricted mode - allow all except explicitly disallowed
378
- if (isUnrestrictedMode(allowedDomains)) {
379
- if (deniedDomains.length === 0) {
380
- return true; // No blocklist, allow all
381
- }
382
- return !matchesDomainPattern(hostname, deniedDomains);
383
- }
384
-
385
- // Complete isolation mode - deny all
386
- if (allowedDomains.length === 0) {
387
- return false;
388
- }
389
-
390
- // Allowlist mode - check if allowed
391
- const isAllowed = matchesDomainPattern(hostname, allowedDomains);
392
-
393
- // Even if allowed, check blocklist
394
- if (isAllowed && deniedDomains.length > 0) {
395
- return !matchesDomainPattern(hostname, deniedDomains);
396
- }
397
-
398
- return isAllowed;
399
- }
400
-
401
- /**
402
- * Extract hostname from CONNECT request
403
- */
404
- function extractConnectHostname(url: string): string | null {
405
- // CONNECT requests are in format: "host:port"
406
- const match = url.match(/^([^:]+):\d+$/);
407
- return match?.[1] ? match[1] : null;
408
- }
409
-
410
- /**
411
- * Handle HTTPS CONNECT tunneling with per-deployment network config
412
- */
413
- async function handleConnect(
414
- req: http.IncomingMessage,
415
- clientSocket: import("stream").Duplex,
416
- head: Buffer
417
- ): Promise<void> {
418
- const url = req.url || "";
419
- const hostname = extractConnectHostname(url);
420
-
421
- if (!hostname) {
422
- logger.warn(`Invalid CONNECT request: ${url}`);
423
- clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
424
- clientSocket.end();
425
- return;
426
- }
427
-
428
- // Validate worker token
429
- const auth = validateProxyAuth(req);
430
- if (!auth) {
431
- logger.warn(`Proxy auth required for CONNECT to ${hostname}`);
432
- try {
433
- clientSocket.write(
434
- 'HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="lobu-proxy"\r\n\r\n'
435
- );
436
- clientSocket.end();
437
- } catch {
438
- // Client may have already disconnected
439
- }
440
- return;
441
- }
442
-
443
- const { deploymentName, tokenData } = auth;
444
-
445
- // Check domain access: global config → grant store
446
- const allowed = await checkDomainAccess(hostname, tokenData.agentId);
447
- if (!allowed) {
448
- logger.warn(
449
- `Blocked CONNECT to ${hostname} (deployment: ${deploymentName})`
450
- );
451
- try {
452
- clientSocket.write(
453
- `HTTP/1.1 403 Domain not allowed: ${hostname}. Network access is configured via lobu.toml or the gateway configuration APIs.\r\nContent-Type: text/plain\r\n\r\n403 Forbidden - Domain not allowed: ${hostname}. Network access is configured via lobu.toml or the gateway configuration APIs.\r\n`
454
- );
455
- clientSocket.end();
456
- } catch {
457
- // Client may have already disconnected
458
- }
459
- return;
460
- }
461
-
462
- const targetResolution = await resolveAndValidateTarget(hostname);
463
- if (!targetResolution.ok) {
464
- logger.warn(
465
- `Blocked CONNECT to ${hostname} (deployment: ${deploymentName}) - ${targetResolution.reason}`
466
- );
467
- try {
468
- clientSocket.write(
469
- `HTTP/1.1 ${targetResolution.statusCode} ${
470
- targetResolution.statusCode === 403 ? "Forbidden" : "Bad Gateway"
471
- }\r\nContent-Type: text/plain\r\n\r\n${targetResolution.clientMessage}\r\n`
472
- );
473
- clientSocket.end();
474
- } catch {
475
- // Client may have already disconnected
476
- }
477
- return;
478
- }
479
-
480
- const resolvedIp = targetResolution.resolvedIp;
481
- if (!resolvedIp) {
482
- clientSocket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
483
- clientSocket.end();
484
- return;
485
- }
486
-
487
- logger.debug(`Allowing CONNECT to ${hostname} via ${resolvedIp}`);
488
-
489
- // Parse host and port
490
- const [host, portStr] = url.split(":");
491
- const port = portStr ? parseInt(portStr, 10) || 443 : 443;
492
-
493
- if (!host) {
494
- logger.warn(`Invalid CONNECT host: ${url}`);
495
- clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
496
- clientSocket.end();
497
- return;
498
- }
499
-
500
- // Establish connection to target
501
- const targetSocket = net.connect(port, resolvedIp, () => {
502
- // Send success response to client
503
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
504
-
505
- // Pipe the connection bidirectionally
506
- targetSocket.write(head);
507
- targetSocket.pipe(clientSocket);
508
- clientSocket.pipe(targetSocket);
509
- });
510
-
511
- targetSocket.on("error", (err) => {
512
- logger.debug(`Target connection error for ${hostname}: ${err.message}`);
513
- try {
514
- clientSocket.end();
515
- } catch {
516
- // Ignore errors when closing already-closed socket
517
- }
518
- });
519
-
520
- clientSocket.on("error", (err) => {
521
- // ECONNRESET is common when clients drop connections - don't log as error
522
- if ((err as NodeJS.ErrnoException).code === "ECONNRESET") {
523
- logger.debug(`Client disconnected for ${hostname} (ECONNRESET)`);
524
- } else {
525
- logger.debug(`Client connection error for ${hostname}: ${err.message}`);
526
- }
527
- try {
528
- targetSocket.end();
529
- } catch {
530
- // Ignore errors when closing already-closed socket
531
- }
532
- });
533
-
534
- // Handle close events to clean up
535
- targetSocket.on("close", () => {
536
- try {
537
- clientSocket.end();
538
- } catch {
539
- // Ignore
540
- }
541
- });
542
-
543
- clientSocket.on("close", () => {
544
- try {
545
- targetSocket.end();
546
- } catch {
547
- // Ignore
548
- }
549
- });
550
- }
551
-
552
- /**
553
- * Handle regular HTTP proxy requests with per-deployment network config
554
- */
555
- async function handleProxyRequest(
556
- req: http.IncomingMessage,
557
- res: http.ServerResponse
558
- ): Promise<void> {
559
- const targetUrl = req.url;
560
-
561
- if (!targetUrl) {
562
- res.writeHead(400, { "Content-Type": "text/plain" });
563
- res.end("Bad Request: No URL provided\n");
564
- return;
565
- }
566
-
567
- let parsedUrl: URL;
568
- try {
569
- parsedUrl = new URL(targetUrl);
570
- } catch {
571
- res.writeHead(400, { "Content-Type": "text/plain" });
572
- res.end("Bad Request: Invalid URL\n");
573
- return;
574
- }
575
-
576
- const hostname = parsedUrl.hostname;
577
-
578
- // Validate worker token
579
- const auth = validateProxyAuth(req);
580
- if (!auth) {
581
- logger.warn(`Proxy auth required for ${req.method} ${hostname}`);
582
- res.writeHead(407, {
583
- "Content-Type": "text/plain",
584
- "Proxy-Authenticate": 'Basic realm="lobu-proxy"',
585
- });
586
- res.end("407 Proxy Authentication Required\n");
587
- return;
588
- }
589
-
590
- const { deploymentName, tokenData } = auth;
591
-
592
- // Check domain access: global config → grant store
593
- const allowed = await checkDomainAccess(hostname, tokenData.agentId);
594
- if (!allowed) {
595
- logger.warn(
596
- `Blocked request to ${hostname} (deployment: ${deploymentName})`
597
- );
598
- res.writeHead(403, `Domain not allowed: ${hostname}`, {
599
- "Content-Type": "text/plain",
600
- });
601
- res.end(
602
- `403 Forbidden - Domain not allowed: ${hostname}. Network access is configured via lobu.toml or the gateway configuration APIs.\n`
603
- );
604
- return;
605
- }
606
-
607
- const targetResolution = await resolveAndValidateTarget(hostname);
608
- if (!targetResolution.ok) {
609
- logger.warn(
610
- `Blocked request to ${hostname} (deployment: ${deploymentName}) - ${targetResolution.reason}`
611
- );
612
- res.writeHead(targetResolution.statusCode ?? 502, {
613
- "Content-Type": "text/plain",
614
- });
615
- res.end(`${targetResolution.clientMessage}\n`);
616
- return;
617
- }
618
-
619
- const resolvedIp = targetResolution.resolvedIp;
620
- if (!resolvedIp) {
621
- res.writeHead(500, { "Content-Type": "text/plain" });
622
- res.end("Internal proxy error\n");
623
- return;
624
- }
625
-
626
- logger.debug(
627
- `Proxying ${req.method} ${hostname}${parsedUrl.pathname} via ${resolvedIp}`
628
- );
629
-
630
- // Remove proxy-authorization header before forwarding
631
- const forwardHeaders = { ...req.headers };
632
- delete forwardHeaders["proxy-authorization"];
633
-
634
- // Forward the request
635
- const options: http.RequestOptions = {
636
- hostname: resolvedIp,
637
- port: parsedUrl.port || (parsedUrl.protocol === "https:" ? 443 : 80),
638
- path: parsedUrl.pathname + parsedUrl.search,
639
- method: req.method,
640
- headers: forwardHeaders,
641
- };
642
-
643
- const proxyReq = http.request(options, (proxyRes) => {
644
- // Forward response headers
645
- res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
646
- // Stream response body
647
- proxyRes.pipe(res);
648
- });
649
-
650
- proxyReq.on("error", (err) => {
651
- logger.error(`Proxy request error for ${hostname}:`, err.message);
652
- if (!res.headersSent) {
653
- res.writeHead(502, { "Content-Type": "text/plain" });
654
- res.end("Bad Gateway: Could not reach target server\n");
655
- } else {
656
- res.end();
657
- }
658
- });
659
-
660
- // Stream request body
661
- req.pipe(proxyReq);
662
- }
663
-
664
- /**
665
- * Start HTTP proxy server with per-deployment network config support.
666
- *
667
- * Workers identify themselves via Proxy-Authorization Basic auth:
668
- * HTTP_PROXY=http://<deploymentName>:<token>@gateway:8118
669
- *
670
- * The proxy validates the encrypted worker token, cross-checks the
671
- * claimed deployment name, and looks up per-deployment network config.
672
- * Returns 407 if authentication fails.
673
- *
674
- * @param port - Port to listen on (default 8118)
675
- * @param host - Bind address (default "::" for all interfaces)
676
- * @returns Promise that resolves with the server once listening, or rejects on error
677
- */
678
- export function startHttpProxy(
679
- port: number = 8118,
680
- host: string = "::"
681
- ): Promise<http.Server> {
682
- return new Promise((resolve, reject) => {
683
- const global = getGlobalConfig();
684
-
685
- const server = http.createServer((req, res) => {
686
- handleProxyRequest(req, res).catch((err) => {
687
- logger.error("Error handling proxy request:", err);
688
- if (!res.headersSent) {
689
- res.writeHead(500, { "Content-Type": "text/plain" });
690
- res.end("Internal proxy error\n");
691
- }
692
- });
693
- });
694
-
695
- // Handle CONNECT method for HTTPS tunneling
696
- server.on("connect", (req, clientSocket, head) => {
697
- handleConnect(req, clientSocket, head).catch((err) => {
698
- logger.error("Error handling CONNECT:", err);
699
- try {
700
- clientSocket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
701
- clientSocket.end();
702
- } catch {
703
- // Ignore
704
- }
705
- });
706
- });
707
-
708
- server.on("error", (err) => {
709
- logger.error("HTTP proxy server error:", err);
710
- reject(err);
711
- });
712
-
713
- server.listen(port, host, () => {
714
- // Remove the startup error listener so it doesn't reject later operational errors
715
- server.removeAllListeners("error");
716
- server.on("error", (err) => {
717
- logger.error("HTTP proxy server error:", err);
718
- });
719
-
720
- let mode: string;
721
- if (isUnrestrictedMode(global.allowedDomains)) {
722
- mode = "unrestricted";
723
- } else if (global.allowedDomains.length > 0) {
724
- mode = "allowlist";
725
- } else {
726
- mode = "complete-isolation";
727
- }
728
-
729
- logger.debug(
730
- `HTTP proxy started on ${host}:${port} (mode=${mode}, allowed=${global.allowedDomains.length}, denied=${global.deniedDomains.length})`
731
- );
732
- resolve(server);
733
- });
734
- });
735
- }
736
-
737
- /**
738
- * Stop HTTP proxy server
739
- */
740
- export function stopHttpProxy(server: http.Server): Promise<void> {
741
- return new Promise((resolve, reject) => {
742
- server.close((err) => {
743
- if (err) {
744
- logger.error("Error stopping HTTP proxy:", err);
745
- reject(err);
746
- } else {
747
- logger.info("HTTP proxy stopped");
748
- resolve();
749
- }
750
- });
751
- });
752
- }