Organizations
Organizations and members
Section titled “Organizations and members”Every change to who holds access (members, team members, capabilities and
invites) is approved with the caller’s passkey: the node answers
412 CONFIRM_REQUIRED and the client signs the challenge with passkey.
const org = await client.createOrg('Acme');await client.getOrg(org); // with the teams you reach or manageawait client.setMember(org, bobId, 'member', passkey); // guest | member | admin | ownerawait client.removeMember(org, bobId, passkey); // your own account id to leaveawait client.setOrgMetadata(org, { logo: '…' });await client.setMemberMetadata(org, bobId, { title: 'engineer' }); // null clears itawait client.deleteOrg(org, passkey); // the owner's passkey; takes its teams, invites and appsOwnership moves with setMember(org, account, 'owner', passkey).
Recovery
Section titled “Recovery”An owner who loses every passkey gets the organization back with its recovery seed. The seed never leaves the client: the node stores only its public key and checks a signature the seed makes for the recovering account.
import { newRecoverySeed, parseRecoverySeed, recoveryPublicKey, recoverySeedText } from '@calimero-network/mero-js';
const seed = newRecoverySeed();showToOwner(recoverySeedText(seed)); // 64 hex digits; keep it offlineconst org = await client.createOrg('Acme', { recoveryKey: await recoveryPublicKey(seed) });// later, or for an organization created without one (the owner, with a passkey):await client.setRecoveryKey(org, await recoveryPublicKey(newRecoverySeed()), passkey);
// Every passkey lost: sign up again with a new passkey, thenconst { readyAt } = await fresh.startRecovery(org, parseRecoverySeed(written));// 72 hours later (the owner may cancelRecovery meanwhile; every member sees getOrg(org).recovery.pending)await fresh.finishRecovery(org, newPasskey); // this account owns it; the old owner leaves itRotating the key (setRecoveryKey) cancels a recovery under way, and the old
seed is refused from then on.
Invites
Section titled “Invites”const token = await client.invite(org, 'member', passkey, [teamId], { ttlSecs: 3600, uses: 5 });// the invitee, signed in (no approval needed):await invitee.redeemInvite(token);Someone without an account yet signs up with the invite and joins the
organization in the same step. On a cluster whose sign-up policy is invite
this is the only way in; read the policy first, without a session, to say so
before any passkey prompt:
const { policy, inviteRequired } = await client.signUpPolicy(); // GET /sign-upawait client.signUp(passkey, 'bob', { invite: token });An unknown, expired or used invite, or a sign-up the policy does not take, is
SignUpRefused, with reason (invite-only, closed, unknown-invite,
expired-invite, org-gone), raised before the browser makes a passkey.
const team = await client.createTeam(org, 'eng', 'invite'); // or 'everyone'; optional parentawait client.setTeamMember(org, team, bobId, passkey, { role: 'readOnly' }); // lead | member | readOnlyawait client.updateTeam(org, team, { name: 'platform', parent: null, metadata: { colour: 'blue' } });await client.removeTeamMember(org, team, bobId, passkey);await client.deleteTeam(org, team); // and the teams below it, and their appsAn app created with access: { kind: 'team', team } is reached by the team’s
members; a read-only member’s calls that would write are refused.
Capabilities
Section titled “Capabilities”Below admin, what a member may do is their role plus capability bits:
import { ORG_CAPABILITIES, orgCapabilities } from '@calimero-network/mero-js';
await client.setCapabilities(org, orgCapabilities('CREATE_APP', 'INVITE_MEMBERS'), passkey, { account: bobId });await client.setCapabilities(org, ORG_CAPABILITIES.CREATE_APP, passkey, { account: bobId, team }); // on one teamawait client.setCapabilities(org, ORG_CAPABILITIES.JOIN_OPEN_TEAMS, passkey); // the organization's default| Bit | Value | Lets a member |
|---|---|---|
CREATE_APP |
1 | create apps (on its team, or org-wide) |
INVITE_MEMBERS |
2 | mint invites |
JOIN_OPEN_TEAMS |
4 | be in open teams (the default) |
MANAGE_MEMBERS |
8 | add and remove a team’s members |
MANAGE_APPLICATION |
16 | upgrade the org’s apps, start upgrade runs, approve outbound hosts |
CREATE_TEAM |
32 | create a top-level team |
DELETE_TEAM |
64 | delete a team |
MANAGE_VISIBILITY |
128 | change who may join a team (open or invite-only) |
MANAGE_METADATA |
256 | set the organization’s metadata |
Upgrade runs
Section titled “Upgrade runs”Upgrade every app of the organization that runs a package, with one passkey approval for the whole run, then follow it:
let run = await client.upgradeOrg(org, { package: 'com.example.kv', version: '1.1.0', team, cascade: true }, passkey);while (!run.finished) { await new Promise((r) => setTimeout(r, 1000)); run = await client.upgradeRun(org, run.runId);}console.log(run.done, run.failed); // failed: app id -> why
if (Object.keys(run.failed).length) await client.retryUpgradeRun(org, run.runId, passkey); // a new runThe org’s owner, or a member holding MANAGE_APPLICATION org-wide, starts one.
Approvals
Section titled “Approvals”Where the cluster leaves these decisions to the owner side (the default), they are made with a passkey and logged as consent. These calls belong to a page on the cluster’s own origin (a console); a session opened on an app’s frontend is refused them, and an app sends its user to the consent page instead.
Releases. Nodes on a mero-tee release get an organization’s data only once
it is approved for that organization (the release the org began on is
approved at creation). A member holding APPROVE_RELEASES approves (the owner
and admins hold it; getOrg(org).releaseApprovers lists who does). An app’s
releases are its publisher’s to approve: the organization its package’s
signing key is bound to, for every instance of the key’s packages; for an
unbound package, the app’s own organization or account. The operator can give a
release a deadline (releaseRules() lists them): past it, a scope that has not
approved it (or newer) creates no apps, installs nothing and upgrades nothing
(ReleaseDeadlineError, getOrg(org).newWorkStopped), while its running apps
keep working:
await client.approveRelease(org, '2.3.120', passkey);await client.approveRelease(null, '2.3.120', passkey); // the account's own appsawait client.approveRelease(org, '2.3.110', passkey, false); // withdrawOutbound HTTPS. An app may call out only to URL prefixes approved for its package’s publisher:
await client.approveOutbound(org, 'com.example.kv', passkey); // every prefix its latest release declaresawait client.approveOutbound(org, 'com.example.kv', passkey, { prefixes: ['https://api.example/'], replace: true });await client.approveOutbound(null, 'com.example.kv', passkey); // the account's own appsPublishers. An organization becomes the publisher of a signing key’s packages only with the key’s own signature:
import { publisherBindingBytes } from '@calimero-network/mero-js';
const { cluster, generation } = await client.publisherBinding(signerId);const signature = sign(publisherBindingBytes(cluster, org, signerId, generation)); // the key's Ed25519, base64urlawait client.bindPublisher(org, { signerId, publicKey, signature }, passkey); // a member holding APPROVE_RELEASESA package whose manifest declares upgrades: "publisher" is then upgraded by
the publisher’s MANAGE_APPLICATION holders, every instance at once:
const run = await client.upgradePublished(org, 'com.example.kv', { version: '1.2.0' }, passkey);Without upgrades (or with "owner"), each instance’s owner upgrades it. A
package may move from publisher to owner in a later release, never back.
Reading what waits. appsReport(org) (any member) answers the approved
releases, the releases the cluster’s nodes run and whether each is approved,
and per app its release, the newer ones it may move to and its last upgrade;
pendingApprovals(org) sums it up.