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