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";
6import type { AtUri, Did } from "@/lib/types/atproto";
7import { atUriSchema, didSchema } from "@/lib/types/atproto";
8import { SERVER_PORT, SERVICE_DID } from "@/lib/env";
9
10export const sessionInfoSchema = z.object({
11 id: z.string(),
12 token: z.string(),
13 fingerprint: z.string(),
14 allowedChannels: z.array(atUriSchema),
15 shardDid: didSchema,
16 latticeDid: didSchema,
17});
18export type SessionInfo = z.infer<typeof sessionInfoSchema>;
19
20export const generateSessionId = () => {
21 return crypto.randomUUID();
22};
23
24export const generateSessionInfo = (
25 sessionId: string,
26 allowedChannels: Array<AtUri>,
27 latticeDid: Did,
28): SessionInfo => {
29 const token = crypto.randomBytes(32).toString("base64url");
30
31 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET);
32 hmac.update(`${token}:${sessionId}`);
33 const fingerprint = hmac.digest("hex");
34
35 const shardDid: Did = SERVICE_DID.includes("localhost")
36 ? `${SERVICE_DID}%3A${SERVER_PORT.toString()}`
37 : SERVICE_DID;
38
39 return {
40 id: sessionId,
41 token,
42 fingerprint,
43 allowedChannels,
44 latticeDid,
45 shardDid,
46 };
47};
48
49export const verifyHandshakeToken = ({
50 token,
51 fingerprint,
52 id: sessionId,
53}: SessionInfo) => {
54 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET);
55 hmac.update(`${token}:${sessionId}`);
56 const expectedFingerprint = hmac.digest("hex");
57
58 try {
59 return crypto.timingSafeEqual(
60 Buffer.from(fingerprint, "hex"),
61 Buffer.from(expectedFingerprint, "hex"),
62 );
63 } catch {
64 return false;
65 }
66};
67
68export const issuedHandshakes = new Map<string, SessionInfo>();
69
70export const issueNewHandshakeToken = ({
71 allowedChannels,
72 latticeDid,
73}: {
74 allowedChannels: Array<AtUri | undefined>;
75 latticeDid: Did;
76}) => {
77 const filteredChannels = allowedChannels.filter(
78 (channels) => channels !== undefined,
79 );
80 const sessionId = generateSessionId();
81 const sessionInfo = generateSessionInfo(
82 sessionId,
83 filteredChannels,
84 latticeDid,
85 );
86 console.log("Issuing new handshake token with session info", sessionInfo);
87 issuedHandshakes.set(sessionInfo.token, sessionInfo);
88 return sessionInfo;
89};
90
91export const activeSessions = new Map<string, WebSocket>();
92
93export const isValidSession = (sessionInfo: SessionInfo) => {
94 return (
95 issuedHandshakes.has(sessionInfo.token) &&
96 verifyHandshakeToken(sessionInfo)
97 );
98};
99
100export const createNewSession = ({
101 sessionInfo,
102 socket,
103}: {
104 sessionInfo: SessionInfo;
105 socket: WebSocket;
106}): Result<{ sessionSocket: WebSocket }, undefined> => {
107 try {
108 issuedHandshakes.delete(sessionInfo.token);
109 } catch {
110 return { ok: false };
111 }
112 activeSessions.set(sessionInfo.id, socket);
113 return { ok: true, data: { sessionSocket: socket } };
114};
115
116export const deleteSession = (
117 sessionInfo: SessionInfo,
118): Result<undefined, undefined> => {
119 if (!activeSessions.has(sessionInfo.id)) return { ok: false };
120 try {
121 activeSessions.delete(sessionInfo.id);
122 } catch {
123 return { ok: false };
124 }
125 return { ok: true };
126};