From cea8d3da28b3c4c40c3095c79b4fb486acf3c6f1 Mon Sep 17 00:00:00 2001 From: iamonuwa Date: Wed, 22 Oct 2025 19:15:45 +0100 Subject: [PATCH 1/6] chore: wallet connect updates --- apps/hub/.env | 2 +- apps/hub/package.json | 3 +- .../src/app/_components/connect-button.tsx | 24 +- apps/hub/src/app/_constants/chain.ts | 46 +++- apps/hub/src/app/_constants/session.ts | 24 ++ apps/hub/src/app/_constants/siwe.ts | 32 +++ .../app/_providers/connectkit-provider.tsx | 15 -- apps/hub/src/app/_providers/index.tsx | 58 ++++- .../app/_providers/query-client-provider.tsx | 31 --- apps/hub/src/app/_providers/siwe-provider.tsx | 34 +++ .../hub/src/app/_providers/vault-provider.tsx | 28 +++ .../hub/src/app/_providers/wagmi-provider.tsx | 54 ----- apps/hub/src/app/api/siwe/logout/route.ts | 124 ++++++++++ apps/hub/src/app/api/siwe/nonce/route.ts | 91 +++++++ apps/hub/src/app/api/siwe/session/route.ts | 115 +++++++++ apps/hub/src/app/api/siwe/verify/route.ts | 160 +++++++++++++ apps/hub/src/utils/session.ts | 224 ++++++++++++++++++ apps/hub/src/wagmi.ts | 96 -------- package.json | 3 +- pnpm-lock.yaml | 194 +++++++++++++-- 20 files changed, 1103 insertions(+), 255 deletions(-) create mode 100644 apps/hub/src/app/_constants/session.ts create mode 100644 apps/hub/src/app/_constants/siwe.ts delete mode 100644 apps/hub/src/app/_providers/connectkit-provider.tsx delete mode 100644 apps/hub/src/app/_providers/query-client-provider.tsx create mode 100644 apps/hub/src/app/_providers/siwe-provider.tsx create mode 100644 apps/hub/src/app/_providers/vault-provider.tsx delete mode 100644 apps/hub/src/app/_providers/wagmi-provider.tsx create mode 100644 apps/hub/src/app/api/siwe/logout/route.ts create mode 100644 apps/hub/src/app/api/siwe/nonce/route.ts create mode 100644 apps/hub/src/app/api/siwe/session/route.ts create mode 100644 apps/hub/src/app/api/siwe/verify/route.ts create mode 100644 apps/hub/src/utils/session.ts delete mode 100644 apps/hub/src/wagmi.ts diff --git a/apps/hub/.env b/apps/hub/.env index 5d2858140..64e0e2f58 100644 --- a/apps/hub/.env +++ b/apps/hub/.env @@ -16,4 +16,4 @@ # – https://vercel.com/docs/cli/env#exporting-development-environment-variables -NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="123" +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="7ab664eee6a734b14327cdf4678a3431" diff --git a/apps/hub/package.json b/apps/hub/package.json index 509c86da3..0f9f9e040 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -27,6 +27,7 @@ "@tanstack/react-table": "^8.21.3", "@vercel/analytics": "^1.5.0", "connectkit": "^1.9.0", + "connectkit-next-siwe": "^0.3.0", "cva": "1.0.0-beta.1", "framer-motion": "^12.0.6", "next": "15.3.0", @@ -38,7 +39,7 @@ "rehype-slug": "^6.0.0", "siwe": "^2.3.2", "ts-pattern": "^5.6.2", - "viem": "^2.21.1", + "viem": "^2.29.1", "wagmi": "2.15.2", "zod": "^3.24.1" }, diff --git a/apps/hub/src/app/_components/connect-button.tsx b/apps/hub/src/app/_components/connect-button.tsx index e8cb92ab6..dfa6a5c04 100644 --- a/apps/hub/src/app/_components/connect-button.tsx +++ b/apps/hub/src/app/_components/connect-button.tsx @@ -2,45 +2,39 @@ import { Button, ShortenAddress } from '@status-im/status-network/components' import { ConnectKitButton } from 'connectkit' -import { useAccount } from 'wagmi' import type { ComponentProps } from 'react' type Props = { size?: ComponentProps['size'] label?: string - shortLabel?: string + className?: string + /** If true, shows the label instead of the shortened address when connected */ + alwaysShowLabel?: boolean } const ConnectButton = (props: Props) => { const { size = '32', label = 'Connect wallet', - shortLabel = 'Connect', + className, + alwaysShowLabel = false, } = props - const { address, isConnected } = useAccount() - return ( - {({ show }) => { + {({ show, isConnected, address }) => { return ( ) diff --git a/apps/hub/src/app/_constants/chain.ts b/apps/hub/src/app/_constants/chain.ts index d2e71f3c6..e4bb3059f 100644 --- a/apps/hub/src/app/_constants/chain.ts +++ b/apps/hub/src/app/_constants/chain.ts @@ -1,20 +1,24 @@ +import { getDefaultConfig } from 'connectkit' +import { defineChain } from 'viem' +import { createConfig, http } from 'wagmi' + +import { clientEnv } from './env.client.mjs' + +import type { + CreateConfigParameters, + CreateConnectorFn, + Transport, +} from 'wagmi' import type { Chain } from 'wagmi/chains' -export const statusNetworkTestnet: Chain = { - id: Number(1660990954), +export const testnet = defineChain({ + id: 1660990954, name: 'Status Network Testnet', - nativeCurrency: { - decimals: 18, - name: 'Ether', - symbol: 'ETH', - }, + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['https://public.sepolia.rpc.status.network'], }, - public: { - http: ['https://public.sepolia.rpc.status.network'], - }, }, blockExplorers: { default: { @@ -22,4 +26,24 @@ export const statusNetworkTestnet: Chain = { url: 'https://sepoliascan.status.network', }, }, -} +}) + +export const getDefaultWagmiConfig = () => + getDefaultConfig({ + chains: [testnet], + transports: { + [testnet.id]: http(testnet.rpcUrls.default.http[0]), + }, + walletConnectProjectId: + clientEnv.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID as string, + appName: 'Status Hub', + appDescription: 'Status Network DeFi Dashboard', + appUrl: 'https://status.app', + appIcon: 'https://status.app/icon.png', + }) as CreateConfigParameters< + readonly [Chain, ...Chain[]], + Record, + readonly CreateConnectorFn[] + > + +export const wagmiConfig = createConfig(getDefaultWagmiConfig()) diff --git a/apps/hub/src/app/_constants/session.ts b/apps/hub/src/app/_constants/session.ts new file mode 100644 index 000000000..d83d586e8 --- /dev/null +++ b/apps/hub/src/app/_constants/session.ts @@ -0,0 +1,24 @@ +type SessionConfig = { + cookieName: string + cookieOptions: { + httpOnly: boolean + secure: boolean + sameSite: 'lax' | 'strict' | 'none' + maxAge: number + path: string + } + password: string +} + +export const sessionConfig: SessionConfig = { + cookieName: 'status-hub-siwe', + cookieOptions: { + httpOnly: true, + secure: false, + sameSite: 'lax', + maxAge: 60 * 60, + path: '/', + }, + password: + 'complex_password_at_least_32_characters_long_replace_in_production_env', +} diff --git a/apps/hub/src/app/_constants/siwe.ts b/apps/hub/src/app/_constants/siwe.ts new file mode 100644 index 000000000..5b1f1dc29 --- /dev/null +++ b/apps/hub/src/app/_constants/siwe.ts @@ -0,0 +1,32 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import { + configureClientSIWE, + configureServerSideSIWE, +} from 'connectkit-next-siwe' + +import { getDefaultWagmiConfig } from './chain' +import { sessionConfig } from './session' + +const wagmiConfig = getDefaultWagmiConfig() + +/** + * Server-side SIWE configuration + * This handles signature verification and session management on the server + */ +export const siweServer = configureServerSideSIWE({ + config: { + chains: wagmiConfig.chains, + transports: wagmiConfig.transports, + }, + session: sessionConfig, +}) + +/** + * Client-side SIWE configuration + * This handles the authentication flow on the client + */ +export const siweClient = configureClientSIWE({ + apiRoutePrefix: '/api/siwe', + statement: 'Sign in with Ethereum to access Status Hub.', +}) diff --git a/apps/hub/src/app/_providers/connectkit-provider.tsx b/apps/hub/src/app/_providers/connectkit-provider.tsx deleted file mode 100644 index c39640cc0..000000000 --- a/apps/hub/src/app/_providers/connectkit-provider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import { ConnectKitProvider as ConnectKit } from 'connectkit' - -type ConnectKitProviderProps = { - children: React.ReactNode -} - -export function ConnectKitProvider({ children }: ConnectKitProviderProps) { - return ( - - {children} - - ) -} diff --git a/apps/hub/src/app/_providers/index.tsx b/apps/hub/src/app/_providers/index.tsx index 1e55ed291..2f80f29db 100644 --- a/apps/hub/src/app/_providers/index.tsx +++ b/apps/hub/src/app/_providers/index.tsx @@ -1,21 +1,61 @@ 'use client' -import { VaultStateProvider } from '../_hooks/useVaultStateContext' -import { ConnectKitProvider } from './connectkit-provider' -import { QueryClientProvider } from './query-client-provider' -import { WagmiProvider } from './wagmi-provider' +import { ToastContainer } from '@status-im/components' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ConnectKitProvider } from 'connectkit' +import { WagmiProvider } from 'wagmi' + +import { testnet, wagmiConfig } from '~constants/chain' +import { VaultStateProvider } from '~hooks/useVaultStateContext' + +import { SiweProvider } from './siwe-provider' +import { VaultProvider } from './vault-provider' interface ProvidersProps { children: React.ReactNode } +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + retryOnMount: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }, + }, +}) + +/** + * Application Providers + * + * Provider hierarchy (order matters): + * 1. WagmiProvider - Blockchain connection & wallet state + * 2. QueryClientProvider - React Query for data fetching + * 3. SiweProvider - SIWE authentication + * 4. ConnectKitProvider - Wallet connection UI + * 5. VaultStateProvider - Vault operation state machine + * 6. VaultProvider - Vault-specific features + */ export function Providers({ children }: ProvidersProps) { return ( - - - - {children} - + + + + + + + {children} + + + + + ) diff --git a/apps/hub/src/app/_providers/query-client-provider.tsx b/apps/hub/src/app/_providers/query-client-provider.tsx deleted file mode 100644 index 4cdc10c62..000000000 --- a/apps/hub/src/app/_providers/query-client-provider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client' - -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -import type React from 'react' - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - retryOnMount: false, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - }, -}) - -type Props = { - children: React.ReactNode -} - -function _QueryClientProvider(props: Props) { - const { children } = props - - return ( - {children} - ) -} - -export { _QueryClientProvider as QueryClientProvider } diff --git a/apps/hub/src/app/_providers/siwe-provider.tsx b/apps/hub/src/app/_providers/siwe-provider.tsx new file mode 100644 index 000000000..50b65c2a9 --- /dev/null +++ b/apps/hub/src/app/_providers/siwe-provider.tsx @@ -0,0 +1,34 @@ +'use client' +import { useRouter } from 'next/navigation' +import { useDisconnect } from 'wagmi' + +import { siweClient } from '../_constants/siwe' + +interface SiweProviderProps { + children: React.ReactNode +} + +export const SiweProvider = ({ children }: SiweProviderProps) => { + const router = useRouter() + const { disconnect } = useDisconnect() + + return ( + { + router.refresh() + }} + onSignOut={() => { + disconnect() + router.refresh() + }} + > + {children} + + ) +} diff --git a/apps/hub/src/app/_providers/vault-provider.tsx b/apps/hub/src/app/_providers/vault-provider.tsx new file mode 100644 index 000000000..025c3eded --- /dev/null +++ b/apps/hub/src/app/_providers/vault-provider.tsx @@ -0,0 +1,28 @@ +'use client' + +import { ActionStatusDialog } from '../_components/stake/action-status-dialog' +import { useActionStatusContent } from '../_components/stake/hooks/use-action-status-content' +import { useVaultStateContext } from '../_hooks/useVaultStateContext' + +export const VaultProvider = ({ children }: { children: React.ReactNode }) => { + const { state: vaultState, reset: resetVault } = useVaultStateContext() + const dialogContent = useActionStatusContent(vaultState) + + // Only render dialog in DOM if state is not idle + const shouldRenderDialog = + vaultState.type !== 'idle' && dialogContent !== null + + return ( + <> + {children} + + {shouldRenderDialog ? ( + + ) : null} + + ) +} diff --git a/apps/hub/src/app/_providers/wagmi-provider.tsx b/apps/hub/src/app/_providers/wagmi-provider.tsx deleted file mode 100644 index a8085e27b..000000000 --- a/apps/hub/src/app/_providers/wagmi-provider.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client' - -import { createConfig, http, WagmiProvider as WagmiProviderBase } from 'wagmi' -import { mainnet } from 'wagmi/chains' -import { injected, metaMask, walletConnect } from 'wagmi/connectors' - -import { clientEnv } from '~constants/env.client.mjs' - -import type React from 'react' - -export const config = createConfig({ - chains: [mainnet], - ssr: false, - connectors: [ - injected(), - metaMask(), - walletConnect({ - projectId: clientEnv.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, - showQrModal: false, - }), - ], - transports: { - // todo: replace public clients - [mainnet.id]: http(), - }, -}) - -declare module 'wagmi' { - interface Register { - config: typeof config - } -} - -type Props = { - children: React.ReactNode -} - -function WagmiProvider(props: Props) { - const { children } = props - - return ( - ['config'] - } - > - {children} - - ) -} - -export { config as wagmiConfig, WagmiProvider } diff --git a/apps/hub/src/app/api/siwe/logout/route.ts b/apps/hub/src/app/api/siwe/logout/route.ts new file mode 100644 index 000000000..a342753ad --- /dev/null +++ b/apps/hub/src/app/api/siwe/logout/route.ts @@ -0,0 +1,124 @@ +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +import { sessionConfig } from '~constants/session' +import { getSession } from '~utils/session' + +/** + * @api {get} /api/siwe/logout Logout + * @apiName Logout + * @apiGroup SIWE + * + * @apiDescription + * Terminates the current SIWE authentication session by destroying session data + * and removing the session cookie. This endpoint securely logs the user out by: + * - Clearing all session data (address, chainId, nonce) + * - Removing the session cookie from the browser + * - Invalidating the encrypted session seal + * + * **Security:** + * - Session destruction is immediate and irreversible + * - Cookie is cleared with maxAge: 0 to ensure browser deletion + * - No data persists after logout + * + * **Use Cases:** + * - User clicks "Disconnect" or "Logout" button + * - Session expiration or invalidation + * - Account switching (logout before new auth) + * - Security-related forced logout + * + * **Flow:** + * 1. Client sends GET request to /api/siwe/logout + * 2. Server retrieves current session from cookie + * 3. Server destroys session data + * 4. Server removes session cookie (sets maxAge: 0) + * 5. Client receives success confirmation + * 6. Client typically disconnects wallet and refreshes UI + * + * @apiSuccess {Boolean} success True if logout succeeded + * @apiSuccess {String} message Success message + * + * @apiSuccessExample {json} Success Response (200): + * { + * "success": true, + * "message": "Logged out successfully" + * } + * + * @apiError (500) {Object} error Server error + * @apiError (500) {String} error.error Error message + * @apiError (500) {String} error.details Detailed error information + * + * @apiErrorExample {json} Logout Failed (500): + * { + * "error": "Failed to logout", + * "details": "Session destruction failed" + * } + * + * @apiExample {curl} cURL Example: + * curl -X GET https://status.app/api/siwe/logout \ + * --cookie "status-hub-siwe=..." + * + * @apiExample {javascript} JavaScript Example: + * const response = await fetch('/api/siwe/logout', { + * credentials: 'include' + * }) + * const result = await response.json() + * + * if (result.success) { + * console.log('Logged out successfully') + * // Disconnect wallet, redirect, etc. + * } + * + * @apiExample {javascript} React Example: + * function LogoutButton() { + * const { disconnect } = useDisconnect() + * const router = useRouter() + * + * const handleLogout = async () => { + * try { + * await fetch('/api/siwe/logout') + * disconnect() // Disconnect wallet + * router.push('/') // Redirect to home + * } catch (error) { + * console.error('Logout failed:', error) + * } + * } + * + * return + * } + */ +export async function GET() { + try { + const cookieStore = await cookies() + const session = await getSession(cookieStore) + + // Clear session data + session.destroy() + + // Delete cookie using Next.js cookies API + cookieStore.set({ + name: sessionConfig.cookieName, + value: '', + ...sessionConfig.cookieOptions, + maxAge: 0, + }) + + return NextResponse.json({ + success: true, + message: 'Logged out successfully', + }) + } catch (error) { + console.error('[SIWE Logout] Logout error:', error) + + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred' + + return NextResponse.json( + { + error: 'Failed to logout', + details: errorMessage, + }, + { status: 500 } + ) + } +} diff --git a/apps/hub/src/app/api/siwe/nonce/route.ts b/apps/hub/src/app/api/siwe/nonce/route.ts new file mode 100644 index 000000000..16b558de1 --- /dev/null +++ b/apps/hub/src/app/api/siwe/nonce/route.ts @@ -0,0 +1,91 @@ +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' +import { generateNonce } from 'siwe' + +import { sessionConfig } from '~constants/session' +import { getSession } from '~utils/session' + +/** + * @api {get} /api/siwe/nonce Get Nonce + * @apiName GetNonce + * @apiGroup SIWE + * + * @apiDescription + * Generates a cryptographically secure random nonce for SIWE message signing. + * The nonce is stored in the session and must be included in the SIWE message + * that the user signs. This prevents replay attacks by ensuring each signature + * request is unique. + * + * **Flow:** + * 1. Client requests nonce from this endpoint + * 2. Server generates nonce and stores in session cookie + * 3. Client includes nonce in SIWE message + * 4. Client sends signed message to /api/siwe/verify + * + * **Idempotency:** + * If a nonce already exists in the session, it will be returned without + * generating a new one. This prevents multiple nonces per session. + * + * @apiSuccess {String} nonce Cryptographically secure random nonce + * + * @apiSuccessExample {text} Success Response (200): + * "a1b2c3d4e5f6g7h8" + * + * @apiError (500) {Object} error Error details + * @apiError (500) {String} error.error Error message + * @apiError (500) {String} error.details Detailed error information + * + * @apiErrorExample {json} Error Response (500): + * { + * "error": "Failed to generate nonce", + * "details": "Session encryption failed" + * } + * + * @apiExample {curl} cURL Example: + * curl -X GET https://status.app/api/siwe/nonce + * + * @apiExample {javascript} JavaScript Example: + * const response = await fetch('/api/siwe/nonce') + * const nonce = await response.text() + */ +export async function GET() { + try { + const cookieStore = await cookies() + const session = await getSession(cookieStore) + + // Generate new nonce if one doesn't exist + if (!session.nonce) { + session.nonce = generateNonce() + + // Seal session and set cookie with nonce + const seal = await session.getSeal() + cookieStore.set({ + name: sessionConfig.cookieName, + value: seal, + ...sessionConfig.cookieOptions, + }) + } + + // Return nonce as plain text (ConnectKit expects text/plain) + return new Response(session.nonce, { + status: 200, + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'Cache-Control': 'no-store, must-revalidate', + }, + }) + } catch (error) { + console.error('[SIWE Nonce] Error generating nonce:', error) + + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred' + + return NextResponse.json( + { + error: 'Failed to generate nonce', + details: errorMessage, + }, + { status: 500 } + ) + } +} diff --git a/apps/hub/src/app/api/siwe/session/route.ts b/apps/hub/src/app/api/siwe/session/route.ts new file mode 100644 index 000000000..382714cff --- /dev/null +++ b/apps/hub/src/app/api/siwe/session/route.ts @@ -0,0 +1,115 @@ +import { NextResponse } from 'next/server' + +import { getReadonlySession, isValidSIWESession } from '~utils/session' + +import type { NextRequest } from 'next/server' + +/** + * @api {get} /api/siwe/session Get Session + * @apiName GetSession + * @apiGroup SIWE + * + * @apiDescription + * Retrieves the current SIWE authentication session data. This endpoint checks + * for an existing authenticated session and returns the user's Ethereum address, + * chain ID, and authentication status. No authentication required - returns null + * values if not authenticated. + * + * **Use Cases:** + * - Check if user is currently authenticated + * - Get authenticated user's wallet address + * - Determine which chain the user authenticated on + * - Conditional UI rendering based on auth status + * + * **Session Storage:** + * Session data is stored in an encrypted iron-session cookie, which is + * automatically included in the request by the browser. + * + * **Authentication Flow:** + * 1. Browser automatically sends session cookie with request + * 2. Server decrypts and validates session cookie + * 3. Returns session data if valid, null values if not authenticated + * + * @apiSuccess {String|null} address Ethereum address of authenticated user (null if not authenticated) + * @apiSuccess {Number|null} chainId Chain ID the user authenticated on (null if not authenticated) + * @apiSuccess {Boolean} isAuthenticated True if user has valid SIWE session + * + * @apiSuccessExample {json} Authenticated Response (200): + * { + * "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4", + * "chainId": 1660990954, + * "isAuthenticated": true + * } + * + * @apiSuccessExample {json} Unauthenticated Response (200): + * { + * "address": null, + * "chainId": null, + * "isAuthenticated": false + * } + * + * @apiError (500) {Object} error Server error + * @apiError (500) {String} error.error Error message + * @apiError (500) {String} error.details Detailed error information + * + * @apiErrorExample {json} Session Retrieval Failed (500): + * { + * "error": "Failed to retrieve session", + * "details": "Session decryption failed" + * } + * + * @apiExample {curl} cURL Example: + * curl -X GET https://status.app/api/siwe/session \ + * --cookie "status-hub-siwe=..." + * + * @apiExample {javascript} JavaScript Example: + * // Fetch automatically includes cookies + * const response = await fetch('/api/siwe/session', { + * credentials: 'include' + * }) + * const session = await response.json() + * + * if (session.isAuthenticated) { + * console.log(`Authenticated as ${session.address}`) + * } else { + * console.log('Not authenticated') + * } + * + * @apiExample {javascript} React Hook Example: + * function useSession() { + * const [session, setSession] = useState(null) + * + * useEffect(() => { + * fetch('/api/siwe/session') + * .then(res => res.json()) + * .then(setSession) + * }, []) + * + * return session + * } + */ +export async function GET(request: NextRequest) { + try { + const session = await getReadonlySession(request.cookies) + + // Return session data (may have undefined values if not authenticated) + return NextResponse.json({ + address: session.address ?? null, + chainId: session.chainId ?? null, + isAuthenticated: isValidSIWESession(session), + }) + } catch (error) { + console.error('[SIWE Session] Session retrieval error:', error) + + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred' + + return NextResponse.json( + { + error: 'Failed to retrieve session', + details: errorMessage, + }, + { status: 500 } + ) + } +} diff --git a/apps/hub/src/app/api/siwe/verify/route.ts b/apps/hub/src/app/api/siwe/verify/route.ts new file mode 100644 index 000000000..5270c066b --- /dev/null +++ b/apps/hub/src/app/api/siwe/verify/route.ts @@ -0,0 +1,160 @@ +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' +import { SiweMessage } from 'siwe' + +import { sessionConfig } from '~constants/session' +import { getSession } from '~utils/session' + +import type { NextRequest } from 'next/server' + +/** + * @api {post} /api/siwe/verify Verify Signature + * @apiName VerifySignature + * @apiGroup SIWE + * + * @apiDescription + * Verifies a SIWE (Sign-In with Ethereum) signature and creates an authenticated + * session. This endpoint validates the cryptographic signature against the SIWE + * message, checks the nonce matches the session, and stores the authenticated + * address in the session. + * + * **Security:** + * - Validates signature cryptographically using secp256k1 + * - Checks nonce matches to prevent replay attacks + * - Verifies message hasn't expired + * - Ensures domain and chain ID are correct + * + * **Flow:** + * 1. Client gets nonce from /api/siwe/nonce + * 2. Client creates SIWE message with nonce + * 3. User signs message in wallet + * 4. Client sends message + signature to this endpoint + * 5. Server verifies and creates authenticated session + * + * @apiParam {String} message The SIWE message that was signed + * @apiParam {String} signature The hex-encoded signature (0x...) + * + * @apiParamExample {json} Request Body: + * { + * "message": "status.app wants you to sign in with your Ethereum account:\n0x...", + * "signature": "0xabc123..." + * } + * + * @apiSuccess {Boolean} success True if verification succeeded + * @apiSuccess {String} address Ethereum address that was authenticated + * @apiSuccess {Number} chainId Chain ID the user authenticated on + * + * @apiSuccessExample {json} Success Response (200): + * { + * "success": true, + * "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4", + * "chainId": 1660990954 + * } + * + * @apiError (400) {Object} error Validation error + * @apiError (400) {String} error.error Error message + * + * @apiError (500) {Object} error Server error + * @apiError (500) {String} error.error Error message + * @apiError (500) {String} error.details Detailed error information + * + * @apiErrorExample {json} Missing Fields (400): + * { + * "error": "Missing message or signature" + * } + * + * @apiErrorExample {json} Invalid Nonce (400): + * { + * "error": "Invalid nonce. Nonce does not match session." + * } + * + * @apiErrorExample {json} Verification Failed (500): + * { + * "error": "Signature verification failed", + * "details": "Signature does not match address" + * } + * + * @apiExample {curl} cURL Example: + * curl -X POST https://status.app/api/siwe/verify \ + * -H "Content-Type: application/json" \ + * -d '{"message": "...", "signature": "0x..."}' + * + * @apiExample {javascript} JavaScript Example: + * const response = await fetch('/api/siwe/verify', { + * method: 'POST', + * headers: { 'Content-Type': 'application/json' }, + * body: JSON.stringify({ message, signature }) + * }) + * const data = await response.json() + */ +export async function POST(request: NextRequest) { + try { + const cookieStore = await cookies() + const session = await getSession(cookieStore) + + // Parse and validate request body + const body = await request.json() + const { message, signature } = body + + if (!message || !signature) { + return NextResponse.json( + { error: 'Missing message or signature' }, + { status: 400 } + ) + } + + // Verify nonce exists in session + if (!session.nonce) { + return NextResponse.json( + { error: 'No nonce in session. Request a nonce first.' }, + { status: 400 } + ) + } + + // Verify the SIWE message signature + const siweMessage = new SiweMessage(message) + const fields = await siweMessage.verify({ signature }) + + // Validate nonce matches session (prevents replay attacks) + if (fields.data.nonce !== session.nonce) { + return NextResponse.json( + { error: 'Invalid nonce. Nonce does not match session.' }, + { status: 400 } + ) + } + + // Store authenticated address and chain ID in session + session.address = fields.data.address + session.chainId = fields.data.chainId + + // Clear nonce after successful verification (prevents reuse) + delete session.nonce + + // Seal and persist session + const seal = await session.getSeal() + cookieStore.set({ + name: sessionConfig.cookieName, + value: seal, + ...sessionConfig.cookieOptions, + }) + + return NextResponse.json({ + success: true, + address: fields.data.address, + chainId: fields.data.chainId, + }) + } catch (error) { + console.error('[SIWE Verify] Signature verification error:', error) + + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred' + + return NextResponse.json( + { + error: 'Signature verification failed', + details: errorMessage, + }, + { status: 500 } + ) + } +} diff --git a/apps/hub/src/utils/session.ts b/apps/hub/src/utils/session.ts new file mode 100644 index 000000000..bc1654f8d --- /dev/null +++ b/apps/hub/src/utils/session.ts @@ -0,0 +1,224 @@ +import { sealData, unsealData } from 'iron-session' + +import { sessionConfig } from '~constants/session' + +import type { IronSession } from 'iron-session' +import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies' +import type { + RequestCookies, + ResponseCookies, +} from 'next/dist/server/web/spec-extension/cookies' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Map of password IDs to password strings for iron-session + * Allows password rotation by using multiple passwords with different IDs + */ +type PasswordsMap = Record + +/** + * Password can be a single string or a map for rotation + */ +type Password = string | PasswordsMap + +/** + * SIWE Session Data + * Extends IronSession with SIWE-specific fields + */ +export interface SIWESessionData extends IronSession { + /** Unique nonce for SIWE message */ + nonce?: string + /** Ethereum address of authenticated user */ + address?: string + /** Chain ID the user authenticated on */ + chainId?: number +} + +/** + * Session with utility methods + * Combines session data with helper functions for sealing and destroying + */ +export interface NextSIWESession extends SIWESessionData { + /** Get sealed session string for cookie storage */ + getSeal: () => Promise + /** Clear session data and cookie */ + destroy: () => void + /** Save session data (iron-session compatibility) */ + save: () => Promise + /** Update session (iron-session compatibility) */ + updateConfig: (newSessionOptions: unknown) => void +} + +/** + * Readonly session without modification methods + */ +export type ReadonlyNextSIWESession = Omit< + NextSIWESession, + 'getSeal' | 'destroy' +> + +/** + * Supported cookie types from Next.js + */ +type CookieStore = ReadonlyRequestCookies | RequestCookies | ResponseCookies + +/** + * Writable cookie types (excludes readonly) + */ +type WritableCookieStore = RequestCookies | ResponseCookies + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Normalize password to map format for iron-session + * Single strings are converted to { 1: password } + * + * @param password - Password string or map + * @returns Password map for iron-session + */ +function normalizePasswordToMap(password: Password): PasswordsMap { + return typeof password === 'string' ? { 1: password } : password +} + +// ============================================================================ +// Session Functions +// ============================================================================ + +/** + * Get a readonly session from cookies + * Does not provide methods to modify or persist the session + * + * @param cookies - Cookie store from Next.js request + * @returns Promise resolving to readonly session data + * + * @example + * ```typescript + * import { cookies } from 'next/headers' + * + * const session = await getReadonlySession(cookies()) + * console.log(session.address) // "0x123..." + * ``` + */ +export async function getReadonlySession( + cookies: CookieStore +): Promise { + const passwordsAsMap = normalizePasswordToMap(sessionConfig.password) + const sealFromCookies = cookies.get(sessionConfig.cookieName) + + // If no cookie exists, return empty session + if (!sealFromCookies?.value) { + return {} as ReadonlyNextSIWESession + } + + try { + // Unseal the session data from the cookie + const session = await unsealData(sealFromCookies.value, { + password: passwordsAsMap, + }) + + return session as ReadonlyNextSIWESession + } catch (error) { + // If unsealing fails (corrupted cookie, wrong password, etc.), return empty + console.error('Failed to unseal session:', error) + return {} as ReadonlyNextSIWESession + } +} + +/** + * Get a full session with modification methods + * Provides getSeal() and destroy() methods for persisting/clearing session + * + * @param cookies - Writable cookie store from Next.js request + * @returns Promise resolving to session with utility methods + * + * @example + * ```typescript + * import { cookies } from 'next/headers' + * + * const session = await getSession(cookies()) + * session.address = '0x123...' + * const seal = await session.getSeal() + * cookies().set(cookieName, seal) + * ``` + */ +export async function getSession( + cookies: WritableCookieStore +): Promise { + const passwordsAsMap = normalizePasswordToMap(sessionConfig.password) + const readonlySession = await getReadonlySession(cookies) + + // Create a mutable copy of the session + const session = { ...readonlySession } as NextSIWESession + + // Add getSeal method to seal and return session data + Object.defineProperty(session, 'getSeal', { + enumerable: false, // Don't include in Object.keys() + writable: false, + value: async function getSeal(): Promise { + const seal = await sealData(session, { + password: passwordsAsMap, + }) + + return seal + }, + }) + + // Add destroy method to clear session + Object.defineProperty(session, 'destroy', { + enumerable: false, // Don't include in Object.keys() + writable: false, + value: function destroy(): void { + // Clear all session data + const keys = Object.keys(session) as Array + keys.forEach(key => { + if (key !== 'getSeal' && key !== 'destroy') { + delete session[key] + } + }) + + // Clear the cookie + cookies.set({ + name: sessionConfig.cookieName, + value: '', + maxAge: 0, + path: sessionConfig.cookieOptions.path, + }) + }, + }) + + return session +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Check if a session is authenticated + * A session is considered authenticated if it has an address + * + * @param session - Session to check + * @returns True if session has an address + */ +export function isAuthenticated( + session: Partial +): session is Required> { + return Boolean(session.address) +} + +/** + * Validate session has required SIWE fields + * + * @param session - Session to validate + * @returns True if session has address and chainId + */ +export function isValidSIWESession( + session: Partial +): session is Required> { + return Boolean(session.address && session.chainId) +} diff --git a/apps/hub/src/wagmi.ts b/apps/hub/src/wagmi.ts deleted file mode 100644 index 2ee534d06..000000000 --- a/apps/hub/src/wagmi.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { createConfig, http } from 'wagmi' - -import type { Config, CreateConfigParameters, Transport } from 'wagmi' -import type { Chain } from 'wagmi/chains' - -// Todo: move this to shared packages - -/** - * Configuration options for Status Hub wagmi config - */ -export interface DefineWagmiConfigOptions { - /** - * Array of chains to support - */ - chains: [Chain, ...Chain[]] - - /** - * Enable server-side rendering support - * @default false - */ - ssr?: boolean - - /** - * Custom RPC URLs per chain (optional) - * Maps chain IDs to RPC URLs - */ - rpcUrls?: Record - - /** - * Batch JSON-RPC requests - * @default undefined - */ - batch?: CreateConfigParameters['batch'] - - /** - * Polling interval in milliseconds - * @default undefined - */ - pollingInterval?: number - - /** - * WalletConnect project ID - * @default undefined - */ - walletConnectProjectId?: string -} - -/** - * Creates a wagmi configuration for Status Hub with the specified chains and options - * - * @param options - Configuration options - * @returns Wagmi config instance - * - * @example - * ```ts - * import { mainnet, optimism } from 'wagmi/chains' - * - * const config = defineWagmiConfig({ - * chains: [mainnet, optimism], - * ssr: true, - * walletConnectProjectId: 'your-project-id', - * rpcUrls: { - * [mainnet.id]: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY' - * } - * }) - * ``` - */ -export const defineWagmiConfig = ( - options: DefineWagmiConfigOptions -): Config => { - const { chains, ssr = false, rpcUrls, batch, pollingInterval } = options - - const transports = chains.reduce( - (acc, chain) => { - const rpcUrl = rpcUrls?.[chain.id] - acc[chain.id] = http(rpcUrl, { - batch: batch ? true : undefined, - ...(pollingInterval && { pollingInterval }), - }) - return acc - }, - {} as Record - ) - - return createConfig({ - chains, - ssr, - transports, - batch, - }) -} - -/** - * Type helper to infer the config type from defineWagmiConfig - */ -export type InferWagmiConfig = ReturnType diff --git a/package.json b/package.json index 87196f915..00f5426c4 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "@scure/base": "^1.2.6" }, "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.1.1", "@changesets/cli": "^2.26.2", + "@ianvs/prettier-plugin-sort-imports": "^4.1.1", "@status-im/eslint-config": "workspace:*", "@tsconfig/strictest": "^2.0.0", "@types/prettier": "^2.7.2", @@ -80,6 +80,7 @@ "dependencies": { "@tanstack/react-query": "^5.90.2", "connectkit": "^1.9.0", + "iron-session": "^8.0.4", "wagmi": "^2.12.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 840176b41..a61030e72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,9 @@ importers: connectkit: specifier: ^1.9.0 version: 1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.1(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + iron-session: + specifier: ^8.0.4 + version: 8.0.4 wagmi: specifier: ^2.12.8 version: 2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) @@ -295,6 +298,9 @@ importers: connectkit: specifier: ^1.9.0 version: 1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + connectkit-next-siwe: + specifier: ^0.3.0 + version: 0.3.0(connectkit@1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)) cva: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1(typescript@5.8.3) @@ -329,7 +335,7 @@ importers: specifier: ^5.6.2 version: 5.7.1 viem: - specifier: ^2.21.1 + specifier: ^2.29.1 version: 2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: 2.15.2 @@ -5849,8 +5855,8 @@ packages: '@radix-ui/react-alert-dialog@1.1.13': resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==} peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' + '@types/react': 19.1.0 + '@types/react-dom': 19.1.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -6303,8 +6309,8 @@ packages: '@radix-ui/react-roving-focus@1.1.9': resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==} peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' + '@types/react': 19.1.0 + '@types/react-dom': 19.1.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -8131,6 +8137,9 @@ packages: '@tybys/wasm-util@0.8.3': resolution: {integrity: sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==} + '@types/accepts@1.3.7': + resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} + '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -8176,6 +8185,15 @@ packages: '@types/connect@3.4.35': resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + '@types/content-disposition@0.5.9': + resolution: {integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==} + + '@types/cookie@0.5.4': + resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==} + + '@types/cookies@0.9.1': + resolution: {integrity: sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==} + '@types/d3-array@3.0.3': resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==} @@ -8302,9 +8320,15 @@ packages: '@types/highlight-words-core@1.2.3': resolution: {integrity: sha512-PWNU/NR0CaYEsK38mcCTyDzeS2TlEGK9kRhRMz1i86jVAe836ZlA3gl6QYpu+CG6IpfNKTgWpEnJuRededvC0g==} + '@types/http-assert@1.5.6': + resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} + '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/is-ci@3.0.0': resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==} @@ -8329,9 +8353,18 @@ packages: '@types/jsonfile@6.1.1': resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} + '@types/keygrip@1.0.6': + resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/koa-compose@3.2.8': + resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} + + '@types/koa@2.15.0': + resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} + '@types/lodash@4.14.191': resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} @@ -10264,6 +10297,16 @@ packages: resolution: {integrity: sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==} engines: {node: '>=18'} + connectkit-next-siwe@0.3.0: + resolution: {integrity: sha512-zUvx1kROpTGJgNst/yzBatQcU1E3pONJvfAcELk1W52B26WJZ5s5ym/hqMBINhT2zVZYV0XKwEexEzfFQey2Yw==} + engines: {node: '>=12.4'} + peerDependencies: + connectkit: '>=1.2.0' + next: '>=12.x' + react: 17.x || 18.x + react-dom: 17.x || 18.x + viem: '>=2.13.3' + connectkit@1.9.0: resolution: {integrity: sha512-bkqg8zK35pWWG2q8xeo41J1mnBP8D2ffOd/ItB12aad9QZZU20SlEeiQM9iYfRyl0JAH1tqIDlZbXajqZBFfDw==} engines: {node: '>=12.4'} @@ -10320,10 +10363,18 @@ packages: resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} engines: {node: '>= 0.6'} + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} @@ -12766,6 +12817,27 @@ packages: resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} engines: {node: '>= 10'} + iron-session@6.3.1: + resolution: {integrity: sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A==} + engines: {node: '>=12'} + peerDependencies: + express: '>=4' + koa: '>=2' + next: '>=10' + peerDependenciesMeta: + express: + optional: true + koa: + optional: true + next: + optional: true + + iron-session@8.0.4: + resolution: {integrity: sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==} + + iron-webcrypto@0.2.8: + resolution: {integrity: sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA==} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -18872,6 +18944,9 @@ packages: zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -28395,7 +28470,7 @@ snapshots: babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 unplugin: 2.3.2 - zod: 3.25.76 + zod: 3.23.8 optionalDependencies: '@tanstack/react-router': 1.120.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) vite: 6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1) @@ -28580,6 +28655,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/accepts@1.3.7': + dependencies: + '@types/node': 20.11.5 + '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.7 @@ -28650,6 +28729,17 @@ snapshots: dependencies: '@types/node': 20.11.5 + '@types/content-disposition@0.5.9': {} + + '@types/cookie@0.5.4': {} + + '@types/cookies@0.9.1': + dependencies: + '@types/connect': 3.4.35 + '@types/express': 4.17.21 + '@types/keygrip': 1.0.6 + '@types/node': 20.11.5 + '@types/d3-array@3.0.3': {} '@types/d3-array@3.2.1': {} @@ -28776,8 +28866,12 @@ snapshots: '@types/highlight-words-core@1.2.3': {} + '@types/http-assert@1.5.6': {} + '@types/http-cache-semantics@4.0.4': {} + '@types/http-errors@2.0.5': {} + '@types/is-ci@3.0.0': dependencies: ci-info: 3.8.0 @@ -28798,10 +28892,27 @@ snapshots: dependencies: '@types/node': 20.11.5 + '@types/keygrip@1.0.6': {} + '@types/keyv@3.1.4': dependencies: '@types/node': 20.11.5 + '@types/koa-compose@3.2.8': + dependencies: + '@types/koa': 2.15.0 + + '@types/koa@2.15.0': + dependencies: + '@types/accepts': 1.3.7 + '@types/content-disposition': 0.5.9 + '@types/cookies': 0.9.1 + '@types/http-assert': 1.5.6 + '@types/http-errors': 2.0.5 + '@types/keygrip': 1.0.6 + '@types/koa-compose': 3.2.8 + '@types/node': 20.11.5 + '@types/lodash@4.14.191': {} '@types/mdast@3.0.15': @@ -32742,6 +32853,18 @@ snapshots: graceful-fs: 4.2.11 xdg-basedir: 5.1.0 + connectkit-next-siwe@0.3.0(connectkit@1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)): + dependencies: + connectkit: 1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + iron-session: 6.3.1(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)) + next: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - express + - koa + connectkit@1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.29.0(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.29.0)(@tanstack/react-query@5.29.0(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.8)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@6.0.3)(viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.25.76))(zod@3.25.76)): dependencies: '@tanstack/react-query': 5.29.0(react@19.1.0) @@ -32866,8 +32989,12 @@ snapshots: cookie@0.4.0: {} + cookie@0.5.0: {} + cookie@0.6.0: {} + cookie@0.7.2: {} + copy-anything@2.0.6: dependencies: is-what: 3.14.1 @@ -34042,7 +34169,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 9.14.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -34061,7 +34188,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 9.14.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -34094,7 +34221,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -34105,7 +34232,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -34133,7 +34260,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.14.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -34162,7 +34289,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.14.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -36011,6 +36138,29 @@ snapshots: ipaddr.js@2.1.0: {} + iron-session@6.3.1(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)): + dependencies: + '@peculiar/webcrypto': 1.5.0 + '@types/cookie': 0.5.4 + '@types/express': 4.17.21 + '@types/koa': 2.15.0 + '@types/node': 17.0.45 + cookie: 0.5.0 + iron-webcrypto: 0.2.8 + optionalDependencies: + express: 4.20.0 + next: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + + iron-session@8.0.4: + dependencies: + cookie: 0.7.2 + iron-webcrypto: 1.2.1 + uncrypto: 0.1.3 + + iron-webcrypto@0.2.8: + dependencies: + buffer: 6.0.3 + iron-webcrypto@1.2.1: {} is-absolute-url@4.0.1: {} @@ -36325,7 +36475,7 @@ snapshots: is-weakset@2.0.3: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-what@3.14.1: {} @@ -38975,10 +39125,10 @@ snapshots: ox@0.6.7(typescript@5.6.2)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.10.1 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@noble/curves': 1.9.0 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.6.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: @@ -38989,10 +39139,10 @@ snapshots: ox@0.6.7(typescript@5.8.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.10.1 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@noble/curves': 1.9.0 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.8.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: @@ -44636,6 +44786,8 @@ snapshots: zod@3.22.4: {} + zod@3.23.8: {} + zod@3.25.76: {} zustand@4.3.7(react@18.3.1): From 9e4c863727a44cb7d8e9c9994646fa06c6036892 Mon Sep 17 00:00:00 2001 From: iamonuwa Date: Fri, 24 Oct 2025 01:02:00 +0100 Subject: [PATCH 2/6] refactor: use sn-api endpoints to handle siwe --- apps/hub/.env | 1 + apps/hub/package.json | 1 - apps/hub/src/app/_constants/env.client.mjs | 2 + apps/hub/src/app/_constants/index.ts | 1 + apps/hub/src/app/_constants/session.ts | 24 -- apps/hub/src/app/_constants/siwe.ts | 103 +++++--- apps/hub/src/app/_providers/index.tsx | 8 +- apps/hub/src/app/_providers/siwe-provider.tsx | 34 --- apps/hub/src/app/api/siwe/logout/route.ts | 124 ---------- apps/hub/src/app/api/siwe/nonce/route.ts | 91 ------- apps/hub/src/app/api/siwe/session/route.ts | 115 --------- apps/hub/src/app/api/siwe/verify/route.ts | 160 ------------- apps/hub/src/utils/session.ts | 224 ------------------ pnpm-lock.yaml | 166 +------------ 14 files changed, 93 insertions(+), 961 deletions(-) delete mode 100644 apps/hub/src/app/_constants/session.ts delete mode 100644 apps/hub/src/app/_providers/siwe-provider.tsx delete mode 100644 apps/hub/src/app/api/siwe/logout/route.ts delete mode 100644 apps/hub/src/app/api/siwe/nonce/route.ts delete mode 100644 apps/hub/src/app/api/siwe/session/route.ts delete mode 100644 apps/hub/src/app/api/siwe/verify/route.ts delete mode 100644 apps/hub/src/utils/session.ts diff --git a/apps/hub/.env b/apps/hub/.env index 64e0e2f58..a1d8b7ebf 100644 --- a/apps/hub/.env +++ b/apps/hub/.env @@ -17,3 +17,4 @@ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="7ab664eee6a734b14327cdf4678a3431" +NEXT_PUBLIC_API_URL=http://localhost:3001/api/v1 \ No newline at end of file diff --git a/apps/hub/package.json b/apps/hub/package.json index 0f9f9e040..1154132ff 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -27,7 +27,6 @@ "@tanstack/react-table": "^8.21.3", "@vercel/analytics": "^1.5.0", "connectkit": "^1.9.0", - "connectkit-next-siwe": "^0.3.0", "cva": "1.0.0-beta.1", "framer-motion": "^12.0.6", "next": "15.3.0", diff --git a/apps/hub/src/app/_constants/env.client.mjs b/apps/hub/src/app/_constants/env.client.mjs index eab850ac1..98c6f152c 100644 --- a/apps/hub/src/app/_constants/env.client.mjs +++ b/apps/hub/src/app/_constants/env.client.mjs @@ -5,11 +5,13 @@ export const envSchema = z.object({ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: z .string() .min(1, 'WalletConnect Project ID is required.'), + NEXT_PUBLIC_API_URL: z.string().url(), }) export const result = envSchema.strip().safeParse({ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, }) if (!result.success) { diff --git a/apps/hub/src/app/_constants/index.ts b/apps/hub/src/app/_constants/index.ts index 4ee2947e8..59ce36462 100644 --- a/apps/hub/src/app/_constants/index.ts +++ b/apps/hub/src/app/_constants/index.ts @@ -1,3 +1,4 @@ export * from './address' export * from './chain' +export * from './siwe' export * from './staking' diff --git a/apps/hub/src/app/_constants/session.ts b/apps/hub/src/app/_constants/session.ts deleted file mode 100644 index d83d586e8..000000000 --- a/apps/hub/src/app/_constants/session.ts +++ /dev/null @@ -1,24 +0,0 @@ -type SessionConfig = { - cookieName: string - cookieOptions: { - httpOnly: boolean - secure: boolean - sameSite: 'lax' | 'strict' | 'none' - maxAge: number - path: string - } - password: string -} - -export const sessionConfig: SessionConfig = { - cookieName: 'status-hub-siwe', - cookieOptions: { - httpOnly: true, - secure: false, - sameSite: 'lax', - maxAge: 60 * 60, - path: '/', - }, - password: - 'complex_password_at_least_32_characters_long_replace_in_production_env', -} diff --git a/apps/hub/src/app/_constants/siwe.ts b/apps/hub/src/app/_constants/siwe.ts index 5b1f1dc29..386692fd5 100644 --- a/apps/hub/src/app/_constants/siwe.ts +++ b/apps/hub/src/app/_constants/siwe.ts @@ -1,32 +1,73 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import { - configureClientSIWE, - configureServerSideSIWE, -} from 'connectkit-next-siwe' - -import { getDefaultWagmiConfig } from './chain' -import { sessionConfig } from './session' - -const wagmiConfig = getDefaultWagmiConfig() - -/** - * Server-side SIWE configuration - * This handles signature verification and session management on the server - */ -export const siweServer = configureServerSideSIWE({ - config: { - chains: wagmiConfig.chains, - transports: wagmiConfig.transports, +import { clientEnv } from './env.client.mjs' + +import type { SIWEConfig } from 'connectkit' + +// API configuration +const API_BASE_URL = clientEnv.NEXT_PUBLIC_API_URL + +const AUTH_ENDPOINTS = { + verify: `${API_BASE_URL}/auth/ethereum`, + session: `${API_BASE_URL}/auth/me`, + logout: `${API_BASE_URL}/auth/logout`, +} as const + +// Helper for API calls +const fetchAPI = async (url: string, options?: RequestInit) => { + const response = await fetch(url, { + credentials: 'include', // Send cookies with cross-origin requests + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + ...options, + }) + return response +} + +export const siweConfig: SIWEConfig = { + getNonce: async () => { + return Math.random().toString(36).substring(2, 15) + }, + + createMessage: async () => { + const payload = { timestamp: Math.floor(Date.now() / 1000) } + return window.btoa(JSON.stringify(payload)) }, - session: sessionConfig, -}) - -/** - * Client-side SIWE configuration - * This handles the authentication flow on the client - */ -export const siweClient = configureClientSIWE({ - apiRoutePrefix: '/api/siwe', - statement: 'Sign in with Ethereum to access Status Hub.', -}) + + verifyMessage: async ({ message, signature }) => { + try { + const response = await fetchAPI(AUTH_ENDPOINTS.verify, { + method: 'POST', + body: JSON.stringify({ payload: message, signature }), + }) + return response.ok + } catch { + return false + } + }, + + getSession: async () => { + try { + const response = await fetchAPI(AUTH_ENDPOINTS.session) + if (!response.ok) return null + + const data = await response.json() + return data.result ?? null + } catch { + return null + } + }, + + signOut: async () => { + try { + const response = await fetchAPI(AUTH_ENDPOINTS.logout, { + method: 'POST', + }) + return response.ok + } catch { + return false + } + }, + + signOutOnDisconnect: true, +} diff --git a/apps/hub/src/app/_providers/index.tsx b/apps/hub/src/app/_providers/index.tsx index 2f80f29db..c917f1e22 100644 --- a/apps/hub/src/app/_providers/index.tsx +++ b/apps/hub/src/app/_providers/index.tsx @@ -2,13 +2,13 @@ import { ToastContainer } from '@status-im/components' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { ConnectKitProvider } from 'connectkit' +import { ConnectKitProvider, SIWEProvider } from 'connectkit' import { WagmiProvider } from 'wagmi' import { testnet, wagmiConfig } from '~constants/chain' +import { siweConfig } from '~constants/siwe' import { VaultStateProvider } from '~hooks/useVaultStateContext' -import { SiweProvider } from './siwe-provider' import { VaultProvider } from './vault-provider' interface ProvidersProps { @@ -42,7 +42,7 @@ export function Providers({ children }: ProvidersProps) { return ( - + - + ) diff --git a/apps/hub/src/app/_providers/siwe-provider.tsx b/apps/hub/src/app/_providers/siwe-provider.tsx deleted file mode 100644 index 50b65c2a9..000000000 --- a/apps/hub/src/app/_providers/siwe-provider.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client' -import { useRouter } from 'next/navigation' -import { useDisconnect } from 'wagmi' - -import { siweClient } from '../_constants/siwe' - -interface SiweProviderProps { - children: React.ReactNode -} - -export const SiweProvider = ({ children }: SiweProviderProps) => { - const router = useRouter() - const { disconnect } = useDisconnect() - - return ( - { - router.refresh() - }} - onSignOut={() => { - disconnect() - router.refresh() - }} - > - {children} - - ) -} diff --git a/apps/hub/src/app/api/siwe/logout/route.ts b/apps/hub/src/app/api/siwe/logout/route.ts deleted file mode 100644 index a342753ad..000000000 --- a/apps/hub/src/app/api/siwe/logout/route.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -import { sessionConfig } from '~constants/session' -import { getSession } from '~utils/session' - -/** - * @api {get} /api/siwe/logout Logout - * @apiName Logout - * @apiGroup SIWE - * - * @apiDescription - * Terminates the current SIWE authentication session by destroying session data - * and removing the session cookie. This endpoint securely logs the user out by: - * - Clearing all session data (address, chainId, nonce) - * - Removing the session cookie from the browser - * - Invalidating the encrypted session seal - * - * **Security:** - * - Session destruction is immediate and irreversible - * - Cookie is cleared with maxAge: 0 to ensure browser deletion - * - No data persists after logout - * - * **Use Cases:** - * - User clicks "Disconnect" or "Logout" button - * - Session expiration or invalidation - * - Account switching (logout before new auth) - * - Security-related forced logout - * - * **Flow:** - * 1. Client sends GET request to /api/siwe/logout - * 2. Server retrieves current session from cookie - * 3. Server destroys session data - * 4. Server removes session cookie (sets maxAge: 0) - * 5. Client receives success confirmation - * 6. Client typically disconnects wallet and refreshes UI - * - * @apiSuccess {Boolean} success True if logout succeeded - * @apiSuccess {String} message Success message - * - * @apiSuccessExample {json} Success Response (200): - * { - * "success": true, - * "message": "Logged out successfully" - * } - * - * @apiError (500) {Object} error Server error - * @apiError (500) {String} error.error Error message - * @apiError (500) {String} error.details Detailed error information - * - * @apiErrorExample {json} Logout Failed (500): - * { - * "error": "Failed to logout", - * "details": "Session destruction failed" - * } - * - * @apiExample {curl} cURL Example: - * curl -X GET https://status.app/api/siwe/logout \ - * --cookie "status-hub-siwe=..." - * - * @apiExample {javascript} JavaScript Example: - * const response = await fetch('/api/siwe/logout', { - * credentials: 'include' - * }) - * const result = await response.json() - * - * if (result.success) { - * console.log('Logged out successfully') - * // Disconnect wallet, redirect, etc. - * } - * - * @apiExample {javascript} React Example: - * function LogoutButton() { - * const { disconnect } = useDisconnect() - * const router = useRouter() - * - * const handleLogout = async () => { - * try { - * await fetch('/api/siwe/logout') - * disconnect() // Disconnect wallet - * router.push('/') // Redirect to home - * } catch (error) { - * console.error('Logout failed:', error) - * } - * } - * - * return - * } - */ -export async function GET() { - try { - const cookieStore = await cookies() - const session = await getSession(cookieStore) - - // Clear session data - session.destroy() - - // Delete cookie using Next.js cookies API - cookieStore.set({ - name: sessionConfig.cookieName, - value: '', - ...sessionConfig.cookieOptions, - maxAge: 0, - }) - - return NextResponse.json({ - success: true, - message: 'Logged out successfully', - }) - } catch (error) { - console.error('[SIWE Logout] Logout error:', error) - - const errorMessage = - error instanceof Error ? error.message : 'Unknown error occurred' - - return NextResponse.json( - { - error: 'Failed to logout', - details: errorMessage, - }, - { status: 500 } - ) - } -} diff --git a/apps/hub/src/app/api/siwe/nonce/route.ts b/apps/hub/src/app/api/siwe/nonce/route.ts deleted file mode 100644 index 16b558de1..000000000 --- a/apps/hub/src/app/api/siwe/nonce/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' -import { generateNonce } from 'siwe' - -import { sessionConfig } from '~constants/session' -import { getSession } from '~utils/session' - -/** - * @api {get} /api/siwe/nonce Get Nonce - * @apiName GetNonce - * @apiGroup SIWE - * - * @apiDescription - * Generates a cryptographically secure random nonce for SIWE message signing. - * The nonce is stored in the session and must be included in the SIWE message - * that the user signs. This prevents replay attacks by ensuring each signature - * request is unique. - * - * **Flow:** - * 1. Client requests nonce from this endpoint - * 2. Server generates nonce and stores in session cookie - * 3. Client includes nonce in SIWE message - * 4. Client sends signed message to /api/siwe/verify - * - * **Idempotency:** - * If a nonce already exists in the session, it will be returned without - * generating a new one. This prevents multiple nonces per session. - * - * @apiSuccess {String} nonce Cryptographically secure random nonce - * - * @apiSuccessExample {text} Success Response (200): - * "a1b2c3d4e5f6g7h8" - * - * @apiError (500) {Object} error Error details - * @apiError (500) {String} error.error Error message - * @apiError (500) {String} error.details Detailed error information - * - * @apiErrorExample {json} Error Response (500): - * { - * "error": "Failed to generate nonce", - * "details": "Session encryption failed" - * } - * - * @apiExample {curl} cURL Example: - * curl -X GET https://status.app/api/siwe/nonce - * - * @apiExample {javascript} JavaScript Example: - * const response = await fetch('/api/siwe/nonce') - * const nonce = await response.text() - */ -export async function GET() { - try { - const cookieStore = await cookies() - const session = await getSession(cookieStore) - - // Generate new nonce if one doesn't exist - if (!session.nonce) { - session.nonce = generateNonce() - - // Seal session and set cookie with nonce - const seal = await session.getSeal() - cookieStore.set({ - name: sessionConfig.cookieName, - value: seal, - ...sessionConfig.cookieOptions, - }) - } - - // Return nonce as plain text (ConnectKit expects text/plain) - return new Response(session.nonce, { - status: 200, - headers: { - 'Content-Type': 'text/plain; charset=utf-8', - 'Cache-Control': 'no-store, must-revalidate', - }, - }) - } catch (error) { - console.error('[SIWE Nonce] Error generating nonce:', error) - - const errorMessage = - error instanceof Error ? error.message : 'Unknown error occurred' - - return NextResponse.json( - { - error: 'Failed to generate nonce', - details: errorMessage, - }, - { status: 500 } - ) - } -} diff --git a/apps/hub/src/app/api/siwe/session/route.ts b/apps/hub/src/app/api/siwe/session/route.ts deleted file mode 100644 index 382714cff..000000000 --- a/apps/hub/src/app/api/siwe/session/route.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { NextResponse } from 'next/server' - -import { getReadonlySession, isValidSIWESession } from '~utils/session' - -import type { NextRequest } from 'next/server' - -/** - * @api {get} /api/siwe/session Get Session - * @apiName GetSession - * @apiGroup SIWE - * - * @apiDescription - * Retrieves the current SIWE authentication session data. This endpoint checks - * for an existing authenticated session and returns the user's Ethereum address, - * chain ID, and authentication status. No authentication required - returns null - * values if not authenticated. - * - * **Use Cases:** - * - Check if user is currently authenticated - * - Get authenticated user's wallet address - * - Determine which chain the user authenticated on - * - Conditional UI rendering based on auth status - * - * **Session Storage:** - * Session data is stored in an encrypted iron-session cookie, which is - * automatically included in the request by the browser. - * - * **Authentication Flow:** - * 1. Browser automatically sends session cookie with request - * 2. Server decrypts and validates session cookie - * 3. Returns session data if valid, null values if not authenticated - * - * @apiSuccess {String|null} address Ethereum address of authenticated user (null if not authenticated) - * @apiSuccess {Number|null} chainId Chain ID the user authenticated on (null if not authenticated) - * @apiSuccess {Boolean} isAuthenticated True if user has valid SIWE session - * - * @apiSuccessExample {json} Authenticated Response (200): - * { - * "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4", - * "chainId": 1660990954, - * "isAuthenticated": true - * } - * - * @apiSuccessExample {json} Unauthenticated Response (200): - * { - * "address": null, - * "chainId": null, - * "isAuthenticated": false - * } - * - * @apiError (500) {Object} error Server error - * @apiError (500) {String} error.error Error message - * @apiError (500) {String} error.details Detailed error information - * - * @apiErrorExample {json} Session Retrieval Failed (500): - * { - * "error": "Failed to retrieve session", - * "details": "Session decryption failed" - * } - * - * @apiExample {curl} cURL Example: - * curl -X GET https://status.app/api/siwe/session \ - * --cookie "status-hub-siwe=..." - * - * @apiExample {javascript} JavaScript Example: - * // Fetch automatically includes cookies - * const response = await fetch('/api/siwe/session', { - * credentials: 'include' - * }) - * const session = await response.json() - * - * if (session.isAuthenticated) { - * console.log(`Authenticated as ${session.address}`) - * } else { - * console.log('Not authenticated') - * } - * - * @apiExample {javascript} React Hook Example: - * function useSession() { - * const [session, setSession] = useState(null) - * - * useEffect(() => { - * fetch('/api/siwe/session') - * .then(res => res.json()) - * .then(setSession) - * }, []) - * - * return session - * } - */ -export async function GET(request: NextRequest) { - try { - const session = await getReadonlySession(request.cookies) - - // Return session data (may have undefined values if not authenticated) - return NextResponse.json({ - address: session.address ?? null, - chainId: session.chainId ?? null, - isAuthenticated: isValidSIWESession(session), - }) - } catch (error) { - console.error('[SIWE Session] Session retrieval error:', error) - - const errorMessage = - error instanceof Error ? error.message : 'Unknown error occurred' - - return NextResponse.json( - { - error: 'Failed to retrieve session', - details: errorMessage, - }, - { status: 500 } - ) - } -} diff --git a/apps/hub/src/app/api/siwe/verify/route.ts b/apps/hub/src/app/api/siwe/verify/route.ts deleted file mode 100644 index 5270c066b..000000000 --- a/apps/hub/src/app/api/siwe/verify/route.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' -import { SiweMessage } from 'siwe' - -import { sessionConfig } from '~constants/session' -import { getSession } from '~utils/session' - -import type { NextRequest } from 'next/server' - -/** - * @api {post} /api/siwe/verify Verify Signature - * @apiName VerifySignature - * @apiGroup SIWE - * - * @apiDescription - * Verifies a SIWE (Sign-In with Ethereum) signature and creates an authenticated - * session. This endpoint validates the cryptographic signature against the SIWE - * message, checks the nonce matches the session, and stores the authenticated - * address in the session. - * - * **Security:** - * - Validates signature cryptographically using secp256k1 - * - Checks nonce matches to prevent replay attacks - * - Verifies message hasn't expired - * - Ensures domain and chain ID are correct - * - * **Flow:** - * 1. Client gets nonce from /api/siwe/nonce - * 2. Client creates SIWE message with nonce - * 3. User signs message in wallet - * 4. Client sends message + signature to this endpoint - * 5. Server verifies and creates authenticated session - * - * @apiParam {String} message The SIWE message that was signed - * @apiParam {String} signature The hex-encoded signature (0x...) - * - * @apiParamExample {json} Request Body: - * { - * "message": "status.app wants you to sign in with your Ethereum account:\n0x...", - * "signature": "0xabc123..." - * } - * - * @apiSuccess {Boolean} success True if verification succeeded - * @apiSuccess {String} address Ethereum address that was authenticated - * @apiSuccess {Number} chainId Chain ID the user authenticated on - * - * @apiSuccessExample {json} Success Response (200): - * { - * "success": true, - * "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4", - * "chainId": 1660990954 - * } - * - * @apiError (400) {Object} error Validation error - * @apiError (400) {String} error.error Error message - * - * @apiError (500) {Object} error Server error - * @apiError (500) {String} error.error Error message - * @apiError (500) {String} error.details Detailed error information - * - * @apiErrorExample {json} Missing Fields (400): - * { - * "error": "Missing message or signature" - * } - * - * @apiErrorExample {json} Invalid Nonce (400): - * { - * "error": "Invalid nonce. Nonce does not match session." - * } - * - * @apiErrorExample {json} Verification Failed (500): - * { - * "error": "Signature verification failed", - * "details": "Signature does not match address" - * } - * - * @apiExample {curl} cURL Example: - * curl -X POST https://status.app/api/siwe/verify \ - * -H "Content-Type: application/json" \ - * -d '{"message": "...", "signature": "0x..."}' - * - * @apiExample {javascript} JavaScript Example: - * const response = await fetch('/api/siwe/verify', { - * method: 'POST', - * headers: { 'Content-Type': 'application/json' }, - * body: JSON.stringify({ message, signature }) - * }) - * const data = await response.json() - */ -export async function POST(request: NextRequest) { - try { - const cookieStore = await cookies() - const session = await getSession(cookieStore) - - // Parse and validate request body - const body = await request.json() - const { message, signature } = body - - if (!message || !signature) { - return NextResponse.json( - { error: 'Missing message or signature' }, - { status: 400 } - ) - } - - // Verify nonce exists in session - if (!session.nonce) { - return NextResponse.json( - { error: 'No nonce in session. Request a nonce first.' }, - { status: 400 } - ) - } - - // Verify the SIWE message signature - const siweMessage = new SiweMessage(message) - const fields = await siweMessage.verify({ signature }) - - // Validate nonce matches session (prevents replay attacks) - if (fields.data.nonce !== session.nonce) { - return NextResponse.json( - { error: 'Invalid nonce. Nonce does not match session.' }, - { status: 400 } - ) - } - - // Store authenticated address and chain ID in session - session.address = fields.data.address - session.chainId = fields.data.chainId - - // Clear nonce after successful verification (prevents reuse) - delete session.nonce - - // Seal and persist session - const seal = await session.getSeal() - cookieStore.set({ - name: sessionConfig.cookieName, - value: seal, - ...sessionConfig.cookieOptions, - }) - - return NextResponse.json({ - success: true, - address: fields.data.address, - chainId: fields.data.chainId, - }) - } catch (error) { - console.error('[SIWE Verify] Signature verification error:', error) - - const errorMessage = - error instanceof Error ? error.message : 'Unknown error occurred' - - return NextResponse.json( - { - error: 'Signature verification failed', - details: errorMessage, - }, - { status: 500 } - ) - } -} diff --git a/apps/hub/src/utils/session.ts b/apps/hub/src/utils/session.ts deleted file mode 100644 index bc1654f8d..000000000 --- a/apps/hub/src/utils/session.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { sealData, unsealData } from 'iron-session' - -import { sessionConfig } from '~constants/session' - -import type { IronSession } from 'iron-session' -import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies' -import type { - RequestCookies, - ResponseCookies, -} from 'next/dist/server/web/spec-extension/cookies' - -// ============================================================================ -// Types -// ============================================================================ - -/** - * Map of password IDs to password strings for iron-session - * Allows password rotation by using multiple passwords with different IDs - */ -type PasswordsMap = Record - -/** - * Password can be a single string or a map for rotation - */ -type Password = string | PasswordsMap - -/** - * SIWE Session Data - * Extends IronSession with SIWE-specific fields - */ -export interface SIWESessionData extends IronSession { - /** Unique nonce for SIWE message */ - nonce?: string - /** Ethereum address of authenticated user */ - address?: string - /** Chain ID the user authenticated on */ - chainId?: number -} - -/** - * Session with utility methods - * Combines session data with helper functions for sealing and destroying - */ -export interface NextSIWESession extends SIWESessionData { - /** Get sealed session string for cookie storage */ - getSeal: () => Promise - /** Clear session data and cookie */ - destroy: () => void - /** Save session data (iron-session compatibility) */ - save: () => Promise - /** Update session (iron-session compatibility) */ - updateConfig: (newSessionOptions: unknown) => void -} - -/** - * Readonly session without modification methods - */ -export type ReadonlyNextSIWESession = Omit< - NextSIWESession, - 'getSeal' | 'destroy' -> - -/** - * Supported cookie types from Next.js - */ -type CookieStore = ReadonlyRequestCookies | RequestCookies | ResponseCookies - -/** - * Writable cookie types (excludes readonly) - */ -type WritableCookieStore = RequestCookies | ResponseCookies - -// ============================================================================ -// Helper Functions -// ============================================================================ - -/** - * Normalize password to map format for iron-session - * Single strings are converted to { 1: password } - * - * @param password - Password string or map - * @returns Password map for iron-session - */ -function normalizePasswordToMap(password: Password): PasswordsMap { - return typeof password === 'string' ? { 1: password } : password -} - -// ============================================================================ -// Session Functions -// ============================================================================ - -/** - * Get a readonly session from cookies - * Does not provide methods to modify or persist the session - * - * @param cookies - Cookie store from Next.js request - * @returns Promise resolving to readonly session data - * - * @example - * ```typescript - * import { cookies } from 'next/headers' - * - * const session = await getReadonlySession(cookies()) - * console.log(session.address) // "0x123..." - * ``` - */ -export async function getReadonlySession( - cookies: CookieStore -): Promise { - const passwordsAsMap = normalizePasswordToMap(sessionConfig.password) - const sealFromCookies = cookies.get(sessionConfig.cookieName) - - // If no cookie exists, return empty session - if (!sealFromCookies?.value) { - return {} as ReadonlyNextSIWESession - } - - try { - // Unseal the session data from the cookie - const session = await unsealData(sealFromCookies.value, { - password: passwordsAsMap, - }) - - return session as ReadonlyNextSIWESession - } catch (error) { - // If unsealing fails (corrupted cookie, wrong password, etc.), return empty - console.error('Failed to unseal session:', error) - return {} as ReadonlyNextSIWESession - } -} - -/** - * Get a full session with modification methods - * Provides getSeal() and destroy() methods for persisting/clearing session - * - * @param cookies - Writable cookie store from Next.js request - * @returns Promise resolving to session with utility methods - * - * @example - * ```typescript - * import { cookies } from 'next/headers' - * - * const session = await getSession(cookies()) - * session.address = '0x123...' - * const seal = await session.getSeal() - * cookies().set(cookieName, seal) - * ``` - */ -export async function getSession( - cookies: WritableCookieStore -): Promise { - const passwordsAsMap = normalizePasswordToMap(sessionConfig.password) - const readonlySession = await getReadonlySession(cookies) - - // Create a mutable copy of the session - const session = { ...readonlySession } as NextSIWESession - - // Add getSeal method to seal and return session data - Object.defineProperty(session, 'getSeal', { - enumerable: false, // Don't include in Object.keys() - writable: false, - value: async function getSeal(): Promise { - const seal = await sealData(session, { - password: passwordsAsMap, - }) - - return seal - }, - }) - - // Add destroy method to clear session - Object.defineProperty(session, 'destroy', { - enumerable: false, // Don't include in Object.keys() - writable: false, - value: function destroy(): void { - // Clear all session data - const keys = Object.keys(session) as Array - keys.forEach(key => { - if (key !== 'getSeal' && key !== 'destroy') { - delete session[key] - } - }) - - // Clear the cookie - cookies.set({ - name: sessionConfig.cookieName, - value: '', - maxAge: 0, - path: sessionConfig.cookieOptions.path, - }) - }, - }) - - return session -} - -// ============================================================================ -// Utility Functions -// ============================================================================ - -/** - * Check if a session is authenticated - * A session is considered authenticated if it has an address - * - * @param session - Session to check - * @returns True if session has an address - */ -export function isAuthenticated( - session: Partial -): session is Required> { - return Boolean(session.address) -} - -/** - * Validate session has required SIWE fields - * - * @param session - Session to validate - * @returns True if session has address and chainId - */ -export function isValidSIWESession( - session: Partial -): session is Required> { - return Boolean(session.address && session.chainId) -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a61030e72..4fab1e23b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -298,9 +298,6 @@ importers: connectkit: specifier: ^1.9.0 version: 1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - connectkit-next-siwe: - specifier: ^0.3.0 - version: 0.3.0(connectkit@1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)) cva: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1(typescript@5.8.3) @@ -8137,9 +8134,6 @@ packages: '@tybys/wasm-util@0.8.3': resolution: {integrity: sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==} - '@types/accepts@1.3.7': - resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} - '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -8185,15 +8179,6 @@ packages: '@types/connect@3.4.35': resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - '@types/content-disposition@0.5.9': - resolution: {integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==} - - '@types/cookie@0.5.4': - resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==} - - '@types/cookies@0.9.1': - resolution: {integrity: sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==} - '@types/d3-array@3.0.3': resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==} @@ -8320,15 +8305,9 @@ packages: '@types/highlight-words-core@1.2.3': resolution: {integrity: sha512-PWNU/NR0CaYEsK38mcCTyDzeS2TlEGK9kRhRMz1i86jVAe836ZlA3gl6QYpu+CG6IpfNKTgWpEnJuRededvC0g==} - '@types/http-assert@1.5.6': - resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} - '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/is-ci@3.0.0': resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==} @@ -8353,18 +8332,9 @@ packages: '@types/jsonfile@6.1.1': resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} - '@types/keygrip@1.0.6': - resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} - '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/koa-compose@3.2.8': - resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} - - '@types/koa@2.15.0': - resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} - '@types/lodash@4.14.191': resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} @@ -10297,16 +10267,6 @@ packages: resolution: {integrity: sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==} engines: {node: '>=18'} - connectkit-next-siwe@0.3.0: - resolution: {integrity: sha512-zUvx1kROpTGJgNst/yzBatQcU1E3pONJvfAcELk1W52B26WJZ5s5ym/hqMBINhT2zVZYV0XKwEexEzfFQey2Yw==} - engines: {node: '>=12.4'} - peerDependencies: - connectkit: '>=1.2.0' - next: '>=12.x' - react: 17.x || 18.x - react-dom: 17.x || 18.x - viem: '>=2.13.3' - connectkit@1.9.0: resolution: {integrity: sha512-bkqg8zK35pWWG2q8xeo41J1mnBP8D2ffOd/ItB12aad9QZZU20SlEeiQM9iYfRyl0JAH1tqIDlZbXajqZBFfDw==} engines: {node: '>=12.4'} @@ -10363,10 +10323,6 @@ packages: resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} engines: {node: '>= 0.6'} - cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -11893,15 +11849,6 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - follow-redirects@1.15.2: - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -12817,27 +12764,9 @@ packages: resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} engines: {node: '>= 10'} - iron-session@6.3.1: - resolution: {integrity: sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A==} - engines: {node: '>=12'} - peerDependencies: - express: '>=4' - koa: '>=2' - next: '>=10' - peerDependenciesMeta: - express: - optional: true - koa: - optional: true - next: - optional: true - iron-session@8.0.4: resolution: {integrity: sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==} - iron-webcrypto@0.2.8: - resolution: {integrity: sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA==} - iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -19153,7 +19082,7 @@ snapshots: '@babel/traverse': 7.27.1(supports-color@5.5.0) '@babel/types': 7.27.1 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -20629,7 +20558,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.4.0 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -20639,7 +20568,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.3 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -28655,10 +28584,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/accepts@1.3.7': - dependencies: - '@types/node': 20.11.5 - '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.7 @@ -28729,17 +28654,6 @@ snapshots: dependencies: '@types/node': 20.11.5 - '@types/content-disposition@0.5.9': {} - - '@types/cookie@0.5.4': {} - - '@types/cookies@0.9.1': - dependencies: - '@types/connect': 3.4.35 - '@types/express': 4.17.21 - '@types/keygrip': 1.0.6 - '@types/node': 20.11.5 - '@types/d3-array@3.0.3': {} '@types/d3-array@3.2.1': {} @@ -28866,12 +28780,8 @@ snapshots: '@types/highlight-words-core@1.2.3': {} - '@types/http-assert@1.5.6': {} - '@types/http-cache-semantics@4.0.4': {} - '@types/http-errors@2.0.5': {} - '@types/is-ci@3.0.0': dependencies: ci-info: 3.8.0 @@ -28892,27 +28802,10 @@ snapshots: dependencies: '@types/node': 20.11.5 - '@types/keygrip@1.0.6': {} - '@types/keyv@3.1.4': dependencies: '@types/node': 20.11.5 - '@types/koa-compose@3.2.8': - dependencies: - '@types/koa': 2.15.0 - - '@types/koa@2.15.0': - dependencies: - '@types/accepts': 1.3.7 - '@types/content-disposition': 0.5.9 - '@types/cookies': 0.9.1 - '@types/http-assert': 1.5.6 - '@types/http-errors': 2.0.5 - '@types/keygrip': 1.0.6 - '@types/koa-compose': 3.2.8 - '@types/node': 20.11.5 - '@types/lodash@4.14.191': {} '@types/mdast@3.0.15': @@ -31925,7 +31818,7 @@ snapshots: axios@0.21.4: dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.9 transitivePeerDependencies: - debug @@ -32853,18 +32746,6 @@ snapshots: graceful-fs: 4.2.11 xdg-basedir: 5.1.0 - connectkit-next-siwe@0.3.0(connectkit@1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)): - dependencies: - connectkit: 1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - iron-session: 6.3.1(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)) - next: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - viem: 2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) - transitivePeerDependencies: - - express - - koa - connectkit@1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.29.0(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0)(viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.25.76))(wagmi@2.15.2(@tanstack/query-core@5.29.0)(@tanstack/react-query@5.29.0(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.8)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@6.0.3)(viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.25.76))(zod@3.25.76)): dependencies: '@tanstack/react-query': 5.29.0(react@19.1.0) @@ -32989,8 +32870,6 @@ snapshots: cookie@0.4.0: {} - cookie@0.5.0: {} - cookie@0.6.0: {} cookie@0.7.2: {} @@ -33378,7 +33257,7 @@ snapshots: dependencies: call-bind: 1.0.7 es-get-iterator: 1.1.2 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-arguments: 1.1.1 is-array-buffer: 3.0.2 is-date-object: 1.0.5 @@ -33877,8 +33756,8 @@ snapshots: es-get-iterator@1.1.2: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 is-arguments: 1.1.1 is-map: 2.0.2 is-set: 2.0.2 @@ -34432,7 +34311,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.4.0 + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.1 @@ -35009,8 +34888,6 @@ snapshots: flatted@3.3.1: {} - follow-redirects@1.15.2: {} - follow-redirects@1.15.9: {} for-each@0.3.3: @@ -35933,7 +35810,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.2 + follow-redirects: 1.15.9 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -36138,29 +36015,12 @@ snapshots: ipaddr.js@2.1.0: {} - iron-session@6.3.1(express@4.20.0)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)): - dependencies: - '@peculiar/webcrypto': 1.5.0 - '@types/cookie': 0.5.4 - '@types/express': 4.17.21 - '@types/koa': 2.15.0 - '@types/node': 17.0.45 - cookie: 0.5.0 - iron-webcrypto: 0.2.8 - optionalDependencies: - express: 4.20.0 - next: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) - iron-session@8.0.4: dependencies: cookie: 0.7.2 iron-webcrypto: 1.2.1 uncrypto: 0.1.3 - iron-webcrypto@0.2.8: - dependencies: - buffer: 6.0.3 - iron-webcrypto@1.2.1: {} is-absolute-url@4.0.1: {} @@ -36191,7 +36051,7 @@ snapshots: is-arguments@1.1.1: dependencies: call-bind: 1.0.7 - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-array-buffer@3.0.2: dependencies: @@ -36251,7 +36111,7 @@ snapshots: is-date-object@1.0.5: dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-decimal@1.0.4: {} @@ -36285,7 +36145,7 @@ snapshots: is-generator-function@1.0.10: dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-glob@4.0.3: dependencies: @@ -36470,7 +36330,7 @@ snapshots: is-weakset@2.0.2: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-weakset@2.0.3: dependencies: From fe9e7cb59835511f8c3cfd81cc128617a344612c Mon Sep 17 00:00:00 2001 From: iamonuwa Date: Tue, 28 Oct 2025 13:27:37 +0000 Subject: [PATCH 3/6] chore: set signOutOnDisconnect to false --- apps/hub/src/app/_constants/chain.ts | 7 +++++-- apps/hub/src/app/_constants/siwe.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/hub/src/app/_constants/chain.ts b/apps/hub/src/app/_constants/chain.ts index e4bb3059f..6c79c921b 100644 --- a/apps/hub/src/app/_constants/chain.ts +++ b/apps/hub/src/app/_constants/chain.ts @@ -1,6 +1,7 @@ import { getDefaultConfig } from 'connectkit' import { defineChain } from 'viem' import { createConfig, http } from 'wagmi' +import { type Chain, mainnet } from 'wagmi/chains' import { clientEnv } from './env.client.mjs' @@ -9,11 +10,12 @@ import type { CreateConnectorFn, Transport, } from 'wagmi' -import type { Chain } from 'wagmi/chains' export const testnet = defineChain({ id: 1660990954, name: 'Status Network Testnet', + testnet: true, + sourceId: 1660990954, nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { @@ -30,8 +32,9 @@ export const testnet = defineChain({ export const getDefaultWagmiConfig = () => getDefaultConfig({ - chains: [testnet], + chains: [mainnet, testnet], transports: { + [mainnet.id]: http(mainnet.rpcUrls.default.http[0]), [testnet.id]: http(testnet.rpcUrls.default.http[0]), }, walletConnectProjectId: diff --git a/apps/hub/src/app/_constants/siwe.ts b/apps/hub/src/app/_constants/siwe.ts index 386692fd5..c19101e73 100644 --- a/apps/hub/src/app/_constants/siwe.ts +++ b/apps/hub/src/app/_constants/siwe.ts @@ -69,5 +69,5 @@ export const siweConfig: SIWEConfig = { } }, - signOutOnDisconnect: true, + signOutOnDisconnect: false, } From 4c4e45b01fd1188fabaf5a46bd391a9c44b2fa84 Mon Sep 17 00:00:00 2001 From: Felicio Date: Wed, 5 Nov 2025 05:15:25 +0000 Subject: [PATCH 4/6] u --- apps/hub/.env | 4 ++-- apps/hub/src/app/_constants/env.client.mjs | 5 +++-- apps/hub/src/app/_constants/siwe.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/hub/.env b/apps/hub/.env index a1d8b7ebf..f3ed585dd 100644 --- a/apps/hub/.env +++ b/apps/hub/.env @@ -16,5 +16,5 @@ # – https://vercel.com/docs/cli/env#exporting-development-environment-variables -NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="7ab664eee6a734b14327cdf4678a3431" -NEXT_PUBLIC_API_URL=http://localhost:3001/api/v1 \ No newline at end of file +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="" +NEXT_PUBLIC_STATUS_NETWORK_API_URL="" \ No newline at end of file diff --git a/apps/hub/src/app/_constants/env.client.mjs b/apps/hub/src/app/_constants/env.client.mjs index 98c6f152c..aa2881576 100644 --- a/apps/hub/src/app/_constants/env.client.mjs +++ b/apps/hub/src/app/_constants/env.client.mjs @@ -5,13 +5,14 @@ export const envSchema = z.object({ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: z .string() .min(1, 'WalletConnect Project ID is required.'), - NEXT_PUBLIC_API_URL: z.string().url(), + NEXT_PUBLIC_STATUS_NETWORK_API_URL: z.string().url(), }) export const result = envSchema.strip().safeParse({ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, - NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + NEXT_PUBLIC_STATUS_NETWORK_API_URL: + process.env.NEXT_PUBLIC_STATUS_NETWORK_API_URL, }) if (!result.success) { diff --git a/apps/hub/src/app/_constants/siwe.ts b/apps/hub/src/app/_constants/siwe.ts index c19101e73..a31553776 100644 --- a/apps/hub/src/app/_constants/siwe.ts +++ b/apps/hub/src/app/_constants/siwe.ts @@ -3,7 +3,7 @@ import { clientEnv } from './env.client.mjs' import type { SIWEConfig } from 'connectkit' // API configuration -const API_BASE_URL = clientEnv.NEXT_PUBLIC_API_URL +const API_BASE_URL = clientEnv.NEXT_PUBLIC_STATUS_NETWORK_API_URL const AUTH_ENDPOINTS = { verify: `${API_BASE_URL}/auth/ethereum`, From e4f64d391493e705bd4b1363a15c3eebd9772ed6 Mon Sep 17 00:00:00 2001 From: Felicio Date: Wed, 5 Nov 2025 05:17:32 +0000 Subject: [PATCH 5/6] u --- apps/hub/.env.development | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 apps/hub/.env.development diff --git a/apps/hub/.env.development b/apps/hub/.env.development new file mode 100644 index 000000000..f3ed585dd --- /dev/null +++ b/apps/hub/.env.development @@ -0,0 +1,20 @@ +# note: next lint doesn't use .env.development +# note: use .env.local instead for now +# +# – https://github.com/vercel/next.js/discussions/32354 + +# .env, .env.development, and .env.production files should be included in your repository as they define defaults. +# .env*.local should be added to .gitignore, as those files are intended to be ignored. +# .env.local is where secrets can be stored. +# +# – https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#default-environment-variables + +# The vercel env pull sub-command will export development environment variables to a local .env file or a different file of your choice. +# +# `vercel env pull .env[.development].local` +# +# – https://vercel.com/docs/cli/env#exporting-development-environment-variables + + +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID="" +NEXT_PUBLIC_STATUS_NETWORK_API_URL="" \ No newline at end of file From 9e31981e078436ea4dd45e248457fb86b4994adc Mon Sep 17 00:00:00 2001 From: Felicio Date: Wed, 5 Nov 2025 14:23:52 +0900 Subject: [PATCH 6/6] Update wallet connect in changeset --- .changeset/tame-gifts-hope.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tame-gifts-hope.md diff --git a/.changeset/tame-gifts-hope.md b/.changeset/tame-gifts-hope.md new file mode 100644 index 000000000..7542eec0b --- /dev/null +++ b/.changeset/tame-gifts-hope.md @@ -0,0 +1,5 @@ +--- +"hub": patch +--- + +update wallet connect