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"; 6import type { AtUri, Did } from "@/lib/types/atproto"; 7import { atUriSchema, didSchema } from "@/lib/types/atproto"; 8import { 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 = SERVICE_DID; 36 37 return { 38 id: sessionId, 39 token, 40 fingerprint, 41 allowedChannels, 42 latticeDid, 43 shardDid, 44 }; 45}; 46 47export const verifyHandshakeToken = ({ 48 token, 49 fingerprint, 50 id: sessionId, 51}: SessionInfo) => { 52 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET); 53 hmac.update(`${token}:${sessionId}`); 54 const expectedFingerprint = hmac.digest("hex"); 55 56 try { 57 return crypto.timingSafeEqual( 58 Buffer.from(fingerprint, "hex"), 59 Buffer.from(expectedFingerprint, "hex"), 60 ); 61 } catch { 62 return false; 63 } 64}; 65 66export const issuedHandshakes = new Map<string, SessionInfo>(); 67 68export const issueNewHandshakeToken = ({ 69 allowedChannels, 70 latticeDid, 71}: { 72 allowedChannels: Array<AtUri | undefined>; 73 latticeDid: Did; 74}) => { 75 const filteredChannels = allowedChannels.filter( 76 (channels) => channels !== undefined, 77 ); 78 const sessionId = generateSessionId(); 79 const sessionInfo = generateSessionInfo( 80 sessionId, 81 filteredChannels, 82 latticeDid, 83 ); 84 issuedHandshakes.set(sessionInfo.token, sessionInfo); 85 return sessionInfo; 86}; 87 88export const activeSessions = new Map<string, WebSocket>(); 89 90export const isValidSession = (sessionInfo: SessionInfo) => { 91 return ( 92 issuedHandshakes.has(sessionInfo.token) && 93 verifyHandshakeToken(sessionInfo) 94 ); 95}; 96 97export const createNewSession = ({ 98 sessionInfo, 99 socket, 100}: { 101 sessionInfo: SessionInfo; 102 socket: WebSocket; 103}): Result<{ sessionSocket: WebSocket }, undefined> => { 104 try { 105 issuedHandshakes.delete(sessionInfo.token); 106 } catch { 107 return { ok: false }; 108 } 109 activeSessions.set(sessionInfo.id, socket); 110 return { ok: true, data: { sessionSocket: socket } }; 111}; 112 113export const deleteSession = ( 114 sessionInfo: SessionInfo, 115): Result<undefined, undefined> => { 116 if (!activeSessions.has(sessionInfo.id)) return { ok: false }; 117 try { 118 activeSessions.delete(sessionInfo.id); 119 } catch { 120 return { ok: false }; 121 } 122 return { ok: true }; 123};