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.
MarmotClientaccepts acapabilitiesoption and stores it onthis.capabilities, but never passes it toKeyPackageManager. WhenKeyPackageManager.create()callsgenerateKeyPackage(), it omits thecapabilitiesparameter entirely, sogenerateKeyPackage()falls back todefaultCapabilities().This means the
capabilitiesconstructor option onMarmotClientis dead code — any custom capabilities (e.g., addingProposalType 0x000A/ SelfRemove for MDK interop) are silently ignored.Reproduction:
Impact:
Amethyst (Android) enforces
required_capabilitieswhen inviting MLS group members. Groups created by Amethyst requireProposalType 0x000A(SelfRemove) inleaf_node.capabilities.proposals. Because marmot-ts ignores the caller'scapabilities, key packages published by marmot-ts clients are rejected by Amethyst with: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 tocreate()internally, so both paths are fixed.