import Surreal from 'surrealdb'; const SURREALDB_URL = process.env.SURREALDB_URL; const SURREALDB_NAMESPACE = process.env.SURREALDB_NS; const SURREALDB_DATABASE = process.env.SURREALDB_DB; const SURREALDB_USER = process.env.SURREALDB_USER; const SURREALDB_PASS = process.env.SURREALDB_PASS; if (!SURREALDB_URL || !SURREALDB_NAMESPACE || !SURREALDB_DATABASE || !SURREALDB_USER || !SURREALDB_PASS) { throw new Error('SurrealDB configuration is missing. Please set SURREALDB_URL, SURREALDB_NS, SURREALDB_DB, SURREALDB_USER, and SURREALDB_PASS in .env'); } interface OAuthState { state: string; code_verifier: string; pds_url: string; created_at: string; } /** * Gets a SurrealDB connection for OAuth state management. * This uses a separate, unauthenticated connection since OAuth happens before user auth. */ async function getDb() { const db = new Surreal(); await db.connect(SURREALDB_URL); // Sign in with root credentials for OAuth state management await db.signin({ username: SURREALDB_USER, password: SURREALDB_PASS, }); await db.use({ namespace: SURREALDB_NAMESPACE, database: SURREALDB_DATABASE }); return db; } /** * Stores OAuth state parameters in SurrealDB for the duration of the OAuth flow. * The state is used as the record ID for easy lookup. * * @param state - The OAuth state parameter (CSRF token) * @param codeVerifier - The PKCE code verifier * @param pdsUrl - The user's PDS URL */ export async function storeOAuthState( state: string, codeVerifier: string, pdsUrl: string ): Promise { const db = await getDb(); try { // Store with a 10 minute TTL (OAuth flows should complete quickly) const created_at = new Date().toISOString(); console.log('[OAuth State] Storing state:', state); // Use CREATE with CONTENT to store all fields const result = await db.create(`oauth_state:⟨${state}⟩`, { state, code_verifier: codeVerifier, pds_url: pdsUrl, created_at, }); console.log('[OAuth State] Stored successfully:', JSON.stringify(result, null, 2)); } finally { await db.close(); } } /** * Retrieves and deletes OAuth state from SurrealDB. * This ensures one-time use of the state parameter (security best practice). * * @param state - The OAuth state parameter to look up * @returns The OAuth state data, or null if not found or expired */ export async function consumeOAuthState( state: string ): Promise<{ codeVerifier: string; pdsUrl: string } | null> { const db = await getDb(); try { console.log('[OAuth State] Retrieving state:', state); // Retrieve the state by record ID const selectResult = await db.select(`oauth_state:⟨${state}⟩`); console.log('[OAuth State] Select result:', JSON.stringify(selectResult, null, 2)); // db.select() returns an array when selecting a specific record ID const result = Array.isArray(selectResult) ? selectResult[0] : selectResult; console.log('[OAuth State] Retrieved record:', JSON.stringify(result, null, 2)); if (!result) { console.log('[OAuth State] No result found for state:', state); return null; } // Check if expired (older than 10 minutes) const createdAt = new Date(result.created_at); const now = new Date(); const ageMinutes = (now.getTime() - createdAt.getTime()) / 1000 / 60; console.log('[OAuth State] State age:', ageMinutes, 'minutes'); if (ageMinutes > 10) { console.log('[OAuth State] State expired, deleting'); // Delete expired state await db.delete(`oauth_state:⟨${state}⟩`); return null; } // Delete the state (one-time use) console.log('[OAuth State] Deleting state (one-time use)'); await db.delete(`oauth_state:⟨${state}⟩`); return { codeVerifier: result.code_verifier, pdsUrl: result.pds_url, }; } finally { await db.close(); } } /** * Cleans up expired OAuth states (should be called periodically). * In production, this would be a cron job or scheduled task. */ export async function cleanupExpiredOAuthStates(): Promise { const db = await getDb(); try { const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString(); const result = await db.query<[OAuthState[]]>( 'DELETE FROM oauth_state WHERE created_at < $cutoff RETURN BEFORE', { cutoff: tenMinutesAgo } ); return result[0]?.length || 0; } finally { await db.close(); } }