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.
Files changed (150) hide show
  1. package/client/dist/assets/AppContent-CRld2UWX.js +513 -0
  2. package/client/dist/assets/CanvasPanel-CB4sweQq.js +34 -0
  3. package/client/dist/assets/CanvasPanel-WhZulBJw.css +1 -0
  4. package/client/dist/assets/DashboardPanel-BXaA-b9z.js +1 -0
  5. package/client/dist/assets/LoginModal-BwkvjfPR.js +19 -0
  6. package/client/dist/assets/{Onboarding-CtIoXiTp.js → Onboarding-2A_5fPxy.js} +1 -1
  7. package/client/dist/assets/{SetupForm-B4p8im5O.js → SetupForm-CH5EA5W0.js} +1 -1
  8. package/client/dist/assets/WorkflowsPanel-CO5g5yGG.js +1 -0
  9. package/client/dist/assets/{ar-SA-G6X2FPQ2-2gfmdvHk.js → ar-SA-G6X2FPQ2-DoJuo98H.js} +2 -2
  10. package/client/dist/assets/{arc-DCZSHhoJ.js → arc-B0wBaTeh.js} +1 -1
  11. package/client/dist/assets/az-AZ-76LH7QW2-xdrt1Z13.js +1 -0
  12. package/client/dist/assets/{bg-BG-XCXSNQG7-D6__XtOK.js → bg-BG-XCXSNQG7-D8NAiF6Y.js} +2 -2
  13. package/client/dist/assets/{blockDiagram-38ab4fdb-Cfbaeyp6.js → blockDiagram-38ab4fdb-DSnyKzK4.js} +2 -2
  14. package/client/dist/assets/{bn-BD-2XOGV67Q-DHNJw3OG.js → bn-BD-2XOGV67Q-B0qWv8_J.js} +2 -2
  15. package/client/dist/assets/{c4Diagram-3d4e48cf-BBCnjOTy.js → c4Diagram-3d4e48cf-DoZJ13XA.js} +2 -2
  16. package/client/dist/assets/{ca-ES-6MX7JW3Y-r5g4o3zQ.js → ca-ES-6MX7JW3Y-RgLhfbZZ.js} +3 -3
  17. package/client/dist/assets/channel-BmO6nY0W.js +1 -0
  18. package/client/dist/assets/classDiagram-70f12bd4-GNyDrRCk.js +2 -0
  19. package/client/dist/assets/classDiagram-v2-f2320105-CxdGhHm2.js +2 -0
  20. package/client/dist/assets/clone-xuHMqFoD.js +1 -0
  21. package/client/dist/assets/{createText-2e5e7dd3-B8jCDmF_.js → createText-2e5e7dd3-DiPywQOa.js} +1 -1
  22. package/client/dist/assets/{cs-CZ-2BRQDIVT-p08jRLRC.js → cs-CZ-2BRQDIVT-BAjmnuoC.js} +2 -2
  23. package/client/dist/assets/{da-DK-5WZEPLOC-CnhOImFf.js → da-DK-5WZEPLOC-JxKVGt8o.js} +2 -2
  24. package/client/dist/assets/{de-DE-XR44H4JA-BunSXZ-Y.js → de-DE-XR44H4JA-CrnRlt4z.js} +2 -2
  25. package/client/dist/assets/{edges-e0da2a9e-CGBBhG8k.js → edges-e0da2a9e-DDsXzXLJ.js} +1 -1
  26. package/client/dist/assets/{el-GR-BZB4AONW-D4wv1oIz.js → el-GR-BZB4AONW-DQd8iogq.js} +2 -2
  27. package/client/dist/assets/{erDiagram-9861fffd-CYaF3q1I.js → erDiagram-9861fffd-CBiCC4rl.js} +2 -2
  28. package/client/dist/assets/{es-ES-U4NZUMDT-CGeTKXgd.js → es-ES-U4NZUMDT-vvUblc5i.js} +2 -2
  29. package/client/dist/assets/{eu-ES-A7QVB2H4-Cayx1TxR.js → eu-ES-A7QVB2H4-De4NNCc1.js} +2 -2
  30. package/client/dist/assets/{fa-IR-HGAKTJCU-CmUg8pmw.js → fa-IR-HGAKTJCU-DFBXqIqq.js} +2 -2
  31. package/client/dist/assets/{fi-FI-Z5N7JZ37-xvHcPhsU.js → fi-FI-Z5N7JZ37-DV9zESPg.js} +2 -2
  32. package/client/dist/assets/{flowDb-956e92f1-C-_LFz70.js → flowDb-956e92f1-BhdSHbdO.js} +1 -1
  33. package/client/dist/assets/{flowDiagram-66a62f08-C1sHdSjn.js → flowDiagram-66a62f08-M-fp1_Ie.js} +2 -2
  34. package/client/dist/assets/flowDiagram-v2-96b9c2cf-C5eiN8Pg.js +1 -0
  35. package/client/dist/assets/{flowchart-elk-definition-4a651766-CNGfpudb.js → flowchart-elk-definition-4a651766-Bp0SonQx.js} +2 -2
  36. package/client/dist/assets/{fr-FR-RHASNOE6-DBoHEcNj.js → fr-FR-RHASNOE6-CKTMXuGk.js} +2 -2
  37. package/client/dist/assets/ganttDiagram-c361ad54-iA737GUS.js +257 -0
  38. package/client/dist/assets/{gitGraphDiagram-72cf32ee-DojCDvlS.js → gitGraphDiagram-72cf32ee-BX-wj-PV.js} +2 -2
  39. package/client/dist/assets/{gl-ES-HMX3MZ6V-p6hrn2cN.js → gl-ES-HMX3MZ6V-Cdiqq4jY.js} +2 -2
  40. package/client/dist/assets/{graph-DXM7lcy1.js → graph-Rxkx3sEa.js} +1 -1
  41. package/client/dist/assets/{he-IL-6SHJWFNN-y2jEX6-0.js → he-IL-6SHJWFNN-gYmR5_KT.js} +2 -2
  42. package/client/dist/assets/{hi-IN-IWLTKZ5I-99pNfyWr.js → hi-IN-IWLTKZ5I-pyqK94AR.js} +2 -2
  43. package/client/dist/assets/{hu-HU-A5ZG7DT2-hygceGMS.js → hu-HU-A5ZG7DT2-DpacJgJy.js} +2 -2
  44. package/client/dist/assets/{id-ID-SAP4L64H-CyIqi1hv.js → id-ID-SAP4L64H-CAvIX-mj.js} +2 -2
  45. package/client/dist/assets/{index-3862675e-4idOQN2N.js → index-3862675e-BX3Fpn6V.js} +1 -1
  46. package/client/dist/assets/{index-BHZfFT_V.js → index-BBlwbHq_.js} +4 -4
  47. package/client/dist/assets/{index-BGmwbRlb.js → index-ClfzLIqY.js} +6 -6
  48. package/client/dist/assets/index-Td4UdtLF.css +1 -0
  49. package/client/dist/assets/{infoDiagram-f8f76790-CFLrHqtc.js → infoDiagram-f8f76790-Ckv8imiv.js} +2 -2
  50. package/client/dist/assets/{it-IT-JPQ66NNP-DzVvVdQI.js → it-IT-JPQ66NNP-BtpNRSce.js} +2 -2
  51. package/client/dist/assets/{ja-JP-DBVTYXUO-BI4fPexV.js → ja-JP-DBVTYXUO-CwJRyY6M.js} +2 -2
  52. package/client/dist/assets/{journeyDiagram-49397b02-C3CFDo8z.js → journeyDiagram-49397b02-DWWZssji.js} +2 -2
  53. package/client/dist/assets/kaa-6HZHGXH3-DIWQEb4A.js +1 -0
  54. package/client/dist/assets/{kab-KAB-ZGHBKWFO-DBI_ri48.js → kab-KAB-ZGHBKWFO-DjGbqhUg.js} +2 -2
  55. package/client/dist/assets/kk-KZ-P5N5QNE5-B_VzJdWf.js +1 -0
  56. package/client/dist/assets/{km-KH-HSX4SM5Z-DOMFSres.js → km-KH-HSX4SM5Z-DUD5mi0o.js} +2 -2
  57. package/client/dist/assets/{ko-KR-MTYHY66A-tb08hXzd.js → ko-KR-MTYHY66A--sDB10db.js} +3 -3
  58. package/client/dist/assets/{ku-TR-6OUDTVRD-DlIQCCY4.js → ku-TR-6OUDTVRD-CKvKrkcX.js} +2 -2
  59. package/client/dist/assets/{layout-B_11mCXA.js → layout-CkB7sSeq.js} +1 -1
  60. package/client/dist/assets/{line-B-qmK_vI.js → line-DC7MA9qY.js} +1 -1
  61. package/client/dist/assets/{linear-Ph6uuYcX.js → linear-C1lBBthf.js} +1 -1
  62. package/client/dist/assets/{lt-LT-XHIRWOB4--qWy24_Z.js → lt-LT-XHIRWOB4-MSZf7xYG.js} +2 -2
  63. package/client/dist/assets/{lv-LV-5QDEKY6T-Bnd_1GDb.js → lv-LV-5QDEKY6T-C-gvvmBB.js} +2 -2
  64. package/client/dist/assets/{mindmap-definition-fc14e90a-Do79tIc0.js → mindmap-definition-fc14e90a-B3O7hztq.js} +2 -2
  65. package/client/dist/assets/{mr-IN-CRQNXWMA-BsV6HaD9.js → mr-IN-CRQNXWMA-XHtBUWQH.js} +2 -2
  66. package/client/dist/assets/my-MM-5M5IBNSE-D9eD2edL.js +1 -0
  67. package/client/dist/assets/{nb-NO-T6EIAALU-Cvf9FdSF.js → nb-NO-T6EIAALU-BlImC6gp.js} +3 -3
  68. package/client/dist/assets/{nl-NL-IS3SIHDZ-DA1yqpXw.js → nl-NL-IS3SIHDZ-CPFhnaSP.js} +2 -2
  69. package/client/dist/assets/{nn-NO-6E72VCQL-89lm3vku.js → nn-NO-6E72VCQL-BMvoJSKQ.js} +2 -2
  70. package/client/dist/assets/{oc-FR-POXYY2M6-BsrjTJQh.js → oc-FR-POXYY2M6-Buye63LS.js} +2 -2
  71. package/client/dist/assets/{pa-IN-N4M65BXN-CczefYaj.js → pa-IN-N4M65BXN-D9uQ3niy.js} +2 -2
  72. package/client/dist/assets/{percentages-BXMCSKIN-Be6p9phi.js → percentages-BXMCSKIN-BzXIakGM.js} +7 -7
  73. package/client/dist/assets/{pieDiagram-8a3498a8-CfblQHdm.js → pieDiagram-8a3498a8-BU38mzx-.js} +3 -3
  74. package/client/dist/assets/{pl-PL-T2D74RX3-DdhH-zcK.js → pl-PL-T2D74RX3-BqM4xdcg.js} +2 -2
  75. package/client/dist/assets/{pt-BR-5N22H2LF-gpwlheL6.js → pt-BR-5N22H2LF-rAjrxGyI.js} +2 -2
  76. package/client/dist/assets/{pt-PT-UZXXM6DQ-Cs87vICi.js → pt-PT-UZXXM6DQ-DXsqcwLt.js} +2 -2
  77. package/client/dist/assets/{quadrantDiagram-120e2f19-CRMSamSP.js → quadrantDiagram-120e2f19-HhK4H1WU.js} +2 -2
  78. package/client/dist/assets/{requirementDiagram-deff3bca-D3LBN016.js → requirementDiagram-deff3bca-aDrcyj-A.js} +2 -2
  79. package/client/dist/assets/{ro-RO-JPDTUUEW-CWTSJ1Dt.js → ro-RO-JPDTUUEW-D_F9UKer.js} +2 -2
  80. package/client/dist/assets/{ru-RU-B4JR7IUQ-Bq7aN2ep.js → ru-RU-B4JR7IUQ-MirqN29p.js} +2 -2
  81. package/client/dist/assets/sankeyDiagram-04a897e0-C6ij7qbQ.js +8 -0
  82. package/client/dist/assets/{sequenceDiagram-704730f1-BRYXVDGX.js → sequenceDiagram-704730f1-C0EKO3th.js} +2 -2
  83. package/client/dist/assets/si-LK-N5RQ5JYF-DyZC3mkC.js +1 -0
  84. package/client/dist/assets/{sk-SK-C5VTKIMK-ByjKQzUb.js → sk-SK-C5VTKIMK-D-ksz-WY.js} +2 -2
  85. package/client/dist/assets/{sl-SI-NN7IZMDC-B8WCyMBU.js → sl-SI-NN7IZMDC-CknuYoQ1.js} +2 -2
  86. package/client/dist/assets/stateDiagram-587899a1-CYoq2VjL.js +1 -0
  87. package/client/dist/assets/stateDiagram-v2-d93cdb3a-C5lbp5px.js +1 -0
  88. package/client/dist/assets/{styles-6aaf32cf-Dr-lfIOW.js → styles-6aaf32cf-Dkfsk8gt.js} +1 -1
  89. package/client/dist/assets/{styles-9a916d00-DS4wRpL7.js → styles-9a916d00-CMYqtcEN.js} +1 -1
  90. package/client/dist/assets/{styles-c10674c1-nKRF6NrH.js → styles-c10674c1-Bp-5OlRU.js} +1 -1
  91. package/client/dist/assets/{subset-shared.chunk-KT79s7KG.js → subset-shared.chunk-kfIB1Zam.js} +3 -3
  92. package/client/dist/assets/subset-worker.chunk-DwQBgc4z.js +1 -0
  93. package/client/dist/assets/{sv-SE-XGPEYMSR-BiIPUVbv.js → sv-SE-XGPEYMSR-DwN13se1.js} +2 -2
  94. package/client/dist/assets/{svgDrawCommon-08f97a94-C3uP9PYr.js → svgDrawCommon-08f97a94-CEgCMqs4.js} +1 -1
  95. package/client/dist/assets/{ta-IN-2NMHFXQM-Cidadso2.js → ta-IN-2NMHFXQM-ejDfFhwa.js} +2 -2
  96. package/client/dist/assets/th-TH-HPSO5L25-Bqc90ZNn.js +2 -0
  97. package/client/dist/assets/{timeline-definition-85554ec2-BSsLsIgF.js → timeline-definition-85554ec2-BmGdKqG0.js} +2 -2
  98. package/client/dist/assets/{tr-TR-DEFEU3FU-DaFcI-KL.js → tr-TR-DEFEU3FU-CJvlPbcW.js} +2 -2
  99. package/client/dist/assets/{uk-UA-QMV73CPH-DkBW36St.js → uk-UA-QMV73CPH-D26-cbWL.js} +3 -3
  100. package/client/dist/assets/vendor-codemirror-D_s0aGBu.js +35 -0
  101. package/client/dist/assets/{vendor-icons-Dh9m_Ydt.js → vendor-icons-aNdOvTr_.js} +159 -119
  102. package/client/dist/assets/{vi-VN-M7AON7JQ-KrtfxOzl.js → vi-VN-M7AON7JQ-MbqIIwYM.js} +2 -2
  103. package/client/dist/assets/{xychartDiagram-e933f94c-CgNgZ4pp.js → xychartDiagram-e933f94c-gfcTauxU.js} +2 -2
  104. package/client/dist/assets/{zh-CN-LNUGB5OW-BQu12RoD.js → zh-CN-LNUGB5OW-BZSmhUdL.js} +3 -3
  105. package/client/dist/assets/zh-HK-E62DVLB3-BJqejpiX.js +1 -0
  106. package/client/dist/assets/{zh-TW-RAJ6MFWO-ffJWgVxn.js → zh-TW-RAJ6MFWO-BBXtV-Uz.js} +2 -2
  107. package/client/dist/index.html +3 -3
  108. package/package.json +5 -2
  109. package/server/cli.js +64 -5
  110. package/server/constants/config.js +29 -3
  111. package/server/database/auth.db +0 -0
  112. package/server/database/db.js +203 -1
  113. package/server/index.js +348 -48
  114. package/server/mcp-server.js +2 -1
  115. package/server/middleware/auth.js +20 -9
  116. package/server/projects.js +95 -202
  117. package/server/relay-client.js +205 -11
  118. package/server/routes/auth.js +6 -0
  119. package/server/routes/commands.js +1 -1
  120. package/server/routes/dashboard.js +52 -0
  121. package/server/routes/projects.js +38 -35
  122. package/server/routes/voice.js +198 -0
  123. package/server/routes/webhooks.js +166 -0
  124. package/server/routes/workflows.js +118 -0
  125. package/server/services/whisperService.js +84 -0
  126. package/server/services/workflowScheduler.js +186 -0
  127. package/client/dist/assets/AppContent-DTZ2FbvM.js +0 -513
  128. package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +0 -6
  129. package/client/dist/assets/CanvasPanel-q4HEqNtV.css +0 -1
  130. package/client/dist/assets/LoginModal-CWoFm0au.js +0 -19
  131. package/client/dist/assets/az-AZ-76LH7QW2-CDdeucRZ.js +0 -1
  132. package/client/dist/assets/channel-O3ovC0x9.js +0 -1
  133. package/client/dist/assets/classDiagram-70f12bd4-D0lhAcxU.js +0 -2
  134. package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +0 -2
  135. package/client/dist/assets/clone-BG9u7vLi.js +0 -1
  136. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +0 -1
  137. package/client/dist/assets/ganttDiagram-c361ad54-B8HJQqjt.js +0 -257
  138. package/client/dist/assets/index-B8wwD_Xo.css +0 -1
  139. package/client/dist/assets/kaa-6HZHGXH3-fwOleoQB.js +0 -1
  140. package/client/dist/assets/kk-KZ-P5N5QNE5-zpl7uvyF.js +0 -1
  141. package/client/dist/assets/my-MM-5M5IBNSE-kZQURVIi.js +0 -1
  142. package/client/dist/assets/sankeyDiagram-04a897e0-CsFqOQZN.js +0 -8
  143. package/client/dist/assets/si-LK-N5RQ5JYF-BBjcNYQh.js +0 -1
  144. package/client/dist/assets/stateDiagram-587899a1-BHoy9LtD.js +0 -1
  145. package/client/dist/assets/stateDiagram-v2-d93cdb3a-BvMUA6bS.js +0 -1
  146. package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +0 -1
  147. package/client/dist/assets/th-TH-HPSO5L25-CFNnJwSv.js +0 -2
  148. package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +0 -20
  149. package/client/dist/assets/vendor-codemirror-rix45NST.js +0 -16
  150. 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 { initializeDatabase, relayTokensDb, subscriptionDb, credentialsDb } from './database/db.js';
74
- import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
75
- import { IS_PLATFORM } from './constants/config.js';
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
- res.setHeader('X-Frame-Options', 'DENY');
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: !!(relay && relay.ws.readyState === 1),
547
- connectedAt: relay?.connectedAt || null
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
- console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
1132
- console.log('📁 Project:', data.options?.projectPath || 'Unknown');
1133
-
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
- );
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
- console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
1144
- console.log('📁 Project:', data.options?.cwd || 'Unknown');
1145
- console.log('🤖 Model:', data.options?.model || 'default');
1146
- await spawnCursor(data.command, data.options, writer);
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
- console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
1149
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1150
- console.log('🤖 Model:', data.options?.model || 'default');
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;
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
- await withUserApiKey('OPENAI_API_KEY', userOpenaiKey, () =>
1158
- queryCodex(data.command, data.options, writer)
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 Anthropic API key from relay handshake headers
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, { ws, user: tokenData, connectedAt: Date.now(), anthropicApiKey });
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', (err) => {
1380
- // relay error
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 "upfynai-code connect" on your local machine.'));
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 - just run the initial command in the project directory
1525
- if (os.platform() === 'win32') {
1526
- shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
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
- shellCommand = `cd "${projectPath}" && ${initialCommand}`;
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 shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
1568
- const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
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)
@@ -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
- const JWT_SECRET = process.env.JWT_SECRET?.trim() || (() => { throw new Error('JWT_SECRET required'); })();
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
- const JWT_SECRET = process.env.JWT_SECRET?.trim();
6
+ let JWT_SECRET = process.env.JWT_SECRET?.trim();
6
7
  if (!JWT_SECRET) {
7
- console.error('[SECURITY] JWT_SECRET environment variable is required. Server cannot start without it.');
8
- process.exit(1);
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 — 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')) {
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: process.env.NODE_ENV === 'production' || !!process.env.VERCEL || !!process.env.RAILWAY_ENVIRONMENT,
85
- sameSite: 'strict',
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
  };