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:
@@ -1,54 +1,24 @@
|
||||
import bs58 from 'bs58';
|
||||
import { browser } from 'wxt/browser';
|
||||
import { BACKEND_HTTP_URL } from '@/shared/config';
|
||||
import { getIdentity } from './identity';
|
||||
|
||||
const SESSION_TOKEN_STORAGE_KEY = 'nexa:sessionToken';
|
||||
|
||||
/**
|
||||
* Wallet-signature auth against the backend's REST flow (see
|
||||
* backend/CLAUDE.md, "Current milestone: authentication + user data"):
|
||||
* request a nonce for our wallet_address, sign it, exchange for a session
|
||||
* token. That token is what the WS client (ws-client.ts) authenticates with.
|
||||
* Session token storage. The token itself is obtained by wallet-auth.ts
|
||||
* (nonce -> Phantom signature -> verify) once a wallet connects — this
|
||||
* module just persists/retrieves it, since ws-client.ts and popup code
|
||||
* shouldn't know how a token was obtained.
|
||||
*/
|
||||
export async function authenticate(): Promise<string> {
|
||||
const identity = await getIdentity();
|
||||
|
||||
const nonceRes = await fetch(`${BACKEND_HTTP_URL}/auth/nonce`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ wallet_address: identity.walletAddress }),
|
||||
});
|
||||
if (!nonceRes.ok) throw new Error(`nonce request failed: ${nonceRes.status}`);
|
||||
const { nonce } = (await nonceRes.json()) as { nonce: string };
|
||||
|
||||
const signatureBytes = identity.sign(new TextEncoder().encode(nonce));
|
||||
|
||||
const verifyRes = await fetch(`${BACKEND_HTTP_URL}/auth/verify`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
wallet_address: identity.walletAddress,
|
||||
signature: bs58.encode(signatureBytes),
|
||||
}),
|
||||
});
|
||||
if (!verifyRes.ok) throw new Error(`verify request failed: ${verifyRes.status}`);
|
||||
const { session_token: sessionToken } = (await verifyRes.json()) as { session_token: string };
|
||||
|
||||
await browser.storage.local.set({ [SESSION_TOKEN_STORAGE_KEY]: sessionToken });
|
||||
return sessionToken;
|
||||
export async function getSessionToken(): Promise<string> {
|
||||
const stored = await browser.storage.local.get(SESSION_TOKEN_STORAGE_KEY);
|
||||
const token = stored[SESSION_TOKEN_STORAGE_KEY] as string | undefined;
|
||||
if (!token) throw new Error('Not authenticated yet — connect a wallet on a supported trading site.');
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a usable session token, authenticating from scratch if none is
|
||||
* stored yet (or `forceRefresh` is set, e.g. after the backend told us via
|
||||
* `auth_expired`/close-4001 that the old one is no longer valid).
|
||||
*/
|
||||
export async function getSessionToken(forceRefresh = false): Promise<string> {
|
||||
if (!forceRefresh) {
|
||||
const stored = await browser.storage.local.get(SESSION_TOKEN_STORAGE_KEY);
|
||||
const token = stored[SESSION_TOKEN_STORAGE_KEY] as string | undefined;
|
||||
if (token) return token;
|
||||
}
|
||||
return authenticate();
|
||||
export async function storeSessionToken(token: string): Promise<void> {
|
||||
await browser.storage.local.set({ [SESSION_TOKEN_STORAGE_KEY]: token });
|
||||
}
|
||||
|
||||
export async function clearSessionToken(): Promise<void> {
|
||||
await browser.storage.local.remove(SESSION_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user