The consent page
A session opened for an app’s package may not create, run or leave an organization, mint, accept or revoke an invite, approve a mero-tee release, upgrade an app instance or open outbound hosts on its own. The app asks the node, and the person decides on the node’s own consent page, with their passkey.
client.requestConsent(_ request:) → ConsentStarted |
POST /consent/requests (sealed): { requestId, url, code, expiresAt }. Refuses a code that is not the page’s (CONSENT_CODE_MISMATCH). |
client.consentResult(_ requestId:) → ConsentResult |
GET /consent/{id}/result (sealed, the same account): the decision now, with its typed outcome |
client.confirmConsentReturn(_ started:, returned:) → ConsentResult |
after the page sent the person back: the result of the request the app started, or an error (below) |
client.openConsentPage(_:options:), client.waitForConsent(_:) |
ask and navigate; poll until decided |
ConsentLauncher.decide(client:request:callback:…) (MeroKitUI) |
all of the above, with the page in ASWebAuthenticationSession (or the browser) |
What to ask, and what comes back
Section titled “What to ask, and what comes back”ConsentRequest |
ConsentResult.outcome once approved |
|---|---|
.createOrg(name:returnTo:) |
.orgCreated(orgId:), result.orgId |
.createInvite(org:role:teams:ttlSecs:uses:allApps:returnTo:) |
.inviteCreated(token:), result.inviteToken |
.acceptInvite(token:returnTo:) |
.inviteAccepted(org:), result.joinedOrg |
.renameOrg(org:name:returnTo:) |
.orgRenamed(orgId:name:), result.newOrgName |
.deleteOrg(org:returnTo:) |
.orgDeleted(apps:), result.deletedApps (the apps deleted with it) |
.leaveOrg(org:returnTo:) |
none |
.removeMember(org:account:returnTo:) |
none |
.setMemberRole(org:account:role:returnTo:) |
none |
.setMemberTeams(org:account:teams:returnTo:), each row a ConsentTeamMembership(team:role:capabilities:) |
none |
.removeMemberTeams(org:account:teams:returnTo:) |
none |
.revokeInvite(org:inviteKey:returnTo:) |
none |
.setCapabilities(org:account:team:capabilities:returnTo:): with account, that member’s OrgCapabilities (organization-wide, or in team); without, a default |
none |
.setAllApps(org:account:allApps:returnTo:) |
none |
.release(...), .upgrade(...), .outbound(...) |
none |
The outcome comes only from the sealed /consent/{id}/result, never from the
URL the page sends the person back to. consentResult refuses an answer for
another request, an approved createOrg, createInvite, acceptInvite,
renameOrg or deleteOrg without its outcome, and an outcome of another kind.
result.changesOrganizations is true for an approved request of any
organization kind: re-read what lists organizations, members or teams.
An invite token is a bearer credential. ConsentRequest, ConsentResult and
ConsentOutcome print it as <redacted> in their descriptions, debug
descriptions and mirrors (print, dump, string interpolation), and no error
carries it. Read it from result.inviteToken, and don’t log it.
returnTo
Section titled “returnTo”The node takes a returnTo only on the app package’s own frontend origin or an
origin its signed manifest names (links.frontend, frontendAliases). For a
native app that is either:
- a universal link on such an https origin,
CloudCallback.https(host:path:), with the app in that origin’sapple-app-site-association; or - on Calimero Cloud, the package’s served page
(
{origin}/{version}/?native-consent=1), which passesrequest_idandresulton to the app’s callback.MeroClient.requestConsenttakes it asreturnTo:.
After the page sends the person back
Section titled “After the page sends the person back”The query on the return URL is a hint. confirmConsentReturn reads the result
of the request this app started and:
- throws
CONSENT_REQUEST_MISMATCHwhen the return names another request; - throws
CONSENT_NOT_DECIDEDwhen the app’s own request is still pending, the sign that a different request was approved, or that the page was forged; - throws
CONSENT_REPLACEDwhen the node answers404for the app’s own request: an account has one undecided consent request per node, and a newer one replaced it (possibly on a forged page). Ask again; - otherwise answers the decision (
approvedordeclined) and its outcome.
In the app
Section titled “In the app”import MeroKitUI
let (started, result) = try await ConsentLauncher.decide( client: cloud, request: .createOrg(name: "Acme", returnTo: callback.url), callback: callback, confirm: { started in await showCode(started.code) }) // false: don't open the pageif let org = result.orgId { open(org) }- With an app scheme, or an https callback on iOS 17.4+ / macOS 14.4+, the
page opens in
ASWebAuthenticationSession, and the return is checked withconfirmConsentReturn. A dismissed sheet answers the result as it stands. - With an https callback on an older system,
ASWebAuthenticationSessiontakes no https callback, so the page opens in the browser and the result is polled (waitForConsent) until the request expires. The universal link arrives inonOpenURL: hand it tosession.handle(url).
MeroAppSession.requestConsent(_:) does the same with the session’s
callback, keeps the result in consentResult, remembers the request so
handle(url) checks a later return against it, and bumps revision when the
approval changed an organization (result.changesOrganizations).
.onOpenURL { url in Task { await session.handle(url) } }ConsentRequest.generated() gives the same body as the generated type, for
client.consent.pageCreate(body:); requestConsent also checks the request
and the node’s confirmation code.