@brightchain/brightchain-lib 0.29.25 → 0.29.27

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 (214) 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/enumeration-translations/index.d.ts +2 -0
  7. package/src/lib/enumeration-translations/index.d.ts.map +1 -1
  8. package/src/lib/enumeration-translations/index.js +1 -0
  9. package/src/lib/enumeration-translations/index.js.map +1 -1
  10. package/src/lib/enumeration-translations/memberStatusType.d.ts +5 -0
  11. package/src/lib/enumeration-translations/memberStatusType.d.ts.map +1 -0
  12. package/src/lib/enumeration-translations/memberStatusType.js +58 -0
  13. package/src/lib/enumeration-translations/memberStatusType.js.map +1 -0
  14. package/src/lib/enumerations/brightChainStrings.d.ts +52 -0
  15. package/src/lib/enumerations/brightChainStrings.d.ts.map +1 -1
  16. package/src/lib/enumerations/brightChainStrings.js +54 -0
  17. package/src/lib/enumerations/brightChainStrings.js.map +1 -1
  18. package/src/lib/enumerations/brightchainFeatures.d.ts +1 -0
  19. package/src/lib/enumerations/brightchainFeatures.d.ts.map +1 -1
  20. package/src/lib/enumerations/brightchainFeatures.js +1 -0
  21. package/src/lib/enumerations/brightchainFeatures.js.map +1 -1
  22. package/src/lib/enumerations/communication.d.ts +7 -1
  23. package/src/lib/enumerations/communication.d.ts.map +1 -1
  24. package/src/lib/enumerations/communication.js +6 -0
  25. package/src/lib/enumerations/communication.js.map +1 -1
  26. package/src/lib/enumerations/messaging/emailErrorType.d.ts +15 -2
  27. package/src/lib/enumerations/messaging/emailErrorType.d.ts.map +1 -1
  28. package/src/lib/enumerations/messaging/emailErrorType.js +17 -1
  29. package/src/lib/enumerations/messaging/emailErrorType.js.map +1 -1
  30. package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts +4 -2
  31. package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts.map +1 -1
  32. package/src/lib/enumerations/messaging/messageEncryptionScheme.js +3 -1
  33. package/src/lib/enumerations/messaging/messageEncryptionScheme.js.map +1 -1
  34. package/src/lib/errors/encryptionErrors.d.ts +71 -0
  35. package/src/lib/errors/encryptionErrors.d.ts.map +1 -0
  36. package/src/lib/errors/encryptionErrors.js +112 -0
  37. package/src/lib/errors/encryptionErrors.js.map +1 -0
  38. package/src/lib/errors/index.d.ts +10 -0
  39. package/src/lib/errors/index.d.ts.map +1 -1
  40. package/src/lib/errors/index.js +13 -0
  41. package/src/lib/errors/index.js.map +1 -1
  42. package/src/lib/i18n/i18n-setup.d.ts +2 -2
  43. package/src/lib/i18n/i18n-setup.d.ts.map +1 -1
  44. package/src/lib/i18n/strings/englishUK.d.ts +2 -2
  45. package/src/lib/i18n/strings/englishUK.d.ts.map +1 -1
  46. package/src/lib/i18n/strings/englishUK.js.map +1 -1
  47. package/src/lib/i18n/strings/englishUs.d.ts +2 -2
  48. package/src/lib/i18n/strings/englishUs.d.ts.map +1 -1
  49. package/src/lib/i18n/strings/englishUs.js +55 -1
  50. package/src/lib/i18n/strings/englishUs.js.map +1 -1
  51. package/src/lib/i18n/strings/french.d.ts +2 -2
  52. package/src/lib/i18n/strings/french.d.ts.map +1 -1
  53. package/src/lib/i18n/strings/french.js +68 -13
  54. package/src/lib/i18n/strings/french.js.map +1 -1
  55. package/src/lib/i18n/strings/german.d.ts +2 -2
  56. package/src/lib/i18n/strings/german.d.ts.map +1 -1
  57. package/src/lib/i18n/strings/german.js +67 -12
  58. package/src/lib/i18n/strings/german.js.map +1 -1
  59. package/src/lib/i18n/strings/japanese.d.ts +2 -2
  60. package/src/lib/i18n/strings/japanese.d.ts.map +1 -1
  61. package/src/lib/i18n/strings/japanese.js +67 -12
  62. package/src/lib/i18n/strings/japanese.js.map +1 -1
  63. package/src/lib/i18n/strings/mandarin.d.ts +2 -2
  64. package/src/lib/i18n/strings/mandarin.d.ts.map +1 -1
  65. package/src/lib/i18n/strings/mandarin.js +67 -12
  66. package/src/lib/i18n/strings/mandarin.js.map +1 -1
  67. package/src/lib/i18n/strings/spanish.d.ts +2 -2
  68. package/src/lib/i18n/strings/spanish.d.ts.map +1 -1
  69. package/src/lib/i18n/strings/spanish.js +67 -12
  70. package/src/lib/i18n/strings/spanish.js.map +1 -1
  71. package/src/lib/i18n/strings/ukrainian.d.ts +2 -2
  72. package/src/lib/i18n/strings/ukrainian.d.ts.map +1 -1
  73. package/src/lib/i18n/strings/ukrainian.js +67 -12
  74. package/src/lib/i18n/strings/ukrainian.js.map +1 -1
  75. package/src/lib/index.d.ts +31 -0
  76. package/src/lib/index.d.ts.map +1 -1
  77. package/src/lib/index.js +29 -1
  78. package/src/lib/index.js.map +1 -1
  79. package/src/lib/interfaces/auth/writeProof.d.ts +2 -0
  80. package/src/lib/interfaces/auth/writeProof.d.ts.map +1 -1
  81. package/src/lib/interfaces/auth/writeProofUtils.d.ts +1 -1
  82. package/src/lib/interfaces/auth/writeProofUtils.d.ts.map +1 -1
  83. package/src/lib/interfaces/auth/writeProofUtils.js +2 -2
  84. package/src/lib/interfaces/auth/writeProofUtils.js.map +1 -1
  85. package/src/lib/interfaces/availability/gossipService.d.ts +99 -1
  86. package/src/lib/interfaces/availability/gossipService.d.ts.map +1 -1
  87. package/src/lib/interfaces/availability/gossipService.js +4 -0
  88. package/src/lib/interfaces/availability/gossipService.js.map +1 -1
  89. package/src/lib/interfaces/communication/blockContentStore.d.ts +57 -0
  90. package/src/lib/interfaces/communication/blockContentStore.d.ts.map +1 -0
  91. package/src/lib/interfaces/communication/blockContentStore.js +21 -0
  92. package/src/lib/interfaces/communication/blockContentStore.js.map +1 -0
  93. package/src/lib/interfaces/communication/chatStorageProvider.d.ts +77 -0
  94. package/src/lib/interfaces/communication/chatStorageProvider.d.ts.map +1 -0
  95. package/src/lib/interfaces/communication/chatStorageProvider.js +25 -0
  96. package/src/lib/interfaces/communication/chatStorageProvider.js.map +1 -0
  97. package/src/lib/interfaces/communication/index.d.ts +4 -0
  98. package/src/lib/interfaces/communication/index.d.ts.map +1 -0
  99. package/src/lib/interfaces/communication/index.js +3 -0
  100. package/src/lib/interfaces/communication/index.js.map +1 -0
  101. package/src/lib/interfaces/communication/server.d.ts +57 -0
  102. package/src/lib/interfaces/communication/server.d.ts.map +1 -0
  103. package/src/lib/interfaces/communication/server.js +14 -0
  104. package/src/lib/interfaces/communication/server.js.map +1 -0
  105. package/src/lib/interfaces/communication.d.ts +60 -5
  106. package/src/lib/interfaces/communication.d.ts.map +1 -1
  107. package/src/lib/interfaces/communication.js +8 -0
  108. package/src/lib/interfaces/communication.js.map +1 -1
  109. package/src/lib/interfaces/communicationEvents.d.ts +59 -1
  110. package/src/lib/interfaces/communicationEvents.d.ts.map +1 -1
  111. package/src/lib/interfaces/events/communicationEventEmitter.d.ts +9 -0
  112. package/src/lib/interfaces/events/communicationEventEmitter.d.ts.map +1 -1
  113. package/src/lib/interfaces/events/communicationEventEmitter.js +9 -0
  114. package/src/lib/interfaces/events/communicationEventEmitter.js.map +1 -1
  115. package/src/lib/interfaces/index.d.ts +4 -1
  116. package/src/lib/interfaces/index.d.ts.map +1 -1
  117. package/src/lib/interfaces/index.js +3 -0
  118. package/src/lib/interfaces/index.js.map +1 -1
  119. package/src/lib/interfaces/messaging/gpgKey.d.ts +93 -0
  120. package/src/lib/interfaces/messaging/gpgKey.d.ts.map +1 -0
  121. package/src/lib/interfaces/messaging/gpgKey.js +12 -0
  122. package/src/lib/interfaces/messaging/gpgKey.js.map +1 -0
  123. package/src/lib/interfaces/messaging/index.d.ts +4 -0
  124. package/src/lib/interfaces/messaging/index.d.ts.map +1 -1
  125. package/src/lib/interfaces/messaging/index.js +4 -0
  126. package/src/lib/interfaces/messaging/index.js.map +1 -1
  127. package/src/lib/interfaces/messaging/keyStore.d.ts +100 -0
  128. package/src/lib/interfaces/messaging/keyStore.d.ts.map +1 -0
  129. package/src/lib/interfaces/messaging/keyStore.js +13 -0
  130. package/src/lib/interfaces/messaging/keyStore.js.map +1 -0
  131. package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts +92 -0
  132. package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts.map +1 -0
  133. package/src/lib/interfaces/messaging/recipientKeyResolver.js +13 -0
  134. package/src/lib/interfaces/messaging/recipientKeyResolver.js.map +1 -0
  135. package/src/lib/interfaces/messaging/smimeCertificate.d.ts +99 -0
  136. package/src/lib/interfaces/messaging/smimeCertificate.d.ts.map +1 -0
  137. package/src/lib/interfaces/messaging/smimeCertificate.js +12 -0
  138. package/src/lib/interfaces/messaging/smimeCertificate.js.map +1 -0
  139. package/src/lib/interfaces/responses/adminDashboardResponse.d.ts +8 -0
  140. package/src/lib/interfaces/responses/adminDashboardResponse.d.ts.map +1 -1
  141. package/src/lib/interfaces/responses/communicationResponses.d.ts +25 -0
  142. package/src/lib/interfaces/responses/communicationResponses.d.ts.map +1 -1
  143. package/src/lib/services/blockService.d.ts +11 -3
  144. package/src/lib/services/blockService.d.ts.map +1 -1
  145. package/src/lib/services/blockService.js +22 -2
  146. package/src/lib/services/blockService.js.map +1 -1
  147. package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts +108 -0
  148. package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts.map +1 -0
  149. package/src/lib/services/communication/__tests__/mockChatStorageProvider.js +284 -0
  150. package/src/lib/services/communication/__tests__/mockChatStorageProvider.js.map +1 -0
  151. package/src/lib/services/communication/attachmentUtils.d.ts +20 -0
  152. package/src/lib/services/communication/attachmentUtils.d.ts.map +1 -0
  153. package/src/lib/services/communication/attachmentUtils.js +43 -0
  154. package/src/lib/services/communication/attachmentUtils.js.map +1 -0
  155. package/src/lib/services/communication/channelService.d.ts +127 -12
  156. package/src/lib/services/communication/channelService.d.ts.map +1 -1
  157. package/src/lib/services/communication/channelService.js +486 -30
  158. package/src/lib/services/communication/channelService.js.map +1 -1
  159. package/src/lib/services/communication/conversationService.d.ts +91 -4
  160. package/src/lib/services/communication/conversationService.d.ts.map +1 -1
  161. package/src/lib/services/communication/conversationService.js +237 -5
  162. package/src/lib/services/communication/conversationService.js.map +1 -1
  163. package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts +36 -0
  164. package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts.map +1 -0
  165. package/src/lib/services/communication/eciesKeyEncryptionHandler.js +30 -0
  166. package/src/lib/services/communication/eciesKeyEncryptionHandler.js.map +1 -0
  167. package/src/lib/services/communication/groupService.d.ts +99 -21
  168. package/src/lib/services/communication/groupService.d.ts.map +1 -1
  169. package/src/lib/services/communication/groupService.js +359 -39
  170. package/src/lib/services/communication/groupService.js.map +1 -1
  171. package/src/lib/services/communication/index.d.ts +6 -1
  172. package/src/lib/services/communication/index.d.ts.map +1 -1
  173. package/src/lib/services/communication/index.js +20 -1
  174. package/src/lib/services/communication/index.js.map +1 -1
  175. package/src/lib/services/communication/keyEpochManager.d.ts +41 -0
  176. package/src/lib/services/communication/keyEpochManager.d.ts.map +1 -0
  177. package/src/lib/services/communication/keyEpochManager.js +59 -0
  178. package/src/lib/services/communication/keyEpochManager.js.map +1 -0
  179. package/src/lib/services/communication/rehydrationHelpers.d.ts +32 -0
  180. package/src/lib/services/communication/rehydrationHelpers.d.ts.map +1 -0
  181. package/src/lib/services/communication/rehydrationHelpers.js +58 -0
  182. package/src/lib/services/communication/rehydrationHelpers.js.map +1 -0
  183. package/src/lib/services/communication/serverService.d.ts +193 -0
  184. package/src/lib/services/communication/serverService.d.ts.map +1 -0
  185. package/src/lib/services/communication/serverService.js +497 -0
  186. package/src/lib/services/communication/serverService.js.map +1 -0
  187. package/src/lib/services/memberStore.d.ts +10 -0
  188. package/src/lib/services/memberStore.d.ts.map +1 -1
  189. package/src/lib/services/memberStore.js +52 -20
  190. package/src/lib/services/memberStore.js.map +1 -1
  191. package/src/lib/services/messaging/emailEncryptionService.d.ts +162 -0
  192. package/src/lib/services/messaging/emailEncryptionService.d.ts.map +1 -1
  193. package/src/lib/services/messaging/emailEncryptionService.js +293 -0
  194. package/src/lib/services/messaging/emailEncryptionService.js.map +1 -1
  195. package/src/lib/services/messaging/emailMessageService.d.ts +64 -0
  196. package/src/lib/services/messaging/emailMessageService.d.ts.map +1 -1
  197. package/src/lib/services/messaging/emailMessageService.js +142 -13
  198. package/src/lib/services/messaging/emailMessageService.js.map +1 -1
  199. package/src/lib/services/messaging/gpgKeyManager.d.ts +130 -0
  200. package/src/lib/services/messaging/gpgKeyManager.d.ts.map +1 -0
  201. package/src/lib/services/messaging/gpgKeyManager.js +381 -0
  202. package/src/lib/services/messaging/gpgKeyManager.js.map +1 -0
  203. package/src/lib/services/messaging/index.d.ts +3 -0
  204. package/src/lib/services/messaging/index.d.ts.map +1 -1
  205. package/src/lib/services/messaging/index.js +3 -0
  206. package/src/lib/services/messaging/index.js.map +1 -1
  207. package/src/lib/services/messaging/recipientKeyResolver.d.ts +47 -0
  208. package/src/lib/services/messaging/recipientKeyResolver.d.ts.map +1 -0
  209. package/src/lib/services/messaging/recipientKeyResolver.js +132 -0
  210. package/src/lib/services/messaging/recipientKeyResolver.js.map +1 -0
  211. package/src/lib/services/messaging/smimeCertificateManager.d.ts +207 -0
  212. package/src/lib/services/messaging/smimeCertificateManager.d.ts.map +1 -0
  213. package/src/lib/services/messaging/smimeCertificateManager.js +696 -0
  214. package/src/lib/services/messaging/smimeCertificateManager.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,109 @@ 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
154
  /** channel name (lowercase) → channelId for uniqueness */
142
155
  nameIndex = new Map();
156
+ /** Whether init() has already been called (idempotency guard). */
157
+ initialized = false;
143
158
  permissionService;
144
159
  encryptKey;
145
160
  messageOps;
146
161
  eventEmitter;
147
162
  randomBytesProvider;
148
- constructor(permissionService, encryptKey = defaultKeyEncryption, messageOps, eventEmitter, randomBytesProvider) {
163
+ /** Optional persistent collection for channels (write-through). */
164
+ channelCollection;
165
+ /** Optional persistent collection for channel messages (write-through). */
166
+ channelMessageCollection;
167
+ /** Optional persistent collection for invite tokens (write-through). */
168
+ inviteTokenCollection;
169
+ /** Optional block content store for storing message content as blocks. */
170
+ blockContentStore;
171
+ constructor(permissionService, encryptKey = defaultKeyEncryption, messageOps, eventEmitter, randomBytesProvider, storageProvider, blockContentStore) {
149
172
  this.permissionService = permissionService;
150
173
  this.encryptKey = encryptKey;
151
174
  this.messageOps =
152
175
  messageOps ?? new messageOperationsService_1.MessageOperationsService(permissionService);
153
176
  this.eventEmitter = eventEmitter ?? new events_1.NullEventEmitter();
154
177
  this.randomBytesProvider = randomBytesProvider ?? platformCrypto_1.getRandomBytes;
178
+ this.blockContentStore = blockContentStore;
179
+ if (storageProvider) {
180
+ this.channelCollection = storageProvider.channels;
181
+ this.channelMessageCollection = storageProvider.channelMessages;
182
+ this.inviteTokenCollection = storageProvider.inviteTokens;
183
+ }
184
+ }
185
+ // ─── Rehydration ──────────────────────────────────────────────────────
186
+ /**
187
+ * Load persisted channels, channel messages, and invite tokens back into
188
+ * in-memory Maps, rebuilding derived indexes, key epoch states, and
189
+ * permission registrations.
190
+ *
191
+ * Idempotent: only the first call performs rehydration. No-op when no
192
+ * storage provider was supplied at construction time.
193
+ *
194
+ * 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
195
+ */
196
+ async init() {
197
+ if (this.initialized)
198
+ return;
199
+ if (!this.channelCollection)
200
+ return;
201
+ this.initialized = true;
202
+ // ── Load channels ─────────────────────────────────────────────────
203
+ let loadedChannels;
204
+ try {
205
+ loadedChannels = await this.channelCollection.findMany();
206
+ }
207
+ catch (error) {
208
+ console.error('[ChannelService] Failed to load from channels collection:', error);
209
+ throw error;
210
+ }
211
+ for (const channel of loadedChannels) {
212
+ this.channels.set(channel.id, channel);
213
+ // Rebuild name index
214
+ this.nameIndex.set(channel.name.toLowerCase(), channel.id);
215
+ // Reconstruct key epoch state
216
+ const epochState = (0, rehydrationHelpers_1.reconstructKeyEpochState)(channel.encryptedSharedKey);
217
+ if (epochState) {
218
+ this.keyEpochStates.set(channel.id, epochState);
219
+ }
220
+ else {
221
+ console.warn(`[ChannelService] Skipping key epoch reconstruction for channel ${channel.id}: malformed encryptedSharedKey`);
222
+ }
223
+ // Register permissions for each member
224
+ for (const member of channel.members) {
225
+ this.permissionService.assignRole(member.memberId, channel.id, member.role);
226
+ }
227
+ }
228
+ // ── Load channel messages ───────────────────────────────────────────
229
+ let loadedMessages;
230
+ try {
231
+ loadedMessages = await this.channelMessageCollection.findMany();
232
+ }
233
+ catch (error) {
234
+ console.error('[ChannelService] Failed to load from channelMessages collection:', error);
235
+ throw error;
236
+ }
237
+ const grouped = (0, rehydrationHelpers_1.groupAndSortMessages)(loadedMessages);
238
+ for (const [contextId, msgs] of grouped) {
239
+ this.messages.set(contextId, msgs);
240
+ }
241
+ // ── Load invite tokens ──────────────────────────────────────────────
242
+ let loadedTokens;
243
+ try {
244
+ loadedTokens = await this.inviteTokenCollection.findMany();
245
+ }
246
+ catch (error) {
247
+ console.error('[ChannelService] Failed to load from inviteTokens collection:', error);
248
+ throw error;
249
+ }
250
+ for (const token of loadedTokens) {
251
+ this.inviteTokens.set(token.token, token);
252
+ }
155
253
  }
156
254
  // ─── Helpers ────────────────────────────────────────────────────────────
157
255
  assertChannelExists(channelId) {
@@ -179,17 +277,102 @@ class ChannelService {
179
277
  generateSymmetricKey() {
180
278
  return this.randomBytesProvider(32);
181
279
  }
182
- encryptKeyForMembers(memberIds, symmetricKey) {
280
+ /**
281
+ * Encrypt a symmetric key for multiple members, returning a Map<memberId, wrappedKey>.
282
+ * Wraps key wrapping failures as KeyUnwrapError with context information.
283
+ *
284
+ * Requirements: 12.3
285
+ */
286
+ encryptKeyForMembers(memberIds, symmetricKey, contextId, epoch) {
183
287
  const encrypted = new Map();
184
288
  for (const id of memberIds) {
185
- encrypted.set(id, this.encryptKey(id, symmetricKey));
289
+ try {
290
+ encrypted.set(id, this.encryptKey(id, symmetricKey));
291
+ }
292
+ catch (error) {
293
+ if (contextId !== undefined && epoch !== undefined) {
294
+ throw new encryptionErrors_1.KeyUnwrapError(contextId, id, epoch);
295
+ }
296
+ throw error;
297
+ }
186
298
  }
187
299
  return encrypted;
188
300
  }
189
- rotateKey(channel) {
301
+ /**
302
+ * Create initial epoch state (epoch 0) for a new channel.
303
+ * Uses KeyEpochManager pattern: creates epoch 0 with wrapped keys for all members.
304
+ *
305
+ * Requirements: 1.3, 2.1
306
+ */
307
+ createInitialEpochState(symmetricKey, memberIds) {
308
+ const epochKeys = new Map();
309
+ epochKeys.set(0, symmetricKey);
310
+ const wrappedKeys = this.encryptKeyForMembers(memberIds, symmetricKey);
311
+ const encryptedEpochKeys = new Map();
312
+ encryptedEpochKeys.set(0, wrappedKeys);
313
+ return { currentEpoch: 0, epochKeys, encryptedEpochKeys };
314
+ }
315
+ /**
316
+ * Add a member to an existing epoch state: wrap ALL epoch keys for the new member.
317
+ * This gives the new member access to full message history.
318
+ *
319
+ * Requirements: 2.2, 6.1, 6.3
320
+ */
321
+ addMemberToEpochState(state, newMemberId) {
322
+ for (const [epoch, rawKey] of state.epochKeys) {
323
+ const epochMap = state.encryptedEpochKeys.get(epoch) ?? new Map();
324
+ epochMap.set(newMemberId, this.encryptKey(newMemberId, rawKey));
325
+ state.encryptedEpochKeys.set(epoch, epochMap);
326
+ }
327
+ }
328
+ /**
329
+ * Rotate key: increment epoch, add new key, delete removed member from ALL epochs,
330
+ * re-wrap ALL epoch keys for remaining members.
331
+ *
332
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
333
+ */
334
+ rotateEpochState(state, newKey, remainingMemberIds, removedMemberId) {
335
+ const newEpoch = state.currentEpoch + 1;
336
+ // Add new epoch key
337
+ state.epochKeys.set(newEpoch, newKey);
338
+ // Delete removed member from ALL epochs
339
+ for (const [, memberMap] of state.encryptedEpochKeys) {
340
+ memberMap.delete(removedMemberId);
341
+ }
342
+ // Re-wrap ALL epoch keys for remaining members
343
+ for (const [epoch, rawKey] of state.epochKeys) {
344
+ state.encryptedEpochKeys.set(epoch, this.encryptKeyForMembers(remainingMemberIds, rawKey));
345
+ }
346
+ return { ...state, currentEpoch: newEpoch };
347
+ }
348
+ /**
349
+ * Rotate the channel's symmetric key using epoch-aware key management.
350
+ * Generates a new key, increments epoch, re-wraps all epoch keys for remaining members,
351
+ * and removes the departed member from all epochs.
352
+ *
353
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5
354
+ */
355
+ rotateKey(channel, removedMemberId) {
190
356
  const newKey = this.generateSymmetricKey();
191
- this.symmetricKeys.set(channel.id, newKey);
192
- channel.encryptedSharedKey = this.encryptKeyForMembers(channel.members.map((m) => m.memberId), newKey);
357
+ const state = this.keyEpochStates.get(channel.id);
358
+ if (state) {
359
+ const remainingMemberIds = channel.members.map((m) => m.memberId);
360
+ const newState = this.rotateEpochState(state, newKey, remainingMemberIds, removedMemberId);
361
+ this.keyEpochStates.set(channel.id, newState);
362
+ channel.encryptedSharedKey = newState.encryptedEpochKeys;
363
+ }
364
+ }
365
+ /**
366
+ * Assert that a key epoch exists in the epoch state for a given context.
367
+ * Throws KeyEpochNotFoundError if the epoch is not found.
368
+ *
369
+ * Requirements: 12.3
370
+ */
371
+ assertEpochExists(contextId, keyEpoch) {
372
+ const state = this.keyEpochStates.get(contextId);
373
+ if (!state || !state.epochKeys.has(keyEpoch)) {
374
+ throw new encryptionErrors_1.KeyEpochNotFoundError(contextId, keyEpoch);
375
+ }
193
376
  }
194
377
  normalizeChannelName(name) {
195
378
  return name.toLowerCase().replace(/\s+/g, '-');
@@ -199,8 +382,9 @@ class ChannelService {
199
382
  * Create a new channel.
200
383
  * Generates a shared symmetric key and encrypts it for the creator.
201
384
  * The creator is assigned the OWNER role.
385
+ * Uses KeyEpochManager pattern to create initial epoch 0.
202
386
  *
203
- * Requirement 10.3: channel creation with visibility.
387
+ * Requirements: 10.3, 1.3, 2.1, 9.1
204
388
  */
205
389
  async createChannel(name, creatorId, visibility, topic = '') {
206
390
  const normalized = this.normalizeChannelName(name);
@@ -217,7 +401,8 @@ class ChannelService {
217
401
  joinedAt: now,
218
402
  },
219
403
  ];
220
- const encryptedSharedKey = this.encryptKeyForMembers([creatorId], symmetricKey);
404
+ // Create initial epoch state (epoch 0) with wrapped key for creator
405
+ const epochState = this.createInitialEpochState(symmetricKey, [creatorId]);
221
406
  const channel = {
222
407
  id: channelId,
223
408
  name: normalized,
@@ -225,7 +410,7 @@ class ChannelService {
225
410
  creatorId,
226
411
  visibility,
227
412
  members,
228
- encryptedSharedKey,
413
+ encryptedSharedKey: epochState.encryptedEpochKeys,
229
414
  createdAt: now,
230
415
  lastMessageAt: now,
231
416
  pinnedMessageIds: [],
@@ -233,9 +418,13 @@ class ChannelService {
233
418
  };
234
419
  this.channels.set(channelId, channel);
235
420
  this.messages.set(channelId, []);
236
- this.symmetricKeys.set(channelId, symmetricKey);
421
+ this.keyEpochStates.set(channelId, epochState);
237
422
  this.nameIndex.set(normalized, channelId);
238
423
  this.permissionService.assignRole(creatorId, channelId, communication_1.DefaultRole.OWNER);
424
+ // Persist to storage provider if available
425
+ if (this.channelCollection) {
426
+ await this.channelCollection.create(channel);
427
+ }
239
428
  return channel;
240
429
  }
241
430
  /**
@@ -292,6 +481,10 @@ class ChannelService {
292
481
  channel.historyVisibleToNewMembers = updates.historyVisibleToNewMembers;
293
482
  }
294
483
  this.eventEmitter.emitChannelUpdated(channelId, channelId, requesterId);
484
+ // Persist channel update to storage provider if available
485
+ if (this.channelCollection) {
486
+ await this.channelCollection.update(channelId, channel);
487
+ }
295
488
  return channel;
296
489
  }
297
490
  /**
@@ -304,13 +497,18 @@ class ChannelService {
304
497
  this.nameIndex.delete(channel.name);
305
498
  this.channels.delete(channelId);
306
499
  this.messages.delete(channelId);
307
- this.symmetricKeys.delete(channelId);
500
+ this.keyEpochStates.delete(channelId);
501
+ // Persist deletion to storage provider if available
502
+ if (this.channelCollection) {
503
+ await this.channelCollection.delete(channelId);
504
+ }
308
505
  }
309
506
  // ─── Join / Leave ─────────────────────────────────────────────────────
310
507
  /**
311
508
  * Join a public channel. Rejects invite-only channels.
509
+ * Wraps ALL epoch keys for the new member so they can read history.
312
510
  *
313
- * Requirement 10.3: add member, reject invite-only without invitation.
511
+ * Requirements: 10.3, 2.2
314
512
  */
315
513
  async joinChannel(channelId, memberId) {
316
514
  const channel = this.assertChannelExists(channelId);
@@ -326,59 +524,184 @@ class ChannelService {
326
524
  role: communication_1.DefaultRole.MEMBER,
327
525
  joinedAt: now,
328
526
  });
329
- const currentKey = this.symmetricKeys.get(channelId);
330
- channel.encryptedSharedKey.set(memberId, this.encryptKey(memberId, currentKey));
527
+ // Add member to epoch state: wrap ALL epoch keys for the new member
528
+ const state = this.keyEpochStates.get(channelId);
529
+ if (state) {
530
+ this.addMemberToEpochState(state, memberId);
531
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
532
+ }
533
+ this.permissionService.assignRole(memberId, channelId, communication_1.DefaultRole.MEMBER);
534
+ // Persist channel update to storage provider if available
535
+ if (this.channelCollection) {
536
+ await this.channelCollection.update(channelId, channel);
537
+ }
538
+ this.eventEmitter.emitMemberJoined('channel', channelId, memberId);
539
+ }
540
+ /**
541
+ * Remove a member from a channel (server-level operation).
542
+ * Bypasses permission checks — used by ServerService when a member is removed
543
+ * from a server so that key rotation is performed for each affected channel.
544
+ * Silently skips if the member is not in the channel.
545
+ *
546
+ * Requirements: 7.1, 7.2, 7.3
547
+ */
548
+ async removeMemberFromChannel(channelId, memberId) {
549
+ const channel = this.assertChannelExists(channelId);
550
+ // Skip if not a member
551
+ if (!channel.members.some((m) => m.memberId === memberId)) {
552
+ return;
553
+ }
554
+ channel.members = channel.members.filter((m) => m.memberId !== memberId);
555
+ if (channel.members.length > 0) {
556
+ this.rotateKey(channel, memberId);
557
+ }
558
+ else {
559
+ // No members left — clean up epoch state
560
+ const state = this.keyEpochStates.get(channelId);
561
+ if (state) {
562
+ for (const [, epochMap] of state.encryptedEpochKeys) {
563
+ epochMap.delete(memberId);
564
+ }
565
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
566
+ }
567
+ }
568
+ // Persist channel update to storage provider if available
569
+ if (this.channelCollection) {
570
+ await this.channelCollection.update(channelId, channel);
571
+ }
572
+ this.eventEmitter.emitMemberLeft('channel', channelId, memberId);
573
+ }
574
+ /**
575
+ * Add a member to a channel (server-level operation).
576
+ * Bypasses visibility checks — used by ServerService when a member joins a server
577
+ * so that all epoch keys are wrapped for the new member across all server channels.
578
+ * Silently skips if the member is already in the channel.
579
+ *
580
+ * Requirements: 6.1, 6.2, 6.3
581
+ */
582
+ async addMemberToChannel(channelId, memberId) {
583
+ const channel = this.assertChannelExists(channelId);
584
+ // Skip if already a member
585
+ if (channel.members.some((m) => m.memberId === memberId)) {
586
+ return;
587
+ }
588
+ const now = new Date();
589
+ channel.members.push({
590
+ memberId,
591
+ role: communication_1.DefaultRole.MEMBER,
592
+ joinedAt: now,
593
+ });
594
+ // Add member to epoch state: wrap ALL epoch keys for the new member
595
+ const state = this.keyEpochStates.get(channelId);
596
+ if (state) {
597
+ this.addMemberToEpochState(state, memberId);
598
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
599
+ }
331
600
  this.permissionService.assignRole(memberId, channelId, communication_1.DefaultRole.MEMBER);
601
+ // Persist channel update to storage provider if available
602
+ if (this.channelCollection) {
603
+ await this.channelCollection.update(channelId, channel);
604
+ }
332
605
  this.eventEmitter.emitMemberJoined('channel', channelId, memberId);
333
606
  }
334
607
  /**
335
608
  * Leave a channel voluntarily. Rotates the shared key.
609
+ *
610
+ * Requirements: 3.5
336
611
  */
337
612
  async leaveChannel(channelId, memberId) {
338
613
  const channel = this.assertChannelExists(channelId);
339
614
  this.assertIsMember(channel, memberId);
340
615
  channel.members = channel.members.filter((m) => m.memberId !== memberId);
341
- channel.encryptedSharedKey.delete(memberId);
342
616
  if (channel.members.length > 0) {
343
- this.rotateKey(channel);
617
+ this.rotateKey(channel, memberId);
618
+ }
619
+ else {
620
+ // No members left — clean up epoch state
621
+ const state = this.keyEpochStates.get(channelId);
622
+ if (state) {
623
+ for (const [, epochMap] of state.encryptedEpochKeys) {
624
+ epochMap.delete(memberId);
625
+ }
626
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
627
+ }
628
+ }
629
+ // Persist channel update to storage provider if available
630
+ if (this.channelCollection) {
631
+ await this.channelCollection.update(channelId, channel);
344
632
  }
345
633
  this.eventEmitter.emitMemberLeft('channel', channelId, memberId);
346
634
  }
347
635
  // ─── Messaging ────────────────────────────────────────────────────────
348
636
  /**
349
637
  * Send a message to a channel.
350
- * Requirement 10.3: messaging with permission and mute checks.
638
+ * Encrypts content with the current epoch's CEK and records the keyEpoch.
639
+ * Optionally accepts attachments which are validated against platform limits.
640
+ *
641
+ * Requirements: 10.3, 1.1, 1.4, 9.1, 11.1, 11.2, 11.4, 11.5
351
642
  */
352
- async sendMessage(channelId, senderId, content) {
643
+ async sendMessage(channelId, senderId, content, attachments) {
353
644
  const channel = this.assertChannelExists(channelId);
354
645
  this.assertIsMember(channel, senderId);
355
646
  this.assertPermission(senderId, channelId, communication_1.Permission.SEND_MESSAGES);
356
647
  this.assertNotMuted(senderId, channelId);
648
+ // Validate and prepare attachment metadata before creating the message
649
+ const attachmentMetadata = attachments?.length
650
+ ? (0, attachmentUtils_1.validateAndPrepareAttachments)(attachments)
651
+ : [];
652
+ const state = this.keyEpochStates.get(channelId);
653
+ const currentEpoch = state?.currentEpoch ?? 0;
654
+ // Store content via block content store if available; otherwise use raw content
655
+ let messageContent = content;
656
+ if (this.blockContentStore) {
657
+ const memberIds = channel.members.map((m) => m.memberId);
658
+ const { blockReference } = await this.blockContentStore.storeContent(content, senderId, memberIds);
659
+ messageContent = blockReference;
660
+ }
357
661
  const now = new Date();
358
662
  const message = {
359
663
  id: (0, uuid_1.v4)(),
360
664
  contextType: 'channel',
361
665
  contextId: channelId,
362
666
  senderId,
363
- encryptedContent: content,
667
+ encryptedContent: messageContent,
364
668
  createdAt: now,
365
669
  editHistory: [],
366
670
  deleted: false,
367
671
  pinned: false,
368
672
  reactions: [],
673
+ keyEpoch: currentEpoch,
674
+ attachments: attachmentMetadata,
369
675
  };
370
676
  this.messages.get(channelId).push(message);
371
677
  channel.lastMessageAt = now;
678
+ // Persist to storage provider if available
679
+ if (this.channelMessageCollection) {
680
+ await this.channelMessageCollection.create(message);
681
+ }
682
+ if (this.channelCollection) {
683
+ await this.channelCollection.update(channelId, channel);
684
+ }
372
685
  this.eventEmitter.emitMessageSent('channel', channelId, message.id, senderId);
373
686
  return message;
374
687
  }
375
688
  /**
376
689
  * Get messages in a channel with cursor-based pagination.
690
+ * Messages are returned with their keyEpoch so clients can decrypt
691
+ * using the appropriate epoch's CEK.
692
+ * Validates that each message's keyEpoch exists in the epoch state;
693
+ * throws KeyEpochNotFoundError if a message references a non-existent epoch.
694
+ *
695
+ * Requirements: 1.2, 12.3
377
696
  */
378
697
  async getMessages(channelId, memberId, cursor, limit = 50) {
379
698
  const channel = this.assertChannelExists(channelId);
380
699
  this.assertIsMember(channel, memberId);
381
700
  const msgs = this.messages.get(channelId) ?? [];
701
+ // Validate that each message's keyEpoch exists in the epoch state
702
+ for (const msg of msgs) {
703
+ this.assertEpochExists(channelId, msg.keyEpoch);
704
+ }
382
705
  return (0, pagination_1.paginateItems)(msgs, cursor, limit);
383
706
  }
384
707
  /**
@@ -389,6 +712,23 @@ class ChannelService {
389
712
  this.assertIsMember(channel, memberId);
390
713
  const msgs = this.messages.get(channelId) ?? [];
391
714
  const lowerQuery = query.toLowerCase();
715
+ if (this.blockContentStore) {
716
+ // Retrieve content from block store for each non-deleted message before searching
717
+ const matching = [];
718
+ for (const m of msgs) {
719
+ if (m.deleted)
720
+ continue;
721
+ const contentBytes = await this.blockContentStore.retrieveContent(m.encryptedContent);
722
+ if (contentBytes) {
723
+ const contentText = new TextDecoder().decode(contentBytes);
724
+ if (contentText.toLowerCase().includes(lowerQuery)) {
725
+ matching.push(m);
726
+ }
727
+ }
728
+ }
729
+ return (0, pagination_1.paginateItems)(matching, cursor, limit);
730
+ }
731
+ // Fall back to current direct string search when block store is absent
392
732
  const matching = msgs.filter((m) => !m.deleted &&
393
733
  typeof m.encryptedContent === 'string' &&
394
734
  m.encryptedContent.toLowerCase().includes(lowerQuery));
@@ -416,12 +756,17 @@ class ChannelService {
416
756
  currentUses: 0,
417
757
  };
418
758
  this.inviteTokens.set(token.token, token);
759
+ // Persist invite token to storage provider if available
760
+ if (this.inviteTokenCollection) {
761
+ await this.inviteTokenCollection.create(token);
762
+ }
419
763
  return token;
420
764
  }
421
765
  /**
422
766
  * Redeem an invite token to join a channel.
767
+ * Wraps ALL epoch keys for the joining member.
423
768
  *
424
- * Requirement 10.3: validate expiry and usage limits.
769
+ * Requirements: 10.3, 2.2
425
770
  */
426
771
  async redeemInvite(token, memberId) {
427
772
  const invite = this.inviteTokens.get(token);
@@ -444,10 +789,21 @@ class ChannelService {
444
789
  role: communication_1.DefaultRole.MEMBER,
445
790
  joinedAt: now,
446
791
  });
447
- const currentKey = this.symmetricKeys.get(channel.id);
448
- channel.encryptedSharedKey.set(memberId, this.encryptKey(memberId, currentKey));
792
+ // Add member to epoch state: wrap ALL epoch keys for the new member
793
+ const state = this.keyEpochStates.get(channel.id);
794
+ if (state) {
795
+ this.addMemberToEpochState(state, memberId);
796
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
797
+ }
449
798
  this.permissionService.assignRole(memberId, channel.id, communication_1.DefaultRole.MEMBER);
450
799
  invite.currentUses++;
800
+ // Persist changes to storage provider if available
801
+ if (this.channelCollection) {
802
+ await this.channelCollection.update(channel.id, channel);
803
+ }
804
+ if (this.inviteTokenCollection) {
805
+ await this.inviteTokenCollection.update(invite.token, invite);
806
+ }
451
807
  this.eventEmitter.emitMemberJoined('channel', channel.id, memberId);
452
808
  }
453
809
  // ─── Moderation ───────────────────────────────────────────────────────
@@ -467,13 +823,17 @@ class ChannelService {
467
823
  if (member) {
468
824
  member.mutedUntil = new Date(Date.now() + durationMs);
469
825
  }
826
+ // Persist channel update to storage provider if available
827
+ if (this.channelCollection) {
828
+ await this.channelCollection.update(channelId, channel);
829
+ }
470
830
  this.eventEmitter.emitMemberMuted('channel', channelId, targetId, requesterId, durationMs);
471
831
  }
472
832
  /**
473
- * Kick a member from a channel. Rotates encryption keys.
833
+ * Kick a member from a channel. Rotates encryption keys using epoch-aware rotation.
474
834
  * Requires KICK_MEMBERS permission.
475
835
  *
476
- * Requirement 10.3: remove member and rotate keys.
836
+ * Requirements: 10.3, 3.1, 3.2, 3.3, 3.4
477
837
  */
478
838
  async kickMember(channelId, requesterId, targetId) {
479
839
  const channel = this.assertChannelExists(channelId);
@@ -481,9 +841,22 @@ class ChannelService {
481
841
  this.assertIsMember(channel, targetId);
482
842
  this.assertPermission(requesterId, channelId, communication_1.Permission.KICK_MEMBERS);
483
843
  channel.members = channel.members.filter((m) => m.memberId !== targetId);
484
- channel.encryptedSharedKey.delete(targetId);
485
844
  if (channel.members.length > 0) {
486
- this.rotateKey(channel);
845
+ this.rotateKey(channel, targetId);
846
+ }
847
+ else {
848
+ // No members left — clean up epoch state
849
+ const state = this.keyEpochStates.get(channelId);
850
+ if (state) {
851
+ for (const [, epochMap] of state.encryptedEpochKeys) {
852
+ epochMap.delete(targetId);
853
+ }
854
+ channel.encryptedSharedKey = state.encryptedEpochKeys;
855
+ }
856
+ }
857
+ // Persist channel update to storage provider if available
858
+ if (this.channelCollection) {
859
+ await this.channelCollection.update(channelId, channel);
487
860
  }
488
861
  this.eventEmitter.emitMemberKicked('channel', channelId, targetId, requesterId);
489
862
  }
@@ -496,7 +869,36 @@ class ChannelService {
496
869
  const channel = this.assertChannelExists(channelId);
497
870
  this.assertIsMember(channel, memberId);
498
871
  const msgs = this.messages.get(channelId) ?? [];
872
+ if (this.blockContentStore) {
873
+ // Find the message manually to handle block storage
874
+ const message = msgs.find((m) => m.id === messageId);
875
+ if (!message)
876
+ throw new ChannelMessageNotFoundError(messageId);
877
+ if (message.senderId !== memberId)
878
+ throw new NotMessageAuthorError();
879
+ // Store new content as a new block
880
+ const memberIds = channel.members.map((m) => m.memberId);
881
+ const { blockReference } = await this.blockContentStore.storeContent(newContent, memberId, memberIds);
882
+ // Push old block reference to edit history
883
+ message.editHistory.push({
884
+ content: message.encryptedContent,
885
+ editedAt: new Date(),
886
+ });
887
+ // Update encryptedContent to new block reference
888
+ message.encryptedContent = blockReference;
889
+ message.editedAt = new Date();
890
+ // Persist edited message to storage provider if available
891
+ if (this.channelMessageCollection) {
892
+ await this.channelMessageCollection.update(messageId, message);
893
+ }
894
+ this.eventEmitter.emitMessageEdited('channel', channelId, messageId, memberId);
895
+ return message;
896
+ }
499
897
  const edited = this.messageOps.editMessage(msgs, messageId, memberId, newContent, (id) => new ChannelMessageNotFoundError(id), () => new NotMessageAuthorError());
898
+ // Persist edited message to storage provider if available
899
+ if (this.channelMessageCollection) {
900
+ await this.channelMessageCollection.update(messageId, edited);
901
+ }
500
902
  this.eventEmitter.emitMessageEdited('channel', channelId, messageId, memberId);
501
903
  return edited;
502
904
  }
@@ -509,6 +911,13 @@ class ChannelService {
509
911
  this.assertIsMember(channel, memberId);
510
912
  const msgs = this.messages.get(channelId) ?? [];
511
913
  this.messageOps.deleteMessage(msgs, channelId, messageId, memberId, (id) => new ChannelMessageNotFoundError(id), (p) => new ChannelPermissionError(p));
914
+ // Persist deleted message to storage provider if available
915
+ if (this.channelMessageCollection) {
916
+ const deletedMsg = msgs.find((m) => m.id === messageId);
917
+ if (deletedMsg) {
918
+ await this.channelMessageCollection.update(messageId, deletedMsg);
919
+ }
920
+ }
512
921
  this.eventEmitter.emitMessageDeleted('channel', channelId, messageId, memberId);
513
922
  }
514
923
  /**
@@ -520,6 +929,16 @@ class ChannelService {
520
929
  this.assertIsMember(channel, memberId);
521
930
  const msgs = this.messages.get(channelId) ?? [];
522
931
  this.messageOps.pinMessage(msgs, channelId, messageId, memberId, channel, (id) => new ChannelMessageNotFoundError(id), (p) => new ChannelPermissionError(p));
932
+ // Persist pin changes to storage provider if available
933
+ if (this.channelMessageCollection) {
934
+ const pinnedMsg = msgs.find((m) => m.id === messageId);
935
+ if (pinnedMsg) {
936
+ await this.channelMessageCollection.update(messageId, pinnedMsg);
937
+ }
938
+ }
939
+ if (this.channelCollection) {
940
+ await this.channelCollection.update(channelId, channel);
941
+ }
523
942
  this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_PINNED, 'channel', channelId, messageId, memberId);
524
943
  }
525
944
  /**
@@ -531,6 +950,16 @@ class ChannelService {
531
950
  this.assertIsMember(channel, memberId);
532
951
  const msgs = this.messages.get(channelId) ?? [];
533
952
  this.messageOps.unpinMessage(msgs, channelId, messageId, memberId, channel, (id) => new ChannelMessageNotFoundError(id), (p) => new ChannelPermissionError(p));
953
+ // Persist unpin changes to storage provider if available
954
+ if (this.channelMessageCollection) {
955
+ const unpinnedMsg = msgs.find((m) => m.id === messageId);
956
+ if (unpinnedMsg) {
957
+ await this.channelMessageCollection.update(messageId, unpinnedMsg);
958
+ }
959
+ }
960
+ if (this.channelCollection) {
961
+ await this.channelCollection.update(channelId, channel);
962
+ }
534
963
  this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_UNPINNED, 'channel', channelId, messageId, memberId);
535
964
  }
536
965
  /**
@@ -542,6 +971,13 @@ class ChannelService {
542
971
  this.assertIsMember(channel, memberId);
543
972
  const msgs = this.messages.get(channelId) ?? [];
544
973
  const reactionId = this.messageOps.addReaction(msgs, messageId, memberId, emoji, (id) => new ChannelMessageNotFoundError(id));
974
+ // Persist reaction change to storage provider if available
975
+ if (this.channelMessageCollection) {
976
+ const msg = msgs.find((m) => m.id === messageId);
977
+ if (msg) {
978
+ await this.channelMessageCollection.update(messageId, msg);
979
+ }
980
+ }
545
981
  this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_ADDED, 'channel', channelId, messageId, memberId, emoji, reactionId);
546
982
  return reactionId;
547
983
  }
@@ -554,14 +990,34 @@ class ChannelService {
554
990
  this.assertIsMember(channel, memberId);
555
991
  const msgs = this.messages.get(channelId) ?? [];
556
992
  this.messageOps.removeReaction(msgs, messageId, reactionId, (id) => new ChannelMessageNotFoundError(id), (id) => new ChannelReactionNotFoundError(id));
993
+ // Persist reaction removal to storage provider if available
994
+ if (this.channelMessageCollection) {
995
+ const msg = msgs.find((m) => m.id === messageId);
996
+ if (msg) {
997
+ await this.channelMessageCollection.update(messageId, msg);
998
+ }
999
+ }
557
1000
  this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_REMOVED, 'channel', channelId, messageId, memberId, '', reactionId);
558
1001
  }
559
1002
  // ─── Accessors (for testing / internal use) ───────────────────────────
560
1003
  getChannelById(channelId) {
561
1004
  return this.channels.get(channelId);
562
1005
  }
1006
+ /**
1007
+ * Get the current epoch's raw symmetric key for a channel.
1008
+ * Returns the key for the latest epoch (backward-compatible accessor).
1009
+ */
563
1010
  getSymmetricKey(channelId) {
564
- return this.symmetricKeys.get(channelId);
1011
+ const state = this.keyEpochStates.get(channelId);
1012
+ if (!state)
1013
+ return undefined;
1014
+ return state.epochKeys.get(state.currentEpoch);
1015
+ }
1016
+ /**
1017
+ * Get the full epoch state for a channel (testing / internal use).
1018
+ */
1019
+ getKeyEpochState(channelId) {
1020
+ return this.keyEpochStates.get(channelId);
565
1021
  }
566
1022
  getAllMessages(channelId) {
567
1023
  return this.messages.get(channelId) ?? [];