@brightchain/brightchain-lib 0.29.26 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/constants.d.ts +3 -26
- package/src/lib/constants.d.ts.map +1 -1
- package/src/lib/constants.js +2 -1
- package/src/lib/constants.js.map +1 -1
- package/src/lib/db/collection.d.ts +1 -1
- package/src/lib/db/collection.d.ts.map +1 -1
- package/src/lib/db/collection.js +2 -1
- package/src/lib/db/collection.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 +61 -0
- package/src/lib/enumerations/brightChainStrings.d.ts.map +1 -1
- package/src/lib/enumerations/brightChainStrings.js +64 -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/friendRequestStatus.d.ts +7 -0
- package/src/lib/enumerations/friendRequestStatus.d.ts.map +1 -0
- package/src/lib/enumerations/friendRequestStatus.js +11 -0
- package/src/lib/enumerations/friendRequestStatus.js.map +1 -0
- package/src/lib/enumerations/friendsErrorCode.d.ts +10 -0
- package/src/lib/enumerations/friendsErrorCode.d.ts.map +1 -0
- package/src/lib/enumerations/friendsErrorCode.js +14 -0
- package/src/lib/enumerations/friendsErrorCode.js.map +1 -0
- package/src/lib/enumerations/friendshipStatus.d.ts +7 -0
- package/src/lib/enumerations/friendshipStatus.d.ts.map +1 -0
- package/src/lib/enumerations/friendshipStatus.js +11 -0
- package/src/lib/enumerations/friendshipStatus.js.map +1 -0
- package/src/lib/enumerations/index.d.ts +3 -0
- package/src/lib/enumerations/index.d.ts.map +1 -1
- package/src/lib/enumerations/index.js +4 -0
- package/src/lib/enumerations/index.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/friendsServiceError.d.ts +20 -0
- package/src/lib/errors/friendsServiceError.d.ts.map +1 -0
- package/src/lib/errors/friendsServiceError.js +48 -0
- package/src/lib/errors/friendsServiceError.js.map +1 -0
- package/src/lib/errors/index.d.ts +15 -0
- package/src/lib/errors/index.d.ts.map +1 -1
- package/src/lib/errors/index.js +21 -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 +65 -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 +78 -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 +77 -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 +77 -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 +77 -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 +77 -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 +77 -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/appSubsystemPlugin.d.ts +69 -0
- package/src/lib/interfaces/appSubsystemPlugin.d.ts.map +1 -0
- package/src/lib/interfaces/appSubsystemPlugin.js +3 -0
- package/src/lib/interfaces/appSubsystemPlugin.js.map +1 -0
- 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 +62 -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/constants.d.ts +2 -0
- package/src/lib/interfaces/constants.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/friends/baseFriendRequest.d.ts +13 -0
- package/src/lib/interfaces/friends/baseFriendRequest.d.ts.map +1 -0
- package/src/lib/interfaces/friends/baseFriendRequest.js +3 -0
- package/src/lib/interfaces/friends/baseFriendRequest.js.map +1 -0
- package/src/lib/interfaces/friends/baseFriendship.d.ts +11 -0
- package/src/lib/interfaces/friends/baseFriendship.d.ts.map +1 -0
- package/src/lib/interfaces/friends/baseFriendship.js +3 -0
- package/src/lib/interfaces/friends/baseFriendship.js.map +1 -0
- package/src/lib/interfaces/friends/friendsService.d.ts +32 -0
- package/src/lib/interfaces/friends/friendsService.d.ts.map +1 -0
- package/src/lib/interfaces/friends/friendsService.js +3 -0
- package/src/lib/interfaces/friends/friendsService.js.map +1 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.d.ts +17 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.d.ts.map +1 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.js +3 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.js.map +1 -0
- package/src/lib/interfaces/friends/index.d.ts +6 -0
- package/src/lib/interfaces/friends/index.d.ts.map +1 -0
- package/src/lib/interfaces/friends/index.js +3 -0
- package/src/lib/interfaces/friends/index.js.map +1 -0
- package/src/lib/interfaces/friends/pagination.d.ts +11 -0
- package/src/lib/interfaces/friends/pagination.d.ts.map +1 -0
- package/src/lib/interfaces/friends/pagination.js +3 -0
- package/src/lib/interfaces/friends/pagination.js.map +1 -0
- package/src/lib/interfaces/index.d.ts +6 -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/interfaces/storage/documentTypes.d.ts +4 -0
- package/src/lib/interfaces/storage/documentTypes.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 +143 -14
- package/src/lib/services/communication/channelService.d.ts.map +1 -1
- package/src/lib/services/communication/channelService.js +562 -41
- 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 +269 -7
- 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 +387 -41
- 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 +495 -0
- package/src/lib/services/communication/serverService.js.map +1 -0
- package/src/lib/services/copyOnWrite.service.d.ts +110 -0
- package/src/lib/services/copyOnWrite.service.d.ts.map +1 -0
- package/src/lib/services/copyOnWrite.service.js +256 -0
- package/src/lib/services/copyOnWrite.service.js.map +1 -0
- package/src/lib/services/index.d.ts +1 -0
- package/src/lib/services/index.d.ts.map +1 -1
- package/src/lib/services/index.js +1 -0
- package/src/lib/services/index.js.map +1 -1
- package/src/lib/services/memberStore.d.ts +17 -1
- package/src/lib/services/memberStore.d.ts.map +1 -1
- package/src/lib/services/memberStore.js +98 -17
- 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
- package/src/lib/utils/index.d.ts +6 -0
- package/src/lib/utils/index.d.ts.map +1 -1
- package/src/lib/utils/index.js +9 -0
- package/src/lib/utils/index.js.map +1 -1
- package/src/lib/utils/sortPair.d.ts +6 -0
- package/src/lib/utils/sortPair.d.ts.map +1 -0
- package/src/lib/utils/sortPair.js +11 -0
- package/src/lib/utils/sortPair.js.map +1 -0
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* GroupService — manages group lifecycle, key management, and messaging.
|
|
4
4
|
*
|
|
5
|
-
* Maintains in-memory stores for groups and messages, with
|
|
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
|
|
278
|
+
*/
|
|
279
|
+
addMemberToEpochState(state, newMemberId) {
|
|
280
|
+
for (const [epoch, rawKey] of state.epochKeys) {
|
|
281
|
+
const epochMap = state.encryptedEpochKeys.get(epoch) ?? new Map();
|
|
282
|
+
epochMap.set(newMemberId, this.encryptKey(newMemberId, rawKey));
|
|
283
|
+
state.encryptedEpochKeys.set(epoch, epochMap);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Rotate key: increment epoch, add new key, delete removed member from ALL epochs,
|
|
288
|
+
* re-wrap ALL epoch keys for remaining members.
|
|
289
|
+
*
|
|
290
|
+
* Requirements: 5.3
|
|
291
|
+
*/
|
|
292
|
+
rotateEpochState(state, newKey, remainingMemberIds, removedMemberId) {
|
|
293
|
+
const newEpoch = state.currentEpoch + 1;
|
|
294
|
+
// Add new epoch key
|
|
295
|
+
state.epochKeys.set(newEpoch, newKey);
|
|
296
|
+
// Delete removed member from ALL epochs
|
|
297
|
+
for (const [, memberMap] of state.encryptedEpochKeys) {
|
|
298
|
+
memberMap.delete(removedMemberId);
|
|
299
|
+
}
|
|
300
|
+
// Re-wrap ALL epoch keys for remaining members
|
|
301
|
+
for (const [epoch, rawKey] of state.epochKeys) {
|
|
302
|
+
state.encryptedEpochKeys.set(epoch, this.encryptKeyForMembers(remainingMemberIds, rawKey));
|
|
303
|
+
}
|
|
304
|
+
return { ...state, currentEpoch: newEpoch };
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Rotate the group's symmetric key using epoch-aware key management.
|
|
308
|
+
* Generates a new key, increments epoch, re-wraps all epoch keys for remaining members,
|
|
309
|
+
* and removes the departed member from all epochs.
|
|
310
|
+
*
|
|
311
|
+
* Requirements: 5.3
|
|
162
312
|
*/
|
|
163
|
-
rotateKey(group) {
|
|
313
|
+
rotateKey(group, removedMemberId) {
|
|
164
314
|
const newKey = this.generateSymmetricKey();
|
|
165
|
-
this.
|
|
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,113 @@ 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 the message with the original readable content for display,
|
|
466
|
+
// while the stored version retains the block reference (magnet URL)
|
|
467
|
+
return this.blockContentStore
|
|
468
|
+
? { ...message, encryptedContent: content }
|
|
469
|
+
: message;
|
|
270
470
|
}
|
|
271
471
|
/**
|
|
272
472
|
* Get messages in a group with cursor-based pagination.
|
|
473
|
+
* Messages are returned with their keyEpoch so clients can decrypt
|
|
474
|
+
* using the appropriate epoch's CEK.
|
|
475
|
+
* Validates that each message's keyEpoch exists in the epoch state;
|
|
476
|
+
* throws KeyEpochNotFoundError if a message references a non-existent epoch.
|
|
477
|
+
*
|
|
478
|
+
* Requirements: 5.1, 12.3
|
|
273
479
|
*/
|
|
274
480
|
async getMessages(groupId, memberId, cursor, limit = 50) {
|
|
275
481
|
const group = this.assertGroupExists(groupId);
|
|
276
482
|
this.assertIsMember(group, memberId);
|
|
277
483
|
const msgs = this.messages.get(groupId) ?? [];
|
|
278
|
-
|
|
484
|
+
// Validate that each message's keyEpoch exists in the epoch state
|
|
485
|
+
for (const msg of msgs) {
|
|
486
|
+
this.assertEpochExists(groupId, msg.keyEpoch);
|
|
487
|
+
}
|
|
488
|
+
const result = (0, pagination_1.paginateItems)(msgs, cursor, limit);
|
|
489
|
+
// Resolve block references (magnet URLs) back to readable content
|
|
490
|
+
if (this.blockContentStore) {
|
|
491
|
+
const resolved = await Promise.all(result.items.map(async (msg) => {
|
|
492
|
+
if (msg.deleted)
|
|
493
|
+
return msg;
|
|
494
|
+
try {
|
|
495
|
+
const contentBytes = await this.blockContentStore.retrieveContent(msg.encryptedContent);
|
|
496
|
+
if (contentBytes) {
|
|
497
|
+
return {
|
|
498
|
+
...msg,
|
|
499
|
+
encryptedContent: new TextDecoder().decode(contentBytes),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Fall back to stored value if retrieval fails
|
|
505
|
+
}
|
|
506
|
+
return msg;
|
|
507
|
+
}));
|
|
508
|
+
return { ...result, items: resolved };
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
279
511
|
}
|
|
280
512
|
// ─── Member management ────────────────────────────────────────────────
|
|
281
513
|
/**
|
|
282
|
-
* Add members to a group.
|
|
283
|
-
*
|
|
514
|
+
* Add members to a group. Wraps ALL epoch keys for new members.
|
|
515
|
+
* Requirements: 10.2, 5.2
|
|
284
516
|
*/
|
|
285
517
|
async addMembers(groupId, requesterId, memberIds) {
|
|
286
518
|
const group = this.assertGroupExists(groupId);
|
|
287
519
|
this.assertIsMember(group, requesterId);
|
|
288
520
|
this.assertPermission(requesterId, groupId, communication_1.Permission.MANAGE_MEMBERS);
|
|
289
521
|
const now = new Date();
|
|
290
|
-
const
|
|
522
|
+
const state = this.keyEpochStates.get(groupId);
|
|
291
523
|
for (const memberId of memberIds) {
|
|
292
524
|
if (group.members.some((m) => m.memberId === memberId)) {
|
|
293
525
|
throw new MemberAlreadyInGroupError(memberId);
|
|
@@ -297,16 +529,23 @@ class GroupService {
|
|
|
297
529
|
role: communication_1.DefaultRole.MEMBER,
|
|
298
530
|
joinedAt: now,
|
|
299
531
|
});
|
|
300
|
-
//
|
|
301
|
-
|
|
532
|
+
// Add member to epoch state: wrap ALL epoch keys for the new member
|
|
533
|
+
if (state) {
|
|
534
|
+
this.addMemberToEpochState(state, memberId);
|
|
535
|
+
group.encryptedSharedKey = state.encryptedEpochKeys;
|
|
536
|
+
}
|
|
302
537
|
this.permissionService.assignRole(memberId, groupId, communication_1.DefaultRole.MEMBER);
|
|
303
538
|
// Emit member joined event
|
|
304
539
|
this.eventEmitter.emitMemberJoined('group', groupId, memberId);
|
|
305
540
|
}
|
|
541
|
+
// Persist group update to storage provider if available
|
|
542
|
+
if (this.groupCollection) {
|
|
543
|
+
await this.groupCollection.update(groupId, group);
|
|
544
|
+
}
|
|
306
545
|
}
|
|
307
546
|
/**
|
|
308
|
-
* Remove a member from a group. Rotates the shared key.
|
|
309
|
-
*
|
|
547
|
+
* Remove a member from a group. Rotates the shared key using epoch-aware rotation.
|
|
548
|
+
* Requirements: 10.2, 5.3
|
|
310
549
|
*/
|
|
311
550
|
async removeMember(groupId, requesterId, targetId) {
|
|
312
551
|
const group = this.assertGroupExists(groupId);
|
|
@@ -314,24 +553,50 @@ class GroupService {
|
|
|
314
553
|
this.assertIsMember(group, targetId);
|
|
315
554
|
this.assertPermission(requesterId, groupId, communication_1.Permission.MANAGE_MEMBERS);
|
|
316
555
|
group.members = group.members.filter((m) => m.memberId !== targetId);
|
|
317
|
-
group.
|
|
318
|
-
|
|
319
|
-
|
|
556
|
+
if (group.members.length > 0) {
|
|
557
|
+
this.rotateKey(group, targetId);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// No members left — clean up epoch state
|
|
561
|
+
const state = this.keyEpochStates.get(groupId);
|
|
562
|
+
if (state) {
|
|
563
|
+
for (const [, epochMap] of state.encryptedEpochKeys) {
|
|
564
|
+
epochMap.delete(targetId);
|
|
565
|
+
}
|
|
566
|
+
group.encryptedSharedKey = state.encryptedEpochKeys;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Persist group update to storage provider if available
|
|
570
|
+
if (this.groupCollection) {
|
|
571
|
+
await this.groupCollection.update(groupId, group);
|
|
572
|
+
}
|
|
320
573
|
// Emit member kicked event
|
|
321
574
|
this.eventEmitter.emitMemberKicked('group', groupId, targetId, requesterId);
|
|
322
575
|
}
|
|
323
576
|
/**
|
|
324
|
-
* Leave a group voluntarily. Rotates the shared key.
|
|
325
|
-
*
|
|
577
|
+
* Leave a group voluntarily. Rotates the shared key using epoch-aware rotation.
|
|
578
|
+
* Requirements: 10.2, 5.3
|
|
326
579
|
*/
|
|
327
580
|
async leaveGroup(groupId, memberId) {
|
|
328
581
|
const group = this.assertGroupExists(groupId);
|
|
329
582
|
this.assertIsMember(group, memberId);
|
|
330
583
|
group.members = group.members.filter((m) => m.memberId !== memberId);
|
|
331
|
-
group.encryptedSharedKey.delete(memberId);
|
|
332
|
-
// Rotate key for remaining members
|
|
333
584
|
if (group.members.length > 0) {
|
|
334
|
-
this.rotateKey(group);
|
|
585
|
+
this.rotateKey(group, memberId);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
// No members left — clean up epoch state
|
|
589
|
+
const state = this.keyEpochStates.get(groupId);
|
|
590
|
+
if (state) {
|
|
591
|
+
for (const [, epochMap] of state.encryptedEpochKeys) {
|
|
592
|
+
epochMap.delete(memberId);
|
|
593
|
+
}
|
|
594
|
+
group.encryptedSharedKey = state.encryptedEpochKeys;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Persist group update to storage provider if available
|
|
598
|
+
if (this.groupCollection) {
|
|
599
|
+
await this.groupCollection.update(groupId, group);
|
|
335
600
|
}
|
|
336
601
|
// Emit member left event
|
|
337
602
|
this.eventEmitter.emitMemberLeft('group', groupId, memberId);
|
|
@@ -345,7 +610,37 @@ class GroupService {
|
|
|
345
610
|
const group = this.assertGroupExists(groupId);
|
|
346
611
|
this.assertIsMember(group, memberId);
|
|
347
612
|
const msgs = this.messages.get(groupId) ?? [];
|
|
613
|
+
if (this.blockContentStore) {
|
|
614
|
+
// Find the message manually to handle block storage
|
|
615
|
+
const message = msgs.find((m) => m.id === messageId);
|
|
616
|
+
if (!message)
|
|
617
|
+
throw new GroupMessageNotFoundError(messageId);
|
|
618
|
+
if (message.senderId !== memberId)
|
|
619
|
+
throw new NotMessageAuthorError();
|
|
620
|
+
// Store new content as a new block
|
|
621
|
+
const memberIds = group.members.map((m) => m.memberId);
|
|
622
|
+
const { blockReference } = await this.blockContentStore.storeContent(newContent, memberId, memberIds);
|
|
623
|
+
// Push old block reference to edit history
|
|
624
|
+
message.editHistory.push({
|
|
625
|
+
content: message.encryptedContent,
|
|
626
|
+
editedAt: new Date(),
|
|
627
|
+
});
|
|
628
|
+
// Update encryptedContent to new block reference
|
|
629
|
+
message.encryptedContent = blockReference;
|
|
630
|
+
message.editedAt = new Date();
|
|
631
|
+
// Persist edited message to storage provider if available
|
|
632
|
+
if (this.groupMessageCollection) {
|
|
633
|
+
await this.groupMessageCollection.update(messageId, message);
|
|
634
|
+
}
|
|
635
|
+
// Emit message edited event
|
|
636
|
+
this.eventEmitter.emitMessageEdited('group', groupId, messageId, memberId);
|
|
637
|
+
return message;
|
|
638
|
+
}
|
|
348
639
|
const edited = this.messageOps.editMessage(msgs, messageId, memberId, newContent, (id) => new GroupMessageNotFoundError(id), () => new NotMessageAuthorError());
|
|
640
|
+
// Persist edited message to storage provider if available
|
|
641
|
+
if (this.groupMessageCollection) {
|
|
642
|
+
await this.groupMessageCollection.update(messageId, edited);
|
|
643
|
+
}
|
|
349
644
|
// Emit message edited event
|
|
350
645
|
this.eventEmitter.emitMessageEdited('group', groupId, messageId, memberId);
|
|
351
646
|
return edited;
|
|
@@ -359,6 +654,13 @@ class GroupService {
|
|
|
359
654
|
this.assertIsMember(group, memberId);
|
|
360
655
|
const msgs = this.messages.get(groupId) ?? [];
|
|
361
656
|
this.messageOps.deleteMessage(msgs, groupId, messageId, memberId, (id) => new GroupMessageNotFoundError(id), (p) => new GroupPermissionError(p));
|
|
657
|
+
// Persist deleted message to storage provider if available
|
|
658
|
+
if (this.groupMessageCollection) {
|
|
659
|
+
const deletedMsg = msgs.find((m) => m.id === messageId);
|
|
660
|
+
if (deletedMsg) {
|
|
661
|
+
await this.groupMessageCollection.update(messageId, deletedMsg);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
362
664
|
// Emit message deleted event
|
|
363
665
|
this.eventEmitter.emitMessageDeleted('group', groupId, messageId, memberId);
|
|
364
666
|
}
|
|
@@ -371,6 +673,16 @@ class GroupService {
|
|
|
371
673
|
this.assertIsMember(group, memberId);
|
|
372
674
|
const msgs = this.messages.get(groupId) ?? [];
|
|
373
675
|
this.messageOps.pinMessage(msgs, groupId, messageId, memberId, group, (id) => new GroupMessageNotFoundError(id), (p) => new GroupPermissionError(p));
|
|
676
|
+
// Persist pin changes to storage provider if available
|
|
677
|
+
if (this.groupMessageCollection) {
|
|
678
|
+
const pinnedMsg = msgs.find((m) => m.id === messageId);
|
|
679
|
+
if (pinnedMsg) {
|
|
680
|
+
await this.groupMessageCollection.update(messageId, pinnedMsg);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (this.groupCollection) {
|
|
684
|
+
await this.groupCollection.update(groupId, group);
|
|
685
|
+
}
|
|
374
686
|
// Emit message pinned event
|
|
375
687
|
this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_PINNED, 'group', groupId, messageId, memberId);
|
|
376
688
|
}
|
|
@@ -383,6 +695,16 @@ class GroupService {
|
|
|
383
695
|
this.assertIsMember(group, memberId);
|
|
384
696
|
const msgs = this.messages.get(groupId) ?? [];
|
|
385
697
|
this.messageOps.unpinMessage(msgs, groupId, messageId, memberId, group, (id) => new GroupMessageNotFoundError(id), (p) => new GroupPermissionError(p));
|
|
698
|
+
// Persist unpin changes to storage provider if available
|
|
699
|
+
if (this.groupMessageCollection) {
|
|
700
|
+
const unpinnedMsg = msgs.find((m) => m.id === messageId);
|
|
701
|
+
if (unpinnedMsg) {
|
|
702
|
+
await this.groupMessageCollection.update(messageId, unpinnedMsg);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (this.groupCollection) {
|
|
706
|
+
await this.groupCollection.update(groupId, group);
|
|
707
|
+
}
|
|
386
708
|
// Emit message unpinned event
|
|
387
709
|
this.eventEmitter.emitMessagePinEvent(communication_1.CommunicationEventType.MESSAGE_UNPINNED, 'group', groupId, messageId, memberId);
|
|
388
710
|
}
|
|
@@ -395,6 +717,13 @@ class GroupService {
|
|
|
395
717
|
this.assertIsMember(group, memberId);
|
|
396
718
|
const msgs = this.messages.get(groupId) ?? [];
|
|
397
719
|
const reactionId = this.messageOps.addReaction(msgs, messageId, memberId, emoji, (id) => new GroupMessageNotFoundError(id));
|
|
720
|
+
// Persist reaction change to storage provider if available
|
|
721
|
+
if (this.groupMessageCollection) {
|
|
722
|
+
const msg = msgs.find((m) => m.id === messageId);
|
|
723
|
+
if (msg) {
|
|
724
|
+
await this.groupMessageCollection.update(messageId, msg);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
398
727
|
// Emit reaction added event
|
|
399
728
|
this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_ADDED, 'group', groupId, messageId, memberId, emoji, reactionId);
|
|
400
729
|
return reactionId;
|
|
@@ -408,6 +737,13 @@ class GroupService {
|
|
|
408
737
|
this.assertIsMember(group, memberId);
|
|
409
738
|
const msgs = this.messages.get(groupId) ?? [];
|
|
410
739
|
this.messageOps.removeReaction(msgs, messageId, reactionId, (id) => new GroupMessageNotFoundError(id), (id) => new ReactionNotFoundError(id));
|
|
740
|
+
// Persist reaction removal to storage provider if available
|
|
741
|
+
if (this.groupMessageCollection) {
|
|
742
|
+
const msg = msgs.find((m) => m.id === messageId);
|
|
743
|
+
if (msg) {
|
|
744
|
+
await this.groupMessageCollection.update(messageId, msg);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
411
747
|
// Emit reaction removed event
|
|
412
748
|
this.eventEmitter.emitReactionEvent(communication_1.CommunicationEventType.REACTION_REMOVED, 'group', groupId, messageId, memberId, '', // emoji not needed for removal
|
|
413
749
|
reactionId);
|
|
@@ -420,10 +756,20 @@ class GroupService {
|
|
|
420
756
|
return this.groups.get(groupId);
|
|
421
757
|
}
|
|
422
758
|
/**
|
|
423
|
-
* Get the raw symmetric key for a group
|
|
759
|
+
* Get the current epoch's raw symmetric key for a group.
|
|
760
|
+
* Returns the key for the latest epoch (backward-compatible accessor).
|
|
424
761
|
*/
|
|
425
762
|
getSymmetricKey(groupId) {
|
|
426
|
-
|
|
763
|
+
const state = this.keyEpochStates.get(groupId);
|
|
764
|
+
if (!state)
|
|
765
|
+
return undefined;
|
|
766
|
+
return state.epochKeys.get(state.currentEpoch);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Get the full epoch state for a group (testing / internal use).
|
|
770
|
+
*/
|
|
771
|
+
getKeyEpochState(groupId) {
|
|
772
|
+
return this.keyEpochStates.get(groupId);
|
|
427
773
|
}
|
|
428
774
|
/**
|
|
429
775
|
* Get all messages for a group without pagination (testing/internal).
|