A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
at main 162 lines 5.5 kB view raw
1/** 2 * Welcome to Cloudflare Workers! This is your first worker. 3 * 4 * - Run `npm run dev` in your terminal to start a development server 5 * - Open a browser tab at http://localhost:8787/ to see your worker in action 6 * - Run `npm run deploy` to publish your worker 7 * 8 * Bind resources to your worker in `wrangler.json`. After adding bindings, a type definition for the 9 * `Env` object can be regenerated with `npm run cf-typegen`. 10 * 11 * Learn more at https://developers.cloudflare.com/workers/ 12 */ 13 14import { fetchOgData, HeadMeta, isHtmlResponse, StripMeta } from './html-rewriter'; 15 16const metadata = { 17 redirect_uris: ['https://rocksky.app/oauth/callback'], 18 response_types: ['code'], 19 grant_types: ['authorization_code', 'refresh_token'], 20 scope: 21 'atproto repo:app.rocksky.album repo:app.rocksky.artist repo:app.rocksky.graph.follow repo:app.rocksky.like repo:app.rocksky.playlist repo:app.rocksky.scrobble repo:app.rocksky.shout repo:app.rocksky.song repo:app.rocksky.feed.generator repo:fm.teal.alpha.feed.play repo:fm.teal.alpha.actor.status', 22 token_endpoint_auth_method: 'private_key_jwt', 23 token_endpoint_auth_signing_alg: 'ES256', 24 jwks_uri: 'https://rocksky.app/jwks.json', 25 application_type: 'web', 26 client_id: 'https://rocksky.app/oauth-client-metadata.json', 27 client_name: 'Rocksky', 28 client_uri: 'https://rocksky.app', 29 dpop_bound_access_tokens: true, 30}; 31 32const jwks = { 33 keys: [ 34 { 35 kty: 'EC', 36 use: 'sig', 37 alg: 'ES256', 38 kid: '2dfa3fd9-57b3-4738-ac27-9e6dadec13b7', 39 crv: 'P-256', 40 x: 'V_00KDnoEPsNqbt0y2Ke8v27Mv9WP70JylDUD5rvIek', 41 y: 'HAyjaQeA2DU6wjZO0ggTadUS6ij1rmiYTxzmWeBKfRc', 42 }, 43 { 44 kty: 'EC', 45 use: 'sig', 46 alg: 'ES256', 47 kid: '5e816ff2-6bff-4177-b1c0-67ad3cd3e7cd', 48 crv: 'P-256', 49 x: 'YwEY5NsoYQVB_G7xPYMl9sUtxRbcPFNffnZcTS5nbPQ', 50 y: '5n5mybPvISyYAnRv1Ii1geqKfXv2GA8p9Xemwx2a8CM', 51 }, 52 { 53 kty: 'EC', 54 use: 'sig', 55 kid: 'a1067a48-a54a-43a0-9758-4d55b51fdd8b', 56 crv: 'P-256', 57 x: 'yq17Nd2DGcjP1i9I0NN3RBmgSbLQUZOtG6ec5GaqzmU', 58 y: 'ieIU9mcfaZwAW5b3WgJkIRgddymG_ckcZ0n1XjbEIvc', 59 }, 60 ], 61}; 62 63export default { 64 async fetch(request, env, ctx): Promise<Response> { 65 const url = new URL(request.url); 66 let redirectToApi = false; 67 68 const API_ROUTES = ['/login', '/profile', '/token', '/now-playing', '/ws', '/oauth-client-metadata.json', '/jwks.json']; 69 70 console.log('Request URL:', url.pathname, url.pathname === '/client-metadata.json'); 71 72 if (url.pathname === '/oauth-client-metadata.json') { 73 return Response.json(metadata); 74 } 75 76 if (url.pathname === '/jwks.json') { 77 return Response.json(jwks); 78 } 79 80 if ( 81 API_ROUTES.includes(url.pathname) || 82 url.pathname.startsWith('/oauth/callback') || 83 url.pathname.startsWith('/users') || 84 url.pathname.startsWith('/albums') || 85 url.pathname.startsWith('/artists') || 86 url.pathname.startsWith('/tracks') || 87 url.pathname.startsWith('/scrobbles') || 88 url.pathname.startsWith('/likes') || 89 url.pathname.startsWith('/spotify') || 90 url.pathname.startsWith('/dropbox/oauth/callback') || 91 url.pathname.startsWith('/googledrive/oauth/callback') || 92 url.pathname.startsWith('/dropbox/files') || 93 url.pathname.startsWith('/dropbox/file') || 94 url.pathname.startsWith('/googledrive/files') || 95 url.pathname.startsWith('/dropbox/login') || 96 url.pathname.startsWith('/googledrive/login') || 97 url.pathname.startsWith('/dropbox/join') || 98 url.pathname.startsWith('/googledrive/join') || 99 url.pathname.startsWith('/search') || 100 url.pathname.startsWith('/public/scrobbles') 101 ) { 102 redirectToApi = true; 103 } 104 105 if (redirectToApi) { 106 const proxyUrl = new URL(request.url); 107 proxyUrl.host = 'api.rocksky.app'; 108 proxyUrl.hostname = 'api.rocksky.app'; 109 return fetch(proxyUrl, request) as any; 110 } 111 112 // check header if from mobile device, android or ios 113 const userAgent = request.headers.get('user-agent'); 114 const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; 115 const isMobile = mobileRegex.test(userAgent!); 116 117 if (isMobile) { 118 const mobileUrl = new URL(request.url); 119 mobileUrl.host = 'm.rocksky.app'; 120 mobileUrl.hostname = 'm.rocksky.app'; 121 const htmlRes = await fetch(mobileUrl, request); 122 if (!htmlRes.ok || !isHtmlResponse(htmlRes)) { 123 return htmlRes; 124 } 125 126 const og = await fetchOgData(url, request); 127 if (!og) return htmlRes; 128 const headers = new Headers(htmlRes.headers); 129 headers.set('cache-control', 'public, max-age=300'); 130 131 const rewritten = new HTMLRewriter() 132 .on('meta[property^="og:"]', new StripMeta()) 133 .on('meta[name^="twitter:"]', new StripMeta()) 134 .on('head', new HeadMeta(og)) 135 .transform(htmlRes); 136 137 return new Response(rewritten.body, { status: htmlRes.status, headers }); 138 } 139 140 const proxyUrl = new URL(request.url); 141 proxyUrl.host = 'rocksky.pages.dev'; 142 proxyUrl.hostname = 'rocksky.pages.dev'; 143 const htmlRes = await fetch(proxyUrl, request); 144 if (!htmlRes.ok || !isHtmlResponse(htmlRes)) { 145 return htmlRes; 146 } 147 148 const og = await fetchOgData(url, request); 149 if (!og) return htmlRes; 150 151 const headers = new Headers(htmlRes.headers); 152 headers.set('cache-control', 'public, max-age=300'); 153 154 const rewritten = new HTMLRewriter() 155 .on('meta[property^="og:"]', new StripMeta()) 156 .on('meta[name^="twitter:"]', new StripMeta()) 157 .on('head', new HeadMeta(og)) 158 .transform(htmlRes); 159 160 return new Response(rewritten.body, { status: htmlRes.status, headers }); 161 }, 162} satisfies ExportedHandler<Env>;