A decentralized music tracking and discovery platform built on AT Protocol 馃幍
rocksky.app
spotify
atproto
lastfm
musicbrainz
scrobbling
listenbrainz
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>;