Skip to content

Account roots

An account root is the signing key an account is named after. It certifies devices into the account, and it is the credential that proves ownership of it to Calimero Cloud. Everything on this page runs offline: no node, no network, no cloud session.

Every other keyholder API assumes a rootSecret already exists. This is where it comes from.

generateAccountRoot(): Promise<RecoverableAccountRoot>

Section titled “generateAccountRoot(): Promise<RecoverableAccountRoot>”

Mint a brand-new root from the platform CSPRNG.

import { generateAccountRoot } from '@calimero-network/mero-js';
const { secret, publicKey, accountId, phrase } = await generateAccountRoot();
// phrase: 24 words — show it once, have the holder write it down, keep nothing

secret is the whole account. It signs device certificates and cloud links, and there is no recovery path that does not go through it or its phrase — so it is never sent anywhere, and in a browser it does not belong in localStorage.

accountId is what an owner adds as a group member, and what a warrant is attributed to. publicKey is safe to publish.

accountRootFromPhrase(phrase): Promise<RecoverableAccountRoot>

Section titled “accountRootFromPhrase(phrase): Promise<RecoverableAccountRoot>”

Restore a root on another device.

const restored = await accountRootFromPhrase(savedPhrase);

Case and whitespace are normalised, so a phrase pasted out of a text editor with a trailing newline or a stray double space restores fine. A word outside the list, a wrong word count, or a failed checksum throws rather than returning some other account — a single mistyped word is the common failure, and the phrase’s checksum catches all but 1 in 256 of them.

accountRootFromSecret(secret): Promise<AccountRoot>

Section titled “accountRootFromSecret(secret): Promise<AccountRoot>”

The public half of a root you already hold, without re-deriving it by hand.

const { publicKey, accountId } = await accountRootFromSecret(rootSecret);

Throws if secret is not 32 hex-encoded bytes, so a truncated paste fails here rather than silently naming an unrelated account.

Section titled “signAccountLink({ rootSecret, nonce }): Promise<string>”

Sign a Calimero Cloud link challenge with the account root. Returns standard base64.

Most callers want CloudClient.linkAccount(), which fetches the challenge and posts the proof around this. Reach for signAccountLink directly when the root lives somewhere the cloud client cannot reach it — a desktop app, a hardware key, an air-gapped machine — and only the signature comes back.

const { nonce } = await cloud.getAccountLinkChallenge();
const signature = await signAccountLink({ rootSecret, nonce }); // wherever the root lives
await cloud.submitAccountLink({ accountId, rootPublicKey, nonce, signature });

The challenge is single-use, so one fetched and abandoned is spent — get it when the holder is ready to sign, not when the screen opens.

The nonce is signed under a domain separator, so a signature gathered for a cloud link cannot be replayed into any other protocol that has a root sign opaque bytes.

signAccountLogin({ rootSecret, nonce }): Promise<string>

Section titled “signAccountLogin({ rootSecret, nonce }): Promise<string>”

Sign a Calimero Cloud login challenge with the account root. Returns standard base64.

The statement it makes is “I hold the root of account X”, which is the one statement a device credential can never make: a routing proof shows the caller holds a device the root certified, but certificates are public and travel in the clear inside every device-link op, so they can only ever say a device of X is asking.

Most callers want CloudClient.signInWithAccount(), which fetches the challenge and posts the proof around this. Reach for this directly for the same reason as signAccountLink — a root the cloud client cannot reach.

const { nonce } = await cloud.getAccountLoginChallenge();
const signature = await signAccountLogin({ rootSecret, nonce }); // wherever the root lives
await cloud.submitAccountLogin({ rootPublicKey, nonce, signature });

A different domain separator from signAccountLink, and that is the point: both are a root signature over a cloud-issued nonce, so without the separation a signature gathered while linking — an operation performed by someone already signed in and trusting the page — would be replayable as a login.

The cloud does not care which of these produced the signature — only that it verifies and names the account being claimed:

  • Generated here, with the phrase written down. The path above.
  • Held by a desktop app or a hardware key, which signs the challenge without the secret ever entering the browser. Pass the resulting signature straight to the link endpoint.