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

fix oauth so it works in prod & dev again :')

byarielm.fyi 13121d80 c4b17bac

verified
Changed files
+79 -91
dist
netlify
functions
infrastructure
oauth
services
+30 -30
dist/index.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <link rel="manifest" href="/site.webmanifest" /> 6 - <link 7 - rel="apple-touch-icon" 8 - sizes="180x180" 9 - href="/apple-touch-icon.png" 10 - /> 11 - <link 12 - rel="icon" 13 - type="image/x-icon" 14 - sizes="32x32" 15 - href="/favicon.ico" 16 - /> 17 - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 18 - 19 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 20 - <title> 21 - ATLast: Sync Your TikTok Follows → ATmosphere (Skylight, Bluesky, 22 - etc.) 23 - </title> 24 - <script type="module" crossorigin src="/assets/index-BmU3Lkw-.js"></script> 25 - <link rel="stylesheet" crossorigin href="/assets/index-DQCpc624.css"> 26 - </head> 27 - <body> 28 - <div id="root"></div> 29 - </body> 30 - </html> 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="manifest" href="/site.webmanifest" /> 6 + <link 7 + rel="apple-touch-icon" 8 + sizes="180x180" 9 + href="/apple-touch-icon.png" 10 + /> 11 + <link 12 + rel="icon" 13 + type="image/x-icon" 14 + sizes="32x32" 15 + href="/favicon.ico" 16 + /> 17 + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 18 + 19 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 20 + <title> 21 + ATLast: Sync Your TikTok Follows → ATmosphere (Skylight, Bluesky, 22 + etc.) 23 + </title> 24 + <script type="module" crossorigin src="/assets/index-DhUfpNfM.js"></script> 25 + <link rel="stylesheet" crossorigin href="/assets/index-jFgtXSoO.css"> 26 + </head> 27 + <body> 28 + <div id="root"></div> 29 + </body> 30 + </html>
+37 -54
netlify/functions/infrastructure/oauth/config.ts
··· 6 6 export function getOAuthConfig(event?: { 7 7 headers: Record<string, string | undefined>; 8 8 }): OAuthConfig { 9 - const host = event?.headers?.host || "default"; 10 - const cacheKey = `oauth-config-${host}`; 9 + // 1. Determine host dynamically 10 + const host = event?.headers?.host; 11 + const cacheKey = `oauth-config-${host || "default"}`; 11 12 12 13 const cached = configCache.get(cacheKey) as OAuthConfig | undefined; 13 14 if (cached) { ··· 15 16 } 16 17 17 18 let baseUrl: string | undefined; 18 - let deployContext: string | undefined; 19 19 20 - if (event?.headers) { 21 - deployContext = event.headers["x-nf-deploy-context"]; 22 - const forwardedProto = event.headers["x-forwarded-proto"] || "https"; 23 - 24 - if (host && !host.includes("localhost") && !host.includes("127.0.0.1")) { 25 - baseUrl = `${forwardedProto}://${host}`; 26 - } 27 - } 28 - 29 - if (!baseUrl) { 30 - baseUrl = process.env.DEPLOY_URL || process.env.URL; 31 - } 32 - 33 - console.log("🔍 OAuth Config:", { 34 - fromHost: event?.headers?.host, 35 - deployContext: deployContext || process.env.CONTEXT, 36 - baseUrl, 37 - envAvailable: { 38 - DEPLOY_URL: !!process.env.DEPLOY_URL, 39 - URL: !!process.env.URL, 40 - }, 41 - }); 20 + // 2. Determine if local based on host header 21 + const isLocal = 22 + !host || host.includes("localhost") || host.includes("127.0.0.1"); 42 23 43 - const isLocalhost = 44 - !baseUrl || baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1"); 24 + // 3. Local oauth config 25 + if (isLocal) { 26 + const currentHost = host || "localhost:8888"; 27 + const protocol = currentHost.includes("127.0.0.1") 28 + ? "http://127.0.0.1" 29 + : "http://localhost"; 45 30 46 - let config: OAuthConfig; 31 + // Redirect URI must use host in address bar 32 + const redirectUri = `${protocol}:${currentHost.split(":")[1] || "8888"}/.netlify/functions/oauth-callback`; 47 33 48 - if (isLocalhost) { 49 - const port = process.env.PORT || "8888"; 34 + // ClientID must start with localhost 35 + // but redirect_uri query inside must match actual redirectUri 50 36 const clientId = `http://localhost?${new URLSearchParams([ 51 - [ 52 - "redirect_uri", 53 - `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`, 54 - ], 37 + ["redirect_uri", redirectUri], 55 38 ["scope", CONFIG.OAUTH_SCOPES], 56 39 ])}`; 57 40 58 41 console.log("Using loopback OAuth for local development"); 59 42 60 - config = { 43 + const config: OAuthConfig = { 61 44 clientId: clientId, 62 - redirectUri: `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`, 45 + redirectUri: redirectUri, 63 46 jwksUri: undefined, 64 47 clientType: "loopback", 65 48 }; 66 - } else { 67 - if (!baseUrl) { 68 - throw new ApiError( 69 - "No public URL available for OAuth configuration", 70 - 500, 71 - "Missing DEPLOY_URL or URL environment variables.", 72 - ); 73 - } 49 + 50 + configCache.set(cacheKey, config, 300000); 51 + return config; 52 + } 53 + 54 + // 4. Production oauth config 55 + console.log("Using confidential OAuth client for:", baseUrl); 74 56 75 - console.log("Using confidential OAuth client for:", baseUrl); 57 + const forwardedProto = event?.headers?.["x-forwarded-proto"] || "https"; 58 + baseUrl = host 59 + ? `${forwardedProto}://${host}` 60 + : process.env.DEPLOY_URL || process.env.URL; 76 61 77 - config = { 78 - clientId: `${baseUrl}/oauth-client-metadata.json`, 79 - redirectUri: `${baseUrl}/.netlify/functions/oauth-callback`, 80 - jwksUri: `${baseUrl}/.netlify/functions/jwks`, 81 - clientType: "discoverable", 82 - usePrivateKey: true, 83 - }; 84 - } 62 + const config: OAuthConfig = { 63 + clientId: `${baseUrl}/oauth-client-metadata.json`, 64 + redirectUri: `${baseUrl}/.netlify/functions/oauth-callback`, 65 + jwksUri: `${baseUrl}/.netlify/functions/jwks`, 66 + clientType: "discoverable", 67 + usePrivateKey: true, 68 + }; 85 69 86 70 configCache.set(cacheKey, config, 300000); 87 - 88 71 return config; 89 72 }
+1 -1
netlify/functions/logout.ts
··· 20 20 console.log("[logout] Session ID from cookie:", sessionId); 21 21 22 22 if (sessionId) { 23 - await SessionService.deleteSession(sessionId); 23 + await SessionService.deleteSession(sessionId, event); 24 24 console.log("[logout] Successfully deleted session:", sessionId); 25 25 } 26 26
+6 -4
netlify/functions/oauth-callback.ts
··· 11 11 const config = getOAuthConfig(event); 12 12 const isDev = config.clientType === "loopback"; 13 13 14 - let currentUrl = isDev 15 - ? config.redirectUri.replace("/.netlify/functions/oauth-callback", "") 16 - : config.redirectUri.replace("/.netlify/functions/oauth-callback", ""); 14 + // Land back on the same host you started from 15 + let currentUrl = config.redirectUri.replace( 16 + "/.netlify/functions/oauth-callback", 17 + "", 18 + ); 17 19 18 20 const params = new URLSearchParams(event.rawUrl.split("?")[1] || ""); 19 21 const code = params.get("code"); ··· 29 31 return redirectResponse(`${currentUrl}/?error=Missing OAuth parameters`); 30 32 } 31 33 32 - const client = await createOAuthClient(); 34 + const client = await createOAuthClient(event); 33 35 34 36 const result = await client.callback(params); 35 37
+5 -2
netlify/functions/services/SessionService.ts
··· 68 68 return { agent, did, client }; 69 69 } 70 70 71 - static async deleteSession(sessionId: string): Promise<void> { 71 + static async deleteSession( 72 + sessionId: string, 73 + event?: HandlerEvent, 74 + ): Promise<void> { 72 75 console.log("[SessionService] Deleting session:", sessionId); 73 76 74 77 const userSession = await userSessions.get(sessionId); ··· 80 83 const did = userSession.did; 81 84 82 85 try { 83 - const client = await createOAuthClient(); 86 + const client = await createOAuthClient(event); 84 87 await client.revoke(did); 85 88 console.log("[SessionService] Revoked OAuth session for DID:", did); 86 89 } catch (error) {