From 184193e655087cb432952ea2ba583ad9772b0923 Mon Sep 17 00:00:00 2001 From: TerryM Date: Tue, 2 Jun 2026 01:00:13 +0800 Subject: [PATCH] docs: note wallet session tradeoffs --- README.md | 19 +++++++++++++------ src/wallet/WalletLoginModal.tsx | 16 +++++++++++++--- src/wallet/api.ts | 2 ++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 068d175..ccbc9ea 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,20 @@ npm test Create a local `.env` only when needed. Do not commit secrets. See `.env.example` for a template. -| Variable | Purpose | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `VITE_API_URL` | API/upload origin. Empty means same-origin and Vite dev proxy handles local `/api` and `/uploads`. Production deploy currently uses `https://api.ark-library.com`. | -| `VITE_DISABLE_ADMIN` | When set to `"true"`, public build redirects admin routes away. Production public deploy sets this to `"true"`. | -| `VITE_ADMIN_ONLY` | When set to `"true"`, builds the admin-only app entry instead of the public app. | -| `VITE_ADMIN_UI_PREFIX` | Optional admin UI base path. If absent in admin-only mode, code uses the secret prefix from `src/adminPaths.ts`. | +| Variable | Purpose | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `VITE_API_URL` | API/upload origin. Empty means same-origin and Vite dev proxy handles local `/api` and `/uploads`. Production deploy currently uses `https://api.ark-library.com`. | +| `VITE_DISABLE_ADMIN` | When set to `"true"`, public build redirects admin routes away. Production public deploy sets this to `"true"`. | +| `VITE_ADMIN_ONLY` | When set to `"true"`, builds the admin-only app entry instead of the public app. | +| `VITE_ADMIN_UI_PREFIX` | Optional admin UI base path. If absent in admin-only mode, code uses the secret prefix from `src/adminPaths.ts`. | | `VITE_USE_MOCK_POSTS` | Telegram-style resource stream (`/browse`, `/category/:slug`) uses mock posts from `src/mocks/mockPosts.ts` only when set to `"true"`. Leave unset or set to `"false"` to hit the real `/api/posts` API. See `.unipi/docs/specs/2026-05-25-posts-api-contract.md`. | +| `VITE_WALLETCONNECT_PROJECT_ID` | Reown/WalletConnect project ID used by the RainbowKit QR fallback for MetaMask/imToken. TokenPocket QR login does not use this. Required before testing or deploying the fallback scan flow. | + +## Wallet login notes + +Wallet login is used to verify address ownership for user favorites. The primary stable paths are injected wallets (`window.ethereum`) and TokenPocket QR callback login. MetaMask/imToken QR login is a RainbowKit/Reown fallback and may be unstable on some China networks. + +The current frontend stores the wallet JWT in `localStorage` as a simple MVP session mechanism. This keeps the implementation small, but any future XSS vulnerability could expose a 30-day wallet session. A more secure future iteration should move wallet sessions to backend-set `httpOnly` cookies or shorten the token lifetime with refresh-token support. ## Project layout diff --git a/src/wallet/WalletLoginModal.tsx b/src/wallet/WalletLoginModal.tsx index 7bd86cf..d7c8f6f 100644 --- a/src/wallet/WalletLoginModal.tsx +++ b/src/wallet/WalletLoginModal.tsx @@ -45,9 +45,13 @@ export function WalletLoginModal() { if (state !== "tpPolling") return; let cancelled = false; + const abortController = new AbortController(); const poll = async () => { try { - const result = await fetchTokenPocketLoginResult(tpRequest.actionId); + const result = await fetchTokenPocketLoginResult( + tpRequest.actionId, + abortController.signal, + ); if (cancelled) return; if (result.status === "completed") { const verified = await verifyWalletSignature({ @@ -66,8 +70,13 @@ export function WalletLoginModal() { setState("idle"); setError(result.error || t("walletTpExpired")); } - } catch { - if (!cancelled) setError(t("walletLoginFailed")); + } catch (err) { + if ( + !cancelled && + !(err instanceof DOMException && err.name === "AbortError") + ) { + setError(t("walletLoginFailed")); + } } }; @@ -75,6 +84,7 @@ export function WalletLoginModal() { const timer = window.setInterval(() => void poll(), pollIntervalMs); return () => { cancelled = true; + abortController.abort(); window.clearInterval(timer); }; }, [completeLogin, loginModalOpen, state, tpRequest, t, showToast]); diff --git a/src/wallet/api.ts b/src/wallet/api.ts index 861cf2c..b6c664f 100644 --- a/src/wallet/api.ts +++ b/src/wallet/api.ts @@ -63,9 +63,11 @@ export function createTokenPocketLoginRequest(): Promise { const res = await fetch( `${apiBase}/api/auth/wallet/tp-result?actionId=${encodeURIComponent(actionId)}`, + { signal }, ); if (!res.ok) throw new Error(await res.text()); return res.json() as Promise;