upfynai-code 2.4.0 → 2.5.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-CRld2UWX.js +513 -0
- package/client/dist/assets/CanvasPanel-CB4sweQq.js +34 -0
- package/client/dist/assets/CanvasPanel-WhZulBJw.css +1 -0
- package/client/dist/assets/DashboardPanel-BXaA-b9z.js +1 -0
- package/client/dist/assets/LoginModal-BwkvjfPR.js +19 -0
- package/client/dist/assets/{Onboarding-CtIoXiTp.js → Onboarding-2A_5fPxy.js} +1 -1
- package/client/dist/assets/{SetupForm-B4p8im5O.js → SetupForm-CH5EA5W0.js} +1 -1
- package/client/dist/assets/WorkflowsPanel-CO5g5yGG.js +1 -0
- package/client/dist/assets/{ar-SA-G6X2FPQ2-2gfmdvHk.js → ar-SA-G6X2FPQ2-DoJuo98H.js} +2 -2
- package/client/dist/assets/{arc-DCZSHhoJ.js → arc-B0wBaTeh.js} +1 -1
- package/client/dist/assets/az-AZ-76LH7QW2-xdrt1Z13.js +1 -0
- package/client/dist/assets/{bg-BG-XCXSNQG7-D6__XtOK.js → bg-BG-XCXSNQG7-D8NAiF6Y.js} +2 -2
- package/client/dist/assets/{blockDiagram-38ab4fdb-Cfbaeyp6.js → blockDiagram-38ab4fdb-DSnyKzK4.js} +2 -2
- package/client/dist/assets/{bn-BD-2XOGV67Q-DHNJw3OG.js → bn-BD-2XOGV67Q-B0qWv8_J.js} +2 -2
- package/client/dist/assets/{c4Diagram-3d4e48cf-BBCnjOTy.js → c4Diagram-3d4e48cf-DoZJ13XA.js} +2 -2
- package/client/dist/assets/{ca-ES-6MX7JW3Y-r5g4o3zQ.js → ca-ES-6MX7JW3Y-RgLhfbZZ.js} +3 -3
- package/client/dist/assets/channel-BmO6nY0W.js +1 -0
- package/client/dist/assets/classDiagram-70f12bd4-GNyDrRCk.js +2 -0
- package/client/dist/assets/classDiagram-v2-f2320105-CxdGhHm2.js +2 -0
- package/client/dist/assets/clone-xuHMqFoD.js +1 -0
- package/client/dist/assets/{createText-2e5e7dd3-B8jCDmF_.js → createText-2e5e7dd3-DiPywQOa.js} +1 -1
- package/client/dist/assets/{cs-CZ-2BRQDIVT-p08jRLRC.js → cs-CZ-2BRQDIVT-BAjmnuoC.js} +2 -2
- package/client/dist/assets/{da-DK-5WZEPLOC-CnhOImFf.js → da-DK-5WZEPLOC-JxKVGt8o.js} +2 -2
- package/client/dist/assets/{de-DE-XR44H4JA-BunSXZ-Y.js → de-DE-XR44H4JA-CrnRlt4z.js} +2 -2
- package/client/dist/assets/{edges-e0da2a9e-CGBBhG8k.js → edges-e0da2a9e-DDsXzXLJ.js} +1 -1
- package/client/dist/assets/{el-GR-BZB4AONW-D4wv1oIz.js → el-GR-BZB4AONW-DQd8iogq.js} +2 -2
- package/client/dist/assets/{erDiagram-9861fffd-CYaF3q1I.js → erDiagram-9861fffd-CBiCC4rl.js} +2 -2
- package/client/dist/assets/{es-ES-U4NZUMDT-CGeTKXgd.js → es-ES-U4NZUMDT-vvUblc5i.js} +2 -2
- package/client/dist/assets/{eu-ES-A7QVB2H4-Cayx1TxR.js → eu-ES-A7QVB2H4-De4NNCc1.js} +2 -2
- package/client/dist/assets/{fa-IR-HGAKTJCU-CmUg8pmw.js → fa-IR-HGAKTJCU-DFBXqIqq.js} +2 -2
- package/client/dist/assets/{fi-FI-Z5N7JZ37-xvHcPhsU.js → fi-FI-Z5N7JZ37-DV9zESPg.js} +2 -2
- package/client/dist/assets/{flowDb-956e92f1-C-_LFz70.js → flowDb-956e92f1-BhdSHbdO.js} +1 -1
- package/client/dist/assets/{flowDiagram-66a62f08-C1sHdSjn.js → flowDiagram-66a62f08-M-fp1_Ie.js} +2 -2
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-C5eiN8Pg.js +1 -0
- package/client/dist/assets/{flowchart-elk-definition-4a651766-CNGfpudb.js → flowchart-elk-definition-4a651766-Bp0SonQx.js} +2 -2
- package/client/dist/assets/{fr-FR-RHASNOE6-DBoHEcNj.js → fr-FR-RHASNOE6-CKTMXuGk.js} +2 -2
- package/client/dist/assets/ganttDiagram-c361ad54-iA737GUS.js +257 -0
- package/client/dist/assets/{gitGraphDiagram-72cf32ee-DojCDvlS.js → gitGraphDiagram-72cf32ee-BX-wj-PV.js} +2 -2
- package/client/dist/assets/{gl-ES-HMX3MZ6V-p6hrn2cN.js → gl-ES-HMX3MZ6V-Cdiqq4jY.js} +2 -2
- package/client/dist/assets/{graph-DXM7lcy1.js → graph-Rxkx3sEa.js} +1 -1
- package/client/dist/assets/{he-IL-6SHJWFNN-y2jEX6-0.js → he-IL-6SHJWFNN-gYmR5_KT.js} +2 -2
- package/client/dist/assets/{hi-IN-IWLTKZ5I-99pNfyWr.js → hi-IN-IWLTKZ5I-pyqK94AR.js} +2 -2
- package/client/dist/assets/{hu-HU-A5ZG7DT2-hygceGMS.js → hu-HU-A5ZG7DT2-DpacJgJy.js} +2 -2
- package/client/dist/assets/{id-ID-SAP4L64H-CyIqi1hv.js → id-ID-SAP4L64H-CAvIX-mj.js} +2 -2
- package/client/dist/assets/{index-3862675e-4idOQN2N.js → index-3862675e-BX3Fpn6V.js} +1 -1
- package/client/dist/assets/{index-BHZfFT_V.js → index-BBlwbHq_.js} +4 -4
- package/client/dist/assets/{index-BGmwbRlb.js → index-ClfzLIqY.js} +6 -6
- package/client/dist/assets/index-Td4UdtLF.css +1 -0
- package/client/dist/assets/{infoDiagram-f8f76790-CFLrHqtc.js → infoDiagram-f8f76790-Ckv8imiv.js} +2 -2
- package/client/dist/assets/{it-IT-JPQ66NNP-DzVvVdQI.js → it-IT-JPQ66NNP-BtpNRSce.js} +2 -2
- package/client/dist/assets/{ja-JP-DBVTYXUO-BI4fPexV.js → ja-JP-DBVTYXUO-CwJRyY6M.js} +2 -2
- package/client/dist/assets/{journeyDiagram-49397b02-C3CFDo8z.js → journeyDiagram-49397b02-DWWZssji.js} +2 -2
- package/client/dist/assets/kaa-6HZHGXH3-DIWQEb4A.js +1 -0
- package/client/dist/assets/{kab-KAB-ZGHBKWFO-DBI_ri48.js → kab-KAB-ZGHBKWFO-DjGbqhUg.js} +2 -2
- package/client/dist/assets/kk-KZ-P5N5QNE5-B_VzJdWf.js +1 -0
- package/client/dist/assets/{km-KH-HSX4SM5Z-DOMFSres.js → km-KH-HSX4SM5Z-DUD5mi0o.js} +2 -2
- package/client/dist/assets/{ko-KR-MTYHY66A-tb08hXzd.js → ko-KR-MTYHY66A--sDB10db.js} +3 -3
- package/client/dist/assets/{ku-TR-6OUDTVRD-DlIQCCY4.js → ku-TR-6OUDTVRD-CKvKrkcX.js} +2 -2
- package/client/dist/assets/{layout-B_11mCXA.js → layout-CkB7sSeq.js} +1 -1
- package/client/dist/assets/{line-B-qmK_vI.js → line-DC7MA9qY.js} +1 -1
- package/client/dist/assets/{linear-Ph6uuYcX.js → linear-C1lBBthf.js} +1 -1
- package/client/dist/assets/{lt-LT-XHIRWOB4--qWy24_Z.js → lt-LT-XHIRWOB4-MSZf7xYG.js} +2 -2
- package/client/dist/assets/{lv-LV-5QDEKY6T-Bnd_1GDb.js → lv-LV-5QDEKY6T-C-gvvmBB.js} +2 -2
- package/client/dist/assets/{mindmap-definition-fc14e90a-Do79tIc0.js → mindmap-definition-fc14e90a-B3O7hztq.js} +2 -2
- package/client/dist/assets/{mr-IN-CRQNXWMA-BsV6HaD9.js → mr-IN-CRQNXWMA-XHtBUWQH.js} +2 -2
- package/client/dist/assets/my-MM-5M5IBNSE-D9eD2edL.js +1 -0
- package/client/dist/assets/{nb-NO-T6EIAALU-Cvf9FdSF.js → nb-NO-T6EIAALU-BlImC6gp.js} +3 -3
- package/client/dist/assets/{nl-NL-IS3SIHDZ-DA1yqpXw.js → nl-NL-IS3SIHDZ-CPFhnaSP.js} +2 -2
- package/client/dist/assets/{nn-NO-6E72VCQL-89lm3vku.js → nn-NO-6E72VCQL-BMvoJSKQ.js} +2 -2
- package/client/dist/assets/{oc-FR-POXYY2M6-BsrjTJQh.js → oc-FR-POXYY2M6-Buye63LS.js} +2 -2
- package/client/dist/assets/{pa-IN-N4M65BXN-CczefYaj.js → pa-IN-N4M65BXN-D9uQ3niy.js} +2 -2
- package/client/dist/assets/{percentages-BXMCSKIN-Be6p9phi.js → percentages-BXMCSKIN-BzXIakGM.js} +7 -7
- package/client/dist/assets/{pieDiagram-8a3498a8-CfblQHdm.js → pieDiagram-8a3498a8-BU38mzx-.js} +3 -3
- package/client/dist/assets/{pl-PL-T2D74RX3-DdhH-zcK.js → pl-PL-T2D74RX3-BqM4xdcg.js} +2 -2
- package/client/dist/assets/{pt-BR-5N22H2LF-gpwlheL6.js → pt-BR-5N22H2LF-rAjrxGyI.js} +2 -2
- package/client/dist/assets/{pt-PT-UZXXM6DQ-Cs87vICi.js → pt-PT-UZXXM6DQ-DXsqcwLt.js} +2 -2
- package/client/dist/assets/{quadrantDiagram-120e2f19-CRMSamSP.js → quadrantDiagram-120e2f19-HhK4H1WU.js} +2 -2
- package/client/dist/assets/{requirementDiagram-deff3bca-D3LBN016.js → requirementDiagram-deff3bca-aDrcyj-A.js} +2 -2
- package/client/dist/assets/{ro-RO-JPDTUUEW-CWTSJ1Dt.js → ro-RO-JPDTUUEW-D_F9UKer.js} +2 -2
- package/client/dist/assets/{ru-RU-B4JR7IUQ-Bq7aN2ep.js → ru-RU-B4JR7IUQ-MirqN29p.js} +2 -2
- package/client/dist/assets/sankeyDiagram-04a897e0-C6ij7qbQ.js +8 -0
- package/client/dist/assets/{sequenceDiagram-704730f1-BRYXVDGX.js → sequenceDiagram-704730f1-C0EKO3th.js} +2 -2
- package/client/dist/assets/si-LK-N5RQ5JYF-DyZC3mkC.js +1 -0
- package/client/dist/assets/{sk-SK-C5VTKIMK-ByjKQzUb.js → sk-SK-C5VTKIMK-D-ksz-WY.js} +2 -2
- package/client/dist/assets/{sl-SI-NN7IZMDC-B8WCyMBU.js → sl-SI-NN7IZMDC-CknuYoQ1.js} +2 -2
- package/client/dist/assets/stateDiagram-587899a1-CYoq2VjL.js +1 -0
- package/client/dist/assets/stateDiagram-v2-d93cdb3a-C5lbp5px.js +1 -0
- package/client/dist/assets/{styles-6aaf32cf-Dr-lfIOW.js → styles-6aaf32cf-Dkfsk8gt.js} +1 -1
- package/client/dist/assets/{styles-9a916d00-DS4wRpL7.js → styles-9a916d00-CMYqtcEN.js} +1 -1
- package/client/dist/assets/{styles-c10674c1-nKRF6NrH.js → styles-c10674c1-Bp-5OlRU.js} +1 -1
- package/client/dist/assets/{subset-shared.chunk-KT79s7KG.js → subset-shared.chunk-kfIB1Zam.js} +3 -3
- package/client/dist/assets/subset-worker.chunk-DwQBgc4z.js +1 -0
- package/client/dist/assets/{sv-SE-XGPEYMSR-BiIPUVbv.js → sv-SE-XGPEYMSR-DwN13se1.js} +2 -2
- package/client/dist/assets/{svgDrawCommon-08f97a94-C3uP9PYr.js → svgDrawCommon-08f97a94-CEgCMqs4.js} +1 -1
- package/client/dist/assets/{ta-IN-2NMHFXQM-Cidadso2.js → ta-IN-2NMHFXQM-ejDfFhwa.js} +2 -2
- package/client/dist/assets/th-TH-HPSO5L25-Bqc90ZNn.js +2 -0
- package/client/dist/assets/{timeline-definition-85554ec2-BSsLsIgF.js → timeline-definition-85554ec2-BmGdKqG0.js} +2 -2
- package/client/dist/assets/{tr-TR-DEFEU3FU-DaFcI-KL.js → tr-TR-DEFEU3FU-CJvlPbcW.js} +2 -2
- package/client/dist/assets/{uk-UA-QMV73CPH-DkBW36St.js → uk-UA-QMV73CPH-D26-cbWL.js} +3 -3
- package/client/dist/assets/vendor-codemirror-D_s0aGBu.js +35 -0
- package/client/dist/assets/{vendor-icons-Dh9m_Ydt.js → vendor-icons-aNdOvTr_.js} +159 -119
- package/client/dist/assets/{vi-VN-M7AON7JQ-KrtfxOzl.js → vi-VN-M7AON7JQ-MbqIIwYM.js} +2 -2
- package/client/dist/assets/{xychartDiagram-e933f94c-CgNgZ4pp.js → xychartDiagram-e933f94c-gfcTauxU.js} +2 -2
- package/client/dist/assets/{zh-CN-LNUGB5OW-BQu12RoD.js → zh-CN-LNUGB5OW-BZSmhUdL.js} +3 -3
- package/client/dist/assets/zh-HK-E62DVLB3-BJqejpiX.js +1 -0
- package/client/dist/assets/{zh-TW-RAJ6MFWO-ffJWgVxn.js → zh-TW-RAJ6MFWO-BBXtV-Uz.js} +2 -2
- package/client/dist/index.html +3 -3
- package/package.json +5 -2
- package/server/cli.js +64 -5
- package/server/constants/config.js +29 -3
- package/server/database/auth.db +0 -0
- package/server/database/db.js +203 -1
- package/server/index.js +348 -48
- package/server/mcp-server.js +2 -1
- package/server/middleware/auth.js +20 -9
- package/server/projects.js +95 -202
- package/server/relay-client.js +205 -11
- package/server/routes/auth.js +6 -0
- package/server/routes/commands.js +1 -1
- package/server/routes/dashboard.js +52 -0
- package/server/routes/projects.js +38 -35
- package/server/routes/voice.js +198 -0
- package/server/routes/webhooks.js +166 -0
- package/server/routes/workflows.js +118 -0
- package/server/services/whisperService.js +84 -0
- package/server/services/workflowScheduler.js +186 -0
- package/client/dist/assets/AppContent-DTZ2FbvM.js +0 -513
- package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +0 -6
- package/client/dist/assets/CanvasPanel-q4HEqNtV.css +0 -1
- package/client/dist/assets/LoginModal-CWoFm0au.js +0 -19
- package/client/dist/assets/az-AZ-76LH7QW2-CDdeucRZ.js +0 -1
- package/client/dist/assets/channel-O3ovC0x9.js +0 -1
- package/client/dist/assets/classDiagram-70f12bd4-D0lhAcxU.js +0 -2
- package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +0 -2
- package/client/dist/assets/clone-BG9u7vLi.js +0 -1
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +0 -1
- package/client/dist/assets/ganttDiagram-c361ad54-B8HJQqjt.js +0 -257
- package/client/dist/assets/index-B8wwD_Xo.css +0 -1
- package/client/dist/assets/kaa-6HZHGXH3-fwOleoQB.js +0 -1
- package/client/dist/assets/kk-KZ-P5N5QNE5-zpl7uvyF.js +0 -1
- package/client/dist/assets/my-MM-5M5IBNSE-kZQURVIi.js +0 -1
- package/client/dist/assets/sankeyDiagram-04a897e0-CsFqOQZN.js +0 -8
- package/client/dist/assets/si-LK-N5RQ5JYF-BBjcNYQh.js +0 -1
- package/client/dist/assets/stateDiagram-587899a1-BHoy9LtD.js +0 -1
- package/client/dist/assets/stateDiagram-v2-d93cdb3a-BvMUA6bS.js +0 -1
- package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +0 -1
- package/client/dist/assets/th-TH-HPSO5L25-CFNnJwSv.js +0 -2
- package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +0 -20
- package/client/dist/assets/vendor-codemirror-rix45NST.js +0 -16
- package/client/dist/assets/zh-HK-E62DVLB3-zx9CvERq.js +0 -1
package/server/index.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Load environment variables before other imports execute
|
|
3
3
|
import './load-env.js';
|
|
4
|
+
|
|
5
|
+
// Strip Claude Code session markers so spawned CLI processes don't fail with
|
|
6
|
+
// "cannot be launched inside another Claude Code session" errors.
|
|
7
|
+
delete process.env.CLAUDECODE;
|
|
8
|
+
delete process.env.CLAUDE_CODE;
|
|
4
9
|
import crypto from 'crypto';
|
|
5
10
|
import fs from 'fs';
|
|
6
11
|
import path from 'path';
|
|
12
|
+
import jwt from 'jsonwebtoken';
|
|
7
13
|
import { fileURLToPath } from 'url';
|
|
8
14
|
import { dirname } from 'path';
|
|
9
15
|
|
|
@@ -70,9 +76,15 @@ import cliAuthRoutes from './routes/cli-auth.js';
|
|
|
70
76
|
import userRoutes from './routes/user.js';
|
|
71
77
|
import codexRoutes from './routes/codex.js';
|
|
72
78
|
import paymentRoutes from './routes/payments.js';
|
|
73
|
-
import
|
|
74
|
-
import
|
|
75
|
-
import
|
|
79
|
+
import webhookRoutes from './routes/webhooks.js';
|
|
80
|
+
import workflowRoutes from './routes/workflows.js';
|
|
81
|
+
import voiceRoutes from './routes/voice.js';
|
|
82
|
+
import dashboardRoutes from './routes/dashboard.js';
|
|
83
|
+
import { initScheduler } from './services/workflowScheduler.js';
|
|
84
|
+
import { initializeDatabase, relayTokensDb, subscriptionDb, credentialsDb, userDb } from './database/db.js';
|
|
85
|
+
import { validateApiKey, authenticateToken, authenticateWebSocket, JWT_SECRET } from './middleware/auth.js';
|
|
86
|
+
import { IS_PLATFORM, IS_LOCAL } from './constants/config.js';
|
|
87
|
+
import { execSync } from 'child_process';
|
|
76
88
|
|
|
77
89
|
// File system watchers for provider project/session folders
|
|
78
90
|
const PROVIDER_WATCH_PATHS = [
|
|
@@ -352,7 +364,13 @@ app.locals.wss = wss;
|
|
|
352
364
|
// Security headers — protect against common web attacks
|
|
353
365
|
app.use((req, res, next) => {
|
|
354
366
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
355
|
-
|
|
367
|
+
// Allow framing from our own frontend domains (Vercel embeds Railway in an iframe)
|
|
368
|
+
const allowedFrameOrigins = (process.env.CORS_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
369
|
+
if (allowedFrameOrigins.length > 0) {
|
|
370
|
+
res.setHeader('Content-Security-Policy', `frame-ancestors 'self' ${allowedFrameOrigins.join(' ')}`);
|
|
371
|
+
} else {
|
|
372
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
373
|
+
}
|
|
356
374
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
357
375
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
358
376
|
if (process.env.NODE_ENV === 'production') {
|
|
@@ -498,6 +516,10 @@ app.use('/api/codex', authenticateToken, codexRoutes);
|
|
|
498
516
|
|
|
499
517
|
// Payment & Subscription Routes (protected)
|
|
500
518
|
app.use('/api/payments', authenticateToken, paymentRoutes);
|
|
519
|
+
app.use('/api/webhooks', authenticateToken, webhookRoutes);
|
|
520
|
+
app.use('/api/workflows', authenticateToken, workflowRoutes);
|
|
521
|
+
app.use('/api/voice', authenticateToken, voiceRoutes);
|
|
522
|
+
app.use('/api/dashboard', authenticateToken, dashboardRoutes);
|
|
501
523
|
|
|
502
524
|
// Agent API Routes (uses API key authentication)
|
|
503
525
|
app.use('/api/agent', agentRoutes);
|
|
@@ -532,6 +554,10 @@ app.delete('/api/relay/tokens/:id', authenticateToken, async (req, res) => {
|
|
|
532
554
|
});
|
|
533
555
|
|
|
534
556
|
app.get('/api/relay/status', authenticateToken, (req, res) => {
|
|
557
|
+
// In local mode, always connected — SDK runs directly on this machine
|
|
558
|
+
if (IS_LOCAL) {
|
|
559
|
+
return res.json({ connected: true, local: true, connectedAt: Date.now() });
|
|
560
|
+
}
|
|
535
561
|
const relay = relayConnections.get(Number(req.user.id));
|
|
536
562
|
res.json({
|
|
537
563
|
connected: !!(relay && relay.ws.readyState === 1),
|
|
@@ -539,12 +565,70 @@ app.get('/api/relay/status', authenticateToken, (req, res) => {
|
|
|
539
565
|
});
|
|
540
566
|
});
|
|
541
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Detect installed AI CLI agents on the local machine (server-side).
|
|
570
|
+
* Used in self-hosted/local mode where no relay is needed.
|
|
571
|
+
*/
|
|
572
|
+
let cachedLocalAgents = null;
|
|
573
|
+
let localAgentsCacheTime = 0;
|
|
574
|
+
function detectLocalAgents() {
|
|
575
|
+
// Cache for 60 seconds
|
|
576
|
+
if (cachedLocalAgents && Date.now() - localAgentsCacheTime < 60000) {
|
|
577
|
+
return cachedLocalAgents;
|
|
578
|
+
}
|
|
579
|
+
const isWindows = process.platform === 'win32';
|
|
580
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
581
|
+
const agents = [
|
|
582
|
+
{ name: 'claude', binary: 'claude', label: 'Claude Code' },
|
|
583
|
+
{ name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
|
|
584
|
+
{ name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
|
|
585
|
+
];
|
|
586
|
+
const detected = {};
|
|
587
|
+
for (const agent of agents) {
|
|
588
|
+
try {
|
|
589
|
+
const result = execSync(`${whichCmd} ${agent.binary}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
590
|
+
detected[agent.name] = { installed: true, path: result.split('\n')[0].trim(), label: agent.label };
|
|
591
|
+
} catch {
|
|
592
|
+
detected[agent.name] = { installed: false, label: agent.label };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
cachedLocalAgents = detected;
|
|
596
|
+
localAgentsCacheTime = Date.now();
|
|
597
|
+
return detected;
|
|
598
|
+
}
|
|
599
|
+
|
|
542
600
|
// Connection status — alias at path the frontend expects
|
|
543
601
|
app.get('/api/auth/connection-status', authenticateToken, (req, res) => {
|
|
544
602
|
const relay = relayConnections.get(Number(req.user.id));
|
|
603
|
+
const connected = !!(relay && relay.ws.readyState === 1);
|
|
604
|
+
|
|
605
|
+
// In local mode, always "connected" — SDK runs directly on this machine
|
|
606
|
+
if (IS_LOCAL) {
|
|
607
|
+
const agents = detectLocalAgents();
|
|
608
|
+
return res.json({
|
|
609
|
+
connected: true,
|
|
610
|
+
local: true,
|
|
611
|
+
connectedAt: Date.now(),
|
|
612
|
+
agents,
|
|
613
|
+
machine: {
|
|
614
|
+
hostname: os.hostname(),
|
|
615
|
+
platform: process.platform,
|
|
616
|
+
cwd: process.cwd(),
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
545
621
|
res.json({
|
|
546
|
-
connected
|
|
547
|
-
|
|
622
|
+
connected,
|
|
623
|
+
local: false,
|
|
624
|
+
connectedAt: relay?.connectedAt || null,
|
|
625
|
+
agents: connected ? (relay.agents || null) : null,
|
|
626
|
+
machine: connected ? {
|
|
627
|
+
hostname: relay.machine,
|
|
628
|
+
platform: relay.platform,
|
|
629
|
+
cwd: relay.cwd,
|
|
630
|
+
version: relay.version,
|
|
631
|
+
} : null
|
|
548
632
|
});
|
|
549
633
|
});
|
|
550
634
|
|
|
@@ -1128,35 +1212,51 @@ function handleChatConnection(ws, request) {
|
|
|
1128
1212
|
}
|
|
1129
1213
|
if (sid) lockedSessionsForThisWs.add(sid);
|
|
1130
1214
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1215
|
+
// Check if user has active relay → route to local machine
|
|
1216
|
+
if (hasActiveRelay(wsUser?.userId)) {
|
|
1217
|
+
await routeViaRelay(wsUser.userId, 'claude-query', data, writer, {
|
|
1218
|
+
response: 'claude-response',
|
|
1219
|
+
complete: 'claude-complete',
|
|
1220
|
+
error: 'claude-error'
|
|
1221
|
+
});
|
|
1222
|
+
} else {
|
|
1223
|
+
// Fall back to server-side SDK
|
|
1224
|
+
const userAnthropicKey = wsUser?.userId
|
|
1225
|
+
? await getUserProviderKey(wsUser.userId, 'anthropic_key')
|
|
1226
|
+
: null;
|
|
1227
|
+
|
|
1228
|
+
await withUserApiKey('ANTHROPIC_API_KEY', userAnthropicKey, () =>
|
|
1229
|
+
queryClaudeSDK(data.command, data.options, writer)
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1142
1232
|
} else if (data.type === 'cursor-command') {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1233
|
+
// Check if user has active relay → route to local machine
|
|
1234
|
+
if (hasActiveRelay(wsUser?.userId)) {
|
|
1235
|
+
await routeViaRelay(wsUser.userId, 'cursor-query', data, writer, {
|
|
1236
|
+
response: 'cursor-response',
|
|
1237
|
+
complete: 'cursor-complete',
|
|
1238
|
+
error: 'cursor-error'
|
|
1239
|
+
});
|
|
1240
|
+
} else {
|
|
1241
|
+
await spawnCursor(data.command, data.options, writer);
|
|
1242
|
+
}
|
|
1147
1243
|
} else if (data.type === 'codex-command') {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1244
|
+
// Check if user has active relay → route to local machine
|
|
1245
|
+
if (hasActiveRelay(wsUser?.userId)) {
|
|
1246
|
+
await routeViaRelay(wsUser.userId, 'codex-query', data, writer, {
|
|
1247
|
+
response: 'codex-response',
|
|
1248
|
+
complete: 'codex-complete',
|
|
1249
|
+
error: 'codex-error'
|
|
1250
|
+
});
|
|
1251
|
+
} else {
|
|
1252
|
+
const userOpenaiKey = wsUser?.userId
|
|
1253
|
+
? await getUserProviderKey(wsUser.userId, 'openai_key')
|
|
1254
|
+
: null;
|
|
1156
1255
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1256
|
+
await withUserApiKey('OPENAI_API_KEY', userOpenaiKey, () =>
|
|
1257
|
+
queryCodex(data.command, data.options, writer)
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1160
1260
|
} else if (data.type === 'openrouter-command') {
|
|
1161
1261
|
console.log('[DEBUG] OpenRouter message:', data.command?.slice(0, 60) || '[empty]');
|
|
1162
1262
|
console.log('🤖 Model:', data.options?.model || OPENROUTER_MODELS.DEFAULT);
|
|
@@ -1288,12 +1388,21 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1288
1388
|
const userId = Number(tokenData.user_id);
|
|
1289
1389
|
const username = tokenData.username;
|
|
1290
1390
|
|
|
1291
|
-
// Extract optional
|
|
1391
|
+
// Extract optional headers from relay handshake
|
|
1292
1392
|
const anthropicApiKey = request?.headers?.['x-anthropic-api-key'] || null;
|
|
1393
|
+
const relayVersion = request?.headers?.['x-upfyn-version'] || null;
|
|
1394
|
+
const relayMachine = request?.headers?.['x-upfyn-machine'] || null;
|
|
1395
|
+
const relayPlatform = request?.headers?.['x-upfyn-platform'] || null;
|
|
1396
|
+
const relayCwd = request?.headers?.['x-upfyn-cwd'] || null;
|
|
1293
1397
|
|
|
1294
1398
|
// Store relay connection with API key in memory only (use Number() for consistent Map key type)
|
|
1295
1399
|
// API key is held per-user in the relay connection, NOT in process.env
|
|
1296
|
-
relayConnections.set(userId, {
|
|
1400
|
+
relayConnections.set(userId, {
|
|
1401
|
+
ws, user: tokenData, connectedAt: Date.now(), anthropicApiKey,
|
|
1402
|
+
version: relayVersion, machine: relayMachine, platform: relayPlatform, cwd: relayCwd,
|
|
1403
|
+
agents: null, // populated when client sends agent-capabilities
|
|
1404
|
+
lastPong: Date.now(),
|
|
1405
|
+
});
|
|
1297
1406
|
|
|
1298
1407
|
ws.send(JSON.stringify({
|
|
1299
1408
|
type: 'relay-connected',
|
|
@@ -1344,8 +1453,33 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1344
1453
|
return;
|
|
1345
1454
|
}
|
|
1346
1455
|
|
|
1456
|
+
// Agent capabilities report from relay client
|
|
1457
|
+
if (data.type === 'agent-capabilities') {
|
|
1458
|
+
const relay = relayConnections.get(userId);
|
|
1459
|
+
if (relay) {
|
|
1460
|
+
relay.agents = data.agents || {};
|
|
1461
|
+
relay.machine = data.machine || relay.machine;
|
|
1462
|
+
}
|
|
1463
|
+
// Broadcast agent info to browser clients
|
|
1464
|
+
for (const client of connectedClients) {
|
|
1465
|
+
try {
|
|
1466
|
+
if (client.readyState === 1) {
|
|
1467
|
+
client.send(JSON.stringify({
|
|
1468
|
+
type: 'relay-agents',
|
|
1469
|
+
userId,
|
|
1470
|
+
agents: data.agents || {},
|
|
1471
|
+
machine: data.machine || {}
|
|
1472
|
+
}));
|
|
1473
|
+
}
|
|
1474
|
+
} catch (e) { /* ignore */ }
|
|
1475
|
+
}
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1347
1479
|
// Heartbeat
|
|
1348
1480
|
if (data.type === 'ping') {
|
|
1481
|
+
const relay = relayConnections.get(userId);
|
|
1482
|
+
if (relay) relay.lastPong = Date.now();
|
|
1349
1483
|
ws.send(JSON.stringify({ type: 'pong' }));
|
|
1350
1484
|
return;
|
|
1351
1485
|
}
|
|
@@ -1354,7 +1488,29 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1354
1488
|
}
|
|
1355
1489
|
});
|
|
1356
1490
|
|
|
1491
|
+
// Server-side heartbeat: ping relay client every 45s, terminate if no pong in 90s
|
|
1492
|
+
const relayHeartbeat = setInterval(() => {
|
|
1493
|
+
const relay = relayConnections.get(userId);
|
|
1494
|
+
if (!relay || relay.ws !== ws) {
|
|
1495
|
+
clearInterval(relayHeartbeat);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
// If no ping received from client in 90s, consider connection stale
|
|
1499
|
+
if (Date.now() - relay.lastPong > 90000) {
|
|
1500
|
+
clearInterval(relayHeartbeat);
|
|
1501
|
+
ws.terminate();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
// Send server-side ping to keep connection alive through proxies
|
|
1505
|
+
try {
|
|
1506
|
+
if (ws.readyState === 1) {
|
|
1507
|
+
ws.send(JSON.stringify({ type: 'server-ping' }));
|
|
1508
|
+
}
|
|
1509
|
+
} catch { /* ignore */ }
|
|
1510
|
+
}, 45000);
|
|
1511
|
+
|
|
1357
1512
|
ws.on('close', () => {
|
|
1513
|
+
clearInterval(relayHeartbeat);
|
|
1358
1514
|
relayConnections.delete(userId);
|
|
1359
1515
|
// Clean up pending requests for this user
|
|
1360
1516
|
for (const [reqId, pending] of pendingRelayRequests) {
|
|
@@ -1364,7 +1520,6 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1364
1520
|
pendingRelayRequests.delete(reqId);
|
|
1365
1521
|
}
|
|
1366
1522
|
}
|
|
1367
|
-
// relay disconnected
|
|
1368
1523
|
|
|
1369
1524
|
// Broadcast relay status
|
|
1370
1525
|
for (const client of connectedClients) {
|
|
@@ -1376,8 +1531,8 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1376
1531
|
}
|
|
1377
1532
|
});
|
|
1378
1533
|
|
|
1379
|
-
ws.on('error', (
|
|
1380
|
-
|
|
1534
|
+
ws.on('error', () => {
|
|
1535
|
+
clearInterval(relayHeartbeat);
|
|
1381
1536
|
});
|
|
1382
1537
|
}
|
|
1383
1538
|
|
|
@@ -1394,7 +1549,7 @@ function sendRelayCommand(userId, action, payload, onStream = null, timeoutMs =
|
|
|
1394
1549
|
return new Promise((resolve, reject) => {
|
|
1395
1550
|
const relay = relayConnections.get(userId);
|
|
1396
1551
|
if (!relay || relay.ws.readyState !== 1) {
|
|
1397
|
-
reject(new Error('No relay connection. Run "
|
|
1552
|
+
reject(new Error('No relay connection. Run "uc connect" on your local machine.'));
|
|
1398
1553
|
return;
|
|
1399
1554
|
}
|
|
1400
1555
|
|
|
@@ -1415,6 +1570,99 @@ function sendRelayCommand(userId, action, payload, onStream = null, timeoutMs =
|
|
|
1415
1570
|
});
|
|
1416
1571
|
}
|
|
1417
1572
|
|
|
1573
|
+
/**
|
|
1574
|
+
* Check if a user has an active relay connection
|
|
1575
|
+
*/
|
|
1576
|
+
function hasActiveRelay(userId) {
|
|
1577
|
+
if (!userId) return false;
|
|
1578
|
+
const relay = relayConnections.get(Number(userId));
|
|
1579
|
+
return relay && relay.ws.readyState === 1;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* Route a chat command through the user's relay connection to their local machine.
|
|
1584
|
+
* Translates relay-stream/relay-complete events into the format the frontend expects.
|
|
1585
|
+
*
|
|
1586
|
+
* @param {number} userId - User ID
|
|
1587
|
+
* @param {string} action - Relay action (claude-query, codex-query, cursor-query)
|
|
1588
|
+
* @param {object} data - Original command data from the browser
|
|
1589
|
+
* @param {object} writer - WebSocket writer to send events to browser
|
|
1590
|
+
* @param {object} eventMap - Maps relay stream data types to chat event types
|
|
1591
|
+
*/
|
|
1592
|
+
async function routeViaRelay(userId, action, data, writer, eventMap = {}) {
|
|
1593
|
+
const sessionId = data.options?.sessionId || `relay-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1594
|
+
|
|
1595
|
+
// Send session-created so the frontend can track this query
|
|
1596
|
+
writer.send({ type: 'session-created', sessionId });
|
|
1597
|
+
|
|
1598
|
+
// Determine event types from the provider
|
|
1599
|
+
const responseType = eventMap.response || 'claude-response';
|
|
1600
|
+
const completeType = eventMap.complete || 'claude-complete';
|
|
1601
|
+
const errorType = eventMap.error || 'claude-error';
|
|
1602
|
+
|
|
1603
|
+
let fullContent = '';
|
|
1604
|
+
|
|
1605
|
+
try {
|
|
1606
|
+
const result = await sendRelayCommand(
|
|
1607
|
+
Number(userId),
|
|
1608
|
+
action,
|
|
1609
|
+
{
|
|
1610
|
+
command: data.command,
|
|
1611
|
+
options: data.options || {}
|
|
1612
|
+
},
|
|
1613
|
+
// onStream callback — translates relay events to chat events
|
|
1614
|
+
(streamData) => {
|
|
1615
|
+
if (streamData.type === 'claude-response' || streamData.type === 'codex-response' || streamData.type === 'cursor-response') {
|
|
1616
|
+
fullContent += streamData.content || '';
|
|
1617
|
+
writer.send({
|
|
1618
|
+
type: responseType,
|
|
1619
|
+
data: {
|
|
1620
|
+
type: 'assistant',
|
|
1621
|
+
message: {
|
|
1622
|
+
type: 'text',
|
|
1623
|
+
text: streamData.content || ''
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
sessionId
|
|
1627
|
+
});
|
|
1628
|
+
} else if (streamData.type === 'claude-error' || streamData.type === 'codex-error' || streamData.type === 'cursor-error') {
|
|
1629
|
+
writer.send({
|
|
1630
|
+
type: responseType,
|
|
1631
|
+
data: {
|
|
1632
|
+
type: 'assistant',
|
|
1633
|
+
message: {
|
|
1634
|
+
type: 'text',
|
|
1635
|
+
text: streamData.content || ''
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
sessionId
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
},
|
|
1642
|
+
600000 // 10 minute timeout for AI queries
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
// Send completion event
|
|
1646
|
+
writer.send({
|
|
1647
|
+
type: completeType,
|
|
1648
|
+
sessionId,
|
|
1649
|
+
exitCode: result?.exitCode ?? 0,
|
|
1650
|
+
isNewSession: !data.options?.sessionId,
|
|
1651
|
+
viaRelay: true
|
|
1652
|
+
});
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
const isRelayLost = error.message?.includes('Relay disconnected') || error.message?.includes('No relay connection') || error.message?.includes('Relay request timed out');
|
|
1655
|
+
writer.send({
|
|
1656
|
+
type: errorType,
|
|
1657
|
+
error: isRelayLost
|
|
1658
|
+
? 'Your machine disconnected. Please reconnect with "uc connect" and try again.'
|
|
1659
|
+
: error.message,
|
|
1660
|
+
sessionId,
|
|
1661
|
+
relayDisconnected: isRelayLost
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1418
1666
|
// Handle shell WebSocket connections
|
|
1419
1667
|
function handleShellConnection(ws) {
|
|
1420
1668
|
if (!pty) {
|
|
@@ -1440,6 +1688,7 @@ function handleShellConnection(ws) {
|
|
|
1440
1688
|
const provider = data.provider || 'claude';
|
|
1441
1689
|
const initialCommand = data.initialCommand;
|
|
1442
1690
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
1691
|
+
const shellType = data.shellType || null;
|
|
1443
1692
|
urlDetectionBuffer = '';
|
|
1444
1693
|
announcedAuthUrls.clear();
|
|
1445
1694
|
|
|
@@ -1521,11 +1770,17 @@ function handleShellConnection(ws) {
|
|
|
1521
1770
|
// Prepare the shell command adapted to the platform and provider
|
|
1522
1771
|
let shellCommand;
|
|
1523
1772
|
if (isPlainShell) {
|
|
1524
|
-
// Plain shell mode -
|
|
1525
|
-
|
|
1526
|
-
|
|
1773
|
+
// Plain shell mode - run initial command or open interactive shell
|
|
1774
|
+
const usesPowerShell = !shellType || shellType === 'powershell';
|
|
1775
|
+
if (initialCommand) {
|
|
1776
|
+
if (usesPowerShell) {
|
|
1777
|
+
shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
|
|
1778
|
+
} else {
|
|
1779
|
+
shellCommand = `cd "${projectPath}" && ${initialCommand}`;
|
|
1780
|
+
}
|
|
1527
1781
|
} else {
|
|
1528
|
-
|
|
1782
|
+
// Interactive shell tab — spawn shell directly (no command wrapper)
|
|
1783
|
+
shellCommand = null;
|
|
1529
1784
|
}
|
|
1530
1785
|
} else if (provider === 'cursor') {
|
|
1531
1786
|
// Use cursor-agent command
|
|
@@ -1563,9 +1818,19 @@ function handleShellConnection(ws) {
|
|
|
1563
1818
|
|
|
1564
1819
|
console.log('🔧 Executing shell command:', shellCommand);
|
|
1565
1820
|
|
|
1566
|
-
// Use appropriate shell based on platform
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1821
|
+
// Use appropriate shell based on platform and requested shellType
|
|
1822
|
+
const shellMap = {
|
|
1823
|
+
'powershell': { cmd: 'powershell.exe', args: ['-Command'] },
|
|
1824
|
+
'cmd': { cmd: 'cmd.exe', args: ['/c'] },
|
|
1825
|
+
'bash': { cmd: 'bash', args: ['-c'] },
|
|
1826
|
+
};
|
|
1827
|
+
const defaultShell = os.platform() === 'win32'
|
|
1828
|
+
? { cmd: 'powershell.exe', args: ['-Command'] }
|
|
1829
|
+
: { cmd: 'bash', args: ['-c'] };
|
|
1830
|
+
const selectedShell = (shellType && shellMap[shellType]) || defaultShell;
|
|
1831
|
+
const shell = selectedShell.cmd;
|
|
1832
|
+
// If shellCommand is null, spawn an interactive shell with no args
|
|
1833
|
+
const shellArgs = shellCommand ? [...selectedShell.args, shellCommand] : [];
|
|
1569
1834
|
|
|
1570
1835
|
// Use terminal dimensions from client if provided, otherwise use defaults
|
|
1571
1836
|
const termCols = data.cols || 80;
|
|
@@ -1576,7 +1841,7 @@ function handleShellConnection(ws) {
|
|
|
1576
1841
|
name: 'xterm-256color',
|
|
1577
1842
|
cols: termCols,
|
|
1578
1843
|
rows: termRows,
|
|
1579
|
-
cwd: os.homedir(),
|
|
1844
|
+
cwd: shellCommand ? os.homedir() : projectPath,
|
|
1580
1845
|
env: {
|
|
1581
1846
|
...process.env,
|
|
1582
1847
|
TERM: 'xterm-256color',
|
|
@@ -2165,9 +2430,29 @@ app.get('*', (req, res) => {
|
|
|
2165
2430
|
return res.status(404).send('Not found');
|
|
2166
2431
|
}
|
|
2167
2432
|
|
|
2433
|
+
// If a JWT token is in the query param and no session cookie exists,
|
|
2434
|
+
// set the cookie now so the client-side AuthContext can authenticate on subsequent API calls.
|
|
2435
|
+
if (req.query?.token && !req.cookies?.session) {
|
|
2436
|
+
try {
|
|
2437
|
+
const decoded = jwt.verify(req.query.token, JWT_SECRET);
|
|
2438
|
+
if (decoded?.userId) {
|
|
2439
|
+
const isSecure = process.env.NODE_ENV === 'production' || !!process.env.RAILWAY_ENVIRONMENT;
|
|
2440
|
+
res.cookie('session', req.query.token, {
|
|
2441
|
+
httpOnly: true,
|
|
2442
|
+
secure: isSecure,
|
|
2443
|
+
sameSite: isSecure ? 'none' : 'strict',
|
|
2444
|
+
maxAge: 30 * 24 * 60 * 60 * 1000,
|
|
2445
|
+
path: '/',
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
} catch (e) {
|
|
2449
|
+
// Invalid token — just serve the page without setting cookie
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2168
2453
|
// Only serve index.html for HTML routes, not for static assets
|
|
2169
2454
|
// Static assets should already be handled by express.static middleware above
|
|
2170
|
-
const indexPath = path.join(__dirname, '../dist/index.html');
|
|
2455
|
+
const indexPath = path.join(__dirname, '../client/dist/index.html');
|
|
2171
2456
|
|
|
2172
2457
|
// Check if dist/index.html exists (production build available)
|
|
2173
2458
|
if (fs.existsSync(indexPath)) {
|
|
@@ -2274,8 +2559,20 @@ async function startServer() {
|
|
|
2274
2559
|
// Initialize authentication database
|
|
2275
2560
|
await initializeDatabase();
|
|
2276
2561
|
|
|
2562
|
+
// In local mode, ensure a default user exists (no signup needed)
|
|
2563
|
+
if (IS_LOCAL) {
|
|
2564
|
+
const hasUsers = await userDb.hasUsers();
|
|
2565
|
+
if (!hasUsers) {
|
|
2566
|
+
const localUsername = os.userInfo().username || 'local';
|
|
2567
|
+
const dummyHash = crypto.randomBytes(32).toString('hex');
|
|
2568
|
+
await userDb.createUser(localUsername, dummyHash);
|
|
2569
|
+
console.log(`${c.ok('[LOCAL]')} Created local user: ${c.bright(localUsername)}`);
|
|
2570
|
+
}
|
|
2571
|
+
console.log(`${c.info('[MODE]')} Running in ${c.bright('LOCAL')} mode (no login required)`);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2277
2574
|
// Check if running in production mode (dist folder exists OR NODE_ENV/RAILWAY set)
|
|
2278
|
-
const distIndexPath = path.join(__dirname, '../dist/index.html');
|
|
2575
|
+
const distIndexPath = path.join(__dirname, '../client/dist/index.html');
|
|
2279
2576
|
const isProduction = fs.existsSync(distIndexPath) || process.env.NODE_ENV === 'production' || !!process.env.RAILWAY_ENVIRONMENT;
|
|
2280
2577
|
|
|
2281
2578
|
// Log Claude implementation mode
|
|
@@ -2298,6 +2595,9 @@ async function startServer() {
|
|
|
2298
2595
|
console.log(`${c.info('[INFO]')} MCP Server: ${c.bright('http://0.0.0.0:' + PORT + '/mcp')}`);
|
|
2299
2596
|
console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
|
|
2300
2597
|
console.log(`${c.tip('[TIP]')} Run "uc status" for full configuration details`);
|
|
2598
|
+
|
|
2599
|
+
// Start workflow cron scheduler
|
|
2600
|
+
initScheduler().catch(err => console.warn('[Scheduler]', err.message));
|
|
2301
2601
|
console.log('');
|
|
2302
2602
|
|
|
2303
2603
|
// Start watching the projects folder for changes (skip on Vercel)
|
package/server/mcp-server.js
CHANGED
|
@@ -31,7 +31,8 @@ import crypto from 'crypto';
|
|
|
31
31
|
import jwt from 'jsonwebtoken';
|
|
32
32
|
import { userDb, apiKeysDb, relayTokensDb } from './database/db.js';
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
import { IS_PLATFORM } from './constants/config.js';
|
|
35
|
+
const JWT_SECRET = process.env.JWT_SECRET?.trim() || (IS_PLATFORM ? crypto.randomBytes(32).toString('hex') : (() => { throw new Error('JWT_SECRET required'); })());
|
|
35
36
|
|
|
36
37
|
// In-memory canvas state (Excalidraw elements, synced via WebSocket with browser clients)
|
|
37
38
|
let canvasElements = [];
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
|
+
import crypto from 'crypto';
|
|
2
3
|
import { userDb, relayTokensDb } from '../database/db.js';
|
|
3
4
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
let JWT_SECRET = process.env.JWT_SECRET?.trim();
|
|
6
7
|
if (!JWT_SECRET) {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
if (IS_PLATFORM) {
|
|
9
|
+
// In local/self-hosted mode, generate a random secret (auth is bypassed anyway)
|
|
10
|
+
JWT_SECRET = crypto.randomBytes(32).toString('hex');
|
|
11
|
+
} else {
|
|
12
|
+
console.error('[SECURITY] JWT_SECRET environment variable is required. Server cannot start without it.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
// Optional static API key middleware
|
|
@@ -27,10 +33,8 @@ const extractToken = (req) => {
|
|
|
27
33
|
const authHeader = req.headers['authorization'];
|
|
28
34
|
if (authHeader?.startsWith('Bearer ')) return authHeader.slice(7);
|
|
29
35
|
|
|
30
|
-
// 3. Query param —
|
|
31
|
-
|
|
32
|
-
if (req.query?.token && req.method === 'GET' &&
|
|
33
|
-
(req.headers.accept || '').includes('text/event-stream')) {
|
|
36
|
+
// 3. Query param — for GET requests (SSE EventSource + iframe embedding)
|
|
37
|
+
if (req.query?.token && req.method === 'GET') {
|
|
34
38
|
return req.query.token;
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -62,6 +66,10 @@ const authenticateToken = async (req, res, next) => {
|
|
|
62
66
|
const user = await userDb.getUserById(decoded.userId);
|
|
63
67
|
if (!user) return res.status(401).json({ error: 'Invalid token. User not found.' });
|
|
64
68
|
req.user = user;
|
|
69
|
+
// If token came from query param, set session cookie for subsequent requests (iframe auto-auth)
|
|
70
|
+
if (req.query?.token && !req.cookies?.session) {
|
|
71
|
+
res.cookie('session', token, COOKIE_OPTIONS);
|
|
72
|
+
}
|
|
65
73
|
next();
|
|
66
74
|
} catch (error) {
|
|
67
75
|
return res.status(403).json({ error: 'Invalid or expired token' });
|
|
@@ -79,10 +87,13 @@ const generateToken = (user) => {
|
|
|
79
87
|
|
|
80
88
|
// Cookie config for httpOnly session
|
|
81
89
|
// Works for both self-hosted (same origin) and split deploy (Vercel proxy → Railway)
|
|
90
|
+
const isSecureEnv = process.env.NODE_ENV === 'production' || !!process.env.VERCEL || !!process.env.RAILWAY_ENVIRONMENT;
|
|
82
91
|
const COOKIE_OPTIONS = {
|
|
83
92
|
httpOnly: true,
|
|
84
|
-
secure:
|
|
85
|
-
|
|
93
|
+
secure: isSecureEnv,
|
|
94
|
+
// 'none' required for cross-origin iframe embedding (Vercel frontend → Railway backend)
|
|
95
|
+
// 'strict' used in local/dev mode where everything is same-origin
|
|
96
|
+
sameSite: isSecureEnv ? 'none' : 'strict',
|
|
86
97
|
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
87
98
|
path: '/',
|
|
88
99
|
};
|