@@ -1006,6 +1006,169 @@ type IdentityAuthnInfo = record {
10061006 recovery_authn_methods : vec AuthnMethod;
10071007};
10081008
1009+ // Which browser a sign-in came from, as a token rather than a name to show. Products
1010+ // get renamed — "Chrome OS" became "ChromeOS", "Mac OS X" became "macOS" — so the name
1011+ // the user reads is derived in the frontend, where a rename reaches every stored record
1012+ // at once. "Brand" is what the client hints call this, and BrowserInfo below is the
1013+ // entry it describes.
1014+ type BrowserBrand = variant {
1015+ Chrome; Safari; Firefox; Edge; Opera; SamsungInternet;
1016+ // A browser this list does not name, shown as the client resolved it. Worth seeing
1017+ // rather than hiding behind a generic label. Named variants are the six that hold
1018+ // 97% of the web between them, because a variant is what earns an icon.
1019+ Other : text;
1020+ };
1021+
1022+ type OperatingSystem = variant {
1023+ Macos; Ios; Ipados; Windows; Android; ChromeOs; Linux; Other : text;
1024+ };
1025+
1026+ // Reported where the client can state it and inferred where it cannot, so unknown is a
1027+ // real answer: the browsers exposing no client hints are the ones this is least sure of.
1028+ type FormFactor = variant { Desktop; Mobile; Tablet; Unknown };
1029+
1030+ // What a browser reported about itself when it registered. Self-reported, so it is
1031+ // something the user reads to recognise their own browser rather than evidence about
1032+ // where a session came from. The canister stores these and never interprets them.
1033+ type BrowserDescription = record {
1034+ brand : BrowserBrand;
1035+ os : OperatingSystem;
1036+ form_factor : FormFactor;
1037+ // The hardware, where the client can name it — Android is the only place that does.
1038+ model : opt text;
1039+ };
1040+
1041+ type BrowserInfo = record {
1042+ id : nat32;
1043+ // Fixed at registration. A sign-in reporting something else registers its own entry,
1044+ // so this describes a registration rather than the last sign-in.
1045+ description : BrowserDescription;
1046+ created_at : Timestamp;
1047+ // Advanced by a sign-in from this browser and by every session refresh it drives.
1048+ last_used : Timestamp;
1049+ // Sessions this browser holds. Zero means it is signed in to nothing.
1050+ session_count : nat32;
1051+ };
1052+
1053+ type PrepareAccountSessionRequest = record {
1054+ identity_number : UserNumber;
1055+ origin : FrontendHostname;
1056+ account_number : opt AccountNumber;
1057+ // The II frontend's own public key.
1058+ session_key : SessionKey;
1059+ // What this browser is, for the user's session list.
1060+ browser_description : BrowserDescription;
1061+ // The browser's own public key, DER-encoded, as the registry currently holds it. A
1062+ // key this anchor has not seen registers a browser under it.
1063+ current_browser_key : PublicKey;
1064+ // What the browser rotates to once this sign-in succeeds. Must differ from
1065+ // current_browser_key: a browser that never rotates keeps a leaked key useful.
1066+ next_browser_key : PublicKey;
1067+ // Signature over session_key and next_browser_key, verified with current_browser_key.
1068+ current_browser_key_signature : blob;
1069+ // Signature by next_browser_key over session_key and current_browser_key, proving the
1070+ // browser holds the key it is announcing.
1071+ next_browser_key_signature : blob;
1072+ // The consented access level, fixed for the session's life.
1073+ permissions : opt Permissions;
1074+ // Clamped to the session maximum.
1075+ valid_for : opt nat64;
1076+ // How long the session may go unminted before it is over, clamped to between
1077+ // 10 minutes and the session's own granted length. Absent leaves the
1078+ // canister's own default.
1079+ max_idle : opt nat64;
1080+ };
1081+
1082+ type PrepareAccountSessionResponse = record {
1083+ user_key : PublicKey;
1084+ // The session's valid_till.
1085+ expiration : Timestamp;
1086+ // Names the session this ceremony created, and is what get_account_session is given
1087+ // to collect the delegation signed for it. Not a credential: it names a session, it
1088+ // does not authorise one.
1089+ session_id : nat64;
1090+ // Which browser this sign-in was attributed to, so the settings list can mark the one
1091+ // the user is looking at, and so the browser knows which registration its key now
1092+ // belongs to. Not a credential: a caller never presents it.
1093+ browser_id : nat32;
1094+ // The principal apps see for this account, so the frontend can tell its own
1095+ // sessions apart without minting a delegation to learn it.
1096+ account_principal : principal;
1097+ };
1098+
1099+ type GetAccountSessionRequest = record {
1100+ identity_number : UserNumber;
1101+ origin : FrontendHostname;
1102+ account_number : opt AccountNumber;
1103+ session_key : SessionKey;
1104+ expiration : Timestamp;
1105+ // The session prepare_account_session created.
1106+ session_id : nat64;
1107+ };
1108+
1109+ type GetAccountSessionResponse = record {
1110+ signed_delegation : SignedDelegation;
1111+ };
1112+
1113+ type AccountSessionError = variant {
1114+ Unauthorized : principal;
1115+ NoSuchAccount;
1116+ NoSuchSession;
1117+ // The session is there, but no delegation was signed for the session_key and
1118+ // expiration asked for. Ask again with the ones prepare_account_session returned;
1119+ // signing in afresh is not the remedy.
1120+ NoSuchDelegation;
1121+ // The browser's key is unusable, or its signature does not verify against it.
1122+ InvalidBrowserKey;
1123+ // The browser presented a key it has already rotated away from, which happens when it
1124+ // never learned that its last sign-in succeeded. It holds the successor that does
1125+ // resolve, so the answer is to promote that one and present it.
1126+ StaleBrowserKey;
1127+ InternalCanisterError : text;
1128+ };
1129+
1130+ type AppPrepareDelegationRequest = record {
1131+ // The key the app delegation delegates to. Nothing about the account is named:
1132+ // the caller's own session chain is what identifies it.
1133+ session_key : SessionKey;
1134+ };
1135+
1136+ type AppPrepareDelegationResponse = record {
1137+ user_key : PublicKey;
1138+ expiration : Timestamp;
1139+ };
1140+
1141+ type AppGetDelegationRequest = record {
1142+ session_key : SessionKey;
1143+ // Must match the prepared value.
1144+ expiration : Timestamp;
1145+ };
1146+
1147+
1148+ type RevokeBrowserSessionsRequest = record {
1149+ identity_number : UserNumber;
1150+ browser_id : nat32;
1151+ };
1152+
1153+ type SessionRevokeError = variant {
1154+ Unauthorized : principal;
1155+ // Raised before the sweep writes anything, so a browser is never left signed out of
1156+ // some of its applications and not others.
1157+ InternalCanisterError : text;
1158+ };
1159+
1160+ type AppSessionError = variant {
1161+ // No usable session behind this caller: revoked, expired, pruned, or never one at
1162+ // all. One outcome, because which of those it is depends on whether a prune has run
1163+ // yet, and because an app can act on none of them differently.
1164+ NoSuchSession;
1165+ // The session is live, but nothing was signed for the session_key and expiration
1166+ // asked for — so the expiration is one app_prepare_delegation never returned. Prepare
1167+ // again and use what comes back; signing in afresh is not the remedy.
1168+ NoSuchDelegation;
1169+ InternalCanisterError : text;
1170+ };
1171+
10091172type IdentityInfo = record {
10101173 authn_methods : vec AuthnMethodData;
10111174 authn_method_registration : opt AuthnMethodRegistrationInfo;
@@ -1026,6 +1189,10 @@ type IdentityInfo = record {
10261189 // shows a "limit reached" notice in the wizard when adding
10271190 // beyond the cap.
10281191 verified_emails : opt vec VerifiedEmail;
1192+ // Browsers this anchor has signed in from (absent when it has never
1193+ // created a session), so the Settings UI can offer "sign this browser
1194+ // out" without a separate call.
1195+ browsers : opt vec BrowserInfo;
10291196 // The anchor's synced trusted-MCP-server config (absent when the
10301197 // anchor never wrote one). Carried here rather than read from the
10311198 // mcp_get_config query so the Settings UI has a certified value to
@@ -1856,6 +2023,31 @@ service : (opt InternetIdentityInit) -> {
18562023 update : AccountUpdate
18572024 ) -> (variant { Ok : AccountInfo; Err: UpdateAccountError });
18582025
2026+ // Creates or reuses a revocable session at one account and signs its identity to
2027+ // the II frontend's own key. Called only by the II frontend, which ships with the
2028+ // canister; requires an anchor access method, so a session can neither spawn nor
2029+ // extend itself.
2030+ prepare_account_session : (PrepareAccountSessionRequest) -> (variant { Ok : PrepareAccountSessionResponse; Err : AccountSessionError });
2031+ get_account_session : (GetAccountSessionRequest) -> (variant { Ok : GetAccountSessionResponse; Err : AccountSessionError }) query;
2032+
2033+ // Mints a short-lived app delegation from a live session. Called by app frontends
2034+ // with the session chain, so revoking the session ends access within one delegation
2035+ // lifetime.
2036+ app_prepare_delegation : (AppPrepareDelegationRequest) -> (variant { Ok : AppPrepareDelegationResponse; Err : AppSessionError });
2037+ app_get_delegation : (AppGetDelegationRequest) -> (variant { Ok : SignedDelegation; Err : AppSessionError }) query;
2038+
2039+ // Signs the calling session out. A session that is already gone is success, so a
2040+ // client that retries, or that signs out twice, does not have to reason about whether
2041+ // its session was still there. An app can revoke only its own session.
2042+ app_revoke_session : () -> (variant { Ok; Err : AppSessionError });
2043+ // Whether the calling session is still usable. For the II frontend's silent
2044+ // re-auth path, which must decide whether it can answer without rendering
2045+ // anything. Advisory: a query reply is not certified, and every mint enforces
2046+ // the same conditions regardless of the answer here.
2047+ check_session : () -> (bool) query;
2048+
2049+ revoke_browser_sessions : (RevokeBrowserSessionsRequest) -> (variant { Ok; Err : SessionRevokeError });
2050+
18592051 prepare_account_delegation : (
18602052 anchor_number : UserNumber,
18612053 origin : FrontendHostname,
0 commit comments