Skip to content

MarmotClient.capabilities is not forwarded to KeyPackageManagergenerateKeyPackage() always uses defaultCapabilities()` #73

@maddadder

Description

@maddadder

MarmotClient accepts a capabilities option and stores it on this.capabilities, but never passes it to KeyPackageManager. When KeyPackageManager.create() calls generateKeyPackage(), it omits the capabilities parameter entirely, so generateKeyPackage() falls back to defaultCapabilities().

This means the capabilities constructor option on MarmotClient is dead code — any custom capabilities (e.g., adding ProposalType 0x000A / SelfRemove for MDK interop) are silently ignored.

Reproduction:

const client = new MarmotClient({
  signer,
  network,
  keyPackageStore: kpStore,
  capabilities: {
    ...defaultCapabilities(),
    proposals: [...defaultCapabilities().proposals, 0x000a],
  },
});

const kp = await client.keyPackages.create({ relays: ["wss://relay.example.com"] });
// kp.publicPackage.leafNode.capabilities.proposals does NOT contain 0x000a
// Expected: it should contain 0x000a because we passed it in capabilities

Impact:

Amethyst (Android) enforces required_capabilities when inviting MLS group members. Groups created by Amethyst require ProposalType 0x000A (SelfRemove) in leaf_node.capabilities.proposals. Because marmot-ts ignores the caller's capabilities, key packages published by marmot-ts clients are rejected by Amethyst with:

Add KeyPackage leaf capabilities don't meet required_capabilities:
missing extensions=[] proposals=[10] credentials=[]

Proposed fix (3 lines across 2 files):

src/client/marmot-client.ts — pass capabilities to KeyPackageManager:

     this.keyPackages = new KeyPackageManager({
       store: options.keyPackageStore,
       signer: options.signer,
       network: options.network,
       clientId: options.clientId,
+      capabilities: this.capabilities,
     });

src/client/key-package-manager.ts — accept, store, and forward capabilities:

 export type KeyPackageManagerOptions = {
   store: GenericKeyValueStore<StoredKeyPackage>;
   clientId?: string;
   signer: EventSigner;
   network: NostrNetworkInterface;
   cryptoProvider?: CryptoProvider;
+  capabilities?: Capabilities;
 };

In the constructor:

     this.clientId = options.clientId;
+    this.capabilities = options.capabilities;

In create():

     const keyPackage = await generateKeyPackage({
       credential,
       ciphersuiteImpl: ciphersuite,
       isLastResort: options.isLastResort,
+      capabilities: this.capabilities,
     });

rotate() delegates to create() internally, so both paths are fixed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions