@xopcai/xopc 0.0.84 → 0.0.86

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 (418) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
  3. package/dist/extensions/feishu/src/plugin.d.ts +2 -0
  4. package/dist/extensions/feishu/src/plugin.js +10 -0
  5. package/dist/extensions/feishu/src/plugin.js.map +1 -1
  6. package/dist/extensions/feishu/src/workflow-progress.d.ts +27 -0
  7. package/dist/extensions/feishu/src/workflow-progress.js +99 -0
  8. package/dist/extensions/feishu/src/workflow-progress.js.map +1 -0
  9. package/dist/extensions/telegram/src/plugin.d.ts +2 -0
  10. package/dist/extensions/telegram/src/plugin.js +11 -1
  11. package/dist/extensions/telegram/src/plugin.js.map +1 -1
  12. package/dist/extensions/telegram/src/routing-integration.js +2 -2
  13. package/dist/extensions/telegram/src/workflow-progress.d.ts +24 -0
  14. package/dist/extensions/telegram/src/workflow-progress.js +73 -0
  15. package/dist/extensions/telegram/src/workflow-progress.js.map +1 -0
  16. package/dist/extensions/telegram/xopc.extension.json +1 -1
  17. package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js +158 -0
  18. package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js.map +1 -0
  19. package/dist/extensions/weixin/src/api/api.js +2 -2
  20. package/dist/extensions/weixin/src/auth/accounts.js +1 -1
  21. package/dist/extensions/weixin/src/cdn/upload.js +1 -1
  22. package/dist/extensions/weixin/src/media/data-url.js +1 -1
  23. package/dist/extensions/weixin/src/messaging/debug-mode.js +1 -1
  24. package/dist/extensions/weixin/src/messaging/inbound.js +1 -1
  25. package/dist/extensions/weixin/src/messaging/process-message.js +1 -1
  26. package/dist/extensions/weixin/src/plugin.d.ts +2 -0
  27. package/dist/extensions/weixin/src/plugin.js +11 -1
  28. package/dist/extensions/weixin/src/plugin.js.map +1 -1
  29. package/dist/extensions/weixin/src/storage/sync-buf.js +1 -1
  30. package/dist/extensions/weixin/src/workflow-progress.d.ts +26 -0
  31. package/dist/extensions/weixin/src/workflow-progress.js +99 -0
  32. package/dist/extensions/weixin/src/workflow-progress.js.map +1 -0
  33. package/dist/gateway/static/root/assets/agents-mS3_HpRI.js +222 -0
  34. package/dist/gateway/static/root/assets/apps-page-DrfytjOb.js +1 -0
  35. package/dist/gateway/static/root/assets/channels-settings-BG6b9KrW.js +1 -0
  36. package/dist/gateway/static/root/assets/channels-status-swr-Bs5kMCMI.js +8 -0
  37. package/dist/gateway/static/root/assets/createLucideIcon-DPHK1VkS.js +1 -0
  38. package/dist/gateway/static/root/assets/cron-api-BuVcZ5zR.js +1 -0
  39. package/dist/gateway/static/root/assets/cron-page-BMrloeFH.js +1 -0
  40. package/dist/gateway/static/root/assets/dist-BTWC-BTN.js +45 -0
  41. package/dist/gateway/static/root/assets/{dist-CqNMNhJM.js → dist-CKU1OOTf.js} +1 -1
  42. package/dist/gateway/static/root/assets/{extension-debug-page-gf2L0kY_.js → extension-debug-page-BdW_46sN.js} +1 -1
  43. package/dist/gateway/static/root/assets/extension-page-DW47KI82.js +1 -0
  44. package/dist/gateway/static/root/assets/extension-settings-page-B-W4x2xP.js +1 -0
  45. package/dist/gateway/static/root/assets/fetch-B2MYHbWg.js +1 -0
  46. package/dist/gateway/static/root/assets/{field-primitives-DTtlp-l8.js → field-primitives-DPG-oJmx.js} +1 -1
  47. package/dist/gateway/static/root/assets/heartbeat-config-api-C8dNts9i.js +1 -0
  48. package/dist/gateway/static/root/assets/index-BmVYculr.js +4700 -0
  49. package/dist/gateway/static/root/assets/index-ew_2L2We.css +1 -0
  50. package/dist/gateway/static/root/assets/logs-page-sTsVWz0X.js +1 -0
  51. package/dist/gateway/static/root/assets/sessions-page-FaG_Vlkb.js +1 -0
  52. package/dist/gateway/static/root/assets/settings-form-section-DuvRQW--.js +1 -0
  53. package/dist/gateway/static/root/assets/settings-page-Bet1OerL.js +3 -0
  54. package/dist/gateway/static/root/assets/share-preview-page-BtG2kLDh.js +2 -0
  55. package/dist/gateway/static/root/assets/skills-page-DhUO235y.js +2 -0
  56. package/dist/gateway/static/root/assets/theme-store-DryYl3qD.js +1 -0
  57. package/dist/gateway/static/root/assets/url-BwNL6Rgk.js +3 -0
  58. package/dist/gateway/static/root/assets/utils-BY7bU1DT.js +1 -0
  59. package/dist/gateway/static/root/assets/voice-api-key-field-CGEydndO.js +1 -0
  60. package/dist/gateway/static/root/index.html +7 -6
  61. package/dist/package.js +1 -1
  62. package/dist/src/agent/agent-manager.js +7 -7
  63. package/dist/src/agent/bootstrap/load-bootstrap-files.js +1 -1
  64. package/dist/src/agent/context/workspace-seed.js +3 -3
  65. package/dist/src/agent/embedded/map-stream-events.js +6 -0
  66. package/dist/src/agent/embedded/map-stream-events.js.map +1 -1
  67. package/dist/src/agent/embedded/subscribe-session.js +24 -0
  68. package/dist/src/agent/embedded/subscribe-session.js.map +1 -1
  69. package/dist/src/agent/embedded/types.d.ts +19 -0
  70. package/dist/src/agent/goals/goal-locale.js +2 -2
  71. package/dist/src/agent/goals/goal-run-store.js +4 -4
  72. package/dist/src/agent/goals/persistent-goal-service.js +1 -1
  73. package/dist/src/agent/goals/post-turn.js +2 -2
  74. package/dist/src/agent/image/load-image-media.js +2 -2
  75. package/dist/src/agent/ipc/bus.js +1 -1
  76. package/dist/src/agent/ipc/inbox.js +2 -2
  77. package/dist/src/agent/ipc/socket.js +1 -1
  78. package/dist/src/agent/memory/builtin-memory-store.js +1 -1
  79. package/dist/src/agent/memory/dreaming/deep-promotion.js +1 -1
  80. package/dist/src/agent/memory/dreaming/events.js +1 -1
  81. package/dist/src/agent/memory/dreaming/last-run.js +1 -1
  82. package/dist/src/agent/memory/dreaming/light-sweep.js +1 -1
  83. package/dist/src/agent/memory/dreaming/preview.js +1 -1
  84. package/dist/src/agent/memory/dreaming/rem-patterns.js +1 -1
  85. package/dist/src/agent/memory/dreaming/short-term-store.js +1 -1
  86. package/dist/src/agent/memory/dreaming/utils.js +1 -1
  87. package/dist/src/agent/memory/plugin-discovery.js +1 -1
  88. package/dist/src/agent/models/manager.js +1 -1
  89. package/dist/src/agent/prompt/service-prompt-builder.js +2 -2
  90. package/dist/src/agent/reply/post-compaction-context.js +1 -1
  91. package/dist/src/agent/reply/startup-context.d.ts +3 -0
  92. package/dist/src/agent/reply/startup-context.js +25 -2
  93. package/dist/src/agent/reply/startup-context.js.map +1 -1
  94. package/dist/src/agent/reply/workspace-boundary-read.js +1 -1
  95. package/dist/src/agent/sandbox/path-policy.js +2 -2
  96. package/dist/src/agent/service/build-direct-message-content.js +1 -1
  97. package/dist/src/agent/service.d.ts +1 -0
  98. package/dist/src/agent/service.js +10 -4
  99. package/dist/src/agent/service.js.map +1 -1
  100. package/dist/src/agent/session/session-inspector.js +1 -1
  101. package/dist/src/agent/skills/config.js +1 -1
  102. package/dist/src/agent/skills/hub-hash.js +2 -2
  103. package/dist/src/agent/skills/hub-lock.js +1 -1
  104. package/dist/src/agent/skills/hub-pull.js +3 -3
  105. package/dist/src/agent/skills/index.js +1 -1
  106. package/dist/src/agent/skills/managed-store.js +1 -1
  107. package/dist/src/agent/skills/scanner.js +1 -1
  108. package/dist/src/agent/skills/skill-manage-ops.js +1 -1
  109. package/dist/src/agent/skills/skill-manager.js +1 -1
  110. package/dist/src/agent/tools/create-share-tool.d.ts +27 -0
  111. package/dist/src/agent/tools/create-share-tool.js +237 -0
  112. package/dist/src/agent/tools/create-share-tool.js.map +1 -0
  113. package/dist/src/agent/tools/dreaming-tool.js +1 -1
  114. package/dist/src/agent/tools/factory.js +35 -1
  115. package/dist/src/agent/tools/factory.js.map +1 -1
  116. package/dist/src/agent/tools/image-generate-tool.js +1 -1
  117. package/dist/src/agent/tools/index.d.ts +2 -0
  118. package/dist/src/agent/tools/index.js +3 -1
  119. package/dist/src/agent/tools/send-media.js +1 -1
  120. package/dist/src/agent/tools/skill-manage-tool.js +1 -1
  121. package/dist/src/agent/tools/workflow-tool.d.ts +41 -0
  122. package/dist/src/agent/tools/workflow-tool.js +271 -0
  123. package/dist/src/agent/tools/workflow-tool.js.map +1 -0
  124. package/dist/src/agent/tools/write.js +1 -1
  125. package/dist/src/agent/workflow/builtins/audit-repo.d.ts +13 -0
  126. package/dist/src/agent/workflow/builtins/audit-repo.js +156 -0
  127. package/dist/src/agent/workflow/builtins/audit-repo.js.map +1 -0
  128. package/dist/src/agent/workflow/builtins/debug-incident.d.ts +13 -0
  129. package/dist/src/agent/workflow/builtins/debug-incident.js +155 -0
  130. package/dist/src/agent/workflow/builtins/debug-incident.js.map +1 -0
  131. package/dist/src/agent/workflow/builtins/index.d.ts +17 -0
  132. package/dist/src/agent/workflow/builtins/index.js +38 -0
  133. package/dist/src/agent/workflow/builtins/index.js.map +1 -0
  134. package/dist/src/agent/workflow/builtins/multi-perspective-review.d.ts +14 -0
  135. package/dist/src/agent/workflow/builtins/multi-perspective-review.js +149 -0
  136. package/dist/src/agent/workflow/builtins/multi-perspective-review.js.map +1 -0
  137. package/dist/src/agent/workflow/builtins/pr-review.d.ts +12 -0
  138. package/dist/src/agent/workflow/builtins/pr-review.js +156 -0
  139. package/dist/src/agent/workflow/builtins/pr-review.js.map +1 -0
  140. package/dist/src/agent/workflow/builtins/research.d.ts +13 -0
  141. package/dist/src/agent/workflow/builtins/research.js +160 -0
  142. package/dist/src/agent/workflow/builtins/research.js.map +1 -0
  143. package/dist/src/agent/workflow/catalog.d.ts +56 -0
  144. package/dist/src/agent/workflow/catalog.js +159 -0
  145. package/dist/src/agent/workflow/catalog.js.map +1 -0
  146. package/dist/src/agent/workflow/channel-capability.d.ts +76 -0
  147. package/dist/src/agent/workflow/channel-capability.js +1 -0
  148. package/dist/src/agent/workflow/index.d.ts +11 -0
  149. package/dist/src/agent/workflow/index.js +10 -0
  150. package/dist/src/agent/workflow/last-run-memory.d.ts +42 -0
  151. package/dist/src/agent/workflow/last-run-memory.js +60 -0
  152. package/dist/src/agent/workflow/last-run-memory.js.map +1 -0
  153. package/dist/src/agent/workflow/parser.d.ts +20 -0
  154. package/dist/src/agent/workflow/parser.js +146 -0
  155. package/dist/src/agent/workflow/parser.js.map +1 -0
  156. package/dist/src/agent/workflow/progress-broker.d.ts +80 -0
  157. package/dist/src/agent/workflow/progress-broker.js +263 -0
  158. package/dist/src/agent/workflow/progress-broker.js.map +1 -0
  159. package/dist/src/agent/workflow/runtime.d.ts +31 -0
  160. package/dist/src/agent/workflow/runtime.js +301 -0
  161. package/dist/src/agent/workflow/runtime.js.map +1 -0
  162. package/dist/src/agent/workflow/snapshot.d.ts +18 -0
  163. package/dist/src/agent/workflow/snapshot.js +144 -0
  164. package/dist/src/agent/workflow/snapshot.js.map +1 -0
  165. package/dist/src/agent/workflow/structured-output-tool.d.ts +33 -0
  166. package/dist/src/agent/workflow/structured-output-tool.js +58 -0
  167. package/dist/src/agent/workflow/structured-output-tool.js.map +1 -0
  168. package/dist/src/agent/workflow/subagent-runner.d.ts +42 -0
  169. package/dist/src/agent/workflow/subagent-runner.js +104 -0
  170. package/dist/src/agent/workflow/subagent-runner.js.map +1 -0
  171. package/dist/src/agent/workflow/types.d.ts +145 -0
  172. package/dist/src/agent/workflow/types.js +1 -0
  173. package/dist/src/auth/credentials.js +3 -3
  174. package/dist/src/auth/profiles/store.js +1 -1
  175. package/dist/src/auth/sync-provider-auth.js +1 -1
  176. package/dist/src/browser/cache-dir-policy.js +1 -1
  177. package/dist/src/browser/cdp-local-launcher.js +2 -2
  178. package/dist/src/browser/providers/browser-ext-install.js +4 -4
  179. package/dist/src/browser/providers/cloakbrowser.js +4 -4
  180. package/dist/src/browser/providers/playwright-doctor.js +1 -1
  181. package/dist/src/browser/stealth.js +1 -1
  182. package/dist/src/channels/attachments/inbound-persist.js +1 -1
  183. package/dist/src/channels/attachments/outbound-tts-persist.js +1 -1
  184. package/dist/src/channels/outbound/persist-store.js +1 -1
  185. package/dist/src/channels/pairing/allow-from-file.js +1 -1
  186. package/dist/src/channels/pairing/pairing-store.js +2 -2
  187. package/dist/src/chat-commands/builtins/config.js +2 -2
  188. package/dist/src/chat-commands/builtins/model.js +40 -23
  189. package/dist/src/chat-commands/builtins/model.js.map +1 -1
  190. package/dist/src/chat-commands/builtins/system.js +30 -15
  191. package/dist/src/chat-commands/builtins/system.js.map +1 -1
  192. package/dist/src/chat-commands/builtins/workflow.d.ts +18 -0
  193. package/dist/src/chat-commands/builtins/workflow.js +172 -0
  194. package/dist/src/chat-commands/builtins/workflow.js.map +1 -0
  195. package/dist/src/chat-commands/context.js +1 -1
  196. package/dist/src/chat-commands/format-output.d.ts +28 -0
  197. package/dist/src/chat-commands/format-output.js +45 -0
  198. package/dist/src/chat-commands/format-output.js.map +1 -0
  199. package/dist/src/chat-commands/index.d.ts +1 -0
  200. package/dist/src/chat-commands/index.js +3 -1
  201. package/dist/src/chat-commands/index.js.map +1 -1
  202. package/dist/src/cli/commands/config.js +2 -2
  203. package/dist/src/cli/commands/doctor/checks/config-health.js +1 -1
  204. package/dist/src/cli/commands/doctor/checks/provider-auth.js +1 -1
  205. package/dist/src/cli/commands/doctor/checks/session-integrity.js +1 -1
  206. package/dist/src/cli/commands/doctor/checks/state-integrity.js +1 -1
  207. package/dist/src/cli/commands/doctor/checks/workspace-status.js +1 -1
  208. package/dist/src/cli/commands/extension-dev.js +1 -1
  209. package/dist/src/cli/commands/extension-marketplace.js +1 -1
  210. package/dist/src/cli/commands/extension-pack.js +1 -1
  211. package/dist/src/cli/commands/gateway/lifecycle.js +10 -4
  212. package/dist/src/cli/commands/gateway/lifecycle.js.map +1 -1
  213. package/dist/src/cli/commands/gateway/shared.js +1 -1
  214. package/dist/src/cli/commands/image.js +1 -1
  215. package/dist/src/cli/commands/init.js +4 -4
  216. package/dist/src/cli/commands/onboard.js +2 -2
  217. package/dist/src/cli/commands/tunnel.js +2 -2
  218. package/dist/src/cli/utils/gateway-client.js +1 -1
  219. package/dist/src/cli/utils/init-workspace-core.js +2 -2
  220. package/dist/src/config/agent-profile.js +1 -1
  221. package/dist/src/config/gateway-bind.js +1 -1
  222. package/dist/src/config/index.js +5 -5
  223. package/dist/src/config/loader.js +2 -2
  224. package/dist/src/config/models-json.js +2 -2
  225. package/dist/src/config/paths-state.js +1 -1
  226. package/dist/src/config/profile.js +2 -2
  227. package/dist/src/config/public-url.d.ts +28 -0
  228. package/dist/src/config/public-url.js +103 -0
  229. package/dist/src/config/public-url.js.map +1 -0
  230. package/dist/src/config/schema.d.ts +82 -0
  231. package/dist/src/config/schema.js +130 -1
  232. package/dist/src/config/schema.js.map +1 -1
  233. package/dist/src/config/workspace-path.js +1 -1
  234. package/dist/src/cron/executor.js +2 -2
  235. package/dist/src/cron/persistence.js +1 -1
  236. package/dist/src/cron/run-log-store.js +1 -1
  237. package/dist/src/daemon/constants.js +1 -1
  238. package/dist/src/daemon/install-plan.js +3 -3
  239. package/dist/src/daemon/install-plan.js.map +1 -1
  240. package/dist/src/daemon/launchd.js +2 -2
  241. package/dist/src/daemon/schtasks.js +38 -1
  242. package/dist/src/daemon/schtasks.js.map +1 -1
  243. package/dist/src/daemon/systemd.js +2 -2
  244. package/dist/src/extensions/bundle-mcp.js +1 -1
  245. package/dist/src/extensions/discover-extensions.js +1 -1
  246. package/dist/src/extensions/health.js +1 -1
  247. package/dist/src/extensions/loader.js +1 -1
  248. package/dist/src/extensions/lockfile.js +2 -2
  249. package/dist/src/gateway/agents-admin.js +2 -2
  250. package/dist/src/gateway/file-path-classifier.js +2 -2
  251. package/dist/src/gateway/heartbeat/service.js +1 -1
  252. package/dist/src/gateway/hono/app.js +33 -2
  253. package/dist/src/gateway/hono/app.js.map +1 -1
  254. package/dist/src/gateway/hono/lib/config-payload.js +1 -1
  255. package/dist/src/gateway/hono/lib/extension-store.js +2 -2
  256. package/dist/src/gateway/hono/lib/static-ui.js +2 -2
  257. package/dist/src/gateway/hono/oauth.js +1 -1
  258. package/dist/src/gateway/hono/routes/agents.js +1 -1
  259. package/dist/src/gateway/hono/routes/auth-registry-extensions.js +1 -1
  260. package/dist/src/gateway/hono/routes/config-patch/misc.js +1 -1
  261. package/dist/src/gateway/hono/routes/dreaming.js +1 -1
  262. package/dist/src/gateway/hono/routes/host-fs.js +2 -2
  263. package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
  264. package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
  265. package/dist/src/gateway/hono/routes/models.js +1 -1
  266. package/dist/src/gateway/hono/routes/shares.js +631 -34
  267. package/dist/src/gateway/hono/routes/shares.js.map +1 -1
  268. package/dist/src/gateway/hono/routes/site-shares.d.ts +3 -0
  269. package/dist/src/gateway/hono/routes/site-shares.js +228 -0
  270. package/dist/src/gateway/hono/routes/site-shares.js.map +1 -0
  271. package/dist/src/gateway/hono/routes/tunnel.js +97 -8
  272. package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
  273. package/dist/src/gateway/hono/routes/workspace.js +5 -5
  274. package/dist/src/gateway/hono/sse.js +2 -2
  275. package/dist/src/gateway/host.d.ts +3 -1
  276. package/dist/src/gateway/host.js +3 -1
  277. package/dist/src/gateway/host.js.map +1 -1
  278. package/dist/src/gateway/lock.js +3 -3
  279. package/dist/src/gateway/ports.d.ts +6 -0
  280. package/dist/src/gateway/ports.js +38 -2
  281. package/dist/src/gateway/ports.js.map +1 -1
  282. package/dist/src/gateway/public-url.d.ts +8 -0
  283. package/dist/src/gateway/public-url.js +10 -0
  284. package/dist/src/gateway/public-url.js.map +1 -0
  285. package/dist/src/gateway/security/origin-check.d.ts +9 -1
  286. package/dist/src/gateway/security/origin-check.js +4 -0
  287. package/dist/src/gateway/security/origin-check.js.map +1 -1
  288. package/dist/src/gateway/server.js +15 -0
  289. package/dist/src/gateway/server.js.map +1 -1
  290. package/dist/src/gateway/service/agent-runner.js +2 -2
  291. package/dist/src/gateway/service/marketplace-service.js +2 -2
  292. package/dist/src/gateway/service/run-gateway-agent.js +2 -2
  293. package/dist/src/gateway/service.js +3 -2
  294. package/dist/src/gateway/service.js.map +1 -1
  295. package/dist/src/gateway/workspace-fs-file-list.js +1 -1
  296. package/dist/src/heartbeat/index.js +1 -1
  297. package/dist/src/i18n/goals-bundle.js +1 -1
  298. package/dist/src/i18n/index.d.ts +1 -0
  299. package/dist/src/i18n/index.js +2 -1
  300. package/dist/src/i18n/locales/share-tool.en.js +15 -0
  301. package/dist/src/i18n/locales/share-tool.en.js.map +1 -0
  302. package/dist/src/i18n/locales/share-tool.zh.js +15 -0
  303. package/dist/src/i18n/locales/share-tool.zh.js.map +1 -0
  304. package/dist/src/i18n/share-tool-bundle.d.ts +20 -0
  305. package/dist/src/i18n/share-tool-bundle.js +56 -0
  306. package/dist/src/i18n/share-tool-bundle.js.map +1 -0
  307. package/dist/src/infra/gateway-processes.js +1 -0
  308. package/dist/src/infra/gateway-processes.js.map +1 -1
  309. package/dist/src/infra/restart.js +2 -2
  310. package/dist/src/infra/update-check.js +1 -1
  311. package/dist/src/infra/update-lock.js +3 -3
  312. package/dist/src/infra/update-runner.js +1 -1
  313. package/dist/src/infra/update-startup.js +2 -2
  314. package/dist/src/infra/write-file-atomic.js +2 -2
  315. package/dist/src/providers/auth-runtime/auth-profile-store.js +1 -1
  316. package/dist/src/providers/index.js +2 -2
  317. package/dist/src/providers/model-registry.js +1 -1
  318. package/dist/src/session/config-store.js +2 -2
  319. package/dist/src/session/parity/jsonl-transcript-io.js +2 -2
  320. package/dist/src/session/parity/sessions-json-file.js +1 -1
  321. package/dist/src/session/parity/transcript-file-lock.js +2 -2
  322. package/dist/src/session/parity/transcript-paths.js +1 -1
  323. package/dist/src/session/search-index-cache.js +1 -1
  324. package/dist/src/session/search-index.js +1 -1
  325. package/dist/src/session/session-title.js +3 -2
  326. package/dist/src/session/session-title.js.map +1 -1
  327. package/dist/src/session/store.js +5 -5
  328. package/dist/src/share/share-auto.d.ts +74 -0
  329. package/dist/src/share/share-auto.js +247 -0
  330. package/dist/src/share/share-auto.js.map +1 -0
  331. package/dist/src/share/share-config.js +63 -4
  332. package/dist/src/share/share-config.js.map +1 -1
  333. package/dist/src/share/share-landing.d.ts +28 -2
  334. package/dist/src/share/share-landing.js +155 -34
  335. package/dist/src/share/share-landing.js.map +1 -1
  336. package/dist/src/share/share-store.d.ts +48 -4
  337. package/dist/src/share/share-store.js +322 -51
  338. package/dist/src/share/share-store.js.map +1 -1
  339. package/dist/src/share/share-thumbnail.d.ts +35 -0
  340. package/dist/src/share/share-thumbnail.js +277 -0
  341. package/dist/src/share/share-thumbnail.js.map +1 -0
  342. package/dist/src/share/share-types.d.ts +68 -10
  343. package/dist/src/share/share-types.js +18 -1
  344. package/dist/src/share/share-types.js.map +1 -1
  345. package/dist/src/share/share-url.js +1 -1
  346. package/dist/src/share/share-zip.d.ts +35 -0
  347. package/dist/src/share/share-zip.js +303 -0
  348. package/dist/src/share/share-zip.js.map +1 -0
  349. package/dist/src/share/site-proxy.d.ts +35 -0
  350. package/dist/src/share/site-proxy.js +234 -0
  351. package/dist/src/share/site-proxy.js.map +1 -0
  352. package/dist/src/share/site-share-config.d.ts +11 -0
  353. package/dist/src/share/site-share-config.js +103 -0
  354. package/dist/src/share/site-share-config.js.map +1 -0
  355. package/dist/src/share/site-share-router.d.ts +23 -0
  356. package/dist/src/share/site-share-router.js +147 -0
  357. package/dist/src/share/site-share-router.js.map +1 -0
  358. package/dist/src/share/site-share-store.d.ts +53 -0
  359. package/dist/src/share/site-share-store.js +400 -0
  360. package/dist/src/share/site-share-store.js.map +1 -0
  361. package/dist/src/share/site-share-types.d.ts +103 -0
  362. package/dist/src/share/site-share-types.js +41 -0
  363. package/dist/src/share/site-share-types.js.map +1 -0
  364. package/dist/src/share/site-static-serve.d.ts +10 -0
  365. package/dist/src/share/site-static-serve.js +145 -0
  366. package/dist/src/share/site-static-serve.js.map +1 -0
  367. package/dist/src/tui/clipboard-image.js +3 -3
  368. package/dist/src/tui/theme-manager.js +1 -1
  369. package/dist/src/tui/tui-commands.js +18 -0
  370. package/dist/src/tui/tui-commands.js.map +1 -1
  371. package/dist/src/tui/tui-keybindings-file.js +1 -1
  372. package/dist/src/tui/tui-scoped-models.js +2 -2
  373. package/dist/src/tui/tui-settings.js +1 -1
  374. package/dist/src/tui/tui-workflow-slash.d.ts +32 -0
  375. package/dist/src/tui/tui-workflow-slash.js +63 -0
  376. package/dist/src/tui/tui-workflow-slash.js.map +1 -0
  377. package/dist/src/tui/tui.js +2 -2
  378. package/dist/src/tunnel/enable-lan-pairing.js +1 -1
  379. package/dist/src/tunnel/frpc-binary.js +3 -3
  380. package/dist/src/tunnel/frpc-config.js +1 -1
  381. package/dist/src/tunnel/frpc-extract.js +1 -1
  382. package/dist/src/tunnel/index.js +2 -2
  383. package/dist/src/tunnel/pair-context.d.ts +7 -1
  384. package/dist/src/tunnel/pair-context.js +25 -9
  385. package/dist/src/tunnel/pair-context.js.map +1 -1
  386. package/dist/src/tunnel/pair-url.d.ts +14 -1
  387. package/dist/src/tunnel/pair-url.js +14 -1
  388. package/dist/src/tunnel/pair-url.js.map +1 -1
  389. package/dist/src/tunnel/tunnel-service.js +2 -2
  390. package/dist/src/tunnel/tunnel-state.js +1 -1
  391. package/dist/src/utils/logger/audit.js +1 -1
  392. package/dist/src/utils/logger/log-store.js +1 -1
  393. package/dist/src/utils/logger/rotation.js +1 -1
  394. package/dist/src/voice/tts/audio.js +1 -1
  395. package/dist/src/voice/tts/providers/edge-speech.js +2 -2
  396. package/package.json +3 -2
  397. package/dist/gateway/static/root/assets/agents-tR-nNP04.js +0 -222
  398. package/dist/gateway/static/root/assets/apps-page-BDw6SP-d.js +0 -1
  399. package/dist/gateway/static/root/assets/button-KafIU8dx.js +0 -1
  400. package/dist/gateway/static/root/assets/channels-settings-DEFd-jj1.js +0 -1
  401. package/dist/gateway/static/root/assets/channels-status-swr-DI5FHdGe.js +0 -8
  402. package/dist/gateway/static/root/assets/cron-api-BSqY8LwW.js +0 -1
  403. package/dist/gateway/static/root/assets/cron-page-D7lVDjcR.js +0 -1
  404. package/dist/gateway/static/root/assets/dist-C57OMHW8.js +0 -48
  405. package/dist/gateway/static/root/assets/extension-page-CQo2Xsmg.js +0 -1
  406. package/dist/gateway/static/root/assets/extension-settings-page-CZf0WoZg.js +0 -1
  407. package/dist/gateway/static/root/assets/fetch-2iRFmd3n.js +0 -3
  408. package/dist/gateway/static/root/assets/heartbeat-config-api-B0drdQEJ.js +0 -1
  409. package/dist/gateway/static/root/assets/index-0Gt3TG4j.js +0 -4693
  410. package/dist/gateway/static/root/assets/index-BuFldCsB.css +0 -1
  411. package/dist/gateway/static/root/assets/logs-page-DMuORLfC.js +0 -1
  412. package/dist/gateway/static/root/assets/sessions-page-_UO8g6NN.js +0 -1
  413. package/dist/gateway/static/root/assets/settings-form-section-DkmHkknc.js +0 -1
  414. package/dist/gateway/static/root/assets/settings-page-Cz8FoW_A.js +0 -3
  415. package/dist/gateway/static/root/assets/skills-page-HrUOxF7H.js +0 -2
  416. package/dist/gateway/static/root/assets/theme-store-D01dJt95.js +0 -1
  417. package/dist/gateway/static/root/assets/utils-BFwcR6pL.js +0 -1
  418. package/dist/gateway/static/root/assets/voice-api-key-field-JF8-aqc5.js +0 -1
@@ -1,14 +1,16 @@
1
+ import { init_public_url, validatePublicUrl } from "../../../config/public-url.js";
1
2
  import { resolveGatewayEffectiveHost } from "../../../config/gateway-bind.js";
3
+ import { loadTunnelState } from "../../../tunnel/tunnel-state.js";
2
4
  import { extractToken } from "../../auth.js";
3
5
  import { TUNNEL_CONSENT_REQUIRED_CODE, TunnelConsentError, assertTunnelMayStart, getTunnelConsentState } from "../../../tunnel/consent.js";
4
6
  import { getTunnelRegistrationSecretMeta, readTunnelRegistrationSecretFromConfigOnly, resolveTunnelBrokerUrl } from "../../../tunnel/env.js";
5
7
  import { createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange } from "../../../tunnel/pairing.js";
6
- import { loadTunnelState } from "../../../tunnel/tunnel-state.js";
7
8
  import { logTunnelAudit } from "../../../tunnel/tunnel-audit.js";
8
9
  import { getTunnelService, hashGatewayToken } from "../../../tunnel/tunnel-service.js";
9
10
  import { configureTunnelFromGatewayConfig } from "../../../tunnel/gateway-lifecycle.js";
10
11
  import { applyTunnelConsentToConfig, setTunnelEnabledInConfig } from "../../../tunnel/tunnel-config.js";
11
12
  import { getClientIpFromHeaders } from "../../security/loopback.js";
13
+ import { resolveReverseProxyPublicUrl } from "../../public-url.js";
12
14
  import { applyLanPairingGatewayPatch } from "../../../tunnel/enable-lan-pairing.js";
13
15
  import { consumeTunnelMutationLimit } from "../../../tunnel/tunnel-rate-limit.js";
14
16
  import "../../../tunnel/index.js";
@@ -16,6 +18,7 @@ import { buildMobileConnectUrlOrder, resolveMobilePairLanUrl, validateMobilePair
16
18
  import { buildMobilePairContext } from "../../../tunnel/pair-context.js";
17
19
  import { consumePairingExchangeFailLimit } from "../../../tunnel/pairing-rate-limit.js";
18
20
  //#region src/gateway/hono/routes/tunnel.ts
21
+ init_public_url();
19
22
  async function configureTunnelFromService(deps, opts) {
20
23
  await configureTunnelFromGatewayConfig(deps.service.currentConfig, opts);
21
24
  }
@@ -62,7 +65,8 @@ function registerTunnelPublicRoutes(app, service) {
62
65
  const context = buildMobilePairContext({
63
66
  config,
64
67
  tunnelPublicUrl: status.publicUrl,
65
- tunnelConnected: status.state === "connected"
68
+ tunnelConnected: status.state === "connected",
69
+ reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config)
66
70
  });
67
71
  return c.json({
68
72
  ok: true,
@@ -74,6 +78,7 @@ function registerTunnelPublicRoutes(app, service) {
74
78
  pairingReady: context.pairingReady,
75
79
  blockReason: context.blockReason ?? null,
76
80
  tunnelConnected: status.state === "connected",
81
+ reverseProxyConfigured: Boolean(resolveReverseProxyPublicUrl(config)),
77
82
  connectUrls: context.connectUrls
78
83
  });
79
84
  });
@@ -121,15 +126,19 @@ function registerTunnelPublicRoutes(app, service) {
121
126
  if (!token) return c.json({ error: "Gateway token not configured" }, 500);
122
127
  const persisted = loadTunnelState();
123
128
  const config = service.currentConfig;
124
- const publicUrl = persisted?.publicUrl?.trim() || null;
129
+ const tunnelUrl = persisted?.publicUrl?.trim() || null;
130
+ const reverseProxyUrl = resolveReverseProxyPublicUrl(config);
125
131
  const lanUrl = resolveMobilePairLanUrl(config);
126
132
  const connectUrls = buildMobileConnectUrlOrder({
127
- baseUrl: publicUrl,
128
- lanUrl
133
+ reverseProxyUrl,
134
+ baseUrl: reverseProxyUrl ?? tunnelUrl,
135
+ lanUrl,
136
+ tunnelUrl
129
137
  });
138
+ const advertisedBaseUrl = reverseProxyUrl ?? tunnelUrl;
130
139
  const payload = await exchangePairingSecretOnce(pairingSecret, () => ({
131
140
  token,
132
- baseUrl: publicUrl,
141
+ baseUrl: advertisedBaseUrl,
133
142
  lanUrl,
134
143
  connectUrls
135
144
  }));
@@ -166,7 +175,8 @@ function registerTunnelRoutes(authenticated, deps) {
166
175
  const context = buildMobilePairContext({
167
176
  config,
168
177
  tunnelPublicUrl: status.publicUrl,
169
- tunnelConnected: status.state === "connected"
178
+ tunnelConnected: status.state === "connected",
179
+ reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config)
170
180
  });
171
181
  return c.json(context);
172
182
  });
@@ -197,7 +207,8 @@ function registerTunnelRoutes(authenticated, deps) {
197
207
  let context = buildMobilePairContext({
198
208
  config: deps.service.currentConfig,
199
209
  tunnelPublicUrl: status.publicUrl,
200
- tunnelConnected: status.state === "connected"
210
+ tunnelConnected: status.state === "connected",
211
+ reverseProxyPublicUrl: resolveReverseProxyPublicUrl(deps.service.currentConfig)
201
212
  });
202
213
  if (patchResult.changed) context = {
203
214
  ...context,
@@ -224,6 +235,84 @@ function registerTunnelRoutes(authenticated, deps) {
224
235
  expiresAt: expiresAt.toISOString()
225
236
  });
226
237
  });
238
+ /**
239
+ * Probe a candidate reverse-proxy URL before persisting it. The check round-trips
240
+ * a `GET /api/tunnel/pair/ping` and validates the response identifies as
241
+ * `service: 'xopc-gateway'`. Surface-area errors are mapped to stable codes so
242
+ * the UI can render targeted hints (TLS / DNS / wrong service / blocked path).
243
+ */
244
+ authenticated.post("/api/tunnel/pair/probe-public", tunnelMutationLimit, async (c) => {
245
+ if (!requireGatewayToken(c)) return c.json({ error: "Gateway token required" }, 401);
246
+ let body;
247
+ try {
248
+ body = await c.req.json();
249
+ } catch {
250
+ return c.json({
251
+ ok: false,
252
+ code: "INVALID_JSON",
253
+ message: "Invalid JSON body"
254
+ }, 400);
255
+ }
256
+ const validation = validatePublicUrl(typeof body.url === "string" ? body.url : "");
257
+ if (validation.ok === false) return c.json({
258
+ ok: false,
259
+ code: validation.code,
260
+ message: validation.message
261
+ });
262
+ const pingUrl = `${validation.url}/api/tunnel/pair/ping`;
263
+ const startedAt = Date.now();
264
+ let response;
265
+ try {
266
+ response = await fetch(pingUrl, {
267
+ method: "GET",
268
+ headers: { Accept: "application/json" },
269
+ signal: AbortSignal.timeout(5e3)
270
+ });
271
+ } catch (error) {
272
+ const message = error instanceof Error ? error.message : String(error);
273
+ const lower = message.toLowerCase();
274
+ let code = "NETWORK_ERROR";
275
+ if (lower.includes("timeout") || lower.includes("aborted")) code = "TIMEOUT";
276
+ else if (lower.includes("certificate") || lower.includes("cert") || lower.includes("tls") || lower.includes("ssl")) code = "TLS_INVALID";
277
+ else if (lower.includes("enotfound") || lower.includes("econnrefused") || lower.includes("eai_again")) code = "DNS_OR_CONN_REFUSED";
278
+ return c.json({
279
+ ok: false,
280
+ code,
281
+ message
282
+ });
283
+ }
284
+ if (response.status === 401 || response.status === 403) return c.json({
285
+ ok: false,
286
+ code: "AUTH_BLOCKED",
287
+ message: `Reverse proxy returned ${response.status}`
288
+ });
289
+ if (!response.ok) return c.json({
290
+ ok: false,
291
+ code: "HTTP_ERROR",
292
+ message: `HTTP ${response.status}`
293
+ });
294
+ let parsed = null;
295
+ try {
296
+ parsed = await response.json();
297
+ } catch {
298
+ return c.json({
299
+ ok: false,
300
+ code: "NOT_XOPC_GATEWAY",
301
+ message: "Response was not JSON"
302
+ });
303
+ }
304
+ if (!parsed || parsed.service !== "xopc-gateway") return c.json({
305
+ ok: false,
306
+ code: "NOT_XOPC_GATEWAY",
307
+ message: "Endpoint did not identify as an xopc gateway"
308
+ });
309
+ return c.json({
310
+ ok: true,
311
+ url: validation.url,
312
+ latencyMs: Date.now() - startedAt,
313
+ mobilePairing: parsed.mobilePairing === true
314
+ });
315
+ });
227
316
  authenticated.get("/api/tunnel/status", async (c) => {
228
317
  await configureTunnelFromService(deps);
229
318
  const config = deps.service.currentConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel.js","names":[],"sources":["../../../../../src/gateway/hono/routes/tunnel.ts"],"sourcesContent":["import type { Hono, MiddlewareHandler } from 'hono';\n\nimport type { Config } from '../../../config/schema.js';\nimport { resolveGatewayEffectiveHost } from '../../../config/gateway-bind.js';\nimport { extractToken } from '../../auth.js';\nimport {\n assertTunnelMayStart,\n getTunnelConsentState,\n TUNNEL_CONSENT_REQUIRED_CODE,\n TunnelConsentError,\n} from '../../../tunnel/consent.js';\nimport { hashGatewayToken } from '../../../tunnel/tunnel-service.js';\nimport { configureTunnelFromGatewayConfig } from '../../../tunnel/gateway-lifecycle.js';\nimport {\n getTunnelRegistrationSecretMeta,\n readTunnelRegistrationSecretFromConfigOnly,\n resolveTunnelBrokerUrl,\n} from '../../../tunnel/env.js';\nimport { getTunnelService } from '../../../tunnel/index.js';\nimport { createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange } from '../../../tunnel/pairing.js';\nimport { buildMobilePairContext } from '../../../tunnel/pair-context.js';\nimport { applyLanPairingGatewayPatch } from '../../../tunnel/enable-lan-pairing.js';\nimport {\n buildMobileConnectUrlOrder,\n resolveMobilePairLanUrl,\n validateMobilePairBaseUrl,\n} from '../../../tunnel/pair-url.js';\nimport { consumePairingExchangeFailLimit } from '../../../tunnel/pairing-rate-limit.js';\nimport { loadTunnelState } from '../../../tunnel/tunnel-state.js';\nimport { logTunnelAudit } from '../../../tunnel/tunnel-audit.js';\nimport {\n applyTunnelConsentToConfig,\n setTunnelEnabledInConfig,\n} from '../../../tunnel/tunnel-config.js';\nimport { consumeTunnelMutationLimit } from '../../../tunnel/tunnel-rate-limit.js';\nimport type { AuthenticatedRouteDeps } from './deps.js';\nimport type { GatewayService } from '../../service.js';\nimport { getClientIpFromHeaders } from '../../security/loopback.js';\n\nasync function configureTunnelFromService(\n deps: AuthenticatedRouteDeps,\n opts?: { force?: boolean },\n): Promise<void> {\n await configureTunnelFromGatewayConfig(deps.service.currentConfig, opts);\n}\n\nfunction enrichTunnelStatus(config: Config, status: ReturnType<ReturnType<typeof getTunnelService>['getStatus']>) {\n const consent = getTunnelConsentState(config);\n const brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n const registrationSecret = getTunnelRegistrationSecretMeta(config, process.env, brokerUrl);\n return {\n ...status,\n consentRequired: consent.consentRequired,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n canAutoStart: consent.canAutoStart,\n registrationSecret,\n };\n}\n\nfunction requireGatewayToken(c: { req: { header: (name: string) => string | undefined } }): string | null {\n return (\n extractToken({\n authorization: c.req.header('authorization') ?? undefined,\n }) ?? null\n );\n}\n\nfunction createTunnelMutationRateLimitMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n const token = requireGatewayToken(c);\n if (!token) {\n return c.json({ error: 'Gateway token required' }, 401);\n }\n const result = consumeTunnelMutationLimit(token);\n if (!result.allowed) {\n c.header('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));\n return c.json(\n {\n error: 'Too many tunnel operations. Try again later.',\n code: 'TUNNEL_RATE_LIMITED',\n retryAfterMs: result.retryAfterMs,\n },\n 429,\n );\n }\n await next();\n };\n}\n\nexport function registerTunnelPublicRoutes(app: Hono, service: GatewayService): void {\n app.get('/api/tunnel/pair/ping', async (c) => {\n const config = service.currentConfig as Config;\n const tunnel = getTunnelService();\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n });\n return c.json({\n ok: true,\n service: 'xopc-gateway',\n mobilePairing: true,\n port: context.port,\n bindMode: context.bindMode,\n listenHost: context.listenHost,\n pairingReady: context.pairingReady,\n blockReason: context.blockReason ?? null,\n tunnelConnected: status.state === 'connected',\n connectUrls: context.connectUrls,\n });\n });\n\n app.post('/api/tunnel/pair/validate-url', async (c) => {\n let body: { baseUrl?: unknown };\n try {\n body = (await c.req.json()) as { baseUrl?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n const baseUrl = typeof body.baseUrl === 'string' ? body.baseUrl : '';\n const result = validateMobilePairBaseUrl(baseUrl);\n if (result.ok === false) {\n return c.json({\n ok: false,\n code: result.code,\n message: result.message,\n });\n }\n return c.json({\n ok: true,\n url: result.url,\n loopback: false,\n probePath: '/api/tunnel/pair/ping',\n });\n });\n\n app.post('/api/tunnel/exchange-token', async (c) => {\n const clientIp =\n getClientIpFromHeaders({\n get: (name: string) => c.req.header(name) ?? undefined,\n }) ?? 'unknown';\n\n let body: { pairingSecret?: unknown };\n try {\n body = (await c.req.json()) as { pairingSecret?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n\n const pairingSecret = typeof body.pairingSecret === 'string' ? body.pairingSecret.trim() : '';\n if (!pairingSecret) {\n return c.json({ error: 'pairingSecret required' }, 400);\n }\n\n const cached = getCachedPairingExchange(pairingSecret);\n if (cached) {\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, phase: 'pairing_exchange', replay: true },\n 'Pairing secret replayed (duplicate mobile exchange)',\n );\n return c.json(cached);\n }\n\n const token = service.getAuthToken();\n if (!token) {\n return c.json({ error: 'Gateway token not configured' }, 500);\n }\n\n const persisted = loadTunnelState();\n const config = service.currentConfig as Config;\n const publicUrl = persisted?.publicUrl?.trim() || null;\n const lanUrl = resolveMobilePairLanUrl(config);\n const connectUrls = buildMobileConnectUrlOrder({ baseUrl: publicUrl, lanUrl });\n\n const payload = await exchangePairingSecretOnce(pairingSecret, () => ({\n token,\n baseUrl: publicUrl,\n lanUrl,\n connectUrls,\n }));\n\n if (!payload) {\n const limited = consumePairingExchangeFailLimit(clientIp);\n if (!limited.allowed) {\n c.header('Retry-After', String(Math.ceil(limited.retryAfterMs / 1000)));\n }\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: false, clientIp, phase: 'pairing_exchange' },\n 'Pairing exchange denied: invalid or expired secret',\n );\n return c.json({ error: 'Invalid or expired pairing secret', code: 'PAIRING_INVALID' }, 401);\n }\n\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, subdomain: persisted?.subdomain ?? null, phase: 'pairing_exchange' },\n 'Pairing secret exchanged for gateway token',\n );\n return c.json(payload);\n });\n}\n\nexport function registerTunnelRoutes(authenticated: Hono, deps: AuthenticatedRouteDeps): void {\n const { strictRateLimitMiddleware } = deps;\n const tunnel = getTunnelService();\n const tunnelMutationLimit = createTunnelMutationRateLimitMiddleware();\n\n authenticated.get('/api/tunnel/pair/context', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n });\n return c.json(context);\n });\n\n authenticated.post('/api/tunnel/pair/enable-lan', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n const patchResult = applyLanPairingGatewayPatch(config);\n if (patchResult.ok === false) {\n return c.json({ ok: false, error: { message: patchResult.message, code: 'LAN_PAIRING_CONFIG' } }, 400);\n }\n\n if (patchResult.changed) {\n const saveResult = await deps.service.saveConfig(config);\n if (!saveResult.saved) {\n return c.json(\n { ok: false, error: { message: saveResult.error ?? 'Failed to save config', code: 'SAVE_FAILED' } },\n 500,\n );\n }\n logTunnelAudit(\n 'tunnel.enable_lan_pairing',\n { gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Gateway bind switched to LAN for mobile pairing',\n );\n }\n\n const status = tunnel.getStatus();\n let context = buildMobilePairContext({\n config: deps.service.currentConfig as Config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n });\n\n if (patchResult.changed) {\n context = {\n ...context,\n pairingReady: false,\n blockReason: 'GATEWAY_LOOPBACK_ONLY',\n };\n }\n\n return c.json({\n ok: true,\n requiresRestart: patchResult.changed,\n context,\n });\n });\n\n authenticated.post('/api/tunnel/pair', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const { secret, expiresAt } = createPairingSecret();\n logTunnelAudit(\n 'tunnel.pair',\n {\n expiresAt: expiresAt.toISOString(),\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Mobile pairing session created',\n );\n return c.json({ pairingSecret: secret, expiresAt: expiresAt.toISOString() });\n });\n\n authenticated.get('/api/tunnel/status', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n return c.json(enrichTunnelStatus(config, tunnel.getStatus()));\n });\n\n authenticated.post('/api/tunnel/consent', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n applyTunnelConsentToConfig(config);\n const result = await deps.service.saveConfig(config);\n if (!result.saved) {\n return c.json({ ok: false, error: result.error ?? 'Failed to save config' }, 500);\n }\n const consent = getTunnelConsentState(config);\n logTunnelAudit(\n 'tunnel.consent',\n {\n consentVersion: consent.currentVersion,\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Remote access security consent recorded',\n );\n return c.json({\n ok: true,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n });\n });\n\n authenticated.post('/api/tunnel/start', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps, { force: true });\n const config = deps.service.currentConfig as Config;\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n try {\n assertTunnelMayStart(config);\n } catch (err) {\n if (err instanceof TunnelConsentError) {\n logTunnelAudit(\n 'tunnel.start_denied',\n { reason: TUNNEL_CONSENT_REQUIRED_CODE, gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Tunnel start denied: consent required',\n );\n return c.json({ error: err.message, code: TUNNEL_CONSENT_REQUIRED_CODE }, 403);\n }\n throw err;\n }\n\n const gateway = config.gateway;\n const port = gateway.port ?? 18790;\n try {\n const qr = await tunnel.start(port, token);\n setTunnelEnabledInConfig(config, true);\n await deps.service.saveConfig(config);\n const status = tunnel.getStatus();\n return c.json({\n publicUrl: qr.publicUrl,\n subdomain: status.subdomain,\n qrPayload: qr.qrPayload,\n lanUrl: qr.lanUrl,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return c.json({ error: message }, 500);\n }\n });\n\n authenticated.post('/api/tunnel/stop', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n let release = false;\n try {\n const body = (await c.req.json().catch(() => ({}))) as { release?: unknown };\n release = body.release === true;\n } catch {\n release = false;\n }\n const { released } = await tunnel.stop({ release });\n setTunnelEnabledInConfig(config, false);\n await deps.service.saveConfig(config);\n return c.json({ ok: true, released });\n });\n\n authenticated.get('/api/tunnel/qr', async (c) => {\n await configureTunnelFromService(deps);\n const gateway = deps.service.currentConfig.gateway;\n const port = gateway.port ?? 18790;\n const host = resolveGatewayEffectiveHost(deps.service.currentConfig);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n const qr = await tunnel.buildQr(port, host);\n return c.json(qr);\n });\n\n authenticated.get('/api/tunnel/transport-status', async (c) => {\n await configureTunnelFromService(deps);\n return c.json({\n transport: { tls: 'broker_terminated' as const },\n });\n });\n\n /**\n * POST /api/tunnel/reveal-registration-secret — plaintext only when stored in config file.\n */\n authenticated.post('/api/tunnel/reveal-registration-secret', strictRateLimitMiddleware, async (c) => {\n const config = deps.service.currentConfig as Config;\n const registrationSecret = readTunnelRegistrationSecretFromConfigOnly(config);\n return c.json({\n ok: true,\n payload: {\n registrationSecret,\n source: registrationSecret ? ('config' as const) : ('none' as const),\n },\n });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tunnel.js","names":[],"sources":["../../../../../src/gateway/hono/routes/tunnel.ts"],"sourcesContent":["import type { Hono, MiddlewareHandler } from 'hono';\n\nimport type { Config } from '../../../config/schema.js';\nimport { resolveGatewayEffectiveHost } from '../../../config/gateway-bind.js';\nimport { extractToken } from '../../auth.js';\nimport {\n assertTunnelMayStart,\n getTunnelConsentState,\n TUNNEL_CONSENT_REQUIRED_CODE,\n TunnelConsentError,\n} from '../../../tunnel/consent.js';\nimport { hashGatewayToken } from '../../../tunnel/tunnel-service.js';\nimport { configureTunnelFromGatewayConfig } from '../../../tunnel/gateway-lifecycle.js';\nimport {\n getTunnelRegistrationSecretMeta,\n readTunnelRegistrationSecretFromConfigOnly,\n resolveTunnelBrokerUrl,\n} from '../../../tunnel/env.js';\nimport { getTunnelService } from '../../../tunnel/index.js';\nimport { createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange } from '../../../tunnel/pairing.js';\nimport { buildMobilePairContext } from '../../../tunnel/pair-context.js';\nimport { applyLanPairingGatewayPatch } from '../../../tunnel/enable-lan-pairing.js';\nimport {\n buildMobileConnectUrlOrder,\n resolveMobilePairLanUrl,\n validateMobilePairBaseUrl,\n} from '../../../tunnel/pair-url.js';\nimport { consumePairingExchangeFailLimit } from '../../../tunnel/pairing-rate-limit.js';\nimport { loadTunnelState } from '../../../tunnel/tunnel-state.js';\nimport { logTunnelAudit } from '../../../tunnel/tunnel-audit.js';\nimport { resolveReverseProxyPublicUrl } from '../../public-url.js';\nimport { validatePublicUrl } from '../../../config/public-url.js';\nimport {\n applyTunnelConsentToConfig,\n setTunnelEnabledInConfig,\n} from '../../../tunnel/tunnel-config.js';\nimport { consumeTunnelMutationLimit } from '../../../tunnel/tunnel-rate-limit.js';\nimport type { AuthenticatedRouteDeps } from './deps.js';\nimport type { GatewayService } from '../../service.js';\nimport { getClientIpFromHeaders } from '../../security/loopback.js';\n\nasync function configureTunnelFromService(\n deps: AuthenticatedRouteDeps,\n opts?: { force?: boolean },\n): Promise<void> {\n await configureTunnelFromGatewayConfig(deps.service.currentConfig, opts);\n}\n\nfunction enrichTunnelStatus(config: Config, status: ReturnType<ReturnType<typeof getTunnelService>['getStatus']>) {\n const consent = getTunnelConsentState(config);\n const brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n const registrationSecret = getTunnelRegistrationSecretMeta(config, process.env, brokerUrl);\n return {\n ...status,\n consentRequired: consent.consentRequired,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n canAutoStart: consent.canAutoStart,\n registrationSecret,\n };\n}\n\nfunction requireGatewayToken(c: { req: { header: (name: string) => string | undefined } }): string | null {\n return (\n extractToken({\n authorization: c.req.header('authorization') ?? undefined,\n }) ?? null\n );\n}\n\nfunction createTunnelMutationRateLimitMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n const token = requireGatewayToken(c);\n if (!token) {\n return c.json({ error: 'Gateway token required' }, 401);\n }\n const result = consumeTunnelMutationLimit(token);\n if (!result.allowed) {\n c.header('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));\n return c.json(\n {\n error: 'Too many tunnel operations. Try again later.',\n code: 'TUNNEL_RATE_LIMITED',\n retryAfterMs: result.retryAfterMs,\n },\n 429,\n );\n }\n await next();\n };\n}\n\nexport function registerTunnelPublicRoutes(app: Hono, service: GatewayService): void {\n app.get('/api/tunnel/pair/ping', async (c) => {\n const config = service.currentConfig as Config;\n const tunnel = getTunnelService();\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config),\n });\n return c.json({\n ok: true,\n service: 'xopc-gateway',\n mobilePairing: true,\n port: context.port,\n bindMode: context.bindMode,\n listenHost: context.listenHost,\n pairingReady: context.pairingReady,\n blockReason: context.blockReason ?? null,\n tunnelConnected: status.state === 'connected',\n reverseProxyConfigured: Boolean(resolveReverseProxyPublicUrl(config)),\n connectUrls: context.connectUrls,\n });\n });\n\n app.post('/api/tunnel/pair/validate-url', async (c) => {\n let body: { baseUrl?: unknown };\n try {\n body = (await c.req.json()) as { baseUrl?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n const baseUrl = typeof body.baseUrl === 'string' ? body.baseUrl : '';\n const result = validateMobilePairBaseUrl(baseUrl);\n if (result.ok === false) {\n return c.json({\n ok: false,\n code: result.code,\n message: result.message,\n });\n }\n return c.json({\n ok: true,\n url: result.url,\n loopback: false,\n probePath: '/api/tunnel/pair/ping',\n });\n });\n\n app.post('/api/tunnel/exchange-token', async (c) => {\n const clientIp =\n getClientIpFromHeaders({\n get: (name: string) => c.req.header(name) ?? undefined,\n }) ?? 'unknown';\n\n let body: { pairingSecret?: unknown };\n try {\n body = (await c.req.json()) as { pairingSecret?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n\n const pairingSecret = typeof body.pairingSecret === 'string' ? body.pairingSecret.trim() : '';\n if (!pairingSecret) {\n return c.json({ error: 'pairingSecret required' }, 400);\n }\n\n const cached = getCachedPairingExchange(pairingSecret);\n if (cached) {\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, phase: 'pairing_exchange', replay: true },\n 'Pairing secret replayed (duplicate mobile exchange)',\n );\n return c.json(cached);\n }\n\n const token = service.getAuthToken();\n if (!token) {\n return c.json({ error: 'Gateway token not configured' }, 500);\n }\n\n const persisted = loadTunnelState();\n const config = service.currentConfig as Config;\n const tunnelUrl = persisted?.publicUrl?.trim() || null;\n const reverseProxyUrl = resolveReverseProxyPublicUrl(config);\n const lanUrl = resolveMobilePairLanUrl(config);\n const connectUrls = buildMobileConnectUrlOrder({\n reverseProxyUrl,\n baseUrl: reverseProxyUrl ?? tunnelUrl,\n lanUrl,\n tunnelUrl,\n });\n // Mobile prefers HTTPS user-deployed URL over FRP broker when both exist.\n const advertisedBaseUrl = reverseProxyUrl ?? tunnelUrl;\n\n const payload = await exchangePairingSecretOnce(pairingSecret, () => ({\n token,\n baseUrl: advertisedBaseUrl,\n lanUrl,\n connectUrls,\n }));\n\n if (!payload) {\n const limited = consumePairingExchangeFailLimit(clientIp);\n if (!limited.allowed) {\n c.header('Retry-After', String(Math.ceil(limited.retryAfterMs / 1000)));\n }\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: false, clientIp, phase: 'pairing_exchange' },\n 'Pairing exchange denied: invalid or expired secret',\n );\n return c.json({ error: 'Invalid or expired pairing secret', code: 'PAIRING_INVALID' }, 401);\n }\n\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, subdomain: persisted?.subdomain ?? null, phase: 'pairing_exchange' },\n 'Pairing secret exchanged for gateway token',\n );\n return c.json(payload);\n });\n}\n\nexport function registerTunnelRoutes(authenticated: Hono, deps: AuthenticatedRouteDeps): void {\n const { strictRateLimitMiddleware } = deps;\n const tunnel = getTunnelService();\n const tunnelMutationLimit = createTunnelMutationRateLimitMiddleware();\n\n authenticated.get('/api/tunnel/pair/context', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config),\n });\n return c.json(context);\n });\n\n authenticated.post('/api/tunnel/pair/enable-lan', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n const patchResult = applyLanPairingGatewayPatch(config);\n if (patchResult.ok === false) {\n return c.json({ ok: false, error: { message: patchResult.message, code: 'LAN_PAIRING_CONFIG' } }, 400);\n }\n\n if (patchResult.changed) {\n const saveResult = await deps.service.saveConfig(config);\n if (!saveResult.saved) {\n return c.json(\n { ok: false, error: { message: saveResult.error ?? 'Failed to save config', code: 'SAVE_FAILED' } },\n 500,\n );\n }\n logTunnelAudit(\n 'tunnel.enable_lan_pairing',\n { gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Gateway bind switched to LAN for mobile pairing',\n );\n }\n\n const status = tunnel.getStatus();\n let context = buildMobilePairContext({\n config: deps.service.currentConfig as Config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(deps.service.currentConfig as Config),\n });\n\n if (patchResult.changed) {\n context = {\n ...context,\n pairingReady: false,\n blockReason: 'GATEWAY_LOOPBACK_ONLY',\n };\n }\n\n return c.json({\n ok: true,\n requiresRestart: patchResult.changed,\n context,\n });\n });\n\n authenticated.post('/api/tunnel/pair', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const { secret, expiresAt } = createPairingSecret();\n logTunnelAudit(\n 'tunnel.pair',\n {\n expiresAt: expiresAt.toISOString(),\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Mobile pairing session created',\n );\n return c.json({ pairingSecret: secret, expiresAt: expiresAt.toISOString() });\n });\n\n /**\n * Probe a candidate reverse-proxy URL before persisting it. The check round-trips\n * a `GET /api/tunnel/pair/ping` and validates the response identifies as\n * `service: 'xopc-gateway'`. Surface-area errors are mapped to stable codes so\n * the UI can render targeted hints (TLS / DNS / wrong service / blocked path).\n */\n authenticated.post('/api/tunnel/pair/probe-public', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n let body: { url?: unknown };\n try {\n body = (await c.req.json()) as { url?: unknown };\n } catch {\n return c.json({ ok: false, code: 'INVALID_JSON', message: 'Invalid JSON body' }, 400);\n }\n const raw = typeof body.url === 'string' ? body.url : '';\n const validation = validatePublicUrl(raw);\n if (validation.ok === false) {\n return c.json({ ok: false, code: validation.code, message: validation.message });\n }\n\n const pingUrl = `${validation.url}/api/tunnel/pair/ping`;\n const startedAt = Date.now();\n let response: Response;\n try {\n response = await fetch(pingUrl, {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(5000),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Node's fetch surfaces TLS errors as TypeError with cause; map heuristically.\n const lower = message.toLowerCase();\n let code: 'TIMEOUT' | 'TLS_INVALID' | 'DNS_OR_CONN_REFUSED' | 'NETWORK_ERROR' = 'NETWORK_ERROR';\n if (lower.includes('timeout') || lower.includes('aborted')) code = 'TIMEOUT';\n else if (lower.includes('certificate') || lower.includes('cert') || lower.includes('tls') || lower.includes('ssl')) {\n code = 'TLS_INVALID';\n } else if (lower.includes('enotfound') || lower.includes('econnrefused') || lower.includes('eai_again')) {\n code = 'DNS_OR_CONN_REFUSED';\n }\n return c.json({ ok: false, code, message });\n }\n\n if (response.status === 401 || response.status === 403) {\n return c.json({ ok: false, code: 'AUTH_BLOCKED', message: `Reverse proxy returned ${response.status}` });\n }\n if (!response.ok) {\n return c.json({ ok: false, code: 'HTTP_ERROR', message: `HTTP ${response.status}` });\n }\n let parsed: { service?: unknown; mobilePairing?: unknown } | null = null;\n try {\n parsed = (await response.json()) as { service?: unknown; mobilePairing?: unknown };\n } catch {\n return c.json({ ok: false, code: 'NOT_XOPC_GATEWAY', message: 'Response was not JSON' });\n }\n if (!parsed || parsed.service !== 'xopc-gateway') {\n return c.json({\n ok: false,\n code: 'NOT_XOPC_GATEWAY',\n message: 'Endpoint did not identify as an xopc gateway',\n });\n }\n return c.json({\n ok: true,\n url: validation.url,\n latencyMs: Date.now() - startedAt,\n mobilePairing: parsed.mobilePairing === true,\n });\n });\n\n authenticated.get('/api/tunnel/status', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n return c.json(enrichTunnelStatus(config, tunnel.getStatus()));\n });\n\n authenticated.post('/api/tunnel/consent', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n applyTunnelConsentToConfig(config);\n const result = await deps.service.saveConfig(config);\n if (!result.saved) {\n return c.json({ ok: false, error: result.error ?? 'Failed to save config' }, 500);\n }\n const consent = getTunnelConsentState(config);\n logTunnelAudit(\n 'tunnel.consent',\n {\n consentVersion: consent.currentVersion,\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Remote access security consent recorded',\n );\n return c.json({\n ok: true,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n });\n });\n\n authenticated.post('/api/tunnel/start', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps, { force: true });\n const config = deps.service.currentConfig as Config;\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n try {\n assertTunnelMayStart(config);\n } catch (err) {\n if (err instanceof TunnelConsentError) {\n logTunnelAudit(\n 'tunnel.start_denied',\n { reason: TUNNEL_CONSENT_REQUIRED_CODE, gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Tunnel start denied: consent required',\n );\n return c.json({ error: err.message, code: TUNNEL_CONSENT_REQUIRED_CODE }, 403);\n }\n throw err;\n }\n\n const gateway = config.gateway;\n const port = gateway.port ?? 18790;\n try {\n const qr = await tunnel.start(port, token);\n setTunnelEnabledInConfig(config, true);\n await deps.service.saveConfig(config);\n const status = tunnel.getStatus();\n return c.json({\n publicUrl: qr.publicUrl,\n subdomain: status.subdomain,\n qrPayload: qr.qrPayload,\n lanUrl: qr.lanUrl,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return c.json({ error: message }, 500);\n }\n });\n\n authenticated.post('/api/tunnel/stop', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n let release = false;\n try {\n const body = (await c.req.json().catch(() => ({}))) as { release?: unknown };\n release = body.release === true;\n } catch {\n release = false;\n }\n const { released } = await tunnel.stop({ release });\n setTunnelEnabledInConfig(config, false);\n await deps.service.saveConfig(config);\n return c.json({ ok: true, released });\n });\n\n authenticated.get('/api/tunnel/qr', async (c) => {\n await configureTunnelFromService(deps);\n const gateway = deps.service.currentConfig.gateway;\n const port = gateway.port ?? 18790;\n const host = resolveGatewayEffectiveHost(deps.service.currentConfig);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n const qr = await tunnel.buildQr(port, host);\n return c.json(qr);\n });\n\n authenticated.get('/api/tunnel/transport-status', async (c) => {\n await configureTunnelFromService(deps);\n return c.json({\n transport: { tls: 'broker_terminated' as const },\n });\n });\n\n /**\n * POST /api/tunnel/reveal-registration-secret — plaintext only when stored in config file.\n */\n authenticated.post('/api/tunnel/reveal-registration-secret', strictRateLimitMiddleware, async (c) => {\n const config = deps.service.currentConfig as Config;\n const registrationSecret = readTunnelRegistrationSecretFromConfigOnly(config);\n return c.json({\n ok: true,\n payload: {\n registrationSecret,\n source: registrationSecret ? ('config' as const) : ('none' as const),\n },\n });\n });\n}\n"],"mappings":"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"}
@@ -1,21 +1,21 @@
1
- import { createLogger } from "../../../utils/logger/index.js";
2
- import { init_logger } from "../../../utils/logger.js";
3
1
  import { init_agent_scope, listAgentEntries, normalizeAgentId, resolveAgentHomeDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../../agent/agent-scope.js";
4
2
  import { extractProfileAgentId } from "../../../config/agent-profile.js";
3
+ import { createLogger } from "../../../utils/logger/index.js";
4
+ import { init_logger } from "../../../utils/logger.js";
5
5
  import { validateWritePath } from "../../../agent/sandbox/path-policy.js";
6
+ import { isPathUnderWorkspace, resolveWorkspaceSafePath, toWorkspaceRelativePosix } from "../../workspace-editor-path.js";
6
7
  import { resolveSafeInboundFilePath } from "../../../channels/attachments/inbound-persist.js";
7
8
  import { getWorkspacePath } from "../../../config/workspace-path-helpers.js";
8
- import { isPathUnderWorkspace, resolveWorkspaceSafePath, toWorkspaceRelativePosix } from "../../workspace-editor-path.js";
9
9
  import { resolveSafeTtsFilePath } from "../../../channels/attachments/outbound-tts-persist.js";
10
10
  import { buildFilePathClassifierContext, classifyFileLocation, displayNameForPath, fileRefSessionKeysMatch, resolveFileReferenceCandidate } from "../../file-path-classifier.js";
11
11
  import { fileReferenceRegistry } from "../../file-reference-registry.js";
12
12
  import { resolveHeartbeatMdPath } from "../../workspace-heartbeat-path.js";
13
13
  import { listWorkspaceRelativeFilesFsFallback } from "../../workspace-fs-file-list.js";
14
14
  import { runRipgrepInDirectory, runRipgrepListFiles } from "../../workspace-ripgrep.js";
15
- import { randomUUID } from "node:crypto";
15
+ import { basename, dirname, join, resolve } from "node:path";
16
16
  import { constants } from "node:fs";
17
+ import { randomUUID } from "node:crypto";
17
18
  import { copyFile, link, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
18
- import { basename, dirname, join, resolve } from "node:path";
19
19
  //#region src/gateway/hono/routes/workspace.ts
20
20
  init_agent_scope();
21
21
  init_logger();
@@ -1,8 +1,8 @@
1
+ import { buildSessionKey, init_session_key, parseSessionKey } from "../../routing/session-key.js";
2
+ import { getDefaultAgentId, init_resolve_route } from "../../routing/resolve-route.js";
1
3
  import { updateAsyncLogContext } from "../../utils/logger/context.js";
2
4
  import { createLogger } from "../../utils/logger/index.js";
3
5
  import { init_logger } from "../../utils/logger.js";
4
- import { buildSessionKey, init_session_key, parseSessionKey } from "../../routing/session-key.js";
5
- import { getDefaultAgentId, init_resolve_route } from "../../routing/resolve-route.js";
6
6
  import { MAX_WEBCHAT_ATTACHMENT_FILE_BYTES } from "../chat-limits.js";
7
7
  import { stringifySSEData } from "./sse-json.js";
8
8
  import { randomUUID } from "node:crypto";
@@ -34,10 +34,12 @@ export declare function resolveGatewayCorsOrigins(params: {
34
34
  }): string[];
35
35
  /** Browser origin (`https://host`) from a gateway public/tunnel root URL. */
36
36
  export declare function originFromGatewayPublicUrl(publicUrl: string | null | undefined): string | null;
37
- /** CORS + CSRF allowlist including active FRP tunnel origin when connected. */
37
+ /** CORS + CSRF allowlist including active FRP tunnel + reverse-proxy origins. */
38
38
  export declare function resolveAllowedBrowserOrigins(params: {
39
39
  configuredOrigins?: string[];
40
40
  port: number;
41
41
  bindHost?: string;
42
42
  tunnelPublicUrl?: string | null;
43
+ /** User-configured reverse-proxy public URL (gateway.publicUrl). */
44
+ reverseProxyPublicUrl?: string | null;
43
45
  }): string[];
@@ -56,7 +56,7 @@ function originFromGatewayPublicUrl(publicUrl) {
56
56
  return null;
57
57
  }
58
58
  }
59
- /** CORS + CSRF allowlist including active FRP tunnel origin when connected. */
59
+ /** CORS + CSRF allowlist including active FRP tunnel + reverse-proxy origins. */
60
60
  function resolveAllowedBrowserOrigins(params) {
61
61
  const origins = resolveGatewayCorsOrigins({
62
62
  configuredOrigins: params.configuredOrigins,
@@ -65,6 +65,8 @@ function resolveAllowedBrowserOrigins(params) {
65
65
  });
66
66
  const tunnelOrigin = originFromGatewayPublicUrl(params.tunnelPublicUrl);
67
67
  if (tunnelOrigin && !origins.includes(tunnelOrigin)) origins.push(tunnelOrigin);
68
+ const reverseProxyOrigin = originFromGatewayPublicUrl(params.reverseProxyPublicUrl);
69
+ if (reverseProxyOrigin && !origins.includes(reverseProxyOrigin)) origins.push(reverseProxyOrigin);
68
70
  return origins;
69
71
  }
70
72
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","names":[],"sources":["../../../src/gateway/host.ts"],"sourcesContent":["import { DEFAULT_GATEWAY_PORT } from '../daemon/constants.js';\n\n/** True when the bind address is local-only (127.x, localhost, ::1). */\nexport function isLoopbackHost(host: string | undefined): boolean {\n if (!host) {\n return true;\n }\n const normalized = host.trim().toLowerCase();\n return (\n normalized === '127.0.0.1' ||\n normalized === 'localhost' ||\n normalized === '::1' ||\n normalized === '0:0:0:0:0:0:0:1'\n );\n}\n\n/** True when the gateway listens on all interfaces. */\nexport function isAllInterfacesHost(host: string | undefined): boolean {\n if (!host) {\n return false;\n }\n const normalized = host.trim();\n return normalized === '0.0.0.0' || normalized === '::' || normalized === '*';\n}\n\n/** Vite dev server origins for the gateway console (`web/` defaults to port 3000). */\nexport const GATEWAY_DEV_CONSOLE_ORIGINS = [\n 'http://localhost:3000',\n 'http://127.0.0.1:3000',\n] as const;\n\n/** Effective HTTP listen port: CLI `--port` override wins over config (default 18790). */\nexport function resolveEffectiveGatewayPort(\n config: { gateway?: { port?: number } },\n listenPortOverride?: number,\n): number {\n if (typeof listenPortOverride === 'number' && Number.isFinite(listenPortOverride)) {\n return listenPortOverride;\n }\n return config.gateway?.port ?? DEFAULT_GATEWAY_PORT;\n}\n\n/** Resolve listen port from a gateway service (supports partial test mocks without `getEffectiveListenPort`). */\nexport function resolveGatewayServiceListenPort(service: {\n currentConfig: { gateway?: { port?: number } };\n getEffectiveListenPort?: () => number;\n}): number {\n if (typeof service.getEffectiveListenPort === 'function') {\n return service.getEffectiveListenPort();\n }\n return resolveEffectiveGatewayPort(service.currentConfig);\n}\n\nexport function buildDefaultCorsOrigins(params: { port: number; bindHost?: string }): string[] {\n const origins = new Set<string>([\n `http://localhost:${params.port}`,\n `http://127.0.0.1:${params.port}`,\n ...GATEWAY_DEV_CONSOLE_ORIGINS,\n ]);\n const bindHost = params.bindHost?.trim();\n if (bindHost && !isLoopbackHost(bindHost) && !isAllInterfacesHost(bindHost)) {\n origins.add(`http://${bindHost}:${params.port}`);\n }\n return [...origins];\n}\n\n/**\n * Effective browser origins for CORS and CSRF checks.\n * Custom `gateway.corsOrigins` (e.g. after LAN pairing) still merge loopback Vite dev origins.\n */\nexport function resolveGatewayCorsOrigins(params: {\n configuredOrigins?: string[];\n port: number;\n bindHost?: string;\n}): string[] {\n const configured = (params.configuredOrigins ?? [])\n .map((origin) => origin.trim())\n .filter(Boolean);\n if (configured.length === 0) {\n return buildDefaultCorsOrigins({ port: params.port, bindHost: params.bindHost });\n }\n return [...new Set([...configured, ...GATEWAY_DEV_CONSOLE_ORIGINS])];\n}\n\n/** Browser origin (`https://host`) from a gateway public/tunnel root URL. */\nexport function originFromGatewayPublicUrl(publicUrl: string | null | undefined): string | null {\n const trimmed = publicUrl?.trim();\n if (!trimmed) return null;\n try {\n return new URL(trimmed).origin.toLowerCase();\n } catch {\n return null;\n }\n}\n\n/** CORS + CSRF allowlist including active FRP tunnel origin when connected. */\nexport function resolveAllowedBrowserOrigins(params: {\n configuredOrigins?: string[];\n port: number;\n bindHost?: string;\n tunnelPublicUrl?: string | null;\n}): string[] {\n const origins = resolveGatewayCorsOrigins({\n configuredOrigins: params.configuredOrigins,\n port: params.port,\n bindHost: params.bindHost,\n });\n const tunnelOrigin = originFromGatewayPublicUrl(params.tunnelPublicUrl);\n if (tunnelOrigin && !origins.includes(tunnelOrigin)) {\n origins.push(tunnelOrigin);\n }\n return origins;\n}\n"],"mappings":";;;AAGA,SAAgB,eAAe,MAAmC;AAChE,KAAI,CAAC,KACH,QAAO;CAET,MAAM,aAAa,KAAK,MAAM,CAAC,aAAa;AAC5C,QACE,eAAe,eACf,eAAe,eACf,eAAe,SACf,eAAe;;;AAKnB,SAAgB,oBAAoB,MAAmC;AACrE,KAAI,CAAC,KACH,QAAO;CAET,MAAM,aAAa,KAAK,MAAM;AAC9B,QAAO,eAAe,aAAa,eAAe,QAAQ,eAAe;;;AAI3E,MAAa,8BAA8B,CACzC,yBACA,wBACD;;AAGD,SAAgB,4BACd,QACA,oBACQ;AACR,KAAI,OAAO,uBAAuB,YAAY,OAAO,SAAS,mBAAmB,CAC/E,QAAO;AAET,QAAO,OAAO,SAAS,QAAA;;;AAIzB,SAAgB,gCAAgC,SAGrC;AACT,KAAI,OAAO,QAAQ,2BAA2B,WAC5C,QAAO,QAAQ,wBAAwB;AAEzC,QAAO,4BAA4B,QAAQ,cAAc;;AAG3D,SAAgB,wBAAwB,QAAuD;CAC7F,MAAM,UAAU,IAAI,IAAY;EAC9B,oBAAoB,OAAO;EAC3B,oBAAoB,OAAO;EAC3B,GAAG;EACJ,CAAC;CACF,MAAM,WAAW,OAAO,UAAU,MAAM;AACxC,KAAI,YAAY,CAAC,eAAe,SAAS,IAAI,CAAC,oBAAoB,SAAS,CACzE,SAAQ,IAAI,UAAU,SAAS,GAAG,OAAO,OAAO;AAElD,QAAO,CAAC,GAAG,QAAQ;;;;;;AAOrB,SAAgB,0BAA0B,QAI7B;CACX,MAAM,cAAc,OAAO,qBAAqB,EAAE,EAC/C,KAAK,WAAW,OAAO,MAAM,CAAC,CAC9B,OAAO,QAAQ;AAClB,KAAI,WAAW,WAAW,EACxB,QAAO,wBAAwB;EAAE,MAAM,OAAO;EAAM,UAAU,OAAO;EAAU,CAAC;AAElF,QAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,YAAY,GAAG,4BAA4B,CAAC,CAAC;;;AAItE,SAAgB,2BAA2B,WAAqD;CAC9F,MAAM,UAAU,WAAW,MAAM;AACjC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI;AACF,SAAO,IAAI,IAAI,QAAQ,CAAC,OAAO,aAAa;SACtC;AACN,SAAO;;;;AAKX,SAAgB,6BAA6B,QAKhC;CACX,MAAM,UAAU,0BAA0B;EACxC,mBAAmB,OAAO;EAC1B,MAAM,OAAO;EACb,UAAU,OAAO;EAClB,CAAC;CACF,MAAM,eAAe,2BAA2B,OAAO,gBAAgB;AACvE,KAAI,gBAAgB,CAAC,QAAQ,SAAS,aAAa,CACjD,SAAQ,KAAK,aAAa;AAE5B,QAAO"}
1
+ {"version":3,"file":"host.js","names":[],"sources":["../../../src/gateway/host.ts"],"sourcesContent":["import { DEFAULT_GATEWAY_PORT } from '../daemon/constants.js';\n\n/** True when the bind address is local-only (127.x, localhost, ::1). */\nexport function isLoopbackHost(host: string | undefined): boolean {\n if (!host) {\n return true;\n }\n const normalized = host.trim().toLowerCase();\n return (\n normalized === '127.0.0.1' ||\n normalized === 'localhost' ||\n normalized === '::1' ||\n normalized === '0:0:0:0:0:0:0:1'\n );\n}\n\n/** True when the gateway listens on all interfaces. */\nexport function isAllInterfacesHost(host: string | undefined): boolean {\n if (!host) {\n return false;\n }\n const normalized = host.trim();\n return normalized === '0.0.0.0' || normalized === '::' || normalized === '*';\n}\n\n/** Vite dev server origins for the gateway console (`web/` defaults to port 3000). */\nexport const GATEWAY_DEV_CONSOLE_ORIGINS = [\n 'http://localhost:3000',\n 'http://127.0.0.1:3000',\n] as const;\n\n/** Effective HTTP listen port: CLI `--port` override wins over config (default 18790). */\nexport function resolveEffectiveGatewayPort(\n config: { gateway?: { port?: number } },\n listenPortOverride?: number,\n): number {\n if (typeof listenPortOverride === 'number' && Number.isFinite(listenPortOverride)) {\n return listenPortOverride;\n }\n return config.gateway?.port ?? DEFAULT_GATEWAY_PORT;\n}\n\n/** Resolve listen port from a gateway service (supports partial test mocks without `getEffectiveListenPort`). */\nexport function resolveGatewayServiceListenPort(service: {\n currentConfig: { gateway?: { port?: number } };\n getEffectiveListenPort?: () => number;\n}): number {\n if (typeof service.getEffectiveListenPort === 'function') {\n return service.getEffectiveListenPort();\n }\n return resolveEffectiveGatewayPort(service.currentConfig);\n}\n\nexport function buildDefaultCorsOrigins(params: { port: number; bindHost?: string }): string[] {\n const origins = new Set<string>([\n `http://localhost:${params.port}`,\n `http://127.0.0.1:${params.port}`,\n ...GATEWAY_DEV_CONSOLE_ORIGINS,\n ]);\n const bindHost = params.bindHost?.trim();\n if (bindHost && !isLoopbackHost(bindHost) && !isAllInterfacesHost(bindHost)) {\n origins.add(`http://${bindHost}:${params.port}`);\n }\n return [...origins];\n}\n\n/**\n * Effective browser origins for CORS and CSRF checks.\n * Custom `gateway.corsOrigins` (e.g. after LAN pairing) still merge loopback Vite dev origins.\n */\nexport function resolveGatewayCorsOrigins(params: {\n configuredOrigins?: string[];\n port: number;\n bindHost?: string;\n}): string[] {\n const configured = (params.configuredOrigins ?? [])\n .map((origin) => origin.trim())\n .filter(Boolean);\n if (configured.length === 0) {\n return buildDefaultCorsOrigins({ port: params.port, bindHost: params.bindHost });\n }\n return [...new Set([...configured, ...GATEWAY_DEV_CONSOLE_ORIGINS])];\n}\n\n/** Browser origin (`https://host`) from a gateway public/tunnel root URL. */\nexport function originFromGatewayPublicUrl(publicUrl: string | null | undefined): string | null {\n const trimmed = publicUrl?.trim();\n if (!trimmed) return null;\n try {\n return new URL(trimmed).origin.toLowerCase();\n } catch {\n return null;\n }\n}\n\n/** CORS + CSRF allowlist including active FRP tunnel + reverse-proxy origins. */\nexport function resolveAllowedBrowserOrigins(params: {\n configuredOrigins?: string[];\n port: number;\n bindHost?: string;\n tunnelPublicUrl?: string | null;\n /** User-configured reverse-proxy public URL (gateway.publicUrl). */\n reverseProxyPublicUrl?: string | null;\n}): string[] {\n const origins = resolveGatewayCorsOrigins({\n configuredOrigins: params.configuredOrigins,\n port: params.port,\n bindHost: params.bindHost,\n });\n const tunnelOrigin = originFromGatewayPublicUrl(params.tunnelPublicUrl);\n if (tunnelOrigin && !origins.includes(tunnelOrigin)) {\n origins.push(tunnelOrigin);\n }\n const reverseProxyOrigin = originFromGatewayPublicUrl(params.reverseProxyPublicUrl);\n if (reverseProxyOrigin && !origins.includes(reverseProxyOrigin)) {\n origins.push(reverseProxyOrigin);\n }\n return origins;\n}\n"],"mappings":";;;AAGA,SAAgB,eAAe,MAAmC;AAChE,KAAI,CAAC,KACH,QAAO;CAET,MAAM,aAAa,KAAK,MAAM,CAAC,aAAa;AAC5C,QACE,eAAe,eACf,eAAe,eACf,eAAe,SACf,eAAe;;;AAKnB,SAAgB,oBAAoB,MAAmC;AACrE,KAAI,CAAC,KACH,QAAO;CAET,MAAM,aAAa,KAAK,MAAM;AAC9B,QAAO,eAAe,aAAa,eAAe,QAAQ,eAAe;;;AAI3E,MAAa,8BAA8B,CACzC,yBACA,wBACD;;AAGD,SAAgB,4BACd,QACA,oBACQ;AACR,KAAI,OAAO,uBAAuB,YAAY,OAAO,SAAS,mBAAmB,CAC/E,QAAO;AAET,QAAO,OAAO,SAAS,QAAA;;;AAIzB,SAAgB,gCAAgC,SAGrC;AACT,KAAI,OAAO,QAAQ,2BAA2B,WAC5C,QAAO,QAAQ,wBAAwB;AAEzC,QAAO,4BAA4B,QAAQ,cAAc;;AAG3D,SAAgB,wBAAwB,QAAuD;CAC7F,MAAM,UAAU,IAAI,IAAY;EAC9B,oBAAoB,OAAO;EAC3B,oBAAoB,OAAO;EAC3B,GAAG;EACJ,CAAC;CACF,MAAM,WAAW,OAAO,UAAU,MAAM;AACxC,KAAI,YAAY,CAAC,eAAe,SAAS,IAAI,CAAC,oBAAoB,SAAS,CACzE,SAAQ,IAAI,UAAU,SAAS,GAAG,OAAO,OAAO;AAElD,QAAO,CAAC,GAAG,QAAQ;;;;;;AAOrB,SAAgB,0BAA0B,QAI7B;CACX,MAAM,cAAc,OAAO,qBAAqB,EAAE,EAC/C,KAAK,WAAW,OAAO,MAAM,CAAC,CAC9B,OAAO,QAAQ;AAClB,KAAI,WAAW,WAAW,EACxB,QAAO,wBAAwB;EAAE,MAAM,OAAO;EAAM,UAAU,OAAO;EAAU,CAAC;AAElF,QAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,YAAY,GAAG,4BAA4B,CAAC,CAAC;;;AAItE,SAAgB,2BAA2B,WAAqD;CAC9F,MAAM,UAAU,WAAW,MAAM;AACjC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI;AACF,SAAO,IAAI,IAAI,QAAQ,CAAC,OAAO,aAAa;SACtC;AACN,SAAO;;;;AAKX,SAAgB,6BAA6B,QAOhC;CACX,MAAM,UAAU,0BAA0B;EACxC,mBAAmB,OAAO;EAC1B,MAAM,OAAO;EACb,UAAU,OAAO;EAClB,CAAC;CACF,MAAM,eAAe,2BAA2B,OAAO,gBAAgB;AACvE,KAAI,gBAAgB,CAAC,QAAQ,SAAS,aAAa,CACjD,SAAQ,KAAK,aAAa;CAE5B,MAAM,qBAAqB,2BAA2B,OAAO,sBAAsB;AACnF,KAAI,sBAAsB,CAAC,QAAQ,SAAS,mBAAmB,CAC7D,SAAQ,KAAK,mBAAmB;AAElC,QAAO"}
@@ -1,9 +1,9 @@
1
- import { createHash } from "node:crypto";
1
+ import path from "node:path";
2
+ import net from "node:net";
2
3
  import fsSync from "node:fs";
4
+ import { createHash } from "node:crypto";
3
5
  import fs from "node:fs/promises";
4
- import path from "node:path";
5
6
  import { homedir } from "os";
6
- import net from "node:net";
7
7
  //#region src/gateway/lock.ts
8
8
  /**
9
9
  * Gateway Lock - Prevents multiple gateway instances from running simultaneously
@@ -11,6 +11,12 @@ export type ForceFreePortResult = {
11
11
  escalatedToSigkill: boolean;
12
12
  };
13
13
  export declare function parseLsofOutput(output: string): PortProcess[];
14
+ /**
15
+ * Parse `netstat -ano` output (Windows) to find PIDs listening on a given port.
16
+ * Example line:
17
+ * TCP 0.0.0.0:18790 0.0.0.0:0 LISTENING 1234
18
+ */
19
+ export declare function parseNetstatOutput(output: string, port: number): PortProcess[];
14
20
  export declare function listPortListeners(port: number): PortProcess[];
15
21
  export declare function forceFreePortAndWait(port: number, opts?: {
16
22
  timeoutMs?: number;
@@ -1,7 +1,7 @@
1
1
  import { createLogger } from "../utils/logger/index.js";
2
2
  import { init_logger } from "../utils/logger.js";
3
- import fsSync from "node:fs";
4
3
  import net from "node:net";
4
+ import fsSync from "node:fs";
5
5
  import { execFileSync } from "node:child_process";
6
6
  //#region src/gateway/ports.ts
7
7
  /**
@@ -105,7 +105,43 @@ function listPortListenersViaProc(port) {
105
105
  }
106
106
  return results;
107
107
  }
108
+ /**
109
+ * Parse `netstat -ano` output (Windows) to find PIDs listening on a given port.
110
+ * Example line:
111
+ * TCP 0.0.0.0:18790 0.0.0.0:0 LISTENING 1234
112
+ */
113
+ function parseNetstatOutput(output, port) {
114
+ const portSuffix = `:${port}`;
115
+ const results = [];
116
+ for (const line of output.split(/\r?\n/)) {
117
+ if (!line.includes("LISTENING")) continue;
118
+ const parts = line.trim().split(/\s+/);
119
+ if (parts.length < 5) continue;
120
+ if (!parts[1].endsWith(portSuffix)) continue;
121
+ const pid = parseInt(parts[parts.length - 1], 10);
122
+ if (Number.isFinite(pid) && pid > 0 && !results.some((p) => p.pid === pid)) results.push({ pid });
123
+ }
124
+ return results;
125
+ }
126
+ function listPortListenersViaNetstat(port) {
127
+ let out;
128
+ try {
129
+ out = execFileSync("netstat", [
130
+ "-ano",
131
+ "-p",
132
+ "TCP"
133
+ ], {
134
+ encoding: "utf-8",
135
+ shell: true,
136
+ timeout: 5e3
137
+ });
138
+ } catch {
139
+ return [];
140
+ }
141
+ return parseNetstatOutput(out, port);
142
+ }
108
143
  function listPortListeners(port) {
144
+ if (process.platform === "win32") return listPortListenersViaNetstat(port);
109
145
  try {
110
146
  return parseLsofOutput(execFileSync("lsof", [
111
147
  "-nP",
@@ -194,6 +230,6 @@ async function checkPortAvailable(port, host = "0.0.0.0") {
194
230
  });
195
231
  }
196
232
  //#endregion
197
- export { checkPortAvailable, forceFreePortAndWait, listPortListeners, parseLsofOutput };
233
+ export { checkPortAvailable, forceFreePortAndWait, listPortListeners, parseLsofOutput, parseNetstatOutput };
198
234
 
199
235
  //# sourceMappingURL=ports.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ports.js","names":["execErr","fs"],"sources":["../../../src/gateway/ports.ts"],"sourcesContent":["/**\n * Ports Management - Port management utilities\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport net from \"node:net\";\nimport { createLogger } from \"../utils/logger.js\";\n\nconst log = createLogger(\"Ports\");\n\nexport type PortProcess = { pid: number; command?: string };\n\nexport type ForceFreePortResult = {\n killed: PortProcess[];\n waitedMs: number;\n escalatedToSigkill: boolean;\n};\n\n// Parse lsof output\nexport function parseLsofOutput(output: string): PortProcess[] {\n const lines = output.split(/\\r?\\n/).filter(Boolean);\n const results: PortProcess[] = [];\n let current: Partial<PortProcess> = {};\n\n for (const line of lines) {\n if (line.startsWith(\"p\")) {\n if (current.pid) {\n results.push(current as PortProcess);\n }\n current = { pid: parseInt(line.slice(1), 10) };\n } else if (line.startsWith(\"c\")) {\n current.command = line.slice(1);\n }\n }\n\n if (current.pid) {\n results.push(current as PortProcess);\n }\n\n return results;\n}\n\n/**\n * Parse `ss -tlnp` output to find PIDs listening on a given port.\n * Example line:\n * LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* users:((\"node\",pid=1234,fd=18))\n */\nfunction listPortListenersViaSs(port: number): PortProcess[] {\n let out: string;\n try {\n out = execFileSync(\"ss\", [\"-tlnp\", `sport = :${port}`], { encoding: \"utf-8\" });\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n if (execErr.status === 1) {\n return []; // No matching sockets\n }\n throw err instanceof Error ? err : new Error(String(err));\n }\n const results: PortProcess[] = [];\n\n for (const line of out.split(/\\r?\\n/)) {\n if (!line.includes(\"LISTEN\")) continue;\n for (const match of line.matchAll(/pid=(\\d+)/g)) {\n const pid = parseInt(match[1], 10);\n if (!results.some((p) => p.pid === pid)) {\n results.push({ pid });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Read /proc/net/tcp (and /proc/net/tcp6) to find PIDs listening on a given port.\n * Falls back to an empty list if /proc is unavailable (non-Linux).\n */\nfunction listPortListenersViaProc(port: number): PortProcess[] {\n const hexPort = port.toString(16).toUpperCase().padStart(4, \"0\");\n const results: PortProcess[] = [];\n const inodeSet = new Set<string>();\n\n for (const procFile of [\"/proc/net/tcp\", \"/proc/net/tcp6\"]) {\n let content: string;\n try {\n content = fs.readFileSync(procFile, \"utf-8\");\n } catch {\n continue;\n }\n\n for (const line of content.split(\"\\n\").slice(1)) {\n const parts = line.trim().split(/\\s+/);\n // state 0A = TCP_LISTEN\n if (parts.length < 10 || parts[3] !== \"0A\") continue;\n const localAddress = parts[1];\n const portHex = localAddress.split(\":\")[1];\n if (portHex?.toUpperCase() !== hexPort) continue;\n inodeSet.add(parts[9]);\n }\n }\n\n if (inodeSet.size === 0) return results;\n\n // Walk /proc/<pid>/fd to match socket inodes to PIDs\n let pidDirs: string[];\n try {\n pidDirs = fs.readdirSync(\"/proc\").filter((name) => /^\\d+$/.test(name));\n } catch {\n return results;\n }\n\n for (const pidStr of pidDirs) {\n const fdDir = `/proc/${pidStr}/fd`;\n let fds: string[];\n try {\n fds = fs.readdirSync(fdDir);\n } catch {\n continue;\n }\n\n for (const fd of fds) {\n let target: string;\n try {\n target = fs.readlinkSync(`${fdDir}/${fd}`);\n } catch {\n continue;\n }\n\n // symlink target looks like \"socket:[12345]\"\n const inodeMatch = /^socket:\\[(\\d+)\\]$/.exec(target);\n if (!inodeMatch || !inodeSet.has(inodeMatch[1])) continue;\n\n const pid = parseInt(pidStr, 10);\n if (!results.some((p) => p.pid === pid)) {\n let command: string | undefined;\n try {\n command = fs.readFileSync(`/proc/${pidStr}/comm`, \"utf-8\").trim();\n } catch {\n // comm not readable — leave undefined\n }\n results.push({ pid, command });\n }\n break;\n }\n }\n\n return results;\n}\n\n// List processes listening on port\nexport function listPortListeners(port: number): PortProcess[] {\n // Try lsof first (macOS + most Linux distros)\n try {\n const out = execFileSync(\"lsof\", [\"-nP\", `-iTCP:${port}`, \"-sTCP:LISTEN\", \"-FpFc\"], {\n encoding: \"utf-8\",\n });\n return parseLsofOutput(out);\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n\n if (execErr.code !== \"ENOENT\") {\n if (execErr.status === 1) return []; // No listeners\n throw err instanceof Error ? err : new Error(String(err));\n }\n // lsof not available — fall through to Linux alternatives\n log.debug({ port }, \"lsof not found; trying ss fallback\");\n }\n\n // Try ss (iproute2, available on most modern Linux systems)\n try {\n return listPortListenersViaSs(port);\n } catch (err: unknown) {\n const execErr = err as { code?: string };\n if (execErr.code !== \"ENOENT\") {\n throw err instanceof Error ? err : new Error(String(err));\n }\n log.debug({ port }, \"ss not found; trying /proc/net/tcp fallback\");\n }\n\n // Last resort: parse /proc/net/tcp directly (no external tools required)\n return listPortListenersViaProc(port);\n}\n\n// Force free port\nexport async function forceFreePortAndWait(\n port: number,\n opts: {\n timeoutMs?: number;\n intervalMs?: number;\n sigtermTimeoutMs?: number;\n } = {}\n): Promise<ForceFreePortResult> {\n const timeoutMs = Math.max(opts.timeoutMs ?? 2000, 0);\n const intervalMs = Math.max(opts.intervalMs ?? 100, 1);\n const sigtermTimeoutMs = Math.min(Math.max(opts.sigtermTimeoutMs ?? 700, 0), timeoutMs);\n\n // 1. Get listener list\n const listeners = listPortListeners(port);\n const killed: PortProcess[] = [...listeners];\n\n // 2. Send SIGTERM\n for (const proc of listeners) {\n try {\n process.kill(proc.pid, \"SIGTERM\");\n log.info({ pid: proc.pid }, \"Sent SIGTERM\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGTERM\");\n }\n }\n\n // 3. Wait for processes to exit\n let waitedMs = 0;\n const checkInterval = () => new Promise<void>((r) => setTimeout(r, intervalMs));\n\n // Wait for SIGTERM to take effect\n const sigtermTries = Math.ceil(sigtermTimeoutMs / intervalMs);\n for (let i = 0; i < sigtermTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const remaining = listPortListeners(port);\n if (remaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: false };\n }\n }\n\n // 4. SIGTERM timeout, send SIGKILL\n const remaining = listPortListeners(port);\n for (const proc of remaining) {\n try {\n process.kill(proc.pid, \"SIGKILL\");\n log.info({ pid: proc.pid }, \"Sent SIGKILL\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGKILL\");\n }\n }\n\n // 5. Wait for SIGKILL to take effect\n const remainingBudget = Math.max(timeoutMs - waitedMs, 0);\n const sigkillTries = Math.ceil(remainingBudget / intervalMs);\n\n for (let i = 0; i < sigkillTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const stillRemaining = listPortListeners(port);\n if (stillRemaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: true };\n }\n }\n\n throw new Error(`Port ${port} still has listeners after force free`);\n}\n\n// Check if port is available\nexport async function checkPortAvailable(port: number, host = \"0.0.0.0\"): Promise<boolean> {\n return new Promise((resolve) => {\n const server = net.createServer();\n\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n resolve(false);\n } else {\n resolve(true);\n }\n });\n\n server.once(\"listening\", () => {\n server.close();\n resolve(true);\n });\n\n server.listen(port, host);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ports.js","names":["execErr","fs"],"sources":["../../../src/gateway/ports.ts"],"sourcesContent":["/**\n * Ports Management - Port management utilities\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport net from \"node:net\";\nimport { createLogger } from \"../utils/logger.js\";\n\nconst log = createLogger(\"Ports\");\n\nexport type PortProcess = { pid: number; command?: string };\n\nexport type ForceFreePortResult = {\n killed: PortProcess[];\n waitedMs: number;\n escalatedToSigkill: boolean;\n};\n\n// Parse lsof output\nexport function parseLsofOutput(output: string): PortProcess[] {\n const lines = output.split(/\\r?\\n/).filter(Boolean);\n const results: PortProcess[] = [];\n let current: Partial<PortProcess> = {};\n\n for (const line of lines) {\n if (line.startsWith(\"p\")) {\n if (current.pid) {\n results.push(current as PortProcess);\n }\n current = { pid: parseInt(line.slice(1), 10) };\n } else if (line.startsWith(\"c\")) {\n current.command = line.slice(1);\n }\n }\n\n if (current.pid) {\n results.push(current as PortProcess);\n }\n\n return results;\n}\n\n/**\n * Parse `ss -tlnp` output to find PIDs listening on a given port.\n * Example line:\n * LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* users:((\"node\",pid=1234,fd=18))\n */\nfunction listPortListenersViaSs(port: number): PortProcess[] {\n let out: string;\n try {\n out = execFileSync(\"ss\", [\"-tlnp\", `sport = :${port}`], { encoding: \"utf-8\" });\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n if (execErr.status === 1) {\n return []; // No matching sockets\n }\n throw err instanceof Error ? err : new Error(String(err));\n }\n const results: PortProcess[] = [];\n\n for (const line of out.split(/\\r?\\n/)) {\n if (!line.includes(\"LISTEN\")) continue;\n for (const match of line.matchAll(/pid=(\\d+)/g)) {\n const pid = parseInt(match[1], 10);\n if (!results.some((p) => p.pid === pid)) {\n results.push({ pid });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Read /proc/net/tcp (and /proc/net/tcp6) to find PIDs listening on a given port.\n * Falls back to an empty list if /proc is unavailable (non-Linux).\n */\nfunction listPortListenersViaProc(port: number): PortProcess[] {\n const hexPort = port.toString(16).toUpperCase().padStart(4, \"0\");\n const results: PortProcess[] = [];\n const inodeSet = new Set<string>();\n\n for (const procFile of [\"/proc/net/tcp\", \"/proc/net/tcp6\"]) {\n let content: string;\n try {\n content = fs.readFileSync(procFile, \"utf-8\");\n } catch {\n continue;\n }\n\n for (const line of content.split(\"\\n\").slice(1)) {\n const parts = line.trim().split(/\\s+/);\n // state 0A = TCP_LISTEN\n if (parts.length < 10 || parts[3] !== \"0A\") continue;\n const localAddress = parts[1];\n const portHex = localAddress.split(\":\")[1];\n if (portHex?.toUpperCase() !== hexPort) continue;\n inodeSet.add(parts[9]);\n }\n }\n\n if (inodeSet.size === 0) return results;\n\n // Walk /proc/<pid>/fd to match socket inodes to PIDs\n let pidDirs: string[];\n try {\n pidDirs = fs.readdirSync(\"/proc\").filter((name) => /^\\d+$/.test(name));\n } catch {\n return results;\n }\n\n for (const pidStr of pidDirs) {\n const fdDir = `/proc/${pidStr}/fd`;\n let fds: string[];\n try {\n fds = fs.readdirSync(fdDir);\n } catch {\n continue;\n }\n\n for (const fd of fds) {\n let target: string;\n try {\n target = fs.readlinkSync(`${fdDir}/${fd}`);\n } catch {\n continue;\n }\n\n // symlink target looks like \"socket:[12345]\"\n const inodeMatch = /^socket:\\[(\\d+)\\]$/.exec(target);\n if (!inodeMatch || !inodeSet.has(inodeMatch[1])) continue;\n\n const pid = parseInt(pidStr, 10);\n if (!results.some((p) => p.pid === pid)) {\n let command: string | undefined;\n try {\n command = fs.readFileSync(`/proc/${pidStr}/comm`, \"utf-8\").trim();\n } catch {\n // comm not readable — leave undefined\n }\n results.push({ pid, command });\n }\n break;\n }\n }\n\n return results;\n}\n\n/**\n * Parse `netstat -ano` output (Windows) to find PIDs listening on a given port.\n * Example line:\n * TCP 0.0.0.0:18790 0.0.0.0:0 LISTENING 1234\n */\nexport function parseNetstatOutput(output: string, port: number): PortProcess[] {\n const portSuffix = `:${port}`;\n const results: PortProcess[] = [];\n\n for (const line of output.split(/\\r?\\n/)) {\n if (!line.includes(\"LISTENING\")) continue;\n const parts = line.trim().split(/\\s+/);\n // Format: TCP <local addr> <foreign addr> LISTENING <pid>\n if (parts.length < 5) continue;\n const localAddr = parts[1];\n if (!localAddr.endsWith(portSuffix)) continue;\n const pid = parseInt(parts[parts.length - 1], 10);\n if (Number.isFinite(pid) && pid > 0 && !results.some((p) => p.pid === pid)) {\n results.push({ pid });\n }\n }\n\n return results;\n}\n\nfunction listPortListenersViaNetstat(port: number): PortProcess[] {\n let out: string;\n try {\n out = execFileSync(\"netstat\", [\"-ano\", \"-p\", \"TCP\"], {\n encoding: \"utf-8\",\n shell: true,\n timeout: 5000,\n });\n } catch {\n return [];\n }\n\n return parseNetstatOutput(out, port);\n}\n\n// List processes listening on port\nexport function listPortListeners(port: number): PortProcess[] {\n // Windows: use netstat -ano\n if (process.platform === \"win32\") {\n return listPortListenersViaNetstat(port);\n }\n\n // Try lsof first (macOS + most Linux distros)\n try {\n const out = execFileSync(\"lsof\", [\"-nP\", `-iTCP:${port}`, \"-sTCP:LISTEN\", \"-FpFc\"], {\n encoding: \"utf-8\",\n });\n return parseLsofOutput(out);\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n\n if (execErr.code !== \"ENOENT\") {\n if (execErr.status === 1) return []; // No listeners\n throw err instanceof Error ? err : new Error(String(err));\n }\n // lsof not available — fall through to Linux alternatives\n log.debug({ port }, \"lsof not found; trying ss fallback\");\n }\n\n // Try ss (iproute2, available on most modern Linux systems)\n try {\n return listPortListenersViaSs(port);\n } catch (err: unknown) {\n const execErr = err as { code?: string };\n if (execErr.code !== \"ENOENT\") {\n throw err instanceof Error ? err : new Error(String(err));\n }\n log.debug({ port }, \"ss not found; trying /proc/net/tcp fallback\");\n }\n\n // Last resort: parse /proc/net/tcp directly (no external tools required)\n return listPortListenersViaProc(port);\n}\n\n// Force free port\nexport async function forceFreePortAndWait(\n port: number,\n opts: {\n timeoutMs?: number;\n intervalMs?: number;\n sigtermTimeoutMs?: number;\n } = {}\n): Promise<ForceFreePortResult> {\n const timeoutMs = Math.max(opts.timeoutMs ?? 2000, 0);\n const intervalMs = Math.max(opts.intervalMs ?? 100, 1);\n const sigtermTimeoutMs = Math.min(Math.max(opts.sigtermTimeoutMs ?? 700, 0), timeoutMs);\n\n // 1. Get listener list\n const listeners = listPortListeners(port);\n const killed: PortProcess[] = [...listeners];\n\n // 2. Send SIGTERM\n for (const proc of listeners) {\n try {\n process.kill(proc.pid, \"SIGTERM\");\n log.info({ pid: proc.pid }, \"Sent SIGTERM\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGTERM\");\n }\n }\n\n // 3. Wait for processes to exit\n let waitedMs = 0;\n const checkInterval = () => new Promise<void>((r) => setTimeout(r, intervalMs));\n\n // Wait for SIGTERM to take effect\n const sigtermTries = Math.ceil(sigtermTimeoutMs / intervalMs);\n for (let i = 0; i < sigtermTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const remaining = listPortListeners(port);\n if (remaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: false };\n }\n }\n\n // 4. SIGTERM timeout, send SIGKILL\n const remaining = listPortListeners(port);\n for (const proc of remaining) {\n try {\n process.kill(proc.pid, \"SIGKILL\");\n log.info({ pid: proc.pid }, \"Sent SIGKILL\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGKILL\");\n }\n }\n\n // 5. Wait for SIGKILL to take effect\n const remainingBudget = Math.max(timeoutMs - waitedMs, 0);\n const sigkillTries = Math.ceil(remainingBudget / intervalMs);\n\n for (let i = 0; i < sigkillTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const stillRemaining = listPortListeners(port);\n if (stillRemaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: true };\n }\n }\n\n throw new Error(`Port ${port} still has listeners after force free`);\n}\n\n// Check if port is available\nexport async function checkPortAvailable(port: number, host = \"0.0.0.0\"): Promise<boolean> {\n return new Promise((resolve) => {\n const server = net.createServer();\n\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n resolve(false);\n } else {\n resolve(true);\n }\n });\n\n server.once(\"listening\", () => {\n server.close();\n resolve(true);\n });\n\n server.listen(port, host);\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runtime accessors for `gateway.publicUrl` (the user-deployed reverse-proxy
3
+ * URL). Centralizes the "is it configured?" + "normalized origin" lookup so
4
+ * callers (pair-context, exchange-token, CORS allowlist, UI status) all agree
5
+ * on the same value.
6
+ */
7
+ import type { Config } from '../config/schema.js';
8
+ export declare function resolveReverseProxyPublicUrl(config: Config | undefined): string | null;
@@ -0,0 +1,10 @@
1
+ import { init_public_url, normalizePublicUrlOrNull } from "../config/public-url.js";
2
+ //#region src/gateway/public-url.ts
3
+ init_public_url();
4
+ function resolveReverseProxyPublicUrl(config) {
5
+ return normalizePublicUrlOrNull(config?.gateway?.publicUrl);
6
+ }
7
+ //#endregion
8
+ export { resolveReverseProxyPublicUrl };
9
+
10
+ //# sourceMappingURL=public-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-url.js","names":[],"sources":["../../../src/gateway/public-url.ts"],"sourcesContent":["/**\n * Runtime accessors for `gateway.publicUrl` (the user-deployed reverse-proxy\n * URL). Centralizes the \"is it configured?\" + \"normalized origin\" lookup so\n * callers (pair-context, exchange-token, CORS allowlist, UI status) all agree\n * on the same value.\n */\n\nimport type { Config } from '../config/schema.js';\nimport { normalizePublicUrlOrNull } from '../config/public-url.js';\n\nexport function resolveReverseProxyPublicUrl(config: Config | undefined): string | null {\n return normalizePublicUrlOrNull(config?.gateway?.publicUrl);\n}\n"],"mappings":";;iBAQmE;AAEnE,SAAgB,6BAA6B,QAA2C;AACtF,QAAO,yBAAyB,QAAQ,SAAS,UAAU"}
@@ -6,7 +6,7 @@
6
6
  */
7
7
  type OriginCheckResult = {
8
8
  ok: true;
9
- matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback';
9
+ matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback' | 'trusted-proxy-same-host';
10
10
  } | {
11
11
  ok: false;
12
12
  reason: string;
@@ -17,5 +17,13 @@ export declare function checkBrowserOrigin(params: {
17
17
  allowedOrigins?: string[];
18
18
  allowHostHeaderOriginFallback?: boolean;
19
19
  isLocalClient?: boolean;
20
+ /**
21
+ * When true, allow `Origin` whose host portion exactly equals the `Host`
22
+ * header. Only flip this on after verifying the TCP source is loopback or
23
+ * inside `gateway.trustedProxies` — otherwise an attacker who can set
24
+ * arbitrary Origin + Host (e.g. via an open SSRF) bypasses CSRF.
25
+ * Enables zero-config reverse-proxy access at the user's own domain.
26
+ */
27
+ autoAllowSameHostFromTrustedProxy?: boolean;
20
28
  }): OriginCheckResult;
21
29
  export {};
@@ -37,6 +37,10 @@ function checkBrowserOrigin(params) {
37
37
  matchedBy: "allowlist"
38
38
  };
39
39
  const requestHost = normalizeHostHeader(params.requestHost);
40
+ if (params.autoAllowSameHostFromTrustedProxy === true && requestHost && parsedOrigin.host === requestHost) return {
41
+ ok: true,
42
+ matchedBy: "trusted-proxy-same-host"
43
+ };
40
44
  if (params.allowHostHeaderOriginFallback === true && requestHost && parsedOrigin.host === requestHost) return {
41
45
  ok: true,
42
46
  matchedBy: "host-header-fallback"