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 createNewSession = ({ 59 sessionInfo, 60 socket, 61}: { 62 sessionInfo: SessionInfo; 63 socket: WebSocket; 64}): Result<undefined, undefined> => { 65 const isValidSession = verifyHandshakeToken(sessionInfo); 66 if (!isValidSession) return { ok: false }; 67 68 try { 69 issuedHandshakes.delete(sessionInfo.id); 70 } catch { 71 return { ok: false }; 72 } 73 activeSessions.set(sessionInfo.id, socket); 74 return { ok: true }; 75}; 76 77export const deleteSession = ( 78 sessionInfo: SessionInfo, 79): Result<undefined, undefined> => { 80 if (!activeSessions.has(sessionInfo.id)) return { ok: false }; 81 try { 82 activeSessions.delete(sessionInfo.id); 83 } catch { 84 return { ok: false }; 85 } 86 return { ok: true }; 87};