social components
inlay.at
atproto
components
sdui
1// auth/storage.ts
2import "server-only";
3import type {
4 NodeSavedSession,
5 NodeSavedSessionStore,
6 NodeSavedState,
7 NodeSavedStateStore,
8 RuntimeLock,
9} from "@atproto/oauth-client-node";
10import pg from "pg";
11
12const { Pool } = pg;
13
14let pool: pg.Pool | null = null;
15
16function getPool(): pg.Pool {
17 if (!pool) {
18 const connectionString = process.env.DATABASE_URL;
19 if (!connectionString) {
20 throw new Error("DATABASE_URL is required for auth storage");
21 }
22 pool = new Pool({ connectionString });
23 }
24 return pool;
25}
26
27function hashStringToInt(str: string): number {
28 let hash = 0;
29 for (let i = 0; i < str.length; i++) {
30 const char = str.charCodeAt(i);
31 hash = (hash << 5) - hash + char;
32 hash = hash & hash;
33 }
34 return hash;
35}
36
37export const requestLock: RuntimeLock = async (key, fn) => {
38 const db = getPool();
39 const lockId = hashStringToInt(key);
40
41 const client = await db.connect();
42 try {
43 await client.query("SELECT pg_advisory_lock($1)", [lockId]);
44 try {
45 return await fn();
46 } finally {
47 await client.query("SELECT pg_advisory_unlock($1)", [lockId]);
48 }
49 } finally {
50 client.release();
51 }
52};
53
54export async function initAuthTables(): Promise<void> {
55 const db = getPool();
56
57 await db.query(`
58 CREATE TABLE IF NOT EXISTS auth_state (
59 key TEXT PRIMARY KEY,
60 state TEXT NOT NULL,
61 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
62 );
63
64 CREATE TABLE IF NOT EXISTS auth_session (
65 key TEXT PRIMARY KEY,
66 session TEXT NOT NULL,
67 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
68 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
69 );
70
71 CREATE INDEX IF NOT EXISTS idx_auth_state_created ON auth_state(created_at);
72 `);
73}
74
75export class StateStore implements NodeSavedStateStore {
76 async get(key: string): Promise<NodeSavedState | undefined> {
77 const db = getPool();
78 const result = await db.query<{ state: string }>(
79 "SELECT state FROM auth_state WHERE key = $1",
80 [key]
81 );
82
83 if (result.rows.length === 0) return undefined;
84 return JSON.parse(result.rows[0].state) as NodeSavedState;
85 }
86
87 async set(key: string, val: NodeSavedState): Promise<void> {
88 const db = getPool();
89 const state = JSON.stringify(val);
90
91 await db.query(
92 `INSERT INTO auth_state (key, state)
93 VALUES ($1, $2)
94 ON CONFLICT (key) DO UPDATE SET state = EXCLUDED.state`,
95 [key, state]
96 );
97
98 // Clean up old states
99 await db.query(
100 "DELETE FROM auth_state WHERE created_at < NOW() - INTERVAL '15 minutes'"
101 );
102 }
103
104 async del(key: string): Promise<void> {
105 const db = getPool();
106 await db.query("DELETE FROM auth_state WHERE key = $1", [key]);
107 }
108}
109
110export class SessionStore implements NodeSavedSessionStore {
111 async get(key: string): Promise<NodeSavedSession | undefined> {
112 const db = getPool();
113 const result = await db.query<{ session: string }>(
114 "SELECT session FROM auth_session WHERE key = $1",
115 [key]
116 );
117
118 if (result.rows.length === 0) {
119 console.log(`[auth:session] not found: ${key}`);
120 return undefined;
121 }
122 return JSON.parse(result.rows[0].session) as NodeSavedSession;
123 }
124
125 async set(key: string, val: NodeSavedSession): Promise<void> {
126 const db = getPool();
127 const session = JSON.stringify(val);
128
129 const existing = await db.query(
130 "SELECT 1 FROM auth_session WHERE key = $1",
131 [key]
132 );
133 const isNew = existing.rows.length === 0;
134
135 await db.query(
136 `INSERT INTO auth_session (key, session, updated_at)
137 VALUES ($1, $2, NOW())
138 ON CONFLICT (key) DO UPDATE SET
139 session = EXCLUDED.session,
140 updated_at = NOW()`,
141 [key, session]
142 );
143
144 console.log(
145 `[auth:session] SET ${key} -> ${isNew ? "created" : "updated"}`
146 );
147 }
148
149 async del(key: string): Promise<void> {
150 const db = getPool();
151 const result = await db.query(
152 "DELETE FROM auth_session WHERE key = $1 RETURNING key",
153 [key]
154 );
155 const deleted = result.rowCount && result.rowCount > 0;
156 console.log(
157 `[auth:session] DEL ${key} -> ${deleted ? "deleted" : "not found"}`
158 );
159 }
160}