diff --git a/.sources/VERSIONS b/.sources/VERSIONS index 8b88edfe..801de098 100644 --- a/.sources/VERSIONS +++ b/.sources/VERSIONS @@ -62,4 +62,4 @@ motoko-core v2.4.0 cdk-rs ic-cdk v0.20.1 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 317f55c candid 2025-12-18 # candid v0.10.20, didc v0.5.4 2e4a2cf response-verification v3.1.0 18c5a37 -internetidentity release-2026-06-15 1f4104b5 +internetidentity release-2026-06-29 5fe49cfd diff --git a/.sources/internetidentity b/.sources/internetidentity index 1f4104b5..5fe49cfd 160000 --- a/.sources/internetidentity +++ b/.sources/internetidentity @@ -1 +1 @@ -Subproject commit 1f4104b53ddaecf745426769e895013168591b17 +Subproject commit 5fe49cfd5070e763d5d70c2160cca2568a459303 diff --git a/public/references/internet-identity.did b/public/references/internet-identity.did index e617bd2d..6f086652 100644 --- a/public/references/internet-identity.did +++ b/public/references/internet-identity.did @@ -247,10 +247,8 @@ type CaptchaConfig = record { }; // One entry of the `sso_credential_migration` backfill. Maps the -// (iss, aud) pair of stored SSO credentials to the discovery domain (and -// optional human-readable name) they were registered through. Field names -// match the `discovered_oidc_configs` query output so the deployer can -// transcribe its result field-for-field. +// (iss, aud) pair of a stored SSO credential to the discovery domain and +// optional human-readable name it resolves to. type SsoCredentialMigrationEntry = record { discovery_domain : text; // Matches the stored credential's `iss`. @@ -295,20 +293,23 @@ type InternetIdentityInit = record { new_flow_origins : opt vec text; // Configurations for OpenID clients openid_configs : opt vec OpenIdConfig; - // Allowlist of domains that may be registered as discoverable SSO - // providers via `add_discoverable_oidc_config`. When set, fully replaces - // the built-in defaults. When unset, falls back to `dfinity.org` - // (production) or `beta.dfinity.org` (everything else), keyed off - // `is_production`. + // Allowlist of domains that may be used as discoverable SSO providers. + // When set, fully replaces the built-in defaults. When unset, falls back + // to `dfinity.org` (production) or `beta.dfinity.org` (everything else), + // keyed off `is_production`. sso_discoverable_domains : opt vec text; + // Deploy flag opening the SSO discovery domain gate to any domain. When + // `true`, `sso_discoverable_domains` (and its defaults) no longer restrict + // which domains may be discovered as SSO providers. Does not relax the + // strict-`https` requirement: serving discovery over plain `http` still + // requires the host to be on the explicit `sso_discoverable_domains` list. + sso_allow_any_domain : opt bool; // One-shot backfill of the `sso_domain` / `sso_name` fields on stored // OpenID credentials. When set, a batched timer-driven migration stamps // every stored credential whose (iss, aud) matches an entry and whose // `sso_domain` is not set yet. Idempotent — already-stamped credentials // are skipped, so re-submitting (e.g. with a corrected list) is safe. - // When unset, no backfill runs. The deployer builds the list from the - // running canister's `discovered_oidc_configs` query before - // submitting the upgrade proposal. + // When unset, no backfill runs. sso_credential_migration : opt vec SsoCredentialMigrationEntry; // Configuration for Web Analytics analytics_config : opt opt AnalyticsConfig; @@ -469,21 +470,25 @@ type OpenIdConfig = record { seed_jwks : opt vec vec record { text; text }; }; -// SSO provider config that uses two-hop discovery. -// The backend fetches https://{discovery_domain}/.well-known/ii-openid-configuration -// for { client_id, openid_configuration } and then fetches the standard OIDC -// discovery at openid_configuration for { issuer, jwks_uri }. -type DiscoverableOidcConfig = record { +// Fully resolved SSO discovery result for the sign-in initiation flow, +// returned by `discover_sso` / `discover_sso_query`. The canister resolves it +// from the domain's two-hop discovery documents, on demand and cached. +type SsoDiscovery = record { discovery_domain : text; + client_id : text; + issuer : text; + authorization_endpoint : text; + scopes : vec text; + name : opt text; }; -// Resolved SSO provider state. -// All fields other than discovery_domain are None until discovery completes. -type OidcConfig = record { - discovery_domain : text; - client_id : opt text; - openid_configuration : opt text; - issuer : opt text; +// State of a domain's SSO discovery, read by `get_sso_discovery`. A failed +// fetch isn't a distinct state — it reads as `Pending` and the frontend times +// out — so the states are resolved, in flight, or not allowed. +type SsoDiscoveryState = variant { + Resolved : SsoDiscovery; + Pending; + NotAllowed; }; type OpenIdCredentialKey = record { Iss; Sub; Aud }; @@ -503,10 +508,8 @@ type OpenIdCredential = record { aud : Aud; last_usage_timestamp : opt Timestamp; metadata : MetadataMapV2; - // SSO discovery domain, looked up by `(iss, aud)` against the - // canister's registered discoverable OIDC configs. `None` for - // direct-provider credentials (Google / Apple / Microsoft) and for - // SSO credentials whose provider is no longer registered. + // SSO discovery domain this credential was verified through. `None` for + // direct-provider credentials (Google / Apple / Microsoft). sso_domain : opt text; // Human-readable SSO name from the domain's // `/.well-known/ii-openid-configuration`. `None` when the domain @@ -555,17 +558,30 @@ type EmailRecoveryCredential = record { last_used : opt Timestamp; }; -type EmailRecoveryChallenge = record { +// A verified email address bound to an anchor. Parallel to +// `EmailRecoveryCredential` but used as an attribute source (and +// surfaced in the smart-routing UI) rather than a recovery +// credential. The `verified_at` timestamp is when DKIM/DMARC +// verification completed successfully. +type VerifiedEmail = record { + address : text; + verified_at : Timestamp; +}; + +type EmailChallenge = record { nonce : text; expires_at : Timestamp; }; -type EmailRecoveryDnsInput = record { +type EmailChallengeDnsInput = record { address : text; dns_proof : opt DnsProofBundle; }; -type EmailRecoverySubmitDkimLeafArg = record { +// Used by both the recovery flow and the verified-emails flow. The +// pending entry's `PendingKind` dispatches to the right anchor sink +// after the cryptographic check; the argument itself is flow-neutral. +type EmailChallengeSubmitDkimLeafArg = record { nonce : text; // The DKIM resolution chain in CNAME order, ending in a TXT. At // least one hop required; bounded by `MAX_CNAME_HOPS = 4` at the @@ -576,7 +592,7 @@ type EmailRecoverySubmitDkimLeafArg = record { // leaf — the DKIM record CNAMEs into an unsigned zone (e.g. // `selector1._domainkey.outlook.com` is a signed CNAME into the // unsigned `outbound.protection.outlook.com`) — it must NOT submit - // an empty vec here; it drives `email_recovery_resolve_via_doh` + // an empty vec here; it drives `email_challenge_resolve_via_doh` // instead, which resolves the key over the canister's DoH path. hops : vec SignedRRset; // Delegation chains for signed zones touched by `hops` that @@ -585,11 +601,11 @@ type EmailRecoverySubmitDkimLeafArg = record { extra_chains : vec DelegationChain; }; -// Argument to email_recovery_resolve_via_doh. Wrapped in a record (like -// EmailRecoverySubmitDkimLeafArg) so the method can grow fields without a +// Argument to email_challenge_resolve_via_doh. Wrapped in a record (like +// EmailChallengeSubmitDkimLeafArg) so the method can grow fields without a // breaking interface change; nonce is the lookup key and is always // required. -type EmailRecoveryResolveViaDohArg = record { +type EmailChallengeResolveViaDohArg = record { nonce : text; }; @@ -646,7 +662,10 @@ type DohFailureReason = variant { ResponseMalformed : text; }; -type EmailRecoveryError = variant { +// Shared by both flows (recovery + verified emails). The variants +// describe inbound-DKIM-challenge failure modes; none are +// recovery-specific. +type EmailChallengeError = variant { Unauthorized : principal; NonceUnknown; NonceExpired; @@ -655,19 +674,33 @@ type EmailRecoveryError = variant { DomainNotSupported : text; EmailVerificationFailed : text; DkimLeafMismatch; - // email_recovery_submit_dkim_leaf was called with an empty `hops` + // email_challenge_submit_dkim_leaf was called with an empty `hops` // vector; an FE that can't walk DNSSEC must drive - // email_recovery_resolve_via_doh instead. + // email_challenge_resolve_via_doh instead. EmptyDkimLeafHops; NoDkimLeafExpected; AddressMismatch; SubjectNotSigned; AddressAlreadyRegistered; AddressNotRegistered; + // The submitted address didn't pass shape validation (missing `@`, + // empty local-part or domain, whitespace, oversized parts). Distinct + // from DomainNotSupported, which is about a valid address whose + // registered domain can't be verified. + InvalidEmailAddress : text; + // The anchor has reached its per-bucket cap (currently only fires + // for the verified-emails bucket). FE shows a "limit reached" + // notice; user must remove an existing entry to add another. + LimitReached : record { limit : nat8 }; InternalCanisterError : text; }; -type EmailRecoveryStatus = variant { +// Polling status returned by `email_challenge_status` — shared between +// the recovery flow and the verified-emails flow. `RegistrationSucceeded` +// covers both "recovery email bound" and "verified email bound"; +// `RecoveryReady` is recovery-only and only emitted when the pending +// entry's `PendingKind` is `Recover`. +type EmailChallengeStatus = variant { Pending; ResolvingDoh; NeedDkimLeaf : record { selector : text }; @@ -677,7 +710,7 @@ type EmailRecoveryStatus = variant { expiration : Timestamp; anchor_number : IdentityNumber; }; - Failed : EmailRecoveryError; + Failed : EmailChallengeError; Expired; }; @@ -687,12 +720,12 @@ type EmailRecoveryStatus = variant { type VerificationPath = variant { Doh; Dnssec }; // Strictly-public, user-copyable diagnostics for one pending challenge -// (see email_recovery_diagnostics). Intended for a support ticket so a +// (see email_challenge_diagnostics). Intended for a support ticket so a // case can be lined up across the SMTP gateway logs and the canister's // production logs via message_id. NO email address, anchor, principal, // delegation/seed, or inner error string — reason_code is the failing // variant's name only. -type EmailRecoveryDiagnostics = record { +type EmailChallengeDiagnostics = record { message_id : opt text; reason_code : text; verification_path : VerificationPath; @@ -853,6 +886,9 @@ type OpenIDRegFinishArg = record { jwt : JWT; salt : Salt; name : text; + // SSO discovery domain the JWT was obtained through, or null for a direct + // provider (Google / Microsoft / Apple). Selects the JWK source. + discovery_domain : opt text; }; // Extra information about registration status for new authentication methods @@ -946,6 +982,11 @@ type IdentityInfo = record { // the first one — but exposing it as a `vec` lets future // multi-credential support land without a candid schema bump. email_recovery : opt vec EmailRecoveryCredential; + // Verified emails bound to this anchor (absent when none is + // configured). Capped at MAX_VERIFIED_EMAILS_PER_ANCHOR; the FE + // shows a "limit reached" notice in the wizard when adding + // beyond the cap. + verified_emails : opt vec VerifiedEmail; }; type IdentityInfoError = variant { @@ -1037,6 +1078,39 @@ type PrepareAccountDelegation = record { expiration : Timestamp; }; +// Result of mcp_prepare_account_delegation. Carries the account_number the +// canister used (the one named in the request, or the anchor's default account +// at target_origin when none was named) so the MCP server can thread the same +// account into mcp_get_account_delegation — the default is mutable, so +// re-resolving it in `get` could otherwise diverge and yield NoSuchDelegation. +type McpPrepareDelegation = record { + user_key : UserKey; + expiration : Timestamp; + account_number : opt AccountNumber; +}; + +type SessionDelegationError = variant { + InternalCanisterError : text; + Unauthorized : principal; + NoSuchDelegation; +}; + +type PrepareSessionDelegation = record { + user_key : UserKey; + expiration : Timestamp; +}; + +// The identity's synced trusted-MCP-server configuration: a master toggle and +// the single MCP server URL the user trusts. Persisted on-chain (keyed by +// anchor), so it follows the identity across all of its devices — unlike the +// device-local CLI-access toggle. `url` is kept verbatim so the Settings UI can +// display/re-probe a path-based endpoint; the connect flow matches trust by +// origin. +type McpConfig = record { + enabled : bool; + url : opt text; +}; + type GetAccountsError = variant { InternalCanisterError : text; Unauthorized : principal; @@ -1214,10 +1288,10 @@ type PrepareAttributeRequest = record { // or `sso:` (e.g. `sso:dfinity.org:email`). // // Each linked credential is addressable via exactly one scope: - // credentials obtained through a `DiscoverableOidcConfig` (two-hop SSO - // discovery) are reachable only via `sso:`; credentials from - // hardcoded OIDC providers (Google, Microsoft, …) are reachable only via - // `openid:`. Under `sso:` only `email` and `name` are supported; + // credentials obtained through SSO two-hop discovery are reachable only + // via `sso:`; credentials from hardcoded OIDC providers (Google, + // Microsoft, …) are reachable only via `openid:`. Under `sso:` + // only `email` and `name` are supported; // under `openid:` `email`, `name`, and `verified_email` are supported. attribute_keys : vec text; }; @@ -1549,63 +1623,121 @@ service : (opt InternetIdentityInit) -> { // OpenID credentials protocol // =========================== - openid_identity_registration_finish : (OpenIDRegFinishArg) -> (variant { Ok : IdRegFinishResult; Err : IdRegFinishError }); - openid_credential_add : (IdentityNumber, JWT, Salt) -> (variant { Ok; Err : OpenIdCredentialAddError }); + openid_identity_registration_finish : (OpenIDRegFinishArg) -> (variant { Ok : IdRegFinishResult; Pending; Err : IdRegFinishError }); + // The trailing `opt text` is the SSO discovery domain (null for a direct + // provider). For SSO sign-ins a cold discovery/JWKS cache yields the + // `Pending` result arm — a retry signal, not an error: the caller re-calls + // the method (and for delegations, polls `openid_get_delegation`, re-calling + // `openid_prepare_delegation` on a `Pending` poll result). + openid_credential_add : (IdentityNumber, JWT, Salt, opt text) -> (variant { Ok; Pending; Err : OpenIdCredentialAddError }); openid_credential_remove : (IdentityNumber, OpenIdCredentialKey) -> (variant { Ok; Err : OpenIdCredentialRemoveError }); - openid_prepare_delegation : (JWT, Salt, SessionKey) -> (variant { Ok : OpenIdPrepareDelegationResponse; Err : OpenIdDelegationError }); - openid_get_delegation : (JWT, Salt, SessionKey, Timestamp) -> (variant { Ok : SignedDelegation; Err : OpenIdDelegationError }) query; - - // Email-recovery protocol - // ======================= - // See `docs/ongoing/email-recovery.md`. Covers both flows: - // - Setup: prepare_add (authenticated) → smtp_request for - // register@id.ai → credential bound to the anchor. Removed - // later via credential_remove. - // - Recovery: prepare_delegation (anonymous, bound to a - // session_key) → smtp_request for recover@id.ai → canister - // stamps a signed delegation seed. The FE then calls - // email_recovery_get_delegation to retrieve the - // SignedDelegation. - // Both flows share the polling status query. - email_recovery_credential_prepare_add : (IdentityNumber, EmailRecoveryDnsInput) -> (variant { Ok : EmailRecoveryChallenge; Err : EmailRecoveryError }); - email_recovery_prepare_delegation : (EmailRecoveryDnsInput, SessionKey) -> (variant { Ok : EmailRecoveryChallenge; Err : EmailRecoveryError }); - email_recovery_status : (text) -> (EmailRecoveryStatus) query; - email_recovery_diagnostics : (text) -> (opt EmailRecoveryDiagnostics) query; - email_recovery_submit_dkim_leaf : (EmailRecoverySubmitDkimLeafArg) -> (variant { Ok; Err : EmailRecoveryError }); - // Resolves the DKIM key over the canister's own allowlist-gated DoH - // path, called with just the nonce. Used for the pure-DoH (Gmail) - // case and as the fallback when the FE can't walk a fully-signed - // DNSSEC resolution (the DKIM record CNAMEs into an unsigned zone). - // Polled: the FE calls it repeatedly while the status is ResolvingDoh. - email_recovery_resolve_via_doh : (EmailRecoveryResolveViaDohArg) -> (variant { Ok; Err : EmailRecoveryError }); - email_recovery_get_delegation : (EmailRecoveryGetDelegationArgs) -> (variant { Ok : SignedDelegation; Err : EmailRecoveryError }) query; - email_recovery_credential_remove : (IdentityNumber, text) -> (variant { Ok; Err : EmailRecoveryError }); - - // SMTP gateway protocol - // ===================== + openid_prepare_delegation : (JWT, Salt, SessionKey, opt text) -> (variant { Ok : OpenIdPrepareDelegationResponse; Pending; Err : OpenIdDelegationError }); + openid_get_delegation : (JWT, Salt, SessionKey, Timestamp, opt text) -> (variant { Ok : SignedDelegation; Pending; Err : OpenIdDelegationError }) query; + + // SSO discovery for the sign-in initiation flow. The frontend polls + // `get_sso_discovery` (query) and, while it reads `Pending`, drives the + // on-demand two-hop discovery fetch with `discover_sso` (update); once the + // fetch completes the query returns `Resolved` with the config. + discover_sso : (text) -> (); + get_sso_discovery : (text) -> (SsoDiscoveryState) query; + + // =================================================================== + // Email-challenge protocol (shared inbound-DKIM primitive) + // =================================================================== + // The flow-neutral surface — used by both the email-recovery flow + // and the verified-emails flow. Methods here are keyed by the + // canister-issued nonce; dispatch to the right anchor sink happens + // inside the canister via `PendingKind` on the pending entry. + // // The off-chain SMTP gateway forwards every inbound message via - // smtp_request. The canister verifies the email cryptographically - // and dispatches by recipient: register@id.ai → setup completion, - // recover@id.ai → recovery delegation stamping. Always returns Ok - // — the gateway shouldn't get a per-message verification signal - // back. The FE sees outcomes via the polling status query. + // `smtp_request`. The canister verifies the email cryptographically + // and dispatches by recipient: `register@` → setup + // completion (either flow), `recover@` → recovery + // delegation stamping. Always returns Ok — the gateway shouldn't + // get a per-message verification signal back. The FE sees outcomes + // via `email_challenge_status`. smtp_request : (SmtpRequest) -> (SmtpResponse); // Called by the gateway at RCPT TO time to decide whether to // accept the connection before pulling the message body. Returns - // Ok for register@id.ai / recover@id.ai (case-insensitive), and - // 550 (mailbox unavailable) for everything else. + // Ok for `register@` / `recover@` (case- + // insensitive), and 550 (mailbox unavailable) for everything else. smtp_request_validate : (SmtpRequest) -> (SmtpResponse) query; + // FE-side polling — the wizard / panel calls these repeatedly to + // drive the "waiting for your email" UI. Status flips through + // `Pending` → `ResolvingDoh` / `NeedDkimLeaf` → terminal. + // Diagnostics returns strictly-public, user-copyable failure + // detail for support tickets (no PII, no address, no anchor). + email_challenge_status : (text) -> (EmailChallengeStatus) query; + email_challenge_diagnostics : (text) -> (opt EmailChallengeDiagnostics) query; + + // DNSSEC-path completion (`submit_dkim_leaf`) and DoH-path + // completion (`resolve_via_doh`). One or the other runs per + // challenge depending on which path the canister picked at + // prepare-time. Both are polled by the FE while the status is + // `NeedDkimLeaf` / `ResolvingDoh`. + email_challenge_submit_dkim_leaf : (EmailChallengeSubmitDkimLeafArg) -> (variant { Ok; Err : EmailChallengeError }); + email_challenge_resolve_via_doh : (EmailChallengeResolveViaDohArg) -> (variant { Ok; Err : EmailChallengeError }); + + // =================================================================== + // Email-recovery flow (recovery-as-login) + // =================================================================== + // Recovery-specific surface built on top of the shared challenge + // primitive above. See `docs/ongoing/email-recovery.md` for the + // full design. + // - Setup: `credential_prepare_add` (authenticated) → + // `smtp_request` for `register@` → credential bound to + // the anchor's `email_recovery`. Removed via `credential_remove`. + // - Recovery: `prepare_delegation` (anonymous, bound to a + // `session_key`) → `smtp_request` for `recover@` → + // canister stamps a signed delegation seed. The FE then calls + // `get_delegation` to retrieve the `SignedDelegation`. + email_recovery_credential_prepare_add : (IdentityNumber, EmailChallengeDnsInput) -> (variant { Ok : EmailChallenge; Err : EmailChallengeError }); + email_recovery_prepare_delegation : (EmailChallengeDnsInput, SessionKey) -> (variant { Ok : EmailChallenge; Err : EmailChallengeError }); + email_recovery_get_delegation : (EmailRecoveryGetDelegationArgs) -> (variant { Ok : SignedDelegation; Err : EmailChallengeError }) query; + email_recovery_credential_remove : (IdentityNumber, text) -> (variant { Ok; Err : EmailChallengeError }); + + // =================================================================== + // Verified-emails flow (attribute source) + // =================================================================== + // Parallel to the recovery flow but the verified address is stored + // on `Anchor::verified_emails` rather than `Anchor::email_recovery`. + // Reuses the same SMTP gateway, DKIM verifier and DMARC alignment, + // but issues nonces with the `II-Verify-` prefix so an inbound + // challenge can never be cross-applied between the two flows. + // Capped at MAX_VERIFIED_EMAILS_PER_ANCHOR (5) addresses per anchor. + verified_email_prepare_add : (IdentityNumber, EmailChallengeDnsInput) -> (variant { Ok : EmailChallenge; Err : EmailChallengeError }); + verified_email_remove : (IdentityNumber, text) -> (variant { Ok; Err : EmailChallengeError }); + + // =================================================================== + // DEPRECATED — remove in a follow-up PR + // =================================================================== + // Legacy aliases for the four `email_challenge_*` methods, kept so + // a stale FE bundle in a browser cache — or any FE build that + // lands before this canister's renamed methods — can still drive + // the inbound-DKIM flow without a "method not found" break + // mid-verification. + // + // The wire bytes are identical to the new methods (Candid is + // structurally typed; the renamed return types match the old + // types' shapes field-for-field), so old clients with bindings + // against the old type names deserialize successfully. + // + // **All methods below must be removed together in a single + // follow-up `chore(be): remove deprecated email_recovery_* method + // aliases` PR**, once every deployed FE has refreshed to the + // `email_challenge_*` names. See `TASKS.md` for the tracked + // follow-up. + email_recovery_status : (text) -> (EmailChallengeStatus) query; + email_recovery_diagnostics : (text) -> (opt EmailChallengeDiagnostics) query; + email_recovery_submit_dkim_leaf : (EmailChallengeSubmitDkimLeafArg) -> (variant { Ok; Err : EmailChallengeError }); + email_recovery_resolve_via_doh : (EmailChallengeResolveViaDohArg) -> (variant { Ok; Err : EmailChallengeError }); + // HTTP Gateway protocol // ===================== http_request : (request : HttpRequest) -> (HttpResponse) query; - // OIDC Discovery - // =============== - discovered_oidc_configs : () -> (vec OidcConfig) query; - add_discoverable_oidc_config : (DiscoverableOidcConfig) -> (); - // Internal Methods // ================ init_salt : () -> (); @@ -1658,6 +1790,86 @@ service : (opt InternetIdentityInit) -> { expiration : Timestamp ) -> (variant { Ok : SignedDelegation; Err : AccountDelegationError }) query; + // Enable/disable the backend /mcp delegation path for an anchor at a given + // MCP server origin. Enabling binds the principal II derives for the anchor + // at that origin; disabling unbinds exactly that principal. No account is + // chosen here (accounts are per-origin and the connector isn't an app) — the + // app account is selected per call on mcp_prepare_account_delegation. The + // origin comes from the connect request, so each user trusts the MCP server + // they choose. + mcp_set_access : ( + anchor_number : UserNumber, + mcp_server_origin : FrontendHostname, + enabled : bool + ) -> (variant { Ok; Err : text }); + + // Whether the anchor has MCP access enabled at mcp_server_origin. + mcp_access_enabled : ( + anchor_number : UserNumber, + mcp_server_origin : FrontendHostname + ) -> (bool) query; + + // Read the identity's synced trusted-MCP-server config (master toggle + the + // trusted server URL). Persisted on-chain, so it follows the identity across + // devices. Read by the Settings UI and the /mcp connect flow (which verifies + // the connecting origin against it). Returns the disabled, no-server default + // for an unauthorized caller or an anchor that never wrote a config. + mcp_get_config : (anchor_number : UserNumber) -> (McpConfig) query; + + // Persist the identity's trusted-MCP-server config so it syncs across the + // identity's devices. Authenticated as the identity, so only the user — never + // a page that initiates a connect request — can change what it trusts. + mcp_set_config : ( + anchor_number : UserNumber, + config : McpConfig + ) -> (variant { Ok; Err : text }); + + // Called by the MCP server, authorized by caller() == the principal bound for + // its anchor at the connect-time mcp_server_origin; the anchor is recovered + // from the caller. Mints a per-app delegation at target_origin. account_number + // names one of the anchor's accounts there to act as (discover them with + // mcp_get_accounts), and null uses the anchor's default account there; an + // account_number that isn't the anchor's at target_origin is rejected as + // Unauthorized. max_ttl is the requested lifetime in ns, defaulting to and + // capped at 5 minutes. The resolved account_number is returned in + // McpPrepareDelegation so it can be threaded into mcp_get_account_delegation + // (the default account at an origin is mutable). + mcp_prepare_account_delegation : ( + target_origin : FrontendHostname, + account_number : opt AccountNumber, + session_key : SessionKey, + max_ttl : opt nat64 + ) -> (variant { Ok : McpPrepareDelegation; Err : AccountDelegationError }); + + // Fetch the delegation prepared above; the anchor is recovered from caller(). + // account_number and expiration must be the values returned by the matching + // mcp_prepare_account_delegation, else this returns NoSuchDelegation. + mcp_get_account_delegation : ( + target_origin : FrontendHostname, + account_number : opt AccountNumber, + session_key : SessionKey, + expiration : Timestamp + ) -> (variant { Ok : SignedDelegation; Err : AccountDelegationError }) query; + + // Called by the MCP server (anchor recovered from caller()): list the anchor's + // accounts at target_origin so the agent can pick which account_number to + // request a delegation for via mcp_prepare_account_delegation. + mcp_get_accounts : ( + target_origin : FrontendHostname + ) -> (variant { Ok : vec AccountInfo; Err : AccountDelegationError }) query; + + prepare_session_delegation : ( + anchor_number : UserNumber, + session_key : SessionKey, + max_ttl : opt nat64 + ) -> (variant { Ok : PrepareSessionDelegation; Err : SessionDelegationError }); + + get_session_delegation : ( + anchor_number : UserNumber, + session_key : SessionKey, + expiration : Timestamp + ) -> (variant { Ok : SignedDelegation; Err : SessionDelegationError }) query; + get_default_account : ( anchor_number : UserNumber, origin : FrontendHostname,