Skip to content

Admin API

mero.admin (AdminApi) is the node’s /admin-api REST surface — the full set of methods across applications, contexts, namespaces, groups, blobs, aliases, capabilities, metadata, upgrades, and TEE. It’s a Sendable struct over the shared HttpClient; every method is async throws.

Method Endpoint
healthCheck() -> HealthStatus GET /health
readinessCheck() -> HealthStatus GET /ready
isAuthed() -> AdminAuthStatus GET /is-authed
Method Endpoint
installApplication(_ request: InstallApplicationRequest) -> InstallApplicationResponseData POST /install-application
installDevApplication(_ request: InstallDevApplicationRequest) -> InstallApplicationResponseData POST /install-dev-application
installFromRegistry(packageName:version:) -> InstallApplicationResponseData POST /install-application by coordinates
getRegistryVersions(registryUrl:packageName:) -> [String] registry bundles (newest first)
uninstallApplication(_ appId: String) -> UninstallApplicationResponseData DELETE /applications/{appId}
listApplications() -> ListApplicationsResponseData GET /applications
getApplication(_ appId: String) -> GetApplicationResponseData GET /applications/{appId}
listApplicationVersions(_ applicationId: String) -> [ApplicationVersionEntry] GET /applications/{applicationId}/versions

A top-level helper compareSemver(_ a: String, _ b: String) -> Int sorts version strings ascending.

Method Endpoint
listPackages() -> ListPackagesResponseData GET /packages
listPackageVersions(_ packageName: String) -> ListVersionsResponseData GET /packages/{packageName}/versions
getLatestPackageVersion(_ packageName: String) -> GetLatestVersionResponseData GET /packages/{packageName}/latest
Method Endpoint
createContext(_ request: CreateContextRequest) -> CreateContextResponseData POST /contexts
deleteContext(_ contextId: String, request: DeleteContextRequest? = nil) -> DeleteContextResponseData DELETE /contexts/{contextId}
getContexts() -> GetContextsResponseData GET /contexts
getContext(_ contextId: String) -> Context GET /contexts/{contextId}
getContextsForApplication(_ applicationId: String) -> GetContextsResponseData GET /contexts/for-application/{applicationId}
getContextGroup(_ contextId: String) -> String? GET /contexts/{contextId}/group
getContextStorage(_ contextId: String) -> ContextStorageResponseData GET /contexts/{contextId}/storage
getContextsWithExecutorsForApplication(_ applicationId: String) -> [ContextWithExecutors] GET /contexts/with-executors/for-application/{applicationId}
joinContext(_ contextId: String) -> JoinContextResponseData POST /contexts/{contextId}/join
leaveContext(_ contextId: String, request: [String: JSONValue]? = nil) POST /contexts/{contextId}/leave
updateContextApplication(_ contextId: String, request: UpdateContextApplicationRequest) POST /contexts/{contextId}/application
syncContext(_ contextId: String? = nil) POST /contexts/sync/{contextId?}
resyncContext(_ contextId: String, request: ResyncContextRequest = .init()) -> ResyncContextResponseData POST /contexts/{contextId}/resync
performIntent(_ contextId: String, request: PerformIntentRequest) -> PerformIntentResponseData POST /contexts/{contextId}/intents
Method Endpoint
generateContextIdentity() -> GenerateContextIdentityResponseData POST /identity/context
getContextIdentities(_ contextId: String) -> GetContextIdentitiesResponseData GET /contexts/{contextId}/identities
getContextIdentitiesOwned(_ contextId: String) -> GetContextIdentitiesResponseData GET /contexts/{contextId}/identities-owned

⚠️ identities-owned answers a different question depending on who asks: the node’s own signing identities for a node-owner session, the calling account’s certified devices for a delegated one. core 0.11.0-rc.41 added identitiesOf (members / node / caller) so the response says which reading it is, instead of leaving a caller to infer it from its own token. It is optional — a node predating the field said nothing, and nil is the honest answer for one. A variant this SDK does not recognise also decodes as nil rather than failing the whole response: the identities are still the identities.

core 0.11.0-rc.21 gave a node one identity, one signing key and one root, and rc.23 deleted the route that asked a namespace who it was. rc.28 then moved device pairing off the namespace as well: a device pairs to an account once and is linked into namespaces afterwards. Revocation stayed per-namespace, because that is where the group key gets rotated.

⚠️ Since rc.27 removed base58, an account id and a device key are both 64 hex characters. getNodeIdentity() is the only thing that maps one to the other, and it matters: every authorization subject is an account, while group membership listings report devices. A device key handed to a role grant succeeds and authorizes nobody.

rc.32 added NodeIdentity.holdsAccountRoot (core#3774): whether this node holds the root key of the account it speaks for, and so whether it can certify another device into that account. It is not “does this node have a root” — a paired node has one of its own and still answers false, because the account it speaks for is rooted elsewhere. The field is defaulted, so a response from an older node decodes as false rather than failing.

rc.41 added NodeIdentity.revokedFrom: which account withdrew this node’s device, and which device it was. Absent on a node no revocation has reached — skipped rather than sent as null — so it is worth checking before reporting a node as merely unauthorized. A device its account revoked is not a login problem.

rc.41 also gave a device a name. AccountDeviceEntry.label is the replicated name the account gave it (every device of the account reads the same one), and labelDevice sets it. rescopeDevice is the counterpart to relinkDevice, which is add-only: it REPLACES the scope, so an application can be taken away again. DeviceScope is tagged — "all" or {"only": [...]} — rather than a list whose emptiness means everything, so the slip a caller can make is never silently the widest ask; an only naming nothing is a 400. Read descoped[].keyRotated before calling a narrowing done: false means the device stopped writing there but still holds the key it had, until an admin rotates.

Method Endpoint
getNodeIdentity() -> NodeIdentity GET /identity
listAccountDevices() -> [AccountDeviceEntry] GET /account/devices
listAccountApplications() -> [AccountApplicationEntry] GET /account/applications
accountPairInit(_ request: AccountPairInitRequest) -> PairDeviceInitResponseData POST /account/pair-init
accountPairComplete(_ request: AccountPairCompleteRequest) -> PairDeviceCompleteResponseData POST /account/pair-complete
relinkDevice(_ deviceId: String, request: RelinkDeviceRequest = .init()) -> RelinkDeviceResponseData POST /account/devices/{deviceId}/relink
rescopeDevice(_ deviceId: String, request: RescopeDeviceRequest) -> RescopeDeviceResponseData PUT /account/devices/{deviceId}/scope
labelDevice(_ deviceId: String, request: LabelDeviceRequest) -> LabelDeviceResponseData PUT /account/devices/{deviceId}/label
revokeDevice(_ namespaceId: String, request: RevokeDeviceRequest) -> RevokeDeviceResponseData POST /namespaces/{namespaceId}/account/revoke

⚠️ Name the context. core 0.11.0-rc.39 removed blob discovery from the DHT, so context_id is now the only way to find a blob a peer holds. Without it getBlob and getBlobInfo see this node’s local store and nothing else, and answer 404 for a blob that plainly exists on the node next to it. The parameter is optional in the type and effectively required in practice.

Discovery is a probe sweep with a 30s deadline on the node’s side, so these calls raise their own timeout when a context is named — a client timeout under that aborts the sweep and reads as “no such blob” rather than as a timeout.

rc.41 added X-Blob-Source to the HEAD, surfaced as GetBlobInfoResponseData.source. local means this node holds the blob and every field is its own; peer means only a context peer does, so hash and mimeType are absent — both are derived from bytes this node does not have, and a HEAD never transfers the blob to find out — and size is that peer’s word, verified by nobody. size is also optional: a peer that reported none omits Content-Length, and “exists, size unknown” is true where 0 would be a lie about a blob that exists.

Method Endpoint
uploadBlob(_ request: UploadBlobRequest) -> BlobInfo PUT /blobs?hash=&context_id= (octet-stream)
listBlobs() -> ListBlobsResponseData GET /blobs
getBlob(_ blobId: String, contextId: String? = nil) -> Data GET /blobs/{blobId}?context_id= (raw bytes)
getBlobInfo(_ blobId: String, contextId: String? = nil) -> GetBlobInfoResponseData HEAD /blobs/{blobId}?context_id=
deleteBlob(_ blobId: String) -> DeleteBlobResponseData DELETE /blobs/{blobId}
Method Endpoint
createContextAlias(_ request: CreateContextAliasRequest) -> CreateAliasResponseData POST /alias/create/context
createApplicationAlias(_ request: CreateApplicationAliasRequest) -> CreateAliasResponseData POST /alias/create/application
lookupContextAlias(_ name: String) -> LookupAliasResponseData POST /alias/lookup/context/{name}
lookupApplicationAlias(_ name: String) -> LookupAliasResponseData POST /alias/lookup/application/{name}
deleteContextAlias(_ name: String) -> DeleteAliasResponseData POST /alias/delete/context/{name}
deleteApplicationAlias(_ name: String) -> DeleteAliasResponseData POST /alias/delete/application/{name}
listContextAliases() -> ListAliasesResponseData GET /alias/list/context
listApplicationAliases() -> ListAliasesResponseData GET /alias/list/application
listDeviceAliases() -> ListAliasesResponseData GET /alias/list/device
createDeviceAlias(_ request: CreateDeviceAliasRequest) -> CreateAliasResponseData POST /alias/create/device
lookupDeviceAlias(_ name: String) -> LookupAliasResponseData POST /alias/lookup/device/{name}
deleteDeviceAlias(_ name: String) -> DeleteAliasResponseData POST /alias/delete/device/{name}

An alias scopes to a device, and a listing comes back as a map keyed by alias name (ListAliasesResponseData.entries, with .aliases for a name-sorted array). The four …ContextIdentityAlias methods this table used to list called /alias/*/identity/…, which no released node serves — a live 0.11.0-rc.32 answers all four with 404.

Method Endpoint
listNamespaces() -> [Namespace] GET /namespaces
getNamespace(_ namespaceId: String) -> Namespace GET /namespaces/{namespaceId}
listNamespacesForApplication(_ applicationId: String) -> [Namespace] GET /namespaces/for-application/{applicationId}
createNamespace(_ request: CreateNamespaceRequest) -> CreateNamespaceResponseData POST /namespaces
deleteNamespace(_ namespaceId: String, request: DeleteNamespaceRequest? = nil) -> DeleteNamespaceResponseData DELETE /namespaces/{namespaceId}
createNamespaceInvitation(_ namespaceId: String, request: … = nil) -> CreateNamespaceInvitationResult POST /namespaces/{namespaceId}/invite
joinNamespace(_ namespaceId: String, request: JoinNamespaceRequest) -> JoinNamespaceResponseData POST /namespaces/{namespaceId}/join
leaveNamespace(_ namespaceId: String, request: [String: JSONValue]? = nil) POST /namespaces/{namespaceId}/leave
createGroupInNamespace(_ namespaceId: String, request: … = nil) -> CreateGroupInNamespaceResponseData POST /namespaces/{namespaceId}/groups
listNamespaceGroups(_ namespaceId: String) -> [SubgroupEntry] GET /namespaces/{namespaceId}/groups
admitJoin(_ namespaceId: String, request: AdmitJoinRequest) -> AdmitJoinResponseData POST /namespaces/{namespaceId}/admit
Method Endpoint
createGroup(_ request: [String: JSONValue]) -> CreateGroupResponseData POST /groups
getGroupInfo(_ groupId: String) -> GroupInfo GET /groups/{groupId}
deleteGroup(_ groupId: String, request: DeleteGroupRequest? = nil) -> DeleteGroupResponseData DELETE /groups/{groupId}
listGroupMembers(_ groupId: String) -> ListGroupMembersResponseData GET /groups/{groupId}/members
listMemberDevices(_ groupId: String, offset: Int? = nil, limit: Int? = nil) -> [MemberDevicesEntry] GET /groups/{groupId}/member-devices
listGroupContexts(_ groupId: String) -> [GroupContextEntry] GET /groups/{groupId}/contexts
addGroupMembers(_ groupId: String, request: AddGroupMembersRequest) POST /groups/{groupId}/members
removeGroupMembers(_ groupId: String, request: RemoveGroupMembersRequest) POST /groups/{groupId}/members/remove
updateMemberRole(_ groupId: String, identity:, request: UpdateMemberRoleRequest) PUT /groups/{groupId}/members/{identity}/role
setMemberAutoFollow(_ groupId: String, identity:, request: [String: JSONValue]) PUT /groups/{groupId}/members/{identity}/auto-follow
leaveGroup(_ groupId: String, request: [String: JSONValue]? = nil) POST /groups/{groupId}/leave
Method Endpoint
getMemberCapabilities(_ groupId: String, identity:) -> MemberCapabilities GET /groups/{groupId}/members/{identity}/capabilities
setMemberCapabilities(_ groupId: String, identity:, request:) PUT /groups/{groupId}/members/{identity}/capabilities
getDefaultCapabilities(_ groupId: String) -> Int via getGroupInfo
setDefaultCapabilities(_ groupId: String, request:) PUT /groups/{groupId}/settings/default-capabilities
getSubgroupVisibility(_ groupId: String) -> String via getGroupInfo
setSubgroupVisibility(_ groupId: String, request:) PUT /groups/{groupId}/settings/subgroup-visibility
getTeeAdmissionPolicy(_ groupId: String) -> GetTeeAdmissionPolicyResponseData GET /groups/{groupId}/settings/tee-admission-policy
setTeeAdmissionPolicy(_ groupId: String, request:) PUT /groups/{groupId}/settings/tee-admission-policy
Method Endpoint
setGroupMetadata(_ groupId: String, request:) PUT /groups/{groupId}/metadata
getGroupMetadata(_ groupId: String) -> MetadataRecord? GET /groups/{groupId}/metadata
setMemberMetadata(_ groupId: String, identity:, request:) PUT /groups/{groupId}/members/{identity}/metadata
getMemberMetadata(_ groupId: String, identity:) -> MetadataRecord? GET /groups/{groupId}/members/{identity}/metadata
setContextMetadata(_ groupId: String, contextId:, request:) PUT /groups/{groupId}/contexts/{contextId}/metadata
getContextMetadata(_ groupId: String, contextId:) -> MetadataRecord? GET /groups/{groupId}/contexts/{contextId}/metadata
Method Endpoint
createGroupInvitation(_ groupId: String, request: … = nil) -> CreateGroupInvitationResult POST /groups/{groupId}/invite
joinGroup(_ request: JoinGroupRequest) -> JoinGroupResponseData POST /groups/join
joinSubgroupInheritance(_ groupId: String) -> JoinSubgroupInheritanceResponseData POST /groups/{groupId}/join-via-inheritance
listSubgroups(_ groupId: String) -> [SubgroupEntry] GET /groups/{groupId}/subgroups
reparentGroup(_ childGroupId: String, request: ReparentGroupRequest) -> ReparentGroupResponseData POST /groups/{childGroupId}/reparent
detachContextFromGroup(_ groupId: String, contextId:, request: … = nil) POST /groups/{groupId}/contexts/{contextId}/remove
issueOwnershipProof(_ groupId: String, request: … = nil) -> JSONValue POST /groups/{groupId}/issue-ownership-proof
issueNamespaceOwnershipProof(_ groupId: String, request: … = nil) -> JSONValue POST /groups/{groupId}/issue-namespace-ownership-proof
Method Endpoint
syncGroup(_ groupId: String, request: … = nil) -> SyncGroupResponseData POST /groups/{groupId}/sync
upgradeGroup(_ groupId: String, request: UpgradeGroupRequest) -> UpgradeGroupResponseData POST /groups/{groupId}/upgrade
getGroupUpgradeStatus(_ groupId: String) -> GroupUpgradeStatus? GET /groups/{groupId}/upgrade/status
retryGroupUpgrade(_ groupId: String, request: … = nil) -> RetryGroupUpgradeResponseData POST /groups/{groupId}/upgrade/retry
getMigrationStatus(_ namespaceId: String) -> MigrationStatus GET /groups/{namespaceId}/migration-status
getCascadeStatus(_ namespaceId: String) -> [CascadeStatusEntry] GET /groups/{namespaceId}/cascade-status
abortMigration(_ namespaceId: String, request: … = nil) -> JSONValue POST /groups/{namespaceId}/migration/abort
Method Endpoint
getTeeInfo() -> TeeInfoResponseData GET /tee/info
teeAttest(_ request: TeeAttestRequest) -> TeeAttestResponseData POST /tee/attest
teeVerifyQuote(_ request: TeeVerifyQuoteRequest) -> TeeVerifyQuoteResponseData POST /tee/verify-quote
Method Endpoint
getPeersCount() -> PeersCountResponseData GET /peers
getNetworkStatus() -> JSONValue GET /network/status
getUsage() -> JSONValue GET /usage
getCertificate() -> String GET /certificate (PEM)
struct Context: Codable, Sendable {
let id: String
let applicationId: String
let serviceName: String?
let contextStateHash: String
let dagHeads: [[Int]]
let applicationVersion: String?
}
struct Namespace: Codable, Sendable {
let namespaceId, appKey, targetApplicationId: String
let createdAt: Int
let name: String?
let memberCount, contextCount, subgroupCount: Int
}
struct GroupInfo: Codable, Sendable { // = GroupInfoResponseData
let groupId, appKey, targetApplicationId: String
let memberCount, contextCount, defaultCapabilities: Int
let subgroupVisibility: String
let activeUpgrade: GroupUpgradeStatus?
let metadata: MetadataRecord?
}
struct MetadataRecord: Codable, Sendable {
let name: String?
let data: [String: String]
let updatedAt: Int
let updatedBy: String
}
struct BlobInfo: Codable, Sendable { let blobId: String; let size: Int }
enum UpgradePolicy: String, Codable, Sendable { case automatic = "Automatic"; case lazyOnAccess = "LazyOnAccess" }

See AdminTypes.swift for the complete set.