From e9d8766e7e89e422026bfe5fb33431104df4c27e Mon Sep 17 00:00:00 2001 From: "pr-automation-bot-public[bot]" Date: Tue, 10 Nov 2026 09:10:24 +0000 Subject: [PATCH] chore: sync II spec to dfinity/internet-identity release-2026-11-06 --- .sources/VERSIONS | 2 +- .sources/internetidentity | 2 +- public/references/internet-identity.did | 371 ++++++++++++++++++++++-- 3 files changed, 348 insertions(+), 27 deletions(-) diff --git a/.sources/VERSIONS b/.sources/VERSIONS index 8c1bd1c0..b3754a04 100644 --- a/.sources/VERSIONS +++ b/.sources/VERSIONS @@ -39,4 +39,4 @@ # ------------------------------------------------------- motoko v1.16.1 01aee06 -internetidentity release-2026-10-16 8d3e876b +internetidentity release-2026-11-06 6eda54fa diff --git a/.sources/internetidentity b/.sources/internetidentity index 8d3e876b..6eda54fa 160000 --- a/.sources/internetidentity +++ b/.sources/internetidentity @@ -1 +1 @@ -Subproject commit 8d3e876b074a5a93e167cb09a550a3c20f4c3ed8 +Subproject commit 6eda54fadafa1d4c2e05522f84fed03705459e76 diff --git a/public/references/internet-identity.did b/public/references/internet-identity.did index dfe966a5..a0ecb3d0 100644 --- a/public/references/internet-identity.did +++ b/public/references/internet-identity.did @@ -1089,6 +1089,37 @@ type BrowserInfo = record { last_used : Timestamp; // Sessions this browser holds. Zero means it is signed in to nothing. session_count : nat32; + // The Home Screen app this browser points at, where it has one. Absent is "not + // installed, or installed and never opened": the record is created by the app's + // first open, so there is nothing to point at before that. Absent too where the + // record this browser points at is gone, and where this deployment does not notify. + notification_app : opt NotificationAppInfo; +}; + +// Asks what this browser's entry says about the app it points at, for a page that has +// the browser key and no session. +type GetNotificationAppStatusRequest = record { + anchor_number : UserNumber; +}; + +type NotificationAppInfo = record { + // Whether the app has registered for Web Push. False means it has been opened and + // the permission has not been given yet. + subscribed : bool; + // Whether this identity wants it to deliver. + enabled : bool; + // When the app last called, which is the only evidence there is that it still + // exists: nothing says when a user deletes a web app from their Home Screen. + last_seen_ns : nat64; +}; + +// What a browser has. An install reads as AwaitingInstall only while its key can still be +// spent; past that the app holding it can never finish, so it reads as nothing. +type NotificationAppStatus = variant { + Linked : NotificationAppInfo; + // When the key this browser handed its install stops being spendable. + AwaitingInstall : nat64; + NoApp; }; type PrepareAccountSessionRequest = record { @@ -1821,6 +1852,24 @@ type NotificationRevokeConsentError = variant { InternalCanisterError : text; }; +// What an app is told about its own consent. A record rather than a bare bool, so a +// second fact about the decision can be added without a breaking change. +type AppNotificationConsentStatusResponse = record { + is_granted : bool; +}; + +type AppNotificationConsentStatusError = variant { + // No live session behind this caller: expired, idle, revoked, or never one at all. + NoSuchSession; + InternalCanisterError : text; +}; + +type AppRevokeNotificationConsentError = variant { + // No live session behind this caller: expired, idle, revoked, or never one at all. + NoSuchSession; + InternalCanisterError : text; +}; + type NotificationGrantConsentRequest = record { anchor_number : UserNumber; origin : text; @@ -1836,15 +1885,35 @@ type NotificationConsentGrantedRequest = record { origin : text; }; -// What a browser uploads when it registers for Web Push, and how it replaces a pool -// that is running out: the same endpoint with a newer jwt_issued_at_ns. Signed with the -// browser key it signs in with, which is what says the subscription is this browser's. +// What a browser uploads when it registers for Web Push. Signed with the browser key it +// signs in with, which is what says the subscription is this browser's. type SetWebPushSubscriptionRequest = record { anchor_number : UserNumber; endpoint : text; // the relay URL the browser was issued - vapid_public_key : blob; // uncompressed SEC1 P-256, echoed to the relay as k= - jwt_signatures : vec blob; // one raw ECDSA signature per validity window - jwt_issued_at_ns : nat64; + // The subscription's own public key, uncompressed SEC1 P-256. Public: it decrypts + // nothing without a private scalar and forges nothing without the auth secret. + p256dh : blob; // 65 bytes + // The public half of the pair the browser minted to encrypt the auth secret with, + // uncompressed SEC1 P-256. The canister needs it to run its half of the agreement. + transport_key : blob; // 65 bytes + // The subscription's 16-byte auth secret under AES-128-GCM, 32 bytes with its tag. + // Encrypted because an ingress call is not confidential, and (endpoint, p256dh, auth) + // is all a forged push the browser will decrypt needs. It is encrypted to the + // transport key prepare_webpush_subscription minted for this caller, whose private half + // never leaves the canister and is dropped as soon as this call spends it. + sealed_auth : blob; // 32 bytes +}; + +// Both keys a browser needs before it can subscribe. +type PrepareWebPushSubscriptionResponse = record { + vapid_public_key : blob; // uncompressed SEC1 P-256 + transport_public_key : blob; // minted for this caller and this attempt +}; + +type PrepareWebPushSubscriptionError = variant { + // The caller signs with no key this identity is signed in from. + InvalidBrowserKey; + InternalCanisterError : text; }; type RemoveWebPushSubscriptionRequest = record { @@ -1855,8 +1924,10 @@ type RemoveWebPushSubscriptionRequest = record { type SetWebPushSubscriptionError = variant { // The caller signs with no key this identity is signed in from. InvalidBrowserKey; - // The pool offered is not newer than the one this endpoint already holds. - StaleJwtPool; + // Nothing here can open sealed_auth: the transport key it was encrypted to has + // expired, or a later prepare_webpush_subscription from this browser replaced it. Fetch the + // keys again and re-send. + TransportKeyExpired; InternalCanisterError : text; }; @@ -1865,17 +1936,151 @@ type RemoveWebPushSubscriptionError = variant { InternalCanisterError : text; }; +// Registers the one-shot key a browser is about to hand its install, so the app that +// spends it is the one this browser sent. Authorized by the caller being a browser this +// identity is signed in from; the principal named here is what authorizes the +// link_notification_app call that follows. +type PrepareNotificationAppLinkRequest = record { + anchor_number : UserNumber; + // The principal of the one-shot key this browser puts in the install URL. + link_principal : principal; +}; + +type PrepareNotificationAppLinkError = variant { + // The caller signs with no key this identity is signed in from. + InvalidBrowserKey; + // The anonymous principal is held by everyone, so an install offered to it is one + // anybody could complete. + LinkPrincipalIsAnonymous; + // This browser already points at an app that is delivering for this identity, and + // an install takes delivery off it. + AppAlreadyDelivering; + InternalCanisterError : text; +}; + +// Numbers a Home Screen app's record. Public, so every call naming one is checked +// against the principal the record belongs to. +type NotificationAppId = nat64; + +type LinkNotificationAppRequest = record { + anchor_number : UserNumber; + // The principal the app signs as, over the key it generated in its own partition. + app_principal : principal; + // The record this app already has, where it is linking a second identity to the app + // it is running as. Null is a first install and takes a record of its own. + app_id : opt NotificationAppId; +}; + +type LinkNotificationAppResponse = record { + // Kept by the app, so a later link names the record it already has. + app_id : NotificationAppId; +}; + +type LinkNotificationAppError = variant { + // The record is keyed by this principal, and the anonymous one is held by everyone, + // so any caller at all would resolve to that record. + AppPrincipalIsAnonymous; + // No browser of this identity is waiting for this caller, or the install it started + // has expired. + NoPendingLink; + // A browser of this identity already signs as that principal. + PrincipalAlreadyInUse; + InternalCanisterError : text; +}; + +// This identity asking the app its browser points at to deliver for it, which is also +// what asks the app to prove it is still there: a reference that is off is skipped when +// the canister picks who to send to, so the two cannot be separated. Probing first would +// find nothing to wake, and switching on first would announce it and then say the same +// thing again on the probe. The probe is the announcement. Where nothing can be woken the +// switch does not stay on. +// +// Never a subscription write: the subscription is the app's and is shared. +type EnableNotificationAppRequest = record { + anchor_number : UserNumber; +}; + +type EnableNotificationAppError = variant { + // The caller signs with no key this identity is signed in from. + InvalidBrowserKey; + // This browser points at no app. + NoNotificationApp; + // Nothing could be woken, so nothing was switched on: the app has registered + // nothing, the browser counts as gone, or there was no room for another outcall. + NotReachable; + InternalCanisterError : text; +}; + +// This identity asking the app its browser points at to stop delivering for it. Never a +// subscription removal: the subscription is the app's and is shared with every other +// identity pointing at it. +type DisableNotificationAppRequest = record { + anchor_number : UserNumber; +}; + +type DisableNotificationAppError = variant { + // The caller signs with no key this identity is signed in from. + InvalidBrowserKey; + // This browser points at no app. + NoNotificationApp; + InternalCanisterError : text; +}; + +// Asks which identities a Home Screen app delivers for, named by the app's own record. +type NotificationAppAnchorsRequest = record { + app_id : NotificationAppId; +}; + +// Offers the Home Screen app this browser has to another identity signed in on the +// same browser, which is the only party that can reach both. The handle says nothing +// about which app it is. +type OfferNotificationAppRequest = record { + anchor_number : UserNumber; +}; + +type OfferNotificationAppError = variant { + // The caller is no browser of this identity. + Unauthorized : principal; + // This browser has no app to offer. + NoNotificationApp; + InternalCanisterError : text; +}; + +// Points a browser of the identity being added at the Home Screen app another identity on +// the same device already has. One device has one such app, so a second identity joins the +// existing one rather than installing its own. +// +// The signature is the whole authority and nothing is stored between the two halves: a +// browser of the offering identity signs over both anchor numbers and the expiry, and this +// call spends it. Only a key that identity still holds verifies, and the expiry is bounded +// from above, so an old signature is worthless. +type ClaimNotificationAppRequest = record { + anchor_number : UserNumber; // the identity being added; the caller is a browser of it + offering_anchor_number : UserNumber; // the identity that has the app + expires_at_ns : nat64; // no more than 30 minutes ahead of canister time + signature : blob; // made by a browser of the offering identity +}; + +type ClaimNotificationAppError = variant { + // The key the caller signed with is no browser key of the identity it named. + InvalidBrowserKey; + // No browser of the offering identity signed this, or the one that did has + // rotated its key past it since. + InvalidClaimToken; + ClaimTokenExpired; + // The offering identity's browser points at no app to hand over. + NoNotificationApp; + InternalCanisterError : text; +}; + type GetWebPushSubscriptionStatusRequest = record { anchor_number : UserNumber; }; // What a browser is registered with, so the frontend can tell a registration that is -// still live from one another identity's re-subscribe left behind, and knows when to -// sign the next pool. +// still live from one another identity's re-subscribe left behind. type WebPushSubscriptionStatus = record { endpoint : text; // compared against the one the browser holds - pool_len : nat32; // windows covered, not a count of unused signatures - issued_at_ns : nat64; // window i expires at issued_at_ns + (i + 1) * window }; // ===== Notifications sent by an app ===== @@ -1884,6 +2089,11 @@ type WebPushSubscriptionStatus = record { // one origin. type NotificationId = nat64; +// Internet Identity's own id for one entry in one browser's inbox, assigned when the entry +// is deposited and unique within that inbox. A wake-up names its entry by this, so the +// service worker shows the notification the push was sent for. +type InboxNotificationId = nat64; + type Urgency = variant { VeryLow; Low; Normal; High }; type Notification = record { @@ -1968,39 +2178,101 @@ type NotificationDelegationError = variant { // ===== Notifications a browser has yet to show ===== -type GetNextNotificationRequest = record { +type GetNotificationRequest = record { anchor_number : UserNumber; + // Whose inbox holds it. An app's key signs for every browser pointing at the app, so + // the browser cannot be taken from the caller alone. + browser_id : nat32; + inbox_notification_id : InboxNotificationId; }; // What a service worker shows for one wake-up: the app and account it is for, the // canister holding its content, and which notification. type NotificationToShow = record { - origin : FrontendHostname; + // Null where Internet Identity sent this itself: it is about the identity rather + // than an app, so there is no origin to name and none is looked up. + origin : opt FrontendHostname; account_number : opt AccountNumber; // null = the unreserved default account canister_id : principal; id : NotificationId; + // Set where Internet Identity sent this itself, and what it has to say. An app's + // notification carries no text: the service worker fetches it from the app with a + // delegation it mints for the purpose, which is what keeps the content off the push + // service. One of these carries no text either, for the same reason, but there is + // nothing to fetch and nobody to mint for. The words are the frontend's, so this + // says what happened rather than what to show. + event : opt NotificationEvent; +}; + +// Something Internet Identity has to tell the user about their own identity. +type NotificationEvent = variant { + // This device now receives this identity's notifications. + NotificationsOn : IdentityForNotification; }; -// Null once nothing is left to show. -type GetNextNotificationResponse = record { +// Which identity a notification is about, in the terms the user knows it by: nobody +// recognises an identity by its number. +type IdentityForNotification = record { + anchor_number : UserNumber; + name : opt text; +}; + +// Asks the canister to wake the Home Screen app this browser points at, so the browser +// can learn whether it is still there. Nothing says when a user deletes a web app, so +// the only way to find out is to send it something and see what happens. +type ProbeNotificationAppRequest = record { + anchor_number : UserNumber; +}; + +type ProbeNotificationAppError = variant { + // The caller signs with no key this identity is signed in from. + InvalidBrowserKey; + // This browser points at no app, so there is nothing to wake. + NoNotificationApp; + // Nothing was sent, so there is nothing to watch for: the app has registered + // nothing, this identity has it switched off or is signed out on this browser, the + // browser counts as gone, or there was no room for another outcall. The browser's + // own registration is never woken instead. + NotReachable; + InternalCanisterError : text; +}; + +// Null where the named entry is gone: taken, expired, or dropped with the registration. +type GetNotificationResponse = record { notification : opt NotificationToShow; }; -type GetNextNotificationError = variant { - // Also a caller that is no browser of the identity. +type GetNotificationError = variant { + // The caller signs with no key this identity is signed in from, and with no key of + // an app one of its browsers routes through. + InvalidBrowserKey; InternalCanisterError : text; }; type RemoveNotificationRequest = record { anchor_number : UserNumber; - // As browser_get_next_notification returned it. - notification : NotificationToShow; + browser_id : nat32; + // As the wake-up that announced it named it. + inbox_notification_id : InboxNotificationId; + // Why the worker could not show it. Absent where it did. + not_shown : opt NotShownReason; +}; + +// Why the service worker could not show a notification: its call to the app's canister +// for the content failed, which only the device sees. +type NotShownReason = variant { + // The call got no answer. + PayloadUnreachable; + // The app's canister rejected the call or trapped. + PayloadRejected; }; type RemoveNotificationResponse = record {}; type RemoveNotificationError = variant { - // Also a caller that is no browser of the identity. + // The caller signs with no key this identity is signed in from, and with no key of + // an app one of its browsers routes through. + InvalidBrowserKey; InternalCanisterError : text; }; @@ -2505,18 +2777,67 @@ service : (opt InternetIdentityInit) -> { prepare_notification_delegation : (PrepareNotificationDelegationRequest) -> (variant { Ok : PrepareNotificationDelegationResponse; Err : NotificationDelegationError }); get_notification_delegation : (GetNotificationDelegationRequest) -> (variant { Ok : GetNotificationDelegationResponse; Err : NotificationDelegationError }) query; - // Called by the service worker on a wake-up, signed with the browser key: the oldest - // notification its browser has yet to show, passing over the ones it skips. - browser_get_next_notification : (GetNextNotificationRequest) -> (variant { Ok : GetNextNotificationResponse; Err : GetNextNotificationError }) query; + // Called by the service worker on a wake-up, signed with the browser key: the entry + // that wake-up named, where it has not expired and the identity still allows it. + browser_get_notification : (GetNotificationRequest) -> (variant { Ok : GetNotificationResponse; Err : GetNotificationError }) query; // Called by the service worker for the one it shows, while it fetches the content. browser_remove_notification : (RemoveNotificationRequest) -> (variant { Ok : RemoveNotificationResponse; Err : RemoveNotificationError }); - // Called by the browser itself, signed with the browser key it signs in with. + // The key a browser subscribes with and every push names. Absent only in the moment + // between the install returning and the answer to the read it starts. + // Both keys a browser needs before it subscribes: the one every push is signed with, + // and a one-shot key it encrypts this subscription's auth secret to. + // + // An update call, because a query reply is signed by one replica. One that answered + // with a VAPID key of its own choosing would bind the subscription to it permanently, + // and the browser has nothing to check the answer against. + // A cold randomness pool yields the `Pending` result arm — a retry signal, not an + // error: asking put the draw in flight, so the caller re-calls the method. + prepare_webpush_subscription : (UserNumber) -> (variant { Ok : PrepareWebPushSubscriptionResponse; Pending; Err : PrepareWebPushSubscriptionError }); + // Called by the browser, or by the Home Screen app it points at: what the app + // registers belongs to its own record, which every identity pointing at it is + // delivered through, so the anchor named here is what authorized the call rather + // than where the subscription goes. set_webpush_subscription : (SetWebPushSubscriptionRequest) -> (variant { Ok; Err : SetWebPushSubscriptionError }); // Called by the identity, so one browser can silence another. remove_webpush_subscription : (RemoveWebPushSubscriptionRequest) -> (variant { Ok; Err : RemoveWebPushSubscriptionError }); + + // Called by the browser starting an install, naming the one-shot key it puts in the + // install URL as the only caller that may complete the link. + prepare_notification_app_link : (PrepareNotificationAppLinkRequest) -> (variant { Ok; Err : PrepareNotificationAppLinkError }); + // Called by the Home Screen app on its first open, signed with that one-shot key. + // Creates the app's record where its key has none, and points the browser at it. + link_notification_app : (LinkNotificationAppRequest) -> (variant { Ok : LinkNotificationAppResponse; Err : LinkNotificationAppError }); + + // Called by the Home Screen app, naming the record it holds and signing as the + // principal that record belongs to. The identities whose browsers point at it, which + // is the whole of what it can deliver for. Empty in four cases, which all mean the + // same thing to the app: the id names nothing, nothing points at it, this deployment + // does not notify, or the caller is not the app that record belongs to. + notification_app_anchors : (NotificationAppAnchorsRequest) -> (vec UserNumber) query; get_webpush_subscription_status : (GetWebPushSubscriptionStatusRequest) -> (opt WebPushSubscriptionStatus) query; // Called by an app's backend canister for the origin it names. app_send_notification : (SendNotificationArg) -> (variant { Ok : SendNotificationResponse; Err : SendNotificationError }); + + // Called by the identity being added, from its own browser on the device, with a + // token the identity that has the app signed for it. + claim_notification_app : (ClaimNotificationAppRequest) -> (variant { Ok; Err : ClaimNotificationAppError }); + + // Called by the browser, which may hold nothing but its key: the install page is a + // tab of its own with no session. + get_notification_app_status : (GetNotificationAppStatusRequest) -> (NotificationAppStatus) query; + + // Called by the identity, from the browser that points at the app. + enable_notification_app : (EnableNotificationAppRequest) -> (variant { Ok : NotificationId; Err : EnableNotificationAppError }); + disable_notification_app : (DisableNotificationAppRequest) -> (variant { Ok; Err : DisableNotificationAppError }); + + // Called by the browser, which watches for the answer leaving its own queue: that + // is what says the app this browser points at is still installed. + probe_notification_app : (ProbeNotificationAppRequest) -> (variant { Ok : NotificationId; Err : ProbeNotificationAppError }); + + + // Called by an app's frontend, as its session. + app_notification_consent_status : () -> (variant { Ok : AppNotificationConsentStatusResponse; Err : AppNotificationConsentStatusError }) query; + app_revoke_notification_consent : () -> (variant { Ok; Err : AppRevokeNotificationConsentError }); };