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