@wireapp/core 46.45.3 → 46.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Account.d.ts +2 -0
- package/lib/Account.d.ts.map +1 -1
- package/lib/Account.js +2 -0
- package/lib/Account.test.js +13 -0
- package/lib/conversation/ConversationService/ConversationService.d.ts +60 -13
- package/lib/conversation/ConversationService/ConversationService.d.ts.map +1 -1
- package/lib/conversation/ConversationService/ConversationService.js +199 -319
- package/lib/conversation/ConversationService/ConversationService.test.js +34 -8
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.d.ts +5 -0
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/MLSService/MLSService.js +1 -1
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.d.ts +78 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.js +173 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.d.ts +2 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.js +117 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.d.ts +167 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.js +316 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.d.ts +2 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.js +248 -0
- package/lib/messagingProtocols/mls/recovery/index.d.ts +5 -0
- package/lib/messagingProtocols/mls/recovery/index.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/index.js +28 -0
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.d.ts +2 -1
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.js +3 -0
- package/package.json +2 -2
|
@@ -29,8 +29,7 @@ const core_crypto_1 = require("@wireapp/core-crypto");
|
|
|
29
29
|
const protocol_messaging_1 = require("@wireapp/protocol-messaging");
|
|
30
30
|
const conversation_2 = require("../../conversation/");
|
|
31
31
|
const mls_1 = require("../../messagingProtocols/mls");
|
|
32
|
-
const
|
|
33
|
-
const CoreCryptoMLSError_1 = require("../../messagingProtocols/mls/MLSService/CoreCryptoMLSError");
|
|
32
|
+
const recovery_1 = require("../../messagingProtocols/mls/recovery");
|
|
34
33
|
const proteus_1 = require("../../messagingProtocols/proteus");
|
|
35
34
|
const util_1 = require("../../util");
|
|
36
35
|
const fullyQualifiedClientIdUtils_1 = require("../../util/fullyQualifiedClientIdUtils");
|
|
@@ -46,8 +45,8 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
46
45
|
messageTimer;
|
|
47
46
|
logger = commons_1.LogFactory.getLogger('@wireapp/core/ConversationService');
|
|
48
47
|
// Track groups currently undergoing recovery due to key material update failure to prevent duplicate work
|
|
49
|
-
recoveringKeyMaterialGroups = new Set();
|
|
50
48
|
groupIdConversationMap = new Map();
|
|
49
|
+
MLSRecoveryOrchestrator;
|
|
51
50
|
constructor(apiClient, proteusService, coreDatabase, groupIdFromConversationId, subconversationService, isMLSConversationRecoveryEnabled, _mlsService) {
|
|
52
51
|
super();
|
|
53
52
|
this.apiClient = apiClient;
|
|
@@ -58,15 +57,27 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
58
57
|
this.isMLSConversationRecoveryEnabled = isMLSConversationRecoveryEnabled;
|
|
59
58
|
this._mlsService = _mlsService;
|
|
60
59
|
this.messageTimer = new conversation_2.MessageTimer();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
this.mlsService.on(mls_1.MLSServiceEvents.KEY_MATERIAL_UPDATE_FAILURE, ({ error, groupId }) => {
|
|
66
|
-
this.logger.warn(`Key material update failure for group ${groupId}`, { error });
|
|
67
|
-
return this.reactToKeyMaterialUpdateFailure({ error, groupId });
|
|
68
|
-
});
|
|
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');
|
|
69
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: (groupId) => this.wipeMLSConversation(groupId),
|
|
80
|
+
});
|
|
70
81
|
}
|
|
71
82
|
get mlsService() {
|
|
72
83
|
if (!this._mlsService) {
|
|
@@ -215,7 +226,17 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
215
226
|
}
|
|
216
227
|
return this.establishMLSGroupConversation(groupId, qualifiedUsers, selfUserId, selfClientId, qualifiedId);
|
|
217
228
|
}
|
|
218
|
-
|
|
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) {
|
|
219
240
|
if (!(await this.isMLSConversationRecoveryEnabled())) {
|
|
220
241
|
throw new Error('MLS conversation recovery is disabled');
|
|
221
242
|
}
|
|
@@ -225,10 +246,6 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
225
246
|
this.logger.error(errorMessage, { conversationId });
|
|
226
247
|
throw new Error(errorMessage);
|
|
227
248
|
}
|
|
228
|
-
if (afterReset) {
|
|
229
|
-
return afterReset(newGroupId);
|
|
230
|
-
}
|
|
231
|
-
return undefined;
|
|
232
249
|
}
|
|
233
250
|
/**
|
|
234
251
|
* Will create a conversation on backend and register it to CoreCrypto once created
|
|
@@ -248,222 +265,113 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
248
265
|
failedToAdd: failures,
|
|
249
266
|
};
|
|
250
267
|
}
|
|
251
|
-
|
|
252
|
-
|
|
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;
|
|
253
286
|
const groupIdBytes = bazinga64_1.Decoder.fromBase64(groupId).asBytes;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
this.logger.warn('Failed to send MLS message', { error, groupId });
|
|
275
|
-
if (!shouldRetry) {
|
|
276
|
-
this.logger.error("Tried to send MLS message but it's still failing after recovery", {
|
|
277
|
-
error,
|
|
278
|
-
groupId,
|
|
279
|
-
});
|
|
280
|
-
throw error;
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Only thrown by core-crypto when we call commitPendingProposals
|
|
284
|
-
*/
|
|
285
|
-
if ((0, CoreCryptoMLSError_1.isBrokenMLSConversationError)(error)) {
|
|
286
|
-
this.logger.info('Failed to send MLS message because broken MLS conversation, triggering a reset', {
|
|
287
|
-
error,
|
|
288
|
-
groupId,
|
|
289
|
-
});
|
|
290
|
-
await this.handleBrokenMLSConversation(conversationId);
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* We may have the same error from core-crypto or from the backend error mapper
|
|
294
|
-
* core-crypto throws its own error class when we call commitPendingProposals
|
|
295
|
-
* backend error mapper throws its own error class when we call postMlsMessage
|
|
296
|
-
*/
|
|
297
|
-
if ((0, CoreCryptoMLSError_1.isMLSStaleMessageError)(error) || error instanceof conversation_1.MLSStaleMessageError) {
|
|
298
|
-
this.logger.info('Failed to send MLS message because of stale message, recovering by joining with external commit', {
|
|
299
|
-
error,
|
|
300
|
-
groupId,
|
|
301
|
-
});
|
|
302
|
-
await this.recoverMLSGroupFromEpochMismatch(conversationId);
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* We may have the same error from core-crypto or from the backend error mapper
|
|
306
|
-
* core-crypto throws its own error class when we call commitPendingProposals
|
|
307
|
-
* backend error mapper throws its own error class when we call postMlsMessage
|
|
308
|
-
*/
|
|
309
|
-
if ((0, CoreCryptoMLSError_1.isMLSGroupOutOfSyncError)(error) || error instanceof conversation_1.MLSGroupOutOfSyncError) {
|
|
310
|
-
this.logger.info('Failed to send MLS message because of group out of sync, recovering by adding missing users', {
|
|
311
|
-
error,
|
|
312
|
-
groupId,
|
|
313
|
-
});
|
|
314
|
-
/**
|
|
315
|
-
* We may get the missing users either from core-crypto error or from the backend error mapper
|
|
316
|
-
*/
|
|
317
|
-
let missingUsers = [];
|
|
318
|
-
if ((0, CoreCryptoMLSError_1.isMLSGroupOutOfSyncError)(error)) {
|
|
319
|
-
missingUsers = (0, CoreCryptoMLSError_1.getMLSGroupOutOfSyncErrorMissingUsers)(error);
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
missingUsers = error.missing_users;
|
|
323
|
-
}
|
|
324
|
-
await this.addUsersToMLSConversation({
|
|
325
|
-
groupId,
|
|
326
|
-
conversationId,
|
|
327
|
-
qualifiedUsers: missingUsers,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
return this.sendMLSMessage(params, false);
|
|
331
|
-
}
|
|
287
|
+
// immediately execute pending commits before sending the message
|
|
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();
|
|
292
|
+
const failedToSend = response?.failed || (response?.failed_to_send ?? []).length > 0
|
|
293
|
+
? {
|
|
294
|
+
queued: response?.failed_to_send,
|
|
295
|
+
failed: response?.failed,
|
|
296
|
+
}
|
|
297
|
+
: undefined;
|
|
298
|
+
return {
|
|
299
|
+
id: payload.messageId,
|
|
300
|
+
sentAt,
|
|
301
|
+
failedToSend,
|
|
302
|
+
state: sentAt ? conversation_2.MessageSendingState.OUTGOING_SENT : conversation_2.MessageSendingState.CANCELED,
|
|
303
|
+
};
|
|
332
304
|
}
|
|
333
305
|
/**
|
|
334
|
-
*
|
|
306
|
+
* Add users to an existing MLS group with recovery.
|
|
335
307
|
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
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.
|
|
311
|
+
*
|
|
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
|
|
339
315
|
*/
|
|
340
|
-
async addUsersToMLSConversation({ qualifiedUsers, groupId, conversationId,
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const { keyPackages, failures: keysClaimingFailures } = await this.mlsService.getKeyPackagesPayload(qualifiedUsers, exisitingClientIdsInGroup);
|
|
346
|
-
// We had cases where did not get any key packages, but still used core-crypto to call the backend (which results in failure).
|
|
347
|
-
if (keyPackages && keyPackages.length > 0) {
|
|
348
|
-
await this.mlsService.addUsersToExistingConversation(groupId, keyPackages);
|
|
349
|
-
//We store the info when user was added (and key material was created), so we will know when to renew it
|
|
350
|
-
await this.mlsService.resetKeyMaterialRenewal(groupId);
|
|
351
|
-
}
|
|
352
|
-
return {
|
|
353
|
-
conversation,
|
|
354
|
-
failedToAdd: keysClaimingFailures,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
this.logger.warn('Failed to add users to MLS conversation', { error, groupId, conversationId });
|
|
359
|
-
if (!shouldRetry) {
|
|
360
|
-
this.logger.warn("Tried to add users to MLS conversation but it's still broken after reset", error);
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
|
-
if ((0, CoreCryptoMLSError_1.isBrokenMLSConversationError)(error)) {
|
|
364
|
-
this.logger.warn("Tried to add users to MLS conversation but it's broken, resetting the conversation", error);
|
|
365
|
-
return this.handleBrokenMLSConversation(conversationId, newGroupId => this.addUsersToMLSConversation({ qualifiedUsers, groupId: newGroupId, conversationId, shouldRetry: false }));
|
|
366
|
-
}
|
|
367
|
-
if ((0, CoreCryptoMLSError_1.isMLSStaleMessageError)(error)) {
|
|
368
|
-
this.logger.info('Failed to add users to MLS conversation because of stale message, recovering by joining with external commit', {
|
|
369
|
-
error,
|
|
370
|
-
groupId,
|
|
371
|
-
});
|
|
372
|
-
await this.recoverMLSGroupFromEpochMismatch(conversationId);
|
|
373
|
-
return this.addUsersToMLSConversation({
|
|
374
|
-
groupId,
|
|
375
|
-
conversationId,
|
|
376
|
-
qualifiedUsers,
|
|
377
|
-
shouldRetry: false,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
if ((0, CoreCryptoMLSError_1.isMLSGroupOutOfSyncError)(error)) {
|
|
381
|
-
this.logger.info('Failed to send MLS message because of group out of sync, recovering by adding missing users', {
|
|
382
|
-
error,
|
|
383
|
-
groupId,
|
|
384
|
-
});
|
|
385
|
-
const missingUsers = (0, CoreCryptoMLSError_1.getMLSGroupOutOfSyncErrorMissingUsers)(error);
|
|
386
|
-
return this.addUsersToMLSConversation({
|
|
387
|
-
groupId,
|
|
388
|
-
conversationId,
|
|
389
|
-
qualifiedUsers: missingUsers,
|
|
390
|
-
shouldRetry: false,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
throw error;
|
|
394
|
-
}
|
|
316
|
+
async addUsersToMLSConversation({ qualifiedUsers, groupId, conversationId, }) {
|
|
317
|
+
return this.MLSRecoveryOrchestrator.execute({
|
|
318
|
+
context: { operationName: recovery_1.OperationName.addUsers, qualifiedConversationId: conversationId, groupId },
|
|
319
|
+
callBack: () => this.performAddUsersToMLSConversationAPI({ qualifiedUsers, groupId, conversationId }),
|
|
320
|
+
});
|
|
395
321
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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);
|
|
326
|
+
const conversation = await this.getConversation(conversationId);
|
|
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
|
|
402
332
|
await this.mlsService.resetKeyMaterialRenewal(groupId);
|
|
403
|
-
return await this.getConversation(conversationId);
|
|
404
|
-
}
|
|
405
|
-
catch (error) {
|
|
406
|
-
if (!shouldRetry) {
|
|
407
|
-
this.logger.warn("Tried to remove users from MLS conversation but it's still broken", error);
|
|
408
|
-
throw error;
|
|
409
|
-
}
|
|
410
|
-
if ((0, CoreCryptoMLSError_1.isBrokenMLSConversationError)(error)) {
|
|
411
|
-
this.logger.info("Tried to remove users from MLS conversation but it's broken, resetting the conversation", error);
|
|
412
|
-
return this.handleBrokenMLSConversation(conversationId, newGroupId => this.removeUsersFromMLSConversation({
|
|
413
|
-
groupId: newGroupId,
|
|
414
|
-
conversationId,
|
|
415
|
-
qualifiedUserIds,
|
|
416
|
-
shouldRetry: false,
|
|
417
|
-
}));
|
|
418
|
-
}
|
|
419
|
-
if ((0, CoreCryptoMLSError_1.isMLSStaleMessageError)(error)) {
|
|
420
|
-
this.logger.info('Failed to remove users from MLS conversation because of stale message, recovering by joining with external commit', {
|
|
421
|
-
error,
|
|
422
|
-
groupId,
|
|
423
|
-
});
|
|
424
|
-
await this.recoverMLSGroupFromEpochMismatch(conversationId);
|
|
425
|
-
}
|
|
426
|
-
if ((0, CoreCryptoMLSError_1.isMLSGroupOutOfSyncError)(error)) {
|
|
427
|
-
this.logger.info('Failed to send MLS message because of group out of sync, recovering by adding missing users', {
|
|
428
|
-
error,
|
|
429
|
-
groupId,
|
|
430
|
-
});
|
|
431
|
-
const missingUsers = (0, CoreCryptoMLSError_1.getMLSGroupOutOfSyncErrorMissingUsers)(error);
|
|
432
|
-
await this.addUsersToMLSConversation({
|
|
433
|
-
groupId,
|
|
434
|
-
conversationId,
|
|
435
|
-
qualifiedUsers: missingUsers,
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
return this.removeUsersFromMLSConversation({
|
|
439
|
-
groupId,
|
|
440
|
-
conversationId,
|
|
441
|
-
qualifiedUserIds,
|
|
442
|
-
shouldRetry: false,
|
|
443
|
-
});
|
|
444
333
|
}
|
|
334
|
+
return {
|
|
335
|
+
conversation,
|
|
336
|
+
failedToAdd: keysClaimingFailures,
|
|
337
|
+
};
|
|
445
338
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
this.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
*/
|
|
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, }) {
|
|
353
|
+
const clientsToRemove = await this.apiClient.api.user.postListClients({ qualified_users: qualifiedUserIds });
|
|
354
|
+
const fullyQualifiedClientIds = (0, fullyQualifiedClientIdUtils_1.mapQualifiedUserClientIdsToFullyQualifiedClientIds)(clientsToRemove.qualified_user_map);
|
|
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
|
|
357
|
+
await this.mlsService.resetKeyMaterialRenewal(groupId);
|
|
358
|
+
return await this.getConversation(conversationId);
|
|
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
|
+
*/
|
|
366
|
+
async joinByExternalCommit(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));
|
|
467
375
|
}
|
|
468
376
|
async refreshGroupIdConversationMap() {
|
|
469
377
|
const conversations = await this.apiClient.api.conversation.getConversationList();
|
|
@@ -480,49 +388,35 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
480
388
|
}
|
|
481
389
|
return this.groupIdConversationMap.get(groupId);
|
|
482
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
|
+
*/
|
|
483
397
|
reactToKeyMaterialUpdateFailure = async ({ error, groupId }) => {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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 });
|
|
487
402
|
return;
|
|
488
403
|
}
|
|
489
|
-
this.recoveringKeyMaterialGroups.add(groupId);
|
|
490
404
|
try {
|
|
491
|
-
this.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
throw error;
|
|
496
|
-
}
|
|
497
|
-
if ((0, CoreCryptoMLSError_1.isBrokenMLSConversationError)(error)) {
|
|
498
|
-
this.logger.info('Tried to update key material for a broken MLS conversation, initiating reset', error);
|
|
499
|
-
return this.handleBrokenMLSConversation(conversation.qualified_id);
|
|
500
|
-
}
|
|
501
|
-
if ((0, CoreCryptoMLSError_1.isMLSStaleMessageError)(error)) {
|
|
502
|
-
this.logger.info('Tried to update key material for a stale MLS conversation, recovering by joining with external commit', {
|
|
503
|
-
error,
|
|
504
|
-
groupId,
|
|
505
|
-
});
|
|
506
|
-
await this.recoverMLSGroupFromEpochMismatch(conversation.qualified_id);
|
|
507
|
-
}
|
|
508
|
-
if ((0, CoreCryptoMLSError_1.isMLSGroupOutOfSyncError)(error)) {
|
|
509
|
-
this.logger.info('Tried to update key material for an out of sync conversation, recovering by adding missing users', {
|
|
510
|
-
error,
|
|
511
|
-
groupId,
|
|
512
|
-
});
|
|
513
|
-
const missingUsers = (0, CoreCryptoMLSError_1.getMLSGroupOutOfSyncErrorMissingUsers)(error);
|
|
514
|
-
await this.addUsersToMLSConversation({
|
|
405
|
+
await this.MLSRecoveryOrchestrator.execute({
|
|
406
|
+
context: {
|
|
407
|
+
operationName: recovery_1.OperationName.keyMaterialUpdate,
|
|
408
|
+
qualifiedConversationId: conversation.qualified_id,
|
|
515
409
|
groupId,
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
+
});
|
|
523
417
|
}
|
|
524
|
-
|
|
525
|
-
this.
|
|
418
|
+
catch (e) {
|
|
419
|
+
this.logger.error('Failed to react to key material update failure', { error: e, groupId });
|
|
526
420
|
}
|
|
527
421
|
};
|
|
528
422
|
async resetMLSConversation(conversationId) {
|
|
@@ -761,21 +655,38 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
761
655
|
throw error;
|
|
762
656
|
}
|
|
763
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
|
+
*/
|
|
764
668
|
async handleMLSMessageAddEvent(event) {
|
|
765
669
|
try {
|
|
766
|
-
|
|
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
|
+
});
|
|
767
684
|
}
|
|
768
685
|
catch (error) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
throw new Error('Qualified conversation id is missing in the event');
|
|
774
|
-
}
|
|
775
|
-
void (0, conversationRejoinQueue_1.queueConversationRejoin)(conversationId.id, () => this.recoverMLSGroupFromEpochMismatch(conversationId, subconv));
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
778
|
-
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;
|
|
779
690
|
}
|
|
780
691
|
}
|
|
781
692
|
async recoverMLSGroupFromEpochMismatch(conversationId, subconversationId) {
|
|
@@ -794,56 +705,25 @@ class ConversationService extends commons_1.TypedEventEmitter {
|
|
|
794
705
|
}
|
|
795
706
|
return this.handleConversationEpochMismatch(mlsConversation, () => this.emit('MLSConversationRecovered', { conversationId: mlsConversation.qualified_id }));
|
|
796
707
|
}
|
|
797
|
-
async handleMLSWelcomeMessageEvent(event, retry = true) {
|
|
798
|
-
try {
|
|
799
|
-
this.logger.info('Handling MLS welcome message event', { event });
|
|
800
|
-
return await this.mlsService.handleMLSWelcomeMessageEvent(event, this.apiClient.validatedClientId);
|
|
801
|
-
}
|
|
802
|
-
catch (error) {
|
|
803
|
-
if (!retry) {
|
|
804
|
-
this.logger.error('Failed to handle MLS welcome message event and unable to recover', { event, error });
|
|
805
|
-
throw error;
|
|
806
|
-
}
|
|
807
|
-
this.logger.warn('Failed to handle MLS welcome message event', { event, error });
|
|
808
|
-
if ((0, core_crypto_1.isMlsOrphanWelcomeError)(error)) {
|
|
809
|
-
return this.handleMlsOrphanWelcomeEvent(event);
|
|
810
|
-
}
|
|
811
|
-
if ((0, core_crypto_1.isMlsConversationAlreadyExistsError)(error)) {
|
|
812
|
-
return this.handleMlsConversationAlreadyExistsEvent(event, error);
|
|
813
|
-
}
|
|
814
|
-
throw error;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
handleMlsOrphanWelcomeEvent(event) {
|
|
818
|
-
this.logger.warn('Received an orphan welcome message, trying to join the conversation via external commit');
|
|
819
|
-
const { qualified_conversation: conversationId } = event;
|
|
820
|
-
// Note that we don't care about a subconversation here, as the welcome message is always for the parent conversation.
|
|
821
|
-
// Subconversations are always joined via external commit.
|
|
822
|
-
if (!conversationId) {
|
|
823
|
-
throw new Error('Qualified conversation id is missing in the event');
|
|
824
|
-
}
|
|
825
|
-
this.logger.warn(`Received an orphan welcome message, joining the conversation (${conversationId.id}) via external commit...`);
|
|
826
|
-
void (0, conversationRejoinQueue_1.queueConversationRejoin)(conversationId.id, () => this.joinByExternalCommit(conversationId));
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
708
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
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.
|
|
832
716
|
*/
|
|
833
|
-
async
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
catch (error) {
|
|
844
|
-
this.logger.error('Failed to handle MLS conversation already exists event', { event, error });
|
|
845
|
-
throw error;
|
|
846
|
-
}
|
|
717
|
+
async handleMLSWelcomeMessageEvent(event) {
|
|
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;
|
|
847
727
|
}
|
|
848
728
|
async handleOtrMessageAddEvent(event) {
|
|
849
729
|
return this.proteusService.handleOtrMessageAddEvent(event);
|