libre-webui 0.22.1 → 0.23.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 (236) hide show
  1. package/backend/dist/index.d.ts.map +1 -1
  2. package/backend/dist/index.js +8 -0
  3. package/backend/dist/index.js.map +1 -1
  4. package/backend/dist/middleware/auth.d.ts +15 -1
  5. package/backend/dist/middleware/auth.d.ts.map +1 -1
  6. package/backend/dist/middleware/auth.js +153 -33
  7. package/backend/dist/middleware/auth.js.map +1 -1
  8. package/backend/dist/persistence/index.d.ts +1 -0
  9. package/backend/dist/persistence/index.d.ts.map +1 -1
  10. package/backend/dist/persistence/index.js.map +1 -1
  11. package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
  12. package/backend/dist/persistence/postgresMigrationRegistry.js +2 -0
  13. package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
  14. package/backend/dist/persistence/postgresPersistence.d.ts.map +1 -1
  15. package/backend/dist/persistence/postgresPersistence.js +4 -0
  16. package/backend/dist/persistence/postgresPersistence.js.map +1 -1
  17. package/backend/dist/persistence/postgresSecurityRepositories.d.ts +7 -0
  18. package/backend/dist/persistence/postgresSecurityRepositories.d.ts.map +1 -0
  19. package/backend/dist/persistence/postgresSecurityRepositories.js +435 -0
  20. package/backend/dist/persistence/postgresSecurityRepositories.js.map +1 -0
  21. package/backend/dist/persistence/postgresTrustFoundationMigration.d.ts +8 -0
  22. package/backend/dist/persistence/postgresTrustFoundationMigration.d.ts.map +1 -0
  23. package/backend/dist/persistence/postgresTrustFoundationMigration.js +151 -0
  24. package/backend/dist/persistence/postgresTrustFoundationMigration.js.map +1 -0
  25. package/backend/dist/persistence/securityTypes.d.ts +196 -0
  26. package/backend/dist/persistence/securityTypes.d.ts.map +1 -0
  27. package/backend/dist/persistence/securityTypes.js +18 -0
  28. package/backend/dist/persistence/securityTypes.js.map +1 -0
  29. package/backend/dist/persistence/sqliteMigrations.d.ts +1 -0
  30. package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
  31. package/backend/dist/persistence/sqliteMigrations.js +311 -0
  32. package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
  33. package/backend/dist/persistence/sqlitePersistence.d.ts.map +1 -1
  34. package/backend/dist/persistence/sqlitePersistence.js +3 -0
  35. package/backend/dist/persistence/sqlitePersistence.js.map +1 -1
  36. package/backend/dist/persistence/sqliteSecurityRepositories.d.ts +6 -0
  37. package/backend/dist/persistence/sqliteSecurityRepositories.d.ts.map +1 -0
  38. package/backend/dist/persistence/sqliteSecurityRepositories.js +497 -0
  39. package/backend/dist/persistence/sqliteSecurityRepositories.js.map +1 -0
  40. package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
  41. package/backend/dist/persistence/sqliteToPostgresMigration.js +55 -0
  42. package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
  43. package/backend/dist/persistence/types.d.ts +4 -0
  44. package/backend/dist/persistence/types.d.ts.map +1 -1
  45. package/backend/dist/platform/jobs/domainJobHandlers.d.ts.map +1 -1
  46. package/backend/dist/platform/jobs/domainJobHandlers.js +7 -0
  47. package/backend/dist/platform/jobs/domainJobHandlers.js.map +1 -1
  48. package/backend/dist/routes/access.d.ts +3 -0
  49. package/backend/dist/routes/access.d.ts.map +1 -0
  50. package/backend/dist/routes/access.js +134 -0
  51. package/backend/dist/routes/access.js.map +1 -0
  52. package/backend/dist/routes/audit.d.ts +3 -0
  53. package/backend/dist/routes/audit.d.ts.map +1 -0
  54. package/backend/dist/routes/audit.js +82 -0
  55. package/backend/dist/routes/audit.js.map +1 -0
  56. package/backend/dist/routes/auth.d.ts.map +1 -1
  57. package/backend/dist/routes/auth.js +322 -11
  58. package/backend/dist/routes/auth.js.map +1 -1
  59. package/backend/dist/routes/groups.d.ts +3 -0
  60. package/backend/dist/routes/groups.d.ts.map +1 -0
  61. package/backend/dist/routes/groups.js +161 -0
  62. package/backend/dist/routes/groups.js.map +1 -0
  63. package/backend/dist/services/apiTokenService.d.ts +51 -0
  64. package/backend/dist/services/apiTokenService.d.ts.map +1 -0
  65. package/backend/dist/services/apiTokenService.js +220 -0
  66. package/backend/dist/services/apiTokenService.js.map +1 -0
  67. package/backend/dist/services/authService.d.ts +20 -4
  68. package/backend/dist/services/authService.d.ts.map +1 -1
  69. package/backend/dist/services/authService.js +44 -6
  70. package/backend/dist/services/authService.js.map +1 -1
  71. package/backend/dist/services/authSessionService.d.ts +34 -0
  72. package/backend/dist/services/authSessionService.d.ts.map +1 -0
  73. package/backend/dist/services/authSessionService.js +205 -0
  74. package/backend/dist/services/authSessionService.js.map +1 -0
  75. package/backend/dist/services/authorizationService.d.ts +64 -0
  76. package/backend/dist/services/authorizationService.d.ts.map +1 -0
  77. package/backend/dist/services/authorizationService.js +176 -0
  78. package/backend/dist/services/authorizationService.js.map +1 -0
  79. package/backend/dist/services/groupService.d.ts +27 -0
  80. package/backend/dist/services/groupService.d.ts.map +1 -0
  81. package/backend/dist/services/groupService.js +245 -0
  82. package/backend/dist/services/groupService.js.map +1 -0
  83. package/backend/dist/services/groupVectorPrincipalResolver.d.ts +12 -0
  84. package/backend/dist/services/groupVectorPrincipalResolver.d.ts.map +1 -0
  85. package/backend/dist/services/groupVectorPrincipalResolver.js +33 -0
  86. package/backend/dist/services/groupVectorPrincipalResolver.js.map +1 -0
  87. package/backend/dist/services/modelAccessService.d.ts +1 -0
  88. package/backend/dist/services/modelAccessService.d.ts.map +1 -1
  89. package/backend/dist/services/modelAccessService.js +3 -5
  90. package/backend/dist/services/modelAccessService.js.map +1 -1
  91. package/backend/dist/services/oauthSecurity.d.ts +12 -1
  92. package/backend/dist/services/oauthSecurity.d.ts.map +1 -1
  93. package/backend/dist/services/oauthSecurity.js +41 -0
  94. package/backend/dist/services/oauthSecurity.js.map +1 -1
  95. package/backend/dist/services/resourceGrantService.d.ts +28 -0
  96. package/backend/dist/services/resourceGrantService.d.ts.map +1 -0
  97. package/backend/dist/services/resourceGrantService.js +192 -0
  98. package/backend/dist/services/resourceGrantService.js.map +1 -0
  99. package/backend/dist/services/securityAuditService.d.ts +35 -0
  100. package/backend/dist/services/securityAuditService.d.ts.map +1 -0
  101. package/backend/dist/services/securityAuditService.js +159 -0
  102. package/backend/dist/services/securityAuditService.js.map +1 -0
  103. package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -1
  104. package/backend/dist/services/simpleGitHubOAuth.js +4 -2
  105. package/backend/dist/services/simpleGitHubOAuth.js.map +1 -1
  106. package/backend/dist/services/simpleOidcOAuth.d.ts +62 -0
  107. package/backend/dist/services/simpleOidcOAuth.d.ts.map +1 -0
  108. package/backend/dist/services/simpleOidcOAuth.js +397 -0
  109. package/backend/dist/services/simpleOidcOAuth.js.map +1 -0
  110. package/backend/dist/services/webSearchService.d.ts +1 -0
  111. package/backend/dist/services/webSearchService.d.ts.map +1 -1
  112. package/backend/dist/services/webSearchService.js +3 -5
  113. package/backend/dist/services/webSearchService.js.map +1 -1
  114. package/backend/dist/services/websocketTicketService.d.ts +3 -1
  115. package/backend/dist/services/websocketTicketService.d.ts.map +1 -1
  116. package/backend/dist/services/websocketTicketService.js +4 -1
  117. package/backend/dist/services/websocketTicketService.js.map +1 -1
  118. package/backend/dist/services/workAccessService.d.ts +1 -0
  119. package/backend/dist/services/workAccessService.d.ts.map +1 -1
  120. package/backend/dist/services/workAccessService.js +3 -5
  121. package/backend/dist/services/workAccessService.js.map +1 -1
  122. package/backend/dist/services/workEventService.d.ts.map +1 -1
  123. package/backend/dist/services/workEventService.js +74 -18
  124. package/backend/dist/services/workEventService.js.map +1 -1
  125. package/backend/dist/services/workRuntimeService.d.ts.map +1 -1
  126. package/backend/dist/services/workRuntimeService.js +19 -1
  127. package/backend/dist/services/workRuntimeService.js.map +1 -1
  128. package/backend/dist/websocketServer.d.ts.map +1 -1
  129. package/backend/dist/websocketServer.js +25 -1
  130. package/backend/dist/websocketServer.js.map +1 -1
  131. package/backend/dist/workTerminalServer.d.ts.map +1 -1
  132. package/backend/dist/workTerminalServer.js +19 -1
  133. package/backend/dist/workTerminalServer.js.map +1 -1
  134. package/backend/dist/worker.js +2 -0
  135. package/backend/dist/worker.js.map +1 -1
  136. package/backend/package.json +1 -1
  137. package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
  138. package/frontend/dist/assets/index-CudKJ5Bs.js +27 -0
  139. package/frontend/dist/assets/index-DBaj1DRj.css +1 -0
  140. package/frontend/dist/index.html +11 -11
  141. package/frontend/dist/js/ArtifactContainer-CigyGLnW.js +1 -0
  142. package/frontend/dist/js/{ArtifactDemoPage-BDBXYv9l.js → ArtifactDemoPage-CITpRQXa.js} +1 -1
  143. package/frontend/dist/js/{ArtifactSandboxFrame-BN16NtKe.js → ArtifactSandboxFrame-C8_7sKm1.js} +2 -2
  144. package/frontend/dist/js/ArtifactSlideOutPanel-D1f9zUZN.js +1 -0
  145. package/frontend/dist/js/ChatPage-D-Q1SGlI.js +23 -0
  146. package/frontend/dist/js/FirstTimeSetup-DwvqU8Jk.js +1 -0
  147. package/frontend/dist/js/{GalleryPage-CV_vbE7X.js → GalleryPage-CFU6YW9r.js} +1 -1
  148. package/frontend/dist/js/{HomePage-P-e7Huco.js → HomePage-CEoZABYG.js} +1 -1
  149. package/frontend/dist/js/{LibreClawPage-Cee06SUM.js → LibreClawPage-VV4sD4-v.js} +1 -1
  150. package/frontend/dist/js/{ModelManagerModals-D7jIT3v1.js → ModelManagerModals-CZAWUxjI.js} +1 -1
  151. package/frontend/dist/js/{ModelSelector-DXucbvrz.js → ModelSelector-DVN9nTCB.js} +1 -1
  152. package/frontend/dist/js/ModelsPage-Ce41crA_.js +2 -0
  153. package/frontend/dist/js/{NotesPage-rEIPWGIb.js → NotesPage-BV08Gh2X.js} +1 -1
  154. package/frontend/dist/js/{PersonasPage-BXtepFuD.js → PersonasPage-CSpK5FvR.js} +2 -2
  155. package/frontend/dist/js/{PluginUsagePage-hhu8cxsu.js → PluginUsagePage-B9YPhusn.js} +1 -1
  156. package/frontend/dist/js/{RichMessageContent-DkiEvdSg.js → RichMessageContent-C3qddYMx.js} +1 -1
  157. package/frontend/dist/js/SettingsModal-CQSumlFr.js +1 -0
  158. package/frontend/dist/js/SystemPage-7KbZlMb_.js +1 -0
  159. package/frontend/dist/js/UserManagementPage-DnyW0Wmx.js +1 -0
  160. package/frontend/dist/js/WorkPage-B3cuSSwH.js +6 -0
  161. package/frontend/dist/js/api-DSzsasYT.js +14 -0
  162. package/frontend/dist/js/{appStore-DRSSjaHn.js → appStore-CYhqdjge.js} +2 -2
  163. package/frontend/dist/js/ar-D0z0P4rC.js +2 -0
  164. package/frontend/dist/js/{authStore-BVykIRec.js → authStore-CnSn5zM7.js} +3 -3
  165. package/frontend/dist/js/bn-BYYU41A7.js +2 -0
  166. package/frontend/dist/js/chatStore-P1aGCgiF.js +3 -0
  167. package/frontend/dist/js/{config-BTlPm-cZ.js → config-EEx_rELa.js} +1 -1
  168. package/frontend/dist/js/cs-BFx6KNZe.js +2 -0
  169. package/frontend/dist/js/da-DSDuY5Du.js +2 -0
  170. package/frontend/dist/js/de-YqhP0FmX.js +2 -0
  171. package/frontend/dist/js/es-Dpr_y1cN.js +2 -0
  172. package/frontend/dist/js/{estree-CfYahF_d.js → estree-Dc6SMAYy.js} +1 -1
  173. package/frontend/dist/js/fr-nVg_iAEC.js +2 -0
  174. package/frontend/dist/js/hi-gHKYCgkP.js +2 -0
  175. package/frontend/dist/js/{html-BJsDX1UZ.js → html-Bax5_Bzw.js} +1 -1
  176. package/frontend/dist/js/id-C_yID1Zk.js +2 -0
  177. package/frontend/dist/js/is-MoRPLbOM.js +2 -0
  178. package/frontend/dist/js/it-BZTscLxR.js +2 -0
  179. package/frontend/dist/js/ja-DpIbtmZq.js +2 -0
  180. package/frontend/dist/js/ko-LkWggmjU.js +2 -0
  181. package/frontend/dist/js/{markdown-GLyuUNig.js → markdown-DxaneQad.js} +1 -1
  182. package/frontend/dist/js/{messageCodeStyles-BHddEFP1.js → messageCodeStyles-CxXrl4jD.js} +2 -2
  183. package/frontend/dist/js/ms-p3XiQSAo.js +2 -0
  184. package/frontend/dist/js/nl-sEHHO2Kb.js +2 -0
  185. package/frontend/dist/js/pl-D2GD6PDW.js +2 -0
  186. package/frontend/dist/js/pt-DPkn3GUn.js +2 -0
  187. package/frontend/dist/js/ru-C42iHrEd.js +2 -0
  188. package/frontend/dist/js/{standalone-Dh56V0YW.js → standalone-CCpjftAD.js} +1 -1
  189. package/frontend/dist/js/sv-CGvHo4os.js +2 -0
  190. package/frontend/dist/js/th-BaDNpCdY.js +2 -0
  191. package/frontend/dist/js/tr-BHQvi1cN.js +2 -0
  192. package/frontend/dist/js/{typescript-DRy7A9w4.js → typescript-DtQEF6kq.js} +1 -1
  193. package/frontend/dist/js/{ui-CuY4IifA.js → ui-CUMAcARd.js} +2 -2
  194. package/frontend/dist/js/{ui-vendor-D3Ahl1x5.js → ui-vendor-54SgntkX.js} +1 -1
  195. package/frontend/dist/js/uk-7pNGS1b2.js +2 -0
  196. package/frontend/dist/js/vi-DgAfh6mo.js +2 -0
  197. package/frontend/dist/js/{workApi-DHuYNHLf.js → workApi-vRuNfpEf.js} +2 -2
  198. package/frontend/dist/js/zh-FRTz6ZqA.js +2 -0
  199. package/package.json +2 -2
  200. package/frontend/dist/assets/index-Jtx9Amhz.js +0 -16
  201. package/frontend/dist/assets/index-kxtZQLV6.css +0 -1
  202. package/frontend/dist/js/ArtifactContainer-fZegtmva.js +0 -1
  203. package/frontend/dist/js/ArtifactSlideOutPanel-x1fS85Ww.js +0 -1
  204. package/frontend/dist/js/ChatPage-C_pXh7WN.js +0 -23
  205. package/frontend/dist/js/FirstTimeSetup-oRrlZnBm.js +0 -1
  206. package/frontend/dist/js/ModelsPage-C3_fZpMM.js +0 -2
  207. package/frontend/dist/js/SettingsModal-BNxCtJ51.js +0 -1
  208. package/frontend/dist/js/SystemPage-BvvKFuoG.js +0 -1
  209. package/frontend/dist/js/UserManagementPage-BccyR7si.js +0 -1
  210. package/frontend/dist/js/WorkPage-BgxrvY3I.js +0 -6
  211. package/frontend/dist/js/api-BvPDxsUQ.js +0 -14
  212. package/frontend/dist/js/ar-xTSvTq05.js +0 -2
  213. package/frontend/dist/js/bn-iYAsohEA.js +0 -2
  214. package/frontend/dist/js/chatStore-UPp5TV_t.js +0 -3
  215. package/frontend/dist/js/cs-YBHN88Hb.js +0 -2
  216. package/frontend/dist/js/da-D7qlLy_k.js +0 -2
  217. package/frontend/dist/js/de-DV671k2c.js +0 -2
  218. package/frontend/dist/js/es-kyBgzNq-.js +0 -2
  219. package/frontend/dist/js/fr-Cw1vut7I.js +0 -2
  220. package/frontend/dist/js/hi-DWEFC024.js +0 -2
  221. package/frontend/dist/js/id-DlMHjjy0.js +0 -2
  222. package/frontend/dist/js/is-B9E_a7dG.js +0 -2
  223. package/frontend/dist/js/it-DawmVbLC.js +0 -2
  224. package/frontend/dist/js/ja-DmVvwY3W.js +0 -2
  225. package/frontend/dist/js/ko-DzOPpkVs.js +0 -2
  226. package/frontend/dist/js/ms-DpsaOPht.js +0 -2
  227. package/frontend/dist/js/nl-B1d8W22n.js +0 -2
  228. package/frontend/dist/js/pl-nCncRVgt.js +0 -2
  229. package/frontend/dist/js/pt-DHw8fGuu.js +0 -2
  230. package/frontend/dist/js/ru-Derl_YP_.js +0 -2
  231. package/frontend/dist/js/sv-pJAfBVo0.js +0 -2
  232. package/frontend/dist/js/th-C_7fIGnk.js +0 -2
  233. package/frontend/dist/js/tr-CPqsJvaP.js +0 -2
  234. package/frontend/dist/js/uk-3o8P_kFi.js +0 -2
  235. package/frontend/dist/js/vi-Dv6N7OVD.js +0 -2
  236. package/frontend/dist/js/zh-qg5iqtPi.js +0 -2
@@ -0,0 +1,134 @@
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
+ /**
18
+ * Resource-grant management (IAM-03). Owners (and grant-admins) manage
19
+ * shares on their own resources; a user can always list what is shared
20
+ * with them and leave a share.
21
+ */
22
+ import express from 'express';
23
+ import rateLimit from '../middleware/sharedRateLimit.js';
24
+ import { authenticate } from '../middleware/auth.js';
25
+ import { createGrant, deleteGrant, listGrantsForActor, listGrantsForResource, ResourceGrantError, } from '../services/resourceGrantService.js';
26
+ import { createLogger } from '../utils/logger.js';
27
+ const router = express.Router();
28
+ const logger = createLogger('access-routes');
29
+ const accessRateLimiter = rateLimit({
30
+ keyPrefix: 'resource-access',
31
+ windowMs: 15 * 60 * 1000,
32
+ max: 240,
33
+ message: { success: false, message: 'Too many requests' },
34
+ standardHeaders: true,
35
+ legacyHeaders: false,
36
+ });
37
+ router.use(accessRateLimiter, authenticate);
38
+ const actorOf = (req) => ({
39
+ userId: req.user.userId,
40
+ role: req.user.role,
41
+ });
42
+ const handleError = (res, error, fallback) => {
43
+ if (error instanceof ResourceGrantError) {
44
+ res
45
+ .status(error.statusCode)
46
+ .json({ success: false, message: error.message });
47
+ return;
48
+ }
49
+ logger.error(fallback, error);
50
+ res.status(500).json({ success: false, message: 'Internal server error' });
51
+ };
52
+ const publicGrant = (grant) => ({
53
+ id: grant.id,
54
+ resourceType: grant.resource_type,
55
+ resourceId: grant.resource_id,
56
+ principalType: grant.principal_type,
57
+ principalId: grant.principal_id,
58
+ permission: grant.permission,
59
+ createdAt: new Date(grant.created_at).toISOString(),
60
+ });
61
+ /** Grants on one of the caller's resources. */
62
+ router.get('/grants', async (req, res) => {
63
+ try {
64
+ const { type, id } = req.query;
65
+ if (typeof type !== 'string' || typeof id !== 'string') {
66
+ res
67
+ .status(400)
68
+ .json({ success: false, message: 'type and id are required' });
69
+ return;
70
+ }
71
+ const grants = await listGrantsForResource(actorOf(req), type, id);
72
+ res.json({ success: true, data: grants.map(publicGrant) });
73
+ }
74
+ catch (error) {
75
+ handleError(res, error, 'Grant list error:');
76
+ }
77
+ });
78
+ /** Share a resource with a user or group. */
79
+ router.post('/grants', async (req, res) => {
80
+ try {
81
+ const { resourceType, resourceId, principalType, principalId, permission } = req.body ?? {};
82
+ for (const [field, value] of Object.entries({
83
+ resourceType,
84
+ resourceId,
85
+ principalType,
86
+ principalId,
87
+ permission,
88
+ })) {
89
+ if (typeof value !== 'string' || !value) {
90
+ res
91
+ .status(400)
92
+ .json({ success: false, message: `${field} is required` });
93
+ return;
94
+ }
95
+ }
96
+ const grant = await createGrant(actorOf(req), {
97
+ resourceType,
98
+ resourceId,
99
+ principalType,
100
+ principalId,
101
+ permission,
102
+ });
103
+ res.status(201).json({ success: true, data: publicGrant(grant) });
104
+ }
105
+ catch (error) {
106
+ handleError(res, error, 'Grant create error:');
107
+ }
108
+ });
109
+ /** Remove a grant (owner, grant-admin, or the granted user leaving). */
110
+ router.delete('/grants/:id', async (req, res) => {
111
+ try {
112
+ const deleted = await deleteGrant(actorOf(req), req.params.id);
113
+ if (!deleted) {
114
+ res.status(404).json({ success: false, message: 'Grant not found' });
115
+ return;
116
+ }
117
+ res.json({ success: true });
118
+ }
119
+ catch (error) {
120
+ handleError(res, error, 'Grant delete error:');
121
+ }
122
+ });
123
+ /** Everything shared with the caller, directly or via groups. */
124
+ router.get('/shared-with-me', async (req, res) => {
125
+ try {
126
+ const grants = await listGrantsForActor(actorOf(req));
127
+ res.json({ success: true, data: grants.map(publicGrant) });
128
+ }
129
+ catch (error) {
130
+ handleError(res, error, 'Shared list error:');
131
+ }
132
+ });
133
+ export default router;
134
+ //# sourceMappingURL=access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/routes/access.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
3
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/routes/audit.ts"],"names":[],"mappings":"AAuBA,QAAA,MAAM,MAAM,4CAAmB,CAAC;eAkEjB,MAAM"}
@@ -0,0 +1,82 @@
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
+ import express from 'express';
18
+ import rateLimit from '../middleware/sharedRateLimit.js';
19
+ import { authenticate, requireAdmin } from '../middleware/auth.js';
20
+ import { queryAuditEvents } from '../services/securityAuditService.js';
21
+ import { createLogger } from '../utils/logger.js';
22
+ const router = express.Router();
23
+ const logger = createLogger('audit-routes');
24
+ const auditRateLimiter = rateLimit({
25
+ keyPrefix: 'security-audit',
26
+ windowMs: 15 * 60 * 1000,
27
+ max: 120,
28
+ message: { success: false, message: 'Too many requests' },
29
+ standardHeaders: true,
30
+ legacyHeaders: false,
31
+ });
32
+ router.use(auditRateLimiter, authenticate, requireAdmin);
33
+ /** Query the security audit log (admin only, redacted at write time). */
34
+ router.get('/', async (req, res) => {
35
+ try {
36
+ const { action, actor, result, targetType, before, after, limit } = req.query;
37
+ const parseEpoch = (value) => {
38
+ if (typeof value !== 'string' || !value)
39
+ return undefined;
40
+ const asNumber = Number(value);
41
+ if (Number.isFinite(asNumber) && asNumber > 0)
42
+ return asNumber;
43
+ const asDate = Date.parse(value);
44
+ return Number.isFinite(asDate) ? asDate : undefined;
45
+ };
46
+ const isResult = (value) => value === 'success' || value === 'denied' || value === 'failure';
47
+ const events = await queryAuditEvents({
48
+ ...(typeof action === 'string' && action ? { action } : {}),
49
+ ...(typeof actor === 'string' && actor ? { actorUserId: actor } : {}),
50
+ ...(isResult(result) ? { result } : {}),
51
+ ...(typeof targetType === 'string' && targetType ? { targetType } : {}),
52
+ ...(parseEpoch(before) !== undefined
53
+ ? { before: parseEpoch(before) }
54
+ : {}),
55
+ ...(parseEpoch(after) !== undefined ? { after: parseEpoch(after) } : {}),
56
+ ...(typeof limit === 'string' && Number.isFinite(Number(limit))
57
+ ? { limit: Number(limit) }
58
+ : {}),
59
+ });
60
+ res.json({
61
+ success: true,
62
+ data: events.map(event => ({
63
+ id: event.id,
64
+ occurredAt: new Date(event.occurred_at).toISOString(),
65
+ actorUserId: event.actor_user_id,
66
+ actorKind: event.actor_kind,
67
+ action: event.action,
68
+ targetType: event.target_type,
69
+ targetId: event.target_id,
70
+ result: event.result,
71
+ requestId: event.request_id,
72
+ details: event.details ? JSON.parse(event.details) : null,
73
+ })),
74
+ });
75
+ }
76
+ catch (error) {
77
+ logger.error('Audit query error:', error);
78
+ res.status(500).json({ success: false, message: 'Internal server error' });
79
+ }
80
+ });
81
+ export default router;
82
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/routes/audit.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AA2CA,QAAA,MAAM,MAAM,4CAAmB,CAAC;eAsoBjB,MAAM"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AAgEA,QAAA,MAAM,MAAM,4CAAmB,CAAC;eAs/BjB,MAAM"}
@@ -22,11 +22,15 @@ import { authService } from '../services/authService.js';
22
22
  import { authenticate, requireAdmin, } from '../middleware/auth.js';
23
23
  import { encryptionService } from '../services/encryptionService.js';
24
24
  import { turnstileService } from '../services/turnstileService.js';
25
- import { beginOAuthFlow, consumeOAuthSessionCookie, consumeOAuthState, setOAuthSessionCookie, } from '../services/oauthSecurity.js';
25
+ import { beginOAuthFlow, beginOAuthFlowWithPayload, consumeOAuthSessionCookie, consumeOAuthState, consumeOAuthStatePayload, setOAuthSessionCookie, } from '../services/oauthSecurity.js';
26
26
  import { validatePasswordStrength } from '../utils/hash.js';
27
27
  import { createLogger } from '../utils/logger.js';
28
28
  import { WebSocketTicketRateLimitError, websocketTicketService, } from '../services/websocketTicketService.js';
29
29
  import { coordinatedRateLimit } from '../middleware/coordinatedRateLimit.js';
30
+ import { findSessionById, listSessionsForUser, revokeAllAuthSessions, revokeAuthSession, } from '../services/authSessionService.js';
31
+ import { createApiToken, findTokenById, isApiTokenScope, listTokensForUser, revokeApiToken, toPublicToken, } from '../services/apiTokenService.js';
32
+ import { hashClientIp, recordAuditEvent, } from '../services/securityAuditService.js';
33
+ import { OidcError, oidcOAuthService } from '../services/simpleOidcOAuth.js';
30
34
  const router = express.Router();
31
35
  const logger = createLogger('auth-routes');
32
36
  // Fallback frontend URL for OAuth redirects
@@ -100,7 +104,7 @@ router.post('/websocket-ticket', generalAuthRateLimiter, authenticate, async (re
100
104
  }
101
105
  const sessionExpiresAt = (req.user?.exp ?? 0) * 1000;
102
106
  try {
103
- const ticket = await websocketTicketService.issue(req.user.userId, sessionExpiresAt, audience, audience === 'work-terminal' ? taskId.trim() : undefined);
107
+ const ticket = await websocketTicketService.issue(req.user.userId, sessionExpiresAt, audience, audience === 'work-terminal' ? taskId.trim() : undefined, req.auth?.kind === 'session' ? req.auth.sessionId : undefined);
104
108
  res.json({ success: true, data: ticket });
105
109
  }
106
110
  catch (error) {
@@ -136,14 +140,31 @@ router.post('/login', loginRateLimiter, async (req, res) => {
136
140
  });
137
141
  return;
138
142
  }
139
- const result = await authService.login(username, password);
143
+ const result = await authService.login(username, password, {
144
+ kind: 'password',
145
+ ip: getClientIp(req),
146
+ userAgent: req.headers['user-agent'],
147
+ });
140
148
  if (!result) {
149
+ void recordAuditEvent({
150
+ action: 'auth.login',
151
+ result: 'failure',
152
+ ipHash: hashClientIp(getClientIp(req)),
153
+ details: { username: String(username).slice(0, 64) },
154
+ });
141
155
  res.status(401).json({
142
156
  success: false,
143
157
  message: 'Invalid credentials',
144
158
  });
145
159
  return;
146
160
  }
161
+ void recordAuditEvent({
162
+ action: 'auth.login',
163
+ result: result.status === 'authenticated' ? 'success' : 'denied',
164
+ actorUserId: result.user.id,
165
+ ipHash: hashClientIp(getClientIp(req)),
166
+ details: { method: 'password', status: result.status },
167
+ });
147
168
  if (result.status === 'pending') {
148
169
  res.status(403).json({
149
170
  success: false,
@@ -175,9 +196,16 @@ router.post('/login', loginRateLimiter, async (req, res) => {
175
196
  */
176
197
  router.post('/logout', generalAuthRateLimiter, authenticate, async (req, res) => {
177
198
  try {
178
- // In a stateless JWT system, logout is handled client-side
179
- // But we can log it for audit purposes
180
- logger.debug(`User ${req.user?.username} logged out`);
199
+ // Revoke the server-side session so the JWT stops working everywhere,
200
+ // not just in this browser.
201
+ if (req.auth?.kind === 'session' && req.user) {
202
+ await revokeAuthSession(req.auth.sessionId, req.user.userId);
203
+ }
204
+ void recordAuditEvent({
205
+ action: 'auth.logout',
206
+ result: 'success',
207
+ actorUserId: req.user?.userId ?? null,
208
+ });
181
209
  res.json({
182
210
  success: true,
183
211
  message: 'Logged out successfully',
@@ -191,6 +219,167 @@ router.post('/logout', generalAuthRateLimiter, authenticate, async (req, res) =>
191
219
  });
192
220
  }
193
221
  });
222
+ /** List this account's sessions, newest activity first. */
223
+ router.get('/sessions', generalAuthRateLimiter, authenticate, async (req, res) => {
224
+ try {
225
+ const sessions = await listSessionsForUser(req.user.userId);
226
+ const currentSessionId = req.auth?.kind === 'session' ? req.auth.sessionId : null;
227
+ res.json({
228
+ success: true,
229
+ data: sessions.map(session => ({
230
+ id: session.id,
231
+ kind: session.kind,
232
+ userAgent: session.user_agent,
233
+ createdAt: new Date(session.created_at).toISOString(),
234
+ lastSeenAt: new Date(session.last_seen_at).toISOString(),
235
+ expiresAt: new Date(session.expires_at).toISOString(),
236
+ revokedAt: session.revoked_at
237
+ ? new Date(session.revoked_at).toISOString()
238
+ : null,
239
+ current: session.id === currentSessionId,
240
+ })),
241
+ });
242
+ }
243
+ catch (error) {
244
+ logger.error('Session inventory error:', error);
245
+ res
246
+ .status(500)
247
+ .json({ success: false, message: 'Internal server error' });
248
+ }
249
+ });
250
+ /** Revoke every other session ("sign out everywhere else"). */
251
+ router.post('/sessions/revoke-others', generalAuthRateLimiter, authenticate, async (req, res) => {
252
+ try {
253
+ const currentSessionId = req.auth?.kind === 'session' ? req.auth.sessionId : undefined;
254
+ const revoked = await revokeAllAuthSessions(req.user.userId, req.user.userId, currentSessionId);
255
+ void recordAuditEvent({
256
+ action: 'session.revoke-others',
257
+ result: 'success',
258
+ actorUserId: req.user.userId,
259
+ details: { revokedCount: revoked.length },
260
+ });
261
+ res.json({ success: true, data: { revokedCount: revoked.length } });
262
+ }
263
+ catch (error) {
264
+ logger.error('Session revocation error:', error);
265
+ res
266
+ .status(500)
267
+ .json({ success: false, message: 'Internal server error' });
268
+ }
269
+ });
270
+ /** Revoke one session by id (own sessions only; admins may revoke any). */
271
+ router.delete('/sessions/:id', generalAuthRateLimiter, authenticate, async (req, res) => {
272
+ try {
273
+ const session = await findSessionById(req.params.id);
274
+ const isOwn = session?.user_id === req.user.userId;
275
+ const isAdmin = req.user.role === 'admin';
276
+ if (!session || (!isOwn && !isAdmin)) {
277
+ res.status(404).json({ success: false, message: 'Session not found' });
278
+ return;
279
+ }
280
+ const revoked = await revokeAuthSession(session.id, req.user.userId);
281
+ void recordAuditEvent({
282
+ action: 'session.revoke',
283
+ result: 'success',
284
+ actorUserId: req.user.userId,
285
+ targetType: 'auth-session',
286
+ targetId: session.id,
287
+ details: { targetUserId: session.user_id },
288
+ });
289
+ res.json({ success: true, data: { revoked } });
290
+ }
291
+ catch (error) {
292
+ logger.error('Session revocation error:', error);
293
+ res
294
+ .status(500)
295
+ .json({ success: false, message: 'Internal server error' });
296
+ }
297
+ });
298
+ /** List this account's API tokens (hashes never leave the server). */
299
+ router.get('/tokens', generalAuthRateLimiter, authenticate, async (req, res) => {
300
+ try {
301
+ const tokens = await listTokensForUser(req.user.userId);
302
+ res.json({ success: true, data: tokens.map(toPublicToken) });
303
+ }
304
+ catch (error) {
305
+ logger.error('Token list error:', error);
306
+ res
307
+ .status(500)
308
+ .json({ success: false, message: 'Internal server error' });
309
+ }
310
+ });
311
+ /** Create a scoped API token; the secret is returned exactly once. */
312
+ router.post('/tokens', generalAuthRateLimiter, authenticate, async (req, res) => {
313
+ try {
314
+ if (req.auth?.kind === 'api-token') {
315
+ res.status(403).json({
316
+ success: false,
317
+ message: 'API tokens cannot create other tokens',
318
+ });
319
+ return;
320
+ }
321
+ const { name, scopes, expiresInDays } = req.body ?? {};
322
+ const requestedScopes = Array.isArray(scopes)
323
+ ? scopes.filter(isApiTokenScope)
324
+ : [];
325
+ if (requestedScopes.includes('admin') && req.user.role !== 'admin') {
326
+ res.status(403).json({
327
+ success: false,
328
+ message: 'Only administrators may mint admin-scoped tokens',
329
+ });
330
+ return;
331
+ }
332
+ const created = await createApiToken(req.user.userId, {
333
+ name: typeof name === 'string' ? name : '',
334
+ scopes: requestedScopes,
335
+ ...(typeof expiresInDays === 'number' ? { expiresInDays } : {}),
336
+ });
337
+ void recordAuditEvent({
338
+ action: 'token.create',
339
+ result: 'success',
340
+ actorUserId: req.user.userId,
341
+ targetType: 'api-token',
342
+ targetId: created.record.id,
343
+ details: { name: created.record.name, scopes: requestedScopes },
344
+ });
345
+ res.status(201).json({
346
+ success: true,
347
+ data: { token: created.token, record: toPublicToken(created.record) },
348
+ });
349
+ }
350
+ catch (error) {
351
+ const message = error instanceof Error ? error.message : 'Internal server error';
352
+ res.status(400).json({ success: false, message });
353
+ }
354
+ });
355
+ /** Revoke an API token (own tokens; admins may revoke any). */
356
+ router.delete('/tokens/:id', generalAuthRateLimiter, authenticate, async (req, res) => {
357
+ try {
358
+ const record = await findTokenById(req.params.id);
359
+ const isOwn = record?.user_id === req.user.userId;
360
+ const isAdmin = req.user.role === 'admin';
361
+ if (!record || (!isOwn && !isAdmin)) {
362
+ res.status(404).json({ success: false, message: 'Token not found' });
363
+ return;
364
+ }
365
+ const revoked = await revokeApiToken(record.id);
366
+ void recordAuditEvent({
367
+ action: 'token.revoke',
368
+ result: 'success',
369
+ actorUserId: req.user.userId,
370
+ targetType: 'api-token',
371
+ targetId: record.id,
372
+ details: { targetUserId: record.user_id },
373
+ });
374
+ res.json({ success: true, data: { revoked } });
375
+ }
376
+ catch (error) {
377
+ logger.error('Token revocation error:', error);
378
+ res
379
+ .status(500)
380
+ .json({ success: false, message: 'Internal server error' });
381
+ }
382
+ });
194
383
  /**
195
384
  * Verify token endpoint
196
385
  */
@@ -312,7 +501,11 @@ router.post('/signup', signupRateLimiter, async (req, res) => {
312
501
  });
313
502
  return;
314
503
  }
315
- const result = await authService.signup(username, password, email);
504
+ const result = await authService.signup(username, password, email, {
505
+ kind: 'signup',
506
+ ip: getClientIp(req),
507
+ userAgent: req.headers['user-agent'],
508
+ });
316
509
  if (!result) {
317
510
  res.status(500).json({
318
511
  success: false,
@@ -320,6 +513,13 @@ router.post('/signup', signupRateLimiter, async (req, res) => {
320
513
  });
321
514
  return;
322
515
  }
516
+ void recordAuditEvent({
517
+ action: 'auth.signup',
518
+ result: 'success',
519
+ actorUserId: result.user.id,
520
+ ipHash: hashClientIp(getClientIp(req)),
521
+ details: { status: result.status },
522
+ });
323
523
  const systemInfo = await authService.getSystemInfo();
324
524
  if (result.status === 'pending') {
325
525
  res.status(202).json({
@@ -397,8 +597,19 @@ router.get('/oauth/github/callback', generalAuthRateLimiter, async (req, res) =>
397
597
  if (user.status !== 'active') {
398
598
  return res.redirect(pendingApprovalRedirect());
399
599
  }
400
- // Generate JWT token using existing auth service
401
- const token = authService.generateToken(user);
600
+ // Issue a revocable session-bound JWT
601
+ const token = await authService.issueSession(user, {
602
+ kind: 'oauth:github',
603
+ ip: getClientIp(req),
604
+ userAgent: req.headers['user-agent'],
605
+ });
606
+ void recordAuditEvent({
607
+ action: 'auth.login',
608
+ result: 'success',
609
+ actorUserId: user.id,
610
+ ipHash: hashClientIp(getClientIp(req)),
611
+ details: { method: 'oauth:github' },
612
+ });
402
613
  logger.debug('GitHub OAuth successful for user:', user.username);
403
614
  setOAuthSessionCookie(req, res, token);
404
615
  res.redirect(frontendRedirect('?auth=success'));
@@ -462,8 +673,19 @@ router.get('/oauth/huggingface/callback', generalAuthRateLimiter, async (req, re
462
673
  if (user.status !== 'active') {
463
674
  return res.redirect(pendingApprovalRedirect());
464
675
  }
465
- // Generate JWT token
466
- const token = authService.generateToken(user);
676
+ // Issue a revocable session-bound JWT
677
+ const token = await authService.issueSession(user, {
678
+ kind: 'oauth:huggingface',
679
+ ip: getClientIp(req),
680
+ userAgent: req.headers['user-agent'],
681
+ });
682
+ void recordAuditEvent({
683
+ action: 'auth.login',
684
+ result: 'success',
685
+ actorUserId: user.id,
686
+ ipHash: hashClientIp(getClientIp(req)),
687
+ details: { method: 'oauth:huggingface' },
688
+ });
467
689
  logger.debug('Hugging Face OAuth successful for user:', user.username);
468
690
  setOAuthSessionCookie(req, res, token);
469
691
  res.redirect(frontendRedirect('?auth=success'));
@@ -479,6 +701,95 @@ router.get('/oauth/huggingface/callback', generalAuthRateLimiter, async (req, re
479
701
  router.get('/oauth/huggingface/status', generalAuthRateLimiter, (req, res) => {
480
702
  res.json({ configured: isHuggingFaceConfigured });
481
703
  });
704
+ /**
705
+ * Generic OIDC - Start authentication (PKCE + state + nonce)
706
+ */
707
+ router.get('/oauth/oidc', generalAuthRateLimiter, async (req, res) => {
708
+ if (!oidcOAuthService.isConfigured()) {
709
+ return res.status(404).json({ error: 'OIDC is not configured' });
710
+ }
711
+ try {
712
+ const pkce = oidcOAuthService.createPkcePair();
713
+ const nonce = oidcOAuthService.createNonce();
714
+ const state = beginOAuthFlowWithPayload(req, res, 'oidc', {
715
+ verifier: pkce.verifier,
716
+ nonce,
717
+ });
718
+ const authUrl = await oidcOAuthService.getAuthUrl(state, nonce, pkce.challenge);
719
+ res.redirect(authUrl);
720
+ }
721
+ catch (error) {
722
+ logger.error('OIDC start error:', error);
723
+ res.redirect(oauthErrorRedirect('oauth_failed'));
724
+ }
725
+ });
726
+ /**
727
+ * Generic OIDC - Handle callback: verify state, exchange with PKCE,
728
+ * validate the ID token against JWKS, then issue a session-bound JWT.
729
+ */
730
+ router.get('/oauth/oidc/callback', generalAuthRateLimiter, async (req, res) => {
731
+ try {
732
+ if (!oidcOAuthService.isConfigured()) {
733
+ return res.redirect(oauthErrorRedirect('oauth_not_configured'));
734
+ }
735
+ const { code, state } = req.query;
736
+ const payload = consumeOAuthStatePayload(req, res, 'oidc', typeof state === 'string' ? state : '');
737
+ if (!payload ||
738
+ !payload.verifier ||
739
+ !payload.nonce ||
740
+ !code ||
741
+ typeof code !== 'string') {
742
+ return res.redirect(oauthErrorRedirect('oauth_failed'));
743
+ }
744
+ const exchanged = await oidcOAuthService.exchangeCode(code, payload.verifier);
745
+ const claims = await oidcOAuthService.verifyIdToken(exchanged.idToken, payload.nonce);
746
+ const user = await oidcOAuthService.processClaims(claims);
747
+ if (user.status !== 'active') {
748
+ return res.redirect(pendingApprovalRedirect());
749
+ }
750
+ const token = await authService.issueSession(user, {
751
+ kind: 'oidc',
752
+ ip: getClientIp(req),
753
+ userAgent: req.headers['user-agent'],
754
+ });
755
+ void recordAuditEvent({
756
+ action: 'auth.login',
757
+ result: 'success',
758
+ actorUserId: user.id,
759
+ ipHash: hashClientIp(getClientIp(req)),
760
+ details: { method: 'oidc' },
761
+ });
762
+ setOAuthSessionCookie(req, res, token);
763
+ res.redirect(frontendRedirect('?auth=success'));
764
+ }
765
+ catch (error) {
766
+ logger.error('OIDC callback error:', error);
767
+ void recordAuditEvent({
768
+ action: 'auth.login',
769
+ result: 'failure',
770
+ ipHash: hashClientIp(getClientIp(req)),
771
+ details: {
772
+ method: 'oidc',
773
+ reason: error instanceof OidcError ? error.code : 'unknown',
774
+ },
775
+ });
776
+ const reason = error instanceof OidcError &&
777
+ (error.code === 'registration-disabled' || error.code === 'email-in-use')
778
+ ? error.code.replace(/-/g, '_')
779
+ : 'oauth_failed';
780
+ res.redirect(oauthErrorRedirect(reason));
781
+ }
782
+ });
783
+ /**
784
+ * Check if generic OIDC is configured (includes the login button label)
785
+ */
786
+ router.get('/oauth/oidc/status', generalAuthRateLimiter, (req, res) => {
787
+ const configured = oidcOAuthService.isConfigured();
788
+ res.json({
789
+ configured,
790
+ ...(configured ? { displayName: oidcOAuthService.displayName } : {}),
791
+ });
792
+ });
482
793
  /**
483
794
  * Exchange the short-lived HttpOnly OAuth transfer cookie for the application's
484
795
  * normal bearer-token response. The cookie is cleared even when it is invalid.