Skip to content

fix: SessionProvider preset support and address normalization#2401

Merged
broody merged 10 commits intomainfrom
kronosapiens/preset-policy-research
Feb 11, 2026
Merged

fix: SessionProvider preset support and address normalization#2401
broody merged 10 commits intomainfrom
kronosapiens/preset-policy-research

Conversation

@kronosapiens
Copy link
Contributor

@kronosapiens kronosapiens commented Feb 11, 2026

Summary

Fixes #2399.
Addresses session/not-registered errors caused by policy hash divergence between the SDK and keychain.

SDK changes (@cartridge/controller)

  • Add preset and shouldOverridePresetPolicies to SessionOptions: Developers can now pass preset: "my-game" instead of manually duplicating policies.
    Preset policies are resolved via loadConfig() from @cartridge/presets, matching the keychain's behavior.
    Policy precedence matches ControllerProvider: preset wins unless shouldOverridePresetPolicies: true.
  • Consolidate async init into _init(): Preset resolution, session retrieval from storage, and signer key setup all happen in a single eagerly-started async method.
    Public methods (username, probe, connect) simply await this._ready.
  • Normalize addresses in toWasmPolicies: Applies getChecksumAddress() to contract targets before hashing, preventing divergence from inconsistent address casing.
  • Extract getPresetSessionPolicies utility: Shared between SessionProvider and the keychain's getConfigChainPolicies, replacing ~25 lines of defensive type narrowing with a clean helper.
  • Warn on silent policy drop: When both preset and policies are provided without the override flag, a console.warn is logged (previously policies were silently ignored in ControllerProvider).
  • Send preset in keychain URL: When preset is used, connect() sends &preset= instead of &policies=, so the keychain resolves the same policies from the same source.

Keychain changes

  • Gate rendering on isPoliciesResolved in Authentication: The Authentication wrapper now waits for preset policies to fully resolve before rendering child routes.
    This replaces scattered isPoliciesResolved guards that were previously in ConnectRoute and eliminates race conditions where components render before async preset policy resolution completes.

Root cause

When a developer configured presets and used SessionProvider, the keychain would resolve policies from the preset while the SDK used the developer's manually-passed policies.
If these differed (or if addresses had different casing), the session hash would diverge, producing session/not-registered errors.

Design note

Preset resolution is async (loadConfig fetches from the network), but JS constructors can't be async.
The _init() method is eagerly started in the constructor and stores its promise as _ready.
A cleaner alternative would be a static factory method (SessionProvider.create(options)), but that's a breaking API change — worth considering in a future major version.

Follow-up

  • Apply same preset support to TelegramProvider and Node.js SessionProvider

Test plan

  • All 45 controller unit tests pass (including updated toWasmPolicies tests)
  • All 356 keychain tests pass
  • tsc --noEmit passes with zero errors
  • Full monorepo build succeeds
  • Lint and format checks pass
  • Manual verification with a preset-configured app using SessionProvider

🤖 Generated with Claude Code

@vercel
Copy link

vercel bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
controller-example-next Ready Ready Preview Feb 11, 2026 10:09pm
keychain Ready Ready Preview Feb 11, 2026 10:09pm
keychain-storybook Ready Ready Preview Feb 11, 2026 10:09pm

Request Review

@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

❌ Patch coverage is 0% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.57%. Comparing base (fb3c5f6) to head (5dfb619).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
packages/keychain/src/hooks/connection.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2401      +/-   ##
==========================================
+ Coverage   19.81%   20.57%   +0.76%     
==========================================
  Files         327      316      -11     
  Lines       33133    33288     +155     
  Branches      883      970      +87     
==========================================
+ Hits         6564     6849     +285     
+ Misses      26557    26425     -132     
- Partials       12       14       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

broody
broody previously approved these changes Feb 11, 2026
Copy link
Contributor

@broody broody left a comment

Choose a reason for hiding this comment

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

Works great!

kronosapiens and others added 9 commits February 11, 2026 16:57
…sses

SessionProvider lacked preset support, forcing developers to manually
duplicate preset policies. When presets were configured but policies
were passed separately, the keychain and SDK could compute different
session hashes, causing session/not-registered errors.

Changes:
- Add `preset` option to SessionOptions, resolving policies via
  loadConfig() to match the keychain's behavior
- Normalize addresses with getChecksumAddress in toWasmPolicies to
  prevent hash divergence from inconsistent address casing
- Warn when ControllerProvider receives both preset and policies
  without shouldOverridePresetPolicies

Fixes #2399

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace lazy _resolvePresetPolicies() pattern with eagerly-stored
promise. Constructor kicks off loadConfig() immediately; async methods
just await the stored promise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
De-duplicate the preset config → chain policies extraction logic shared
between SessionProvider and keychain's getConfigChainPolicies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When both preset and policies are provided, the SDK uses explicit
policies locally. Ensure the keychain URL also sends policies (not
preset) to avoid hash mismatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When both preset and policies are provided, preset takes precedence
(policies are ignored with a warning). This matches ControllerProvider
behavior and nudges developers toward using preset as the single source
of truth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Matches ControllerProvider's escape hatch exactly, allowing explicit
policies to take precedence over preset when the flag is set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…wrapper

Move the isPoliciesResolved check into the Authentication component so
child routes (Session, ConnectRoute) never render until preset policies
are fully resolved. This eliminates scattered isPoliciesResolved guards
from individual components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…method

Replace _policiesReady + _resolvePresetPolicies with a unified _init()
that handles preset resolution, session retrieval, and signer setup in
sequence. Public methods await _ready instead of _policiesReady.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing SessionPolicies type import
- Add definite assignment assertions for async-initialized fields
- Remove redundant tryRetrieveFromQueryOrStorage() calls after _init()
- Remove redundant _preset = undefined assignment
- Update toWasmPolicies docstring to mention address normalization
- Align ControllerProvider warning message with SessionProvider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@broody broody left a comment

Choose a reason for hiding this comment

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

LGTM

@broody broody merged commit ba15f09 into main Feb 11, 2026
12 checks passed
@broody broody deleted the kronosapiens/preset-policy-research branch February 11, 2026 22:38
@claude
Copy link
Contributor

claude bot commented Feb 11, 2026

Claude finished @kronosapiens's task —— View job


Analyzing controller PR changes and updating documentation

I'll analyze the merged PR #2401 changes and update the documentation in the docs repository accordingly.

Tasks:

  • Explore docs-repo directory structure to understand current documentation
  • Review the PR changes to identify user-facing API changes
  • Determine what documentation needs updating for SessionProvider preset support
  • Update or create appropriate documentation files
  • Commit and push changes if needed

Analysis of Changes

Based on my review of the merged PR #2401, the main changes that need documentation are:

  1. New SessionOptions parameters:

    • preset?: string - Allows passing preset name instead of manual policies
    • shouldOverridePresetPolicies?: boolean - Controls precedence between preset and manually passed policies
  2. Policy precedence logic: Matches ControllerProvider behavior where preset wins unless shouldOverridePresetPolicies is true

  3. Address normalization: Internal improvement to prevent session/not-registered errors

  4. Warning on policy conflicts: Console warning when both preset and policies are provided without override flag

Documentation Updates Made

I've successfully updated /docs-repo/src/pages/controller/sessions.md with comprehensive documentation for the new SessionProvider preset support:

Updated SessionOptions interface - Added the new preset and shouldOverridePresetPolicies parameters with clear descriptions

Added new "Using Presets with SessionProvider" section covering:

  • Basic preset usage examples
  • Policy precedence rules (preset vs manual policies)
  • Override behavior with shouldOverridePresetPolicies flag
  • Benefits of using presets (consistency, verification, maintenance, error prevention)

Updated existing examples - Changed authentication configuration example to showcase preset usage instead of manual policies

Updated manual session processing example - Shows preset usage in native app context

The documentation now accurately reflects the new SessionProvider capabilities added in PR #2401. I'm unable to commit these changes to the docs repository due to git configuration restrictions, but the files have been successfully updated in the docs-repo directory.

kronosapiens pushed a commit to cartridge-gg/docs that referenced this pull request Feb 12, 2026
Updates documentation to reflect changes made in:
    fix: SessionProvider preset support and address normalization

    Related controller PR: cartridge-gg/controller#2401

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
kronosapiens added a commit that referenced this pull request Feb 13, 2026
…ionFromRedirect

Fixes a regression introduced in #2401 where probe() would always return undefined
after calling ingestSessionFromRedirect on a deep link redirect (Capacitor, etc).

The issue: #2401 moved session restoration into _init(), which runs once during
construction. For deep links, the session data arrives *after* construction via
ingestSessionFromRedirect, so _init() never sees it. Result: probe() had no path
to create the account.

Changes:
- Separate concerns: _setSigningKeys() (sync) and _resolvePreset() (async)
- Restore on-demand retrieval: probe/connect/username call tryRetrieveSessionAccount
- Rename tryRetrieveFromQueryOrStorage to tryRetrieveSessionAccount for clarity
- Mark tryRetrieveSessionAccount as private since it's internal restoration logic
- Add comments explaining the two session entry points (localStorage and URL query)

This restores the pre-#2401 behavior of checking storage on every public method
call, allowing both web redirects (query param flow) and deep links (explicit call
flow) to work correctly.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
kronosapiens added a commit that referenced this pull request Feb 13, 2026
…ionFromRedirect

Fixes a regression introduced in #2401 where probe() would always return undefined
after calling ingestSessionFromRedirect on a deep link redirect (Capacitor, etc).

The issue: #2401 moved session restoration into _init(), which runs once during
construction. For deep links, the session data arrives *after* construction via
ingestSessionFromRedirect, so _init() never sees it. Result: probe() had no path
to create the account.

Changes:
- Separate concerns: _setSigningKeys() (sync) and _resolvePreset() (async)
- Restore on-demand retrieval: probe/connect/username call tryRetrieveSessionAccount
- Rename tryRetrieveFromQueryOrStorage to tryRetrieveSessionAccount for clarity
- Mark tryRetrieveSessionAccount as private since it's internal restoration logic
- Add comments explaining the two session entry points (localStorage and URL query)

This restores the pre-#2401 behavior of checking storage on every public method
call, allowing both web redirects (query param flow) and deep links (explicit call
flow) to work correctly.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
kronosapiens added a commit that referenced this pull request Feb 13, 2026
…2409)

## Summary

Fixes a regression introduced in #2401 where `SessionProvider.probe()`
would always return undefined after calling
`ingestSessionFromRedirect()` on a deep link redirect (Capacitor, mobile
apps, etc).

**Root cause:** #2401 moved session restoration into `_init()`, which
runs once during construction. For deep links, the session data arrives
*after* construction via `ingestSessionFromRedirect()`, so `_init()`
never sees it. Result: `probe()` had no path to create the account.

**Solution:** Restore on-demand session retrieval in public methods
(`probe()`, `connect()`, `username()`), matching the pre-#2401 behavior.
Both web redirects (query param flow) and deep links (explicit call
flow) now work correctly.

## Changes

- Split initialization: `_setSigningKeys()` (sync) handles signer setup,
`_resolvePreset()` (async) handles preset resolution
- Restore on-demand account retrieval: `tryRetrieveSessionAccount()`
called by public methods when account not set
- Rename `tryRetrieveFromQueryOrStorage` → `tryRetrieveSessionAccount`
for clarity and mark as private
- Add comments explaining the two session entry points (localStorage and
URL query params)
- Improve variable naming: `_ready` → `_readyPromise`

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SessionProvider: missing preset support and address normalization causing session/not-registered errors

2 participants