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

add loopback client metadata for oauth in local dev!!!!!

+37 -23
netlify/functions/batch-follow-users.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 67 67 }; 68 68 } 69 69 70 - // Initialize OAuth client 71 70 const config = getOAuthConfig(); 72 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 73 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 71 + const isDev = config.clientType === 'loopback'; 74 72 75 - const client = new NodeOAuthClient({ 76 - clientMetadata: { 77 - client_id: config.clientId, 78 - client_name: 'ATlast', 79 - client_uri: config.clientId.replace('/client-metadata.json', ''), 80 - redirect_uris: [config.redirectUri], 81 - scope: 'atproto transition:generic', 82 - grant_types: ['authorization_code', 'refresh_token'], 83 - response_types: ['code'], 84 - application_type: 'web', 85 - token_endpoint_auth_method: 'private_key_jwt', 86 - token_endpoint_auth_signing_alg: 'ES256', 87 - dpop_bound_access_tokens: true, 88 - jwks_uri: config.jwksUri, 89 - }, 90 - keyset: [privateKey], 91 - stateStore: stateStore as any, 92 - sessionStore: sessionStore as any, 93 - }); 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(normalizedKey, 'main-key'); 87 + 88 + client = new NodeOAuthClient({ 89 + clientMetadata: { 90 + client_id: config.clientId, 91 + client_name: 'ATlast', 92 + client_uri: config.clientId.replace('/client-metadata.json', ''), 93 + redirect_uris: [config.redirectUri], 94 + scope: 'atproto transition:generic', 95 + grant_types: ['authorization_code', 'refresh_token'], 96 + response_types: ['code'], 97 + application_type: 'web', 98 + token_endpoint_auth_method: 'private_key_jwt', 99 + token_endpoint_auth_signing_alg: 'ES256', 100 + dpop_bound_access_tokens: true, 101 + jwks_uri: config.jwksUri, 102 + }, 103 + keyset: [privateKey], 104 + stateStore: stateStore as any, 105 + sessionStore: sessionStore as any, 106 + }); 107 + } 94 108 95 109 // Restore OAuth session 96 110 const oauthSession = await client.restore(userSession.did);
+37 -23
netlify/functions/batch-search-actors.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 58 58 }; 59 59 } 60 60 61 - // Initialize OAuth client 62 61 const config = getOAuthConfig(); 63 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 64 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 62 + const isDev = config.clientType === 'loopback'; 65 63 66 - const client = new NodeOAuthClient({ 67 - clientMetadata: { 68 - client_id: config.clientId, 69 - client_name: 'ATlast', 70 - client_uri: config.clientId.replace('/client-metadata.json', ''), 71 - redirect_uris: [config.redirectUri], 72 - scope: 'atproto transition:generic', 73 - grant_types: ['authorization_code', 'refresh_token'], 74 - response_types: ['code'], 75 - application_type: 'web', 76 - token_endpoint_auth_method: 'private_key_jwt', 77 - token_endpoint_auth_signing_alg: 'ES256', 78 - dpop_bound_access_tokens: true, 79 - jwks_uri: config.jwksUri, 80 - }, 81 - keyset: [privateKey], 82 - stateStore: stateStore as any, 83 - sessionStore: sessionStore as any, 84 - }); 64 + let client: NodeOAuthClient; 65 + 66 + if (isDev) { 67 + // Loopback 68 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 69 + client = new NodeOAuthClient({ 70 + clientMetadata: clientMetadata, 71 + stateStore: stateStore as any, 72 + sessionStore: sessionStore as any, 73 + }); 74 + } else { 75 + // Production with private key 76 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 77 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 78 + 79 + client = new NodeOAuthClient({ 80 + clientMetadata: { 81 + client_id: config.clientId, 82 + client_name: 'ATlast', 83 + client_uri: config.clientId.replace('/client-metadata.json', ''), 84 + redirect_uris: [config.redirectUri], 85 + scope: 'atproto transition:generic', 86 + grant_types: ['authorization_code', 'refresh_token'], 87 + response_types: ['code'], 88 + application_type: 'web', 89 + token_endpoint_auth_method: 'private_key_jwt', 90 + token_endpoint_auth_signing_alg: 'ES256', 91 + dpop_bound_access_tokens: true, 92 + jwks_uri: config.jwksUri, 93 + }, 94 + keyset: [privateKey], 95 + stateStore: stateStore as any, 96 + sessionStore: sessionStore as any, 97 + }); 98 + } 85 99 86 100 // Restore OAuth session 87 101 const oauthSession = await client.restore(userSession.did);
+36 -12
netlify/functions/client-metadata.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 2 3 3 export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => { 4 + 4 5 try { 5 6 // Get the host that's requesting the metadata 6 7 // This will be different for production vs preview deploys vs dev --live ··· 16 17 }; 17 18 } 18 19 19 - // Build the redirect URI based on the requesting host 20 + // Check if this is a loopback/development request 21 + const isLoopback = requestHost.startsWith('127.0.0.1') || 22 + requestHost.startsWith('[::1]') || 23 + requestHost === 'localhost'; 24 + 25 + if (isLoopback) { 26 + // For loopback clients, return minimal metadata 27 + // NOTE: In practice, the OAuth server won't fetch this because 28 + // loopback clients use hardcoded metadata on the server side 29 + const appUrl = `http://${requestHost}`; 30 + const redirectUri = `${appUrl}/.netlify/functions/oauth-callback`; 31 + 32 + return { 33 + statusCode: 200, 34 + headers: { 35 + 'Content-Type': 'application/json', 36 + 'Access-Control-Allow-Origin': '*', 37 + }, 38 + body: JSON.stringify({ 39 + client_id: appUrl, // Just the origin for loopback 40 + client_name: 'ATlast (Local Dev)', 41 + client_uri: appUrl, 42 + redirect_uris: [redirectUri], 43 + scope: 'atproto transition:generic', 44 + grant_types: ['authorization_code', 'refresh_token'], 45 + response_types: ['code'], 46 + application_type: 'web', 47 + token_endpoint_auth_method: 'none', // No auth for loopback 48 + dpop_bound_access_tokens: true, 49 + }), 50 + }; 51 + } 52 + 53 + // Production: Confidential client metadata 20 54 const redirectUri = `https://${requestHost}/.netlify/functions/oauth-callback`; 21 55 const appUrl = `https://${requestHost}`; 22 56 const jwksUri = `https://${requestHost}/.netlify/functions/jwks`; 23 57 const clientId = `https://${requestHost}/.netlify/functions/client-metadata`; 24 - 25 - console.log('Client metadata generated for host:', { 26 - requestHost, 27 - redirectUri, 28 - appUrl, 29 - clientId, 30 - jwksUri, 31 - }); 32 58 33 59 const metadata = { 34 60 client_id: clientId, ··· 50 76 headers: { 51 77 'Content-Type': 'application/json', 52 78 'Access-Control-Allow-Origin': '*', 53 - 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', 54 - 'Pragma': 'no-cache', 55 - 'Expires': '0', 79 + 'Cache-Control': 'no-store' 56 80 }, 57 81 body: JSON.stringify(metadata), 58 82 };
+70 -58
netlify/functions/oauth-callback.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 13 13 } 14 14 15 15 export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => { 16 - let currentHost = process.env.DEPLOY_URL 17 - ? new URL(process.env.DEPLOY_URL).host 18 - : (event.headers['x-forwarded-host'] || event.headers.host); 19 - let currentUrl = currentHost ? `https://${currentHost}` : process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://atlast.byarielm.fyi'; 20 - const fallbackUrl = currentUrl; 16 + const config = getOAuthConfig(); 17 + const isDev = config.clientType === 'loopback'; 18 + 19 + let currentUrl = isDev 20 + ? 'http://127.0.0.1:8888' 21 + : (process.env.DEPLOY_URL 22 + ? `https://${new URL(process.env.DEPLOY_URL).host}` 23 + : process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://atlast.byarielm.fyi'); 21 24 22 25 try { 23 26 const params = new URLSearchParams(event.rawUrl.split('?')[1] || ''); 24 27 const code = params.get('code'); 25 28 const state = params.get('state'); 26 29 27 - console.log('OAuth callback - Host:', currentHost); 28 - console.log('OAuth callback - currentUrl resolved to:', currentUrl); 30 + console.log('OAuth callback - Mode:', isDev ? 'loopback' : 'production'); 31 + console.log('OAuth callback - URL:', currentUrl); 29 32 30 33 if (!code || !state) { 31 34 return { ··· 37 40 }; 38 41 } 39 42 40 - if (!process.env.OAUTH_PRIVATE_KEY) { 41 - console.error('OAUTH_PRIVATE_KEY not set'); 42 - return { 43 - statusCode: 302, 44 - headers: { 45 - 'Location': `${currentUrl}/?error=Server configuration error` 46 - }, 47 - body: '' 48 - }; 49 - } 50 - 51 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 52 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 53 - 54 - // All URIs must now be based on the current deploy URL/host 55 - const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 56 - const jwksUri = `${currentUrl}/.netlify/functions/jwks`; // NOW DYNAMIC URL 57 - const clientId = `${currentUrl}/.netlify/functions/client-metadata`; // NOW DYNAMIC URL 43 + let client: NodeOAuthClient; 58 44 59 - console.log('OAuth callback URLs:', { 60 - redirectUri, 61 - jwksUri, 62 - clientId, 63 - currentUrl 64 - }); 45 + if (isDev) { 46 + // LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset 47 + console.log('🔧 Loopback callback'); 48 + 49 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 50 + 51 + client = new NodeOAuthClient({ 52 + clientMetadata: clientMetadata, 53 + // No keyset for loopback! 54 + stateStore: stateStore as any, 55 + sessionStore: sessionStore as any, 56 + }); 57 + } else { 58 + // PRODUCTION MODE 59 + if (!process.env.OAUTH_PRIVATE_KEY) { 60 + console.error('OAUTH_PRIVATE_KEY not set'); 61 + return { 62 + statusCode: 302, 63 + headers: { 'Location': `${currentUrl}/?error=Server configuration error` }, 64 + body: '' 65 + }; 66 + } 65 67 66 - // Build metadata dynamically based on the current environment 67 - const clientMetadata = { 68 - client_id: clientId, // NOW DYNAMIC URL 69 - client_name: 'ATlast', 70 - client_uri: currentUrl, 71 - redirect_uris: [redirectUri], 72 - scope: 'atproto transition:generic', 73 - grant_types: ['authorization_code', 'refresh_token'], 74 - response_types: ['code'], 75 - application_type: 'web', 76 - token_endpoint_auth_method: 'private_key_jwt', 77 - token_endpoint_auth_signing_alg: 'ES256', 78 - dpop_bound_access_tokens: true, 79 - jwks_uri: jwksUri, // NOW DYNAMIC URL 80 - }; 68 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 69 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 70 + 71 + const currentHost = process.env.DEPLOY_URL 72 + ? new URL(process.env.DEPLOY_URL).host 73 + : (event.headers['x-forwarded-host'] || event.headers.host); 74 + 75 + currentUrl = `https://${currentHost}`; 76 + const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 77 + const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 78 + const clientId = `${currentUrl}/.netlify/functions/client-metadata`; 81 79 82 - // Initialize OAuth client with dynamic metadata 83 - const client = new NodeOAuthClient({ 84 - clientMetadata: clientMetadata as any, 85 - keyset: [privateKey], 86 - stateStore: stateStore as any, 87 - sessionStore: sessionStore as any, 88 - }); 80 + client = new NodeOAuthClient({ 81 + clientMetadata: { 82 + client_id: clientId, 83 + client_name: 'ATlast', 84 + client_uri: currentUrl, 85 + redirect_uris: [redirectUri], 86 + scope: 'atproto transition:generic', 87 + grant_types: ['authorization_code', 'refresh_token'], 88 + response_types: ['code'], 89 + application_type: 'web', 90 + token_endpoint_auth_method: 'private_key_jwt', 91 + token_endpoint_auth_signing_alg: 'ES256', 92 + dpop_bound_access_tokens: true, 93 + jwks_uri: jwksUri, 94 + } as any, 95 + keyset: [privateKey], 96 + stateStore: stateStore as any, 97 + sessionStore: sessionStore as any, 98 + }); 99 + } 89 100 90 101 const result = await client.callback(params); 91 102 92 - // Store a simple session mapping: sessionId -> DID 103 + // Store session 93 104 const sessionId = crypto.randomUUID(); 94 105 const did = result.session.did; 95 - 96 106 await userSessions.set(sessionId, { did }); 97 107 98 - const cookieFlags = 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/; Secure'; 108 + // Cookie flags - no Secure flag for loopback 109 + const cookieFlags = isDev 110 + ? 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/' 111 + : 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/; Secure'; 99 112 100 113 return { 101 114 statusCode: 302, ··· 108 121 109 122 } catch (error) { 110 123 console.error('OAuth callback error:', error); 111 - 112 124 return { 113 125 statusCode: 302, 114 126 headers: {
+23 -5
netlify/functions/oauth-config.ts
··· 1 1 export function getOAuthConfig() { 2 - // In Netlify, process.env.URL is automatically set to the public URL. 2 + // Development: loopback client for local dev 3 + const isDev = process.env.NODE_ENV === 'development' || process.env.NETLIFY_DEV === 'true'; 4 + 5 + if (isDev) { 6 + const port = process.env.PORT || '8888'; 7 + 8 + // Special loopback client_id format with query params 9 + const clientId = `http://localhost?${new URLSearchParams([ 10 + ['redirect_uri', `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`], 11 + ['scope', 'atproto transition:generic'], 12 + ])}`; 13 + 14 + return { 15 + clientId: clientId, 16 + redirectUri: `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`, 17 + jwksUri: undefined, 18 + clientType: 'loopback' as const, 19 + }; 20 + } 21 + 22 + // Production: discoverable client logic 3 23 const baseUrl = process.env.URL || process.env.DEPLOY_PRIME_URL; 4 24 5 25 if (process.env.NETLIFY && !process.env.URL) { 6 - // This is a safety check for a critical configuration issue on Netlify 7 26 throw new Error('process.env.URL is required in Netlify environment'); 8 27 } 9 28 ··· 13 32 CONTEXT: process.env.CONTEXT, 14 33 using: baseUrl 15 34 }); 16 - 17 - const redirectUri = `${baseUrl}/.netlify/functions/oauth-callback`; 18 35 19 36 return { 20 37 clientId: `${baseUrl}/.netlify/functions/client-metadata`, // discoverable client URL 21 - redirectUri, 38 + redirectUri: `${baseUrl}/.netlify/functions/oauth-callback`, 22 39 jwksUri: `${baseUrl}/.netlify/functions/jwks`, 23 40 clientType: 'discoverable' as const, 41 + usePrivateKey: true, 24 42 }; 25 43 }
+61 -77
netlify/functions/oauth-start.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; 6 - import { initDB } from './db'; // initDB is only kept for manual setup/migrations 7 6 8 7 interface OAuthStartRequestBody { 9 8 login_hint?: string; 10 - origin?: string; // The actual origin the frontend is running on 9 + origin?: string; 11 10 } 12 11 13 12 function normalizePrivateKey(key: string): string { ··· 19 18 20 19 export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => { 21 20 try { 22 - // await initDB(); 23 - 24 - // Parse request body 25 21 let loginHint: string | undefined = undefined; 26 - let requestOrigin: string | undefined = undefined; 27 22 28 23 if (event.body) { 29 24 const parsed: OAuthStartRequestBody = JSON.parse(event.body); 30 25 loginHint = parsed.login_hint; 31 - requestOrigin = parsed.origin; 32 26 } 33 27 34 - // Validate login hint is provided 35 28 if (!loginHint) { 36 29 return { 37 30 statusCode: 400, ··· 39 32 body: JSON.stringify({ error: 'login_hint (handle or DID) is required' }), 40 33 }; 41 34 } 42 - 43 - console.log('OAuth Start - Request origin:', requestOrigin); 44 35 45 - // Validate private key 46 - if (!process.env.OAUTH_PRIVATE_KEY) { 47 - console.error('OAUTH_PRIVATE_KEY not set'); 48 - return { 49 - statusCode: 500, 50 - headers: { 'Content-Type': 'application/json' }, 51 - body: JSON.stringify({ error: 'Server configuration error' }), 52 - }; 53 - } 36 + const config = getOAuthConfig(); 37 + const isDev = config.clientType === 'loopback'; 54 38 55 - // Initialize OAuth client 56 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 57 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 39 + let client: NodeOAuthClient; 58 40 59 - // Use the dynamic config that Netlify sets for the build 60 - const currentHost = process.env.DEPLOY_URL 61 - ? new URL(process.env.DEPLOY_URL).host 62 - : (event.headers['x-forwarded-host'] || event.headers.host); 63 - 64 - if (!currentHost) { 65 - console.error('Missing host header in function request'); 66 - return { 67 - statusCode: 500, 68 - headers: { 'Content-Type': 'application/json' }, 69 - body: JSON.stringify({ error: 'Server could not determine current host for redirect' }), 70 - }; 71 - } 72 - 73 - const currentUrl = `https://${currentHost}`; 41 + if (isDev) { 42 + // LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset 43 + console.log('🔧 Using loopback OAuth client for development'); 44 + console.log('Client ID:', config.clientId); 45 + 46 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 47 + 48 + client = new NodeOAuthClient({ 49 + clientMetadata: clientMetadata, 50 + stateStore: stateStore as any, 51 + sessionStore: sessionStore as any, 52 + }); 53 + } else { 54 + // PRODUCTION MODE: Full confidential client with keyset 55 + console.log('🔐 Using confidential OAuth client for production'); 56 + 57 + if (!process.env.OAUTH_PRIVATE_KEY) { 58 + throw new Error('OAUTH_PRIVATE_KEY required for production'); 59 + } 74 60 75 - // Now, dynamically define the URIs using the CURRENT HOST 76 - const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 77 - const appUrl = currentUrl; 78 - const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 79 - const clientId = `${currentUrl}/.netlify/functions/client-metadata`; 80 - 81 - console.log('OAuth URLs:', { 82 - redirectUri, 83 - appUrl, 84 - jwksUri, 85 - clientId, 86 - requestOrigin 87 - }); 61 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY); 62 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 88 63 89 - // Build metadata dynamically from environment 90 - const clientMetadata = { 91 - client_id: clientId, 92 - client_name: 'ATlast', 93 - client_uri: appUrl, 94 - redirect_uris: [redirectUri], 95 - scope: 'atproto transition:generic', 96 - grant_types: ['authorization_code', 'refresh_token'], 97 - response_types: ['code'], 98 - application_type: 'web', 99 - token_endpoint_auth_method: 'private_key_jwt', 100 - token_endpoint_auth_signing_alg: 'ES256', 101 - dpop_bound_access_tokens: true, 102 - jwks_uri: jwksUri, 103 - }; 64 + const currentHost = process.env.DEPLOY_URL 65 + ? new URL(process.env.DEPLOY_URL).host 66 + : (event.headers['x-forwarded-host'] || event.headers.host); 104 67 105 - console.log('Client metadata:', clientMetadata); 68 + if (!currentHost) { 69 + throw new Error('Missing host header'); 70 + } 106 71 107 - // Initialize NodeOAuthClient with typed stores 108 - const client = new NodeOAuthClient({ 109 - clientMetadata: clientMetadata as any, 110 - keyset: [privateKey], 111 - stateStore: stateStore as any, 112 - sessionStore: sessionStore as any, 113 - }); 72 + const currentUrl = `https://${currentHost}`; 73 + const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`; 74 + const jwksUri = `${currentUrl}/.netlify/functions/jwks`; 75 + const clientId = `${currentUrl}/.netlify/functions/client-metadata`; 114 76 115 - console.log('OAuth client initialized with redirect_uri:', redirectUri); 77 + client = new NodeOAuthClient({ 78 + clientMetadata: { 79 + client_id: clientId, 80 + client_name: 'ATlast', 81 + client_uri: currentUrl, 82 + redirect_uris: [redirectUri], 83 + scope: 'atproto transition:generic', 84 + grant_types: ['authorization_code', 'refresh_token'], 85 + response_types: ['code'], 86 + application_type: 'web', 87 + token_endpoint_auth_method: 'private_key_jwt', 88 + token_endpoint_auth_signing_alg: 'ES256', 89 + dpop_bound_access_tokens: true, 90 + jwks_uri: jwksUri, 91 + } as any, 92 + keyset: [privateKey], 93 + stateStore: stateStore as any, 94 + sessionStore: sessionStore as any, 95 + }); 96 + } 116 97 117 - // Generate authorization URL 118 98 const authUrl = await client.authorize(loginHint, { 119 99 scope: 'atproto transition:generic', 120 100 }); ··· 129 109 return { 130 110 statusCode: 500, 131 111 headers: { 'Content-Type': 'application/json' }, 132 - body: JSON.stringify({ error: 'Failed to start OAuth flow' }), 112 + body: JSON.stringify({ 113 + error: 'Failed to start OAuth flow', 114 + details: error instanceof Error ? error.message : 'Unknown error', 115 + stack: error instanceof Error ? error.stack : undefined 116 + }), 133 117 }; 134 118 } 135 119 };
+37 -22
netlify/functions/session.ts
··· 1 1 import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions'; 2 - import { NodeOAuthClient } from '@atproto/oauth-client-node'; 2 + import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node'; 3 3 import { JoseKey } from '@atproto/jwk-jose'; 4 4 import { stateStore, sessionStore, userSessions } from './oauth-stores-db'; 5 5 import { getOAuthConfig } from './oauth-config'; ··· 106 106 // Cache miss - fetch full profile 107 107 try { 108 108 const config = getOAuthConfig(); 109 - const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 110 - const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 109 + const isDev = config.clientType === 'loopback'; 110 + 111 + let client: NodeOAuthClient; 112 + 113 + if (isDev) { 114 + // Loopback 115 + const clientMetadata = atprotoLoopbackClientMetadata(config.clientId); 116 + client = new NodeOAuthClient({ 117 + clientMetadata: clientMetadata, 118 + stateStore: stateStore as any, 119 + sessionStore: sessionStore as any, 120 + }); 121 + } else { 122 + // Production with private key 123 + const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!); 124 + const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key'); 111 125 112 - const client = new NodeOAuthClient({ 113 - clientMetadata: { 114 - client_id: config.clientId, 115 - client_name: 'ATlast', 116 - client_uri: config.clientId.replace('/client-metadata.json', ''), 117 - redirect_uris: [config.redirectUri], 118 - scope: 'atproto transition:generic', 119 - grant_types: ['authorization_code', 'refresh_token'], 120 - response_types: ['code'], 121 - application_type: 'web', 122 - token_endpoint_auth_method: 'private_key_jwt', 123 - token_endpoint_auth_signing_alg: 'ES256', 124 - dpop_bound_access_tokens: true, 125 - jwks_uri: config.jwksUri, 126 - }, 127 - keyset: [privateKey], 128 - stateStore: stateStore as any, 129 - sessionStore: sessionStore as any, 130 - }); 126 + client = new NodeOAuthClient({ 127 + clientMetadata: { 128 + client_id: config.clientId, 129 + client_name: 'ATlast', 130 + client_uri: config.clientId.replace('/client-metadata.json', ''), 131 + redirect_uris: [config.redirectUri], 132 + scope: 'atproto transition:generic', 133 + grant_types: ['authorization_code', 'refresh_token'], 134 + response_types: ['code'], 135 + application_type: 'web', 136 + token_endpoint_auth_method: 'private_key_jwt', 137 + token_endpoint_auth_signing_alg: 'ES256', 138 + dpop_bound_access_tokens: true, 139 + jwks_uri: config.jwksUri, 140 + }, 141 + keyset: [privateKey], 142 + stateStore: stateStore as any, 143 + sessionStore: sessionStore as any, 144 + }); 145 + } 131 146 132 147 // Restore OAuth session 133 148 const oauthSession = await client.restore(did);