@xopcai/xopc 0.0.27 → 0.0.29

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 (239) hide show
  1. package/dist/extensions/telegram/xopc.extension.json +1 -1
  2. package/dist/extensions/weixin/src/adapters/onboard-cli.d.ts +7 -0
  3. package/dist/extensions/weixin/src/adapters/onboard-cli.js +61 -0
  4. package/dist/extensions/weixin/src/adapters/onboard-cli.js.map +1 -0
  5. package/dist/extensions/weixin/src/cli/qr-login.d.ts +5 -0
  6. package/dist/extensions/weixin/src/cli/qr-login.js +1 -1
  7. package/dist/extensions/weixin/src/cli/qr-login.js.map +1 -1
  8. package/dist/extensions/weixin/src/index.js +1 -1
  9. package/dist/extensions/weixin/src/plugin.d.ts +1 -0
  10. package/dist/extensions/weixin/src/plugin.js +2 -0
  11. package/dist/extensions/weixin/src/plugin.js.map +1 -1
  12. package/dist/gateway/static/root/assets/agents-CkgFSiCY.js +216 -0
  13. package/dist/gateway/static/root/assets/agents-CkgFSiCY.js.map +1 -0
  14. package/dist/gateway/static/root/assets/{apps-page-CBBh_Ww8.js → apps-page-Bmq19MS-.js} +2 -2
  15. package/dist/gateway/static/root/assets/{apps-page-CBBh_Ww8.js.map → apps-page-Bmq19MS-.js.map} +1 -1
  16. package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js +9 -0
  17. package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js.map +1 -0
  18. package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js +2 -0
  19. package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js.map +1 -0
  20. package/dist/gateway/static/root/assets/{cron-utils-08gdQfl9.js → cron-utils-N1PqD2DB.js} +2 -2
  21. package/dist/gateway/static/root/assets/{cron-utils-08gdQfl9.js.map → cron-utils-N1PqD2DB.js.map} +1 -1
  22. package/dist/gateway/static/root/assets/{dist-C1MrygQH.js → dist--p2HQ2QF.js} +2 -2
  23. package/dist/gateway/static/root/assets/{dist-C1MrygQH.js.map → dist--p2HQ2QF.js.map} +1 -1
  24. package/dist/gateway/static/root/assets/{extension-debug-page-DN3HKUGS.js → extension-debug-page-DwHCB_6T.js} +2 -2
  25. package/dist/gateway/static/root/assets/{extension-debug-page-DN3HKUGS.js.map → extension-debug-page-DwHCB_6T.js.map} +1 -1
  26. package/dist/gateway/static/root/assets/{extension-page-CoFDHZtZ.js → extension-page-BsYwQIex.js} +2 -2
  27. package/dist/gateway/static/root/assets/{extension-page-CoFDHZtZ.js.map → extension-page-BsYwQIex.js.map} +1 -1
  28. package/dist/gateway/static/root/assets/{extension-settings-page-BcPCu_Go.js → extension-settings-page-nsisEgjB.js} +2 -2
  29. package/dist/gateway/static/root/assets/{extension-settings-page-BcPCu_Go.js.map → extension-settings-page-nsisEgjB.js.map} +1 -1
  30. package/dist/gateway/static/root/assets/index-CR8zUHGR.js +4734 -0
  31. package/dist/gateway/static/root/assets/{index-PfkB8N37.js.map → index-CR8zUHGR.js.map} +1 -1
  32. package/dist/gateway/static/root/assets/index-Dnfha4O2.css +1 -0
  33. package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js +2 -0
  34. package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js.map +1 -0
  35. package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js +2 -0
  36. package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js.map +1 -0
  37. package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js +2 -0
  38. package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js.map +1 -0
  39. package/dist/gateway/static/root/assets/skills-page-Clg8deH0.js +3 -0
  40. package/dist/gateway/static/root/assets/{skills-page-BmBDCEbY.js.map → skills-page-Clg8deH0.js.map} +1 -1
  41. package/dist/gateway/static/root/index.html +2 -2
  42. package/dist/package.js +1 -1
  43. package/dist/src/agent/lifecycle/hook-handler.d.ts +2 -0
  44. package/dist/src/agent/lifecycle/hook-handler.js +24 -0
  45. package/dist/src/agent/lifecycle/hook-handler.js.map +1 -1
  46. package/dist/src/agent/messaging/command-handler.js +10 -2
  47. package/dist/src/agent/messaging/command-handler.js.map +1 -1
  48. package/dist/src/agent/service/process-direct-streaming.js +77 -20
  49. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  50. package/dist/src/agent/service.d.ts +15 -0
  51. package/dist/src/agent/service.js +21 -1
  52. package/dist/src/agent/service.js.map +1 -1
  53. package/dist/src/channels/weixin/index.js +1 -1
  54. package/dist/src/cli/agent-chat-log-level-preset.d.ts +8 -0
  55. package/dist/src/cli/agent-chat-log-level-preset.js +25 -0
  56. package/dist/src/cli/agent-chat-log-level-preset.js.map +1 -0
  57. package/dist/src/cli/commands/agent/interactive.js +4 -2
  58. package/dist/src/cli/commands/agent/interactive.js.map +1 -1
  59. package/dist/src/cli/commands/agent/stream-renderer.d.ts +14 -0
  60. package/dist/src/cli/commands/agent/stream-renderer.js +99 -0
  61. package/dist/src/cli/commands/agent/stream-renderer.js.map +1 -0
  62. package/dist/src/cli/commands/agent.js +2 -2
  63. package/dist/src/cli/commands/agent.js.map +1 -1
  64. package/dist/src/cli/commands/onboard.js +77 -93
  65. package/dist/src/cli/commands/onboard.js.map +1 -1
  66. package/dist/src/cli/commands/tui.d.ts +1 -0
  67. package/dist/src/cli/commands/tui.js +40 -0
  68. package/dist/src/cli/commands/tui.js.map +1 -0
  69. package/dist/src/cli/index.d.ts +2 -0
  70. package/dist/src/cli/index.js +7 -3
  71. package/dist/src/cli/index.js.map +1 -1
  72. package/dist/src/config/schema.d.ts +6 -0
  73. package/dist/src/config/schema.js +11 -3
  74. package/dist/src/config/schema.js.map +1 -1
  75. package/dist/src/extensions/hooks.js +5 -1
  76. package/dist/src/extensions/hooks.js.map +1 -1
  77. package/dist/src/extensions/loader.d.ts +1 -0
  78. package/dist/src/extensions/loader.js +3 -1
  79. package/dist/src/extensions/loader.js.map +1 -1
  80. package/dist/src/extensions/sdk/index.d.ts +1 -1
  81. package/dist/src/extensions/sdk/index.js.map +1 -1
  82. package/dist/src/extensions/types/core.d.ts +8 -0
  83. package/dist/src/extensions/types/hooks.d.ts +16 -1
  84. package/dist/src/extensions/types/hooks.js +1 -0
  85. package/dist/src/extensions/types/hooks.js.map +1 -1
  86. package/dist/src/gateway/agents-admin.d.ts +19 -1
  87. package/dist/src/gateway/agents-admin.js +164 -3
  88. package/dist/src/gateway/agents-admin.js.map +1 -1
  89. package/dist/src/gateway/auth.d.ts +17 -3
  90. package/dist/src/gateway/auth.js +35 -16
  91. package/dist/src/gateway/auth.js.map +1 -1
  92. package/dist/src/gateway/hono/app.js +31 -1
  93. package/dist/src/gateway/hono/app.js.map +1 -1
  94. package/dist/src/gateway/hono/lib/config-payload.d.ts +1 -1
  95. package/dist/src/gateway/hono/middleware/auth.js +4 -3
  96. package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
  97. package/dist/src/gateway/hono/middleware/scopes.d.ts +15 -0
  98. package/dist/src/gateway/hono/middleware/scopes.js +41 -0
  99. package/dist/src/gateway/hono/middleware/scopes.js.map +1 -0
  100. package/dist/src/gateway/hono/routes/agents.js +59 -5
  101. package/dist/src/gateway/hono/routes/agents.js.map +1 -1
  102. package/dist/src/gateway/hono/routes/config.js +2 -2
  103. package/dist/src/gateway/hono/routes/config.js.map +1 -1
  104. package/dist/src/gateway/hono/routes/public-gateway.js +1 -0
  105. package/dist/src/gateway/hono/routes/public-gateway.js.map +1 -1
  106. package/dist/src/gateway/hono/routes/sessions.js +17 -0
  107. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  108. package/dist/src/gateway/security/audit.d.ts +18 -0
  109. package/dist/src/gateway/security/audit.js +68 -0
  110. package/dist/src/gateway/security/audit.js.map +1 -0
  111. package/dist/src/gateway/security/csp.d.ts +19 -0
  112. package/dist/src/gateway/security/csp.js +52 -0
  113. package/dist/src/gateway/security/csp.js.map +1 -0
  114. package/dist/src/gateway/security/dangerous-tools.d.ts +20 -0
  115. package/dist/src/gateway/security/dangerous-tools.js +46 -0
  116. package/dist/src/gateway/security/dangerous-tools.js.map +1 -0
  117. package/dist/src/gateway/security/flood-guard.d.ts +28 -0
  118. package/dist/src/gateway/security/flood-guard.js +42 -0
  119. package/dist/src/gateway/security/flood-guard.js.map +1 -0
  120. package/dist/src/gateway/security/index.d.ts +9 -0
  121. package/dist/src/gateway/security/index.js +10 -0
  122. package/dist/src/gateway/security/known-weak-secrets.d.ts +10 -0
  123. package/dist/src/gateway/security/known-weak-secrets.js +36 -0
  124. package/dist/src/gateway/security/known-weak-secrets.js.map +1 -0
  125. package/dist/src/gateway/security/operator-scopes.d.ts +37 -0
  126. package/dist/src/gateway/security/operator-scopes.js +137 -0
  127. package/dist/src/gateway/security/operator-scopes.js.map +1 -0
  128. package/dist/src/gateway/security/origin-check.d.ts +21 -0
  129. package/dist/src/gateway/security/origin-check.js +56 -0
  130. package/dist/src/gateway/security/origin-check.js.map +1 -0
  131. package/dist/src/gateway/security/preauth-connection-budget.d.ts +17 -0
  132. package/dist/src/gateway/security/preauth-connection-budget.js +49 -0
  133. package/dist/src/gateway/security/preauth-connection-budget.js.map +1 -0
  134. package/dist/src/gateway/security/secret-equal.d.ts +8 -0
  135. package/dist/src/gateway/security/secret-equal.js +30 -0
  136. package/dist/src/gateway/security/secret-equal.js.map +1 -0
  137. package/dist/src/gateway/service.d.ts +3 -1
  138. package/dist/src/gateway/service.js +40 -4
  139. package/dist/src/gateway/service.js.map +1 -1
  140. package/dist/src/session/client-history.d.ts +21 -0
  141. package/dist/src/session/client-history.js +89 -0
  142. package/dist/src/session/client-history.js.map +1 -0
  143. package/dist/src/session/index.d.ts +1 -0
  144. package/dist/src/session/index.js +2 -1
  145. package/dist/src/session/manager.d.ts +2 -0
  146. package/dist/src/session/manager.js +5 -0
  147. package/dist/src/session/manager.js.map +1 -1
  148. package/dist/src/session/thinking-resolve.js +1 -1
  149. package/dist/src/session/thinking-resolve.js.map +1 -1
  150. package/dist/src/tui/backends/embedded-backend.d.ts +42 -0
  151. package/dist/src/tui/backends/embedded-backend.js +173 -0
  152. package/dist/src/tui/backends/embedded-backend.js.map +1 -0
  153. package/dist/src/tui/backends/gateway-sse-backend.d.ts +53 -0
  154. package/dist/src/tui/backends/gateway-sse-backend.js +256 -0
  155. package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -0
  156. package/dist/src/tui/chat-history.d.ts +4 -0
  157. package/dist/src/tui/chat-history.js +29 -0
  158. package/dist/src/tui/chat-history.js.map +1 -0
  159. package/dist/src/tui/components/assistant-message.d.ts +6 -0
  160. package/dist/src/tui/components/assistant-message.js +19 -0
  161. package/dist/src/tui/components/assistant-message.js.map +1 -0
  162. package/dist/src/tui/components/chat-log.d.ts +21 -0
  163. package/dist/src/tui/components/chat-log.js +113 -0
  164. package/dist/src/tui/components/chat-log.js.map +1 -0
  165. package/dist/src/tui/components/custom-editor.d.ts +14 -0
  166. package/dist/src/tui/components/custom-editor.js +50 -0
  167. package/dist/src/tui/components/custom-editor.js.map +1 -0
  168. package/dist/src/tui/components/fuzzy-filter.d.ts +17 -0
  169. package/dist/src/tui/components/fuzzy-filter.js +85 -0
  170. package/dist/src/tui/components/fuzzy-filter.js.map +1 -0
  171. package/dist/src/tui/components/searchable-select-list.d.ts +39 -0
  172. package/dist/src/tui/components/searchable-select-list.js +257 -0
  173. package/dist/src/tui/components/searchable-select-list.js.map +1 -0
  174. package/dist/src/tui/components/tool-execution.d.ts +16 -0
  175. package/dist/src/tui/components/tool-execution.js +76 -0
  176. package/dist/src/tui/components/tool-execution.js.map +1 -0
  177. package/dist/src/tui/components/user-message.d.ts +6 -0
  178. package/dist/src/tui/components/user-message.js +22 -0
  179. package/dist/src/tui/components/user-message.js.map +1 -0
  180. package/dist/src/tui/sse-consumer.d.ts +15 -0
  181. package/dist/src/tui/sse-consumer.js +75 -0
  182. package/dist/src/tui/sse-consumer.js.map +1 -0
  183. package/dist/src/tui/stream-assembler.d.ts +22 -0
  184. package/dist/src/tui/stream-assembler.js +63 -0
  185. package/dist/src/tui/stream-assembler.js.map +1 -0
  186. package/dist/src/tui/theme.d.ts +73 -0
  187. package/dist/src/tui/theme.js +157 -0
  188. package/dist/src/tui/theme.js.map +1 -0
  189. package/dist/src/tui/tui-agent-events.d.ts +7 -0
  190. package/dist/src/tui/tui-agent-events.js +103 -0
  191. package/dist/src/tui/tui-agent-events.js.map +1 -0
  192. package/dist/src/tui/tui-backend.d.ts +80 -0
  193. package/dist/src/tui/tui-backend.js +1 -0
  194. package/dist/src/tui/tui-commands.d.ts +23 -0
  195. package/dist/src/tui/tui-commands.js +165 -0
  196. package/dist/src/tui/tui-commands.js.map +1 -0
  197. package/dist/src/tui/tui-lifecycle.d.ts +26 -0
  198. package/dist/src/tui/tui-lifecycle.js +57 -0
  199. package/dist/src/tui/tui-lifecycle.js.map +1 -0
  200. package/dist/src/tui/tui-local-shell.d.ts +28 -0
  201. package/dist/src/tui/tui-local-shell.js +147 -0
  202. package/dist/src/tui/tui-local-shell.js.map +1 -0
  203. package/dist/src/tui/tui-overlays.d.ts +8 -0
  204. package/dist/src/tui/tui-overlays.js +22 -0
  205. package/dist/src/tui/tui-overlays.js.map +1 -0
  206. package/dist/src/tui/tui-picker-overlay.d.ts +26 -0
  207. package/dist/src/tui/tui-picker-overlay.js +69 -0
  208. package/dist/src/tui/tui-picker-overlay.js.map +1 -0
  209. package/dist/src/tui/tui-stdio-filter.d.ts +17 -0
  210. package/dist/src/tui/tui-stdio-filter.js +96 -0
  211. package/dist/src/tui/tui-stdio-filter.js.map +1 -0
  212. package/dist/src/tui/tui-submit.d.ts +25 -0
  213. package/dist/src/tui/tui-submit.js +102 -0
  214. package/dist/src/tui/tui-submit.js.map +1 -0
  215. package/dist/src/tui/tui-suspend.d.ts +10 -0
  216. package/dist/src/tui/tui-suspend.js +18 -0
  217. package/dist/src/tui/tui-suspend.js.map +1 -0
  218. package/dist/src/tui/tui-types.d.ts +86 -0
  219. package/dist/src/tui/tui-types.js +21 -0
  220. package/dist/src/tui/tui-types.js.map +1 -0
  221. package/dist/src/tui/tui.d.ts +5 -0
  222. package/dist/src/tui/tui.js +389 -0
  223. package/dist/src/tui/tui.js.map +1 -0
  224. package/package.json +5 -3
  225. package/dist/gateway/static/root/assets/agents-w8_jzuiX.js +0 -216
  226. package/dist/gateway/static/root/assets/agents-w8_jzuiX.js.map +0 -1
  227. package/dist/gateway/static/root/assets/channels-settings-DUKRPC7C.js +0 -9
  228. package/dist/gateway/static/root/assets/channels-settings-DUKRPC7C.js.map +0 -1
  229. package/dist/gateway/static/root/assets/cron-page-S18t1yG-.js +0 -2
  230. package/dist/gateway/static/root/assets/cron-page-S18t1yG-.js.map +0 -1
  231. package/dist/gateway/static/root/assets/index-OT4cGzon.css +0 -1
  232. package/dist/gateway/static/root/assets/index-PfkB8N37.js +0 -4734
  233. package/dist/gateway/static/root/assets/logs-page-DoWe1GWy.js +0 -2
  234. package/dist/gateway/static/root/assets/logs-page-DoWe1GWy.js.map +0 -1
  235. package/dist/gateway/static/root/assets/sessions-page-2uOYwEwd.js +0 -2
  236. package/dist/gateway/static/root/assets/sessions-page-2uOYwEwd.js.map +0 -1
  237. package/dist/gateway/static/root/assets/settings-page-fQWswCuq.js +0 -2
  238. package/dist/gateway/static/root/assets/settings-page-fQWswCuq.js.map +0 -1
  239. package/dist/gateway/static/root/assets/skills-page-BmBDCEbY.js +0 -3
@@ -1 +1 @@
1
- {"version":3,"file":"public-gateway.js","names":[],"sources":["../../../../../src/gateway/hono/routes/public-gateway.ts"],"sourcesContent":["import type { Hono } from 'hono';\n\nimport { PACKAGE_VERSION } from '../../../package-version.js';\nimport type { GatewayService } from '../../service.js';\nimport { serveStaticFile } from '../lib/static-ui.js';\n\nexport function registerPublicGatewayRoutes(app: Hono, service: GatewayService): void {\n app.get('/health', (c) => {\n return c.json(service.getHealth());\n });\n\n /** Public liveness probe (no auth) — minimal payload for CLI / load balancers. */\n app.get('/api/health', (c) => {\n const health = service.getHealth();\n return c.json({\n status: 'ok',\n version: health.version,\n uptime: health.uptime,\n });\n });\n\n app.get('/api', (c) => {\n return c.json({\n service: 'xopc-gateway',\n version: PACKAGE_VERSION,\n transport: 'streamable-http',\n endpoints: [\n 'GET /health',\n 'GET /api/health',\n 'GET /status',\n 'GET /api/status',\n 'POST /api/agent (SSE stream / JSON)',\n 'POST /api/agent/abort',\n 'POST /api/agent/steer',\n 'POST /api/send',\n 'GET /api/events (SSE stream)',\n 'GET /api/channels/status',\n 'POST /api/channels/weixin/login/start',\n 'GET /api/channels/weixin/login/:sessionKey',\n 'GET /api/config',\n 'GET /api/agents',\n 'POST /api/agents',\n 'PATCH /api/agents/:id',\n 'DELETE /api/agents/:id',\n 'GET/PUT /api/agents/:id/files/...',\n 'DELETE /api/providers/:providerId/key',\n 'PATCH /api/config',\n 'POST /api/config/reload',\n 'POST /api/heartbeat/trigger',\n '... /api/cron/*',\n 'GET/PATCH /api/sessions/:key/agent-config',\n '... /api/sessions/*',\n 'GET /api/host/fs/meta',\n 'GET /api/host/fs/list',\n ],\n });\n });\n\n app.get('/assets/*', (c) => {\n const path = c.req.path.replace('/assets/', '');\n const response = serveStaticFile(`assets/${path}`);\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n /** From `web/public/channel-icons/` (Vite copies to static root). Public: img requests send no Bearer token. */\n app.get('/channel-icons/*', (c) => {\n const path = c.req.path.replace('/channel-icons/', '');\n const response = serveStaticFile(`channel-icons/${path}`);\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/favicon.ico', (c) => {\n const response = serveStaticFile('favicon.ico');\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/logo.svg', (c) => {\n const response = serveStaticFile('logo.svg');\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/logo-dark.svg', (c) => {\n const response = serveStaticFile('logo-dark.svg');\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/', (c) => {\n const response = serveStaticFile('index.html');\n if (response) return response;\n return c.text('UI not found', 404);\n });\n}\n"],"mappings":";;;sBAE8D;AAI9D,SAAgB,4BAA4B,KAAW,SAA+B;AACpF,KAAI,IAAI,YAAY,MAAM;AACxB,SAAO,EAAE,KAAK,QAAQ,WAAW,CAAC;GAClC;;AAGF,KAAI,IAAI,gBAAgB,MAAM;EAC5B,MAAM,SAAS,QAAQ,WAAW;AAClC,SAAO,EAAE,KAAK;GACZ,QAAQ;GACR,SAAS,OAAO;GAChB,QAAQ,OAAO;GAChB,CAAC;GACF;AAEF,KAAI,IAAI,SAAS,MAAM;AACrB,SAAO,EAAE,KAAK;GACZ,SAAS;GACT,SAAS;GACT,WAAW;GACX,WAAW;IACT;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACD;GACF,CAAC;GACF;AAEF,KAAI,IAAI,cAAc,MAAM;EAE1B,MAAM,WAAW,gBAAgB,UADpB,EAAE,IAAI,KAAK,QAAQ,YAAY,GACG,GAAG;AAClD,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;;AAGF,KAAI,IAAI,qBAAqB,MAAM;EAEjC,MAAM,WAAW,gBAAgB,iBADpB,EAAE,IAAI,KAAK,QAAQ,mBAAmB,GACG,GAAG;AACzD,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,iBAAiB,MAAM;EAC7B,MAAM,WAAW,gBAAgB,cAAc;AAC/C,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,cAAc,MAAM;EAC1B,MAAM,WAAW,gBAAgB,WAAW;AAC5C,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,mBAAmB,MAAM;EAC/B,MAAM,WAAW,gBAAgB,gBAAgB;AACjD,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,MAAM,MAAM;EAClB,MAAM,WAAW,gBAAgB,aAAa;AAC9C,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,gBAAgB,IAAI;GAClC"}
1
+ {"version":3,"file":"public-gateway.js","names":[],"sources":["../../../../../src/gateway/hono/routes/public-gateway.ts"],"sourcesContent":["import type { Hono } from 'hono';\n\nimport { PACKAGE_VERSION } from '../../../package-version.js';\nimport type { GatewayService } from '../../service.js';\nimport { serveStaticFile } from '../lib/static-ui.js';\n\nexport function registerPublicGatewayRoutes(app: Hono, service: GatewayService): void {\n app.get('/health', (c) => {\n return c.json(service.getHealth());\n });\n\n /** Public liveness probe (no auth) — minimal payload for CLI / load balancers. */\n app.get('/api/health', (c) => {\n const health = service.getHealth();\n return c.json({\n status: 'ok',\n version: health.version,\n uptime: health.uptime,\n });\n });\n\n app.get('/api', (c) => {\n return c.json({\n service: 'xopc-gateway',\n version: PACKAGE_VERSION,\n transport: 'streamable-http',\n endpoints: [\n 'GET /health',\n 'GET /api/health',\n 'GET /status',\n 'GET /api/status',\n 'POST /api/agent (SSE stream / JSON)',\n 'POST /api/agent/abort',\n 'POST /api/agent/steer',\n 'POST /api/send',\n 'GET /api/events (SSE stream)',\n 'GET /api/channels/status',\n 'POST /api/channels/weixin/login/start',\n 'GET /api/channels/weixin/login/:sessionKey',\n 'GET /api/config',\n 'GET /api/agents',\n 'POST /api/agents',\n 'PATCH /api/agents/:id',\n 'DELETE /api/agents/:id',\n 'GET/PUT/DELETE /api/agents/:id/avatar',\n 'GET/PUT /api/agents/:id/files/...',\n 'DELETE /api/providers/:providerId/key',\n 'PATCH /api/config',\n 'POST /api/config/reload',\n 'POST /api/heartbeat/trigger',\n '... /api/cron/*',\n 'GET/PATCH /api/sessions/:key/agent-config',\n '... /api/sessions/*',\n 'GET /api/host/fs/meta',\n 'GET /api/host/fs/list',\n ],\n });\n });\n\n app.get('/assets/*', (c) => {\n const path = c.req.path.replace('/assets/', '');\n const response = serveStaticFile(`assets/${path}`);\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n /** From `web/public/channel-icons/` (Vite copies to static root). Public: img requests send no Bearer token. */\n app.get('/channel-icons/*', (c) => {\n const path = c.req.path.replace('/channel-icons/', '');\n const response = serveStaticFile(`channel-icons/${path}`);\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/favicon.ico', (c) => {\n const response = serveStaticFile('favicon.ico');\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/logo.svg', (c) => {\n const response = serveStaticFile('logo.svg');\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/logo-dark.svg', (c) => {\n const response = serveStaticFile('logo-dark.svg');\n if (response) return response;\n return c.text('Not found', 404);\n });\n\n app.get('/', (c) => {\n const response = serveStaticFile('index.html');\n if (response) return response;\n return c.text('UI not found', 404);\n });\n}\n"],"mappings":";;;sBAE8D;AAI9D,SAAgB,4BAA4B,KAAW,SAA+B;AACpF,KAAI,IAAI,YAAY,MAAM;AACxB,SAAO,EAAE,KAAK,QAAQ,WAAW,CAAC;GAClC;;AAGF,KAAI,IAAI,gBAAgB,MAAM;EAC5B,MAAM,SAAS,QAAQ,WAAW;AAClC,SAAO,EAAE,KAAK;GACZ,QAAQ;GACR,SAAS,OAAO;GAChB,QAAQ,OAAO;GAChB,CAAC;GACF;AAEF,KAAI,IAAI,SAAS,MAAM;AACrB,SAAO,EAAE,KAAK;GACZ,SAAS;GACT,SAAS;GACT,WAAW;GACX,WAAW;IACT;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACD;GACF,CAAC;GACF;AAEF,KAAI,IAAI,cAAc,MAAM;EAE1B,MAAM,WAAW,gBAAgB,UADpB,EAAE,IAAI,KAAK,QAAQ,YAAY,GACG,GAAG;AAClD,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;;AAGF,KAAI,IAAI,qBAAqB,MAAM;EAEjC,MAAM,WAAW,gBAAgB,iBADpB,EAAE,IAAI,KAAK,QAAQ,mBAAmB,GACG,GAAG;AACzD,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,iBAAiB,MAAM;EAC7B,MAAM,WAAW,gBAAgB,cAAc;AAC/C,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,cAAc,MAAM;EAC1B,MAAM,WAAW,gBAAgB,WAAW;AAC5C,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,mBAAmB,MAAM;EAC/B,MAAM,WAAW,gBAAgB,gBAAgB;AACjD,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,aAAa,IAAI;GAC/B;AAEF,KAAI,IAAI,MAAM,MAAM;EAClB,MAAM,WAAW,gBAAgB,aAAa;AAC9C,MAAI,SAAU,QAAO;AACrB,SAAO,EAAE,KAAK,gBAAgB,IAAI;GAClC"}
@@ -1,5 +1,6 @@
1
1
  import { buildSessionKey, init_session_key, parseSessionKey } from "../../../routing/session-key.js";
2
2
  import { agentExists, getDefaultAgentId, init_resolve_route } from "../../../routing/resolve-route.js";
3
+ import { messagesToClientHistory } from "../../../session/client-history.js";
3
4
  //#region src/gateway/hono/routes/sessions.ts
4
5
  init_session_key();
5
6
  init_resolve_route();
@@ -92,6 +93,22 @@ function registerSessionsRoutes(authenticated, deps) {
92
93
  }, 400);
93
94
  return c.json({ ok: true });
94
95
  });
96
+ authenticated.get("/api/sessions/:key/messages", async (c) => {
97
+ const key = c.req.param("key");
98
+ const limitRaw = c.req.query("limit");
99
+ const parsedLimit = limitRaw ? Number.parseInt(limitRaw, 10) : void 0;
100
+ const limit = parsedLimit !== void 0 && Number.isFinite(parsedLimit) ? Math.min(500, Math.max(1, parsedLimit)) : void 0;
101
+ const session = await service.getSession(key);
102
+ if (!session) return c.json({
103
+ ok: false,
104
+ error: "Session not found"
105
+ }, 404);
106
+ const messages = messagesToClientHistory(session.messages, { limit });
107
+ return c.json({
108
+ ok: true,
109
+ payload: { messages }
110
+ });
111
+ });
95
112
  authenticated.get("/api/sessions/:key", async (c) => {
96
113
  const key = c.req.param("key");
97
114
  const session = await service.getSession(key);
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.js","names":[],"sources":["../../../../../src/gateway/hono/routes/sessions.ts"],"sourcesContent":["import type { Hono } from 'hono';\n\nimport { buildSessionKey, parseSessionKey } from '../../../routing/session-key.js';\nimport { agentExists, getDefaultAgentId } from '../../../routing/resolve-route.js';\nimport type { AuthenticatedRouteDeps } from './deps.js';\n\nexport function registerSessionsRoutes(authenticated: Hono, deps: AuthenticatedRouteDeps): void {\n const { service } = deps;\n\n // ========== Session REST API (/api/sessions) ==========\n\n // POST /api/sessions - Create new session (reuses empty sessions)\n authenticated.post('/api/sessions', async (c) => {\n const body = await c.req.json().catch(() => ({}));\n const channel = body.channel || 'webchat';\n const routingCfg = service.currentConfig;\n let agentId =\n typeof body.agentId === 'string' && body.agentId.trim()\n ? body.agentId.trim().toLowerCase()\n : getDefaultAgentId(routingCfg);\n if (!agentExists(agentId, routingCfg)) {\n agentId = getDefaultAgentId(routingCfg);\n }\n\n // If a specific chat_id is provided, use it (for advanced use cases)\n // Otherwise, try to find and reuse an existing empty session\n if (body.chat_id) {\n const sessionKey = buildSessionKey({\n agentId,\n source: channel,\n accountId: 'default',\n peerKind: 'direct',\n peerId: body.chat_id,\n });\n\n await service.sessionManagerInstance.saveMessages(sessionKey, []);\n const session = await service.getSession(sessionKey);\n return c.json({ session }, 201);\n }\n \n // Look for existing empty sessions to reuse\n const existingSessions = await service.listSessions({\n channel,\n limit: 50,\n sortBy: 'updatedAt',\n sortOrder: 'desc',\n });\n \n // Reuse an empty session only when it matches the requested agent (session key embeds agent id).\n const emptySession = existingSessions.items.find((s) => {\n if (s.messageCount !== 0) return false;\n const parsed = parseSessionKey(s.key);\n return parsed?.agentId === agentId;\n });\n \n if (emptySession) {\n // Return existing empty session instead of creating a new one\n const session = await service.getSession(emptySession.key);\n return c.json({ session, reused: true }, 200);\n }\n \n // No empty session found, create a new one\n const chatId = `chat_${Date.now()}`;\n const sessionKey = buildSessionKey({\n agentId,\n source: channel,\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n\n await service.sessionManagerInstance.saveMessages(sessionKey, []);\n\n const session = await service.getSession(sessionKey);\n return c.json({ session }, 201);\n });\n\n // GET /api/sessions - List sessions\n authenticated.get('/api/sessions', async (c) => {\n const query = c.req.query();\n const result = await service.listSessions({\n status: query.status as any,\n search: query.search,\n channel: query.channel,\n limit: query.limit ? parseInt(query.limit) : undefined,\n offset: query.offset ? parseInt(query.offset) : undefined,\n });\n return c.json(result);\n });\n\n // GET /api/sessions/stats - Get session stats (must be before /:key)\n authenticated.get('/api/sessions/stats', async (c) => {\n const result = await service.getSessionStats();\n return c.json(result);\n });\n\n // GET /api/sessions/chat-ids - Get unique chat IDs from sessions (must be before /:key)\n authenticated.get('/api/sessions/chat-ids', async (c) => {\n const channel = c.req.query('channel');\n const chatIds = await service.getSessionChatIds(channel || undefined);\n return c.json({ ok: true, payload: { chatIds } });\n });\n\n // GET /api/sessions/:key/agent-config — resolved session agent settings (thinking, etc.)\n authenticated.get('/api/sessions/:key/agent-config', async (c) => {\n const key = c.req.param('key');\n const payload = await service.getSessionAgentConfig(key);\n return c.json({ ok: true, payload });\n });\n\n authenticated.patch('/api/sessions/:key/agent-config', async (c) => {\n const key = c.req.param('key');\n const body = await c.req.json().catch(() => ({}));\n const result = await service.patchSessionAgentConfig(key, body);\n if (!result.ok) {\n return c.json({ ok: false, error: result.error }, 400);\n }\n return c.json({ ok: true });\n });\n\n // GET /api/sessions/:key - Get single session (must be after /stats and /chat-ids)\n authenticated.get('/api/sessions/:key', async (c) => {\n const key = c.req.param('key');\n const session = await service.getSession(key);\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n return c.json({ session });\n });\n\n // GET /api/sessions/:key/export - Export session (must be before /:key)\n authenticated.get('/api/sessions/:key/export', async (c) => {\n const key = c.req.param('key');\n const format = c.req.query('format') as any || 'json';\n const result = await service.exportSession(key, format);\n return c.json(result);\n });\n\n // DELETE /api/sessions/:key/messages — delete a range of messages by index (before whole-session DELETE)\n authenticated.delete('/api/sessions/:key/messages', async (c) => {\n const key = c.req.param('key');\n const body = await c.req.json().catch(() => ({}));\n const startIndex = typeof body.startIndex === 'number' ? body.startIndex : -1;\n const count = typeof body.count === 'number' ? body.count : 0;\n if (startIndex < 0 || count <= 0) {\n return c.json({ error: 'Invalid startIndex or count' }, 400);\n }\n const loaded = await service.sessionManagerInstance.loadMessages(key);\n if (!loaded || startIndex >= loaded.length) {\n return c.json({ error: 'Index out of range' }, 400);\n }\n const deleteCount = Math.min(count, loaded.length - startIndex);\n const next = loaded.slice(0, startIndex).concat(loaded.slice(startIndex + deleteCount));\n await service.sessionManagerInstance.saveMessages(key, next);\n return c.json({ ok: true, deleted: deleteCount });\n });\n\n // DELETE /api/sessions/:key - Delete session\n authenticated.delete('/api/sessions/:key', async (c) => {\n const key = c.req.param('key');\n const result = await service.deleteSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/archive - Archive session\n authenticated.post('/api/sessions/:key/archive', async (c) => {\n const key = c.req.param('key');\n const result = await service.archiveSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/unarchive - Unarchive session\n authenticated.post('/api/sessions/:key/unarchive', async (c) => {\n const key = c.req.param('key');\n const result = await service.unarchiveSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/pin - Pin session\n authenticated.post('/api/sessions/:key/pin', async (c) => {\n const key = c.req.param('key');\n const result = await service.pinSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/unpin - Unpin session\n authenticated.post('/api/sessions/:key/unpin', async (c) => {\n const key = c.req.param('key');\n const result = await service.unpinSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/rename - Rename session\n authenticated.post('/api/sessions/:key/rename', async (c) => {\n const key = c.req.param('key');\n\n const body = await c.req.json();\n const { name } = body;\n const result = await service.renameSession(key, name);\n return c.json(result);\n });\n\n // ========== Subagent REST API (/api/subagents) ==========\n\n // GET /api/subagents - List subagent sessions\n authenticated.get('/api/subagents', async (c) => {\n const query = c.req.query();\n const result = await service.listSubagents({\n limit: query.limit ? parseInt(query.limit) : undefined,\n offset: query.offset ? parseInt(query.offset) : undefined,\n });\n return c.json(result);\n });\n\n // GET /api/subagents/:key - Get subagent session detail\n authenticated.get('/api/subagents/:key', async (c) => {\n const key = c.req.param('key');\n // Verify it's a subagent session\n if (!key.startsWith('subagent:')) {\n return c.json({ error: 'Not a subagent session' }, 400);\n }\n const session = await service.getSession(key);\n if (!session) {\n return c.json({ error: 'Subagent session not found' }, 404);\n }\n return c.json({ session });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sessions.js","names":[],"sources":["../../../../../src/gateway/hono/routes/sessions.ts"],"sourcesContent":["import type { Hono } from 'hono';\n\nimport { buildSessionKey, parseSessionKey } from '../../../routing/session-key.js';\nimport { agentExists, getDefaultAgentId } from '../../../routing/resolve-route.js';\nimport type { AuthenticatedRouteDeps } from './deps.js';\nimport { messagesToClientHistory } from '../../../session/client-history.js';\n\nexport function registerSessionsRoutes(authenticated: Hono, deps: AuthenticatedRouteDeps): void {\n const { service } = deps;\n\n // ========== Session REST API (/api/sessions) ==========\n\n // POST /api/sessions - Create new session (reuses empty sessions)\n authenticated.post('/api/sessions', async (c) => {\n const body = await c.req.json().catch(() => ({}));\n const channel = body.channel || 'webchat';\n const routingCfg = service.currentConfig;\n let agentId =\n typeof body.agentId === 'string' && body.agentId.trim()\n ? body.agentId.trim().toLowerCase()\n : getDefaultAgentId(routingCfg);\n if (!agentExists(agentId, routingCfg)) {\n agentId = getDefaultAgentId(routingCfg);\n }\n\n // If a specific chat_id is provided, use it (for advanced use cases)\n // Otherwise, try to find and reuse an existing empty session\n if (body.chat_id) {\n const sessionKey = buildSessionKey({\n agentId,\n source: channel,\n accountId: 'default',\n peerKind: 'direct',\n peerId: body.chat_id,\n });\n\n await service.sessionManagerInstance.saveMessages(sessionKey, []);\n const session = await service.getSession(sessionKey);\n return c.json({ session }, 201);\n }\n \n // Look for existing empty sessions to reuse\n const existingSessions = await service.listSessions({\n channel,\n limit: 50,\n sortBy: 'updatedAt',\n sortOrder: 'desc',\n });\n \n // Reuse an empty session only when it matches the requested agent (session key embeds agent id).\n const emptySession = existingSessions.items.find((s) => {\n if (s.messageCount !== 0) return false;\n const parsed = parseSessionKey(s.key);\n return parsed?.agentId === agentId;\n });\n \n if (emptySession) {\n // Return existing empty session instead of creating a new one\n const session = await service.getSession(emptySession.key);\n return c.json({ session, reused: true }, 200);\n }\n \n // No empty session found, create a new one\n const chatId = `chat_${Date.now()}`;\n const sessionKey = buildSessionKey({\n agentId,\n source: channel,\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n\n await service.sessionManagerInstance.saveMessages(sessionKey, []);\n\n const session = await service.getSession(sessionKey);\n return c.json({ session }, 201);\n });\n\n // GET /api/sessions - List sessions\n authenticated.get('/api/sessions', async (c) => {\n const query = c.req.query();\n const result = await service.listSessions({\n status: query.status as any,\n search: query.search,\n channel: query.channel,\n limit: query.limit ? parseInt(query.limit) : undefined,\n offset: query.offset ? parseInt(query.offset) : undefined,\n });\n return c.json(result);\n });\n\n // GET /api/sessions/stats - Get session stats (must be before /:key)\n authenticated.get('/api/sessions/stats', async (c) => {\n const result = await service.getSessionStats();\n return c.json(result);\n });\n\n // GET /api/sessions/chat-ids - Get unique chat IDs from sessions (must be before /:key)\n authenticated.get('/api/sessions/chat-ids', async (c) => {\n const channel = c.req.query('channel');\n const chatIds = await service.getSessionChatIds(channel || undefined);\n return c.json({ ok: true, payload: { chatIds } });\n });\n\n // GET /api/sessions/:key/agent-config — resolved session agent settings (thinking, etc.)\n authenticated.get('/api/sessions/:key/agent-config', async (c) => {\n const key = c.req.param('key');\n const payload = await service.getSessionAgentConfig(key);\n return c.json({ ok: true, payload });\n });\n\n authenticated.patch('/api/sessions/:key/agent-config', async (c) => {\n const key = c.req.param('key');\n const body = await c.req.json().catch(() => ({}));\n const result = await service.patchSessionAgentConfig(key, body);\n if (!result.ok) {\n return c.json({ ok: false, error: result.error }, 400);\n }\n return c.json({ ok: true });\n });\n\n // GET /api/sessions/:key/messages — flattened transcript for TUI / clients\n authenticated.get('/api/sessions/:key/messages', async (c) => {\n const key = c.req.param('key');\n const limitRaw = c.req.query('limit');\n const parsedLimit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;\n const limit =\n parsedLimit !== undefined && Number.isFinite(parsedLimit)\n ? Math.min(500, Math.max(1, parsedLimit))\n : undefined;\n\n const session = await service.getSession(key);\n if (!session) {\n return c.json({ ok: false, error: 'Session not found' }, 404);\n }\n\n const messages = messagesToClientHistory(session.messages, { limit });\n return c.json({ ok: true, payload: { messages } });\n });\n\n // GET /api/sessions/:key - Get single session (must be after /stats and /chat-ids)\n authenticated.get('/api/sessions/:key', async (c) => {\n const key = c.req.param('key');\n const session = await service.getSession(key);\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n return c.json({ session });\n });\n\n // GET /api/sessions/:key/export - Export session (must be before /:key)\n authenticated.get('/api/sessions/:key/export', async (c) => {\n const key = c.req.param('key');\n const format = c.req.query('format') as any || 'json';\n const result = await service.exportSession(key, format);\n return c.json(result);\n });\n\n // DELETE /api/sessions/:key/messages — delete a range of messages by index (before whole-session DELETE)\n authenticated.delete('/api/sessions/:key/messages', async (c) => {\n const key = c.req.param('key');\n const body = await c.req.json().catch(() => ({}));\n const startIndex = typeof body.startIndex === 'number' ? body.startIndex : -1;\n const count = typeof body.count === 'number' ? body.count : 0;\n if (startIndex < 0 || count <= 0) {\n return c.json({ error: 'Invalid startIndex or count' }, 400);\n }\n const loaded = await service.sessionManagerInstance.loadMessages(key);\n if (!loaded || startIndex >= loaded.length) {\n return c.json({ error: 'Index out of range' }, 400);\n }\n const deleteCount = Math.min(count, loaded.length - startIndex);\n const next = loaded.slice(0, startIndex).concat(loaded.slice(startIndex + deleteCount));\n await service.sessionManagerInstance.saveMessages(key, next);\n return c.json({ ok: true, deleted: deleteCount });\n });\n\n // DELETE /api/sessions/:key - Delete session\n authenticated.delete('/api/sessions/:key', async (c) => {\n const key = c.req.param('key');\n const result = await service.deleteSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/archive - Archive session\n authenticated.post('/api/sessions/:key/archive', async (c) => {\n const key = c.req.param('key');\n const result = await service.archiveSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/unarchive - Unarchive session\n authenticated.post('/api/sessions/:key/unarchive', async (c) => {\n const key = c.req.param('key');\n const result = await service.unarchiveSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/pin - Pin session\n authenticated.post('/api/sessions/:key/pin', async (c) => {\n const key = c.req.param('key');\n const result = await service.pinSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/unpin - Unpin session\n authenticated.post('/api/sessions/:key/unpin', async (c) => {\n const key = c.req.param('key');\n const result = await service.unpinSession(key);\n return c.json(result);\n });\n\n // POST /api/sessions/:key/rename - Rename session\n authenticated.post('/api/sessions/:key/rename', async (c) => {\n const key = c.req.param('key');\n\n const body = await c.req.json();\n const { name } = body;\n const result = await service.renameSession(key, name);\n return c.json(result);\n });\n\n // ========== Subagent REST API (/api/subagents) ==========\n\n // GET /api/subagents - List subagent sessions\n authenticated.get('/api/subagents', async (c) => {\n const query = c.req.query();\n const result = await service.listSubagents({\n limit: query.limit ? parseInt(query.limit) : undefined,\n offset: query.offset ? parseInt(query.offset) : undefined,\n });\n return c.json(result);\n });\n\n // GET /api/subagents/:key - Get subagent session detail\n authenticated.get('/api/subagents/:key', async (c) => {\n const key = c.req.param('key');\n // Verify it's a subagent session\n if (!key.startsWith('subagent:')) {\n return c.json({ error: 'Not a subagent session' }, 400);\n }\n const session = await service.getSession(key);\n if (!session) {\n return c.json({ error: 'Subagent session not found' }, 404);\n }\n return c.json({ session });\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { ResolvedGatewayAuth } from '../auth.js';
2
+ export type SecurityAuditFinding = {
3
+ checkId: string;
4
+ severity: 'critical' | 'warn' | 'info';
5
+ title: string;
6
+ detail: string;
7
+ };
8
+ /**
9
+ * Audit the gateway configuration at startup and log security findings.
10
+ *
11
+ * This provides an early-warning system similar to OpenClaw's `security audit`
12
+ * command, adapted for xopc's configuration surface.
13
+ */
14
+ export declare function auditGatewayConfig(params: {
15
+ auth: ResolvedGatewayAuth;
16
+ host?: string;
17
+ corsOrigins?: string[];
18
+ }): SecurityAuditFinding[];
@@ -0,0 +1,68 @@
1
+ import { createLogger } from "../../utils/logger/index.js";
2
+ import { init_logger } from "../../utils/logger.js";
3
+ //#region src/gateway/security/audit.ts
4
+ init_logger();
5
+ const log = createLogger("SecurityAudit");
6
+ /**
7
+ * Audit the gateway configuration at startup and log security findings.
8
+ *
9
+ * This provides an early-warning system similar to OpenClaw's `security audit`
10
+ * command, adapted for xopc's configuration surface.
11
+ */
12
+ function auditGatewayConfig(params) {
13
+ const findings = [];
14
+ if (params.auth.mode === "none") if (!(!params.host || params.host === "127.0.0.1" || params.host === "localhost" || params.host === "::1")) findings.push({
15
+ checkId: "gateway.auth.none_on_network",
16
+ severity: "critical",
17
+ title: "Gateway has no authentication on a network-accessible address",
18
+ detail: `Auth mode is "none" but gateway binds to ${params.host}. Any host on the network can access the gateway without credentials. Set gateway.auth.mode to "token" and configure a token.`
19
+ });
20
+ else findings.push({
21
+ checkId: "gateway.auth.none_loopback",
22
+ severity: "warn",
23
+ title: "Gateway authentication is disabled",
24
+ detail: "Auth mode is \"none\". This is acceptable for local development but should not be used in production."
25
+ });
26
+ if (params.corsOrigins?.includes("*")) findings.push({
27
+ checkId: "gateway.cors.wildcard",
28
+ severity: "warn",
29
+ title: "CORS allows all origins",
30
+ detail: "corsOrigins includes \"*\". Any website can make authenticated API calls to the gateway if it can obtain the token."
31
+ });
32
+ if (params.corsOrigins && params.corsOrigins.length > 20) findings.push({
33
+ checkId: "gateway.cors.excessive_origins",
34
+ severity: "info",
35
+ title: "Large number of CORS origins configured",
36
+ detail: `${params.corsOrigins.length} CORS origins configured. Review whether all are necessary.`
37
+ });
38
+ if (params.auth.mode === "token" && params.auth.token) {
39
+ if (!process.env.XOPC_GATEWAY_TOKEN) findings.push({
40
+ checkId: "gateway.auth.auto_generated_token",
41
+ severity: "info",
42
+ title: "Gateway token was auto-generated",
43
+ detail: "No explicit XOPC_GATEWAY_TOKEN set. The token was auto-generated and will change on each restart. Set XOPC_GATEWAY_TOKEN for a stable token."
44
+ });
45
+ }
46
+ for (const finding of findings) {
47
+ const logData = {
48
+ checkId: finding.checkId,
49
+ detail: finding.detail
50
+ };
51
+ switch (finding.severity) {
52
+ case "critical":
53
+ log.error(logData, `Security audit: ${finding.title}`);
54
+ break;
55
+ case "warn":
56
+ log.warn(logData, `Security audit: ${finding.title}`);
57
+ break;
58
+ case "info":
59
+ log.info(logData, `Security audit: ${finding.title}`);
60
+ break;
61
+ }
62
+ }
63
+ return findings;
64
+ }
65
+ //#endregion
66
+ export { auditGatewayConfig };
67
+
68
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","names":[],"sources":["../../../../src/gateway/security/audit.ts"],"sourcesContent":["import type { ResolvedGatewayAuth } from '../auth.js';\nimport { createLogger } from '../../utils/logger.js';\n\nconst log = createLogger('SecurityAudit');\n\nexport type SecurityAuditFinding = {\n checkId: string;\n severity: 'critical' | 'warn' | 'info';\n title: string;\n detail: string;\n};\n\n/**\n * Audit the gateway configuration at startup and log security findings.\n *\n * This provides an early-warning system similar to OpenClaw's `security audit`\n * command, adapted for xopc's configuration surface.\n */\nexport function auditGatewayConfig(params: {\n auth: ResolvedGatewayAuth;\n host?: string;\n corsOrigins?: string[];\n}): SecurityAuditFinding[] {\n const findings: SecurityAuditFinding[] = [];\n\n // Check: no auth on non-loopback bind\n if (params.auth.mode === 'none') {\n const isLoopback = !params.host ||\n params.host === '127.0.0.1' ||\n params.host === 'localhost' ||\n params.host === '::1';\n\n if (!isLoopback) {\n findings.push({\n checkId: 'gateway.auth.none_on_network',\n severity: 'critical',\n title: 'Gateway has no authentication on a network-accessible address',\n detail: `Auth mode is \"none\" but gateway binds to ${params.host}. ` +\n 'Any host on the network can access the gateway without credentials. ' +\n 'Set gateway.auth.mode to \"token\" and configure a token.',\n });\n } else {\n findings.push({\n checkId: 'gateway.auth.none_loopback',\n severity: 'warn',\n title: 'Gateway authentication is disabled',\n detail: 'Auth mode is \"none\". This is acceptable for local development ' +\n 'but should not be used in production.',\n });\n }\n }\n\n // Check: wildcard CORS origins\n if (params.corsOrigins?.includes('*')) {\n findings.push({\n checkId: 'gateway.cors.wildcard',\n severity: 'warn',\n title: 'CORS allows all origins',\n detail: 'corsOrigins includes \"*\". Any website can make authenticated API calls ' +\n 'to the gateway if it can obtain the token.',\n });\n }\n\n // Check: too many CORS origins may indicate misconfiguration\n if (params.corsOrigins && params.corsOrigins.length > 20) {\n findings.push({\n checkId: 'gateway.cors.excessive_origins',\n severity: 'info',\n title: 'Large number of CORS origins configured',\n detail: `${params.corsOrigins.length} CORS origins configured. Review whether all are necessary.`,\n });\n }\n\n // Check: token mode without explicit token (auto-generated)\n if (params.auth.mode === 'token' && params.auth.token) {\n const envToken = process.env.XOPC_GATEWAY_TOKEN;\n if (!envToken) {\n findings.push({\n checkId: 'gateway.auth.auto_generated_token',\n severity: 'info',\n title: 'Gateway token was auto-generated',\n detail: 'No explicit XOPC_GATEWAY_TOKEN set. The token was auto-generated and will ' +\n 'change on each restart. Set XOPC_GATEWAY_TOKEN for a stable token.',\n });\n }\n }\n\n // Emit findings as log entries\n for (const finding of findings) {\n const logData = { checkId: finding.checkId, detail: finding.detail };\n switch (finding.severity) {\n case 'critical':\n log.error(logData, `Security audit: ${finding.title}`);\n break;\n case 'warn':\n log.warn(logData, `Security audit: ${finding.title}`);\n break;\n case 'info':\n log.info(logData, `Security audit: ${finding.title}`);\n break;\n }\n }\n\n return findings;\n}\n"],"mappings":";;;aACqD;AAErD,MAAM,MAAM,aAAa,gBAAgB;;;;;;;AAezC,SAAgB,mBAAmB,QAIR;CACzB,MAAM,WAAmC,EAAE;AAG3C,KAAI,OAAO,KAAK,SAAS,OAMvB,KAAI,EALe,CAAC,OAAO,QACzB,OAAO,SAAS,eAChB,OAAO,SAAS,eAChB,OAAO,SAAS,OAGhB,UAAS,KAAK;EACZ,SAAS;EACT,UAAU;EACV,OAAO;EACP,QAAQ,4CAA4C,OAAO,KAAK;EAGjE,CAAC;KAEF,UAAS,KAAK;EACZ,SAAS;EACT,UAAU;EACV,OAAO;EACP,QAAQ;EAET,CAAC;AAKN,KAAI,OAAO,aAAa,SAAS,IAAI,CACnC,UAAS,KAAK;EACZ,SAAS;EACT,UAAU;EACV,OAAO;EACP,QAAQ;EAET,CAAC;AAIJ,KAAI,OAAO,eAAe,OAAO,YAAY,SAAS,GACpD,UAAS,KAAK;EACZ,SAAS;EACT,UAAU;EACV,OAAO;EACP,QAAQ,GAAG,OAAO,YAAY,OAAO;EACtC,CAAC;AAIJ,KAAI,OAAO,KAAK,SAAS,WAAW,OAAO,KAAK;MAE1C,CADa,QAAQ,IAAI,mBAE3B,UAAS,KAAK;GACZ,SAAS;GACT,UAAU;GACV,OAAO;GACP,QAAQ;GAET,CAAC;;AAKN,MAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,UAAU;GAAE,SAAS,QAAQ;GAAS,QAAQ,QAAQ;GAAQ;AACpE,UAAQ,QAAQ,UAAhB;GACE,KAAK;AACH,QAAI,MAAM,SAAS,mBAAmB,QAAQ,QAAQ;AACtD;GACF,KAAK;AACH,QAAI,KAAK,SAAS,mBAAmB,QAAQ,QAAQ;AACrD;GACF,KAAK;AACH,QAAI,KAAK,SAAS,mBAAmB,QAAQ,QAAQ;AACrD;;;AAIN,QAAO"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Compute SHA-256 CSP hashes for inline `<script>` blocks in an HTML string.
3
+ * Only scripts without a `src` attribute are considered inline.
4
+ */
5
+ export declare function computeInlineScriptHashes(html: string): string[];
6
+ /**
7
+ * Build a Content-Security-Policy header string.
8
+ *
9
+ * For the gateway console, we use:
10
+ * - `script-src 'self'` + optional SHA-256 hashes for inline scripts (no unsafe-inline)
11
+ * - `style-src 'self' 'unsafe-inline'` (Tailwind + runtime style injection)
12
+ * - `frame-ancestors 'none'` (prevent clickjacking)
13
+ * - `base-uri 'none'` (prevent base tag hijacking)
14
+ * - `object-src 'none'` (prevent plugin execution)
15
+ */
16
+ export declare function buildGatewayConsoleCspHeader(options?: {
17
+ inlineScriptHashes?: string[];
18
+ connectSrc?: string;
19
+ }): string;
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region src/gateway/security/csp.ts
3
+ /**
4
+ * Compute SHA-256 CSP hashes for inline `<script>` blocks in an HTML string.
5
+ * Only scripts without a `src` attribute are considered inline.
6
+ */
7
+ function computeInlineScriptHashes(html) {
8
+ const hashes = [];
9
+ const scriptRegex = /<script(?:\s[^>]*)?>([^]*?)<\/script>/gi;
10
+ let match;
11
+ while ((match = scriptRegex.exec(html)) !== null) {
12
+ if (hasSrcAttribute(match[0].slice(0, match[0].indexOf(">") + 1))) continue;
13
+ const content = match[1];
14
+ if (!content) continue;
15
+ const hash = createHash("sha256").update(content, "utf8").digest("base64");
16
+ hashes.push(`sha256-${hash}`);
17
+ }
18
+ return hashes;
19
+ }
20
+ const ATTRIBUTE_NAME_REGEX = /\s([^\s=/>]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/g;
21
+ function hasSrcAttribute(openTag) {
22
+ return Array.from(openTag.matchAll(ATTRIBUTE_NAME_REGEX)).some((attrMatch) => attrMatch[1]?.toLowerCase() === "src");
23
+ }
24
+ /**
25
+ * Build a Content-Security-Policy header string.
26
+ *
27
+ * For the gateway console, we use:
28
+ * - `script-src 'self'` + optional SHA-256 hashes for inline scripts (no unsafe-inline)
29
+ * - `style-src 'self' 'unsafe-inline'` (Tailwind + runtime style injection)
30
+ * - `frame-ancestors 'none'` (prevent clickjacking)
31
+ * - `base-uri 'none'` (prevent base tag hijacking)
32
+ * - `object-src 'none'` (prevent plugin execution)
33
+ */
34
+ function buildGatewayConsoleCspHeader(options) {
35
+ const hashes = options?.inlineScriptHashes;
36
+ return [
37
+ "default-src 'self'",
38
+ "base-uri 'none'",
39
+ "object-src 'none'",
40
+ "frame-ancestors 'none'",
41
+ hashes?.length ? `script-src 'self' ${hashes.map((hash) => `'${hash}'`).join(" ")}` : "script-src 'self'",
42
+ "style-src 'self' 'unsafe-inline'",
43
+ "img-src 'self' data: blob: https:",
44
+ "font-src 'self'",
45
+ `connect-src ${options?.connectSrc ?? "'self' ws: wss:"}`,
46
+ "worker-src 'self'"
47
+ ].join("; ");
48
+ }
49
+ //#endregion
50
+ export { buildGatewayConsoleCspHeader, computeInlineScriptHashes };
51
+
52
+ //# sourceMappingURL=csp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csp.js","names":[],"sources":["../../../../src/gateway/security/csp.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\n/**\n * Compute SHA-256 CSP hashes for inline `<script>` blocks in an HTML string.\n * Only scripts without a `src` attribute are considered inline.\n */\nexport function computeInlineScriptHashes(html: string): string[] {\n const hashes: string[] = [];\n const scriptRegex = /<script(?:\\s[^>]*)?>([^]*?)<\\/script>/gi;\n let match: RegExpExecArray | null;\n while ((match = scriptRegex.exec(html)) !== null) {\n const openTag = match[0].slice(0, match[0].indexOf('>') + 1);\n if (hasSrcAttribute(openTag)) {\n continue;\n }\n const content = match[1];\n if (!content) {\n continue;\n }\n const hash = createHash('sha256').update(content, 'utf8').digest('base64');\n hashes.push(`sha256-${hash}`);\n }\n return hashes;\n}\n\nconst ATTRIBUTE_NAME_REGEX = /\\s([^\\s=/>]+)(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+))?/g;\n\nfunction hasSrcAttribute(openTag: string): boolean {\n return Array.from(openTag.matchAll(ATTRIBUTE_NAME_REGEX)).some(\n (attrMatch) => attrMatch[1]?.toLowerCase() === 'src',\n );\n}\n\n/**\n * Build a Content-Security-Policy header string.\n *\n * For the gateway console, we use:\n * - `script-src 'self'` + optional SHA-256 hashes for inline scripts (no unsafe-inline)\n * - `style-src 'self' 'unsafe-inline'` (Tailwind + runtime style injection)\n * - `frame-ancestors 'none'` (prevent clickjacking)\n * - `base-uri 'none'` (prevent base tag hijacking)\n * - `object-src 'none'` (prevent plugin execution)\n */\nexport function buildGatewayConsoleCspHeader(options?: {\n inlineScriptHashes?: string[];\n connectSrc?: string;\n}): string {\n const hashes = options?.inlineScriptHashes;\n const scriptSrc = hashes?.length\n ? `script-src 'self' ${hashes.map((hash) => `'${hash}'`).join(' ')}`\n : \"script-src 'self'\";\n const connectSrc = options?.connectSrc ?? \"'self' ws: wss:\";\n\n return [\n \"default-src 'self'\",\n \"base-uri 'none'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n scriptSrc,\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: blob: https:\",\n \"font-src 'self'\",\n `connect-src ${connectSrc}`,\n \"worker-src 'self'\",\n ].join('; ');\n}\n"],"mappings":";;;;;;AAMA,SAAgB,0BAA0B,MAAwB;CAChE,MAAM,SAAmB,EAAE;CAC3B,MAAM,cAAc;CACpB,IAAI;AACJ,SAAQ,QAAQ,YAAY,KAAK,KAAK,MAAM,MAAM;AAEhD,MAAI,gBADY,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,IAAI,GAAG,EAC/B,CAAC,CAC1B;EAEF,MAAM,UAAU,MAAM;AACtB,MAAI,CAAC,QACH;EAEF,MAAM,OAAO,WAAW,SAAS,CAAC,OAAO,SAAS,OAAO,CAAC,OAAO,SAAS;AAC1E,SAAO,KAAK,UAAU,OAAO;;AAE/B,QAAO;;AAGT,MAAM,uBAAuB;AAE7B,SAAS,gBAAgB,SAA0B;AACjD,QAAO,MAAM,KAAK,QAAQ,SAAS,qBAAqB,CAAC,CAAC,MACvD,cAAc,UAAU,IAAI,aAAa,KAAK,MAChD;;;;;;;;;;;;AAaH,SAAgB,6BAA6B,SAGlC;CACT,MAAM,SAAS,SAAS;AAMxB,QAAO;EACL;EACA;EACA;EACA;EATgB,QAAQ,SACtB,qBAAqB,OAAO,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,KAAK,IAAI,KAChE;EASF;EACA;EACA;EACA,eAXiB,SAAS,cAAc;EAYxC;EACD,CAAC,KAAK,KAAK"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tools denied via Gateway HTTP tool invoke endpoints by default.
3
+ *
4
+ * These are high-risk because they enable command execution, file mutation,
5
+ * session orchestration, or control-plane actions that don't belong on a
6
+ * non-interactive HTTP surface.
7
+ */
8
+ export declare const DEFAULT_GATEWAY_HTTP_TOOL_DENY: readonly string[];
9
+ /**
10
+ * Check whether a tool name is in the default HTTP deny list.
11
+ */
12
+ export declare function isDangerousHttpTool(toolName: string): boolean;
13
+ /**
14
+ * Filter a list of tool names, removing those on the HTTP deny list.
15
+ * Returns only the safe tools.
16
+ */
17
+ export declare function filterDangerousHttpTools(toolNames: string[]): {
18
+ allowed: string[];
19
+ denied: string[];
20
+ };
@@ -0,0 +1,46 @@
1
+ //#region src/gateway/security/dangerous-tools.ts
2
+ /**
3
+ * Tools denied via Gateway HTTP tool invoke endpoints by default.
4
+ *
5
+ * These are high-risk because they enable command execution, file mutation,
6
+ * session orchestration, or control-plane actions that don't belong on a
7
+ * non-interactive HTTP surface.
8
+ */
9
+ const DEFAULT_GATEWAY_HTTP_TOOL_DENY = [
10
+ "exec",
11
+ "spawn",
12
+ "shell",
13
+ "fs_write",
14
+ "fs_delete",
15
+ "fs_move",
16
+ "apply_patch",
17
+ "sessions_spawn",
18
+ "sessions_send",
19
+ "cron",
20
+ "gateway"
21
+ ];
22
+ const DANGEROUS_TOOL_SET = new Set(DEFAULT_GATEWAY_HTTP_TOOL_DENY);
23
+ /**
24
+ * Check whether a tool name is in the default HTTP deny list.
25
+ */
26
+ function isDangerousHttpTool(toolName) {
27
+ return DANGEROUS_TOOL_SET.has(toolName.toLowerCase());
28
+ }
29
+ /**
30
+ * Filter a list of tool names, removing those on the HTTP deny list.
31
+ * Returns only the safe tools.
32
+ */
33
+ function filterDangerousHttpTools(toolNames) {
34
+ const allowed = [];
35
+ const denied = [];
36
+ for (const name of toolNames) if (isDangerousHttpTool(name)) denied.push(name);
37
+ else allowed.push(name);
38
+ return {
39
+ allowed,
40
+ denied
41
+ };
42
+ }
43
+ //#endregion
44
+ export { DEFAULT_GATEWAY_HTTP_TOOL_DENY, filterDangerousHttpTools, isDangerousHttpTool };
45
+
46
+ //# sourceMappingURL=dangerous-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dangerous-tools.js","names":[],"sources":["../../../../src/gateway/security/dangerous-tools.ts"],"sourcesContent":["/**\n * Tools denied via Gateway HTTP tool invoke endpoints by default.\n *\n * These are high-risk because they enable command execution, file mutation,\n * session orchestration, or control-plane actions that don't belong on a\n * non-interactive HTTP surface.\n */\nexport const DEFAULT_GATEWAY_HTTP_TOOL_DENY: readonly string[] = [\n // Direct command execution — immediate RCE surface\n 'exec',\n // Arbitrary child process creation\n 'spawn',\n // Shell command execution\n 'shell',\n // Arbitrary file mutation on the host\n 'fs_write',\n // Arbitrary file deletion on the host\n 'fs_delete',\n // Arbitrary file move/rename on the host\n 'fs_move',\n // Patch application can rewrite arbitrary files\n 'apply_patch',\n // Session orchestration — spawning agents remotely is RCE\n 'sessions_spawn',\n // Cross-session injection — message injection across sessions\n 'sessions_send',\n // Persistent automation control plane — can create/update/remove scheduled runs\n 'cron',\n // Gateway control plane — prevents gateway reconfiguration via HTTP\n 'gateway',\n] as const;\n\nconst DANGEROUS_TOOL_SET: ReadonlySet<string> = new Set(DEFAULT_GATEWAY_HTTP_TOOL_DENY);\n\n/**\n * Check whether a tool name is in the default HTTP deny list.\n */\nexport function isDangerousHttpTool(toolName: string): boolean {\n return DANGEROUS_TOOL_SET.has(toolName.toLowerCase());\n}\n\n/**\n * Filter a list of tool names, removing those on the HTTP deny list.\n * Returns only the safe tools.\n */\nexport function filterDangerousHttpTools(toolNames: string[]): {\n allowed: string[];\n denied: string[];\n} {\n const allowed: string[] = [];\n const denied: string[] = [];\n for (const name of toolNames) {\n if (isDangerousHttpTool(name)) {\n denied.push(name);\n } else {\n allowed.push(name);\n }\n }\n return { allowed, denied };\n}\n"],"mappings":";;;;;;;;AAOA,MAAa,iCAAoD;CAE/D;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CAEA;CACD;AAED,MAAM,qBAA0C,IAAI,IAAI,+BAA+B;;;;AAKvF,SAAgB,oBAAoB,UAA2B;AAC7D,QAAO,mBAAmB,IAAI,SAAS,aAAa,CAAC;;;;;;AAOvD,SAAgB,yBAAyB,WAGvC;CACA,MAAM,UAAoB,EAAE;CAC5B,MAAM,SAAmB,EAAE;AAC3B,MAAK,MAAM,QAAQ,UACjB,KAAI,oBAAoB,KAAK,CAC3B,QAAO,KAAK,KAAK;KAEjB,SAAQ,KAAK,KAAK;AAGtB,QAAO;EAAE;EAAS;EAAQ"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Unauthorized flood guard for WebSocket connections.
3
+ *
4
+ * Tracks repeated unauthorized message attempts on a single WS connection
5
+ * and decides when to forcibly close it. Prevents a rogue client from
6
+ * holding a socket open and spamming unauthorized requests.
7
+ */
8
+ export type FloodGuardOptions = {
9
+ /** Close the connection after this many unauthorized messages. @default 10 */
10
+ closeAfter?: number;
11
+ /** Log every N unauthorized messages to prevent log spam. @default 100 */
12
+ logEvery?: number;
13
+ };
14
+ export type FloodGuardDecision = {
15
+ shouldClose: boolean;
16
+ shouldLog: boolean;
17
+ count: number;
18
+ suppressedSinceLastLog: number;
19
+ };
20
+ export declare class UnauthorizedFloodGuard {
21
+ private readonly closeAfter;
22
+ private readonly logEvery;
23
+ private count;
24
+ private suppressedSinceLastLog;
25
+ constructor(options?: FloodGuardOptions);
26
+ registerUnauthorized(): FloodGuardDecision;
27
+ reset(): void;
28
+ }
@@ -0,0 +1,42 @@
1
+ //#region src/gateway/security/flood-guard.ts
2
+ const DEFAULT_CLOSE_AFTER = 10;
3
+ const DEFAULT_LOG_EVERY = 100;
4
+ var UnauthorizedFloodGuard = class {
5
+ closeAfter;
6
+ logEvery;
7
+ count = 0;
8
+ suppressedSinceLastLog = 0;
9
+ constructor(options) {
10
+ this.closeAfter = Math.max(1, Math.floor(options?.closeAfter ?? DEFAULT_CLOSE_AFTER));
11
+ this.logEvery = Math.max(1, Math.floor(options?.logEvery ?? DEFAULT_LOG_EVERY));
12
+ }
13
+ registerUnauthorized() {
14
+ this.count += 1;
15
+ const shouldClose = this.count > this.closeAfter;
16
+ if (!(this.count === 1 || this.count % this.logEvery === 0 || shouldClose)) {
17
+ this.suppressedSinceLastLog += 1;
18
+ return {
19
+ shouldClose,
20
+ shouldLog: false,
21
+ count: this.count,
22
+ suppressedSinceLastLog: 0
23
+ };
24
+ }
25
+ const suppressedSinceLastLog = this.suppressedSinceLastLog;
26
+ this.suppressedSinceLastLog = 0;
27
+ return {
28
+ shouldClose,
29
+ shouldLog: true,
30
+ count: this.count,
31
+ suppressedSinceLastLog
32
+ };
33
+ }
34
+ reset() {
35
+ this.count = 0;
36
+ this.suppressedSinceLastLog = 0;
37
+ }
38
+ };
39
+ //#endregion
40
+ export { UnauthorizedFloodGuard };
41
+
42
+ //# sourceMappingURL=flood-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flood-guard.js","names":[],"sources":["../../../../src/gateway/security/flood-guard.ts"],"sourcesContent":["/**\n * Unauthorized flood guard for WebSocket connections.\n *\n * Tracks repeated unauthorized message attempts on a single WS connection\n * and decides when to forcibly close it. Prevents a rogue client from\n * holding a socket open and spamming unauthorized requests.\n */\n\nexport type FloodGuardOptions = {\n /** Close the connection after this many unauthorized messages. @default 10 */\n closeAfter?: number;\n /** Log every N unauthorized messages to prevent log spam. @default 100 */\n logEvery?: number;\n};\n\nexport type FloodGuardDecision = {\n shouldClose: boolean;\n shouldLog: boolean;\n count: number;\n suppressedSinceLastLog: number;\n};\n\nconst DEFAULT_CLOSE_AFTER = 10;\nconst DEFAULT_LOG_EVERY = 100;\n\nexport class UnauthorizedFloodGuard {\n private readonly closeAfter: number;\n private readonly logEvery: number;\n private count = 0;\n private suppressedSinceLastLog = 0;\n\n constructor(options?: FloodGuardOptions) {\n this.closeAfter = Math.max(1, Math.floor(options?.closeAfter ?? DEFAULT_CLOSE_AFTER));\n this.logEvery = Math.max(1, Math.floor(options?.logEvery ?? DEFAULT_LOG_EVERY));\n }\n\n registerUnauthorized(): FloodGuardDecision {\n this.count += 1;\n const shouldClose = this.count > this.closeAfter;\n const shouldLog = this.count === 1 || this.count % this.logEvery === 0 || shouldClose;\n\n if (!shouldLog) {\n this.suppressedSinceLastLog += 1;\n return {\n shouldClose,\n shouldLog: false,\n count: this.count,\n suppressedSinceLastLog: 0,\n };\n }\n\n const suppressedSinceLastLog = this.suppressedSinceLastLog;\n this.suppressedSinceLastLog = 0;\n return {\n shouldClose,\n shouldLog: true,\n count: this.count,\n suppressedSinceLastLog,\n };\n }\n\n reset(): void {\n this.count = 0;\n this.suppressedSinceLastLog = 0;\n }\n}\n"],"mappings":";AAsBA,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;AAE1B,IAAa,yBAAb,MAAoC;CAClC;CACA;CACA,QAAgB;CAChB,yBAAiC;CAEjC,YAAY,SAA6B;AACvC,OAAK,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,cAAc,oBAAoB,CAAC;AACrF,OAAK,WAAW,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,YAAY,kBAAkB,CAAC;;CAGjF,uBAA2C;AACzC,OAAK,SAAS;EACd,MAAM,cAAc,KAAK,QAAQ,KAAK;AAGtC,MAAI,EAFc,KAAK,UAAU,KAAK,KAAK,QAAQ,KAAK,aAAa,KAAK,cAE1D;AACd,QAAK,0BAA0B;AAC/B,UAAO;IACL;IACA,WAAW;IACX,OAAO,KAAK;IACZ,wBAAwB;IACzB;;EAGH,MAAM,yBAAyB,KAAK;AACpC,OAAK,yBAAyB;AAC9B,SAAO;GACL;GACA,WAAW;GACX,OAAO,KAAK;GACZ;GACD;;CAGH,QAAc;AACZ,OAAK,QAAQ;AACb,OAAK,yBAAyB"}
@@ -0,0 +1,9 @@
1
+ export { safeEqualSecret } from './secret-equal.js';
2
+ export { assertGatewayAuthNotKnownWeak, KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, } from './known-weak-secrets.js';
3
+ export { checkBrowserOrigin } from './origin-check.js';
4
+ export { computeInlineScriptHashes, buildGatewayConsoleCspHeader } from './csp.js';
5
+ export { ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE, KNOWN_OPERATOR_SCOPES, DEFAULT_OPERATOR_SCOPES, isOperatorScope, authorizeRouteScope, authorizeScope, parseScopesHeader, type OperatorScope, } from './operator-scopes.js';
6
+ export { DEFAULT_GATEWAY_HTTP_TOOL_DENY, isDangerousHttpTool, filterDangerousHttpTools, } from './dangerous-tools.js';
7
+ export { createPreauthConnectionBudget, getMaxPreauthConnectionsPerIp, type PreauthConnectionBudget, } from './preauth-connection-budget.js';
8
+ export { UnauthorizedFloodGuard, type FloodGuardOptions, type FloodGuardDecision, } from './flood-guard.js';
9
+ export { auditGatewayConfig, type SecurityAuditFinding, } from './audit.js';
@@ -0,0 +1,10 @@
1
+ import { safeEqualSecret } from "./secret-equal.js";
2
+ import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, assertGatewayAuthNotKnownWeak } from "./known-weak-secrets.js";
3
+ import { auditGatewayConfig } from "./audit.js";
4
+ import { buildGatewayConsoleCspHeader, computeInlineScriptHashes } from "./csp.js";
5
+ import { checkBrowserOrigin } from "./origin-check.js";
6
+ import { ADMIN_SCOPE, DEFAULT_OPERATOR_SCOPES, KNOWN_OPERATOR_SCOPES, READ_SCOPE, WRITE_SCOPE, authorizeRouteScope, authorizeScope, isOperatorScope, parseScopesHeader } from "./operator-scopes.js";
7
+ import { DEFAULT_GATEWAY_HTTP_TOOL_DENY, filterDangerousHttpTools, isDangerousHttpTool } from "./dangerous-tools.js";
8
+ import { UnauthorizedFloodGuard } from "./flood-guard.js";
9
+ import { createPreauthConnectionBudget, getMaxPreauthConnectionsPerIp } from "./preauth-connection-budget.js";
10
+ export { ADMIN_SCOPE, DEFAULT_GATEWAY_HTTP_TOOL_DENY, DEFAULT_OPERATOR_SCOPES, KNOWN_OPERATOR_SCOPES, KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, READ_SCOPE, UnauthorizedFloodGuard, WRITE_SCOPE, assertGatewayAuthNotKnownWeak, auditGatewayConfig, authorizeRouteScope, authorizeScope, buildGatewayConsoleCspHeader, checkBrowserOrigin, computeInlineScriptHashes, createPreauthConnectionBudget, filterDangerousHttpTools, getMaxPreauthConnectionsPerIp, isDangerousHttpTool, isOperatorScope, parseScopesHeader, safeEqualSecret };
@@ -0,0 +1,10 @@
1
+ import type { ResolvedGatewayAuth } from '../auth.js';
2
+ /**
3
+ * Placeholder credentials that have shipped in `.env.example` or been used as
4
+ * copy-paste examples in onboarding docs. If any of these becomes the resolved
5
+ * gateway credential, reject it at startup. The operator almost certainly
6
+ * copied an example file verbatim without replacing the sentinel, which would
7
+ * otherwise leave the gateway protected by a publicly-known credential.
8
+ */
9
+ export declare const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS: readonly ["change-me-to-a-long-random-token", "change-me-now", "your-secret-token-here", "test-token", "my-token", "token", "secret", "password", "123456", "abc123"];
10
+ export declare function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void;
@@ -0,0 +1,36 @@
1
+ //#region src/gateway/security/known-weak-secrets.ts
2
+ /**
3
+ * Placeholder credentials that have shipped in `.env.example` or been used as
4
+ * copy-paste examples in onboarding docs. If any of these becomes the resolved
5
+ * gateway credential, reject it at startup. The operator almost certainly
6
+ * copied an example file verbatim without replacing the sentinel, which would
7
+ * otherwise leave the gateway protected by a publicly-known credential.
8
+ */
9
+ const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS = [
10
+ "change-me-to-a-long-random-token",
11
+ "change-me-now",
12
+ "your-secret-token-here",
13
+ "test-token",
14
+ "my-token",
15
+ "token",
16
+ "secret",
17
+ "password",
18
+ "123456",
19
+ "abc123"
20
+ ];
21
+ const KNOWN_WEAK_GATEWAY_TOKENS = new Set(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS);
22
+ /**
23
+ * Minimum acceptable token length. Short tokens are vulnerable to brute-force
24
+ * even with rate limiting.
25
+ */
26
+ const MIN_TOKEN_LENGTH = 16;
27
+ function assertGatewayAuthNotKnownWeak(auth) {
28
+ if (auth.mode !== "token" || !auth.token) return;
29
+ const token = auth.token.trim();
30
+ if (KNOWN_WEAK_GATEWAY_TOKENS.has(token)) throw new Error("Invalid config: gateway auth token is set to a published example placeholder from docs or .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) and set XOPC_GATEWAY_TOKEN or gateway.auth.token before starting the gateway.");
31
+ if (token.length < MIN_TOKEN_LENGTH) throw new Error(`Invalid config: gateway auth token is too short (${token.length} chars, minimum ${MIN_TOKEN_LENGTH}). Use a strong random token (e.g. \`openssl rand -hex 32\`).`);
32
+ }
33
+ //#endregion
34
+ export { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, assertGatewayAuthNotKnownWeak };
35
+
36
+ //# sourceMappingURL=known-weak-secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-weak-secrets.js","names":[],"sources":["../../../../src/gateway/security/known-weak-secrets.ts"],"sourcesContent":["import type { ResolvedGatewayAuth } from '../auth.js';\n\n/**\n * Placeholder credentials that have shipped in `.env.example` or been used as\n * copy-paste examples in onboarding docs. If any of these becomes the resolved\n * gateway credential, reject it at startup. The operator almost certainly\n * copied an example file verbatim without replacing the sentinel, which would\n * otherwise leave the gateway protected by a publicly-known credential.\n */\n\nexport const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS = [\n 'change-me-to-a-long-random-token',\n 'change-me-now',\n 'your-secret-token-here',\n 'test-token',\n 'my-token',\n 'token',\n 'secret',\n 'password',\n '123456',\n 'abc123',\n] as const;\n\nconst KNOWN_WEAK_GATEWAY_TOKENS: ReadonlySet<string> = new Set(\n KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS,\n);\n\n/**\n * Minimum acceptable token length. Short tokens are vulnerable to brute-force\n * even with rate limiting.\n */\nconst MIN_TOKEN_LENGTH = 16;\n\nexport function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void {\n if (auth.mode !== 'token' || !auth.token) {\n return;\n }\n\n const token = auth.token.trim();\n\n if (KNOWN_WEAK_GATEWAY_TOKENS.has(token)) {\n throw new Error(\n 'Invalid config: gateway auth token is set to a published example placeholder ' +\n 'from docs or .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) ' +\n 'and set XOPC_GATEWAY_TOKEN or gateway.auth.token before starting the gateway.',\n );\n }\n\n if (token.length < MIN_TOKEN_LENGTH) {\n throw new Error(\n `Invalid config: gateway auth token is too short (${token.length} chars, minimum ${MIN_TOKEN_LENGTH}). ` +\n 'Use a strong random token (e.g. `openssl rand -hex 32`).',\n );\n }\n}\n"],"mappings":";;;;;;;;AAUA,MAAa,wCAAwC;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,4BAAiD,IAAI,IACzD,sCACD;;;;;AAMD,MAAM,mBAAmB;AAEzB,SAAgB,8BAA8B,MAAiC;AAC7E,KAAI,KAAK,SAAS,WAAW,CAAC,KAAK,MACjC;CAGF,MAAM,QAAQ,KAAK,MAAM,MAAM;AAE/B,KAAI,0BAA0B,IAAI,MAAM,CACtC,OAAM,IAAI,MACR,6OAGD;AAGH,KAAI,MAAM,SAAS,iBACjB,OAAM,IAAI,MACR,oDAAoD,MAAM,OAAO,kBAAkB,iBAAiB,+DAErG"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Operator scope system for fine-grained gateway API authorization.
3
+ *
4
+ * Each gateway API method maps to one or more required scopes.
5
+ * Clients declare their scopes at connection time; the gateway enforces
6
+ * that the declared scopes cover the method being invoked.
7
+ */
8
+ export declare const ADMIN_SCOPE: "operator.admin";
9
+ export declare const READ_SCOPE: "operator.read";
10
+ export declare const WRITE_SCOPE: "operator.write";
11
+ export type OperatorScope = typeof ADMIN_SCOPE | typeof READ_SCOPE | typeof WRITE_SCOPE;
12
+ export declare const KNOWN_OPERATOR_SCOPES: ReadonlySet<OperatorScope>;
13
+ export declare function isOperatorScope(value: unknown): value is OperatorScope;
14
+ /** Default scopes granted to authenticated CLI / direct connections. */
15
+ export declare const DEFAULT_OPERATOR_SCOPES: OperatorScope[];
16
+ /**
17
+ * Check whether a set of granted scopes satisfies the required scope for a route.
18
+ */
19
+ export declare function authorizeRouteScope(routePath: string, grantedScopes: readonly OperatorScope[]): {
20
+ allowed: true;
21
+ } | {
22
+ allowed: false;
23
+ requiredScope: OperatorScope;
24
+ };
25
+ /**
26
+ * Check whether any of the granted scopes satisfies the required scope.
27
+ */
28
+ export declare function authorizeScope(requiredScope: OperatorScope, grantedScopes: readonly OperatorScope[]): {
29
+ allowed: true;
30
+ } | {
31
+ allowed: false;
32
+ requiredScope: OperatorScope;
33
+ };
34
+ /**
35
+ * Parse scopes from a comma-separated header value.
36
+ */
37
+ export declare function parseScopesHeader(headerValue: string | undefined): OperatorScope[];