Skip to content

feat(be): record that a session is still in use - #4244

Merged
sea-snake merged 104 commits into
feat/app-delegation-from-sessionfrom
feat/session-refresh-stamps
Sep 10, 2026
Merged

feat(be): record that a session is still in use#4244
sea-snake merged 104 commits into
feat/app-delegation-from-sessionfrom
feat/session-refresh-stamps

Conversation

@sea-snake

@sea-snake sea-snake commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Design: #4224. Overview: #4230.

"This browser used this app 3 minutes ago" against "5 weeks ago" is what makes a session list worth reading, and what lets someone spot a session they do not recognise still being used rather than merely still existing.

Every refresh stamps, and three consumers make coarsening it unattractive: the session cap orders live sessions on this field, so inside a coarsening interval every session would look equally idle; the browser registry cap orders on the same signal, where an hour is long enough to drop a browser in use; and a user-facing list that can be an hour stale does not answer the question it exists to answer.

What the stamps cost, stated separately, because they are not the same write. last_refreshed_ns and the reference's last_used ride free: the list blob is rewritten either way, so they change bytes already in flight. The browser registry's last_used does not — stamp_browser_use is a read plus a write of the whole anchor, which serialises every passkey, OpenID credential, recovery key and verified email, pre-checks the email-recovery binding against its reverse index, and re-syncs five reverse indices by diffing vectors, all to move one u64. So this PR adds one anchor read-modify-write per five minutes of app use, per live session, where before there was one per sign-in. That is accepted here rather than argued away: the registry cap orders on this field, and a browser in use dropping out of the list is worse than the write. The coarsening argument above is about last_refreshed_ns and the user-facing list, and does not apply to the device field.

The same write carries three stamps:

Field Lives on Drives
last_used the account reference account eviction (#4235)
last_refreshed the session record the session cap (#4267) and the user-facing list
last_used the device record the registry cap (#4242) and the settings list

Stamping the device is the only reason refresh touches the anchor: it authenticates by session chain and never runs check_authorization. What that write buys is a use signal a sign-in stamp cannot give the browser list.

Stable writes scale with 1/T alongside the calls, so lowering the app-delegation TTL multiplies both.

Tests: session_refresh_stamp_tests (8), including a stamp for a session that is gone being refused rather than reporting success, and a refresh advancing the device's last_used while leaving its enrolment timestamp alone. PocketIC drives two refreshes a minute apart and reads the device back off identity_info.

@sea-snake
sea-snake requested a review from a team as a code owner August 19, 2026 01:27
@zeropath-ai

zeropath-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 5363e32.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► src/internet_identity/src/sessions.rs
    Improve app_prepare_delegation by handling locator and adding pre-stamp storage recording
► src/internet_identity/src/storage.rs
    Add record_session_use to track and stamp session usage and prune dead sessions
► src/internet_identity/src/storage/anchor.rs
    Add stamp_browser_use to advance a browser's last_used timestamp
► src/internet_identity/src/storage/tests.rs
    Extend tests with session_refresh_stamp_tests to validate new behavior

Copilot AI lite review requested due to automatic review settings August 19, 2026 01:47
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 7d6a294 to 0b043da Compare August 19, 2026 01:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 0b043da to 72c91a1 Compare August 19, 2026 02:57
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 72c91a1 to d8bd528 Compare August 19, 2026 03:47
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from d8bd528 to 474d22b Compare August 19, 2026 06:28
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 474d22b to 77d5897 Compare August 19, 2026 07:07
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 77d5897 to 78a570f Compare August 19, 2026 08:17
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 78a570f to b7b407e Compare August 19, 2026 08:37
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from b7b407e to 2676560 Compare August 19, 2026 10:21
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 2676560 to 41a2e22 Compare August 20, 2026 10:57
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 41a2e22 to 752fe5a Compare August 20, 2026 13:08
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 752fe5a to 0502a3f Compare August 20, 2026 13:52
@sea-snake
sea-snake force-pushed the feat/session-refresh-stamps branch from 0502a3f to 5c573c2 Compare August 20, 2026 15:24
Comment thread src/internet_identity/src/sessions.rs Outdated
sea-snake and others added 3 commits September 9, 2026 17:34
…resh-stamps

# Conflicts:
#	src/internet_identity/src/sessions.rs
#	src/internet_identity/src/storage/anchor.rs
`record_session_use` returned `Ok(false)` for three different kinds of
"what you named is not there", and `?` at the call site caught only the `Err` —
so an app delegation was minted for a session no list holds, which is the one
outcome the design exists to prevent. It returns `Result<(), StorageError>` now,
with `SessionNotFound`, and the case stops being ignorable because `?` handles
it. Nobody made a decision from the bool, so it should not have been one.

The refusals also move above the stamp. Returning `Err` on the IC commits
whatever was written before it — only a trap rolls back — so a seed that will
not derive left the session recorded as used and the caller told the call
failed.

`stamp_browser_use`'s doc still claimed a return value it does not have, and a
saving no caller could make from a value it never receives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
sea-snake and others added 3 commits September 9, 2026 20:51
The stamp in `app_prepare_delegation` flattened every `StorageError` into
`InternalCanisterError`, including the not-found one it gained when
`record_session_use` stopped returning a bool. That case is a session revoked
between `authorize_session` and the stamp — a race, and the same answer the
caller would have got a moment earlier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
@sea-snake
sea-snake removed this pull request from stack #4276 September 9, 2026 20:34
@sea-snake
sea-snake added this pull request to stack #4323 September 9, 2026 20:34
@sea-snake
sea-snake removed this pull request from stack #4323 September 9, 2026 20:42
@sea-snake
sea-snake added this pull request to stack #4325 September 9, 2026 20:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Two moderate test-coverage gaps remain unresolved.

Review details

Suppressed comments (2)

src/internet_identity/src/sessions.rs:383

  • The new update-path side effect is only covered by direct Storage unit tests. The existing PocketIC session tests exercise app_prepare_delegation and browser listing separately, but no end-to-end test refreshes a session and then verifies the updated BrowserInfo.last_used through identity_info; this is the boundary where wiring the stamp or persisting the modified anchor could regress. Please add the described PocketIC regression test.
    storage_borrow_mut(|storage| storage.record_session_use(&locator, now)).map_err(
        |err| match err {
            StorageError::SessionNotFound { .. } => AppSessionError::NoSuchSession,
            other => AppSessionError::InternalCanisterError(other.to_string()),
        },
    )?;

src/internet_identity/src/storage/tests.rs:6585

  • This test does not exercise the unregistered-browser case named here: storage_with_session() calls create_session, which registers a browser through resolve_browser, just like the registered-browser helper. As written, it is only another registered-browser refresh test and would not catch a regression in the intended no-registry-entry behavior. Remove the browser entry (or construct a session without one) before calling record_session_use.
    fn a_refresh_for_a_device_the_anchor_never_registered_still_stamps_the_session() {
        let (mut storage, anchor_number, key) = storage_with_session();

        storage.record_session_use(&key, 9_000).unwrap();
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

sea-snake and others added 2 commits September 10, 2026 12:27
`a_refresh_for_a_device_the_anchor_never_registered_still_stamps_the_session`
built its session with `storage_with_session`, which calls `create_session` and
so registers a browser — leaving it a duplicate of the registered-browser
refresh above it, under a name claiming the opposite.

The state it named is unreachable. A browser is given up only at `MAX_BROWSERS`
in `resolve_browser`; the write gate sweeps that browser's sessions in the same
write, and `sync_session_index` takes their index entries with them. The session
is gone too, so a mint stops at `NoSuchSession` in `find_caller_session` long
before the stamp is reached.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
@sea-snake

Copy link
Copy Markdown
Contributor Author

Both suppressed findings from the Copilot review, answered — they land differently.

sessions.rs:383 — the missing PocketIC coverage. It exists, one PR up. should_advance_the_device_last_used_on_every_refresh at #4245 asserts created_at == last_used at enrolment, advances 300 seconds, calls app_prepare_delegation, and reads the browser back through identity_info — exactly the boundary named, wiring included. This stack merges whole, so no change here.

storage/tests.rs:6585 — the test that doesn't test what it says. Correct, and the test is now deleted rather than repaired. storage_with_session() calls create_session, which registers a browser through resolve_browser, so the test was a duplicate of the registered-browser refresh above it under a name claiming the opposite.

Constructing the orphan, as suggested, would pin a state that cannot occur. A browser is given up only at MAX_BROWSERS inside resolve_browser; the write gate sweeps that browser's sessions in the same write; and sync_session_index removes their principal-index entries along with them. So when the browser goes the session goes, and a later mint stops at NoSuchSession in find_caller_session before record_session_use is reached at all.

That also settles stamp_browser_use's browser-not-found branch, which we looked at while here: it stays a no-op. It is unreachable for the reason above, and nothing observable would go wrong if it were reached — browsers_info() builds the devices list from registry entries, so a missing entry means no row rather than a stale one. The same if also covers the ordinary case of a repeat stamp inside one message, which must no-op.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ

@sea-snake
sea-snake merged commit 0cc5641 into main Sep 10, 2026
43 checks passed
@sea-snake
sea-snake deleted the feat/session-refresh-stamps branch September 10, 2026 11:48
@sea-snake
sea-snake restored the feat/session-refresh-stamps branch September 10, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:revocable-app-sessions Design: revocable app sessions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants