Something went wrong. Try again.
A Takuzu game native to the AT Protocol. atkuzu.tomplanche.com
binairo atproto game takuzu
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164// A cookie session store based on https://lucia-auth.com/ examples which is recommended from Svelte's docs// Creates a cookie that links to a session store inside the database allowing the atproto oauth session to be loaded
import { db } from "./db";import { atpOAuthClient } from "./atproto/client";import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding";import { sha256 } from "@oslojs/crypto/sha2";import { DAY } from "@atproto/common";import { sessionStore } from "$lib/server/db/schema";import type { RequestEvent } from "@sveltejs/kit";import { eq } from "drizzle-orm";import { Agent } from "@atproto/api";import type { NodeOAuthClient } from "@atproto/oauth-client-node";import { logger } from "$lib/server/logger";
export class SessionRestorationError extends Error { constructor(message: string) { super(message); this.name = "SessionRestorationError"; }}
// This is a sliding expiration for the cookie session. Can change it if you want it to be less or more.// The actual atproto session goes for a while if it's a confidential client as long as it's refreshed// https://atproto.com/specs/oauth#tokens-and-session-lifetimeconst DEFAULT_EXPIRY = 30 * DAY;
const NULL_SESSION_RESPONSE = { atpAgent: null, did: null, handle: null };
export class Session { db: typeof db; atpOAuthClient: NodeOAuthClient;
constructor(database: typeof db, oauthClient: NodeOAuthClient) { this.db = database; this.atpOAuthClient = oauthClient; }
async validateSessionToken(token: string): Promise<SessionValidationResult> { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const result = await this.db .select() .from(sessionStore) .where(eq(sessionStore.id, sessionId)) .limit(1); if (result.length > 1) { throw new Error("Multiple sessions found for token. Should not happen"); } if (result.length === 0) { return NULL_SESSION_RESPONSE; } const session = result[0];
if (Date.now() >= session.expiresAt.getTime()) { await this.invalidateSession(session.id); logger.warn(`Session expired for the did: ${session.did}`); return NULL_SESSION_RESPONSE; } if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { session.expiresAt = new Date(Date.now() + DEFAULT_EXPIRY); await this.db.update(sessionStore).set(session).where(eq(sessionStore.id, sessionId)); } try { const oAuthSession = await this.atpOAuthClient.restore(session.did);
const agent = new Agent(oAuthSession); return { atpAgent: agent, did: session.did, handle: session.handle }; } catch (err) { const errorMessage = (err as Error).message; logger.warn(`Error restoring session for did: ${session.did}, error: ${errorMessage}`); //Counting any error when restoring a session as a failed session resume and deleting the users web browser session //You can go further and capture different types of errors await this.invalidateUserSessions(session.did); throw new SessionRestorationError( `Failed to restore your session: ${errorMessage}. Please log in again.` ); } }
private setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void { event.cookies.set("session", token, { httpOnly: true, path: "/", secure: import.meta.env.PROD, sameSite: "lax", expires: expiresAt }); }
deleteSessionTokenCookie(event: RequestEvent): void { event.cookies.set("session", "", { httpOnly: true, path: "/", secure: import.meta.env.PROD, sameSite: "lax", maxAge: 0 }); }
async invalidateSession(sessionId: string) { await this.db.delete(sessionStore).where(eq(sessionStore.id, sessionId)); }
async invalidateSessionByToken(token: string) { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); await this.invalidateSession(sessionId); }
async invalidateUserSessions(did: string) { await this.db.delete(sessionStore).where(eq(sessionStore.did, did)); }
private async createSession(token: string, did: string, handle: string) { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const expiresAt = new Date(Date.now() + DEFAULT_EXPIRY); const session = { id: sessionId, did, handle, expiresAt, createdAt: new Date() }; await this.db.insert(sessionStore).values(session); return session; }
private generateSessionToken(): string { const tokenBytes = new Uint8Array(20); crypto.getRandomValues(tokenBytes); return encodeBase32LowerCaseNoPadding(tokenBytes); }
async createAndSetSession(event: RequestEvent, did: string, handle: string) { const token = this.generateSessionToken(); const session = await this.createSession(token, did, handle); this.setSessionTokenCookie(event, token, session.expiresAt); return session; }
async getSessionFromRequest(event: RequestEvent): Promise<SessionValidationResult> { const token = event.cookies.get("session"); if (!token) { return NULL_SESSION_RESPONSE; } try { return await this.validateSessionToken(token); } catch (err) { //We delete the cookie on any error and pass along the error this.deleteSessionTokenCookie(event); throw err; } }}
type SessionValidationResult = | { atpAgent: Agent; did: string; handle: string } | { atpAgent: null; did: null; handle: null };
let sessionManager: Promise<Session> | null = null;
export const getSessionManager = async (): Promise<Session> => { if (!sessionManager) { sessionManager = (async () => { const client = await atpOAuthClient(); return new Session(db, client); })(); } return sessionManager;};