ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

oauth refactor: revoke oauth if logout, eliminate code dup, add error mssgs, session managers

+1 -1
netlify.toml
··· 17 17 [[headers]] 18 18 for = "/.well-known/*" 19 19 [headers.values] 20 - Access-Control-Allow-Origin = "*" 20 + Access-Control-Allow-Origin = "*"
+18 -76
netlify/functions/batch-follow-users.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions"; 2 - import { 3 - NodeOAuthClient, 4 - atprotoLoopbackClientMetadata, 5 - } from "@atproto/oauth-client-node"; 6 - import { JoseKey } from "@atproto/jwk-jose"; 7 - import { stateStore, sessionStore, userSessions } from "./oauth-stores-db"; 8 - import { getOAuthConfig } from "./oauth-config"; 9 - import { Agent } from "@atproto/api"; 2 + import { SessionManager } from "./session-manager"; 10 3 import cookie from "cookie"; 11 - 12 - function normalizePrivateKey(key: string): string { 13 - if (!key.includes("\n") && key.includes("\\n")) { 14 - return key.replace(/\\n/g, "\n"); 15 - } 16 - return key; 17 - } 18 4 19 5 export const handler: Handler = async ( 20 6 event: HandlerEvent, ··· 66 52 }; 67 53 } 68 54 69 - // Get DID from session 70 - const userSession = await userSessions.get(sessionId); 71 - if (!userSession) { 72 - return { 73 - statusCode: 401, 74 - headers: { "Content-Type": "application/json" }, 75 - body: JSON.stringify({ error: "Invalid or expired session" }), 76 - }; 77 - } 78 - 79 - const config = getOAuthConfig(); 80 - const isDev = config.clientType === "loopback"; 81 - 82 - let client: NodeOAuthClient; 83 - 84 - if (isDev) { 85 - // Loopback 86 - const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 87 - client = new NodeOAuthClient({ 88 - clientMetadata: clientMetadata, 89 - stateStore: stateStore as any, 90 - sessionStore: sessionStore as any, 91 - }); 92 - } else { 93 - // Production with private key 94 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 95 - const privateKey = await JoseKey.fromImportable( 96 - normalizedKey, 97 - "main-key", 98 - ); 99 - 100 - client = new NodeOAuthClient({ 101 - clientMetadata: { 102 - client_id: config.clientId, 103 - client_name: "ATlast", 104 - client_uri: config.clientId.replace( 105 - "/oauth-client-metadata.json", 106 - "", 107 - ), 108 - redirect_uris: [config.redirectUri], 109 - scope: "atproto transition:generic", 110 - grant_types: ["authorization_code", "refresh_token"], 111 - response_types: ["code"], 112 - application_type: "web", 113 - token_endpoint_auth_method: "private_key_jwt", 114 - token_endpoint_auth_signing_alg: "ES256", 115 - dpop_bound_access_tokens: true, 116 - jwks_uri: config.jwksUri, 117 - }, 118 - keyset: [privateKey], 119 - stateStore: stateStore as any, 120 - sessionStore: sessionStore as any, 121 - }); 122 - } 123 - 124 - // Restore OAuth session 125 - const oauthSession = await client.restore(userSession.did); 126 - 127 - // Create agent from OAuth session 128 - const agent = new Agent(oauthSession); 55 + // Get authenticated agent using SessionManager 56 + const { agent, did: userDid } = 57 + await SessionManager.getAgentForSession(sessionId); 129 58 130 59 // Follow all users 131 60 const results = []; ··· 135 64 for (const did of dids) { 136 65 try { 137 66 await agent.api.com.atproto.repo.createRecord({ 138 - repo: userSession.did, 67 + repo: userDid, 139 68 collection: "app.bsky.graph.follow", 140 69 record: { 141 70 $type: "app.bsky.graph.follow", ··· 199 128 }; 200 129 } catch (error) { 201 130 console.error("Batch follow error:", error); 131 + 132 + // Handle authentication errors specifically 133 + if (error instanceof Error && error.message.includes("session")) { 134 + return { 135 + statusCode: 401, 136 + headers: { "Content-Type": "application/json" }, 137 + body: JSON.stringify({ 138 + error: "Invalid or expired session", 139 + details: error.message, 140 + }), 141 + }; 142 + } 143 + 202 144 return { 203 145 statusCode: 500, 204 146 headers: { "Content-Type": "application/json" },
+16 -75
netlify/functions/batch-search-actors.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions"; 2 - import { 3 - NodeOAuthClient, 4 - atprotoLoopbackClientMetadata, 5 - } from "@atproto/oauth-client-node"; 6 - import { JoseKey } from "@atproto/jwk-jose"; 7 - import { stateStore, sessionStore, userSessions } from "./oauth-stores-db"; 8 - import { getOAuthConfig } from "./oauth-config"; 9 - import { Agent } from "@atproto/api"; 2 + import { SessionManager } from "./session-manager"; 10 3 import cookie from "cookie"; 11 - 12 - function normalizePrivateKey(key: string): string { 13 - if (!key.includes("\n") && key.includes("\\n")) { 14 - return key.replace(/\\n/g, "\n"); 15 - } 16 - return key; 17 - } 18 4 19 5 export const handler: Handler = async ( 20 6 event: HandlerEvent, ··· 57 43 }; 58 44 } 59 45 60 - // Get DID from session 61 - const userSession = await userSessions.get(sessionId); 62 - if (!userSession) { 63 - return { 64 - statusCode: 401, 65 - headers: { "Content-Type": "application/json" }, 66 - body: JSON.stringify({ error: "Invalid or expired session" }), 67 - }; 68 - } 69 - 70 - const config = getOAuthConfig(); 71 - const isDev = config.clientType === "loopback"; 72 - 73 - let client: NodeOAuthClient; 74 - 75 - if (isDev) { 76 - // Loopback 77 - const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 78 - client = new NodeOAuthClient({ 79 - clientMetadata: clientMetadata, 80 - stateStore: stateStore as any, 81 - sessionStore: sessionStore as any, 82 - }); 83 - } else { 84 - // Production with private key 85 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 86 - const privateKey = await JoseKey.fromImportable( 87 - normalizedKey, 88 - "main-key", 89 - ); 90 - 91 - client = new NodeOAuthClient({ 92 - clientMetadata: { 93 - client_id: config.clientId, 94 - client_name: "ATlast", 95 - client_uri: config.clientId.replace( 96 - "/oauth-client-metadata.json", 97 - "", 98 - ), 99 - redirect_uris: [config.redirectUri], 100 - scope: "atproto transition:generic", 101 - grant_types: ["authorization_code", "refresh_token"], 102 - response_types: ["code"], 103 - application_type: "web", 104 - token_endpoint_auth_method: "private_key_jwt", 105 - token_endpoint_auth_signing_alg: "ES256", 106 - dpop_bound_access_tokens: true, 107 - jwks_uri: config.jwksUri, 108 - }, 109 - keyset: [privateKey], 110 - stateStore: stateStore as any, 111 - sessionStore: sessionStore as any, 112 - }); 113 - } 114 - 115 - // Restore OAuth session 116 - const oauthSession = await client.restore(userSession.did); 117 - 118 - // Create agent from OAuth session 119 - const agent = new Agent(oauthSession); 46 + // Get authenticated agent using SessionManager 47 + const { agent } = await SessionManager.getAgentForSession(sessionId); 120 48 121 49 // Search all usernames in parallel 122 50 const searchPromises = usernames.map(async (username) => { ··· 230 158 }; 231 159 } catch (error) { 232 160 console.error("Batch search error:", error); 161 + 162 + // Handle authentication errors specifically 163 + if (error instanceof Error && error.message.includes("session")) { 164 + return { 165 + statusCode: 401, 166 + headers: { "Content-Type": "application/json" }, 167 + body: JSON.stringify({ 168 + error: "Invalid or expired session", 169 + details: error.message, 170 + }), 171 + }; 172 + } 173 + 233 174 return { 234 175 statusCode: 500, 235 176 headers: { "Content-Type": "application/json" },
+65
netlify/functions/client.ts
··· 1 + import { 2 + NodeOAuthClient, 3 + atprotoLoopbackClientMetadata, 4 + } from "@atproto/oauth-client-node"; 5 + import { JoseKey } from "@atproto/jwk-jose"; 6 + import { stateStore, sessionStore } from "./oauth-stores-db"; 7 + import { getOAuthConfig } from "./oauth-config"; 8 + 9 + function normalizePrivateKey(key: string): string { 10 + if (!key.includes("\n") && key.includes("\\n")) { 11 + return key.replace(/\\n/g, "\n"); 12 + } 13 + return key; 14 + } 15 + 16 + /** 17 + * Creates and returns a configured OAuth client based on environment 18 + * Centralizes the client creation logic used across all endpoints 19 + */ 20 + export async function createOAuthClient(): Promise<NodeOAuthClient> { 21 + const config = getOAuthConfig(); 22 + const isDev = config.clientType === "loopback"; 23 + 24 + if (isDev) { 25 + // Loopback mode for local development 26 + console.log("[oauth-client] Creating loopback OAuth client"); 27 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 28 + 29 + return new NodeOAuthClient({ 30 + clientMetadata: clientMetadata, 31 + stateStore: stateStore as any, 32 + sessionStore: sessionStore as any, 33 + }); 34 + } else { 35 + // Production mode with private key 36 + console.log("[oauth-client] Creating production OAuth client"); 37 + 38 + if (!process.env.OAUTH_PRIVATE_KEY) { 39 + throw new Error("OAUTH_PRIVATE_KEY is required for production"); 40 + } 41 + 42 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 43 + const privateKey = await JoseKey.fromImportable(normalizedKey, "main-key"); 44 + 45 + return new NodeOAuthClient({ 46 + clientMetadata: { 47 + client_id: config.clientId, 48 + client_name: "ATlast", 49 + client_uri: config.clientId.replace("/oauth-client-metadata.json", ""), 50 + redirect_uris: [config.redirectUri], 51 + scope: "atproto transition:generic", 52 + grant_types: ["authorization_code", "refresh_token"], 53 + response_types: ["code"], 54 + application_type: "web", 55 + token_endpoint_auth_method: "private_key_jwt", 56 + token_endpoint_auth_signing_alg: "ES256", 57 + dpop_bound_access_tokens: true, 58 + jwks_uri: config.jwksUri, 59 + }, 60 + keyset: [privateKey], 61 + stateStore: stateStore as any, 62 + sessionStore: sessionStore as any, 63 + }); 64 + } 65 + }
+4 -8
netlify/functions/logout.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions"; 2 - import { userSessions } from "./oauth-stores-db"; 2 + import { SessionManager } from "./session-manager"; 3 3 import { getOAuthConfig } from "./oauth-config"; 4 4 import cookie from "cookie"; 5 5 ··· 27 27 console.log("[logout] Session ID from cookie:", sessionId); 28 28 29 29 if (sessionId) { 30 - // Get the DID before deleting 31 - const userSession = await userSessions.get(sessionId); 32 - const did = userSession?.did; 33 - 34 - // Delete session from database 35 - await userSessions.del(sessionId); 36 - console.log("[logout] Deleted session from database"); 30 + // Use SessionManager to properly clean up both user and OAuth sessions 31 + await SessionManager.deleteSession(sessionId); 32 + console.log("[logout] Successfully deleted session:", sessionId); 37 33 } 38 34 39 35 // Clear the session cookie with matching flags from when it was set
+17 -77
netlify/functions/oauth-callback.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions"; 2 - import { 3 - NodeOAuthClient, 4 - atprotoLoopbackClientMetadata, 5 - } from "@atproto/oauth-client-node"; 6 - import { JoseKey } from "@atproto/jwk-jose"; 7 - import { stateStore, sessionStore, userSessions } from "./oauth-stores-db"; 2 + import { createOAuthClient } from "./client"; 3 + import { userSessions } from "./oauth-stores-db"; 8 4 import { getOAuthConfig } from "./oauth-config"; 9 5 import * as crypto from "crypto"; 10 6 11 - function normalizePrivateKey(key: string): string { 12 - if (!key.includes("\n") && key.includes("\\n")) { 13 - return key.replace(/\\n/g, "\n"); 14 - } 15 - return key; 16 - } 17 - 18 7 export const handler: Handler = async ( 19 8 event: HandlerEvent, 20 9 ): Promise<HandlerResponse> => { ··· 34 23 const code = params.get("code"); 35 24 const state = params.get("state"); 36 25 37 - console.log("OAuth callback - Mode:", isDev ? "loopback" : "production"); 38 - console.log("OAuth callback - URL:", currentUrl); 26 + console.log( 27 + "[oauth-callback] Processing callback - Mode:", 28 + isDev ? "loopback" : "production", 29 + ); 30 + console.log("[oauth-callback] URL:", currentUrl); 39 31 40 32 if (!code || !state) { 41 33 return { ··· 47 39 }; 48 40 } 49 41 50 - let client: NodeOAuthClient; 51 - 52 - if (isDev) { 53 - // LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset 54 - console.log("🔧 Loopback callback"); 55 - 56 - const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 57 - 58 - client = new NodeOAuthClient({ 59 - clientMetadata: clientMetadata, 60 - // No keyset for loopback! 61 - stateStore: stateStore as any, 62 - sessionStore: sessionStore as any, 63 - }); 64 - } else { 65 - // PRODUCTION MODE 66 - if (!process.env.OAUTH_PRIVATE_KEY) { 67 - console.error("OAUTH_PRIVATE_KEY not set"); 68 - return { 69 - statusCode: 302, 70 - headers: { 71 - Location: `${currentUrl}/?error=Server configuration error`, 72 - }, 73 - body: "", 74 - }; 75 - } 76 - 77 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 78 - const privateKey = await JoseKey.fromImportable( 79 - normalizedKey, 80 - "main-key", 81 - ); 42 + // Create OAuth client using shared helper 43 + const client = await createOAuthClient(); 82 44 83 - const currentHost = process.env.DEPLOY_URL 84 - ? new URL(process.env.DEPLOY_URL).host 85 - : event.headers["x-forwarded-host"] || event.headers.host; 45 + // Process the OAuth callback 46 + const result = await client.callback(params); 86 47 87 - currentUrl = `https://${currentHost}`; 88 - const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 89 - const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 90 - const clientId = `${currentUrl}/oauth-client-metadata.json`; 91 - 92 - client = new NodeOAuthClient({ 93 - clientMetadata: { 94 - client_id: clientId, 95 - client_name: "ATlast", 96 - client_uri: currentUrl, 97 - redirect_uris: [redirectUri], 98 - scope: "atproto transition:generic", 99 - grant_types: ["authorization_code", "refresh_token"], 100 - response_types: ["code"], 101 - application_type: "web", 102 - token_endpoint_auth_method: "private_key_jwt", 103 - token_endpoint_auth_signing_alg: "ES256", 104 - dpop_bound_access_tokens: true, 105 - jwks_uri: jwksUri, 106 - } as any, 107 - keyset: [privateKey], 108 - stateStore: stateStore as any, 109 - sessionStore: sessionStore as any, 110 - }); 111 - } 112 - 113 - const result = await client.callback(params); 48 + console.log( 49 + "[oauth-callback] Successfully authenticated DID:", 50 + result.session.did, 51 + ); 114 52 115 53 // Store session 116 54 const sessionId = crypto.randomUUID(); 117 55 const did = result.session.did; 118 56 await userSessions.set(sessionId, { did }); 57 + 58 + console.log("[oauth-callback] Created user session:", sessionId); 119 59 120 60 // Cookie flags - no Secure flag for loopback 121 61 const cookieFlags = isDev
+7 -77
netlify/functions/oauth-start.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions"; 2 - import { 3 - NodeOAuthClient, 4 - atprotoLoopbackClientMetadata, 5 - } from "@atproto/oauth-client-node"; 6 - import { JoseKey } from "@atproto/jwk-jose"; 7 - import { stateStore, sessionStore } from "./oauth-stores-db"; 8 - import { getOAuthConfig } from "./oauth-config"; 2 + import { createOAuthClient } from "./client"; 9 3 10 4 interface OAuthStartRequestBody { 11 5 login_hint?: string; 12 6 origin?: string; 13 7 } 14 8 15 - function normalizePrivateKey(key: string): string { 16 - if (!key.includes("\n") && key.includes("\\n")) { 17 - return key.replace(/\\n/g, "\n"); 18 - } 19 - return key; 20 - } 21 - 22 9 export const handler: Handler = async ( 23 10 event: HandlerEvent, 24 11 ): Promise<HandlerResponse> => { ··· 40 27 }; 41 28 } 42 29 43 - const config = getOAuthConfig(); 44 - const isDev = config.clientType === "loopback"; 45 - 46 - let client: NodeOAuthClient; 47 - 48 - if (isDev) { 49 - // LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset 50 - console.log("🔧 Using loopback OAuth client for development"); 51 - console.log("Client ID:", config.clientId); 52 - 53 - const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 54 - 55 - client = new NodeOAuthClient({ 56 - clientMetadata: clientMetadata, 57 - stateStore: stateStore as any, 58 - sessionStore: sessionStore as any, 59 - }); 60 - } else { 61 - // PRODUCTION MODE: Full confidential client with keyset 62 - console.log("🔐 Using confidential OAuth client for production"); 63 - 64 - if (!process.env.OAUTH_PRIVATE_KEY) { 65 - throw new Error("OAUTH_PRIVATE_KEY required for production"); 66 - } 67 - 68 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 69 - const privateKey = await JoseKey.fromImportable( 70 - normalizedKey, 71 - "main-key", 72 - ); 30 + console.log("[oauth-start] Starting OAuth flow for:", loginHint); 73 31 74 - const currentHost = process.env.DEPLOY_URL 75 - ? new URL(process.env.DEPLOY_URL).host 76 - : event.headers["x-forwarded-host"] || event.headers.host; 77 - 78 - if (!currentHost) { 79 - throw new Error("Missing host header"); 80 - } 81 - 82 - const currentUrl = `https://${currentHost}`; 83 - const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 84 - const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 85 - const clientId = `${currentUrl}/oauth-client-metadata.json`; 86 - 87 - client = new NodeOAuthClient({ 88 - clientMetadata: { 89 - client_id: clientId, 90 - client_name: "ATlast", 91 - client_uri: currentUrl, 92 - redirect_uris: [redirectUri], 93 - scope: "atproto transition:generic", 94 - grant_types: ["authorization_code", "refresh_token"], 95 - response_types: ["code"], 96 - application_type: "web", 97 - token_endpoint_auth_method: "private_key_jwt", 98 - token_endpoint_auth_signing_alg: "ES256", 99 - dpop_bound_access_tokens: true, 100 - jwks_uri: jwksUri, 101 - } as any, 102 - keyset: [privateKey], 103 - stateStore: stateStore as any, 104 - sessionStore: sessionStore as any, 105 - }); 106 - } 32 + // Create OAuth client using shared helper 33 + const client = await createOAuthClient(); 107 34 35 + // Start the authorization flow 108 36 const authUrl = await client.authorize(loginHint, { 109 37 scope: "atproto transition:generic", 110 38 }); 39 + 40 + console.log("[oauth-start] Generated auth URL for:", loginHint); 111 41 112 42 return { 113 43 statusCode: 200,
+92
netlify/functions/session-manager.ts
··· 1 + import { Agent } from "@atproto/api"; 2 + import { createOAuthClient } from "./client"; 3 + import { userSessions } from "./oauth-stores-db"; 4 + import type { NodeOAuthClient } from "@atproto/oauth-client-node"; 5 + 6 + /** 7 + * Session Manager - Coordinates between user sessions and OAuth sessions 8 + * Provides a clean interface for session operations across the application 9 + */ 10 + export class SessionManager { 11 + /** 12 + * Get an authenticated Agent for a given session ID 13 + * Handles both user session lookup and OAuth session restoration 14 + */ 15 + static async getAgentForSession(sessionId: string): Promise<{ 16 + agent: Agent; 17 + did: string; 18 + client: NodeOAuthClient; 19 + }> { 20 + console.log("[SessionManager] Getting agent for session:", sessionId); 21 + 22 + // Get user session 23 + const userSession = await userSessions.get(sessionId); 24 + if (!userSession) { 25 + throw new Error("Invalid or expired session"); 26 + } 27 + 28 + const did = userSession.did; 29 + console.log("[SessionManager] Found user session for DID:", did); 30 + 31 + // Create OAuth client 32 + const client = await createOAuthClient(); 33 + 34 + // Restore OAuth session 35 + const oauthSession = await client.restore(did); 36 + console.log("[SessionManager] Restored OAuth session for DID:", did); 37 + 38 + // Create agent from OAuth session 39 + const agent = new Agent(oauthSession); 40 + 41 + return { agent, did, client }; 42 + } 43 + 44 + /** 45 + * Delete a session and clean up associated OAuth sessions 46 + * Ensures both user_sessions and oauth_sessions are cleaned up 47 + */ 48 + static async deleteSession(sessionId: string): Promise<void> { 49 + console.log("[SessionManager] Deleting session:", sessionId); 50 + 51 + // Get user session first 52 + const userSession = await userSessions.get(sessionId); 53 + if (!userSession) { 54 + console.log("[SessionManager] Session not found:", sessionId); 55 + return; 56 + } 57 + 58 + const did = userSession.did; 59 + 60 + try { 61 + // Create OAuth client and revoke the session 62 + const client = await createOAuthClient(); 63 + 64 + // Try to revoke at the PDS (this also deletes from oauth_sessions) 65 + await client.revoke(did); 66 + console.log("[SessionManager] Revoked OAuth session for DID:", did); 67 + } catch (error) { 68 + // If revocation fails, the OAuth session might already be invalid 69 + console.log("[SessionManager] Could not revoke OAuth session:", error); 70 + } 71 + 72 + // Delete user session 73 + await userSessions.del(sessionId); 74 + console.log("[SessionManager] Deleted user session:", sessionId); 75 + } 76 + 77 + /** 78 + * Verify a session exists and is valid 79 + */ 80 + static async verifySession(sessionId: string): Promise<boolean> { 81 + const userSession = await userSessions.get(sessionId); 82 + return userSession !== null; 83 + } 84 + 85 + /** 86 + * Get the DID for a session without creating an agent 87 + */ 88 + static async getDIDForSession(sessionId: string): Promise<string | null> { 89 + const userSession = await userSessions.get(sessionId); 90 + return userSession?.did || null; 91 + } 92 + }
+16 -72
netlify/functions/session.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions"; 2 - import { 3 - NodeOAuthClient, 4 - atprotoLoopbackClientMetadata, 5 - } from "@atproto/oauth-client-node"; 6 - import { JoseKey } from "@atproto/jwk-jose"; 7 - import { stateStore, sessionStore, userSessions } from "./oauth-stores-db"; 8 - import { getOAuthConfig } from "./oauth-config"; 9 - import { Agent } from "@atproto/api"; 2 + import { SessionManager } from "./session-manager"; 10 3 import cookie from "cookie"; 11 - 12 - function normalizePrivateKey(key: string): string { 13 - if (!key.includes("\n") && key.includes("\\n")) { 14 - return key.replace(/\\n/g, "\n"); 15 - } 16 - return key; 17 - } 18 4 19 5 // In-memory cache for profile 20 6 const profileCache = new Map<string, { data: any; timestamp: number }>(); ··· 38 24 }; 39 25 } 40 26 41 - // Check database for session 42 - const userSession = await userSessions.get(sessionId); 43 - 44 - if (!userSession) { 27 + // Verify session exists 28 + const isValid = await SessionManager.verifySession(sessionId); 29 + if (!isValid) { 45 30 return { 46 31 statusCode: 401, 47 32 headers: { "Content-Type": "application/json" }, ··· 49 34 }; 50 35 } 51 36 52 - const did = userSession.did; 37 + // Get DID from session 38 + const did = await SessionManager.getDIDForSession(sessionId); 39 + if (!did) { 40 + return { 41 + statusCode: 401, 42 + headers: { "Content-Type": "application/json" }, 43 + body: JSON.stringify({ error: "Invalid session" }), 44 + }; 45 + } 46 + 53 47 const now = Date.now(); 54 48 55 49 // Check profile cache ··· 71 65 72 66 // Cache miss - fetch full profile 73 67 try { 74 - const config = getOAuthConfig(); 75 - const isDev = config.clientType === "loopback"; 76 - 77 - let client: NodeOAuthClient; 78 - 79 - if (isDev) { 80 - // Loopback 81 - const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 82 - client = new NodeOAuthClient({ 83 - clientMetadata: clientMetadata, 84 - stateStore: stateStore as any, 85 - sessionStore: sessionStore as any, 86 - }); 87 - } else { 88 - // Production with private key 89 - const normalizedKey = normalizePrivateKey( 90 - process.env.OAUTH_PRIVATE_KEY!, 91 - ); 92 - const privateKey = await JoseKey.fromImportable( 93 - normalizedKey, 94 - "main-key", 95 - ); 96 - 97 - client = new NodeOAuthClient({ 98 - clientMetadata: { 99 - client_id: config.clientId, 100 - client_name: "ATlast", 101 - client_uri: config.clientId.replace( 102 - "/oauth-client-metadata.json", 103 - "", 104 - ), 105 - redirect_uris: [config.redirectUri], 106 - scope: "atproto transition:generic", 107 - grant_types: ["authorization_code", "refresh_token"], 108 - response_types: ["code"], 109 - application_type: "web", 110 - token_endpoint_auth_method: "private_key_jwt", 111 - token_endpoint_auth_signing_alg: "ES256", 112 - dpop_bound_access_tokens: true, 113 - jwks_uri: config.jwksUri, 114 - }, 115 - keyset: [privateKey], 116 - stateStore: stateStore as any, 117 - sessionStore: sessionStore as any, 118 - }); 119 - } 120 - 121 - // Restore OAuth session 122 - const oauthSession = await client.restore(did); 123 - 124 - // Create agent from OAuth session 125 - const agent = new Agent(oauthSession); 68 + // Get authenticated agent using SessionManager 69 + const { agent } = await SessionManager.getAgentForSession(sessionId); 126 70 127 71 // Get profile 128 72 const profile = await agent.getProfile({ actor: did });