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

Configure Feed

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

fix: cookies fetched with backup strategies, linting + misc

byarielm.fyi 9224f892 c393643d

verified
+151 -109
+47 -47
CONTRIBUTING.md
··· 93 93 ``` 94 94 95 95 3. Generate OAuth Keys 96 - ```bash 97 - # Generate private key 98 - openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem 99 - 100 - # Extract public key 101 - openssl ec -in private-key.pem -pubout -out public-key.pem 102 - 103 - # View private key (copy for .env) 104 - cat private-key.pem 105 - ``` 96 + ```bash 97 + # Generate private key 98 + openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem 99 + 100 + # Extract public key 101 + openssl ec -in private-key.pem -pubout -out public-key.pem 102 + 103 + # View private key (copy for .env) 104 + cat private-key.pem 105 + ``` 106 106 107 107 4. Extract Public Key JWK 108 - ```bash 109 - node -e " 110 - const fs = require('fs'); 111 - const jose = require('jose'); 112 - const pem = fs.readFileSync('public-key.pem', 'utf8'); 113 - jose.importSPKI(pem, 'ES256').then(key => { 114 - return jose.exportJWK(key); 115 - }).then(jwk => { 116 - console.log(JSON.stringify(jwk, null, 2)); 117 - }); 118 - " 119 - ``` 108 + ```bash 109 + node -e " 110 + const fs = require('fs'); 111 + const jose = require('jose'); 112 + const pem = fs.readFileSync('public-key.pem', 'utf8'); 113 + jose.importSPKI(pem, 'ES256').then(key => { 114 + return jose.exportJWK(key); 115 + }).then(jwk => { 116 + console.log(JSON.stringify(jwk, null, 2)); 117 + }); 118 + " 119 + ``` 120 120 121 121 5. Update netlify/functions/jwks.ts 122 122 ··· 124 124 125 125 6. Create .env 126 126 127 - ```bash 128 - VITE_LOCAL_MOCK=false 129 - VITE_API_BASE=/.netlify/functions 130 - 131 - # Database (choose one) 132 - NETLIFY_DATABASE_URL=postgresql://user:pass@host/db # Neon 133 - # NETLIFY_DATABASE_URL=postgresql://localhost/atlast_dev # Local 134 - 135 - # OAuth (paste your private key) 136 - OAUTH_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END PRIVATE KEY-----" 137 - 138 - # Local URLs (MUST use 127.0.0.1 for OAuth) 139 - URL=http://127.0.0.1:8888 140 - DEPLOY_URL=http://127.0.0.1:8888 141 - DEPLOY_PRIME_URL=http://127.0.0.1:8888 142 - CONTEXT=dev 143 - ``` 127 + ```bash 128 + VITE_LOCAL_MOCK=false 129 + VITE_API_BASE=/.netlify/functions 130 + 131 + # Database (choose one) 132 + NETLIFY_DATABASE_URL=postgresql://user:pass@host/db # Neon 133 + # NETLIFY_DATABASE_URL=postgresql://localhost/atlast_dev # Local 134 + 135 + # OAuth (paste your private key) 136 + OAUTH_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END PRIVATE KEY-----" 137 + 138 + # Local URLs (MUST use 127.0.0.1 for OAuth) 139 + URL=http://127.0.0.1:8888 140 + DEPLOY_URL=http://127.0.0.1:8888 141 + DEPLOY_PRIME_URL=http://127.0.0.1:8888 142 + CONTEXT=dev 143 + ``` 144 144 145 145 7. Initialize Database 146 - ```bash 147 - pnpm run init-db 148 - ``` 146 + ```bash 147 + pnpm run init-db 148 + ``` 149 149 150 150 8. Start Development Server 151 - ```bash 152 - npx netlify-cli dev --filter @atlast/web 153 - # Or use the alias: 154 - pnpm run dev 155 - ``` 151 + ```bash 152 + npx netlify-cli dev --filter @atlast/web 153 + # Or use the alias: 154 + pnpm run dev 155 + ``` 156 156 157 157 9. Test OAuth 158 158
+10 -10
docs/git-history.json
··· 1 1 [ 2 2 { 3 - "hash": "15b67054a684ebb2a21761a1774ba15f9b1c29e2", 4 - "short_hash": "15b6705", 3 + "hash": "18f4ca7017ea6ca5d959f5d9596690c17b171bc3", 4 + "short_hash": "18f4ca7", 5 5 "author": "Ariel M. Lighty", 6 - "date": "2025-12-28T20:38:38-05:00", 7 - "message": "fix: add health check function for extension server detection\n\n- Created /health function endpoint with CORS support\n- Updated checkServerHealth to use function endpoint instead of root URL\n- Fixes Firefox extension server detection with proper CORS headers", 8 - "files_changed": 5 6 + "date": "2025-12-28T21:38:11-05:00", 7 + "message": "fix: pass event to errorResponse for proper CORS on errors\n\n- Error middleware now passes event parameter to errorResponse\n- Fixes Firefox extension CORS headers on authentication errors\n- Both withErrorHandling and withAuthErrorHandling updated\n- Extension origin properly reflected in all error responses", 8 + "files_changed": 3 9 9 }, 10 10 { 11 11 "hash": "603cf0a187850664336a12c9e5cbb49038906f53", ··· 16 16 "files_changed": 4 17 17 }, 18 18 { 19 - "hash": "bd3aabb75abb1875aef125610fcdccb14967a8e3", 20 - "short_hash": "bd3aabb", 19 + "hash": "603cf0a187850664336a12c9e5cbb49038906f53", 20 + "short_hash": "603cf0a", 21 21 "author": "Ariel M. Lighty", 22 - "date": "2025-12-27T22:10:11-05:00", 23 - "message": "fix: extension dark mode and build mode messaging\n\n- Changed darkMode from 'class' to 'media' for automatic system preference detection\n- Made server offline message conditional on build mode (dev vs prod)\n- Hide dev server instructions in production builds", 24 - "files_changed": 5 22 + "date": "2025-12-27T22:42:43-05:00", 23 + "message": "fix: CORS for extension credentialed requests\n\nUpdated CORS headers to support credentials from Chrome extensions:\n- Added getCorsHeaders() to detect chrome-extension:// origins\n- Changed from wildcard Access-Control-Allow-Origin to specific origin\n- Added Access-Control-Allow-Credentials: true for credentialed requests\n- Updated session endpoint to pass event for CORS header detection", 24 + "files_changed": 4 25 25 }, 26 26 { 27 27 "hash": "bd3aabb75abb1875aef125610fcdccb14967a8e3",
+6 -6
docs/graph-data.json
··· 4329 4329 "node_type": "observation", 4330 4330 "title": "CORS fully working - Firefox extension origin properly reflected with credentials. But cookies not sent from extension despite credentials:include. Cookie set in web context not accessible from extension context due to Firefox cookie partitioning.", 4331 4331 "description": null, 4332 - "status": "pending", 4332 + "status": "completed", 4333 4333 "created_at": "2025-12-28T21:46:45.822343200-05:00", 4334 - "updated_at": "2025-12-28T21:46:45.822343200-05:00", 4334 + "updated_at": "2025-12-28T22:51:46.665792900-05:00", 4335 4335 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 4336 4336 }, 4337 4337 { ··· 4340 4340 "node_type": "action", 4341 4341 "title": "Updated extension checkSession to read cookie via browser.cookies API and pass as query parameter. Workaround for Firefox SameSite=Lax cookie partitioning.", 4342 4342 "description": null, 4343 - "status": "pending", 4343 + "status": "completed", 4344 4344 "created_at": "2025-12-28T21:52:22.059862700-05:00", 4345 - "updated_at": "2025-12-28T21:52:22.059862700-05:00", 4345 + "updated_at": "2025-12-28T22:51:46.765539200-05:00", 4346 4346 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 4347 4347 }, 4348 4348 { ··· 4351 4351 "node_type": "outcome", 4352 4352 "title": "Extension now uses browser.cookies.get() API to read session cookie and pass as query parameter. Workaround for Firefox SameSite=Lax cookie partitioning in extensions. Extension rebuilt successfully.", 4353 4353 "description": null, 4354 - "status": "pending", 4354 + "status": "completed", 4355 4355 "created_at": "2025-12-28T22:51:31.578965200-05:00", 4356 - "updated_at": "2025-12-28T22:51:31.578965200-05:00", 4356 + "updated_at": "2025-12-28T22:51:46.868827600-05:00", 4357 4357 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 4358 4358 } 4359 4359 ],
+1 -1
netlify.toml
··· 10 10 targetPort = 5173 11 11 port = 8888 12 12 functionsPort = 9999 13 - autoLaunch = false 13 + autoLaunch = true 14 14 15 15 [[redirects]] 16 16 from = "/oauth-client-metadata.json"
+2 -2
package.json
··· 1 1 { 2 2 "name": "@atlast/root", 3 - "homepage": "https://byarielm.github.io/ATlast", 3 + "homepage": "https://atlast.byarielm.fyi", 4 4 "private": true, 5 5 "version": "0.0.1", 6 6 "type": "module", 7 7 "scripts": { 8 8 "dev": "npx netlify-cli dev --filter @atlast/web", 9 - "dev:mock": "pnpm --filter @atlast/web dev", 9 + "dev:mock": "pnpm dev:mock --filter @atlast/web", 10 10 "dev:full": "npx netlify-cli dev --filter @atlast/web", 11 11 "build": "pnpm --filter @atlast/web build", 12 12 "init-db": "tsx scripts/init-local-db.ts",
+3 -10
packages/extension/manifest.firefox.json
··· 3 3 "name": "ATlast Importer", 4 4 "version": "1.0.0", 5 5 "description": "Import your Twitter/X follows to find them on Bluesky", 6 - "permissions": [ 7 - "activeTab", 8 - "storage", 9 - "cookies" 10 - ], 6 + "permissions": ["activeTab", "storage", "cookies"], 11 7 "host_permissions": [ 12 8 "https://twitter.com/*", 13 9 "https://x.com/*", ··· 21 17 }, 22 18 "content_scripts": [ 23 19 { 24 - "matches": [ 25 - "https://twitter.com/*", 26 - "https://x.com/*" 27 - ], 20 + "matches": ["https://twitter.com/*", "https://x.com/*"], 28 21 "js": ["content/index.js"], 29 22 "run_at": "document_idle" 30 23 } ··· 44 37 }, 45 38 "browser_specific_settings": { 46 39 "gecko": { 47 - "id": "atlast-importer@byarielm.fyi", 40 + "id": "atlast-importer-dev@byarielm.fyi", 48 41 "strict_min_version": "109.0" 49 42 } 50 43 }
+82 -33
packages/extension/src/lib/api-client.ts
··· 2 2 * ATlast API client for extension 3 3 */ 4 4 5 - import browser from 'webextension-polyfill'; 5 + import browser from "webextension-polyfill"; 6 6 7 7 // These are replaced at build time by esbuild 8 8 declare const __ATLAST_API_URL__: string; ··· 35 35 * Upload scraped usernames to ATlast 36 36 */ 37 37 export async function uploadToATlast( 38 - request: ExtensionImportRequest 38 + request: ExtensionImportRequest, 39 39 ): Promise<ExtensionImportResponse> { 40 40 const url = `${ATLAST_API_URL}/.netlify/functions/extension-import`; 41 41 42 42 try { 43 43 const response = await fetch(url, { 44 - method: 'POST', 45 - credentials: 'include', // Include cookies for auth 44 + method: "POST", 45 + credentials: "include", // Include cookies for auth 46 46 headers: { 47 - 'Content-Type': 'application/json' 47 + "Content-Type": "application/json", 48 48 }, 49 - body: JSON.stringify(request) 49 + body: JSON.stringify(request), 50 50 }); 51 51 52 52 if (!response.ok) { ··· 55 55 } 56 56 57 57 // Backend wraps response in ApiResponse structure: { success: true, data: {...} } 58 - const apiResponse: { success: boolean; data: ExtensionImportResponse } = await response.json(); 58 + const apiResponse: { success: boolean; data: ExtensionImportResponse } = 59 + await response.json(); 59 60 return apiResponse.data; 60 61 } catch (error) { 61 - console.error('[API Client] Upload error:', error); 62 + console.error("[API Client] Upload error:", error); 62 63 throw error instanceof Error 63 64 ? error 64 - : new Error('Failed to upload to ATlast'); 65 + : new Error("Failed to upload to ATlast"); 65 66 } 66 67 } 67 68 ··· 82 83 const controller = new AbortController(); 83 84 const timeoutId = setTimeout(() => controller.abort(), 3000); 84 85 85 - const response = await fetch(`${ATLAST_API_URL}/.netlify/functions/health`, { 86 - method: 'GET', 87 - signal: controller.signal, 88 - credentials: 'include', // Include for CORS 89 - }); 86 + const response = await fetch( 87 + `${ATLAST_API_URL}/.netlify/functions/health`, 88 + { 89 + method: "GET", 90 + signal: controller.signal, 91 + credentials: "include", // Include for CORS 92 + }, 93 + ); 90 94 91 95 clearTimeout(timeoutId); 92 96 93 97 // Any successful response means server is running 94 98 return response.ok; 95 99 } catch (error) { 96 - console.error('[API Client] Server health check failed:', error); 100 + console.error("[API Client] Server health check failed:", error); 97 101 return false; 98 102 } 99 103 } ··· 116 120 avatar?: string; 117 121 } | null> { 118 122 try { 119 - // Try to get session cookie using browser.cookies API 120 - // This works around Firefox's cookie partitioning for extensions 123 + console.log("[API Client] Checking session..."); 124 + 121 125 let sessionId: string | null = null; 122 126 127 + // Strategy 1: browser.cookies API 123 128 try { 124 - const cookieName = __BUILD_MODE__ === 'production' ? 'atlast_session' : 'atlast_session_dev'; 125 - const cookie = await browser.cookies.get({ 126 - url: ATLAST_API_URL, 127 - name: cookieName 128 - }); 129 + const cookieName = 130 + __BUILD_MODE__ === "production" 131 + ? "atlast_session" 132 + : "atlast_session_dev"; 133 + 134 + const urls = [ 135 + ATLAST_API_URL, 136 + "http://127.0.0.1:8888", 137 + "http://localhost:8888", 138 + ]; 129 139 130 - if (cookie) { 131 - sessionId = cookie.value; 132 - console.log('[API Client] Found session cookie:', cookieName); 140 + for (const url of urls) { 141 + try { 142 + const cookie = await browser.cookies.get({ 143 + url, 144 + name: cookieName, 145 + }); 146 + if (cookie?.value) { 147 + sessionId = cookie.value; 148 + console.log( 149 + "[API Client] Found session cookie via browser.cookies API", 150 + ); 151 + break; 152 + } 153 + } catch (err) { 154 + console.log(`[API Client] Failed to read cookie from ${url}:`, err); 155 + } 133 156 } 134 157 } catch (cookieError) { 135 - console.log('[API Client] Could not read cookie:', cookieError); 158 + console.log("[API Client] browser.cookies failed:", cookieError); 159 + } 160 + 161 + // Strategy 2: document.cookie if we have content script context 162 + if (!sessionId && typeof document !== "undefined") { 163 + const cookieName = 164 + __BUILD_MODE__ === "production" 165 + ? "atlast_session" 166 + : "atlast_session_dev"; 167 + const cookies = document.cookie.split(";"); 168 + const sessionCookie = cookies.find((c) => 169 + c.trim().startsWith(`${cookieName}=`), 170 + ); 171 + if (sessionCookie) { 172 + sessionId = sessionCookie.split("=")[1]; 173 + console.log("[API Client] Found session cookie via document.cookie"); 174 + } 136 175 } 137 176 138 177 // Build URL with session parameter if we have one ··· 140 179 ? `${ATLAST_API_URL}/.netlify/functions/session?session=${sessionId}` 141 180 : `${ATLAST_API_URL}/.netlify/functions/session`; 142 181 182 + console.log("[API Client] Checking session at:", url); 183 + 143 184 const response = await fetch(url, { 144 - method: 'GET', 145 - credentials: 'include', // Include cookies as fallback 185 + method: "GET", 186 + credentials: "include", 146 187 headers: { 147 - 'Accept': 'application/json' 148 - } 188 + Accept: "application/json", 189 + ...(sessionId 190 + ? { 191 + Cookie: `${__BUILD_MODE__ === "production" ? "atlast_session" : "atlast_session_dev"}=${sessionId}`, 192 + } 193 + : {}), 194 + }, 149 195 }); 150 196 151 197 if (!response.ok) { 152 - console.log('[API Client] Not logged in'); 198 + console.log("[API Client] Session check failed:", response.status); 153 199 return null; 154 200 } 155 201 156 - // Backend wraps response in ApiResponse structure: { success: true, data: {...} } 157 202 const apiResponse: { success: boolean; data: any } = await response.json(); 203 + console.log( 204 + "[API Client] Session check succeeded:", 205 + apiResponse.data.handle, 206 + ); 158 207 return apiResponse.data; 159 208 } catch (error) { 160 - console.error('[API Client] Session check failed:', error); 209 + console.error("[API Client] Session check failed:", error); 161 210 return null; 162 211 } 163 212 }