@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.
- package/package.json +10 -4
- package/src/index.d.ts +1 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +2 -1
- package/src/index.js.map +1 -1
- package/src/lib/enumeration-translations/index.d.ts +2 -0
- package/src/lib/enumeration-translations/index.d.ts.map +1 -1
- package/src/lib/enumeration-translations/index.js +1 -0
- package/src/lib/enumeration-translations/index.js.map +1 -1
- package/src/lib/enumeration-translations/memberStatusType.d.ts +5 -0
- package/src/lib/enumeration-translations/memberStatusType.d.ts.map +1 -0
- package/src/lib/enumeration-translations/memberStatusType.js +58 -0
- package/src/lib/enumeration-translations/memberStatusType.js.map +1 -0
- package/src/lib/enumerations/brightChainStrings.d.ts +43 -0
- package/src/lib/enumerations/brightChainStrings.d.ts.map +1 -1
- package/src/lib/enumerations/brightChainStrings.js +44 -0
- package/src/lib/enumerations/brightChainStrings.js.map +1 -1
- package/src/lib/enumerations/brightchainFeatures.d.ts +1 -0
- package/src/lib/enumerations/brightchainFeatures.d.ts.map +1 -1
- package/src/lib/enumerations/brightchainFeatures.js +1 -0
- package/src/lib/enumerations/brightchainFeatures.js.map +1 -1
- package/src/lib/enumerations/communication.d.ts +7 -1
- package/src/lib/enumerations/communication.d.ts.map +1 -1
- package/src/lib/enumerations/communication.js +6 -0
- package/src/lib/enumerations/communication.js.map +1 -1
- package/src/lib/enumerations/messaging/emailErrorType.d.ts +15 -2
- package/src/lib/enumerations/messaging/emailErrorType.d.ts.map +1 -1
- package/src/lib/enumerations/messaging/emailErrorType.js +17 -1
- package/src/lib/enumerations/messaging/emailErrorType.js.map +1 -1
- package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts +4 -2
- package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts.map +1 -1
- package/src/lib/enumerations/messaging/messageEncryptionScheme.js +3 -1
- package/src/lib/enumerations/messaging/messageEncryptionScheme.js.map +1 -1
- package/src/lib/errors/encryptionErrors.d.ts +71 -0
- package/src/lib/errors/encryptionErrors.d.ts.map +1 -0
- package/src/lib/errors/encryptionErrors.js +112 -0
- package/src/lib/errors/encryptionErrors.js.map +1 -0
- package/src/lib/errors/index.d.ts +10 -0
- package/src/lib/errors/index.d.ts.map +1 -1
- package/src/lib/errors/index.js +13 -0
- package/src/lib/errors/index.js.map +1 -1
- package/src/lib/i18n/i18n-setup.d.ts +2 -2
- package/src/lib/i18n/i18n-setup.d.ts.map +1 -1
- package/src/lib/i18n/strings/englishUK.d.ts +2 -2
- package/src/lib/i18n/strings/englishUK.d.ts.map +1 -1
- package/src/lib/i18n/strings/englishUK.js.map +1 -1
- package/src/lib/i18n/strings/englishUs.d.ts +2 -2
- package/src/lib/i18n/strings/englishUs.d.ts.map +1 -1
- package/src/lib/i18n/strings/englishUs.js +45 -1
- package/src/lib/i18n/strings/englishUs.js.map +1 -1
- package/src/lib/i18n/strings/french.d.ts +2 -2
- package/src/lib/i18n/strings/french.d.ts.map +1 -1
- package/src/lib/i18n/strings/french.js +58 -13
- package/src/lib/i18n/strings/french.js.map +1 -1
- package/src/lib/i18n/strings/german.d.ts +2 -2
- package/src/lib/i18n/strings/german.d.ts.map +1 -1
- package/src/lib/i18n/strings/german.js +57 -12
- package/src/lib/i18n/strings/german.js.map +1 -1
- package/src/lib/i18n/strings/japanese.d.ts +2 -2
- package/src/lib/i18n/strings/japanese.d.ts.map +1 -1
- package/src/lib/i18n/strings/japanese.js +57 -12
- package/src/lib/i18n/strings/japanese.js.map +1 -1
- package/src/lib/i18n/strings/mandarin.d.ts +2 -2
- package/src/lib/i18n/strings/mandarin.d.ts.map +1 -1
- package/src/lib/i18n/strings/mandarin.js +57 -12
- package/src/lib/i18n/strings/mandarin.js.map +1 -1
- package/src/lib/i18n/strings/spanish.d.ts +2 -2
- package/src/lib/i18n/strings/spanish.d.ts.map +1 -1
- package/src/lib/i18n/strings/spanish.js +57 -12
- package/src/lib/i18n/strings/spanish.js.map +1 -1
- package/src/lib/i18n/strings/ukrainian.d.ts +2 -2
- package/src/lib/i18n/strings/ukrainian.d.ts.map +1 -1
- package/src/lib/i18n/strings/ukrainian.js +57 -12
- package/src/lib/i18n/strings/ukrainian.js.map +1 -1
- package/src/lib/index.d.ts +31 -0
- package/src/lib/index.d.ts.map +1 -1
- package/src/lib/index.js +29 -1
- package/src/lib/index.js.map +1 -1
- package/src/lib/interfaces/auth/writeProof.d.ts +2 -0
- package/src/lib/interfaces/auth/writeProof.d.ts.map +1 -1
- package/src/lib/interfaces/auth/writeProofUtils.d.ts +1 -1
- package/src/lib/interfaces/auth/writeProofUtils.d.ts.map +1 -1
- package/src/lib/interfaces/auth/writeProofUtils.js +2 -2
- package/src/lib/interfaces/auth/writeProofUtils.js.map +1 -1
- package/src/lib/interfaces/availability/gossipService.d.ts +99 -1
- package/src/lib/interfaces/availability/gossipService.d.ts.map +1 -1
- package/src/lib/interfaces/availability/gossipService.js +4 -0
- package/src/lib/interfaces/availability/gossipService.js.map +1 -1
- package/src/lib/interfaces/communication/blockContentStore.d.ts +57 -0
- package/src/lib/interfaces/communication/blockContentStore.d.ts.map +1 -0
- package/src/lib/interfaces/communication/blockContentStore.js +21 -0
- package/src/lib/interfaces/communication/blockContentStore.js.map +1 -0
- package/src/lib/interfaces/communication/chatStorageProvider.d.ts +77 -0
- package/src/lib/interfaces/communication/chatStorageProvider.d.ts.map +1 -0
- package/src/lib/interfaces/communication/chatStorageProvider.js +25 -0
- package/src/lib/interfaces/communication/chatStorageProvider.js.map +1 -0
- package/src/lib/interfaces/communication/index.d.ts +4 -0
- package/src/lib/interfaces/communication/index.d.ts.map +1 -0
- package/src/lib/interfaces/communication/index.js +3 -0
- package/src/lib/interfaces/communication/index.js.map +1 -0
- package/src/lib/interfaces/communication/server.d.ts +57 -0
- package/src/lib/interfaces/communication/server.d.ts.map +1 -0
- package/src/lib/interfaces/communication/server.js +14 -0
- package/src/lib/interfaces/communication/server.js.map +1 -0
- package/src/lib/interfaces/communication.d.ts +60 -5
- package/src/lib/interfaces/communication.d.ts.map +1 -1
- package/src/lib/interfaces/communication.js +8 -0
- package/src/lib/interfaces/communication.js.map +1 -1
- package/src/lib/interfaces/communicationEvents.d.ts +59 -1
- package/src/lib/interfaces/communicationEvents.d.ts.map +1 -1
- package/src/lib/interfaces/events/communicationEventEmitter.d.ts +9 -0
- package/src/lib/interfaces/events/communicationEventEmitter.d.ts.map +1 -1
- package/src/lib/interfaces/events/communicationEventEmitter.js +9 -0
- package/src/lib/interfaces/events/communicationEventEmitter.js.map +1 -1
- package/src/lib/interfaces/index.d.ts +4 -1
- package/src/lib/interfaces/index.d.ts.map +1 -1
- package/src/lib/interfaces/index.js +3 -0
- package/src/lib/interfaces/index.js.map +1 -1
- package/src/lib/interfaces/messaging/gpgKey.d.ts +93 -0
- package/src/lib/interfaces/messaging/gpgKey.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/gpgKey.js +12 -0
- package/src/lib/interfaces/messaging/gpgKey.js.map +1 -0
- package/src/lib/interfaces/messaging/index.d.ts +4 -0
- package/src/lib/interfaces/messaging/index.d.ts.map +1 -1
- package/src/lib/interfaces/messaging/index.js +4 -0
- package/src/lib/interfaces/messaging/index.js.map +1 -1
- package/src/lib/interfaces/messaging/keyStore.d.ts +100 -0
- package/src/lib/interfaces/messaging/keyStore.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/keyStore.js +13 -0
- package/src/lib/interfaces/messaging/keyStore.js.map +1 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts +92 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.js +13 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.js.map +1 -0
- package/src/lib/interfaces/messaging/smimeCertificate.d.ts +99 -0
- package/src/lib/interfaces/messaging/smimeCertificate.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/smimeCertificate.js +12 -0
- package/src/lib/interfaces/messaging/smimeCertificate.js.map +1 -0
- package/src/lib/interfaces/responses/adminDashboardResponse.d.ts +8 -0
- package/src/lib/interfaces/responses/adminDashboardResponse.d.ts.map +1 -1
- package/src/lib/interfaces/responses/communicationResponses.d.ts +25 -0
- package/src/lib/interfaces/responses/communicationResponses.d.ts.map +1 -1
- package/src/lib/services/blockService.d.ts +11 -3
- package/src/lib/services/blockService.d.ts.map +1 -1
- package/src/lib/services/blockService.js +22 -2
- package/src/lib/services/blockService.js.map +1 -1
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts +108 -0
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts.map +1 -0
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.js +284 -0
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.js.map +1 -0
- package/src/lib/services/communication/attachmentUtils.d.ts +20 -0
- package/src/lib/services/communication/attachmentUtils.d.ts.map +1 -0
- package/src/lib/services/communication/attachmentUtils.js +43 -0
- package/src/lib/services/communication/attachmentUtils.js.map +1 -0
- package/src/lib/services/communication/channelService.d.ts +127 -12
- package/src/lib/services/communication/channelService.d.ts.map +1 -1
- package/src/lib/services/communication/channelService.js +486 -30
- package/src/lib/services/communication/channelService.js.map +1 -1
- package/src/lib/services/communication/conversationService.d.ts +91 -4
- package/src/lib/services/communication/conversationService.d.ts.map +1 -1
- package/src/lib/services/communication/conversationService.js +237 -5
- package/src/lib/services/communication/conversationService.js.map +1 -1
- package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts +36 -0
- package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts.map +1 -0
- package/src/lib/services/communication/eciesKeyEncryptionHandler.js +30 -0
- package/src/lib/services/communication/eciesKeyEncryptionHandler.js.map +1 -0
- package/src/lib/services/communication/groupService.d.ts +99 -21
- package/src/lib/services/communication/groupService.d.ts.map +1 -1
- package/src/lib/services/communication/groupService.js +359 -39
- package/src/lib/services/communication/groupService.js.map +1 -1
- package/src/lib/services/communication/index.d.ts +6 -1
- package/src/lib/services/communication/index.d.ts.map +1 -1
- package/src/lib/services/communication/index.js +20 -1
- package/src/lib/services/communication/index.js.map +1 -1
- package/src/lib/services/communication/keyEpochManager.d.ts +41 -0
- package/src/lib/services/communication/keyEpochManager.d.ts.map +1 -0
- package/src/lib/services/communication/keyEpochManager.js +59 -0
- package/src/lib/services/communication/keyEpochManager.js.map +1 -0
- package/src/lib/services/communication/rehydrationHelpers.d.ts +32 -0
- package/src/lib/services/communication/rehydrationHelpers.d.ts.map +1 -0
- package/src/lib/services/communication/rehydrationHelpers.js +58 -0
- package/src/lib/services/communication/rehydrationHelpers.js.map +1 -0
- package/src/lib/services/communication/serverService.d.ts +193 -0
- package/src/lib/services/communication/serverService.d.ts.map +1 -0
- package/src/lib/services/communication/serverService.js +497 -0
- package/src/lib/services/communication/serverService.js.map +1 -0
- package/src/lib/services/memberStore.d.ts +10 -0
- package/src/lib/services/memberStore.d.ts.map +1 -1
- package/src/lib/services/memberStore.js +41 -16
- package/src/lib/services/memberStore.js.map +1 -1
- package/src/lib/services/messaging/emailEncryptionService.d.ts +162 -0
- package/src/lib/services/messaging/emailEncryptionService.d.ts.map +1 -1
- package/src/lib/services/messaging/emailEncryptionService.js +293 -0
- package/src/lib/services/messaging/emailEncryptionService.js.map +1 -1
- package/src/lib/services/messaging/emailMessageService.d.ts +64 -0
- package/src/lib/services/messaging/emailMessageService.d.ts.map +1 -1
- package/src/lib/services/messaging/emailMessageService.js +142 -13
- package/src/lib/services/messaging/emailMessageService.js.map +1 -1
- package/src/lib/services/messaging/gpgKeyManager.d.ts +130 -0
- package/src/lib/services/messaging/gpgKeyManager.d.ts.map +1 -0
- package/src/lib/services/messaging/gpgKeyManager.js +381 -0
- package/src/lib/services/messaging/gpgKeyManager.js.map +1 -0
- package/src/lib/services/messaging/index.d.ts +3 -0
- package/src/lib/services/messaging/index.d.ts.map +1 -1
- package/src/lib/services/messaging/index.js +3 -0
- package/src/lib/services/messaging/index.js.map +1 -1
- package/src/lib/services/messaging/recipientKeyResolver.d.ts +47 -0
- package/src/lib/services/messaging/recipientKeyResolver.d.ts.map +1 -0
- package/src/lib/services/messaging/recipientKeyResolver.js +132 -0
- package/src/lib/services/messaging/recipientKeyResolver.js.map +1 -0
- package/src/lib/services/messaging/smimeCertificateManager.d.ts +207 -0
- package/src/lib/services/messaging/smimeCertificateManager.d.ts.map +1 -0
- package/src/lib/services/messaging/smimeCertificateManager.js +696 -0
- 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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
161
|
-
*
|
|
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
|
-
|
|
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.
|
|
166
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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:
|
|
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.
|
|
283
|
-
*
|
|
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
|
|
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
|
-
//
|
|
301
|
-
|
|
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
|
-
*
|
|
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.
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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).
|