@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
@@ -45,10 +45,13 @@ const client_1 = require("@wireapp/api-client/lib/client");
45
45
  const conversation_1 = require("@wireapp/api-client/lib/conversation");
46
46
  const event_1 = require("@wireapp/api-client/lib/event");
47
47
  const http_1 = require("@wireapp/api-client/lib/http");
48
+ const team_1 = require("@wireapp/api-client/lib/team");
48
49
  const http_status_codes_1 = require("http-status-codes");
49
50
  const api_client_1 = require("@wireapp/api-client");
51
+ const core_crypto_1 = require("@wireapp/core-crypto");
50
52
  const __1 = require("..");
51
53
  const CoreCryptoMLSError_1 = require("../../messagingProtocols/mls/MLSService/CoreCryptoMLSError");
54
+ const MLSService_1 = require("../../messagingProtocols/mls/MLSService/MLSService");
52
55
  const MessagingProtocols = __importStar(require("../../messagingProtocols/proteus"));
53
56
  const CoreDB_1 = require("../../storage/CoreDB");
54
57
  const PayloadHelper = __importStar(require("../../test/PayloadHelper"));
@@ -86,9 +89,14 @@ const mockedProteusService = {
86
89
  encryptGenericMessage: () => Promise.resolve(),
87
90
  sendProteusMessage: () => Promise.resolve({ sentAt: new Date() }),
88
91
  };
92
+ const apiClients = [];
89
93
  describe('ConversationService', () => {
94
+ afterAll(() => {
95
+ apiClients.forEach(client => client.disconnect());
96
+ });
90
97
  async function buildConversationService() {
91
98
  const client = new api_client_1.APIClient({ urls: api_client_1.APIClient.BACKEND.STAGING });
99
+ apiClients.push(client);
92
100
  jest.spyOn(client.api.conversation, 'postMlsMessage').mockReturnValue(Promise.resolve({
93
101
  events: [],
94
102
  time: new Date().toISOString(),
@@ -106,13 +114,14 @@ describe('ConversationService', () => {
106
114
  }));
107
115
  jest
108
116
  .spyOn(client.api.user, 'getUserSupportedProtocols')
109
- .mockReturnValue(Promise.resolve([conversation_1.ConversationProtocol.MLS, conversation_1.ConversationProtocol.PROTEUS]));
117
+ .mockReturnValue(Promise.resolve([team_1.CONVERSATION_PROTOCOL.MLS, team_1.CONVERSATION_PROTOCOL.PROTEUS]));
110
118
  client.context = {
111
119
  clientType: client_1.ClientType.NONE,
112
120
  userId: PayloadHelper.getUUID(),
113
121
  clientId: PayloadHelper.getUUID(),
114
122
  };
115
123
  const mockedMLSService = {
124
+ on: jest.fn(),
116
125
  encryptMessage: () => { },
117
126
  commitPendingProposals: () => Promise.resolve(),
118
127
  getEpoch: () => Promise.resolve(),
@@ -124,8 +133,10 @@ describe('ConversationService', () => {
124
133
  conversationExists: jest.fn(),
125
134
  isConversationEstablished: jest.fn(),
126
135
  tryEstablishingMLSGroup: jest.fn(),
136
+ getClientIdsInGroup: jest.fn(),
127
137
  getKeyPackagesPayload: jest.fn(),
128
138
  addUsersToExistingConversation: jest.fn(),
139
+ removeClientsFromConversation: jest.fn(),
129
140
  resetKeyMaterialRenewal: jest.fn(),
130
141
  handleMLSWelcomeMessageEvent: jest.fn(),
131
142
  };
@@ -134,7 +145,7 @@ describe('ConversationService', () => {
134
145
  const mockedSubconversationService = {
135
146
  joinConferenceSubconversation: jest.fn(),
136
147
  };
137
- const conversationService = new __1.ConversationService(client, mockedProteusService, mockedDb, groupIdFromConversationId, mockedSubconversationService, mockedMLSService);
148
+ const conversationService = new __1.ConversationService(client, mockedProteusService, mockedDb, groupIdFromConversationId, mockedSubconversationService, () => Promise.resolve(true), mockedMLSService);
138
149
  jest.spyOn(conversationService, 'joinByExternalCommit');
139
150
  jest.spyOn(conversationService, 'emit');
140
151
  return [
@@ -157,7 +168,7 @@ describe('ConversationService', () => {
157
168
  const sentTime = new Date().toISOString();
158
169
  mockedProteusService.sendMessage = jest.fn().mockResolvedValue({ sentAt: sentTime });
159
170
  const promise = conversationService.send({
160
- protocol: conversation_1.ConversationProtocol.PROTEUS,
171
+ protocol: team_1.CONVERSATION_PROTOCOL.PROTEUS,
161
172
  conversationId: { id: 'conv1', domain: '' },
162
173
  payload: message,
163
174
  });
@@ -166,6 +177,80 @@ describe('ConversationService', () => {
166
177
  });
167
178
  });
168
179
  });
180
+ describe('removeUsersFromMLSConversation', () => {
181
+ it('recovers and retries when stale-message occurs during remove users commit upload', async () => {
182
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
183
+ const mockGroupId = 'groupId-stale-remove';
184
+ const mockConversationId = { id: PayloadHelper.getUUID(), domain: 'staging.zinfra.io' };
185
+ const qualifiedUserIds = [
186
+ { id: 'test-id-1', domain: 'test-domain' },
187
+ { id: 'test-id-2', domain: 'test-domain' },
188
+ ];
189
+ const staleMessageError = {
190
+ type: core_crypto_1.ErrorType.Mls,
191
+ context: {
192
+ type: core_crypto_1.MlsErrorType.MessageRejected,
193
+ context: {
194
+ reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
195
+ },
196
+ },
197
+ };
198
+ // First removal attempt fails with stale, second succeeds
199
+ jest
200
+ .spyOn(mlsService, 'removeClientsFromConversation')
201
+ .mockRejectedValueOnce(staleMessageError)
202
+ .mockResolvedValueOnce(undefined);
203
+ const remoteEpoch = 6;
204
+ const localEpoch = 5;
205
+ jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
206
+ jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
207
+ jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValue({
208
+ qualified_id: mockConversationId,
209
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
210
+ epoch: remoteEpoch,
211
+ group_id: mockGroupId,
212
+ });
213
+ await conversationService.removeUsersFromMLSConversation({
214
+ groupId: mockGroupId,
215
+ conversationId: mockConversationId,
216
+ qualifiedUserIds,
217
+ });
218
+ expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
219
+ expect(mlsService.removeClientsFromConversation).toHaveBeenCalledTimes(2);
220
+ expect(mlsService.resetKeyMaterialRenewal).toHaveBeenCalledWith(mockGroupId);
221
+ });
222
+ });
223
+ describe('joinByExternalCommit', () => {
224
+ it('retries join when stale-message occurs during external commit join', async () => {
225
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
226
+ const conversationId = { id: 'conv-join-stale', domain: 'staging.zinfra.io' };
227
+ const staleMessageError = {
228
+ type: core_crypto_1.ErrorType.Mls,
229
+ context: {
230
+ type: core_crypto_1.MlsErrorType.MessageRejected,
231
+ context: {
232
+ reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
233
+ },
234
+ },
235
+ };
236
+ jest
237
+ .spyOn(mlsService, 'joinByExternalCommit')
238
+ .mockRejectedValueOnce(staleMessageError)
239
+ .mockResolvedValueOnce(undefined);
240
+ const remoteEpoch = 10;
241
+ const localEpoch = 9;
242
+ jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
243
+ jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
244
+ jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
245
+ qualified_id: conversationId,
246
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
247
+ epoch: remoteEpoch,
248
+ group_id: 'gid-join-stale',
249
+ });
250
+ await conversationService.joinByExternalCommit(conversationId);
251
+ expect(mlsService.joinByExternalCommit).toHaveBeenCalledTimes(2);
252
+ });
253
+ });
169
254
  describe('"send MLS"', () => {
170
255
  const groupId = PayloadHelper.getUUID();
171
256
  const messages = [
@@ -181,7 +266,7 @@ describe('ConversationService', () => {
181
266
  it(`calls callbacks when sending '${type}' message is starting and successful`, async () => {
182
267
  const [conversationService] = await buildConversationService();
183
268
  const promise = conversationService.send({
184
- protocol: conversation_1.ConversationProtocol.MLS,
269
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
185
270
  groupId,
186
271
  payload: message,
187
272
  conversationId: { id: '', domain: '' },
@@ -190,26 +275,62 @@ describe('ConversationService', () => {
190
275
  expect(result.state).toBe(__1.MessageSendingState.OUTGOING_SENT);
191
276
  });
192
277
  });
193
- it('rejoins a MLS group when failed encrypting MLS message', async () => {
278
+ it('rejoins a MLS group when stale-message error occurs during commit bundle upload', async () => {
194
279
  const [conversationService, { apiClient, mlsService }] = await buildConversationService();
195
280
  const mockGroupId = 'AAEAAH87aajaQ011i+rNLmwpy0sAZGl5YS53aXJlamxpbms=';
196
281
  const mockConversationId = { id: 'mockConversationId', domain: 'staging.zinfra.io' };
197
282
  const mockedMessage = MessageBuilder.buildTextMessage({ text: 'test' });
283
+ const staleMessageError = new conversation_1.MLSStaleMessageError('', http_1.BackendErrorLabel.MLS_STALE_MESSAGE, http_status_codes_1.StatusCodes.CONFLICT);
284
+ // First attempt to upload commit bundle fails with stale-message, second attempt succeeds
198
285
  jest
199
- .spyOn(apiClient.api.conversation, 'postMlsMessage')
200
- .mockRejectedValueOnce(new http_1.BackendError('', http_1.BackendErrorLabel.MLS_STALE_MESSAGE, http_status_codes_1.StatusCodes.CONFLICT));
286
+ .spyOn(mlsService, 'commitPendingProposals')
287
+ .mockRejectedValueOnce(staleMessageError)
288
+ .mockResolvedValueOnce(undefined);
289
+ const remoteEpoch = 5;
290
+ const localEpoch = 4;
291
+ jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
292
+ jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
293
+ jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
294
+ qualified_id: mockConversationId,
295
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
296
+ epoch: remoteEpoch,
297
+ group_id: mockGroupId,
298
+ });
299
+ await conversationService.send({
300
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
301
+ groupId: mockGroupId,
302
+ payload: mockedMessage,
303
+ conversationId: mockConversationId,
304
+ });
305
+ // Recovery via external commit should have been triggered
306
+ expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
307
+ expect(conversationService.emit).toHaveBeenCalledWith('MLSConversationRecovered', {
308
+ conversationId: mockConversationId,
309
+ });
310
+ // Because the failure happened before posting the message, postMlsMessage should be called only once (after recovery)
311
+ expect(apiClient.api.conversation.postMlsMessage).toHaveBeenCalledTimes(1);
312
+ // commitPendingProposals is called twice: first fails, second succeeds
313
+ expect(mlsService.commitPendingProposals).toHaveBeenCalledTimes(2);
314
+ });
315
+ it('rejoins a MLS group when failed encrypting MLS message', async () => {
316
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
317
+ const mockGroupId = 'AAEAAH87aajaQ011i+rNLmwpy0sAZGl5YS53aXJlamxpbms=';
318
+ const mockConversationId = { id: 'mockConversationId', domain: 'staging.zinfra.io' };
319
+ const mockedMessage = MessageBuilder.buildTextMessage({ text: 'test' });
320
+ const staleMessageError = new conversation_1.MLSStaleMessageError('', http_1.BackendErrorLabel.MLS_STALE_MESSAGE, http_status_codes_1.StatusCodes.CONFLICT);
321
+ jest.spyOn(apiClient.api.conversation, 'postMlsMessage').mockRejectedValueOnce(staleMessageError);
201
322
  const remoteEpoch = 5;
202
323
  const localEpoch = 4;
203
324
  jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
204
325
  jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
205
326
  jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
206
327
  qualified_id: mockConversationId,
207
- protocol: conversation_1.ConversationProtocol.MLS,
328
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
208
329
  epoch: remoteEpoch,
209
330
  group_id: mockGroupId,
210
331
  });
211
332
  await conversationService.send({
212
- protocol: conversation_1.ConversationProtocol.MLS,
333
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
213
334
  groupId: mockGroupId,
214
335
  payload: mockedMessage,
215
336
  conversationId: mockConversationId,
@@ -220,6 +341,34 @@ describe('ConversationService', () => {
220
341
  });
221
342
  expect(apiClient.api.conversation.postMlsMessage).toHaveBeenCalledTimes(2);
222
343
  });
344
+ it('adds missing users to MLS group and retries when group is out of sync during send', async () => {
345
+ const [conversationService, { apiClient }] = await buildConversationService();
346
+ const mockGroupId = 'AAEAAH87aajaQ011i+rNLmwpy0sAZGl5YS53aXJlamxpbms=';
347
+ const mockConversationId = { id: 'mockConversationId', domain: 'staging.zinfra.io' };
348
+ const mockedMessage = MessageBuilder.buildTextMessage({ text: 'test' });
349
+ const missingUsers = [
350
+ { id: 'user-1', domain: 'staging.zinfra.io' },
351
+ { id: 'user-2', domain: 'staging.zinfra.io' },
352
+ ];
353
+ const outOfSyncError = new conversation_1.MLSGroupOutOfSyncError(http_status_codes_1.StatusCodes.CONFLICT, missingUsers, http_1.BackendErrorLabel.MLS_GROUP_OUT_OF_SYNC);
354
+ // First send fails with out-of-sync, second succeeds via default mock
355
+ jest.spyOn(apiClient.api.conversation, 'postMlsMessage').mockRejectedValueOnce(outOfSyncError);
356
+ const addUsersSpy = jest
357
+ .spyOn(conversationService, 'performAddUsersToMLSConversationAPI')
358
+ .mockResolvedValueOnce({ conversation: { members: { others: [] } } });
359
+ await conversationService.send({
360
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
361
+ groupId: mockGroupId,
362
+ payload: mockedMessage,
363
+ conversationId: mockConversationId,
364
+ });
365
+ expect(addUsersSpy).toHaveBeenCalledWith({
366
+ groupId: mockGroupId,
367
+ conversationId: mockConversationId,
368
+ qualifiedUsers: missingUsers,
369
+ });
370
+ expect(apiClient.api.conversation.postMlsMessage).toHaveBeenCalledTimes(2);
371
+ });
223
372
  });
224
373
  describe('handleConversationsEpochMismatch', () => {
225
374
  beforeEach(() => {
@@ -229,7 +378,7 @@ describe('ConversationService', () => {
229
378
  return {
230
379
  group_id: 'group-id',
231
380
  qualified_id: { id: conversationId || 'conversation-id', domain: 'staging.zinfra.io' },
232
- protocol: conversation_1.ConversationProtocol.MLS,
381
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
233
382
  epoch,
234
383
  };
235
384
  };
@@ -279,7 +428,7 @@ describe('ConversationService', () => {
279
428
  const remoteEpoch = 1;
280
429
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
281
430
  qualified_id: mockConversationId,
282
- protocol: conversation_1.ConversationProtocol.MLS,
431
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
283
432
  epoch: remoteEpoch,
284
433
  group_id: mockGroupId,
285
434
  });
@@ -298,19 +447,19 @@ describe('ConversationService', () => {
298
447
  const updatedEpoch = 2;
299
448
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
300
449
  qualified_id: mockConversationId,
301
- protocol: conversation_1.ConversationProtocol.MLS,
450
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
302
451
  epoch: remoteEpoch,
303
452
  group_id: mockGroupId,
304
453
  });
305
454
  // The 2nd request we make after joining the conversation with external commit
306
455
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
307
456
  qualified_id: mockConversationId,
308
- protocol: conversation_1.ConversationProtocol.MLS,
457
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
309
458
  epoch: updatedEpoch,
310
459
  group_id: mockGroupId,
311
460
  });
312
461
  jest.spyOn(mlsService, 'isConversationEstablished').mockResolvedValueOnce(false);
313
- jest.spyOn(mlsService, 'joinByExternalCommit').mockResolvedValueOnce({ events: [], time: '' });
462
+ jest.spyOn(mlsService, 'joinByExternalCommit');
314
463
  const establishedConversation = await conversationService.establishMLS1to1Conversation(mockGroupId, selfUser, otherUserId);
315
464
  expect(mlsService.registerConversation).not.toHaveBeenCalled();
316
465
  expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
@@ -326,14 +475,14 @@ describe('ConversationService', () => {
326
475
  const updatedEpoch = 1;
327
476
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
328
477
  qualified_id: mockConversationId,
329
- protocol: conversation_1.ConversationProtocol.MLS,
478
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
330
479
  epoch: remoteEpoch,
331
480
  group_id: mockGroupId,
332
481
  });
333
482
  // The 2nd request we make after successfully registering a group
334
483
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
335
484
  qualified_id: mockConversationId,
336
- protocol: conversation_1.ConversationProtocol.MLS,
485
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
337
486
  epoch: updatedEpoch,
338
487
  group_id: mockGroupId,
339
488
  });
@@ -355,21 +504,21 @@ describe('ConversationService', () => {
355
504
  const updatedEpoch = 1;
356
505
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
357
506
  qualified_id: mockConversationId,
358
- protocol: conversation_1.ConversationProtocol.MLS,
507
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
359
508
  epoch: remoteEpoch,
360
509
  group_id: mockGroupId,
361
510
  });
362
511
  // The 2nd request we make when retrying to register the conversation
363
512
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
364
513
  qualified_id: mockConversationId,
365
- protocol: conversation_1.ConversationProtocol.MLS,
514
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
366
515
  epoch: remoteEpoch,
367
516
  group_id: mockGroupId,
368
517
  });
369
518
  // The 3rd request we make after successfully registering a group
370
519
  jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
371
520
  qualified_id: mockConversationId,
372
- protocol: conversation_1.ConversationProtocol.MLS,
521
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
373
522
  epoch: updatedEpoch,
374
523
  group_id: mockGroupId,
375
524
  });
@@ -398,7 +547,7 @@ describe('ConversationService', () => {
398
547
  jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
399
548
  jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
400
549
  qualified_id: conversationId,
401
- protocol: conversation_1.ConversationProtocol.MLS,
550
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
402
551
  epoch: remoteEpoch,
403
552
  group_id: mockGroupId,
404
553
  });
@@ -432,19 +581,46 @@ describe('ConversationService', () => {
432
581
  expect(subconversationService.joinConferenceSubconversation).toHaveBeenCalledWith(conversationId, 'groupId');
433
582
  });
434
583
  it('joins a MLS conversation if it was sent an orphan welcome message', async () => {
435
- const [conversationService, { apiClient, mlsService }] = await buildConversationService();
584
+ const [conversationService, { mlsService }] = await buildConversationService();
436
585
  const conversationId = { id: 'conversationId', domain: 'staging.zinfra.io' };
437
586
  const mockMLSWelcomeMessageEvent = createMLSWelcomeMessageEventMock(conversationId);
438
587
  const orphanWelcomeMessageError = new Error();
439
- orphanWelcomeMessageError.name = CoreCryptoMLSError_1.CORE_CRYPTO_ERROR_NAMES.MlsErrorOrphanWelcomeMessage;
588
+ // Simulate core-crypto orphan welcome classification for mapper
589
+ orphanWelcomeMessageError.name = core_crypto_1.MlsErrorType.OrphanWelcome;
590
+ orphanWelcomeMessageError.context = { type: core_crypto_1.MlsErrorType.OrphanWelcome };
591
+ orphanWelcomeMessageError.type = core_crypto_1.ErrorType.Mls;
440
592
  jest.spyOn(mlsService, 'handleMLSWelcomeMessageEvent').mockRejectedValueOnce(orphanWelcomeMessageError);
441
- jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
442
- qualified_id: conversationId,
443
- protocol: conversation_1.ConversationProtocol.MLS,
444
- });
593
+ // Ensure welcome processing is attempted (succeeds after recovery join)
594
+ jest.spyOn(mlsService, 'handleMLSWelcomeMessageEvent').mockResolvedValueOnce(undefined);
445
595
  await conversationService.handleEvent(mockMLSWelcomeMessageEvent);
446
596
  await new Promise(resolve => setImmediate(resolve));
447
- expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(conversationId);
597
+ // Orchestrator triggers a low-level join (performJoinByExternalCommitAPI -> mlsService.joinByExternalCommit)
598
+ expect(mlsService.joinByExternalCommit).toHaveBeenCalled();
599
+ });
600
+ it('wipes local MLS state and retries when welcome fails with ConversationAlreadyExists', async () => {
601
+ const [conversationService, { mlsService }] = await buildConversationService();
602
+ const conversationId = { id: 'conversationId', domain: 'staging.zinfra.io' };
603
+ const mockMLSWelcomeMessageEvent = createMLSWelcomeMessageEventMock(conversationId);
604
+ // Create a base64 group id and its byte-array form to simulate core-crypto error context
605
+ const expectedGroupId = 'AXNhbXBsZQ=='; // base64 for '\u0001sample'
606
+ const conversationIdArray = Array.from(Buffer.from(expectedGroupId, 'base64'));
607
+ const existsError = {};
608
+ // Simulate core-crypto "conversation already exists" classification for the mapper
609
+ existsError.type = core_crypto_1.ErrorType.Mls;
610
+ existsError.context = {
611
+ type: core_crypto_1.MlsErrorType.ConversationAlreadyExists,
612
+ context: { conversationId: conversationIdArray },
613
+ };
614
+ const welcomeSpy = jest
615
+ .spyOn(mlsService, 'handleMLSWelcomeMessageEvent')
616
+ .mockRejectedValueOnce(existsError)
617
+ .mockResolvedValueOnce(undefined);
618
+ await conversationService.handleEvent(mockMLSWelcomeMessageEvent);
619
+ await new Promise(resolve => setImmediate(resolve));
620
+ // Expect a single wipe with the extracted group id, and a single retry of welcome handling
621
+ expect(mlsService.wipeConversation).toHaveBeenCalledTimes(1);
622
+ expect(mlsService.wipeConversation).toHaveBeenCalledWith(expectedGroupId);
623
+ expect(welcomeSpy).toHaveBeenCalledTimes(2);
448
624
  });
449
625
  });
450
626
  describe('getConversations', () => {
@@ -511,21 +687,22 @@ describe('ConversationService', () => {
511
687
  .map(() => ({ id: PayloadHelper.getUUID(), domain: 'local.wire.com' }));
512
688
  const selfUserToAdd = { id: 'self-user-id', domain: 'local.wire.com', skipOwnClientId: apiClient.clientId };
513
689
  const qualifiedUsers = [...otherUsersToAdd, selfUserToAdd];
514
- jest.spyOn(mlsService, 'getKeyPackagesPayload').mockResolvedValueOnce({ keyPackages: [], failures: [] });
690
+ jest
691
+ .spyOn(mlsService, 'getKeyPackagesPayload')
692
+ .mockResolvedValueOnce({ keyPackages: [new Uint8Array(0)], failures: [] });
515
693
  jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
516
694
  qualified_id: mockConversationId,
517
- protocol: conversation_1.ConversationProtocol.MLS,
695
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
518
696
  epoch: 1,
519
697
  group_id: mockGroupId,
520
698
  });
521
- const mlsMessage = { events: [], time: '', failures: [] };
522
- jest.spyOn(mlsService, 'addUsersToExistingConversation').mockResolvedValueOnce(mlsMessage);
699
+ jest.spyOn(mlsService, 'addUsersToExistingConversation');
523
700
  await conversationService.addUsersToMLSConversation({
524
701
  qualifiedUsers,
525
702
  groupId: mockGroupId,
526
703
  conversationId: mockConversationId,
527
704
  });
528
- expect(mlsService.getKeyPackagesPayload).toHaveBeenCalledWith(qualifiedUsers);
705
+ expect(mlsService.getKeyPackagesPayload).toHaveBeenCalledWith(qualifiedUsers, undefined);
529
706
  expect(mlsService.resetKeyMaterialRenewal).toHaveBeenCalledWith(mockGroupId);
530
707
  });
531
708
  it('should return failure reasons for users it was not possible to claim keys for', async () => {
@@ -541,20 +718,11 @@ describe('ConversationService', () => {
541
718
  reason: __1.AddUsersFailureReasons.OFFLINE_FOR_TOO_LONG,
542
719
  users: [otherUsersToAdd[0]],
543
720
  };
544
- const addUsersFailure = {
545
- reason: __1.AddUsersFailureReasons.UNREACHABLE_BACKENDS,
546
- users: [otherUsersToAdd[1]],
547
- backends: [otherUsersToAdd[1].domain],
548
- };
549
- const mlsFailure = {
550
- reason: __1.AddUsersFailureReasons.NOT_MLS_CAPABLE,
551
- users: [otherUsersToAdd[2]],
552
- };
553
721
  jest.spyOn(apiClient.api.user, 'getUserSupportedProtocols').mockImplementation(id => {
554
722
  if (id === otherUsersToAdd[2]) {
555
- return Promise.resolve([conversation_1.ConversationProtocol.PROTEUS]);
723
+ return Promise.resolve([team_1.CONVERSATION_PROTOCOL.PROTEUS]);
556
724
  }
557
- return Promise.resolve([conversation_1.ConversationProtocol.MLS, conversation_1.ConversationProtocol.PROTEUS]);
725
+ return Promise.resolve([team_1.CONVERSATION_PROTOCOL.MLS, team_1.CONVERSATION_PROTOCOL.PROTEUS]);
558
726
  });
559
727
  jest.spyOn(mlsService, 'getKeyPackagesPayload').mockResolvedValueOnce({
560
728
  keyPackages: [new Uint8Array(0)],
@@ -562,18 +730,63 @@ describe('ConversationService', () => {
562
730
  });
563
731
  jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
564
732
  qualified_id: mockConversationId,
565
- protocol: conversation_1.ConversationProtocol.MLS,
733
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
566
734
  epoch: 1,
567
735
  group_id: mockGroupId,
568
736
  });
569
- const mlsMessage = { events: [], time: '', failures: [addUsersFailure] };
570
- jest.spyOn(mlsService, 'addUsersToExistingConversation').mockResolvedValueOnce(mlsMessage);
737
+ jest.spyOn(mlsService, 'addUsersToExistingConversation');
571
738
  const { failedToAdd } = await conversationService.addUsersToMLSConversation({
572
739
  qualifiedUsers,
573
740
  groupId: mockGroupId,
574
741
  conversationId: mockConversationId,
575
742
  });
576
- expect(failedToAdd).toEqual([keysClaimingFailure, addUsersFailure, mlsFailure]);
743
+ expect(failedToAdd).toEqual([keysClaimingFailure]);
744
+ });
745
+ it('recovers and retries when stale-message occurs during add users commit upload', async () => {
746
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
747
+ const mockGroupId = 'groupId-stale-add';
748
+ const mockConversationId = { id: PayloadHelper.getUUID(), domain: 'local.wire.com' };
749
+ const otherUsersToAdd = Array(2)
750
+ .fill(0)
751
+ .map(() => ({ id: PayloadHelper.getUUID(), domain: 'local.wire.com' }));
752
+ const qualifiedUsers = [...otherUsersToAdd];
753
+ const staleMessageError = {
754
+ type: core_crypto_1.ErrorType.Mls,
755
+ context: {
756
+ type: core_crypto_1.MlsErrorType.MessageRejected,
757
+ context: {
758
+ reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
759
+ },
760
+ },
761
+ };
762
+ const getKPSpy = jest.spyOn(mlsService, 'getKeyPackagesPayload');
763
+ getKPSpy.mockResolvedValue({
764
+ keyPackages: [new Uint8Array(0)],
765
+ failures: [],
766
+ });
767
+ // Simulate commit upload failing once with stale, then succeeding
768
+ jest
769
+ .spyOn(mlsService, 'addUsersToExistingConversation')
770
+ .mockRejectedValueOnce(staleMessageError)
771
+ .mockResolvedValueOnce(undefined);
772
+ const remoteEpoch = 5;
773
+ const localEpoch = 4;
774
+ jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
775
+ jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
776
+ const getConvSpy = jest.spyOn(apiClient.api.conversation, 'getConversation');
777
+ getConvSpy.mockResolvedValue({
778
+ qualified_id: mockConversationId,
779
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
780
+ epoch: remoteEpoch,
781
+ group_id: mockGroupId,
782
+ });
783
+ await conversationService.addUsersToMLSConversation({
784
+ qualifiedUsers,
785
+ groupId: mockGroupId,
786
+ conversationId: mockConversationId,
787
+ });
788
+ expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
789
+ expect(mlsService.addUsersToExistingConversation).toHaveBeenCalledTimes(2);
577
790
  });
578
791
  });
579
792
  describe('tryEstablishingMLSGroup', () => {
@@ -620,6 +833,186 @@ describe('ConversationService', () => {
620
833
  expect(conversationService.addUsersToMLSConversation).not.toHaveBeenCalled();
621
834
  });
622
835
  });
836
+ describe('reactToKeyMaterialUpdateFailure', () => {
837
+ function getKeyMaterialFailureHandler(mlsService) {
838
+ const onMock = mlsService.on;
839
+ const call = onMock.mock.calls.find(([event]) => event === MLSService_1.MLSServiceEvents.KEY_MATERIAL_UPDATE_FAILURE);
840
+ expect(call).toBeTruthy();
841
+ return call[1];
842
+ }
843
+ it('resets a broken MLS conversation', async () => {
844
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
845
+ const groupId = 'group-1';
846
+ const qualified_id = { id: 'conv-1', domain: 'staging.zinfra.io' };
847
+ jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValueOnce({
848
+ found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 1 }],
849
+ });
850
+ const resetSpy = jest
851
+ .spyOn(conversationService, 'handleBrokenMLSConversation')
852
+ .mockResolvedValue(undefined);
853
+ const addUsersSpy = jest.spyOn(conversationService, 'addUsersToMLSConversation');
854
+ const handler = getKeyMaterialFailureHandler(mlsService);
855
+ const brokenErr = {
856
+ type: core_crypto_1.ErrorType.Mls,
857
+ context: {
858
+ type: core_crypto_1.MlsErrorType.MessageRejected,
859
+ context: {
860
+ reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.BROKEN_MLS_CONVERSATION }),
861
+ },
862
+ },
863
+ };
864
+ await handler({ error: brokenErr, groupId });
865
+ expect(resetSpy).toHaveBeenCalledWith(qualified_id);
866
+ expect(addUsersSpy).not.toHaveBeenCalled();
867
+ });
868
+ it('adds missing users when group is out of sync', async () => {
869
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
870
+ const groupId = 'group-2';
871
+ const qualified_id = { id: 'conv-2', domain: 'staging.zinfra.io' };
872
+ const missingUsers = [
873
+ { id: 'u1', domain: 'staging.zinfra.io' },
874
+ { id: 'u2', domain: 'staging.zinfra.io' },
875
+ ];
876
+ jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValueOnce({
877
+ found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 1 }],
878
+ });
879
+ const addUsersSpy = jest
880
+ .spyOn(conversationService, 'performAddUsersToMLSConversationAPI')
881
+ .mockResolvedValue({ conversation: {} });
882
+ const resetSpy = jest
883
+ .spyOn(conversationService, 'handleBrokenMLSConversation')
884
+ .mockResolvedValue(undefined);
885
+ const handler = getKeyMaterialFailureHandler(mlsService);
886
+ const outOfSyncErr = {
887
+ type: core_crypto_1.ErrorType.Mls,
888
+ context: {
889
+ type: core_crypto_1.MlsErrorType.MessageRejected,
890
+ context: {
891
+ reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({
892
+ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_GROUP_OUT_OF_SYNC,
893
+ missing_users: missingUsers,
894
+ }),
895
+ },
896
+ },
897
+ };
898
+ await handler({ error: outOfSyncErr, groupId });
899
+ expect(addUsersSpy).toHaveBeenCalledWith({
900
+ groupId,
901
+ conversationId: qualified_id,
902
+ qualifiedUsers: missingUsers,
903
+ });
904
+ expect(resetSpy).not.toHaveBeenCalled();
905
+ });
906
+ it('deduplicates concurrent recoveries for the same group id', async () => {
907
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
908
+ const groupId = 'group-dup';
909
+ const qualified_id = { id: 'conv-dup', domain: 'staging.zinfra.io' };
910
+ jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValue({
911
+ found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 1 }],
912
+ });
913
+ // Make the recovery hang until we resolve it, to simulate overlapping calls
914
+ let resolveDeferred;
915
+ const deferred = new Promise(res => (resolveDeferred = res));
916
+ const resetSpy = jest
917
+ .spyOn(conversationService, 'handleBrokenMLSConversation')
918
+ .mockReturnValue(deferred);
919
+ const handler = getKeyMaterialFailureHandler(mlsService);
920
+ const brokenErr = {
921
+ type: core_crypto_1.ErrorType.Mls,
922
+ context: {
923
+ type: core_crypto_1.MlsErrorType.MessageRejected,
924
+ context: {
925
+ reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.BROKEN_MLS_CONVERSATION }),
926
+ },
927
+ },
928
+ };
929
+ const p1 = handler({ error: brokenErr, groupId });
930
+ const p2 = handler({ error: brokenErr, groupId });
931
+ // Complete the first recovery
932
+ if (resolveDeferred) {
933
+ resolveDeferred();
934
+ }
935
+ await Promise.allSettled([p1, p2]);
936
+ expect(resetSpy).toHaveBeenCalledTimes(1);
937
+ });
938
+ it('handles stale-message by rejoining via external commit and emits recovery event', async () => {
939
+ const [conversationService, { apiClient, mlsService }] = await buildConversationService();
940
+ const groupId = 'group-stale';
941
+ const qualified_id = { id: 'conv-stale', domain: 'staging.zinfra.io' };
942
+ jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValueOnce({
943
+ found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 2 }],
944
+ });
945
+ const handler = getKeyMaterialFailureHandler(mlsService);
946
+ const staleMessageError = {
947
+ type: core_crypto_1.ErrorType.Mls,
948
+ context: {
949
+ type: core_crypto_1.MlsErrorType.MessageRejected,
950
+ context: {
951
+ reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
952
+ },
953
+ },
954
+ };
955
+ const remoteEpoch = 3;
956
+ const localEpoch = 2;
957
+ jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
958
+ jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
959
+ jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
960
+ qualified_id,
961
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
962
+ epoch: remoteEpoch,
963
+ group_id: groupId,
964
+ });
965
+ await handler({ error: staleMessageError, groupId });
966
+ // Expect a rejoin on stale and a recovery event
967
+ expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(qualified_id);
968
+ expect(conversationService.emit).toHaveBeenCalledWith('MLSConversationRecovered', { conversationId: qualified_id });
969
+ });
970
+ });
971
+ describe('groupIdConversationMap cache', () => {
972
+ function makeConversation(group_id, id) {
973
+ return {
974
+ group_id,
975
+ qualified_id: { id, domain: 'staging.zinfra.io' },
976
+ protocol: team_1.CONVERSATION_PROTOCOL.MLS,
977
+ epoch: 1,
978
+ };
979
+ }
980
+ it('refreshGroupIdConversationMap builds the cache from backend list', async () => {
981
+ const [conversationService, { apiClient }] = await buildConversationService();
982
+ const conv1 = makeConversation('g-1', 'c-1');
983
+ const conv2 = makeConversation('g-2', 'c-2');
984
+ const getListSpy = jest
985
+ .spyOn(apiClient.api.conversation, 'getConversationList')
986
+ .mockResolvedValueOnce({ found: [conv1, conv2] });
987
+ await conversationService.refreshGroupIdConversationMap();
988
+ expect(getListSpy).toHaveBeenCalledTimes(1);
989
+ // Validate through the private cache map
990
+ const cache = conversationService.groupIdConversationMap;
991
+ expect(cache.get('g-1')?.qualified_id.id).toBe('c-1');
992
+ expect(cache.get('g-2')?.qualified_id.id).toBe('c-2');
993
+ });
994
+ it('getConversationByGroupId uses cache when available without backend call', async () => {
995
+ const [conversationService, { apiClient }] = await buildConversationService();
996
+ const conv = makeConversation('g-hit', 'c-hit');
997
+ // Pre-populate cache
998
+ conversationService.groupIdConversationMap.set('g-hit', conv);
999
+ const getListSpy = jest
1000
+ .spyOn(apiClient.api.conversation, 'getConversationList')
1001
+ .mockImplementation(() => Promise.reject(new Error('should not be called')));
1002
+ const result = await conversationService.getConversationByGroupId('g-hit');
1003
+ expect(result?.qualified_id.id).toBe('c-hit');
1004
+ expect(getListSpy).not.toHaveBeenCalled();
1005
+ });
1006
+ it('getConversationByGroupId refreshes on cache miss and returns undefined if not found', async () => {
1007
+ const [conversationService, { apiClient }] = await buildConversationService();
1008
+ const getListSpy = jest
1009
+ .spyOn(apiClient.api.conversation, 'getConversationList')
1010
+ .mockResolvedValueOnce({ found: [makeConversation('g-else', 'c-else')] });
1011
+ const result = await conversationService.getConversationByGroupId('g-missing');
1012
+ expect(result).toBeUndefined();
1013
+ expect(getListSpy).toHaveBeenCalledTimes(1);
1014
+ });
1015
+ });
623
1016
  });
624
1017
  function generateImage() {
625
1018
  const image = {
@@ -636,6 +1029,7 @@ function generateImage() {
636
1029
  keyBytes: Buffer.from([]),
637
1030
  sha256: Buffer.from([]),
638
1031
  token: '',
1032
+ domain: 'example.com',
639
1033
  },
640
1034
  };
641
1035
  }