• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LukaJCB / ts-mls / 17283659198

28 Aug 2025 02:05AM UTC coverage: 93.75% (+1.7%) from 92.046%
17283659198

push

github

web-flow
Migrate from jest to vitest and parallelize tests (#90)

1121 of 1250 branches covered (89.68%)

Branch coverage included in aggregate %.

388 of 393 new or added lines in 63 files covered. (98.73%)

57 existing lines in 13 files now uncovered.

6364 of 6734 relevant lines covered (94.51%)

51774.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.29
/src/createCommit.ts
1
import { addHistoricalReceiverData, makePskIndex, throwIfDefined, validateRatchetTree } from "./clientState.js"
1✔
2
import { AuthenticatedContentCommit } from "./authenticatedContent.js"
3
import {
1✔
4
  ClientState,
5
  applyProposals,
6
  nextEpochContext,
7
  ApplyProposalsResult,
8
  exportSecret,
9
  checkCanSendHandshakeMessages,
10
  GroupActiveState,
11
} from "./clientState.js"
12
import { CiphersuiteImpl } from "./crypto/ciphersuite.js"
13
import { decryptWithLabel } from "./crypto/hpke.js"
1✔
14
import { deriveSecret } from "./crypto/kdf.js"
1✔
15
import {
1✔
16
  createContentCommitSignature,
17
  createConfirmationTag,
18
  FramedContentAuthDataCommit,
19
  FramedContentCommit,
20
} from "./framedContent.js"
21
import { GroupContext, encodeGroupContext } from "./groupContext.js"
1✔
22
import {
1✔
23
  GroupInfo,
24
  GroupInfoTBS,
25
  ratchetTreeFromExtension,
26
  signGroupInfo,
27
  verifyGroupInfoSignature,
28
} from "./groupInfo.js"
29
import { KeyPackage, makeKeyPackageRef, PrivateKeyPackage } from "./keyPackage.js"
1✔
30
import { initializeEpoch, EpochSecrets } from "./keySchedule.js"
1✔
31
import { MLSMessage } from "./message.js"
32
import { protect } from "./messageProtection.js"
1✔
33
import { protectPublicMessage } from "./messageProtectionPublic.js"
1✔
34
import { pathToPathSecrets } from "./pathSecrets.js"
1✔
35
import { mergePrivateKeyPaths, updateLeafKey, toPrivateKeyPath, PrivateKeyPath } from "./privateKeyPath.js"
1✔
36
import { Proposal, ProposalExternalInit } from "./proposal.js"
37
import { ProposalOrRef } from "./proposalOrRefType.js"
38
import { PskIndex } from "./pskIndex.js"
39
import {
1✔
40
  RatchetTree,
41
  addLeafNode,
42
  encodeRatchetTree,
43
  getCredentialFromLeafIndex,
44
  getSignaturePublicKeyFromLeafIndex,
45
  removeLeafNode,
46
} from "./ratchetTree.js"
47
import { createSecretTree, SecretTree } from "./secretTree.js"
1✔
48
import { treeHashRoot } from "./treeHash.js"
1✔
49
import { LeafIndex, leafWidth, NodeIndex, nodeToLeafIndex, toLeafIndex, toNodeIndex } from "./treemath.js"
1✔
50
import { createUpdatePath, PathSecret, firstCommonAncestor, UpdatePath, firstMatchAncestor } from "./updatePath.js"
1✔
51
import { base64ToBytes } from "./util/byteArray.js"
1✔
52
import { Welcome, encryptGroupInfo, EncryptedGroupSecrets, encryptGroupSecrets } from "./welcome.js"
1✔
53
import { CryptoVerificationError, InternalError, UsageError, ValidationError } from "./mlsError.js"
1✔
54
import { ClientConfig, defaultClientConfig } from "./clientConfig.js"
1✔
55
import { Extension, extensionsSupportedByCapabilities } from "./extension.js"
1✔
56

57
export interface MLSContext {
58
  state: ClientState
59
  cipherSuite: CiphersuiteImpl
60
  pskIndex?: PskIndex
61
}
62

63
export interface CreateCommitResult {
64
  newState: ClientState
65
  welcome: Welcome | undefined
66
  commit: MLSMessage
67
}
68

69
export interface CreateCommitOptions {
70
  wireAsPublicMessage?: boolean
71
  extraProposals?: Proposal[]
72
  ratchetTreeExtension?: boolean
73
  groupInfoExtensions?: Extension[]
74
  authenticatedData?: Uint8Array
75
}
76

77
export async function createCommit(context: MLSContext, options?: CreateCommitOptions): Promise<CreateCommitResult> {
1,976✔
78
  const { state, pskIndex = makePskIndex(state, {}), cipherSuite } = context
1,976✔
79
  const {
1,976✔
80
    wireAsPublicMessage = false,
1,976✔
81
    extraProposals = [],
1,976✔
82
    ratchetTreeExtension = false,
1,976✔
83
    authenticatedData = new Uint8Array(),
1,976✔
84
    groupInfoExtensions = [],
1,976✔
85
  } = options ?? {}
1,976✔
86

87
  checkCanSendHandshakeMessages(state)
1,976✔
88

89
  const wireformat = wireAsPublicMessage ? "mls_public_message" : "mls_private_message"
1,976✔
90

91
  const allProposals = bundleAllProposals(state, extraProposals)
1,976✔
92

93
  const res = await applyProposals(
1,976✔
94
    state,
1,976✔
95
    allProposals,
1,976✔
96
    toLeafIndex(state.privatePath.leafIndex),
1,976✔
97
    pskIndex,
1,976✔
98
    true,
1,976✔
99
    cipherSuite,
1,976✔
100
  )
1,976✔
101

102
  if (res.additionalResult.kind === "externalCommit") throw new UsageError("Cannot create externalCommit as a member")
1,976✔
103

104
  const suspendedPendingReinit = res.additionalResult.kind === "reinit" ? res.additionalResult.reinit : undefined
1,976✔
105

106
  const [tree, updatePath, pathSecrets, newPrivateKey] = res.needsUpdatePath
1,976✔
107
    ? await createUpdatePath(
532✔
108
        res.tree,
532✔
109
        toLeafIndex(state.privatePath.leafIndex),
532✔
110
        state.groupContext,
532✔
111
        state.signaturePrivateKey,
532✔
112
        cipherSuite,
532✔
113
      )
532✔
114
    : [res.tree, undefined, [] as PathSecret[], undefined]
1,083✔
115

116
  const updatedExtensions =
1,976✔
117
    res.additionalResult.kind === "memberCommit" && res.additionalResult.extensions.length > 0
1,976✔
UNCOV
118
      ? res.additionalResult.extensions
✔
119
      : state.groupContext.extensions
1,615✔
120

121
  const groupContextWithExtensions = { ...state.groupContext, extensions: updatedExtensions }
1,976✔
122

123
  const privateKeys = mergePrivateKeyPaths(
1,976✔
124
    newPrivateKey !== undefined
1,976✔
125
      ? updateLeafKey(state.privatePath, await cipherSuite.hpke.exportPrivateKey(newPrivateKey))
532✔
126
      : state.privatePath,
1,083✔
127
    await toPrivateKeyPath(pathToPathSecrets(pathSecrets), state.privatePath.leafIndex, cipherSuite),
1,976✔
128
  )
1,615✔
129

130
  const lastPathSecret = pathSecrets.at(-1)
1,615✔
131

132
  const commitSecret =
1,615✔
133
    lastPathSecret === undefined
1,615✔
134
      ? new Uint8Array(cipherSuite.kdf.size)
1,083✔
135
      : await deriveSecret(lastPathSecret.secret, "path", cipherSuite.kdf)
532✔
136

137
  const { signature, framedContent } = await createContentCommitSignature(
532✔
138
    state.groupContext,
532✔
139
    wireformat,
532✔
140
    { proposals: allProposals, path: updatePath },
532✔
141
    { senderType: "member", leafIndex: state.privatePath.leafIndex },
532✔
142
    authenticatedData,
532✔
143
    state.signaturePrivateKey,
532✔
144
    cipherSuite.signature,
532✔
145
  )
532✔
146

147
  const treeHash = await treeHashRoot(tree, cipherSuite.hash)
1,615✔
148

149
  const updatedGroupContext = await nextEpochContext(
1,615✔
150
    groupContextWithExtensions,
1,615✔
151
    wireformat,
1,615✔
152
    framedContent,
1,615✔
153
    signature,
1,615✔
154
    treeHash,
1,615✔
155
    state.confirmationTag,
1,615✔
156
    cipherSuite.hash,
1,615✔
157
  )
1,615✔
158

159
  const epochSecrets = await initializeEpoch(
1,615✔
160
    state.keySchedule.initSecret,
1,615✔
161
    commitSecret,
1,615✔
162
    updatedGroupContext,
1,615✔
163
    res.pskSecret,
1,615✔
164
    cipherSuite.kdf,
1,615✔
165
  )
1,615✔
166

167
  const confirmationTag = await createConfirmationTag(
1,615✔
168
    epochSecrets.keySchedule.confirmationKey,
1,615✔
169
    updatedGroupContext.confirmedTranscriptHash,
1,615✔
170
    cipherSuite.hash,
1,615✔
171
  )
1,615✔
172

173
  const authData: FramedContentAuthDataCommit = {
1,615✔
174
    contentType: framedContent.contentType,
1,615✔
175
    signature,
1,615✔
176
    confirmationTag,
1,615✔
177
  }
1,615✔
178

179
  const [commit] = await protectCommit(
1,615✔
180
    wireAsPublicMessage,
1,615✔
181
    state,
1,615✔
182
    authenticatedData,
1,615✔
183
    framedContent,
1,615✔
184
    authData,
1,615✔
185
    cipherSuite,
1,615✔
186
  )
1,615✔
187

188
  const welcome: Welcome | undefined = await createWelcome(
1,615✔
189
    ratchetTreeExtension,
1,615✔
190
    updatedGroupContext,
1,615✔
191
    confirmationTag,
1,615✔
192
    state,
1,615✔
193
    tree,
1,615✔
194
    cipherSuite,
1,615✔
195
    epochSecrets,
1,615✔
196
    res,
1,615✔
197
    pathSecrets,
1,615✔
198
    groupInfoExtensions,
1,615✔
199
  )
1,615✔
200

201
  const groupActiveState: GroupActiveState = res.selfRemoved
1,615!
UNCOV
202
    ? { kind: "removedFromGroup" }
✔
203
    : suspendedPendingReinit !== undefined
1,615✔
204
      ? { kind: "suspendedPendingReinit", reinit: suspendedPendingReinit }
38✔
205
      : { kind: "active" }
1,577✔
206

207
  const newState: ClientState = {
1,976✔
208
    groupContext: updatedGroupContext,
1,976✔
209
    ratchetTree: tree,
1,976✔
210
    secretTree: await createSecretTree(
1,976✔
211
      leafWidth(tree.length),
1,976✔
212
      epochSecrets.keySchedule.encryptionSecret,
1,976✔
213
      cipherSuite.kdf,
1,976✔
214
    ),
1,976✔
215
    keySchedule: epochSecrets.keySchedule,
1,615✔
216
    privatePath: privateKeys,
1,615✔
217
    unappliedProposals: {},
1,615✔
218
    historicalReceiverData: addHistoricalReceiverData(state),
1,615✔
219
    confirmationTag,
1,615✔
220
    signaturePrivateKey: state.signaturePrivateKey,
1,615✔
221
    groupActiveState,
1,615✔
222
    clientConfig: state.clientConfig,
1,615✔
223
  }
1,615✔
224

225
  return { newState, welcome, commit }
1,615✔
226
}
1,615✔
227

228
function bundleAllProposals(state: ClientState, extraProposals: Proposal[]): ProposalOrRef[] {
1,938✔
229
  const refs: ProposalOrRef[] = Object.keys(state.unappliedProposals).map((p) => ({
1,938✔
230
    proposalOrRefType: "reference",
95✔
231
    reference: base64ToBytes(p),
95✔
232
  }))
1,938✔
233

234
  const proposals: ProposalOrRef[] = extraProposals.map((p) => ({ proposalOrRefType: "proposal", proposal: p }))
1,938✔
235

236
  return [...refs, ...proposals]
1,938✔
237
}
1,938✔
238

239
async function createWelcome(
1,615✔
240
  ratchetTreeExtension: boolean,
1,615✔
241
  groupContext: GroupContext,
1,615✔
242
  confirmationTag: Uint8Array,
1,615✔
243
  state: ClientState,
1,615✔
244
  tree: RatchetTree,
1,615✔
245
  cs: CiphersuiteImpl,
1,615✔
246
  epochSecrets: EpochSecrets,
1,615✔
247
  res: ApplyProposalsResult,
1,615✔
248
  pathSecrets: PathSecret[],
1,615✔
249
  extensions: Extension[],
1,615✔
250
): Promise<Welcome | undefined> {
1,615✔
251
  const groupInfo = ratchetTreeExtension
1,615✔
252
    ? await createGroupInfoWithRatchetTree(groupContext, confirmationTag, state, tree, extensions, cs)
190✔
253
    : await createGroupInfo(groupContext, confirmationTag, state, extensions, cs)
1,425✔
254

255
  const encryptedGroupInfo = await encryptGroupInfo(groupInfo, epochSecrets.welcomeSecret, cs)
1,425✔
256

257
  const encryptedGroupSecrets: EncryptedGroupSecrets[] =
1,615✔
258
    res.additionalResult.kind === "memberCommit"
1,615✔
259
      ? await Promise.all(
1,577✔
260
          res.additionalResult.addedLeafNodes.map(([leafNodeIndex, keyPackage]) => {
1,577✔
261
            return createEncryptedGroupSecrets(
931✔
262
              tree,
931✔
263
              leafNodeIndex,
931✔
264
              state,
931✔
265
              pathSecrets,
931✔
266
              cs,
931✔
267
              keyPackage,
931✔
268
              encryptedGroupInfo,
931✔
269
              epochSecrets,
931✔
270
              res,
931✔
271
            )
931✔
272
          }),
1,577✔
273
        )
1,577✔
274
      : []
38✔
275

276
  return encryptedGroupSecrets.length > 0
1,615✔
277
    ? {
798✔
278
        cipherSuite: groupContext.cipherSuite,
798✔
279
        secrets: encryptedGroupSecrets,
798✔
280
        encryptedGroupInfo,
798✔
281
      }
798✔
282
    : undefined
817✔
283
}
1,615✔
284

285
async function createEncryptedGroupSecrets(
931✔
286
  tree: RatchetTree,
931✔
287
  leafNodeIndex: LeafIndex,
931✔
288
  state: ClientState,
931✔
289
  pathSecrets: PathSecret[],
931✔
290
  cs: CiphersuiteImpl,
931✔
291
  keyPackage: KeyPackage,
931✔
292
  encryptedGroupInfo: Uint8Array,
931✔
293
  epochSecrets: EpochSecrets,
931✔
294
  res: ApplyProposalsResult,
931✔
295
) {
931✔
296
  const nodeIndex = firstCommonAncestor(tree, leafNodeIndex, toLeafIndex(state.privatePath.leafIndex))
931✔
297
  const pathSecret = pathSecrets.find((ps) => ps.nodeIndex === nodeIndex)
931✔
298
  const pk = await cs.hpke.importPublicKey(keyPackage.initKey)
931✔
299
  const egs = await encryptGroupSecrets(
931✔
300
    pk,
931✔
301
    encryptedGroupInfo,
931✔
302
    { joinerSecret: epochSecrets.joinerSecret, pathSecret: pathSecret?.secret, psks: res.pskIds },
931!
303
    cs.hpke,
931✔
304
  )
931✔
305

306
  const ref = await makeKeyPackageRef(keyPackage, cs.hash)
931✔
307

308
  return { newMember: ref, encryptedGroupSecrets: { kemOutput: egs.enc, ciphertext: egs.ct } }
931✔
309
}
931✔
310

311
export async function createGroupInfo(
1,729✔
312
  groupContext: GroupContext,
1,729✔
313
  confirmationTag: Uint8Array,
1,729✔
314
  state: ClientState,
1,729✔
315
  extensions: Extension[],
1,729✔
316
  cs: CiphersuiteImpl,
1,729✔
317
): Promise<GroupInfo> {
1,729✔
318
  const groupInfoTbs: GroupInfoTBS = {
1,729✔
319
    groupContext: groupContext,
1,729✔
320
    extensions: extensions,
1,729✔
321
    confirmationTag,
1,729✔
322
    signer: state.privatePath.leafIndex,
1,729✔
323
  }
1,729✔
324

325
  return signGroupInfo(groupInfoTbs, state.signaturePrivateKey, cs.signature)
1,729✔
326
}
1,729✔
327

328
export async function createGroupInfoWithRatchetTree(
190✔
329
  groupContext: GroupContext,
190✔
330
  confirmationTag: Uint8Array,
190✔
331
  state: ClientState,
190✔
332
  tree: RatchetTree,
190✔
333
  extensions: Extension[],
190✔
334
  cs: CiphersuiteImpl,
190✔
335
): Promise<GroupInfo> {
190✔
336
  const encodedTree = encodeRatchetTree(tree)
190✔
337

338
  const gi = await createGroupInfo(
190✔
339
    groupContext,
190✔
340
    confirmationTag,
190✔
341
    state,
190✔
342
    [...extensions, { extensionType: "ratchet_tree", extensionData: encodedTree }],
190✔
343
    cs,
190✔
344
  )
190✔
345

346
  return gi
190✔
347
}
190✔
348

349
export async function createGroupInfoWithExternalPub(
76✔
350
  state: ClientState,
76✔
351
  extensions: Extension[],
76✔
352
  cs: CiphersuiteImpl,
76✔
353
): Promise<GroupInfo> {
76✔
354
  const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret)
76✔
355
  const externalPub = await cs.hpke.exportPublicKey(externalKeyPair.publicKey)
76✔
356

357
  const gi = await createGroupInfo(
76✔
358
    state.groupContext,
76✔
359
    state.confirmationTag,
76✔
360
    state,
76✔
361
    [...extensions, { extensionType: "external_pub", extensionData: externalPub }],
76✔
362
    cs,
76✔
363
  )
76✔
364

365
  return gi
76✔
366
}
76✔
367

368
export async function createGroupInfoWithExternalPubAndRatchetTree(
38✔
369
  state: ClientState,
38✔
370
  extensions: Extension[],
38✔
371
  cs: CiphersuiteImpl,
38✔
372
): Promise<GroupInfo> {
38✔
373
  const encodedTree = encodeRatchetTree(state.ratchetTree)
38✔
374

375
  const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret)
38✔
376
  const externalPub = await cs.hpke.exportPublicKey(externalKeyPair.publicKey)
38✔
377

378
  const gi = await createGroupInfo(
38✔
379
    state.groupContext,
38✔
380
    state.confirmationTag,
38✔
381
    state,
38✔
382
    [
38✔
383
      ...extensions,
38✔
384
      { extensionType: "external_pub", extensionData: externalPub },
38✔
385
      { extensionType: "ratchet_tree", extensionData: encodedTree },
38✔
386
    ],
38✔
387
    cs,
38✔
388
  )
38✔
389

390
  return gi
38✔
391
}
38✔
392

393
async function protectCommit(
1,615✔
394
  publicMessage: boolean,
1,615✔
395
  state: ClientState,
1,615✔
396
  authenticatedData: Uint8Array,
1,615✔
397
  content: FramedContentCommit,
1,615✔
398
  authData: FramedContentAuthDataCommit,
1,615✔
399
  cs: CiphersuiteImpl,
1,615✔
400
): Promise<[MLSMessage, SecretTree]> {
1,615✔
401
  const wireformat = publicMessage ? "mls_public_message" : "mls_private_message"
1,615✔
402

403
  const authenticatedContent: AuthenticatedContentCommit = {
1,615✔
404
    wireformat,
1,615✔
405
    content,
1,615✔
406
    auth: authData,
1,615✔
407
  }
1,615✔
408

409
  if (publicMessage) {
1,615✔
410
    const msg = await protectPublicMessage(
76✔
411
      state.keySchedule.membershipKey,
76✔
412
      state.groupContext,
76✔
413
      authenticatedContent,
76✔
414
      cs,
76✔
415
    )
76✔
416

417
    return [{ version: "mls10", wireformat: "mls_public_message", publicMessage: msg }, state.secretTree]
76✔
418
  } else {
1,539✔
419
    const res = await protect(
1,539✔
420
      state.keySchedule.senderDataSecret,
1,539✔
421
      authenticatedData,
1,539✔
422
      state.groupContext,
1,539✔
423
      state.secretTree,
1,539✔
424
      { ...content, auth: authData },
1,539✔
425
      state.privatePath.leafIndex,
1,539✔
426
      state.clientConfig.paddingConfig,
1,539✔
427
      cs,
1,539✔
428
    )
1,539✔
429

430
    return [{ version: "mls10", wireformat: "mls_private_message", privateMessage: res.privateMessage }, res.tree]
1,539✔
431
  }
1,539✔
432
}
1,615✔
433

434
export async function applyUpdatePathSecret(
6,599✔
435
  tree: RatchetTree,
6,599✔
436
  privatePath: PrivateKeyPath,
6,599✔
437
  senderLeafIndex: LeafIndex,
6,599✔
438
  gc: GroupContext,
6,599✔
439
  path: UpdatePath,
6,599✔
440
  excludeNodes: NodeIndex[],
6,599✔
441
  cs: CiphersuiteImpl,
6,599✔
442
): Promise<{ nodeIndex: NodeIndex; pathSecret: Uint8Array }> {
6,599✔
443
  const {
6,599✔
444
    nodeIndex: ancestorNodeIndex,
6,599✔
445
    resolution,
6,599✔
446
    updateNode,
6,599✔
447
  } = firstMatchAncestor(tree, toLeafIndex(privatePath.leafIndex), senderLeafIndex, path)
6,599✔
448

449
  for (const [i, nodeIndex] of filterNewLeaves(resolution, excludeNodes).entries()) {
6,599✔
450
    if (privatePath.privateKeys[nodeIndex] !== undefined) {
7,683✔
451
      const key = await cs.hpke.importPrivateKey(privatePath.privateKeys[nodeIndex])
6,599✔
452
      const ct = updateNode!.encryptedPathSecret[i]!
6,599✔
453

454
      const pathSecret = await decryptWithLabel(
6,599✔
455
        key,
6,599✔
456
        "UpdatePathNode",
6,599✔
457
        encodeGroupContext(gc),
6,599✔
458
        ct.kemOutput,
6,599✔
459
        ct.ciphertext,
6,599✔
460
        cs.hpke,
6,599✔
461
      )
6,599✔
462
      return { nodeIndex: ancestorNodeIndex, pathSecret }
6,599✔
463
    }
6,599✔
464
  }
7,683!
465

466
  throw new InternalError("No overlap between provided private keys and update path")
×
UNCOV
467
}
×
468

469
export async function joinGroupExternal(
38✔
470
  groupInfo: GroupInfo,
38✔
471
  keyPackage: KeyPackage,
38✔
472
  privateKeys: PrivateKeyPackage,
38✔
473
  resync: boolean,
38✔
474
  cs: CiphersuiteImpl,
38✔
475
  tree?: RatchetTree,
38✔
476
  clientConfig: ClientConfig = defaultClientConfig,
38✔
477
  authenticatedData: Uint8Array = new Uint8Array(),
38✔
478
) {
38✔
479
  const externalPub = groupInfo.extensions.find((ex) => ex.extensionType === "external_pub")
38✔
480

481
  if (externalPub === undefined) throw new UsageError("Could not find external_pub extension")
38!
482

483
  const allExtensionsSupported = extensionsSupportedByCapabilities(
38✔
484
    groupInfo.groupContext.extensions,
38✔
485
    keyPackage.leafNode.capabilities,
38✔
486
  )
38✔
487
  if (!allExtensionsSupported) throw new UsageError("client does not support every extension in the GroupContext")
38!
488

489
  const { enc, secret: initSecret } = await exportSecret(externalPub.extensionData, cs)
38✔
490

491
  const ratchetTree = ratchetTreeFromExtension(groupInfo) ?? tree
38!
492

493
  if (ratchetTree === undefined) throw new UsageError("No RatchetTree passed and no ratchet_tree extension")
38!
494

495
  throwIfDefined(
38✔
496
    await validateRatchetTree(
38✔
497
      ratchetTree,
38✔
498
      groupInfo.groupContext,
38✔
499
      clientConfig.lifetimeConfig,
38✔
500
      clientConfig.authService,
38✔
501
      groupInfo.groupContext.treeHash,
38✔
502
      cs,
38✔
503
    ),
38✔
504
  )
38✔
505

506
  const signaturePublicKey = getSignaturePublicKeyFromLeafIndex(ratchetTree, toLeafIndex(groupInfo.signer))
38✔
507

508
  const signerCredential = getCredentialFromLeafIndex(ratchetTree, toLeafIndex(groupInfo.signer))
38✔
509

510
  const credentialVerified = await clientConfig.authService.validateCredential(signerCredential, signaturePublicKey)
38✔
511

512
  if (!credentialVerified) throw new ValidationError("Could not validate credential")
38!
513

514
  const groupInfoSignatureVerified = await verifyGroupInfoSignature(groupInfo, signaturePublicKey, cs.signature)
38✔
515

516
  if (!groupInfoSignatureVerified) throw new CryptoVerificationError("Could not verify groupInfo Signature")
38!
517

518
  const formerLeafIndex = resync
38✔
519
    ? nodeToLeafIndex(
19✔
520
        toNodeIndex(
19✔
521
          ratchetTree.findIndex((n) => {
19✔
522
            if (n !== undefined && n.nodeType === "leaf") {
95✔
523
              return clientConfig.keyPackageEqualityConfig.compareKeyPackageToLeafNode(keyPackage, n.leaf)
57✔
524
            }
57✔
525
            return false
38✔
526
          }),
19✔
527
        ),
19✔
528
      )
19✔
529
    : undefined
19✔
530

531
  const updatedTree = formerLeafIndex !== undefined ? removeLeafNode(ratchetTree, formerLeafIndex) : ratchetTree
38✔
532

533
  const [treeWithNewLeafNode, newLeafNodeIndex] = addLeafNode(updatedTree, keyPackage.leafNode)
38✔
534

535
  const [newTree, updatePath, pathSecrets, newPrivateKey] = await createUpdatePath(
38✔
536
    treeWithNewLeafNode,
38✔
537
    nodeToLeafIndex(newLeafNodeIndex),
38✔
538
    groupInfo.groupContext,
38✔
539
    privateKeys.signaturePrivateKey,
38✔
540
    cs,
38✔
541
  )
38✔
542

543
  const privateKeyPath = updateLeafKey(
38✔
544
    await toPrivateKeyPath(pathToPathSecrets(pathSecrets), nodeToLeafIndex(newLeafNodeIndex), cs),
38✔
545
    await cs.hpke.exportPrivateKey(newPrivateKey),
38✔
546
  )
38✔
547

548
  const lastPathSecret = pathSecrets.at(-1)
38✔
549

550
  const commitSecret =
38✔
551
    lastPathSecret === undefined
38!
UNCOV
552
      ? new Uint8Array(cs.kdf.size)
×
553
      : await deriveSecret(lastPathSecret.secret, "path", cs.kdf)
38✔
554

555
  const externalInitProposal: ProposalExternalInit = {
38✔
556
    proposalType: "external_init",
38✔
557
    externalInit: { kemOutput: enc },
38✔
558
  }
38✔
559
  const proposals: Proposal[] =
38✔
560
    formerLeafIndex !== undefined
38✔
561
      ? [{ proposalType: "remove", remove: { removed: formerLeafIndex } }, externalInitProposal]
19✔
562
      : [externalInitProposal]
19✔
563

564
  const pskSecret = new Uint8Array(cs.kdf.size)
38✔
565

566
  const { signature, framedContent } = await createContentCommitSignature(
38✔
567
    groupInfo.groupContext,
38✔
568
    "mls_public_message",
38✔
569
    { proposals: proposals.map((p) => ({ proposalOrRefType: "proposal", proposal: p })), path: updatePath },
38✔
570
    {
38✔
571
      senderType: "new_member_commit",
38✔
572
    },
38✔
573
    authenticatedData,
38✔
574
    privateKeys.signaturePrivateKey,
38✔
575
    cs.signature,
38✔
576
  )
38✔
577

578
  const treeHash = await treeHashRoot(newTree, cs.hash)
38✔
579

580
  const groupContext = await nextEpochContext(
38✔
581
    groupInfo.groupContext,
38✔
582
    "mls_public_message",
38✔
583
    framedContent,
38✔
584
    signature,
38✔
585
    treeHash,
38✔
586
    groupInfo.confirmationTag,
38✔
587
    cs.hash,
38✔
588
  )
38✔
589

590
  const epochSecrets = await initializeEpoch(initSecret, commitSecret, groupContext, pskSecret, cs.kdf)
38✔
591

592
  const confirmationTag = await createConfirmationTag(
38✔
593
    epochSecrets.keySchedule.confirmationKey,
38✔
594
    groupContext.confirmedTranscriptHash,
38✔
595
    cs.hash,
38✔
596
  )
38✔
597

598
  const state: ClientState = {
38✔
599
    ratchetTree: newTree,
38✔
600
    groupContext: groupContext,
38✔
601
    secretTree: await createSecretTree(leafWidth(newTree.length), epochSecrets.keySchedule.encryptionSecret, cs.kdf),
38✔
602
    privatePath: privateKeyPath,
38✔
603
    confirmationTag,
38✔
604
    historicalReceiverData: new Map(),
38✔
605
    signaturePrivateKey: privateKeys.signaturePrivateKey,
38✔
606
    keySchedule: epochSecrets.keySchedule,
38✔
607
    unappliedProposals: {},
38✔
608
    groupActiveState: { kind: "active" },
38✔
609
    clientConfig,
38✔
610
  }
38✔
611

612
  const authenticatedContent: AuthenticatedContentCommit = {
38✔
613
    content: framedContent,
38✔
614
    auth: { signature, confirmationTag, contentType: "commit" },
38✔
615
    wireformat: "mls_public_message",
38✔
616
  }
38✔
617

618
  const msg = await protectPublicMessage(epochSecrets.keySchedule.membershipKey, groupContext, authenticatedContent, cs)
38✔
619

620
  return { publicMessage: msg, newState: state }
38✔
621
}
38✔
622
export function filterNewLeaves(resolution: NodeIndex[], excludeNodes: NodeIndex[]): NodeIndex[] {
1✔
623
  const set = new Set(excludeNodes)
6,599✔
624
  return resolution.filter((i) => !set.has(i))
6,599✔
625
}
6,599✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc