upfynai-code 3.0.4 → 3.2.0

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 (258) hide show
  1. package/README.md +69 -92
  2. package/bin/cli.js +191 -0
  3. package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
  4. package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
  5. package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
  6. package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
  7. package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
  8. package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
  9. package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
  10. package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
  11. package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
  12. package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
  13. package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
  14. package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
  15. package/dist/client/assets/index-CnNNzw9A.css +1 -0
  16. package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
  17. package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
  18. package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
  19. package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
  20. package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
  21. package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
  22. package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
  23. package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
  24. package/dist/client/favicon.png +0 -0
  25. package/dist/client/icons/icon-128x128.png +0 -0
  26. package/dist/client/icons/icon-144x144.png +0 -0
  27. package/dist/client/icons/icon-152x152.png +0 -0
  28. package/dist/client/icons/icon-192x192.png +0 -0
  29. package/dist/client/icons/icon-384x384.png +0 -0
  30. package/dist/client/icons/icon-512x512.png +0 -0
  31. package/dist/client/icons/icon-72x72.png +0 -0
  32. package/dist/client/icons/icon-96x96.png +0 -0
  33. package/{client/dist → dist/client}/index.html +37 -36
  34. package/dist/client/logo-128.png +0 -0
  35. package/dist/client/logo-256.png +0 -0
  36. package/dist/client/logo-32.png +0 -0
  37. package/dist/client/logo-512.png +0 -0
  38. package/dist/client/logo-64.png +0 -0
  39. package/dist/client/logo.png +0 -0
  40. package/{client/dist → dist/client}/manifest.json +12 -12
  41. package/{client/dist → dist/client}/mcp-docs.html +1 -1
  42. package/{client/dist → dist/client}/sw.js +2 -2
  43. package/package.json +56 -105
  44. package/scripts/postinstall.js +9 -0
  45. package/scripts/prepublish.js +77 -0
  46. package/src/animation.js +228 -0
  47. package/src/auth.js +142 -0
  48. package/src/config.js +40 -0
  49. package/src/connect.js +416 -0
  50. package/src/launch.js +81 -0
  51. package/src/mcp.js +57 -0
  52. package/src/permissions.js +140 -0
  53. package/src/persistent-shell.js +261 -0
  54. package/src/server.js +54 -0
  55. package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
  56. package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
  57. package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
  58. package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
  59. package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
  60. package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
  61. package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
  62. package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
  63. package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
  64. package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
  65. package/client/dist/assets/index-46kkVu2i.css +0 -1
  66. package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
  67. package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
  68. package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
  69. package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
  70. package/client/dist/favicon.png +0 -0
  71. package/client/dist/icons/icon-128x128.png +0 -0
  72. package/client/dist/icons/icon-144x144.png +0 -0
  73. package/client/dist/icons/icon-152x152.png +0 -0
  74. package/client/dist/icons/icon-192x192.png +0 -0
  75. package/client/dist/icons/icon-384x384.png +0 -0
  76. package/client/dist/icons/icon-512x512.png +0 -0
  77. package/client/dist/icons/icon-72x72.png +0 -0
  78. package/client/dist/icons/icon-96x96.png +0 -0
  79. package/client/dist/logo-128.png +0 -0
  80. package/client/dist/logo-256.png +0 -0
  81. package/client/dist/logo-32.png +0 -0
  82. package/client/dist/logo-512.png +0 -0
  83. package/client/dist/logo-64.png +0 -0
  84. package/commands/upfynai-connect.md +0 -59
  85. package/commands/upfynai-disconnect.md +0 -31
  86. package/commands/upfynai-doctor.md +0 -99
  87. package/commands/upfynai-export.md +0 -49
  88. package/commands/upfynai-local.md +0 -82
  89. package/commands/upfynai-status.md +0 -75
  90. package/commands/upfynai-stop.md +0 -49
  91. package/commands/upfynai-uninstall.md +0 -58
  92. package/commands/upfynai.md +0 -69
  93. package/scripts/build-client.js +0 -17
  94. package/scripts/fix-node-pty.js +0 -67
  95. package/scripts/install-commands.js +0 -78
  96. package/server/agent-loop.js +0 -242
  97. package/server/auto-compact.js +0 -99
  98. package/server/browser.js +0 -131
  99. package/server/claude-sdk.js +0 -797
  100. package/server/cli-ui.js +0 -798
  101. package/server/cli.js +0 -751
  102. package/server/constants/config.js +0 -31
  103. package/server/cursor-cli.js +0 -270
  104. package/server/database/auth.db +0 -0
  105. package/server/database/db.js +0 -1547
  106. package/server/database/init.sql +0 -70
  107. package/server/index.js +0 -3813
  108. package/server/load-env.js +0 -26
  109. package/server/mcp-server.js +0 -621
  110. package/server/middleware/auth.js +0 -184
  111. package/server/middleware/relayHelpers.js +0 -44
  112. package/server/middleware/sandboxRouter.js +0 -174
  113. package/server/openai-codex.js +0 -403
  114. package/server/openrouter.js +0 -137
  115. package/server/projects.js +0 -1807
  116. package/server/provider-factory.js +0 -174
  117. package/server/relay-client.js +0 -390
  118. package/server/routes/agent.js +0 -1234
  119. package/server/routes/auth.js +0 -559
  120. package/server/routes/browser.js +0 -419
  121. package/server/routes/canvas.js +0 -53
  122. package/server/routes/cli-auth.js +0 -263
  123. package/server/routes/codex.js +0 -396
  124. package/server/routes/commands.js +0 -707
  125. package/server/routes/composio.js +0 -176
  126. package/server/routes/cursor.js +0 -770
  127. package/server/routes/dashboard.js +0 -295
  128. package/server/routes/git.js +0 -1208
  129. package/server/routes/keys.js +0 -34
  130. package/server/routes/mcp-utils.js +0 -48
  131. package/server/routes/mcp.js +0 -661
  132. package/server/routes/payments.js +0 -227
  133. package/server/routes/projects.js +0 -754
  134. package/server/routes/sessions.js +0 -146
  135. package/server/routes/settings.js +0 -261
  136. package/server/routes/taskmaster.js +0 -1928
  137. package/server/routes/user.js +0 -106
  138. package/server/routes/vapi-chat.js +0 -624
  139. package/server/routes/voice.js +0 -235
  140. package/server/routes/webhooks.js +0 -166
  141. package/server/routes/workflows.js +0 -312
  142. package/server/sandbox.js +0 -120
  143. package/server/services/browser-ai.js +0 -154
  144. package/server/services/composio.js +0 -204
  145. package/server/services/sessionRegistry.js +0 -139
  146. package/server/services/whisperService.js +0 -84
  147. package/server/services/workflowScheduler.js +0 -211
  148. package/server/tests/relay-flow.test.js +0 -570
  149. package/server/tests/sessions.test.js +0 -259
  150. package/server/utils/commandParser.js +0 -303
  151. package/server/utils/email.js +0 -66
  152. package/server/utils/gitConfig.js +0 -24
  153. package/server/utils/mcp-detector.js +0 -198
  154. package/server/utils/taskmaster-websocket.js +0 -129
  155. package/shared/integrationCatalog.d.ts +0 -12
  156. package/shared/integrationCatalog.js +0 -172
  157. package/shared/modelConstants.js +0 -96
  158. /package/{shared → dist}/agents/claude.js +0 -0
  159. /package/{shared → dist}/agents/codex.js +0 -0
  160. /package/{shared → dist}/agents/cursor.js +0 -0
  161. /package/{shared → dist}/agents/detect.js +0 -0
  162. /package/{shared → dist}/agents/exec.js +0 -0
  163. /package/{shared → dist}/agents/files.js +0 -0
  164. /package/{shared → dist}/agents/git.js +0 -0
  165. /package/{shared → dist}/agents/gitagent.js +0 -0
  166. /package/{shared → dist}/agents/index.js +0 -0
  167. /package/{shared → dist}/agents/shell.js +0 -0
  168. /package/{shared → dist}/agents/utils.js +0 -0
  169. /package/{client/dist → dist/client}/api-docs.html +0 -0
  170. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  171. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  172. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  173. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  174. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  175. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  176. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  177. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  178. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  179. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  180. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  181. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  182. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  183. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  184. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  185. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  186. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  187. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  188. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  189. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  190. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  191. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  192. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  193. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  194. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  195. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  196. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  197. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  198. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  199. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  200. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  201. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  202. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  203. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  204. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  205. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  206. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  207. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  208. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  209. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  210. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  211. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  212. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  213. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  214. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  215. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  216. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  217. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  218. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  219. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  220. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  221. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  222. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  223. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  224. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  225. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  226. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  227. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  228. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  229. /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
  230. /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
  231. /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
  232. /package/{client/dist → dist/client}/clear-cache.html +0 -0
  233. /package/{client/dist → dist/client}/convert-icons.md +0 -0
  234. /package/{client/dist → dist/client}/favicon.svg +0 -0
  235. /package/{client/dist → dist/client}/generate-icons.js +0 -0
  236. /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
  237. /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
  238. /package/{client/dist → dist/client}/icons/codex.svg +0 -0
  239. /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
  240. /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
  241. /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
  242. /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
  243. /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
  244. /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
  245. /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
  246. /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
  247. /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
  248. /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
  249. /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
  250. /package/{client/dist → dist/client}/logo.svg +0 -0
  251. /package/{client/dist → dist/client}/offline.html +0 -0
  252. /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
  253. /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
  254. /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
  255. /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
  256. /package/{shared → dist}/gitagent/index.js +0 -0
  257. /package/{shared → dist}/gitagent/parser.js +0 -0
  258. /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
@@ -1,570 +0,0 @@
1
- /**
2
- * End-to-end tests for Relay AI Chat Flow & Multi-User Isolation
3
- *
4
- * Tests verify:
5
- * 1. Relay message routing (UI → Backend → CLI → AI → Response)
6
- * 2. Streaming event translation (relay-stream → content_block_delta)
7
- * 3. Multi-user isolation (broadcasts, sessions, projects, relay connections)
8
- * 4. Session locking and ownership
9
- * 5. Agent abstraction (executeAction dispatch)
10
- *
11
- * Run: node --test backend/server/tests/relay-flow.test.js
12
- */
13
-
14
- import { describe, it } from 'node:test';
15
- import assert from 'node:assert/strict';
16
-
17
- // ── Test 1: Relay Connection Isolation ──
18
-
19
- describe('Relay Connection Isolation', () => {
20
- it('relayConnections map keys by userId — different users get separate entries', () => {
21
- const relayConnections = new Map();
22
-
23
- // User A connects
24
- relayConnections.set(1, { ws: { readyState: 1 }, user: { userId: 1, username: 'userA' }, connectedAt: Date.now() });
25
- // User B connects
26
- relayConnections.set(2, { ws: { readyState: 1 }, user: { userId: 2, username: 'userB' }, connectedAt: Date.now() });
27
-
28
- assert.equal(relayConnections.size, 2, 'Two separate relay connections');
29
- assert.equal(relayConnections.get(1).user.username, 'userA');
30
- assert.equal(relayConnections.get(2).user.username, 'userB');
31
-
32
- // User A's lookup cannot access User B's relay
33
- const relayA = relayConnections.get(1);
34
- const relayB = relayConnections.get(2);
35
- assert.notEqual(relayA.ws, relayB.ws, 'Different users have different WebSocket connections');
36
- });
37
-
38
- it('hasActiveRelay returns true only for the correct user', () => {
39
- const relayConnections = new Map();
40
- relayConnections.set(1, { ws: { readyState: 1 } });
41
- relayConnections.set(2, { ws: { readyState: 3 } }); // User 2 disconnected (readyState=3)
42
-
43
- function hasActiveRelay(userId) {
44
- if (!userId) return false;
45
- const relay = relayConnections.get(Number(userId));
46
- return relay && relay.ws.readyState === 1;
47
- }
48
-
49
- assert.equal(hasActiveRelay(1), true, 'User 1 has active relay');
50
- assert.ok(!hasActiveRelay(2), 'User 2 relay is disconnected');
51
- assert.ok(!hasActiveRelay(3), 'User 3 has no relay');
52
- assert.ok(!hasActiveRelay(null), 'Null userId returns falsy');
53
- assert.ok(!hasActiveRelay(undefined), 'Undefined userId returns falsy');
54
- });
55
-
56
- it('sendRelayCommand targets only the correct userId relay', () => {
57
- const relayConnections = new Map();
58
- const sentMessages = { user1: [], user2: [] };
59
-
60
- relayConnections.set(1, {
61
- ws: {
62
- readyState: 1,
63
- send: (msg) => sentMessages.user1.push(JSON.parse(msg)),
64
- }
65
- });
66
- relayConnections.set(2, {
67
- ws: {
68
- readyState: 1,
69
- send: (msg) => sentMessages.user2.push(JSON.parse(msg)),
70
- }
71
- });
72
-
73
- // Send command to user 1's relay
74
- const relay = relayConnections.get(1);
75
- relay.ws.send(JSON.stringify({ type: 'relay-command', action: 'claude-query', command: 'hello' }));
76
-
77
- assert.equal(sentMessages.user1.length, 1, 'User 1 received the command');
78
- assert.equal(sentMessages.user2.length, 0, 'User 2 did NOT receive the command');
79
- assert.equal(sentMessages.user1[0].action, 'claude-query');
80
- });
81
- });
82
-
83
- // ── Test 2: Broadcast User Filtering ──
84
-
85
- describe('Broadcast User Filtering', () => {
86
- it('relay-status broadcast only reaches clients with matching userId', () => {
87
- const connectedClients = new Set();
88
- const received = { userA: [], userB: [], noAuth: [] };
89
-
90
- const clientA = {
91
- readyState: 1,
92
- _wsUser: { userId: 1 },
93
- send: (msg) => received.userA.push(JSON.parse(msg)),
94
- };
95
- const clientB = {
96
- readyState: 1,
97
- _wsUser: { userId: 2 },
98
- send: (msg) => received.userB.push(JSON.parse(msg)),
99
- };
100
- const clientNoAuth = {
101
- readyState: 1,
102
- _wsUser: null,
103
- send: (msg) => received.noAuth.push(JSON.parse(msg)),
104
- };
105
-
106
- connectedClients.add(clientA);
107
- connectedClients.add(clientB);
108
- connectedClients.add(clientNoAuth);
109
-
110
- // Broadcast relay-status for userId=1
111
- const userId = 1;
112
- for (const client of connectedClients) {
113
- try {
114
- if (client.readyState === 1 && client._wsUser?.userId === userId) {
115
- client.send(JSON.stringify({ type: 'relay-status', userId, connected: true }));
116
- }
117
- } catch { /* ignore */ }
118
- }
119
-
120
- assert.equal(received.userA.length, 1, 'User A received the broadcast');
121
- assert.equal(received.userB.length, 0, 'User B did NOT receive it');
122
- assert.equal(received.noAuth.length, 0, 'Unauthenticated client did NOT receive it');
123
- assert.equal(received.userA[0].type, 'relay-status');
124
- assert.equal(received.userA[0].connected, true);
125
- });
126
-
127
- it('projects_updated broadcast only sends user-owned projects', () => {
128
- const connectedClients = new Set();
129
- const received = { userA: [], userB: [] };
130
-
131
- const clientA = {
132
- readyState: 1,
133
- _wsUser: { userId: 1 },
134
- send: (msg) => received.userA.push(JSON.parse(msg)),
135
- };
136
- const clientB = {
137
- readyState: 1,
138
- _wsUser: { userId: 2 },
139
- send: (msg) => received.userB.push(JSON.parse(msg)),
140
- };
141
- connectedClients.add(clientA);
142
- connectedClients.add(clientB);
143
-
144
- // Simulate per-user project lists
145
- const userProjects = {
146
- 1: [{ name: 'projectA' }],
147
- 2: [{ name: 'projectB' }],
148
- };
149
-
150
- for (const client of connectedClients) {
151
- if (client.readyState !== 1) continue;
152
- const uid = client._wsUser?.userId;
153
- if (uid && userProjects[uid]) {
154
- client.send(JSON.stringify({ type: 'projects_updated', projects: userProjects[uid] }));
155
- }
156
- }
157
-
158
- assert.equal(received.userA.length, 1);
159
- assert.equal(received.userB.length, 1);
160
- assert.equal(received.userA[0].projects[0].name, 'projectA', 'User A sees only projectA');
161
- assert.equal(received.userB[0].projects[0].name, 'projectB', 'User B sees only projectB');
162
- });
163
- });
164
-
165
- // ── Test 3: Streaming Event Translation ──
166
-
167
- describe('Streaming Event Translation', () => {
168
- it('relay-stream with claude-response translates to content_block_delta', () => {
169
- const frontendMessages = [];
170
- const writer = {
171
- send: (msg) => frontendMessages.push(msg),
172
- };
173
-
174
- // Simulate onStream callback from routeViaRelay
175
- const responseType = 'claude-response';
176
- const sessionId = 'session-123';
177
- let fullContent = '';
178
-
179
- function onStream(streamData) {
180
- if (streamData.type === 'claude-response') {
181
- const chunk = streamData.content || '';
182
- if (chunk) {
183
- fullContent += chunk;
184
- writer.send({
185
- type: responseType,
186
- data: { type: 'content_block_delta', delta: { text: chunk } },
187
- sessionId,
188
- });
189
- }
190
- }
191
- }
192
-
193
- // Simulate streaming chunks
194
- onStream({ type: 'claude-response', content: 'Hello' });
195
- onStream({ type: 'claude-response', content: ', I am Claude.' });
196
- onStream({ type: 'claude-response', content: ' How can I help?' });
197
-
198
- assert.equal(frontendMessages.length, 3, 'Three streaming events sent to frontend');
199
- assert.equal(frontendMessages[0].data.type, 'content_block_delta');
200
- assert.equal(frontendMessages[0].data.delta.text, 'Hello');
201
- assert.equal(frontendMessages[1].data.delta.text, ', I am Claude.');
202
- assert.equal(frontendMessages[2].data.delta.text, ' How can I help?');
203
- assert.equal(fullContent, 'Hello, I am Claude. How can I help?', 'Full content accumulated correctly');
204
- });
205
-
206
- it('claude-system event translates to relay-session-id', () => {
207
- const frontendMessages = [];
208
- const writer = { send: (msg) => frontendMessages.push(msg) };
209
- const sessionId = 'session-456';
210
- let capturedCliSessionId = null;
211
-
212
- function onStream(streamData) {
213
- if (streamData.type === 'claude-system' && streamData.sessionId) {
214
- capturedCliSessionId = streamData.sessionId;
215
- writer.send({
216
- type: 'relay-session-id',
217
- sessionId,
218
- cliSessionId: capturedCliSessionId,
219
- model: streamData.model,
220
- cwd: streamData.cwd,
221
- });
222
- }
223
- }
224
-
225
- onStream({ type: 'claude-system', sessionId: 'cli-sess-789', model: 'claude-sonnet-4-6', cwd: '/home/user/project' });
226
-
227
- assert.equal(frontendMessages.length, 1);
228
- assert.equal(frontendMessages[0].type, 'relay-session-id');
229
- assert.equal(frontendMessages[0].cliSessionId, 'cli-sess-789');
230
- assert.equal(frontendMessages[0].model, 'claude-sonnet-4-6');
231
- assert.equal(capturedCliSessionId, 'cli-sess-789');
232
- });
233
-
234
- it('completion event includes viaRelay: true', () => {
235
- const frontendMessages = [];
236
- const writer = { send: (msg) => frontendMessages.push(msg) };
237
-
238
- // Simulate routeViaRelay completion
239
- writer.send({
240
- type: 'claude-complete',
241
- sessionId: 'session-123',
242
- cliSessionId: 'cli-sess-456',
243
- exitCode: 0,
244
- isNewSession: true,
245
- viaRelay: true,
246
- });
247
-
248
- assert.equal(frontendMessages[0].type, 'claude-complete');
249
- assert.equal(frontendMessages[0].viaRelay, true, 'Must indicate response came via relay');
250
- assert.equal(frontendMessages[0].exitCode, 0);
251
- });
252
- });
253
-
254
- // ── Test 4: Session Ownership Isolation ──
255
-
256
- describe('Session Ownership Isolation', () => {
257
- it('sessionOwners map enforces user isolation', () => {
258
- const sessionOwners = new Map();
259
-
260
- // User 1 creates a session
261
- sessionOwners.set('session-A', 1);
262
- // User 2 creates a session
263
- sessionOwners.set('session-B', 2);
264
-
265
- // Simulate getUserSessions filter
266
- function getUserSessions(userId, allSessions) {
267
- return allSessions.filter(s => {
268
- const owner = sessionOwners.get(s.sessionId);
269
- return !owner || owner === userId;
270
- });
271
- }
272
-
273
- const allSessions = [
274
- { sessionId: 'session-A', provider: 'claude' },
275
- { sessionId: 'session-B', provider: 'claude' },
276
- { sessionId: 'session-C', provider: 'cursor' }, // unowned
277
- ];
278
-
279
- const user1Sessions = getUserSessions(1, allSessions);
280
- const user2Sessions = getUserSessions(2, allSessions);
281
-
282
- assert.equal(user1Sessions.length, 2, 'User 1 sees session-A + unowned session-C');
283
- assert.ok(user1Sessions.some(s => s.sessionId === 'session-A'), 'User 1 sees their own session');
284
- assert.ok(!user1Sessions.some(s => s.sessionId === 'session-B'), 'User 1 cannot see User 2 session');
285
-
286
- assert.equal(user2Sessions.length, 2, 'User 2 sees session-B + unowned session-C');
287
- assert.ok(user2Sessions.some(s => s.sessionId === 'session-B'), 'User 2 sees their own session');
288
- assert.ok(!user2Sessions.some(s => s.sessionId === 'session-A'), 'User 2 cannot see User 1 session');
289
- });
290
-
291
- it('abort rejects when user does not own the session', () => {
292
- const sessionOwners = new Map();
293
- sessionOwners.set('session-X', 1); // owned by user 1
294
-
295
- const userId = 2; // user 2 tries to abort
296
- const owner = sessionOwners.get('session-X');
297
- const canAbort = !owner || owner === userId;
298
-
299
- assert.equal(canAbort, false, 'User 2 cannot abort User 1 session');
300
- });
301
- });
302
-
303
- // ── Test 5: Agent Action Dispatch ──
304
-
305
- describe('Agent Action Dispatch', () => {
306
- it('executeAction routes to correct handler', async () => {
307
- const callLog = [];
308
- const actionMap = new Map([
309
- ['claude-query', async (params) => { callLog.push('claude'); return { exitCode: 0 }; }],
310
- ['cursor-query', async (params) => { callLog.push('cursor'); return { exitCode: 0 }; }],
311
- ['codex-query', async (params) => { callLog.push('codex'); return { exitCode: 0 }; }],
312
- ['shell-exec', async (params) => { callLog.push('shell'); return { output: 'hello' }; }],
313
- ]);
314
-
315
- async function executeAction(action, params) {
316
- const handler = actionMap.get(action);
317
- if (!handler) throw new Error(`Unknown action: ${action}`);
318
- return handler(params);
319
- }
320
-
321
- await executeAction('claude-query', { command: 'hey' });
322
- await executeAction('cursor-query', { command: 'hey' });
323
- await executeAction('shell-exec', { command: 'ls' });
324
-
325
- assert.deepEqual(callLog, ['claude', 'cursor', 'shell']);
326
-
327
- // Unknown action throws
328
- await assert.rejects(
329
- () => executeAction('unknown-action', {}),
330
- { message: 'Unknown action: unknown-action' }
331
- );
332
- });
333
-
334
- it('streaming actions are identified correctly', () => {
335
- const streamingActions = new Set(['claude-query', 'claude-task-query', 'codex-query', 'cursor-query']);
336
-
337
- function isStreamingAction(action) {
338
- return streamingActions.has(action);
339
- }
340
-
341
- assert.equal(isStreamingAction('claude-query'), true);
342
- assert.equal(isStreamingAction('codex-query'), true);
343
- assert.equal(isStreamingAction('cursor-query'), true);
344
- assert.equal(isStreamingAction('shell-exec'), false);
345
- assert.equal(isStreamingAction('file-read'), false);
346
- assert.equal(isStreamingAction('git-status'), false);
347
- });
348
- });
349
-
350
- // ── Test 6: Relay Payload Validation ──
351
-
352
- describe('Relay Payload Validation', () => {
353
- it('validates action is in allowlist', () => {
354
- const ALLOWED_RELAY_ACTIONS = new Set([
355
- 'claude-query', 'claude-task-query', 'cursor-query', 'codex-query',
356
- 'shell-exec', 'file-read', 'file-write', 'file-list',
357
- 'git-status', 'git-diff', 'git-log', 'git-branches',
358
- 'gitagent-parse', 'detect-agents',
359
- ]);
360
-
361
- function validateRelayPayload(action) {
362
- if (!ALLOWED_RELAY_ACTIONS.has(action)) {
363
- return { valid: false, reason: `Action "${action}" is not allowed` };
364
- }
365
- return { valid: true };
366
- }
367
-
368
- assert.equal(validateRelayPayload('claude-query').valid, true);
369
- assert.equal(validateRelayPayload('shell-exec').valid, true);
370
- assert.equal(validateRelayPayload('rm-rf-everything').valid, false, 'Unknown action blocked');
371
- assert.equal(validateRelayPayload('').valid, false, 'Empty action blocked');
372
- });
373
- });
374
-
375
- // ── Test 7: IP Pinning Security ──
376
-
377
- describe('IP Pinning Security', () => {
378
- it('blocks relay connection from different IP when another is active', () => {
379
- const relayConnections = new Map();
380
-
381
- // User 1 connects from IP A
382
- relayConnections.set(1, { ws: { readyState: 1 }, clientIp: '1.2.3.4', connectedAt: Date.now() });
383
-
384
- // Same user tries from IP B
385
- const existing = relayConnections.get(1);
386
- const newIp = '5.6.7.8';
387
- const blocked = existing && existing.ws.readyState === 1 && existing.clientIp !== newIp;
388
-
389
- assert.equal(blocked, true, 'Second connection from different IP is blocked');
390
- });
391
-
392
- it('allows reconnection from same IP', () => {
393
- const relayConnections = new Map();
394
- relayConnections.set(1, { ws: { readyState: 1 }, clientIp: '1.2.3.4' });
395
-
396
- const existing = relayConnections.get(1);
397
- const sameIp = '1.2.3.4';
398
- const blocked = existing && existing.ws.readyState === 1 && existing.clientIp !== sameIp;
399
-
400
- assert.equal(blocked, false, 'Reconnection from same IP is allowed');
401
- });
402
-
403
- it('allows new connection when previous relay is disconnected', () => {
404
- const relayConnections = new Map();
405
- relayConnections.set(1, { ws: { readyState: 3 }, clientIp: '1.2.3.4' }); // disconnected
406
-
407
- const existing = relayConnections.get(1);
408
- const newIp = '5.6.7.8';
409
- const blocked = existing && existing.ws.readyState === 1 && existing.clientIp !== newIp;
410
-
411
- assert.equal(blocked, false, 'New connection allowed when previous relay is disconnected');
412
- });
413
- });
414
-
415
- // ── Test 8: Dashboard Stats Cache ──
416
-
417
- describe('Dashboard Stats Cache', () => {
418
- it('cache returns fresh data within TTL', () => {
419
- const cache = new Map();
420
- const CACHE_TTL = 60_000;
421
-
422
- function getCachedStats(userId) {
423
- const entry = cache.get(userId);
424
- if (entry && Date.now() - entry.ts < CACHE_TTL) return entry.data;
425
- return null;
426
- }
427
-
428
- function setCachedStats(userId, data) {
429
- cache.set(userId, { data, ts: Date.now() });
430
- }
431
-
432
- // Set cache
433
- const testData = { account: { username: 'testuser' } };
434
- setCachedStats(1, testData);
435
-
436
- // Immediately retrieve — should hit
437
- const cached = getCachedStats(1);
438
- assert.deepEqual(cached, testData, 'Cache hit returns data');
439
-
440
- // Different user — should miss
441
- const otherUser = getCachedStats(2);
442
- assert.equal(otherUser, null, 'Different user has no cache');
443
- });
444
-
445
- it('cache invalidation clears entry for specific user', () => {
446
- const cache = new Map();
447
-
448
- cache.set(1, { data: { user: 'A' }, ts: Date.now() });
449
- cache.set(2, { data: { user: 'B' }, ts: Date.now() });
450
-
451
- // Invalidate user 1 only
452
- cache.delete(1);
453
-
454
- assert.equal(cache.has(1), false, 'User 1 cache invalidated');
455
- assert.equal(cache.has(2), true, 'User 2 cache unaffected');
456
- });
457
- });
458
-
459
- // ── Test 9: Frontend SessionStorage Stale-While-Revalidate ──
460
-
461
- describe('Stale-While-Revalidate Pattern', () => {
462
- it('shows cached data immediately then updates with fresh data', () => {
463
- // Simulate sessionStorage
464
- const storage = {};
465
- const setItem = (k, v) => { storage[k] = v; };
466
- const getItem = (k) => storage[k] || null;
467
-
468
- // Initial state — no cache
469
- let stats = null;
470
- let loading = true;
471
-
472
- const cached = getItem('dash_stats');
473
- assert.equal(cached, null, 'No cache initially');
474
- assert.equal(loading, true, 'Loading state true initially');
475
-
476
- // Simulate first load — data comes from server
477
- const serverData = { account: { username: 'user1' }, apiKeys: { keys: [] } };
478
- stats = serverData;
479
- loading = false;
480
- setItem('dash_stats', JSON.stringify(serverData));
481
-
482
- assert.equal(loading, false);
483
- assert.deepEqual(stats, serverData);
484
-
485
- // Simulate second page load — cache hit
486
- stats = null;
487
- loading = true;
488
-
489
- const cachedStr = getItem('dash_stats');
490
- if (cachedStr) {
491
- stats = JSON.parse(cachedStr);
492
- loading = false; // Show cached data immediately
493
- }
494
-
495
- assert.equal(loading, false, 'Loading resolved immediately from cache');
496
- assert.deepEqual(stats.account.username, 'user1', 'Cached data shown immediately');
497
- });
498
- });
499
-
500
- // ── Test 10: Multi-User Chat Isolation (End-to-End) ──
501
-
502
- describe('Multi-User Chat Isolation (E2E)', () => {
503
- it('two users sending commands simultaneously route to correct relays', () => {
504
- const relayConnections = new Map();
505
- const relayReceived = { user1: [], user2: [] };
506
-
507
- relayConnections.set(1, {
508
- ws: { readyState: 1, send: (msg) => relayReceived.user1.push(JSON.parse(msg)) }
509
- });
510
- relayConnections.set(2, {
511
- ws: { readyState: 1, send: (msg) => relayReceived.user2.push(JSON.parse(msg)) }
512
- });
513
-
514
- // User 1 sends a claude-command
515
- function routeToRelay(userId, action, command) {
516
- const relay = relayConnections.get(userId);
517
- if (!relay || relay.ws.readyState !== 1) throw new Error('No relay');
518
- relay.ws.send(JSON.stringify({ type: 'relay-command', action, command }));
519
- }
520
-
521
- routeToRelay(1, 'claude-query', 'Hey, I am user 1');
522
- routeToRelay(2, 'claude-query', 'Hey, I am user 2');
523
- routeToRelay(1, 'claude-query', 'Second message from user 1');
524
-
525
- assert.equal(relayReceived.user1.length, 2, 'User 1 relay received 2 commands');
526
- assert.equal(relayReceived.user2.length, 1, 'User 2 relay received 1 command');
527
- assert.equal(relayReceived.user1[0].command, 'Hey, I am user 1');
528
- assert.equal(relayReceived.user2[0].command, 'Hey, I am user 2');
529
- assert.equal(relayReceived.user1[1].command, 'Second message from user 1');
530
- });
531
-
532
- it('streaming responses go back to the correct frontend client', () => {
533
- // Simulate pending relay requests with different userIds
534
- const pendingRelayRequests = new Map();
535
- const frontendReceived = { user1: [], user2: [] };
536
-
537
- const writer1 = { send: (msg) => frontendReceived.user1.push(msg) };
538
- const writer2 = { send: (msg) => frontendReceived.user2.push(msg) };
539
-
540
- pendingRelayRequests.set('req-1', {
541
- userId: 1,
542
- onStream: (data) => {
543
- if (data.type === 'claude-response') {
544
- writer1.send({ type: 'claude-response', data: { type: 'content_block_delta', delta: { text: data.content } } });
545
- }
546
- }
547
- });
548
- pendingRelayRequests.set('req-2', {
549
- userId: 2,
550
- onStream: (data) => {
551
- if (data.type === 'claude-response') {
552
- writer2.send({ type: 'claude-response', data: { type: 'content_block_delta', delta: { text: data.content } } });
553
- }
554
- }
555
- });
556
-
557
- // Relay stream for user 1
558
- const pending1 = pendingRelayRequests.get('req-1');
559
- pending1.onStream({ type: 'claude-response', content: 'Hello user 1!' });
560
-
561
- // Relay stream for user 2
562
- const pending2 = pendingRelayRequests.get('req-2');
563
- pending2.onStream({ type: 'claude-response', content: 'Hello user 2!' });
564
-
565
- assert.equal(frontendReceived.user1.length, 1);
566
- assert.equal(frontendReceived.user2.length, 1);
567
- assert.equal(frontendReceived.user1[0].data.delta.text, 'Hello user 1!');
568
- assert.equal(frontendReceived.user2[0].data.delta.text, 'Hello user 2!');
569
- });
570
- });