upfynai-code 3.0.4 → 3.2.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 (258) hide show
  1. package/README.md +69 -92
  2. package/bin/cli.js +191 -0
  3. package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
  4. package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
  5. package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
  6. package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
  7. package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
  8. package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
  9. package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
  10. package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
  11. package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
  12. package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
  13. package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
  14. package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
  15. package/dist/client/assets/index-CnNNzw9A.css +1 -0
  16. package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
  17. package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
  18. package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
  19. package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
  20. package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
  21. package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
  22. package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
  23. package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
  24. package/dist/client/favicon.png +0 -0
  25. package/dist/client/icons/icon-128x128.png +0 -0
  26. package/dist/client/icons/icon-144x144.png +0 -0
  27. package/dist/client/icons/icon-152x152.png +0 -0
  28. package/dist/client/icons/icon-192x192.png +0 -0
  29. package/dist/client/icons/icon-384x384.png +0 -0
  30. package/dist/client/icons/icon-512x512.png +0 -0
  31. package/dist/client/icons/icon-72x72.png +0 -0
  32. package/dist/client/icons/icon-96x96.png +0 -0
  33. package/{client/dist → dist/client}/index.html +37 -36
  34. package/dist/client/logo-128.png +0 -0
  35. package/dist/client/logo-256.png +0 -0
  36. package/dist/client/logo-32.png +0 -0
  37. package/dist/client/logo-512.png +0 -0
  38. package/dist/client/logo-64.png +0 -0
  39. package/dist/client/logo.png +0 -0
  40. package/{client/dist → dist/client}/manifest.json +12 -12
  41. package/{client/dist → dist/client}/mcp-docs.html +1 -1
  42. package/{client/dist → dist/client}/sw.js +2 -2
  43. package/package.json +56 -105
  44. package/scripts/postinstall.js +9 -0
  45. package/scripts/prepublish.js +77 -0
  46. package/src/animation.js +228 -0
  47. package/src/auth.js +142 -0
  48. package/src/config.js +40 -0
  49. package/src/connect.js +416 -0
  50. package/src/launch.js +81 -0
  51. package/src/mcp.js +57 -0
  52. package/src/permissions.js +140 -0
  53. package/src/persistent-shell.js +261 -0
  54. package/src/server.js +54 -0
  55. package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
  56. package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
  57. package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
  58. package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
  59. package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
  60. package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
  61. package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
  62. package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
  63. package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
  64. package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
  65. package/client/dist/assets/index-46kkVu2i.css +0 -1
  66. package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
  67. package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
  68. package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
  69. package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
  70. package/client/dist/favicon.png +0 -0
  71. package/client/dist/icons/icon-128x128.png +0 -0
  72. package/client/dist/icons/icon-144x144.png +0 -0
  73. package/client/dist/icons/icon-152x152.png +0 -0
  74. package/client/dist/icons/icon-192x192.png +0 -0
  75. package/client/dist/icons/icon-384x384.png +0 -0
  76. package/client/dist/icons/icon-512x512.png +0 -0
  77. package/client/dist/icons/icon-72x72.png +0 -0
  78. package/client/dist/icons/icon-96x96.png +0 -0
  79. package/client/dist/logo-128.png +0 -0
  80. package/client/dist/logo-256.png +0 -0
  81. package/client/dist/logo-32.png +0 -0
  82. package/client/dist/logo-512.png +0 -0
  83. package/client/dist/logo-64.png +0 -0
  84. package/commands/upfynai-connect.md +0 -59
  85. package/commands/upfynai-disconnect.md +0 -31
  86. package/commands/upfynai-doctor.md +0 -99
  87. package/commands/upfynai-export.md +0 -49
  88. package/commands/upfynai-local.md +0 -82
  89. package/commands/upfynai-status.md +0 -75
  90. package/commands/upfynai-stop.md +0 -49
  91. package/commands/upfynai-uninstall.md +0 -58
  92. package/commands/upfynai.md +0 -69
  93. package/scripts/build-client.js +0 -17
  94. package/scripts/fix-node-pty.js +0 -67
  95. package/scripts/install-commands.js +0 -78
  96. package/server/agent-loop.js +0 -242
  97. package/server/auto-compact.js +0 -99
  98. package/server/browser.js +0 -131
  99. package/server/claude-sdk.js +0 -797
  100. package/server/cli-ui.js +0 -798
  101. package/server/cli.js +0 -751
  102. package/server/constants/config.js +0 -31
  103. package/server/cursor-cli.js +0 -270
  104. package/server/database/auth.db +0 -0
  105. package/server/database/db.js +0 -1547
  106. package/server/database/init.sql +0 -70
  107. package/server/index.js +0 -3813
  108. package/server/load-env.js +0 -26
  109. package/server/mcp-server.js +0 -621
  110. package/server/middleware/auth.js +0 -184
  111. package/server/middleware/relayHelpers.js +0 -44
  112. package/server/middleware/sandboxRouter.js +0 -174
  113. package/server/openai-codex.js +0 -403
  114. package/server/openrouter.js +0 -137
  115. package/server/projects.js +0 -1807
  116. package/server/provider-factory.js +0 -174
  117. package/server/relay-client.js +0 -390
  118. package/server/routes/agent.js +0 -1234
  119. package/server/routes/auth.js +0 -559
  120. package/server/routes/browser.js +0 -419
  121. package/server/routes/canvas.js +0 -53
  122. package/server/routes/cli-auth.js +0 -263
  123. package/server/routes/codex.js +0 -396
  124. package/server/routes/commands.js +0 -707
  125. package/server/routes/composio.js +0 -176
  126. package/server/routes/cursor.js +0 -770
  127. package/server/routes/dashboard.js +0 -295
  128. package/server/routes/git.js +0 -1208
  129. package/server/routes/keys.js +0 -34
  130. package/server/routes/mcp-utils.js +0 -48
  131. package/server/routes/mcp.js +0 -661
  132. package/server/routes/payments.js +0 -227
  133. package/server/routes/projects.js +0 -754
  134. package/server/routes/sessions.js +0 -146
  135. package/server/routes/settings.js +0 -261
  136. package/server/routes/taskmaster.js +0 -1928
  137. package/server/routes/user.js +0 -106
  138. package/server/routes/vapi-chat.js +0 -624
  139. package/server/routes/voice.js +0 -235
  140. package/server/routes/webhooks.js +0 -166
  141. package/server/routes/workflows.js +0 -312
  142. package/server/sandbox.js +0 -120
  143. package/server/services/browser-ai.js +0 -154
  144. package/server/services/composio.js +0 -204
  145. package/server/services/sessionRegistry.js +0 -139
  146. package/server/services/whisperService.js +0 -84
  147. package/server/services/workflowScheduler.js +0 -211
  148. package/server/tests/relay-flow.test.js +0 -570
  149. package/server/tests/sessions.test.js +0 -259
  150. package/server/utils/commandParser.js +0 -303
  151. package/server/utils/email.js +0 -66
  152. package/server/utils/gitConfig.js +0 -24
  153. package/server/utils/mcp-detector.js +0 -198
  154. package/server/utils/taskmaster-websocket.js +0 -129
  155. package/shared/integrationCatalog.d.ts +0 -12
  156. package/shared/integrationCatalog.js +0 -172
  157. package/shared/modelConstants.js +0 -96
  158. /package/{shared → dist}/agents/claude.js +0 -0
  159. /package/{shared → dist}/agents/codex.js +0 -0
  160. /package/{shared → dist}/agents/cursor.js +0 -0
  161. /package/{shared → dist}/agents/detect.js +0 -0
  162. /package/{shared → dist}/agents/exec.js +0 -0
  163. /package/{shared → dist}/agents/files.js +0 -0
  164. /package/{shared → dist}/agents/git.js +0 -0
  165. /package/{shared → dist}/agents/gitagent.js +0 -0
  166. /package/{shared → dist}/agents/index.js +0 -0
  167. /package/{shared → dist}/agents/shell.js +0 -0
  168. /package/{shared → dist}/agents/utils.js +0 -0
  169. /package/{client/dist → dist/client}/api-docs.html +0 -0
  170. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  171. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  172. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  173. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  174. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  175. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  176. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  177. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  178. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  179. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  180. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  181. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  182. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  183. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  184. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  185. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  186. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  187. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  188. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  189. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  190. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  191. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  192. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  193. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  194. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  195. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  196. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  197. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  198. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  199. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  200. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  201. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  202. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  203. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  204. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  205. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  206. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  207. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  208. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  209. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  210. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  211. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  212. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  213. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  214. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  215. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  216. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  217. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  218. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  219. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  220. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  221. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  222. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  223. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  224. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  225. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  226. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  227. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  228. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  229. /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
  230. /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
  231. /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
  232. /package/{client/dist → dist/client}/clear-cache.html +0 -0
  233. /package/{client/dist → dist/client}/convert-icons.md +0 -0
  234. /package/{client/dist → dist/client}/favicon.svg +0 -0
  235. /package/{client/dist → dist/client}/generate-icons.js +0 -0
  236. /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
  237. /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
  238. /package/{client/dist → dist/client}/icons/codex.svg +0 -0
  239. /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
  240. /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
  241. /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
  242. /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
  243. /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
  244. /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
  245. /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
  246. /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
  247. /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
  248. /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
  249. /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
  250. /package/{client/dist → dist/client}/logo.svg +0 -0
  251. /package/{client/dist → dist/client}/offline.html +0 -0
  252. /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
  253. /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
  254. /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
  255. /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
  256. /package/{shared → dist}/gitagent/index.js +0 -0
  257. /package/{shared → dist}/gitagent/parser.js +0 -0
  258. /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
@@ -1,559 +0,0 @@
1
- import express from 'express';
2
- import crypto from 'crypto';
3
- // bcrypt removed — plaintext password storage for admin access
4
- let OAuth2Client;
5
- try {
6
- OAuth2Client = (await import('google-auth-library')).OAuth2Client;
7
- } catch {
8
- // google-auth-library not available — Google OAuth will be disabled
9
- }
10
- import { db, userDb, subscriptionDb, relayTokensDb, apiKeysDb, resetTokenDb } from '../database/db.js';
11
- import { generateToken, authenticateToken, setSessionCookie, clearSessionCookie } from '../middleware/auth.js';
12
- import { sendPasswordResetEmail } from '../utils/email.js';
13
-
14
- const googleClient = (process.env.GOOGLE_CLIENT_ID && OAuth2Client) ? new OAuth2Client(process.env.GOOGLE_CLIENT_ID) : null;
15
-
16
- const router = express.Router();
17
-
18
- // Check auth status and setup requirements
19
- router.get('/status', async (req, res) => {
20
- try {
21
- const hasUsers = await userDb.hasUsers();
22
- res.json({
23
- needsSetup: !hasUsers,
24
- isAuthenticated: false
25
- });
26
- } catch (error) {
27
- // auth status error
28
- res.status(500).json({ error: 'Internal server error' });
29
- }
30
- });
31
-
32
- // User registration — allows multiple users (rate limited)
33
- router.post('/register', (req, res, next) => {
34
- const rl = req.app.locals.authRateLimit;
35
- if (rl) return rl(req, res, next);
36
- next();
37
- }, async (req, res) => {
38
- try {
39
- const { username, password, email, phone, firstName, lastName } = req.body;
40
-
41
- // Validate input
42
- if (!password) {
43
- return res.status(400).json({ error: 'Password is required' });
44
- }
45
- if (password.length < 8) {
46
- return res.status(400).json({ error: 'Password must be at least 8 characters' });
47
- }
48
- if (password.length > 128) {
49
- return res.status(400).json({ error: 'Password is too long' });
50
- }
51
-
52
- // Sanitize and validate name/phone inputs
53
- const fName = (firstName || '').trim().slice(0, 50);
54
- const lName = (lastName || '').trim().slice(0, 50);
55
- const displayName = username || [fName, lName].filter(Boolean).join(' ') || 'User';
56
-
57
- if (displayName.length < 2) {
58
- return res.status(400).json({ error: 'Name must be at least 2 characters' });
59
- }
60
-
61
- // Check if email is already registered
62
- if (email) {
63
- const existingUser = await userDb.getUserByUsername(email);
64
- if (existingUser) {
65
- return res.status(409).json({ error: 'An account with this email already exists. Please sign in.' });
66
- }
67
- }
68
-
69
- // Store password directly (plaintext for admin access)
70
- const passwordHash = password;
71
-
72
- // Create user
73
- const user = await userDb.createUser(displayName, passwordHash, email || null, phone || null, fName || null, lName || null);
74
-
75
- // Auto-create default relay token + API key for the new user
76
- let connectToken = null;
77
- try {
78
- const relayToken = await relayTokensDb.createToken(user.id, 'default');
79
- connectToken = relayToken.token;
80
- await apiKeysDb.createApiKey(user.id, 'default');
81
- } catch { /* non-critical — user can create manually later */ }
82
-
83
- // Generate token + set cookie
84
- const token = generateToken(user);
85
- setSessionCookie(res, token);
86
- await userDb.updateLastLogin(user.id);
87
-
88
- // New user — no subscription yet
89
- res.json({
90
- success: true,
91
- user: { id: user.user_code || `upc-${String(user.id).padStart(3, '0')}`, username: user.username, first_name: user.first_name, last_name: user.last_name, email: email || null, phone: phone || null, access_override: user.access_override || null, subscription: null },
92
- token, // still returned for backward compat / API clients
93
- connectToken // relay token for CLI connection
94
- });
95
-
96
- } catch (error) {
97
- // registration error
98
- if (error.message?.includes('UNIQUE')) {
99
- res.status(409).json({ error: 'An account with this name already exists. Try a different name or sign in.' });
100
- } else {
101
- res.status(500).json({ error: 'Internal server error' });
102
- }
103
- }
104
- });
105
-
106
- // User login (rate limited)
107
- router.post('/login', (req, res, next) => {
108
- const rl = req.app.locals.authRateLimit;
109
- if (rl) return rl(req, res, next);
110
- next();
111
- }, async (req, res) => {
112
- try {
113
- const { username, password } = req.body;
114
-
115
- if (!username || !password) {
116
- return res.status(400).json({ error: 'Identifier and password are required' });
117
- }
118
-
119
- const user = await userDb.getUserByUsername(username.trim());
120
- if (!user) {
121
- return res.status(401).json({ error: 'Invalid credentials' });
122
- }
123
-
124
- const isValidPassword = (password === user.password_hash);
125
- if (!isValidPassword) {
126
- return res.status(401).json({ error: 'Invalid credentials' });
127
- }
128
-
129
- const updatedUser = user;
130
-
131
- // Generate token + set cookie
132
- const token = generateToken(updatedUser);
133
- setSessionCookie(res, token);
134
- await userDb.updateLastLogin(updatedUser.id);
135
-
136
- // Backfill relay token + API key if missing (for users created before auto-provisioning)
137
- try {
138
- const existingTokens = await relayTokensDb.getTokens(updatedUser.id);
139
- if (existingTokens.length === 0) {
140
- await relayTokensDb.createToken(updatedUser.id, 'default');
141
- }
142
- const existingKeys = await apiKeysDb.getApiKeys(updatedUser.id);
143
- if (existingKeys.length === 0) {
144
- await apiKeysDb.createApiKey(updatedUser.id, 'default');
145
- }
146
- } catch { /* non-critical backfill */ }
147
-
148
- // Include active subscription if any
149
- let subscription = null;
150
- try {
151
- await subscriptionDb.expireOverdue();
152
- const sub = await subscriptionDb.getActiveSub(updatedUser.id);
153
- if (sub) {
154
- subscription = { id: sub.id, planId: sub.plan_id, status: sub.status, startsAt: sub.starts_at, expiresAt: sub.expires_at };
155
- }
156
- } catch { /* non-critical */ }
157
-
158
- res.json({
159
- success: true,
160
- 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 },
161
- token // backward compat
162
- });
163
-
164
- } catch (error) {
165
- // login error
166
- res.status(500).json({ error: 'Internal server error' });
167
- }
168
- });
169
-
170
- // Get current user (protected route) — includes active subscription
171
- router.get('/user', authenticateToken, async (req, res) => {
172
- try {
173
- // Expire overdue subs, then fetch active
174
- await subscriptionDb.expireOverdue();
175
- const sub = await subscriptionDb.getActiveSub(req.user.id);
176
-
177
- // Map internal id → user_code for frontend, include all user details
178
- const user = {
179
- id: req.user.user_code || `upc-${String(req.user.id).padStart(3, '0')}`,
180
- username: req.user.username,
181
- first_name: req.user.first_name,
182
- last_name: req.user.last_name,
183
- email: req.user.email,
184
- phone: req.user.phone,
185
- access_override: req.user.access_override || null,
186
- created_at: req.user.created_at,
187
- };
188
-
189
- if (sub) {
190
- user.subscription = {
191
- id: sub.id,
192
- planId: sub.plan_id,
193
- status: sub.status,
194
- startsAt: sub.starts_at,
195
- expiresAt: sub.expires_at,
196
- };
197
- } else {
198
- user.subscription = null;
199
- }
200
-
201
- res.json({ user });
202
- } catch (error) {
203
- // user fetch error
204
- const u = req.user;
205
- res.json({ user: { id: u.user_code || `upc-${String(u.id).padStart(3, '0')}`, username: u.username, first_name: u.first_name, last_name: u.last_name, email: u.email, phone: u.phone } });
206
- }
207
- });
208
-
209
- // Get a fresh JWT for the current session (used by frontend to pass to iframe)
210
- router.get('/token', authenticateToken, (req, res) => {
211
- const token = generateToken(req.user);
212
- res.json({ token });
213
- });
214
-
215
- // Get user's tokens — relay token (for Connect + MCP) and API key
216
- router.get('/connect-token', authenticateToken, async (req, res) => {
217
- try {
218
- // Relay token — used for both CLI connect and MCP auth
219
- const tokens = await relayTokensDb.getTokens(req.user.id);
220
- let active = tokens.find(t => t.is_active);
221
- if (!active) {
222
- // Auto-create if none exists (backfill for existing users)
223
- active = await relayTokensDb.createToken(req.user.id, 'default');
224
- }
225
-
226
- // API key — also available if user needs it
227
- const keys = await apiKeysDb.getApiKeys(req.user.id);
228
- let activeKey = keys.find(k => k.is_active);
229
- if (!activeKey) {
230
- activeKey = await apiKeysDb.createApiKey(req.user.id, 'default');
231
- }
232
-
233
- res.json({
234
- token: active.token, // relay token — works for Connect + MCP
235
- apiKey: activeKey.api_key, // API key — alternative auth method
236
- userCode: req.user.user_code || null,
237
- });
238
- } catch (error) {
239
- res.status(500).json({ error: 'Could not fetch connect token' });
240
- }
241
- });
242
-
243
- // Update profile — phone only (email and other sensitive fields are read-only)
244
- router.patch('/profile', authenticateToken, async (req, res) => {
245
- try {
246
- const userId = req.user.id;
247
- const { phone } = req.body;
248
-
249
- if (phone === undefined) {
250
- return res.status(400).json({ error: 'No fields to update' });
251
- }
252
-
253
- const trimmed = (phone || '').trim().slice(0, 20);
254
- if (trimmed && !/^[+]?[\d\s()-]{7,20}$/.test(trimmed)) {
255
- return res.status(400).json({ error: 'Invalid phone format' });
256
- }
257
-
258
- await db.execute({ sql: 'UPDATE users SET phone = ? WHERE id = ?', args: [trimmed || null, userId] });
259
-
260
- const updated = await userDb.getUserById(userId);
261
- res.json({
262
- success: true,
263
- user: { phone: updated?.phone || null }
264
- });
265
- } catch (error) {
266
- res.status(500).json({ error: 'Failed to update profile' });
267
- }
268
- });
269
-
270
- // Regenerate relay token
271
- router.post('/regenerate-token', authenticateToken, async (req, res) => {
272
- try {
273
- await relayTokensDb.deactivateAll(req.user.id);
274
- const newToken = await relayTokensDb.createToken(req.user.id, 'default');
275
- res.json({
276
- success: true,
277
- token: newToken.token,
278
- connectCommand: `uc connect --token ${newToken.token}`,
279
- message: 'Relay token regenerated. Update your CLI connection.',
280
- });
281
- } catch (error) {
282
- res.status(500).json({ error: 'Failed to regenerate relay token' });
283
- }
284
- });
285
-
286
- // Logout — clear the session cookie
287
- router.post('/logout', authenticateToken, (req, res) => {
288
- clearSessionCookie(res);
289
- res.json({ success: true, message: 'Logged out successfully' });
290
- });
291
-
292
- // Change password
293
- router.patch('/password', authenticateToken, async (req, res) => {
294
- try {
295
- const { currentPassword, newPassword } = req.body;
296
- if (!currentPassword || !newPassword) {
297
- return res.status(400).json({ error: 'Current password and new password are required' });
298
- }
299
- if (newPassword.length < 6) {
300
- return res.status(400).json({ error: 'New password must be at least 6 characters' });
301
- }
302
-
303
- const result = await db.execute({ sql: 'SELECT password_hash FROM users WHERE id = ?', args: [req.user.id] });
304
- const row = result.rows?.[0];
305
- if (!row) return res.status(404).json({ error: 'User not found' });
306
-
307
- const valid = (currentPassword === row.password_hash);
308
- if (!valid) return res.status(401).json({ error: 'Current password is incorrect' });
309
-
310
- await db.execute({ sql: 'UPDATE users SET password_hash = ? WHERE id = ?', args: [newPassword, req.user.id] });
311
-
312
- res.json({ success: true, message: 'Password changed successfully' });
313
- } catch (error) {
314
- res.status(500).json({ error: 'Failed to change password' });
315
- }
316
- });
317
-
318
- // Delete account (soft-delete: sets is_active = 0)
319
- router.delete('/account', authenticateToken, async (req, res) => {
320
- try {
321
- const { password } = req.body;
322
- if (!password) return res.status(400).json({ error: 'Password is required to delete account' });
323
-
324
- const result = await db.execute({ sql: 'SELECT password_hash FROM users WHERE id = ?', args: [req.user.id] });
325
- const row = result.rows?.[0];
326
- if (!row) return res.status(404).json({ error: 'User not found' });
327
-
328
- const valid = (password === row.password_hash);
329
- if (!valid) return res.status(401).json({ error: 'Incorrect password' });
330
-
331
- // Soft-delete: deactivate user and all their API keys
332
- await db.execute({ sql: 'UPDATE users SET is_active = 0 WHERE id = ?', args: [req.user.id] });
333
- await db.execute({ sql: 'UPDATE api_keys SET is_active = 0 WHERE user_id = ?', args: [req.user.id] });
334
-
335
- clearSessionCookie(res);
336
- res.json({ success: true, message: 'Account deleted' });
337
- } catch (error) {
338
- res.status(500).json({ error: 'Failed to delete account' });
339
- }
340
- });
341
-
342
- // ─── Google OAuth Sign-In ────────────────────────────────────────────────────
343
- // Accepts either:
344
- // { token } — GSI ID token (popup flow)
345
- // { code, redirect_uri } — OAuth 2.0 authorization code (redirect flow)
346
-
347
- router.post('/google', (req, res, next) => {
348
- const rl = req.app.locals.authRateLimit;
349
- if (rl) return rl(req, res, next);
350
- next();
351
- }, async (req, res) => {
352
- try {
353
- if (!googleClient) {
354
- return res.status(503).json({ error: 'Google sign-in is not configured' });
355
- }
356
-
357
- let { token } = req.body;
358
- const { code, redirect_uri } = req.body;
359
-
360
- // OAuth 2.0 code exchange — convert authorization code to ID token
361
- if (code && !token) {
362
- if (!process.env.GOOGLE_CLIENT_SECRET) {
363
- return res.status(503).json({ error: 'Google OAuth code exchange is not configured' });
364
- }
365
- try {
366
- const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
367
- method: 'POST',
368
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
369
- body: new URLSearchParams({
370
- code,
371
- client_id: process.env.GOOGLE_CLIENT_ID,
372
- client_secret: process.env.GOOGLE_CLIENT_SECRET,
373
- redirect_uri: redirect_uri || `${req.headers.origin || 'https://cli.upfyn.com'}/auth/callback`,
374
- grant_type: 'authorization_code',
375
- }),
376
- });
377
- const tokens = await tokenRes.json();
378
- if (tokens.error) {
379
- return res.status(401).json({ error: 'Failed to exchange Google authorization code' });
380
- }
381
- token = tokens.id_token;
382
- } catch {
383
- return res.status(401).json({ error: 'Failed to exchange Google authorization code' });
384
- }
385
- }
386
-
387
- if (!token) {
388
- return res.status(400).json({ error: 'Google token or authorization code is required' });
389
- }
390
-
391
- // Verify the Google ID token
392
- let ticket;
393
- try {
394
- ticket = await googleClient.verifyIdToken({
395
- idToken: token,
396
- audience: process.env.GOOGLE_CLIENT_ID,
397
- });
398
- } catch {
399
- return res.status(401).json({ error: 'Invalid Google token' });
400
- }
401
-
402
- const payload = ticket.getPayload();
403
- const googleId = payload.sub;
404
- const email = payload.email;
405
- const firstName = payload.given_name || '';
406
- const lastName = payload.family_name || '';
407
-
408
- if (!email) {
409
- return res.status(400).json({ error: 'Google account must have an email address' });
410
- }
411
-
412
- // Try to find existing user — first by Google ID, then by email
413
- let user = await userDb.getUserByGoogleId(googleId);
414
-
415
- if (!user) {
416
- // Check if a user with this email already exists (registered with email/password)
417
- user = await userDb.getUserByEmail(email);
418
-
419
- if (user) {
420
- // Link Google ID to existing account
421
- await userDb.setUserGoogleId(user.id, googleId);
422
- } else {
423
- // Create new user — random password (they'll use Google to login)
424
- const randomPassword = crypto.randomBytes(32).toString('hex');
425
- const passwordHash = randomPassword;
426
- const displayName = [firstName, lastName].filter(Boolean).join(' ') || email.split('@')[0];
427
-
428
- user = await userDb.createUser(displayName, passwordHash, email, null, firstName || null, lastName || null);
429
-
430
- // Link Google ID
431
- await userDb.setUserGoogleId(user.id, googleId);
432
-
433
- // Auto-create default relay token + API key
434
- try {
435
- await relayTokensDb.createToken(user.id, 'default');
436
- await apiKeysDb.createApiKey(user.id, 'default');
437
- } catch { /* non-critical */ }
438
- }
439
- }
440
-
441
- // Generate token + set cookie (same as normal login)
442
- const jwtToken = generateToken(user);
443
- setSessionCookie(res, jwtToken);
444
- await userDb.updateLastLogin(user.id);
445
-
446
- // Backfill relay token + API key if missing
447
- try {
448
- const existingTokens = await relayTokensDb.getTokens(user.id);
449
- if (existingTokens.length === 0) {
450
- await relayTokensDb.createToken(user.id, 'default');
451
- }
452
- const existingKeys = await apiKeysDb.getApiKeys(user.id);
453
- if (existingKeys.length === 0) {
454
- await apiKeysDb.createApiKey(user.id, 'default');
455
- }
456
- } catch { /* non-critical */ }
457
-
458
- // Include active subscription if any
459
- let subscription = null;
460
- try {
461
- await subscriptionDb.expireOverdue();
462
- const sub = await subscriptionDb.getActiveSub(user.id);
463
- if (sub) {
464
- subscription = { id: sub.id, planId: sub.plan_id, status: sub.status, startsAt: sub.starts_at, expiresAt: sub.expires_at };
465
- }
466
- } catch { /* non-critical */ }
467
-
468
- res.json({
469
- success: true,
470
- user: {
471
- id: user.user_code || `upc-${String(user.id).padStart(3, '0')}`,
472
- username: user.username,
473
- first_name: user.first_name || firstName,
474
- last_name: user.last_name || lastName,
475
- email: user.email || email,
476
- phone: user.phone || null,
477
- access_override: user.access_override || null,
478
- subscription,
479
- },
480
- token: jwtToken,
481
- });
482
- } catch (error) {
483
- res.status(500).json({ error: 'Internal server error' });
484
- }
485
- });
486
-
487
- // ─── Forgot Password ─────────────────────────────────────────────────────────
488
-
489
- router.post('/forgot-password', (req, res, next) => {
490
- const rl = req.app.locals.authRateLimit;
491
- if (rl) return rl(req, res, next);
492
- next();
493
- }, async (req, res) => {
494
- try {
495
- const { email } = req.body;
496
- if (!email) {
497
- return res.status(400).json({ error: 'Email is required' });
498
- }
499
-
500
- // Always return success to prevent email enumeration
501
- const successResponse = { success: true, message: 'If an account with that email exists, a password reset link has been sent.' };
502
-
503
- const user = await userDb.getUserByEmail(email.trim());
504
- if (!user) {
505
- return res.json(successResponse);
506
- }
507
-
508
- // Generate secure reset token
509
- const resetToken = crypto.randomBytes(32).toString('hex');
510
- const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
511
-
512
- await resetTokenDb.create(user.id, resetToken, expiresAt);
513
-
514
- // Send email
515
- await sendPasswordResetEmail(email.trim(), resetToken);
516
-
517
- // Clean up old tokens periodically
518
- try { await resetTokenDb.cleanExpired(); } catch { /* non-critical */ }
519
-
520
- res.json(successResponse);
521
- } catch (error) {
522
- res.status(500).json({ error: 'Internal server error' });
523
- }
524
- });
525
-
526
- // ─── Reset Password ──────────────────────────────────────────────────────────
527
-
528
- router.post('/reset-password', async (req, res) => {
529
- try {
530
- const { token, newPassword } = req.body;
531
-
532
- if (!token || !newPassword) {
533
- return res.status(400).json({ error: 'Token and new password are required' });
534
- }
535
- if (newPassword.length < 8) {
536
- return res.status(400).json({ error: 'Password must be at least 8 characters' });
537
- }
538
- if (newPassword.length > 128) {
539
- return res.status(400).json({ error: 'Password is too long' });
540
- }
541
-
542
- const resetRecord = await resetTokenDb.getValid(token);
543
- if (!resetRecord) {
544
- return res.status(400).json({ error: 'Invalid or expired reset link. Please request a new one.' });
545
- }
546
-
547
- // Update password (plaintext for admin access)
548
- await userDb.updatePasswordHash(resetRecord.user_id, newPassword);
549
-
550
- // Mark token as used
551
- await resetTokenDb.markUsed(resetRecord.id);
552
-
553
- res.json({ success: true, message: 'Password has been reset successfully. You can now sign in.' });
554
- } catch (error) {
555
- res.status(500).json({ error: 'Internal server error' });
556
- }
557
- });
558
-
559
- export default router;