decentralised message store
1import type { WebSocket } from "ws"; 2import * as crypto from "node:crypto"; 3import { SESSIONS_SECRET } from "@/lib/utils/crypto"; 4import { z } from "zod"; 5import type { Result } from "@/lib/utils/result"; 6 7export const sessionInfoSchema = z.object({ 8 id: z.string(), 9 token: z.string(), 10 fingerprint: z.string(), 11}); 12export type SessionInfo = z.infer<typeof sessionInfoSchema>; 13 14export const generateSessionId = () => { 15 return crypto.randomUUID(); 16}; 17 18export const generateSessionInfo = (sessionId: string): SessionInfo => { 19 const token = crypto.randomBytes(32).toString("base64url"); 20 21 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET); 22 hmac.update(`${token}:${sessionId}`); 23 const fingerprint = hmac.digest("hex"); 24 25 return { id: sessionId, token, fingerprint }; 26}; 27 28export const verifyHandshakeToken = ({ 29 token, 30 fingerprint, 31 id: sessionId, 32}: SessionInfo) => { 33 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET); 34 hmac.update(`${token}:${sessionId}`); 35 const expectedFingerprint = hmac.digest("hex"); 36 37 try { 38 return crypto.timingSafeEqual( 39 Buffer.from(fingerprint, "hex"), 40 Buffer.from(expectedFingerprint, "hex"), 41 ); 42 } catch { 43 return false; 44 } 45}; 46 47export const issuedHandshakes = new Map<string, SessionInfo>(); 48 49export const issueNewHandshakeToken = () => { 50 const sessionId = generateSessionId(); 51 const sessionInfo = generateSessionInfo(sessionId); 52 issuedHandshakes.set(sessionInfo.id, sessionInfo); 53 return sessionInfo; 54}; 55 56export const activeSessions = new Map<string, WebSocket>(); 57 58export const isValidSession = (sessionInfo: SessionInfo) => { 59 return ( 60 issuedHandshakes.has(sessionInfo.id) && 61 verifyHandshakeToken(sessionInfo) 62 ); 63}; 64 65export const createNewSession = ({ 66 sessionInfo, 67 socket, 68}: { 69 sessionInfo: SessionInfo; 70 socket: WebSocket; 71}): Result<{ sessionSocket: WebSocket }, undefined> => { 72 try { 73 issuedHandshakes.delete(sessionInfo.id); 74 } catch { 75 return { ok: false }; 76 } 77 activeSessions.set(sessionInfo.id, socket); 78 return { ok: true, data: { sessionSocket: socket } }; 79}; 80 81export const deleteSession = ( 82 sessionInfo: SessionInfo, 83): Result<undefined, undefined> => { 84 if (!activeSessions.has(sessionInfo.id)) return { ok: false }; 85 try { 86 activeSessions.delete(sessionInfo.id); 87 } catch { 88 return { ok: false }; 89 } 90 return { ok: true }; 91};