an atproto based link aggregator
1import type {
2 NodeSavedSession,
3 NodeSavedSessionStore,
4 NodeSavedState,
5 NodeSavedStateStore
6} from '@atproto/oauth-client-node';
7import { eq, lt } from 'drizzle-orm';
8import type { Database } from '../db';
9import { authState, authSession } from '../db/schema';
10
11/**
12 * Database-backed state store for OAuth flow.
13 * States are short-lived (15 min) and used during the authorization flow.
14 */
15export class StateStore implements NodeSavedStateStore {
16 constructor(private db: Database) {}
17
18 async get(key: string): Promise<NodeSavedState | undefined> {
19 const result = await this.db.select().from(authState).where(eq(authState.key, key)).limit(1);
20
21 if (result.length === 0) return undefined;
22 return JSON.parse(result[0].state) as NodeSavedState;
23 }
24
25 async set(key: string, val: NodeSavedState): Promise<void> {
26 const state = JSON.stringify(val);
27 const now = new Date().toISOString();
28
29 await this.db.insert(authState).values({ key, state, createdAt: now }).onConflictDoUpdate({
30 target: authState.key,
31 set: { state }
32 });
33
34 // Clean up old states (older than 15 minutes)
35 const fifteenMinutesAgo = new Date(Date.now() - 15 * 60 * 1000).toISOString();
36 await this.db.delete(authState).where(lt(authState.createdAt, fifteenMinutesAgo));
37 }
38
39 async del(key: string): Promise<void> {
40 await this.db.delete(authState).where(eq(authState.key, key));
41 }
42}
43
44/**
45 * Database-backed session store for OAuth sessions.
46 * Sessions are long-lived and store the authenticated user's tokens.
47 */
48export class SessionStore implements NodeSavedSessionStore {
49 constructor(private db: Database) {}
50
51 async get(key: string): Promise<NodeSavedSession | undefined> {
52 const result = await this.db
53 .select()
54 .from(authSession)
55 .where(eq(authSession.key, key))
56 .limit(1);
57
58 if (result.length === 0) return undefined;
59 return JSON.parse(result[0].session) as NodeSavedSession;
60 }
61
62 async set(key: string, val: NodeSavedSession): Promise<void> {
63 const session = JSON.stringify(val);
64 const now = new Date().toISOString();
65
66 await this.db
67 .insert(authSession)
68 .values({ key, session, createdAt: now, updatedAt: now })
69 .onConflictDoUpdate({
70 target: authSession.key,
71 set: { session, updatedAt: now }
72 });
73 }
74
75 async del(key: string): Promise<void> {
76 await this.db.delete(authSession).where(eq(authSession.key, key));
77 }
78}