@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
@@ -6,7 +6,17 @@
6
6
  * Supports channel creation with visibility modes, join/leave, messaging,
7
7
  * search, invite token generation/redemption, mute/kick operations.
8
8
  *
9
- * Requirements: 10.3
9
+ * Uses epoch-aware key management via IKeyEpochState for forward secrecy.
10
+ * Each key rotation creates a new epoch. Messages record which epoch they
11
+ * were encrypted under. On member removal, all epoch keys are re-wrapped
12
+ * for remaining members only.
13
+ *
14
+ * When an IChatStorageProvider is injected, channels, channel messages, and
15
+ * invite tokens are also persisted to the provider's collections (write-through).
16
+ * Sync helper methods continue to read from the in-memory Maps so their
17
+ * signatures remain unchanged.
18
+ *
19
+ * Requirements: 10.3, 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 9.1
10
20
  */
11
21
  Object.defineProperty(exports, "__esModule", { value: true });
12
22
  exports.ChannelService = exports.InviteTokenNotFoundError = exports.InviteTokenExpiredError = exports.ChannelJoinDeniedError = exports.ChannelNameConflictError = exports.MemberAlreadyInChannelError = exports.ChannelReactionNotFoundError = exports.NotMessageAuthorError = exports.ChannelMemberMutedError = exports.ChannelPermissionError = exports.ChannelMessageNotFoundError = exports.NotChannelMemberError = exports.ChannelNotFoundError = void 0;
@@ -14,9 +24,12 @@ exports.extractChannelKeyFromDefault = extractChannelKeyFromDefault;
14
24
  const uuid_1 = require("uuid");
15
25
  const platformCrypto_1 = require("../../crypto/platformCrypto");
16
26
  const communication_1 = require("../../enumerations/communication");
27
+ const attachmentUtils_1 = require("./attachmentUtils");
17
28
  const events_1 = require("../../interfaces/events");
18
29
  const pagination_1 = require("../../utils/pagination");
30
+ const encryptionErrors_1 = require("../../errors/encryptionErrors");
19
31
  const messageOperationsService_1 = require("./messageOperationsService");
32
+ const rehydrationHelpers_1 = require("./rehydrationHelpers");
20
33
  // ─── Error classes ──────────────────────────────────────────────────────────
21
34
  class ChannelNotFoundError extends Error {
22
35
  constructor(channelId) {
@@ -134,24 +147,115 @@ class ChannelService {
134
147
  channels = new Map();
135
148
  /** channelId → messages (ordered by createdAt ascending) */
136
149
  messages = new Map();
137
- /** channelId → raw symmetric key (Uint8Array) */
138
- symmetricKeys = new Map();
150
+ /** channelId → epoch-aware key state (raw keys + wrapped keys per epoch) */
151
+ keyEpochStates = new Map();
139
152
  /** token string → IInviteToken */
140
153
  inviteTokens = new Map();
141
- /** channel name (lowercase) → channelId for uniqueness */
154
+ /**
155
+ * Per-server channel name uniqueness index.
156
+ * Outer key = serverId (or '__standalone__' for channels without a server).
157
+ * Inner key = normalized channel name → channelId.
158
+ */
142
159
  nameIndex = new Map();
160
+ /** Sentinel key for channels that don't belong to any server. */
161
+ static STANDALONE_KEY = '__standalone__';
162
+ /** Whether init() has already been called (idempotency guard). */
163
+ initialized = false;
143
164
  permissionService;
144
165
  encryptKey;
145
166
  messageOps;
146
167
  eventEmitter;
147
168
  randomBytesProvider;
148
- constructor(permissionService, encryptKey = defaultKeyEncryption, messageOps, eventEmitter, randomBytesProvider) {
169
+ /** Optional persistent collection for channels (write-through). */
170
+ channelCollection;
171
+ /** Optional persistent collection for channel messages (write-through). */
172
+ channelMessageCollection;
173
+ /** Optional persistent collection for invite tokens (write-through). */
174
+ inviteTokenCollection;
175
+ /** Optional block content store for storing message content as blocks. */
176
+ blockContentStore;
177
+ constructor(permissionService, encryptKey = defaultKeyEncryption, messageOps, eventEmitter, randomBytesProvider, storageProvider, blockContentStore) {
149
178
  this.permissionService = permissionService;
150
179
  this.encryptKey = encryptKey;
151
180
  this.messageOps =
152
181
  messageOps ?? new messageOperationsService_1.MessageOperationsService(permissionService);
153
182
  this.eventEmitter = eventEmitter ?? new events_1.NullEventEmitter();
154
183
  this.randomBytesProvider = randomBytesProvider ?? platformCrypto_1.getRandomBytes;
184
+ this.blockContentStore = blockContentStore;
185
+ if (storageProvider) {
186
+ this.channelCollection = storageProvider.channels;
187
+ this.channelMessageCollection = storageProvider.channelMessages;
188
+ this.inviteTokenCollection = storageProvider.inviteTokens;
189
+ }
190
+ }
191
+ // ─── Rehydration ──────────────────────────────────────────────────────
192
+ /**
193
+ * Load persisted channels, channel messages, and invite tokens back into
194
+ * in-memory Maps, rebuilding derived indexes, key epoch states, and
195
+ * permission registrations.
196
+ *
197
+ * Idempotent: only the first call performs rehydration. No-op when no
198
+ * storage provider was supplied at construction time.
199
+ *
200
+ * Requirements: 1.3, 1.5, 1.6, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 8.1, 8.2, 8.3, 9.1, 9.3, 9.4
201
+ */
202
+ async init() {
203
+ if (this.initialized)
204
+ return;
205
+ if (!this.channelCollection)
206
+ return;
207
+ this.initialized = true;
208
+ // ── Load channels ─────────────────────────────────────────────────
209
+ let loadedChannels;
210
+ try {
211
+ loadedChannels = await this.channelCollection.findMany();
212
+ }
213
+ catch (error) {
214
+ console.error('[ChannelService] Failed to load from channels collection:', error);
215
+ throw error;
216
+ }
217
+ for (const channel of loadedChannels) {
218
+ this.channels.set(channel.id, channel);
219
+ // Rebuild name index (scoped by serverId)
220
+ this.registerName(this.nameScope(channel.serverId), channel.name.toLowerCase(), channel.id);
221
+ // Reconstruct key epoch state
222
+ const epochState = (0, rehydrationHelpers_1.reconstructKeyEpochState)(channel.encryptedSharedKey);
223
+ if (epochState) {
224
+ this.keyEpochStates.set(channel.id, epochState);
225
+ }
226
+ else {
227
+ console.warn(`[ChannelService] Skipping key epoch reconstruction for channel ${channel.id}: malformed encryptedSharedKey`);
228
+ }
229
+ // Register permissions for each member
230
+ for (const member of channel.members) {
231
+ this.permissionService.assignRole(member.memberId, channel.id, member.role);
232
+ }
233
+ }
234
+ // ── Load channel messages ───────────────────────────────────────────
235
+ let loadedMessages;
236
+ try {
237
+ loadedMessages = await this.channelMessageCollection.findMany();
238
+ }
239
+ catch (error) {
240
+ console.error('[ChannelService] Failed to load from channelMessages collection:', error);
241
+ throw error;
242
+ }
243
+ const grouped = (0, rehydrationHelpers_1.groupAndSortMessages)(loadedMessages);
244
+ for (const [contextId, msgs] of grouped) {
245
+ this.messages.set(contextId, msgs);
246
+ }
247
+ // ── Load invite tokens ──────────────────────────────────────────────
248
+ let loadedTokens;
249
+ try {
250
+ loadedTokens = await this.inviteTokenCollection.findMany();
251
+ }
252
+ catch (error) {
253
+ console.error('[ChannelService] Failed to load from inviteTokens collection:', error);
254
+ throw error;
255
+ }
256
+ for (const token of loadedTokens) {
257
+ this.inviteTokens.set(token.token, token);
258
+ }
155
259
  }
156
260
  // ─── Helpers ────────────────────────────────────────────────────────────
157
261
  assertChannelExists(channelId) {
@@ -179,32 +283,150 @@ class ChannelService {
179
283
  generateSymmetricKey() {
180
284
  return this.randomBytesProvider(32);
181
285
  }
182
- encryptKeyForMembers(memberIds, symmetricKey) {
286
+ /**
287
+ * Encrypt a symmetric key for multiple members, returning a Map<memberId, wrappedKey>.
288
+ * Wraps key wrapping failures as KeyUnwrapError with context information.
289
+ *
290
+ * Requirements: 12.3
291
+ */
292
+ encryptKeyForMembers(memberIds, symmetricKey, contextId, epoch) {
183
293
  const encrypted = new Map();
184
294
  for (const id of memberIds) {
185
- encrypted.set(id, this.encryptKey(id, symmetricKey));
295
+ try {
296
+ encrypted.set(id, this.encryptKey(id, symmetricKey));
297
+ }
298
+ catch (error) {
299
+ if (contextId !== undefined && epoch !== undefined) {
300
+ throw new encryptionErrors_1.KeyUnwrapError(contextId, id, epoch);
301
+ }
302
+ throw error;
303
+ }
186
304
  }
187
305
  return encrypted;
188
306
  }
189
- rotateKey(channel) {
307
+ /**
308
+ * Create initial epoch state (epoch 0) for a new channel.
309
+ * Uses KeyEpochManager pattern: creates epoch 0 with wrapped keys for all members.
310
+ *
311
+ * Requirements: 1.3, 2.1
312
+ */
313
+ createInitialEpochState(symmetricKey, memberIds) {
314
+ const epochKeys = new Map();
315
+ epochKeys.set(0, symmetricKey);
316
+ const wrappedKeys = this.encryptKeyForMembers(memberIds, symmetricKey);
317
+ const encryptedEpochKeys = new Map();
318
+ encryptedEpochKeys.set(0, wrappedKeys);
319
+ return { currentEpoch: 0, epochKeys, encryptedEpochKeys };
320
+ }
321
+ /**
322
+ * Add a member to an existing epoch state: wrap ALL epoch keys for the new member.
323
+ * This gives the new member access to full message history.
324
+ *
325
+ * Requirements: 2.2, 6.1, 6.3
326
+ */
327
+ addMemberToEpochState(state, newMemberId) {
328
+ for (const [epoch, rawKey] of state.epochKeys) {
329
+ const epochMap = state.encryptedEpochKeys.get(epoch) ?? new Map();
330
+ epochMap.set(newMemberId, this.encryptKey(newMemberId, rawKey));
331
+ state.encryptedEpochKeys.set(epoch, epochMap);
332
+ }
333
+ }
334
+ /**
335
+ * Rotate key: increment epoch, add new key, delete removed member from ALL epochs,
336
+ * re-wrap ALL epoch keys for remaining members.
337
+ *
338
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
339
+ */
340
+ rotateEpochState(state, newKey, remainingMemberIds, removedMemberId) {
341
+ const newEpoch = state.currentEpoch + 1;
342
+ // Add new epoch key
343
+ state.epochKeys.set(newEpoch, newKey);
344
+ // Delete removed member from ALL epochs
345
+ for (const [, memberMap] of state.encryptedEpochKeys) {
346
+ memberMap.delete(removedMemberId);
347
+ }
348
+ // Re-wrap ALL epoch keys for remaining members
349
+ for (const [epoch, rawKey] of state.epochKeys) {
350
+ state.encryptedEpochKeys.set(epoch, this.encryptKeyForMembers(remainingMemberIds, rawKey));
351
+ }
352
+ return { ...state, currentEpoch: newEpoch };
353
+ }
354
+ /**
355
+ * Rotate the channel's symmetric key using epoch-aware key management.
356
+ * Generates a new key, increments epoch, re-wraps all epoch keys for remaining members,
357
+ * and removes the departed member from all epochs.
358
+ *
359
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5
360
+ */
361
+ rotateKey(channel, removedMemberId) {
190
362
  const newKey = this.generateSymmetricKey();
191
- this.symmetricKeys.set(channel.id, newKey);
192
- channel.encryptedSharedKey = this.encryptKeyForMembers(channel.members.map((m) => m.memberId), newKey);
363
+ const state = this.keyEpochStates.get(channel.id);
364
+ if (state) {
365
+ const remainingMemberIds = channel.members.map((m) => m.memberId);
366
+ const newState = this.rotateEpochState(state, newKey, remainingMemberIds, removedMemberId);
367
+ this.keyEpochStates.set(channel.id, newState);
368
+ channel.encryptedSharedKey = newState.encryptedEpochKeys;
369
+ }
370
+ }
371
+ /**
372
+ * Assert that a key epoch exists in the epoch state for a given context.
373
+ * Throws KeyEpochNotFoundError if the epoch is not found.
374
+ *
375
+ * Requirements: 12.3
376
+ */
377
+ assertEpochExists(contextId, keyEpoch) {
378
+ const state = this.keyEpochStates.get(contextId);
379
+ if (!state || !state.epochKeys.has(keyEpoch)) {
380
+ throw new encryptionErrors_1.KeyEpochNotFoundError(contextId, keyEpoch);
381
+ }
193
382
  }
194
383
  normalizeChannelName(name) {
195
384
  return name.toLowerCase().replace(/\s+/g, '-');
196
385
  }
386
+ /** Get the nameIndex scope key for a channel's server (or standalone). */
387
+ nameScope(serverId) {
388
+ return serverId ?? ChannelService.STANDALONE_KEY;
389
+ }
390
+ /** Check if a channel name is taken within a given server scope. */
391
+ isNameTaken(scope, normalized, excludeChannelId) {
392
+ const scopeMap = this.nameIndex.get(scope);
393
+ if (!scopeMap)
394
+ return false;
395
+ const existingId = scopeMap.get(normalized);
396
+ return !!existingId && existingId !== excludeChannelId;
397
+ }
398
+ /** Register a channel name in the scoped index. */
399
+ registerName(scope, normalized, channelId) {
400
+ let scopeMap = this.nameIndex.get(scope);
401
+ if (!scopeMap) {
402
+ scopeMap = new Map();
403
+ this.nameIndex.set(scope, scopeMap);
404
+ }
405
+ scopeMap.set(normalized, channelId);
406
+ }
407
+ /** Remove a channel name from the scoped index. */
408
+ unregisterName(scope, normalized) {
409
+ const scopeMap = this.nameIndex.get(scope);
410
+ if (scopeMap) {
411
+ scopeMap.delete(normalized);
412
+ if (scopeMap.size === 0) {
413
+ this.nameIndex.delete(scope);
414
+ }
415
+ }
416
+ }
197
417
  // ─── Channel lifecycle ──────────────────────────────────────────────────
198
418
  /**
199
419
  * Create a new channel.
200
420
  * Generates a shared symmetric key and encrypts it for the creator.
201
421
  * The creator is assigned the OWNER role.
422
+ * Uses KeyEpochManager pattern to create initial epoch 0.
202
423
  *
203
- * Requirement 10.3: channel creation with visibility.
424
+ * Requirements: 10.3, 1.3, 2.1, 9.1
204
425
  */
205
- async createChannel(name, creatorId, visibility, topic = '') {
426
+ async createChannel(name, creatorId, visibility, topic = '', serverId) {
206
427
  const normalized = this.normalizeChannelName(name);
207
- if (this.nameIndex.has(normalized)) {
428
+ const scope = this.nameScope(serverId);
429
+ if (this.isNameTaken(scope, normalized)) {
208
430
  throw new ChannelNameConflictError(name);
209
431
  }
210
432
  const now = new Date();
@@ -217,7 +439,8 @@ class ChannelService {
217
439
  joinedAt: now,
218
440
  },
219
441
  ];
220
- const encryptedSharedKey = this.encryptKeyForMembers([creatorId], symmetricKey);
442
+ // Create initial epoch state (epoch 0) with wrapped key for creator
443
+ const epochState = this.createInitialEpochState(symmetricKey, [creatorId]);
221
444
  const channel = {
222
445
  id: channelId,
223
446
  name: normalized,
@@ -225,17 +448,22 @@ class ChannelService {
225
448
  creatorId,
226
449
  visibility,
227
450
  members,
228
- encryptedSharedKey,
451
+ encryptedSharedKey: epochState.encryptedEpochKeys,
229
452
  createdAt: now,
230
453
  lastMessageAt: now,
231
454
  pinnedMessageIds: [],
232
455
  historyVisibleToNewMembers: true,
456
+ serverId,
233
457
  };
234
458
  this.channels.set(channelId, channel);
235
459
  this.messages.set(channelId, []);
236
- this.symmetricKeys.set(channelId, symmetricKey);
237
- this.nameIndex.set(normalized, channelId);
460
+ this.keyEpochStates.set(channelId, epochState);
461
+ this.registerName(scope, normalized, channelId);
238
462
  this.permissionService.assignRole(creatorId, channelId, communication_1.DefaultRole.OWNER);
463
+ // Persist to storage provider if available
464
+ if (this.channelCollection) {
465
+ await this.channelCollection.create(channel);
466
+ }
239
467
  return channel;
240
468
  }
241
469
  /**
@@ -274,13 +502,13 @@ class ChannelService {
274
502
  this.assertPermission(requesterId, channelId, communication_1.Permission.MANAGE_CHANNEL);
275
503
  if (updates.name !== undefined) {
276
504
  const normalized = this.normalizeChannelName(updates.name);
277
- const existingId = this.nameIndex.get(normalized);
278
- if (existingId && existingId !== channelId) {
505
+ const scope = this.nameScope(channel.serverId);
506
+ if (this.isNameTaken(scope, normalized, channelId)) {
279
507
  throw new ChannelNameConflictError(updates.name);
280
508
  }
281
- this.nameIndex.delete(channel.name);
509
+ this.unregisterName(scope, channel.name);
282
510
  channel.name = normalized;
283
- this.nameIndex.set(normalized, channelId);
511
+ this.registerName(scope, normalized, channelId);
284
512
  }
285
513
  if (updates.topic !== undefined) {
286
514
  channel.topic = updates.topic;
@@ -292,6 +520,10 @@ class ChannelService {
292
520
  channel.historyVisibleToNewMembers = updates.historyVisibleToNewMembers;
293
521
  }
294
522
  this.eventEmitter.emitChannelUpdated(channelId, channelId, requesterId);
523
+ // Persist channel update to storage provider if available
524
+ if (this.channelCollection) {
525
+ await this.channelCollection.update(channelId, channel);
526
+ }
295
527
  return channel;
296
528
  }
297
529
  /**
@@ -301,16 +533,21 @@ class ChannelService {
301
533
  const channel = this.assertChannelExists(channelId);
302
534
  this.assertIsMember(channel, requesterId);
303
535
  this.assertPermission(requesterId, channelId, communication_1.Permission.MANAGE_CHANNEL);
304
- this.nameIndex.delete(channel.name);
536
+ this.unregisterName(this.nameScope(channel.serverId), channel.name);
305
537
  this.channels.delete(channelId);
306
538
  this.messages.delete(channelId);
307
- this.symmetricKeys.delete(channelId);
539
+ this.keyEpochStates.delete(channelId);
540
+ // Persist deletion to storage provider if available
541
+ if (this.channelCollection) {
542
+ await this.channelCollection.delete(channelId);
543
+ }
308
544
  }
309
545
  // ─── Join / Leave ─────────────────────────────────────────────────────
310
546
  /**
311
547
  * Join a public channel. Rejects invite-only channels.
548
+ * Wraps ALL epoch keys for the new member so they can read history.
312
549
  *
313
- * Requirement 10.3: add member, reject invite-only without invitation.
550
+ * Requirements: 10.3, 2.2
314
551
  */
315
552
  async joinChannel(channelId, memberId) {
316
553
  const channel = this.assertChannelExists(channelId);
@@ -326,60 +563,211 @@ class ChannelService {
326
563
  role: communication_1.DefaultRole.MEMBER,
327
564
  joinedAt: now,
328
565
  });
329
- const currentKey = this.symmetricKeys.get(channelId);
330
- channel.encryptedSharedKey.set(memberId, this.encryptKey(memberId, currentKey));
566
+ // Add member to epoch state: wrap ALL epoch keys for the new member
567
+ const state = this.keyEpochStates.get(channelId);
568
+ if (state) {
569
+ this.addMemberToEpochState(state, memberId);
570
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
571
+ }
572
+ this.permissionService.assignRole(memberId, channelId, communication_1.DefaultRole.MEMBER);
573
+ // Persist channel update to storage provider if available
574
+ if (this.channelCollection) {
575
+ await this.channelCollection.update(channelId, channel);
576
+ }
577
+ this.eventEmitter.emitMemberJoined('channel', channelId, memberId);
578
+ }
579
+ /**
580
+ * Remove a member from a channel (server-level operation).
581
+ * Bypasses permission checks — used by ServerService when a member is removed
582
+ * from a server so that key rotation is performed for each affected channel.
583
+ * Silently skips if the member is not in the channel.
584
+ *
585
+ * Requirements: 7.1, 7.2, 7.3
586
+ */
587
+ async removeMemberFromChannel(channelId, memberId) {
588
+ const channel = this.assertChannelExists(channelId);
589
+ // Skip if not a member
590
+ if (!channel.members.some((m) => m.memberId === memberId)) {
591
+ return;
592
+ }
593
+ channel.members = channel.members.filter((m) => m.memberId !== memberId);
594
+ if (channel.members.length > 0) {
595
+ this.rotateKey(channel, memberId);
596
+ }
597
+ else {
598
+ // No members left — clean up epoch state
599
+ const state = this.keyEpochStates.get(channelId);
600
+ if (state) {
601
+ for (const [, epochMap] of state.encryptedEpochKeys) {
602
+ epochMap.delete(memberId);
603
+ }
604
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
605
+ }
606
+ }
607
+ // Persist channel update to storage provider if available
608
+ if (this.channelCollection) {
609
+ await this.channelCollection.update(channelId, channel);
610
+ }
611
+ this.eventEmitter.emitMemberLeft('channel', channelId, memberId);
612
+ }
613
+ /**
614
+ * Add a member to a channel (server-level operation).
615
+ * Bypasses visibility checks — used by ServerService when a member joins a server
616
+ * so that all epoch keys are wrapped for the new member across all server channels.
617
+ * Silently skips if the member is already in the channel.
618
+ *
619
+ * Requirements: 6.1, 6.2, 6.3
620
+ */
621
+ async addMemberToChannel(channelId, memberId) {
622
+ const channel = this.assertChannelExists(channelId);
623
+ // Skip if already a member
624
+ if (channel.members.some((m) => m.memberId === memberId)) {
625
+ return;
626
+ }
627
+ const now = new Date();
628
+ channel.members.push({
629
+ memberId,
630
+ role: communication_1.DefaultRole.MEMBER,
631
+ joinedAt: now,
632
+ });
633
+ // Add member to epoch state: wrap ALL epoch keys for the new member
634
+ const state = this.keyEpochStates.get(channelId);
635
+ if (state) {
636
+ this.addMemberToEpochState(state, memberId);
637
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
638
+ }
331
639
  this.permissionService.assignRole(memberId, channelId, communication_1.DefaultRole.MEMBER);
640
+ // Persist channel update to storage provider if available
641
+ if (this.channelCollection) {
642
+ await this.channelCollection.update(channelId, channel);
643
+ }
332
644
  this.eventEmitter.emitMemberJoined('channel', channelId, memberId);
333
645
  }
334
646
  /**
335
647
  * Leave a channel voluntarily. Rotates the shared key.
648
+ *
649
+ * Requirements: 3.5
336
650
  */
337
651
  async leaveChannel(channelId, memberId) {
338
652
  const channel = this.assertChannelExists(channelId);
339
653
  this.assertIsMember(channel, memberId);
340
654
  channel.members = channel.members.filter((m) => m.memberId !== memberId);
341
- channel.encryptedSharedKey.delete(memberId);
342
655
  if (channel.members.length > 0) {
343
- this.rotateKey(channel);
656
+ this.rotateKey(channel, memberId);
657
+ }
658
+ else {
659
+ // No members left — clean up epoch state
660
+ const state = this.keyEpochStates.get(channelId);
661
+ if (state) {
662
+ for (const [, epochMap] of state.encryptedEpochKeys) {
663
+ epochMap.delete(memberId);
664
+ }
665
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
666
+ }
667
+ }
668
+ // Persist channel update to storage provider if available
669
+ if (this.channelCollection) {
670
+ await this.channelCollection.update(channelId, channel);
344
671
  }
345
672
  this.eventEmitter.emitMemberLeft('channel', channelId, memberId);
346
673
  }
347
674
  // ─── Messaging ────────────────────────────────────────────────────────
348
675
  /**
349
676
  * Send a message to a channel.
350
- * Requirement 10.3: messaging with permission and mute checks.
677
+ * Encrypts content with the current epoch's CEK and records the keyEpoch.
678
+ * Optionally accepts attachments which are validated against platform limits.
679
+ *
680
+ * Requirements: 10.3, 1.1, 1.4, 9.1, 11.1, 11.2, 11.4, 11.5
351
681
  */
352
- async sendMessage(channelId, senderId, content) {
682
+ async sendMessage(channelId, senderId, content, attachments) {
353
683
  const channel = this.assertChannelExists(channelId);
354
684
  this.assertIsMember(channel, senderId);
355
685
  this.assertPermission(senderId, channelId, communication_1.Permission.SEND_MESSAGES);
356
686
  this.assertNotMuted(senderId, channelId);
687
+ // Validate and prepare attachment metadata before creating the message
688
+ const attachmentMetadata = attachments?.length
689
+ ? (0, attachmentUtils_1.validateAndPrepareAttachments)(attachments)
690
+ : [];
691
+ const state = this.keyEpochStates.get(channelId);
692
+ const currentEpoch = state?.currentEpoch ?? 0;
693
+ // Store content via block content store if available; otherwise use raw content
694
+ let messageContent = content;
695
+ if (this.blockContentStore) {
696
+ const memberIds = channel.members.map((m) => m.memberId);
697
+ const { blockReference } = await this.blockContentStore.storeContent(content, senderId, memberIds);
698
+ messageContent = blockReference;
699
+ }
357
700
  const now = new Date();
358
701
  const message = {
359
702
  id: (0, uuid_1.v4)(),
360
703
  contextType: 'channel',
361
704
  contextId: channelId,
362
705
  senderId,
363
- encryptedContent: content,
706
+ encryptedContent: messageContent,
364
707
  createdAt: now,
365
708
  editHistory: [],
366
709
  deleted: false,
367
710
  pinned: false,
368
711
  reactions: [],
712
+ keyEpoch: currentEpoch,
713
+ attachments: attachmentMetadata,
369
714
  };
370
715
  this.messages.get(channelId).push(message);
371
716
  channel.lastMessageAt = now;
717
+ // Persist to storage provider if available
718
+ if (this.channelMessageCollection) {
719
+ await this.channelMessageCollection.create(message);
720
+ }
721
+ if (this.channelCollection) {
722
+ await this.channelCollection.update(channelId, channel);
723
+ }
372
724
  this.eventEmitter.emitMessageSent('channel', channelId, message.id, senderId);
373
- return message;
725
+ // Return the message with the original readable content for display,
726
+ // while the stored version retains the block reference (magnet URL)
727
+ return this.blockContentStore
728
+ ? { ...message, encryptedContent: content }
729
+ : message;
374
730
  }
375
731
  /**
376
732
  * Get messages in a channel with cursor-based pagination.
733
+ * Messages are returned with their keyEpoch so clients can decrypt
734
+ * using the appropriate epoch's CEK.
735
+ * Validates that each message's keyEpoch exists in the epoch state;
736
+ * throws KeyEpochNotFoundError if a message references a non-existent epoch.
737
+ *
738
+ * Requirements: 1.2, 12.3
377
739
  */
378
740
  async getMessages(channelId, memberId, cursor, limit = 50) {
379
741
  const channel = this.assertChannelExists(channelId);
380
742
  this.assertIsMember(channel, memberId);
381
743
  const msgs = this.messages.get(channelId) ?? [];
382
- return (0, pagination_1.paginateItems)(msgs, cursor, limit);
744
+ // Validate that each message's keyEpoch exists in the epoch state
745
+ for (const msg of msgs) {
746
+ this.assertEpochExists(channelId, msg.keyEpoch);
747
+ }
748
+ const result = (0, pagination_1.paginateItems)(msgs, cursor, limit);
749
+ // Resolve block references (magnet URLs) back to readable content
750
+ if (this.blockContentStore) {
751
+ const resolved = await Promise.all(result.items.map(async (msg) => {
752
+ if (msg.deleted)
753
+ return msg;
754
+ try {
755
+ const contentBytes = await this.blockContentStore.retrieveContent(msg.encryptedContent);
756
+ if (contentBytes) {
757
+ return {
758
+ ...msg,
759
+ encryptedContent: new TextDecoder().decode(contentBytes),
760
+ };
761
+ }
762
+ }
763
+ catch {
764
+ // Fall back to stored value if retrieval fails
765
+ }
766
+ return msg;
767
+ }));
768
+ return { ...result, items: resolved };
769
+ }
770
+ return result;
383
771
  }
384
772
  /**
385
773
  * Search messages in a channel by keyword.
@@ -389,6 +777,23 @@ class ChannelService {
389
777
  this.assertIsMember(channel, memberId);
390
778
  const msgs = this.messages.get(channelId) ?? [];
391
779
  const lowerQuery = query.toLowerCase();
780
+ if (this.blockContentStore) {
781
+ // Retrieve content from block store for each non-deleted message before searching
782
+ const matching = [];
783
+ for (const m of msgs) {
784
+ if (m.deleted)
785
+ continue;
786
+ const contentBytes = await this.blockContentStore.retrieveContent(m.encryptedContent);
787
+ if (contentBytes) {
788
+ const contentText = new TextDecoder().decode(contentBytes);
789
+ if (contentText.toLowerCase().includes(lowerQuery)) {
790
+ matching.push(m);
791
+ }
792
+ }
793
+ }
794
+ return (0, pagination_1.paginateItems)(matching, cursor, limit);
795
+ }
796
+ // Fall back to current direct string search when block store is absent
392
797
  const matching = msgs.filter((m) => !m.deleted &&
393
798
  typeof m.encryptedContent === 'string' &&
394
799
  m.encryptedContent.toLowerCase().includes(lowerQuery));
@@ -416,12 +821,17 @@ class ChannelService {
416
821
  currentUses: 0,
417
822
  };
418
823
  this.inviteTokens.set(token.token, token);
824
+ // Persist invite token to storage provider if available
825
+ if (this.inviteTokenCollection) {
826
+ await this.inviteTokenCollection.create(token);
827
+ }
419
828
  return token;
420
829
  }
421
830
  /**
422
831
  * Redeem an invite token to join a channel.
832
+ * Wraps ALL epoch keys for the joining member.
423
833
  *
424
- * Requirement 10.3: validate expiry and usage limits.
834
+ * Requirements: 10.3, 2.2
425
835
  */
426
836
  async redeemInvite(token, memberId) {
427
837
  const invite = this.inviteTokens.get(token);
@@ -444,10 +854,21 @@ class ChannelService {
444
854
  role: communication_1.DefaultRole.MEMBER,
445
855
  joinedAt: now,
446
856
  });
447
- const currentKey = this.symmetricKeys.get(channel.id);
448
- channel.encryptedSharedKey.set(memberId, this.encryptKey(memberId, currentKey));
857
+ // Add member to epoch state: wrap ALL epoch keys for the new member
858
+ const state = this.keyEpochStates.get(channel.id);
859
+ if (state) {
860
+ this.addMemberToEpochState(state, memberId);
861
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
862
+ }
449
863
  this.permissionService.assignRole(memberId, channel.id, communication_1.DefaultRole.MEMBER);
450
864
  invite.currentUses++;
865
+ // Persist changes to storage provider if available
866
+ if (this.channelCollection) {
867
+ await this.channelCollection.update(channel.id, channel);
868
+ }
869
+ if (this.inviteTokenCollection) {
870
+ await this.inviteTokenCollection.update(invite.token, invite);
871
+ }
451
872
  this.eventEmitter.emitMemberJoined('channel', channel.id, memberId);
452
873
  }
453
874
  // ─── Moderation ───────────────────────────────────────────────────────
@@ -467,13 +888,17 @@ class ChannelService {
467
888
  if (member) {
468
889
  member.mutedUntil = new Date(Date.now() + durationMs);
469
890
  }
891
+ // Persist channel update to storage provider if available
892
+ if (this.channelCollection) {
893
+ await this.channelCollection.update(channelId, channel);
894
+ }
470
895
  this.eventEmitter.emitMemberMuted('channel', channelId, targetId, requesterId, durationMs);
471
896
  }
472
897
  /**
473
- * Kick a member from a channel. Rotates encryption keys.
898
+ * Kick a member from a channel. Rotates encryption keys using epoch-aware rotation.
474
899
  * Requires KICK_MEMBERS permission.
475
900
  *
476
- * Requirement 10.3: remove member and rotate keys.
901
+ * Requirements: 10.3, 3.1, 3.2, 3.3, 3.4
477
902
  */
478
903
  async kickMember(channelId, requesterId, targetId) {
479
904
  const channel = this.assertChannelExists(channelId);
@@ -481,9 +906,22 @@ class ChannelService {
481
906
  this.assertIsMember(channel, targetId);
482
907
  this.assertPermission(requesterId, channelId, communication_1.Permission.KICK_MEMBERS);
483
908
  channel.members = channel.members.filter((m) => m.memberId !== targetId);
484
- channel.encryptedSharedKey.delete(targetId);
485
909
  if (channel.members.length > 0) {
486
- this.rotateKey(channel);
910
+ this.rotateKey(channel, targetId);
911
+ }
912
+ else {
913
+ // No members left — clean up epoch state
914
+ const state = this.keyEpochStates.get(channelId);
915
+ if (state) {
916
+ for (const [, epochMap] of state.encryptedEpochKeys) {
917
+ epochMap.delete(targetId);
918
+ }
919
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
920
+ }
921
+ }
922
+ // Persist channel update to storage provider if available
923
+ if (this.channelCollection) {
924
+ await this.channelCollection.update(channelId, channel);
487
925
  }
488
926
  this.eventEmitter.emitMemberKicked('channel', channelId, targetId, requesterId);
489
927
  }
@@ -496,7 +934,36 @@ class ChannelService {
496
934
  const channel = this.assertChannelExists(channelId);
497
935
  this.assertIsMember(channel, memberId);
498
936
  const msgs = this.messages.get(channelId) ?? [];
937
+ if (this.blockContentStore) {
938
+ // Find the message manually to handle block storage
939
+ const message = msgs.find((m) => m.id === messageId);
940
+ if (!message)
941
+ throw new ChannelMessageNotFoundError(messageId);
942
+ if (message.senderId !== memberId)
943
+ throw new NotMessageAuthorError();
944
+ // Store new content as a new block
945
+ const memberIds = channel.members.map((m) => m.memberId);
946
+ const { blockReference } = await this.blockContentStore.storeContent(newContent, memberId, memberIds);
947
+ // Push old block reference to edit history
948
+ message.editHistory.push({
949
+ content: message.encryptedContent,
950
+ editedAt: new Date(),
951
+ });
952
+ // Update encryptedContent to new block reference
953
+ message.encryptedContent = blockReference;
954
+ message.editedAt = new Date();
955
+ // Persist edited message to storage provider if available
956
+ if (this.channelMessageCollection) {
957
+ await this.channelMessageCollection.update(messageId, message);
958
+ }
959
+ this.eventEmitter.emitMessageEdited('channel', channelId, messageId, memberId);
960
+ return message;
961
+ }
499
962
  const edited = this.messageOps.editMessage(msgs, messageId, memberId, newContent, (id) => new ChannelMessageNotFoundError(id), () => new NotMessageAuthorError());
963
+ // Persist edited message to storage provider if available
964
+ if (this.channelMessageCollection) {
965
+ await this.channelMessageCollection.update(messageId, edited);
966
+ }
500
967
  this.eventEmitter.emitMessageEdited('channel', channelId, messageId, memberId);
501
968
  return edited;
502
969
  }
@@ -509,6 +976,13 @@ class ChannelService {
509
976
  this.assertIsMember(channel, memberId);
510
977
  const msgs = this.messages.get(channelId) ?? [];
511
978
  this.messageOps.deleteMessage(msgs, channelId, messageId, memberId, (id) => new ChannelMessageNotFoundError(id), (p) => new ChannelPermissionError(p));
979
+ // Persist deleted message to storage provider if available
980
+ if (this.channelMessageCollection) {
981
+ const deletedMsg = msgs.find((m) => m.id === messageId);
982
+ if (deletedMsg) {
983
+ await this.channelMessageCollection.update(messageId, deletedMsg);
984
+ }
985
+ }
512
986
  this.eventEmitter.emitMessageDeleted('channel', channelId, messageId, memberId);
513
987
  }
514
988
  /**
@@ -520,6 +994,16 @@ class ChannelService {
520
994
  this.assertIsMember(channel, memberId);
521
995
  const msgs = this.messages.get(channelId) ?? [];
522
996
  this.messageOps.pinMessage(msgs, channelId, messageId, memberId, channel, (id) => new ChannelMessageNotFoundError(id), (p) => new ChannelPermissionError(p));
997
+ // Persist pin changes to storage provider if available
998
+ if (this.channelMessageCollection) {
999
+ const pinnedMsg = msgs.find((m) => m.id === messageId);
1000
+ if (pinnedMsg) {
1001
+ await this.channelMessageCollection.update(messageId, pinnedMsg);
1002
+ }
1003
+ }
1004
+ if (this.channelCollection) {
1005
+ await this.channelCollection.update(channelId, channel);
1006
+ }
523
1007
  this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_PINNED, 'channel', channelId, messageId, memberId);
524
1008
  }
525
1009
  /**
@@ -531,6 +1015,16 @@ class ChannelService {
531
1015
  this.assertIsMember(channel, memberId);
532
1016
  const msgs = this.messages.get(channelId) ?? [];
533
1017
  this.messageOps.unpinMessage(msgs, channelId, messageId, memberId, channel, (id) => new ChannelMessageNotFoundError(id), (p) => new ChannelPermissionError(p));
1018
+ // Persist unpin changes to storage provider if available
1019
+ if (this.channelMessageCollection) {
1020
+ const unpinnedMsg = msgs.find((m) => m.id === messageId);
1021
+ if (unpinnedMsg) {
1022
+ await this.channelMessageCollection.update(messageId, unpinnedMsg);
1023
+ }
1024
+ }
1025
+ if (this.channelCollection) {
1026
+ await this.channelCollection.update(channelId, channel);
1027
+ }
534
1028
  this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_UNPINNED, 'channel', channelId, messageId, memberId);
535
1029
  }
536
1030
  /**
@@ -542,6 +1036,13 @@ class ChannelService {
542
1036
  this.assertIsMember(channel, memberId);
543
1037
  const msgs = this.messages.get(channelId) ?? [];
544
1038
  const reactionId = this.messageOps.addReaction(msgs, messageId, memberId, emoji, (id) => new ChannelMessageNotFoundError(id));
1039
+ // Persist reaction change to storage provider if available
1040
+ if (this.channelMessageCollection) {
1041
+ const msg = msgs.find((m) => m.id === messageId);
1042
+ if (msg) {
1043
+ await this.channelMessageCollection.update(messageId, msg);
1044
+ }
1045
+ }
545
1046
  this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_ADDED, 'channel', channelId, messageId, memberId, emoji, reactionId);
546
1047
  return reactionId;
547
1048
  }
@@ -554,14 +1055,34 @@ class ChannelService {
554
1055
  this.assertIsMember(channel, memberId);
555
1056
  const msgs = this.messages.get(channelId) ?? [];
556
1057
  this.messageOps.removeReaction(msgs, messageId, reactionId, (id) => new ChannelMessageNotFoundError(id), (id) => new ChannelReactionNotFoundError(id));
1058
+ // Persist reaction removal to storage provider if available
1059
+ if (this.channelMessageCollection) {
1060
+ const msg = msgs.find((m) => m.id === messageId);
1061
+ if (msg) {
1062
+ await this.channelMessageCollection.update(messageId, msg);
1063
+ }
1064
+ }
557
1065
  this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_REMOVED, 'channel', channelId, messageId, memberId, '', reactionId);
558
1066
  }
559
1067
  // ─── Accessors (for testing / internal use) ───────────────────────────
560
1068
  getChannelById(channelId) {
561
1069
  return this.channels.get(channelId);
562
1070
  }
1071
+ /**
1072
+ * Get the current epoch's raw symmetric key for a channel.
1073
+ * Returns the key for the latest epoch (backward-compatible accessor).
1074
+ */
563
1075
  getSymmetricKey(channelId) {
564
- return this.symmetricKeys.get(channelId);
1076
+ const state = this.keyEpochStates.get(channelId);
1077
+ if (!state)
1078
+ return undefined;
1079
+ return state.epochKeys.get(state.currentEpoch);
1080
+ }
1081
+ /**
1082
+ * Get the full epoch state for a channel (testing / internal use).
1083
+ */
1084
+ getKeyEpochState(channelId) {
1085
+ return this.keyEpochStates.get(channelId);
565
1086
  }
566
1087
  getAllMessages(channelId) {
567
1088
  return this.messages.get(channelId) ?? [];