libre-webui 0.2.4

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 (233) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +204 -0
  3. package/backend/dist/db.d.ts +19 -0
  4. package/backend/dist/db.d.ts.map +1 -0
  5. package/backend/dist/db.js +355 -0
  6. package/backend/dist/db.js.map +1 -0
  7. package/backend/dist/env.d.ts +2 -0
  8. package/backend/dist/env.d.ts.map +1 -0
  9. package/backend/dist/env.js +22 -0
  10. package/backend/dist/env.js.map +1 -0
  11. package/backend/dist/index.d.ts +4 -0
  12. package/backend/dist/index.d.ts.map +1 -0
  13. package/backend/dist/index.js +751 -0
  14. package/backend/dist/index.js.map +1 -0
  15. package/backend/dist/middleware/auth.d.ts +18 -0
  16. package/backend/dist/middleware/auth.d.ts.map +1 -0
  17. package/backend/dist/middleware/auth.js +98 -0
  18. package/backend/dist/middleware/auth.js.map +1 -0
  19. package/backend/dist/middleware/index.d.ts +5 -0
  20. package/backend/dist/middleware/index.d.ts.map +1 -0
  21. package/backend/dist/middleware/index.js +62 -0
  22. package/backend/dist/middleware/index.js.map +1 -0
  23. package/backend/dist/models/personaModel.d.ts +37 -0
  24. package/backend/dist/models/personaModel.d.ts.map +1 -0
  25. package/backend/dist/models/personaModel.js +269 -0
  26. package/backend/dist/models/personaModel.js.map +1 -0
  27. package/backend/dist/models/userModel.d.ts +86 -0
  28. package/backend/dist/models/userModel.d.ts.map +1 -0
  29. package/backend/dist/models/userModel.js +212 -0
  30. package/backend/dist/models/userModel.js.map +1 -0
  31. package/backend/dist/routes/auth.d.ts +3 -0
  32. package/backend/dist/routes/auth.d.ts.map +1 -0
  33. package/backend/dist/routes/auth.js +389 -0
  34. package/backend/dist/routes/auth.js.map +1 -0
  35. package/backend/dist/routes/chat.d.ts +3 -0
  36. package/backend/dist/routes/chat.d.ts.map +1 -0
  37. package/backend/dist/routes/chat.js +767 -0
  38. package/backend/dist/routes/chat.js.map +1 -0
  39. package/backend/dist/routes/documents.d.ts +3 -0
  40. package/backend/dist/routes/documents.d.ts.map +1 -0
  41. package/backend/dist/routes/documents.js +244 -0
  42. package/backend/dist/routes/documents.js.map +1 -0
  43. package/backend/dist/routes/ollama.d.ts +3 -0
  44. package/backend/dist/routes/ollama.d.ts.map +1 -0
  45. package/backend/dist/routes/ollama.js +549 -0
  46. package/backend/dist/routes/ollama.js.map +1 -0
  47. package/backend/dist/routes/personas.d.ts +3 -0
  48. package/backend/dist/routes/personas.d.ts.map +1 -0
  49. package/backend/dist/routes/personas.js +505 -0
  50. package/backend/dist/routes/personas.js.map +1 -0
  51. package/backend/dist/routes/plugins.d.ts +3 -0
  52. package/backend/dist/routes/plugins.d.ts.map +1 -0
  53. package/backend/dist/routes/plugins.js +417 -0
  54. package/backend/dist/routes/plugins.js.map +1 -0
  55. package/backend/dist/routes/preferences.d.ts +3 -0
  56. package/backend/dist/routes/preferences.d.ts.map +1 -0
  57. package/backend/dist/routes/preferences.js +303 -0
  58. package/backend/dist/routes/preferences.js.map +1 -0
  59. package/backend/dist/routes/tts.d.ts +3 -0
  60. package/backend/dist/routes/tts.d.ts.map +1 -0
  61. package/backend/dist/routes/tts.js +304 -0
  62. package/backend/dist/routes/tts.js.map +1 -0
  63. package/backend/dist/routes/users.d.ts +3 -0
  64. package/backend/dist/routes/users.d.ts.map +1 -0
  65. package/backend/dist/routes/users.js +246 -0
  66. package/backend/dist/routes/users.js.map +1 -0
  67. package/backend/dist/services/authService.d.ts +51 -0
  68. package/backend/dist/services/authService.d.ts.map +1 -0
  69. package/backend/dist/services/authService.js +153 -0
  70. package/backend/dist/services/authService.js.map +1 -0
  71. package/backend/dist/services/chatService.d.ts +52 -0
  72. package/backend/dist/services/chatService.d.ts.map +1 -0
  73. package/backend/dist/services/chatService.js +645 -0
  74. package/backend/dist/services/chatService.js.map +1 -0
  75. package/backend/dist/services/documentService.d.ts +34 -0
  76. package/backend/dist/services/documentService.d.ts.map +1 -0
  77. package/backend/dist/services/documentService.js +428 -0
  78. package/backend/dist/services/documentService.js.map +1 -0
  79. package/backend/dist/services/encryptionService.d.ts +62 -0
  80. package/backend/dist/services/encryptionService.d.ts.map +1 -0
  81. package/backend/dist/services/encryptionService.js +284 -0
  82. package/backend/dist/services/encryptionService.js.map +1 -0
  83. package/backend/dist/services/memoryService.d.ts +140 -0
  84. package/backend/dist/services/memoryService.d.ts.map +1 -0
  85. package/backend/dist/services/memoryService.js +867 -0
  86. package/backend/dist/services/memoryService.js.map +1 -0
  87. package/backend/dist/services/mutationEngineService.d.ts +49 -0
  88. package/backend/dist/services/mutationEngineService.d.ts.map +1 -0
  89. package/backend/dist/services/mutationEngineService.js +432 -0
  90. package/backend/dist/services/mutationEngineService.js.map +1 -0
  91. package/backend/dist/services/ollamaService.d.ts +55 -0
  92. package/backend/dist/services/ollamaService.d.ts.map +1 -0
  93. package/backend/dist/services/ollamaService.js +450 -0
  94. package/backend/dist/services/ollamaService.js.map +1 -0
  95. package/backend/dist/services/personaService.d.ts +67 -0
  96. package/backend/dist/services/personaService.d.ts.map +1 -0
  97. package/backend/dist/services/personaService.js +373 -0
  98. package/backend/dist/services/personaService.js.map +1 -0
  99. package/backend/dist/services/pluginService.d.ts +42 -0
  100. package/backend/dist/services/pluginService.d.ts.map +1 -0
  101. package/backend/dist/services/pluginService.js +961 -0
  102. package/backend/dist/services/pluginService.js.map +1 -0
  103. package/backend/dist/services/preferencesService.d.ts +35 -0
  104. package/backend/dist/services/preferencesService.d.ts.map +1 -0
  105. package/backend/dist/services/preferencesService.js +255 -0
  106. package/backend/dist/services/preferencesService.js.map +1 -0
  107. package/backend/dist/services/simpleGitHubOAuth.d.ts +48 -0
  108. package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -0
  109. package/backend/dist/services/simpleGitHubOAuth.js +203 -0
  110. package/backend/dist/services/simpleGitHubOAuth.js.map +1 -0
  111. package/backend/dist/services/simpleHuggingFaceOAuth.d.ts +43 -0
  112. package/backend/dist/services/simpleHuggingFaceOAuth.d.ts.map +1 -0
  113. package/backend/dist/services/simpleHuggingFaceOAuth.js +159 -0
  114. package/backend/dist/services/simpleHuggingFaceOAuth.js.map +1 -0
  115. package/backend/dist/services/userService.d.ts +1 -0
  116. package/backend/dist/services/userService.d.ts.map +1 -0
  117. package/backend/dist/services/userService.js +18 -0
  118. package/backend/dist/services/userService.js.map +1 -0
  119. package/backend/dist/storage.d.ts +55 -0
  120. package/backend/dist/storage.d.ts.map +1 -0
  121. package/backend/dist/storage.js +741 -0
  122. package/backend/dist/storage.js.map +1 -0
  123. package/backend/dist/test-encryption.d.ts +2 -0
  124. package/backend/dist/test-encryption.d.ts.map +1 -0
  125. package/backend/dist/test-encryption.js +64 -0
  126. package/backend/dist/test-encryption.js.map +1 -0
  127. package/backend/dist/types/index.d.ts +523 -0
  128. package/backend/dist/types/index.d.ts.map +1 -0
  129. package/backend/dist/types/index.js +31 -0
  130. package/backend/dist/types/index.js.map +1 -0
  131. package/backend/dist/utils/generationUtils.d.ts +10 -0
  132. package/backend/dist/utils/generationUtils.d.ts.map +1 -0
  133. package/backend/dist/utils/generationUtils.js +49 -0
  134. package/backend/dist/utils/generationUtils.js.map +1 -0
  135. package/backend/dist/utils/hash.d.ts +29 -0
  136. package/backend/dist/utils/hash.d.ts.map +1 -0
  137. package/backend/dist/utils/hash.js +73 -0
  138. package/backend/dist/utils/hash.js.map +1 -0
  139. package/backend/dist/utils/jwt.d.ts +37 -0
  140. package/backend/dist/utils/jwt.d.ts.map +1 -0
  141. package/backend/dist/utils/jwt.js +86 -0
  142. package/backend/dist/utils/jwt.js.map +1 -0
  143. package/bin/cli.js +150 -0
  144. package/electron/main.js +322 -0
  145. package/frontend/dist/_redirects +1 -0
  146. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  147. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  148. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  149. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  150. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  151. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  152. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  153. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  154. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  155. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  156. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  157. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  158. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  159. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  160. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  161. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  162. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  163. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  164. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  165. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  166. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  167. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  168. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  169. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  170. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  171. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  172. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  173. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  174. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  175. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  176. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  177. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  178. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  179. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  180. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  181. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  182. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  183. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  184. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  185. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  186. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  187. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  188. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  189. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  190. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  191. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  192. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  193. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  194. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  195. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  196. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  197. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  198. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  199. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  200. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  201. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  202. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  203. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  204. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  205. package/frontend/dist/assets/index-CRQkB7Wz.js +3 -0
  206. package/frontend/dist/css/index-B1OjddR-.css +1 -0
  207. package/frontend/dist/favicon-dark.png +0 -0
  208. package/frontend/dist/favicon-light.png +0 -0
  209. package/frontend/dist/index.html +23 -0
  210. package/frontend/dist/js/ArtifactContainer-c_oi7XMs.js +23 -0
  211. package/frontend/dist/js/ArtifactDemoPage-CdfwJVXu.js +98 -0
  212. package/frontend/dist/js/ChatPage-CyotkmS0.js +281 -0
  213. package/frontend/dist/js/ModelsPage-DNaziPHc.js +2 -0
  214. package/frontend/dist/js/PersonasPage-DcnbJf8Q.js +13 -0
  215. package/frontend/dist/js/UserManagementPage-DtTf92dS.js +1 -0
  216. package/frontend/dist/js/markdown-vendor-D-79K2xZ.js +22 -0
  217. package/frontend/dist/js/react-vendor-N--QU9DW.js +8 -0
  218. package/frontend/dist/js/router-vendor-B-t91v39.js +3 -0
  219. package/frontend/dist/js/ui-vendor-VxSCY_bv.js +177 -0
  220. package/frontend/dist/js/utils-vendor-DNzxLBGx.js +6 -0
  221. package/frontend/dist/logo-dark.png +0 -0
  222. package/frontend/dist/logo-light.png +0 -0
  223. package/frontend/dist/logo.svg +14 -0
  224. package/package.json +128 -0
  225. package/plugins/anthropic.json +25 -0
  226. package/plugins/elevenlabs.json +58 -0
  227. package/plugins/gemini.json +57 -0
  228. package/plugins/github.json +23 -0
  229. package/plugins/groq.json +25 -0
  230. package/plugins/mistral.json +73 -0
  231. package/plugins/openai-tts.json +38 -0
  232. package/plugins/openai.json +132 -0
  233. package/plugins/openrouter.json +353 -0
@@ -0,0 +1,751 @@
1
+ /*
2
+ * Libre WebUI
3
+ * Copyright (C) 2025 Kroonen AI, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ // Load environment variables FIRST before any other imports
18
+ import './env.js';
19
+ /*
20
+ * Libre WebUI
21
+ * Copyright (C) 2025 Kroonen AI, Inc.
22
+ *
23
+ * Licensed under the Apache License, Version 2.0 (the "License");
24
+ * you may not use this file except in compliance with the License.
25
+ * You may obtain a copy of the License at:
26
+ *
27
+ * http://www.apache.org/licenses/LICENSE-2.0
28
+ *
29
+ * Unless required by applicable law or agreed to in writing, software
30
+ * distributed under the License is distributed on an "AS IS" BASIS,
31
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32
+ * See the License for the specific language governing permissions and
33
+ * limitations under the License.
34
+ */
35
+ import express from 'express';
36
+ import rateLimit from 'express-rate-limit';
37
+ import cors from 'cors';
38
+ import helmet from 'helmet';
39
+ import morgan from 'morgan';
40
+ import { createServer } from 'http';
41
+ import { WebSocketServer } from 'ws';
42
+ import { errorHandler, notFoundHandler, requestLogger, } from './middleware/index.js';
43
+ import { optionalAuth } from './middleware/auth.js';
44
+ import ollamaRoutes from './routes/ollama.js';
45
+ import chatRoutes from './routes/chat.js';
46
+ import preferencesRoutes from './routes/preferences.js';
47
+ import pluginRoutes from './routes/plugins.js';
48
+ import documentRoutes from './routes/documents.js';
49
+ import authRoutes from './routes/auth.js';
50
+ import usersRoutes from './routes/users.js';
51
+ import personaRoutes from './routes/personas.js';
52
+ import ttsRoutes from './routes/tts.js';
53
+ import ollamaService from './services/ollamaService.js';
54
+ import chatService from './services/chatService.js';
55
+ import { GitHubOAuthService } from './services/simpleGitHubOAuth.js';
56
+ import { HuggingFaceOAuthService } from './services/simpleHuggingFaceOAuth.js';
57
+ import pluginService from './services/pluginService.js';
58
+ import preferencesService from './services/preferencesService.js';
59
+ import documentService from './services/documentService.js';
60
+ import { mergeGenerationOptions } from './utils/generationUtils.js';
61
+ import { verifyToken } from './utils/jwt.js';
62
+ const app = express();
63
+ const port = process.env.PORT || 3001;
64
+ const corsOrigins = process.env.CORS_ORIGIN?.split(',') || [
65
+ 'http://localhost:5173',
66
+ 'http://localhost:3000',
67
+ 'http://localhost:8080',
68
+ ];
69
+ // Multi-user safe CORS configuration
70
+ const corsConfig = {
71
+ origin: (origin, callback) => {
72
+ // Allow requests with no origin (mobile apps, etc.)
73
+ if (!origin)
74
+ return callback(null, true);
75
+ // Check if the origin is in our allowed list
76
+ if (corsOrigins.indexOf(origin) !== -1) {
77
+ callback(null, true);
78
+ }
79
+ else {
80
+ // In development mode, allow network access (when --host is used)
81
+ // This allows access from network IPs like http://192.168.x.x:8080 or http://10.x.x.x:8080
82
+ const isDevelopment = process.env.NODE_ENV !== 'production';
83
+ const isNetworkOrigin = origin &&
84
+ /^https?:\/\/(?:192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|127\.|localhost)/.test(origin);
85
+ if (isDevelopment && isNetworkOrigin) {
86
+ callback(null, true);
87
+ }
88
+ else {
89
+ callback(new Error('Not allowed by CORS'));
90
+ }
91
+ }
92
+ },
93
+ };
94
+ // Security middleware
95
+ app.use(helmet({
96
+ // COEP - disable in Docker/development to avoid proxy issues
97
+ crossOriginEmbedderPolicy: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
98
+ ? true
99
+ : false,
100
+ // Content Security Policy - Docker-aware configuration
101
+ contentSecurityPolicy: {
102
+ directives: {
103
+ defaultSrc: ["'self'"],
104
+ scriptSrc: [
105
+ "'self'",
106
+ ...(process.env.NODE_ENV === 'production'
107
+ ? [] // Strict in production
108
+ : ["'unsafe-inline'", "'unsafe-eval'"]), // Allow for dev tools
109
+ ],
110
+ styleSrc: [
111
+ "'self'",
112
+ "'unsafe-inline'", // Required for styled-components and CSS-in-JS
113
+ 'https://fonts.googleapis.com',
114
+ ],
115
+ imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
116
+ connectSrc: [
117
+ "'self'",
118
+ 'ws:',
119
+ 'wss:',
120
+ 'https:',
121
+ 'http:',
122
+ // WebSocket connections - flexible for Docker networking
123
+ `ws://localhost:${port}`,
124
+ `wss://localhost:${port}`,
125
+ 'ws://libre-webui:3001',
126
+ 'wss://libre-webui:3001',
127
+ ...(process.env.NODE_ENV !== 'production'
128
+ ? [
129
+ 'http://localhost:*',
130
+ 'ws://localhost:*',
131
+ 'http://libre-webui:*',
132
+ 'ws://libre-webui:*',
133
+ ]
134
+ : []),
135
+ ],
136
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
137
+ objectSrc: ["'none'"],
138
+ frameAncestors: ["'self'"],
139
+ formAction: ["'self'"],
140
+ upgradeInsecureRequests: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
141
+ ? []
142
+ : null,
143
+ baseUri: ["'self'"],
144
+ manifestSrc: ["'self'"],
145
+ workerSrc: ["'self'", 'blob:'],
146
+ },
147
+ },
148
+ // HSTS - disabled in Docker to avoid reverse proxy conflicts
149
+ hsts: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
150
+ ? {
151
+ maxAge: 31536000, // 1 year
152
+ includeSubDomains: true,
153
+ preload: true,
154
+ }
155
+ : false, // Disabled in Docker/development
156
+ // Prevent clickjacking
157
+ frameguard: { action: 'deny' },
158
+ // Prevent MIME type sniffing
159
+ noSniff: true,
160
+ // Hide X-Powered-By header
161
+ hidePoweredBy: true,
162
+ // Prevent XSS attacks
163
+ xssFilter: true,
164
+ }));
165
+ // CORS configuration
166
+ app.use(cors({
167
+ ...corsConfig,
168
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
169
+ allowedHeaders: ['Content-Type', 'Authorization'],
170
+ credentials: true,
171
+ }));
172
+ // Logging
173
+ if (process.env.NODE_ENV !== 'test') {
174
+ app.use(morgan('combined'));
175
+ }
176
+ app.use(requestLogger);
177
+ // Body parsing
178
+ app.use(express.json({ limit: '10mb' }));
179
+ app.use(express.urlencoded({ extended: true }));
180
+ // Health check endpoint
181
+ app.get('/health', (req, res) => {
182
+ res.json({
183
+ success: true,
184
+ message: 'Libre WebUI Backend is running',
185
+ timestamp: new Date().toISOString(),
186
+ });
187
+ });
188
+ // Static files are served by a separate frontend server on port 8080
189
+ // Backend only serves API endpoints
190
+ // Rate limiter for the /api/personas route
191
+ const personasRateLimiter = rateLimit({
192
+ windowMs: 15 * 60 * 1000, // 15 minutes
193
+ max: 100, // limit each IP to 100 requests per windowMs
194
+ message: {
195
+ success: false,
196
+ error: 'Too many requests from this IP, please try again later.',
197
+ },
198
+ standardHeaders: true,
199
+ legacyHeaders: false,
200
+ });
201
+ // Rate limiter for the /api/preferences route
202
+ const preferencesRateLimiter = rateLimit({
203
+ windowMs: 15 * 60 * 1000, // 15 minutes
204
+ max: 100, // limit each IP to 100 requests per windowMs
205
+ message: {
206
+ success: false,
207
+ error: 'Too many requests from this IP, please try again later.',
208
+ },
209
+ standardHeaders: true,
210
+ legacyHeaders: false,
211
+ });
212
+ // Rate limiter for the /api/ollama route
213
+ const ollamaRateLimiter = rateLimit({
214
+ windowMs: 15 * 60 * 1000, // 15 minutes
215
+ max: 10000, // limit each IP to 10000 requests per windowMs (very high limit for streaming chunks)
216
+ message: {
217
+ success: false,
218
+ error: 'Too many requests from this IP, please try again later.',
219
+ },
220
+ standardHeaders: true,
221
+ legacyHeaders: false,
222
+ });
223
+ // Rate limiter for the /api/documents route
224
+ const documentsRateLimiter = rateLimit({
225
+ windowMs: 15 * 60 * 1000, // 15 minutes
226
+ max: 100, // limit each IP to 100 requests per windowMs
227
+ message: {
228
+ success: false,
229
+ error: 'Too many requests from this IP, please try again later.',
230
+ },
231
+ standardHeaders: true,
232
+ legacyHeaders: false,
233
+ });
234
+ // Rate limiter for the /api/auth route (general limit, specific limits applied within route)
235
+ const authRateLimiter = rateLimit({
236
+ windowMs: 15 * 60 * 1000, // 15 minutes
237
+ max: 100, // limit each IP to 100 requests per windowMs (higher level limit)
238
+ message: {
239
+ success: false,
240
+ error: 'Too many requests from this IP, please try again later.',
241
+ },
242
+ standardHeaders: true,
243
+ legacyHeaders: false,
244
+ });
245
+ // Rate limiter for the /api/users route (general limit, specific limits applied within route)
246
+ const usersRateLimiter = rateLimit({
247
+ windowMs: 15 * 60 * 1000, // 15 minutes
248
+ max: 50, // limit each IP to 50 requests per windowMs (moderate limit for user management)
249
+ message: {
250
+ success: false,
251
+ error: 'Too many requests from this IP, please try again later.',
252
+ },
253
+ standardHeaders: true,
254
+ legacyHeaders: false,
255
+ });
256
+ // Rate limiter for the /api/chat route (general limit, specific limits applied within route)
257
+ const chatRateLimiter = rateLimit({
258
+ windowMs: 15 * 60 * 1000, // 15 minutes
259
+ max: 1000, // limit each IP to 1000 requests per windowMs (high limit for chat interactions)
260
+ message: {
261
+ success: false,
262
+ error: 'Too many requests from this IP, please try again later.',
263
+ },
264
+ standardHeaders: true,
265
+ legacyHeaders: false,
266
+ });
267
+ // Rate limiter for TTS routes (higher limit for info endpoints, generation has stricter limits in route)
268
+ const ttsRateLimiter = rateLimit({
269
+ windowMs: 15 * 60 * 1000, // 15 minutes
270
+ max: 500, // limit each IP to 500 requests per windowMs
271
+ message: {
272
+ success: false,
273
+ error: 'Too many TTS requests from this IP, please try again later.',
274
+ },
275
+ standardHeaders: true,
276
+ legacyHeaders: false,
277
+ });
278
+ // API routes
279
+ app.use('/api/auth', authRateLimiter, optionalAuth, authRoutes);
280
+ app.use('/api/users', usersRateLimiter, optionalAuth, usersRoutes);
281
+ app.use('/api/ollama', ollamaRateLimiter, ollamaRoutes);
282
+ app.use('/api/chat', chatRateLimiter, optionalAuth, chatRoutes);
283
+ app.use('/api/preferences', preferencesRateLimiter, optionalAuth, preferencesRoutes);
284
+ app.use('/api/plugins', pluginRoutes);
285
+ app.use('/api/documents', documentsRateLimiter, documentRoutes);
286
+ app.use('/api/personas', personasRateLimiter, optionalAuth, personaRoutes);
287
+ app.use('/api/tts', ttsRateLimiter, optionalAuth, ttsRoutes);
288
+ // Serve frontend static files in production (for npx libre-webui)
289
+ if (process.env.NODE_ENV === 'production' ||
290
+ process.env.SERVE_FRONTEND === 'true') {
291
+ const pathModule = await import('path');
292
+ const urlModule = await import('url');
293
+ const fsModule = await import('fs');
294
+ const __filename = urlModule.fileURLToPath(import.meta.url);
295
+ const __dirname = pathModule.dirname(__filename);
296
+ // Try multiple possible frontend locations
297
+ const possiblePaths = [
298
+ pathModule.join(__dirname, '../../frontend/dist'), // npm package structure
299
+ pathModule.join(__dirname, '../../../frontend/dist'), // development
300
+ pathModule.join(process.cwd(), 'frontend/dist'), // running from project root
301
+ ];
302
+ let frontendPath = '';
303
+ for (const p of possiblePaths) {
304
+ if (fsModule.existsSync(pathModule.join(p, 'index.html'))) {
305
+ frontendPath = p;
306
+ break;
307
+ }
308
+ }
309
+ if (frontendPath) {
310
+ console.log(`Serving frontend from: ${frontendPath}`);
311
+ // Rate limiter for static files
312
+ const staticRateLimiter = rateLimit({
313
+ windowMs: 15 * 60 * 1000, // 15 minutes
314
+ max: 1000, // limit each IP to 1000 requests per windowMs
315
+ message: 'Too many requests, please try again later.',
316
+ standardHeaders: true,
317
+ legacyHeaders: false,
318
+ });
319
+ app.use(staticRateLimiter, express.static(frontendPath));
320
+ // SPA fallback - serve index.html for all non-API routes
321
+ const indexPath = pathModule.join(frontendPath, 'index.html');
322
+ // Root route
323
+ app.get('/', staticRateLimiter, (_req, res) => {
324
+ res.sendFile(indexPath);
325
+ });
326
+ // All other non-API routes (Express 5 wildcard syntax)
327
+ app.get('/{*splat}', staticRateLimiter, (req, res, next) => {
328
+ if (req.path.startsWith('/api/') || req.path.startsWith('/ws')) {
329
+ return next();
330
+ }
331
+ res.sendFile(indexPath);
332
+ });
333
+ }
334
+ else {
335
+ console.warn('Frontend build not found. Run `npm run build:frontend` first.');
336
+ }
337
+ }
338
+ // Error handling
339
+ app.use(notFoundHandler);
340
+ app.use(errorHandler);
341
+ // Create HTTP server
342
+ const server = createServer(app);
343
+ // WebSocket server for real-time chat streaming
344
+ const wss = new WebSocketServer({
345
+ server,
346
+ path: '/ws',
347
+ });
348
+ wss.on('connection', (ws, req) => {
349
+ console.log('WebSocket client connected');
350
+ // Extract and verify auth token from query parameters
351
+ let userId = 'default';
352
+ try {
353
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
354
+ const token = url.searchParams.get('token');
355
+ if (token) {
356
+ // Verify JWT token using the same logic as the auth middleware
357
+ const decoded = verifyToken(token);
358
+ userId = decoded.userId;
359
+ console.log('WebSocket authenticated for user:', userId);
360
+ }
361
+ else {
362
+ console.log('WebSocket connection without auth token, using default user');
363
+ }
364
+ }
365
+ catch (error) {
366
+ console.error('WebSocket auth error:', error);
367
+ // Continue with default user for backward compatibility
368
+ }
369
+ ws.on('message', async (data) => {
370
+ try {
371
+ const message = JSON.parse(data.toString());
372
+ if (message.type === 'chat_stream') {
373
+ const { sessionId, content, images, format, options, assistantMessageId, regenerate, originalMessageId, } = message.data;
374
+ console.log('Backend: Received chat_stream for session:', sessionId, 'with images:', !!images, 'format:', !!format, 'regenerate:', !!regenerate, 'originalMessageId:', originalMessageId);
375
+ // Get session with user authentication
376
+ const session = chatService.getSession(sessionId, userId);
377
+ if (!session) {
378
+ console.log('Backend: Session not found:', sessionId, 'for user:', userId);
379
+ ws.send(JSON.stringify({
380
+ type: 'error',
381
+ data: {
382
+ error: 'Session not found',
383
+ code: 'SESSION_NOT_FOUND',
384
+ message: 'The requested session does not exist or does not belong to the current user. Please create a new session.',
385
+ sessionId: sessionId,
386
+ userId: userId,
387
+ },
388
+ }));
389
+ return;
390
+ }
391
+ // Add user message with images if provided (skip for regenerations)
392
+ let userMessage;
393
+ if (!regenerate) {
394
+ userMessage = chatService.addMessage(sessionId, {
395
+ role: 'user',
396
+ content,
397
+ images: images || undefined,
398
+ }, userId);
399
+ if (!userMessage) {
400
+ ws.send(JSON.stringify({
401
+ type: 'error',
402
+ data: { error: 'Failed to add user message' },
403
+ }));
404
+ return;
405
+ }
406
+ // Send user message confirmation
407
+ ws.send(JSON.stringify({
408
+ type: 'user_message',
409
+ data: userMessage,
410
+ }));
411
+ }
412
+ // RAG: Get relevant document context for the user's query
413
+ const relevantContext = await documentService.getRelevantContext(content, sessionId);
414
+ let enhancedContent = content;
415
+ if (relevantContext.length > 0) {
416
+ console.log(`Found ${relevantContext.length} relevant document chunks for query`);
417
+ // Inject document context into the user message
418
+ const contextString = relevantContext.join('\n\n---\n\n');
419
+ enhancedContent = `Context from uploaded documents:\n\n${contextString}\n\n---\n\nUser question: ${content}`;
420
+ // Update the user message with enhanced content that includes document context
421
+ // We'll create a new message with the enhanced content for the AI model
422
+ console.log('Enhanced user message with document context');
423
+ }
424
+ // Use the modern chat completion API instead of legacy generate API
425
+ // This supports multimodal input and structured outputs
426
+ const contextMessages = chatService.getMessagesForContext(sessionId);
427
+ // Convert our messages to Ollama format
428
+ const ollamaMessages = contextMessages.map((msg, index) => {
429
+ const ollamaMessage = {
430
+ role: msg.role,
431
+ content: msg.content,
432
+ };
433
+ // Debug: Log what we're sending to Ollama
434
+ if (msg.role === 'system') {
435
+ console.log(`🚀 [DEBUG] Sending to Ollama - System message: "${msg.content.substring(0, 150)}${msg.content.length > 150 ? '...' : ''}"`);
436
+ }
437
+ // Use enhanced content for the last user message if we have document context
438
+ if (msg.role === 'user' &&
439
+ index === contextMessages.length - 1 &&
440
+ relevantContext.length > 0) {
441
+ ollamaMessage.content = enhancedContent;
442
+ }
443
+ // Process images: strip data URL prefix if present
444
+ if (msg.images && msg.images.length > 0) {
445
+ ollamaMessage.images = msg.images.map(img => {
446
+ // Strip data URL prefix if present (e.g., "data:image/png;base64,")
447
+ if (typeof img === 'string' && img.includes(',')) {
448
+ const base64Index = img.indexOf(',');
449
+ if (base64Index !== -1) {
450
+ return img.substring(base64Index + 1);
451
+ }
452
+ }
453
+ return img;
454
+ });
455
+ }
456
+ return ollamaMessage;
457
+ });
458
+ let assistantContent = '';
459
+ let finalStatistics = undefined;
460
+ console.log('Backend: Using assistantMessageId:', assistantMessageId);
461
+ // Resolve the actual model name (handles persona IDs)
462
+ let actualModelName = session.model;
463
+ if (session.model.startsWith('persona:')) {
464
+ try {
465
+ const personaId = session.model.replace('persona:', '');
466
+ console.log(`[WebSocket] DEBUG: Resolving persona ${personaId} for user ${userId}`);
467
+ const { personaService } = await import('./services/personaService.js');
468
+ // Try to get persona for the current user first, then fallback to 'default'
469
+ let persona = await personaService.getPersonaById(personaId, userId);
470
+ if (!persona && userId !== 'default') {
471
+ console.log(`[WebSocket] DEBUG: Persona not found for user ${userId}, trying default user`);
472
+ persona = await personaService.getPersonaById(personaId, 'default');
473
+ }
474
+ console.log(`[WebSocket] DEBUG: Persona lookup result:`, persona
475
+ ? `Found persona with model: ${persona.model}`
476
+ : 'Persona not found');
477
+ if (persona && persona.model) {
478
+ actualModelName = persona.model;
479
+ console.log(`[WebSocket] Resolved persona ${personaId} to model: ${actualModelName}`);
480
+ }
481
+ else {
482
+ console.warn(`[WebSocket] Persona ${personaId} not found, using original model: ${session.model}`);
483
+ }
484
+ }
485
+ catch (error) {
486
+ console.error(`[WebSocket] Error resolving persona model:`, error);
487
+ }
488
+ }
489
+ // Check if there's an active plugin for this model
490
+ console.log(`[WebSocket] Looking for plugin for model: ${actualModelName}`);
491
+ const activePlugin = pluginService.getActivePluginForModel(actualModelName);
492
+ console.log(`[WebSocket] Found plugin:`, activePlugin ? activePlugin.id : 'none');
493
+ if (activePlugin) {
494
+ console.log(`[WebSocket] Using plugin ${activePlugin.id} for model ${actualModelName}`);
495
+ try {
496
+ // Get user's preferred generation options
497
+ const userGenerationOptions = preferencesService.getGenerationOptions();
498
+ // Merge user preferences with request options
499
+ const mergedOptions = mergeGenerationOptions(userGenerationOptions, options);
500
+ // Get messages for context
501
+ const contextMessages = chatService.getMessagesForContext(sessionId);
502
+ // Use plugin for generation (non-streaming for now)
503
+ // For regenerations, the user message is already in context; for new messages, we need to add it
504
+ const messagesForPlugin = regenerate
505
+ ? contextMessages
506
+ : contextMessages.concat([userMessage]);
507
+ const pluginResponse = await pluginService.executePluginRequest(actualModelName, messagesForPlugin, mergedOptions);
508
+ // Get the content from plugin response
509
+ assistantContent =
510
+ pluginResponse.choices[0]?.message?.content || '';
511
+ // Send the complete response as chunks to simulate streaming
512
+ const words = assistantContent.split(' ');
513
+ const BATCH_SIZE = 3; // Send 3 words at a time to reduce message frequency
514
+ for (let i = 0; i < words.length; i += BATCH_SIZE) {
515
+ const batch = words.slice(i, i + BATCH_SIZE);
516
+ const chunk = words.slice(0, i + batch.length).join(' ');
517
+ const isLast = i + BATCH_SIZE >= words.length;
518
+ ws.send(JSON.stringify({
519
+ type: 'assistant_chunk',
520
+ data: {
521
+ content: batch.join(' ') + (isLast ? '' : ' '),
522
+ total: chunk,
523
+ done: isLast,
524
+ messageId: assistantMessageId,
525
+ },
526
+ }));
527
+ // Small delay to simulate streaming but with better batching
528
+ if (!isLast) {
529
+ await new Promise(resolve => setTimeout(resolve, 100));
530
+ }
531
+ }
532
+ // Save the complete assistant message
533
+ if (assistantContent && assistantMessageId) {
534
+ console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
535
+ // Calculate branching fields if this is a regeneration
536
+ let branchingFields = {};
537
+ if (regenerate && originalMessageId) {
538
+ // Find the original message to get its parentId or use its ID as parent
539
+ const originalMsg = session.messages.find(m => m.id === originalMessageId);
540
+ const parentId = originalMsg?.parentId || originalMessageId;
541
+ // Count existing siblings to determine branch index
542
+ const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
543
+ branchingFields = {
544
+ parentId,
545
+ branchIndex: siblingCount, // New branch gets next index
546
+ isActive: true,
547
+ };
548
+ console.log('Backend: Setting branching fields:', branchingFields);
549
+ }
550
+ const assistantMessage = chatService.addMessage(sessionId, {
551
+ role: 'assistant',
552
+ content: assistantContent,
553
+ model: session.model,
554
+ id: assistantMessageId,
555
+ ...branchingFields,
556
+ }, userId);
557
+ console.log('Backend: Assistant message saved:', !!assistantMessage);
558
+ // Send completion signal
559
+ ws.send(JSON.stringify({
560
+ type: 'assistant_complete',
561
+ data: assistantMessage,
562
+ }));
563
+ }
564
+ return; // Exit early since we handled the request via plugin
565
+ }
566
+ catch (pluginError) {
567
+ console.error('Plugin failed, falling back to Ollama:', pluginError);
568
+ // Continue to Ollama fallback below
569
+ }
570
+ }
571
+ console.log(`[WebSocket] No plugin found or plugin failed, using Ollama for model: ${actualModelName}`);
572
+ // Reuse the actualModelName variable that was already resolved above
573
+ // If we're here, it means either there was no plugin or plugin failed
574
+ // The actualModelName was already resolved in the earlier code block
575
+ // Get user's preferred generation options
576
+ const userGenerationOptions = preferencesService.getGenerationOptions();
577
+ // Merge user preferences with request options
578
+ const mergedOptions = mergeGenerationOptions(userGenerationOptions, options);
579
+ // Create chat request with advanced features
580
+ const chatRequest = {
581
+ model: actualModelName,
582
+ messages: ollamaMessages,
583
+ stream: true,
584
+ options: mergedOptions,
585
+ };
586
+ // Add structured output format if specified
587
+ if (format) {
588
+ chatRequest.format = format;
589
+ }
590
+ // Stream response from Ollama using chat completion
591
+ await ollamaService.generateChatStreamResponse(chatRequest, chunk => {
592
+ if (chunk.message?.content) {
593
+ assistantContent += chunk.message.content;
594
+ // Send streaming chunk with the provided message ID
595
+ ws.send(JSON.stringify({
596
+ type: 'assistant_chunk',
597
+ data: {
598
+ content: chunk.message.content,
599
+ total: assistantContent,
600
+ done: chunk.done,
601
+ messageId: assistantMessageId,
602
+ },
603
+ }));
604
+ }
605
+ // Capture final statistics when streaming is done
606
+ if (chunk.done) {
607
+ finalStatistics = {
608
+ total_duration: chunk.total_duration,
609
+ load_duration: chunk.load_duration,
610
+ prompt_eval_count: chunk.prompt_eval_count,
611
+ prompt_eval_duration: chunk.prompt_eval_duration,
612
+ eval_count: chunk.eval_count,
613
+ eval_duration: chunk.eval_duration,
614
+ created_at: chunk.created_at,
615
+ model: chunk.model,
616
+ };
617
+ // Calculate tokens per second if we have the necessary data
618
+ if (chunk.eval_count && chunk.eval_duration) {
619
+ finalStatistics.tokens_per_second =
620
+ Math.round((chunk.eval_count / (chunk.eval_duration / 1e9)) * 100) / 100;
621
+ }
622
+ }
623
+ }, error => {
624
+ ws.send(JSON.stringify({
625
+ type: 'error',
626
+ data: { error: error.message },
627
+ }));
628
+ }, () => {
629
+ // Save the complete assistant message with the provided ID
630
+ if (assistantContent && assistantMessageId) {
631
+ console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
632
+ // Calculate branching fields if this is a regeneration
633
+ let branchingFields = {};
634
+ if (regenerate && originalMessageId) {
635
+ // Find the original message to get its parentId or use its ID as parent
636
+ const originalMsg = session.messages.find(m => m.id === originalMessageId);
637
+ const parentId = originalMsg?.parentId || originalMessageId;
638
+ // Count existing siblings to determine branch index
639
+ const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
640
+ branchingFields = {
641
+ parentId,
642
+ branchIndex: siblingCount, // New branch gets next index
643
+ isActive: true,
644
+ };
645
+ console.log('Backend: Setting branching fields:', branchingFields);
646
+ }
647
+ console.log('Backend: About to save assistant message:', {
648
+ sessionId,
649
+ messageId: assistantMessageId,
650
+ contentLength: assistantContent.length,
651
+ hasBranchingFields: Object.keys(branchingFields).length > 0,
652
+ branchingFields,
653
+ });
654
+ const assistantMessage = chatService.addMessage(sessionId, {
655
+ role: 'assistant',
656
+ content: assistantContent,
657
+ model: session.model,
658
+ id: assistantMessageId,
659
+ statistics: finalStatistics,
660
+ ...branchingFields,
661
+ }, userId);
662
+ console.log('Backend: Assistant message saved:', !!assistantMessage, assistantMessage
663
+ ? {
664
+ id: assistantMessage.id,
665
+ contentLength: assistantMessage.content.length,
666
+ }
667
+ : 'FAILED TO SAVE');
668
+ // Send completion signal with statistics
669
+ ws.send(JSON.stringify({
670
+ type: 'assistant_complete',
671
+ data: {
672
+ content: assistantContent,
673
+ role: 'assistant',
674
+ timestamp: Date.now(),
675
+ messageId: assistantMessageId,
676
+ statistics: finalStatistics,
677
+ ...branchingFields,
678
+ },
679
+ }));
680
+ }
681
+ });
682
+ }
683
+ }
684
+ catch (error) {
685
+ console.error('WebSocket error:', error);
686
+ const errorMessage = error instanceof Error ? error.message : String(error);
687
+ ws.send(JSON.stringify({
688
+ type: 'error',
689
+ data: { error: errorMessage },
690
+ }));
691
+ }
692
+ });
693
+ ws.on('close', () => {
694
+ console.log('WebSocket client disconnected');
695
+ });
696
+ ws.on('error', error => {
697
+ console.error('WebSocket error:', error);
698
+ });
699
+ // Send initial connection confirmation
700
+ ws.send(JSON.stringify({
701
+ type: 'connected',
702
+ data: { message: 'Connected to Libre WebUI' },
703
+ }));
704
+ });
705
+ // Start server
706
+ server.listen({ port, host: '0.0.0.0' }, () => {
707
+ console.log(`🚀 Libre WebUI Backend running on port ${port}`);
708
+ console.log(`📡 WebSocket server running on ws://localhost:${port}/ws`);
709
+ console.log(`🌐 CORS enabled for: ${corsOrigins.join(', ')}`);
710
+ // Check OAuth providers configuration on startup
711
+ const githubOAuth = new GitHubOAuthService();
712
+ const hfOAuth = new HuggingFaceOAuthService();
713
+ const githubConfigured = githubOAuth.isConfigured();
714
+ const hfConfigured = hfOAuth.isConfigured();
715
+ if (githubConfigured || hfConfigured) {
716
+ console.log('🔐 SSO Configuration:');
717
+ if (githubConfigured) {
718
+ console.log(' ✅ GitHub OAuth configured and ready');
719
+ }
720
+ if (hfConfigured) {
721
+ console.log(' ✅ Hugging Face OAuth configured and ready');
722
+ }
723
+ }
724
+ else {
725
+ console.log('â„šī¸ No SSO providers configured (optional)');
726
+ }
727
+ // Check Ollama connection on startup
728
+ ollamaService.isHealthy().then(isHealthy => {
729
+ if (isHealthy) {
730
+ console.log('✅ Ollama service is connected and ready');
731
+ }
732
+ else {
733
+ console.log("âš ī¸ Ollama service is not available - make sure it's running on http://localhost:11434");
734
+ }
735
+ });
736
+ });
737
+ // Graceful shutdown
738
+ process.on('SIGTERM', () => {
739
+ console.log('SIGTERM signal received: closing HTTP server');
740
+ server.close(() => {
741
+ console.log('HTTP server closed');
742
+ });
743
+ });
744
+ process.on('SIGINT', () => {
745
+ console.log('SIGINT signal received: closing HTTP server');
746
+ server.close(() => {
747
+ console.log('HTTP server closed');
748
+ });
749
+ });
750
+ export default app;
751
+ //# sourceMappingURL=index.js.map