@wireapp/core 46.46.6-beta.14.f6fd03fe6 → 46.46.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/lib/Account.d.ts +156 -51
  2. package/lib/Account.d.ts.map +1 -1
  3. package/lib/Account.js +503 -127
  4. package/lib/Account.test.js +158 -147
  5. package/lib/broadcast/AvailabilityType.d.ts +1 -1
  6. package/lib/broadcast/AvailabilityType.d.ts.map +1 -1
  7. package/lib/broadcast/BroadcastService.d.ts +1 -1
  8. package/lib/broadcast/BroadcastService.d.ts.map +1 -1
  9. package/lib/broadcast/BroadcastService.js +1 -1
  10. package/lib/client/ClientService.d.ts +4 -3
  11. package/lib/client/ClientService.d.ts.map +1 -1
  12. package/lib/client/ClientService.js +19 -5
  13. package/lib/conversation/AbortReason.d.ts +1 -1
  14. package/lib/conversation/AbortReason.d.ts.map +1 -1
  15. package/lib/conversation/AssetService/AssetService.d.ts +12 -30
  16. package/lib/conversation/AssetService/AssetService.d.ts.map +1 -1
  17. package/lib/conversation/AssetService/AssetService.js +1 -10
  18. package/lib/conversation/AssetService/AssetService.test.js +8 -3
  19. package/lib/conversation/ClientActionType.d.ts +1 -1
  20. package/lib/conversation/ClientActionType.d.ts.map +1 -1
  21. package/lib/conversation/ClientActionType.js +1 -1
  22. package/lib/conversation/ConversationService/ConversationService.d.ts +98 -14
  23. package/lib/conversation/ConversationService/ConversationService.d.ts.map +1 -1
  24. package/lib/conversation/ConversationService/ConversationService.js +314 -101
  25. package/lib/conversation/ConversationService/ConversationService.test.js +441 -47
  26. package/lib/conversation/ConversationService/ConversationService.types.d.ts +5 -4
  27. package/lib/conversation/ConversationService/ConversationService.types.d.ts.map +1 -1
  28. package/lib/conversation/ConversationService/Utility/getConversationQualifiedMembers.d.ts.map +1 -1
  29. package/lib/conversation/ConversationService/Utility/getConversationQualifiedMembers.js +6 -3
  30. package/lib/conversation/SubconversationService/SubconversationService.d.ts.map +1 -1
  31. package/lib/conversation/SubconversationService/SubconversationService.js +158 -11
  32. package/lib/conversation/SubconversationService/SubconversationService.test.js +8 -2
  33. package/lib/conversation/content/AssetContent.d.ts +1 -1
  34. package/lib/conversation/content/AssetContent.d.ts.map +1 -1
  35. package/lib/conversation/content/ButtonActionConfirmationContent.d.ts +1 -1
  36. package/lib/conversation/content/ButtonActionConfirmationContent.d.ts.map +1 -1
  37. package/lib/conversation/content/ButtonActionContent.d.ts +1 -1
  38. package/lib/conversation/content/ButtonActionContent.d.ts.map +1 -1
  39. package/lib/conversation/content/ClearedContent.d.ts +1 -1
  40. package/lib/conversation/content/ClearedContent.d.ts.map +1 -1
  41. package/lib/conversation/content/ClientActionContent.d.ts +1 -1
  42. package/lib/conversation/content/ClientActionContent.d.ts.map +1 -1
  43. package/lib/conversation/content/CompositeContent.d.ts +1 -1
  44. package/lib/conversation/content/CompositeContent.d.ts.map +1 -1
  45. package/lib/conversation/content/ConfirmationContent.d.ts +1 -1
  46. package/lib/conversation/content/ConfirmationContent.d.ts.map +1 -1
  47. package/lib/conversation/content/DeletedContent.d.ts +1 -1
  48. package/lib/conversation/content/DeletedContent.d.ts.map +1 -1
  49. package/lib/conversation/content/HiddenContent.d.ts +1 -1
  50. package/lib/conversation/content/HiddenContent.d.ts.map +1 -1
  51. package/lib/conversation/content/KnockContent.d.ts +1 -1
  52. package/lib/conversation/content/KnockContent.d.ts.map +1 -1
  53. package/lib/conversation/content/LinkPreviewContent.d.ts +1 -1
  54. package/lib/conversation/content/LinkPreviewContent.d.ts.map +1 -1
  55. package/lib/conversation/content/MentionContent.d.ts +1 -1
  56. package/lib/conversation/content/MentionContent.d.ts.map +1 -1
  57. package/lib/conversation/content/MultipartContent.d.ts +1 -1
  58. package/lib/conversation/content/MultipartContent.d.ts.map +1 -1
  59. package/lib/conversation/content/QuoteContent.d.ts +1 -1
  60. package/lib/conversation/content/QuoteContent.d.ts.map +1 -1
  61. package/lib/conversation/content/TweetContent.d.ts +1 -1
  62. package/lib/conversation/content/TweetContent.d.ts.map +1 -1
  63. package/lib/conversation/content/index.d.ts +1 -1
  64. package/lib/conversation/content/index.d.ts.map +1 -1
  65. package/lib/conversation/content/index.js +1 -1
  66. package/lib/conversation/message/MessageBuilder.d.ts +1 -1
  67. package/lib/conversation/message/MessageBuilder.d.ts.map +1 -1
  68. package/lib/conversation/message/MessageBuilder.js +1 -1
  69. package/lib/conversation/message/MessageService.d.ts.map +1 -1
  70. package/lib/conversation/message/MessageService.js +1 -1
  71. package/lib/conversation/message/MessageService.test.js +7 -1
  72. package/lib/conversation/message/MessageToProtoMapper.d.ts +1 -1
  73. package/lib/conversation/message/MessageToProtoMapper.d.ts.map +1 -1
  74. package/lib/conversation/message/MessageToProtoMapper.js +1 -1
  75. package/lib/conversation/message/messageSender.js +2 -2
  76. package/lib/cryptography/AssetCryptography/EncryptedAsset.d.ts +2 -2
  77. package/lib/cryptography/AssetCryptography/EncryptedAsset.d.ts.map +1 -1
  78. package/lib/messagingProtocols/common.types.d.ts +9 -0
  79. package/lib/messagingProtocols/common.types.d.ts.map +1 -1
  80. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIService.types.d.ts +2 -2
  81. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIService.types.d.ts.map +1 -1
  82. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIService.types.js +2 -1
  83. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.d.ts +1 -1
  84. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.d.ts.map +1 -1
  85. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.js +13 -11
  86. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.test.js +21 -16
  87. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceInternal.d.ts +9 -3
  88. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceInternal.d.ts.map +1 -1
  89. package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceInternal.js +31 -12
  90. package/lib/messagingProtocols/mls/E2EIdentityService/Helper/index.d.ts +6 -0
  91. package/lib/messagingProtocols/mls/E2EIdentityService/Helper/index.d.ts.map +1 -1
  92. package/lib/messagingProtocols/mls/E2EIdentityService/Helper/index.js +19 -1
  93. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/IncomingProposalsQueue.d.ts +7 -0
  94. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/IncomingProposalsQueue.d.ts.map +1 -0
  95. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/IncomingProposalsQueue.js +48 -0
  96. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/index.d.ts +2 -0
  97. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/{IncomingMessagesQueue → IncomingProposalsQueue}/index.d.ts.map +1 -1
  98. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/{IncomingMessagesQueue → IncomingProposalsQueue}/index.js +1 -1
  99. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/index.d.ts +0 -1
  100. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/index.d.ts.map +1 -1
  101. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/index.js +0 -1
  102. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.d.ts.map +1 -1
  103. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.js +23 -14
  104. package/lib/messagingProtocols/mls/EventHandler/events/welcomeMessage/welcomeMessage.d.ts.map +1 -1
  105. package/lib/messagingProtocols/mls/EventHandler/events/welcomeMessage/welcomeMessage.js +5 -2
  106. package/lib/messagingProtocols/mls/EventHandler/events/welcomeMessage/welcomeMessage.test.js +13 -3
  107. package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.d.ts +38 -2
  108. package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.d.ts.map +1 -1
  109. package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.js +41 -6
  110. package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.test.d.ts +2 -0
  111. package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.test.d.ts.map +1 -0
  112. package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.test.js +124 -0
  113. package/lib/messagingProtocols/mls/MLSService/MLSService.d.ts +38 -34
  114. package/lib/messagingProtocols/mls/MLSService/MLSService.d.ts.map +1 -1
  115. package/lib/messagingProtocols/mls/MLSService/MLSService.js +267 -208
  116. package/lib/messagingProtocols/mls/MLSService/MLSService.test.js +157 -160
  117. package/lib/messagingProtocols/mls/MLSService/commitBundleUtil.js +3 -3
  118. package/lib/messagingProtocols/mls/MLSService/commitBundleUtil.test.js +5 -5
  119. package/lib/messagingProtocols/mls/conversationRejoinQueue.js +2 -2
  120. package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.d.ts +78 -0
  121. package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.d.ts.map +1 -0
  122. package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.js +173 -0
  123. package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.d.ts +2 -0
  124. package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.d.ts.map +1 -0
  125. package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.js +117 -0
  126. package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.d.ts +167 -0
  127. package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.d.ts.map +1 -0
  128. package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.js +317 -0
  129. package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.d.ts +2 -0
  130. package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.d.ts.map +1 -0
  131. package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.js +248 -0
  132. package/lib/messagingProtocols/mls/recovery/index.d.ts +5 -0
  133. package/lib/messagingProtocols/mls/recovery/index.d.ts.map +1 -0
  134. package/lib/messagingProtocols/mls/recovery/index.js +28 -0
  135. package/lib/messagingProtocols/mls/types.d.ts +0 -8
  136. package/lib/messagingProtocols/mls/types.d.ts.map +1 -1
  137. package/lib/messagingProtocols/proteus/EventHandler/events/otrMessageAdd/otrMessageAdd.d.ts.map +1 -1
  138. package/lib/messagingProtocols/proteus/EventHandler/events/otrMessageAdd/otrMessageAdd.js +7 -1
  139. package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.d.ts +8 -15
  140. package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.d.ts.map +1 -1
  141. package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.js +97 -62
  142. package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CryptoClient.types.d.ts +0 -6
  143. package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CryptoClient.types.d.ts.map +1 -1
  144. package/lib/messagingProtocols/proteus/ProteusService/DecryptionErrorGenerator/DecryptionErrorGenerator.d.ts +1 -6
  145. package/lib/messagingProtocols/proteus/ProteusService/DecryptionErrorGenerator/DecryptionErrorGenerator.d.ts.map +1 -1
  146. package/lib/messagingProtocols/proteus/ProteusService/DecryptionErrorGenerator/DecryptionErrorGenerator.js +19 -22
  147. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.d.ts +5 -3
  148. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.d.ts.map +1 -1
  149. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.js +11 -24
  150. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.mocks.d.ts +1 -0
  151. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.mocks.d.ts.map +1 -1
  152. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.mocks.js +11 -2
  153. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.test.js +13 -9
  154. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.types.d.ts +3 -2
  155. package/lib/messagingProtocols/proteus/ProteusService/ProteusService.types.d.ts.map +1 -1
  156. package/lib/messagingProtocols/proteus/ProteusService/WithMockedGenerics.test.js +11 -4
  157. package/lib/messagingProtocols/proteus/ProteusService/cryptoMigrationStateStore.d.ts +0 -4
  158. package/lib/messagingProtocols/proteus/ProteusService/cryptoMigrationStateStore.d.ts.map +1 -1
  159. package/lib/messagingProtocols/proteus/ProteusService/cryptoMigrationStateStore.js +0 -5
  160. package/lib/messagingProtocols/proteus/ProteusService/identityClearer.d.ts +2 -1
  161. package/lib/messagingProtocols/proteus/ProteusService/identityClearer.d.ts.map +1 -1
  162. package/lib/messagingProtocols/proteus/ProteusService/identityClearer.js +8 -2
  163. package/lib/messagingProtocols/proteus/Utility/SessionHandler/SessionHandler.test.js +4 -0
  164. package/lib/messagingProtocols/proteus/Utility/getGenericMessageParams.d.ts +1 -1
  165. package/lib/messagingProtocols/proteus/Utility/getGenericMessageParams.d.ts.map +1 -1
  166. package/lib/messagingProtocols/proteus/Utility/getGenericMessageParams.js +1 -1
  167. package/lib/notification/NotificationService.d.ts +20 -6
  168. package/lib/notification/NotificationService.d.ts.map +1 -1
  169. package/lib/notification/NotificationService.js +23 -14
  170. package/lib/notification/NotificationService.test.js +8 -0
  171. package/lib/secretStore/secretKeyGenerator.d.ts +1 -0
  172. package/lib/secretStore/secretKeyGenerator.d.ts.map +1 -1
  173. package/lib/secretStore/secretKeyGenerator.js +3 -1
  174. package/lib/self/SelfService.d.ts +2 -2
  175. package/lib/self/SelfService.d.ts.map +1 -1
  176. package/lib/self/SelfService.test.js +5 -2
  177. package/lib/team/TeamService.d.ts +5 -2
  178. package/lib/team/TeamService.d.ts.map +1 -1
  179. package/lib/team/TeamService.js +12 -2
  180. package/lib/test/StoreHelper.d.ts +2 -0
  181. package/lib/test/StoreHelper.d.ts.map +1 -0
  182. package/lib/test/StoreHelper.js +27 -0
  183. package/lib/user/UserService.d.ts +2 -2
  184. package/lib/user/UserService.d.ts.map +1 -1
  185. package/lib/user/UserService.js +3 -3
  186. package/lib/util/TypePredicateUtil.d.ts.map +1 -1
  187. package/lib/util/TypePredicateUtil.js +2 -2
  188. package/package.json +3 -3
  189. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/IncomingMesssagesQueue.d.ts +0 -4
  190. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/IncomingMesssagesQueue.d.ts.map +0 -1
  191. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/IncomingMesssagesQueue.js +0 -69
  192. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/index.d.ts +0 -2
  193. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.test.d.ts +0 -2
  194. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.test.d.ts.map +0 -1
  195. package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.test.js +0 -98
@@ -19,16 +19,17 @@
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.ConversationService = void 0;
22
- const protocol_messaging_1 = require("@pydio/protocol-messaging");
23
22
  const conversation_1 = require("@wireapp/api-client/lib/conversation");
24
23
  const data_1 = require("@wireapp/api-client/lib/conversation/data");
25
24
  const event_1 = require("@wireapp/api-client/lib/event");
26
- const http_1 = require("@wireapp/api-client/lib/http");
25
+ const team_1 = require("@wireapp/api-client/lib/team");
27
26
  const bazinga64_1 = require("bazinga64");
28
27
  const commons_1 = require("@wireapp/commons");
28
+ const core_crypto_1 = require("@wireapp/core-crypto");
29
+ const protocol_messaging_1 = require("@wireapp/protocol-messaging");
29
30
  const conversation_2 = require("../../conversation/");
30
- const conversationRejoinQueue_1 = require("../../messagingProtocols/mls/conversationRejoinQueue");
31
- const CoreCryptoMLSError_1 = require("../../messagingProtocols/mls/MLSService/CoreCryptoMLSError");
31
+ const mls_1 = require("../../messagingProtocols/mls");
32
+ const recovery_1 = require("../../messagingProtocols/mls/recovery");
32
33
  const proteus_1 = require("../../messagingProtocols/proteus");
33
34
  const util_1 = require("../../util");
34
35
  const fullyQualifiedClientIdUtils_1 = require("../../util/fullyQualifiedClientIdUtils");
@@ -39,18 +40,44 @@ class ConversationService extends commons_1.TypedEventEmitter {
39
40
  coreDatabase;
40
41
  groupIdFromConversationId;
41
42
  subconversationService;
43
+ isMLSConversationRecoveryEnabled;
42
44
  _mlsService;
43
45
  messageTimer;
44
46
  logger = commons_1.LogFactory.getLogger('@wireapp/core/ConversationService');
45
- constructor(apiClient, proteusService, coreDatabase, groupIdFromConversationId, subconversationService, _mlsService) {
47
+ // Track groups currently undergoing recovery due to key material update failure to prevent duplicate work
48
+ groupIdConversationMap = new Map();
49
+ MLSRecoveryOrchestrator;
50
+ constructor(apiClient, proteusService, coreDatabase, groupIdFromConversationId, subconversationService, isMLSConversationRecoveryEnabled, _mlsService) {
46
51
  super();
47
52
  this.apiClient = apiClient;
48
53
  this.proteusService = proteusService;
49
54
  this.coreDatabase = coreDatabase;
50
55
  this.groupIdFromConversationId = groupIdFromConversationId;
51
56
  this.subconversationService = subconversationService;
57
+ this.isMLSConversationRecoveryEnabled = isMLSConversationRecoveryEnabled;
52
58
  this._mlsService = _mlsService;
53
59
  this.messageTimer = new conversation_2.MessageTimer();
60
+ // Make MLS recovery orchestrator mandatory in this service
61
+ if (!this._mlsService) {
62
+ throw new Error('MLSService is required to construct ConversationService with MLS capabilities');
63
+ }
64
+ this.mlsService.on(mls_1.MLSServiceEvents.MLS_EVENT_DISTRIBUTED, data => {
65
+ this.emit(mls_1.MLSServiceEvents.MLS_EVENT_DISTRIBUTED, data);
66
+ });
67
+ this.mlsService.on(mls_1.MLSServiceEvents.KEY_MATERIAL_UPDATE_FAILURE, ({ error, groupId }) => {
68
+ this.logger.warn(`Key material update failure for group ${groupId}`, { error });
69
+ return this.reactToKeyMaterialUpdateFailure({ error, groupId });
70
+ });
71
+ // Initialize MLS recovery orchestrator with default policies and single-flight de-duplication
72
+ const mapper = (0, recovery_1.createDefaultMlsErrorMapper)();
73
+ this.MLSRecoveryOrchestrator = new recovery_1.MlsRecoveryOrchestratorImpl(mapper, recovery_1.minimalDefaultPolicies, {
74
+ // Call the low-level API to avoid nested recovery when orchestrator triggers an external commit join
75
+ joinViaExternalCommit: (conversationId) => this.performJoinByExternalCommitAPI(conversationId),
76
+ resetAndReestablish: (conversationId) => this.handleBrokenMLSConversation(conversationId),
77
+ recoverFromEpochMismatch: (conversationId, subconvId) => this.recoverMLSGroupFromEpochMismatch(conversationId, subconvId),
78
+ addMissingUsers: (conversationId, groupId, users) => this.performAddUsersToMLSConversationAPI({ conversationId, groupId, qualifiedUsers: users }),
79
+ wipeMLSConversation: this.wipeMLSConversation,
80
+ });
54
81
  }
55
82
  get mlsService() {
56
83
  if (!this._mlsService) {
@@ -116,7 +143,7 @@ class ConversationService extends commons_1.TypedEventEmitter {
116
143
  */
117
144
  async send(params) {
118
145
  function isMLS(params) {
119
- return params.protocol === conversation_1.ConversationProtocol.MLS;
146
+ return params.protocol === team_1.CONVERSATION_PROTOCOL.MLS;
120
147
  }
121
148
  return (0, messageSender_1.sendMessage)(() => (isMLS(params) ? this.sendMLSMessage(params) : this.proteusService.sendMessage(params)));
122
149
  }
@@ -197,42 +224,71 @@ class ConversationService extends commons_1.TypedEventEmitter {
197
224
  if (!groupId) {
198
225
  throw new Error('No group_id found in response which is required for creating MLS conversations.');
199
226
  }
200
- const { events, failures } = await this.mlsService.registerConversation(groupId, qualifiedUsers.concat(selfUserId), {
227
+ return this.establishMLSGroupConversation(groupId, qualifiedUsers, selfUserId, selfClientId, qualifiedId);
228
+ }
229
+ /**
230
+ * Centralized handler for scenarios where an MLS conversation is detected as broken.
231
+ * It resets the conversation and then invokes the provided callback so callers can retry
232
+ * their original operation (e.g., re-adding/removing users, re-joining, etc.) with the new group id.
233
+ *
234
+ * Contract:
235
+ * - input: conversationId to reset; callback invoked after reset with the new group id
236
+ * - output: the value returned by the callback
237
+ * - error: throws if reset fails or new group id is missing
238
+ */
239
+ async handleBrokenMLSConversation(conversationId) {
240
+ if (!(await this.isMLSConversationRecoveryEnabled())) {
241
+ throw new Error('MLS conversation recovery is disabled');
242
+ }
243
+ const { conversation: { group_id: newGroupId }, } = await this.resetMLSConversation(conversationId);
244
+ if (!newGroupId) {
245
+ const errorMessage = 'Tried to reset MLS conversation but no group_id found in response';
246
+ this.logger.error(errorMessage, { conversationId });
247
+ throw new Error(errorMessage);
248
+ }
249
+ }
250
+ /**
251
+ * Will create a conversation on backend and register it to CoreCrypto once created
252
+ * @param conversationData
253
+ */
254
+ async establishMLSGroupConversation(groupId, userIdsToAdd, selfUserId, selfClientId, conversationQualifiedId) {
255
+ const failures = await this.mlsService.registerConversation(groupId, userIdsToAdd.concat(selfUserId), {
201
256
  creator: {
202
257
  user: selfUserId,
203
258
  client: selfClientId,
204
259
  },
205
260
  });
206
261
  // We fetch the fresh version of the conversation created on backend with the newly added users
207
- const conversation = await this.apiClient.api.conversation.getConversation(qualifiedId);
262
+ const conversation = await this.apiClient.api.conversation.getConversation(conversationQualifiedId);
208
263
  return {
209
- events,
210
264
  conversation,
211
265
  failedToAdd: failures,
212
266
  };
213
267
  }
214
- async sendMLSMessage(params, shouldRetry = true) {
215
- const { payload, groupId, conversationId } = params;
268
+ /**
269
+ * Send an MLS message wrapped with recovery.
270
+ *
271
+ * Uses the MLS recovery orchestrator to handle transient MLS errors (for example, wrong epoch)
272
+ * according to per-operation policies. When configured, the original send is retried once
273
+ * after a successful recovery. Unrecoverable errors are re-thrown by the orchestrator.
274
+ * The low-level send logic lives in {@link performSendMLSMessageAPI}.
275
+ */
276
+ async sendMLSMessage(params) {
277
+ const { groupId, conversationId } = params;
278
+ return this.MLSRecoveryOrchestrator.execute({
279
+ context: { operationName: recovery_1.OperationName.send, qualifiedConversationId: conversationId, groupId },
280
+ callBack: () => this.performSendMLSMessageAPI(params),
281
+ });
282
+ }
283
+ // Low-level API for sending an MLS message without any recovery logic
284
+ async performSendMLSMessageAPI(params) {
285
+ const { payload, groupId } = params;
216
286
  const groupIdBytes = bazinga64_1.Decoder.fromBase64(groupId).asBytes;
217
287
  // immediately execute pending commits before sending the message
218
- await this.mlsService.commitPendingProposals(groupId);
219
- const encrypted = await this.mlsService.encryptMessage(groupIdBytes, protocol_messaging_1.GenericMessage.encode(payload).finish());
220
- let response = null;
221
- let sentAt = '';
222
- try {
223
- response = await this.apiClient.api.conversation.postMlsMessage(encrypted);
224
- sentAt = response.time?.length > 0 ? response.time : new Date().toISOString();
225
- }
226
- catch (error) {
227
- const isMLSStaleMessageError = error instanceof http_1.BackendError && error.label === http_1.BackendErrorLabel.MLS_STALE_MESSAGE;
228
- if (isMLSStaleMessageError) {
229
- await this.recoverMLSGroupFromEpochMismatch(conversationId);
230
- if (shouldRetry) {
231
- return this.sendMLSMessage(params, false);
232
- }
233
- }
234
- throw error;
235
- }
288
+ await this.mlsService.commitPendingProposals(groupId, true, params);
289
+ const encrypted = await this.mlsService.encryptMessage(new core_crypto_1.ConversationId(groupIdBytes), protocol_messaging_1.GenericMessage.encode(payload).finish());
290
+ const response = await this.apiClient.api.conversation.postMlsMessage(encrypted);
291
+ const sentAt = response.time?.length > 0 ? response.time : new Date().toISOString();
236
292
  const failedToSend = response?.failed || (response?.failed_to_send ?? []).length > 0
237
293
  ? {
238
294
  queued: response?.failed_to_send,
@@ -247,40 +303,166 @@ class ConversationService extends commons_1.TypedEventEmitter {
247
303
  };
248
304
  }
249
305
  /**
250
- * Will add users to existing MLS group by claiming their key packages and passing them to CoreCrypto.addClientsToConversation
306
+ * Add users to an existing MLS group with recovery.
307
+ *
308
+ * Claims key packages and passes them to CoreCrypto.addClientsToConversation. The MLS recovery
309
+ * orchestrator handles recoverable failures (e.g., wrong epoch) and may retry the operation once
310
+ * depending on policy. The optional shouldRetry flag is ignored; retries are governed by policies.
251
311
  *
252
- * @param qualifiedUsers List of qualified user ids (with optional skipOwnClientId field - if provided we will not claim key package for this self client)
253
- * @param groupId Id of the group to which we want to add users
254
- * @param conversationId Id of the conversation to which we want to add users
312
+ * @param qualifiedUsers List of qualified user ids (use skipOwnClientId on self to avoid claiming its key package)
313
+ * @param groupId Id of the MLS group to add users to
314
+ * @param conversationId Qualified id of the conversation
255
315
  */
256
316
  async addUsersToMLSConversation({ qualifiedUsers, groupId, conversationId, }) {
257
- const { keyPackages, failures: keysClaimingFailures } = await this.mlsService.getKeyPackagesPayload(qualifiedUsers);
258
- const { events, failures } = keyPackages.length > 0
259
- ? await this.mlsService.addUsersToExistingConversation(groupId, keyPackages)
260
- : { events: [], failures: [] };
317
+ return this.MLSRecoveryOrchestrator.execute({
318
+ context: { operationName: recovery_1.OperationName.addUsers, qualifiedConversationId: conversationId, groupId },
319
+ callBack: () => this.performAddUsersToMLSConversationAPI({ qualifiedUsers, groupId, conversationId }),
320
+ });
321
+ }
322
+ // Low-level API to add users without any recovery logic; used by orchestrator and direct callers
323
+ async performAddUsersToMLSConversationAPI({ qualifiedUsers, groupId, conversationId, }) {
324
+ this.logger.info(`Adding users to MLS conversation`, { groupId, conversationId, qualifiedUsers });
325
+ const exisitingClientIdsInGroup = await this.mlsService.getClientIdsInGroup(groupId);
261
326
  const conversation = await this.getConversation(conversationId);
262
- //We store the info when user was added (and key material was created), so we will know when to renew it
263
- await this.mlsService.resetKeyMaterialRenewal(groupId);
327
+ const { keyPackages, failures: keysClaimingFailures } = await this.mlsService.getKeyPackagesPayload(qualifiedUsers, exisitingClientIdsInGroup);
328
+ // We had cases where did not get any key packages, but still used core-crypto to call the backend (which results in failure).
329
+ if (keyPackages && keyPackages?.length > 0) {
330
+ await this.mlsService.addUsersToExistingConversation(groupId, keyPackages);
331
+ // We store the info when user was added (and key material was created), so we will know when to renew it
332
+ await this.mlsService.resetKeyMaterialRenewal(groupId);
333
+ }
264
334
  return {
265
- events,
266
335
  conversation,
267
- failedToAdd: [...keysClaimingFailures, ...failures],
336
+ failedToAdd: keysClaimingFailures,
268
337
  };
269
338
  }
339
+ /**
340
+ * Remove users from an existing MLS group with recovery.
341
+ *
342
+ * The MLS recovery orchestrator handles recoverable failures and may retry the operation once
343
+ * depending on policy. The optional shouldRetry flag is ignored; retries are policy-driven.
344
+ */
270
345
  async removeUsersFromMLSConversation({ groupId, conversationId, qualifiedUserIds, }) {
346
+ return this.MLSRecoveryOrchestrator.execute({
347
+ context: { operationName: recovery_1.OperationName.removeUsers, qualifiedConversationId: conversationId, groupId },
348
+ callBack: () => this.performRemoveUsersFromMLSConversationAPI({ groupId, conversationId, qualifiedUserIds }),
349
+ });
350
+ }
351
+ // Low-level API to remove users without recovery logic; used by orchestrator and direct callers
352
+ async performRemoveUsersFromMLSConversationAPI({ groupId, conversationId, qualifiedUserIds, }) {
271
353
  const clientsToRemove = await this.apiClient.api.user.postListClients({ qualified_users: qualifiedUserIds });
272
354
  const fullyQualifiedClientIds = (0, fullyQualifiedClientIdUtils_1.mapQualifiedUserClientIdsToFullyQualifiedClientIds)(clientsToRemove.qualified_user_map);
273
- const messageResponse = await this.mlsService.removeClientsFromConversation(groupId, fullyQualifiedClientIds);
274
- //key material gets updated after removing a user from the group, so we can reset last key update time value in the store
355
+ await this.mlsService.removeClientsFromConversation(groupId, fullyQualifiedClientIds);
356
+ // key material gets updated after removing a user from the group, so we can reset last key update time value in the store
275
357
  await this.mlsService.resetKeyMaterialRenewal(groupId);
276
- const conversation = await this.getConversation(conversationId);
277
- return {
278
- events: messageResponse.events,
279
- conversation,
280
- };
358
+ return await this.getConversation(conversationId);
281
359
  }
360
+ /**
361
+ * Join an MLS conversation via external commit with recovery.
362
+ *
363
+ * If the group is not established or is out of date, the orchestrator recovers accordingly.
364
+ * The join operation itself is not automatically re-run by policy.
365
+ */
282
366
  async joinByExternalCommit(conversationId) {
283
- return this.mlsService.joinByExternalCommit(() => this.apiClient.api.conversation.getGroupInfo(conversationId));
367
+ await this.MLSRecoveryOrchestrator.execute({
368
+ context: { operationName: recovery_1.OperationName.joinExternalCommit, qualifiedConversationId: conversationId },
369
+ callBack: () => this.performJoinByExternalCommitAPI(conversationId),
370
+ });
371
+ }
372
+ // Low-level API call for joining via external commit (no recovery logic)
373
+ async performJoinByExternalCommitAPI(conversationId) {
374
+ await this.mlsService.joinByExternalCommit(() => this.apiClient.api.conversation.getGroupInfo(conversationId));
375
+ }
376
+ async refreshGroupIdConversationMap() {
377
+ const conversations = await this.apiClient.api.conversation.getConversationList();
378
+ this.groupIdConversationMap.clear();
379
+ for (const conversation of conversations.found || []) {
380
+ if (conversation.group_id) {
381
+ this.groupIdConversationMap.set(conversation.group_id, conversation);
382
+ }
383
+ }
384
+ }
385
+ async getConversationByGroupId(groupId) {
386
+ if (!this.groupIdConversationMap.has(groupId)) {
387
+ await this.refreshGroupIdConversationMap();
388
+ }
389
+ return this.groupIdConversationMap.get(groupId);
390
+ }
391
+ /**
392
+ * React to a key material update failure using the recovery orchestrator.
393
+ *
394
+ * The original error is forwarded to the orchestrator under the 'keyMaterialUpdate' operation
395
+ * so it can map and apply the configured recovery policy. Unrecoverable errors are logged.
396
+ */
397
+ reactToKeyMaterialUpdateFailure = async ({ error, groupId }) => {
398
+ this.logger.info(`Reacting to key material update failure for group ${groupId}`);
399
+ const conversation = await this.getConversationByGroupId(groupId);
400
+ if (!conversation) {
401
+ this.logger.warn(`No conversation found for group ${groupId}`, { error });
402
+ return;
403
+ }
404
+ try {
405
+ await this.MLSRecoveryOrchestrator.execute({
406
+ context: {
407
+ operationName: recovery_1.OperationName.keyMaterialUpdate,
408
+ qualifiedConversationId: conversation.qualified_id,
409
+ groupId,
410
+ },
411
+ callBack: async () => {
412
+ // Surface the ORIGINAL error to the orchestrator for mapping & policy resolution
413
+ // Deliberately throwing the raw value so mapper can recognize core-crypto/API error shapes
414
+ throw error;
415
+ },
416
+ });
417
+ }
418
+ catch (e) {
419
+ this.logger.error('Failed to react to key material update failure', { error: e, groupId });
420
+ }
421
+ };
422
+ async resetMLSConversation(conversationId) {
423
+ this.logger.info(`Resetting MLS conversation with id ${conversationId.id}`);
424
+ // STEP 1: Fetch the conversation to retrieve the group ID & epoch
425
+ const conversation = await this.apiClient.api.conversation.getConversation(conversationId);
426
+ const { group_id: groupId, epoch } = conversation;
427
+ if (!groupId || !epoch) {
428
+ const errorMessage = 'Could not find group id or epoch for the conversation';
429
+ this.logger.error(errorMessage, { conversationId });
430
+ throw new Error(errorMessage);
431
+ }
432
+ // STEP 2: Request backend to reset the conversation
433
+ this.logger.info(`Requesting backend to reset the conversation (group_id: ${groupId}, epoch: ${String(epoch)})`);
434
+ await this.apiClient.api.conversation.resetMLSConversation({
435
+ epoch,
436
+ groupId,
437
+ });
438
+ // STEP 3: fetch self user info
439
+ this.logger.info(`Re-establishing the conversation by re-adding all members (conversation_id: ${conversationId.id})`);
440
+ const { validatedClientId: clientId, userId, domain } = this.apiClient;
441
+ if (!userId || !domain) {
442
+ const errorMessage = 'Could not find userId or domain of the self user';
443
+ this.logger.error(errorMessage, { conversationId });
444
+ throw new Error(errorMessage);
445
+ }
446
+ const selfUserQualifiedId = { id: userId, domain };
447
+ // STEP 4: Fetch the updated conversation data from backend to retrieve the new group ID
448
+ const updatedConversation = await this.apiClient.api.conversation.getConversation(conversationId);
449
+ const { group_id: newGroupId, members } = updatedConversation;
450
+ this.logger.info(`MLS conversation new group ID fetched from backend ${conversationId.id}`, {
451
+ newGroupId,
452
+ });
453
+ if (!newGroupId || !clientId) {
454
+ throw new Error(`Failed to recover MLS conversation: missing groupId (${newGroupId}), or clientId (${clientId})`);
455
+ }
456
+ const usersToReAdd = members.others.map(member => member.qualified_id).filter(userId => !!userId);
457
+ // STEP 5: Re-establish the conversation by re-adding all members
458
+ return await this.establishMLSGroupConversation(newGroupId, usersToReAdd, selfUserQualifiedId, clientId, conversationId).then(result => {
459
+ this.logger.info(`Successfully reset MLS conversation`, {
460
+ conversationId: conversationId.id,
461
+ oldGroupId: groupId,
462
+ newGroupId,
463
+ });
464
+ return result;
465
+ });
284
466
  }
285
467
  /**
286
468
  * Will check if mls group exists locally.
@@ -297,9 +479,9 @@ class ConversationService extends commons_1.TypedEventEmitter {
297
479
  async isMLSGroupEstablishedLocally(groupId) {
298
480
  return this.mlsService.isConversationEstablished(groupId);
299
481
  }
300
- async wipeMLSConversation(groupId) {
482
+ wipeMLSConversation = async (groupId) => {
301
483
  return this.mlsService.wipeConversation(groupId);
302
- }
484
+ };
303
485
  async matchesEpoch(groupId, backendEpoch) {
304
486
  const localEpoch = await this.mlsService.getEpoch(groupId);
305
487
  this.logger.debug(`Comparing conversation's (group_id: ${groupId}) local and backend epoch number: {local: ${String(localEpoch)}, backend: ${backendEpoch}}`);
@@ -367,8 +549,14 @@ class ConversationService extends commons_1.TypedEventEmitter {
367
549
  async hasEpochMismatch(groupId, epoch) {
368
550
  const isEstablished = await this.mlsGroupExistsLocally(groupId);
369
551
  const doesEpochMatch = isEstablished && (await this.matchesEpoch(groupId, epoch));
370
- //if conversation is not established or epoch does not match -> try to rejoin
371
- return !isEstablished || !doesEpochMatch;
552
+ // if conversation is not established or epoch does not match -> try to rejoin
553
+ const hasEpochMismatch = !isEstablished || !doesEpochMatch;
554
+ this.logger.info(`Conversation (group_id: ${groupId}) epoch mismatch check result: ${hasEpochMismatch}`, {
555
+ isEstablished,
556
+ doesEpochMatch,
557
+ epoch,
558
+ });
559
+ return hasEpochMismatch;
372
560
  }
373
561
  /**
374
562
  * Get a MLS 1:1-conversation with a given user.
@@ -438,47 +626,71 @@ class ConversationService extends commons_1.TypedEventEmitter {
438
626
  * @param qualifiedUsers - list of qualified users to add to the group (should not include the self user)
439
627
  */
440
628
  async tryEstablishingMLSGroup({ groupId, conversationId, selfUserId, qualifiedUsers, }) {
441
- const wasGroupEstablishedBySelfClient = await this.mlsService.tryEstablishingMLSGroup(groupId);
442
- if (!wasGroupEstablishedBySelfClient) {
443
- this.logger.debug('Group was not established by self client, skipping adding users to the group.');
444
- return;
445
- }
446
- this.logger.debug('Group was established by self client, adding other users to the group...');
447
- const usersToAdd = [
448
- ...qualifiedUsers,
449
- { ...selfUserId, skipOwnClientId: this.apiClient.validatedClientId },
450
- ];
451
- const { conversation } = await this.addUsersToMLSConversation({
452
- conversationId,
453
- groupId,
454
- qualifiedUsers: usersToAdd,
455
- });
456
- const addedUsers = conversation.members.others;
457
- if (addedUsers.length > 0) {
458
- this.logger.debug(`Successfully added ${addedUsers} users to the group.`);
629
+ try {
630
+ const wasGroupEstablishedBySelfClient = await this.mlsService.tryEstablishingMLSGroup(groupId);
631
+ if (!wasGroupEstablishedBySelfClient) {
632
+ this.logger.debug('Group was not established by self client, skipping adding users to the group.');
633
+ return;
634
+ }
635
+ this.logger.debug('Group was established by self client, adding other users to the group...');
636
+ const usersToAdd = [
637
+ ...qualifiedUsers,
638
+ { ...selfUserId, skipOwnClientId: this.apiClient.validatedClientId },
639
+ ];
640
+ const { conversation } = await this.addUsersToMLSConversation({
641
+ conversationId,
642
+ groupId,
643
+ qualifiedUsers: usersToAdd,
644
+ });
645
+ const addedUsers = conversation.members.others;
646
+ if (addedUsers.length > 0) {
647
+ this.logger.debug(`Successfully added ${addedUsers} users to the group.`);
648
+ }
649
+ else {
650
+ this.logger.debug('No other users were added to the group.');
651
+ }
459
652
  }
460
- else {
461
- this.logger.debug('No other users were added to the group.');
653
+ catch (error) {
654
+ this.logger.error('Failed to establish MLS group', error);
655
+ throw error;
462
656
  }
463
657
  }
658
+ /**
659
+ * Handle an inbound MLS message-add event with recovery.
660
+ *
661
+ * Policies (see MlsRecoveryOrchestrator):
662
+ * - WrongEpoch.handleMessageAdd → recover from epoch mismatch and re-run once.
663
+ * - GroupOutOfSync.handleMessageAdd → not handled here; the error bubbles.
664
+ *
665
+ * Returns the decrypted payload when available. Unknown or unrecoverable errors are logged
666
+ * and result in null so event processing can continue safely.
667
+ */
464
668
  async handleMLSMessageAddEvent(event) {
465
669
  try {
466
- return await this.mlsService.handleMLSMessageAddEvent(event, this.groupIdFromConversationId);
670
+ const { qualified_conversation: qualifiedConversationId, subconv } = event;
671
+ if (!qualifiedConversationId) {
672
+ throw new Error('Qualified conversation id is missing in the MLS message-add event');
673
+ }
674
+ return await this.MLSRecoveryOrchestrator.execute({
675
+ context: {
676
+ operationName: recovery_1.OperationName.handleMessageAdd,
677
+ qualifiedConversationId,
678
+ subconvId: subconv,
679
+ },
680
+ callBack: async () => {
681
+ return this.mlsService.handleMLSMessageAddEvent(event, this.groupIdFromConversationId);
682
+ },
683
+ });
467
684
  }
468
685
  catch (error) {
469
- if ((0, CoreCryptoMLSError_1.isCoreCryptoMLSWrongEpochError)(error)) {
470
- this.logger.warn(`Received message for the wrong epoch in conversation ${event.conversation}, handling epoch mismatch...`);
471
- const { qualified_conversation: conversationId, subconv } = event;
472
- if (!conversationId) {
473
- throw new Error('Qualified conversation id is missing in the event');
474
- }
475
- (0, conversationRejoinQueue_1.queueConversationRejoin)(conversationId.id, () => this.recoverMLSGroupFromEpochMismatch(conversationId, subconv));
476
- return null;
477
- }
478
- throw error;
686
+ // For unmapped or unrecoverable errors, avoid surfacing exceptions from event handling
687
+ // and instead log and return null so the event processing queue can continue safely.
688
+ this.logger.error('Failed to handle MLS message-add event after recovery; returning null', { error, event });
689
+ return null;
479
690
  }
480
691
  }
481
692
  async recoverMLSGroupFromEpochMismatch(conversationId, subconversationId) {
693
+ this.logger.info(`Recovering MLS group from epoch mismatch`, { conversationId, subconversationId });
482
694
  if (subconversationId) {
483
695
  const parentGroupId = await this.groupIdFromConversationId(conversationId);
484
696
  const subconversation = await this.apiClient.api.conversation.getSubconversation(conversationId, subconversationId);
@@ -493,24 +705,25 @@ class ConversationService extends commons_1.TypedEventEmitter {
493
705
  }
494
706
  return this.handleConversationEpochMismatch(mlsConversation, () => this.emit('MLSConversationRecovered', { conversationId: mlsConversation.qualified_id }));
495
707
  }
708
+ /**
709
+ * Handle an MLS welcome event with recovery.
710
+ *
711
+ * Policies (see MlsRecoveryOrchestrator):
712
+ * - OrphanWelcome → join via external commit (no auto re-run).
713
+ * - ConversationAlreadyExists → wipe local state and re-run welcome once.
714
+ *
715
+ * Always resolves to null; the effects are applied to local state.
716
+ */
496
717
  async handleMLSWelcomeMessageEvent(event) {
497
- try {
498
- return await this.mlsService.handleMLSWelcomeMessageEvent(event, this.apiClient.validatedClientId);
499
- }
500
- catch (error) {
501
- if ((0, CoreCryptoMLSError_1.isCoreCryptoMLSOrphanWelcomeMessageError)(error)) {
502
- const { qualified_conversation: conversationId } = event;
503
- // Note that we don't care about a subconversation here, as the welcome message is always for the parent conversation.
504
- // Subconversations are always joined via external commit.
505
- if (!conversationId) {
506
- throw new Error('Qualified conversation id is missing in the event');
507
- }
508
- this.logger.warn(`Received an orphan welcome message, joining the conversation (${conversationId.id}) via external commit...`);
509
- void (0, conversationRejoinQueue_1.queueConversationRejoin)(conversationId.id, () => this.joinByExternalCommit(conversationId));
510
- return null;
511
- }
512
- throw error;
513
- }
718
+ this.logger.info('Handling MLS welcome message event (orchestrated)', { event });
719
+ await this.MLSRecoveryOrchestrator.execute({
720
+ context: {
721
+ operationName: recovery_1.OperationName.handleWelcome,
722
+ qualifiedConversationId: event.qualified_conversation,
723
+ },
724
+ callBack: () => this.mlsService.handleMLSWelcomeMessageEvent(event, this.apiClient.validatedClientId),
725
+ });
726
+ return null;
514
727
  }
515
728
  async handleOtrMessageAddEvent(event) {
516
729
  return this.proteusService.handleOtrMessageAddEvent(event);