upfynai-code 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dist/assets/AppContent-DTZ2FbvM.js +513 -0
- package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +6 -0
- package/client/dist/assets/LoginModal-CWoFm0au.js +19 -0
- package/client/dist/assets/MarkdownPreview-CYdvwJaV.js +1 -0
- package/client/dist/assets/{Onboarding-Coxo6mFA.js → Onboarding-CtIoXiTp.js} +1 -1
- package/client/dist/assets/{SetupForm-BzYOsbji.js → SetupForm-B4p8im5O.js} +1 -1
- package/client/dist/assets/{ar-SA-G6X2FPQ2-Bmw2-hDt.js → ar-SA-G6X2FPQ2-2gfmdvHk.js} +1 -1
- package/client/dist/assets/{arc-BMqY7_Ci.js → arc-DCZSHhoJ.js} +1 -1
- package/client/dist/assets/{az-AZ-76LH7QW2-Dh1le_qs.js → az-AZ-76LH7QW2-CDdeucRZ.js} +1 -1
- package/client/dist/assets/{bg-BG-XCXSNQG7-Cbav8Z9z.js → bg-BG-XCXSNQG7-D6__XtOK.js} +1 -1
- package/client/dist/assets/{blockDiagram-38ab4fdb-ChHJxsXw.js → blockDiagram-38ab4fdb-Cfbaeyp6.js} +3 -3
- package/client/dist/assets/{bn-BD-2XOGV67Q-DCNjOaWz.js → bn-BD-2XOGV67Q-DHNJw3OG.js} +1 -1
- package/client/dist/assets/{c4Diagram-3d4e48cf-b8Xue4Z6.js → c4Diagram-3d4e48cf-BBCnjOTy.js} +1 -1
- package/client/dist/assets/{ca-ES-6MX7JW3Y-Dl_vM7NS.js → ca-ES-6MX7JW3Y-r5g4o3zQ.js} +1 -1
- package/client/dist/assets/channel-O3ovC0x9.js +1 -0
- package/client/dist/assets/{classDiagram-70f12bd4-BheP7Ggo.js → classDiagram-70f12bd4-D0lhAcxU.js} +1 -1
- package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +2 -0
- package/client/dist/assets/clone-BG9u7vLi.js +1 -0
- package/client/dist/assets/{createText-2e5e7dd3-_n4jI_fO.js → createText-2e5e7dd3-B8jCDmF_.js} +1 -1
- package/client/dist/assets/{cs-CZ-2BRQDIVT-ftsKDdz4.js → cs-CZ-2BRQDIVT-p08jRLRC.js} +1 -1
- package/client/dist/assets/{da-DK-5WZEPLOC-DAjdwGRO.js → da-DK-5WZEPLOC-CnhOImFf.js} +1 -1
- package/client/dist/assets/{de-DE-XR44H4JA-BJXczHGT.js → de-DE-XR44H4JA-BunSXZ-Y.js} +1 -1
- package/client/dist/assets/{edges-e0da2a9e-CfPZr4YM.js → edges-e0da2a9e-CGBBhG8k.js} +2 -2
- package/client/dist/assets/{el-GR-BZB4AONW-DW2p_uy7.js → el-GR-BZB4AONW-D4wv1oIz.js} +1 -1
- package/client/dist/assets/{erDiagram-9861fffd-CF33V-Of.js → erDiagram-9861fffd-CYaF3q1I.js} +1 -1
- package/client/dist/assets/{es-ES-U4NZUMDT-DLOIGnrl.js → es-ES-U4NZUMDT-CGeTKXgd.js} +1 -1
- package/client/dist/assets/{eu-ES-A7QVB2H4-LJXbf89m.js → eu-ES-A7QVB2H4-Cayx1TxR.js} +1 -1
- package/client/dist/assets/{fa-IR-HGAKTJCU-Dvx65fgW.js → fa-IR-HGAKTJCU-CmUg8pmw.js} +1 -1
- package/client/dist/assets/{fi-FI-Z5N7JZ37-EoL65BQh.js → fi-FI-Z5N7JZ37-xvHcPhsU.js} +1 -1
- package/client/dist/assets/{flowDb-956e92f1-HgoXVy2H.js → flowDb-956e92f1-C-_LFz70.js} +3 -3
- package/client/dist/assets/flowDiagram-66a62f08-C1sHdSjn.js +4 -0
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +1 -0
- package/client/dist/assets/{flowchart-elk-definition-4a651766-DJbI2dpv.js → flowchart-elk-definition-4a651766-CNGfpudb.js} +7 -7
- package/client/dist/assets/{fr-FR-RHASNOE6-DNk_jdDs.js → fr-FR-RHASNOE6-DBoHEcNj.js} +1 -1
- package/client/dist/assets/{ganttDiagram-c361ad54-2XX670FU.js → ganttDiagram-c361ad54-B8HJQqjt.js} +1 -1
- package/client/dist/assets/{gitGraphDiagram-72cf32ee-CcUfruAo.js → gitGraphDiagram-72cf32ee-DojCDvlS.js} +1 -1
- package/client/dist/assets/{gl-ES-HMX3MZ6V-dxzFjZlG.js → gl-ES-HMX3MZ6V-p6hrn2cN.js} +1 -1
- package/client/dist/assets/{graph-BSbiMSBC.js → graph-DXM7lcy1.js} +1 -1
- package/client/dist/assets/{he-IL-6SHJWFNN-Cogsfdt1.js → he-IL-6SHJWFNN-y2jEX6-0.js} +1 -1
- package/client/dist/assets/{hi-IN-IWLTKZ5I-L6wbgi4F.js → hi-IN-IWLTKZ5I-99pNfyWr.js} +1 -1
- package/client/dist/assets/{hu-HU-A5ZG7DT2-DSA6ZDsH.js → hu-HU-A5ZG7DT2-hygceGMS.js} +1 -1
- package/client/dist/assets/{id-ID-SAP4L64H-BK_vGGS6.js → id-ID-SAP4L64H-CyIqi1hv.js} +1 -1
- package/client/dist/assets/{image-blob-reduce.esm-BLtmMM_J.js → image-blob-reduce.esm-D6s-rqMO.js} +6 -1
- package/client/dist/assets/{index-3862675e-Bv32HUgT.js → index-3862675e-4idOQN2N.js} +1 -1
- package/client/dist/assets/{index-BPwf8Fw3.js → index-BGmwbRlb.js} +6 -6
- package/client/dist/assets/index-BHZfFT_V.js +97 -0
- package/client/dist/assets/{infoDiagram-f8f76790-w4mR4pxn.js → infoDiagram-f8f76790-CFLrHqtc.js} +1 -1
- package/client/dist/assets/{it-IT-JPQ66NNP-BLdHYMhn.js → it-IT-JPQ66NNP-DzVvVdQI.js} +1 -1
- package/client/dist/assets/{ja-JP-DBVTYXUO-B_vmexl_.js → ja-JP-DBVTYXUO-BI4fPexV.js} +1 -1
- package/client/dist/assets/{journeyDiagram-49397b02-D9nmO17e.js → journeyDiagram-49397b02-C3CFDo8z.js} +1 -1
- package/client/dist/assets/{kaa-6HZHGXH3-5s-3jl6F.js → kaa-6HZHGXH3-fwOleoQB.js} +1 -1
- package/client/dist/assets/{kab-KAB-ZGHBKWFO-2QaVDuSf.js → kab-KAB-ZGHBKWFO-DBI_ri48.js} +1 -1
- package/client/dist/assets/{kk-KZ-P5N5QNE5-CTC52Vbi.js → kk-KZ-P5N5QNE5-zpl7uvyF.js} +1 -1
- package/client/dist/assets/{km-KH-HSX4SM5Z-DxawH8UZ.js → km-KH-HSX4SM5Z-DOMFSres.js} +1 -1
- package/client/dist/assets/{ko-KR-MTYHY66A-CmosEM8_.js → ko-KR-MTYHY66A-tb08hXzd.js} +1 -1
- package/client/dist/assets/{ku-TR-6OUDTVRD-DbiLen4y.js → ku-TR-6OUDTVRD-DlIQCCY4.js} +1 -1
- package/client/dist/assets/{layout-jmt3H9tA.js → layout-B_11mCXA.js} +1 -1
- package/client/dist/assets/{line-JTlRayUJ.js → line-B-qmK_vI.js} +1 -1
- package/client/dist/assets/{linear-DJeB5p7x.js → linear-Ph6uuYcX.js} +1 -1
- package/client/dist/assets/{lt-LT-XHIRWOB4-CH15wrjA.js → lt-LT-XHIRWOB4--qWy24_Z.js} +1 -1
- package/client/dist/assets/{lv-LV-5QDEKY6T-dhgfPuCQ.js → lv-LV-5QDEKY6T-Bnd_1GDb.js} +1 -1
- package/client/dist/assets/mindmap-definition-fc14e90a-Do79tIc0.js +425 -0
- package/client/dist/assets/{mr-IN-CRQNXWMA-3Gi6iq7A.js → mr-IN-CRQNXWMA-BsV6HaD9.js} +1 -1
- package/client/dist/assets/{my-MM-5M5IBNSE-CpH4rdJj.js → my-MM-5M5IBNSE-kZQURVIi.js} +1 -1
- package/client/dist/assets/{nb-NO-T6EIAALU-Du6iiGql.js → nb-NO-T6EIAALU-Cvf9FdSF.js} +1 -1
- package/client/dist/assets/{nl-NL-IS3SIHDZ-BGvsd1MT.js → nl-NL-IS3SIHDZ-DA1yqpXw.js} +1 -1
- package/client/dist/assets/{nn-NO-6E72VCQL-B-odvJZW.js → nn-NO-6E72VCQL-89lm3vku.js} +1 -1
- package/client/dist/assets/{oc-FR-POXYY2M6-COC8xNjo.js → oc-FR-POXYY2M6-BsrjTJQh.js} +1 -1
- package/client/dist/assets/{pa-IN-N4M65BXN-CE21PUQH.js → pa-IN-N4M65BXN-CczefYaj.js} +1 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
- package/client/dist/assets/percentages-BXMCSKIN-Be6p9phi.js +207 -0
- package/client/dist/assets/pica-CQIY57Tf.js +7 -0
- package/client/dist/assets/{pieDiagram-8a3498a8-Cvfh7Qr5.js → pieDiagram-8a3498a8-CfblQHdm.js} +2 -2
- package/client/dist/assets/{pl-PL-T2D74RX3-D4xFVSoT.js → pl-PL-T2D74RX3-DdhH-zcK.js} +1 -1
- package/client/dist/assets/{pt-BR-5N22H2LF-CCq257gA.js → pt-BR-5N22H2LF-gpwlheL6.js} +1 -1
- package/client/dist/assets/{pt-PT-UZXXM6DQ-1l8gt5vA.js → pt-PT-UZXXM6DQ-Cs87vICi.js} +1 -1
- package/client/dist/assets/{quadrantDiagram-120e2f19-BA0js1aD.js → quadrantDiagram-120e2f19-CRMSamSP.js} +1 -1
- package/client/dist/assets/{requirementDiagram-deff3bca-B0QNFfIn.js → requirementDiagram-deff3bca-D3LBN016.js} +1 -1
- package/client/dist/assets/{ro-RO-JPDTUUEW-yosBW01E.js → ro-RO-JPDTUUEW-CWTSJ1Dt.js} +1 -1
- package/client/dist/assets/roundRect-0PYZxl1G.js +1 -0
- package/client/dist/assets/{ru-RU-B4JR7IUQ-8LkEJUix.js → ru-RU-B4JR7IUQ-Bq7aN2ep.js} +1 -1
- package/client/dist/assets/{sankeyDiagram-04a897e0-D4T9eCXn.js → sankeyDiagram-04a897e0-CsFqOQZN.js} +3 -3
- package/client/dist/assets/{sequenceDiagram-704730f1-CfBUTCrO.js → sequenceDiagram-704730f1-BRYXVDGX.js} +1 -1
- package/client/dist/assets/{si-LK-N5RQ5JYF-D8rjbqtd.js → si-LK-N5RQ5JYF-BBjcNYQh.js} +1 -1
- package/client/dist/assets/{sk-SK-C5VTKIMK-Bg14sAzN.js → sk-SK-C5VTKIMK-ByjKQzUb.js} +1 -1
- package/client/dist/assets/{sl-SI-NN7IZMDC-CMTib6Zs.js → sl-SI-NN7IZMDC-B8WCyMBU.js} +1 -1
- package/client/dist/assets/{stateDiagram-587899a1-BGgvmVSZ.js → stateDiagram-587899a1-BHoy9LtD.js} +1 -1
- package/client/dist/assets/{stateDiagram-v2-d93cdb3a-Qn3DpYuO.js → stateDiagram-v2-d93cdb3a-BvMUA6bS.js} +1 -1
- package/client/dist/assets/{styles-6aaf32cf-IdVZLPrD.js → styles-6aaf32cf-Dr-lfIOW.js} +2 -2
- package/client/dist/assets/{styles-9a916d00-BAC3L45X.js → styles-9a916d00-DS4wRpL7.js} +1 -1
- package/client/dist/assets/{styles-c10674c1-COhXxX8c.js → styles-c10674c1-nKRF6NrH.js} +1 -1
- package/client/dist/assets/{subset-shared.chunk-BWHnFai4.js → subset-shared.chunk-KT79s7KG.js} +64 -2
- package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +1 -0
- package/client/dist/assets/{sv-SE-XGPEYMSR-C1425rOF.js → sv-SE-XGPEYMSR-BiIPUVbv.js} +1 -1
- package/client/dist/assets/{svgDrawCommon-08f97a94-Cfk-fgnN.js → svgDrawCommon-08f97a94-C3uP9PYr.js} +1 -1
- package/client/dist/assets/{ta-IN-2NMHFXQM-BHHo1zpF.js → ta-IN-2NMHFXQM-Cidadso2.js} +1 -1
- package/client/dist/assets/{th-TH-HPSO5L25-CZVzm_WT.js → th-TH-HPSO5L25-CFNnJwSv.js} +1 -1
- package/client/dist/assets/{timeline-definition-85554ec2-VAvuJith.js → timeline-definition-85554ec2-BSsLsIgF.js} +1 -1
- package/client/dist/assets/{tr-TR-DEFEU3FU-DE1lclCq.js → tr-TR-DEFEU3FU-DaFcI-KL.js} +1 -1
- package/client/dist/assets/{uk-UA-QMV73CPH-D4lJZ85O.js → uk-UA-QMV73CPH-DkBW36St.js} +1 -1
- package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +20 -0
- package/client/dist/assets/vendor-codemirror-rix45NST.js +16 -0
- package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
- package/client/dist/assets/vendor-icons-Dh9m_Ydt.js +596 -0
- package/client/dist/assets/{vendor-markdown-CIVH08vJ.js → vendor-markdown-BXEi_H3G.js} +3 -3
- package/client/dist/assets/vendor-react-9mUTKBHH.js +67 -0
- package/client/dist/assets/{vendor-syntax-Djb62v3a.js → vendor-syntax-DnmwQQJF.js} +14 -7
- package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
- package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
- package/client/dist/assets/{vi-VN-M7AON7JQ-Dgc_SShk.js → vi-VN-M7AON7JQ-KrtfxOzl.js} +1 -1
- package/client/dist/assets/{xychartDiagram-e933f94c-BeyVBJhb.js → xychartDiagram-e933f94c-CgNgZ4pp.js} +1 -1
- package/client/dist/assets/{zh-CN-LNUGB5OW-MH4Yh8in.js → zh-CN-LNUGB5OW-BQu12RoD.js} +1 -1
- package/client/dist/assets/{zh-HK-E62DVLB3-D4XHehjx.js → zh-HK-E62DVLB3-zx9CvERq.js} +1 -1
- package/client/dist/assets/{zh-TW-RAJ6MFWO--efj3evj.js → zh-TW-RAJ6MFWO-ffJWgVxn.js} +1 -1
- package/client/dist/index.html +6 -6
- package/commands/upfynai-connect.md +31 -18
- package/commands/upfynai.md +45 -26
- package/package.json +1 -1
- package/server/cli-ui.js +785 -0
- package/server/cli.js +235 -161
- package/server/index.js +159 -107
- package/server/middleware/auth.js +9 -5
- package/server/openrouter.js +137 -0
- package/server/relay-client.js +158 -47
- package/server/routes/agent.js +54 -19
- package/server/routes/auth.js +59 -22
- package/server/routes/settings.js +91 -0
- package/shared/modelConstants.js +29 -0
- package/client/dist/assets/AppContent-CTSHQdyq.js +0 -513
- package/client/dist/assets/CanvasPanel-Cig0Mo9s.js +0 -6
- package/client/dist/assets/LoginModal-silya-zP.js +0 -11
- package/client/dist/assets/MarkdownPreview-B3c7OEj6.js +0 -1
- package/client/dist/assets/channel-CSnvHe_M.js +0 -1
- package/client/dist/assets/classDiagram-v2-f2320105-xtym7GEZ.js +0 -2
- package/client/dist/assets/clone-B75abXxS.js +0 -1
- package/client/dist/assets/flowDiagram-66a62f08-tffoET0H.js +0 -4
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Byc3JCHh.js +0 -1
- package/client/dist/assets/index-D1urGMYu.js +0 -95
- package/client/dist/assets/mindmap-definition-fc14e90a-BOOrexmz.js +0 -415
- package/client/dist/assets/pdf-TYrZqVzP.js +0 -12
- package/client/dist/assets/percentages-BXMCSKIN-C9GT0OD3.js +0 -199
- package/client/dist/assets/pica-VkdyTzi8.js +0 -2
- package/client/dist/assets/roundRect-mAH3dD0p.js +0 -1
- package/client/dist/assets/subset-worker.chunk-C8QUSruZ.js +0 -1
- package/client/dist/assets/vendor-codemirror-BARtJV1V.js +0 -16
- package/client/dist/assets/vendor-codemirror-langs-52_y1wip.js +0 -20
- package/client/dist/assets/vendor-i18n-ByAl-gdx.js +0 -1
- package/client/dist/assets/vendor-icons-D33IkSIf.js +0 -1
- package/client/dist/assets/vendor-react-CHoMc7ka.js +0 -8
- package/client/dist/assets/vendor-xterm-DBb3RXlu.js +0 -66
- package/client/dist/assets/vendor-xterm-DrlLKa8f.css +0 -1
package/server/relay-client.js
CHANGED
|
@@ -17,19 +17,27 @@ import path from 'path';
|
|
|
17
17
|
import { spawn } from 'child_process';
|
|
18
18
|
import { promises as fsPromises } from 'fs';
|
|
19
19
|
import crypto from 'crypto';
|
|
20
|
+
import {
|
|
21
|
+
c,
|
|
22
|
+
showConnectStartup,
|
|
23
|
+
showConnectionBanner,
|
|
24
|
+
logRelayEvent,
|
|
25
|
+
createSpinner,
|
|
26
|
+
} from './cli-ui.js';
|
|
27
|
+
|
|
28
|
+
// Load package.json for version
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
const __filename_rc = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname_rc = path.dirname(__filename_rc);
|
|
32
|
+
let VERSION = '0.0.0';
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname_rc, '../package.json'), 'utf8'));
|
|
35
|
+
VERSION = pkg.version;
|
|
36
|
+
} catch { /* ignore */ }
|
|
20
37
|
|
|
21
38
|
const CONFIG_DIR = path.join(os.homedir(), '.upfynai');
|
|
22
39
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
23
40
|
|
|
24
|
-
// ANSI colors
|
|
25
|
-
const c = {
|
|
26
|
-
green: (t) => `\x1b[32m${t}\x1b[0m`,
|
|
27
|
-
red: (t) => `\x1b[31m${t}\x1b[0m`,
|
|
28
|
-
cyan: (t) => `\x1b[36m${t}\x1b[0m`,
|
|
29
|
-
dim: (t) => `\x1b[2m${t}\x1b[0m`,
|
|
30
|
-
bold: (t) => `\x1b[1m${t}\x1b[0m`,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
41
|
function loadConfig() {
|
|
34
42
|
try {
|
|
35
43
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
@@ -90,11 +98,9 @@ async function handleRelayCommand(data, ws) {
|
|
|
90
98
|
try {
|
|
91
99
|
switch (action) {
|
|
92
100
|
case 'claude-query': {
|
|
93
|
-
// Run Claude via local Agent SDK / CLI
|
|
94
101
|
const { command, options } = data;
|
|
95
|
-
|
|
102
|
+
logRelayEvent('>', `Claude query: ${command?.slice(0, 60)}...`, 'cyan');
|
|
96
103
|
|
|
97
|
-
// Use claude CLI for the query
|
|
98
104
|
const args = ['--print'];
|
|
99
105
|
if (options?.projectPath) args.push('--cwd', options.projectPath);
|
|
100
106
|
if (options?.sessionId) args.push('--continue', options.sessionId);
|
|
@@ -133,7 +139,11 @@ async function handleRelayCommand(data, ws) {
|
|
|
133
139
|
|
|
134
140
|
case 'shell-command': {
|
|
135
141
|
const { command: cmd, cwd } = data;
|
|
136
|
-
|
|
142
|
+
// Block dangerous shell patterns
|
|
143
|
+
if (!cmd || typeof cmd !== 'string') throw new Error('Invalid command');
|
|
144
|
+
const dangerous = ['rm -rf /', 'mkfs', 'dd if=', ':(){', 'fork bomb', '> /dev/sd'];
|
|
145
|
+
if (dangerous.some(d => cmd.includes(d))) throw new Error('Command blocked for safety');
|
|
146
|
+
logRelayEvent('$', `Shell: ${cmd?.slice(0, 50)}`, 'dim');
|
|
137
147
|
const result = await execCommand(cmd, [], { cwd: cwd || os.homedir(), timeout: 60000 });
|
|
138
148
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
|
|
139
149
|
break;
|
|
@@ -141,14 +151,26 @@ async function handleRelayCommand(data, ws) {
|
|
|
141
151
|
|
|
142
152
|
case 'file-read': {
|
|
143
153
|
const { filePath } = data;
|
|
144
|
-
|
|
154
|
+
if (!filePath || typeof filePath !== 'string') throw new Error('Invalid file path');
|
|
155
|
+
// Block reading sensitive system files
|
|
156
|
+
const normalizedPath = path.resolve(filePath);
|
|
157
|
+
const blocked = ['/etc/shadow', '/etc/passwd', '.ssh/id_rsa', '.env'];
|
|
158
|
+
if (blocked.some(b => normalizedPath.includes(b))) throw new Error('Access denied');
|
|
159
|
+
logRelayEvent('R', `Read: ${filePath}`, 'dim');
|
|
160
|
+
const content = await fsPromises.readFile(normalizedPath, 'utf8');
|
|
145
161
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content } }));
|
|
146
162
|
break;
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
case 'file-write': {
|
|
150
166
|
const { filePath: fp, content: fileContent } = data;
|
|
151
|
-
|
|
167
|
+
if (!fp || typeof fp !== 'string') throw new Error('Invalid file path');
|
|
168
|
+
// Block writing to sensitive locations
|
|
169
|
+
const normalizedFp = path.resolve(fp);
|
|
170
|
+
const blockedDirs = ['/etc/', '/usr/bin/', '/usr/sbin/', 'System32', '.ssh/'];
|
|
171
|
+
if (blockedDirs.some(d => normalizedFp.includes(d))) throw new Error('Access denied');
|
|
172
|
+
logRelayEvent('W', `Write: ${fp}`, 'dim');
|
|
173
|
+
await fsPromises.writeFile(normalizedFp, fileContent, 'utf8');
|
|
152
174
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { success: true } }));
|
|
153
175
|
break;
|
|
154
176
|
}
|
|
@@ -162,7 +184,7 @@ async function handleRelayCommand(data, ws) {
|
|
|
162
184
|
|
|
163
185
|
case 'git-operation': {
|
|
164
186
|
const { gitCommand, cwd: gitCwd } = data;
|
|
165
|
-
|
|
187
|
+
logRelayEvent('G', `Git: ${gitCommand}`, 'dim');
|
|
166
188
|
const result = await execCommand('git', [gitCommand], { cwd: gitCwd });
|
|
167
189
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
|
|
168
190
|
break;
|
|
@@ -192,7 +214,7 @@ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
|
|
|
192
214
|
try {
|
|
193
215
|
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
194
216
|
const items = [];
|
|
195
|
-
for (const entry of entries.slice(0, 100)) {
|
|
217
|
+
for (const entry of entries.slice(0, 100)) {
|
|
196
218
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
197
219
|
const item = { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file' };
|
|
198
220
|
if (entry.isDirectory() && currentDepth < maxDepth - 1) {
|
|
@@ -207,7 +229,22 @@ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
|
|
|
207
229
|
}
|
|
208
230
|
|
|
209
231
|
/**
|
|
210
|
-
*
|
|
232
|
+
* Create WebSocket connection with optional API key in handshake
|
|
233
|
+
*/
|
|
234
|
+
function createRelayConnection(wsUrl, config = {}) {
|
|
235
|
+
const headers = {};
|
|
236
|
+
// Send API key in WebSocket headers if available
|
|
237
|
+
if (config.anthropicApiKey) {
|
|
238
|
+
headers['x-anthropic-api-key'] = config.anthropicApiKey;
|
|
239
|
+
}
|
|
240
|
+
headers['x-upfyn-version'] = VERSION;
|
|
241
|
+
headers['x-upfyn-machine'] = os.hostname();
|
|
242
|
+
|
|
243
|
+
return new WebSocket(wsUrl, { headers });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Main connect function (interactive — with animation and logging)
|
|
211
248
|
*/
|
|
212
249
|
export async function connectToServer(options = {}) {
|
|
213
250
|
const config = loadConfig();
|
|
@@ -215,43 +252,44 @@ export async function connectToServer(options = {}) {
|
|
|
215
252
|
const relayKey = options.key || config.relayKey;
|
|
216
253
|
|
|
217
254
|
if (!relayKey) {
|
|
218
|
-
console.
|
|
219
|
-
console.log('
|
|
220
|
-
console.log(
|
|
255
|
+
console.log('');
|
|
256
|
+
console.log(` ${c.red('FAIL')} No relay key provided.`);
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(` ${c.gray('Get your relay token from the web UI:')}`);
|
|
259
|
+
console.log(` ${c.dim('1.')} Sign in at ${c.cyan('https://cli.upfyn.com')}`);
|
|
260
|
+
console.log(` ${c.dim('2.')} Click ${c.bright('Connect')} button`);
|
|
261
|
+
console.log(` ${c.dim('3.')} Copy the command and run it here`);
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(` ${c.gray('Or run:')} ${c.bright('uc connect --server <url> --key upfyn_your_token')}`);
|
|
264
|
+
console.log('');
|
|
221
265
|
process.exit(1);
|
|
222
266
|
}
|
|
223
267
|
|
|
224
268
|
// Save config for future use
|
|
225
269
|
saveConfig({ ...config, server: serverUrl, relayKey });
|
|
226
270
|
|
|
271
|
+
// Show beautiful startup with rocket animation
|
|
272
|
+
await showConnectStartup(
|
|
273
|
+
serverUrl,
|
|
274
|
+
os.hostname(),
|
|
275
|
+
os.userInfo().username,
|
|
276
|
+
VERSION
|
|
277
|
+
);
|
|
278
|
+
|
|
227
279
|
const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
|
|
228
280
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
console.log(` Machine: ${c.dim(os.hostname())}`);
|
|
232
|
-
console.log(` User: ${c.dim(os.userInfo().username)}\n`);
|
|
281
|
+
const spinner = createSpinner('Connecting to server...');
|
|
282
|
+
spinner.start();
|
|
233
283
|
|
|
234
284
|
let reconnectAttempts = 0;
|
|
235
285
|
const MAX_RECONNECT = 10;
|
|
236
286
|
|
|
237
287
|
function connect() {
|
|
238
|
-
const ws =
|
|
288
|
+
const ws = createRelayConnection(wsUrl, config);
|
|
239
289
|
|
|
240
290
|
ws.on('open', () => {
|
|
241
291
|
reconnectAttempts = 0;
|
|
242
|
-
|
|
243
|
-
console.log(c.dim(' Press Ctrl+C to disconnect.\n'));
|
|
244
|
-
|
|
245
|
-
// Send heartbeat every 30 seconds
|
|
246
|
-
const heartbeat = setInterval(() => {
|
|
247
|
-
if (ws.readyState === 1) {
|
|
248
|
-
ws.send(JSON.stringify({ type: 'ping' }));
|
|
249
|
-
}
|
|
250
|
-
}, 30000);
|
|
251
|
-
|
|
252
|
-
ws.on('close', () => {
|
|
253
|
-
clearInterval(heartbeat);
|
|
254
|
-
});
|
|
292
|
+
// Don't stop spinner yet — wait for relay-connected message
|
|
255
293
|
});
|
|
256
294
|
|
|
257
295
|
ws.on('message', (rawMessage) => {
|
|
@@ -259,7 +297,12 @@ export async function connectToServer(options = {}) {
|
|
|
259
297
|
const data = JSON.parse(rawMessage);
|
|
260
298
|
|
|
261
299
|
if (data.type === 'relay-connected') {
|
|
262
|
-
|
|
300
|
+
spinner.stop('Connected!');
|
|
301
|
+
// Extract username from message if possible
|
|
302
|
+
const nameMatch = data.message?.match(/Connected as (.+?)\./);
|
|
303
|
+
const username = nameMatch ? nameMatch[1] : 'Unknown';
|
|
304
|
+
showConnectionBanner(username, serverUrl);
|
|
305
|
+
logRelayEvent('*', 'Relay active -- waiting for commands...', 'green');
|
|
263
306
|
return;
|
|
264
307
|
}
|
|
265
308
|
|
|
@@ -271,44 +314,112 @@ export async function connectToServer(options = {}) {
|
|
|
271
314
|
if (data.type === 'pong') return;
|
|
272
315
|
|
|
273
316
|
if (data.type === 'error') {
|
|
274
|
-
|
|
317
|
+
spinner.fail(`Server error: ${data.error}`);
|
|
275
318
|
return;
|
|
276
319
|
}
|
|
277
320
|
} catch (e) {
|
|
278
|
-
|
|
321
|
+
logRelayEvent('!', `Parse error: ${e.message}`, 'red');
|
|
279
322
|
}
|
|
280
323
|
});
|
|
281
324
|
|
|
282
325
|
ws.on('close', (code) => {
|
|
283
326
|
if (code === 1000) {
|
|
284
|
-
|
|
327
|
+
logRelayEvent('-', 'Disconnected gracefully.', 'dim');
|
|
285
328
|
process.exit(0);
|
|
286
329
|
}
|
|
287
330
|
|
|
288
331
|
reconnectAttempts++;
|
|
289
332
|
if (reconnectAttempts <= MAX_RECONNECT) {
|
|
290
333
|
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
291
|
-
|
|
334
|
+
logRelayEvent('~', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
|
|
292
335
|
setTimeout(connect, delay);
|
|
293
336
|
} else {
|
|
294
|
-
|
|
337
|
+
logRelayEvent('X', 'Max reconnection attempts reached. Exiting.', 'red');
|
|
295
338
|
process.exit(1);
|
|
296
339
|
}
|
|
297
340
|
});
|
|
298
341
|
|
|
299
342
|
ws.on('error', (err) => {
|
|
300
343
|
if (err.code === 'ECONNREFUSED') {
|
|
301
|
-
|
|
344
|
+
spinner.fail(`Cannot reach ${serverUrl}. Is the server running?`);
|
|
302
345
|
}
|
|
303
346
|
// close handler will trigger reconnect
|
|
304
347
|
});
|
|
348
|
+
|
|
349
|
+
// Heartbeat every 30 seconds
|
|
350
|
+
const heartbeat = setInterval(() => {
|
|
351
|
+
if (ws.readyState === 1) {
|
|
352
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
353
|
+
} else {
|
|
354
|
+
clearInterval(heartbeat);
|
|
355
|
+
}
|
|
356
|
+
}, 30000);
|
|
305
357
|
}
|
|
306
358
|
|
|
307
359
|
connect();
|
|
308
360
|
|
|
309
361
|
// Graceful shutdown
|
|
310
362
|
process.on('SIGINT', () => {
|
|
311
|
-
console.log(
|
|
363
|
+
console.log('');
|
|
364
|
+
logRelayEvent('-', 'Disconnecting...', 'dim');
|
|
312
365
|
process.exit(0);
|
|
313
366
|
});
|
|
314
367
|
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Background connect function (silent — used when uc launches Claude Code)
|
|
371
|
+
* Runs relay in the background without animation or user-facing output.
|
|
372
|
+
*/
|
|
373
|
+
export function connectToServerBackground(options = {}) {
|
|
374
|
+
const config = loadConfig();
|
|
375
|
+
const serverUrl = options.server || config.server;
|
|
376
|
+
const relayKey = options.key || config.relayKey;
|
|
377
|
+
|
|
378
|
+
if (!serverUrl || !relayKey) return;
|
|
379
|
+
|
|
380
|
+
const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
|
|
381
|
+
|
|
382
|
+
let reconnectAttempts = 0;
|
|
383
|
+
const MAX_RECONNECT = 5;
|
|
384
|
+
|
|
385
|
+
function connect() {
|
|
386
|
+
const ws = createRelayConnection(wsUrl, config);
|
|
387
|
+
|
|
388
|
+
ws.on('message', (rawMessage) => {
|
|
389
|
+
try {
|
|
390
|
+
const data = JSON.parse(rawMessage);
|
|
391
|
+
if (data.type === 'relay-command') {
|
|
392
|
+
handleRelayCommand(data, ws);
|
|
393
|
+
}
|
|
394
|
+
} catch { /* ignore */ }
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
ws.on('open', () => {
|
|
398
|
+
reconnectAttempts = 0;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
ws.on('close', (code) => {
|
|
402
|
+
if (code === 1000) return;
|
|
403
|
+
reconnectAttempts++;
|
|
404
|
+
if (reconnectAttempts <= MAX_RECONNECT) {
|
|
405
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
406
|
+
setTimeout(connect, delay);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
ws.on('error', () => {
|
|
411
|
+
// silent — close handler will reconnect
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Heartbeat
|
|
415
|
+
const heartbeat = setInterval(() => {
|
|
416
|
+
if (ws.readyState === 1) {
|
|
417
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
418
|
+
} else {
|
|
419
|
+
clearInterval(heartbeat);
|
|
420
|
+
}
|
|
421
|
+
}, 30000);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
connect();
|
|
425
|
+
}
|
package/server/routes/agent.js
CHANGED
|
@@ -4,15 +4,34 @@ import path from 'path';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
|
-
import { userDb, apiKeysDb, githubTokensDb } from '../database/db.js';
|
|
7
|
+
import { userDb, apiKeysDb, githubTokensDb, credentialsDb } from '../database/db.js';
|
|
8
8
|
import { addProjectManually } from '../projects.js';
|
|
9
9
|
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
10
10
|
import { spawnCursor } from '../cursor-cli.js';
|
|
11
11
|
import { queryCodex } from '../openai-codex.js';
|
|
12
12
|
import { Octokit } from '@octokit/rest';
|
|
13
13
|
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
|
|
14
|
+
import { queryOpenRouter, OPENROUTER_MODELS } from '../openrouter.js';
|
|
14
15
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
15
16
|
|
|
17
|
+
// BYOK helper: get user's stored API key for a provider
|
|
18
|
+
async function getUserProviderKey(userId, providerType) {
|
|
19
|
+
if (!userId) return null;
|
|
20
|
+
try {
|
|
21
|
+
const creds = await credentialsDb.getCredentials(userId, providerType);
|
|
22
|
+
const active = creds.find(c => c.is_active);
|
|
23
|
+
return active?.credential_value || null;
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function withUserApiKey(envKey, userKey, fn) {
|
|
28
|
+
if (!userKey) return fn();
|
|
29
|
+
const prev = process.env[envKey];
|
|
30
|
+
process.env[envKey] = userKey;
|
|
31
|
+
try { return await fn(); }
|
|
32
|
+
finally { if (prev !== undefined) process.env[envKey] = prev; else delete process.env[envKey]; }
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
const router = express.Router();
|
|
17
36
|
|
|
18
37
|
/**
|
|
@@ -939,17 +958,22 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
939
958
|
});
|
|
940
959
|
}
|
|
941
960
|
|
|
942
|
-
// Start the appropriate session
|
|
961
|
+
// Start the appropriate session (with BYOK key injection where applicable)
|
|
962
|
+
const userId = req.user?.id;
|
|
963
|
+
|
|
943
964
|
if (provider === 'claude') {
|
|
944
965
|
console.log('🤖 Starting Claude SDK session');
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
966
|
+
const userKey = await getUserProviderKey(userId, 'anthropic_key');
|
|
967
|
+
|
|
968
|
+
await withUserApiKey('ANTHROPIC_API_KEY', userKey, () =>
|
|
969
|
+
queryClaudeSDK(message.trim(), {
|
|
970
|
+
projectPath: finalProjectPath,
|
|
971
|
+
cwd: finalProjectPath,
|
|
972
|
+
sessionId: null,
|
|
973
|
+
model: model,
|
|
974
|
+
permissionMode: 'bypassPermissions'
|
|
975
|
+
}, writer)
|
|
976
|
+
);
|
|
953
977
|
|
|
954
978
|
} else if (provider === 'cursor') {
|
|
955
979
|
console.log('🖱️ Starting Cursor CLI session');
|
|
@@ -957,19 +981,30 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
957
981
|
await spawnCursor(message.trim(), {
|
|
958
982
|
projectPath: finalProjectPath,
|
|
959
983
|
cwd: finalProjectPath,
|
|
960
|
-
sessionId: null,
|
|
984
|
+
sessionId: null,
|
|
961
985
|
model: model || undefined,
|
|
962
|
-
skipPermissions: true
|
|
986
|
+
skipPermissions: true
|
|
963
987
|
}, writer);
|
|
964
988
|
} else if (provider === 'codex') {
|
|
965
989
|
console.log('🤖 Starting Codex SDK session');
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
990
|
+
const userKey = await getUserProviderKey(userId, 'openai_key');
|
|
991
|
+
|
|
992
|
+
await withUserApiKey('OPENAI_API_KEY', userKey, () =>
|
|
993
|
+
queryCodex(message.trim(), {
|
|
994
|
+
projectPath: finalProjectPath,
|
|
995
|
+
cwd: finalProjectPath,
|
|
996
|
+
sessionId: null,
|
|
997
|
+
model: model || CODEX_MODELS.DEFAULT,
|
|
998
|
+
permissionMode: 'bypassPermissions'
|
|
999
|
+
}, writer)
|
|
1000
|
+
);
|
|
1001
|
+
} else if (provider === 'openrouter') {
|
|
1002
|
+
console.log('🌐 Starting OpenRouter session');
|
|
1003
|
+
const userKey = await getUserProviderKey(userId, 'openrouter_key');
|
|
1004
|
+
|
|
1005
|
+
await queryOpenRouter(message.trim(), {
|
|
1006
|
+
model: model || OPENROUTER_MODELS.DEFAULT,
|
|
1007
|
+
apiKey: userKey,
|
|
973
1008
|
}, writer);
|
|
974
1009
|
}
|
|
975
1010
|
|
package/server/routes/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import bcrypt from 'bcryptjs';
|
|
3
|
-
import { userDb, subscriptionDb, relayTokensDb, apiKeysDb } from '../database/db.js';
|
|
3
|
+
import { db, userDb, subscriptionDb, relayTokensDb, apiKeysDb } from '../database/db.js';
|
|
4
4
|
import { generateToken, authenticateToken, setSessionCookie, clearSessionCookie } from '../middleware/auth.js';
|
|
5
5
|
|
|
6
6
|
const router = express.Router();
|
|
@@ -19,8 +19,12 @@ router.get('/status', async (req, res) => {
|
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
// User registration — allows multiple users
|
|
23
|
-
router.post('/register',
|
|
22
|
+
// User registration — allows multiple users (rate limited)
|
|
23
|
+
router.post('/register', (req, res, next) => {
|
|
24
|
+
const rl = req.app.locals.authRateLimit;
|
|
25
|
+
if (rl) return rl(req, res, next);
|
|
26
|
+
next();
|
|
27
|
+
}, async (req, res) => {
|
|
24
28
|
try {
|
|
25
29
|
const { username, password, email, phone, firstName, lastName } = req.body;
|
|
26
30
|
|
|
@@ -28,13 +32,16 @@ router.post('/register', async (req, res) => {
|
|
|
28
32
|
if (!password) {
|
|
29
33
|
return res.status(400).json({ error: 'Password is required' });
|
|
30
34
|
}
|
|
31
|
-
if (password.length <
|
|
32
|
-
return res.status(400).json({ error: 'Password must be at least
|
|
35
|
+
if (password.length < 8) {
|
|
36
|
+
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
|
37
|
+
}
|
|
38
|
+
if (password.length > 128) {
|
|
39
|
+
return res.status(400).json({ error: 'Password is too long' });
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
//
|
|
36
|
-
const fName = (firstName || '').trim();
|
|
37
|
-
const lName = (lastName || '').trim();
|
|
42
|
+
// Sanitize and validate name/phone inputs
|
|
43
|
+
const fName = (firstName || '').trim().slice(0, 50);
|
|
44
|
+
const lName = (lastName || '').trim().slice(0, 50);
|
|
38
45
|
const displayName = username || [fName, lName].filter(Boolean).join(' ') || 'User';
|
|
39
46
|
|
|
40
47
|
if (displayName.length < 2) {
|
|
@@ -86,39 +93,69 @@ router.post('/register', async (req, res) => {
|
|
|
86
93
|
}
|
|
87
94
|
});
|
|
88
95
|
|
|
89
|
-
// User login
|
|
90
|
-
router.post('/login',
|
|
96
|
+
// User login (rate limited)
|
|
97
|
+
router.post('/login', (req, res, next) => {
|
|
98
|
+
const rl = req.app.locals.authRateLimit;
|
|
99
|
+
if (rl) return rl(req, res, next);
|
|
100
|
+
next();
|
|
101
|
+
}, async (req, res) => {
|
|
91
102
|
try {
|
|
92
|
-
const { username, password } = req.body;
|
|
103
|
+
const { username, password, firstName, lastName, phone } = req.body;
|
|
93
104
|
|
|
94
105
|
if (!username || !password) {
|
|
95
|
-
return res.status(400).json({ error: '
|
|
106
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
const user = await userDb.getUserByUsername(username.trim());
|
|
99
110
|
if (!user) {
|
|
100
|
-
return res.status(401).json({ error: 'Invalid
|
|
111
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
|
104
115
|
if (!isValidPassword) {
|
|
105
|
-
return res.status(401).json({ error: 'Invalid
|
|
116
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Update name/phone if provided and different from stored values
|
|
120
|
+
const fName = (firstName || '').trim().slice(0, 50);
|
|
121
|
+
const lName = (lastName || '').trim().slice(0, 50);
|
|
122
|
+
const ph = (phone || '').trim().slice(0, 20);
|
|
123
|
+
const updates = [];
|
|
124
|
+
const args = [];
|
|
125
|
+
if (fName && fName !== user.first_name) { updates.push('first_name = ?'); args.push(fName); }
|
|
126
|
+
if (lName && lName !== user.last_name) { updates.push('last_name = ?'); args.push(lName); }
|
|
127
|
+
if (ph && ph !== user.phone) { updates.push('phone = ?'); args.push(ph); }
|
|
128
|
+
if (fName && lName && `${fName} ${lName}` !== user.username) {
|
|
129
|
+
updates.push('username = ?');
|
|
130
|
+
args.push(`${fName} ${lName}`);
|
|
131
|
+
} else if (fName && !lName && fName !== user.username && !user.last_name) {
|
|
132
|
+
updates.push('username = ?');
|
|
133
|
+
args.push(fName);
|
|
134
|
+
}
|
|
135
|
+
if (updates.length > 0) {
|
|
136
|
+
try {
|
|
137
|
+
args.push(user.id);
|
|
138
|
+
await db.execute({ sql: `UPDATE users SET ${updates.join(', ')} WHERE id = ?`, args });
|
|
139
|
+
} catch { /* non-critical profile update */ }
|
|
106
140
|
}
|
|
107
141
|
|
|
142
|
+
// Re-fetch user to get updated fields
|
|
143
|
+
const updatedUser = updates.length > 0 ? (await userDb.getUserById(user.id)) || user : user;
|
|
144
|
+
|
|
108
145
|
// Generate token + set cookie
|
|
109
|
-
const token = generateToken(
|
|
146
|
+
const token = generateToken(updatedUser);
|
|
110
147
|
setSessionCookie(res, token);
|
|
111
|
-
await userDb.updateLastLogin(
|
|
148
|
+
await userDb.updateLastLogin(updatedUser.id);
|
|
112
149
|
|
|
113
150
|
// Backfill relay token + API key if missing (for users created before auto-provisioning)
|
|
114
151
|
try {
|
|
115
|
-
const existingTokens = await relayTokensDb.getTokens(
|
|
152
|
+
const existingTokens = await relayTokensDb.getTokens(updatedUser.id);
|
|
116
153
|
if (existingTokens.length === 0) {
|
|
117
|
-
await relayTokensDb.createToken(
|
|
154
|
+
await relayTokensDb.createToken(updatedUser.id, 'default');
|
|
118
155
|
}
|
|
119
|
-
const existingKeys = await apiKeysDb.getApiKeys(
|
|
156
|
+
const existingKeys = await apiKeysDb.getApiKeys(updatedUser.id);
|
|
120
157
|
if (existingKeys.length === 0) {
|
|
121
|
-
await apiKeysDb.createApiKey(
|
|
158
|
+
await apiKeysDb.createApiKey(updatedUser.id, 'default');
|
|
122
159
|
}
|
|
123
160
|
} catch { /* non-critical backfill */ }
|
|
124
161
|
|
|
@@ -126,7 +163,7 @@ router.post('/login', async (req, res) => {
|
|
|
126
163
|
let subscription = null;
|
|
127
164
|
try {
|
|
128
165
|
await subscriptionDb.expireOverdue();
|
|
129
|
-
const sub = await subscriptionDb.getActiveSub(
|
|
166
|
+
const sub = await subscriptionDb.getActiveSub(updatedUser.id);
|
|
130
167
|
if (sub) {
|
|
131
168
|
subscription = { id: sub.id, planId: sub.plan_id, status: sub.status, startsAt: sub.starts_at, expiresAt: sub.expires_at };
|
|
132
169
|
}
|
|
@@ -134,7 +171,7 @@ router.post('/login', async (req, res) => {
|
|
|
134
171
|
|
|
135
172
|
res.json({
|
|
136
173
|
success: true,
|
|
137
|
-
user: { id:
|
|
174
|
+
user: { id: updatedUser.user_code || `upc-${String(updatedUser.id).padStart(3, '0')}`, username: updatedUser.username, first_name: updatedUser.first_name, last_name: updatedUser.last_name, email: updatedUser.email, phone: updatedUser.phone, access_override: updatedUser.access_override || null, subscription },
|
|
138
175
|
token // backward compat
|
|
139
176
|
});
|
|
140
177
|
|
|
@@ -175,4 +175,95 @@ router.patch('/credentials/:credentialId/toggle', async (req, res) => {
|
|
|
175
175
|
}
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
// ===============================
|
|
179
|
+
// AI Provider Keys (BYOK)
|
|
180
|
+
// ===============================
|
|
181
|
+
|
|
182
|
+
const AI_PROVIDER_TYPES = [
|
|
183
|
+
'anthropic_key',
|
|
184
|
+
'openai_key',
|
|
185
|
+
'openrouter_key',
|
|
186
|
+
'google_key',
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// Get all AI provider keys for the authenticated user (masked values)
|
|
190
|
+
router.get('/ai-providers', async (req, res) => {
|
|
191
|
+
try {
|
|
192
|
+
const allCreds = [];
|
|
193
|
+
for (const type of AI_PROVIDER_TYPES) {
|
|
194
|
+
const creds = await credentialsDb.getCredentials(req.user.id, type);
|
|
195
|
+
allCreds.push(...creds.map(c => ({
|
|
196
|
+
id: c.id,
|
|
197
|
+
credential_name: c.credential_name,
|
|
198
|
+
credential_type: c.credential_type,
|
|
199
|
+
description: c.description,
|
|
200
|
+
is_active: c.is_active,
|
|
201
|
+
created_at: c.created_at,
|
|
202
|
+
// Mask the key — show first 8 and last 4 chars
|
|
203
|
+
masked_value: c.credential_value
|
|
204
|
+
? c.credential_value.slice(0, 8) + '...' + c.credential_value.slice(-4)
|
|
205
|
+
: '***',
|
|
206
|
+
})));
|
|
207
|
+
}
|
|
208
|
+
res.json({ providers: allCreds });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
res.status(500).json({ error: 'Failed to fetch AI provider keys' });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Save an AI provider key
|
|
215
|
+
router.post('/ai-providers', async (req, res) => {
|
|
216
|
+
try {
|
|
217
|
+
const { providerType, apiKey, name } = req.body;
|
|
218
|
+
|
|
219
|
+
if (!providerType || !AI_PROVIDER_TYPES.includes(providerType)) {
|
|
220
|
+
return res.status(400).json({ error: `Invalid provider type. Supported: ${AI_PROVIDER_TYPES.join(', ')}` });
|
|
221
|
+
}
|
|
222
|
+
if (!apiKey || !apiKey.trim()) {
|
|
223
|
+
return res.status(400).json({ error: 'API key is required' });
|
|
224
|
+
}
|
|
225
|
+
if (apiKey.trim().length < 10 || apiKey.trim().length > 256) {
|
|
226
|
+
return res.status(400).json({ error: 'Invalid API key length' });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const label = providerType.replace('_key', '').replace('_', ' ');
|
|
230
|
+
const credName = name?.trim() || `${label} API key`;
|
|
231
|
+
|
|
232
|
+
// Deactivate existing keys of same type (user should only have one active per provider)
|
|
233
|
+
const existing = await credentialsDb.getCredentials(req.user.id, providerType);
|
|
234
|
+
for (const cred of existing) {
|
|
235
|
+
if (cred.is_active) {
|
|
236
|
+
await credentialsDb.toggleCredential(req.user.id, cred.id, false);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = await credentialsDb.createCredential(
|
|
241
|
+
req.user.id,
|
|
242
|
+
credName,
|
|
243
|
+
providerType,
|
|
244
|
+
apiKey.trim(),
|
|
245
|
+
`User-provided ${label} API key`
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
res.json({ success: true, credential: { id: result.id, credential_type: providerType, credential_name: credName } });
|
|
249
|
+
} catch (error) {
|
|
250
|
+
res.status(500).json({ error: 'Failed to save AI provider key' });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Delete an AI provider key
|
|
255
|
+
router.delete('/ai-providers/:credentialId', async (req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
const { credentialId } = req.params;
|
|
258
|
+
const success = await credentialsDb.deleteCredential(req.user.id, parseInt(credentialId));
|
|
259
|
+
if (success) {
|
|
260
|
+
res.json({ success: true });
|
|
261
|
+
} else {
|
|
262
|
+
res.status(404).json({ error: 'Provider key not found' });
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
res.status(500).json({ error: 'Failed to delete provider key' });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
178
269
|
export default router;
|