import { createJwtUtils } from '@bytelyst/auth'; import { config } from '../config/index.js'; import logger from '../utils/logger.js'; import { supabaseService } from './SupabaseService.js'; export interface VerifiedTradingAuth { userId: string | null; role?: string; email?: string; displayName?: string; plan?: string; productId?: string; source: 'platform' | 'supabase' | null; error?: string; } let cachedJwtUtils: ReturnType | null | undefined; function normalizeRole(role: unknown): string | undefined { const value = String(role || '').trim().toLowerCase(); return value || undefined; } function getPlatformJwtUtils(): ReturnType | null { if (cachedJwtUtils !== undefined) { return cachedJwtUtils; } if (!config.PLATFORM_AUTH_ENABLED) { cachedJwtUtils = null; return cachedJwtUtils; } const hasHs256 = Boolean(config.JWT_SECRET); const hasRs256 = Boolean(config.PLATFORM_JWT_PUBLIC_KEY || config.PLATFORM_JWT_JWKS_URL); if (!hasHs256 && !hasRs256) { cachedJwtUtils = null; return cachedJwtUtils; } cachedJwtUtils = createJwtUtils({ issuer: config.PLATFORM_JWT_ISSUER, algorithm: hasRs256 ? 'RS256' : 'HS256', rsaPublicKey: config.PLATFORM_JWT_PUBLIC_KEY || undefined, jwksUrl: config.PLATFORM_JWT_JWKS_URL || undefined, }); return cachedJwtUtils; } export async function verifyTradingAccessToken(token: string): Promise { const jwtUtils = getPlatformJwtUtils(); if (jwtUtils) { try { const payload = await jwtUtils.verifyToken(token); if (!payload) { throw new Error('Platform token verification returned null payload'); } const productId = String(payload?.productId || '').trim(); if (!payload?.sub) { return { userId: null, source: null, error: 'Platform token missing subject claim' }; } if (payload?.type && payload.type !== 'access') { return { userId: null, source: null, error: 'Platform token is not an access token' }; } if (productId && productId !== config.PRODUCT_ID) { return { userId: null, source: null, error: `Platform token product mismatch (${productId})` }; } return { userId: String(payload.sub), role: normalizeRole(payload.role), email: typeof payload.email === 'string' ? payload.email : undefined, displayName: typeof payload.displayName === 'string' ? payload.displayName : typeof payload.name === 'string' ? payload.name : undefined, plan: typeof payload.plan === 'string' ? payload.plan : undefined, productId: productId || config.PRODUCT_ID, source: 'platform', }; } catch (error) { logger.warn(`[Auth] Platform token verification failed, falling back to legacy verifier: ${error instanceof Error ? error.message : 'unknown error'}`); } } const legacy = await supabaseService.verifyAccessToken(token); if (!legacy.userId) { return { userId: null, source: null, error: legacy.error || 'invalid token', }; } return { userId: legacy.userId, source: 'supabase', }; } /** * Authoritative admin check for trading API scoping: platform JWT role first, then legacy user-store admin flag. * Call sites should use this instead of SupabaseService.isAdmin so platform sessions stay consistent. */ export async function isTradingAdmin(userId: string, tokenRole?: string | null): Promise { const normalizedRole = normalizeRole(tokenRole); if (normalizedRole === 'admin' || normalizedRole === 'super_admin') { return true; } return supabaseService.isAdmin(userId); }