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 { 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 = SERVICE_DID;
36
37 return {
38 id: sessionId,
39 token,
40 fingerprint,
41 allowedChannels,
42 latticeDid,
43 shardDid,
44 };
45};
46
47export const verifyHandshakeToken = ({
48 token,
49 fingerprint,
50 id: sessionId,
51}: SessionInfo) => {
52 const hmac = crypto.createHmac("sha256", SESSIONS_SECRET);
53 hmac.update(`${token}:${sessionId}`);
54 const expectedFingerprint = hmac.digest("hex");
55
56 try {
57 return crypto.timingSafeEqual(
58 Buffer.from(fingerprint, "hex"),
59 Buffer.from(expectedFingerprint, "hex"),
60 );
61 } catch {
62 return false;
63 }
64};
65
66export const issuedHandshakes = new Map<string, SessionInfo>();
67
68export const issueNewHandshakeToken = ({
69 allowedChannels,
70 latticeDid,
71}: {
72 allowedChannels: Array<AtUri | undefined>;
73 latticeDid: Did;
74}) => {
75 const filteredChannels = allowedChannels.filter(
76 (channels) => channels !== undefined,
77 );
78 const sessionId = generateSessionId();
79 const sessionInfo = generateSessionInfo(
80 sessionId,
81 filteredChannels,
82 latticeDid,
83 );
84 issuedHandshakes.set(sessionInfo.token, sessionInfo);
85 return sessionInfo;
86};
87
88export const activeSessions = new Map<string, WebSocket>();
89
90export const isValidSession = (sessionInfo: SessionInfo) => {
91 return (
92 issuedHandshakes.has(sessionInfo.token) &&
93 verifyHandshakeToken(sessionInfo)
94 );
95};
96
97export const createNewSession = ({
98 sessionInfo,
99 socket,
100}: {
101 sessionInfo: SessionInfo;
102 socket: WebSocket;
103}): Result<{ sessionSocket: WebSocket }, undefined> => {
104 try {
105 issuedHandshakes.delete(sessionInfo.token);
106 } catch {
107 return { ok: false };
108 }
109 activeSessions.set(sessionInfo.id, socket);
110 return { ok: true, data: { sessionSocket: socket } };
111};
112
113export const deleteSession = (
114 sessionInfo: SessionInfo,
115): Result<undefined, undefined> => {
116 if (!activeSessions.has(sessionInfo.id)) return { ok: false };
117 try {
118 activeSessions.delete(sessionInfo.id);
119 } catch {
120 return { ok: false };
121 }
122 return { ok: true };
123};