Highly ambitious ATProtocol AppView service and sdks
at fix-postgres 149 lines 4.2 kB view raw
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}