@brightchain/brightchain-lib 0.29.26 → 0.30.1

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 (290) hide show
  1. package/package.json +10 -4
  2. package/src/index.d.ts +1 -1
  3. package/src/index.d.ts.map +1 -1
  4. package/src/index.js +2 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/constants.d.ts +3 -26
  7. package/src/lib/constants.d.ts.map +1 -1
  8. package/src/lib/constants.js +2 -1
  9. package/src/lib/constants.js.map +1 -1
  10. package/src/lib/db/collection.d.ts +1 -1
  11. package/src/lib/db/collection.d.ts.map +1 -1
  12. package/src/lib/db/collection.js +2 -1
  13. package/src/lib/db/collection.js.map +1 -1
  14. package/src/lib/enumeration-translations/index.d.ts +2 -0
  15. package/src/lib/enumeration-translations/index.d.ts.map +1 -1
  16. package/src/lib/enumeration-translations/index.js +1 -0
  17. package/src/lib/enumeration-translations/index.js.map +1 -1
  18. package/src/lib/enumeration-translations/memberStatusType.d.ts +5 -0
  19. package/src/lib/enumeration-translations/memberStatusType.d.ts.map +1 -0
  20. package/src/lib/enumeration-translations/memberStatusType.js +58 -0
  21. package/src/lib/enumeration-translations/memberStatusType.js.map +1 -0
  22. package/src/lib/enumerations/brightChainStrings.d.ts +61 -0
  23. package/src/lib/enumerations/brightChainStrings.d.ts.map +1 -1
  24. package/src/lib/enumerations/brightChainStrings.js +64 -0
  25. package/src/lib/enumerations/brightChainStrings.js.map +1 -1
  26. package/src/lib/enumerations/brightchainFeatures.d.ts +1 -0
  27. package/src/lib/enumerations/brightchainFeatures.d.ts.map +1 -1
  28. package/src/lib/enumerations/brightchainFeatures.js +1 -0
  29. package/src/lib/enumerations/brightchainFeatures.js.map +1 -1
  30. package/src/lib/enumerations/communication.d.ts +7 -1
  31. package/src/lib/enumerations/communication.d.ts.map +1 -1
  32. package/src/lib/enumerations/communication.js +6 -0
  33. package/src/lib/enumerations/communication.js.map +1 -1
  34. package/src/lib/enumerations/friendRequestStatus.d.ts +7 -0
  35. package/src/lib/enumerations/friendRequestStatus.d.ts.map +1 -0
  36. package/src/lib/enumerations/friendRequestStatus.js +11 -0
  37. package/src/lib/enumerations/friendRequestStatus.js.map +1 -0
  38. package/src/lib/enumerations/friendsErrorCode.d.ts +10 -0
  39. package/src/lib/enumerations/friendsErrorCode.d.ts.map +1 -0
  40. package/src/lib/enumerations/friendsErrorCode.js +14 -0
  41. package/src/lib/enumerations/friendsErrorCode.js.map +1 -0
  42. package/src/lib/enumerations/friendshipStatus.d.ts +7 -0
  43. package/src/lib/enumerations/friendshipStatus.d.ts.map +1 -0
  44. package/src/lib/enumerations/friendshipStatus.js +11 -0
  45. package/src/lib/enumerations/friendshipStatus.js.map +1 -0
  46. package/src/lib/enumerations/index.d.ts +3 -0
  47. package/src/lib/enumerations/index.d.ts.map +1 -1
  48. package/src/lib/enumerations/index.js +4 -0
  49. package/src/lib/enumerations/index.js.map +1 -1
  50. package/src/lib/enumerations/messaging/emailErrorType.d.ts +15 -2
  51. package/src/lib/enumerations/messaging/emailErrorType.d.ts.map +1 -1
  52. package/src/lib/enumerations/messaging/emailErrorType.js +17 -1
  53. package/src/lib/enumerations/messaging/emailErrorType.js.map +1 -1
  54. package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts +4 -2
  55. package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts.map +1 -1
  56. package/src/lib/enumerations/messaging/messageEncryptionScheme.js +3 -1
  57. package/src/lib/enumerations/messaging/messageEncryptionScheme.js.map +1 -1
  58. package/src/lib/errors/encryptionErrors.d.ts +71 -0
  59. package/src/lib/errors/encryptionErrors.d.ts.map +1 -0
  60. package/src/lib/errors/encryptionErrors.js +112 -0
  61. package/src/lib/errors/encryptionErrors.js.map +1 -0
  62. package/src/lib/errors/friendsServiceError.d.ts +20 -0
  63. package/src/lib/errors/friendsServiceError.d.ts.map +1 -0
  64. package/src/lib/errors/friendsServiceError.js +48 -0
  65. package/src/lib/errors/friendsServiceError.js.map +1 -0
  66. package/src/lib/errors/index.d.ts +15 -0
  67. package/src/lib/errors/index.d.ts.map +1 -1
  68. package/src/lib/errors/index.js +21 -0
  69. package/src/lib/errors/index.js.map +1 -1
  70. package/src/lib/i18n/i18n-setup.d.ts +2 -2
  71. package/src/lib/i18n/i18n-setup.d.ts.map +1 -1
  72. package/src/lib/i18n/strings/englishUK.d.ts +2 -2
  73. package/src/lib/i18n/strings/englishUK.d.ts.map +1 -1
  74. package/src/lib/i18n/strings/englishUK.js.map +1 -1
  75. package/src/lib/i18n/strings/englishUs.d.ts +2 -2
  76. package/src/lib/i18n/strings/englishUs.d.ts.map +1 -1
  77. package/src/lib/i18n/strings/englishUs.js +65 -1
  78. package/src/lib/i18n/strings/englishUs.js.map +1 -1
  79. package/src/lib/i18n/strings/french.d.ts +2 -2
  80. package/src/lib/i18n/strings/french.d.ts.map +1 -1
  81. package/src/lib/i18n/strings/french.js +78 -13
  82. package/src/lib/i18n/strings/french.js.map +1 -1
  83. package/src/lib/i18n/strings/german.d.ts +2 -2
  84. package/src/lib/i18n/strings/german.d.ts.map +1 -1
  85. package/src/lib/i18n/strings/german.js +77 -12
  86. package/src/lib/i18n/strings/german.js.map +1 -1
  87. package/src/lib/i18n/strings/japanese.d.ts +2 -2
  88. package/src/lib/i18n/strings/japanese.d.ts.map +1 -1
  89. package/src/lib/i18n/strings/japanese.js +77 -12
  90. package/src/lib/i18n/strings/japanese.js.map +1 -1
  91. package/src/lib/i18n/strings/mandarin.d.ts +2 -2
  92. package/src/lib/i18n/strings/mandarin.d.ts.map +1 -1
  93. package/src/lib/i18n/strings/mandarin.js +77 -12
  94. package/src/lib/i18n/strings/mandarin.js.map +1 -1
  95. package/src/lib/i18n/strings/spanish.d.ts +2 -2
  96. package/src/lib/i18n/strings/spanish.d.ts.map +1 -1
  97. package/src/lib/i18n/strings/spanish.js +77 -12
  98. package/src/lib/i18n/strings/spanish.js.map +1 -1
  99. package/src/lib/i18n/strings/ukrainian.d.ts +2 -2
  100. package/src/lib/i18n/strings/ukrainian.d.ts.map +1 -1
  101. package/src/lib/i18n/strings/ukrainian.js +77 -12
  102. package/src/lib/i18n/strings/ukrainian.js.map +1 -1
  103. package/src/lib/index.d.ts +31 -0
  104. package/src/lib/index.d.ts.map +1 -1
  105. package/src/lib/index.js +29 -1
  106. package/src/lib/index.js.map +1 -1
  107. package/src/lib/interfaces/appSubsystemPlugin.d.ts +69 -0
  108. package/src/lib/interfaces/appSubsystemPlugin.d.ts.map +1 -0
  109. package/src/lib/interfaces/appSubsystemPlugin.js +3 -0
  110. package/src/lib/interfaces/appSubsystemPlugin.js.map +1 -0
  111. package/src/lib/interfaces/auth/writeProof.d.ts +2 -0
  112. package/src/lib/interfaces/auth/writeProof.d.ts.map +1 -1
  113. package/src/lib/interfaces/auth/writeProofUtils.d.ts +1 -1
  114. package/src/lib/interfaces/auth/writeProofUtils.d.ts.map +1 -1
  115. package/src/lib/interfaces/auth/writeProofUtils.js +2 -2
  116. package/src/lib/interfaces/auth/writeProofUtils.js.map +1 -1
  117. package/src/lib/interfaces/availability/gossipService.d.ts +99 -1
  118. package/src/lib/interfaces/availability/gossipService.d.ts.map +1 -1
  119. package/src/lib/interfaces/availability/gossipService.js +4 -0
  120. package/src/lib/interfaces/availability/gossipService.js.map +1 -1
  121. package/src/lib/interfaces/communication/blockContentStore.d.ts +57 -0
  122. package/src/lib/interfaces/communication/blockContentStore.d.ts.map +1 -0
  123. package/src/lib/interfaces/communication/blockContentStore.js +21 -0
  124. package/src/lib/interfaces/communication/blockContentStore.js.map +1 -0
  125. package/src/lib/interfaces/communication/chatStorageProvider.d.ts +77 -0
  126. package/src/lib/interfaces/communication/chatStorageProvider.d.ts.map +1 -0
  127. package/src/lib/interfaces/communication/chatStorageProvider.js +25 -0
  128. package/src/lib/interfaces/communication/chatStorageProvider.js.map +1 -0
  129. package/src/lib/interfaces/communication/index.d.ts +4 -0
  130. package/src/lib/interfaces/communication/index.d.ts.map +1 -0
  131. package/src/lib/interfaces/communication/index.js +3 -0
  132. package/src/lib/interfaces/communication/index.js.map +1 -0
  133. package/src/lib/interfaces/communication/server.d.ts +57 -0
  134. package/src/lib/interfaces/communication/server.d.ts.map +1 -0
  135. package/src/lib/interfaces/communication/server.js +14 -0
  136. package/src/lib/interfaces/communication/server.js.map +1 -0
  137. package/src/lib/interfaces/communication.d.ts +62 -5
  138. package/src/lib/interfaces/communication.d.ts.map +1 -1
  139. package/src/lib/interfaces/communication.js +8 -0
  140. package/src/lib/interfaces/communication.js.map +1 -1
  141. package/src/lib/interfaces/communicationEvents.d.ts +59 -1
  142. package/src/lib/interfaces/communicationEvents.d.ts.map +1 -1
  143. package/src/lib/interfaces/constants.d.ts +2 -0
  144. package/src/lib/interfaces/constants.d.ts.map +1 -1
  145. package/src/lib/interfaces/events/communicationEventEmitter.d.ts +9 -0
  146. package/src/lib/interfaces/events/communicationEventEmitter.d.ts.map +1 -1
  147. package/src/lib/interfaces/events/communicationEventEmitter.js +9 -0
  148. package/src/lib/interfaces/events/communicationEventEmitter.js.map +1 -1
  149. package/src/lib/interfaces/friends/baseFriendRequest.d.ts +13 -0
  150. package/src/lib/interfaces/friends/baseFriendRequest.d.ts.map +1 -0
  151. package/src/lib/interfaces/friends/baseFriendRequest.js +3 -0
  152. package/src/lib/interfaces/friends/baseFriendRequest.js.map +1 -0
  153. package/src/lib/interfaces/friends/baseFriendship.d.ts +11 -0
  154. package/src/lib/interfaces/friends/baseFriendship.d.ts.map +1 -0
  155. package/src/lib/interfaces/friends/baseFriendship.js +3 -0
  156. package/src/lib/interfaces/friends/baseFriendship.js.map +1 -0
  157. package/src/lib/interfaces/friends/friendsService.d.ts +32 -0
  158. package/src/lib/interfaces/friends/friendsService.d.ts.map +1 -0
  159. package/src/lib/interfaces/friends/friendsService.js +3 -0
  160. package/src/lib/interfaces/friends/friendsService.js.map +1 -0
  161. package/src/lib/interfaces/friends/friendsSuggestionProvider.d.ts +17 -0
  162. package/src/lib/interfaces/friends/friendsSuggestionProvider.d.ts.map +1 -0
  163. package/src/lib/interfaces/friends/friendsSuggestionProvider.js +3 -0
  164. package/src/lib/interfaces/friends/friendsSuggestionProvider.js.map +1 -0
  165. package/src/lib/interfaces/friends/index.d.ts +6 -0
  166. package/src/lib/interfaces/friends/index.d.ts.map +1 -0
  167. package/src/lib/interfaces/friends/index.js +3 -0
  168. package/src/lib/interfaces/friends/index.js.map +1 -0
  169. package/src/lib/interfaces/friends/pagination.d.ts +11 -0
  170. package/src/lib/interfaces/friends/pagination.d.ts.map +1 -0
  171. package/src/lib/interfaces/friends/pagination.js +3 -0
  172. package/src/lib/interfaces/friends/pagination.js.map +1 -0
  173. package/src/lib/interfaces/index.d.ts +6 -1
  174. package/src/lib/interfaces/index.d.ts.map +1 -1
  175. package/src/lib/interfaces/index.js +3 -0
  176. package/src/lib/interfaces/index.js.map +1 -1
  177. package/src/lib/interfaces/messaging/gpgKey.d.ts +93 -0
  178. package/src/lib/interfaces/messaging/gpgKey.d.ts.map +1 -0
  179. package/src/lib/interfaces/messaging/gpgKey.js +12 -0
  180. package/src/lib/interfaces/messaging/gpgKey.js.map +1 -0
  181. package/src/lib/interfaces/messaging/index.d.ts +4 -0
  182. package/src/lib/interfaces/messaging/index.d.ts.map +1 -1
  183. package/src/lib/interfaces/messaging/index.js +4 -0
  184. package/src/lib/interfaces/messaging/index.js.map +1 -1
  185. package/src/lib/interfaces/messaging/keyStore.d.ts +100 -0
  186. package/src/lib/interfaces/messaging/keyStore.d.ts.map +1 -0
  187. package/src/lib/interfaces/messaging/keyStore.js +13 -0
  188. package/src/lib/interfaces/messaging/keyStore.js.map +1 -0
  189. package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts +92 -0
  190. package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts.map +1 -0
  191. package/src/lib/interfaces/messaging/recipientKeyResolver.js +13 -0
  192. package/src/lib/interfaces/messaging/recipientKeyResolver.js.map +1 -0
  193. package/src/lib/interfaces/messaging/smimeCertificate.d.ts +99 -0
  194. package/src/lib/interfaces/messaging/smimeCertificate.d.ts.map +1 -0
  195. package/src/lib/interfaces/messaging/smimeCertificate.js +12 -0
  196. package/src/lib/interfaces/messaging/smimeCertificate.js.map +1 -0
  197. package/src/lib/interfaces/responses/adminDashboardResponse.d.ts +8 -0
  198. package/src/lib/interfaces/responses/adminDashboardResponse.d.ts.map +1 -1
  199. package/src/lib/interfaces/responses/communicationResponses.d.ts +25 -0
  200. package/src/lib/interfaces/responses/communicationResponses.d.ts.map +1 -1
  201. package/src/lib/interfaces/storage/documentTypes.d.ts +4 -0
  202. package/src/lib/interfaces/storage/documentTypes.d.ts.map +1 -1
  203. package/src/lib/services/blockService.d.ts +11 -3
  204. package/src/lib/services/blockService.d.ts.map +1 -1
  205. package/src/lib/services/blockService.js +22 -2
  206. package/src/lib/services/blockService.js.map +1 -1
  207. package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts +108 -0
  208. package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts.map +1 -0
  209. package/src/lib/services/communication/__tests__/mockChatStorageProvider.js +284 -0
  210. package/src/lib/services/communication/__tests__/mockChatStorageProvider.js.map +1 -0
  211. package/src/lib/services/communication/attachmentUtils.d.ts +20 -0
  212. package/src/lib/services/communication/attachmentUtils.d.ts.map +1 -0
  213. package/src/lib/services/communication/attachmentUtils.js +43 -0
  214. package/src/lib/services/communication/attachmentUtils.js.map +1 -0
  215. package/src/lib/services/communication/channelService.d.ts +143 -14
  216. package/src/lib/services/communication/channelService.d.ts.map +1 -1
  217. package/src/lib/services/communication/channelService.js +562 -41
  218. package/src/lib/services/communication/channelService.js.map +1 -1
  219. package/src/lib/services/communication/conversationService.d.ts +91 -4
  220. package/src/lib/services/communication/conversationService.d.ts.map +1 -1
  221. package/src/lib/services/communication/conversationService.js +269 -7
  222. package/src/lib/services/communication/conversationService.js.map +1 -1
  223. package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts +36 -0
  224. package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts.map +1 -0
  225. package/src/lib/services/communication/eciesKeyEncryptionHandler.js +30 -0
  226. package/src/lib/services/communication/eciesKeyEncryptionHandler.js.map +1 -0
  227. package/src/lib/services/communication/groupService.d.ts +99 -21
  228. package/src/lib/services/communication/groupService.d.ts.map +1 -1
  229. package/src/lib/services/communication/groupService.js +387 -41
  230. package/src/lib/services/communication/groupService.js.map +1 -1
  231. package/src/lib/services/communication/index.d.ts +6 -1
  232. package/src/lib/services/communication/index.d.ts.map +1 -1
  233. package/src/lib/services/communication/index.js +20 -1
  234. package/src/lib/services/communication/index.js.map +1 -1
  235. package/src/lib/services/communication/keyEpochManager.d.ts +41 -0
  236. package/src/lib/services/communication/keyEpochManager.d.ts.map +1 -0
  237. package/src/lib/services/communication/keyEpochManager.js +59 -0
  238. package/src/lib/services/communication/keyEpochManager.js.map +1 -0
  239. package/src/lib/services/communication/rehydrationHelpers.d.ts +32 -0
  240. package/src/lib/services/communication/rehydrationHelpers.d.ts.map +1 -0
  241. package/src/lib/services/communication/rehydrationHelpers.js +58 -0
  242. package/src/lib/services/communication/rehydrationHelpers.js.map +1 -0
  243. package/src/lib/services/communication/serverService.d.ts +193 -0
  244. package/src/lib/services/communication/serverService.d.ts.map +1 -0
  245. package/src/lib/services/communication/serverService.js +495 -0
  246. package/src/lib/services/communication/serverService.js.map +1 -0
  247. package/src/lib/services/copyOnWrite.service.d.ts +110 -0
  248. package/src/lib/services/copyOnWrite.service.d.ts.map +1 -0
  249. package/src/lib/services/copyOnWrite.service.js +256 -0
  250. package/src/lib/services/copyOnWrite.service.js.map +1 -0
  251. package/src/lib/services/index.d.ts +1 -0
  252. package/src/lib/services/index.d.ts.map +1 -1
  253. package/src/lib/services/index.js +1 -0
  254. package/src/lib/services/index.js.map +1 -1
  255. package/src/lib/services/memberStore.d.ts +17 -1
  256. package/src/lib/services/memberStore.d.ts.map +1 -1
  257. package/src/lib/services/memberStore.js +98 -17
  258. package/src/lib/services/memberStore.js.map +1 -1
  259. package/src/lib/services/messaging/emailEncryptionService.d.ts +162 -0
  260. package/src/lib/services/messaging/emailEncryptionService.d.ts.map +1 -1
  261. package/src/lib/services/messaging/emailEncryptionService.js +293 -0
  262. package/src/lib/services/messaging/emailEncryptionService.js.map +1 -1
  263. package/src/lib/services/messaging/emailMessageService.d.ts +64 -0
  264. package/src/lib/services/messaging/emailMessageService.d.ts.map +1 -1
  265. package/src/lib/services/messaging/emailMessageService.js +142 -13
  266. package/src/lib/services/messaging/emailMessageService.js.map +1 -1
  267. package/src/lib/services/messaging/gpgKeyManager.d.ts +130 -0
  268. package/src/lib/services/messaging/gpgKeyManager.d.ts.map +1 -0
  269. package/src/lib/services/messaging/gpgKeyManager.js +381 -0
  270. package/src/lib/services/messaging/gpgKeyManager.js.map +1 -0
  271. package/src/lib/services/messaging/index.d.ts +3 -0
  272. package/src/lib/services/messaging/index.d.ts.map +1 -1
  273. package/src/lib/services/messaging/index.js +3 -0
  274. package/src/lib/services/messaging/index.js.map +1 -1
  275. package/src/lib/services/messaging/recipientKeyResolver.d.ts +47 -0
  276. package/src/lib/services/messaging/recipientKeyResolver.d.ts.map +1 -0
  277. package/src/lib/services/messaging/recipientKeyResolver.js +132 -0
  278. package/src/lib/services/messaging/recipientKeyResolver.js.map +1 -0
  279. package/src/lib/services/messaging/smimeCertificateManager.d.ts +207 -0
  280. package/src/lib/services/messaging/smimeCertificateManager.d.ts.map +1 -0
  281. package/src/lib/services/messaging/smimeCertificateManager.js +696 -0
  282. package/src/lib/services/messaging/smimeCertificateManager.js.map +1 -0
  283. package/src/lib/utils/index.d.ts +6 -0
  284. package/src/lib/utils/index.d.ts.map +1 -1
  285. package/src/lib/utils/index.js +9 -0
  286. package/src/lib/utils/index.js.map +1 -1
  287. package/src/lib/utils/sortPair.d.ts +6 -0
  288. package/src/lib/utils/sortPair.d.ts.map +1 -0
  289. package/src/lib/utils/sortPair.js +11 -0
  290. package/src/lib/utils/sortPair.js.map +1 -0
@@ -2,12 +2,17 @@
2
2
  /**
3
3
  * GroupService — manages group lifecycle, key management, and messaging.
4
4
  *
5
- * Maintains in-memory stores for groups and messages, with symmetric key
6
- * generation per group and ECIES-style per-member key encryption.
7
- * Supports member management with key rotation on departure,
8
- * message edit/delete/pin/reaction operations.
5
+ * Maintains in-memory stores for groups and messages, with epoch-aware
6
+ * symmetric key management via IKeyEpochState. Each key rotation creates
7
+ * a new epoch. Messages record which epoch they were encrypted under.
8
+ * On member removal, all epoch keys are re-wrapped for remaining members only.
9
9
  *
10
- * Requirements: 10.2
10
+ * When an IChatStorageProvider is injected, groups and group messages are
11
+ * also persisted to the provider's collections (write-through). Sync helper
12
+ * methods continue to read from the in-memory Maps so their signatures
13
+ * remain unchanged.
14
+ *
15
+ * Requirements: 10.2, 5.1, 5.2, 5.3, 5.4, 9.3
11
16
  */
12
17
  Object.defineProperty(exports, "__esModule", { value: true });
13
18
  exports.GroupService = exports.MemberAlreadyInGroupError = exports.ReactionNotFoundError = exports.NotMessageAuthorError = exports.MemberMutedError = exports.GroupPermissionError = exports.GroupMessageNotFoundError = exports.NotGroupMemberError = exports.GroupNotFoundError = void 0;
@@ -15,9 +20,12 @@ exports.extractKeyFromDefault = extractKeyFromDefault;
15
20
  const uuid_1 = require("uuid");
16
21
  const platformCrypto_1 = require("../../crypto/platformCrypto");
17
22
  const communication_1 = require("../../enumerations/communication");
23
+ const attachmentUtils_1 = require("./attachmentUtils");
18
24
  const events_1 = require("../../interfaces/events");
19
25
  const pagination_1 = require("../../utils/pagination");
26
+ const encryptionErrors_1 = require("../../errors/encryptionErrors");
20
27
  const messageOperationsService_1 = require("./messageOperationsService");
28
+ const rehydrationHelpers_1 = require("./rehydrationHelpers");
21
29
  // ─── Error classes ──────────────────────────────────────────────────────────
22
30
  class GroupNotFoundError extends Error {
23
31
  constructor(groupId) {
@@ -108,20 +116,86 @@ class GroupService {
108
116
  groups = new Map();
109
117
  /** groupId → messages (ordered by createdAt ascending) */
110
118
  messages = new Map();
111
- /** groupId → raw symmetric key (Uint8Array) */
112
- symmetricKeys = new Map();
119
+ /** groupId → epoch-aware key state (raw keys + wrapped keys per epoch) */
120
+ keyEpochStates = new Map();
113
121
  permissionService;
114
122
  encryptKey;
115
123
  messageOps;
116
124
  eventEmitter;
117
125
  randomBytesProvider;
118
- constructor(permissionService, encryptKey = defaultKeyEncryption, messageOps, eventEmitter, randomBytesProvider) {
126
+ /** Optional persistent collection for groups (write-through). */
127
+ groupCollection;
128
+ /** Optional persistent collection for group messages (write-through). */
129
+ groupMessageCollection;
130
+ /** Optional block content store for storing message content as blocks. */
131
+ blockContentStore;
132
+ /** Whether init() has already been called (idempotency guard). */
133
+ initialized = false;
134
+ constructor(permissionService, encryptKey = defaultKeyEncryption, messageOps, eventEmitter, randomBytesProvider, storageProvider, blockContentStore) {
119
135
  this.permissionService = permissionService;
120
136
  this.encryptKey = encryptKey;
121
137
  this.messageOps =
122
138
  messageOps ?? new messageOperationsService_1.MessageOperationsService(permissionService);
123
139
  this.eventEmitter = eventEmitter ?? new events_1.NullEventEmitter();
124
140
  this.randomBytesProvider = randomBytesProvider ?? platformCrypto_1.getRandomBytes;
141
+ this.blockContentStore = blockContentStore;
142
+ if (storageProvider) {
143
+ this.groupCollection = storageProvider.groups;
144
+ this.groupMessageCollection = storageProvider.groupMessages;
145
+ }
146
+ }
147
+ // ─── Rehydration ──────────────────────────────────────────────────────
148
+ /**
149
+ * Load persisted groups and group messages back into in-memory Maps.
150
+ *
151
+ * Idempotent: only the first call performs rehydration. No-op when no
152
+ * storage provider was supplied at construction time.
153
+ *
154
+ * Requirements: 1.2, 1.5, 1.6, 3.1, 3.2, 3.3, 3.4, 3.5, 8.1, 8.2, 8.3, 9.1, 9.4
155
+ */
156
+ async init() {
157
+ if (this.initialized)
158
+ return;
159
+ if (!this.groupCollection)
160
+ return;
161
+ this.initialized = true;
162
+ // ── Load groups ───────────────────────────────────────────────────
163
+ let loadedGroups;
164
+ try {
165
+ loadedGroups = await this.groupCollection.findMany();
166
+ }
167
+ catch (error) {
168
+ console.error('[GroupService] Failed to load from groups collection:', error);
169
+ throw error;
170
+ }
171
+ for (const group of loadedGroups) {
172
+ this.groups.set(group.id, group);
173
+ // Reconstruct key epoch state
174
+ const epochState = (0, rehydrationHelpers_1.reconstructKeyEpochState)(group.encryptedSharedKey);
175
+ if (epochState) {
176
+ this.keyEpochStates.set(group.id, epochState);
177
+ }
178
+ else {
179
+ console.warn(`[GroupService] Skipping key epoch reconstruction for group ${group.id}: malformed encryptedSharedKey`);
180
+ }
181
+ // Register permissions for each member
182
+ for (const member of group.members) {
183
+ this.permissionService.assignRole(member.memberId, group.id, member.role);
184
+ }
185
+ }
186
+ // ── Load group messages ─────────────────────────────────────────────
187
+ let loadedMessages;
188
+ try {
189
+ loadedMessages = await this.groupMessageCollection.findMany();
190
+ }
191
+ catch (error) {
192
+ console.error('[GroupService] Failed to load from groupMessages collection:', error);
193
+ throw error;
194
+ }
195
+ const grouped = (0, rehydrationHelpers_1.groupAndSortMessages)(loadedMessages);
196
+ for (const [contextId, msgs] of grouped) {
197
+ this.messages.set(contextId, msgs);
198
+ }
125
199
  }
126
200
  // ─── Helpers ────────────────────────────────────────────────────────────
127
201
  assertGroupExists(groupId) {
@@ -149,29 +223,111 @@ class GroupService {
149
223
  generateSymmetricKey() {
150
224
  return this.randomBytesProvider(32); // AES-256
151
225
  }
152
- encryptKeyForMembers(memberIds, symmetricKey) {
226
+ /**
227
+ * Encrypt a symmetric key for multiple members, returning a Map<memberId, wrappedKey>.
228
+ * Wraps key wrapping failures as KeyUnwrapError with context information.
229
+ *
230
+ * Requirements: 12.3
231
+ */
232
+ encryptKeyForMembers(memberIds, symmetricKey, contextId, epoch) {
153
233
  const encrypted = new Map();
154
234
  for (const id of memberIds) {
155
- encrypted.set(id, this.encryptKey(id, symmetricKey));
235
+ try {
236
+ encrypted.set(id, this.encryptKey(id, symmetricKey));
237
+ }
238
+ catch (error) {
239
+ if (contextId !== undefined && epoch !== undefined) {
240
+ throw new encryptionErrors_1.KeyUnwrapError(contextId, id, epoch);
241
+ }
242
+ throw error;
243
+ }
156
244
  }
157
245
  return encrypted;
158
246
  }
159
247
  /**
160
- * Rotate the group's symmetric key and re-encrypt for remaining members.
161
- * Requirement 10.2: key rotation on member departure.
248
+ * Assert that a key epoch exists in the epoch state for a given context.
249
+ * Throws KeyEpochNotFoundError if the epoch is not found.
250
+ *
251
+ * Requirements: 12.3
252
+ */
253
+ assertEpochExists(contextId, keyEpoch) {
254
+ const state = this.keyEpochStates.get(contextId);
255
+ if (!state || !state.epochKeys.has(keyEpoch)) {
256
+ throw new encryptionErrors_1.KeyEpochNotFoundError(contextId, keyEpoch);
257
+ }
258
+ }
259
+ /**
260
+ * Create initial epoch state (epoch 0) for a new group.
261
+ * Uses KeyEpochManager pattern: creates epoch 0 with wrapped keys for all members.
262
+ *
263
+ * Requirements: 5.1
264
+ */
265
+ createInitialEpochState(symmetricKey, memberIds) {
266
+ const epochKeys = new Map();
267
+ epochKeys.set(0, symmetricKey);
268
+ const wrappedKeys = this.encryptKeyForMembers(memberIds, symmetricKey);
269
+ const encryptedEpochKeys = new Map();
270
+ encryptedEpochKeys.set(0, wrappedKeys);
271
+ return { currentEpoch: 0, epochKeys, encryptedEpochKeys };
272
+ }
273
+ /**
274
+ * Add a member to an existing epoch state: wrap ALL epoch keys for the new member.
275
+ * This gives the new member access to full message history.
276
+ *
277
+ * Requirements: 5.2
278
+ */
279
+ addMemberToEpochState(state, newMemberId) {
280
+ for (const [epoch, rawKey] of state.epochKeys) {
281
+ const epochMap = state.encryptedEpochKeys.get(epoch) ?? new Map();
282
+ epochMap.set(newMemberId, this.encryptKey(newMemberId, rawKey));
283
+ state.encryptedEpochKeys.set(epoch, epochMap);
284
+ }
285
+ }
286
+ /**
287
+ * Rotate key: increment epoch, add new key, delete removed member from ALL epochs,
288
+ * re-wrap ALL epoch keys for remaining members.
289
+ *
290
+ * Requirements: 5.3
291
+ */
292
+ rotateEpochState(state, newKey, remainingMemberIds, removedMemberId) {
293
+ const newEpoch = state.currentEpoch + 1;
294
+ // Add new epoch key
295
+ state.epochKeys.set(newEpoch, newKey);
296
+ // Delete removed member from ALL epochs
297
+ for (const [, memberMap] of state.encryptedEpochKeys) {
298
+ memberMap.delete(removedMemberId);
299
+ }
300
+ // Re-wrap ALL epoch keys for remaining members
301
+ for (const [epoch, rawKey] of state.epochKeys) {
302
+ state.encryptedEpochKeys.set(epoch, this.encryptKeyForMembers(remainingMemberIds, rawKey));
303
+ }
304
+ return { ...state, currentEpoch: newEpoch };
305
+ }
306
+ /**
307
+ * Rotate the group's symmetric key using epoch-aware key management.
308
+ * Generates a new key, increments epoch, re-wraps all epoch keys for remaining members,
309
+ * and removes the departed member from all epochs.
310
+ *
311
+ * Requirements: 5.3
162
312
  */
163
- rotateKey(group) {
313
+ rotateKey(group, removedMemberId) {
164
314
  const newKey = this.generateSymmetricKey();
165
- this.symmetricKeys.set(group.id, newKey);
166
- group.encryptedSharedKey = this.encryptKeyForMembers(group.members.map((m) => m.memberId), newKey);
315
+ const state = this.keyEpochStates.get(group.id);
316
+ if (state) {
317
+ const remainingMemberIds = group.members.map((m) => m.memberId);
318
+ const newState = this.rotateEpochState(state, newKey, remainingMemberIds, removedMemberId);
319
+ this.keyEpochStates.set(group.id, newState);
320
+ group.encryptedSharedKey = newState.encryptedEpochKeys;
321
+ }
167
322
  }
168
323
  // ─── Group lifecycle ──────────────────────────────────────────────────
169
324
  /**
170
325
  * Create a new group with the given members.
171
326
  * Generates a shared symmetric key and encrypts it for each member.
172
327
  * The creator is assigned the OWNER role; others get MEMBER.
328
+ * Uses KeyEpochManager pattern to create initial epoch 0.
173
329
  *
174
- * Requirement 10.2: group creation with symmetric key management.
330
+ * Requirements: 10.2, 5.1, 9.3
175
331
  */
176
332
  async createGroup(name, creatorId, memberIds) {
177
333
  const now = new Date();
@@ -186,30 +342,36 @@ class GroupService {
186
342
  role: id === creatorId ? communication_1.DefaultRole.OWNER : communication_1.DefaultRole.MEMBER,
187
343
  joinedAt: now,
188
344
  }));
189
- const encryptedSharedKey = this.encryptKeyForMembers(allMemberIds, symmetricKey);
345
+ // Create initial epoch state (epoch 0) with wrapped keys for all members
346
+ const epochState = this.createInitialEpochState(symmetricKey, allMemberIds);
190
347
  const group = {
191
348
  id: groupId,
192
349
  name,
193
350
  creatorId,
194
351
  members,
195
- encryptedSharedKey,
352
+ encryptedSharedKey: epochState.encryptedEpochKeys,
196
353
  createdAt: now,
197
354
  lastMessageAt: now,
198
355
  pinnedMessageIds: [],
199
356
  };
200
357
  this.groups.set(groupId, group);
201
358
  this.messages.set(groupId, []);
202
- this.symmetricKeys.set(groupId, symmetricKey);
359
+ this.keyEpochStates.set(groupId, epochState);
203
360
  // Register roles in PermissionService
204
361
  for (const member of members) {
205
362
  this.permissionService.assignRole(member.memberId, groupId, member.role);
206
363
  }
364
+ // Persist to storage provider if available
365
+ if (this.groupCollection) {
366
+ await this.groupCollection.create(group);
367
+ }
207
368
  // Emit group created event
208
369
  this.eventEmitter.emitGroupCreated(groupId, groupId, creatorId, allMemberIds);
209
370
  return group;
210
371
  }
211
372
  /**
212
373
  * Create a group from a promoted conversation, preserving message history.
374
+ * Generates a FRESH CEK (not reusing the DM key) per Requirement 5.4.
213
375
  * Used by ConversationService.promoteToGroup.
214
376
  */
215
377
  async createGroupFromConversation(conversationId, existingParticipants, newMemberIds, existingMessages, requesterId) {
@@ -217,6 +379,7 @@ class GroupService {
217
379
  ...existingParticipants,
218
380
  ...newMemberIds.filter((id) => !existingParticipants.includes(id)),
219
381
  ];
382
+ // createGroup generates a FRESH CEK — does not reuse the DM key
220
383
  const group = await this.createGroup(`Group from conversation`, requesterId, allMemberIds.filter((id) => id !== requesterId));
221
384
  group.promotedFromConversation = conversationId;
222
385
  // Migrate existing messages into the group
@@ -228,6 +391,14 @@ class GroupService {
228
391
  contextId: group.id,
229
392
  };
230
393
  groupMessages.push(migratedMsg);
394
+ // Persist migrated message to storage provider if available
395
+ if (this.groupMessageCollection) {
396
+ await this.groupMessageCollection.create(migratedMsg);
397
+ }
398
+ }
399
+ // Persist group update (promotedFromConversation) to storage provider if available
400
+ if (this.groupCollection) {
401
+ await this.groupCollection.update(group.id, group);
231
402
  }
232
403
  return group;
233
404
  }
@@ -242,52 +413,113 @@ class GroupService {
242
413
  // ─── Messaging ────────────────────────────────────────────────────────
243
414
  /**
244
415
  * Send a message to a group.
245
- * Requirement 10.2: group messaging with permission and mute checks.
416
+ * Encrypts content with the current epoch's CEK and records the keyEpoch.
417
+ * Optionally accepts attachments which are validated against platform limits.
418
+ *
419
+ * Requirements: 10.2, 9.3, 11.1, 11.2, 11.4, 11.5
246
420
  */
247
- async sendMessage(groupId, senderId, content) {
421
+ async sendMessage(groupId, senderId, content, attachments) {
248
422
  const group = this.assertGroupExists(groupId);
249
423
  this.assertIsMember(group, senderId);
250
424
  this.assertPermission(senderId, groupId, communication_1.Permission.SEND_MESSAGES);
251
425
  this.assertNotMuted(senderId, groupId);
426
+ // Validate and prepare attachment metadata before creating the message
427
+ const attachmentMetadata = attachments?.length
428
+ ? (0, attachmentUtils_1.validateAndPrepareAttachments)(attachments)
429
+ : [];
430
+ const state = this.keyEpochStates.get(groupId);
431
+ const currentEpoch = state?.currentEpoch ?? 0;
432
+ // Store content via block content store if available; otherwise use raw content
433
+ let messageContent = content;
434
+ if (this.blockContentStore) {
435
+ const memberIds = group.members.map((m) => m.memberId);
436
+ const { blockReference } = await this.blockContentStore.storeContent(content, senderId, memberIds);
437
+ messageContent = blockReference;
438
+ }
252
439
  const now = new Date();
253
440
  const message = {
254
441
  id: (0, uuid_1.v4)(),
255
442
  contextType: 'group',
256
443
  contextId: groupId,
257
444
  senderId,
258
- encryptedContent: content,
445
+ encryptedContent: messageContent,
259
446
  createdAt: now,
260
447
  editHistory: [],
261
448
  deleted: false,
262
449
  pinned: false,
263
450
  reactions: [],
451
+ keyEpoch: currentEpoch,
452
+ attachments: attachmentMetadata,
264
453
  };
265
454
  this.messages.get(groupId).push(message);
266
455
  group.lastMessageAt = now;
456
+ // Persist to storage provider if available
457
+ if (this.groupMessageCollection) {
458
+ await this.groupMessageCollection.create(message);
459
+ }
460
+ if (this.groupCollection) {
461
+ await this.groupCollection.update(groupId, group);
462
+ }
267
463
  // Emit message sent event
268
464
  this.eventEmitter.emitMessageSent('group', groupId, message.id, senderId);
269
- return message;
465
+ // Return the message with the original readable content for display,
466
+ // while the stored version retains the block reference (magnet URL)
467
+ return this.blockContentStore
468
+ ? { ...message, encryptedContent: content }
469
+ : message;
270
470
  }
271
471
  /**
272
472
  * Get messages in a group with cursor-based pagination.
473
+ * Messages are returned with their keyEpoch so clients can decrypt
474
+ * using the appropriate epoch's CEK.
475
+ * Validates that each message's keyEpoch exists in the epoch state;
476
+ * throws KeyEpochNotFoundError if a message references a non-existent epoch.
477
+ *
478
+ * Requirements: 5.1, 12.3
273
479
  */
274
480
  async getMessages(groupId, memberId, cursor, limit = 50) {
275
481
  const group = this.assertGroupExists(groupId);
276
482
  this.assertIsMember(group, memberId);
277
483
  const msgs = this.messages.get(groupId) ?? [];
278
- return (0, pagination_1.paginateItems)(msgs, cursor, limit);
484
+ // Validate that each message's keyEpoch exists in the epoch state
485
+ for (const msg of msgs) {
486
+ this.assertEpochExists(groupId, msg.keyEpoch);
487
+ }
488
+ const result = (0, pagination_1.paginateItems)(msgs, cursor, limit);
489
+ // Resolve block references (magnet URLs) back to readable content
490
+ if (this.blockContentStore) {
491
+ const resolved = await Promise.all(result.items.map(async (msg) => {
492
+ if (msg.deleted)
493
+ return msg;
494
+ try {
495
+ const contentBytes = await this.blockContentStore.retrieveContent(msg.encryptedContent);
496
+ if (contentBytes) {
497
+ return {
498
+ ...msg,
499
+ encryptedContent: new TextDecoder().decode(contentBytes),
500
+ };
501
+ }
502
+ }
503
+ catch {
504
+ // Fall back to stored value if retrieval fails
505
+ }
506
+ return msg;
507
+ }));
508
+ return { ...result, items: resolved };
509
+ }
510
+ return result;
279
511
  }
280
512
  // ─── Member management ────────────────────────────────────────────────
281
513
  /**
282
- * Add members to a group. Re-encrypts the shared key for new members.
283
- * Requirement 10.2: membership management.
514
+ * Add members to a group. Wraps ALL epoch keys for new members.
515
+ * Requirements: 10.2, 5.2
284
516
  */
285
517
  async addMembers(groupId, requesterId, memberIds) {
286
518
  const group = this.assertGroupExists(groupId);
287
519
  this.assertIsMember(group, requesterId);
288
520
  this.assertPermission(requesterId, groupId, communication_1.Permission.MANAGE_MEMBERS);
289
521
  const now = new Date();
290
- const currentKey = this.symmetricKeys.get(groupId);
522
+ const state = this.keyEpochStates.get(groupId);
291
523
  for (const memberId of memberIds) {
292
524
  if (group.members.some((m) => m.memberId === memberId)) {
293
525
  throw new MemberAlreadyInGroupError(memberId);
@@ -297,16 +529,23 @@ class GroupService {
297
529
  role: communication_1.DefaultRole.MEMBER,
298
530
  joinedAt: now,
299
531
  });
300
- // Encrypt existing key for new member
301
- group.encryptedSharedKey.set(memberId, this.encryptKey(memberId, currentKey));
532
+ // Add member to epoch state: wrap ALL epoch keys for the new member
533
+ if (state) {
534
+ this.addMemberToEpochState(state, memberId);
535
+ group.encryptedSharedKey = state.encryptedEpochKeys;
536
+ }
302
537
  this.permissionService.assignRole(memberId, groupId, communication_1.DefaultRole.MEMBER);
303
538
  // Emit member joined event
304
539
  this.eventEmitter.emitMemberJoined('group', groupId, memberId);
305
540
  }
541
+ // Persist group update to storage provider if available
542
+ if (this.groupCollection) {
543
+ await this.groupCollection.update(groupId, group);
544
+ }
306
545
  }
307
546
  /**
308
- * Remove a member from a group. Rotates the shared key.
309
- * Requirement 10.2: key rotation on member removal.
547
+ * Remove a member from a group. Rotates the shared key using epoch-aware rotation.
548
+ * Requirements: 10.2, 5.3
310
549
  */
311
550
  async removeMember(groupId, requesterId, targetId) {
312
551
  const group = this.assertGroupExists(groupId);
@@ -314,24 +553,50 @@ class GroupService {
314
553
  this.assertIsMember(group, targetId);
315
554
  this.assertPermission(requesterId, groupId, communication_1.Permission.MANAGE_MEMBERS);
316
555
  group.members = group.members.filter((m) => m.memberId !== targetId);
317
- group.encryptedSharedKey.delete(targetId);
318
- // Rotate key for remaining members
319
- this.rotateKey(group);
556
+ if (group.members.length > 0) {
557
+ this.rotateKey(group, targetId);
558
+ }
559
+ else {
560
+ // No members left — clean up epoch state
561
+ const state = this.keyEpochStates.get(groupId);
562
+ if (state) {
563
+ for (const [, epochMap] of state.encryptedEpochKeys) {
564
+ epochMap.delete(targetId);
565
+ }
566
+ group.encryptedSharedKey = state.encryptedEpochKeys;
567
+ }
568
+ }
569
+ // Persist group update to storage provider if available
570
+ if (this.groupCollection) {
571
+ await this.groupCollection.update(groupId, group);
572
+ }
320
573
  // Emit member kicked event
321
574
  this.eventEmitter.emitMemberKicked('group', groupId, targetId, requesterId);
322
575
  }
323
576
  /**
324
- * Leave a group voluntarily. Rotates the shared key.
325
- * Requirement 10.2: key rotation on member departure.
577
+ * Leave a group voluntarily. Rotates the shared key using epoch-aware rotation.
578
+ * Requirements: 10.2, 5.3
326
579
  */
327
580
  async leaveGroup(groupId, memberId) {
328
581
  const group = this.assertGroupExists(groupId);
329
582
  this.assertIsMember(group, memberId);
330
583
  group.members = group.members.filter((m) => m.memberId !== memberId);
331
- group.encryptedSharedKey.delete(memberId);
332
- // Rotate key for remaining members
333
584
  if (group.members.length > 0) {
334
- this.rotateKey(group);
585
+ this.rotateKey(group, memberId);
586
+ }
587
+ else {
588
+ // No members left — clean up epoch state
589
+ const state = this.keyEpochStates.get(groupId);
590
+ if (state) {
591
+ for (const [, epochMap] of state.encryptedEpochKeys) {
592
+ epochMap.delete(memberId);
593
+ }
594
+ group.encryptedSharedKey = state.encryptedEpochKeys;
595
+ }
596
+ }
597
+ // Persist group update to storage provider if available
598
+ if (this.groupCollection) {
599
+ await this.groupCollection.update(groupId, group);
335
600
  }
336
601
  // Emit member left event
337
602
  this.eventEmitter.emitMemberLeft('group', groupId, memberId);
@@ -345,7 +610,37 @@ class GroupService {
345
610
  const group = this.assertGroupExists(groupId);
346
611
  this.assertIsMember(group, memberId);
347
612
  const msgs = this.messages.get(groupId) ?? [];
613
+ if (this.blockContentStore) {
614
+ // Find the message manually to handle block storage
615
+ const message = msgs.find((m) => m.id === messageId);
616
+ if (!message)
617
+ throw new GroupMessageNotFoundError(messageId);
618
+ if (message.senderId !== memberId)
619
+ throw new NotMessageAuthorError();
620
+ // Store new content as a new block
621
+ const memberIds = group.members.map((m) => m.memberId);
622
+ const { blockReference } = await this.blockContentStore.storeContent(newContent, memberId, memberIds);
623
+ // Push old block reference to edit history
624
+ message.editHistory.push({
625
+ content: message.encryptedContent,
626
+ editedAt: new Date(),
627
+ });
628
+ // Update encryptedContent to new block reference
629
+ message.encryptedContent = blockReference;
630
+ message.editedAt = new Date();
631
+ // Persist edited message to storage provider if available
632
+ if (this.groupMessageCollection) {
633
+ await this.groupMessageCollection.update(messageId, message);
634
+ }
635
+ // Emit message edited event
636
+ this.eventEmitter.emitMessageEdited('group', groupId, messageId, memberId);
637
+ return message;
638
+ }
348
639
  const edited = this.messageOps.editMessage(msgs, messageId, memberId, newContent, (id) => new GroupMessageNotFoundError(id), () => new NotMessageAuthorError());
640
+ // Persist edited message to storage provider if available
641
+ if (this.groupMessageCollection) {
642
+ await this.groupMessageCollection.update(messageId, edited);
643
+ }
349
644
  // Emit message edited event
350
645
  this.eventEmitter.emitMessageEdited('group', groupId, messageId, memberId);
351
646
  return edited;
@@ -359,6 +654,13 @@ class GroupService {
359
654
  this.assertIsMember(group, memberId);
360
655
  const msgs = this.messages.get(groupId) ?? [];
361
656
  this.messageOps.deleteMessage(msgs, groupId, messageId, memberId, (id) => new GroupMessageNotFoundError(id), (p) => new GroupPermissionError(p));
657
+ // Persist deleted message to storage provider if available
658
+ if (this.groupMessageCollection) {
659
+ const deletedMsg = msgs.find((m) => m.id === messageId);
660
+ if (deletedMsg) {
661
+ await this.groupMessageCollection.update(messageId, deletedMsg);
662
+ }
663
+ }
362
664
  // Emit message deleted event
363
665
  this.eventEmitter.emitMessageDeleted('group', groupId, messageId, memberId);
364
666
  }
@@ -371,6 +673,16 @@ class GroupService {
371
673
  this.assertIsMember(group, memberId);
372
674
  const msgs = this.messages.get(groupId) ?? [];
373
675
  this.messageOps.pinMessage(msgs, groupId, messageId, memberId, group, (id) => new GroupMessageNotFoundError(id), (p) => new GroupPermissionError(p));
676
+ // Persist pin changes to storage provider if available
677
+ if (this.groupMessageCollection) {
678
+ const pinnedMsg = msgs.find((m) => m.id === messageId);
679
+ if (pinnedMsg) {
680
+ await this.groupMessageCollection.update(messageId, pinnedMsg);
681
+ }
682
+ }
683
+ if (this.groupCollection) {
684
+ await this.groupCollection.update(groupId, group);
685
+ }
374
686
  // Emit message pinned event
375
687
  this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_PINNED, 'group', groupId, messageId, memberId);
376
688
  }
@@ -383,6 +695,16 @@ class GroupService {
383
695
  this.assertIsMember(group, memberId);
384
696
  const msgs = this.messages.get(groupId) ?? [];
385
697
  this.messageOps.unpinMessage(msgs, groupId, messageId, memberId, group, (id) => new GroupMessageNotFoundError(id), (p) => new GroupPermissionError(p));
698
+ // Persist unpin changes to storage provider if available
699
+ if (this.groupMessageCollection) {
700
+ const unpinnedMsg = msgs.find((m) => m.id === messageId);
701
+ if (unpinnedMsg) {
702
+ await this.groupMessageCollection.update(messageId, unpinnedMsg);
703
+ }
704
+ }
705
+ if (this.groupCollection) {
706
+ await this.groupCollection.update(groupId, group);
707
+ }
386
708
  // Emit message unpinned event
387
709
  this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_UNPINNED, 'group', groupId, messageId, memberId);
388
710
  }
@@ -395,6 +717,13 @@ class GroupService {
395
717
  this.assertIsMember(group, memberId);
396
718
  const msgs = this.messages.get(groupId) ?? [];
397
719
  const reactionId = this.messageOps.addReaction(msgs, messageId, memberId, emoji, (id) => new GroupMessageNotFoundError(id));
720
+ // Persist reaction change to storage provider if available
721
+ if (this.groupMessageCollection) {
722
+ const msg = msgs.find((m) => m.id === messageId);
723
+ if (msg) {
724
+ await this.groupMessageCollection.update(messageId, msg);
725
+ }
726
+ }
398
727
  // Emit reaction added event
399
728
  this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_ADDED, 'group', groupId, messageId, memberId, emoji, reactionId);
400
729
  return reactionId;
@@ -408,6 +737,13 @@ class GroupService {
408
737
  this.assertIsMember(group, memberId);
409
738
  const msgs = this.messages.get(groupId) ?? [];
410
739
  this.messageOps.removeReaction(msgs, messageId, reactionId, (id) => new GroupMessageNotFoundError(id), (id) => new ReactionNotFoundError(id));
740
+ // Persist reaction removal to storage provider if available
741
+ if (this.groupMessageCollection) {
742
+ const msg = msgs.find((m) => m.id === messageId);
743
+ if (msg) {
744
+ await this.groupMessageCollection.update(messageId, msg);
745
+ }
746
+ }
411
747
  // Emit reaction removed event
412
748
  this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_REMOVED, 'group', groupId, messageId, memberId, '', // emoji not needed for removal
413
749
  reactionId);
@@ -420,10 +756,20 @@ class GroupService {
420
756
  return this.groups.get(groupId);
421
757
  }
422
758
  /**
423
- * Get the raw symmetric key for a group (testing only).
759
+ * Get the current epoch's raw symmetric key for a group.
760
+ * Returns the key for the latest epoch (backward-compatible accessor).
424
761
  */
425
762
  getSymmetricKey(groupId) {
426
- return this.symmetricKeys.get(groupId);
763
+ const state = this.keyEpochStates.get(groupId);
764
+ if (!state)
765
+ return undefined;
766
+ return state.epochKeys.get(state.currentEpoch);
767
+ }
768
+ /**
769
+ * Get the full epoch state for a group (testing / internal use).
770
+ */
771
+ getKeyEpochState(groupId) {
772
+ return this.keyEpochStates.get(groupId);
427
773
  }
428
774
  /**
429
775
  * Get all messages for a group without pagination (testing/internal).