decentralised sync engine
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 57export const issuedLatticeTokens = new Map<string, LatticeSessionInfo>(); 58 59export const issueNewLatticeToken = ({ 60 allowedChannels, 61 clientDid, 62}: { 63 allowedChannels: Array<AtUri | undefined>; 64 clientDid: Did; 65}) => { 66 const filteredChannels = allowedChannels.filter( 67 (channels) => channels !== undefined, 68 ); 69 const sessionId = generateSessionId(); 70 const sessionInfo = generateLatticeSessionInfo( 71 sessionId, 72 filteredChannels, 73 clientDid, 74 ); 75 console.log("Issuing new handshake token with session info", sessionInfo); 76 issuedLatticeTokens.set(sessionInfo.token, sessionInfo); 77 return sessionInfo; 78}; 79 80export const activeSessions = new Map<string, WebSocket>(); 81 82export const isValidSession = (sessionInfo: LatticeSessionInfo) => { 83 return ( 84 issuedLatticeTokens.has(sessionInfo.token) && 85 verifyLatticeToken(sessionInfo) 86 ); 87}; 88 89export const createNewSession = ({ 90 sessionInfo, 91 socket, 92}: { 93 sessionInfo: LatticeSessionInfo; 94 socket: WebSocket; 95}): Result<{ sessionSocket: WebSocket }, undefined> => { 96 try { 97 issuedLatticeTokens.delete(sessionInfo.token); 98 } catch { 99 return { ok: false }; 100 } 101 activeSessions.set(sessionInfo.id, socket); 102 return { ok: true, data: { sessionSocket: socket } }; 103}; 104 105export const deleteSession = ( 106 sessionInfo: LatticeSessionInfo, 107): Result<undefined, undefined> => { 108 if (!activeSessions.has(sessionInfo.id)) return { ok: false }; 109 try { 110 activeSessions.delete(sessionInfo.id); 111 } catch { 112 return { ok: false }; 113 } 114 return { ok: true }; 115};