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.
CAPABILITIES
Section titled “CAPABILITIES”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];CAN_AUTHOR_ON_BEHALF
Section titled “CAN_AUTHOR_ON_BEHALF”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, or any warrant already issued against it, needing to be reissued.
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.
Granting it once, for a whole fleet
Section titled “Granting it once, for a whole fleet”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);meroctl --node node1 group settings set-default-capabilities <NAMESPACE_ID> \ --can-join-open-subgroups --can-author-on-behalfSet 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
grantAuthorshipfor 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.
Helpers
Section titled “Helpers”All three 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;Example
Section titled “Example”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 });}