decentralised sync engine
at main 3.2 kB view raw
1import type { WebSocket } from "ws"; 2import * as crypto from "node:crypto"; 3import { SESSIONS_SECRET } from "@/lib/utils/crypto"; 4import type { Result } from "@/lib/utils/result"; 5import type { AtUri, Did } from "@/lib/types/atproto"; 6import { SERVER_PORT, SERVICE_DID } from "@/lib/env"; 7import type { LatticeSessionInfo } from "@/lib/types/handshake"; 8 9export const generateSessionId = () => { 10 return crypto.randomUUID(); 11}; 12 13export const generateLatticeSessionInfo = ( 14 sessionId: string, 15 allowedChannels: Array<AtUri>, 16 clientDid: Did, 17): LatticeSessionInfo => { 18 const token = crypto.randomBytes(32).toString("base64url"); 19 20 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET); 21 hmac.update(`${token}:${sessionId}`); 22 const fingerprint = hmac.digest("hex"); 23 24 const latticeDid: Did = SERVICE_DID.includes("localhost") 25 ? `${SERVICE_DID}%3A${SERVER_PORT.toString()}` 26 : SERVICE_DID; 27 28 return { 29 id: sessionId, 30 token, 31 fingerprint, 32 allowedChannels, 33 latticeDid, 34 clientDid, 35 }; 36}; 37 38export const verifyLatticeToken = ({ 39 token, 40 fingerprint, 41 id: sessionId, 42}: LatticeSessionInfo) => { 43 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET); 44 hmac.update(`${token}:${sessionId}`); 45 const expectedFingerprint = hmac.digest("hex"); 46 47 try { 48 return crypto.timingSafeEqual( 49 Buffer.from(fingerprint, "hex"), 50 Buffer.from(expectedFingerprint, "hex"), 51 ); 52 } catch { 53 return false; 54 } 55}; 56 57// map of session tokens to session info objects 58export const issuedLatticeTokens = new Map<string, LatticeSessionInfo>(); 59 60export const issueNewLatticeToken = ({ 61 allowedChannels, 62 clientDid, 63}: { 64 allowedChannels: Array<AtUri | undefined>; 65 clientDid: Did; 66}) => { 67 const filteredChannels = allowedChannels.filter( 68 (channels) => channels !== undefined, 69 ); 70 const sessionId = generateSessionId(); 71 const sessionInfo = generateLatticeSessionInfo( 72 sessionId, 73 filteredChannels, 74 clientDid, 75 ); 76 console.log("Issuing new handshake token with session info", sessionInfo); 77 issuedLatticeTokens.set(sessionInfo.token, sessionInfo); 78 return sessionInfo; 79}; 80 81export const clientSessions = new Map<LatticeSessionInfo, WebSocket>(); 82 83export const isValidSession = (sessionInfo: LatticeSessionInfo) => { 84 return ( 85 issuedLatticeTokens.has(sessionInfo.token) && 86 verifyLatticeToken(sessionInfo) 87 ); 88}; 89 90export const createNewSession = ({ 91 sessionInfo, 92 socket, 93}: { 94 sessionInfo: LatticeSessionInfo; 95 socket: WebSocket; 96}): Result<{ sessionSocket: WebSocket }, undefined> => { 97 try { 98 issuedLatticeTokens.delete(sessionInfo.token); 99 } catch { 100 return { ok: false }; 101 } 102 clientSessions.set(sessionInfo, socket); 103 return { ok: true, data: { sessionSocket: socket } }; 104}; 105 106export const deleteSession = ( 107 sessionInfo: LatticeSessionInfo, 108): Result<undefined, undefined> => { 109 if (!clientSessions.has(sessionInfo)) return { ok: false }; 110 try { 111 clientSessions.delete(sessionInfo); 112 } catch { 113 return { ok: false }; 114 } 115 return { ok: true }; 116};