palaryn 0.1.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/anomaly/detector.d.ts +7 -4
  5. package/dist/src/anomaly/detector.d.ts.map +1 -1
  6. package/dist/src/anomaly/detector.js +22 -12
  7. package/dist/src/anomaly/detector.js.map +1 -1
  8. package/dist/src/audit/logger.d.ts +10 -0
  9. package/dist/src/audit/logger.d.ts.map +1 -1
  10. package/dist/src/audit/logger.js +52 -38
  11. package/dist/src/audit/logger.js.map +1 -1
  12. package/dist/src/auth/routes.d.ts.map +1 -1
  13. package/dist/src/auth/routes.js +35 -0
  14. package/dist/src/auth/routes.js.map +1 -1
  15. package/dist/src/budget/manager.d.ts +5 -0
  16. package/dist/src/budget/manager.d.ts.map +1 -1
  17. package/dist/src/budget/manager.js +32 -0
  18. package/dist/src/budget/manager.js.map +1 -1
  19. package/dist/src/budget/model-pricing.d.ts +20 -0
  20. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  21. package/dist/src/budget/model-pricing.js +107 -0
  22. package/dist/src/budget/model-pricing.js.map +1 -0
  23. package/dist/src/budget/usage-extractor.d.ts +3 -1
  24. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  25. package/dist/src/budget/usage-extractor.js +47 -3
  26. package/dist/src/budget/usage-extractor.js.map +1 -1
  27. package/dist/src/config/defaults.d.ts.map +1 -1
  28. package/dist/src/config/defaults.js +65 -13
  29. package/dist/src/config/defaults.js.map +1 -1
  30. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  31. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  32. package/dist/src/dlp/tool-patterns.js +34 -0
  33. package/dist/src/dlp/tool-patterns.js.map +1 -0
  34. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  35. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  36. package/dist/src/executor/filesystem-executor.js +192 -0
  37. package/dist/src/executor/filesystem-executor.js.map +1 -0
  38. package/dist/src/executor/http-executor.d.ts.map +1 -1
  39. package/dist/src/executor/http-executor.js +22 -2
  40. package/dist/src/executor/http-executor.js.map +1 -1
  41. package/dist/src/executor/index.d.ts +4 -0
  42. package/dist/src/executor/index.d.ts.map +1 -1
  43. package/dist/src/executor/index.js +9 -1
  44. package/dist/src/executor/index.js.map +1 -1
  45. package/dist/src/executor/shell-executor.d.ts +22 -0
  46. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  47. package/dist/src/executor/shell-executor.js +119 -0
  48. package/dist/src/executor/shell-executor.js.map +1 -0
  49. package/dist/src/executor/sql-executor.d.ts +29 -0
  50. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  51. package/dist/src/executor/sql-executor.js +114 -0
  52. package/dist/src/executor/sql-executor.js.map +1 -0
  53. package/dist/src/executor/websocket-executor.d.ts +26 -0
  54. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  55. package/dist/src/executor/websocket-executor.js +205 -0
  56. package/dist/src/executor/websocket-executor.js.map +1 -0
  57. package/dist/src/interceptor/index.d.ts +2 -0
  58. package/dist/src/interceptor/index.d.ts.map +1 -0
  59. package/dist/src/interceptor/index.js +6 -0
  60. package/dist/src/interceptor/index.js.map +1 -0
  61. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  62. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  63. package/dist/src/interceptor/provider-interceptor.js +302 -0
  64. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  65. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  66. package/dist/src/mcp/auth-verifier.js +3 -2
  67. package/dist/src/mcp/auth-verifier.js.map +1 -1
  68. package/dist/src/mcp/bridge.d.ts +14 -10
  69. package/dist/src/mcp/bridge.d.ts.map +1 -1
  70. package/dist/src/mcp/bridge.js +51 -227
  71. package/dist/src/mcp/bridge.js.map +1 -1
  72. package/dist/src/mcp/http-transport.d.ts +2 -0
  73. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  74. package/dist/src/mcp/http-transport.js +117 -66
  75. package/dist/src/mcp/http-transport.js.map +1 -1
  76. package/dist/src/mcp/internal-auth.d.ts +13 -0
  77. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  78. package/dist/src/mcp/internal-auth.js +12 -0
  79. package/dist/src/mcp/internal-auth.js.map +1 -0
  80. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  81. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  82. package/dist/src/mcp/tool-definitions.js +491 -0
  83. package/dist/src/mcp/tool-definitions.js.map +1 -0
  84. package/dist/src/middleware/auth.js.map +1 -1
  85. package/dist/src/middleware/session.js.map +1 -1
  86. package/dist/src/middleware/validate.d.ts +8 -0
  87. package/dist/src/middleware/validate.d.ts.map +1 -1
  88. package/dist/src/middleware/validate.js +45 -0
  89. package/dist/src/middleware/validate.js.map +1 -1
  90. package/dist/src/policy/engine.d.ts +4 -0
  91. package/dist/src/policy/engine.d.ts.map +1 -1
  92. package/dist/src/policy/engine.js +117 -0
  93. package/dist/src/policy/engine.js.map +1 -1
  94. package/dist/src/saas/routes.d.ts.map +1 -1
  95. package/dist/src/saas/routes.js +355 -22
  96. package/dist/src/saas/routes.js.map +1 -1
  97. package/dist/src/server/app.d.ts.map +1 -1
  98. package/dist/src/server/app.js +24 -3
  99. package/dist/src/server/app.js.map +1 -1
  100. package/dist/src/server/gateway.d.ts.map +1 -1
  101. package/dist/src/server/gateway.js +17 -0
  102. package/dist/src/server/gateway.js.map +1 -1
  103. package/dist/src/server/index.d.ts.map +1 -1
  104. package/dist/src/server/index.js +18 -0
  105. package/dist/src/server/index.js.map +1 -1
  106. package/dist/src/storage/interfaces.d.ts +14 -3
  107. package/dist/src/storage/interfaces.d.ts.map +1 -1
  108. package/dist/src/storage/memory.d.ts +2 -0
  109. package/dist/src/storage/memory.d.ts.map +1 -1
  110. package/dist/src/storage/memory.js +6 -0
  111. package/dist/src/storage/memory.js.map +1 -1
  112. package/dist/src/storage/postgres.d.ts +5 -0
  113. package/dist/src/storage/postgres.d.ts.map +1 -1
  114. package/dist/src/storage/postgres.js +16 -0
  115. package/dist/src/storage/postgres.js.map +1 -1
  116. package/dist/src/storage/redis.d.ts +10 -0
  117. package/dist/src/storage/redis.d.ts.map +1 -1
  118. package/dist/src/storage/redis.js +65 -0
  119. package/dist/src/storage/redis.js.map +1 -1
  120. package/dist/src/types/budget.d.ts +4 -0
  121. package/dist/src/types/budget.d.ts.map +1 -1
  122. package/dist/src/types/config.d.ts +58 -0
  123. package/dist/src/types/config.d.ts.map +1 -1
  124. package/dist/src/types/events.d.ts +1 -0
  125. package/dist/src/types/events.d.ts.map +1 -1
  126. package/dist/src/types/policy.d.ts +11 -1
  127. package/dist/src/types/policy.d.ts.map +1 -1
  128. package/dist/src/types/tool-result.d.ts +11 -0
  129. package/dist/src/types/tool-result.d.ts.map +1 -1
  130. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  131. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  132. package/dist/tests/unit/app-routes.test.js +715 -0
  133. package/dist/tests/unit/app-routes.test.js.map +1 -0
  134. package/dist/tests/unit/audit-logger.test.js +105 -0
  135. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  136. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  137. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  138. package/dist/tests/unit/auth-providers.test.js +279 -0
  139. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  140. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  141. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  142. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  143. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  144. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  145. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  146. package/dist/tests/unit/auth-verifier.test.js +505 -0
  147. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  148. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  149. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  150. package/dist/tests/unit/billing-routes.test.js +432 -0
  151. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  152. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  153. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  154. package/dist/tests/unit/config-defaults.test.js +119 -0
  155. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  156. package/dist/tests/unit/defaults.test.js +0 -10
  157. package/dist/tests/unit/defaults.test.js.map +1 -1
  158. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  159. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  160. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  161. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  162. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  163. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  164. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  165. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  166. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  167. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  168. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  169. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  170. package/dist/tests/unit/logger.test.d.ts +2 -0
  171. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  172. package/dist/tests/unit/logger.test.js +97 -0
  173. package/dist/tests/unit/logger.test.js.map +1 -0
  174. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  175. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  176. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  177. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  178. package/dist/tests/unit/metrics.test.js +102 -0
  179. package/dist/tests/unit/metrics.test.js.map +1 -1
  180. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  181. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  182. package/dist/tests/unit/model-pricing.test.js +87 -0
  183. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  184. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  185. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  186. package/dist/tests/unit/oauth-stores.test.js +260 -0
  187. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  188. package/dist/tests/unit/policy-engine.test.js +466 -0
  189. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  190. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  191. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  192. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  193. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  194. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  195. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  196. package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
  197. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  198. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  199. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  200. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  201. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  202. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  203. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  204. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  205. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  206. package/dist/tests/unit/saas-routes.test.js +3 -3
  207. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  208. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  209. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  210. package/dist/tests/unit/shell-executor.test.js +145 -0
  211. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  212. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  213. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  214. package/dist/tests/unit/sql-executor.test.js +177 -0
  215. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  216. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  217. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  218. package/dist/tests/unit/stream-proxy.test.js +147 -0
  219. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  220. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  221. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  222. package/dist/tests/unit/tool-definitions.test.js +184 -0
  223. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  224. package/dist/tests/unit/usage-extractor.test.js +140 -0
  225. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  226. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  227. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  228. package/dist/tests/unit/webhook-handler.test.js +453 -0
  229. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  230. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  231. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  232. package/dist/tests/unit/webhook-routes.test.js +69 -0
  233. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  234. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  235. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  236. package/dist/tests/unit/websocket-executor.test.js +121 -0
  237. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  238. package/package.json +8 -2
  239. package/policy-packs/demo_fail.yaml +41 -0
  240. package/policy-packs/full_tools.yaml +136 -0
  241. package/src/admin/index.ts +1 -0
  242. package/src/admin/routes.ts +509 -0
  243. package/src/admin/templates.ts +572 -0
  244. package/src/anomaly/detector.ts +730 -0
  245. package/src/anomaly/index.ts +1 -0
  246. package/src/approval/manager.ts +569 -0
  247. package/src/approval/webhook.ts +133 -0
  248. package/src/audit/logger.ts +490 -0
  249. package/src/auth/index.ts +5 -0
  250. package/src/auth/password.ts +21 -0
  251. package/src/auth/pkce.ts +22 -0
  252. package/src/auth/providers.ts +208 -0
  253. package/src/auth/routes.ts +561 -0
  254. package/src/auth/session.ts +84 -0
  255. package/src/billing/index.ts +6 -0
  256. package/src/billing/plan-enforcer.ts +135 -0
  257. package/src/billing/routes.ts +229 -0
  258. package/src/billing/stripe-client.ts +58 -0
  259. package/src/billing/webhook-handler.ts +182 -0
  260. package/src/billing/webhook-routes.ts +28 -0
  261. package/src/budget/manager.ts +679 -0
  262. package/src/budget/model-pricing.ts +119 -0
  263. package/src/budget/usage-extractor.ts +214 -0
  264. package/src/cli.ts +91 -0
  265. package/src/config/defaults.ts +261 -0
  266. package/src/config/validate.ts +88 -0
  267. package/src/dlp/composite-scanner.ts +213 -0
  268. package/src/dlp/index.ts +9 -0
  269. package/src/dlp/interfaces.ts +34 -0
  270. package/src/dlp/patterns.ts +30 -0
  271. package/src/dlp/prompt-injection-backend.ts +181 -0
  272. package/src/dlp/prompt-injection-patterns.ts +302 -0
  273. package/src/dlp/regex-backend.ts +181 -0
  274. package/src/dlp/scanner.ts +502 -0
  275. package/src/dlp/text-normalizer.ts +225 -0
  276. package/src/dlp/tool-patterns.ts +35 -0
  277. package/src/dlp/trufflehog-backend.ts +190 -0
  278. package/src/executor/filesystem-executor.ts +196 -0
  279. package/src/executor/http-executor.ts +349 -0
  280. package/src/executor/index.ts +9 -0
  281. package/src/executor/interfaces.ts +11 -0
  282. package/src/executor/noop-executor.ts +23 -0
  283. package/src/executor/registry.ts +64 -0
  284. package/src/executor/shell-executor.ts +148 -0
  285. package/src/executor/slack-executor.ts +176 -0
  286. package/src/executor/sql-executor.ts +146 -0
  287. package/src/executor/websocket-executor.ts +211 -0
  288. package/src/index.ts +24 -0
  289. package/src/interceptor/index.ts +1 -0
  290. package/src/interceptor/provider-interceptor.ts +315 -0
  291. package/src/mcp/auth-verifier.ts +152 -0
  292. package/src/mcp/bridge.ts +703 -0
  293. package/src/mcp/http-transport.ts +698 -0
  294. package/src/mcp/index.ts +9 -0
  295. package/src/mcp/internal-auth.ts +14 -0
  296. package/src/mcp/oauth-pages.ts +139 -0
  297. package/src/mcp/oauth-postgres-stores.ts +278 -0
  298. package/src/mcp/oauth-provider.ts +536 -0
  299. package/src/mcp/oauth-stores.ts +202 -0
  300. package/src/mcp/server.ts +55 -0
  301. package/src/mcp/tool-definitions.ts +562 -0
  302. package/src/metrics/collector.ts +357 -0
  303. package/src/metrics/index.ts +1 -0
  304. package/src/middleware/auth.ts +814 -0
  305. package/src/middleware/session.ts +85 -0
  306. package/src/middleware/validate.ts +130 -0
  307. package/src/policy/engine.ts +815 -0
  308. package/src/policy/index.ts +2 -0
  309. package/src/policy/opa-engine.ts +829 -0
  310. package/src/proxy/forward-proxy.ts +649 -0
  311. package/src/proxy/index.ts +1 -0
  312. package/src/ratelimit/limiter.ts +196 -0
  313. package/src/replay/engine.ts +142 -0
  314. package/src/replay/index.ts +1 -0
  315. package/src/saas/index.ts +1 -0
  316. package/src/saas/routes.ts +2178 -0
  317. package/src/server/app.ts +985 -0
  318. package/src/server/errors.ts +49 -0
  319. package/src/server/gateway.ts +1130 -0
  320. package/src/server/index.ts +307 -0
  321. package/src/server/logger.ts +255 -0
  322. package/src/server/stream-proxy.ts +202 -0
  323. package/src/storage/file-persistence.ts +315 -0
  324. package/src/storage/index.ts +4 -0
  325. package/src/storage/interfaces.ts +287 -0
  326. package/src/storage/memory.ts +686 -0
  327. package/src/storage/postgres.ts +1831 -0
  328. package/src/storage/redis.ts +835 -0
  329. package/src/tracing/index.ts +1 -0
  330. package/src/tracing/provider.ts +100 -0
  331. package/src/trust/calculator.ts +141 -0
  332. package/src/trust/index.ts +7 -0
  333. package/src/types/budget.ts +36 -0
  334. package/src/types/config.ts +278 -0
  335. package/src/types/events.ts +41 -0
  336. package/src/types/express.d.ts +14 -0
  337. package/src/types/index.ts +7 -0
  338. package/src/types/policy.ts +83 -0
  339. package/src/types/stripe-config.ts +11 -0
  340. package/src/types/subscription.ts +59 -0
  341. package/src/types/tool-call.ts +47 -0
  342. package/src/types/tool-result.ts +82 -0
  343. package/src/types/user.ts +125 -0
  344. package/tsconfig.json +24 -0
@@ -0,0 +1,649 @@
1
+ import * as http from 'http';
2
+ import * as https from 'https';
3
+ import * as net from 'net';
4
+ import * as dns from 'dns';
5
+ import { URL } from 'url';
6
+ import { randomUUID } from 'crypto';
7
+ import { Gateway, PreExecuteResult } from '../server/gateway';
8
+ import { GatewayConfig, ProxyConfig } from '../types/config';
9
+ import { ToolCall } from '../types/tool-call';
10
+ import { parseProxyAuth } from '../middleware/auth';
11
+ import { isPrivateIP } from '../executor/http-executor';
12
+ import { logger } from '../server/logger';
13
+
14
+ /** Maximum accumulated response body for post-DLP scanning (50 MB) */
15
+ const MAX_ACCUMULATED_BYTES = 50 * 1024 * 1024;
16
+
17
+ /** DNS lookup timeout in ms */
18
+ const DNS_LOOKUP_TIMEOUT_MS = 5000;
19
+
20
+ /** Maximum request body size in bytes (10 MB) — prevents OOM DoS */
21
+ const MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
22
+
23
+ /** Global counter for total bytes being accumulated across all active proxy responses */
24
+ let globalAccumulatedBytes = 0;
25
+ /** Maximum total bytes accumulated across all concurrent proxy responses (500 MB) */
26
+ const MAX_GLOBAL_ACCUMULATED_BYTES = 500 * 1024 * 1024;
27
+
28
+ /** Headers that must never be forwarded or logged */
29
+ const SENSITIVE_HEADERS = new Set([
30
+ 'proxy-authorization', 'proxy-connection',
31
+ 'x-palaryn-workspace', 'x-palaryn-actor',
32
+ 'authorization', 'cookie', 'x-api-key',
33
+ ]);
34
+
35
+ /** Map HTTP method to tool capability */
36
+ function methodToCapability(method: string): 'read' | 'write' | 'delete' | 'admin' {
37
+ switch (method.toUpperCase()) {
38
+ case 'GET':
39
+ case 'HEAD':
40
+ case 'OPTIONS':
41
+ return 'read';
42
+ case 'DELETE':
43
+ return 'delete';
44
+ default:
45
+ return 'write';
46
+ }
47
+ }
48
+
49
+ /** Check if a hostname matches a domain pattern (supports leading wildcard) */
50
+ function matchesDomain(hostname: string, pattern: string): boolean {
51
+ // Strip null bytes and normalize to prevent injection attacks
52
+ const clean = hostname.replace(/\0/g, '').toLowerCase().trim();
53
+ const cleanPattern = pattern.replace(/\0/g, '').toLowerCase().trim();
54
+ if (!clean || !cleanPattern) return false;
55
+ if (cleanPattern === clean) return true;
56
+ if (cleanPattern.startsWith('*.')) {
57
+ const suffix = cleanPattern.slice(2);
58
+ return clean === suffix || clean.endsWith('.' + suffix);
59
+ }
60
+ return false;
61
+ }
62
+
63
+ /** Check if a hostname matches any passthrough domain */
64
+ function isPassthrough(hostname: string, patterns: string[]): boolean {
65
+ return patterns.some(p => matchesDomain(hostname, p));
66
+ }
67
+
68
+ /**
69
+ * Validate resolved IP is not private (SSRF protection) with DNS timeout.
70
+ * Returns the first valid resolved IP address for DNS pinning — the caller
71
+ * should connect to this IP instead of the hostname to prevent DNS rebinding.
72
+ */
73
+ export function validateResolvedIP(hostname: string): Promise<string> {
74
+ if (net.isIP(hostname)) {
75
+ if (isPrivateIP(hostname)) {
76
+ return Promise.reject(new Error(`SSRF blocked: IP ${hostname} is a private/reserved address`));
77
+ }
78
+ return Promise.resolve(hostname);
79
+ }
80
+
81
+ const dnsPromise = new Promise<string>((resolve, reject) => {
82
+ dns.lookup(hostname, { all: true }, (err, addresses) => {
83
+ if (err) return reject(err);
84
+ if (!addresses || addresses.length === 0) {
85
+ return reject(new Error(`DNS lookup failed: no addresses found for "${hostname}"`));
86
+ }
87
+ for (const addr of addresses) {
88
+ if (isPrivateIP(addr.address)) {
89
+ return reject(
90
+ new Error(`SSRF blocked: hostname "${hostname}" resolved to private IP ${addr.address}`)
91
+ );
92
+ }
93
+ }
94
+ // Return the first resolved IP for DNS pinning
95
+ resolve(addresses[0].address);
96
+ });
97
+ });
98
+
99
+ const timeoutPromise = new Promise<never>((_, reject) => {
100
+ setTimeout(() => reject(new Error(`DNS lookup timeout for "${hostname}" after ${DNS_LOOKUP_TIMEOUT_MS}ms`)), DNS_LOOKUP_TIMEOUT_MS);
101
+ });
102
+
103
+ return Promise.race([dnsPromise, timeoutPromise]);
104
+ }
105
+
106
+ /** Strip sensitive headers from a headers record */
107
+ function stripSensitiveHeaders(headers: Record<string, string>): Record<string, string> {
108
+ const result: Record<string, string> = {};
109
+ for (const [key, value] of Object.entries(headers)) {
110
+ if (!SENSITIVE_HEADERS.has(key.toLowerCase())) {
111
+ result[key] = value;
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+
117
+ /** Build a synthetic ToolCall from an incoming proxy request */
118
+ export function buildToolCallFromProxy(
119
+ method: string,
120
+ targetUrl: string,
121
+ headers: Record<string, string>,
122
+ body: string | undefined,
123
+ workspaceId: string,
124
+ actorId: string,
125
+ ): ToolCall {
126
+ const parsedUrl = new URL(targetUrl);
127
+ const toolName = `http.${method.toLowerCase()}`;
128
+
129
+ // Remove sensitive headers before forwarding
130
+ const forwardHeaders = stripSensitiveHeaders(headers);
131
+
132
+ // Parse body if it looks like JSON
133
+ let parsedBody: unknown = body;
134
+ if (body && typeof body === 'string') {
135
+ try {
136
+ parsedBody = JSON.parse(body);
137
+ } catch (_e) {
138
+ /* Not valid JSON — keep body as raw string */
139
+ }
140
+ }
141
+
142
+ return {
143
+ tool_call_id: randomUUID(),
144
+ task_id: randomUUID(),
145
+ workspace_id: workspaceId,
146
+ actor: {
147
+ type: 'agent',
148
+ id: actorId,
149
+ },
150
+ source: {
151
+ platform: 'forward_proxy',
152
+ },
153
+ tool: {
154
+ name: toolName,
155
+ capability: methodToCapability(method),
156
+ },
157
+ args: {
158
+ method: method.toUpperCase(),
159
+ url: targetUrl,
160
+ headers: forwardHeaders,
161
+ body: parsedBody,
162
+ },
163
+ context: {
164
+ purpose: 'Proxied HTTP request',
165
+ labels: ['proxy', parsedUrl.hostname],
166
+ },
167
+ };
168
+ }
169
+
170
+ /** Forward the request to the target and pipe the response back.
171
+ * When pinnedIP is provided (from SSRF DNS validation), the TCP connection
172
+ * goes to that IP while the Host header and TLS SNI use the original hostname,
173
+ * preventing DNS rebinding attacks.
174
+ */
175
+ function forwardRequest(
176
+ method: string,
177
+ targetUrl: string,
178
+ headers: Record<string, string>,
179
+ body: string | undefined,
180
+ clientRes: http.ServerResponse,
181
+ gateway: Gateway,
182
+ toolCall: ToolCall,
183
+ pre: PreExecuteResult,
184
+ timeoutMs: number,
185
+ pinnedIP?: string,
186
+ ): void {
187
+ const parsedUrl = new URL(targetUrl);
188
+ const isHttps = parsedUrl.protocol === 'https:';
189
+ const transport = isHttps ? https : http;
190
+
191
+ // Strip sensitive headers, then set host
192
+ const forwardHeaders = stripSensitiveHeaders(headers);
193
+ forwardHeaders['host'] = parsedUrl.host;
194
+
195
+ const requestOptions: http.RequestOptions & { servername?: string } = {
196
+ // Use pinned IP for the actual connection to prevent DNS rebinding
197
+ hostname: pinnedIP || parsedUrl.hostname,
198
+ port: parsedUrl.port || (isHttps ? 443 : 80),
199
+ path: parsedUrl.pathname + parsedUrl.search,
200
+ method: method.toUpperCase(),
201
+ headers: forwardHeaders,
202
+ timeout: timeoutMs,
203
+ };
204
+
205
+ // When using a pinned IP with HTTPS, set servername for correct TLS SNI
206
+ if (pinnedIP && isHttps) {
207
+ requestOptions.servername = parsedUrl.hostname;
208
+ }
209
+
210
+ // Pin DNS resolution to the pre-validated IP to prevent TOCTOU rebinding
211
+ if (pinnedIP) {
212
+ const family = pinnedIP.includes(':') ? 6 : 4;
213
+ const agent = new (isHttps ? https : http).Agent({
214
+ lookup: (_hostname: string, _options: any, callback: Function) => {
215
+ callback(null, pinnedIP, family);
216
+ },
217
+ } as any);
218
+ requestOptions.agent = agent;
219
+ }
220
+
221
+ const proxyReq = transport.request(requestOptions, (upstreamRes) => {
222
+ const httpStatus = upstreamRes.statusCode || 502;
223
+
224
+ // Collect response headers
225
+ const responseHeaders: Record<string, string> = {};
226
+ for (const [key, value] of Object.entries(upstreamRes.headers)) {
227
+ if (value) responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
228
+ }
229
+
230
+ // Remove hop-by-hop headers
231
+ delete responseHeaders['transfer-encoding'];
232
+ delete responseHeaders['connection'];
233
+ delete responseHeaders['keep-alive'];
234
+ delete responseHeaders['proxy-authenticate'];
235
+ delete responseHeaders['proxy-authorization'];
236
+ delete responseHeaders['upgrade'];
237
+
238
+ // Write status and headers to client
239
+ clientRes.writeHead(httpStatus, responseHeaders);
240
+
241
+ // Accumulate response body for post-DLP
242
+ let accumulated = '';
243
+ let accumulatedBytes = 0;
244
+ let truncated = false;
245
+
246
+ upstreamRes.on('data', (chunk: Buffer) => {
247
+ clientRes.write(chunk);
248
+ accumulatedBytes += chunk.length;
249
+ globalAccumulatedBytes += chunk.length;
250
+ if (accumulatedBytes <= MAX_ACCUMULATED_BYTES && globalAccumulatedBytes <= MAX_GLOBAL_ACCUMULATED_BYTES) {
251
+ accumulated += chunk.toString();
252
+ } else if (!truncated) {
253
+ truncated = true;
254
+ const reason = globalAccumulatedBytes > MAX_GLOBAL_ACCUMULATED_BYTES
255
+ ? `global accumulated bytes (${MAX_GLOBAL_ACCUMULATED_BYTES})`
256
+ : `per-request limit (${MAX_ACCUMULATED_BYTES})`;
257
+ logger.warn(`Response body truncated at ${reason} bytes for DLP scan`, { component: 'forward-proxy', target: targetUrl });
258
+ }
259
+ });
260
+
261
+ upstreamRes.on('end', () => {
262
+ clientRes.end();
263
+ globalAccumulatedBytes = Math.max(0, globalAccumulatedBytes - accumulatedBytes);
264
+
265
+ // Run post-execute pipeline in background (DLP scan, budget recording, audit)
266
+ gateway.postExecute(toolCall, {
267
+ http_status: httpStatus,
268
+ body: accumulated,
269
+ headers: responseHeaders,
270
+ }, pre).catch(err => {
271
+ logger.error('postExecute failed', { component: 'forward-proxy', error: err instanceof Error ? err.message : String(err) });
272
+ });
273
+ });
274
+
275
+ upstreamRes.on('error', (err) => {
276
+ // Release accumulated bytes to prevent counter leak on error
277
+ globalAccumulatedBytes = Math.max(0, globalAccumulatedBytes - accumulatedBytes);
278
+ if (!clientRes.headersSent) {
279
+ clientRes.writeHead(502, { 'Content-Type': 'application/json' });
280
+ }
281
+ clientRes.end(JSON.stringify({ error: `Upstream error: ${err.message}` }));
282
+ });
283
+ });
284
+
285
+ proxyReq.on('error', (err) => {
286
+ if (!clientRes.headersSent) {
287
+ clientRes.writeHead(502, { 'Content-Type': 'application/json' });
288
+ }
289
+ clientRes.end(JSON.stringify({ error: `Proxy connection failed: ${err.message}` }));
290
+ });
291
+
292
+ proxyReq.on('timeout', () => {
293
+ proxyReq.destroy();
294
+ if (!clientRes.headersSent) {
295
+ clientRes.writeHead(504, { 'Content-Type': 'application/json' });
296
+ }
297
+ clientRes.end(JSON.stringify({ error: `Upstream request timeout after ${timeoutMs}ms` }));
298
+ });
299
+
300
+ // Handle client disconnect
301
+ clientRes.on('close', () => {
302
+ if (!proxyReq.destroyed) {
303
+ proxyReq.destroy();
304
+ }
305
+ });
306
+
307
+ if (body) {
308
+ proxyReq.write(body);
309
+ }
310
+ proxyReq.end();
311
+ }
312
+
313
+ /**
314
+ * Handle a CONNECT tunnel request (HTTPS).
315
+ * Phase 1: Domain-level policy only — no content inspection.
316
+ * Builds a synthetic ToolCall for policy evaluation, then either
317
+ * establishes a TCP tunnel or returns 403.
318
+ */
319
+ async function handleConnect(
320
+ req: http.IncomingMessage,
321
+ socket: net.Socket,
322
+ head: Buffer,
323
+ gateway: Gateway,
324
+ config: GatewayConfig,
325
+ proxyConfig: ProxyConfig,
326
+ ): Promise<void> {
327
+ const target = req.url || '';
328
+ const [hostname, portStr] = target.split(':');
329
+ const port = parseInt(portStr, 10) || 443;
330
+
331
+ // Auth
332
+ const headers: Record<string, string> = {};
333
+ for (const [key, value] of Object.entries(req.headers)) {
334
+ if (typeof value === 'string') headers[key] = value;
335
+ }
336
+
337
+ const auth = parseProxyAuth(headers, proxyConfig, config.auth);
338
+ if (!auth.authenticated) {
339
+ socket.write('HTTP/1.1 407 Proxy Authentication Required\r\n');
340
+ socket.write('Proxy-Authenticate: Basic realm="Palaryn Proxy"\r\n');
341
+ socket.write('Content-Type: application/json\r\n\r\n');
342
+ socket.write(JSON.stringify({ error: auth.error }));
343
+ socket.end();
344
+ return;
345
+ }
346
+
347
+ // Passthrough domains skip policy entirely
348
+ if (proxyConfig.passthrough_domains && isPassthrough(hostname, proxyConfig.passthrough_domains)) {
349
+ return establishTunnel(hostname, port, socket, head);
350
+ }
351
+
352
+ // Build a synthetic ToolCall for domain-level policy evaluation
353
+ const toolCall = buildToolCallFromProxy(
354
+ 'CONNECT',
355
+ `https://${hostname}:${port}`,
356
+ headers,
357
+ undefined,
358
+ auth.workspace_id!,
359
+ auth.actor_id!,
360
+ );
361
+
362
+ // Run pre-execute pipeline (policy, rate limit, etc.)
363
+ try {
364
+ const pre = await gateway.preExecute(toolCall);
365
+ if (!pre.allowed) {
366
+ const reason = pre.result?.error || pre.result?.policy?.reasons?.join(', ') || 'Blocked by policy';
367
+ socket.write('HTTP/1.1 403 Forbidden\r\n');
368
+ socket.write('Content-Type: application/json\r\n\r\n');
369
+ socket.write(JSON.stringify({ error: reason }));
370
+ socket.end();
371
+ return;
372
+ }
373
+ } catch (err) {
374
+ const message = err instanceof Error ? err.message : 'Policy evaluation failed';
375
+ socket.write('HTTP/1.1 500 Internal Server Error\r\n');
376
+ socket.write('Content-Type: application/json\r\n\r\n');
377
+ socket.write(JSON.stringify({ error: message }));
378
+ socket.end();
379
+ return;
380
+ }
381
+
382
+ // SSRF protection with DNS pinning
383
+ let connectHost = hostname;
384
+ if (proxyConfig.ssrf_protection !== false) {
385
+ try {
386
+ connectHost = await validateResolvedIP(hostname);
387
+ } catch (err) {
388
+ const message = err instanceof Error ? err.message : 'SSRF blocked';
389
+ socket.write('HTTP/1.1 403 Forbidden\r\n');
390
+ socket.write('Content-Type: application/json\r\n\r\n');
391
+ socket.write(JSON.stringify({ error: message }));
392
+ socket.end();
393
+ return;
394
+ }
395
+ }
396
+
397
+ return establishTunnel(connectHost, port, socket, head);
398
+ }
399
+
400
+ /** Establish a raw TCP tunnel for CONNECT requests */
401
+ function establishTunnel(
402
+ hostname: string,
403
+ port: number,
404
+ clientSocket: net.Socket,
405
+ head: Buffer,
406
+ ): Promise<void> {
407
+ return new Promise((resolve, reject) => {
408
+ const upstream = net.connect(port, hostname, () => {
409
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
410
+ if (head.length > 0) {
411
+ upstream.write(head);
412
+ }
413
+ upstream.pipe(clientSocket);
414
+ clientSocket.pipe(upstream);
415
+ resolve();
416
+ });
417
+
418
+ upstream.on('error', (err) => {
419
+ clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
420
+ clientSocket.end();
421
+ resolve(); // Don't reject — just close the tunnel
422
+ });
423
+
424
+ clientSocket.on('error', () => {
425
+ upstream.destroy();
426
+ resolve();
427
+ });
428
+
429
+ // Timeout for tunnel establishment
430
+ upstream.setTimeout(30000, () => {
431
+ upstream.destroy();
432
+ clientSocket.write('HTTP/1.1 504 Gateway Timeout\r\n\r\n');
433
+ clientSocket.end();
434
+ resolve();
435
+ });
436
+ });
437
+ }
438
+
439
+ export interface ForwardProxyServer {
440
+ server: http.Server;
441
+ listen: (port: number, callback?: () => void) => http.Server;
442
+ close: (callback?: (err?: Error) => void) => http.Server;
443
+ }
444
+
445
+ /**
446
+ * Create a forward HTTP proxy server that routes all requests through
447
+ * the Palaryn gateway pipeline (policy, DLP, budget, audit).
448
+ *
449
+ * Usage:
450
+ * const proxy = createForwardProxy(gateway, config);
451
+ * proxy.listen(3128);
452
+ *
453
+ * Clients configure:
454
+ * HTTP_PROXY=http://workspace:apikey@localhost:3128
455
+ */
456
+ export function createForwardProxy(
457
+ gateway: Gateway,
458
+ config: GatewayConfig,
459
+ ): ForwardProxyServer {
460
+ const proxyConfig = config.proxy || {
461
+ enabled: true,
462
+ port: 3128,
463
+ require_auth: true,
464
+ };
465
+
466
+ const timeoutMs = config.executor?.http?.timeout_ms || 15000;
467
+ const ssrfEnabled = proxyConfig.ssrf_protection !== false; // default: true
468
+
469
+ const server = http.createServer(async (req, res) => {
470
+ try {
471
+ // Proxy requests have absolute URLs: GET http://example.com/path HTTP/1.1
472
+ const targetUrl = req.url || '';
473
+
474
+ // Non-proxy requests (relative URL) get a 400
475
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
476
+ res.writeHead(400, { 'Content-Type': 'application/json' });
477
+ res.end(JSON.stringify({
478
+ error: 'Not a proxy request. Use this server as an HTTP proxy (HTTP_PROXY env var).',
479
+ }));
480
+ return;
481
+ }
482
+
483
+ const method = (req.method || 'GET').toUpperCase();
484
+ const parsedUrl = new URL(targetUrl);
485
+
486
+ // Extract headers as flat record
487
+ const headers: Record<string, string> = {};
488
+ for (const [key, value] of Object.entries(req.headers)) {
489
+ if (typeof value === 'string') headers[key] = value;
490
+ else if (Array.isArray(value)) headers[key] = value.join(', ');
491
+ }
492
+
493
+ // Auth
494
+ const auth = parseProxyAuth(headers, proxyConfig, config.auth);
495
+ if (!auth.authenticated) {
496
+ res.writeHead(407, {
497
+ 'Content-Type': 'application/json',
498
+ 'Proxy-Authenticate': 'Basic realm="Palaryn Proxy"',
499
+ });
500
+ res.end(JSON.stringify({ error: auth.error }));
501
+ return;
502
+ }
503
+
504
+ // Passthrough domains skip policy eval and DLP, but still run rate limiting and audit
505
+ if (proxyConfig.passthrough_domains && isPassthrough(parsedUrl.hostname, proxyConfig.passthrough_domains)) {
506
+ const bodyChunks: Buffer[] = [];
507
+ let requestBodyBytes = 0;
508
+ let bodyAborted = false;
509
+ req.on('data', (chunk: Buffer) => {
510
+ if (bodyAborted) return;
511
+ requestBodyBytes += chunk.length;
512
+ if (requestBodyBytes > MAX_REQUEST_BODY_BYTES) {
513
+ bodyAborted = true;
514
+ req.destroy();
515
+ res.writeHead(413, { 'Content-Type': 'application/json' });
516
+ res.end(JSON.stringify({ error: 'Request body too large', error_code: 'VALIDATION_FAILED' }));
517
+ return;
518
+ }
519
+ bodyChunks.push(chunk);
520
+ });
521
+ req.on('end', async () => {
522
+ if (bodyAborted) return;
523
+ const body = bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString() : undefined;
524
+ const toolCall = buildToolCallFromProxy(method, targetUrl, headers, body, auth.workspace_id!, auth.actor_id!);
525
+
526
+ // Rate limit check even for passthrough domains
527
+ const rateLimiter = gateway.getRateLimiter();
528
+ const rateLimitResult = rateLimiter.check(toolCall);
529
+ if (!rateLimitResult.allowed) {
530
+ res.writeHead(429, { 'Content-Type': 'application/json' });
531
+ res.end(JSON.stringify({
532
+ error: `Rate limit exceeded (${rateLimitResult.blocked_by}): ${rateLimitResult.current}/${rateLimitResult.limit} requests in window`,
533
+ error_code: 'RATE_LIMIT_EXCEEDED',
534
+ }));
535
+ return;
536
+ }
537
+
538
+ // Audit logging for passthrough domains
539
+ const auditLogger = gateway.getAuditLogger();
540
+ auditLogger.logToolCallReceived(toolCall);
541
+
542
+ const pre: PreExecuteResult = { allowed: true, stepTimings: {}, startTime: Date.now() };
543
+ forwardRequest(method, targetUrl, headers, body, res, gateway, toolCall, pre, timeoutMs);
544
+ });
545
+ return;
546
+ }
547
+
548
+ // Read request body with size limit
549
+ const bodyChunks: Buffer[] = [];
550
+ let requestBodyBytes = 0;
551
+ let bodyAborted = false;
552
+ req.on('data', (chunk: Buffer) => {
553
+ if (bodyAborted) return;
554
+ requestBodyBytes += chunk.length;
555
+ if (requestBodyBytes > MAX_REQUEST_BODY_BYTES) {
556
+ bodyAborted = true;
557
+ req.destroy();
558
+ res.writeHead(413, { 'Content-Type': 'application/json' });
559
+ res.end(JSON.stringify({ error: 'Request body too large', error_code: 'VALIDATION_FAILED' }));
560
+ return;
561
+ }
562
+ bodyChunks.push(chunk);
563
+ });
564
+ req.on('end', async () => {
565
+ if (bodyAborted) return;
566
+ const body = bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString() : undefined;
567
+
568
+ // Build synthetic ToolCall
569
+ const toolCall = buildToolCallFromProxy(
570
+ method,
571
+ targetUrl,
572
+ headers,
573
+ body,
574
+ auth.workspace_id!,
575
+ auth.actor_id!,
576
+ );
577
+
578
+ // SSRF protection with DNS pinning
579
+ let pinnedIP: string | undefined;
580
+ if (ssrfEnabled) {
581
+ try {
582
+ pinnedIP = await validateResolvedIP(parsedUrl.hostname);
583
+ } catch (err) {
584
+ const message = err instanceof Error ? err.message : 'SSRF blocked';
585
+ res.writeHead(403, { 'Content-Type': 'application/json' });
586
+ res.end(JSON.stringify({ error: message }));
587
+ return;
588
+ }
589
+ }
590
+
591
+ // Run pre-execute pipeline (rate limit, anomaly, policy, DLP, budget)
592
+ try {
593
+ const pre = await gateway.preExecute(toolCall);
594
+
595
+ if (!pre.allowed) {
596
+ const status = pre.result?.status === 'needs_approval' ? 202 : 403;
597
+ res.writeHead(status, { 'Content-Type': 'application/json' });
598
+ res.end(JSON.stringify(pre.result));
599
+ return;
600
+ }
601
+
602
+ // Forward the request to the target
603
+ // Use the processed tool call args (may have been transformed by policy)
604
+ const forwardBody = pre.processedToolCall
605
+ ? (typeof pre.processedToolCall.args.body === 'string'
606
+ ? pre.processedToolCall.args.body
607
+ : pre.processedToolCall.args.body ? JSON.stringify(pre.processedToolCall.args.body) : body)
608
+ : body;
609
+ const forwardHeaders = pre.processedToolCall?.args.headers as Record<string, string> || headers;
610
+
611
+ forwardRequest(method, targetUrl, forwardHeaders, forwardBody, res, gateway, toolCall, pre, timeoutMs, pinnedIP);
612
+
613
+ } catch (err) {
614
+ const message = err instanceof Error ? err.message : 'Internal proxy error';
615
+ if (!res.headersSent) {
616
+ res.writeHead(500, { 'Content-Type': 'application/json' });
617
+ res.end(JSON.stringify({ error: message }));
618
+ }
619
+ }
620
+ });
621
+
622
+ } catch (err) {
623
+ const message = err instanceof Error ? err.message : 'Internal proxy error';
624
+ if (!res.headersSent) {
625
+ res.writeHead(500, { 'Content-Type': 'application/json' });
626
+ res.end(JSON.stringify({ error: message }));
627
+ }
628
+ }
629
+ });
630
+
631
+ // Handle CONNECT method for HTTPS tunneling
632
+ server.on('connect', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
633
+ handleConnect(req, socket, head, gateway, config, proxyConfig).catch(err => {
634
+ logger.error('CONNECT handler error', { component: 'forward-proxy', error: err instanceof Error ? err.message : String(err) });
635
+ try {
636
+ socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
637
+ socket.end();
638
+ } catch (e) {
639
+ /* Socket already closed — expected when client disconnects */
640
+ }
641
+ });
642
+ });
643
+
644
+ return {
645
+ server,
646
+ listen: (port: number, callback?: () => void) => server.listen(port, callback),
647
+ close: (callback?: (err?: Error) => void) => server.close(callback),
648
+ };
649
+ }
@@ -0,0 +1 @@
1
+ export { createForwardProxy, buildToolCallFromProxy, validateResolvedIP, ForwardProxyServer } from './forward-proxy';