upfynai-code 2.3.0 → 2.4.1
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.
- package/client/dist/assets/AppContent-DTZ2FbvM.js +513 -0
- package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +6 -0
- package/client/dist/assets/LoginModal-CWoFm0au.js +19 -0
- package/client/dist/assets/MarkdownPreview-CYdvwJaV.js +1 -0
- package/client/dist/assets/{Onboarding-Coxo6mFA.js → Onboarding-CtIoXiTp.js} +1 -1
- package/client/dist/assets/{SetupForm-BzYOsbji.js → SetupForm-B4p8im5O.js} +1 -1
- package/client/dist/assets/{ar-SA-G6X2FPQ2-Bmw2-hDt.js → ar-SA-G6X2FPQ2-2gfmdvHk.js} +1 -1
- package/client/dist/assets/{arc-BMqY7_Ci.js → arc-DCZSHhoJ.js} +1 -1
- package/client/dist/assets/{az-AZ-76LH7QW2-Dh1le_qs.js → az-AZ-76LH7QW2-CDdeucRZ.js} +1 -1
- package/client/dist/assets/{bg-BG-XCXSNQG7-Cbav8Z9z.js → bg-BG-XCXSNQG7-D6__XtOK.js} +1 -1
- package/client/dist/assets/{blockDiagram-38ab4fdb-ChHJxsXw.js → blockDiagram-38ab4fdb-Cfbaeyp6.js} +3 -3
- package/client/dist/assets/{bn-BD-2XOGV67Q-DCNjOaWz.js → bn-BD-2XOGV67Q-DHNJw3OG.js} +1 -1
- package/client/dist/assets/{c4Diagram-3d4e48cf-b8Xue4Z6.js → c4Diagram-3d4e48cf-BBCnjOTy.js} +1 -1
- package/client/dist/assets/{ca-ES-6MX7JW3Y-Dl_vM7NS.js → ca-ES-6MX7JW3Y-r5g4o3zQ.js} +1 -1
- package/client/dist/assets/channel-O3ovC0x9.js +1 -0
- package/client/dist/assets/{classDiagram-70f12bd4-BheP7Ggo.js → classDiagram-70f12bd4-D0lhAcxU.js} +1 -1
- package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +2 -0
- package/client/dist/assets/clone-BG9u7vLi.js +1 -0
- package/client/dist/assets/{createText-2e5e7dd3-_n4jI_fO.js → createText-2e5e7dd3-B8jCDmF_.js} +1 -1
- package/client/dist/assets/{cs-CZ-2BRQDIVT-ftsKDdz4.js → cs-CZ-2BRQDIVT-p08jRLRC.js} +1 -1
- package/client/dist/assets/{da-DK-5WZEPLOC-DAjdwGRO.js → da-DK-5WZEPLOC-CnhOImFf.js} +1 -1
- package/client/dist/assets/{de-DE-XR44H4JA-BJXczHGT.js → de-DE-XR44H4JA-BunSXZ-Y.js} +1 -1
- package/client/dist/assets/{edges-e0da2a9e-CfPZr4YM.js → edges-e0da2a9e-CGBBhG8k.js} +2 -2
- package/client/dist/assets/{el-GR-BZB4AONW-DW2p_uy7.js → el-GR-BZB4AONW-D4wv1oIz.js} +1 -1
- package/client/dist/assets/{erDiagram-9861fffd-CF33V-Of.js → erDiagram-9861fffd-CYaF3q1I.js} +1 -1
- package/client/dist/assets/{es-ES-U4NZUMDT-DLOIGnrl.js → es-ES-U4NZUMDT-CGeTKXgd.js} +1 -1
- package/client/dist/assets/{eu-ES-A7QVB2H4-LJXbf89m.js → eu-ES-A7QVB2H4-Cayx1TxR.js} +1 -1
- package/client/dist/assets/{fa-IR-HGAKTJCU-Dvx65fgW.js → fa-IR-HGAKTJCU-CmUg8pmw.js} +1 -1
- package/client/dist/assets/{fi-FI-Z5N7JZ37-EoL65BQh.js → fi-FI-Z5N7JZ37-xvHcPhsU.js} +1 -1
- package/client/dist/assets/{flowDb-956e92f1-HgoXVy2H.js → flowDb-956e92f1-C-_LFz70.js} +3 -3
- package/client/dist/assets/flowDiagram-66a62f08-C1sHdSjn.js +4 -0
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +1 -0
- package/client/dist/assets/{flowchart-elk-definition-4a651766-DJbI2dpv.js → flowchart-elk-definition-4a651766-CNGfpudb.js} +7 -7
- package/client/dist/assets/{fr-FR-RHASNOE6-DNk_jdDs.js → fr-FR-RHASNOE6-DBoHEcNj.js} +1 -1
- package/client/dist/assets/{ganttDiagram-c361ad54-2XX670FU.js → ganttDiagram-c361ad54-B8HJQqjt.js} +1 -1
- package/client/dist/assets/{gitGraphDiagram-72cf32ee-CcUfruAo.js → gitGraphDiagram-72cf32ee-DojCDvlS.js} +1 -1
- package/client/dist/assets/{gl-ES-HMX3MZ6V-dxzFjZlG.js → gl-ES-HMX3MZ6V-p6hrn2cN.js} +1 -1
- package/client/dist/assets/{graph-BSbiMSBC.js → graph-DXM7lcy1.js} +1 -1
- package/client/dist/assets/{he-IL-6SHJWFNN-Cogsfdt1.js → he-IL-6SHJWFNN-y2jEX6-0.js} +1 -1
- package/client/dist/assets/{hi-IN-IWLTKZ5I-L6wbgi4F.js → hi-IN-IWLTKZ5I-99pNfyWr.js} +1 -1
- package/client/dist/assets/{hu-HU-A5ZG7DT2-DSA6ZDsH.js → hu-HU-A5ZG7DT2-hygceGMS.js} +1 -1
- package/client/dist/assets/{id-ID-SAP4L64H-BK_vGGS6.js → id-ID-SAP4L64H-CyIqi1hv.js} +1 -1
- package/client/dist/assets/{image-blob-reduce.esm-BLtmMM_J.js → image-blob-reduce.esm-D6s-rqMO.js} +6 -1
- package/client/dist/assets/{index-3862675e-Bv32HUgT.js → index-3862675e-4idOQN2N.js} +1 -1
- package/client/dist/assets/{index-BPwf8Fw3.js → index-BGmwbRlb.js} +6 -6
- package/client/dist/assets/index-BHZfFT_V.js +97 -0
- package/client/dist/assets/{infoDiagram-f8f76790-w4mR4pxn.js → infoDiagram-f8f76790-CFLrHqtc.js} +1 -1
- package/client/dist/assets/{it-IT-JPQ66NNP-BLdHYMhn.js → it-IT-JPQ66NNP-DzVvVdQI.js} +1 -1
- package/client/dist/assets/{ja-JP-DBVTYXUO-B_vmexl_.js → ja-JP-DBVTYXUO-BI4fPexV.js} +1 -1
- package/client/dist/assets/{journeyDiagram-49397b02-D9nmO17e.js → journeyDiagram-49397b02-C3CFDo8z.js} +1 -1
- package/client/dist/assets/{kaa-6HZHGXH3-5s-3jl6F.js → kaa-6HZHGXH3-fwOleoQB.js} +1 -1
- package/client/dist/assets/{kab-KAB-ZGHBKWFO-2QaVDuSf.js → kab-KAB-ZGHBKWFO-DBI_ri48.js} +1 -1
- package/client/dist/assets/{kk-KZ-P5N5QNE5-CTC52Vbi.js → kk-KZ-P5N5QNE5-zpl7uvyF.js} +1 -1
- package/client/dist/assets/{km-KH-HSX4SM5Z-DxawH8UZ.js → km-KH-HSX4SM5Z-DOMFSres.js} +1 -1
- package/client/dist/assets/{ko-KR-MTYHY66A-CmosEM8_.js → ko-KR-MTYHY66A-tb08hXzd.js} +1 -1
- package/client/dist/assets/{ku-TR-6OUDTVRD-DbiLen4y.js → ku-TR-6OUDTVRD-DlIQCCY4.js} +1 -1
- package/client/dist/assets/{layout-jmt3H9tA.js → layout-B_11mCXA.js} +1 -1
- package/client/dist/assets/{line-JTlRayUJ.js → line-B-qmK_vI.js} +1 -1
- package/client/dist/assets/{linear-DJeB5p7x.js → linear-Ph6uuYcX.js} +1 -1
- package/client/dist/assets/{lt-LT-XHIRWOB4-CH15wrjA.js → lt-LT-XHIRWOB4--qWy24_Z.js} +1 -1
- package/client/dist/assets/{lv-LV-5QDEKY6T-dhgfPuCQ.js → lv-LV-5QDEKY6T-Bnd_1GDb.js} +1 -1
- package/client/dist/assets/mindmap-definition-fc14e90a-Do79tIc0.js +425 -0
- package/client/dist/assets/{mr-IN-CRQNXWMA-3Gi6iq7A.js → mr-IN-CRQNXWMA-BsV6HaD9.js} +1 -1
- package/client/dist/assets/{my-MM-5M5IBNSE-CpH4rdJj.js → my-MM-5M5IBNSE-kZQURVIi.js} +1 -1
- package/client/dist/assets/{nb-NO-T6EIAALU-Du6iiGql.js → nb-NO-T6EIAALU-Cvf9FdSF.js} +1 -1
- package/client/dist/assets/{nl-NL-IS3SIHDZ-BGvsd1MT.js → nl-NL-IS3SIHDZ-DA1yqpXw.js} +1 -1
- package/client/dist/assets/{nn-NO-6E72VCQL-B-odvJZW.js → nn-NO-6E72VCQL-89lm3vku.js} +1 -1
- package/client/dist/assets/{oc-FR-POXYY2M6-COC8xNjo.js → oc-FR-POXYY2M6-BsrjTJQh.js} +1 -1
- package/client/dist/assets/{pa-IN-N4M65BXN-CE21PUQH.js → pa-IN-N4M65BXN-CczefYaj.js} +1 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
- package/client/dist/assets/percentages-BXMCSKIN-Be6p9phi.js +207 -0
- package/client/dist/assets/pica-CQIY57Tf.js +7 -0
- package/client/dist/assets/{pieDiagram-8a3498a8-Cvfh7Qr5.js → pieDiagram-8a3498a8-CfblQHdm.js} +2 -2
- package/client/dist/assets/{pl-PL-T2D74RX3-D4xFVSoT.js → pl-PL-T2D74RX3-DdhH-zcK.js} +1 -1
- package/client/dist/assets/{pt-BR-5N22H2LF-CCq257gA.js → pt-BR-5N22H2LF-gpwlheL6.js} +1 -1
- package/client/dist/assets/{pt-PT-UZXXM6DQ-1l8gt5vA.js → pt-PT-UZXXM6DQ-Cs87vICi.js} +1 -1
- package/client/dist/assets/{quadrantDiagram-120e2f19-BA0js1aD.js → quadrantDiagram-120e2f19-CRMSamSP.js} +1 -1
- package/client/dist/assets/{requirementDiagram-deff3bca-B0QNFfIn.js → requirementDiagram-deff3bca-D3LBN016.js} +1 -1
- package/client/dist/assets/{ro-RO-JPDTUUEW-yosBW01E.js → ro-RO-JPDTUUEW-CWTSJ1Dt.js} +1 -1
- package/client/dist/assets/roundRect-0PYZxl1G.js +1 -0
- package/client/dist/assets/{ru-RU-B4JR7IUQ-8LkEJUix.js → ru-RU-B4JR7IUQ-Bq7aN2ep.js} +1 -1
- package/client/dist/assets/{sankeyDiagram-04a897e0-D4T9eCXn.js → sankeyDiagram-04a897e0-CsFqOQZN.js} +3 -3
- package/client/dist/assets/{sequenceDiagram-704730f1-CfBUTCrO.js → sequenceDiagram-704730f1-BRYXVDGX.js} +1 -1
- package/client/dist/assets/{si-LK-N5RQ5JYF-D8rjbqtd.js → si-LK-N5RQ5JYF-BBjcNYQh.js} +1 -1
- package/client/dist/assets/{sk-SK-C5VTKIMK-Bg14sAzN.js → sk-SK-C5VTKIMK-ByjKQzUb.js} +1 -1
- package/client/dist/assets/{sl-SI-NN7IZMDC-CMTib6Zs.js → sl-SI-NN7IZMDC-B8WCyMBU.js} +1 -1
- package/client/dist/assets/{stateDiagram-587899a1-BGgvmVSZ.js → stateDiagram-587899a1-BHoy9LtD.js} +1 -1
- package/client/dist/assets/{stateDiagram-v2-d93cdb3a-Qn3DpYuO.js → stateDiagram-v2-d93cdb3a-BvMUA6bS.js} +1 -1
- package/client/dist/assets/{styles-6aaf32cf-IdVZLPrD.js → styles-6aaf32cf-Dr-lfIOW.js} +2 -2
- package/client/dist/assets/{styles-9a916d00-BAC3L45X.js → styles-9a916d00-DS4wRpL7.js} +1 -1
- package/client/dist/assets/{styles-c10674c1-COhXxX8c.js → styles-c10674c1-nKRF6NrH.js} +1 -1
- package/client/dist/assets/{subset-shared.chunk-BWHnFai4.js → subset-shared.chunk-KT79s7KG.js} +64 -2
- package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +1 -0
- package/client/dist/assets/{sv-SE-XGPEYMSR-C1425rOF.js → sv-SE-XGPEYMSR-BiIPUVbv.js} +1 -1
- package/client/dist/assets/{svgDrawCommon-08f97a94-Cfk-fgnN.js → svgDrawCommon-08f97a94-C3uP9PYr.js} +1 -1
- package/client/dist/assets/{ta-IN-2NMHFXQM-BHHo1zpF.js → ta-IN-2NMHFXQM-Cidadso2.js} +1 -1
- package/client/dist/assets/{th-TH-HPSO5L25-CZVzm_WT.js → th-TH-HPSO5L25-CFNnJwSv.js} +1 -1
- package/client/dist/assets/{timeline-definition-85554ec2-VAvuJith.js → timeline-definition-85554ec2-BSsLsIgF.js} +1 -1
- package/client/dist/assets/{tr-TR-DEFEU3FU-DE1lclCq.js → tr-TR-DEFEU3FU-DaFcI-KL.js} +1 -1
- package/client/dist/assets/{uk-UA-QMV73CPH-D4lJZ85O.js → uk-UA-QMV73CPH-DkBW36St.js} +1 -1
- package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +20 -0
- package/client/dist/assets/vendor-codemirror-rix45NST.js +16 -0
- package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
- package/client/dist/assets/vendor-icons-Dh9m_Ydt.js +596 -0
- package/client/dist/assets/{vendor-markdown-CIVH08vJ.js → vendor-markdown-BXEi_H3G.js} +3 -3
- package/client/dist/assets/vendor-react-9mUTKBHH.js +67 -0
- package/client/dist/assets/{vendor-syntax-Djb62v3a.js → vendor-syntax-DnmwQQJF.js} +14 -7
- package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
- package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
- package/client/dist/assets/{vi-VN-M7AON7JQ-Dgc_SShk.js → vi-VN-M7AON7JQ-KrtfxOzl.js} +1 -1
- package/client/dist/assets/{xychartDiagram-e933f94c-BeyVBJhb.js → xychartDiagram-e933f94c-CgNgZ4pp.js} +1 -1
- package/client/dist/assets/{zh-CN-LNUGB5OW-MH4Yh8in.js → zh-CN-LNUGB5OW-BQu12RoD.js} +1 -1
- package/client/dist/assets/{zh-HK-E62DVLB3-D4XHehjx.js → zh-HK-E62DVLB3-zx9CvERq.js} +1 -1
- package/client/dist/assets/{zh-TW-RAJ6MFWO--efj3evj.js → zh-TW-RAJ6MFWO-ffJWgVxn.js} +1 -1
- package/client/dist/index.html +128 -66
- package/client/dist/manifest.json +61 -15
- package/client/dist/sw.js +19 -55
- package/commands/upfynai-connect.md +31 -18
- package/commands/upfynai.md +45 -26
- package/package.json +1 -1
- package/server/cli-ui.js +320 -169
- package/server/cli.js +255 -23
- package/server/constants/config.js +29 -3
- package/server/database/auth.db +0 -0
- package/server/index.js +380 -121
- package/server/mcp-server.js +2 -1
- package/server/middleware/auth.js +18 -8
- package/server/openrouter.js +137 -0
- package/server/relay-client.js +262 -18
- package/server/routes/agent.js +54 -19
- package/server/routes/auth.js +23 -12
- package/server/routes/commands.js +1 -1
- package/server/routes/settings.js +91 -0
- package/shared/modelConstants.js +29 -0
- package/client/dist/assets/AppContent-CTSHQdyq.js +0 -513
- package/client/dist/assets/CanvasPanel-Cig0Mo9s.js +0 -6
- package/client/dist/assets/LoginModal-silya-zP.js +0 -11
- package/client/dist/assets/MarkdownPreview-B3c7OEj6.js +0 -1
- package/client/dist/assets/channel-CSnvHe_M.js +0 -1
- package/client/dist/assets/classDiagram-v2-f2320105-xtym7GEZ.js +0 -2
- package/client/dist/assets/clone-B75abXxS.js +0 -1
- package/client/dist/assets/flowDiagram-66a62f08-tffoET0H.js +0 -4
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Byc3JCHh.js +0 -1
- package/client/dist/assets/index-BnXuHrpJ.js +0 -523
- package/client/dist/assets/index-BwxNox94.css +0 -1
- package/client/dist/assets/index-D1urGMYu.js +0 -95
- package/client/dist/assets/mindmap-definition-fc14e90a-BOOrexmz.js +0 -415
- package/client/dist/assets/pdf-TYrZqVzP.js +0 -12
- package/client/dist/assets/percentages-BXMCSKIN-C9GT0OD3.js +0 -199
- package/client/dist/assets/pica-VkdyTzi8.js +0 -2
- package/client/dist/assets/roundRect-mAH3dD0p.js +0 -1
- package/client/dist/assets/subset-worker.chunk-C8QUSruZ.js +0 -1
- package/client/dist/assets/vendor-codemirror-BARtJV1V.js +0 -16
- package/client/dist/assets/vendor-codemirror-langs-52_y1wip.js +0 -20
- package/client/dist/assets/vendor-i18n-ByAl-gdx.js +0 -1
- package/client/dist/assets/vendor-icons-D33IkSIf.js +0 -1
- package/client/dist/assets/vendor-react-CHoMc7ka.js +0 -8
- package/client/dist/assets/vendor-xterm-DBb3RXlu.js +0 -66
- package/client/dist/assets/vendor-xterm-DrlLKa8f.css +0 -1
- package/client/dist/llms.txt +0 -40
- package/client/dist/robots.txt +0 -11
- 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,9 +70,10 @@ 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, userDb } from './database/db.js';
|
|
73
74
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
74
|
-
import { IS_PLATFORM } from './constants/config.js';
|
|
75
|
+
import { IS_PLATFORM, IS_LOCAL } from './constants/config.js';
|
|
76
|
+
import { execSync } from 'child_process';
|
|
75
77
|
|
|
76
78
|
// File system watchers for provider project/session folders
|
|
77
79
|
const PROVIDER_WATCH_PATHS = [
|
|
@@ -348,6 +350,18 @@ if (server) {
|
|
|
348
350
|
// Make WebSocket server available to routes
|
|
349
351
|
app.locals.wss = wss;
|
|
350
352
|
|
|
353
|
+
// Security headers — protect against common web attacks
|
|
354
|
+
app.use((req, res, next) => {
|
|
355
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
356
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
357
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
358
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
359
|
+
if (process.env.NODE_ENV === 'production') {
|
|
360
|
+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
361
|
+
}
|
|
362
|
+
next();
|
|
363
|
+
});
|
|
364
|
+
|
|
351
365
|
// CORS: require explicit CORS_ORIGINS in production, restrict to same-origin otherwise
|
|
352
366
|
const CORS_ORIGINS = process.env.CORS_ORIGINS
|
|
353
367
|
? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
|
|
@@ -367,6 +381,44 @@ app.use(express.json({
|
|
|
367
381
|
}));
|
|
368
382
|
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
|
369
383
|
|
|
384
|
+
// Rate limiting for auth endpoints — prevent brute force attacks
|
|
385
|
+
const authRateLimitMap = new Map();
|
|
386
|
+
const AUTH_RATE_WINDOW = 15 * 60 * 1000; // 15 minutes
|
|
387
|
+
const AUTH_RATE_MAX = 10; // max attempts per window
|
|
388
|
+
|
|
389
|
+
function authRateLimit(req, res, next) {
|
|
390
|
+
const key = req.ip || req.connection.remoteAddress || 'unknown';
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
const entry = authRateLimitMap.get(key);
|
|
393
|
+
|
|
394
|
+
if (entry) {
|
|
395
|
+
// Clean expired entries
|
|
396
|
+
if (now - entry.windowStart > AUTH_RATE_WINDOW) {
|
|
397
|
+
authRateLimitMap.set(key, { windowStart: now, count: 1 });
|
|
398
|
+
return next();
|
|
399
|
+
}
|
|
400
|
+
entry.count++;
|
|
401
|
+
if (entry.count > AUTH_RATE_MAX) {
|
|
402
|
+
return res.status(429).json({ error: 'Too many attempts. Please try again later.' });
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
authRateLimitMap.set(key, { windowStart: now, count: 1 });
|
|
406
|
+
}
|
|
407
|
+
next();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Clean up stale rate limit entries every 30 minutes
|
|
411
|
+
setInterval(() => {
|
|
412
|
+
const now = Date.now();
|
|
413
|
+
for (const [key, entry] of authRateLimitMap) {
|
|
414
|
+
if (now - entry.windowStart > AUTH_RATE_WINDOW) {
|
|
415
|
+
authRateLimitMap.delete(key);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}, 30 * 60 * 1000);
|
|
419
|
+
|
|
420
|
+
app.locals.authRateLimit = authRateLimit;
|
|
421
|
+
|
|
370
422
|
// Vercel serverless: lazy DB initialization on first request
|
|
371
423
|
let dbInitialized = false;
|
|
372
424
|
if (process.env.VERCEL) {
|
|
@@ -488,12 +540,70 @@ app.get('/api/relay/status', authenticateToken, (req, res) => {
|
|
|
488
540
|
});
|
|
489
541
|
});
|
|
490
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Detect installed AI CLI agents on the local machine (server-side).
|
|
545
|
+
* Used in self-hosted/local mode where no relay is needed.
|
|
546
|
+
*/
|
|
547
|
+
let cachedLocalAgents = null;
|
|
548
|
+
let localAgentsCacheTime = 0;
|
|
549
|
+
function detectLocalAgents() {
|
|
550
|
+
// Cache for 60 seconds
|
|
551
|
+
if (cachedLocalAgents && Date.now() - localAgentsCacheTime < 60000) {
|
|
552
|
+
return cachedLocalAgents;
|
|
553
|
+
}
|
|
554
|
+
const isWindows = process.platform === 'win32';
|
|
555
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
556
|
+
const agents = [
|
|
557
|
+
{ name: 'claude', binary: 'claude', label: 'Claude Code' },
|
|
558
|
+
{ name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
|
|
559
|
+
{ name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
|
|
560
|
+
];
|
|
561
|
+
const detected = {};
|
|
562
|
+
for (const agent of agents) {
|
|
563
|
+
try {
|
|
564
|
+
const result = execSync(`${whichCmd} ${agent.binary}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
565
|
+
detected[agent.name] = { installed: true, path: result.split('\n')[0].trim(), label: agent.label };
|
|
566
|
+
} catch {
|
|
567
|
+
detected[agent.name] = { installed: false, label: agent.label };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
cachedLocalAgents = detected;
|
|
571
|
+
localAgentsCacheTime = Date.now();
|
|
572
|
+
return detected;
|
|
573
|
+
}
|
|
574
|
+
|
|
491
575
|
// Connection status — alias at path the frontend expects
|
|
492
576
|
app.get('/api/auth/connection-status', authenticateToken, (req, res) => {
|
|
493
577
|
const relay = relayConnections.get(Number(req.user.id));
|
|
578
|
+
const connected = !!(relay && relay.ws.readyState === 1);
|
|
579
|
+
|
|
580
|
+
// In local mode, always "connected" — SDK runs directly on this machine
|
|
581
|
+
if (IS_LOCAL) {
|
|
582
|
+
const agents = detectLocalAgents();
|
|
583
|
+
return res.json({
|
|
584
|
+
connected: true,
|
|
585
|
+
local: true,
|
|
586
|
+
connectedAt: Date.now(),
|
|
587
|
+
agents,
|
|
588
|
+
machine: {
|
|
589
|
+
hostname: os.hostname(),
|
|
590
|
+
platform: process.platform,
|
|
591
|
+
cwd: process.cwd(),
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
494
596
|
res.json({
|
|
495
|
-
connected
|
|
496
|
-
|
|
597
|
+
connected,
|
|
598
|
+
local: false,
|
|
599
|
+
connectedAt: relay?.connectedAt || null,
|
|
600
|
+
agents: connected ? (relay.agents || null) : null,
|
|
601
|
+
machine: connected ? {
|
|
602
|
+
hostname: relay.machine,
|
|
603
|
+
platform: relay.platform,
|
|
604
|
+
cwd: relay.cwd,
|
|
605
|
+
version: relay.version,
|
|
606
|
+
} : null
|
|
497
607
|
});
|
|
498
608
|
});
|
|
499
609
|
|
|
@@ -520,77 +630,15 @@ app.use(express.static(path.join(__dirname, '../client/dist'), {
|
|
|
520
630
|
// /api/config endpoint removed - no longer needed
|
|
521
631
|
// Frontend now uses window.location for WebSocket URLs
|
|
522
632
|
|
|
523
|
-
// System update endpoint
|
|
524
|
-
|
|
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
|
-
});
|
|
633
|
+
// System update endpoint — REMOVED for security (shell command execution risk)
|
|
634
|
+
// Use `uc update` from CLI instead
|
|
587
635
|
|
|
588
636
|
app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
589
637
|
try {
|
|
590
638
|
const projects = await getProjects(broadcastProgress);
|
|
591
639
|
res.json(projects);
|
|
592
640
|
} catch (error) {
|
|
593
|
-
res.status(500).json({ error: error
|
|
641
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
594
642
|
}
|
|
595
643
|
});
|
|
596
644
|
|
|
@@ -600,7 +648,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
|
|
|
600
648
|
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
|
|
601
649
|
res.json(result);
|
|
602
650
|
} catch (error) {
|
|
603
|
-
res.status(500).json({ error: error
|
|
651
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
604
652
|
}
|
|
605
653
|
});
|
|
606
654
|
|
|
@@ -625,7 +673,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateT
|
|
|
625
673
|
res.json(result);
|
|
626
674
|
}
|
|
627
675
|
} catch (error) {
|
|
628
|
-
res.status(500).json({ error: error
|
|
676
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
629
677
|
}
|
|
630
678
|
});
|
|
631
679
|
|
|
@@ -636,7 +684,7 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
|
|
|
636
684
|
await renameProject(req.params.projectName, displayName);
|
|
637
685
|
res.json({ success: true });
|
|
638
686
|
} catch (error) {
|
|
639
|
-
res.status(500).json({ error: error
|
|
687
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
640
688
|
}
|
|
641
689
|
});
|
|
642
690
|
|
|
@@ -650,7 +698,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|
|
650
698
|
res.json({ success: true });
|
|
651
699
|
} catch (error) {
|
|
652
700
|
// session delete error
|
|
653
|
-
res.status(500).json({ error: error
|
|
701
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
654
702
|
}
|
|
655
703
|
});
|
|
656
704
|
|
|
@@ -662,7 +710,7 @@ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) =>
|
|
|
662
710
|
await deleteProject(projectName, force);
|
|
663
711
|
res.json({ success: true });
|
|
664
712
|
} catch (error) {
|
|
665
|
-
res.status(500).json({ error: error
|
|
713
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
666
714
|
}
|
|
667
715
|
});
|
|
668
716
|
|
|
@@ -679,7 +727,7 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => {
|
|
|
679
727
|
res.json({ success: true, project });
|
|
680
728
|
} catch (error) {
|
|
681
729
|
console.error('Error creating project:', error);
|
|
682
|
-
res.status(500).json({ error: error
|
|
730
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
683
731
|
}
|
|
684
732
|
});
|
|
685
733
|
|
|
@@ -832,13 +880,11 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
832
880
|
return res.status(404).json({ error: 'Project not found' });
|
|
833
881
|
}
|
|
834
882
|
|
|
835
|
-
//
|
|
836
|
-
const resolved = path.
|
|
837
|
-
? path.resolve(filePath)
|
|
838
|
-
: path.resolve(projectRoot, filePath);
|
|
883
|
+
// Always resolve relative to project root to prevent path traversal
|
|
884
|
+
const resolved = path.resolve(projectRoot, filePath);
|
|
839
885
|
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
840
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
841
|
-
return res.status(403).json({ error: '
|
|
886
|
+
if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
|
|
887
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
842
888
|
}
|
|
843
889
|
|
|
844
890
|
const content = await fsPromises.readFile(resolved, 'utf8');
|
|
@@ -850,7 +896,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
850
896
|
} else if (error.code === 'EACCES') {
|
|
851
897
|
res.status(403).json({ error: 'Permission denied' });
|
|
852
898
|
} else {
|
|
853
|
-
res.status(500).json({ error: error
|
|
899
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
854
900
|
}
|
|
855
901
|
}
|
|
856
902
|
});
|
|
@@ -872,10 +918,11 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
872
918
|
return res.status(404).json({ error: 'Project not found' });
|
|
873
919
|
}
|
|
874
920
|
|
|
875
|
-
|
|
921
|
+
// Resolve path relative to project root to prevent path traversal
|
|
922
|
+
const resolved = path.resolve(projectRoot, filePath);
|
|
876
923
|
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
877
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
878
|
-
return res.status(403).json({ error: '
|
|
924
|
+
if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
|
|
925
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
879
926
|
}
|
|
880
927
|
|
|
881
928
|
// Check if file exists
|
|
@@ -903,7 +950,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
903
950
|
} catch (error) {
|
|
904
951
|
console.error('Error serving binary file:', error);
|
|
905
952
|
if (!res.headersSent) {
|
|
906
|
-
res.status(500).json({ error: error
|
|
953
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
907
954
|
}
|
|
908
955
|
}
|
|
909
956
|
});
|
|
@@ -929,13 +976,11 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
929
976
|
return res.status(404).json({ error: 'Project not found' });
|
|
930
977
|
}
|
|
931
978
|
|
|
932
|
-
//
|
|
933
|
-
const resolved = path.
|
|
934
|
-
? path.resolve(filePath)
|
|
935
|
-
: path.resolve(projectRoot, filePath);
|
|
979
|
+
// Always resolve relative to project root to prevent path traversal
|
|
980
|
+
const resolved = path.resolve(projectRoot, filePath);
|
|
936
981
|
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
937
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
938
|
-
return res.status(403).json({ error: '
|
|
982
|
+
if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectRoot)) {
|
|
983
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
939
984
|
}
|
|
940
985
|
|
|
941
986
|
// Write the new content
|
|
@@ -953,7 +998,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
953
998
|
} else if (error.code === 'EACCES') {
|
|
954
999
|
res.status(403).json({ error: 'Permission denied' });
|
|
955
1000
|
} else {
|
|
956
|
-
res.status(500).json({ error: error
|
|
1001
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
957
1002
|
}
|
|
958
1003
|
}
|
|
959
1004
|
});
|
|
@@ -985,7 +1030,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|
|
985
1030
|
res.json(files);
|
|
986
1031
|
} catch (error) {
|
|
987
1032
|
console.error('[ERROR] File tree error:', error.message);
|
|
988
|
-
res.status(500).json({ error: error
|
|
1033
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
989
1034
|
}
|
|
990
1035
|
});
|
|
991
1036
|
|
|
@@ -1001,9 +1046,9 @@ if (wss) wss.on('connection', (ws, request) => {
|
|
|
1001
1046
|
if (pathname === '/shell') {
|
|
1002
1047
|
handleShellConnection(ws);
|
|
1003
1048
|
} else if (pathname === '/ws') {
|
|
1004
|
-
handleChatConnection(ws);
|
|
1049
|
+
handleChatConnection(ws, request);
|
|
1005
1050
|
} else if (pathname === '/relay') {
|
|
1006
|
-
handleRelayConnection(ws, urlObj.searchParams.get('token'));
|
|
1051
|
+
handleRelayConnection(ws, urlObj.searchParams.get('token'), request);
|
|
1007
1052
|
} else {
|
|
1008
1053
|
// unknown WebSocket path
|
|
1009
1054
|
ws.close();
|
|
@@ -1036,9 +1081,44 @@ class WebSocketWriter {
|
|
|
1036
1081
|
}
|
|
1037
1082
|
}
|
|
1038
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* Look up a user's stored API key for a given provider.
|
|
1086
|
+
* Falls back to server env vars if user has none stored.
|
|
1087
|
+
* @param {number} userId
|
|
1088
|
+
* @param {string} providerType - e.g. 'anthropic_key', 'openai_key', 'openrouter_key', 'google_key'
|
|
1089
|
+
* @returns {Promise<string|null>}
|
|
1090
|
+
*/
|
|
1091
|
+
async function getUserProviderKey(userId, providerType) {
|
|
1092
|
+
if (!userId) return null;
|
|
1093
|
+
try {
|
|
1094
|
+
const creds = await credentialsDb.getCredentials(userId, providerType);
|
|
1095
|
+
const active = creds.find(c => c.is_active);
|
|
1096
|
+
return active?.credential_value || null;
|
|
1097
|
+
} catch { return null; }
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Temporarily set environment variable for an AI SDK call, then restore.
|
|
1102
|
+
* @param {string} envKey - e.g. 'ANTHROPIC_API_KEY'
|
|
1103
|
+
* @param {string|null} userKey - user's BYOK key, null to skip
|
|
1104
|
+
* @param {Function} fn - async function to execute with the key set
|
|
1105
|
+
*/
|
|
1106
|
+
async function withUserApiKey(envKey, userKey, fn) {
|
|
1107
|
+
if (!userKey) return fn();
|
|
1108
|
+
const prev = process.env[envKey];
|
|
1109
|
+
process.env[envKey] = userKey;
|
|
1110
|
+
try {
|
|
1111
|
+
return await fn();
|
|
1112
|
+
} finally {
|
|
1113
|
+
if (prev !== undefined) process.env[envKey] = prev;
|
|
1114
|
+
else delete process.env[envKey];
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1039
1118
|
// Handle chat WebSocket connections
|
|
1040
|
-
function handleChatConnection(ws) {
|
|
1119
|
+
function handleChatConnection(ws, request) {
|
|
1041
1120
|
// chat WebSocket connected
|
|
1121
|
+
const wsUser = request?.user || null;
|
|
1042
1122
|
|
|
1043
1123
|
// Add to connected clients for project updates
|
|
1044
1124
|
connectedClients.add(ws);
|
|
@@ -1107,24 +1187,64 @@ function handleChatConnection(ws) {
|
|
|
1107
1187
|
}
|
|
1108
1188
|
if (sid) lockedSessionsForThisWs.add(sid);
|
|
1109
1189
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1190
|
+
// Check if user has active relay → route to local machine
|
|
1191
|
+
if (hasActiveRelay(wsUser?.userId)) {
|
|
1192
|
+
await routeViaRelay(wsUser.userId, 'claude-query', data, writer, {
|
|
1193
|
+
response: 'claude-response',
|
|
1194
|
+
complete: 'claude-complete',
|
|
1195
|
+
error: 'claude-error'
|
|
1196
|
+
});
|
|
1197
|
+
} else {
|
|
1198
|
+
// Fall back to server-side SDK
|
|
1199
|
+
const userAnthropicKey = wsUser?.userId
|
|
1200
|
+
? await getUserProviderKey(wsUser.userId, 'anthropic_key')
|
|
1201
|
+
: null;
|
|
1202
|
+
|
|
1203
|
+
await withUserApiKey('ANTHROPIC_API_KEY', userAnthropicKey, () =>
|
|
1204
|
+
queryClaudeSDK(data.command, data.options, writer)
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1116
1207
|
} else if (data.type === 'cursor-command') {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1208
|
+
// Check if user has active relay → route to local machine
|
|
1209
|
+
if (hasActiveRelay(wsUser?.userId)) {
|
|
1210
|
+
await routeViaRelay(wsUser.userId, 'cursor-query', data, writer, {
|
|
1211
|
+
response: 'cursor-response',
|
|
1212
|
+
complete: 'cursor-complete',
|
|
1213
|
+
error: 'cursor-error'
|
|
1214
|
+
});
|
|
1215
|
+
} else {
|
|
1216
|
+
await spawnCursor(data.command, data.options, writer);
|
|
1217
|
+
}
|
|
1122
1218
|
} else if (data.type === 'codex-command') {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1219
|
+
// Check if user has active relay → route to local machine
|
|
1220
|
+
if (hasActiveRelay(wsUser?.userId)) {
|
|
1221
|
+
await routeViaRelay(wsUser.userId, 'codex-query', data, writer, {
|
|
1222
|
+
response: 'codex-response',
|
|
1223
|
+
complete: 'codex-complete',
|
|
1224
|
+
error: 'codex-error'
|
|
1225
|
+
});
|
|
1226
|
+
} else {
|
|
1227
|
+
const userOpenaiKey = wsUser?.userId
|
|
1228
|
+
? await getUserProviderKey(wsUser.userId, 'openai_key')
|
|
1229
|
+
: null;
|
|
1230
|
+
|
|
1231
|
+
await withUserApiKey('OPENAI_API_KEY', userOpenaiKey, () =>
|
|
1232
|
+
queryCodex(data.command, data.options, writer)
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
} else if (data.type === 'openrouter-command') {
|
|
1236
|
+
console.log('[DEBUG] OpenRouter message:', data.command?.slice(0, 60) || '[empty]');
|
|
1237
|
+
console.log('🤖 Model:', data.options?.model || OPENROUTER_MODELS.DEFAULT);
|
|
1238
|
+
|
|
1239
|
+
// BYOK: OpenRouter requires user's own API key
|
|
1240
|
+
const userOrKey = wsUser?.userId
|
|
1241
|
+
? await getUserProviderKey(wsUser.userId, 'openrouter_key')
|
|
1242
|
+
: null;
|
|
1243
|
+
|
|
1244
|
+
await queryOpenRouter(data.command, {
|
|
1245
|
+
...data.options,
|
|
1246
|
+
apiKey: userOrKey,
|
|
1247
|
+
}, writer);
|
|
1128
1248
|
} else if (data.type === 'cursor-resume') {
|
|
1129
1249
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
1130
1250
|
// cursor resume session
|
|
@@ -1226,7 +1346,7 @@ function handleChatConnection(ws) {
|
|
|
1226
1346
|
}
|
|
1227
1347
|
|
|
1228
1348
|
// Handle relay WebSocket connections (local machine ↔ server bridge)
|
|
1229
|
-
async function handleRelayConnection(ws, token) {
|
|
1349
|
+
async function handleRelayConnection(ws, token, request) {
|
|
1230
1350
|
if (!token) {
|
|
1231
1351
|
ws.send(JSON.stringify({ type: 'error', error: 'Relay token required. Use ?token=upfyn_xxx' }));
|
|
1232
1352
|
ws.close();
|
|
@@ -1243,9 +1363,20 @@ async function handleRelayConnection(ws, token) {
|
|
|
1243
1363
|
const userId = Number(tokenData.user_id);
|
|
1244
1364
|
const username = tokenData.username;
|
|
1245
1365
|
|
|
1246
|
-
//
|
|
1247
|
-
|
|
1248
|
-
|
|
1366
|
+
// Extract optional headers from relay handshake
|
|
1367
|
+
const anthropicApiKey = request?.headers?.['x-anthropic-api-key'] || null;
|
|
1368
|
+
const relayVersion = request?.headers?.['x-upfyn-version'] || null;
|
|
1369
|
+
const relayMachine = request?.headers?.['x-upfyn-machine'] || null;
|
|
1370
|
+
const relayPlatform = request?.headers?.['x-upfyn-platform'] || null;
|
|
1371
|
+
const relayCwd = request?.headers?.['x-upfyn-cwd'] || null;
|
|
1372
|
+
|
|
1373
|
+
// Store relay connection with API key in memory only (use Number() for consistent Map key type)
|
|
1374
|
+
// API key is held per-user in the relay connection, NOT in process.env
|
|
1375
|
+
relayConnections.set(userId, {
|
|
1376
|
+
ws, user: tokenData, connectedAt: Date.now(), anthropicApiKey,
|
|
1377
|
+
version: relayVersion, machine: relayMachine, platform: relayPlatform, cwd: relayCwd,
|
|
1378
|
+
agents: null // populated when client sends agent-capabilities
|
|
1379
|
+
});
|
|
1249
1380
|
|
|
1250
1381
|
ws.send(JSON.stringify({
|
|
1251
1382
|
type: 'relay-connected',
|
|
@@ -1296,6 +1427,29 @@ async function handleRelayConnection(ws, token) {
|
|
|
1296
1427
|
return;
|
|
1297
1428
|
}
|
|
1298
1429
|
|
|
1430
|
+
// Agent capabilities report from relay client
|
|
1431
|
+
if (data.type === 'agent-capabilities') {
|
|
1432
|
+
const relay = relayConnections.get(userId);
|
|
1433
|
+
if (relay) {
|
|
1434
|
+
relay.agents = data.agents || {};
|
|
1435
|
+
relay.machine = data.machine || relay.machine;
|
|
1436
|
+
}
|
|
1437
|
+
// Broadcast agent info to browser clients
|
|
1438
|
+
for (const client of connectedClients) {
|
|
1439
|
+
try {
|
|
1440
|
+
if (client.readyState === 1) {
|
|
1441
|
+
client.send(JSON.stringify({
|
|
1442
|
+
type: 'relay-agents',
|
|
1443
|
+
userId,
|
|
1444
|
+
agents: data.agents || {},
|
|
1445
|
+
machine: data.machine || {}
|
|
1446
|
+
}));
|
|
1447
|
+
}
|
|
1448
|
+
} catch (e) { /* ignore */ }
|
|
1449
|
+
}
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1299
1453
|
// Heartbeat
|
|
1300
1454
|
if (data.type === 'ping') {
|
|
1301
1455
|
ws.send(JSON.stringify({ type: 'pong' }));
|
|
@@ -1346,7 +1500,7 @@ function sendRelayCommand(userId, action, payload, onStream = null, timeoutMs =
|
|
|
1346
1500
|
return new Promise((resolve, reject) => {
|
|
1347
1501
|
const relay = relayConnections.get(userId);
|
|
1348
1502
|
if (!relay || relay.ws.readyState !== 1) {
|
|
1349
|
-
reject(new Error('No relay connection. Run "
|
|
1503
|
+
reject(new Error('No relay connection. Run "uc connect" on your local machine.'));
|
|
1350
1504
|
return;
|
|
1351
1505
|
}
|
|
1352
1506
|
|
|
@@ -1367,6 +1521,95 @@ function sendRelayCommand(userId, action, payload, onStream = null, timeoutMs =
|
|
|
1367
1521
|
});
|
|
1368
1522
|
}
|
|
1369
1523
|
|
|
1524
|
+
/**
|
|
1525
|
+
* Check if a user has an active relay connection
|
|
1526
|
+
*/
|
|
1527
|
+
function hasActiveRelay(userId) {
|
|
1528
|
+
if (!userId) return false;
|
|
1529
|
+
const relay = relayConnections.get(Number(userId));
|
|
1530
|
+
return relay && relay.ws.readyState === 1;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Route a chat command through the user's relay connection to their local machine.
|
|
1535
|
+
* Translates relay-stream/relay-complete events into the format the frontend expects.
|
|
1536
|
+
*
|
|
1537
|
+
* @param {number} userId - User ID
|
|
1538
|
+
* @param {string} action - Relay action (claude-query, codex-query, cursor-query)
|
|
1539
|
+
* @param {object} data - Original command data from the browser
|
|
1540
|
+
* @param {object} writer - WebSocket writer to send events to browser
|
|
1541
|
+
* @param {object} eventMap - Maps relay stream data types to chat event types
|
|
1542
|
+
*/
|
|
1543
|
+
async function routeViaRelay(userId, action, data, writer, eventMap = {}) {
|
|
1544
|
+
const sessionId = data.options?.sessionId || `relay-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1545
|
+
|
|
1546
|
+
// Send session-created so the frontend can track this query
|
|
1547
|
+
writer.send({ type: 'session-created', sessionId });
|
|
1548
|
+
|
|
1549
|
+
// Determine event types from the provider
|
|
1550
|
+
const responseType = eventMap.response || 'claude-response';
|
|
1551
|
+
const completeType = eventMap.complete || 'claude-complete';
|
|
1552
|
+
const errorType = eventMap.error || 'claude-error';
|
|
1553
|
+
|
|
1554
|
+
let fullContent = '';
|
|
1555
|
+
|
|
1556
|
+
try {
|
|
1557
|
+
const result = await sendRelayCommand(
|
|
1558
|
+
Number(userId),
|
|
1559
|
+
action,
|
|
1560
|
+
{
|
|
1561
|
+
command: data.command,
|
|
1562
|
+
options: data.options || {}
|
|
1563
|
+
},
|
|
1564
|
+
// onStream callback — translates relay events to chat events
|
|
1565
|
+
(streamData) => {
|
|
1566
|
+
if (streamData.type === 'claude-response' || streamData.type === 'codex-response' || streamData.type === 'cursor-response') {
|
|
1567
|
+
fullContent += streamData.content || '';
|
|
1568
|
+
writer.send({
|
|
1569
|
+
type: responseType,
|
|
1570
|
+
data: {
|
|
1571
|
+
type: 'assistant',
|
|
1572
|
+
message: {
|
|
1573
|
+
type: 'text',
|
|
1574
|
+
text: streamData.content || ''
|
|
1575
|
+
}
|
|
1576
|
+
},
|
|
1577
|
+
sessionId
|
|
1578
|
+
});
|
|
1579
|
+
} else if (streamData.type === 'claude-error' || streamData.type === 'codex-error' || streamData.type === 'cursor-error') {
|
|
1580
|
+
writer.send({
|
|
1581
|
+
type: responseType,
|
|
1582
|
+
data: {
|
|
1583
|
+
type: 'assistant',
|
|
1584
|
+
message: {
|
|
1585
|
+
type: 'text',
|
|
1586
|
+
text: streamData.content || ''
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
sessionId
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
600000 // 10 minute timeout for AI queries
|
|
1594
|
+
);
|
|
1595
|
+
|
|
1596
|
+
// Send completion event
|
|
1597
|
+
writer.send({
|
|
1598
|
+
type: completeType,
|
|
1599
|
+
sessionId,
|
|
1600
|
+
exitCode: result?.exitCode ?? 0,
|
|
1601
|
+
isNewSession: !data.options?.sessionId,
|
|
1602
|
+
viaRelay: true
|
|
1603
|
+
});
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
writer.send({
|
|
1606
|
+
type: errorType,
|
|
1607
|
+
error: error.message,
|
|
1608
|
+
sessionId
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1370
1613
|
// Handle shell WebSocket connections
|
|
1371
1614
|
function handleShellConnection(ws) {
|
|
1372
1615
|
if (!pty) {
|
|
@@ -1707,9 +1950,13 @@ app.post('/api/transcribe', authenticateToken, async (req, res) => {
|
|
|
1707
1950
|
return res.status(400).json({ error: 'No audio file provided' });
|
|
1708
1951
|
}
|
|
1709
1952
|
|
|
1710
|
-
|
|
1953
|
+
// BYOK: check user's stored OpenAI key first, fall back to server env
|
|
1954
|
+
const userOpenaiKey = req.user?.id
|
|
1955
|
+
? await getUserProviderKey(req.user.id, 'openai_key')
|
|
1956
|
+
: null;
|
|
1957
|
+
const apiKey = userOpenaiKey || process.env.OPENAI_API_KEY;
|
|
1711
1958
|
if (!apiKey) {
|
|
1712
|
-
return res.status(500).json({ error: 'OpenAI API key not configured.
|
|
1959
|
+
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
1960
|
}
|
|
1714
1961
|
|
|
1715
1962
|
try {
|
|
@@ -1831,7 +2078,7 @@ Agent instructions:`;
|
|
|
1831
2078
|
|
|
1832
2079
|
} catch (error) {
|
|
1833
2080
|
console.error('Transcription error:', error);
|
|
1834
|
-
res.status(500).json({ error: error
|
|
2081
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
1835
2082
|
}
|
|
1836
2083
|
});
|
|
1837
2084
|
} catch (error) {
|
|
@@ -2222,6 +2469,18 @@ async function startServer() {
|
|
|
2222
2469
|
// Initialize authentication database
|
|
2223
2470
|
await initializeDatabase();
|
|
2224
2471
|
|
|
2472
|
+
// In local mode, ensure a default user exists (no signup needed)
|
|
2473
|
+
if (IS_LOCAL) {
|
|
2474
|
+
const hasUsers = await userDb.hasUsers();
|
|
2475
|
+
if (!hasUsers) {
|
|
2476
|
+
const localUsername = os.userInfo().username || 'local';
|
|
2477
|
+
const dummyHash = crypto.randomBytes(32).toString('hex');
|
|
2478
|
+
await userDb.createUser(localUsername, dummyHash);
|
|
2479
|
+
console.log(`${c.ok('[LOCAL]')} Created local user: ${c.bright(localUsername)}`);
|
|
2480
|
+
}
|
|
2481
|
+
console.log(`${c.info('[MODE]')} Running in ${c.bright('LOCAL')} mode (no login required)`);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2225
2484
|
// Check if running in production mode (dist folder exists OR NODE_ENV/RAILWAY set)
|
|
2226
2485
|
const distIndexPath = path.join(__dirname, '../dist/index.html');
|
|
2227
2486
|
const isProduction = fs.existsSync(distIndexPath) || process.env.NODE_ENV === 'production' || !!process.env.RAILWAY_ENVIRONMENT;
|