-
Notifications
You must be signed in to change notification settings - Fork 44
feat: add headless mode support to controller SDK #2315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| // Only open modal if NOT headless | ||
| if (!headlessOptions) { | ||
| this.iframes.keychain.open(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty username causes connection hang with hidden modal
Medium Severity
The conditions for skipping the modal and for using headless authentication are misaligned. In controller.ts, the modal is skipped when headlessOptions is truthy (!headlessOptions is false). In connect.ts, headless auth is used when username && credentials are both truthy. If headlessOptions is provided with an empty username "", the modal is not opened (object is truthy), but headless auth is skipped (empty string is falsy), causing the code to fall through to the UI flow. Since the modal is hidden, the UI renders invisibly, the Promise never resolves, and the connection hangs indefinitely.
🔬 Verification Test
Why verification test was not possible: This requires the full iframe/modal infrastructure to demonstrate the hang. The bug can be traced through code:
headlessOptions = { username: "", credentials: { type: "password", password: "x" } }- In controller.ts line 254:
!headlessOptions=!{}=false→ modal NOT opened - In connect.ts line 174:
"" && credentials=""(falsy) → headless auth skipped - Falls through to line 179-191, calling
navigate()to show connect UI - The iframe exists but container has
display: none(never opened) - User cannot see or interact with the UI
- Promise never resolves → connection hangs forever
Additional Locations (1)
| if (headless?.username && headless?.credentials) { | ||
| // Perform headless authentication without UI | ||
| return authenticateHeadless(headless.username, headless.credentials); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent headless detection causes hung promise on empty username
Medium Severity
Inconsistent headless mode detection between controller.ts and connect.ts can cause a hung promise. The controller checks !headlessOptions (object existence) to decide whether to open the modal, while the keychain's connect checks headless?.username && headless?.credentials (truthy property values) to decide whether to use headless authentication. When headless is provided with an empty string username, the modal isn't opened but the code falls through to the UI-based flow, which waits for user interaction callbacks that can never arrive since the modal is hidden.
Additional Locations (1)
| password, | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repeated headless login attempts fail due to singleton iframe
High Severity
The HeadlessLogin component creates a new Controller on each login attempt, but the underlying IFrame class uses singleton-style DOM element management - it checks for an existing "controller" element and won't add a second one. After the first login attempt, subsequent Controller instances have their iframes created but never added to the DOM, so the penpal connection never establishes, this.keychain remains undefined, and connect() immediately returns undefined with a "Not ready to connect" error. Users clicking the login button multiple times will see the first attempt work (or fail for unimplemented reasons) but all subsequent attempts fail instantly.
| return authenticateHeadless( | ||
| headless.username, | ||
| headless.credentials, | ||
| ).then((result) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Headless mode ignores configured chain, always uses mainnet
High Severity
The authenticateHeadless function accepts an optional chainId parameter that defaults to mainnet, but the connect handler never passes a chainId when calling it. The HeadlessOptions type doesn't include chainId, and neither does ConnectOptions, so even though users configure defaultChainId: SN_SEPOLIA in the Controller SDK, headless authentication always uses mainnet. This causes headless authentication to operate on the wrong chain regardless of configuration.
Additional Locations (1)
Adds programmatic authentication support without UI display for server-side applications and automated scripts. Architecture: Controller SDK → Keychain iframe (no UI) → Backend API Key changes: - Added headless option to controller configuration - Modified connect() to conditionally display modal - Added type-safe credential interfaces using discriminated unions - Added headless-specific error classes - Supports 10 auth methods: password, webauthn, OAuth, wallets Implementation preserves existing architecture with minimal changes (~15 lines in connect method). All authentication logic remains in keychain package. Fully backwards compatible. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds basic infrastructure for headless authentication in the keychain package. Changes: - Updated connect() to accept username and credentials parameters - Created headless.ts with authentication framework - Added type imports for HeadlessCredentialData - Maintains backwards compatibility with old connect signatures Implementation status: - ✅ Controller SDK side complete (passes username/credentials) - ✅ Keychain Penpal method signature updated - ⏳ Backend authentication logic needs implementation - ⏳ Password decryption flow needs integration - ⏳ Controller instance creation needs implementation The framework is in place but requires backend API integration to: 1. Fetch user controller data 2. Decrypt credentials 3. Create/load Controller instance 4. Store in window.controller This allows the Controller SDK to work immediately (it passes the data), while the keychain will return "not yet implemented" errors until the backend integration is completed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improves the headless mode API by using a single options parameter instead of multiple separate parameters. Changes: - Added ConnectOptions interface with signupOptions and headless fields - Updated Keychain.connect() to accept ConnectOptions - Updated keychain implementation with better signature detection - Fixed bug: usernames starting with "http" now handled correctly (checks for "http://" or "https://" explicitly) - Fixed bug: example code now returns consistent address values - Added HeadlessLogin component to Next.js example for testing Benefits: - Cleaner API with single options parameter - More extensible for future options - Better type safety - Fixes signature detection edge case Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Implement full authentication flow for password, WebAuthn, and OAuth methods - Integrate with existing backend APIs and controller login - Add proper error handling and type safety - Support all EIP-191 authentication methods (Google, Discord, MetaMask, Rabby, Phantom) - Mark Argent, Braavos, and SIWS as not yet implemented (require special handling) - Update HeadlessLogin example to reflect implementation status - All authentication methods now properly fetch controller data from backend - Decrypt credentials and create controller instances with proper session management
- Remove password authentication from example (still supported in headless mode) - Add Passkey (WebAuthn) authentication button with env var configuration - Add MetaMask authentication button with wallet detection - Update UI with two side-by-side authentication buttons - Add proper TypeScript types for window.ethereum - Improve user feedback with method-specific loading states - Update documentation to explain requirements for each auth method
- Remove global Window interface declaration that conflicted with existing types - Use inline type casting with EthereumProvider interface instead - Prevents TypeScript error: 'Subsequent property declarations must have the same type' - Fixes CI build failures in ts and storybook jobs
- Always wait for keychain Penpal connection to be established - Previously only waited when lazy-loading the iframe - Fixes 'Not ready to connect' error in headless mode - Controller now properly waits for iframe to establish connection before use
Add temporary logging to help identify where the headless MetaMask authentication flow hangs. Logs timing for fetchController and Controller.login calls with enhanced error reporting. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This PR adds headless mode support to the Controller SDK, enabling programmatic authentication without UI for server-side applications and automated scripts.
Architecture
Controller SDK → Keychain iframe (no UI) → Backend API
Key Changes
Implementation Details
See
HEADLESS_MODE.mdandIMPLEMENTATION_SUMMARY.mdfor complete details.Note
Enables programmatic, UI-less auth via a new headless mode across the Controller SDK and keychain.
headlesstoControllerOptions, new type-safe credential unions (HeadlessCredentialData),ConnectOptions, and headless-specific errors; updatesKeychain.connectto acceptConnectOptionsandcontroller.connect()to skip opening the modal when headlessauthenticateHeadlessfor password, WebAuthn, and EIP-191 flows; updatesconnectto parse old/new signatures and route to headless authOAuthConnectionmodels anddeploy/disconnectOAuthmutations; prunes email/phone filtersHEADLESS_MODE.md, comprehensiveexamples/headless-simple.ts, and Next.jsHeadlessLogincomponent wired into the example appWritten by Cursor Bugbot for commit 5c34a5e. This will update automatically on new commits. Configure here.