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

Configure Feed

Select the types of activity you want to include in your feed.

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

+236 -386
+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 });