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";
6
7export const sessionInfoSchema = z.object({
8 id: z.string(),
9 token: z.string(),
10 fingerprint: z.string(),
11});
12export type SessionInfo = z.infer<typeof sessionInfoSchema>;
13
14export const generateSessionId = () => {
15 return crypto.randomUUID();
16};
17
18export const generateSessionInfo = (sessionId: string): SessionInfo => {
19 const token = crypto.randomBytes(32).toString("base64url");
20
21 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET);
22 hmac.update(`${token}:${sessionId}`);
23 const fingerprint = hmac.digest("hex");
24
25 return { id: sessionId, token, fingerprint };
26};
27
28export const verifyHandshakeToken = ({
29 token,
30 fingerprint,
31 id: sessionId,
32}: SessionInfo) => {
33 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET);
34 hmac.update(`${token}:${sessionId}`);
35 const expectedFingerprint = hmac.digest("hex");
36
37 try {
38 return crypto.timingSafeEqual(
39 Buffer.from(fingerprint, "hex"),
40 Buffer.from(expectedFingerprint, "hex"),
41 );
42 } catch {
43 return false;
44 }
45};
46
47export const issuedHandshakes = new Map<string, SessionInfo>();
48
49export const issueNewHandshakeToken = () => {
50 const sessionId = generateSessionId();
51 const sessionInfo = generateSessionInfo(sessionId);
52 issuedHandshakes.set(sessionInfo.id, sessionInfo);
53 return sessionInfo;
54};
55
56export const activeSessions = new Map<string, WebSocket>();
57
58export const createNewSession = ({
59 sessionInfo,
60 socket,
61}: {
62 sessionInfo: SessionInfo;
63 socket: WebSocket;
64}): Result<undefined, undefined> => {
65 const isValidSession = verifyHandshakeToken(sessionInfo);
66 if (!isValidSession) return { ok: false };
67
68 try {
69 issuedHandshakes.delete(sessionInfo.id);
70 } catch {
71 return { ok: false };
72 }
73 activeSessions.set(sessionInfo.id, socket);
74 return { ok: true };
75};
76
77export const deleteSession = (
78 sessionInfo: SessionInfo,
79): Result<undefined, undefined> => {
80 if (!activeSessions.has(sessionInfo.id)) return { ok: false };
81 try {
82 activeSessions.delete(sessionInfo.id);
83 } catch {
84 return { ok: false };
85 }
86 return { ok: true };
87};