forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1import type { SessionAdapter, SessionData, SessionOptions, SessionUser } from "./types.ts";
2import { parseCookie, serializeCookie } from "./cookie.ts";
3
4const DEFAULT_SESSION_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days
5const DEFAULT_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour
6const DEFAULT_COOKIE_NAME = "slice-session";
7
8export class SessionStore {
9 private adapter: SessionAdapter;
10 private options: Required<SessionOptions>;
11 private cleanupTimer?: number;
12
13 constructor(options: SessionOptions) {
14 this.adapter = options.adapter;
15 this.options = {
16 adapter: options.adapter,
17 cookieName: options.cookieName ?? DEFAULT_COOKIE_NAME,
18 cookieOptions: {
19 httpOnly: true,
20 secure: true,
21 sameSite: "lax",
22 path: "/",
23 ...options.cookieOptions,
24 },
25 sessionTTL: options.sessionTTL ?? DEFAULT_SESSION_TTL,
26 cleanupInterval: options.cleanupInterval ?? DEFAULT_CLEANUP_INTERVAL,
27 generateId: options.generateId ?? (() => crypto.randomUUID()),
28 };
29
30 // Start cleanup timer
31 this.startCleanupTimer();
32 }
33
34 private startCleanupTimer() {
35 if (this.cleanupTimer) {
36 clearInterval(this.cleanupTimer);
37 }
38
39 this.cleanupTimer = setInterval(() => {
40 this.cleanup().catch(console.error);
41 }, this.options.cleanupInterval);
42 }
43
44 async createSession(userId: string, handle?: string, data?: Record<string, unknown>): Promise<string> {
45 const sessionId = this.options.generateId();
46 const now = Date.now();
47
48 const sessionData: SessionData = {
49 sessionId,
50 userId,
51 handle,
52 isAuthenticated: true,
53 data,
54 createdAt: now,
55 expiresAt: now + this.options.sessionTTL,
56 lastAccessedAt: now,
57 };
58
59 await this.adapter.set(sessionId, sessionData);
60 return sessionId;
61 }
62
63 async getSession(sessionId: string): Promise<SessionData | null> {
64 if (!sessionId) return null;
65
66 const session = await this.adapter.get(sessionId);
67 if (!session) return null;
68
69 // Check if session is expired
70 if (session.expiresAt < Date.now()) {
71 await this.adapter.delete(sessionId);
72 return null;
73 }
74
75 // Update last accessed time
76 await this.adapter.update(sessionId, {
77 lastAccessedAt: Date.now(),
78 });
79
80 return session;
81 }
82
83 async updateSession(sessionId: string, updates: Partial<SessionData>): Promise<boolean> {
84 const session = await this.getSession(sessionId);
85 if (!session) return false;
86
87 // Extend expiration on update
88 const extendedExpiration = Date.now() + this.options.sessionTTL;
89
90 return await this.adapter.update(sessionId, {
91 ...updates,
92 expiresAt: extendedExpiration,
93 lastAccessedAt: Date.now(),
94 });
95 }
96
97 async deleteSession(sessionId: string): Promise<void> {
98 await this.adapter.delete(sessionId);
99 }
100
101 async cleanup(): Promise<number> {
102 return await this.adapter.cleanup(Date.now());
103 }
104
105 // Get session from request cookies
106 async getSessionFromRequest(request: Request): Promise<SessionData | null> {
107 const cookieHeader = request.headers.get("cookie");
108 if (!cookieHeader) return null;
109
110 const cookies = parseCookie(cookieHeader);
111 const sessionId = cookies[this.options.cookieName];
112 if (!sessionId) return null;
113
114 return await this.getSession(sessionId);
115 }
116
117 // Get user info from session
118 async getCurrentUser(request: Request): Promise<SessionUser> {
119 const session = await this.getSessionFromRequest(request);
120
121 if (!session) {
122 return {
123 isAuthenticated: false,
124 };
125 }
126
127 return {
128 sessionId: session.sessionId,
129 sub: session.userId,
130 handle: session.handle,
131 isAuthenticated: session.isAuthenticated,
132 ...session.data,
133 };
134 }
135
136 // Create session cookie header
137 createSessionCookie(sessionId: string): string {
138 return serializeCookie(this.options.cookieName, sessionId, {
139 ...this.options.cookieOptions,
140 maxAge: Math.floor(this.options.sessionTTL / 1000), // Convert to seconds
141 });
142 }
143
144 // Create logout cookie header (clears the session)
145 createLogoutCookie(): string {
146 return serializeCookie(this.options.cookieName, "", {
147 ...this.options.cookieOptions,
148 maxAge: 0,
149 });
150 }
151
152 // Cleanup on destroy
153 destroy() {
154 if (this.cleanupTimer) {
155 clearInterval(this.cleanupTimer);
156 this.cleanupTimer = undefined;
157 }
158 }
159}