Connect to the real Nexa backend, replacing the mock lock toggle

Adds the wire-protocol WebSocket client (ws-client.ts) and the REST
auth flow (backend-client.ts) against the now-live backend, using a
locally-generated ed25519 keypair (identity.ts) as a stand-in wallet
signer until real wallet-extension integration is built.

lock-state.ts's setLockState is now applyLockState, called only by
the WS client on an incoming lock_state message -- the backend is
the sole source of truth for lock state, so there's no other writer
anymore. The popup's dev mock controls are replaced with a live
connection-status + lock-state display.

Verified end-to-end against the real backend (nonce -> verify -> /me
-> ws lock_state) using the same tweetnacl/bs58 libs shipped in the
extension.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YXiHuScXrjxBh7yFGAPq3B
This commit is contained in:
2026-09-07 09:10:18 +02:00
co-authored by claude
parent 5643eebde7
commit 3b6054f2f0
14 changed files with 371 additions and 98 deletions
+54
View File
@@ -0,0 +1,54 @@
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.
*/
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;
}
/**
* 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();
}