Replace stub wallet keypair with real Phantom sign-in

Auth now goes through an actual connected Solana wallet instead of a
locally-generated ed25519 keypair. Phantom's window.solana provider
is only reachable from a page's own JS world, not an isolated-world
content script, so this adds a second world:'MAIN' content script
(wallet-bridge.content.ts + wallet-bridge/inject.ts) that talks to
window.solana directly and relays to the isolated world via window
CustomEvents (wallet-bridge/relay.ts), matched by request id.

wallet-connect.ts orchestrates: try a silent onlyIfTrusted connect on
load; if that fails, show an on-page banner (wallet-bridge/banner.ts)
whose click handler is what actually calls connect() -- Phantom
requires a real user gesture for the approval popup on a first-ever
connect, which a click relayed from the extension popup wouldn't
count as by the time it reaches the wallet.

background/wallet-auth.ts runs the REST auth flow (nonce -> ask the
tab's content script to sign it -> verify -> store session token)
once a wallet reports connected. ws-client.ts no longer force-retries
with a known-bad token on auth failure; it calls onAuthExpired
instead (which clears the token and prompts a silent wallet
reconnect) and exposes reconnectNow() so background.ts can
short-circuit the backoff wait once a fresh token exists.

identity.ts and its tweetnacl dependency are gone -- no more stub
signer.

Untested against real Phantom (no browser automation available this
session) -- flagged in CLAUDE.md as needing manual verification,
along with a note that world:'MAIN' needs Firefox 128+.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YXiHuScXrjxBh7yFGAPq3B
This commit is contained in:
2026-09-07 12:20:17 +02:00
co-authored by claude
parent 58e32caa5c
commit 56a2ff9930
15 changed files with 413 additions and 136 deletions
+13 -3
View File
@@ -1,17 +1,21 @@
import { getSessionToken } from '@/background/backend-client';
import { clearSessionToken, getSessionToken } from '@/background/backend-client';
import { getConnectionStatus, setConnectionStatus } from '@/background/connection-status';
import { applyLockState, getLockState } from '@/background/lock-state';
import { handleWalletConnected, requestWalletReconnect } from '@/background/wallet-auth';
import { connectWsClient } from '@/background/ws-client';
import type { NexaMessage } from '@/shared/messaging';
export default defineBackground(() => {
connectWsClient({
const ws = connectWsClient({
getToken: getSessionToken,
onLockState: (state) => void applyLockState(state),
onStatusChange: (status) => void setConnectionStatus(status),
onAuthExpired: () => {
void clearSessionToken().then(() => requestWalletReconnect());
},
});
browser.runtime.onMessage.addListener((message: NexaMessage, _sender, sendResponse) => {
browser.runtime.onMessage.addListener((message: NexaMessage, sender, sendResponse) => {
switch (message?.type) {
case 'nexa:get-lock-state':
getLockState().then(sendResponse);
@@ -27,6 +31,12 @@ export default defineBackground(() => {
browser.action.openPopup().catch(() => undefined);
return false;
case 'nexa:wallet-connected':
handleWalletConnected(sender.tab?.id, message.walletAddress)
.then(() => ws.reconnectNow())
.catch(() => undefined); // signing/verify failed — ws-client's own retry loop keeps trying
return false;
default:
return undefined;
}