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.
- 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 +194 -21
- package/server/index.js +159 -107
- package/server/middleware/auth.js +9 -5
- package/server/openrouter.js +137 -0
- package/server/relay-client.js +104 -17
- package/server/routes/agent.js +54 -19
- package/server/routes/auth.js +23 -12
- 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,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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
836
|
-
const resolved = path.
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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: '
|
|
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
|
|
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
|
-
//
|
|
933
|
-
const resolved = path.
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1115
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
31
|
-
|
|
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
|
+
}
|