diff --git a/docs/superpowers/plans/2026-05-24-websocket-refactor.md b/docs/superpowers/plans/2026-05-24-websocket-refactor.md new file mode 100644 index 00000000..81b1e3f1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-24-websocket-refactor.md @@ -0,0 +1,321 @@ +# WebSocket Integration Refactor Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. + +**Goal:** Single, correct WebSocket integration — one token utility, one client hook, all consumers using it. + +**Architecture:** Consolidate `src/lib/auth/wsToken.js` into `src/lib/ws-token.js`, make `useSecureSocket` the sole client-side entry point (with serverless bypass, SSL detection, token refresh), migrate all 4 direct `io()` callers, and fix the Redis crash bug in `socket-server.js`. + +**Tech Stack:** socket.io 4.x, socket.io-client 4.x, Next.js App Router, JWT + +--- + +### Task 1: Consolidate token utilities into `src/lib/ws-token.js` + +**Files:** +- Delete: `src/lib/auth/wsToken.js` +- Modify: `src/lib/ws-token.js` — add `signWsToken` convenience (with `type: 'websocket'` baked in) to replace the deleted file +- Modify: `src/socket/namespaces/voice.js:2` — switch import from `@/lib/auth/wsToken` to `@/lib/ws-token` +- Modify: `src/app/api/interview/sessions/route.js:7` — switch import from `@/lib/auth/wsToken` to `@/lib/ws-token` +- Modify: `src/app/api/interview/sessions/[id]/rehydrate/route.js:15` — switch import from `@/lib/auth/wsToken` to `@/lib/ws-token` + +**Step 1: Add `signWsToken` to `src/lib/ws-token.js`** + +Add after the existing `generateWsToken` function: + +```js +/** + * Generates a short-lived token for WebSocket authentication. + * @param {Object} payload — e.g. { userId, sessionId } + * @returns {string} Signed JWT valid for 15 minutes with type: 'websocket' + */ +export function signWsToken(payload) { + if (!JWT_SECRET) { + throw new Error('JWT_SECRET environment variable is required') + } + return jwt.sign( + { ...payload, type: 'websocket' }, + JWT_SECRET, + { expiresIn: '15m' } + ) +} +``` + +This ensures all tokens carry `type: 'websocket'` so the main auth middleware's type check passes. + +**Step 2: Update voice.js import** + +```js +// src/socket/namespaces/voice.js line 2 +// Before: +import { verifyWsToken } from '@/lib/auth/wsToken' +// After: +import { verifyWsToken } from '@/lib/ws-token' +``` + +**Step 3: Update interview session route** + +```js +// src/app/api/interview/sessions/route.js line 7 +// Before: +import { signWsToken } from '@/lib/auth/wsToken' +// After: +import { signWsToken } from '@/lib/ws-token' +``` + +**Step 4: Update interview rehydrate route** + +```js +// src/app/api/interview/sessions/[id]/rehydrate/route.js +// Before: +import { signWsToken } from '@/lib/auth/wsToken' +// After: +import { signWsToken } from '@/lib/ws-token' +``` + +**Step 5: Delete `src/lib/auth/wsToken.js`** + +```bash +rm src/lib/auth/wsToken.js +``` + +**Step 6: Run tests/lint** + +```bash +npx prettier --write src/lib/ws-token.js src/socket/namespaces/voice.js src/app/api/interview/sessions/route.js src/app/api/interview/sessions/\[id\]/rehydrate/route.js +npx eslint --fix src/lib/ws-token.js src/socket/namespaces/voice.js src/app/api/interview/sessions/route.js src/app/api/interview/sessions/\[id\]/rehydrate/route.js +``` + +--- + +### Task 2: Fix Redis crash in socket-server.js + +**Files:** +- Modify: `src/lib/socket-server.js:111-113` + +**Problem:** `redisClient.duplicate()` is called before the try/catch block. If `redisClient` was never connected or initialized, this throws and kills the entire Socket.IO server. + +**Step 1: Move `duplicate()` calls inside try/catch with null checks** + +```js +// Replace lines 111-135: +let adapterEnabled = false +let pubClient = null +let subClient = null +try { + pubClient = redisClient?.duplicate() + subClient = redisClient?.duplicate() + if (!pubClient || !subClient) { + console.warn('[Socket.IO] Redis client unavailable, running without adapter') + } else { + const [pubReady, subReady] = await Promise.all([ + safeConnectRedisClient(pubClient, 'Redis adapter pubClient'), + safeConnectRedisClient(subClient, 'Redis adapter subClient'), + ]) + if (pubReady && subReady) { + serverIo.adapter(createAdapter(pubClient, subClient)) + adapterEnabled = true + } else { + await Promise.all([ + safeQuitRedisClient(pubClient), + safeQuitRedisClient(subClient), + ]) + console.warn('[Socket.IO] Redis adapter disabled, running in single-node mode') + } + } +} catch (adapterError) { + if (pubClient) safeQuitRedisClient(pubClient) + if (subClient) safeQuitRedisClient(subClient) + console.warn( + `[Socket.IO] Redis adapter setup failed, continuing without adapter: ${adapterError.message}` + ) +} +``` + +--- + +### Task 3: Fix `useSecureSocket` token refresh race + +**Files:** +- Modify: `src/hooks/useSecureSocket.js:225` + +**Problem:** When token refresh fires but the socket is still connected, the new token is stored but never applied until disconnect. Should force a reconnect with the fresh token. + +**Step 1: Replace the reconnect-if-disconnected logic with always-reconnect** + +Replace lines 218-232: + +```js +refreshTimerRef.current = setTimeout(async () => { + try { + console.log('[useSecureSocket] Refreshing authentication token') + const { wsToken } = await fetchWsToken() + tokenRef.current = wsToken + + // Always reconnect to apply the new token + if (socketRef.current) { + socketRef.current.disconnect() + } + await connect() + } catch (err) { + console.error('[useSecureSocket] Token refresh failed:', err.message) + } +}, delay) +``` + +--- + +### Task 4: Migrate `ScorecardView.jsx` to `useSecureSocket` + +**Files:** +- Modify: `src/features/interview/ScorecardView.jsx` + +**Step 1: Replace direct `io()` with `useSecureSocket`** + +Remove the socket setup code in the useEffect (lines 70-108). Add the hook: + +```js +import { useSecureSocket } from '@/hooks/useSecureSocket' + +// Inside component: +const { socket, isConnected } = useSecureSocket('/interview', { + sessionId, + scope: 'interview', + onConnect: useCallback(async () => { + setIsSocketConnected(true) + socketRef.current = socket + socket?.emit('interview:join') + if (!resultFoundRef.current) { + await fetchResult() + } + }, [sessionId]), + onDisconnect: useCallback(() => setIsSocketConnected(false), []), +}) +``` + +Replace `socketRef.current` usage with the `socket` from the hook. + +Remove the `/rehydrate` fetch call since `useSecureSocket` already handles token fetching via `/api/auth/ws-token`. + +--- + +### Task 5: Migrate `ContestDetailPage.jsx` to `useSecureSocket` + +**Files:** +- Modify: `src/features/contests/components/ContestDetailPage.jsx` + +**Step 1: Remove direct `io` import, add `useSecureSocket`** + +```js +// Remove: +import { io } from 'socket.io-client' +// Add: +import { useSecureSocket } from '@/hooks/useSecureSocket' +``` + +**Step 2: Replace direct socket with hook** + +```js +const { socket, isConnected } = useSecureSocket('', { + scope: 'general', +}) +``` + +Remove the raw `io()` call. Move socket event listeners into a `useEffect` that watches `socket`. + +--- + +### Task 6: Migrate contest `result/page.jsx` to `useSecureSocket` + +**Files:** +- Modify: `src/app/contests/[id]/result/page.jsx` + +**Step 1: Replace `require('socket.io-client')` with `useSecureSocket`** + +This component is likely a client component already. Add: + +```js +import { useSecureSocket } from '@/hooks/useSecureSocket' +``` + +**Step 2: Use the hook** + +```js +const { socket } = useSecureSocket('', { + scope: 'general', + onConnect: useCallback(() => { + console.log('[ResultPage] Socket connected') + }, []), +}) +``` + +Add a `useEffect` that watches `socket` and attaches the `contest:result_finalized` listener, cleaning up on unmount. + +--- + +### Task 7: Migrate `InterviewShell.jsx` to `useSecureSocket` + +**Files:** +- Modify: `src/features/interview/InterviewShell.jsx` + +**Step 1: Replace direct `io` import** + +```js +// Remove: +import { io } from 'socket.io-client' +// Add: +import { useSecureSocket } from '@/hooks/useSecureSocket' +``` + +**Step 2: Replace socket creation with hook** + +The InterviewShell likely connects to `/interview` namespace. The interview flow gets its sessionId and token from the session creation response, so use: + +```js +const { socket, isConnected } = useSecureSocket('/interview', { + sessionId: sessionIdFromProps, + scope: 'interview', +}) +``` + +Remove `useRef` and `useEffect` that create the raw socket. Clean up any manual token handling. + +--- + +### Task 8: Remove hardcoded localhost URLs + +**Files:** +- Remove from: `ScorecardView.jsx:76`, `result/page.jsx:66` + +All hardcoded `http://localhost:...` fallbacks become dead code after migration since `useSecureSocket` derives the URL dynamically from `/api/auth/ws-token`. + +After Tasks 4-7 are complete, verify no `localhost` socket URLs remain: + +```bash +rg 'localhost.*socket' src/ --include='*.{jsx,js}' +# Expected: 0 matches (false positives possible, inspect each) +``` + +--- + +### Verification + +**Step 1: Check no remaining references to `@/lib/auth/wsToken`** + +```bash +rg 'from.*auth/wsToken' src/ +# Expected: 0 matches +``` + +**Step 2: Check no direct `io()` calls remain** + +```bash +rg "from 'socket.io-client'" src/ --include='*.{jsx,js}' +# Expected: 0 matches (useSecureSocket replaces all) +``` + +**Step 3: Build check** + +```bash +npx next build --no-lint 2>&1 | head -30 +``` diff --git a/src/app/api/interview/sessions/[id]/rehydrate/route.js b/src/app/api/interview/sessions/[id]/rehydrate/route.js index 590d46cb..5d80ab98 100644 --- a/src/app/api/interview/sessions/[id]/rehydrate/route.js +++ b/src/app/api/interview/sessions/[id]/rehydrate/route.js @@ -5,7 +5,7 @@ import { InterviewSnapshot } from '@/models/InterviewSnapshot.model' import { InterviewSession } from '@/models/InterviewSession.model' import { Problem } from '@/models/Problem.models' import { protect } from '@/middlewares/auth.middleware' -import { signWsToken } from '@/lib/auth/wsToken' +import { signWsToken } from '@/lib/ws-token' import { asyncHandler } from '@/lib/asyncHandler' import { redisClient } from '@/lib/redis' import { aiEnginePort } from '@/lib/ai-engine' diff --git a/src/app/api/interview/sessions/route.js b/src/app/api/interview/sessions/route.js index 1f7c88ed..2976fea2 100644 --- a/src/app/api/interview/sessions/route.js +++ b/src/app/api/interview/sessions/route.js @@ -4,7 +4,7 @@ import { InterviewSession } from '@/models/InterviewSession.model' import { createSession } from '@/services/interviewSession.service' import dbConnect from '@/lib/mongodb' import { asyncHandler } from '@/lib/asyncHandler' -import { signWsToken } from '@/lib/auth/wsToken' +import { signWsToken } from '@/lib/ws-token' import { Problem } from '@/models/Problem.models' export const POST = asyncHandler(async (req) => { diff --git a/src/app/blog/[slug]/page.jsx b/src/app/blog/[slug]/page.jsx index 6645211b..5e2eb26a 100644 --- a/src/app/blog/[slug]/page.jsx +++ b/src/app/blog/[slug]/page.jsx @@ -142,7 +142,7 @@ export default function BlogDetailPage() { if (!post) { return (
-

Post not found

+

Post not found

Return to Arena Journal diff --git a/src/app/blog/page.jsx b/src/app/blog/page.jsx index afaea9a1..c40cc15f 100644 --- a/src/app/blog/page.jsx +++ b/src/app/blog/page.jsx @@ -130,6 +130,7 @@ export default function BlogListingPage() { placeholder="Search articles..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + aria-label="Search articles" className="bg-bg-subtle border-border text-text-primary placeholder:text-text-muted focus:ring-accent w-full rounded-md border py-2 pr-4 pl-10 text-sm transition-all focus:ring-2 focus:outline-none" />
@@ -170,6 +171,7 @@ export default function BlogListingPage() { diff --git a/src/features/problems/components/ProblemsToolbar.jsx b/src/features/problems/components/ProblemsToolbar.jsx index c1627f4c..0627e7c5 100644 --- a/src/features/problems/components/ProblemsToolbar.jsx +++ b/src/features/problems/components/ProblemsToolbar.jsx @@ -40,6 +40,7 @@ export default function ProblemsToolbar({ sortBy, setSortBy, setSidebarOpen, tot value={sortBy} onChange={(e) => setSortBy(e.target.value)} className="bg-bg-page border-border text-text-primary focus:ring-accent duration-normal cursor-pointer rounded-md border px-3 py-2 pr-8 text-sm transition-colors focus:border-transparent focus:ring-2 focus:outline-none" + aria-label="Sort by" > diff --git a/src/hooks/useSecureSocket.js b/src/hooks/useSecureSocket.js index 81dc012a..78dbf64d 100644 --- a/src/hooks/useSecureSocket.js +++ b/src/hooks/useSecureSocket.js @@ -31,6 +31,7 @@ export function useSecureSocket(namespace = '', options = {}) { const tokenRequestRef = useRef(null) const connectingRef = useRef(false) const connectRef = useRef(null) + const connectGenRef = useRef(0) const { onConnect, @@ -50,28 +51,29 @@ export function useSecureSocket(namespace = '', options = {}) { } try { - tokenRequestRef.current = fetch('/api/auth/ws-token', { + const promise = fetch('/api/auth/ws-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ sessionId, scope }), credentials: 'include', - }) - - const response = await tokenRequestRef.current + }).then(async (response) => { + if (!response.ok) { + throw new Error(`Token fetch failed: ${response.status}`) + } - if (!response.ok) { - throw new Error(`Token fetch failed: ${response.status}`) - } + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Token generation failed') + } - const data = await response.json() - if (!data.success) { - throw new Error(data.error || 'Token generation failed') - } + tokenRef.current = data.wsToken + return data + }) - tokenRef.current = data.wsToken - return data + tokenRequestRef.current = promise + return await promise } catch (err) { console.error('[useSecureSocket] Token fetch error:', err.message) setError(err.message) @@ -91,10 +93,7 @@ export function useSecureSocket(namespace = '', options = {}) { return } - if (connectingRef.current) { - console.log('[useSecureSocket] Connection already in progress') - return - } + const gen = ++connectGenRef.current if (socketRef.current?.connected) { console.log('[useSecureSocket] Already connected') @@ -108,6 +107,8 @@ export function useSecureSocket(namespace = '', options = {}) { const data = await fetchWsToken() // console.log("Socket Console Data: ", data); + if (gen !== connectGenRef.current) return + if (data && data.enabled === false) { console.log( `[useSecureSocket] Real-time sockets are disabled on the server (serverless mode). Bypassing connection to namespace: ${namespace}` @@ -118,18 +119,23 @@ export function useSecureSocket(namespace = '', options = {}) { return } - const { wsToken, socketUrl } = data; + const { wsToken, socketUrl } = data + if (!wsToken) { + throw new Error('WebSocket token is empty') + } //console.log("Socket Console: ", socketUrl); - let socketUrl_ = process.env.NEXT_PUBLIC_SOCKET_URL || socketUrl; + let socketUrl_ = process.env.NEXT_PUBLIC_SOCKET_URL || socketUrl // Localhost protocol sanitization if (typeof window !== 'undefined') { - const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; - const isPageHttp = window.location.protocol === 'http:'; - + const isLocalhost = + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' + const isPageHttp = window.location.protocol === 'http:' + if (isLocalhost && isPageHttp && socketUrl_) { - socketUrl_ = socketUrl_.replace(/^https:\/\//i, 'http://'); + socketUrl_ = socketUrl_.replace(/^https:\/\//i, 'http://') } } @@ -198,7 +204,9 @@ export function useSecureSocket(namespace = '', options = {}) { console.warn('[useSecureSocket] Connection failed:', err.message) setError(err.message) } finally { - connectingRef.current = false + if (gen === connectGenRef.current) { + connectingRef.current = false + } } }, [user, namespace, fetchWsToken, onConnect, onDisconnect, onError, autoReconnect]) @@ -221,10 +229,15 @@ export function useSecureSocket(namespace = '', options = {}) { const { wsToken } = await fetchWsToken() tokenRef.current = wsToken - // Reconnect with new token if needed - if (socketRef.current?.disconnected) { - await connect() + // Force reconnect to apply the updated auth token + if (socketRef.current?.connected) { + socketRef.current.removeAllListeners() + socketRef.current.disconnect() + socketRef.current = null + setSocket(null) + setIsConnected(false) } + await connect() } catch (err) { console.error('[useSecureSocket] Token refresh failed:', err.message) // Token refresh failed, will attempt on next connect @@ -251,6 +264,9 @@ export function useSecureSocket(namespace = '', options = {}) { connectRef.current?.() return () => { + // Increment generation to invalidate any in-flight connect call + connectGenRef.current++ + // Cleanup timeout if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current) @@ -262,7 +278,6 @@ export function useSecureSocket(namespace = '', options = {}) { socketRef.current = null setSocket(null) setIsConnected(false) - connectingRef.current = false } }, [user]) diff --git a/src/lib/auth/wsToken.js b/src/lib/auth/wsToken.js deleted file mode 100644 index bf81829f..00000000 --- a/src/lib/auth/wsToken.js +++ /dev/null @@ -1,29 +0,0 @@ -import jwt from 'jsonwebtoken' - -const JWT_SECRET = process.env.JWT_SECRET - -function checkSecret() { - if (!JWT_SECRET && process.env.NEXT_PHASE !== 'phase-production-build') { - throw new Error('Please define JWT_SECRET in .env.local') - } -} - -/** - * Generates a short-lived token for WebSocket authentication. - * @param {Object} payload - Data to encode in the token (e.g., userId, sessionId) - * @returns {string} - Signed JWT valid for 15 minutes - */ -export function signWsToken(payload) { - checkSecret() - return jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' }) -} - -/** - * Verifies a WebSocket token. - * @param {string} token - * @returns {Object} - Decoded payload - */ -export function verifyWsToken(token) { - checkSecret() - return jwt.verify(token, JWT_SECRET) -} diff --git a/src/lib/socket-server.js b/src/lib/socket-server.js index c7b6550b..ae49f2ef 100644 --- a/src/lib/socket-server.js +++ b/src/lib/socket-server.js @@ -109,26 +109,34 @@ export async function initSocketServer() { // Setup Redis adapter when available, but keep Socket.IO online without it. let adapterEnabled = false - const pubClient = redisClient.duplicate() - const subClient = redisClient.duplicate() + let pubClient = null + let subClient = null try { - const [pubReady, subReady] = await Promise.all([ - safeConnectRedisClient(pubClient, 'Redis adapter pubClient'), - safeConnectRedisClient(subClient, 'Redis adapter subClient'), - ]) - - if (pubReady && subReady) { - serverIo.adapter(createAdapter(pubClient, subClient)) - adapterEnabled = true + pubClient = redisClient?.duplicate() + subClient = redisClient?.duplicate() + if (!pubClient || !subClient) { + console.warn('[Socket.IO] Redis client unavailable, running without adapter') } else { - await Promise.all([ - safeQuitRedisClient(pubClient), - safeQuitRedisClient(subClient), + const [pubReady, subReady] = await Promise.all([ + safeConnectRedisClient(pubClient, 'Redis adapter pubClient'), + safeConnectRedisClient(subClient, 'Redis adapter subClient'), ]) - console.warn('[Socket.IO] Redis adapter disabled, running in single-node mode') + if (pubReady && subReady) { + serverIo.adapter(createAdapter(pubClient, subClient)) + adapterEnabled = true + } else { + await Promise.all([ + safeQuitRedisClient(pubClient), + safeQuitRedisClient(subClient), + ]) + console.warn( + '[Socket.IO] Redis adapter disabled, running in single-node mode' + ) + } } } catch (adapterError) { - await Promise.all([safeQuitRedisClient(pubClient), safeQuitRedisClient(subClient)]) + if (pubClient) safeQuitRedisClient(pubClient) + if (subClient) safeQuitRedisClient(subClient) console.warn( `[Socket.IO] Redis adapter setup failed, continuing without adapter: ${adapterError.message}` ) @@ -213,6 +221,10 @@ export async function initSocketServer() { // Redis Subscriber for submission events try { + if (!redisClient) { + console.warn('[Socket.IO] Redis unavailable, skipping subscriber setup') + throw new Error('Redis client unavailable') + } const redisSubClient = redisClient.duplicate() await redisSubClient.connect() diff --git a/src/lib/ws-token.js b/src/lib/ws-token.js index 2f181478..f5a62aca 100644 --- a/src/lib/ws-token.js +++ b/src/lib/ws-token.js @@ -8,6 +8,19 @@ const JWT_SECRET = process.env.JWT_SECRET const WS_TOKEN_EXPIRY = '1h' // WebSocket tokens live 1 hour const REFRESH_THRESHOLD = 5 * 60 * 1000 // Refresh token if < 5 min left +/** + * Generates a short-lived token for WebSocket authentication. + * Required payload keys: { userId, sessionId } + * @param {Object} payload — must include userId and sessionId + * @returns {string} Signed JWT valid for 15 minutes with type: 'websocket' + */ +export function signWsToken(payload) { + if (!JWT_SECRET) { + throw new Error('JWT_SECRET environment variable is required') + } + return jwt.sign({ ...payload, type: 'websocket' }, JWT_SECRET, { expiresIn: WS_TOKEN_EXPIRY }) +} + export function generateWsToken(userId, sessionData = {}) { if (!JWT_SECRET) { throw new Error('JWT_SECRET environment variable is required') diff --git a/src/shared/components/ui/DevNoticeModal.jsx b/src/shared/components/ui/DevNoticeModal.jsx new file mode 100644 index 00000000..9e94342b --- /dev/null +++ b/src/shared/components/ui/DevNoticeModal.jsx @@ -0,0 +1,74 @@ +'use client' + +import { useEffect, useState } from 'react' +import { X } from 'lucide-react' + +export default function DevNoticeModal() { + const [open, setOpen] = useState(false) + + useEffect(() => { + const stored = localStorage.getItem('dev_notice_dismissed') + if (stored === 'true') return + + const isDeployEnv = + window.location.hostname.includes('vercel') || + window.location.hostname.includes('now.sh') || + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + process.env.NEXT_PUBLIC_VERCEL_ENV + + if (!isDeployEnv) return + + const timer = setTimeout(() => setOpen(true), 600) + return () => clearTimeout(timer) + }, []) + + const dismiss = () => { + setOpen(false) + try { + localStorage.setItem('dev_notice_dismissed', 'true') + } catch {} + } + + if (!open) return null + + return ( +
+
+
+

Development Notice

+ +
+ +
+

+ This instance of CodeArena is running on{' '} + Vercel's free tier{' '} + and is currently under active development. +

+

+ As a result, certain features including real-time collaboration, + WebSocket connections, and code execution may be limited or behave + differently than they would in a full production environment. +

+

+ We're working hard to bring the complete experience. Thank you for + your understanding and support. +

+
+ + +
+
+ ) +} diff --git a/src/socket/namespaces/interview.js b/src/socket/namespaces/interview.js index ab40ecf5..ebd636ed 100644 --- a/src/socket/namespaces/interview.js +++ b/src/socket/namespaces/interview.js @@ -1,5 +1,5 @@ import crypto from 'crypto' -import { verifyWsToken } from '@/lib/auth/wsToken' +import { verifyWsToken } from '@/lib/ws-token' import { InterviewSession } from '@/models/InterviewSession.model' import { InterviewMessage } from '@/models/InterviewMessage.model' import { InterviewSnapshot } from '@/models/InterviewSnapshot.model' diff --git a/src/socket/namespaces/voice.js b/src/socket/namespaces/voice.js index 0f21cee7..bf40ae94 100644 --- a/src/socket/namespaces/voice.js +++ b/src/socket/namespaces/voice.js @@ -1,5 +1,5 @@ import { DeepgramClient } from '@deepgram/sdk' -import { verifyWsToken } from '@/lib/auth/wsToken' +import { verifyWsToken } from '@/lib/ws-token' import { isSessionActive } from '@/services/sessionGuard' import { hasVoiceAccess } from '@/services/accessControl.service'