diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..95d6687 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server' +import { + clearSessionCookies, + readRefreshToken, + revokeSession, +} from '@/lib/auth/session' + +export async function POST(request: NextRequest): Promise { + try { + const refreshToken = readRefreshToken(request) + if (refreshToken) { + await revokeSession(refreshToken) + } + + const response = NextResponse.json({ ok: true }, { status: 200 }) + clearSessionCookies(response) + return response + } catch { + return NextResponse.json( + { + error: 'Failed to log out', + code: 'LOGOUT_FAILED', + }, + { status: 500 } + ) + } +} diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts new file mode 100644 index 0000000..0ab07e2 --- /dev/null +++ b/app/api/auth/me/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server' +import { withAuth } from '@/lib/auth/middleware' + +export const GET = withAuth(async (_request, auth) => { + return NextResponse.json( + { + walletAddress: auth.walletAddress, + authenticated: true, + }, + { status: 200 } + ) +}) diff --git a/app/api/auth/nonce/route.ts b/app/api/auth/nonce/route.ts new file mode 100644 index 0000000..22a23e7 --- /dev/null +++ b/app/api/auth/nonce/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server' +import { NONCE_TTL_SECONDS } from '@/lib/auth/constants' +import { randomNonce, sha256Hex } from '@/lib/auth/crypto' +import { saveNonce } from '@/lib/auth/store' +import { + buildAuthMessage, + isValidStellarAddress, + normalizeWalletAddress, +} from '@/lib/auth/stellar' + +interface NonceRequestBody { + walletAddress?: string +} + +export async function POST(request: NextRequest): Promise { + try { + const body: NonceRequestBody = await request.json() + const walletAddress = body.walletAddress?.trim() + + if (!walletAddress || !isValidStellarAddress(walletAddress)) { + return NextResponse.json( + { + error: 'Invalid wallet address', + code: 'INVALID_WALLET_ADDRESS', + }, + { status: 400 } + ) + } + + const normalizedWallet = normalizeWalletAddress(walletAddress) + const nonce = randomNonce() + const expiresAt = new Date(Date.now() + NONCE_TTL_SECONDS * 1000) + await saveNonce({ + walletAddress: normalizedWallet, + nonceHash: sha256Hex(nonce), + expiresAt, + }) + + return NextResponse.json( + { + walletAddress: normalizedWallet, + nonce, + message: buildAuthMessage(normalizedWallet, nonce), + expiresAt: expiresAt.toISOString(), + }, + { status: 200 } + ) + } catch { + return NextResponse.json( + { + error: 'Failed to create auth nonce', + code: 'NONCE_ISSUE_FAILED', + }, + { status: 500 } + ) + } +} diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts new file mode 100644 index 0000000..21e5d6a --- /dev/null +++ b/app/api/auth/refresh/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server' +import { + readRefreshToken, + rotateSession, + setSessionCookies, +} from '@/lib/auth/session' + +export async function POST(request: NextRequest): Promise { + try { + const refreshToken = readRefreshToken(request) + if (!refreshToken) { + return NextResponse.json( + { + error: 'Refresh token is required', + code: 'REFRESH_TOKEN_REQUIRED', + }, + { status: 401 } + ) + } + + const session = await rotateSession(request, refreshToken) + if (!session) { + return NextResponse.json( + { + error: 'Invalid or expired refresh token', + code: 'INVALID_REFRESH_TOKEN', + }, + { status: 401 } + ) + } + + const response = NextResponse.json( + { + walletAddress: session.walletAddress, + accessTokenExpiresAt: session.accessTokenExpiresAt.toISOString(), + refreshTokenExpiresAt: session.refreshTokenExpiresAt.toISOString(), + }, + { status: 200 } + ) + setSessionCookies(response, session) + + return response + } catch { + return NextResponse.json( + { + error: 'Failed to refresh session', + code: 'SESSION_REFRESH_FAILED', + }, + { status: 500 } + ) + } +} diff --git a/app/api/auth/verify/route.ts b/app/api/auth/verify/route.ts new file mode 100644 index 0000000..f0c8a6b --- /dev/null +++ b/app/api/auth/verify/route.ts @@ -0,0 +1,125 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createSession, setSessionCookies } from '@/lib/auth/session' +import { consumeNonce, hasActiveNonce } from '@/lib/auth/store' +import { sha256Hex } from '@/lib/auth/crypto' +import { + buildAuthMessage, + isValidStellarAddress, + normalizeWalletAddress, + verifyStellarSignature, +} from '@/lib/auth/stellar' + +interface VerifyRequestBody { + walletAddress?: string + nonce?: string + signature?: string + message?: string +} + +export async function POST(request: NextRequest): Promise { + try { + const body: VerifyRequestBody = await request.json() + + const walletAddress = body.walletAddress?.trim() + const nonce = body.nonce?.trim() + const signature = body.signature?.trim() + + if (!walletAddress || !nonce || !signature) { + return NextResponse.json( + { + error: 'Missing walletAddress, nonce, or signature', + code: 'INVALID_AUTH_PAYLOAD', + }, + { status: 400 } + ) + } + + if (!isValidStellarAddress(walletAddress)) { + return NextResponse.json( + { + error: 'Invalid wallet address', + code: 'INVALID_WALLET_ADDRESS', + }, + { status: 400 } + ) + } + + const normalizedWallet = normalizeWalletAddress(walletAddress) + const nonceHash = sha256Hex(nonce) + const expectedMessage = buildAuthMessage(normalizedWallet, nonce) + + if (body.message && body.message !== expectedMessage) { + return NextResponse.json( + { + error: 'Signed message does not match expected format', + code: 'MESSAGE_MISMATCH', + }, + { status: 400 } + ) + } + + const nonceIsValid = await hasActiveNonce({ + walletAddress: normalizedWallet, + nonceHash, + }) + if (!nonceIsValid) { + return NextResponse.json( + { + error: 'Nonce is invalid, expired, or already used', + code: 'INVALID_NONCE', + }, + { status: 401 } + ) + } + + const signatureIsValid = verifyStellarSignature({ + walletAddress: normalizedWallet, + message: expectedMessage, + signature, + }) + if (!signatureIsValid) { + return NextResponse.json( + { + error: 'Invalid wallet signature', + code: 'INVALID_SIGNATURE', + }, + { status: 401 } + ) + } + + const consumed = await consumeNonce({ + walletAddress: normalizedWallet, + nonceHash, + }) + if (!consumed) { + return NextResponse.json( + { + error: 'Nonce is invalid, expired, or already used', + code: 'INVALID_NONCE', + }, + { status: 401 } + ) + } + + const session = await createSession(request, normalizedWallet) + const response = NextResponse.json( + { + walletAddress: normalizedWallet, + accessTokenExpiresAt: session.accessTokenExpiresAt.toISOString(), + refreshTokenExpiresAt: session.refreshTokenExpiresAt.toISOString(), + }, + { status: 200 } + ) + setSessionCookies(response, session) + + return response + } catch { + return NextResponse.json( + { + error: 'Authentication failed', + code: 'AUTH_VERIFICATION_FAILED', + }, + { status: 500 } + ) + } +} diff --git a/app/layout.tsx b/app/layout.tsx index eea5d30..2d44398 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,12 +1,8 @@ import React from "react" import type { Metadata } from 'next' -import { Geist, Geist_Mono } from 'next/font/google' import { Analytics } from '@vercel/analytics/next' import './globals.css' -const _geist = Geist({ subsets: ["latin"] }); -const _geistMono = Geist_Mono({ subsets: ["latin"] }); - export const metadata: Metadata = { title: 'TaskChain', description: 'Web3-powered freelance marketplace with escrow-based payments on Stellar blockchain. Protect your work and payments with smart contract security.', diff --git a/docs/pr-evidence/build.txt b/docs/pr-evidence/build.txt new file mode 100644 index 0000000..7c71792 Binary files /dev/null and b/docs/pr-evidence/build.txt differ diff --git a/docs/pr-evidence/lint.txt b/docs/pr-evidence/lint.txt new file mode 100644 index 0000000..2d2bfeb Binary files /dev/null and b/docs/pr-evidence/lint.txt differ diff --git a/env.example b/env.example index 9f64d39..260e789 100644 --- a/env.example +++ b/env.example @@ -1 +1,2 @@ -DATABASE_URL=your db url \ No newline at end of file +DATABASE_URL=your db url +JWT_SECRET=replace_with_a_long_random_secret_min_32_chars diff --git a/lib/auth/constants.ts b/lib/auth/constants.ts new file mode 100644 index 0000000..55be11e --- /dev/null +++ b/lib/auth/constants.ts @@ -0,0 +1,6 @@ +export const ACCESS_TOKEN_COOKIE = 'tc_access_token' +export const REFRESH_TOKEN_COOKIE = 'tc_refresh_token' + +export const ACCESS_TOKEN_TTL_SECONDS = 15 * 60 +export const REFRESH_TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60 +export const NONCE_TTL_SECONDS = 5 * 60 diff --git a/lib/auth/crypto.ts b/lib/auth/crypto.ts new file mode 100644 index 0000000..5946f94 --- /dev/null +++ b/lib/auth/crypto.ts @@ -0,0 +1,44 @@ +import { createHash, randomBytes, timingSafeEqual } from 'crypto' + +function toBase64Url(input: Buffer | string): string { + const buffer = typeof input === 'string' ? Buffer.from(input, 'utf8') : input + return buffer + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, '') +} + +export function fromBase64Url(value: string): Buffer { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/') + const padding = normalized.length % 4 + const padded = + padding === 0 ? normalized : normalized + '='.repeat(4 - padding) + return Buffer.from(padded, 'base64') +} + +export function sha256Hex(value: string): string { + return createHash('sha256').update(value, 'utf8').digest('hex') +} + +export function randomNonce(bytes = 32): string { + return toBase64Url(randomBytes(bytes)) +} + +export function randomId(bytes = 24): string { + return toBase64Url(randomBytes(bytes)) +} + +export function safeEqual(a: string, b: string): boolean { + const left = Buffer.from(a, 'utf8') + const right = Buffer.from(b, 'utf8') + if (left.length !== right.length) { + return false + } + + return timingSafeEqual(left, right) +} + +export function encodeBase64Url(value: Buffer | string): string { + return toBase64Url(value) +} diff --git a/lib/auth/jwt.ts b/lib/auth/jwt.ts new file mode 100644 index 0000000..cc288e2 --- /dev/null +++ b/lib/auth/jwt.ts @@ -0,0 +1,125 @@ +import { createHmac } from 'crypto' +import { encodeBase64Url, fromBase64Url, randomId, safeEqual } from '@/lib/auth/crypto' + +type TokenType = 'access' | 'refresh' + +interface JwtHeader { + alg: 'HS256' + typ: 'JWT' +} + +export interface SessionTokenPayload { + sub: string + wallet: string + jti: string + type: TokenType + iat: number + exp: number +} + +function createSignature(input: string, secret: string): string { + const digest = createHmac('sha256', secret).update(input).digest() + return encodeBase64Url(digest) +} + +export function signSessionToken({ + subject, + walletAddress, + type, + expiresInSeconds, + secret, +}: { + subject: string + walletAddress: string + type: TokenType + expiresInSeconds: number + secret: string +}): { token: string; payload: SessionTokenPayload } { + const now = Math.floor(Date.now() / 1000) + const payload: SessionTokenPayload = { + sub: subject, + wallet: walletAddress, + jti: randomId(), + type, + iat: now, + exp: now + expiresInSeconds, + } + + const header: JwtHeader = { + alg: 'HS256', + typ: 'JWT', + } + + const encodedHeader = encodeBase64Url(JSON.stringify(header)) + const encodedPayload = encodeBase64Url(JSON.stringify(payload)) + const signingInput = `${encodedHeader}.${encodedPayload}` + const signature = createSignature(signingInput, secret) + + return { + token: `${signingInput}.${signature}`, + payload, + } +} + +function parseJson(input: Buffer): T | null { + try { + const parsed: unknown = JSON.parse(input.toString('utf8')) + return parsed as T + } catch { + return null + } +} + +function isSessionPayload(value: unknown): value is SessionTokenPayload { + if (!value || typeof value !== 'object') { + return false + } + + const payload = value as Record + return ( + typeof payload.sub === 'string' && + typeof payload.wallet === 'string' && + typeof payload.jti === 'string' && + (payload.type === 'access' || payload.type === 'refresh') && + typeof payload.iat === 'number' && + typeof payload.exp === 'number' + ) +} + +export function verifySessionToken( + token: string, + secret: string +): SessionTokenPayload | null { + const parts = token.split('.') + if (parts.length !== 3) { + return null + } + + const [encodedHeader, encodedPayload, signature] = parts + if (!encodedHeader || !encodedPayload || !signature) { + return null + } + + const signingInput = `${encodedHeader}.${encodedPayload}` + const expectedSignature = createSignature(signingInput, secret) + if (!safeEqual(signature, expectedSignature)) { + return null + } + + const header = parseJson(fromBase64Url(encodedHeader)) + if (!header || header.alg !== 'HS256' || header.typ !== 'JWT') { + return null + } + + const payload = parseJson(fromBase64Url(encodedPayload)) + if (!payload || !isSessionPayload(payload)) { + return null + } + + const now = Math.floor(Date.now() / 1000) + if (payload.exp <= now) { + return null + } + + return payload +} diff --git a/lib/auth/middleware.ts b/lib/auth/middleware.ts new file mode 100644 index 0000000..3dbcaff --- /dev/null +++ b/lib/auth/middleware.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server' +import { readAccessToken, verifyAccessToken } from '@/lib/auth/session' + +export interface AuthContext { + walletAddress: string + tokenJti: string +} + +type AuthenticatedHandler = ( + request: NextRequest, + auth: AuthContext +) => Promise | NextResponse + +function unauthorizedResponse(): NextResponse { + return NextResponse.json( + { error: 'Unauthorized', code: 'AUTH_REQUIRED' }, + { status: 401 } + ) +} + +export function withAuth(handler: AuthenticatedHandler) { + return async (request: NextRequest): Promise => { + const token = readAccessToken(request) + if (!token) { + return unauthorizedResponse() + } + + const payload = verifyAccessToken(token) + if (!payload) { + return unauthorizedResponse() + } + + return handler(request, { + walletAddress: payload.walletAddress, + tokenJti: payload.jti, + }) + } +} diff --git a/lib/auth/session.ts b/lib/auth/session.ts new file mode 100644 index 0000000..55b14d9 --- /dev/null +++ b/lib/auth/session.ts @@ -0,0 +1,207 @@ +import { NextRequest, NextResponse } from 'next/server' +import { + ACCESS_TOKEN_COOKIE, + ACCESS_TOKEN_TTL_SECONDS, + REFRESH_TOKEN_COOKIE, + REFRESH_TOKEN_TTL_SECONDS, +} from '@/lib/auth/constants' +import { sha256Hex } from '@/lib/auth/crypto' +import { signSessionToken, verifySessionToken } from '@/lib/auth/jwt' +import { + findValidRefreshToken, + revokeRefreshToken, + storeRefreshToken, + touchRefreshToken, +} from '@/lib/auth/store' +import { normalizeWalletAddress } from '@/lib/auth/stellar' + +export interface AuthSession { + walletAddress: string + accessToken: string + refreshToken: string + accessTokenExpiresAt: Date + refreshTokenExpiresAt: Date + refreshJti: string +} + +function getJwtSecret(): string { + const secret = process.env.JWT_SECRET + if (!secret || secret.length < 32) { + throw new Error('JWT_SECRET must be set and at least 32 chars long') + } + + return secret +} + +function getClientIp(request: NextRequest): string | null { + const forwardedFor = request.headers.get('x-forwarded-for') + if (forwardedFor) { + return forwardedFor.split(',')[0]?.trim() ?? null + } + + return request.headers.get('x-real-ip') +} + +export async function createSession( + request: NextRequest, + walletAddress: string +): Promise { + const normalizedWallet = normalizeWalletAddress(walletAddress) + const secret = getJwtSecret() + + const access = signSessionToken({ + subject: normalizedWallet, + walletAddress: normalizedWallet, + type: 'access', + expiresInSeconds: ACCESS_TOKEN_TTL_SECONDS, + secret, + }) + const refresh = signSessionToken({ + subject: normalizedWallet, + walletAddress: normalizedWallet, + type: 'refresh', + expiresInSeconds: REFRESH_TOKEN_TTL_SECONDS, + secret, + }) + + const accessTokenExpiresAt = new Date(access.payload.exp * 1000) + const refreshTokenExpiresAt = new Date(refresh.payload.exp * 1000) + + await storeRefreshToken({ + walletAddress: normalizedWallet, + jti: refresh.payload.jti, + tokenHash: sha256Hex(refresh.token), + expiresAt: refreshTokenExpiresAt, + userAgent: request.headers.get('user-agent'), + ipAddress: getClientIp(request), + }) + + return { + walletAddress: normalizedWallet, + accessToken: access.token, + refreshToken: refresh.token, + accessTokenExpiresAt, + refreshTokenExpiresAt, + refreshJti: refresh.payload.jti, + } +} + +export async function rotateSession( + request: NextRequest, + refreshToken: string +): Promise { + const secret = getJwtSecret() + const payload = verifySessionToken(refreshToken, secret) + if (!payload || payload.type !== 'refresh') { + return null + } + + const tokenHash = sha256Hex(refreshToken) + const isValid = await findValidRefreshToken({ + walletAddress: payload.wallet, + jti: payload.jti, + tokenHash, + }) + if (!isValid) { + return null + } + + await touchRefreshToken(payload.jti) + const session = await createSession(request, payload.wallet) + await revokeRefreshToken({ + jti: payload.jti, + replacedByJti: session.refreshJti, + }) + + return session +} + +export async function revokeSession(refreshToken: string): Promise { + const secret = getJwtSecret() + const payload = verifySessionToken(refreshToken, secret) + if (!payload || payload.type !== 'refresh') { + return + } + + await revokeRefreshToken({ jti: payload.jti }) +} + +export function readAccessToken(request: NextRequest): string | null { + const authorization = request.headers.get('authorization') + if (authorization?.startsWith('Bearer ')) { + return authorization.slice('Bearer '.length).trim() + } + + return request.cookies.get(ACCESS_TOKEN_COOKIE)?.value ?? null +} + +export function readRefreshToken(request: NextRequest): string | null { + return request.cookies.get(REFRESH_TOKEN_COOKIE)?.value ?? null +} + +export function verifyAccessToken(token: string): { + walletAddress: string + jti: string +} | null { + const secret = getJwtSecret() + const payload = verifySessionToken(token, secret) + if (!payload || payload.type !== 'access') { + return null + } + + return { + walletAddress: payload.wallet, + jti: payload.jti, + } +} + +export function setSessionCookies( + response: NextResponse, + session: AuthSession +): void { + const secure = process.env.NODE_ENV === 'production' + + response.cookies.set({ + name: ACCESS_TOKEN_COOKIE, + value: session.accessToken, + httpOnly: true, + secure, + sameSite: 'lax', + path: '/', + expires: session.accessTokenExpiresAt, + }) + + response.cookies.set({ + name: REFRESH_TOKEN_COOKIE, + value: session.refreshToken, + httpOnly: true, + secure, + sameSite: 'lax', + path: '/', + expires: session.refreshTokenExpiresAt, + }) +} + +export function clearSessionCookies(response: NextResponse): void { + const secure = process.env.NODE_ENV === 'production' + + response.cookies.set({ + name: ACCESS_TOKEN_COOKIE, + value: '', + httpOnly: true, + secure, + sameSite: 'lax', + path: '/', + maxAge: 0, + }) + + response.cookies.set({ + name: REFRESH_TOKEN_COOKIE, + value: '', + httpOnly: true, + secure, + sameSite: 'lax', + path: '/', + maxAge: 0, + }) +} diff --git a/lib/auth/stellar.ts b/lib/auth/stellar.ts new file mode 100644 index 0000000..45328d9 --- /dev/null +++ b/lib/auth/stellar.ts @@ -0,0 +1,146 @@ +import { createPublicKey, verify } from 'crypto' +import { fromBase64Url } from '@/lib/auth/crypto' + +const STELLAR_ACCOUNT_VERSION_BYTE = 6 << 3 +const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' + +function crc16Xmodem(data: Uint8Array): number { + let crc = 0x0000 + + for (const value of data) { + crc ^= value << 8 + for (let i = 0; i < 8; i += 1) { + if ((crc & 0x8000) !== 0) { + crc = (crc << 1) ^ 0x1021 + } else { + crc <<= 1 + } + crc &= 0xffff + } + } + + return crc +} + +function decodeBase32(input: string): Uint8Array { + const normalized = input.trim().toUpperCase().replace(/=+$/g, '') + let bits = 0 + let value = 0 + const output: number[] = [] + + for (const char of normalized) { + const index = BASE32_ALPHABET.indexOf(char) + if (index === -1) { + throw new Error('Invalid base32 character') + } + + value = (value << 5) | index + bits += 5 + + if (bits >= 8) { + bits -= 8 + output.push((value >>> bits) & 0xff) + } + } + + return Uint8Array.from(output) +} + +function getSignatureBuffer(signature: string): Buffer { + const trimmed = signature.trim() + + if (/^0x[0-9a-f]+$/i.test(trimmed)) { + return Buffer.from(trimmed.slice(2), 'hex') + } + + if (/^[0-9a-f]+$/i.test(trimmed) && trimmed.length % 2 === 0) { + return Buffer.from(trimmed, 'hex') + } + + try { + return fromBase64Url(trimmed) + } catch { + return Buffer.from(trimmed, 'base64') + } +} + +export function normalizeWalletAddress(walletAddress: string): string { + return walletAddress.trim().toUpperCase() +} + +export function getStellarPublicKey(walletAddress: string): Buffer { + const normalized = normalizeWalletAddress(walletAddress) + const decoded = decodeBase32(normalized) + + if (decoded.length !== 35) { + throw new Error('Invalid Stellar address length') + } + + const payload = decoded.subarray(0, 33) + const checksum = decoded.subarray(33, 35) + if (payload[0] !== STELLAR_ACCOUNT_VERSION_BYTE) { + throw new Error('Invalid Stellar address version byte') + } + + const expectedChecksum = crc16Xmodem(payload) + const actualChecksum = checksum[0] | (checksum[1] << 8) + if (expectedChecksum !== actualChecksum) { + throw new Error('Invalid Stellar address checksum') + } + + return Buffer.from(payload.subarray(1)) +} + +export function isValidStellarAddress(walletAddress: string): boolean { + try { + getStellarPublicKey(walletAddress) + return true + } catch { + return false + } +} + +export function verifyStellarSignature({ + walletAddress, + message, + signature, +}: { + walletAddress: string + message: string + signature: string +}): boolean { + const rawKey = getStellarPublicKey(walletAddress) + const signatureBuffer = getSignatureBuffer(signature) + + if (signatureBuffer.length !== 64) { + return false + } + + const publicKey = createPublicKey({ + key: { + kty: 'OKP', + crv: 'Ed25519', + x: rawKey.toString('base64url'), + }, + format: 'jwk', + }) + + return verify( + null, + Buffer.from(message, 'utf8'), + publicKey, + signatureBuffer + ) +} + +export function buildAuthMessage( + walletAddress: string, + nonce: string +): string { + const normalized = normalizeWalletAddress(walletAddress) + return [ + 'TaskChain Authentication', + `Wallet: ${normalized}`, + `Nonce: ${nonce}`, + ].join('\n') +} diff --git a/lib/auth/store.ts b/lib/auth/store.ts new file mode 100644 index 0000000..8d324e6 --- /dev/null +++ b/lib/auth/store.ts @@ -0,0 +1,146 @@ +import { sql } from '@/lib/db' + +export async function saveNonce({ + walletAddress, + nonceHash, + expiresAt, +}: { + walletAddress: string + nonceHash: string + expiresAt: Date +}): Promise { + await sql` + INSERT INTO auth_nonces (wallet_address, nonce_hash, expires_at) + VALUES (${walletAddress}, ${nonceHash}, ${expiresAt.toISOString()}) + ` +} + +export async function hasActiveNonce({ + walletAddress, + nonceHash, +}: { + walletAddress: string + nonceHash: string +}): Promise { + const rows = await sql<{ id: number }[]>` + SELECT id + FROM auth_nonces + WHERE wallet_address = ${walletAddress} + AND nonce_hash = ${nonceHash} + AND used_at IS NULL + AND expires_at > NOW() + ORDER BY created_at DESC + LIMIT 1 + ` + + return rows.length > 0 +} + +export async function consumeNonce({ + walletAddress, + nonceHash, +}: { + walletAddress: string + nonceHash: string +}): Promise { + const rows = await sql<{ id: number }[]>` + WITH target AS ( + SELECT id + FROM auth_nonces + WHERE wallet_address = ${walletAddress} + AND nonce_hash = ${nonceHash} + AND used_at IS NULL + AND expires_at > NOW() + ORDER BY created_at DESC + LIMIT 1 + ) + UPDATE auth_nonces + SET used_at = NOW() + WHERE id IN (SELECT id FROM target) + RETURNING id + ` + + return rows.length > 0 +} + +export async function storeRefreshToken({ + walletAddress, + jti, + tokenHash, + expiresAt, + userAgent, + ipAddress, +}: { + walletAddress: string + jti: string + tokenHash: string + expiresAt: Date + userAgent: string | null + ipAddress: string | null +}): Promise { + await sql` + INSERT INTO auth_refresh_tokens ( + wallet_address, + jti, + token_hash, + expires_at, + user_agent, + ip_address + ) + VALUES ( + ${walletAddress}, + ${jti}, + ${tokenHash}, + ${expiresAt.toISOString()}, + ${userAgent}, + ${ipAddress} + ) + ` +} + +export async function revokeRefreshToken({ + jti, + replacedByJti, +}: { + jti: string + replacedByJti?: string +}): Promise { + await sql` + UPDATE auth_refresh_tokens + SET revoked_at = NOW(), + replaced_by_jti = COALESCE(${replacedByJti ?? null}, replaced_by_jti) + WHERE jti = ${jti} + AND revoked_at IS NULL + ` +} + +export async function touchRefreshToken(jti: string): Promise { + await sql` + UPDATE auth_refresh_tokens + SET last_used_at = NOW() + WHERE jti = ${jti} + ` +} + +export async function findValidRefreshToken({ + walletAddress, + jti, + tokenHash, +}: { + walletAddress: string + jti: string + tokenHash: string +}): Promise { + const rows = await sql<{ id: number }[]>` + SELECT id + FROM auth_refresh_tokens + WHERE wallet_address = ${walletAddress} + AND jti = ${jti} + AND token_hash = ${tokenHash} + AND revoked_at IS NULL + AND expires_at > NOW() + LIMIT 1 + ` + + return rows.length > 0 +} diff --git a/lib/db.ts b/lib/db.ts index 4ffd72a..94f9592 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,7 +1,9 @@ import { neon } from '@neondatabase/serverless' -if (!process.env.DATABASE_URL) { - throw new Error('DATABASE_URL is not set') -} +const databaseUrl = process.env.DATABASE_URL -export const sql = neon(process.env.DATABASE_URL) +export const sql: ReturnType = databaseUrl + ? neon(databaseUrl) + : ((() => { + throw new Error('DATABASE_URL is not set') + }) as ReturnType) diff --git a/package-lock.json b/package-lock.json index 5742c20..724a9f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", + "baseline-browser-mapping": "^2.10.0", "eslint": "^9.39.3", "eslint-config-next": "^16.1.6", "postcss": "^8.5", @@ -552,31 +553,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", + "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", @@ -2717,49 +2718,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", "cpu": [ "arm64" ], @@ -2770,13 +2771,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], @@ -2787,13 +2788,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", "cpu": [ "x64" ], @@ -2804,13 +2805,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", "cpu": [ "x64" ], @@ -2821,13 +2822,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", "cpu": [ "arm" ], @@ -2838,13 +2839,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", "cpu": [ "arm64" ], @@ -2855,13 +2856,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", "cpu": [ "arm64" ], @@ -2872,13 +2873,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", "cpu": [ "x64" ], @@ -2889,13 +2890,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", "cpu": [ "x64" ], @@ -2906,13 +2907,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2928,21 +2929,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", "cpu": [ "arm64" ], @@ -2953,13 +2954,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", "cpu": [ "x64" ], @@ -2970,21 +2971,21 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" } }, "node_modules/@tybys/wasm-util": { @@ -3083,9 +3084,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3103,12 +3104,11 @@ } }, "node_modules/@types/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", - "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3119,7 +3119,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3336,9 +3335,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3959,9 +3958,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "funding": [ { "type": "opencollective", @@ -3979,7 +3978,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -4038,12 +4037,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", - "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { @@ -4089,7 +4091,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4165,9 +4166,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001765", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", - "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", "funding": [ { "type": "opencollective", @@ -4619,17 +4620,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "license": "ISC" }, "node_modules/embla-carousel": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.5.1", @@ -4661,14 +4661,14 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -6435,9 +6435,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -6451,23 +6451,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -6486,9 +6486,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -6507,9 +6507,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -6528,9 +6528,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -6549,9 +6549,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -6570,9 +6570,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -6591,9 +6591,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -6612,9 +6612,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -6633,9 +6633,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -6654,9 +6654,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -6675,9 +6675,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -6712,9 +6712,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -6800,9 +6800,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", + "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", "dev": true, "license": "ISC", "dependencies": { @@ -6875,7 +6875,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", @@ -7305,7 +7304,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7423,7 +7421,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7454,7 +7451,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7463,11 +7459,10 @@ } }, "node_modules/react-hook-form": { - "version": "7.71.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", - "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7809,9 +7804,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, "license": "ISC", "bin": { @@ -8246,9 +8241,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "license": "MIT", "funding": { "type": "github", @@ -8256,11 +8251,10 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", diff --git a/package.json b/package.json index 0065226..f016fae 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", + "baseline-browser-mapping": "^2.10.0", "eslint": "^9.39.3", "eslint-config-next": "^16.1.6", "postcss": "^8.5", diff --git a/scripts/002-auth-tables.sql b/scripts/002-auth-tables.sql new file mode 100644 index 0000000..2727048 --- /dev/null +++ b/scripts/002-auth-tables.sql @@ -0,0 +1,36 @@ +-- Auth tables for wallet nonce verification and refresh-token sessions + +CREATE TABLE IF NOT EXISTS auth_nonces ( + id BIGSERIAL PRIMARY KEY, + wallet_address VARCHAR(56) NOT NULL, + nonce_hash CHAR(64) NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_auth_nonces_wallet_created + ON auth_nonces (wallet_address, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_auth_nonces_active + ON auth_nonces (wallet_address, expires_at) + WHERE used_at IS NULL; + +CREATE TABLE IF NOT EXISTS auth_refresh_tokens ( + id BIGSERIAL PRIMARY KEY, + wallet_address VARCHAR(56) NOT NULL, + jti VARCHAR(128) UNIQUE NOT NULL, + token_hash CHAR(64) UNIQUE NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, + replaced_by_jti VARCHAR(128), + user_agent TEXT, + ip_address INET, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_used_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_auth_refresh_wallet + ON auth_refresh_tokens (wallet_address, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_auth_refresh_active + ON auth_refresh_tokens (wallet_address, expires_at) + WHERE revoked_at IS NULL;