1// The jevstrudel Worker, at https://strudel.lmjtfy.fun. Its workers.dev 2// host, the site's address until 2026-10-05, answers every request with a 3// redirect there (moved.ts). The static site (`wrangler deploy --assets`) 4// has its big folders served by Cloudflare's asset layer before this runs; 5// pages and the files at the root come through the Worker first 6// (wrangler.json, run_worker_first), which is what lets the old host 7// redirect them, and fall through to the assets at the end. The paths that 8// are not files: 9// 10// GET /jev/auth/moved a session arriving from the old address (moved.ts) 11// POST /jev/v1/systemone the Jev relay (relay.ts), everywhere 12// POST /jev/votes a "do you agree with Jev?" vote } votes.ts, 13// GET /jev/votes/summary the vote counts } everywhere 14// … /jev/auth/* passkey accounts (auth.ts): register, sign 15// in and out, who is signed in; everywhere 16// GET, POST /jev/me/radio a signed-in listener's radio history (radio.ts) 17// POST /jev/reactions a listener's 🔥 or 😴 } listening.ts, 18// GET /jev/reactions a song's tally, all listeners } everywhere 19// POST /jev/performances a performance, for a replay link } 20// GET /jev/performances/<id> one, to replay it } 21// GET /jev/party/<room> a listening party's WebSocket (party.ts), everywhere 22// GET /jev/parties[/<id>] live parties, and a listed one's room (party.ts) 23// GET /jev/lobby the lobby's WebSocket: who is listening to what 24// (lobby.ts), everywhere 25// GET /jev/data[/*] everything the site stores, for anyone to read 26// (data.ts): people too, never a secret; everywhere 27// … /jev/listeners/* listeners' songs, covers, comments and pitches 28// (content.ts): public to read, signed in to 29// write, screened by Jev first; everywhere 30// POST /jev/mcp the hosted MCP, for everyone's own AI, with an 31// OAuth token (hosted-mcp.ts); its OAuth under 32// /jev/oauth/* and /.well-known/oauth-* (oauth.ts); 33// /jev/me/tabs, a signed-in tab joining its 34// user's hub, and /jev/me/apps, connected apps; 35// everywhere 36// GET /strudel/status open MCP tabs; the page's probe } dev only: 37// GET /strudel/ws a tab's WebSocket to the hub } they need 38// POST /mcp MCP over HTTP (mcp.ts) } the HUB binding 39// 40// Its cron trigger (wrangler.json) runs the work Jev owes listeners' 41// content, screening and scoring, every minute (content-jobs.ts). 42import { auth, AUTH_PREFIX } from './auth'; 43import { content, LISTENERS_PREFIX } from './content'; 44import { data, DATA_PREFIX } from './data'; 45import { sweep } from './content-jobs'; 46import { d1Content } from './content-store'; 47import type { Env } from './env'; 48import { mcp } from './mcp'; 49import { arrived, isOldHost, moved, MOVED_PATH } from './moved'; 50import { oauth } from './oauth'; 51import { radio, RADIO_PATH } from './radio'; 52import { listening, PERFORMANCES_PATH, REACTIONS_PATH } from './listening'; 53import { d1Listening } from './listening-store'; 54import { lobby, LOBBY_PATH } from './lobby'; 55import { parties, PARTIES_PATH, party, PARTY_PREFIX } from './party'; 56import { relay, RELAY_PATH } from './relay'; 57import { refuseCrossOrigin } from './session'; 58import { SUMMARY_PATH, votes, VOTES_PATH } from './votes'; 59import { d1Votes } from './votes-store'; 60 61export { JevBudget } from './budget'; 62export { Hub } from './hub'; 63export { LobbyRoom } from './lobby'; 64export { Party, PartyDirectory } from './party'; 65export { TabHub } from './tab-hub'; 66 67export default { 68 async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { 69 const { pathname, hostname } = new URL(request.url); 70 // the site's old address answers only with the way to the new (moved.ts) 71 if (isOldHost(hostname)) return moved(request, env); 72 // writes only from this site's own pages: never another site's, nor the 73 // sandbox listeners' songs play in (session.ts) 74 const crossOrigin = refuseCrossOrigin(request); 75 if (crossOrigin) return crossOrigin; 76 if (pathname === RELAY_PATH) return relay(request, env, ctx); 77 const hosted = await oauth(request, env, ctx); 78 if (hosted) return hosted; 79 if (pathname === VOTES_PATH || pathname === SUMMARY_PATH) { 80 return votes(request, env, d1Votes(env.DB)); 81 } 82 if (pathname === MOVED_PATH) return arrived(request, env); 83 if (pathname.startsWith(AUTH_PREFIX)) return auth(request, env); 84 if (pathname === RADIO_PATH) return radio(request, env); 85 if (pathname === REACTIONS_PATH || pathname === PERFORMANCES_PATH || pathname.startsWith(`${PERFORMANCES_PATH}/`)) { 86 return listening(request, env, d1Listening(env.DB)); 87 } 88 if (pathname.startsWith(PARTY_PREFIX)) return party(request, env); 89 if (pathname === PARTIES_PATH || pathname.startsWith(`${PARTIES_PATH}/`)) return parties(request, env); 90 if (pathname === LOBBY_PATH) return lobby(request, env); 91 if (pathname.startsWith(LISTENERS_PREFIX)) return content(request, env, ctx); 92 if (pathname === DATA_PREFIX || pathname.startsWith(`${DATA_PREFIX}/`)) return data(request, env); 93 if (pathname === '/mcp') return mcp(request, env); 94 if (env.HUB && pathname === '/strudel/ws') return env.HUB.get(env.HUB.idFromName('hub')).fetch(request); 95 if (env.HUB && pathname === '/strudel/status') { 96 const sessions = await env.HUB.get(env.HUB.idFromName('hub')).sessions(); 97 return Response.json({ active_sessions: sessions }); 98 } 99 // everything else is the static site: pages come through here 100 // (wrangler.json, run_worker_first) so the old address can redirect them 101 return env.ASSETS ? env.ASSETS.fetch(request) : new Response('not found', { status: 404 }); 102 }, 103 104 async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> { 105 ctx.waitUntil(sweep(env, d1Content(env.DB)).then(() => undefined)); 106 }, 107} satisfies ExportedHandler<Env>;