Sifa professional network API (Fastify, AT Protocol, Jetstream) sifa.id/
at main 64 lines 2.5 kB view raw
1import type { NodeSavedSession, NodeSavedSessionStore } from '@atproto/oauth-client-node'; 2import type { Database } from '../db/index.js'; 3import { oauthSessions } from '../db/schema/index.js'; 4import { eq } from 'drizzle-orm'; 5 6/** 7 * Database-backed session store for ATproto OAuth. 8 * Implements the SimpleStore<string, NodeSavedSession> interface required by NodeOAuthClient. 9 * 10 * Sessions are persisted in the oauth_sessions PostgreSQL table so they survive 11 * server restarts. The token_set column stores the full NodeSavedSession value 12 * as JSONB. 13 */ 14export class DbSessionStore implements NodeSavedSessionStore { 15 constructor(private db: Database) {} 16 17 async get(key: string): Promise<NodeSavedSession | undefined> { 18 const rows = await this.db 19 .select() 20 .from(oauthSessions) 21 .where(eq(oauthSessions.sessionId, key)) 22 .limit(1); 23 24 if (rows.length === 0) return undefined; 25 const row = rows[0]; 26 if (!row) return undefined; 27 return row.tokenSet as unknown as NodeSavedSession; 28 } 29 30 async set(key: string, val: NodeSavedSession): Promise<void> { 31 // NodeSavedSession is ToDpopJwkValue<Session>, which includes dpopJwk as a 32 // serialisable JWK object. We store the entire value in token_set JSONB and 33 // extract metadata fields for queryability. Casts are needed because the 34 // exact shape of NodeSavedSession varies across @atproto/oauth-client-node 35 // versions and the Drizzle jsonb column type is `unknown`. 36 const session = val as Record<string, unknown>; 37 38 await this.db 39 .insert(oauthSessions) 40 .values({ 41 sessionId: key, 42 did: (session.did as string) ?? '', 43 handle: (session.handle as string) ?? '', 44 pdsUrl: (session.pdsUrl as string) ?? '', 45 tokenSet: val as unknown as Record<string, unknown>, 46 dpopKey: (session.dpopJwk as Record<string, unknown>) ?? {}, 47 expiresAt: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000), // 180 days 48 }) 49 .onConflictDoUpdate({ 50 target: oauthSessions.sessionId, 51 set: { 52 tokenSet: val as unknown as Record<string, unknown>, 53 did: (session.did as string) ?? '', 54 handle: (session.handle as string) ?? '', 55 pdsUrl: (session.pdsUrl as string) ?? '', 56 dpopKey: (session.dpopJwk as Record<string, unknown>) ?? {}, 57 }, 58 }); 59 } 60 61 async del(key: string): Promise<void> { 62 await this.db.delete(oauthSessions).where(eq(oauthSessions.sessionId, key)); 63 } 64}