Skip to content

CalimeroClient

// A Calimero Cloud cluster by name: its nodes come from the node directory,
// each attested under `policy` before anything is sent to it.
await CalimeroClient.connect({
cluster: string, // e.g. 'eu-shared-1'
policy: CloudPolicy,
directory?: string, // default DEFAULT_DIRECTORY (Calimero Cloud's)
zone?: string, // try this zone's nodes first
fetch?: typeof fetch,
retry?: { retries?: number; baseDelayMs?: number; maxDelayMs?: number },
random?: () => number, // for the weighted order; default Math.random
}) // throws DirectoryError (reason 'unreachable' | 'not-found' | 'bad-response' | 'empty')
// A fixed list of nodes, or a cluster you run yourself.
new CalimeroClient({
baseUrl: string, // one node of the cluster
nodes?: string[], // other nodes, moved to on 421 WRONG_NODE or when one stays unavailable
policy: CloudPolicy, // which nodes to trust; see Trust and the sealed channel
fetch?: typeof fetch, // carries attestation and envelopes; default the global fetch
retry?: { retries?: number; baseDelayMs?: number; maxDelayMs?: number }, // default 6, 200 ms, 2 s
})

client.nodeUrl is the node in use and client.nodes every node the client may move to. Every route below is sent inside the sealed channel. passkey is a PasskeyProvider (browserPasskey() in a page); a method that takes one answers the node’s 412 CONFIRM_REQUIRED with it.

Method Route
signUp(passkey, name, { origin?, invite? }) → CalimeroSession (the new passkey is asked twice: to register, then to sign the account it creates, 412 CONFIRM_REQUIRED; invite joins its organization, SignUpRefused when the policy or the invite refuses) POST auth/passkey/register/begin, …/finish
signUpPolicy() → { policy, orgInvites, signUpInvites, inviteRequired } (no session) GET sign-up
signIn(passkey, { origin? }) → CalimeroSession POST auth/passkey/login/begin, …/finish
createRecoveryCode(passkey) → the code, once (core #4844) POST auth/recovery
removeRecoveryCode(passkey) DELETE auth/recovery
startAccountRecovery(code, passkey) → AccountRecovery (signed out; passkey creates the new passkey; WrongRecoveryCode) POST auth/recovery/begin, …/start
cancelAccountRecovery(passkey) (a passkey the account already has) POST auth/recovery/cancel
finishAccountRecovery(passkey, accountId?) → CalimeroSession (at readyAt; RecoveryNotReady before) POST auth/recovery/finish
refresh() → CalimeroSession (concurrent calls share one refresh; the client also refreshes by itself on a 401) POST auth/refresh
signOut() POST auth/logout
signOutEverywhere() POST auth/logout-all
useSession(session), useApiKey(key), session, nodeUrl (the node in use) (local)
me() → MeInfo (accountId, via, and the package app and origin a session is bound to) GET auth/me
usage() GET auth/usage
addPasskey(passkey, approver) → credential id (approver: a passkey the account already has) POST auth/passkeys/add/begin, …/finish
labelPasskey(credentialId, label) POST auth/passkeys/{cred}
removePasskey(credentialId, passkey) DELETE auth/passkeys/{cred}
createApiKey({ label?, apps?, methods?, readOnly?, allowConfirm?, ttlSecs?, upgrades? }, passkey) → { keyId, key } (a passkey session; upgrades: { app, package? } makes an upgrade key) POST auth/apikeys
listApiKeys() GET auth/apikeys
revokeApiKey(keyId, passkey) (approved with the account’s passkey) DELETE auth/apikeys/{key}
attestation() → AttestationDoc (unverified, for display) GET attestation (in the clear)
Method Route
installPackage(package, version) → PackageInstalled POST packages/install
uploadPackage(mpk) → PackageInstalled POST packages
packages() → PackageInfo[] GET packages
package(name) → PackageInfo GET packages/{package}
uninstallPackage(name, version?) DELETE packages/{package}[/{version}]
uploadBundle(wasm) → hash (development clusters) POST bundles

A package’s managers register the web origins of a frontend they host themselves, so it can sign people in with passkeys. A session opened there is bound to the package. See Sign in from your own site.

Method Route
packageOrigins.list(package) → PackageOrigin[] ({ origin, rpId, addedBy, addedAt, served }) GET package-origins/{package}
packageOrigins.add(package, origin, passkey?, { rpId?, served? }) → { package, origin, rpId, addedBy, addedAt, served } POST package-origins/{package} (412 over __add_origin)
packageOrigins.remove(package, origin, passkey?) → { package, origin, removed: true } DELETE package-origins/{package} (412 over __remove_origin)

add refuses with BadOrigin (BAD_ORIGIN, BAD_RP_ID), OriginTaken and QuotaExceeded; list and remove with NotFound. Without a passkey the 412 reaches the caller as ConfirmRequired.

served: true registers a custom domain: the cluster also serves the package’s frontend on it from the signed bundle. The origin must be https://host with no port, and the consent the passkey signs names it ({ origin, rpId, served: true }). It further refuses with NotServing (the cluster serves no frontends), ClusterNotUpgraded (a node is on an older release), QuotaExceeded (4 served per package, 64 per cluster) and OriginTaken (held with the other served; remove it first).

Method Route
createApp(name, bundle | { package, version?, service? }, { org?, access?, confirmMethods?, initArgs? }) → app id POST apps
listApps({ package?, bundle? }) → AppInfo[] GET apps
getApp(app) GET apps/{app}
deleteApp(app) DELETE apps/{app}
setAccess(app, access) POST apps/{app}/access
setAppMetadata(app, metadata) POST apps/{app}/metadata
appStorage(app) GET apps/{app}/storage
appAbi(app) GET apps/{app}/abi
upgradeApp(app, { version?, package?, service?, bundle?, migrate? }, passkey?) POST apps/{app}/upgrade
transferApp(app, to, passkey) POST apps/{app}/transfer
call(app, method, args?, idempotencyKey?) → CallResult POST apps/{app}/intents
callConfirmed(app, method, args, passkey, idempotencyKey?) → CallResult POST apps/{app}/intents
view(app, method, args?, { stale?, minIndex? }) → ViewResult POST apps/{app}/views
events(app, from?, signal?) → async iterable of CloudEvent GET apps/{app}/events
uploadBlob(app, bytes) → { blobId, id, size } POST apps/{app}/blobs
listBlobs(app) → BlobInfo[] GET apps/{app}/blobs
blobInfo(app, id) → BlobInfo GET apps/{app}/blobs/{id}/info
downloadBlob(app, id) → bytes GET apps/{app}/blobs/{id}
deleteBlob(app, id) DELETE apps/{app}/blobs/{id}
publishPresence(app, seq, state | null) POST apps/{app}/presence
presence(app) → PresenceEntry[] GET apps/{app}/presence
presenceStream(app, signal?) → async iterable of CloudEvent GET apps/{app}/presence/stream
Method Route
setAlias(alias, app) PUT auth/aliases/{alias}
alias(alias) → app id GET auth/aliases/{alias}
aliases() → { alias: appId } GET auth/aliases
deleteAlias(alias) DELETE auth/aliases/{alias}
Method Route
createOrg(name, { recoveryKey? }) → org id POST orgs
getOrg(org) → OrgInfo (releaseApprovers, publishes, newWorkStopped, recovery) GET orgs/{org}
deleteOrg(org, passkey) DELETE orgs/{org}
setOrgMetadata(org, metadata) POST orgs/{org}/metadata
setMember(org, account, role, passkey) POST orgs/{org}/members
removeMember(org, account, passkey) (also to leave: your own account) DELETE orgs/{org}/members/{account}
setMemberMetadata(org, account, metadata) POST orgs/{org}/members/{account}/metadata
setCapabilities(org, bits, passkey, { account?, team? }) POST orgs/{org}/capabilities
createTeam(org, name, join, parent?) → team id POST orgs/{org}/teams
getTeam(org, team) GET orgs/{org}/teams/{team}
updateTeam(org, team, { name?, join?, parent?, metadata? }) POST orgs/{org}/teams/{team}
deleteTeam(org, team) DELETE orgs/{org}/teams/{team}
setTeamMember(org, team, account, passkey, { role?, lead?, capabilities? }) POST orgs/{org}/teams/{team}/members
removeTeamMember(org, team, account, passkey) DELETE orgs/{org}/teams/{team}/members/{account}
invite(org, role, passkey, teams?, { ttlSecs?, uses? }) → token POST orgs/{org}/invites
redeemInvite(token) (no approval) POST invites/{token}/redeem
setRecoveryKey(org, publicKey | null, passkey) (the owner) POST orgs/{org}/recovery
startRecovery(org, seed) → { startedAt, readyAt } (signs locally) POST orgs/{org}/recovery/start
cancelRecovery(org, passkey) (the owner) POST orgs/{org}/recovery/cancel
finishRecovery(org, passkey) → { org, previousOwners } POST orgs/{org}/recovery/finish
upgradeOrg(org, { package, version, team?, cascade?, migrate? }, passkey) → UpgradeRun POST orgs/{org}/upgrade
upgradeRun(org, run) → UpgradeRun GET orgs/{org}/upgrades/{run}
retryUpgradeRun(org, run, passkey) → UpgradeRun POST orgs/{org}/upgrades/{run}/retry
approveRelease(org | null, version, passkey, approved?) POST orgs/{org}/releases or auth/releases
publisherBinding(signerId) → { signerId, cluster, generation, bound } GET publishers/{signerId}
bindPublisher(org, { signerId, publicKey, signature }, passkey) POST orgs/{org}/publishers
upgradePublished(org, package, { version, migrate? }, passkey) → UpgradeRun POST orgs/{org}/packages/{package}/upgrade
approveOutbound(org | null, package, passkey, { prefixes?, replace? }) POST orgs/{org}/outbound or auth/outbound

The approval routes above refuse a session opened on an app’s frontend (403). An app sends its user to the node’s consent page instead (guide).

Method Route
appsReport(org | null, { after?, limit? }) → AppsReport GET orgs/{org}/apps or auth/apps
appsReportAll(org | null) → AppsReport (every page) the same, following next
releaseRules() → { governance, deadlines } GET releases
pendingApprovals(org?) → PendingApprovals GET auth/apps (or orgs/{org}/apps), GET releases
requestConsent(request) → { requestId, url, expiresAt, code? }; show code to the user before sending them to url, and they should see the same code on the page POST consent/requests
openConsentPage(request, navigate? | { navigate?, confirm? }): checks code against confirmationCode(requestId) (else CalimeroError CONSENT_CODE_MISMATCH), runs confirm(started) so the app can show code, and navigates unless it answers false POST consent/requests, then navigates to url
consentResult(requestId) → { requestId, kind, status, decidedAt } GET consent/{id}/result
waitForConsent(requestId, { intervalMs?, signal?, expiresAt? }) polls GET consent/{id}/result

With the operator token (useApiKey(token)):

Method Route
opsApps({ after?, limit? }) → OpsAppsReport GET ops/apps
opsOrgs({ after?, limit? }) → OpsOrgsReport GET ops/orgs
opsServedOrigins() → { fresh, origins: [{ origin, host, label, addedAt }] } GET ops/served-origins
setReleaseDeadline(version, after) POST ops/releases/{version}/deadline
liftReleaseDeadline(version) DELETE ops/releases/{version}/deadline

get, post, put and delete(path, body?) send any request through the sealed channel with the client’s credential and map a refusal to a typed error. postConfirmed(path, body, passkey) and deleteConfirmed(path, passkey, body?) answer a 412 CONFIRM_REQUIRED.

  • The rest of /ops/*, the operator’s API: use core’s meroctl.
  • /apps/{app}/attestation and receipt verification (core’s Client::verify_app_cluster, which checks every replica’s receipt key and the cluster’s governance): not yet in this client.
  • /internal/*: node to node only.

fetchAttestedKey, claimedRelease, MOCK_QUOTE_HEADER, browserPasskey, b64url, fromB64url, errorFrom, ORG_CAPABILITIES, orgCapabilities, DEFAULT_ORG_CAPABILITIES; for approvals releaseConsent, withdrawConsent, upgradeConsent, consentReturn, confirmationCode, pendingApprovalsFrom, stalledBy, publisherBindingMessage, publisherBindingBytes, releaseNotesUrl, verifyReleaseUrl; the sealed transport (createSealedFetch, transportKeyBinding, SealedTransportError, StaleTransportKeyError); quote and release verification (createQuoteVerifier, trustedMeasurementsFromReleases, createSignedReleaseVerifier, trustSignedRelease, verifySignedNodeRelease, fetchNodeRelease, fetchGithubNodeRelease, cloudNodeReleaseUrl); and the private agent gate (attestAgent, provisionAgent, releaseAgent, …).