decentralised message store
at main 3.4 kB view raw
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"; 6import type { AtUri, Did } from "@/lib/types/atproto"; 7import { atUriSchema, didSchema } from "@/lib/types/atproto"; 8import { SERVER_PORT, SERVICE_DID } from "@/lib/env"; 9 10export const sessionInfoSchema = z.object({ 11 id: z.string(), 12 token: z.string(), 13 fingerprint: z.string(), 14 allowedChannels: z.array(atUriSchema), 15 shardDid: didSchema, 16 latticeDid: didSchema, 17}); 18export type SessionInfo = z.infer<typeof sessionInfoSchema>; 19 20export const generateSessionId = () => { 21 return crypto.randomUUID(); 22}; 23 24export const generateSessionInfo = ( 25 sessionId: string, 26 allowedChannels: Array<AtUri>, 27 latticeDid: Did, 28): SessionInfo => { 29 const token = crypto.randomBytes(32).toString("base64url"); 30 31 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET); 32 hmac.update(`${token}:${sessionId}`); 33 const fingerprint = hmac.digest("hex"); 34 35 const shardDid: Did = SERVICE_DID.includes("localhost") 36 ? `${SERVICE_DID}%3A${SERVER_PORT.toString()}` 37 : SERVICE_DID; 38 39 return { 40 id: sessionId, 41 token, 42 fingerprint, 43 allowedChannels, 44 latticeDid, 45 shardDid, 46 }; 47}; 48 49export const verifyHandshakeToken = ({ 50 token, 51 fingerprint, 52 id: sessionId, 53}: SessionInfo) => { 54 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET); 55 hmac.update(`${token}:${sessionId}`); 56 const expectedFingerprint = hmac.digest("hex"); 57 58 try { 59 return crypto.timingSafeEqual( 60 Buffer.from(fingerprint, "hex"), 61 Buffer.from(expectedFingerprint, "hex"), 62 ); 63 } catch { 64 return false; 65 } 66}; 67 68export const issuedHandshakes = new Map<string, SessionInfo>(); 69 70export const issueNewHandshakeToken = ({ 71 allowedChannels, 72 latticeDid, 73}: { 74 allowedChannels: Array<AtUri | undefined>; 75 latticeDid: Did; 76}) => { 77 const filteredChannels = allowedChannels.filter( 78 (channels) => channels !== undefined, 79 ); 80 const sessionId = generateSessionId(); 81 const sessionInfo = generateSessionInfo( 82 sessionId, 83 filteredChannels, 84 latticeDid, 85 ); 86 console.log("Issuing new handshake token with session info", sessionInfo); 87 issuedHandshakes.set(sessionInfo.token, sessionInfo); 88 return sessionInfo; 89}; 90 91export const activeSessions = new Map<string, WebSocket>(); 92 93export const isValidSession = (sessionInfo: SessionInfo) => { 94 return ( 95 issuedHandshakes.has(sessionInfo.token) && 96 verifyHandshakeToken(sessionInfo) 97 ); 98}; 99 100export const createNewSession = ({ 101 sessionInfo, 102 socket, 103}: { 104 sessionInfo: SessionInfo; 105 socket: WebSocket; 106}): Result<{ sessionSocket: WebSocket }, undefined> => { 107 try { 108 issuedHandshakes.delete(sessionInfo.token); 109 } catch { 110 return { ok: false }; 111 } 112 activeSessions.set(sessionInfo.id, socket); 113 return { ok: true, data: { sessionSocket: socket } }; 114}; 115 116export const deleteSession = ( 117 sessionInfo: SessionInfo, 118): Result<undefined, undefined> => { 119 if (!activeSessions.has(sessionInfo.id)) return { ok: false }; 120 try { 121 activeSessions.delete(sessionInfo.id); 122 } catch { 123 return { ok: false }; 124 } 125 return { ok: true }; 126};