forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1import type { OAuthStorage, OAuthTokens } from "../types.ts";
2import type { InStatement, TransactionMode } from "@libsql/client";
3
4// Val Town's SQLite ResultSet (doesn't have toJSON method)
5interface ValTownResultSet {
6 columns: string[];
7 columnTypes: string[];
8 rows: unknown[][];
9 rowsAffected: number;
10 lastInsertRowid?: bigint;
11}
12
13interface SQLiteInstance {
14 execute(statement: InStatement): Promise<ValTownResultSet>;
15 batch(
16 statements: InStatement[],
17 mode?: TransactionMode,
18 ): Promise<ValTownResultSet[]>;
19}
20
21export class ValTownSQLiteOAuthStorage implements OAuthStorage {
22 private sqlite: SQLiteInstance;
23
24 constructor(sqlite: SQLiteInstance) {
25 this.sqlite = sqlite;
26 this.initTables();
27 }
28
29 private async initTables(): Promise<void> {
30 // Create tokens table
31 await this.sqlite.execute(`
32 CREATE TABLE IF NOT EXISTS oauth_tokens (
33 id INTEGER PRIMARY KEY,
34 session_id TEXT,
35 access_token TEXT NOT NULL,
36 token_type TEXT NOT NULL,
37 expires_at INTEGER,
38 refresh_token TEXT,
39 scope TEXT,
40 created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
41 UNIQUE(session_id)
42 )
43 `);
44
45 // Create states table with automatic cleanup
46 await this.sqlite.execute(`
47 CREATE TABLE IF NOT EXISTS oauth_states (
48 state TEXT PRIMARY KEY,
49 code_verifier TEXT NOT NULL,
50 timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
51 )
52 `);
53
54 // Create index for cleanup efficiency
55 await this.sqlite.execute(`
56 CREATE INDEX IF NOT EXISTS idx_oauth_states_timestamp ON oauth_states(timestamp)
57 `);
58 }
59
60 async getTokens(sessionId: string): Promise<OAuthTokens | null> {
61 const result = await this.sqlite.execute({
62 sql: `SELECT access_token, token_type, expires_at, refresh_token, scope
63 FROM oauth_tokens
64 WHERE session_id = ?
65 LIMIT 1`,
66 args: [sessionId],
67 });
68
69 if (result.rows.length === 0) return null;
70
71 const row = result.rows[0];
72
73 return {
74 accessToken: row[0] as string,
75 tokenType: row[1] as string,
76 expiresAt: (row[2] as number | null) ?? undefined,
77 refreshToken: (row[3] as string | null) ?? undefined,
78 scope: (row[4] as string | null) ?? undefined,
79 };
80 }
81
82 async setTokens(tokens: OAuthTokens, sessionId: string): Promise<void> {
83 await this.clearTokens(sessionId);
84
85 await this.sqlite.execute({
86 sql:
87 `INSERT INTO oauth_tokens (session_id, access_token, token_type, expires_at, refresh_token, scope)
88 VALUES (?, ?, ?, ?, ?, ?)`,
89 args: [
90 sessionId,
91 tokens.accessToken,
92 tokens.tokenType,
93 tokens.expiresAt ?? null,
94 tokens.refreshToken ?? null,
95 tokens.scope ?? null,
96 ],
97 });
98 }
99
100 async clearTokens(sessionId: string): Promise<void> {
101 await this.sqlite.execute({
102 sql: "DELETE FROM oauth_tokens WHERE session_id = ?",
103 args: [sessionId],
104 });
105 }
106
107 async getState(state: string): Promise<string | null> {
108 const result = await this.sqlite.execute({
109 sql: "SELECT code_verifier FROM oauth_states WHERE state = ?",
110 args: [state],
111 });
112
113 if (result.rows.length === 0) return null;
114
115 const codeVerifier = result.rows[0][0] as string;
116
117 // Delete after use (one-time use)
118 await this.clearState(state);
119
120 return codeVerifier;
121 }
122
123 async setState(state: string, codeVerifier: string): Promise<void> {
124 await this.sqlite.execute({
125 sql:
126 `INSERT OR REPLACE INTO oauth_states (state, code_verifier, timestamp)
127 VALUES (?, ?, ?)`,
128 args: [state, codeVerifier, Date.now()],
129 });
130
131 // Auto-cleanup expired states
132 await this.cleanup();
133 }
134
135 async clearState(state: string): Promise<void> {
136 await this.sqlite.execute({
137 sql: "DELETE FROM oauth_states WHERE state = ?",
138 args: [state],
139 });
140 }
141
142 private async cleanup(): Promise<void> {
143 const cutoff = Date.now() - (10 * 60 * 1000); // 10 minutes ago
144 await this.sqlite.execute({
145 sql: "DELETE FROM oauth_states WHERE timestamp < ?",
146 args: [cutoff],
147 });
148 }
149}