upfynai-code 2.4.1 → 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 +1 -1
- package/server/database/auth.db +0 -0
- package/server/database/db.js +203 -1
- package/server/index.js +111 -18
- package/server/middleware/auth.js +11 -6
- package/server/projects.js +95 -202
- package/server/relay-client.js +47 -10
- package/server/routes/auth.js +6 -0
- 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/database/db.js
CHANGED
|
@@ -170,6 +170,63 @@ CREATE TABLE IF NOT EXISTS payments (
|
|
|
170
170
|
CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id);
|
|
171
171
|
CREATE INDEX IF NOT EXISTS idx_payments_order_id ON payments(razorpay_order_id);
|
|
172
172
|
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
|
|
173
|
+
|
|
174
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
175
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
176
|
+
user_id INTEGER NOT NULL,
|
|
177
|
+
name TEXT NOT NULL,
|
|
178
|
+
url TEXT NOT NULL,
|
|
179
|
+
method TEXT NOT NULL DEFAULT 'POST',
|
|
180
|
+
headers TEXT DEFAULT '{}',
|
|
181
|
+
description TEXT,
|
|
182
|
+
is_active BOOLEAN DEFAULT 1,
|
|
183
|
+
last_triggered DATETIME,
|
|
184
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
185
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
186
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_user_id ON webhooks(user_id);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active);
|
|
191
|
+
|
|
192
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
193
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
194
|
+
user_id INTEGER NOT NULL,
|
|
195
|
+
name TEXT NOT NULL,
|
|
196
|
+
description TEXT,
|
|
197
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
198
|
+
schedule TEXT DEFAULT NULL,
|
|
199
|
+
schedule_enabled BOOLEAN DEFAULT 0,
|
|
200
|
+
schedule_timezone TEXT DEFAULT 'UTC',
|
|
201
|
+
is_active BOOLEAN DEFAULT 1,
|
|
202
|
+
last_run DATETIME,
|
|
203
|
+
next_run DATETIME,
|
|
204
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
205
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
206
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_user_id ON workflows(user_id);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_active ON workflows(is_active);
|
|
211
|
+
|
|
212
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
213
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
214
|
+
workflow_id INTEGER NOT NULL,
|
|
215
|
+
user_id INTEGER NOT NULL,
|
|
216
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
217
|
+
steps_completed INTEGER DEFAULT 0,
|
|
218
|
+
total_steps INTEGER DEFAULT 0,
|
|
219
|
+
result TEXT,
|
|
220
|
+
error TEXT,
|
|
221
|
+
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
222
|
+
completed_at DATETIME,
|
|
223
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,
|
|
224
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_id ON workflow_runs(workflow_id);
|
|
228
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_user_id ON workflow_runs(user_id);
|
|
229
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
|
|
173
230
|
`;
|
|
174
231
|
|
|
175
232
|
// ─── Migrations ─────────────────────────────────────────────────────────────────
|
|
@@ -603,4 +660,149 @@ const relayTokensDb = {
|
|
|
603
660
|
}
|
|
604
661
|
};
|
|
605
662
|
|
|
606
|
-
|
|
663
|
+
// ─── Webhook DB ──────────────────────────────────────────────────────────────
|
|
664
|
+
|
|
665
|
+
const webhookDb = {
|
|
666
|
+
getAll: async (userId) => {
|
|
667
|
+
const result = await db.execute({
|
|
668
|
+
sql: 'SELECT * FROM webhooks WHERE user_id = ? ORDER BY created_at DESC',
|
|
669
|
+
args: [userId]
|
|
670
|
+
});
|
|
671
|
+
return result.rows;
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
getById: async (id, userId) => {
|
|
675
|
+
const result = await db.execute({
|
|
676
|
+
sql: 'SELECT * FROM webhooks WHERE id = ? AND user_id = ?',
|
|
677
|
+
args: [id, userId]
|
|
678
|
+
});
|
|
679
|
+
return getRow(result);
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
create: async (userId, { name, url, method, headers, description }) => {
|
|
683
|
+
const result = await db.execute({
|
|
684
|
+
sql: 'INSERT INTO webhooks (user_id, name, url, method, headers, description) VALUES (?, ?, ?, ?, ?, ?)',
|
|
685
|
+
args: [userId, name, url, method || 'POST', headers || '{}', description || null]
|
|
686
|
+
});
|
|
687
|
+
return { id: Number(result.lastInsertRowid), name, url, method: method || 'POST' };
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
update: async (id, userId, { name, url, method, headers, description }) => {
|
|
691
|
+
const result = await db.execute({
|
|
692
|
+
sql: 'UPDATE webhooks SET name = ?, url = ?, method = ?, headers = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
|
|
693
|
+
args: [name, url, method, headers || '{}', description || null, id, userId]
|
|
694
|
+
});
|
|
695
|
+
return result.rowsAffected > 0;
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
delete: async (id, userId) => {
|
|
699
|
+
await ensureForeignKeys();
|
|
700
|
+
const result = await db.execute({
|
|
701
|
+
sql: 'DELETE FROM webhooks WHERE id = ? AND user_id = ?',
|
|
702
|
+
args: [id, userId]
|
|
703
|
+
});
|
|
704
|
+
return result.rowsAffected > 0;
|
|
705
|
+
},
|
|
706
|
+
|
|
707
|
+
updateLastTriggered: async (id) => {
|
|
708
|
+
await db.execute({
|
|
709
|
+
sql: 'UPDATE webhooks SET last_triggered = CURRENT_TIMESTAMP WHERE id = ?',
|
|
710
|
+
args: [id]
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// ─── Workflow DB ─────────────────────────────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
const workflowDb = {
|
|
718
|
+
getAll: async (userId) => {
|
|
719
|
+
const result = await db.execute({
|
|
720
|
+
sql: 'SELECT * FROM workflows WHERE user_id = ? ORDER BY created_at DESC',
|
|
721
|
+
args: [userId]
|
|
722
|
+
});
|
|
723
|
+
return result.rows;
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
getById: async (id, userId) => {
|
|
727
|
+
const result = await db.execute({
|
|
728
|
+
sql: 'SELECT * FROM workflows WHERE id = ? AND user_id = ?',
|
|
729
|
+
args: [id, userId]
|
|
730
|
+
});
|
|
731
|
+
return getRow(result);
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
create: async (userId, { name, description, steps, schedule, schedule_enabled, schedule_timezone }) => {
|
|
735
|
+
const result = await db.execute({
|
|
736
|
+
sql: 'INSERT INTO workflows (user_id, name, description, steps, schedule, schedule_enabled, schedule_timezone) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
737
|
+
args: [userId, name, description || null, JSON.stringify(steps || []), schedule || null, schedule_enabled ? 1 : 0, schedule_timezone || 'UTC']
|
|
738
|
+
});
|
|
739
|
+
return { id: Number(result.lastInsertRowid), name };
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
update: async (id, userId, { name, description, steps, schedule, schedule_enabled, schedule_timezone }) => {
|
|
743
|
+
const result = await db.execute({
|
|
744
|
+
sql: 'UPDATE workflows SET name = ?, description = ?, steps = ?, schedule = ?, schedule_enabled = ?, schedule_timezone = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
|
|
745
|
+
args: [name, description || null, JSON.stringify(steps || []), schedule || null, schedule_enabled ? 1 : 0, schedule_timezone || 'UTC', id, userId]
|
|
746
|
+
});
|
|
747
|
+
return result.rowsAffected > 0;
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
delete: async (id, userId) => {
|
|
751
|
+
await ensureForeignKeys();
|
|
752
|
+
const result = await db.execute({
|
|
753
|
+
sql: 'DELETE FROM workflows WHERE id = ? AND user_id = ?',
|
|
754
|
+
args: [id, userId]
|
|
755
|
+
});
|
|
756
|
+
return result.rowsAffected > 0;
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
updateLastRun: async (id) => {
|
|
760
|
+
await db.execute({
|
|
761
|
+
sql: 'UPDATE workflows SET last_run = CURRENT_TIMESTAMP WHERE id = ?',
|
|
762
|
+
args: [id]
|
|
763
|
+
});
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
getScheduled: async () => {
|
|
767
|
+
const result = await db.execute(
|
|
768
|
+
'SELECT * FROM workflows WHERE schedule IS NOT NULL AND schedule_enabled = 1 AND is_active = 1'
|
|
769
|
+
);
|
|
770
|
+
return result.rows;
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
updateNextRun: async (id, nextRun) => {
|
|
774
|
+
await db.execute({
|
|
775
|
+
sql: 'UPDATE workflows SET next_run = ? WHERE id = ?',
|
|
776
|
+
args: [nextRun, id]
|
|
777
|
+
});
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
createRun: async (workflowId, userId, totalSteps) => {
|
|
781
|
+
const result = await db.execute({
|
|
782
|
+
sql: 'INSERT INTO workflow_runs (workflow_id, user_id, status, total_steps) VALUES (?, ?, ?, ?)',
|
|
783
|
+
args: [workflowId, userId, 'running', totalSteps]
|
|
784
|
+
});
|
|
785
|
+
return { id: Number(result.lastInsertRowid) };
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
updateRun: async (runId, { status, stepsCompleted, result: runResult, error }) => {
|
|
789
|
+
const sets = ['status = ?'];
|
|
790
|
+
const args = [status];
|
|
791
|
+
if (stepsCompleted !== undefined) { sets.push('steps_completed = ?'); args.push(stepsCompleted); }
|
|
792
|
+
if (runResult !== undefined) { sets.push('result = ?'); args.push(typeof runResult === 'string' ? runResult : JSON.stringify(runResult)); }
|
|
793
|
+
if (error !== undefined) { sets.push('error = ?'); args.push(error); }
|
|
794
|
+
if (status === 'completed' || status === 'failed') { sets.push('completed_at = CURRENT_TIMESTAMP'); }
|
|
795
|
+
args.push(runId);
|
|
796
|
+
await db.execute({ sql: `UPDATE workflow_runs SET ${sets.join(', ')} WHERE id = ?`, args });
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
getRuns: async (workflowId, userId) => {
|
|
800
|
+
const result = await db.execute({
|
|
801
|
+
sql: 'SELECT * FROM workflow_runs WHERE workflow_id = ? AND user_id = ? ORDER BY started_at DESC LIMIT 20',
|
|
802
|
+
args: [workflowId, userId]
|
|
803
|
+
});
|
|
804
|
+
return result.rows;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, relayTokensDb, githubTokensDb, subscriptionDb, paymentDb, webhookDb, workflowDb, PLAN_DURATIONS };
|
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,8 +76,13 @@ 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';
|
|
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';
|
|
73
84
|
import { initializeDatabase, relayTokensDb, subscriptionDb, credentialsDb, userDb } from './database/db.js';
|
|
74
|
-
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
85
|
+
import { validateApiKey, authenticateToken, authenticateWebSocket, JWT_SECRET } from './middleware/auth.js';
|
|
75
86
|
import { IS_PLATFORM, IS_LOCAL } from './constants/config.js';
|
|
76
87
|
import { execSync } from 'child_process';
|
|
77
88
|
|
|
@@ -353,7 +364,13 @@ app.locals.wss = wss;
|
|
|
353
364
|
// Security headers — protect against common web attacks
|
|
354
365
|
app.use((req, res, next) => {
|
|
355
366
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
356
|
-
|
|
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
|
+
}
|
|
357
374
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
358
375
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
359
376
|
if (process.env.NODE_ENV === 'production') {
|
|
@@ -499,6 +516,10 @@ app.use('/api/codex', authenticateToken, codexRoutes);
|
|
|
499
516
|
|
|
500
517
|
// Payment & Subscription Routes (protected)
|
|
501
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);
|
|
502
523
|
|
|
503
524
|
// Agent API Routes (uses API key authentication)
|
|
504
525
|
app.use('/api/agent', agentRoutes);
|
|
@@ -533,6 +554,10 @@ app.delete('/api/relay/tokens/:id', authenticateToken, async (req, res) => {
|
|
|
533
554
|
});
|
|
534
555
|
|
|
535
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
|
+
}
|
|
536
561
|
const relay = relayConnections.get(Number(req.user.id));
|
|
537
562
|
res.json({
|
|
538
563
|
connected: !!(relay && relay.ws.readyState === 1),
|
|
@@ -1375,7 +1400,8 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1375
1400
|
relayConnections.set(userId, {
|
|
1376
1401
|
ws, user: tokenData, connectedAt: Date.now(), anthropicApiKey,
|
|
1377
1402
|
version: relayVersion, machine: relayMachine, platform: relayPlatform, cwd: relayCwd,
|
|
1378
|
-
agents: null // populated when client sends agent-capabilities
|
|
1403
|
+
agents: null, // populated when client sends agent-capabilities
|
|
1404
|
+
lastPong: Date.now(),
|
|
1379
1405
|
});
|
|
1380
1406
|
|
|
1381
1407
|
ws.send(JSON.stringify({
|
|
@@ -1452,6 +1478,8 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1452
1478
|
|
|
1453
1479
|
// Heartbeat
|
|
1454
1480
|
if (data.type === 'ping') {
|
|
1481
|
+
const relay = relayConnections.get(userId);
|
|
1482
|
+
if (relay) relay.lastPong = Date.now();
|
|
1455
1483
|
ws.send(JSON.stringify({ type: 'pong' }));
|
|
1456
1484
|
return;
|
|
1457
1485
|
}
|
|
@@ -1460,7 +1488,29 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1460
1488
|
}
|
|
1461
1489
|
});
|
|
1462
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
|
+
|
|
1463
1512
|
ws.on('close', () => {
|
|
1513
|
+
clearInterval(relayHeartbeat);
|
|
1464
1514
|
relayConnections.delete(userId);
|
|
1465
1515
|
// Clean up pending requests for this user
|
|
1466
1516
|
for (const [reqId, pending] of pendingRelayRequests) {
|
|
@@ -1470,7 +1520,6 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1470
1520
|
pendingRelayRequests.delete(reqId);
|
|
1471
1521
|
}
|
|
1472
1522
|
}
|
|
1473
|
-
// relay disconnected
|
|
1474
1523
|
|
|
1475
1524
|
// Broadcast relay status
|
|
1476
1525
|
for (const client of connectedClients) {
|
|
@@ -1482,8 +1531,8 @@ async function handleRelayConnection(ws, token, request) {
|
|
|
1482
1531
|
}
|
|
1483
1532
|
});
|
|
1484
1533
|
|
|
1485
|
-
ws.on('error', (
|
|
1486
|
-
|
|
1534
|
+
ws.on('error', () => {
|
|
1535
|
+
clearInterval(relayHeartbeat);
|
|
1487
1536
|
});
|
|
1488
1537
|
}
|
|
1489
1538
|
|
|
@@ -1602,10 +1651,14 @@ async function routeViaRelay(userId, action, data, writer, eventMap = {}) {
|
|
|
1602
1651
|
viaRelay: true
|
|
1603
1652
|
});
|
|
1604
1653
|
} catch (error) {
|
|
1654
|
+
const isRelayLost = error.message?.includes('Relay disconnected') || error.message?.includes('No relay connection') || error.message?.includes('Relay request timed out');
|
|
1605
1655
|
writer.send({
|
|
1606
1656
|
type: errorType,
|
|
1607
|
-
error:
|
|
1608
|
-
|
|
1657
|
+
error: isRelayLost
|
|
1658
|
+
? 'Your machine disconnected. Please reconnect with "uc connect" and try again.'
|
|
1659
|
+
: error.message,
|
|
1660
|
+
sessionId,
|
|
1661
|
+
relayDisconnected: isRelayLost
|
|
1609
1662
|
});
|
|
1610
1663
|
}
|
|
1611
1664
|
}
|
|
@@ -1635,6 +1688,7 @@ function handleShellConnection(ws) {
|
|
|
1635
1688
|
const provider = data.provider || 'claude';
|
|
1636
1689
|
const initialCommand = data.initialCommand;
|
|
1637
1690
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
1691
|
+
const shellType = data.shellType || null;
|
|
1638
1692
|
urlDetectionBuffer = '';
|
|
1639
1693
|
announcedAuthUrls.clear();
|
|
1640
1694
|
|
|
@@ -1716,11 +1770,17 @@ function handleShellConnection(ws) {
|
|
|
1716
1770
|
// Prepare the shell command adapted to the platform and provider
|
|
1717
1771
|
let shellCommand;
|
|
1718
1772
|
if (isPlainShell) {
|
|
1719
|
-
// Plain shell mode -
|
|
1720
|
-
|
|
1721
|
-
|
|
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
|
+
}
|
|
1722
1781
|
} else {
|
|
1723
|
-
|
|
1782
|
+
// Interactive shell tab — spawn shell directly (no command wrapper)
|
|
1783
|
+
shellCommand = null;
|
|
1724
1784
|
}
|
|
1725
1785
|
} else if (provider === 'cursor') {
|
|
1726
1786
|
// Use cursor-agent command
|
|
@@ -1758,9 +1818,19 @@ function handleShellConnection(ws) {
|
|
|
1758
1818
|
|
|
1759
1819
|
console.log('🔧 Executing shell command:', shellCommand);
|
|
1760
1820
|
|
|
1761
|
-
// Use appropriate shell based on platform
|
|
1762
|
-
const
|
|
1763
|
-
|
|
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] : [];
|
|
1764
1834
|
|
|
1765
1835
|
// Use terminal dimensions from client if provided, otherwise use defaults
|
|
1766
1836
|
const termCols = data.cols || 80;
|
|
@@ -1771,7 +1841,7 @@ function handleShellConnection(ws) {
|
|
|
1771
1841
|
name: 'xterm-256color',
|
|
1772
1842
|
cols: termCols,
|
|
1773
1843
|
rows: termRows,
|
|
1774
|
-
cwd: os.homedir(),
|
|
1844
|
+
cwd: shellCommand ? os.homedir() : projectPath,
|
|
1775
1845
|
env: {
|
|
1776
1846
|
...process.env,
|
|
1777
1847
|
TERM: 'xterm-256color',
|
|
@@ -2360,9 +2430,29 @@ app.get('*', (req, res) => {
|
|
|
2360
2430
|
return res.status(404).send('Not found');
|
|
2361
2431
|
}
|
|
2362
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
|
+
|
|
2363
2453
|
// Only serve index.html for HTML routes, not for static assets
|
|
2364
2454
|
// Static assets should already be handled by express.static middleware above
|
|
2365
|
-
const indexPath = path.join(__dirname, '../dist/index.html');
|
|
2455
|
+
const indexPath = path.join(__dirname, '../client/dist/index.html');
|
|
2366
2456
|
|
|
2367
2457
|
// Check if dist/index.html exists (production build available)
|
|
2368
2458
|
if (fs.existsSync(indexPath)) {
|
|
@@ -2482,7 +2572,7 @@ async function startServer() {
|
|
|
2482
2572
|
}
|
|
2483
2573
|
|
|
2484
2574
|
// Check if running in production mode (dist folder exists OR NODE_ENV/RAILWAY set)
|
|
2485
|
-
const distIndexPath = path.join(__dirname, '../dist/index.html');
|
|
2575
|
+
const distIndexPath = path.join(__dirname, '../client/dist/index.html');
|
|
2486
2576
|
const isProduction = fs.existsSync(distIndexPath) || process.env.NODE_ENV === 'production' || !!process.env.RAILWAY_ENVIRONMENT;
|
|
2487
2577
|
|
|
2488
2578
|
// Log Claude implementation mode
|
|
@@ -2505,6 +2595,9 @@ async function startServer() {
|
|
|
2505
2595
|
console.log(`${c.info('[INFO]')} MCP Server: ${c.bright('http://0.0.0.0:' + PORT + '/mcp')}`);
|
|
2506
2596
|
console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
|
|
2507
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));
|
|
2508
2601
|
console.log('');
|
|
2509
2602
|
|
|
2510
2603
|
// Start watching the projects folder for changes (skip on Vercel)
|
|
@@ -33,10 +33,8 @@ const extractToken = (req) => {
|
|
|
33
33
|
const authHeader = req.headers['authorization'];
|
|
34
34
|
if (authHeader?.startsWith('Bearer ')) return authHeader.slice(7);
|
|
35
35
|
|
|
36
|
-
// 3. Query param —
|
|
37
|
-
|
|
38
|
-
if (req.query?.token && req.method === 'GET' &&
|
|
39
|
-
(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') {
|
|
40
38
|
return req.query.token;
|
|
41
39
|
}
|
|
42
40
|
|
|
@@ -68,6 +66,10 @@ const authenticateToken = async (req, res, next) => {
|
|
|
68
66
|
const user = await userDb.getUserById(decoded.userId);
|
|
69
67
|
if (!user) return res.status(401).json({ error: 'Invalid token. User not found.' });
|
|
70
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
|
+
}
|
|
71
73
|
next();
|
|
72
74
|
} catch (error) {
|
|
73
75
|
return res.status(403).json({ error: 'Invalid or expired token' });
|
|
@@ -85,10 +87,13 @@ const generateToken = (user) => {
|
|
|
85
87
|
|
|
86
88
|
// Cookie config for httpOnly session
|
|
87
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;
|
|
88
91
|
const COOKIE_OPTIONS = {
|
|
89
92
|
httpOnly: true,
|
|
90
|
-
secure:
|
|
91
|
-
|
|
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',
|
|
92
97
|
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
93
98
|
path: '/',
|
|
94
99
|
};
|