docs: design for app sessions in the client library - #4279
Open
sea-snake wants to merge 85 commits into
Open
Conversation
The canister side of revocable sessions is built and no client uses it, so no application can reach the feature. This is the design for holding the session inside @icp-sdk/auth: an application keeps calling signIn() and getIdentity() and receives an identity, while the library acquires a session, mints five-minute app delegations from it, and revokes at sign-out. Sessions stay out of the public API. Adds the design and the specification it links to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
✅ No security or compliance issues detected. Reviewed everything up to 5be869d. Security Overview
Detected Code Changes
|
The design said a delegation is minted when a caller needs one, which hid the part that decides both the cost and the feel: an agent holds the identity for hours, so the identity refreshes from inside the per-request hook. Refresh is driven by requests and never by a timer. A request served with life still left starts a mint in the background; one arriving with almost none waits for it; an idle app makes no calls. Both thresholds are derived from what they cover rather than chosen, because minting early discards the rest of a delegation's life: a 45-second pre-mint threshold refreshes an active session about every four and a quarter minutes against a floor of five, where two minutes would refresh every three and add two thirds again to the update calls of every active session. Records the reason a timer is wrong beyond its cost, that the last-refreshed stamp is what II shows the user as use; the guard against minting without end against an almost expired session, since a delegation lasts min(5 minutes, what remains of the session); and that mint calls need no new configuration, taking the canister from the session chain's targets and the host from the identity provider's origin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a design + requirements specification for implementing revocable app sessions inside @icp-sdk/auth’s AuthClient, including on-demand minting/refreshing of short-lived (5-minute) app delegations and canister-side revocation on signOut().
Changes:
- Document the client-library architecture: session stored in
AuthClient, app delegations minted/rotated from the session, and revocation on sign-out. - Specify refresh behavior driven by requests (no timers), including thresholds, concurrency (single in-flight mint), and failure classification.
- Specify compatibility fallback to
icrc34_delegationwhenii_session_delegationis not available.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| docs/ongoing/client-app-sessions.md | High-level design and rationale for sessions living inside AuthClient, including request-driven refresh and sign-out revocation. |
| docs/ongoing/client-app-sessions-spec.md | Detailed requirements/spec for acquisition, minting/refresh, failure handling, hint semantics, and compatibility behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Refreshing whenever a request arrived with little life left needed a threshold wide enough for a request to turn up inside it, and everything a threshold discards is paid for by every active session forever. Scheduling one mint for the moment it is needed reduces the threshold to what covers a mint, so an active session refreshes at nearly the five-minute floor rather than every four and a quarter, and an application making a request every couple of minutes stops stalling. The schedule checks at fire time that the application is still active, without which an application idle since seconds after its last request would refresh anyway and stamp the session as used. Requests stay the guarantee, since browsers throttle timers in hidden tabs and fire them late after a sleep. Also records that the account principal is not derivable from the session chain, which is rooted at the session's own key rather than the account's, so it is taken from the first mint and stored beside the chain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sequence showed getIdentity() calling the canister, which is not what happens and would have an implementer minting once per call. It now shows the mint as part of signing in, getIdentity() returning the same identity without a call, and requests being signed from what that identity holds. Adds the refresh sequence to the design, so the invisibility of a mint landing between two requests is visible, and a flowchart to the spec covering every path that reaches a mint with its guards: the block margin, the fire-time activity check, the almost expired session, and what a failure does in the foreground against the background. Records why the identity refreshes itself rather than AuthClient handing out a fresh one per call: an application gives the identity to an agent once and the agent keeps the object, so a snapshot would sign with a dead delegation and no later call would reach it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AuthClient is for Internet Identity, so signIn() asks for a session and expects one. Carrying a second acquisition path, and inspecting advertised scopes to choose between them, is overhead for a case the library does not serve. A provider that cannot answer is a failed sign-in. Recorded in out of scope so the negotiation is not added back on the assumption it was overlooked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One graph covering request arrival, the scheduled fire, two further entry points, the call itself and failure classification laid out as unreadable spaghetti: four concerns and four converging entry points is more than the layout can place. Three small charts instead, one question each: what a request finds, what a scheduled mint checks, and how a mint ends. The entry points that need no decision, and the one-in-flight rule, are a sentence rather than nodes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guard said the application had to have been active recently, which named no signal and no window, so it could not be implemented or tested as written. A scheduled mint now cancels unless the delegation it is replacing signed at least one request. Signing is the only activity the library can observe, and the replaced delegation's own lifetime is the window, since that delegation is what the question is about. No constant, and nothing to tune: an application making a request at least once per delegation lifetime refreshes for as long as that holds, and one that stops refreshes once more and then lets the next lapse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check read as "was this delegation ever used", which suggests one request keeps a session refreshing for its whole thirty days. The rule already stops after one extra refresh, because the delegation a refresh produces has to be used to justify the refresh after it, but nothing said so. The diamond now asks whether this delegation signed a request, and both documents state that the question is asked of each delegation separately, which is what bounds the chain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A backgrounded tab has its timers throttled, so its delegation lapses and the first click after the user returns waits for a mint. Becoming visible or regaining focus is a second or two ahead of that click, which is room to hide one. The identity still decides whether a mint is due, so a glance at a tab holding a healthy delegation costs nothing. AuthClient runs outside a browser too, so the identity holds no reference to a DOM: AuthClient calls a refresh entry point on it, and the listening lives in a separable piece constructed only where those APIs exist, as idle detection already is. On by default, off with disableForegroundRefresh, released by dispose(). Where there is no DOM the schedule and the request paths are the whole mechanism and nothing is incorrect, a request after a long gap simply waits. CookieDelegationStorage reads the same events inline and is explicitly not the model: a cookie store is browser-only by definition and may assume what it needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check_session is II's own, for the silent re-auth path, and the overview says so. Having the client library call it contradicted a document one PR below this one. Nothing is lost by removing it. The library does not ask whether a session is alive, it tries to use it, and a mint answering NoMatchingSession is the answer. A page load already starts a mint in the background, so the mint it needs anyway is also the check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A store shaped around a bare delegation chain has nowhere to put the account key, which is what lets a restored session answer for its principal without minting, and its cross-subdomain hint would take a five-minute app delegation's expiry where a sibling needs the session's. So the client's store holds a session: the chain and the account key as one record. The distinction is not cosmetic, since a session is what delegations are minted from and outlives any one of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec said the request carries no lifetime, which read as a property of sessions when it was only true of the method as first built: the plumbing for a requested maximum runs all the way to valid_for on the ceremony, and the session handler was passing undefined where the ICRC-34 one passes what the app asked for. maxTimeToLive keeps meaning what it meant for a delegation, the longest the thing being granted may live, and stays a ceiling: the user's choice at consent wins over it, an organization's cap narrows it further, and the canister clamps the result. An access level remains the user's alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It reads the stored session and nothing else, so a page load answers without a mint, a network call or an asynchronous store, which is what storing the account key alongside the chain buys. It is also optimistic about revocation, and that is a change in kind: the answer could previously go stale only by expiry, which a client can compute, and can now go stale because someone acted. A session revoked at the canister or from another browser reads as authenticated until a mint is told otherwise, which is the only thing that consults the canister. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MINT-1 said getIdentity() resolves to an identity carrying an app delegation, while the page-load requirement says a restored session mints in the background without getIdentity() waiting. Both cannot be true, and an implementer following the first would block a reload on a round trip. It now says the identity obtains a delegation when one is needed, with the two cases named: signing in leaves it holding the one minted during the ceremony, a restored session starts with none. Adds what the chain reports before the first mint, since an empty chain rooted at the account key is what no authority yet looks like and a consumer reading it deserves to know. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tabs of an origin share their storage, so they share the session, but not the delegation minted from it: the app key lives in memory, so each tab mints for a key of its own and five tabs cost five update calls and five stable writes every five minutes for one person's one session. They can share it without persisting anything. A non-extractable key survives a structured clone, which is what lets one live in IndexedDB, and the same property lets it cross a BroadcastChannel as a handle that signs and cannot be exported. A tab opening asks and adopts what a running tab offers, so it mints nothing. The design is the coordination, not the sharing, because the two things wanted pull apart: not minting five times wants one tab responsible, not failing to mint wants no tab load-bearing. So coordination may only suppress a mint and never be required for one. Every tab schedules its own refresh as if alone; the channel only offers the chance to stand down. With every message lost each tab mints, which is the cost of no coordination rather than a failure, and no tab waits on another when none acts. Records what makes suppression work in practice: jittered wake-ups, because a shared expiry otherwise wakes every tab in the same instant; re-reading before minting; announcing a mint before making it, which narrows the double-mint window from a mint to a message; and a timeout on a stood-down tab, whose failure mode is a second mint rather than none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pieces are shared at different distances and the design never said so: one session per domain, a chain and a channel per origin, a hint cookie across siblings. A diagram now shows the nesting, and the consequence is stated plainly, that the floor is one mint per active origin rather than one per domain, since each origin signs with its own key and the only thing crossing between siblings is a cookie a chain does not fit in. That mismatch hid a defect. One session serves every sibling, so signing in on one replaces what the other holds, and the other finds out on its next mint. Removing the shared hint there would tell the sibling that just signed in that its session is gone. So the acts are separated: signing out removes the local session and the hint, finding out a chain is stale removes the local session only, which leaves the discovering sibling in the state silent re-auth exists for. A sequence diagram walks that recovery, and records why it works rather than leaving it looking like luck: II keeps its record under the effective origin siblings share, so the ceremony replaced the record the other sibling then finds. Adds that a hint may outlive its session and so is never authority to skip asking the user, and that two siblings asking at once is safe because asking without rendering never creates a session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design said a chain does not fit in a cookie, which is a claim about size and the wrong reason. A delegation is issued to a key, and each origin signs with a key of its own, so one minted for a sibling authorises nothing here. Sharing a delegation would mean sharing the key it was issued to, and a cookie carries bytes, so sharing a key through one means handing out the private material to anything on the domain that reads a cookie and to every request that carries it. That is also what makes the difference between the two mechanisms legible: tabs of an origin pass a key handle that signs without being exportable, and there is no such thing to pass between origins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thirty seconds of jittered wake-up is ten per cent more refreshes on every active session, which is the waste this design rejects elsewhere, and the announce and stand-down protocol it needed was an approximation of a lock. A named lock does the job properly. Tabs that wake together queue rather than each starting a mint, which removes the double-mint window instead of narrowing it, so no wake-up has to be spread out. Whoever holds it looks again before acting, since the delegation may already have been replaced. And liveness stops resting on a timeout: a browser releases a lock when the context holding it goes away, so a tab closed mid-mint lets the next one proceed and nothing has to guess how long to wait for a tab that is not coming back. Where there is no such lock every tab mints, which is the cost of no coordination rather than a failure, so the lock is relied on for what this costs and never for whether it works. A request that needs a delegation now queues on it too: waiting costs at most the mint it would have made, and often ends with another tab's delegation and no mint at all. Drops the wake-up jitter, the claim window and the mint result window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tab adopted a key only at first use and only if an answer arrived in time, which fails in the most ordinary case there is: a browser restoring several tabs at once has them all ask in the same instant with none of them able to answer yet, so each makes its own and the origin ends up with as many keys, and as many mints, as it has tabs. Nothing converged afterwards either. Acquiring under the lock a mint already takes makes that start sequential: the first tab makes a key and every tab after it finds someone able to answer. A tab's key is settled for as long as it lives, since adopting one later would mean throwing away a working key and the delegation issued to it, possibly with a mint in flight, to save a call. Without a lock tabs may each keep their own and never converge, which is the degradation already accepted and is bounded by the number of tabs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app key was a long-lived thing an origin shared and tabs had to agree on, which needed a lock around acquiring it, a rule that a tab's key never changes, and an argument about why late convergence was not worth it. None of that is needed if the key is generated by the mint that gets a delegation for it. A key outliving its delegation carries no authority, so there was never a reason to keep one. A mint makes a key and gets a delegation for it in one act, and the pair expires and is replaced together, which is where rotation comes from now rather than from a policy. Sharing the pair is also what makes tabs converge with nothing electing a winner. Two tabs that each ended up with a pair, because a browser restored them in the same instant with neither able to answer, converge at the next mint: one pair is produced, its broadcast reaches both, both adopt it. Divergence costs an extra mint or two and lasts at most one delegation's lifetime, where sharing a key alone would have left an origin minting once per tab for as long as those tabs lived. Records that replacing a pair cannot disturb a request in flight, since a request is signed and its delegation attached in the same act. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The identity is one object for the life of the session while what it signs with is replaced every five minutes, and nothing said how both hold. It reaches the current pair rather than holding one, which is also why an application can keep the identity across a rotation without noticing: it never sees the app key, and the principal it does see comes from the account. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The client derived all three of them: the canister id from the session chain's targets, the agent host from the origin of the authorize URL, and root-key fetching from whether that origin was loopback. The first reads the library's own configuration out of a credential, and the second treats a URL's origin as a promise about which canister answers there, which holds for the deployment the default names and for no other. So the identity provider is two values, the authorize URL and the canister id, each defaulting to its mainnet value, and the agent gets its own options. The chain's targets keep their part as a check: a session chain names that canister and nothing else, and the unrestricted case is refused hardest, because the session key signs with that chain. AGENT-1 through AGENT-5 replace the four that said none of this was configured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design argued about three constants it never stated, so the prose reasoned in ratios where a number would have done. It now states all five once, next to the argument that needs them, and eight tables carry what enumerative paragraphs were carrying: the two keys side by side, what is stored and where, the three sharing scopes, every trigger that can start a mint, the outcomes of a failure, signing out against finding out, and the two addresses Internet Identity is configured as. Three claims were wrong. A tab does wait for another tab's pair, for 200 milliseconds, which the design denied while never mentioning the window at all. The Summary described a library that mints only when a call needs one, which is not what the Approach builds. And nothing said that upgrading signs everyone out, which was inferable only from two out-of-scope bullets. The scope diagram moved below the paragraphs that define its nouns, gained the session key it had omitted, and lost an edge that pointed out of a subgraph and meant nothing. The tab section, sixty lines carrying four arguments, is now four subsections with a sequence for the channel, a flowchart for the lock, and its rules as a numbered list rather than the same rule stated three times. In the spec, AGENT-5 said an unrestricted session chain is accepted where the implementation refuses it, which is the reversal that survived when the check was tightened. MINT-17 cited the requirement forbidding a persisted delegation instead of the one persisting the account key. The constants table had its columns the wrong way round and omitted the channel name, the lock name, the mainnet defaults and the cookie attributes, so four requirements could not be audited. A state machine, a storage matrix, a request matrix and an options table replace prose that was carrying the same content, and ten requirements that had imported the design's argumentation now carry the requirement alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The account key appeared only in a diagram note, two diagram nodes and a storage row, never in prose, and the storage row put it in localStorage next to a section saying keys live in non-extractable IndexedDB. A reader concludes a third private key is stored in the clear. It is a public key, it is the `user_key` the canister returns, and the document now says so where the other two keys are introduced. The session record was keyed three incompatible ways: four components in one paragraph, one record per browser in a table, three components and no browser in the recovery argument. It is keyed on the identity, the application, the account and the browser, in all three places. One sentence had no main clause, left by an edit that spliced a definition into another clause. Two diagrams said the opposite of the design: the lock flowchart asked whether a lock was available, which reads as whether it is free, so its yes branch appeared to queue when the lock was taken; and the tab handshake drew its branch as two consecutive notes, so read literally a tab adopted an offered pair and then minted one anyway. The first is relabelled, the second is an alt/else, and the hint cookie's read is an arrow from the cookie rather than to it. The refresh section ran sixty-three lines with no subheadings while a shorter section had three. It has four. The count above the durations table said four lifetimes where the table lists three and two marks. Line wrapping was hard-wrapped in the middle third and one line per paragraph either side, which is two formatter passes stitched together. The whole file now matches the sibling designs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One construction was carrying most of the explanatory sentences in both files. The colon-reveal and "rather than" pairing is down by roughly half, thirty-two sentences whose only function was transition are gone, eighteen openings that announced a count a table already carried are gone, and eleven paragraphs the spec had imported from the design are back to one copy. Four requirements said things the implementation does not do. KEY-4 forbade any public export from returning the session key, which a pluggable store cannot honour, so it is now about the methods of AuthClient. ERR-3 forbade a key without its chain, which the restore path produces on purpose, because the redirect flow writes a key on the outbound load and deleting it would destroy a sign-in in progress. MINT-13 promised an unconditional page-load mint when that mint arrives through the foreground trigger and inherits its conditions. API-1 called identityProvider the only breaking change when the storage option split, keyType went, and five exports went with them. Two behaviours had no requirement. A delegation carrying a permissions field is refused and the session kept, which is a sixth mint outcome the five-way diagram did not have. And a hint whose derived expiry has already passed removes the cookie instead of writing a stale one, so signing out is not the only path that clears it. API-2 said isAuthenticated reads the stored session and nothing else. With the cookie store it also reads the hint and drops the local copy when the cookie names another account, which is how a sign-out on a sibling reaches this origin. The cookie's attributes are stated as the code sets them, Secure included, which follows the scheme rather than loopback. Requirement labels are back on their own lines after a reflow ran them into their text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An application supplies four leaves in two pairs and nothing composes them, so a namespace is what changes slots and it changes all four. A leaf declares whether another tab reads it, which is the one thing only it knows; AuthClient owns the mint lock and takes it when both halves of the app pair are shared, since sharing needs both. A spent app delegation keeps its chain and loses its key: the chain signs nothing without it and is the only record of the account, so the account key stops being a stored field. One cookie per delegation slot, the account from the app's and the session's expiry from the session's, assembled by AuthClient. Signing out steals the lock, so a mint in flight discards its result instead of writing a pair back over a cleared store. Memory leaves ship alongside the rest, with what backs them and what the configuration gives up.
The hint is one record — the account's principal and the session's expiry — and neither fact belongs to a credential, so it stops being published per delegation slot and becomes a leaf supplied beside the two pairs. That removes what the per-slot binding had made necessary: a cookie each, half of both unread, and an app cookie whose lifetime had to be stretched from five minutes to the session's because its own chain said nothing useful. One record's expiry is the session's. A store now holds two fields and knows nothing about chains, so the derivation lives in the library and the same two implementations serve every configuration: a cookie to reach a sibling, localStorage to answer this origin before a memory-backed pair has anything.
A credential is an identity and the delegation authorising it, so it is one record under one slot. That makes the torn half unwritable rather than something every reader has to interpret, and it retires the requirements that existed to say what a mismatch meant. Synchronicity moves to where the requirement actually is. Two questions cannot wait — who is signed in, and until when — and those two facts are the published record, so the credentials become asynchronous and the client answers from what it read at construction. The hint becomes that record's store and is named for what it holds. A store declares two facts about its medium, `shared` and `durable`, and nothing else about coordination is configurable: the lock is the library's, the notification is the library's channel unless a store has to move values itself, and a change is readable before it is announced. A ceremony's key gets its own slot and is promoted on return, so a sign-in that is abandoned or overtaken cannot disturb a live session, and a redirect with a store that can neither keep nor share the key is refused before it navigates.
The record of who is signed in and until when is not a summary of the credentials; it decides what the sign-in is. Where the two disagree the state wins and the material is discarded, and nothing runs the other way — missing or spent material is a reason to acquire more, never a reason to change who is signed in. One rule then covers a sign-out in this tab, in another, on a sibling subdomain, and a session that simply ended. isAuthenticated() and getPrincipal() read it, so they stay synchronous without the client caching credentials and without an answer that is only as fresh as its last read. The credential store loses subscribe and the client loses the channel it would have carried: a mint changes material, and a tab that wants a delegation takes the lock and reads rather than waiting to be told. The state store is the only notification path left, and every medium it uses already raises an event. An expired app credential is removed whole. Keeping its spent chain was only ever to remember the account, which the state now holds.
…er will not resume
Seven days where an application asks for nothing, so the bound applies to the sessions it exists for rather than only the ones that opted in. Under this library's 8-hour ceiling it is clamped away and changes nothing yet; it is what holds once that ceiling rises, which is where the note now says so.
The idle bound, activity as its trigger and resumability were recorded as not built. All three landed, so the "not built yet" table is gone and the stages are grouped as the two stacks they are, with the guide and the shared-memory store that were missing from the list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #4228. Fourth design in the set, below the overview. Documentation only.
The canister side of revocable sessions is designed and built across #4224 and #4228, and nothing calls it.
@icp-sdk/authholds no session, mints no app delegations and revokes nothing at sign-out, so no application can reach the feature, and the guide for sharing a sign-in across sibling subdomains promises a sign-out propagation that no code performs.Adds
docs/ongoing/client-app-sessions.mdanddocs/ongoing/client-app-sessions-spec.md. The design puts the session insideAuthClient:signIn()acquires one, the identity handed to the application carries a five-minute app delegation that it replaces as it ages, andsignOut()callsapp_revoke_sessionbefore clearing local state. Sessions add no public type, option or method, so an application callssignIn()andgetIdentity()exactly as it does today.Read the refresh section hardest, and its two diagrams: a sequence showing a mint landing between two requests where nothing notices, and a flowchart in the spec covering every path that reaches a mint with the guard on each.
Refresh is armed by requests and scheduled for the moment it is needed, never run on a recurring timer. That is what keeps the pre-mint threshold at 15 seconds: a threshold that instead has to catch a passing request must be wide enough that one turns up inside it, and everything it discards is paid for permanently, since minting early throws away the rest of a delegation's life. An active session refreshes about every four and three quarter minutes against the canister's five-minute floor; a two-minute threshold would refresh every three and add two thirds again to the update calls and stable writes of every active session.
The decisions most likely to be wrong if left implicit:
app_prepare_delegationstamps the session's last-refreshed time, which is what II's settings screen shows the user as use and what the session cap reclaims on. Refreshing on a schedule regardless of use turns that column into "has a tab open". The scheduled mint therefore re-checks activity at the moment it fires and cancels if there has been none.min(5 minutes, what remains of the session), so refreshing on the delegation's remaining life alone mints without end against an almost expired session. A session with less than the block margin left is over.AuthClientdoes not hand out a fresh one per call. An application gives the identity to an agent once and the agent keeps the object, so a snapshot of one delegation would sign with it until it expired and no later call would reach the agent holding it.account_principalbut the transport result carries only the chain, so it comes from the first mint'suser_keyand is stored beside the chain, which is what lets a reload answergetPrincipal()without minting.CookieDelegationStoragederives its hint from whatever delegation it is handed, so a five-minute one would tell a sibling the session expires in five minutes and there is nothing to resume.NoMatchingSessionis terminal and everything else is transient, in the background as well as the foreground. Signing a user out because the network dropped is worse than a call that failed.Internet Identity is configured as two values, the authorize URL a ceremony renders at and the canister that mints, each defaulting to its mainnet value, with the agent's own options passed through. Deriving either from the other reads well until an application needs a deployment of its own: a URL's origin is not a promise about which canister answers there, and taking the canister id from the session chain would leave the library reading its own configuration out of a credential. The chain's
targetskeep their part as a check, refusing a chain that names another canister or none at all.Also corrects #4228, which said the client side "already exists" in
@icp-sdk/auth. It is specified there and not built.