@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
@@ -0,0 +1,137 @@
1
+ //#region src/gateway/security/operator-scopes.ts
2
+ /**
3
+ * Operator scope system for fine-grained gateway API authorization.
4
+ *
5
+ * Each gateway API method maps to one or more required scopes.
6
+ * Clients declare their scopes at connection time; the gateway enforces
7
+ * that the declared scopes cover the method being invoked.
8
+ */
9
+ const ADMIN_SCOPE = "operator.admin";
10
+ const READ_SCOPE = "operator.read";
11
+ const WRITE_SCOPE = "operator.write";
12
+ const KNOWN_OPERATOR_SCOPES = new Set([
13
+ ADMIN_SCOPE,
14
+ READ_SCOPE,
15
+ WRITE_SCOPE
16
+ ]);
17
+ function isOperatorScope(value) {
18
+ return typeof value === "string" && KNOWN_OPERATOR_SCOPES.has(value);
19
+ }
20
+ /** Default scopes granted to authenticated CLI / direct connections. */
21
+ const DEFAULT_OPERATOR_SCOPES = [
22
+ ADMIN_SCOPE,
23
+ READ_SCOPE,
24
+ WRITE_SCOPE
25
+ ];
26
+ /**
27
+ * Route-level scope requirements.
28
+ * Maps HTTP route path prefixes to required minimum scope.
29
+ */
30
+ const ROUTE_SCOPE_REQUIREMENTS = [
31
+ {
32
+ prefix: "/api/config",
33
+ scope: ADMIN_SCOPE
34
+ },
35
+ {
36
+ prefix: "/api/update",
37
+ scope: ADMIN_SCOPE
38
+ },
39
+ {
40
+ prefix: "/api/extensions/registry",
41
+ scope: ADMIN_SCOPE
42
+ },
43
+ {
44
+ prefix: "/api/agent",
45
+ scope: WRITE_SCOPE
46
+ },
47
+ {
48
+ prefix: "/api/sessions",
49
+ scope: WRITE_SCOPE
50
+ },
51
+ {
52
+ prefix: "/api/cron",
53
+ scope: WRITE_SCOPE
54
+ },
55
+ {
56
+ prefix: "/api/channels",
57
+ scope: WRITE_SCOPE
58
+ },
59
+ {
60
+ prefix: "/api/agents",
61
+ scope: WRITE_SCOPE
62
+ },
63
+ {
64
+ prefix: "/api/workspace",
65
+ scope: WRITE_SCOPE
66
+ },
67
+ {
68
+ prefix: "/api/skills",
69
+ scope: WRITE_SCOPE
70
+ },
71
+ {
72
+ prefix: "/api/commands",
73
+ scope: WRITE_SCOPE
74
+ },
75
+ {
76
+ prefix: "/api/host-fs",
77
+ scope: WRITE_SCOPE
78
+ },
79
+ {
80
+ prefix: "/api/status",
81
+ scope: READ_SCOPE
82
+ },
83
+ {
84
+ prefix: "/api/models",
85
+ scope: READ_SCOPE
86
+ },
87
+ {
88
+ prefix: "/api/logs",
89
+ scope: READ_SCOPE
90
+ },
91
+ {
92
+ prefix: "/api/doctor",
93
+ scope: READ_SCOPE
94
+ },
95
+ {
96
+ prefix: "/api/events",
97
+ scope: READ_SCOPE
98
+ }
99
+ ];
100
+ /** Scope hierarchy: admin > write > read. */
101
+ const SCOPE_HIERARCHY = {
102
+ [ADMIN_SCOPE]: 3,
103
+ [WRITE_SCOPE]: 2,
104
+ [READ_SCOPE]: 1
105
+ };
106
+ function scopeSatisfies(granted, required) {
107
+ return SCOPE_HIERARCHY[granted] >= SCOPE_HIERARCHY[required];
108
+ }
109
+ /**
110
+ * Check whether a set of granted scopes satisfies the required scope for a route.
111
+ */
112
+ function authorizeRouteScope(routePath, grantedScopes) {
113
+ const routeRequirement = ROUTE_SCOPE_REQUIREMENTS.find((entry) => routePath.startsWith(entry.prefix));
114
+ if (!routeRequirement) return authorizeScope(READ_SCOPE, grantedScopes);
115
+ return authorizeScope(routeRequirement.scope, grantedScopes);
116
+ }
117
+ /**
118
+ * Check whether any of the granted scopes satisfies the required scope.
119
+ */
120
+ function authorizeScope(requiredScope, grantedScopes) {
121
+ if (grantedScopes.some((granted) => scopeSatisfies(granted, requiredScope))) return { allowed: true };
122
+ return {
123
+ allowed: false,
124
+ requiredScope
125
+ };
126
+ }
127
+ /**
128
+ * Parse scopes from a comma-separated header value.
129
+ */
130
+ function parseScopesHeader(headerValue) {
131
+ if (!headerValue?.trim()) return [...DEFAULT_OPERATOR_SCOPES];
132
+ return headerValue.split(",").map((scope) => scope.trim()).filter(isOperatorScope);
133
+ }
134
+ //#endregion
135
+ export { ADMIN_SCOPE, DEFAULT_OPERATOR_SCOPES, KNOWN_OPERATOR_SCOPES, READ_SCOPE, WRITE_SCOPE, authorizeRouteScope, authorizeScope, isOperatorScope, parseScopesHeader };
136
+
137
+ //# sourceMappingURL=operator-scopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator-scopes.js","names":[],"sources":["../../../../src/gateway/security/operator-scopes.ts"],"sourcesContent":["/**\n * Operator scope system for fine-grained gateway API authorization.\n *\n * Each gateway API method maps to one or more required scopes.\n * Clients declare their scopes at connection time; the gateway enforces\n * that the declared scopes cover the method being invoked.\n */\n\nexport const ADMIN_SCOPE = 'operator.admin' as const;\nexport const READ_SCOPE = 'operator.read' as const;\nexport const WRITE_SCOPE = 'operator.write' as const;\n\nexport type OperatorScope =\n | typeof ADMIN_SCOPE\n | typeof READ_SCOPE\n | typeof WRITE_SCOPE;\n\nconst KNOWN_OPERATOR_SCOPE_VALUES: readonly OperatorScope[] = [\n ADMIN_SCOPE,\n READ_SCOPE,\n WRITE_SCOPE,\n];\n\nexport const KNOWN_OPERATOR_SCOPES: ReadonlySet<OperatorScope> = new Set(\n KNOWN_OPERATOR_SCOPE_VALUES,\n);\n\nexport function isOperatorScope(value: unknown): value is OperatorScope {\n return typeof value === 'string' && KNOWN_OPERATOR_SCOPES.has(value as OperatorScope);\n}\n\n/** Default scopes granted to authenticated CLI / direct connections. */\nexport const DEFAULT_OPERATOR_SCOPES: OperatorScope[] = [\n ADMIN_SCOPE,\n READ_SCOPE,\n WRITE_SCOPE,\n];\n\n/**\n * Route-level scope requirements.\n * Maps HTTP route path prefixes to required minimum scope.\n */\nconst ROUTE_SCOPE_REQUIREMENTS: ReadonlyArray<{ prefix: string; scope: OperatorScope }> = [\n // Admin routes\n { prefix: '/api/config', scope: ADMIN_SCOPE },\n { prefix: '/api/update', scope: ADMIN_SCOPE },\n { prefix: '/api/extensions/registry', scope: ADMIN_SCOPE },\n\n // Write routes\n { prefix: '/api/agent', scope: WRITE_SCOPE },\n { prefix: '/api/sessions', scope: WRITE_SCOPE },\n { prefix: '/api/cron', scope: WRITE_SCOPE },\n { prefix: '/api/channels', scope: WRITE_SCOPE },\n { prefix: '/api/agents', scope: WRITE_SCOPE },\n { prefix: '/api/workspace', scope: WRITE_SCOPE },\n { prefix: '/api/skills', scope: WRITE_SCOPE },\n { prefix: '/api/commands', scope: WRITE_SCOPE },\n { prefix: '/api/host-fs', scope: WRITE_SCOPE },\n\n // Read routes\n { prefix: '/api/status', scope: READ_SCOPE },\n { prefix: '/api/models', scope: READ_SCOPE },\n { prefix: '/api/logs', scope: READ_SCOPE },\n { prefix: '/api/doctor', scope: READ_SCOPE },\n { prefix: '/api/events', scope: READ_SCOPE },\n];\n\n/** Scope hierarchy: admin > write > read. */\nconst SCOPE_HIERARCHY: Record<OperatorScope, number> = {\n [ADMIN_SCOPE]: 3,\n [WRITE_SCOPE]: 2,\n [READ_SCOPE]: 1,\n};\n\nfunction scopeSatisfies(granted: OperatorScope, required: OperatorScope): boolean {\n return SCOPE_HIERARCHY[granted] >= SCOPE_HIERARCHY[required];\n}\n\n/**\n * Check whether a set of granted scopes satisfies the required scope for a route.\n */\nexport function authorizeRouteScope(\n routePath: string,\n grantedScopes: readonly OperatorScope[],\n): { allowed: true } | { allowed: false; requiredScope: OperatorScope } {\n const routeRequirement = ROUTE_SCOPE_REQUIREMENTS.find(\n (entry) => routePath.startsWith(entry.prefix),\n );\n\n if (!routeRequirement) {\n // Routes without explicit scope requirements default to read\n return authorizeScope(READ_SCOPE, grantedScopes);\n }\n\n return authorizeScope(routeRequirement.scope, grantedScopes);\n}\n\n/**\n * Check whether any of the granted scopes satisfies the required scope.\n */\nexport function authorizeScope(\n requiredScope: OperatorScope,\n grantedScopes: readonly OperatorScope[],\n): { allowed: true } | { allowed: false; requiredScope: OperatorScope } {\n const hasSufficientScope = grantedScopes.some(\n (granted) => scopeSatisfies(granted, requiredScope),\n );\n\n if (hasSufficientScope) {\n return { allowed: true };\n }\n return { allowed: false, requiredScope };\n}\n\n/**\n * Parse scopes from a comma-separated header value.\n */\nexport function parseScopesHeader(headerValue: string | undefined): OperatorScope[] {\n if (!headerValue?.trim()) {\n return [...DEFAULT_OPERATOR_SCOPES];\n }\n return headerValue\n .split(',')\n .map((scope) => scope.trim())\n .filter(isOperatorScope);\n}\n"],"mappings":";;;;;;;;AAQA,MAAa,cAAc;AAC3B,MAAa,aAAa;AAC1B,MAAa,cAAc;AAa3B,MAAa,wBAAoD,IAAI,IACnE;CANA;CACA;CACA;CAIA,CACD;AAED,SAAgB,gBAAgB,OAAwC;AACtE,QAAO,OAAO,UAAU,YAAY,sBAAsB,IAAI,MAAuB;;;AAIvF,MAAa,0BAA2C;CACtD;CACA;CACA;CACD;;;;;AAMD,MAAM,2BAAoF;CAExF;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAA4B,OAAO;EAAa;CAG1D;EAAE,QAAQ;EAAc,OAAO;EAAa;CAC5C;EAAE,QAAQ;EAAiB,OAAO;EAAa;CAC/C;EAAE,QAAQ;EAAa,OAAO;EAAa;CAC3C;EAAE,QAAQ;EAAiB,OAAO;EAAa;CAC/C;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAAkB,OAAO;EAAa;CAChD;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAAiB,OAAO;EAAa;CAC/C;EAAE,QAAQ;EAAgB,OAAO;EAAa;CAG9C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC5C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC5C;EAAE,QAAQ;EAAa,OAAO;EAAY;CAC1C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC5C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC7C;;AAGD,MAAM,kBAAiD;EACpD,cAAc;EACd,cAAc;EACd,aAAa;CACf;AAED,SAAS,eAAe,SAAwB,UAAkC;AAChF,QAAO,gBAAgB,YAAY,gBAAgB;;;;;AAMrD,SAAgB,oBACd,WACA,eACsE;CACtE,MAAM,mBAAmB,yBAAyB,MAC/C,UAAU,UAAU,WAAW,MAAM,OAAO,CAC9C;AAED,KAAI,CAAC,iBAEH,QAAO,eAAe,YAAY,cAAc;AAGlD,QAAO,eAAe,iBAAiB,OAAO,cAAc;;;;;AAM9D,SAAgB,eACd,eACA,eACsE;AAKtE,KAJ2B,cAAc,MACtC,YAAY,eAAe,SAAS,cAAc,CAG/B,CACpB,QAAO,EAAE,SAAS,MAAM;AAE1B,QAAO;EAAE,SAAS;EAAO;EAAe;;;;;AAM1C,SAAgB,kBAAkB,aAAkD;AAClF,KAAI,CAAC,aAAa,MAAM,CACtB,QAAO,CAAC,GAAG,wBAAwB;AAErC,QAAO,YACJ,MAAM,IAAI,CACV,KAAK,UAAU,MAAM,MAAM,CAAC,CAC5B,OAAO,gBAAgB"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Browser Origin checking for CSRF protection on HTTP and WebSocket requests.
3
+ *
4
+ * Validates that browser-initiated requests come from an allowed origin.
5
+ * Non-browser requests (no Origin header) are handled by other auth layers.
6
+ */
7
+ type OriginCheckResult = {
8
+ ok: true;
9
+ matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback';
10
+ } | {
11
+ ok: false;
12
+ reason: string;
13
+ };
14
+ export declare function checkBrowserOrigin(params: {
15
+ requestHost?: string;
16
+ origin?: string;
17
+ allowedOrigins?: string[];
18
+ allowHostHeaderOriginFallback?: boolean;
19
+ isLocalClient?: boolean;
20
+ }): OriginCheckResult;
21
+ export {};
@@ -0,0 +1,56 @@
1
+ //#region src/gateway/security/origin-check.ts
2
+ const LOOPBACK_HOSTNAMES = new Set([
3
+ "localhost",
4
+ "127.0.0.1",
5
+ "::1",
6
+ "[::1]"
7
+ ]);
8
+ function isLoopbackHost(hostname) {
9
+ return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
10
+ }
11
+ function parseOriginHost(originRaw) {
12
+ const trimmed = (originRaw ?? "").trim();
13
+ if (!trimmed || trimmed === "null") return null;
14
+ try {
15
+ const url = new URL(trimmed);
16
+ return {
17
+ origin: url.origin.toLowerCase(),
18
+ host: url.host.toLowerCase(),
19
+ hostname: url.hostname.toLowerCase()
20
+ };
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function normalizeHostHeader(hostRaw) {
26
+ return (hostRaw ?? "").trim().toLowerCase();
27
+ }
28
+ function checkBrowserOrigin(params) {
29
+ const parsedOrigin = parseOriginHost(params.origin);
30
+ if (!parsedOrigin) return {
31
+ ok: false,
32
+ reason: "origin missing or invalid"
33
+ };
34
+ const allowlist = new Set((params.allowedOrigins ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean));
35
+ if (allowlist.has("*") || allowlist.has(parsedOrigin.origin)) return {
36
+ ok: true,
37
+ matchedBy: "allowlist"
38
+ };
39
+ const requestHost = normalizeHostHeader(params.requestHost);
40
+ if (params.allowHostHeaderOriginFallback === true && requestHost && parsedOrigin.host === requestHost) return {
41
+ ok: true,
42
+ matchedBy: "host-header-fallback"
43
+ };
44
+ if (params.isLocalClient && isLoopbackHost(parsedOrigin.hostname)) return {
45
+ ok: true,
46
+ matchedBy: "local-loopback"
47
+ };
48
+ return {
49
+ ok: false,
50
+ reason: "origin not allowed"
51
+ };
52
+ }
53
+ //#endregion
54
+ export { checkBrowserOrigin };
55
+
56
+ //# sourceMappingURL=origin-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origin-check.js","names":[],"sources":["../../../../src/gateway/security/origin-check.ts"],"sourcesContent":["/**\n * Browser Origin checking for CSRF protection on HTTP and WebSocket requests.\n *\n * Validates that browser-initiated requests come from an allowed origin.\n * Non-browser requests (no Origin header) are handled by other auth layers.\n */\n\ntype OriginCheckResult =\n | { ok: true; matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback' }\n | { ok: false; reason: string };\n\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);\n\nfunction isLoopbackHost(hostname: string): boolean {\n return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());\n}\n\nfunction parseOriginHost(originRaw?: string): { origin: string; host: string; hostname: string } | null {\n const trimmed = (originRaw ?? '').trim();\n if (!trimmed || trimmed === 'null') {\n return null;\n }\n try {\n const url = new URL(trimmed);\n return {\n origin: url.origin.toLowerCase(),\n host: url.host.toLowerCase(),\n hostname: url.hostname.toLowerCase(),\n };\n } catch {\n return null;\n }\n}\n\nfunction normalizeHostHeader(hostRaw?: string): string {\n const trimmed = (hostRaw ?? '').trim().toLowerCase();\n // Strip port if present for comparison\n return trimmed;\n}\n\nexport function checkBrowserOrigin(params: {\n requestHost?: string;\n origin?: string;\n allowedOrigins?: string[];\n allowHostHeaderOriginFallback?: boolean;\n isLocalClient?: boolean;\n}): OriginCheckResult {\n const parsedOrigin = parseOriginHost(params.origin);\n if (!parsedOrigin) {\n return { ok: false, reason: 'origin missing or invalid' };\n }\n\n const allowlist = new Set(\n (params.allowedOrigins ?? [])\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean),\n );\n if (allowlist.has('*') || allowlist.has(parsedOrigin.origin)) {\n return { ok: true, matchedBy: 'allowlist' };\n }\n\n const requestHost = normalizeHostHeader(params.requestHost);\n if (\n params.allowHostHeaderOriginFallback === true &&\n requestHost &&\n parsedOrigin.host === requestHost\n ) {\n return { ok: true, matchedBy: 'host-header-fallback' };\n }\n\n // Dev fallback only for genuinely local socket clients, not Host-header claims.\n if (params.isLocalClient && isLoopbackHost(parsedOrigin.hostname)) {\n return { ok: true, matchedBy: 'local-loopback' };\n }\n\n return { ok: false, reason: 'origin not allowed' };\n}\n"],"mappings":";AAWA,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;CAAQ,CAAC;AAE9E,SAAS,eAAe,UAA2B;AACjD,QAAO,mBAAmB,IAAI,SAAS,aAAa,CAAC;;AAGvD,SAAS,gBAAgB,WAA+E;CACtG,MAAM,WAAW,aAAa,IAAI,MAAM;AACxC,KAAI,CAAC,WAAW,YAAY,OAC1B,QAAO;AAET,KAAI;EACF,MAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,SAAO;GACL,QAAQ,IAAI,OAAO,aAAa;GAChC,MAAM,IAAI,KAAK,aAAa;GAC5B,UAAU,IAAI,SAAS,aAAa;GACrC;SACK;AACN,SAAO;;;AAIX,SAAS,oBAAoB,SAA0B;AAGrD,SAFiB,WAAW,IAAI,MAAM,CAAC,aAEzB;;AAGhB,SAAgB,mBAAmB,QAMb;CACpB,MAAM,eAAe,gBAAgB,OAAO,OAAO;AACnD,KAAI,CAAC,aACH,QAAO;EAAE,IAAI;EAAO,QAAQ;EAA6B;CAG3D,MAAM,YAAY,IAAI,KACnB,OAAO,kBAAkB,EAAE,EACzB,KAAK,UAAU,MAAM,MAAM,CAAC,aAAa,CAAC,CAC1C,OAAO,QAAQ,CACnB;AACD,KAAI,UAAU,IAAI,IAAI,IAAI,UAAU,IAAI,aAAa,OAAO,CAC1D,QAAO;EAAE,IAAI;EAAM,WAAW;EAAa;CAG7C,MAAM,cAAc,oBAAoB,OAAO,YAAY;AAC3D,KACE,OAAO,kCAAkC,QACzC,eACA,aAAa,SAAS,YAEtB,QAAO;EAAE,IAAI;EAAM,WAAW;EAAwB;AAIxD,KAAI,OAAO,iBAAiB,eAAe,aAAa,SAAS,CAC/D,QAAO;EAAE,IAAI;EAAM,WAAW;EAAkB;AAGlD,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAsB"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Pre-authentication connection budget per IP address.
3
+ *
4
+ * Limits the number of concurrent unauthenticated connections from a single
5
+ * IP to prevent connection-flood DoS attacks. Once a connection authenticates
6
+ * successfully, its slot is released.
7
+ */
8
+ export declare function getMaxPreauthConnectionsPerIp(env?: NodeJS.ProcessEnv): number;
9
+ export type PreauthConnectionBudget = {
10
+ /** Try to acquire a slot for the given client IP. Returns false if the budget is exhausted. */
11
+ acquire(clientIp: string | undefined): boolean;
12
+ /** Release a slot when the connection authenticates or closes. */
13
+ release(clientIp: string | undefined): void;
14
+ /** Current number of tracked IPs. */
15
+ size(): number;
16
+ };
17
+ export declare function createPreauthConnectionBudget(limit?: number): PreauthConnectionBudget;
@@ -0,0 +1,49 @@
1
+ //#region src/gateway/security/preauth-connection-budget.ts
2
+ /**
3
+ * Pre-authentication connection budget per IP address.
4
+ *
5
+ * Limits the number of concurrent unauthenticated connections from a single
6
+ * IP to prevent connection-flood DoS attacks. Once a connection authenticates
7
+ * successfully, its slot is released.
8
+ */
9
+ const DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP = 32;
10
+ const UNKNOWN_CLIENT_IP_BUDGET_KEY = "__xopc_unknown_client_ip__";
11
+ function getMaxPreauthConnectionsPerIp(env = process.env) {
12
+ const configured = env.XOPC_MAX_PREAUTH_CONNECTIONS_PER_IP;
13
+ if (!configured) return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;
14
+ const parsed = Number(configured);
15
+ if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;
16
+ return Math.max(1, Math.floor(parsed));
17
+ }
18
+ function createPreauthConnectionBudget(limit = getMaxPreauthConnectionsPerIp()) {
19
+ const counts = /* @__PURE__ */ new Map();
20
+ function normalizeBudgetKey(clientIp) {
21
+ return clientIp?.trim() || UNKNOWN_CLIENT_IP_BUDGET_KEY;
22
+ }
23
+ return {
24
+ acquire(clientIp) {
25
+ const ip = normalizeBudgetKey(clientIp);
26
+ const next = (counts.get(ip) ?? 0) + 1;
27
+ if (next > limit) return false;
28
+ counts.set(ip, next);
29
+ return true;
30
+ },
31
+ release(clientIp) {
32
+ const ip = normalizeBudgetKey(clientIp);
33
+ const current = counts.get(ip);
34
+ if (current === void 0) return;
35
+ if (current <= 1) {
36
+ counts.delete(ip);
37
+ return;
38
+ }
39
+ counts.set(ip, current - 1);
40
+ },
41
+ size() {
42
+ return counts.size;
43
+ }
44
+ };
45
+ }
46
+ //#endregion
47
+ export { createPreauthConnectionBudget, getMaxPreauthConnectionsPerIp };
48
+
49
+ //# sourceMappingURL=preauth-connection-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preauth-connection-budget.js","names":[],"sources":["../../../../src/gateway/security/preauth-connection-budget.ts"],"sourcesContent":["/**\n * Pre-authentication connection budget per IP address.\n *\n * Limits the number of concurrent unauthenticated connections from a single\n * IP to prevent connection-flood DoS attacks. Once a connection authenticates\n * successfully, its slot is released.\n */\n\nconst DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP = 32;\nconst UNKNOWN_CLIENT_IP_BUDGET_KEY = '__xopc_unknown_client_ip__';\n\nexport function getMaxPreauthConnectionsPerIp(env: NodeJS.ProcessEnv = process.env): number {\n const configured = env.XOPC_MAX_PREAUTH_CONNECTIONS_PER_IP;\n if (!configured) {\n return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;\n }\n const parsed = Number(configured);\n if (!Number.isFinite(parsed) || parsed < 1) {\n return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;\n }\n return Math.max(1, Math.floor(parsed));\n}\n\nexport type PreauthConnectionBudget = {\n /** Try to acquire a slot for the given client IP. Returns false if the budget is exhausted. */\n acquire(clientIp: string | undefined): boolean;\n /** Release a slot when the connection authenticates or closes. */\n release(clientIp: string | undefined): void;\n /** Current number of tracked IPs. */\n size(): number;\n};\n\nexport function createPreauthConnectionBudget(\n limit = getMaxPreauthConnectionsPerIp(),\n): PreauthConnectionBudget {\n const counts = new Map<string, number>();\n\n function normalizeBudgetKey(clientIp: string | undefined): string {\n const ip = clientIp?.trim();\n // Keep unresolved IPs capped under a shared fallback bucket\n // instead of failing open.\n return ip || UNKNOWN_CLIENT_IP_BUDGET_KEY;\n }\n\n return {\n acquire(clientIp) {\n const ip = normalizeBudgetKey(clientIp);\n const next = (counts.get(ip) ?? 0) + 1;\n if (next > limit) {\n return false;\n }\n counts.set(ip, next);\n return true;\n },\n\n release(clientIp) {\n const ip = normalizeBudgetKey(clientIp);\n const current = counts.get(ip);\n if (current === undefined) {\n return;\n }\n if (current <= 1) {\n counts.delete(ip);\n return;\n }\n counts.set(ip, current - 1);\n },\n\n size() {\n return counts.size;\n },\n };\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,yCAAyC;AAC/C,MAAM,+BAA+B;AAErC,SAAgB,8BAA8B,MAAyB,QAAQ,KAAa;CAC1F,MAAM,aAAa,IAAI;AACvB,KAAI,CAAC,WACH,QAAO;CAET,MAAM,SAAS,OAAO,WAAW;AACjC,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,SAAS,EACvC,QAAO;AAET,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC;;AAYxC,SAAgB,8BACd,QAAQ,+BAA+B,EACd;CACzB,MAAM,yBAAS,IAAI,KAAqB;CAExC,SAAS,mBAAmB,UAAsC;AAIhE,SAHW,UAAU,MAAM,IAGd;;AAGf,QAAO;EACL,QAAQ,UAAU;GAChB,MAAM,KAAK,mBAAmB,SAAS;GACvC,MAAM,QAAQ,OAAO,IAAI,GAAG,IAAI,KAAK;AACrC,OAAI,OAAO,MACT,QAAO;AAET,UAAO,IAAI,IAAI,KAAK;AACpB,UAAO;;EAGT,QAAQ,UAAU;GAChB,MAAM,KAAK,mBAAmB,SAAS;GACvC,MAAM,UAAU,OAAO,IAAI,GAAG;AAC9B,OAAI,YAAY,KAAA,EACd;AAEF,OAAI,WAAW,GAAG;AAChB,WAAO,OAAO,GAAG;AACjB;;AAEF,UAAO,IAAI,IAAI,UAAU,EAAE;;EAG7B,OAAO;AACL,UAAO,OAAO;;EAEjB"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Constant-time string comparison to prevent timing attacks.
3
+ *
4
+ * Uses `crypto.timingSafeEqual` with padding so both buffers always have
5
+ * the same byte length. The actual length is checked separately to reject
6
+ * mismatches without leaking position information.
7
+ */
8
+ export declare function safeEqualSecret(provided: string | undefined | null, expected: string | undefined | null): boolean;
@@ -0,0 +1,30 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ //#region src/gateway/security/secret-equal.ts
3
+ /**
4
+ * Pad a Buffer to the target length by allocating a zeroed buffer and copying.
5
+ */
6
+ function padSecretBytes(bytes, length) {
7
+ if (bytes.length === length) return bytes;
8
+ const padded = Buffer.alloc(length);
9
+ bytes.copy(padded);
10
+ return padded;
11
+ }
12
+ /**
13
+ * Constant-time string comparison to prevent timing attacks.
14
+ *
15
+ * Uses `crypto.timingSafeEqual` with padding so both buffers always have
16
+ * the same byte length. The actual length is checked separately to reject
17
+ * mismatches without leaking position information.
18
+ */
19
+ function safeEqualSecret(provided, expected) {
20
+ if (typeof provided !== "string" || typeof expected !== "string") return false;
21
+ const providedBytes = Buffer.from(provided, "utf8");
22
+ const expectedBytes = Buffer.from(expected, "utf8");
23
+ const byteLength = Math.max(providedBytes.length, expectedBytes.length);
24
+ if (byteLength === 0) return true;
25
+ return timingSafeEqual(padSecretBytes(providedBytes, byteLength), padSecretBytes(expectedBytes, byteLength)) && providedBytes.length === expectedBytes.length;
26
+ }
27
+ //#endregion
28
+ export { safeEqualSecret };
29
+
30
+ //# sourceMappingURL=secret-equal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-equal.js","names":[],"sources":["../../../../src/gateway/security/secret-equal.ts"],"sourcesContent":["import { timingSafeEqual } from 'node:crypto';\n\n/**\n * Pad a Buffer to the target length by allocating a zeroed buffer and copying.\n */\nfunction padSecretBytes(bytes: Buffer, length: number): Buffer {\n if (bytes.length === length) {\n return bytes;\n }\n const padded = Buffer.alloc(length);\n bytes.copy(padded);\n return padded;\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks.\n *\n * Uses `crypto.timingSafeEqual` with padding so both buffers always have\n * the same byte length. The actual length is checked separately to reject\n * mismatches without leaking position information.\n */\nexport function safeEqualSecret(\n provided: string | undefined | null,\n expected: string | undefined | null,\n): boolean {\n if (typeof provided !== 'string' || typeof expected !== 'string') {\n return false;\n }\n const providedBytes = Buffer.from(provided, 'utf8');\n const expectedBytes = Buffer.from(expected, 'utf8');\n const byteLength = Math.max(providedBytes.length, expectedBytes.length);\n if (byteLength === 0) {\n return true;\n }\n return (\n timingSafeEqual(\n padSecretBytes(providedBytes, byteLength),\n padSecretBytes(expectedBytes, byteLength),\n ) && providedBytes.length === expectedBytes.length\n );\n}\n"],"mappings":";;;;;AAKA,SAAS,eAAe,OAAe,QAAwB;AAC7D,KAAI,MAAM,WAAW,OACnB,QAAO;CAET,MAAM,SAAS,OAAO,MAAM,OAAO;AACnC,OAAM,KAAK,OAAO;AAClB,QAAO;;;;;;;;;AAUT,SAAgB,gBACd,UACA,UACS;AACT,KAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SACtD,QAAO;CAET,MAAM,gBAAgB,OAAO,KAAK,UAAU,OAAO;CACnD,MAAM,gBAAgB,OAAO,KAAK,UAAU,OAAO;CACnD,MAAM,aAAa,KAAK,IAAI,cAAc,QAAQ,cAAc,OAAO;AACvE,KAAI,eAAe,EACjB,QAAO;AAET,QACE,gBACE,eAAe,eAAe,WAAW,EACzC,eAAe,eAAe,WAAW,CAC1C,IAAI,cAAc,WAAW,cAAc"}
@@ -170,6 +170,8 @@ export declare class GatewayService {
170
170
  }, unknown>;
171
171
  /** Abort an in-flight webchat agent run (matches `runId` from SSE `status`). */
172
172
  abortAgentRun(runId: string): boolean;
173
+ /** Background drain for extension-initiated webchat turns (`scheduleWebchatContinuation`). */
174
+ private drainScheduledWebchatContinuation;
173
175
  /**
174
176
  * Queue steering text for an active webchat run (`Agent.steer` / tool-boundary injection).
175
177
  * `chatId` is the same as `POST /api/agent` body (`sessionKey` or legacy peer id).
@@ -429,7 +431,7 @@ export declare class GatewayService {
429
431
  /**
430
432
  * Get current auth mode.
431
433
  */
432
- getAuthMode(): 'none' | 'token';
434
+ getAuthMode(): 'none' | 'token' | 'password';
433
435
  /**
434
436
  * Get current auth token (for CLI server integration).
435
437
  * Returns undefined if mode is 'none'.
@@ -35,6 +35,8 @@ import { computeBundledExtensionExtensionsPatch } from "../extensions/bundled-ex
35
35
  import { HeartbeatService, heartbeatRunnerConfigFromConfig } from "./heartbeat/service.js";
36
36
  import "./heartbeat/index.js";
37
37
  import { assertGatewayAuthConfigured, extractToken, resolveGatewayAuth, validateToken } from "./auth.js";
38
+ import { assertGatewayAuthNotKnownWeak } from "./security/known-weak-secrets.js";
39
+ import { auditGatewayConfig } from "./security/audit.js";
38
40
  import { AgentRunRelay } from "./agent-run-relay.js";
39
41
  import { ClarifyBridge } from "./clarify-bridge.js";
40
42
  import { downloadSkillZipBuffer, fetchMarketplacePackageDetail, listSkillPackages, resolveSkillZipDownloadUrl, resolveSkillsStoreBaseUrl, skillIdForMarketplaceInstall } from "../agent/skills/skills-store-client.js";
@@ -86,6 +88,12 @@ var GatewayService = class {
86
88
  this.config = loadConfig(this.configPath);
87
89
  this.auth = resolveGatewayAuth({ authConfig: this.config.gateway?.auth });
88
90
  assertGatewayAuthConfigured(this.auth);
91
+ assertGatewayAuthNotKnownWeak(this.auth);
92
+ auditGatewayConfig({
93
+ auth: this.auth,
94
+ host: this.config.gateway?.host,
95
+ corsOrigins: this.config.gateway?.corsOrigins
96
+ });
89
97
  if (this.auth.mode === "token") {
90
98
  const tokenPreview = this.auth.token ? `${this.auth.token.slice(0, 4)}***` : "none";
91
99
  log.info({
@@ -199,7 +207,12 @@ var GatewayService = class {
199
207
  this.channelManager.enableOutboundPersistence(resolveAgentDir(this.config, getDefaultAgentId(this.config)));
200
208
  if (this.extensionLoader) this.extensionLoader.setRuntimeContext({
201
209
  bus: this.bus,
202
- sessionManager: this.sessionManager
210
+ sessionManager: this.sessionManager,
211
+ scheduleWebchatContinuation: (sessionKey, continuationMessage) => {
212
+ queueMicrotask(() => {
213
+ this.drainScheduledWebchatContinuation(sessionKey, continuationMessage);
214
+ });
215
+ }
203
216
  });
204
217
  await this.loadExtensionsAndRegisterChannels();
205
218
  await this.channelManager.initialize();
@@ -609,7 +622,8 @@ var GatewayService = class {
609
622
  peerKind: "direct",
610
623
  peerId: chatId
611
624
  });
612
- const stampedMessage = prependEnvelopeTimestamp(message, this.agentService.resolveUserTimezoneForSession(sessionKey));
625
+ const timezone = this.agentService.resolveUserTimezoneForSession(sessionKey);
626
+ const stampedMessage = message.trimStart().startsWith("/") ? message : prependEnvelopeTimestamp(message, timezone);
613
627
  const prepared = await this.agentService.prepareInboundAttachments(sessionKey, cappedAttachments);
614
628
  try {
615
629
  await this._saveUserMessage(sessionKey, message, prepared);
@@ -623,6 +637,7 @@ var GatewayService = class {
623
637
  if (!runAbort) throw new Error("run abort controller missing for webchat");
624
638
  const mergedSignal = runOptions?.signal ? AbortSignal.any([runOptions.signal, runAbort.signal]) : runAbort.signal;
625
639
  this.activeWebchatRunBySession.set(sessionKey, runId);
640
+ let streamError;
626
641
  try {
627
642
  this.emit("agent.stream", {
628
643
  sessionKey,
@@ -651,9 +666,10 @@ var GatewayService = class {
651
666
  };
652
667
  } catch (error) {
653
668
  log.error({ error }, "Agent processing failed");
669
+ streamError = error instanceof Error ? error.message : "Unknown error";
654
670
  const errorEvent = {
655
671
  type: "error",
656
- content: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
672
+ content: `Error: ${streamError}`
657
673
  };
658
674
  this.runRelay.publish(runId, errorEvent);
659
675
  this.emit("agent.stream", {
@@ -664,11 +680,19 @@ var GatewayService = class {
664
680
  yield errorEvent;
665
681
  return {
666
682
  status: "error",
667
- summary: error instanceof Error ? error.message : "Unknown error"
683
+ summary: streamError
668
684
  };
669
685
  } finally {
670
686
  this.activeWebchatRunBySession.delete(sessionKey);
671
687
  this.runAbortControllers.delete(runId);
688
+ const assistantPlainText = this.agentService.getLastAssistantPlainText(sessionKey);
689
+ await this.agentService.emitWebchatTurnComplete({
690
+ sessionKey,
691
+ inboundUserText: message,
692
+ assistantPlainText,
693
+ aborted: mergedSignal.aborted,
694
+ ...streamError !== void 0 ? { streamError } : {}
695
+ });
672
696
  }
673
697
  }
674
698
  const correlationMeta = inboundCorrelationMetadataFromAsyncLogContext();
@@ -706,6 +730,18 @@ var GatewayService = class {
706
730
  c.abort();
707
731
  return true;
708
732
  }
733
+ /** Background drain for extension-initiated webchat turns (`scheduleWebchatContinuation`). */
734
+ async drainScheduledWebchatContinuation(sessionKey, message) {
735
+ try {
736
+ const gen = this.runAgent(message, "webchat", sessionKey, void 0, void 0, void 0);
737
+ for await (const _ of gen);
738
+ } catch (err) {
739
+ log.warn({
740
+ err,
741
+ sessionKey
742
+ }, "Scheduled webchat continuation failed");
743
+ }
744
+ }
709
745
  /**
710
746
  * Queue steering text for an active webchat run (`Agent.steer` / tool-boundary injection).
711
747
  * `chatId` is the same as `POST /api/agent` body (`sessionKey` or legacy peer id).