Skip to content

Capabilities

A group member’s capabilities are stored as a single unsigned 32-bit bitmask. The SDK exports the bit constants that core assigns, plus small helpers for reading and editing a mask. These are used with the group capability methods on sdk.admin (getMemberCapabilities, setMemberCapabilities, getDefaultCapabilities, setDefaultCapabilities). The groups & governance guide shows them in a full flow.

The assigned bits (core MemberCapabilities, bits 0–9):

const CAPABILITIES = {
CAN_CREATE_CONTEXT: 1 << 0,
CAN_INVITE_MEMBERS: 1 << 1,
CAN_JOIN_OPEN_SUBGROUPS: 1 << 2,
MANAGE_MEMBERS: 1 << 3,
MANAGE_APPLICATION: 1 << 4,
CAN_CREATE_SUBGROUP: 1 << 5,
CAN_DELETE_SUBGROUP: 1 << 6,
CAN_MANAGE_VISIBILITY: 1 << 7,
CAN_MANAGE_METADATA: 1 << 8,
CAN_AUTHOR_ON_BEHALF: 1 << 9,
} as const;
type CapabilityName = keyof typeof CAPABILITIES; // 'CAN_CREATE_CONTEXT' | …
type CapabilityBit = (typeof CAPABILITIES)[CapabilityName];

The grant delegated execution runs on: it lets a member publish writes attributed to another member, under a warrant that member signed. Held by relays.

It is implied by nothing — not by membership, not by admin, and it is deliberately not propagated by the subgroup-admit cascade. So every group is authorship-closed until an admin sets this bit, and a relay without it is refused before anything executes. Granting it is the step that makes a namespace writable by keyholders who run no node:

await sdk.admin.grantAuthorship(groupId, relayAccount);

Note the subject is the relay’s account, not its signing key — which is what lets the relay rotate keys without the grant needing to be reissued. Warrants name the key as well, so a re-key voids the ones it has not spent, and their authors sign new ones.

grantAuthorship is read-modify-write, and that is the point: setMemberCapabilities replaces the mask, so adding this bit by hand means re-passing every flag the member already holds, and forgetting one revokes it silently. It is a no-op when the member already has the grant.

A namespace that keeps admitting TEE nodes would otherwise need one admin-signed op per admission — published at the moment a node is assigned and the admin is not watching. Core seeds a non-admin member’s capability row from the group’s default mask at admission, so putting the bit there is what makes an attested node land open:

await sdk.admin.openToDelegatedExecution(namespaceId);
Terminal window
meroctl --node node1 group settings set-default-capabilities <NAMESPACE_ID> \
--can-join-open-subgroups --can-author-on-behalf

Set it on the namespace (the root group): a grant resolves through the membership anchor, so one at the root reaches every Open subgroup beneath it, where contexts actually live. A Restricted subgroup is a membership boundary and needs its own.

Three limits, none of which the API can paper over:

  • Not retroactive. The mask is copied at admission, so members already in the group keep what they have — use grantAuthorship for those. Calling this at namespace creation is the only point that needs no backfill.
  • Never reaches admins. Core seeds the default for non-admin roles only, and the capability is not implied by admin, so an admin’s own node stays closed.
  • Reaches every non-admin member, not only attested TEE nodes. What that does not allow: a holder cannot forge. The write is authorized by the author’s own warrant — signed by their device key, committed to one context, method and the exact arguments, nonce checked unspent — so a holder can only spend a warrant it was deliberately handed. It does gain the arguments in cleartext and the choice of when, or whether, to publish.

Named masks for the defaults apps actually hand out, so a call reads as intent rather than a magic number:

const CAPABILITY_PRESETS = {
NONE: 0,
JOIN_ONLY: 4, // CAN_JOIN_OPEN_SUBGROUPS: what a namespace starts with
CONTRIBUTOR: 7, // + CAN_CREATE_CONTEXT, CAN_INVITE_MEMBERS
COLLABORATOR: 231, // + CAN_CREATE_SUBGROUP, CAN_DELETE_SUBGROUP, CAN_MANAGE_VISIBILITY
} as const;
type CapabilityPreset = keyof typeof CAPABILITY_PRESETS;
await sdk.admin.setDefaultCapabilities(namespaceId, {
defaultCapabilities: CAPABILITY_PRESETS.COLLABORATOR,
});

No preset includes CAN_AUTHOR_ON_BEHALF; that grant is always deliberate (see below).

// A u32 mask from capability names and/or bit values. Throws on an unknown name.
capMask(...caps: Array<CapabilityName | number>): number;
capMask('CAN_CREATE_CONTEXT', 'CAN_INVITE_MEMBERS'); // 3
// The names of the assigned bits set in `mask`, in bit order.
capNames(mask: number): CapabilityName[];
capNames(231); // ['CAN_CREATE_CONTEXT', 'CAN_INVITE_MEMBERS', …]

All of these normalize to unsigned 32-bit (>>> 0), so high bits like 1 << 31 behave correctly.

// True if `mask` has every bit of `cap` set.
hasCap(mask: number, cap: number): boolean;
// `mask` with every bit of `cap` set.
withCap(mask: number, cap: number): number;
// `mask` with every bit of `cap` cleared.
withoutCap(mask: number, cap: number): number;
import { CAPABILITIES, hasCap, withCap, withoutCap } from '@calimero-network/mero-js';
const current = await sdk.admin.getMemberCapabilities(groupId, identity);
if (!hasCap(current.capabilities, CAPABILITIES.CAN_INVITE_MEMBERS)) {
// grant invite + subgroup creation, revoke metadata management
let next = withCap(current.capabilities, CAPABILITIES.CAN_INVITE_MEMBERS);
next = withCap(next, CAPABILITIES.CAN_CREATE_SUBGROUP);
next = withoutCap(next, CAPABILITIES.CAN_MANAGE_METADATA);
await sdk.admin.setMemberCapabilities(groupId, identity, { capabilities: next });
}