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

fix oauth for netlify dev --live mode :| also attempted to fix weird bars in mobiel safari but failed RIP

byarielm.fyi 083ea36c f086236e

verified
Changed files
+199 -83
dist
netlify
functions
core
middleware
infrastructure
oauth
services
src
+35 -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-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> 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 20 + name="viewport" 21 + content="width=device-width, initial-scale=1.0, viewport-fit=cover" 22 + /> 23 + <meta name="apple-mobile-web-app-capable" content="yes" /> 24 + <meta 25 + name="apple-mobile-web-app-status-bar-style" 26 + content="black-translucent" 27 + /> 28 + <title>ATLast: Find Your People in the ATmosphere</title> 29 + <script type="module" crossorigin src="/assets/index-D7a6vDuT.js"></script> 30 + <link rel="stylesheet" crossorigin href="/assets/index-CIYGhL08.css"> 31 + </head> 32 + <body> 33 + <div id="root"></div> 34 + </body> 35 + </html>
+10 -5
index.html
··· 16 16 /> 17 17 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 18 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> 19 + <meta 20 + name="viewport" 21 + content="width=device-width, initial-scale=1.0, viewport-fit=cover" 22 + /> 23 + <meta name="apple-mobile-web-app-capable" content="yes" /> 24 + <meta 25 + name="apple-mobile-web-app-status-bar-style" 26 + content="black-translucent" 27 + /> 28 + <title>ATLast: Find Your People in the ATmosphere</title> 24 29 </head> 25 30 <body> 26 31 <div id="root"></div>
+7 -1
netlify/functions/core/middleware/error.middleware.ts
··· 12 12 try { 13 13 return await handler(event); 14 14 } catch (error) { 15 - console.error("Handler error:", error); 15 + console.error( 16 + "Handler error:", 17 + error instanceof Error ? error.message : String(error), 18 + ); 19 + if (error instanceof Error && error.stack) { 20 + console.error("Stack trace:", error.stack); 21 + } 16 22 17 23 if (error instanceof ApiError) { 18 24 return errorResponse(error.message, error.statusCode, error.details);
+30 -8
netlify/functions/infrastructure/oauth/config.ts
··· 1 1 import { OAuthConfig } from "../../core/types"; 2 - import { ApiError } from "../../core/errors"; 3 2 import { configCache } from "../cache/CacheService"; 4 3 import { CONFIG } from "../../core/config/constants"; 5 4 ··· 8 7 }): OAuthConfig { 9 8 // 1. Determine host dynamically 10 9 const host = event?.headers?.host; 11 - const cacheKey = `oauth-config-${host || "default"}`; 10 + console.log("[oauth-config] Host from headers:", host); 11 + console.log("[oauth-config] All relevant headers:", { 12 + host: event?.headers?.host, 13 + "x-forwarded-host": event?.headers?.["x-forwarded-host"], 14 + "x-forwarded-proto": event?.headers?.["x-forwarded-proto"], 15 + "x-nf-deploy-context": event?.headers?.["x-nf-deploy-context"], 16 + }); 12 17 18 + const cacheKey = `oauth-config-${host || "default"}`; 13 19 const cached = configCache.get(cacheKey) as OAuthConfig | undefined; 14 20 if (cached) { 15 21 return cached; ··· 17 23 18 24 let baseUrl: string | undefined; 19 25 20 - // 2. Determine if local based on host header 26 + // 2. Check if we're in Netlify Live mode (--live) 27 + // In --live mode, DEPLOY_URL will be the tunnel URL even though host header is localhost 28 + const deployUrl = process.env.DEPLOY_URL || process.env.URL; 29 + const isNetlifyLive = deployUrl?.includes(".netlify.live"); 30 + 31 + // 3. Determine if local based on host header AND not in live mode 21 32 const isLocal = 22 - !host || host.includes("localhost") || host.includes("127.0.0.1"); 33 + !isNetlifyLive && 34 + (!host || host.includes("localhost") || host.includes("127.0.0.1")); 23 35 24 36 // 3. Local oauth config 25 37 if (isLocal) { ··· 51 63 return config; 52 64 } 53 65 54 - // 4. Production oauth config 66 + // 4. Production + Live oauth config 55 67 console.log("Using confidential OAuth client for:", baseUrl); 56 68 57 69 const forwardedProto = event?.headers?.["x-forwarded-proto"] || "https"; 58 - baseUrl = host 59 - ? `${forwardedProto}://${host}` 60 - : process.env.DEPLOY_URL || process.env.URL; 70 + // If we're in Netlify Live mode, use the DEPLOY_URL (tunnel URL) 71 + // Otherwise use the host header 72 + if (isNetlifyLive) { 73 + baseUrl = deployUrl; 74 + console.log("Using Netlify Live tunnel for OAuth:", baseUrl); 75 + } else { 76 + baseUrl = host ? `${forwardedProto}://${host}` : deployUrl; 77 + console.log("Using confidential OAuth client for:", baseUrl); 78 + } 79 + 80 + if (!baseUrl) { 81 + throw new Error("No base URL available for OAuth configuration"); 82 + } 61 83 62 84 const config: OAuthConfig = { 63 85 clientId: `${baseUrl}/oauth-client-metadata.json`,
+20 -4
netlify/functions/oauth-callback.ts
··· 50 50 51 51 console.log("[oauth-callback] Created user session:", sessionId); 52 52 53 - const cookieName = isDev ? "atlast_session_dev" : "atlast_session"; 54 - const cookieFlags = isDev 55 - ? `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/` 56 - : `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/; Secure`; 53 + // Determine cookie configuration 54 + // Use DEPLOY_URL to detect Netlify Live mode 55 + const isNetlifyLive = (process.env.DEPLOY_URL || process.env.URL)?.includes( 56 + ".netlify.live", 57 + ); 58 + const isSecure = currentUrl.startsWith("https://") || isNetlifyLive; 59 + 60 + // Use dev cookie for development, otherwise production cookie 61 + const cookieName = 62 + isDev && !isNetlifyLive ? "atlast_session_dev" : "atlast_session"; 63 + const cookieFlags = isSecure 64 + ? `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/; Secure` 65 + : `HttpOnly; SameSite=Lax; Max-Age=${CONFIG.COOKIE_MAX_AGE}; Path=/`; 66 + 67 + console.log( 68 + "[oauth-callback] Setting cookie:", 69 + cookieName, 70 + "for URL:", 71 + currentUrl, 72 + ); 57 73 58 74 return redirectResponse( 59 75 `${currentUrl}/?session=${sessionId}`,
+58 -17
netlify/functions/oauth-start.ts
··· 2 2 import { createOAuthClient } from "./infrastructure/oauth/OAuthClientFactory"; 3 3 import { successResponse } from "./utils"; 4 4 import { withErrorHandling } from "./core/middleware"; 5 - import { ValidationError } from "./core/errors"; 6 - import { CONFIG } from "./core/config/constants"; 5 + import { ValidationError, ApiError } from "./core/errors"; 7 6 8 7 interface OAuthStartRequestBody { 9 8 login_hint?: string; ··· 13 12 const oauthStartHandler: SimpleHandler = async (event) => { 14 13 let loginHint: string | undefined = undefined; 15 14 16 - if (event.body) { 17 - const parsed: OAuthStartRequestBody = JSON.parse(event.body); 18 - loginHint = parsed.login_hint; 19 - } 15 + try { 16 + if (event.body) { 17 + const parsed: OAuthStartRequestBody = JSON.parse(event.body); 18 + loginHint = parsed.login_hint; 19 + } 20 20 21 - if (!loginHint) { 22 - throw new ValidationError("login_hint (handle or DID) is required"); 23 - } 21 + if (!loginHint) { 22 + throw new ValidationError("login_hint (handle or DID) is required"); 23 + } 24 24 25 - console.log("[oauth-start] Starting OAuth flow for:", loginHint); 25 + console.log("[oauth-start] Starting OAuth flow for:", loginHint); 26 26 27 - const client = await createOAuthClient(event); 27 + let client; 28 + try { 29 + client = await createOAuthClient(event); 30 + console.log("[oauth-start] OAuth client created successfully"); 31 + } catch (clientError) { 32 + console.error( 33 + "[oauth-start] Failed to create OAuth client:", 34 + clientError instanceof Error 35 + ? clientError.message 36 + : String(clientError), 37 + ); 38 + throw new ApiError( 39 + "Failed to create OAuth client", 40 + 500, 41 + clientError instanceof Error ? clientError.message : "Unknown error", 42 + ); 43 + } 28 44 29 - const authUrl = await client.authorize(loginHint, { 30 - scope: CONFIG.OAUTH_SCOPES, 31 - }); 45 + let authUrl; 46 + try { 47 + authUrl = await client.authorize(loginHint, { 48 + scope: "atproto transition:generic", 49 + }); 50 + console.log("[oauth-start] Generated auth URL successfully"); 51 + } catch (authorizeError) { 52 + console.error( 53 + "[oauth-start] Failed to authorize:", 54 + authorizeError instanceof Error 55 + ? authorizeError.message 56 + : String(authorizeError), 57 + ); 58 + throw new ApiError( 59 + "Failed to generate authorization URL", 60 + 500, 61 + authorizeError instanceof Error 62 + ? authorizeError.message 63 + : "Unknown error", 64 + ); 65 + } 32 66 33 - console.log("[oauth-start] Generated auth URL for:", loginHint); 34 - 35 - return successResponse({ url: authUrl.toString() }); 67 + console.log("[oauth-start] Returning auth URL for:", loginHint); 68 + return successResponse({ url: authUrl.toString() }); 69 + } catch (error) { 70 + // This will be caught by withErrorHandling, but log it here too for clarity 71 + console.error( 72 + "[oauth-start] Top-level error:", 73 + error instanceof Error ? error.message : String(error), 74 + ); 75 + throw error; 76 + } 36 77 }; 37 78 38 79 export const handler = withErrorHandling(oauthStartHandler);
+27 -14
netlify/functions/services/SessionService.ts
··· 40 40 console.log("[SessionService] Found user session for DID:", did); 41 41 42 42 // Cache the OAuth client per session for 5 minutes 43 - const cacheKey = `oauth-client-${sessionId}`; 43 + const host = event.headers?.host || "default"; 44 + const cacheKey = `oauth-client-${sessionId}-${host}`; 44 45 let client = configCache.get(cacheKey) as NodeOAuthClient | null; 45 46 46 47 if (!client) { ··· 51 52 console.log("[SessionService] Using cached OAuth client"); 52 53 } 53 54 54 - const oauthSession = await client.restore(did); 55 - console.log("[SessionService] Restored OAuth session for DID:", did); 55 + try { 56 + const oauthSession = await client.restore(did); 57 + console.log("[SessionService] Restored OAuth session for DID:", did); 58 + 59 + // Log token rotation for monitoring 60 + // The restore() call automatically refreshes if needed 61 + const sessionData = await sessionStore.get(did); 62 + if (sessionData) { 63 + // Token refresh happens transparently in restore() 64 + // Just log for monitoring purposes 65 + console.log("[SessionService] OAuth session restored/refreshed"); 66 + } 56 67 57 - // Log token rotation for monitoring 58 - // The restore() call automatically refreshes if needed 59 - const sessionData = await sessionStore.get(did); 60 - if (sessionData) { 61 - // Token refresh happens transparently in restore() 62 - // Just log for monitoring purposes 63 - console.log("[SessionService] OAuth session restored/refreshed"); 68 + const agent = new Agent(oauthSession); 69 + return { agent, did, client }; 70 + } catch (error) { 71 + console.error( 72 + "[SessionService] Failed to restore session:", 73 + error instanceof Error ? error.message : String(error), 74 + ); 75 + // Clear the cached client if restore fails - it might be stale or misconfigured 76 + configCache.delete(cacheKey); 77 + throw new AuthenticationError( 78 + "Failed to restore OAuth session", 79 + error instanceof Error ? error.message : "Session restoration failed", 80 + ); 64 81 } 65 - 66 - const agent = new Agent(oauthSession); 67 - 68 - return { agent, did, client }; 69 82 } 70 83 71 84 static async deleteSession(
+12 -4
src/index.css
··· 19 19 font-family: "Rubik", "Fira Sans", sans-serif; 20 20 } 21 21 22 + html, 23 + body { 24 + min-height: 100vh; 25 + min-height: -webkit-fill-available; 26 + } 27 + 22 28 body { 23 29 @apply bg-gradient-to-br 24 - from-cyan-50 via-purple-50 to-pink-50 25 - dark:from-indigo-950 dark:via-purple-900 dark:to-slate-900 26 - text-slate-900 dark:text-slate-100 27 - transition-colors duration-300; 30 + from-cyan-50 via-purple-50 to-pink-50 31 + dark:from-indigo-950 dark:via-purple-900 dark:to-slate-900 32 + text-slate-900 dark:text-slate-100 33 + transition-colors duration-300; 34 + padding: env(safe-area-inset-top) env(safe-area-inset-right) 35 + env(safe-area-inset-bottom) env(safe-area-inset-left); 28 36 } 29 37 30 38 button {