import type { SessionAdapter, SessionData, SessionOptions, SessionUser } from "./types.ts"; import { parseCookie, serializeCookie } from "./cookie.ts"; const DEFAULT_SESSION_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days const DEFAULT_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour const DEFAULT_COOKIE_NAME = "slice-session"; export class SessionStore { private adapter: SessionAdapter; private options: Required; private cleanupTimer?: number; constructor(options: SessionOptions) { this.adapter = options.adapter; this.options = { adapter: options.adapter, cookieName: options.cookieName ?? DEFAULT_COOKIE_NAME, cookieOptions: { httpOnly: true, secure: true, sameSite: "lax", path: "/", ...options.cookieOptions, }, sessionTTL: options.sessionTTL ?? DEFAULT_SESSION_TTL, cleanupInterval: options.cleanupInterval ?? DEFAULT_CLEANUP_INTERVAL, generateId: options.generateId ?? (() => crypto.randomUUID()), }; // Start cleanup timer this.startCleanupTimer(); } private startCleanupTimer() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(() => { this.cleanup().catch(console.error); }, this.options.cleanupInterval); } async createSession(userId: string, handle?: string, data?: Record): Promise { const sessionId = this.options.generateId(); const now = Date.now(); const sessionData: SessionData = { sessionId, userId, handle, isAuthenticated: true, data, createdAt: now, expiresAt: now + this.options.sessionTTL, lastAccessedAt: now, }; await this.adapter.set(sessionId, sessionData); return sessionId; } async getSession(sessionId: string): Promise { if (!sessionId) return null; const session = await this.adapter.get(sessionId); if (!session) return null; // Check if session is expired if (session.expiresAt < Date.now()) { await this.adapter.delete(sessionId); return null; } // Update last accessed time await this.adapter.update(sessionId, { lastAccessedAt: Date.now(), }); return session; } async updateSession(sessionId: string, updates: Partial): Promise { const session = await this.getSession(sessionId); if (!session) return false; // Extend expiration on update const extendedExpiration = Date.now() + this.options.sessionTTL; return await this.adapter.update(sessionId, { ...updates, expiresAt: extendedExpiration, lastAccessedAt: Date.now(), }); } async deleteSession(sessionId: string): Promise { await this.adapter.delete(sessionId); } async cleanup(): Promise { return await this.adapter.cleanup(Date.now()); } // Get session from request cookies async getSessionFromRequest(request: Request): Promise { const cookieHeader = request.headers.get("cookie"); if (!cookieHeader) return null; const cookies = parseCookie(cookieHeader); const sessionId = cookies[this.options.cookieName]; if (!sessionId) return null; return await this.getSession(sessionId); } // Get user info from session async getCurrentUser(request: Request): Promise { const session = await this.getSessionFromRequest(request); if (!session) { return { isAuthenticated: false, }; } return { sessionId: session.sessionId, sub: session.userId, handle: session.handle, isAuthenticated: session.isAuthenticated, ...session.data, }; } // Create session cookie header createSessionCookie(sessionId: string): string { return serializeCookie(this.options.cookieName, sessionId, { ...this.options.cookieOptions, maxAge: Math.floor(this.options.sessionTTL / 1000), // Convert to seconds }); } // Create logout cookie header (clears the session) createLogoutCookie(): string { return serializeCookie(this.options.cookieName, "", { ...this.options.cookieOptions, maxAge: 0, }); } // Cleanup on destroy destroy() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } } }