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
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};