connectCloud
connectCloud is the login path for a user who has no node.
Two connection shapes, one SDK
Section titled “Two connection shapes, one SDK”createMeroJs({ baseUrl }) is the node connection: point at a node, hold a
credential on it, drive its admin API and JSON-RPC. It assumes the caller has a
node — their own, or one they have an account on.
connectCloud(...) is for the caller who has neither. They sign in with the
identity they already use, and the cloud answers with an attested TEE relay
serving their namespace. Writes go through delegated
execution: signed on their machine, run on the relay,
attributed back to them.
These are not interchangeable options to pick at random — they answer different
questions. “Which node do I talk to?” has an answer for a desktop app and no
answer at all for a web app whose user has never installed anything.
connectCloud is that missing answer, which is why the login step it replaces
is the node-URL prompt, not the authentication.
import { connectCloud } from '@calimero-network/mero-js';
const connection = await connectCloud({ googleIdToken, // or sessionToken, from a previous sign-in authorAccount, authorProof, deviceSecret,});
await connection.execute(contextId, 'set', { key: 'k', value: 'v' });Options
Section titled “Options”interface ConnectCloudOptions { cloudBaseUrl?: string; // default 'https://manager.cloud.calimero.network' sessionToken?: string; // a stored MDMA session — provide this or googleIdToken googleIdToken?: string; // a Google ID token to exchange — provide this or sessionToken onSession?: (s) => void; // persist a new / rolling-refreshed session
authorAccount: string; // hex — whose writes these will be authorProof: string; // hex borsh AccountProof<DeviceCert> deviceSecret?: string; // hex ed25519 seed; never transmitted signer?: Signer; // instead of deviceSecret, for a key with no hex form
namespaceId?: string; // required only when the account owns several nonces?: NonceSource; // defaults to localStorage, keyed by device public key ttlSeconds?: number; // default 300 seal?: VerifyTransportQuote; // attest and seal every call to the relay (see below) fetch?: typeof fetch; timeoutMs?: number;}Pass deviceSecret or signer, never both — they could name different
keys, and nothing downstream could tell which one signed. A browser should
prefer the signer: a device key held as a non-extractable CryptoKey has no hex
form to pass, and a secret that exists as a string can be read by anything on
the origin.
namespaceId is optional only when the account owns exactly one namespace.
With several there is no defensible default: guessing would land a write in
another of the account’s tenants rather than failing, so it throws and names
them.
nonces defaults to a localStorage source keyed by the author’s device
public key (derived locally — the secret must never end up in a storage key
another script can enumerate), so a page reload continues the sequence instead
of replaying it. In a runtime with no localStorage — Node, a worker, an edge
runtime — it falls back to an in-memory counter, which is correct there because
such a process owns its whole sequence. See nonce
sources for why a restart is the one fatal
case.
seal takes a quote verifier, normally createQuoteVerifier with the
measurements of the releases you trust, from trustedMeasurementsFromReleases
(which images to trust). With it, the relay the cloud picked is attested by this client,
and every call to it is sealed to the key its TD holds: the warrant and the
method’s arguments are readable only inside the relay’s TD, not at the host’s
ingress where TLS ends. The cloud calls stay plain; they carry no intent.
Without seal, calls to the relay are ordinary HTTPS, and the only assurance
about the relay is that the cloud checked it at registration. See sealing the
intent.
The connection
Section titled “The connection”interface CloudConnection { cloud: CloudClient; // namespaces, billing, HA, sign-out relay: RelayClient; // the relay chosen for this namespace namespaceId: string; namespace: CloudNamespace; relayInfo: CloudRelay; // what the cloud reported about the chosen relay execute<T>(contextId, method, argsJson?): Promise<IntentResult<T>>;}execute is relay.execute — the shortcut for the one thing this connection
exists to do. Reach for connection.relay for describe(), and
connection.cloud for everything else about the account.
Why it throws instead of degrading
Section titled “Why it throws instead of degrading”Every failure to reach a writable relay is something the user has to go and do, and they are four different things. A single “could not connect” would send them to the wrong one, so each gets its own message:
| Situation | Message says |
|---|---|
| the account owns no namespaces | claim one, then enable HA |
| it owns several and none was named | pass namespaceId, and here they are |
| the namespace has no relays and HA is off | enable HA in the cloud |
| HA is on but nothing is assigned yet | retry shortly |
every node is a ReadOnlyTee replica |
an admin must set the TEE admission policy with mode: 'relay' |
| every relay lacks the grant (nodes reporting no role) | an admin must grant CAN_AUTHOR_ON_BEHALF to this account |
A relay is chosen on the cloud’s canExecute, so a RelayTee needs no grant and a
ReadOnlyTee — a replica — is never picked, whatever its grant. The grant only
decides for a node too old to report a role, and that message names the
executor account, because that is the value the admin has to grant the
capability to:
meroctl --node node1 group members set-capabilities <GROUP_ID> <EXECUTOR_ACCOUNT> \ --can-author-on-behalfSigning in with an account root
Section titled “Signing in with an account root”connectCloudWithAccount is the same connection reached by the other door: no
Google login anywhere in it, just the account root proving the account is yours.
import { connectCloudWithAccount } from '@calimero-network/mero-js';
const connection = await connectCloudWithAccount({ root, // a 64-hex root secret, or a Signer over one authorProof, signer: deviceSigner, // or deviceSecret});The root opens the session; the device still signs the warrants, which is why a device key is required alongside it — a root used as a device key would put the account key into every intent.
authorAccount defaults to the account the root derives, and that default is
the point: it is the only account that root can open a session as, so naming
another would have the cloud answer with this root’s namespaces while every
warrant claimed somebody else’s authorship. Pass it explicitly only when the
device certificate was issued by a different root.
Everything after the session is connectCloud, unchanged — same namespace
choice, same messages, same relay.
Where the intent goes
Section titled “Where the intent goes”Straight to the relay. The cloud is the directory — it is handed an address and a public account id — and never sees a warrant, a device secret, or the method and arguments of a write.