Sifa professional network API (Fastify, AT Protocol, Jetstream)
sifa.id/
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}