forked from tangled.org/core
this repo has no description
1export default { 2 async fetch(request, env) { 3 const url = new URL(request.url); 4 const { pathname } = url; 5 6 if (!pathname || pathname === '/') { 7 return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare. 8You can't use this directly unforunately since all requests are signed and may only originate from the appview.`); 9 } 10 11 const cache = caches.default; 12 13 let cacheKey = request.url; 14 let response = await cache.match(cacheKey); 15 if (response) { 16 return response; 17 } 18 19 const pathParts = pathname.slice(1).split('/'); 20 if (pathParts.length < 2) { 21 return new Response('Bad URL', { status: 400 }); 22 } 23 24 const [signatureHex, actor] = pathParts; 25 26 const actorBytes = new TextEncoder().encode(actor); 27 28 const key = await crypto.subtle.importKey( 29 'raw', 30 new TextEncoder().encode(env.AVATAR_SHARED_SECRET), 31 { name: 'HMAC', hash: 'SHA-256' }, 32 false, 33 ['sign', 'verify'], 34 ); 35 36 const computedSigBuffer = await crypto.subtle.sign('HMAC', key, actorBytes); 37 const computedSig = Array.from(new Uint8Array(computedSigBuffer)) 38 .map((b) => b.toString(16).padStart(2, '0')) 39 .join(''); 40 41 console.log({ 42 level: 'debug', 43 message: 'avatar request for: ' + actor, 44 computedSignature: computedSig, 45 providedSignature: signatureHex, 46 }); 47 48 const sigBytes = Uint8Array.from(signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16))); 49 const valid = await crypto.subtle.verify('HMAC', key, sigBytes, actorBytes); 50 51 if (!valid) { 52 return new Response('Invalid signature', { status: 403 }); 53 } 54 55 try { 56 const profileResponse = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, { method: 'GET' }); 57 const profile = await profileResponse.json(); 58 const avatar = profile.avatar; 59 60 if (!avatar) { 61 return new Response(`avatar not found for ${actor}.`, { status: 404 }); 62 } 63 64 // fetch the actual avatar image 65 const avatarResponse = await fetch(avatar); 66 if (!avatarResponse.ok) { 67 return new Response(`failed to fetch avatar for ${actor}.`, { status: avatarResponse.status }); 68 } 69 70 const avatarData = await avatarResponse.arrayBuffer(); 71 const contentType = avatarResponse.headers.get('content-type') || 'image/jpeg'; 72 73 response = new Response(avatarData, { 74 headers: { 75 'Content-Type': contentType, 76 'Cache-Control': 'public, max-age=43200', // 12 h 77 }, 78 }); 79 80 // cache it in cf using request.url as the key 81 await cache.put(cacheKey, response.clone()); 82 83 return response; 84 } catch (error) { 85 return new Response(`error fetching avatar: ${error.message}`, { status: 500 }); 86 } 87 }, 88};