upfynai-code 2.3.0 → 2.4.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 (158) hide show
  1. package/client/dist/assets/AppContent-DTZ2FbvM.js +513 -0
  2. package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +6 -0
  3. package/client/dist/assets/LoginModal-CWoFm0au.js +19 -0
  4. package/client/dist/assets/MarkdownPreview-CYdvwJaV.js +1 -0
  5. package/client/dist/assets/{Onboarding-Coxo6mFA.js → Onboarding-CtIoXiTp.js} +1 -1
  6. package/client/dist/assets/{SetupForm-BzYOsbji.js → SetupForm-B4p8im5O.js} +1 -1
  7. package/client/dist/assets/{ar-SA-G6X2FPQ2-Bmw2-hDt.js → ar-SA-G6X2FPQ2-2gfmdvHk.js} +1 -1
  8. package/client/dist/assets/{arc-BMqY7_Ci.js → arc-DCZSHhoJ.js} +1 -1
  9. package/client/dist/assets/{az-AZ-76LH7QW2-Dh1le_qs.js → az-AZ-76LH7QW2-CDdeucRZ.js} +1 -1
  10. package/client/dist/assets/{bg-BG-XCXSNQG7-Cbav8Z9z.js → bg-BG-XCXSNQG7-D6__XtOK.js} +1 -1
  11. package/client/dist/assets/{blockDiagram-38ab4fdb-ChHJxsXw.js → blockDiagram-38ab4fdb-Cfbaeyp6.js} +3 -3
  12. package/client/dist/assets/{bn-BD-2XOGV67Q-DCNjOaWz.js → bn-BD-2XOGV67Q-DHNJw3OG.js} +1 -1
  13. package/client/dist/assets/{c4Diagram-3d4e48cf-b8Xue4Z6.js → c4Diagram-3d4e48cf-BBCnjOTy.js} +1 -1
  14. package/client/dist/assets/{ca-ES-6MX7JW3Y-Dl_vM7NS.js → ca-ES-6MX7JW3Y-r5g4o3zQ.js} +1 -1
  15. package/client/dist/assets/channel-O3ovC0x9.js +1 -0
  16. package/client/dist/assets/{classDiagram-70f12bd4-BheP7Ggo.js → classDiagram-70f12bd4-D0lhAcxU.js} +1 -1
  17. package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +2 -0
  18. package/client/dist/assets/clone-BG9u7vLi.js +1 -0
  19. package/client/dist/assets/{createText-2e5e7dd3-_n4jI_fO.js → createText-2e5e7dd3-B8jCDmF_.js} +1 -1
  20. package/client/dist/assets/{cs-CZ-2BRQDIVT-ftsKDdz4.js → cs-CZ-2BRQDIVT-p08jRLRC.js} +1 -1
  21. package/client/dist/assets/{da-DK-5WZEPLOC-DAjdwGRO.js → da-DK-5WZEPLOC-CnhOImFf.js} +1 -1
  22. package/client/dist/assets/{de-DE-XR44H4JA-BJXczHGT.js → de-DE-XR44H4JA-BunSXZ-Y.js} +1 -1
  23. package/client/dist/assets/{edges-e0da2a9e-CfPZr4YM.js → edges-e0da2a9e-CGBBhG8k.js} +2 -2
  24. package/client/dist/assets/{el-GR-BZB4AONW-DW2p_uy7.js → el-GR-BZB4AONW-D4wv1oIz.js} +1 -1
  25. package/client/dist/assets/{erDiagram-9861fffd-CF33V-Of.js → erDiagram-9861fffd-CYaF3q1I.js} +1 -1
  26. package/client/dist/assets/{es-ES-U4NZUMDT-DLOIGnrl.js → es-ES-U4NZUMDT-CGeTKXgd.js} +1 -1
  27. package/client/dist/assets/{eu-ES-A7QVB2H4-LJXbf89m.js → eu-ES-A7QVB2H4-Cayx1TxR.js} +1 -1
  28. package/client/dist/assets/{fa-IR-HGAKTJCU-Dvx65fgW.js → fa-IR-HGAKTJCU-CmUg8pmw.js} +1 -1
  29. package/client/dist/assets/{fi-FI-Z5N7JZ37-EoL65BQh.js → fi-FI-Z5N7JZ37-xvHcPhsU.js} +1 -1
  30. package/client/dist/assets/{flowDb-956e92f1-HgoXVy2H.js → flowDb-956e92f1-C-_LFz70.js} +3 -3
  31. package/client/dist/assets/flowDiagram-66a62f08-C1sHdSjn.js +4 -0
  32. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +1 -0
  33. package/client/dist/assets/{flowchart-elk-definition-4a651766-DJbI2dpv.js → flowchart-elk-definition-4a651766-CNGfpudb.js} +7 -7
  34. package/client/dist/assets/{fr-FR-RHASNOE6-DNk_jdDs.js → fr-FR-RHASNOE6-DBoHEcNj.js} +1 -1
  35. package/client/dist/assets/{ganttDiagram-c361ad54-2XX670FU.js → ganttDiagram-c361ad54-B8HJQqjt.js} +1 -1
  36. package/client/dist/assets/{gitGraphDiagram-72cf32ee-CcUfruAo.js → gitGraphDiagram-72cf32ee-DojCDvlS.js} +1 -1
  37. package/client/dist/assets/{gl-ES-HMX3MZ6V-dxzFjZlG.js → gl-ES-HMX3MZ6V-p6hrn2cN.js} +1 -1
  38. package/client/dist/assets/{graph-BSbiMSBC.js → graph-DXM7lcy1.js} +1 -1
  39. package/client/dist/assets/{he-IL-6SHJWFNN-Cogsfdt1.js → he-IL-6SHJWFNN-y2jEX6-0.js} +1 -1
  40. package/client/dist/assets/{hi-IN-IWLTKZ5I-L6wbgi4F.js → hi-IN-IWLTKZ5I-99pNfyWr.js} +1 -1
  41. package/client/dist/assets/{hu-HU-A5ZG7DT2-DSA6ZDsH.js → hu-HU-A5ZG7DT2-hygceGMS.js} +1 -1
  42. package/client/dist/assets/{id-ID-SAP4L64H-BK_vGGS6.js → id-ID-SAP4L64H-CyIqi1hv.js} +1 -1
  43. package/client/dist/assets/{image-blob-reduce.esm-BLtmMM_J.js → image-blob-reduce.esm-D6s-rqMO.js} +6 -1
  44. package/client/dist/assets/{index-3862675e-Bv32HUgT.js → index-3862675e-4idOQN2N.js} +1 -1
  45. package/client/dist/assets/{index-BPwf8Fw3.js → index-BGmwbRlb.js} +6 -6
  46. package/client/dist/assets/index-BHZfFT_V.js +97 -0
  47. package/client/dist/assets/{infoDiagram-f8f76790-w4mR4pxn.js → infoDiagram-f8f76790-CFLrHqtc.js} +1 -1
  48. package/client/dist/assets/{it-IT-JPQ66NNP-BLdHYMhn.js → it-IT-JPQ66NNP-DzVvVdQI.js} +1 -1
  49. package/client/dist/assets/{ja-JP-DBVTYXUO-B_vmexl_.js → ja-JP-DBVTYXUO-BI4fPexV.js} +1 -1
  50. package/client/dist/assets/{journeyDiagram-49397b02-D9nmO17e.js → journeyDiagram-49397b02-C3CFDo8z.js} +1 -1
  51. package/client/dist/assets/{kaa-6HZHGXH3-5s-3jl6F.js → kaa-6HZHGXH3-fwOleoQB.js} +1 -1
  52. package/client/dist/assets/{kab-KAB-ZGHBKWFO-2QaVDuSf.js → kab-KAB-ZGHBKWFO-DBI_ri48.js} +1 -1
  53. package/client/dist/assets/{kk-KZ-P5N5QNE5-CTC52Vbi.js → kk-KZ-P5N5QNE5-zpl7uvyF.js} +1 -1
  54. package/client/dist/assets/{km-KH-HSX4SM5Z-DxawH8UZ.js → km-KH-HSX4SM5Z-DOMFSres.js} +1 -1
  55. package/client/dist/assets/{ko-KR-MTYHY66A-CmosEM8_.js → ko-KR-MTYHY66A-tb08hXzd.js} +1 -1
  56. package/client/dist/assets/{ku-TR-6OUDTVRD-DbiLen4y.js → ku-TR-6OUDTVRD-DlIQCCY4.js} +1 -1
  57. package/client/dist/assets/{layout-jmt3H9tA.js → layout-B_11mCXA.js} +1 -1
  58. package/client/dist/assets/{line-JTlRayUJ.js → line-B-qmK_vI.js} +1 -1
  59. package/client/dist/assets/{linear-DJeB5p7x.js → linear-Ph6uuYcX.js} +1 -1
  60. package/client/dist/assets/{lt-LT-XHIRWOB4-CH15wrjA.js → lt-LT-XHIRWOB4--qWy24_Z.js} +1 -1
  61. package/client/dist/assets/{lv-LV-5QDEKY6T-dhgfPuCQ.js → lv-LV-5QDEKY6T-Bnd_1GDb.js} +1 -1
  62. package/client/dist/assets/mindmap-definition-fc14e90a-Do79tIc0.js +425 -0
  63. package/client/dist/assets/{mr-IN-CRQNXWMA-3Gi6iq7A.js → mr-IN-CRQNXWMA-BsV6HaD9.js} +1 -1
  64. package/client/dist/assets/{my-MM-5M5IBNSE-CpH4rdJj.js → my-MM-5M5IBNSE-kZQURVIi.js} +1 -1
  65. package/client/dist/assets/{nb-NO-T6EIAALU-Du6iiGql.js → nb-NO-T6EIAALU-Cvf9FdSF.js} +1 -1
  66. package/client/dist/assets/{nl-NL-IS3SIHDZ-BGvsd1MT.js → nl-NL-IS3SIHDZ-DA1yqpXw.js} +1 -1
  67. package/client/dist/assets/{nn-NO-6E72VCQL-B-odvJZW.js → nn-NO-6E72VCQL-89lm3vku.js} +1 -1
  68. package/client/dist/assets/{oc-FR-POXYY2M6-COC8xNjo.js → oc-FR-POXYY2M6-BsrjTJQh.js} +1 -1
  69. package/client/dist/assets/{pa-IN-N4M65BXN-CE21PUQH.js → pa-IN-N4M65BXN-CczefYaj.js} +1 -1
  70. package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
  71. package/client/dist/assets/percentages-BXMCSKIN-Be6p9phi.js +207 -0
  72. package/client/dist/assets/pica-CQIY57Tf.js +7 -0
  73. package/client/dist/assets/{pieDiagram-8a3498a8-Cvfh7Qr5.js → pieDiagram-8a3498a8-CfblQHdm.js} +2 -2
  74. package/client/dist/assets/{pl-PL-T2D74RX3-D4xFVSoT.js → pl-PL-T2D74RX3-DdhH-zcK.js} +1 -1
  75. package/client/dist/assets/{pt-BR-5N22H2LF-CCq257gA.js → pt-BR-5N22H2LF-gpwlheL6.js} +1 -1
  76. package/client/dist/assets/{pt-PT-UZXXM6DQ-1l8gt5vA.js → pt-PT-UZXXM6DQ-Cs87vICi.js} +1 -1
  77. package/client/dist/assets/{quadrantDiagram-120e2f19-BA0js1aD.js → quadrantDiagram-120e2f19-CRMSamSP.js} +1 -1
  78. package/client/dist/assets/{requirementDiagram-deff3bca-B0QNFfIn.js → requirementDiagram-deff3bca-D3LBN016.js} +1 -1
  79. package/client/dist/assets/{ro-RO-JPDTUUEW-yosBW01E.js → ro-RO-JPDTUUEW-CWTSJ1Dt.js} +1 -1
  80. package/client/dist/assets/roundRect-0PYZxl1G.js +1 -0
  81. package/client/dist/assets/{ru-RU-B4JR7IUQ-8LkEJUix.js → ru-RU-B4JR7IUQ-Bq7aN2ep.js} +1 -1
  82. package/client/dist/assets/{sankeyDiagram-04a897e0-D4T9eCXn.js → sankeyDiagram-04a897e0-CsFqOQZN.js} +3 -3
  83. package/client/dist/assets/{sequenceDiagram-704730f1-CfBUTCrO.js → sequenceDiagram-704730f1-BRYXVDGX.js} +1 -1
  84. package/client/dist/assets/{si-LK-N5RQ5JYF-D8rjbqtd.js → si-LK-N5RQ5JYF-BBjcNYQh.js} +1 -1
  85. package/client/dist/assets/{sk-SK-C5VTKIMK-Bg14sAzN.js → sk-SK-C5VTKIMK-ByjKQzUb.js} +1 -1
  86. package/client/dist/assets/{sl-SI-NN7IZMDC-CMTib6Zs.js → sl-SI-NN7IZMDC-B8WCyMBU.js} +1 -1
  87. package/client/dist/assets/{stateDiagram-587899a1-BGgvmVSZ.js → stateDiagram-587899a1-BHoy9LtD.js} +1 -1
  88. package/client/dist/assets/{stateDiagram-v2-d93cdb3a-Qn3DpYuO.js → stateDiagram-v2-d93cdb3a-BvMUA6bS.js} +1 -1
  89. package/client/dist/assets/{styles-6aaf32cf-IdVZLPrD.js → styles-6aaf32cf-Dr-lfIOW.js} +2 -2
  90. package/client/dist/assets/{styles-9a916d00-BAC3L45X.js → styles-9a916d00-DS4wRpL7.js} +1 -1
  91. package/client/dist/assets/{styles-c10674c1-COhXxX8c.js → styles-c10674c1-nKRF6NrH.js} +1 -1
  92. package/client/dist/assets/{subset-shared.chunk-BWHnFai4.js → subset-shared.chunk-KT79s7KG.js} +64 -2
  93. package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +1 -0
  94. package/client/dist/assets/{sv-SE-XGPEYMSR-C1425rOF.js → sv-SE-XGPEYMSR-BiIPUVbv.js} +1 -1
  95. package/client/dist/assets/{svgDrawCommon-08f97a94-Cfk-fgnN.js → svgDrawCommon-08f97a94-C3uP9PYr.js} +1 -1
  96. package/client/dist/assets/{ta-IN-2NMHFXQM-BHHo1zpF.js → ta-IN-2NMHFXQM-Cidadso2.js} +1 -1
  97. package/client/dist/assets/{th-TH-HPSO5L25-CZVzm_WT.js → th-TH-HPSO5L25-CFNnJwSv.js} +1 -1
  98. package/client/dist/assets/{timeline-definition-85554ec2-VAvuJith.js → timeline-definition-85554ec2-BSsLsIgF.js} +1 -1
  99. package/client/dist/assets/{tr-TR-DEFEU3FU-DE1lclCq.js → tr-TR-DEFEU3FU-DaFcI-KL.js} +1 -1
  100. package/client/dist/assets/{uk-UA-QMV73CPH-D4lJZ85O.js → uk-UA-QMV73CPH-DkBW36St.js} +1 -1
  101. package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +20 -0
  102. package/client/dist/assets/vendor-codemirror-rix45NST.js +16 -0
  103. package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
  104. package/client/dist/assets/vendor-icons-Dh9m_Ydt.js +596 -0
  105. package/client/dist/assets/{vendor-markdown-CIVH08vJ.js → vendor-markdown-BXEi_H3G.js} +3 -3
  106. package/client/dist/assets/vendor-react-9mUTKBHH.js +67 -0
  107. package/client/dist/assets/{vendor-syntax-Djb62v3a.js → vendor-syntax-DnmwQQJF.js} +14 -7
  108. package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
  109. package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
  110. package/client/dist/assets/{vi-VN-M7AON7JQ-Dgc_SShk.js → vi-VN-M7AON7JQ-KrtfxOzl.js} +1 -1
  111. package/client/dist/assets/{xychartDiagram-e933f94c-BeyVBJhb.js → xychartDiagram-e933f94c-CgNgZ4pp.js} +1 -1
  112. package/client/dist/assets/{zh-CN-LNUGB5OW-MH4Yh8in.js → zh-CN-LNUGB5OW-BQu12RoD.js} +1 -1
  113. package/client/dist/assets/{zh-HK-E62DVLB3-D4XHehjx.js → zh-HK-E62DVLB3-zx9CvERq.js} +1 -1
  114. package/client/dist/assets/{zh-TW-RAJ6MFWO--efj3evj.js → zh-TW-RAJ6MFWO-ffJWgVxn.js} +1 -1
  115. package/client/dist/index.html +128 -66
  116. package/client/dist/manifest.json +61 -15
  117. package/client/dist/sw.js +19 -55
  118. package/commands/upfynai-connect.md +31 -18
  119. package/commands/upfynai.md +45 -26
  120. package/package.json +1 -1
  121. package/server/cli-ui.js +320 -169
  122. package/server/cli.js +194 -21
  123. package/server/index.js +159 -107
  124. package/server/middleware/auth.js +9 -5
  125. package/server/openrouter.js +137 -0
  126. package/server/relay-client.js +104 -17
  127. package/server/routes/agent.js +54 -19
  128. package/server/routes/auth.js +23 -12
  129. package/server/routes/settings.js +91 -0
  130. package/shared/modelConstants.js +29 -0
  131. package/client/dist/assets/AppContent-CTSHQdyq.js +0 -513
  132. package/client/dist/assets/CanvasPanel-Cig0Mo9s.js +0 -6
  133. package/client/dist/assets/LoginModal-silya-zP.js +0 -11
  134. package/client/dist/assets/MarkdownPreview-B3c7OEj6.js +0 -1
  135. package/client/dist/assets/channel-CSnvHe_M.js +0 -1
  136. package/client/dist/assets/classDiagram-v2-f2320105-xtym7GEZ.js +0 -2
  137. package/client/dist/assets/clone-B75abXxS.js +0 -1
  138. package/client/dist/assets/flowDiagram-66a62f08-tffoET0H.js +0 -4
  139. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Byc3JCHh.js +0 -1
  140. package/client/dist/assets/index-BnXuHrpJ.js +0 -523
  141. package/client/dist/assets/index-BwxNox94.css +0 -1
  142. package/client/dist/assets/index-D1urGMYu.js +0 -95
  143. package/client/dist/assets/mindmap-definition-fc14e90a-BOOrexmz.js +0 -415
  144. package/client/dist/assets/pdf-TYrZqVzP.js +0 -12
  145. package/client/dist/assets/percentages-BXMCSKIN-C9GT0OD3.js +0 -199
  146. package/client/dist/assets/pica-VkdyTzi8.js +0 -2
  147. package/client/dist/assets/roundRect-mAH3dD0p.js +0 -1
  148. package/client/dist/assets/subset-worker.chunk-C8QUSruZ.js +0 -1
  149. package/client/dist/assets/vendor-codemirror-BARtJV1V.js +0 -16
  150. package/client/dist/assets/vendor-codemirror-langs-52_y1wip.js +0 -20
  151. package/client/dist/assets/vendor-i18n-ByAl-gdx.js +0 -1
  152. package/client/dist/assets/vendor-icons-D33IkSIf.js +0 -1
  153. package/client/dist/assets/vendor-react-CHoMc7ka.js +0 -8
  154. package/client/dist/assets/vendor-xterm-DBb3RXlu.js +0 -66
  155. package/client/dist/assets/vendor-xterm-DrlLKa8f.css +0 -1
  156. package/client/dist/llms.txt +0 -40
  157. package/client/dist/robots.txt +0 -11
  158. package/client/dist/sitemap.xml +0 -45
package/server/index.js CHANGED
@@ -54,6 +54,7 @@ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSess
54
54
  import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
55
55
  import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
56
56
  import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
57
+ import { queryOpenRouter, OPENROUTER_MODELS } from './openrouter.js';
57
58
  import { createMcpServer, mountMcpServer } from './mcp-server.js';
58
59
  import gitRoutes from './routes/git.js';
59
60
  import authRoutes from './routes/auth.js';
@@ -69,7 +70,7 @@ import cliAuthRoutes from './routes/cli-auth.js';
69
70
  import userRoutes from './routes/user.js';
70
71
  import codexRoutes from './routes/codex.js';
71
72
  import paymentRoutes from './routes/payments.js';
72
- import { initializeDatabase, relayTokensDb, subscriptionDb } from './database/db.js';
73
+ import { initializeDatabase, relayTokensDb, subscriptionDb, credentialsDb } from './database/db.js';
73
74
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
74
75
  import { IS_PLATFORM } from './constants/config.js';
75
76
 
@@ -348,6 +349,18 @@ if (server) {
348
349
  // Make WebSocket server available to routes
349
350
  app.locals.wss = wss;
350
351
 
352
+ // Security headers — protect against common web attacks
353
+ app.use((req, res, next) => {
354
+ res.setHeader('X-Content-Type-Options', 'nosniff');
355
+ res.setHeader('X-Frame-Options', 'DENY');
356
+ res.setHeader('X-XSS-Protection', '1; mode=block');
357
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
358
+ if (process.env.NODE_ENV === 'production') {
359
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
360
+ }
361
+ next();
362
+ });
363
+
351
364
  // CORS: require explicit CORS_ORIGINS in production, restrict to same-origin otherwise
352
365
  const CORS_ORIGINS = process.env.CORS_ORIGINS
353
366
  ? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
@@ -367,6 +380,44 @@ app.use(express.json({
367
380
  }));
368
381
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
369
382
 
383
+ // Rate limiting for auth endpoints — prevent brute force attacks
384
+ const authRateLimitMap = new Map();
385
+ const AUTH_RATE_WINDOW = 15 * 60 * 1000; // 15 minutes
386
+ const AUTH_RATE_MAX = 10; // max attempts per window
387
+
388
+ function authRateLimit(req, res, next) {
389
+ const key = req.ip || req.connection.remoteAddress || 'unknown';
390
+ const now = Date.now();
391
+ const entry = authRateLimitMap.get(key);
392
+
393
+ if (entry) {
394
+ // Clean expired entries
395
+ if (now - entry.windowStart > AUTH_RATE_WINDOW) {
396
+ authRateLimitMap.set(key, { windowStart: now, count: 1 });
397
+ return next();
398
+ }
399
+ entry.count++;
400
+ if (entry.count > AUTH_RATE_MAX) {
401
+ return res.status(429).json({ error: 'Too many attempts. Please try again later.' });
402
+ }
403
+ } else {
404
+ authRateLimitMap.set(key, { windowStart: now, count: 1 });
405
+ }
406
+ next();
407
+ }
408
+
409
+ // Clean up stale rate limit entries every 30 minutes
410
+ setInterval(() => {
411
+ const now = Date.now();
412
+ for (const [key, entry] of authRateLimitMap) {
413
+ if (now - entry.windowStart > AUTH_RATE_WINDOW) {
414
+ authRateLimitMap.delete(key);
415
+ }
416
+ }
417
+ }, 30 * 60 * 1000);
418
+
419
+ app.locals.authRateLimit = authRateLimit;
420
+
370
421
  // Vercel serverless: lazy DB initialization on first request
371
422
  let dbInitialized = false;
372
423
  if (process.env.VERCEL) {
@@ -520,77 +571,15 @@ app.use(express.static(path.join(__dirname, '../client/dist'), {
520
571
  // /api/config endpoint removed - no longer needed
521
572
  // Frontend now uses window.location for WebSocket URLs
522
573
 
523
- // System update endpoint
524
- app.post('/api/system/update', authenticateToken, async (req, res) => {
525
- try {
526
- // Get the project root directory (parent of server directory)
527
- const projectRoot = path.join(__dirname, '..');
528
-
529
- console.log('Starting system update from directory:', projectRoot);
530
-
531
- // Run the update command
532
- const updateCommand = 'git checkout main && git pull && npm install';
533
-
534
- const child = spawn('sh', ['-c', updateCommand], {
535
- cwd: projectRoot,
536
- env: process.env
537
- });
538
-
539
- let output = '';
540
- let errorOutput = '';
541
-
542
- child.stdout.on('data', (data) => {
543
- const text = data.toString();
544
- output += text;
545
- console.log('Update output:', text);
546
- });
547
-
548
- child.stderr.on('data', (data) => {
549
- const text = data.toString();
550
- errorOutput += text;
551
- console.error('Update error:', text);
552
- });
553
-
554
- child.on('close', (code) => {
555
- if (code === 0) {
556
- res.json({
557
- success: true,
558
- output: output || 'Update completed successfully',
559
- message: 'Update completed. Please restart the server to apply changes.'
560
- });
561
- } else {
562
- res.status(500).json({
563
- success: false,
564
- error: 'Update command failed',
565
- output: output,
566
- errorOutput: errorOutput
567
- });
568
- }
569
- });
570
-
571
- child.on('error', (error) => {
572
- console.error('Update process error:', error);
573
- res.status(500).json({
574
- success: false,
575
- error: error.message
576
- });
577
- });
578
-
579
- } catch (error) {
580
- console.error('System update error:', error);
581
- res.status(500).json({
582
- success: false,
583
- error: error.message
584
- });
585
- }
586
- });
574
+ // System update endpoint — REMOVED for security (shell command execution risk)
575
+ // Use `uc update` from CLI instead
587
576
 
588
577
  app.get('/api/projects', authenticateToken, async (req, res) => {
589
578
  try {
590
579
  const projects = await getProjects(broadcastProgress);
591
580
  res.json(projects);
592
581
  } catch (error) {
593
- res.status(500).json({ error: error.message });
582
+ res.status(500).json({ error: 'Internal server error' });
594
583
  }
595
584
  });
596
585
 
@@ -600,7 +589,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
600
589
  const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
601
590
  res.json(result);
602
591
  } catch (error) {
603
- res.status(500).json({ error: error.message });
592
+ res.status(500).json({ error: 'Internal server error' });
604
593
  }
605
594
  });
606
595
 
@@ -625,7 +614,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateT
625
614
  res.json(result);
626
615
  }
627
616
  } catch (error) {
628
- res.status(500).json({ error: error.message });
617
+ res.status(500).json({ error: 'Internal server error' });
629
618
  }
630
619
  });
631
620
 
@@ -636,7 +625,7 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
636
625
  await renameProject(req.params.projectName, displayName);
637
626
  res.json({ success: true });
638
627
  } catch (error) {
639
- res.status(500).json({ error: error.message });
628
+ res.status(500).json({ error: 'Internal server error' });
640
629
  }
641
630
  });
642
631
 
@@ -650,7 +639,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
650
639
  res.json({ success: true });
651
640
  } catch (error) {
652
641
  // session delete error
653
- res.status(500).json({ error: error.message });
642
+ res.status(500).json({ error: 'Internal server error' });
654
643
  }
655
644
  });
656
645
 
@@ -662,7 +651,7 @@ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) =>
662
651
  await deleteProject(projectName, force);
663
652
  res.json({ success: true });
664
653
  } catch (error) {
665
- res.status(500).json({ error: error.message });
654
+ res.status(500).json({ error: 'Internal server error' });
666
655
  }
667
656
  });
668
657
 
@@ -679,7 +668,7 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => {
679
668
  res.json({ success: true, project });
680
669
  } catch (error) {
681
670
  console.error('Error creating project:', error);
682
- res.status(500).json({ error: error.message });
671
+ res.status(500).json({ error: 'Internal server error' });
683
672
  }
684
673
  });
685
674
 
@@ -832,13 +821,11 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
832
821
  return res.status(404).json({ error: 'Project not found' });
833
822
  }
834
823
 
835
- // Handle both absolute and relative paths
836
- const resolved = path.isAbsolute(filePath)
837
- ? path.resolve(filePath)
838
- : path.resolve(projectRoot, filePath);
824
+ // Always resolve relative to project root to prevent path traversal
825
+ const resolved = path.resolve(projectRoot, filePath);
839
826
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
840
- if (!resolved.startsWith(normalizedRoot)) {
841
- return res.status(403).json({ error: 'Path must be under project root' });
827
+ if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
828
+ return res.status(403).json({ error: 'Access denied' });
842
829
  }
843
830
 
844
831
  const content = await fsPromises.readFile(resolved, 'utf8');
@@ -850,7 +837,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
850
837
  } else if (error.code === 'EACCES') {
851
838
  res.status(403).json({ error: 'Permission denied' });
852
839
  } else {
853
- res.status(500).json({ error: error.message });
840
+ res.status(500).json({ error: 'Internal server error' });
854
841
  }
855
842
  }
856
843
  });
@@ -872,10 +859,11 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
872
859
  return res.status(404).json({ error: 'Project not found' });
873
860
  }
874
861
 
875
- const resolved = path.resolve(filePath);
862
+ // Resolve path relative to project root to prevent path traversal
863
+ const resolved = path.resolve(projectRoot, filePath);
876
864
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
877
- if (!resolved.startsWith(normalizedRoot)) {
878
- return res.status(403).json({ error: 'Path must be under project root' });
865
+ if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
866
+ return res.status(403).json({ error: 'Access denied' });
879
867
  }
880
868
 
881
869
  // Check if file exists
@@ -903,7 +891,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
903
891
  } catch (error) {
904
892
  console.error('Error serving binary file:', error);
905
893
  if (!res.headersSent) {
906
- res.status(500).json({ error: error.message });
894
+ res.status(500).json({ error: 'Internal server error' });
907
895
  }
908
896
  }
909
897
  });
@@ -929,13 +917,11 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
929
917
  return res.status(404).json({ error: 'Project not found' });
930
918
  }
931
919
 
932
- // Handle both absolute and relative paths
933
- const resolved = path.isAbsolute(filePath)
934
- ? path.resolve(filePath)
935
- : path.resolve(projectRoot, filePath);
920
+ // Always resolve relative to project root to prevent path traversal
921
+ const resolved = path.resolve(projectRoot, filePath);
936
922
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
937
- if (!resolved.startsWith(normalizedRoot)) {
938
- return res.status(403).json({ error: 'Path must be under project root' });
923
+ if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
924
+ return res.status(403).json({ error: 'Access denied' });
939
925
  }
940
926
 
941
927
  // Write the new content
@@ -953,7 +939,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
953
939
  } else if (error.code === 'EACCES') {
954
940
  res.status(403).json({ error: 'Permission denied' });
955
941
  } else {
956
- res.status(500).json({ error: error.message });
942
+ res.status(500).json({ error: 'Internal server error' });
957
943
  }
958
944
  }
959
945
  });
@@ -985,7 +971,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
985
971
  res.json(files);
986
972
  } catch (error) {
987
973
  console.error('[ERROR] File tree error:', error.message);
988
- res.status(500).json({ error: error.message });
974
+ res.status(500).json({ error: 'Internal server error' });
989
975
  }
990
976
  });
991
977
 
@@ -1001,9 +987,9 @@ if (wss) wss.on('connection', (ws, request) => {
1001
987
  if (pathname === '/shell') {
1002
988
  handleShellConnection(ws);
1003
989
  } else if (pathname === '/ws') {
1004
- handleChatConnection(ws);
990
+ handleChatConnection(ws, request);
1005
991
  } else if (pathname === '/relay') {
1006
- handleRelayConnection(ws, urlObj.searchParams.get('token'));
992
+ handleRelayConnection(ws, urlObj.searchParams.get('token'), request);
1007
993
  } else {
1008
994
  // unknown WebSocket path
1009
995
  ws.close();
@@ -1036,9 +1022,44 @@ class WebSocketWriter {
1036
1022
  }
1037
1023
  }
1038
1024
 
1025
+ /**
1026
+ * Look up a user's stored API key for a given provider.
1027
+ * Falls back to server env vars if user has none stored.
1028
+ * @param {number} userId
1029
+ * @param {string} providerType - e.g. 'anthropic_key', 'openai_key', 'openrouter_key', 'google_key'
1030
+ * @returns {Promise<string|null>}
1031
+ */
1032
+ async function getUserProviderKey(userId, providerType) {
1033
+ if (!userId) return null;
1034
+ try {
1035
+ const creds = await credentialsDb.getCredentials(userId, providerType);
1036
+ const active = creds.find(c => c.is_active);
1037
+ return active?.credential_value || null;
1038
+ } catch { return null; }
1039
+ }
1040
+
1041
+ /**
1042
+ * Temporarily set environment variable for an AI SDK call, then restore.
1043
+ * @param {string} envKey - e.g. 'ANTHROPIC_API_KEY'
1044
+ * @param {string|null} userKey - user's BYOK key, null to skip
1045
+ * @param {Function} fn - async function to execute with the key set
1046
+ */
1047
+ async function withUserApiKey(envKey, userKey, fn) {
1048
+ if (!userKey) return fn();
1049
+ const prev = process.env[envKey];
1050
+ process.env[envKey] = userKey;
1051
+ try {
1052
+ return await fn();
1053
+ } finally {
1054
+ if (prev !== undefined) process.env[envKey] = prev;
1055
+ else delete process.env[envKey];
1056
+ }
1057
+ }
1058
+
1039
1059
  // Handle chat WebSocket connections
1040
- function handleChatConnection(ws) {
1060
+ function handleChatConnection(ws, request) {
1041
1061
  // chat WebSocket connected
1062
+ const wsUser = request?.user || null;
1042
1063
 
1043
1064
  // Add to connected clients for project updates
1044
1065
  connectedClients.add(ws);
@@ -1109,22 +1130,46 @@ function handleChatConnection(ws) {
1109
1130
 
1110
1131
  console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
1111
1132
  console.log('📁 Project:', data.options?.projectPath || 'Unknown');
1112
- // session message received
1113
1133
 
1114
- // Use Claude Agents SDK
1115
- await queryClaudeSDK(data.command, data.options, writer);
1134
+ // BYOK: look up user's Anthropic API key, inject if available
1135
+ const userAnthropicKey = wsUser?.userId
1136
+ ? await getUserProviderKey(wsUser.userId, 'anthropic_key')
1137
+ : null;
1138
+
1139
+ await withUserApiKey('ANTHROPIC_API_KEY', userAnthropicKey, () =>
1140
+ queryClaudeSDK(data.command, data.options, writer)
1141
+ );
1116
1142
  } else if (data.type === 'cursor-command') {
1117
1143
  console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
1118
1144
  console.log('📁 Project:', data.options?.cwd || 'Unknown');
1119
- // session message received
1120
1145
  console.log('🤖 Model:', data.options?.model || 'default');
1121
1146
  await spawnCursor(data.command, data.options, writer);
1122
1147
  } else if (data.type === 'codex-command') {
1123
1148
  console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
1124
1149
  console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1125
- // session message received
1126
1150
  console.log('🤖 Model:', data.options?.model || 'default');
1127
- await queryCodex(data.command, data.options, writer);
1151
+
1152
+ // BYOK: look up user's OpenAI API key, inject if available
1153
+ const userOpenaiKey = wsUser?.userId
1154
+ ? await getUserProviderKey(wsUser.userId, 'openai_key')
1155
+ : null;
1156
+
1157
+ await withUserApiKey('OPENAI_API_KEY', userOpenaiKey, () =>
1158
+ queryCodex(data.command, data.options, writer)
1159
+ );
1160
+ } else if (data.type === 'openrouter-command') {
1161
+ console.log('[DEBUG] OpenRouter message:', data.command?.slice(0, 60) || '[empty]');
1162
+ console.log('🤖 Model:', data.options?.model || OPENROUTER_MODELS.DEFAULT);
1163
+
1164
+ // BYOK: OpenRouter requires user's own API key
1165
+ const userOrKey = wsUser?.userId
1166
+ ? await getUserProviderKey(wsUser.userId, 'openrouter_key')
1167
+ : null;
1168
+
1169
+ await queryOpenRouter(data.command, {
1170
+ ...data.options,
1171
+ apiKey: userOrKey,
1172
+ }, writer);
1128
1173
  } else if (data.type === 'cursor-resume') {
1129
1174
  // Backward compatibility: treat as cursor-command with resume and no prompt
1130
1175
  // cursor resume session
@@ -1226,7 +1271,7 @@ function handleChatConnection(ws) {
1226
1271
  }
1227
1272
 
1228
1273
  // Handle relay WebSocket connections (local machine ↔ server bridge)
1229
- async function handleRelayConnection(ws, token) {
1274
+ async function handleRelayConnection(ws, token, request) {
1230
1275
  if (!token) {
1231
1276
  ws.send(JSON.stringify({ type: 'error', error: 'Relay token required. Use ?token=upfyn_xxx' }));
1232
1277
  ws.close();
@@ -1243,9 +1288,12 @@ async function handleRelayConnection(ws, token) {
1243
1288
  const userId = Number(tokenData.user_id);
1244
1289
  const username = tokenData.username;
1245
1290
 
1246
- // Store relay connection (use Number() to ensure consistent Map key type)
1247
- relayConnections.set(userId, { ws, user: tokenData, connectedAt: Date.now() });
1248
- // relay connection established
1291
+ // Extract optional Anthropic API key from relay handshake headers
1292
+ const anthropicApiKey = request?.headers?.['x-anthropic-api-key'] || null;
1293
+
1294
+ // Store relay connection with API key in memory only (use Number() for consistent Map key type)
1295
+ // API key is held per-user in the relay connection, NOT in process.env
1296
+ relayConnections.set(userId, { ws, user: tokenData, connectedAt: Date.now(), anthropicApiKey });
1249
1297
 
1250
1298
  ws.send(JSON.stringify({
1251
1299
  type: 'relay-connected',
@@ -1707,9 +1755,13 @@ app.post('/api/transcribe', authenticateToken, async (req, res) => {
1707
1755
  return res.status(400).json({ error: 'No audio file provided' });
1708
1756
  }
1709
1757
 
1710
- const apiKey = process.env.OPENAI_API_KEY;
1758
+ // BYOK: check user's stored OpenAI key first, fall back to server env
1759
+ const userOpenaiKey = req.user?.id
1760
+ ? await getUserProviderKey(req.user.id, 'openai_key')
1761
+ : null;
1762
+ const apiKey = userOpenaiKey || process.env.OPENAI_API_KEY;
1711
1763
  if (!apiKey) {
1712
- return res.status(500).json({ error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in server environment.' });
1764
+ return res.status(500).json({ error: 'OpenAI API key not configured. Add your key in Settings > AI Providers, or ask the admin to set OPENAI_API_KEY.' });
1713
1765
  }
1714
1766
 
1715
1767
  try {
@@ -1831,7 +1883,7 @@ Agent instructions:`;
1831
1883
 
1832
1884
  } catch (error) {
1833
1885
  console.error('Transcription error:', error);
1834
- res.status(500).json({ error: error.message });
1886
+ res.status(500).json({ error: 'Internal server error' });
1835
1887
  }
1836
1888
  });
1837
1889
  } catch (error) {
@@ -18,17 +18,21 @@ const validateApiKey = (req, res, next) => {
18
18
  next();
19
19
  };
20
20
 
21
- // Extract JWT from request: cookie → Bearer header → query param
21
+ // Extract JWT from request: cookie → Bearer header → query param (SSE only)
22
22
  const extractToken = (req) => {
23
- // 1. httpOnly cookie (browser sessions)
23
+ // 1. httpOnly cookie (browser sessions — primary auth method)
24
24
  if (req.cookies?.session) return req.cookies.session;
25
25
 
26
- // 2. Bearer header (API clients, MCP, backward compat)
26
+ // 2. Bearer header (API clients, MCP)
27
27
  const authHeader = req.headers['authorization'];
28
28
  if (authHeader?.startsWith('Bearer ')) return authHeader.slice(7);
29
29
 
30
- // 3. Query param (SSE EventSource fallback)
31
- if (req.query?.token) return req.query.token;
30
+ // 3. Query param — ONLY for SSE EventSource (which cannot set custom headers)
31
+ // Restricted to GET requests with Accept: text/event-stream to minimize exposure
32
+ if (req.query?.token && req.method === 'GET' &&
33
+ (req.headers.accept || '').includes('text/event-stream')) {
34
+ return req.query.token;
35
+ }
32
36
 
33
37
  return null;
34
38
  };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * OpenRouter Integration
3
+ *
4
+ * Lightweight wrapper for OpenRouter API, which provides access to
5
+ * hundreds of AI models (GPT-4, Claude, Gemini, Llama, Mistral, etc.)
6
+ * through a single API key and endpoint.
7
+ *
8
+ * Users provide their own OpenRouter API key via BYOK settings.
9
+ */
10
+
11
+ const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
12
+
13
+ // Popular models available on OpenRouter
14
+ export const OPENROUTER_MODELS = {
15
+ OPTIONS: [
16
+ { value: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4' },
17
+ { value: 'anthropic/claude-opus-4', label: 'Claude Opus 4' },
18
+ { value: 'openai/gpt-4o', label: 'GPT-4o' },
19
+ { value: 'openai/o3', label: 'O3' },
20
+ { value: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
21
+ { value: 'meta-llama/llama-4-maverick', label: 'Llama 4 Maverick' },
22
+ { value: 'mistralai/mistral-large', label: 'Mistral Large' },
23
+ { value: 'deepseek/deepseek-r1', label: 'DeepSeek R1' },
24
+ ],
25
+ DEFAULT: 'anthropic/claude-sonnet-4',
26
+ };
27
+
28
+ /**
29
+ * Query OpenRouter API with streaming support
30
+ * @param {string} message - User message
31
+ * @param {Object} options - { model, apiKey, systemPrompt }
32
+ * @param {Object} writer - WebSocketWriter or SSEStreamWriter
33
+ */
34
+ export async function queryOpenRouter(message, options = {}, writer) {
35
+ const {
36
+ model = OPENROUTER_MODELS.DEFAULT,
37
+ apiKey,
38
+ systemPrompt,
39
+ sessionId,
40
+ } = options;
41
+
42
+ if (!apiKey) {
43
+ writer.send({
44
+ type: 'error',
45
+ error: 'OpenRouter API key required. Add your key in Settings > AI Providers.',
46
+ });
47
+ return;
48
+ }
49
+
50
+ // Generate session ID
51
+ const sid = sessionId || `or-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
52
+ writer.send({ type: 'session-created', sessionId: sid });
53
+
54
+ const messages = [];
55
+ if (systemPrompt) {
56
+ messages.push({ role: 'system', content: systemPrompt });
57
+ }
58
+ messages.push({ role: 'user', content: message });
59
+
60
+ try {
61
+ const response = await fetch(`${OPENROUTER_BASE_URL}/chat/completions`, {
62
+ method: 'POST',
63
+ headers: {
64
+ 'Authorization': `Bearer ${apiKey}`,
65
+ 'Content-Type': 'application/json',
66
+ 'HTTP-Referer': 'https://cli.upfyn.com',
67
+ 'X-Title': 'Upfyn-Code',
68
+ },
69
+ body: JSON.stringify({
70
+ model,
71
+ messages,
72
+ stream: true,
73
+ }),
74
+ });
75
+
76
+ if (!response.ok) {
77
+ const errBody = await response.text();
78
+ let errMsg = `OpenRouter API error (${response.status})`;
79
+ try {
80
+ const parsed = JSON.parse(errBody);
81
+ errMsg = parsed.error?.message || errMsg;
82
+ } catch {}
83
+ writer.send({ type: 'error', error: errMsg, sessionId: sid });
84
+ return;
85
+ }
86
+
87
+ // Stream SSE response
88
+ const reader = response.body.getReader();
89
+ const decoder = new TextDecoder();
90
+ let buffer = '';
91
+ let fullContent = '';
92
+
93
+ while (true) {
94
+ const { done, value } = await reader.read();
95
+ if (done) break;
96
+
97
+ buffer += decoder.decode(value, { stream: true });
98
+ const lines = buffer.split('\n');
99
+ buffer = lines.pop() || '';
100
+
101
+ for (const line of lines) {
102
+ if (!line.startsWith('data: ')) continue;
103
+ const data = line.slice(6).trim();
104
+ if (data === '[DONE]') break;
105
+
106
+ try {
107
+ const parsed = JSON.parse(data);
108
+ const delta = parsed.choices?.[0]?.delta?.content;
109
+ if (delta) {
110
+ fullContent += delta;
111
+ writer.send({
112
+ type: 'assistant',
113
+ content: delta,
114
+ sessionId: sid,
115
+ });
116
+ }
117
+ } catch {}
118
+ }
119
+ }
120
+
121
+ // Send completion
122
+ writer.send({
123
+ type: 'result',
124
+ subtype: 'success',
125
+ sessionId: sid,
126
+ content: fullContent,
127
+ model,
128
+ provider: 'openrouter',
129
+ });
130
+ } catch (error) {
131
+ writer.send({
132
+ type: 'error',
133
+ error: `OpenRouter request failed: ${error.message}`,
134
+ sessionId: sid,
135
+ });
136
+ }
137
+ }