@calimero-network/mero-react

中文文档
11.0.3发布于 2026/11/02MITTypeScriptESM + CJS构建来源已验证近 30 天下载 7422

React bindings for the Calimero 1.0 client (mero-js CalimeroClient)

原始使用文档
文档目录17 节
  1. Installation
  2. Quick start
  3. <MeroProvider>
  4. useMero()
  5. Hooks
  6. App: calls, views, events
  7. Apps
  8. Organizations and teams
  9. Invites
  10. Consent page
  11. Blobs, presence, account
  12. Errors
  13. Components
  14. Theming
  15. Storage
  16. Migrating from 10.x
  17. License

@calimero-network/mero-react

React bindings for Calimero 1.0, on the mero-js 28 client (CalimeroClient).

A 1.0 node runs in an attested TDX cluster. The client verifies the node's attestation, seals every request to the node's attested key, and signs people in with passkeys. mero-react puts one client in context and builds the hooks on top of it:

  • passkey sign-in and sign-up;
  • sessions that refresh by themselves;
  • calls, views and live events on an app instance;
  • organizations, teams and invites;
  • the node's consent page for approvals;
  • blobs and presence;
  • typed errors your UI can switch on.

Upgrading from 10.x? 11.0 is a rewrite for core 1.0, which removed /admin-api, /jsonrpc, /ws, /sse, mero-auth and /auth/token. Read Migrating from 10.x below.

Installation

pnpm add @calimero-network/mero-react

@calimero-network/mero-js (^28.0) comes with it, and every mero-js export is re-exported, so apps need only this import. React 18 or 19 is a peer dependency.

Quick start

import {
  MeroProvider,
  ConnectButton,
  useMero,
  useView,
  useExecute,
  useSubscription,
} from '@calimero-network/mero-react';

export function Root() {
  return (
    // A frontend the node serves at {label}.{apps-domain}:
    <MeroProvider servedByNode minReleaseVersion="2.3.109" appId={APP_ID}>
      <App />
    </MeroProvider>
  );
}

function App() {
  const { isAuthenticated } = useMero();
  const items = useView<string[]>('list_items');
  const { execute, failure } = useExecute();
  useSubscription(undefined, (event) => {
    if (event.kind === 'ItemAdded') void items.refetch();
  });

  if (!isAuthenticated) return <ConnectButton appName="Notes" />;
  return (
    <>
      <ul>{items.data?.map((item) => <li key={item}>{item}</li>)}</ul>
      <button onClick={() => execute('add_item', { text: 'hello' })}>Add</button>
      {failure && <p role="alert">{failure.message}</p>}
    </>
  );
}

A frontend hosted elsewhere reaches a Calimero Cloud cluster by name, failing over between its nodes:

<MeroProvider cluster="eu-shared-1" minReleaseVersion="2.3.109" appId={APP_ID}>

For local development against merod local (three --dev nodes with mock attestation on 127.0.0.1:2501-2503):

<MeroProvider
  nodeUrl="http://127.0.0.1:2501"
  nodes={['http://127.0.0.1:2502', 'http://127.0.0.1:2503']}
  allowMock
>

allowMock accepts mock attestation quotes. Never set it from anything a visitor controls, such as a URL parameter or the environment of a deployed build.

<MeroProvider>

PropTypeWhat it does
minReleaseVersionstringThe oldest mero-tee release to trust, for example 2.3.109. This is shorthand for policy: { release: { minReleaseVersion } }.
policyCloudPolicyThe full attestation policy (profile, release source, verifyQuote, appHash). Takes precedence over minReleaseVersion.
allowMockbooleanAccept mock quotes (merod local, --dev clusters). The provider throws if minReleaseVersion, policy and allowMock are all missing.
servedByNodebooleanThe node serves this page. The node is then window.location.origin, and a passkey ceremony here opens a session bound to this app's package. ConnectButton skips the node field.
nodeUrlstringThe node to use when the page is not node-served. Without it (and without cluster), the provider uses the node or cluster kept from the last visit, or waits for connectToNode / connectToCluster.
nodesstring[]Other nodes of the cluster, with nodeUrl. On 421 WRONG_NODE, or when a node stays unavailable, the client moves to the next one.
clusterstringA Calimero Cloud cluster by name (eu-shared-1), in place of nodeUrl. The client reads the cluster's nodes from the public node directory (mero-js CalimeroClient.connect) and fails over between them. The directory only picks which nodes to try: each is attested under your policy first. nodeUrl wins when both are given.
zone, directorystringWith cluster: the zone whose nodes are tried first, and another node directory than mero-js DEFAULT_DIRECTORY.
appIdstringThe app instance that hooks default to.
sessionStoreSessionStoreWhere the session is kept. The default is localStorageSessionStore; memorySessionStore() keeps it for the tab's lifetime only.
passkeyPasskeyProviderThe WebAuthn prompt. The default is mero-js browserPasskey().
fetch, retryPassed through to CalimeroClient.
clientCalimeroClientUse this client instead of building one, for tests or custom setups.
onError(failure) => voidCalled with every failure that a hook or a sign-in reports.

useMero()

const {
  client,            // CalimeroClient | null (null until a node is known, or a cluster's nodes are)
  nodeUrl,           // the node in use
  cluster,           // the cluster connected to by name, or null
  isConnecting,      // the node directory is being read
  connectToNode,     // (url) => void
  connectToCluster,  // (name) => void
  disconnect,
  session, accountId, isAuthenticated, isLoading,
  signIn,            // () => Promise<CalimeroSession | null>: an existing passkey
  signUp,            // (name) => Promise<CalimeroSession | null>: a new account and passkey
  signOut, signOutEverywhere,
  appId, setAppId,
  passkey, servedByNode, allowMock,
  failure,           // the last sign-in, connection or session failure, described
  reportError,
} = useMero();

Sessions. A 1.0 session is a session token (v2.…) that lasts 15 minutes, plus a refresh token (rt_…) that lasts 30 days and can be used once.

  • The client refreshes by itself on a 401. One refresh is shared by every request waiting on it, because reusing a refresh token revokes the session. When the node that issued the session is gone, the refresh goes to another node of the cluster (mero-js#318).
  • The provider saves every new session to sessionStore, so a reload resumes the session.
  • When a refresh is refused, hooks get LoginRequired, and the provider ends the session and sets failure.kind = 'login'.
  • mero is still on the value as a deprecated alias of client.

Hooks

Every read hook returns { data, loading, error, failure, refetch }. Every action hook returns its action plus { loading, error, failure }.

  • error is the error exactly as the client threw it, so instanceof AppError still works.
  • failure is describeError(error); see Errors.
  • An action resolves null when it fails. Pass { throwOnError: true } to make it throw instead.

App: calls, views, events

HookReturns
useApp(appId?){ appId, app, call, view, loading, error, failure, refetch }. call and view are bound to the app and throw.
useExecute(appId?, opts?){ execute, call, loading, error, failure }. execute(method, args?, { idempotencyKey?, confirm? }) resolves the method's result; call resolves the whole CallResult (index, events). confirm: true answers 412 CONFIRM_REQUIRED with the passkey (callConfirmed).
useView<T>(method, args?, { appId?, stale?, minIndex?, enabled? }){ data: T, index, … }. It reads again when the method or the arguments change.
useSubscription(appIds?, callback, opts?)Calls callback with each AppEvent ({ appId, id, index, seq, kind, data }).
useAppAbi(appId?)The app's ABI.

appId defaults to the provider's appId.

How useSubscription behaves:

  • Each app gets one SSE stream with an AbortController. Unmounting, or changing the app ids, aborts it.
  • A stream that ends or fails reopens with backoff from the last event it saw. Events at or before that one are skipped, so nothing is delivered twice.
  • from defaults to 'live'. Pass a log index to replay from it (0 replays the whole history).
  • onOpen is called each time a stream opens. Refetch views there to cover a gap.

Apps

HookWas (10.x)
useApps({ package?, bundle? })useContexts
useCreateApp().create({ name, bundle, org?, access?, confirmMethods?, initArgs? })useCreateContext
useDeleteApp().deleteApp(appId)useDeleteContext
useSetAppMetadata().setMetadata(appId, metadata)useSetContextMetadata
useUpgradeApp(appId?).upgrade({ version, … })useUpgradeGroup. It asks for the passkey.
usePackages()none (new)

Organizations and teams

HookWas (10.x)
useOrg(orgId)useNamespace
useCreateOrg().create(name)useCreateNamespace
useDeleteOrg().deleteOrg(orgId)useDeleteNamespace. It asks for the passkey.
useSetMember().setMember(org, account, role)useUpdateMemberRole, and useAddGroupMembers at org level. It asks for the passkey.
useRemoveMember().removeMember(org, account)none (new). It asks for the passkey.
useSetOrgMetadata(), useSetMemberMetadata()useSetGroupMetadata, useSetMemberMetadata
useSetCapabilities().setCapabilities(org, bits, { account?, team? })useSetDefaultCapabilities. It asks for the passkey.
useTeam(org, team)useGroupInfo
useCreateTeam().create({ orgId, name, join?, parent? })useCreateGroupInNamespace
useUpdateTeam().update(org, team, { name?, join?, parent?, metadata? })useSetGroupMetadata, useReparentGroup, useSetSubgroupVisibility
useDeleteTeam(), useSetTeamMember(), useRemoveTeamMember()useDeleteGroup, useAddGroupMembers, useRemoveGroupMembers. Setting and removing a team member ask for the passkey.

Passkey step-ups. Since mero-js 28 the node approves membership changes, capabilities, invites, passkeys and API keys with the caller's passkey: it answers 412 CONFIRM_REQUIRED and the client answers that with the provider's passkey (browserPasskey() by default), so the person sees a passkey prompt. A dismissed prompt fails the action with failure.kind = 'cancelled'.

Invites

HookNotes
useCreateInvite().create({ orgId, role?, teams?, ttlSecs?, uses?, linkBase? })Resolves { token, link }. It asks for the passkey. The link carries the token in the fragment (#invite=…), so it stays out of server logs.
useRedeemInvite().redeem(token)POST /invites/{token}/redeem. No passkey needed.
useInviteRedemption({ autoRedeem? })The whole link flow. It reads the token from the address bar and removes it there, keeps it through sign-in, and redeems it once the person is signed in. status is idle, waiting-for-sign-in, redeeming, redeemed or failed.

inviteLink(token, base?) and readInviteToken(href) are exported too.

Approvals happen only on the page the node serves on its own origin, never inside an app. A session opened on an app's frontend cannot approve anything.

const consent = useConsent();
await consent.open(upgradeConsent(appId, '1.2.0', window.location.href), {
  // Show the confirmation code before leaving; the node's page shows the same one.
  confirm: (started) => window.confirm(`The approval page will show the code ${started.code}. Continue?`),
}); // then navigates to the node's page
// … back on returnTo:
if (consent.returned) await consent.check(consent.returned.requestId);    // confirm the hint
// or, when the page was opened in another window:
await consent.wait(started.requestId);

useConsent() returns { request, open, check, wait, cancel, started, code, result, returned, … }. code is the confirmation code (XXX-XXX) the consent page shows for the request under way: show it next to the link or in open's confirm, so the person can check they landed on the page their request opened. open throws CONSENT_CODE_MISMATCH when the node's code does not match the request (mero-js confirmationCode). Build requests with mero-js releaseConsent, withdrawConsent and upgradeConsent.

Blobs, presence, account

HookNotes
useUploadBlob({ appId? }).upload(bytes | Blob | File)Resolves { blobId, id, size }. Blobs now belong to an app instance.
useBlobInfo(blobId, { appId? }), useBlobs(appId?)
useBlobUrl(blobId, { appId?, mimeType? }){ url } is an object URL. The bytes come over the sealed channel, and the URL is revoked on unmount.
useDeleteBlob({ appId? }).deleteBlob(blobId)
useEphemeral<T>(appId, { codec?, initial?, throttleMs?, includeSelf? })Presence. Returns { peers: Map<account, T>, entries, setPresence, leave, error }. It sends a heartbeat every 5 s and retracts on unmount.
useAccount()GET /auth/me. Was useNodeIdentity.
usePasskeys(){ add, remove, label } for the account's passkeys. Adding and removing are approved with a passkey the account already has; add({ passkey?, approver? }) takes other providers for the new passkey and the approving one.
useApiKeys(){ keys, create, revoke, refetch, … } for API keys (agents, servers). create({ label?, apps?, methods?, readOnly?, allowConfirm?, ttlSecs? }) asks for the passkey, needs a passkey session, and resolves { keyId, key }: the key is shown once.

Errors

mero-js maps every refusal to a typed error (errorFrom, mero-js#312). mero-react keeps that error in error and adds failure = describeError(error):

type Failure = {
  kind: FailureKind;          // see the table
  message: string;            // a sentence you can show as it is
  retryable: boolean;
  status?: number; code?: string;
  appCode?: string; fields?: Record<string, unknown>;   // kind 'app'
  committed?: false | 'unknown';                         // kind 'unavailable'
  release?: string;                                      // kind 'release-deadline'
  error: unknown;
};
kindFrom
appAppError (422 APP_ERROR): the app's own error, with appCode and named fields
trapTrapError (422 TRAP)
loginLoginRequired: the refresh was refused. The provider ends the session.
confirm / cancelledConfirmRequired (412) / ConfirmCancelled (the person dismissed the passkey prompt)
wrong-nodeWrongNode / 421 NOT_LEADER after failover ran out
unavailableUnavailableError (503 or no answer after retries). Check committed.
not-found, forbidden, too-large, idempotency, rate-limited404, 403, 413, 409 IDEMPOTENCY_MISMATCH, 429
release-deadline, consent-stale, operator-signature409 RELEASE_DEADLINE, 409 CONSENT_STALE, 428 OPERATOR_SIGNATURE_REQUIRED
attestationThe sealed channel or the attestation check failed
directoryDirectoryError: the node directory does not know the cluster (code: 'not-found'), lists no usable node (empty) or could not be read

Components

  • <ConnectButton appName? allowSignUp? initialTab? theme? logoOnly? label? />. Signed out, it shows "Sign in" and opens LoginModal. Signed in, it shows the account, with a menu for Sign out and Sign out everywhere.
  • <LoginModal isOpen onClose onSignedIn? initialTab? allowSignUp? appName? theme? />. Sign in with a passkey, or create an account, which asks for a name. Off a node-served page it asks first for the node URL or a cluster name.

Theming

Pass a partial MeroTheme (primary, primaryHover, primaryText, background, backgroundSecondary, backgroundTertiary, border, text, textSecondary, error, overlay, radius), or set the --mero-* CSS variables globally:

:root {
  --mero-accent: #ff4081;
  --mero-accent-hover: #e91e63;
}

The dialog's classes are mero-dialog-overlay, mero-dialog-panel, mero-dialog-title, mero-dialog-body, mero-dialog-field, mero-dialog-input, mero-dialog-button, mero-dialog-primary, mero-dialog-secondary and mero-dialog-link. The button's classes are mero-connect-button, mero-dropdown and mero-dropdown-item.

Storage

  • localStorageSessionStore and memorySessionStore(): session stores.
  • getNodeUrl / setNodeUrl / clearNodeUrl, getCluster / setCluster / clearCluster and getAppId / setAppId / clearAppId.
  • clearAllStorage() clears all of the above.
  • Keys: mero:session, mero:nodeUrl, mero:cluster, mero:appId. 10.x tokens are not read.

Migrating from 10.x

core 1.0 is a different node, and mero-js 28 is a different client, so 11.0 is a breaking release. Most app ports are mechanical:

  1. Provider.
    • Drop mode, packageName, packageVersion, registryUrl, cloudBaseUrl, timeoutMs, allowedNodeUrls and tokenStore.
    • Add minReleaseVersion (production) or allowMock (merod local).
    • Keep servedByNode. Pass appId if the app works on one instance.
  2. Client.
    • useMero().mero (a MeroJs or relay client) is now useMero().client (a CalimeroClient).
    • mero.rpc.execute({ contextId, method, argsJson, executorPublicKey }) is now client.call(appId, method, args), or client.view(…) for reads.
  3. Vocabulary.
    • context is now app instance (appId);
    • namespace is now organization;
    • group is now team;
    • executor / node identity is now the signed-in account.
  4. Auth.
    • There is no redirect, callback or access_token in the URL any more.
    • connectToNode(url) only picks the node. Signing in is signIn() or signUp(name) (passkeys), or <ConnectButton />.
  5. Errors. Switch on failure.kind, or instanceof AppError / ConfirmRequired / WrongNode, instead of HTTP status checks.
10.x11.0
MeroProvider mode={AppMode.X}MeroProvider minReleaseVersion=… | allowMock (no modes, no permission scopes)
useMero().merouseMero().client (mero is kept as a deprecated alias)
useMero().contextId / contextIdentity / applicationIduseMero().appId / accountId / none
useMero().isOnlinenone: the client retries and fails over by itself
connectToNode(url) + redirect loginconnectToNode(url) + signIn() / signUp(name)
connectWithAccount, isDelegated, can, adminnone (no relays, no delegated accounts)
useExecute(contextId, executorId).execute(m, p)useExecute(appId).execute(m, args, { confirm?, idempotencyKey? })
noneuseView(method, args), useApp(appId)
useSubscription(contextIds, cb) / { contextIds, groupIds }useSubscription(appIds, cb, { from?, onOpen? })
useContexts, useApplicationContexts, useNamespacesForApplicationuseApps({ package? })
useCreateContext, useCreatePrivateContextuseCreateApp (access for private apps)
useDeleteContext, useSetContextMetadatauseDeleteApp, useSetAppMetadata
useNamespace, useNamespaces, useCreateNamespace, useDeleteNamespaceuseOrg, useAccount (orgs), useCreateOrg, useDeleteOrg
useGroupInfo, useNamespaceGroups, useSubgroups, useContextGroupuseTeam (teams are listed in useOrg)
useCreateGroupInNamespace, useDeleteGroupuseCreateTeam, useDeleteTeam
useAddGroupMembers, useRemoveGroupMembers, useGroupMembersuseSetTeamMember, useRemoveTeamMember, useTeam
useUpdateMemberRole, useGroupCapabilities, useSetDefaultCapabilitiesuseSetMember, useOrg, useSetCapabilities
useSetGroupMetadata, useGroupMetadata, useMemberMetadatauseSetOrgMetadata / useUpdateTeam, useOrg / useTeam
useReparentGroup, useSetSubgroupVisibilityuseUpdateTeam({ parent, join })
useCreateNamespaceInvitation, useGroupInvitationsuseCreateInvite
useRedeemInvitation, useJoinNamespace, useJoinGroup, useJoinContextuseRedeemInvite
useInviteRedemption, useJoinInvitationuseInviteRedemption (no relay or sync step)
useUpgradeGroup, useInstallFromRegistryuseUpgradeApp, or useConsent + upgradeConsent from an app frontend
useGroupUpgradeStatus, useRetryGroupUpgrademero-js client.upgradeRun / retryUpgradeRun (org upgrade runs)
useUploadBlob, useBlobInfo, useBlobUrlSame names, now per app ({ appId })
useEphemeral(contextId, opts)useEphemeral(appId, opts): peers keyed by account
useNodeIdentityuseAccount
classifyError(err) / ClassifiedErrordescribeError(err) / Failure
getPermissionsForMode, AppMode, ConnectionTyperemoved (no permission scopes)
localStorageTokenStore, get/setContextId, get/setApplicationId, get/setContextIdentitylocalStorageSessionStore, get/setAppId

Removed with no replacement (v1 or desktop only):

  • admin-API hooks: useSyncGroup, useResyncContext, useJoinSubgroupInheritance, useDetachContextFromGroup, useSetTeeAdmissionPolicy, useContextDiscovery;
  • migration hooks and components: useMigrationStatus, useMigrationEvents, useMyAuthoredMigration, MigrationPendingBanner, MigrationAdminPanel;
  • version hooks: useAppVersion, useLatestVersion, useGroupAppVersion;
  • the delegated / relay / account-enrolment layer: useAccountEnrolment, useDelegatedBootstrap, ConnectButtonAccount, AccountSignInPanel, and the relay, joinWithNode and bootstrapFromInvitation re-exports;
  • invitation UI: InvitationGate, InvitationDialog, JoinSyncBanner, useJoinSync;
  • local-node discovery: discoverLocalNodes;
  • desktop (Tauri): isDesktopWindow.

License

MIT

版本与发布记录

@calimero-network/mero-react 共发布 88 个版本,首次发布于 2026/01/26;过去 12 个月发布了 88 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
11.0.3latest2026/11/026 天前
11.0.22026/11/026 天前
11.0.12026/11/026 天前
11.0.02026/11/026 天前
10.3.02026/10/2514 天前
10.2.12026/10/2118 天前

其他发布通道:beta → 1.0.0-beta.1。

关于 @calimero-network/mero-react 的常见问题

@calimero-network/mero-react 是什么?

React bindings for the Calimero 1.0 client (mero-js CalimeroClient)

如何安装 @calimero-network/mero-react?

在项目目录运行 npm install @calimero-network/mero-react,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.0.0 || ^19.0.0)、react-dom(^18.0.0 || ^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@calimero-network/mero-react 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@calimero-network/mero-react 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@calimero-network/mero-react 可以商用吗?

@calimero-network/mero-react 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@calimero-network/mero-react 的下载量有多少?

根据 npm 官方下载统计,@calimero-network/mero-react 在 2026/10/09 – 2026/11/07 这 30 天内被下载 7,422 次,日均约 247 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@calimero-network/mero-react 还在维护吗?

最新版本 11.0.3 发布于 2026/11/02(6 天前);至今共发布 88 个版本,其中过去 12 个月发布了 88 个;首次发布于 2026/01/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。