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
getIntentRelay(_ contextId: String) -> IntentRelayInfo GET /contexts/{contextId}/intents
queryContext(_ contextId: String, request: QueryContextRequest) -> QueryContextResponseData POST /contexts/{contextId}/query
postPresenceIntent(_ contextId: String, request: PresenceIntentRequest) POST /contexts/{contextId}/presence-intents
getWarrantNonce(_ contextId: String, authorDeviceKey: String) -> WarrantNonceState GET /contexts/{contextId}/warrant-nonce/{key}
getWarrantNonceAsAuthor(_ contextId: String, authorProof: String) -> WarrantNonceState POST /contexts/{contextId}/warrant-nonce

Account reads and delegated intents (core rc.83)

Section titled “Account reads and delegated intents (core rc.83)”

queryContext reads a context as the session’s account. It needs an account-authenticated (account_proof) session, membership of the owning group (403 otherwise), and a method the ABI marks read-only (409 otherwise). A method error is a 400 with type: "FunctionCallError", readable through MeroError.refusal (see Error model).

getIntentRelay answers what a warrant for this node must name: executorAccount, executorKey, releaseBytecodeId and releaseVersion (the last three are new in rc.83, and a warrant minted for rc.41 is refused). canAuthorOnBehalf: false is the default state of every context, not an error. The warrant, author proof and presence signature are minted by the caller; these methods only carry them.

The warrant-nonce routes are in mero-js but not served by core rc.83, which answers 404. Treat a 404 as “fall back to your own counter”. Nonces are UInt64 and decoded exactly.

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
linkAccountDevice(_ namespaceId: String, request: LinkAccountDeviceRequest) -> LinkAccountDeviceResponseData POST /namespaces/{namespaceId}/account/link-device
signWithAccountRoot(_ request: AccountSignWithRootRequest) -> AccountSignWithRootResponseData POST /account/sign-with-root
sealToAccount(_ groupId: String, account: String, request: SealToAccountRequest) -> SealedEnvelope POST /groups/{groupId}/accounts/{account}/seal

rc.83: the pairing statement is now 72 bytes (144 hex) with a timestamp, and goes stale after 300 s. The SDK relays it opaquely, so nothing changes for a caller beyond completing the pairing promptly. signWithAccountRoot returns a base64 signature; AccountSignWithRootRequest(domain:text:) hex-encodes a text challenge for you.

⚠️ 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
changeNamespaceAdmin(_ namespaceId: String, request: ChangeNamespaceAdminRequest) POST /namespaces/{namespaceId}/admin
listNamespaceGroups(_ namespaceId: String) -> [SubgroupEntry] GET /namespaces/{namespaceId}/groups
admitJoin(_ namespaceId: String, request: AdmitJoinRequest) -> AdmitJoinResponseData POST /namespaces/{namespaceId}/admit

⚠️ Subgroup visibility. core rc.83 changed what an absent visibility means on POST /namespaces/{id}/groups: it now creates an open subgroup (it used to be restricted). createGroupInNamespace therefore always sends one, and sends "open" (CreateGroupInNamespaceRequest.defaultVisibility) when you name none, matching core and mero-js. Pass visibility: "restricted" to keep a subgroup closed.

rc.83 also added Namespace.founding (the founder account and salt the id was derived from) and Namespace.heldOps (group ops this node holds unapplied for want of a key). CreateNamespaceResponseData.founding carries the same founding. A namespace outside an account session’s scope answers 404.

Method Endpoint
createGroup(_ request: CreateGroupRequest) -> CreateGroupResponseData POST /groups
createGroup(_ request: [String: JSONValue]) -> CreateGroupResponseData POST /groups
getGroupInfo(_ groupId: String) -> GroupInfo GET /groups/{groupId}
deleteGroup(_ groupId: String, request: DeleteGroupRequest? = nil) -> DeleteGroupResponseData DELETE /groups/{groupId}
ownerDeleteGroup(_ groupId: String, request: RootGuardedOpRequest = .init()) POST /groups/{groupId}/owner-delete
transferOwnership(_ groupId: String, request: TransferOwnershipRequest) POST /groups/{groupId}/transfer-ownership
listGroupMembers(_ groupId: String) -> ListGroupMembersResponseData GET /groups/{groupId}/members
listMemberDevices(_ groupId: String, offset: Int? = nil, limit: Int? = nil) -> [MemberDevicesEntry] GET /groups/{groupId}/member-devices
listGroupMemberDevices(_ groupId: String, offset: Int? = nil, limit: Int? = nil) -> [MemberDevicesEntry] same (the mero-js name)
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

⚠️ No groupId on create. core rc.83 derives every group id and refuses a POST /groups body that names one (deny_unknown_fields), so CreateGroupRequest.groupId was removed. Do not put one in the dictionary form either.

GroupInfo gained namespaceId and ownerOpCounter in rc.83. A member role may now be "RelayTee".

transferOwnership, ownerDeleteGroup, changeNamespaceAdmin, setTeeAdmissionPolicy and setTeeAuthoringPolicy / disableTeeAuthoringPolicy take an optional rootProof: hex borsh of an AccountProof<OwnerOpAuthorization> that names the group’s namespaceId and its current ownerOpCounter (both from getGroupInfo). Omit it on a node that holds the owner’s account root; never send "".

Refusal Status
Proof required, signer unbound, wrong account 403
Stale counter: re-read ownerOpCounter and re-sign 409
Invalid or mismatched proof, epoch below recorded 400
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
setTeeAuthoringPolicy(_ groupId: String, request: SetTeeAuthoringPolicyRequest) PUT /groups/{groupId}/settings/tee-authoring-policy
disableTeeAuthoringPolicy(_ groupId: String, request: RootGuardedOpRequest = .init()) DELETE /groups/{groupId}/settings/tee-authoring-policy
openToDelegatedExecution(_ groupId: String) -> DelegatedExecutionChange default caps + CAN_AUTHOR_ON_BEHALF
grantAuthorship(_ groupId: String, account: String) -> DelegatedExecutionChange member caps + CAN_AUTHOR_ON_BEHALF

The TEE admission policy has two forms since rc.83, and the node validates them strictly (400):

  • SetTeeAdmissionPolicyRequest.measurement(...): allowedMrtd and allowedRtmr1/2/3 must all be non-empty, even with acceptMock: true.
  • SetTeeAdmissionPolicyRequest.signedRelease(...): admit nodes running a signed release with one of allowedProfiles; every measurement list is sent empty.

Both take mode (.replica, the default, or .relay) and rootProof. The GET now reports enabled, signedRelease and mode.

openToDelegatedExecution and grantAuthorship are read-modify-write: they add Capabilities.canAuthorOnBehalf (bit 9) and keep every other bit. A relay needs this grant before it can run intents for members.

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

Delegated creation and governance (core rc.83)

Section titled “Delegated creation and governance (core rc.83)”
Method Endpoint
getContextIntentRelay(_ groupId: String, author: String? = nil) -> ContextIntentRelayInfo GET /groups/{groupId}/context-intents
createContextIntent(_ groupId: String, request: CreateContextIntentRequest) -> CreateContextIntentResponseData POST /groups/{groupId}/context-intents
getGovernanceIntentRelay(_ groupId: String) -> GovernanceIntentRelayInfo GET /groups/{groupId}/governance-intents
governanceIntent(_ groupId: String, request: GovernanceIntentRequest) -> GovernanceIntentResponseData POST /groups/{groupId}/governance-intents

These, /contexts/{id}/intents and /presence-intents are the delegated-execution routes. They are public (the warrant is the credential) on a node run with admin.delegated_access = true, and protected otherwise.

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: IssueOwnershipProofRequest) -> IssueOwnershipProofResponseData POST /groups/{groupId}/issue-ownership-proof
issueNamespaceOwnershipProof(_ groupId: String, request: IssueNamespaceOwnershipProofRequest) -> IssueOwnershipProofResponseData POST /groups/{groupId}/issue-namespace-ownership-proof
issueOwnershipProof(_ groupId: String, request: [String: JSONValue]? = nil) -> JSONValue untyped form of the above
issueNamespaceOwnershipProof(_ groupId: String, request: [String: JSONValue]? = nil) -> JSONValue untyped form of the above

A namespace ownership proof carries founding and credential since rc.83, which let a verifier check that the signer belongs to the account that founded the namespace.

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
teeRegistrationAttest(_ request: TeeRegistrationAttestRequest) -> TeeAttestResponseData POST /tee/registration-attest

TeeAttestRequest takes bindNodeKey, bindTransportKey and includeCollateral (rc.83), answered by boundPublicKey, transportPublicKey and collateral. The flags are sent only when set.

teeVerifyQuote is gone: the node has not served POST /tee/verify-quote since before rc.41. Verify a quote client-side against the collateral teeAttest returns with includeCollateral: true.

Method Endpoint
getPeersCount() -> PeersCountResponseData GET /peers
getNetworkStatus() -> JSONValue GET /network/status
getUsage() -> JSONValue GET /usage

getCertificate is gone: core rc.83 removed GET /certificate.

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
let appVersion: String?
let founding: NamespaceFounding? // rc.83
let heldOps: NamespaceHeldOps? // rc.83
}
struct GroupInfo: Codable, Sendable { // = GroupInfoResponseData
let groupId, appKey, targetApplicationId: String
let memberCount, contextCount, defaultCapabilities: Int
let subgroupVisibility: String
let activeUpgrade: GroupUpgradeStatus?
let metadata: MetadataRecord?
let groupStateHash: String?
let namespaceId: String? // rc.83
let ownerOpCounter: UInt64? // rc.83
}
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.