Skip to content

refactor: replace WASM dependency in SessionProvider with pure TypeScript#2542

Open
kronosapiens wants to merge 22 commits into
mainfrom
kronosapiens/refactor-session-signer
Open

refactor: replace WASM dependency in SessionProvider with pure TypeScript#2542
kronosapiens wants to merge 22 commits into
mainfrom
kronosapiens/refactor-session-signer

Conversation

@kronosapiens
Copy link
Copy Markdown
Contributor

@kronosapiens kronosapiens commented Apr 6, 2026

Closes #2518

This PR integrates the TS implementation of the SessionProvider from StarkZap, dropping the WASM dependency from controller-rs and unlocking React Native integration.

Summary

  • Replaces all @cartridge/controller-wasm imports in the session/node paths with pure TypeScript implementations using only starknet.js primitives
  • Adds 9 new files under packages/controller/src/session/internal/ implementing signerToGuid, Poseidon merkle trees, SNIP-9 v2 outside execution signing, session token serialization, and GraphQL subscription polling
  • Swaps imports in 6 existing files (session/provider.ts, session/account.ts, session/index.ts, node/provider.ts, node/account.ts, utils.ts) — no public API changes
  • Replaces localeCompare with byte-order string comparison in toWasmPolicies to ensure deterministic policy sorting across locales
  • Removes @cartridge/controller-wasm as a dependency in package/controller

Benefits

  • Bundle size: Replaces 3.7MB of WASM binaries (2.5MB controller + 1.2MB session) with 28KB of JavaScript. Every bundler that touches controller no longer needs to handle WASM compilation or require vite-plugin-wasm/vite-plugin-top-level-await.
  • Debuggability: The WASM version returns opaque error codes (e.g. {"code":131}). The TS version gives readable stack traces and can be debugged with a console.log and a rebuild. Every developer who hits a session issue benefits.
  • Platform compatibility: WASM is not supported in React Native, and requires special bundler plugins in every consuming project. The TS version is plain ES modules — it works everywhere.
  • Deployment coupling: The WASM package is versioned independently (@cartridge/controller-wasm@0.10.0). Fixing a session signing bug currently requires: fix in Rust → build WASM → publish package → bump version in controller → release. With TS it's one PR.
  • Auditability: The team and integrators can read and review the signing logic in TypeScript. The WASM binary is opaque — for security-sensitive code like session token construction, readability matters.

Key files

New files under packages/controller/src/session/internal/ (93% statement / 94% line coverage):

File Purpose Stmts Lines
execution.ts Builds and signs SNIP-9 v2 outside execution payloads. Handles nonce generation, time bounds, call serialization, and session token construction with Stark curve signing. 99% 99%
merkle.ts Computes Poseidon Merkle trees and proofs over session policies. Supports CallPolicy, TypedDataPolicy, and ApprovalPolicy leaf types. 99% 98%
utils.ts Felt normalization, selector computation, contract address padding, and signerToGuid (Poseidon hash of domain + public key). 100% 100%
account.ts CartridgeSessionAccount — holds session credentials and submits signed payloads to the Cartridge RPC via cartridge_addExecuteOutsideTransaction. 94% 94%
subscribe.ts Polls the Cartridge GraphQL API for session registration confirmation with exponential backoff and timeout. 93% 97%
errors.ts Error types for session failures: SessionProtocolError, SessionTimeoutError, and SNIP-9 compatibility detection. 80% 83%
types.ts Shared type definitions: CallPolicy, TypedDataPolicy, ApprovalPolicy, Session.

Notes

Decoupling from controller-rs

This creates a second implementation of session signing alongside controller-rs, which is still consumed by the keychain. The two implementations both conform to the same on-chain session protocol (SNIP-9 v2 + Cartridge session token format). Correctness is enforced by the contract at submission time — if either implementation produces a malformed payload, it is rejected by the RPC. If the protocol changes, both implementations will need to be updated.

Behavioral change: execute() fallback removed

The old code silently fell back to this.controller.execute() (direct invocation) when executeFromOutside() failed. This fallback could never succeed under Cartridge's current session registration flow (due to required guardian signature replacement), so the new code throws a SessionProtocolError with a clear message instead.

References

Test plan

  • All 111 controller tests pass (52 existing + 59 new)
  • All 407 keychain tests pass
  • Connector test passes
  • Build succeeds (pnpm build)
  • Lint and format checks pass (pnpm lint)
  • Verified session flow end-to-end on iOS (Capacitor) against Sepolia
  • Verify executeFromOutside produces payloads accepted by Cartridge relayer

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 6, 2026

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

Project Deployment Actions Updated (UTC)
controller-example-next Ready Ready Preview Apr 21, 2026 2:04pm
keychain Ready Ready Preview Apr 21, 2026 2:04pm
keychain-storybook Ready Ready Preview Apr 21, 2026 2:04pm

Request Review

@kronosapiens kronosapiens force-pushed the kronosapiens/refactor-session-signer branch from e4f1996 to 000d731 Compare April 6, 2026 22:04
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 21.30%. Comparing base (00e9912) to head (e6b5001).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2542      +/-   ##
==========================================
- Coverage   21.41%   21.30%   -0.11%     
==========================================
  Files         339      342       +3     
  Lines       37969    38436     +467     
  Branches     1216     1224       +8     
==========================================
+ Hits         8131     8190      +59     
- Misses      29819    30227     +408     
  Partials       19       19              

☔ 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.

@kronosapiens
Copy link
Copy Markdown
Contributor Author

kronosapiens commented Apr 15, 2026

Confirming that this code has been manually exercised using the Capacitor example in /examples/capacitor and can correctly register sessions and make transactions.

https://sepolia.voyager.online/contract/0x034f27844fd897c18be6f697ba20603d61d4c26e46a9519e0131a31b9f3f1af3

kronosapiens and others added 22 commits April 21, 2026 09:53
…ript

Replace @cartridge/controller-wasm imports in session/node paths with pure TS
implementations using only starknet.js primitives, enabling React Native support
and simplifying the build pipeline. Adds 45 new tests covering guid derivation,
merkle tree construction, outside execution signing, session account operations,
and GraphQL subscription polling.

Closes #2518

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The class has identical behavior to the WASM original and the original
name never implied a WASM implementation, so keep the established name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…on.ts to execution.ts

The session/ts/ directory already establishes context, making the
prefixes redundant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Small single-function module absorbed into existing utilities file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "ts" name was meaningless — everything is TypeScript. "internal"
clearly signals these are implementation details behind the session API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
node/ was reaching into session/internal/ directly. Re-export
CartridgeSessionAccount and signerToGuid from session/index.ts so
node/ imports from ../session instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop JsFelt (inline string), rename JsCall to SessionCall, remove
unused JsOutsideExecutionV3 and JsSignedOutsideExecution. The Js
prefixes were WASM artifacts with no meaning in pure TypeScript.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Felt communicates domain intent better than raw string. Renamed from
JsFelt to Felt since the Js prefix was a WASM artifact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove broken CartridgeSessionAccount.execute() that used an
  outside-execution signature for a regular invoke transaction
- Add isSnip9CompatibilityError() (ported from reference) so callers
  can distinguish SNIP-9 incompatibility from real failures
- Replace catch-all fallback in session/account.ts and node/account.ts
  with selective SNIP-9 detection; non-SNIP-9 errors now propagate
  immediately instead of being swallowed
- Fix localeCompare sorting in toWasmPolicies to use pure byte-order
  comparison, matching the reference and eliminating locale-dependent
  merkle root differences
- Add executeBefore > executeAfter validation in buildSignedOutsideExecutionV3
- Add test coverage: time-bound validation, isSnip9CompatibilityError,
  error propagation, and locale-independent address sorting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tests for normalizeFelt, selectorFromEntrypoint, and
normalizeContractAddress. Add multi-call test for
buildSignedOutsideExecutionV3 to exercise calldata serialization
across multiple calls with different lengths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TypedDataPolicy was using the CallPolicy type hash ("Allowed Method")
instead of its own ("Allowed Type"). ApprovalPolicy was hashing all
three fields (target, spender, amount) instead of converting to
CallPolicy(target, approve_selector) as the WASM boundary does.
Also fixes the proof index selector for ApprovalPolicy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Explains that the fallback is blocked by Cartridge's guardian
co-signing policy, not by a protocol limitation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- hashPolicyLeaf returns 0x0 for unauthorized policies (matching WASM)
- subscribeCreateSession throws immediately on GraphQL errors instead
  of retrying, adds per-request AbortController timeout
- Rename misleading execute() test to clarify it tests the internal
  CartridgeSessionAccount class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove 23 tests that were subsumed by stronger tests, tested
starknet.js rather than our code, or exercised the same branch
at different scale. 134 → 111 total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The capacitor:// origin is not allowed by the RPC server's CORS policy,
causing fetch requests to fail in the native WebView. CapacitorHttp
routes requests through the native HTTP layer instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…oller package

The session provider now uses pure TypeScript — no WASM imports remain
in packages/controller/src/.

Co-Authored-By: Claude Opus 4.6 (1M context) <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.

Replace WASM dependency in SessionProvider with pure TypeScript session implementation

1 participant