libre-webui 0.25.0 → 0.26.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 (510) hide show
  1. package/README.md +32 -22
  2. package/backend/dist/index.d.ts.map +1 -1
  3. package/backend/dist/index.js +26 -4
  4. package/backend/dist/index.js.map +1 -1
  5. package/backend/dist/middleware/auth.d.ts +7 -0
  6. package/backend/dist/middleware/auth.d.ts.map +1 -1
  7. package/backend/dist/middleware/auth.js +26 -0
  8. package/backend/dist/middleware/auth.js.map +1 -1
  9. package/backend/dist/middleware/index.d.ts +20 -0
  10. package/backend/dist/middleware/index.d.ts.map +1 -1
  11. package/backend/dist/middleware/index.js +105 -2
  12. package/backend/dist/middleware/index.js.map +1 -1
  13. package/backend/dist/models/personaModel.d.ts.map +1 -1
  14. package/backend/dist/models/personaModel.js +2 -0
  15. package/backend/dist/models/personaModel.js.map +1 -1
  16. package/backend/dist/observability/otel.d.ts +40 -0
  17. package/backend/dist/observability/otel.d.ts.map +1 -0
  18. package/backend/dist/observability/otel.js +337 -0
  19. package/backend/dist/observability/otel.js.map +1 -0
  20. package/backend/dist/observability/requestContext.d.ts +14 -0
  21. package/backend/dist/observability/requestContext.d.ts.map +1 -0
  22. package/backend/dist/observability/requestContext.js +21 -0
  23. package/backend/dist/observability/requestContext.js.map +1 -0
  24. package/backend/dist/persistence/chatGenerationTypes.d.ts +9 -0
  25. package/backend/dist/persistence/chatGenerationTypes.d.ts.map +1 -1
  26. package/backend/dist/persistence/extensionTypes.d.ts +13 -0
  27. package/backend/dist/persistence/extensionTypes.d.ts.map +1 -1
  28. package/backend/dist/persistence/extensionTypes.js.map +1 -1
  29. package/backend/dist/persistence/index.d.ts +1 -1
  30. package/backend/dist/persistence/index.d.ts.map +1 -1
  31. package/backend/dist/persistence/index.js.map +1 -1
  32. package/backend/dist/persistence/postgresAgentFoundationMigration.d.ts +9 -0
  33. package/backend/dist/persistence/postgresAgentFoundationMigration.d.ts.map +1 -0
  34. package/backend/dist/persistence/postgresAgentFoundationMigration.js +164 -0
  35. package/backend/dist/persistence/postgresAgentFoundationMigration.js.map +1 -0
  36. package/backend/dist/persistence/postgresExtensionRepositories.d.ts.map +1 -1
  37. package/backend/dist/persistence/postgresExtensionRepositories.js +46 -2
  38. package/backend/dist/persistence/postgresExtensionRepositories.js.map +1 -1
  39. package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.d.ts +10 -0
  40. package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.d.ts.map +1 -0
  41. package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.js +148 -0
  42. package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.js.map +1 -0
  43. package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
  44. package/backend/dist/persistence/postgresMigrationRegistry.js +10 -0
  45. package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
  46. package/backend/dist/persistence/postgresNotesV2Migration.d.ts +4 -0
  47. package/backend/dist/persistence/postgresNotesV2Migration.d.ts.map +1 -0
  48. package/backend/dist/persistence/postgresNotesV2Migration.js +57 -0
  49. package/backend/dist/persistence/postgresNotesV2Migration.js.map +1 -0
  50. package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
  51. package/backend/dist/persistence/postgresResourceRepositories.js +1646 -11
  52. package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
  53. package/backend/dist/persistence/postgresSkillFilesMigration.d.ts +9 -0
  54. package/backend/dist/persistence/postgresSkillFilesMigration.d.ts.map +1 -0
  55. package/backend/dist/persistence/postgresSkillFilesMigration.js +50 -0
  56. package/backend/dist/persistence/postgresSkillFilesMigration.js.map +1 -0
  57. package/backend/dist/persistence/postgresTeamCollaborationMigration.d.ts +10 -0
  58. package/backend/dist/persistence/postgresTeamCollaborationMigration.d.ts.map +1 -0
  59. package/backend/dist/persistence/postgresTeamCollaborationMigration.js +161 -0
  60. package/backend/dist/persistence/postgresTeamCollaborationMigration.js.map +1 -0
  61. package/backend/dist/persistence/resourceTypes.d.ts +541 -3
  62. package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
  63. package/backend/dist/persistence/resourceTypes.js.map +1 -1
  64. package/backend/dist/persistence/sqliteExtensionRepositories.d.ts.map +1 -1
  65. package/backend/dist/persistence/sqliteExtensionRepositories.js +26 -4
  66. package/backend/dist/persistence/sqliteExtensionRepositories.js.map +1 -1
  67. package/backend/dist/persistence/sqliteMigrations.d.ts +9 -0
  68. package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
  69. package/backend/dist/persistence/sqliteMigrations.js +1196 -29
  70. package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
  71. package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
  72. package/backend/dist/persistence/sqliteResourceRepositories.js +1523 -134
  73. package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
  74. package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
  75. package/backend/dist/persistence/sqliteToPostgresMigration.js +257 -3
  76. package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
  77. package/backend/dist/platform/jobs/domainJobContracts.d.ts +8 -0
  78. package/backend/dist/platform/jobs/domainJobContracts.d.ts.map +1 -1
  79. package/backend/dist/platform/jobs/domainJobContracts.js +8 -0
  80. package/backend/dist/platform/jobs/domainJobContracts.js.map +1 -1
  81. package/backend/dist/platform/jobs/domainJobHandlers.d.ts.map +1 -1
  82. package/backend/dist/platform/jobs/domainJobHandlers.js +310 -3
  83. package/backend/dist/platform/jobs/domainJobHandlers.js.map +1 -1
  84. package/backend/dist/platform/jobs/embeddedDurableJobWorker.d.ts.map +1 -1
  85. package/backend/dist/platform/jobs/embeddedDurableJobWorker.js +4 -2
  86. package/backend/dist/platform/jobs/embeddedDurableJobWorker.js.map +1 -1
  87. package/backend/dist/platform/jobs/postgresDurableJobWorker.d.ts.map +1 -1
  88. package/backend/dist/platform/jobs/postgresDurableJobWorker.js +4 -2
  89. package/backend/dist/platform/jobs/postgresDurableJobWorker.js.map +1 -1
  90. package/backend/dist/platform/storage/pgVectorStore.d.ts +2 -1
  91. package/backend/dist/platform/storage/pgVectorStore.d.ts.map +1 -1
  92. package/backend/dist/platform/storage/pgVectorStore.js +51 -0
  93. package/backend/dist/platform/storage/pgVectorStore.js.map +1 -1
  94. package/backend/dist/platform/storage/platformDomainRepositories.d.ts +8 -2
  95. package/backend/dist/platform/storage/platformDomainRepositories.d.ts.map +1 -1
  96. package/backend/dist/platform/storage/postgresPlatformDomainRepositories.d.ts.map +1 -1
  97. package/backend/dist/platform/storage/postgresPlatformDomainRepositories.js +25 -4
  98. package/backend/dist/platform/storage/postgresPlatformDomainRepositories.js.map +1 -1
  99. package/backend/dist/platform/storage/sqliteEncryptedVectorStore.d.ts +2 -1
  100. package/backend/dist/platform/storage/sqliteEncryptedVectorStore.d.ts.map +1 -1
  101. package/backend/dist/platform/storage/sqliteEncryptedVectorStore.js +35 -0
  102. package/backend/dist/platform/storage/sqliteEncryptedVectorStore.js.map +1 -1
  103. package/backend/dist/platform/storage/sqlitePlatformDomainRepositories.d.ts.map +1 -1
  104. package/backend/dist/platform/storage/sqlitePlatformDomainRepositories.js +26 -3
  105. package/backend/dist/platform/storage/sqlitePlatformDomainRepositories.js.map +1 -1
  106. package/backend/dist/platform/storage/vectorStore.d.ts +14 -0
  107. package/backend/dist/platform/storage/vectorStore.d.ts.map +1 -1
  108. package/backend/dist/platform/storage/vectorStore.js.map +1 -1
  109. package/backend/dist/routes/access.d.ts.map +1 -1
  110. package/backend/dist/routes/access.js +84 -3
  111. package/backend/dist/routes/access.js.map +1 -1
  112. package/backend/dist/routes/calendar.d.ts.map +1 -1
  113. package/backend/dist/routes/calendar.js +125 -22
  114. package/backend/dist/routes/calendar.js.map +1 -1
  115. package/backend/dist/routes/channels.d.ts +3 -0
  116. package/backend/dist/routes/channels.d.ts.map +1 -0
  117. package/backend/dist/routes/channels.js +447 -0
  118. package/backend/dist/routes/channels.js.map +1 -0
  119. package/backend/dist/routes/chat.d.ts.map +1 -1
  120. package/backend/dist/routes/chat.js +195 -7
  121. package/backend/dist/routes/chat.js.map +1 -1
  122. package/backend/dist/routes/costs.d.ts +3 -0
  123. package/backend/dist/routes/costs.d.ts.map +1 -0
  124. package/backend/dist/routes/costs.js +191 -0
  125. package/backend/dist/routes/costs.js.map +1 -0
  126. package/backend/dist/routes/documents.d.ts.map +1 -1
  127. package/backend/dist/routes/documents.js +64 -21
  128. package/backend/dist/routes/documents.js.map +1 -1
  129. package/backend/dist/routes/evaluations.d.ts +3 -0
  130. package/backend/dist/routes/evaluations.d.ts.map +1 -0
  131. package/backend/dist/routes/evaluations.js +352 -0
  132. package/backend/dist/routes/evaluations.js.map +1 -0
  133. package/backend/dist/routes/imageGen.d.ts.map +1 -1
  134. package/backend/dist/routes/imageGen.js +2 -1
  135. package/backend/dist/routes/imageGen.js.map +1 -1
  136. package/backend/dist/routes/media.d.ts.map +1 -1
  137. package/backend/dist/routes/media.js +228 -7
  138. package/backend/dist/routes/media.js.map +1 -1
  139. package/backend/dist/routes/notes.d.ts.map +1 -1
  140. package/backend/dist/routes/notes.js +149 -36
  141. package/backend/dist/routes/notes.js.map +1 -1
  142. package/backend/dist/routes/notifications.d.ts +3 -0
  143. package/backend/dist/routes/notifications.d.ts.map +1 -0
  144. package/backend/dist/routes/notifications.js +213 -0
  145. package/backend/dist/routes/notifications.js.map +1 -0
  146. package/backend/dist/routes/openaiCompat.d.ts +3 -0
  147. package/backend/dist/routes/openaiCompat.d.ts.map +1 -0
  148. package/backend/dist/routes/openaiCompat.js +314 -0
  149. package/backend/dist/routes/openaiCompat.js.map +1 -0
  150. package/backend/dist/routes/personas.js +1 -1
  151. package/backend/dist/routes/personas.js.map +1 -1
  152. package/backend/dist/routes/prompts.d.ts +3 -0
  153. package/backend/dist/routes/prompts.d.ts.map +1 -0
  154. package/backend/dist/routes/prompts.js +173 -0
  155. package/backend/dist/routes/prompts.js.map +1 -0
  156. package/backend/dist/routes/search.d.ts.map +1 -1
  157. package/backend/dist/routes/search.js +29 -1
  158. package/backend/dist/routes/search.js.map +1 -1
  159. package/backend/dist/routes/skills.d.ts +3 -0
  160. package/backend/dist/routes/skills.d.ts.map +1 -0
  161. package/backend/dist/routes/skills.js +246 -0
  162. package/backend/dist/routes/skills.js.map +1 -0
  163. package/backend/dist/routes/stt.d.ts.map +1 -1
  164. package/backend/dist/routes/stt.js +2 -1
  165. package/backend/dist/routes/stt.js.map +1 -1
  166. package/backend/dist/routes/tools.d.ts +3 -0
  167. package/backend/dist/routes/tools.d.ts.map +1 -0
  168. package/backend/dist/routes/tools.js +359 -0
  169. package/backend/dist/routes/tools.js.map +1 -0
  170. package/backend/dist/routes/tts.d.ts.map +1 -1
  171. package/backend/dist/routes/tts.js +63 -8
  172. package/backend/dist/routes/tts.js.map +1 -1
  173. package/backend/dist/routes/voiceAccess.d.ts +3 -0
  174. package/backend/dist/routes/voiceAccess.d.ts.map +1 -0
  175. package/backend/dist/routes/voiceAccess.js +102 -0
  176. package/backend/dist/routes/voiceAccess.js.map +1 -0
  177. package/backend/dist/services/apiTokenService.d.ts +2 -2
  178. package/backend/dist/services/apiTokenService.d.ts.map +1 -1
  179. package/backend/dist/services/apiTokenService.js +7 -0
  180. package/backend/dist/services/apiTokenService.js.map +1 -1
  181. package/backend/dist/services/authorizationService.d.ts +8 -4
  182. package/backend/dist/services/authorizationService.d.ts.map +1 -1
  183. package/backend/dist/services/authorizationService.js +31 -1
  184. package/backend/dist/services/authorizationService.js.map +1 -1
  185. package/backend/dist/services/automationSchedulerService.d.ts.map +1 -1
  186. package/backend/dist/services/automationSchedulerService.js +33 -3
  187. package/backend/dist/services/automationSchedulerService.js.map +1 -1
  188. package/backend/dist/services/automationService.d.ts +4 -1
  189. package/backend/dist/services/automationService.d.ts.map +1 -1
  190. package/backend/dist/services/automationService.js +25 -2
  191. package/backend/dist/services/automationService.js.map +1 -1
  192. package/backend/dist/services/builtinToolsService.d.ts +20 -0
  193. package/backend/dist/services/builtinToolsService.d.ts.map +1 -0
  194. package/backend/dist/services/builtinToolsService.js +661 -0
  195. package/backend/dist/services/builtinToolsService.js.map +1 -0
  196. package/backend/dist/services/calendarService.d.ts +74 -0
  197. package/backend/dist/services/calendarService.d.ts.map +1 -0
  198. package/backend/dist/services/calendarService.js +339 -0
  199. package/backend/dist/services/calendarService.js.map +1 -0
  200. package/backend/dist/services/channelService.d.ts +195 -0
  201. package/backend/dist/services/channelService.d.ts.map +1 -0
  202. package/backend/dist/services/channelService.js +1057 -0
  203. package/backend/dist/services/channelService.js.map +1 -0
  204. package/backend/dist/services/chatRequestService.d.ts.map +1 -1
  205. package/backend/dist/services/chatRequestService.js +29 -1
  206. package/backend/dist/services/chatRequestService.js.map +1 -1
  207. package/backend/dist/services/chatService.d.ts +61 -1
  208. package/backend/dist/services/chatService.d.ts.map +1 -1
  209. package/backend/dist/services/chatService.js +218 -1
  210. package/backend/dist/services/chatService.js.map +1 -1
  211. package/backend/dist/services/chatToolRuntimeService.d.ts +55 -0
  212. package/backend/dist/services/chatToolRuntimeService.d.ts.map +1 -0
  213. package/backend/dist/services/chatToolRuntimeService.js +389 -0
  214. package/backend/dist/services/chatToolRuntimeService.js.map +1 -0
  215. package/backend/dist/services/costGovernanceService.d.ts +102 -0
  216. package/backend/dist/services/costGovernanceService.d.ts.map +1 -0
  217. package/backend/dist/services/costGovernanceService.js +463 -0
  218. package/backend/dist/services/costGovernanceService.js.map +1 -0
  219. package/backend/dist/services/dataArchiveService.d.ts +6 -0
  220. package/backend/dist/services/dataArchiveService.d.ts.map +1 -1
  221. package/backend/dist/services/dataArchiveService.js +23 -4
  222. package/backend/dist/services/dataArchiveService.js.map +1 -1
  223. package/backend/dist/services/documentService.d.ts +58 -1
  224. package/backend/dist/services/documentService.d.ts.map +1 -1
  225. package/backend/dist/services/documentService.js +289 -84
  226. package/backend/dist/services/documentService.js.map +1 -1
  227. package/backend/dist/services/durableChatGenerationService.d.ts +9 -0
  228. package/backend/dist/services/durableChatGenerationService.d.ts.map +1 -1
  229. package/backend/dist/services/durableChatGenerationService.js +152 -11
  230. package/backend/dist/services/durableChatGenerationService.js.map +1 -1
  231. package/backend/dist/services/evaluationService.d.ts +111 -0
  232. package/backend/dist/services/evaluationService.d.ts.map +1 -0
  233. package/backend/dist/services/evaluationService.js +403 -0
  234. package/backend/dist/services/evaluationService.js.map +1 -0
  235. package/backend/dist/services/galleryService.d.ts +6 -0
  236. package/backend/dist/services/galleryService.d.ts.map +1 -1
  237. package/backend/dist/services/galleryService.js +26 -0
  238. package/backend/dist/services/galleryService.js.map +1 -1
  239. package/backend/dist/services/mcpClientService.d.ts +29 -0
  240. package/backend/dist/services/mcpClientService.d.ts.map +1 -0
  241. package/backend/dist/services/mcpClientService.js +189 -0
  242. package/backend/dist/services/mcpClientService.js.map +1 -0
  243. package/backend/dist/services/noteService.d.ts +71 -0
  244. package/backend/dist/services/noteService.d.ts.map +1 -0
  245. package/backend/dist/services/noteService.js +397 -0
  246. package/backend/dist/services/noteService.js.map +1 -0
  247. package/backend/dist/services/notificationService.d.ts +55 -0
  248. package/backend/dist/services/notificationService.d.ts.map +1 -0
  249. package/backend/dist/services/notificationService.js +308 -0
  250. package/backend/dist/services/notificationService.js.map +1 -0
  251. package/backend/dist/services/openApiToolService.d.ts +38 -0
  252. package/backend/dist/services/openApiToolService.d.ts.map +1 -0
  253. package/backend/dist/services/openApiToolService.js +254 -0
  254. package/backend/dist/services/openApiToolService.js.map +1 -0
  255. package/backend/dist/services/personaService.d.ts +18 -2
  256. package/backend/dist/services/personaService.d.ts.map +1 -1
  257. package/backend/dist/services/personaService.js +113 -4
  258. package/backend/dist/services/personaService.js.map +1 -1
  259. package/backend/dist/services/pluginImageGenerationService.d.ts +19 -0
  260. package/backend/dist/services/pluginImageGenerationService.d.ts.map +1 -1
  261. package/backend/dist/services/pluginImageGenerationService.js +121 -0
  262. package/backend/dist/services/pluginImageGenerationService.js.map +1 -1
  263. package/backend/dist/services/pluginService.d.ts +7 -0
  264. package/backend/dist/services/pluginService.d.ts.map +1 -1
  265. package/backend/dist/services/pluginService.js +8 -2
  266. package/backend/dist/services/pluginService.js.map +1 -1
  267. package/backend/dist/services/promptService.d.ts +74 -0
  268. package/backend/dist/services/promptService.d.ts.map +1 -0
  269. package/backend/dist/services/promptService.js +513 -0
  270. package/backend/dist/services/promptService.js.map +1 -0
  271. package/backend/dist/services/resourceGrantService.d.ts.map +1 -1
  272. package/backend/dist/services/resourceGrantService.js +62 -17
  273. package/backend/dist/services/resourceGrantService.js.map +1 -1
  274. package/backend/dist/services/searchService.d.ts +29 -0
  275. package/backend/dist/services/searchService.d.ts.map +1 -0
  276. package/backend/dist/services/searchService.js +0 -0
  277. package/backend/dist/services/searchService.js.map +1 -0
  278. package/backend/dist/services/securityAuditService.d.ts.map +1 -1
  279. package/backend/dist/services/securityAuditService.js +2 -1
  280. package/backend/dist/services/securityAuditService.js.map +1 -1
  281. package/backend/dist/services/sharedResourceAccess.d.ts +25 -0
  282. package/backend/dist/services/sharedResourceAccess.d.ts.map +1 -0
  283. package/backend/dist/services/sharedResourceAccess.js +59 -0
  284. package/backend/dist/services/sharedResourceAccess.js.map +1 -0
  285. package/backend/dist/services/skillService.d.ts +137 -0
  286. package/backend/dist/services/skillService.d.ts.map +1 -0
  287. package/backend/dist/services/skillService.js +852 -0
  288. package/backend/dist/services/skillService.js.map +1 -0
  289. package/backend/dist/services/toolAccessService.d.ts +9 -0
  290. package/backend/dist/services/toolAccessService.d.ts.map +1 -0
  291. package/backend/dist/services/toolAccessService.js +60 -0
  292. package/backend/dist/services/toolAccessService.js.map +1 -0
  293. package/backend/dist/services/toolApprovalService.d.ts +34 -0
  294. package/backend/dist/services/toolApprovalService.d.ts.map +1 -0
  295. package/backend/dist/services/toolApprovalService.js +188 -0
  296. package/backend/dist/services/toolApprovalService.js.map +1 -0
  297. package/backend/dist/services/toolGatewayService.d.ts +53 -0
  298. package/backend/dist/services/toolGatewayService.d.ts.map +1 -0
  299. package/backend/dist/services/toolGatewayService.js +249 -0
  300. package/backend/dist/services/toolGatewayService.js.map +1 -0
  301. package/backend/dist/services/toolServerService.d.ts +57 -0
  302. package/backend/dist/services/toolServerService.d.ts.map +1 -0
  303. package/backend/dist/services/toolServerService.js +521 -0
  304. package/backend/dist/services/toolServerService.js.map +1 -0
  305. package/backend/dist/services/voiceAccessService.d.ts +10 -0
  306. package/backend/dist/services/voiceAccessService.d.ts.map +1 -0
  307. package/backend/dist/services/voiceAccessService.js +74 -0
  308. package/backend/dist/services/voiceAccessService.js.map +1 -0
  309. package/backend/dist/services/voiceProfileService.d.ts +20 -0
  310. package/backend/dist/services/voiceProfileService.d.ts.map +1 -1
  311. package/backend/dist/services/voiceProfileService.js +60 -0
  312. package/backend/dist/services/voiceProfileService.js.map +1 -1
  313. package/backend/dist/storage.d.ts +28 -1
  314. package/backend/dist/storage.d.ts.map +1 -1
  315. package/backend/dist/storage.js +100 -1
  316. package/backend/dist/storage.js.map +1 -1
  317. package/backend/dist/storageMappers.d.ts +2 -2
  318. package/backend/dist/storageMappers.d.ts.map +1 -1
  319. package/backend/dist/storageMappers.js.map +1 -1
  320. package/backend/dist/types/index.d.ts +197 -2
  321. package/backend/dist/types/index.d.ts.map +1 -1
  322. package/backend/dist/types/index.js.map +1 -1
  323. package/backend/dist/types/tools.d.ts +137 -0
  324. package/backend/dist/types/tools.d.ts.map +1 -0
  325. package/backend/dist/types/tools.js +25 -0
  326. package/backend/dist/types/tools.js.map +1 -0
  327. package/backend/dist/utils/chatDocumentContext.d.ts +26 -1
  328. package/backend/dist/utils/chatDocumentContext.d.ts.map +1 -1
  329. package/backend/dist/utils/chatDocumentContext.js +87 -18
  330. package/backend/dist/utils/chatDocumentContext.js.map +1 -1
  331. package/backend/dist/utils/documentExtraction.d.ts +69 -0
  332. package/backend/dist/utils/documentExtraction.d.ts.map +1 -0
  333. package/backend/dist/utils/documentExtraction.js +545 -0
  334. package/backend/dist/utils/documentExtraction.js.map +1 -0
  335. package/backend/dist/utils/hybridRetrieval.d.ts +34 -0
  336. package/backend/dist/utils/hybridRetrieval.d.ts.map +1 -0
  337. package/backend/dist/utils/hybridRetrieval.js +119 -0
  338. package/backend/dist/utils/hybridRetrieval.js.map +1 -0
  339. package/backend/dist/utils/icsCalendar.d.ts +27 -0
  340. package/backend/dist/utils/icsCalendar.d.ts.map +1 -0
  341. package/backend/dist/utils/icsCalendar.js +313 -0
  342. package/backend/dist/utils/icsCalendar.js.map +1 -0
  343. package/backend/dist/utils/imageEditUpload.d.ts +29 -0
  344. package/backend/dist/utils/imageEditUpload.d.ts.map +1 -0
  345. package/backend/dist/utils/imageEditUpload.js +82 -0
  346. package/backend/dist/utils/imageEditUpload.js.map +1 -0
  347. package/backend/dist/utils/logRedaction.d.ts +2 -0
  348. package/backend/dist/utils/logRedaction.d.ts.map +1 -0
  349. package/backend/dist/utils/logRedaction.js +66 -0
  350. package/backend/dist/utils/logRedaction.js.map +1 -0
  351. package/backend/dist/utils/logger.d.ts +3 -0
  352. package/backend/dist/utils/logger.d.ts.map +1 -1
  353. package/backend/dist/utils/logger.js +63 -20
  354. package/backend/dist/utils/logger.js.map +1 -1
  355. package/backend/dist/utils/pluginChatAdapter.d.ts +5 -1
  356. package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
  357. package/backend/dist/utils/pluginChatAdapter.js +83 -0
  358. package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
  359. package/backend/dist/utils/pluginDefinitionTrust.js +1 -1
  360. package/backend/dist/utils/resourceLimits.d.ts +61 -2
  361. package/backend/dist/utils/resourceLimits.d.ts.map +1 -1
  362. package/backend/dist/utils/resourceLimits.js +59 -0
  363. package/backend/dist/utils/resourceLimits.js.map +1 -1
  364. package/backend/dist/utils/toolEgress.d.ts +22 -0
  365. package/backend/dist/utils/toolEgress.d.ts.map +1 -0
  366. package/backend/dist/utils/toolEgress.js +157 -0
  367. package/backend/dist/utils/toolEgress.js.map +1 -0
  368. package/backend/dist/utils/websocketMessages.d.ts +6 -0
  369. package/backend/dist/utils/websocketMessages.d.ts.map +1 -1
  370. package/backend/dist/utils/websocketMessages.js +8 -0
  371. package/backend/dist/utils/websocketMessages.js.map +1 -1
  372. package/backend/dist/utils/zipArchive.d.ts +32 -0
  373. package/backend/dist/utils/zipArchive.d.ts.map +1 -0
  374. package/backend/dist/utils/zipArchive.js +171 -0
  375. package/backend/dist/utils/zipArchive.js.map +1 -0
  376. package/backend/dist/websocketServer.d.ts.map +1 -1
  377. package/backend/dist/websocketServer.js +142 -35
  378. package/backend/dist/websocketServer.js.map +1 -1
  379. package/backend/package.json +1 -1
  380. package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
  381. package/frontend/dist/assets/index-BPsH9FII.js +80 -0
  382. package/frontend/dist/assets/index-BtXF0Oxa.css +1 -0
  383. package/frontend/dist/index.html +18 -18
  384. package/frontend/dist/js/{ArtifactContainer-CpUi6ekF.js → ArtifactContainer-CIoFoMv8.js} +1 -1
  385. package/frontend/dist/js/{ArtifactDemoPage-BeNs3gHp.js → ArtifactDemoPage-D0JEIAbZ.js} +1 -1
  386. package/frontend/dist/js/{ArtifactSandboxFrame-qsj_Y3LV.js → ArtifactSandboxFrame-REazyIrx.js} +2 -2
  387. package/frontend/dist/js/ArtifactSlideOutPanel-A-i94ghF.js +1 -0
  388. package/frontend/dist/js/AutomationsPage-Dow_lbIf.js +1 -0
  389. package/frontend/dist/js/{Button-BPf_pOxH.js → Button-C0r3J9G_.js} +1 -1
  390. package/frontend/dist/js/CalendarPage-DLjasZOd.js +1 -0
  391. package/frontend/dist/js/ChannelsPage-C8hvTD7J.js +1 -0
  392. package/frontend/dist/js/ChatPage-BAgBHUES.js +16 -0
  393. package/frontend/dist/js/EvaluationsPage-Bm8FRoeX.js +2 -0
  394. package/frontend/dist/js/{FirstTimeSetup-Df0YJni3.js → FirstTimeSetup-BxJapNul.js} +1 -1
  395. package/frontend/dist/js/GalleryPage-DHz9DkK7.js +1 -0
  396. package/frontend/dist/js/HomePage-BqNy-FWX.js +1 -0
  397. package/frontend/dist/js/LibreClawPage-BDzDILjY.js +1 -0
  398. package/frontend/dist/js/{ModelManagerModals-DcLGYN3l.js → ModelManagerModals-DX9eT5r2.js} +1 -1
  399. package/frontend/dist/js/ModelSelector-ol8mK2ja.js +1 -0
  400. package/frontend/dist/js/NotesPage-DzHEx2O4.js +3 -0
  401. package/frontend/dist/js/{OptimizedSyntaxHighlighter-Dz7D1LPB.js → OptimizedSyntaxHighlighter-CZNPwN0i.js} +1 -1
  402. package/frontend/dist/js/PersonasPage-M_WKH2a9.js +13 -0
  403. package/frontend/dist/js/PluginUsagePage-Ctoka6JZ.js +1 -0
  404. package/frontend/dist/js/RichMessageContent-BqgafH4p.js +3 -0
  405. package/frontend/dist/js/SettingsModal-R9EDadAV.js +19 -0
  406. package/frontend/dist/js/{SettingsToggle-woG_WebF.js → SettingsToggle-BIVyfGjl.js} +1 -1
  407. package/frontend/dist/js/SystemPage-aOkrSuY7.js +1 -0
  408. package/frontend/dist/js/UserManagementPage-BOuaX3ay.js +1 -0
  409. package/frontend/dist/js/WorkPage-DxZISerQ.js +6 -0
  410. package/frontend/dist/js/api-6bSMtSNh.js +14 -0
  411. package/frontend/dist/js/{appStore-0JnCUlEq.js → appStore-CdObhQBb.js} +2 -2
  412. package/frontend/dist/js/ar-DT_VLBO3.js +2 -0
  413. package/frontend/dist/js/{authStore-DbMdvNgJ.js → authStore-DbqqrfZv.js} +3 -3
  414. package/frontend/dist/js/{babel-BsYoxD-7.js → babel-DSqqk5kt.js} +1 -1
  415. package/frontend/dist/js/bn-BrvEqhGA.js +2 -0
  416. package/frontend/dist/js/chatStore-DbZPJNsu.js +3 -0
  417. package/frontend/dist/js/{config-DSOYQ9a3.js → config-8KdgjtEx.js} +1 -1
  418. package/frontend/dist/js/cs-BthnM_YI.js +2 -0
  419. package/frontend/dist/js/da-DCth-6_j.js +2 -0
  420. package/frontend/dist/js/de-BBWt0gJ_.js +2 -0
  421. package/frontend/dist/js/es-DuslOPqM.js +2 -0
  422. package/frontend/dist/js/{estree-BcnBcOAx.js → estree-BU5-LV_1.js} +3 -3
  423. package/frontend/dist/js/evaluationsApi-CwZJsgeG.js +1 -0
  424. package/frontend/dist/js/fr-DZAeg46S.js +2 -0
  425. package/frontend/dist/js/hi-ijDSiY77.js +2 -0
  426. package/frontend/dist/js/{html-DkY55Jys.js → html-DeoAy2bo.js} +2 -2
  427. package/frontend/dist/js/id-BS6qS0oq.js +2 -0
  428. package/frontend/dist/js/is-CO2PMDte.js +2 -0
  429. package/frontend/dist/js/it-ChhxT4hs.js +2 -0
  430. package/frontend/dist/js/ja-cyeqUeJN.js +2 -0
  431. package/frontend/dist/js/ko-CfqHTLXj.js +2 -0
  432. package/frontend/dist/js/{markdown-IZSI9DXw.js → markdown-DOLsYkBY.js} +3 -3
  433. package/frontend/dist/js/markdown-core-BK7WVOb7.js +32 -0
  434. package/frontend/dist/js/{markdown-math-DK846gXU.js → markdown-math-e1ByS3KY.js} +5 -5
  435. package/frontend/dist/js/{messageCodeStyles-BFNR9Wv6.js → messageCodeStyles-DhVgizF-.js} +2 -2
  436. package/frontend/dist/js/ms-DZ5pACe1.js +2 -0
  437. package/frontend/dist/js/nl-BsRsnOVp.js +2 -0
  438. package/frontend/dist/js/pl-DXSTAW4M.js +2 -0
  439. package/frontend/dist/js/{postcss-BJ0okz7n.js → postcss-Bg15EcFE.js} +1 -1
  440. package/frontend/dist/js/pt-DmDx0JxB.js +2 -0
  441. package/frontend/dist/js/{react-vendor-C5yShNlM.js → react-vendor-BG_HPV6X.js} +1 -1
  442. package/frontend/dist/js/router-vendor-CbexJIsv.js +1 -0
  443. package/frontend/dist/js/ru-d2g8EWfY.js +2 -0
  444. package/frontend/dist/js/{standalone-Csw3jfJ_.js → standalone-BO5pzI8_.js} +3 -3
  445. package/frontend/dist/js/sv-CIhYUTVQ.js +2 -0
  446. package/frontend/dist/js/{syntax-highlight-3EEX6CGL.js → syntax-highlight-DcR9Vhb7.js} +3 -3
  447. package/frontend/dist/js/th-B3wSxanB.js +2 -0
  448. package/frontend/dist/js/tr-R4UGgPOY.js +2 -0
  449. package/frontend/dist/js/{typescript-CnF4UVx7.js → typescript-6GJ8APmn.js} +1 -1
  450. package/frontend/dist/js/ui-CV0LYmkM.js +2 -0
  451. package/frontend/dist/js/ui-vendor-CWjIBW-z.js +177 -0
  452. package/frontend/dist/js/uk-BzUF4Vkt.js +2 -0
  453. package/frontend/dist/js/{useTranslation-Bho4HCyg.js → useTranslation-5b5UhAc9.js} +1 -1
  454. package/frontend/dist/js/{utils-D9W1Lnkn.js → utils-BGrdX9ie.js} +1 -1
  455. package/frontend/dist/js/{utils-vendor-DWh39_3x.js → utils-vendor-9bQ62WeD.js} +1 -1
  456. package/frontend/dist/js/vi-CKc6Fg4h.js +2 -0
  457. package/frontend/dist/js/{workApi-C3uy9EDT.js → workApi-BruWTlHM.js} +3 -3
  458. package/frontend/dist/js/{yaml-DzT5hBSH.js → yaml-DUeWcOOH.js} +2 -2
  459. package/frontend/dist/js/zh-CXL-kDe8.js +2 -0
  460. package/package.json +2 -2
  461. package/plugins/openai.json +52 -9
  462. package/frontend/dist/assets/index-6o-Hc2wS.js +0 -41
  463. package/frontend/dist/assets/index-D-pM957b.css +0 -1
  464. package/frontend/dist/js/ArtifactSlideOutPanel-CSFRluWA.js +0 -1
  465. package/frontend/dist/js/AutomationsPage-KUeWoZNv.js +0 -1
  466. package/frontend/dist/js/CalendarPage-D9I972w8.js +0 -1
  467. package/frontend/dist/js/ChatPage-CmBx4oAL.js +0 -16
  468. package/frontend/dist/js/GalleryPage-bLCrdJN_.js +0 -1
  469. package/frontend/dist/js/HomePage-Uc6PEg-U.js +0 -1
  470. package/frontend/dist/js/LibreClawPage-CG63DHg1.js +0 -1
  471. package/frontend/dist/js/ModelSelector-yxJC64rM.js +0 -1
  472. package/frontend/dist/js/ModelsPage-1R_TxgmJ.js +0 -2
  473. package/frontend/dist/js/NotesPage-L65j5HoW.js +0 -1
  474. package/frontend/dist/js/PersonasPage-CKeBpHIH.js +0 -13
  475. package/frontend/dist/js/PluginUsagePage-BX1SKMbW.js +0 -1
  476. package/frontend/dist/js/RichMessageContent-DiySVYGB.js +0 -3
  477. package/frontend/dist/js/SettingsModal-68SG1uZ2.js +0 -1
  478. package/frontend/dist/js/SystemPage-CytpZiQV.js +0 -1
  479. package/frontend/dist/js/UserManagementPage-BzJBvZiS.js +0 -1
  480. package/frontend/dist/js/WorkPage-C8gr0QJe.js +0 -6
  481. package/frontend/dist/js/api-BDtqBtqw.js +0 -14
  482. package/frontend/dist/js/ar-CSpi37Gr.js +0 -2
  483. package/frontend/dist/js/bn-CyH1BeDD.js +0 -2
  484. package/frontend/dist/js/chatStore-CHQ2uINS.js +0 -3
  485. package/frontend/dist/js/cs-Cv2NfRK5.js +0 -2
  486. package/frontend/dist/js/da-D6D6IvPK.js +0 -2
  487. package/frontend/dist/js/de-BNoJJTrT.js +0 -2
  488. package/frontend/dist/js/es-kzo9C7q_.js +0 -2
  489. package/frontend/dist/js/fr-CjHYvJRp.js +0 -2
  490. package/frontend/dist/js/hi-BU98AdCQ.js +0 -2
  491. package/frontend/dist/js/id-CpdedejX.js +0 -2
  492. package/frontend/dist/js/is-Cp5dDWBH.js +0 -2
  493. package/frontend/dist/js/it-CuH_cK4U.js +0 -2
  494. package/frontend/dist/js/ja-BWXXXiHn.js +0 -2
  495. package/frontend/dist/js/ko-DQgv2M1t.js +0 -2
  496. package/frontend/dist/js/markdown-core-BmPPZiuD.js +0 -31
  497. package/frontend/dist/js/ms-DCcRoIcX.js +0 -2
  498. package/frontend/dist/js/nl-CbKR1_EU.js +0 -2
  499. package/frontend/dist/js/pl-CRyJZccn.js +0 -2
  500. package/frontend/dist/js/pt-BXYmHFGH.js +0 -2
  501. package/frontend/dist/js/router-vendor-DGFGM3EP.js +0 -1
  502. package/frontend/dist/js/ru-M9SyZG4b.js +0 -2
  503. package/frontend/dist/js/sv-DJ486LTU.js +0 -2
  504. package/frontend/dist/js/th-Ch7gKtZv.js +0 -2
  505. package/frontend/dist/js/tr-ClehWhSC.js +0 -2
  506. package/frontend/dist/js/ui-CC-K19wO.js +0 -2
  507. package/frontend/dist/js/ui-vendor-B1UgO3vd.js +0 -177
  508. package/frontend/dist/js/uk-BqAd5zHF.js +0 -2
  509. package/frontend/dist/js/vi-D9gjlDVB.js +0 -2
  510. package/frontend/dist/js/zh-DKTyHkiR.js +0 -2
@@ -0,0 +1,1057 @@
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
+ * Channels (CHANNEL-01/02): public, private, and direct-message
19
+ * conversations with a persistent, idempotent, ordered timeline plus
20
+ * threads, reactions, pins, and per-member unread cursors.
21
+ *
22
+ * SQL is authoritative for every row; the durable event ledger only fans
23
+ * the same facts out to live subscribers (stream `channel:<id>`), so a
24
+ * missed event is recovered by reading the timeline, never replayed into
25
+ * divergence. Message and channel text is encrypted at rest with the same
26
+ * boundary as chat messages. Deletion is a tombstone: content is cleared
27
+ * but the timeline entry survives so threads never dangle.
28
+ */
29
+ import { randomUUID } from 'node:crypto';
30
+ import { Readable } from 'node:stream';
31
+ import { getPersistence } from '../persistence/index.js';
32
+ import { PersistenceResourceLimitError, } from '../persistence/resourceTypes.js';
33
+ import { encryptionService } from './encryptionService.js';
34
+ import { userModel } from '../models/userModel.js';
35
+ import { getDurableEventGateway } from '../platform/events/index.js';
36
+ import { channelEventStreamId, CHANNEL_MENTION_IDEMPOTENCY_SCOPE, CHANNEL_MENTION_JOB_TYPE, } from '../platform/jobs/domainJobContracts.js';
37
+ import { getDurableJobRuntime } from '../platform/jobs/durableJobRuntime.js';
38
+ import { getCoordinator } from '../platform/coordination/service.js';
39
+ import { getPlatformStorageRuntime } from '../platform/storage/index.js';
40
+ import { createLogger } from '../utils/logger.js';
41
+ import { MAX_CHANNEL_ATTACHMENT_BYTES, MAX_CHANNEL_ATTACHMENTS_PER_MESSAGE, MAX_CHANNEL_MENTION_CONTEXT_MESSAGES, MAX_CHANNEL_DESCRIPTION_LENGTH, MAX_CHANNEL_EMOJI_LENGTH, MAX_CHANNEL_MEMBERS, MAX_CHANNEL_MESSAGE_LENGTH, MAX_CHANNEL_MESSAGES, MAX_CHANNEL_NAME_LENGTH, MAX_CHANNEL_PAGE_SIZE, MAX_CHANNEL_PINNED_LISTED, MAX_CHANNEL_REACTIONS_PER_MESSAGE, MAX_CHANNEL_THREAD_PAGE_SIZE, MAX_CHANNELS_LISTED, MAX_CHANNELS_PER_USER, } from '../utils/resourceLimits.js';
42
+ const logger = createLogger('channels');
43
+ export class ChannelError extends Error {
44
+ constructor(message, statusCode) {
45
+ super(message);
46
+ this.statusCode = statusCode;
47
+ this.name = 'ChannelError';
48
+ }
49
+ }
50
+ const repositories = () => getPersistence(encryptionService).repositories.resources;
51
+ const channels = () => repositories().channels;
52
+ const messages = () => repositories().channelMessages;
53
+ const notFound = () => new ChannelError('Channel not found', 404);
54
+ export const CHANNEL_ATTACHMENT_BLOB_PURPOSE = 'channel-attachment';
55
+ const PENDING_ATTACHMENT_TTL_MS = 15 * 60 * 1000;
56
+ const pendingAttachmentKey = (userId, attachmentId) => `channel-upload:${userId}:${attachmentId}`;
57
+ const isChannelType = (value) => value === 'public' || value === 'private' || value === 'dm';
58
+ /** Deterministic identity for a DM pair, order-independent. */
59
+ export const dmKeyFor = (left, right) => ['dm', ...[left, right].sort()].join(':');
60
+ const decryptOptional = (value) => {
61
+ if (!value)
62
+ return undefined;
63
+ try {
64
+ return encryptionService.decrypt(value);
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ };
70
+ const mapChannel = (row) => ({
71
+ id: row.id,
72
+ type: row.type,
73
+ name: row.name ? (decryptOptional(row.name) ?? '') : '',
74
+ ...(row.description ? { description: decryptOptional(row.description) } : {}),
75
+ createdBy: row.created_by,
76
+ createdAt: row.created_at,
77
+ updatedAt: row.updated_at,
78
+ ...(row.archived_at ? { archivedAt: row.archived_at } : {}),
79
+ });
80
+ const readMetadata = (row) => {
81
+ if (!row.metadata)
82
+ return {};
83
+ try {
84
+ const decoded = JSON.parse(encryptionService.decrypt(row.metadata));
85
+ return {
86
+ ...(decoded.pending === true ? { pending: true } : {}),
87
+ ...(typeof decoded.error === 'string' ? { error: decoded.error } : {}),
88
+ };
89
+ }
90
+ catch {
91
+ return {};
92
+ }
93
+ };
94
+ const mapMessage = (row, context) => {
95
+ const deleted = row.deleted_at !== null;
96
+ const metadata = deleted ? {} : readMetadata(row);
97
+ return {
98
+ id: row.id,
99
+ channelId: row.channel_id,
100
+ ...(row.parent_id ? { parentId: row.parent_id } : {}),
101
+ authorKind: row.author_kind === 'model' ? 'model' : 'user',
102
+ ...(row.model ? { model: row.model } : {}),
103
+ author: row.user_id
104
+ ? {
105
+ userId: row.user_id,
106
+ username: context.usernames.get(row.user_id) ?? row.user_id,
107
+ }
108
+ : null,
109
+ content: deleted ? '' : (decryptOptional(row.content) ?? ''),
110
+ createdAt: row.created_at,
111
+ updatedAt: row.updated_at,
112
+ ...(row.edited_at ? { editedAt: row.edited_at } : {}),
113
+ ...(deleted ? { deleted: true } : {}),
114
+ ...(row.pinned_at ? { pinnedAt: row.pinned_at } : {}),
115
+ ...(context.replyCounts[row.id]
116
+ ? { replyCount: context.replyCounts[row.id] }
117
+ : {}),
118
+ ...(context.reactions.has(row.id)
119
+ ? { reactions: context.reactions.get(row.id) }
120
+ : {}),
121
+ ...(context.attachments.has(row.id)
122
+ ? { attachments: context.attachments.get(row.id) }
123
+ : {}),
124
+ ...metadata,
125
+ };
126
+ };
127
+ const resolveUsernames = async (userIds) => {
128
+ const names = new Map();
129
+ for (const userId of new Set(userIds)) {
130
+ try {
131
+ const user = await userModel.getUserById(userId);
132
+ if (user)
133
+ names.set(userId, user.username);
134
+ }
135
+ catch {
136
+ // A deleted account renders as its identifier.
137
+ }
138
+ }
139
+ return names;
140
+ };
141
+ /** Live fan-out is best-effort; SQL rows are the recovery path. */
142
+ const appendChannelEvent = async (channelId, eventType, subjectId, actorUserId, payload) => {
143
+ try {
144
+ await getDurableEventGateway().append({
145
+ eventId: randomUUID(),
146
+ streamId: channelEventStreamId(channelId),
147
+ eventType,
148
+ subjectId,
149
+ actorUserId,
150
+ payload: { mode: 'encrypted', value: payload },
151
+ });
152
+ }
153
+ catch (error) {
154
+ logger.warn('Channel event fan-out failed; timeline reads recover', {
155
+ channelId,
156
+ eventType,
157
+ error,
158
+ });
159
+ }
160
+ };
161
+ const buildViewContext = async (actorUserId, rows) => {
162
+ const messageIds = rows.map(row => row.id);
163
+ const rootIds = rows.filter(row => !row.parent_id).map(row => row.id);
164
+ const [replyCounts, reactionRows, attachmentRows] = await Promise.all([
165
+ rootIds.length > 0
166
+ ? messages().countThreadReplies(rootIds)
167
+ : Promise.resolve({}),
168
+ messages().listReactionsForMessages(messageIds),
169
+ messages().listAttachmentsForMessages(messageIds),
170
+ ]);
171
+ const reactions = new Map();
172
+ for (const reaction of reactionRows) {
173
+ const list = reactions.get(reaction.message_id) ?? [];
174
+ const existing = list.find(entry => entry.emoji === reaction.emoji);
175
+ if (existing) {
176
+ existing.count += 1;
177
+ if (reaction.user_id === actorUserId)
178
+ existing.mine = true;
179
+ }
180
+ else {
181
+ list.push({
182
+ emoji: reaction.emoji,
183
+ count: 1,
184
+ mine: reaction.user_id === actorUserId,
185
+ });
186
+ reactions.set(reaction.message_id, list);
187
+ }
188
+ }
189
+ const attachments = new Map();
190
+ for (const attachment of attachmentRows) {
191
+ const list = attachments.get(attachment.message_id) ?? [];
192
+ list.push({
193
+ id: attachment.id,
194
+ filename: attachment.filename,
195
+ contentType: attachment.content_type,
196
+ size: attachment.size,
197
+ });
198
+ attachments.set(attachment.message_id, list);
199
+ }
200
+ const usernames = await resolveUsernames(rows.flatMap(row => (row.user_id ? [row.user_id] : [])));
201
+ return { actorUserId, usernames, replyCounts, reactions, attachments };
202
+ };
203
+ const mapRows = async (actorUserId, rows) => {
204
+ const context = await buildViewContext(actorUserId, rows);
205
+ return rows.map(row => mapMessage(row, context));
206
+ };
207
+ export class ChannelService {
208
+ /** Membership gate used by every channel read and write. */
209
+ async requireMember(channelId, userId) {
210
+ const channel = await channels().findById(channelId);
211
+ if (!channel)
212
+ throw notFound();
213
+ const member = await channels().findMember(channelId, userId);
214
+ if (!member)
215
+ throw notFound();
216
+ return { channel, member };
217
+ }
218
+ async createChannel(actor, input) {
219
+ if (!isChannelType(input.type) || input.type === 'dm') {
220
+ throw new ChannelError('Channel type must be public or private', 400);
221
+ }
222
+ const name = (input.name ?? '').trim();
223
+ if (!name || name.length > MAX_CHANNEL_NAME_LENGTH) {
224
+ throw new ChannelError('Channel name is required', 400);
225
+ }
226
+ if ((input.description?.length ?? 0) > MAX_CHANNEL_DESCRIPTION_LENGTH) {
227
+ throw new ChannelError('Channel description is too long', 400);
228
+ }
229
+ const now = Date.now();
230
+ const channel = {
231
+ id: randomUUID(),
232
+ type: input.type,
233
+ name: encryptionService.encrypt(name),
234
+ description: input.description
235
+ ? encryptionService.encrypt(input.description)
236
+ : null,
237
+ dm_key: null,
238
+ created_by: actor.userId,
239
+ created_at: now,
240
+ updated_at: now,
241
+ archived_at: null,
242
+ };
243
+ try {
244
+ await channels().insertWithOwner(channel, {
245
+ channel_id: channel.id,
246
+ user_id: actor.userId,
247
+ role: 'owner',
248
+ joined_at: now,
249
+ last_read_at: now,
250
+ }, MAX_CHANNELS_PER_USER);
251
+ }
252
+ catch (error) {
253
+ if (error instanceof PersistenceResourceLimitError) {
254
+ throw new ChannelError(`A user may create at most ${MAX_CHANNELS_PER_USER} channels`, 409);
255
+ }
256
+ throw error;
257
+ }
258
+ for (const memberId of new Set(input.memberIds ?? [])) {
259
+ if (memberId === actor.userId)
260
+ continue;
261
+ await this.addMember({ userId: actor.userId }, channel.id, memberId);
262
+ }
263
+ return mapChannel(channel);
264
+ }
265
+ /** Opens (or returns) the deterministic DM channel between two users. */
266
+ async openDm(actor, peerUserId) {
267
+ if (!peerUserId || peerUserId === actor.userId) {
268
+ throw new ChannelError('A direct message needs another user', 400);
269
+ }
270
+ const peer = await userModel.getUserById(peerUserId);
271
+ if (!peer)
272
+ throw new ChannelError('User not found', 404);
273
+ const dmKey = dmKeyFor(actor.userId, peerUserId);
274
+ const existing = await channels().findByDmKey(dmKey);
275
+ if (existing) {
276
+ // Re-attach either participant who previously left.
277
+ const now = Date.now();
278
+ for (const userId of [actor.userId, peerUserId]) {
279
+ if (!(await channels().findMember(existing.id, userId))) {
280
+ await channels().upsertMember({
281
+ channel_id: existing.id,
282
+ user_id: userId,
283
+ role: 'member',
284
+ joined_at: now,
285
+ last_read_at: userId === actor.userId ? now : 0,
286
+ }, MAX_CHANNEL_MEMBERS);
287
+ }
288
+ }
289
+ return mapChannel(existing);
290
+ }
291
+ const now = Date.now();
292
+ const channel = {
293
+ id: randomUUID(),
294
+ type: 'dm',
295
+ name: encryptionService.encrypt(''),
296
+ description: null,
297
+ dm_key: dmKey,
298
+ created_by: actor.userId,
299
+ created_at: now,
300
+ updated_at: now,
301
+ archived_at: null,
302
+ };
303
+ await channels().insertWithOwner(channel, {
304
+ channel_id: channel.id,
305
+ user_id: actor.userId,
306
+ role: 'member',
307
+ joined_at: now,
308
+ last_read_at: now,
309
+ }, MAX_CHANNELS_PER_USER);
310
+ await channels().upsertMember({
311
+ channel_id: channel.id,
312
+ user_id: peerUserId,
313
+ role: 'member',
314
+ joined_at: now,
315
+ last_read_at: 0,
316
+ }, MAX_CHANNEL_MEMBERS);
317
+ return mapChannel(channel);
318
+ }
319
+ /** The actor's channels with unread counts, newest activity first. */
320
+ async listMine(actor) {
321
+ const memberships = await channels().listForUser(actor.userId, MAX_CHANNELS_LISTED);
322
+ const unread = await channels().unreadSummaryForUser(actor.userId);
323
+ const unreadByChannel = new Map(unread.map(row => [row.channel_id, row]));
324
+ const summaries = [];
325
+ for (const { channel, member } of memberships) {
326
+ const summary = {
327
+ ...mapChannel(channel),
328
+ role: member.role,
329
+ isMember: true,
330
+ lastReadAt: member.last_read_at,
331
+ unreadCount: unreadByChannel.get(channel.id)?.unread_count ?? 0,
332
+ latestMessageAt: unreadByChannel.get(channel.id)?.latest_message_at ?? null,
333
+ };
334
+ if (channel.type === 'dm') {
335
+ const members = await channels().listMembers(channel.id, 4);
336
+ const peer = members.find(entry => entry.user_id !== actor.userId);
337
+ if (peer) {
338
+ const names = await resolveUsernames([peer.user_id]);
339
+ summary.dmPeer = {
340
+ userId: peer.user_id,
341
+ username: names.get(peer.user_id) ?? peer.user_id,
342
+ };
343
+ }
344
+ }
345
+ summaries.push(summary);
346
+ }
347
+ return summaries;
348
+ }
349
+ /** Browseable public channels with membership state. */
350
+ async listPublic(actor) {
351
+ const rows = await channels().listPublic(MAX_CHANNELS_LISTED);
352
+ const summaries = [];
353
+ for (const row of rows) {
354
+ const member = await channels().findMember(row.id, actor.userId);
355
+ summaries.push({
356
+ ...mapChannel(row),
357
+ isMember: Boolean(member),
358
+ ...(member ? { role: member.role } : {}),
359
+ });
360
+ }
361
+ return summaries;
362
+ }
363
+ async getChannel(actor, channelId) {
364
+ const channel = await channels().findById(channelId);
365
+ if (!channel)
366
+ throw notFound();
367
+ const member = await channels().findMember(channelId, actor.userId);
368
+ if (!member && channel.type !== 'public')
369
+ throw notFound();
370
+ const summary = {
371
+ ...mapChannel(channel),
372
+ isMember: Boolean(member),
373
+ ...(member ? { role: member.role } : {}),
374
+ ...(member ? { lastReadAt: member.last_read_at } : {}),
375
+ };
376
+ if (channel.type === 'dm' && member) {
377
+ const members = await channels().listMembers(channel.id, 4);
378
+ const peer = members.find(entry => entry.user_id !== actor.userId);
379
+ if (peer) {
380
+ const names = await resolveUsernames([peer.user_id]);
381
+ summary.dmPeer = {
382
+ userId: peer.user_id,
383
+ username: names.get(peer.user_id) ?? peer.user_id,
384
+ };
385
+ }
386
+ }
387
+ return summary;
388
+ }
389
+ async updateChannel(actor, channelId, updates) {
390
+ const { channel, member } = await this.requireMember(channelId, actor.userId);
391
+ if (member.role !== 'owner')
392
+ throw notFound();
393
+ if (channel.type === 'dm') {
394
+ throw new ChannelError('Direct messages cannot be renamed', 400);
395
+ }
396
+ const next = { ...channel, updated_at: Date.now() };
397
+ if (updates.name !== undefined) {
398
+ const name = updates.name.trim();
399
+ if (!name || name.length > MAX_CHANNEL_NAME_LENGTH) {
400
+ throw new ChannelError('Channel name is required', 400);
401
+ }
402
+ next.name = encryptionService.encrypt(name);
403
+ }
404
+ if (updates.description !== undefined) {
405
+ if (updates.description.length > MAX_CHANNEL_DESCRIPTION_LENGTH) {
406
+ throw new ChannelError('Channel description is too long', 400);
407
+ }
408
+ next.description = updates.description
409
+ ? encryptionService.encrypt(updates.description)
410
+ : null;
411
+ }
412
+ if (updates.archived !== undefined) {
413
+ next.archived_at = updates.archived ? Date.now() : null;
414
+ }
415
+ await channels().update(next);
416
+ await appendChannelEvent(channelId, 'channel.updated.v1', channelId, actor.userId, {
417
+ type: 'channel_updated',
418
+ channelId,
419
+ });
420
+ return mapChannel(next);
421
+ }
422
+ async deleteChannel(actor, channelId) {
423
+ const { channel, member } = await this.requireMember(channelId, actor.userId);
424
+ if (channel.type !== 'dm' && member.role !== 'owner')
425
+ throw notFound();
426
+ const blobs = await messages().listAttachmentBlobIds(channelId);
427
+ await channels().delete(channelId);
428
+ if (blobs.length > 0) {
429
+ try {
430
+ const { getPlatformStorageRuntime } = await import('../platform/storage/index.js');
431
+ const blobStore = getPlatformStorageRuntime().blobStore;
432
+ for (const blob of blobs) {
433
+ if (!blob.created_by)
434
+ continue;
435
+ await blobStore
436
+ .delete({ id: blob.blob_id, ownerUserId: blob.created_by })
437
+ .catch(() => undefined);
438
+ }
439
+ }
440
+ catch (error) {
441
+ logger.warn('Channel attachment blob cleanup failed', { error });
442
+ }
443
+ }
444
+ }
445
+ async addMember(actor, channelId, userId) {
446
+ const { channel, member } = await this.requireMember(channelId, actor.userId);
447
+ if (channel.type === 'dm') {
448
+ throw new ChannelError('Direct messages are limited to two people', 400);
449
+ }
450
+ if (channel.type === 'private' && member.role !== 'owner') {
451
+ throw notFound();
452
+ }
453
+ const user = await userModel.getUserById(userId);
454
+ if (!user)
455
+ throw new ChannelError('User not found', 404);
456
+ try {
457
+ await channels().upsertMember({
458
+ channel_id: channelId,
459
+ user_id: userId,
460
+ role: 'member',
461
+ joined_at: Date.now(),
462
+ last_read_at: 0,
463
+ }, MAX_CHANNEL_MEMBERS);
464
+ }
465
+ catch (error) {
466
+ if (error instanceof PersistenceResourceLimitError) {
467
+ throw new ChannelError(`A channel may have at most ${MAX_CHANNEL_MEMBERS} members`, 409);
468
+ }
469
+ throw error;
470
+ }
471
+ await appendChannelEvent(channelId, 'channel.member.v1', userId, actor.userId, {
472
+ type: 'member_added',
473
+ channelId,
474
+ userId,
475
+ });
476
+ return {
477
+ userId,
478
+ username: user.username,
479
+ role: 'member',
480
+ joinedAt: Date.now(),
481
+ };
482
+ }
483
+ /** Join a public channel. */
484
+ async join(actor, channelId) {
485
+ const channel = await channels().findById(channelId);
486
+ if (!channel || channel.type !== 'public' || channel.archived_at) {
487
+ throw notFound();
488
+ }
489
+ try {
490
+ await channels().upsertMember({
491
+ channel_id: channelId,
492
+ user_id: actor.userId,
493
+ role: 'member',
494
+ joined_at: Date.now(),
495
+ last_read_at: Date.now(),
496
+ }, MAX_CHANNEL_MEMBERS);
497
+ }
498
+ catch (error) {
499
+ if (error instanceof PersistenceResourceLimitError) {
500
+ throw new ChannelError(`A channel may have at most ${MAX_CHANNEL_MEMBERS} members`, 409);
501
+ }
502
+ throw error;
503
+ }
504
+ await appendChannelEvent(channelId, 'channel.member.v1', actor.userId, actor.userId, { type: 'member_added', channelId, userId: actor.userId });
505
+ }
506
+ async removeMember(actor, channelId, userId) {
507
+ const { channel, member } = await this.requireMember(channelId, actor.userId);
508
+ const leaving = userId === actor.userId;
509
+ if (!leaving && member.role !== 'owner')
510
+ throw notFound();
511
+ if (channel.type === 'dm' && !leaving) {
512
+ throw new ChannelError('Direct-message members cannot be removed', 400);
513
+ }
514
+ if (leaving && member.role === 'owner') {
515
+ throw new ChannelError('The channel owner must delete the channel instead of leaving it', 400);
516
+ }
517
+ const removed = await channels().removeMember(channelId, userId);
518
+ if (!removed)
519
+ throw notFound();
520
+ await appendChannelEvent(channelId, 'channel.member.v1', userId, actor.userId, {
521
+ type: 'member_removed',
522
+ channelId,
523
+ userId,
524
+ });
525
+ }
526
+ async listMembers(actor, channelId) {
527
+ await this.requireMember(channelId, actor.userId);
528
+ const rows = await channels().listMembers(channelId, MAX_CHANNEL_MEMBERS);
529
+ const usernames = await resolveUsernames(rows.map(row => row.user_id));
530
+ return rows.map(row => ({
531
+ userId: row.user_id,
532
+ username: usernames.get(row.user_id) ?? row.user_id,
533
+ role: row.role,
534
+ joinedAt: row.joined_at,
535
+ }));
536
+ }
537
+ /**
538
+ * Idempotent post: the client supplies the message id, so a retried
539
+ * request lands on the same timeline entry and fans out exactly once.
540
+ */
541
+ async postMessage(actor, channelId, input) {
542
+ const { channel } = await this.requireMember(channelId, actor.userId);
543
+ if (channel.archived_at) {
544
+ throw new ChannelError('This channel is archived', 409);
545
+ }
546
+ const content = input.content ?? '';
547
+ // Pending model replies legitimately start empty; user posts need
548
+ // either text or an attachment.
549
+ if (!content.trim() &&
550
+ (input.attachments?.length ?? 0) === 0 &&
551
+ input.authorKind !== 'model') {
552
+ throw new ChannelError('Message content is required', 400);
553
+ }
554
+ if (content.length > MAX_CHANNEL_MESSAGE_LENGTH) {
555
+ throw new ChannelError('Message content is too long', 400);
556
+ }
557
+ if (input.parentId) {
558
+ const parent = await messages().findById(input.parentId);
559
+ if (!parent || parent.channel_id !== channelId)
560
+ throw notFound();
561
+ if (parent.parent_id) {
562
+ throw new ChannelError('Threads are one level deep', 400);
563
+ }
564
+ }
565
+ const now = Date.now();
566
+ const record = {
567
+ id: input.id ?? randomUUID(),
568
+ channel_id: channelId,
569
+ user_id: actor.userId,
570
+ parent_id: input.parentId ?? null,
571
+ author_kind: input.authorKind === 'model' ? 'model' : 'user',
572
+ model: input.model ?? null,
573
+ content: encryptionService.encrypt(content),
574
+ metadata: input.metadata
575
+ ? encryptionService.encrypt(JSON.stringify(input.metadata))
576
+ : null,
577
+ created_at: now,
578
+ updated_at: now,
579
+ edited_at: null,
580
+ deleted_at: null,
581
+ pinned_at: null,
582
+ pinned_by: null,
583
+ };
584
+ let stored;
585
+ let inserted;
586
+ try {
587
+ ({ stored, inserted } = await messages().insertIfAbsent(record, input.attachments ?? [], MAX_CHANNEL_MESSAGES));
588
+ }
589
+ catch (error) {
590
+ if (error instanceof PersistenceResourceLimitError) {
591
+ throw new ChannelError('This channel is full', 409);
592
+ }
593
+ throw error;
594
+ }
595
+ if (inserted && stored.user_id === actor.userId) {
596
+ // The author has read their own message.
597
+ await channels().advanceLastRead(channelId, actor.userId, now);
598
+ }
599
+ const [view] = await mapRows(actor.userId, [stored]);
600
+ if (inserted) {
601
+ await appendChannelEvent(channelId, 'channel.message.v1', stored.id, actor.userId, { type: 'message_created', message: view });
602
+ }
603
+ return { message: view, created: inserted };
604
+ }
605
+ async editMessage(actor, messageId, content) {
606
+ const row = await messages().findById(messageId);
607
+ if (!row || row.deleted_at)
608
+ throw notFound();
609
+ await this.requireMember(row.channel_id, actor.userId);
610
+ if (row.user_id !== actor.userId || row.author_kind !== 'user') {
611
+ throw notFound();
612
+ }
613
+ if (!content.trim() || content.length > MAX_CHANNEL_MESSAGE_LENGTH) {
614
+ throw new ChannelError('Message content is required', 400);
615
+ }
616
+ const now = Date.now();
617
+ const next = {
618
+ ...row,
619
+ content: encryptionService.encrypt(content),
620
+ updated_at: now,
621
+ edited_at: now,
622
+ };
623
+ await messages().update(next);
624
+ const [view] = await mapRows(actor.userId, [next]);
625
+ await appendChannelEvent(row.channel_id, 'channel.message.v1', messageId, actor.userId, { type: 'message_edited', message: view });
626
+ return view;
627
+ }
628
+ /**
629
+ * Tombstone deletion: the row survives with cleared content so replies
630
+ * and the ordered timeline stay intact. Attachments are removed.
631
+ */
632
+ async deleteMessage(actor, messageId) {
633
+ const row = await messages().findById(messageId);
634
+ if (!row || row.deleted_at)
635
+ throw notFound();
636
+ const { member } = await this.requireMember(row.channel_id, actor.userId);
637
+ if (row.user_id !== actor.userId && member.role !== 'owner') {
638
+ throw notFound();
639
+ }
640
+ const now = Date.now();
641
+ const next = {
642
+ ...row,
643
+ content: encryptionService.encrypt(''),
644
+ metadata: null,
645
+ updated_at: now,
646
+ deleted_at: now,
647
+ pinned_at: null,
648
+ pinned_by: null,
649
+ };
650
+ await messages().update(next);
651
+ await appendChannelEvent(row.channel_id, 'channel.message.v1', messageId, actor.userId, { type: 'message_deleted', messageId, channelId: row.channel_id });
652
+ }
653
+ async setPinned(actor, messageId, pinned) {
654
+ const row = await messages().findById(messageId);
655
+ if (!row || row.deleted_at)
656
+ throw notFound();
657
+ await this.requireMember(row.channel_id, actor.userId);
658
+ const now = Date.now();
659
+ const next = {
660
+ ...row,
661
+ pinned_at: pinned ? now : null,
662
+ pinned_by: pinned ? actor.userId : null,
663
+ updated_at: now,
664
+ };
665
+ await messages().update(next);
666
+ const [view] = await mapRows(actor.userId, [next]);
667
+ await appendChannelEvent(row.channel_id, 'channel.pin.v1', messageId, actor.userId, { type: pinned ? 'message_pinned' : 'message_unpinned', message: view });
668
+ return view;
669
+ }
670
+ async listPinned(actor, channelId) {
671
+ await this.requireMember(channelId, actor.userId);
672
+ const rows = await messages().listPinned(channelId, MAX_CHANNEL_PINNED_LISTED);
673
+ return mapRows(actor.userId, rows);
674
+ }
675
+ async react(actor, messageId, emoji, add) {
676
+ const normalized = (emoji ?? '').trim();
677
+ if (!normalized ||
678
+ normalized.length > MAX_CHANNEL_EMOJI_LENGTH ||
679
+ [...normalized].some(character => {
680
+ const codePoint = character.codePointAt(0) ?? 0;
681
+ return codePoint <= 31 || codePoint === 127;
682
+ })) {
683
+ throw new ChannelError('Invalid reaction', 400);
684
+ }
685
+ const row = await messages().findById(messageId);
686
+ if (!row || row.deleted_at)
687
+ throw notFound();
688
+ await this.requireMember(row.channel_id, actor.userId);
689
+ let changed;
690
+ if (add) {
691
+ try {
692
+ changed = await messages().addReaction({
693
+ id: randomUUID(),
694
+ message_id: messageId,
695
+ user_id: actor.userId,
696
+ emoji: normalized,
697
+ created_at: Date.now(),
698
+ }, MAX_CHANNEL_REACTIONS_PER_MESSAGE);
699
+ }
700
+ catch (error) {
701
+ if (error instanceof PersistenceResourceLimitError) {
702
+ throw new ChannelError('This message has too many reactions', 409);
703
+ }
704
+ throw error;
705
+ }
706
+ }
707
+ else {
708
+ changed = await messages().removeReaction(messageId, actor.userId, normalized);
709
+ }
710
+ if (changed) {
711
+ await appendChannelEvent(row.channel_id, 'channel.reaction.v1', messageId, actor.userId, {
712
+ type: add ? 'reaction_added' : 'reaction_removed',
713
+ messageId,
714
+ channelId: row.channel_id,
715
+ emoji: normalized,
716
+ userId: actor.userId,
717
+ });
718
+ }
719
+ }
720
+ /** Root timeline page (newest-first before / oldest-first after). */
721
+ async listTimeline(actor, channelId, options) {
722
+ await this.requireMember(channelId, actor.userId);
723
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), MAX_CHANNEL_PAGE_SIZE);
724
+ const rows = await messages().listPage(channelId, {
725
+ parentId: null,
726
+ ...(options.before ? { before: options.before } : {}),
727
+ ...(options.after ? { after: options.after } : {}),
728
+ limit,
729
+ });
730
+ // Deliver oldest-first regardless of the paging direction.
731
+ const ordered = options.after ? rows : [...rows].reverse();
732
+ return mapRows(actor.userId, ordered);
733
+ }
734
+ async listThread(actor, parentId) {
735
+ const parent = await messages().findById(parentId);
736
+ if (!parent)
737
+ throw notFound();
738
+ await this.requireMember(parent.channel_id, actor.userId);
739
+ const rows = await messages().listThread(parentId, MAX_CHANNEL_THREAD_PAGE_SIZE);
740
+ return mapRows(actor.userId, [parent, ...rows]);
741
+ }
742
+ /** Advance the read cursor; it never moves backwards. */
743
+ async markRead(actor, channelId, lastReadAt) {
744
+ await this.requireMember(channelId, actor.userId);
745
+ const at = lastReadAt !== undefined && Number.isSafeInteger(lastReadAt)
746
+ ? lastReadAt
747
+ : Date.now();
748
+ await channels().advanceLastRead(channelId, actor.userId, at);
749
+ }
750
+ async findMessage(messageId) {
751
+ return messages().findById(messageId);
752
+ }
753
+ /**
754
+ * Stores an uploaded file as a blob owned by the uploader and parks its
755
+ * descriptor in the coordination cache until a message claims it. The
756
+ * cache is shared across replicas, so upload and post may hit different
757
+ * nodes.
758
+ */
759
+ async uploadAttachment(actor, channelId, file) {
760
+ await this.requireMember(channelId, actor.userId);
761
+ if (file.buffer.length === 0) {
762
+ throw new ChannelError('The attachment is empty', 400);
763
+ }
764
+ if (file.buffer.length > MAX_CHANNEL_ATTACHMENT_BYTES) {
765
+ throw new ChannelError('The attachment exceeds the maximum size', 400);
766
+ }
767
+ const attachmentId = randomUUID();
768
+ const platform = getPlatformStorageRuntime();
769
+ const blob = await platform.blobStore.put({
770
+ ownerUserId: actor.userId,
771
+ purpose: CHANNEL_ATTACHMENT_BLOB_PURPOSE,
772
+ contentType: file.contentType,
773
+ originalFilename: file.filename,
774
+ expectedSize: file.buffer.length,
775
+ metadata: {
776
+ resourceType: 'channel-attachment',
777
+ resourceId: attachmentId,
778
+ },
779
+ source: Readable.from(file.buffer),
780
+ });
781
+ const coordinator = getCoordinator();
782
+ await coordinator.setCache(pendingAttachmentKey(actor.userId, attachmentId), {
783
+ channelId,
784
+ blobId: blob.id,
785
+ filename: file.filename,
786
+ contentType: file.contentType,
787
+ size: file.buffer.length,
788
+ }, PENDING_ATTACHMENT_TTL_MS);
789
+ return {
790
+ id: attachmentId,
791
+ filename: file.filename,
792
+ contentType: file.contentType,
793
+ size: file.buffer.length,
794
+ };
795
+ }
796
+ async claimAttachments(actor, channelId, messageId, attachmentIds) {
797
+ if (attachmentIds.length === 0)
798
+ return [];
799
+ if (attachmentIds.length > MAX_CHANNEL_ATTACHMENTS_PER_MESSAGE) {
800
+ throw new ChannelError(`A message may carry at most ${MAX_CHANNEL_ATTACHMENTS_PER_MESSAGE} attachments`, 400);
801
+ }
802
+ const coordinator = getCoordinator();
803
+ const records = [];
804
+ for (const attachmentId of new Set(attachmentIds)) {
805
+ const pending = await coordinator.consumeCache(pendingAttachmentKey(actor.userId, attachmentId));
806
+ if (!pending || pending.channelId !== channelId) {
807
+ throw new ChannelError('Attachment upload expired', 400);
808
+ }
809
+ records.push({
810
+ id: attachmentId,
811
+ message_id: messageId,
812
+ channel_id: channelId,
813
+ blob_id: pending.blobId,
814
+ filename: pending.filename,
815
+ content_type: pending.contentType,
816
+ size: pending.size,
817
+ created_by: actor.userId,
818
+ created_at: Date.now(),
819
+ });
820
+ }
821
+ return records;
822
+ }
823
+ /**
824
+ * The user-facing post: claims uploaded attachments and, when the
825
+ * composer named a model, appends a pending model reply and enqueues the
826
+ * durable mention job under the invoking user's identity.
827
+ */
828
+ async postUserMessage(actor, channelId, input) {
829
+ const messageId = input.id ?? randomUUID();
830
+ const attachments = await this.claimAttachments(actor, channelId, messageId, input.attachmentIds ?? []);
831
+ const posted = await this.postMessage(actor, channelId, {
832
+ id: messageId,
833
+ content: input.content,
834
+ ...(input.parentId ? { parentId: input.parentId } : {}),
835
+ attachments,
836
+ });
837
+ if (posted.created) {
838
+ await this.notifyForMessage(actor, channelId, messageId, input.content, input.parentId).catch(error => {
839
+ logger.warn('Channel notification fan-out failed', { error });
840
+ });
841
+ }
842
+ if (!posted.created || !input.mentionModel)
843
+ return posted;
844
+ // The pending reply and its durable job share deterministic identities,
845
+ // so a retried request never produces a second model answer.
846
+ const replyId = `${messageId}-model`;
847
+ await this.postMessage(actor, channelId, {
848
+ id: replyId,
849
+ content: '',
850
+ ...(input.parentId ? { parentId: input.parentId } : {}),
851
+ authorKind: 'model',
852
+ model: input.mentionModel,
853
+ metadata: { pending: true },
854
+ });
855
+ try {
856
+ await getDurableJobRuntime().service.enqueue({
857
+ jobType: CHANNEL_MENTION_JOB_TYPE,
858
+ actorUserId: actor.userId,
859
+ payload: {
860
+ mode: 'encrypted',
861
+ value: {
862
+ channelId,
863
+ promptMessageId: messageId,
864
+ replyMessageId: replyId,
865
+ model: input.mentionModel,
866
+ ...(input.mentionProviderType
867
+ ? { providerType: input.mentionProviderType }
868
+ : {}),
869
+ ...(input.mentionProviderId
870
+ ? { providerId: input.mentionProviderId }
871
+ : {}),
872
+ },
873
+ },
874
+ idempotencyScope: CHANNEL_MENTION_IDEMPOTENCY_SCOPE,
875
+ idempotencyKey: replyId,
876
+ maxAttempts: 2,
877
+ });
878
+ }
879
+ catch (error) {
880
+ logger.error('Failed to enqueue a channel model mention', { error });
881
+ await this.failModelReply(replyId, 'The model request could not start');
882
+ }
883
+ return posted;
884
+ }
885
+ /**
886
+ * In-app notifications for a new message: the DM peer, `@username`
887
+ * mentions of channel members, and the thread-root author.
888
+ */
889
+ async notifyForMessage(actor, channelId, messageId, content, parentId) {
890
+ const channel = await channels().findById(channelId);
891
+ if (!channel)
892
+ return;
893
+ const { notificationService } = await import('./notificationService.js');
894
+ const authorNames = await resolveUsernames([actor.userId]);
895
+ const authorName = authorNames.get(actor.userId) ?? actor.userId;
896
+ const channelLabel = channel.type === 'dm'
897
+ ? 'a direct message'
898
+ : `#${decryptOptional(channel.name) ?? 'channel'}`;
899
+ const preview = content.slice(0, 140);
900
+ const recipients = new Map();
901
+ if (channel.type === 'dm') {
902
+ const members = await channels().listMembers(channelId, 4);
903
+ for (const member of members) {
904
+ if (member.user_id === actor.userId)
905
+ continue;
906
+ recipients.set(member.user_id, {
907
+ type: 'channel-dm',
908
+ title: `${authorName} sent you a direct message`,
909
+ });
910
+ }
911
+ }
912
+ else {
913
+ const mentioned = new Set([...content.matchAll(/@([A-Za-z0-9._-]+)/g)].map(match => match[1].toLowerCase()));
914
+ if (mentioned.size > 0) {
915
+ const members = await channels().listMembers(channelId, MAX_CHANNEL_MEMBERS);
916
+ const names = await resolveUsernames(members.map(member => member.user_id));
917
+ for (const member of members) {
918
+ if (member.user_id === actor.userId)
919
+ continue;
920
+ const username = names.get(member.user_id);
921
+ if (username && mentioned.has(username.toLowerCase())) {
922
+ recipients.set(member.user_id, {
923
+ type: 'channel-mention',
924
+ title: `${authorName} mentioned you in ${channelLabel}`,
925
+ });
926
+ }
927
+ }
928
+ }
929
+ if (parentId) {
930
+ const parent = await messages().findById(parentId);
931
+ if (parent?.user_id &&
932
+ parent.user_id !== actor.userId &&
933
+ !recipients.has(parent.user_id) &&
934
+ parent.author_kind === 'user') {
935
+ recipients.set(parent.user_id, {
936
+ type: 'channel-mention',
937
+ title: `${authorName} replied to your message in ${channelLabel}`,
938
+ });
939
+ }
940
+ }
941
+ }
942
+ for (const [userId, entry] of recipients) {
943
+ // Only members receive notifications, even if a mention names an
944
+ // outsider's username.
945
+ const member = await channels().findMember(channelId, userId);
946
+ if (!member)
947
+ continue;
948
+ await notificationService.publish({
949
+ userId,
950
+ type: entry.type,
951
+ title: entry.title,
952
+ body: preview,
953
+ href: `/channels?channel=${channelId}`,
954
+ sourceKey: `channel-message:${messageId}:${userId}`,
955
+ });
956
+ }
957
+ }
958
+ /** Reply completion used by the durable mention job. */
959
+ async completeModelReply(replyMessageId, content) {
960
+ const row = await messages().findById(replyMessageId);
961
+ if (!row || row.deleted_at)
962
+ return;
963
+ const now = Date.now();
964
+ const next = {
965
+ ...row,
966
+ content: encryptionService.encrypt(content),
967
+ metadata: null,
968
+ updated_at: now,
969
+ };
970
+ await messages().update(next);
971
+ const [view] = await mapRows(row.user_id ?? 'default', [next]);
972
+ await appendChannelEvent(row.channel_id, 'channel.message.v1', replyMessageId, row.user_id ?? 'default', { type: 'message_edited', message: view });
973
+ }
974
+ async failModelReply(replyMessageId, errorSummary) {
975
+ const row = await messages().findById(replyMessageId);
976
+ if (!row || row.deleted_at)
977
+ return;
978
+ const now = Date.now();
979
+ const next = {
980
+ ...row,
981
+ metadata: encryptionService.encrypt(JSON.stringify({ error: errorSummary })),
982
+ updated_at: now,
983
+ };
984
+ await messages().update(next);
985
+ const [view] = await mapRows(row.user_id ?? 'default', [next]);
986
+ await appendChannelEvent(row.channel_id, 'channel.message.v1', replyMessageId, row.user_id ?? 'default', { type: 'message_edited', message: view });
987
+ }
988
+ /**
989
+ * Recent decrypted conversation context for a model mention, oldest
990
+ * first, ending at the prompting message.
991
+ */
992
+ async mentionContext(channelId, promptMessageId) {
993
+ const prompt = await messages().findById(promptMessageId);
994
+ if (!prompt)
995
+ return [];
996
+ const rows = await messages().listPage(channelId, {
997
+ parentId: prompt.parent_id ? prompt.parent_id : null,
998
+ before: { created_at: prompt.created_at, id: prompt.id },
999
+ limit: MAX_CHANNEL_MENTION_CONTEXT_MESSAGES,
1000
+ });
1001
+ const ordered = [...rows].reverse();
1002
+ ordered.push(prompt);
1003
+ const usernames = await resolveUsernames(ordered.flatMap(row => (row.user_id ? [row.user_id] : [])));
1004
+ return ordered.flatMap(row => {
1005
+ if (row.deleted_at)
1006
+ return [];
1007
+ const content = decryptOptional(row.content);
1008
+ if (!content)
1009
+ return [];
1010
+ const author = row.author_kind === 'model'
1011
+ ? `${row.model ?? 'assistant'} (model)`
1012
+ : (usernames.get(row.user_id ?? '') ?? 'someone');
1013
+ return [{ author, content }];
1014
+ });
1015
+ }
1016
+ async openAttachment(actor, attachmentId) {
1017
+ const attachment = await messages().findAttachment(attachmentId);
1018
+ if (!attachment)
1019
+ throw notFound();
1020
+ await this.requireMember(attachment.channel_id, actor.userId);
1021
+ if (!attachment.created_by)
1022
+ throw notFound();
1023
+ const body = await getPlatformStorageRuntime().blobStore.open({
1024
+ id: attachment.blob_id,
1025
+ ownerUserId: attachment.created_by,
1026
+ });
1027
+ return {
1028
+ attachment: {
1029
+ id: attachment.id,
1030
+ filename: attachment.filename,
1031
+ contentType: attachment.content_type,
1032
+ size: attachment.size,
1033
+ },
1034
+ body,
1035
+ };
1036
+ }
1037
+ /** Recent decrypted candidates from the actor's channels, for search. */
1038
+ async searchCandidates(actor, maximum) {
1039
+ const memberships = await channels().listForUser(actor.userId, MAX_CHANNELS_LISTED);
1040
+ const rows = await messages().listRecentForChannels(memberships.map(entry => entry.channel.id), maximum);
1041
+ return rows.flatMap(row => {
1042
+ const content = decryptOptional(row.content);
1043
+ return content
1044
+ ? [
1045
+ {
1046
+ channelId: row.channel_id,
1047
+ messageId: row.id,
1048
+ content,
1049
+ createdAt: row.created_at,
1050
+ },
1051
+ ]
1052
+ : [];
1053
+ });
1054
+ }
1055
+ }
1056
+ export const channelService = new ChannelService();
1057
+ //# sourceMappingURL=channelService.js.map