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

LukaJCB / ts-mls / 17250768247

26 Aug 2025 09:21PM UTC coverage: 92.046% (+0.02%) from 92.027%
17250768247

push

github

web-flow
Bump eslint from 9.33.0 to 9.34.0 (#85)

Bumps [eslint](https://github.com/eslint/eslint) from 9.33.0 to 9.34.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.33.0...v9.34.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.34.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

761 of 992 branches covered (76.71%)

Branch coverage included in aggregate %.

2699 of 2767 relevant lines covered (97.54%)

154412.44 hits per line

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

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

51
export interface MLSContext {
52
  state: ClientState
53
  cipherSuite: CiphersuiteImpl
54
  pskIndex?: PskIndex
55
}
56

57
export interface CreateCommitResult {
58
  newState: ClientState
59
  welcome: Welcome | undefined
60
  commit: MLSMessage
61
}
62

63
export interface CreateCommitOptions {
64
  wireAsPublicMessage?: boolean
65
  extraProposals?: Proposal[]
66
  ratchetTreeExtension?: boolean
67
  groupInfoExtensions?: Extension[]
68
  authenticatedData?: Uint8Array
69
}
70

71
export async function createCommit(context: MLSContext, options?: CreateCommitOptions): Promise<CreateCommitResult> {
56✔
72
  const { state, pskIndex = makePskIndex(state, {}), cipherSuite } = context
3,952✔
73
  const {
74
    wireAsPublicMessage = false,
1,824✔
75
    extraProposals = [],
665✔
76
    ratchetTreeExtension = false,
1,748✔
77
    authenticatedData = new Uint8Array(),
1,976✔
78
    groupInfoExtensions = [],
1,976✔
79
  } = options ?? {}
3,952✔
80

81
  checkCanSendHandshakeMessages(state)
3,952✔
82

83
  const wireformat = wireAsPublicMessage ? "mls_public_message" : "mls_private_message"
3,876✔
84

85
  const allProposals = bundleAllProposals(state, extraProposals)
3,876✔
86

87
  const res = await applyProposals(
3,876✔
88
    state,
89
    allProposals,
90
    toLeafIndex(state.privatePath.leafIndex),
91
    pskIndex,
92
    true,
93
    cipherSuite,
94
  )
95

96
  if (res.additionalResult.kind === "externalCommit") throw new UsageError("Cannot create externalCommit as a member")
3,230!
97

98
  const suspendedPendingReinit = res.additionalResult.kind === "reinit" ? res.additionalResult.reinit : undefined
3,230✔
99

100
  const [tree, updatePath, pathSecrets, newPrivateKey] = res.needsUpdatePath
3,230✔
101
    ? await createUpdatePath(
102
        res.tree,
103
        toLeafIndex(state.privatePath.leafIndex),
104
        state.groupContext,
105
        state.signaturePrivateKey,
106
        cipherSuite,
107
      )
108
    : [res.tree, undefined, [] as PathSecret[], undefined]
109

110
  const updatedExtensions =
111
    res.additionalResult.kind === "memberCommit" && res.additionalResult.extensions.length > 0
3,230!
112
      ? res.additionalResult.extensions
113
      : state.groupContext.extensions
114

115
  const groupContextWithExtensions = { ...state.groupContext, extensions: updatedExtensions }
3,230✔
116

117
  const privateKeys = mergePrivateKeyPaths(
3,230✔
118
    newPrivateKey !== undefined
1,615✔
119
      ? updateLeafKey(state.privatePath, await cipherSuite.hpke.exportPrivateKey(newPrivateKey))
120
      : state.privatePath,
121
    await toPrivateKeyPath(pathToPathSecrets(pathSecrets), state.privatePath.leafIndex, cipherSuite),
122
  )
123

124
  const lastPathSecret = pathSecrets.at(-1)
3,230✔
125

126
  const commitSecret =
127
    lastPathSecret === undefined
3,230✔
128
      ? new Uint8Array(cipherSuite.kdf.size)
129
      : await deriveSecret(lastPathSecret.secret, "path", cipherSuite.kdf)
130

131
  const { signature, framedContent } = await createContentCommitSignature(
3,230✔
132
    state.groupContext,
133
    wireformat,
134
    { proposals: allProposals, path: updatePath },
135
    { senderType: "member", leafIndex: state.privatePath.leafIndex },
136
    authenticatedData,
137
    state.signaturePrivateKey,
138
    cipherSuite.signature,
139
  )
140

141
  const treeHash = await treeHashRoot(tree, cipherSuite.hash)
3,230✔
142

143
  const updatedGroupContext = await nextEpochContext(
3,230✔
144
    groupContextWithExtensions,
145
    wireformat,
146
    framedContent,
147
    signature,
148
    treeHash,
149
    state.confirmationTag,
150
    cipherSuite.hash,
151
  )
152

153
  const epochSecrets = await initializeEpoch(
3,230✔
154
    state.keySchedule.initSecret,
155
    commitSecret,
156
    updatedGroupContext,
157
    res.pskSecret,
158
    cipherSuite.kdf,
159
  )
160

161
  const confirmationTag = await createConfirmationTag(
3,230✔
162
    epochSecrets.keySchedule.confirmationKey,
163
    updatedGroupContext.confirmedTranscriptHash,
164
    cipherSuite.hash,
165
  )
166

167
  const authData: FramedContentAuthDataCommit = {
3,230✔
168
    contentType: framedContent.contentType,
169
    signature,
170
    confirmationTag,
171
  }
172

173
  const [commit] = await protectCommit(
3,230✔
174
    wireAsPublicMessage,
175
    state,
176
    authenticatedData,
177
    framedContent,
178
    authData,
179
    cipherSuite,
180
  )
181

182
  const welcome: Welcome | undefined = await createWelcome(
3,230✔
183
    ratchetTreeExtension,
184
    updatedGroupContext,
185
    confirmationTag,
186
    state,
187
    tree,
188
    cipherSuite,
189
    epochSecrets,
190
    res,
191
    pathSecrets,
192
    groupInfoExtensions,
193
  )
194

195
  const groupActiveState: GroupActiveState = res.selfRemoved
3,230!
196
    ? { kind: "removedFromGroup" }
197
    : suspendedPendingReinit !== undefined
1,615✔
198
      ? { kind: "suspendedPendingReinit", reinit: suspendedPendingReinit }
199
      : { kind: "active" }
200

201
  const newState: ClientState = {
3,230✔
202
    groupContext: updatedGroupContext,
203
    ratchetTree: tree,
204
    secretTree: await createSecretTree(
205
      leafWidth(tree.length),
206
      epochSecrets.keySchedule.encryptionSecret,
207
      cipherSuite.kdf,
208
    ),
209
    keySchedule: epochSecrets.keySchedule,
210
    privatePath: privateKeys,
211
    unappliedProposals: {},
212
    historicalReceiverData: addHistoricalReceiverData(state),
213
    confirmationTag,
214
    signaturePrivateKey: state.signaturePrivateKey,
215
    groupActiveState,
216
    clientConfig: state.clientConfig,
217
  }
218

219
  return { newState, welcome, commit }
3,230✔
220
}
221

222
function bundleAllProposals(state: ClientState, extraProposals: Proposal[]): ProposalOrRef[] {
223
  const refs: ProposalOrRef[] = Object.keys(state.unappliedProposals).map((p) => ({
3,876✔
224
    proposalOrRefType: "reference",
225
    reference: base64ToBytes(p),
226
  }))
227

228
  const proposals: ProposalOrRef[] = extraProposals.map((p) => ({ proposalOrRefType: "proposal", proposal: p }))
3,876✔
229

230
  return [...refs, ...proposals]
3,876✔
231
}
232

233
async function createWelcome(
234
  ratchetTreeExtension: boolean,
235
  groupContext: GroupContext,
236
  confirmationTag: Uint8Array,
237
  state: ClientState,
238
  tree: RatchetTree,
239
  cs: CiphersuiteImpl,
240
  epochSecrets: EpochSecrets,
241
  res: ApplyProposalsResult,
242
  pathSecrets: PathSecret[],
243
  extensions: Extension[],
244
): Promise<Welcome | undefined> {
245
  const groupInfo = ratchetTreeExtension
3,230✔
246
    ? await createGroupInfoWithRatchetTree(groupContext, confirmationTag, state, tree, extensions, cs)
247
    : await createGroupInfo(groupContext, confirmationTag, state, extensions, cs)
248

249
  const encryptedGroupInfo = await encryptGroupInfo(groupInfo, epochSecrets.welcomeSecret, cs)
3,230✔
250

251
  const encryptedGroupSecrets: EncryptedGroupSecrets[] =
252
    res.additionalResult.kind === "memberCommit"
3,230✔
253
      ? await Promise.all(
254
          res.additionalResult.addedLeafNodes.map(([leafNodeIndex, keyPackage]) => {
255
            return createEncryptedGroupSecrets(
1,862✔
256
              tree,
257
              leafNodeIndex,
258
              state,
259
              pathSecrets,
260
              cs,
261
              keyPackage,
262
              encryptedGroupInfo,
263
              epochSecrets,
264
              res,
265
            )
266
          }),
267
        )
268
      : []
269

270
  return encryptedGroupSecrets.length > 0
3,230✔
271
    ? {
272
        cipherSuite: groupContext.cipherSuite,
273
        secrets: encryptedGroupSecrets,
274
        encryptedGroupInfo,
275
      }
276
    : undefined
277
}
278

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

300
  const ref = await makeKeyPackageRef(keyPackage, cs.hash)
1,862✔
301

302
  return { newMember: ref, encryptedGroupSecrets: { kemOutput: egs.enc, ciphertext: egs.ct } }
1,862✔
303
}
304

305
export async function createGroupInfo(
56✔
306
  groupContext: GroupContext,
307
  confirmationTag: Uint8Array,
308
  state: ClientState,
309
  extensions: Extension[],
310
  cs: CiphersuiteImpl,
311
): Promise<GroupInfo> {
312
  const groupInfoTbs: GroupInfoTBS = {
3,458✔
313
    groupContext: groupContext,
314
    extensions: extensions,
315
    confirmationTag,
316
    signer: state.privatePath.leafIndex,
317
  }
318

319
  return signGroupInfo(groupInfoTbs, state.signaturePrivateKey, cs.signature)
3,458✔
320
}
321

322
export async function createGroupInfoWithRatchetTree(
56✔
323
  groupContext: GroupContext,
324
  confirmationTag: Uint8Array,
325
  state: ClientState,
326
  tree: RatchetTree,
327
  extensions: Extension[],
328
  cs: CiphersuiteImpl,
329
): Promise<GroupInfo> {
330
  const encodedTree = encodeRatchetTree(tree)
380✔
331

332
  const gi = await createGroupInfo(
380✔
333
    groupContext,
334
    confirmationTag,
335
    state,
336
    [...extensions, { extensionType: "ratchet_tree", extensionData: encodedTree }],
337
    cs,
338
  )
339

340
  return gi
380✔
341
}
342

343
export async function createGroupInfoWithExternalPub(
56✔
344
  state: ClientState,
345
  extensions: Extension[],
346
  cs: CiphersuiteImpl,
347
): Promise<GroupInfo> {
348
  const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret)
152✔
349
  const externalPub = await cs.hpke.exportPublicKey(externalKeyPair.publicKey)
152✔
350

351
  const gi = await createGroupInfo(
152✔
352
    state.groupContext,
353
    state.confirmationTag,
354
    state,
355
    [...extensions, { extensionType: "external_pub", extensionData: externalPub }],
356
    cs,
357
  )
358

359
  return gi
152✔
360
}
361

362
export async function createGroupInfoWithExternalPubAndRatchetTree(
56✔
363
  state: ClientState,
364
  extensions: Extension[],
365
  cs: CiphersuiteImpl,
366
): Promise<GroupInfo> {
367
  const encodedTree = encodeRatchetTree(state.ratchetTree)
76✔
368

369
  const externalKeyPair = await cs.hpke.deriveKeyPair(state.keySchedule.externalSecret)
76✔
370
  const externalPub = await cs.hpke.exportPublicKey(externalKeyPair.publicKey)
76✔
371

372
  const gi = await createGroupInfo(
76✔
373
    state.groupContext,
374
    state.confirmationTag,
375
    state,
376
    [
377
      ...extensions,
378
      { extensionType: "external_pub", extensionData: externalPub },
379
      { extensionType: "ratchet_tree", extensionData: encodedTree },
380
    ],
381
    cs,
382
  )
383

384
  return gi
76✔
385
}
386

387
async function protectCommit(
388
  publicMessage: boolean,
389
  state: ClientState,
390
  authenticatedData: Uint8Array,
391
  content: FramedContentCommit,
392
  authData: FramedContentAuthDataCommit,
393
  cs: CiphersuiteImpl,
394
): Promise<[MLSMessage, SecretTree]> {
395
  const wireformat = publicMessage ? "mls_public_message" : "mls_private_message"
3,230✔
396

397
  const authenticatedContent: AuthenticatedContentCommit = {
3,230✔
398
    wireformat,
399
    content,
400
    auth: authData,
401
  }
402

403
  if (publicMessage) {
3,230✔
404
    const msg = await protectPublicMessage(
152✔
405
      state.keySchedule.membershipKey,
406
      state.groupContext,
407
      authenticatedContent,
408
      cs,
409
    )
410

411
    return [{ version: "mls10", wireformat: "mls_public_message", publicMessage: msg }, state.secretTree]
152✔
412
  } else {
413
    const res = await protect(
3,078✔
414
      state.keySchedule.senderDataSecret,
415
      authenticatedData,
416
      state.groupContext,
417
      state.secretTree,
418
      { ...content, auth: authData },
419
      state.privatePath.leafIndex,
420
      state.clientConfig.paddingConfig,
421
      cs,
422
    )
423

424
    return [{ version: "mls10", wireformat: "mls_private_message", privateMessage: res.privateMessage }, res.tree]
3,078✔
425
  }
426
}
427

428
export async function applyUpdatePathSecret(
56✔
429
  tree: RatchetTree,
430
  privatePath: PrivateKeyPath,
431
  senderLeafIndex: LeafIndex,
432
  gc: GroupContext,
433
  path: UpdatePath,
434
  excludeNodes: NodeIndex[],
435
  cs: CiphersuiteImpl,
436
): Promise<{ nodeIndex: NodeIndex; pathSecret: Uint8Array }> {
437
  const {
438
    nodeIndex: ancestorNodeIndex,
439
    resolution,
440
    updateNode,
441
  } = firstMatchAncestor(tree, toLeafIndex(privatePath.leafIndex), senderLeafIndex, path)
13,198✔
442

443
  for (const [i, nodeIndex] of filterNewLeaves(resolution, excludeNodes).entries()) {
13,198✔
444
    if (privatePath.privateKeys[nodeIndex] !== undefined) {
15,264✔
445
      const key = await cs.hpke.importPrivateKey(privatePath.privateKeys[nodeIndex])
13,198✔
446
      const ct = updateNode!.encryptedPathSecret[i]!
13,198✔
447

448
      const pathSecret = await decryptWithLabel(
13,198✔
449
        key,
450
        "UpdatePathNode",
451
        encodeGroupContext(gc),
452
        ct.kemOutput,
453
        ct.ciphertext,
454
        cs.hpke,
455
      )
456
      return { nodeIndex: ancestorNodeIndex, pathSecret }
13,198✔
457
    }
458
  }
459

460
  throw new InternalError("No overlap between provided private keys and update path")
×
461
}
462

463
export async function joinGroupExternal(
56✔
464
  groupInfo: GroupInfo,
465
  keyPackage: KeyPackage,
466
  privateKeys: PrivateKeyPackage,
467
  resync: boolean,
468
  cs: CiphersuiteImpl,
469
  tree?: RatchetTree,
470
  clientConfig: ClientConfig = defaultClientConfig,
38✔
471
  authenticatedData: Uint8Array = new Uint8Array(),
38✔
472
) {
473
  const externalPub = groupInfo.extensions.find((ex) => ex.extensionType === "external_pub")
76✔
474

475
  if (externalPub === undefined) throw new UsageError("Could not find external_pub extension")
76!
476

477
  const allExtensionsSupported = extensionsSupportedByCapabilities(
76✔
478
    groupInfo.groupContext.extensions,
479
    keyPackage.leafNode.capabilities,
480
  )
481
  if (!allExtensionsSupported) throw new UsageError("client does not support every extension in the GroupContext")
76!
482

483
  const { enc, secret: initSecret } = await exportSecret(externalPub.extensionData, cs)
76✔
484

485
  const ratchetTree = ratchetTreeFromExtension(groupInfo) ?? tree
76!
486

487
  if (ratchetTree === undefined) throw new UsageError("No RatchetTree passed and no ratchet_tree extension")
76!
488

489
  throwIfDefined(
76✔
490
    await validateRatchetTree(
491
      ratchetTree,
492
      groupInfo.groupContext,
493
      clientConfig.lifetimeConfig,
494
      clientConfig.authService,
495
      groupInfo.groupContext.treeHash,
496
      cs,
497
    ),
498
  )
499

500
  const signaturePublicKey = getSignaturePublicKeyFromLeafIndex(ratchetTree, toLeafIndex(groupInfo.signer))
76✔
501

502
  const signerCredential = getCredentialFromLeafIndex(ratchetTree, toLeafIndex(groupInfo.signer))
76✔
503

504
  const credentialVerified = await clientConfig.authService.validateCredential(signerCredential, signaturePublicKey)
76✔
505

506
  if (!credentialVerified) throw new ValidationError("Could not validate credential")
76!
507

508
  const groupInfoSignatureVerified = await verifyGroupInfoSignature(groupInfo, signaturePublicKey, cs.signature)
76✔
509

510
  if (!groupInfoSignatureVerified) throw new CryptoVerificationError("Could not verify groupInfo Signature")
76!
511

512
  const formerLeafIndex = resync
76✔
513
    ? nodeToLeafIndex(
514
        toNodeIndex(
515
          ratchetTree.findIndex((n) => {
516
            if (n !== undefined && n.nodeType === "leaf") {
190✔
517
              return clientConfig.keyPackageEqualityConfig.compareKeyPackageToLeafNode(keyPackage, n.leaf)
114✔
518
            }
519
            return false
76✔
520
          }),
521
        ),
522
      )
523
    : undefined
524

525
  const updatedTree = formerLeafIndex !== undefined ? removeLeafNode(ratchetTree, formerLeafIndex) : ratchetTree
76✔
526

527
  const [treeWithNewLeafNode, newLeafNodeIndex] = addLeafNode(updatedTree, keyPackage.leafNode)
76✔
528

529
  const [newTree, updatePath, pathSecrets, newPrivateKey] = await createUpdatePath(
76✔
530
    treeWithNewLeafNode,
531
    nodeToLeafIndex(newLeafNodeIndex),
532
    groupInfo.groupContext,
533
    privateKeys.signaturePrivateKey,
534
    cs,
535
  )
536

537
  const privateKeyPath = updateLeafKey(
76✔
538
    await toPrivateKeyPath(pathToPathSecrets(pathSecrets), nodeToLeafIndex(newLeafNodeIndex), cs),
539
    await cs.hpke.exportPrivateKey(newPrivateKey),
540
  )
541

542
  const lastPathSecret = pathSecrets.at(-1)
76✔
543

544
  const commitSecret =
545
    lastPathSecret === undefined
76!
546
      ? new Uint8Array(cs.kdf.size)
547
      : await deriveSecret(lastPathSecret.secret, "path", cs.kdf)
548

549
  const externalInitProposal: ProposalExternalInit = {
76✔
550
    proposalType: "external_init",
551
    externalInit: { kemOutput: enc },
552
  }
553
  const proposals: Proposal[] =
554
    formerLeafIndex !== undefined
76✔
555
      ? [{ proposalType: "remove", remove: { removed: formerLeafIndex } }, externalInitProposal]
556
      : [externalInitProposal]
557

558
  const pskSecret = new Uint8Array(cs.kdf.size)
76✔
559

560
  const { signature, framedContent } = await createContentCommitSignature(
76✔
561
    groupInfo.groupContext,
562
    "mls_public_message",
563
    { proposals: proposals.map((p) => ({ proposalOrRefType: "proposal", proposal: p })), path: updatePath },
114✔
564
    {
565
      senderType: "new_member_commit",
566
    },
567
    authenticatedData,
568
    privateKeys.signaturePrivateKey,
569
    cs.signature,
570
  )
571

572
  const treeHash = await treeHashRoot(newTree, cs.hash)
76✔
573

574
  const groupContext = await nextEpochContext(
76✔
575
    groupInfo.groupContext,
576
    "mls_public_message",
577
    framedContent,
578
    signature,
579
    treeHash,
580
    groupInfo.confirmationTag,
581
    cs.hash,
582
  )
583

584
  const epochSecrets = await initializeEpoch(initSecret, commitSecret, groupContext, pskSecret, cs.kdf)
76✔
585

586
  const confirmationTag = await createConfirmationTag(
76✔
587
    epochSecrets.keySchedule.confirmationKey,
588
    groupContext.confirmedTranscriptHash,
589
    cs.hash,
590
  )
591

592
  const state: ClientState = {
76✔
593
    ratchetTree: newTree,
594
    groupContext: groupContext,
595
    secretTree: await createSecretTree(leafWidth(newTree.length), epochSecrets.keySchedule.encryptionSecret, cs.kdf),
596
    privatePath: privateKeyPath,
597
    confirmationTag,
598
    historicalReceiverData: new Map(),
599
    signaturePrivateKey: privateKeys.signaturePrivateKey,
600
    keySchedule: epochSecrets.keySchedule,
601
    unappliedProposals: {},
602
    groupActiveState: { kind: "active" },
603
    clientConfig,
604
  }
605

606
  const authenticatedContent: AuthenticatedContentCommit = {
76✔
607
    content: framedContent,
608
    auth: { signature, confirmationTag, contentType: "commit" },
609
    wireformat: "mls_public_message",
610
  }
611

612
  const msg = await protectPublicMessage(epochSecrets.keySchedule.membershipKey, groupContext, authenticatedContent, cs)
76✔
613

614
  return { publicMessage: msg, newState: state }
76✔
615
}
616
export function filterNewLeaves(resolution: NodeIndex[], excludeNodes: NodeIndex[]): NodeIndex[] {
56✔
617
  const set = new Set(excludeNodes)
13,198✔
618
  return resolution.filter((i) => !set.has(i))
16,892✔
619
}
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