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