Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
bd7536b
feat(be): register the browser a session was created from
sea-snake Aug 19, 2026
1d022d3
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Aug 25, 2026
3db45a5
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Aug 25, 2026
798ba8a
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Aug 25, 2026
5a8840e
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Aug 31, 2026
19aa239
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Aug 31, 2026
22846fc
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Aug 31, 2026
e1b87ea
fix(be): a browser cannot name itself its own successor
sea-snake Sep 1, 2026
7f76d6d
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 1, 2026
37f3f0d
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 1, 2026
07e6f08
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 4, 2026
ade6372
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 4, 2026
3d12799
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 4, 2026
40ba0b2
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 4, 2026
42619ad
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 4, 2026
ace5663
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 4, 2026
32775a1
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 4, 2026
5214ec8
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
6738a36
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
f1058d7
feat: accept only the successor a browser announced
sea-snake Sep 5, 2026
6dbe7af
fix: a browser retrying a lost sign-in keeps its own successor
sea-snake Sep 5, 2026
c4a3740
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
eabba79
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
28eb159
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
ff6d8bd
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
a7f91c3
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
cb3c7db
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
6b85363
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
f4f9509
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
864665e
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
e2e5ee3
refactor(be): name the account reference list rather than a row
sea-snake Sep 5, 2026
e1d55df
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 5, 2026
ec58360
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 6, 2026
0cba35f
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 6, 2026
ff2c444
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 6, 2026
9206522
refactor(be): a browser, not a session device
sea-snake Sep 6, 2026
d60ae4b
refactor(be): a browser's stale key says browser
sea-snake Sep 6, 2026
68ddb99
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
e258533
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
db6f3f9
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
2b81c84
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
7d46b04
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
ec901e7
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
8befb66
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
a51d25e
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
fe562e4
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
6c6fd7e
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
dd819f0
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
9a1cf63
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 7, 2026
2281b22
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 8, 2026
9a88ba0
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 8, 2026
7e69b08
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 8, 2026
d417061
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 8, 2026
a9d363b
chore: merge feat/session-record-storage
sea-snake Sep 8, 2026
d8d8edb
chore: merge feat/session-record-storage
sea-snake Sep 8, 2026
3a8668d
chore: merge feat/session-record-storage
sea-snake Sep 8, 2026
4835dcf
feat(internet_identity): a browser describes itself in tokens, not a …
sea-snake Sep 8, 2026
22e4612
feat(internet_identity): name the six browsers that earn an icon
sea-snake Sep 9, 2026
fbfeed3
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 9, 2026
fa1bf88
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 9, 2026
a13f19e
Merge branch 'feat/session-record-storage' into feat/session-devices-…
sea-snake Sep 9, 2026
a727d29
refactor(browsers): the anchor decides what a browser reveals
sea-snake Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/frontend/src/lib/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,43 @@ export const idlFactory = ({ IDL }) => {
'address' : IDL.Text,
'last_used' : IDL.Opt(Timestamp),
});
const OperatingSystem = IDL.Variant({
'Ios' : IDL.Null,
'Linux' : IDL.Null,
'Android' : IDL.Null,
'Macos' : IDL.Null,
'ChromeOs' : IDL.Null,
'Windows' : IDL.Null,
'Other' : IDL.Text,
'Ipados' : IDL.Null,
});
const FormFactor = IDL.Variant({
'Unknown' : IDL.Null,
'Tablet' : IDL.Null,
'Desktop' : IDL.Null,
'Mobile' : IDL.Null,
});
const BrowserBrand = IDL.Variant({
'Edge' : IDL.Null,
'Firefox' : IDL.Null,
'Safari' : IDL.Null,
'SamsungInternet' : IDL.Null,
'Opera' : IDL.Null,
'Other' : IDL.Text,
'Chrome' : IDL.Null,
});
const BrowserDescription = IDL.Record({
'os' : OperatingSystem,
'model' : IDL.Opt(IDL.Text),
'form_factor' : FormFactor,
'brand' : BrowserBrand,
});
const BrowserInfo = IDL.Record({
'id' : IDL.Nat32,
'description' : BrowserDescription,
'created_at' : Timestamp,
'last_used' : Timestamp,
});
const McpConfig = IDL.Record({
'url' : IDL.Opt(IDL.Text),
'enabled' : IDL.Bool,
Expand All @@ -575,6 +612,7 @@ export const idlFactory = ({ IDL }) => {
'name' : IDL.Opt(IDL.Text),
'email_recovery' : IDL.Opt(IDL.Vec(EmailRecoveryCredential)),
'created_at' : IDL.Opt(Timestamp),
'browsers' : IDL.Opt(IDL.Vec(BrowserInfo)),
'mcp_config' : IDL.Opt(McpConfig),
'authn_method_registration' : IDL.Opt(AuthnMethodRegistrationInfo),
'openid_credentials' : IDL.Opt(IDL.Vec(OpenIdCredential)),
Expand Down
70 changes: 70 additions & 0 deletions src/frontend/src/lib/generated/internet_identity_types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,14 @@ export interface EmailRecoveryGetDelegationArgs {
'expiration' : Timestamp,
'nonce' : string,
}
/**
* Reported where the client can state it and inferred where it cannot, so unknown is a
* real answer: the browsers exposing no client hints are the ones this is least sure of.
*/
export type FormFactor = { 'Unknown' : null } |
{ 'Tablet' : null } |
{ 'Desktop' : null } |
{ 'Mobile' : null };
export type FrontendHostname = string;
export type GetAccountError = {
'NoSuchOrigin' : { 'anchor_number' : UserNumber }
Expand Down Expand Up @@ -942,6 +950,12 @@ export interface IdentityInfo {
* The timestamp at which the anchor was created
*/
'created_at' : [] | [Timestamp],
/**
* Browsers this anchor has signed in from (absent when it has never
* created a session), so the Settings UI can offer "sign this browser
* out" without a separate call.
*/
'browsers' : [] | [Array<BrowserInfo>],
/**
* The anchor's synced trusted-MCP-server config (absent when the
* anchor never wrote one). Carried here rather than read from the
Expand Down Expand Up @@ -1297,6 +1311,14 @@ export interface OpenIdPrepareDelegationResponse {
'expiration' : Timestamp,
'anchor_number' : UserNumber,
}
export type OperatingSystem = { 'Ios' : null } |
{ 'Linux' : null } |
{ 'Android' : null } |
{ 'Macos' : null } |
{ 'ChromeOs' : null } |
{ 'Windows' : null } |
{ 'Other' : string } |
{ 'Ipados' : null };
/**
* The delegation permissions a caller requests, mirroring the ICP protocol's
* request-delegation `permissions` values. `queries` yields a queries-only
Expand Down Expand Up @@ -1528,6 +1550,54 @@ export type Salt = Uint8Array | number[];
export type SessionDelegationError = { 'NoSuchDelegation' : null } |
{ 'InternalCanisterError' : string } |
{ 'Unauthorized' : Principal };
/**
* Which browser a sign-in came from, as a token rather than a name to show. Products
* get renamed — "Chrome OS" became "ChromeOS", "Mac OS X" became "macOS" — so the name
* the user reads is derived in the frontend, where a rename reaches every stored record
* at once. "Brand" is what the client hints call this, and BrowserInfo below is the
* entry it describes.
*/
export type BrowserBrand = { 'Edge' : null } |
{ 'Firefox' : null } |
{ 'Safari' : null } |
{ 'SamsungInternet' : null } |
{ 'Opera' : null } |
{
/**
* A browser this list does not name, shown as the client resolved it. Worth seeing
* rather than hiding behind a generic label. Named variants are the six that hold
* 97% of the web between them, because a variant is what earns an icon.
*/
'Other' : string
} |
{ 'Chrome' : null };
/**
* What a browser reported about itself when it registered. Self-reported, so it is
* something the user reads to recognise their own browser rather than evidence about
* where a session came from. The canister stores these and never interprets them.
*/
export interface BrowserDescription {
'os' : OperatingSystem,
/**
* The hardware, where the client can name it — Android is the only place that does.
*/
'model' : [] | [string],
'form_factor' : FormFactor,
'brand' : BrowserBrand,
}
export interface BrowserInfo {
'id' : number,
/**
* Fixed at registration. A sign-in reporting something else registers its own entry,
* so this describes a registration rather than the last sign-in.
*/
'description' : BrowserDescription,
'created_at' : Timestamp,
/**
* Advanced by a sign-in from this browser and by every session refresh it drives.
*/
'last_used' : Timestamp,
}
export type SessionKey = PublicKey;
export type SetDefaultAccountError = {
'NoSuchOrigin' : { 'anchor_number' : UserNumber }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const baseIdentityInfo: IdentityInfo = {
created_at: [],
authn_method_registration: [],
openid_credentials: [],
browsers: [],
mcp_config: [],
};

Expand Down
46 changes: 46 additions & 0 deletions src/internet_identity/internet_identity.did
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,48 @@ type IdentityAuthnInfo = record {
recovery_authn_methods : vec AuthnMethod;
};

// Which browser a sign-in came from, as a token rather than a name to show. Products
// get renamed — "Chrome OS" became "ChromeOS", "Mac OS X" became "macOS" — so the name
// the user reads is derived in the frontend, where a rename reaches every stored record
// at once. "Brand" is what the client hints call this, and BrowserInfo below is the
// entry it describes.
type BrowserBrand = variant {
Chrome; Safari; Firefox; Edge; Opera; SamsungInternet;
// A browser this list does not name, shown as the client resolved it. Worth seeing
// rather than hiding behind a generic label. Named variants are the six that hold
// 97% of the web between them, because a variant is what earns an icon.
Other : text;
};

type OperatingSystem = variant {
Macos; Ios; Ipados; Windows; Android; ChromeOs; Linux; Other : text;
};

// Reported where the client can state it and inferred where it cannot, so unknown is a
// real answer: the browsers exposing no client hints are the ones this is least sure of.
type FormFactor = variant { Desktop; Mobile; Tablet; Unknown };

// What a browser reported about itself when it registered. Self-reported, so it is
// something the user reads to recognise their own browser rather than evidence about
// where a session came from. The canister stores these and never interprets them.
type BrowserDescription = record {
brand : BrowserBrand;
os : OperatingSystem;
form_factor : FormFactor;
Comment thread
sea-snake marked this conversation as resolved.
// The hardware, where the client can name it — Android is the only place that does.
model : opt text;
};

type BrowserInfo = record {
id : nat32;
// Fixed at registration. A sign-in reporting something else registers its own entry,
// so this describes a registration rather than the last sign-in.
description : BrowserDescription;
created_at : Timestamp;
// Advanced by a sign-in from this browser and by every session refresh it drives.
last_used : Timestamp;
};

type IdentityInfo = record {
authn_methods : vec AuthnMethodData;
authn_method_registration : opt AuthnMethodRegistrationInfo;
Expand All @@ -1026,6 +1068,10 @@ type IdentityInfo = record {
// shows a "limit reached" notice in the wizard when adding
// beyond the cap.
verified_emails : opt vec VerifiedEmail;
// Browsers this anchor has signed in from (absent when it has never
// created a session), so the Settings UI can offer "sign this browser
// out" without a separate call.
browsers : opt vec BrowserInfo;
// The anchor's synced trusted-MCP-server config (absent when the
// anchor never wrote one). Carried here rather than read from the
// mcp_get_config query so the Settings UI has a certified value to
Expand Down
2 changes: 2 additions & 0 deletions src/internet_identity/src/email_recovery/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ mod tests {

fn anchor_with(address: Option<&str>) -> Anchor {
let mut a = Anchor {
browsers: vec![],
next_browser_id: 0,
anchor_number: 1,
devices: vec![],
openid_credentials: vec![],
Expand Down
3 changes: 3 additions & 0 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,8 @@ mod v2_api {
Some(stored_verified_emails)
};

let browsers = state::anchor(identity_number).browsers_info();

let identity_info = IdentityInfo {
authn_methods: anchor_info
.devices
Expand All @@ -1113,6 +1115,7 @@ mod v2_api {
created_at: anchor_info.created_at,
email_recovery,
verified_emails,
browsers,
// The same config `mcp_get_config` serves, but certified: this is
// an update call, so the Settings UI can render the trusted server
// — and base the config it writes back — on a value no single node
Expand Down
2 changes: 2 additions & 0 deletions src/internet_identity/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,8 @@ impl<M: Memory + Clone> Storage<M> {
created_at_ns: _,
name: _,
verified_emails: _,
browsers: _,
next_browser_id: _,
}) = previous_anchor_maybe
{
(
Expand Down
Loading
Loading