jevstrudel.git / worker / src / moved.ts
moved.tsannotatedmoved.tssource100 lines · 5.1 KB · raw

The site's address. It is https://strudel.lmjtfy.fun (wrangler.json's custom domain, SITE_HOST); it was the Worker's own workers.dev host until 2026-10-05, and that host still reaches this Worker, which answers there with nothing but the way to the new address:

everything 301 (GET, HEAD) or 308 to the same path and query on SITE_HOST /sw.js a service worker that removes itself. A returning visitor's browser holds the old site in its cache and would never ask for a page, so never see the redirect; it does ask for /sw.js, and a redirect there is refused by browsers, which keeps the old worker. This one unregisters, empties the caches and reloads its tabs, which then meet the 301. a signed-in page 302 through /jev/auth/moved on SITE_HOST with a one-time token: the session cookie is this host's only, and the account's passkeys were made here and sign in here only (auth.ts), so without it an account from before the move could never be reached again. The token is taken there once, within a minute, for a session on the new host, and this host's session ends with it. The page then asks for a passkey for the new address (passkeyHere in /jev/auth/me).

Nothing else is answered on the old host: no relay, no sign-in, no MCP.

27import { d1Accounts, type Accounts } from './accounts-store';
28import type { Env } from './env';
29import { newSessionToken, SESSION_TTL_MS, sessionCookie, sessionToken } from './session';
30import config from '../wrangler.json';
32export const SITE_HOST = config.routes[0].pattern;
33export const MOVED_PATH = '/jev/auth/moved';
34export const HANDOFF_TTL_MS = 60 * 1000;

The host the site moved from: this Worker's workers.dev name, and any preview of it. Dev (localhost) and the site's own host are neither.

38export const isOldHost = (hostname: string) => hostname.endsWith('.workers.dev');
40const REMOVE_SERVICE_WORKER = `// jevstrudel moved to https://${SITE_HOST}/
41self.addEventListener('install', () => self.skipWaiting());
42self.addEventListener('activate', (event) => {
43  event.waitUntil(
44    (async () => {
45      await self.registration.unregister();
46      for (const key of await caches.keys()) await caches.delete(key);
47      for (const client of await self.clients.matchAll({ type: 'window' })) client.navigate(client.url);
48    })(),
49  );
50});
51`;

A request to the old host: where it went.

54export async function moved(request: Request, env: Env, accounts: Accounts = d1Accounts(env.DB)): Promise<Response> {
55  const url = new URL(request.url);
56  const page = request.method === 'GET' || request.method === 'HEAD';
57  if (page && url.pathname === '/sw.js') {
58    return new Response(REMOVE_SERVICE_WORKER, {
59      headers: { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' },
60    });
61  }
62  const there = `https://${SITE_HOST}`;
63  const token = page ? sessionToken(request) : null;
64  // only a navigation carries a session across: a page's own fetches would
65  // each spend a token nothing follows
66  if (token && request.headers.get('Sec-Fetch-Mode') === 'navigate') {
67    const user = await accounts.sessionUser(token);
68    if (user) {
69      const handoff = newSessionToken();
70      await accounts.putHandoff(user.id, handoff, token, HANDOFF_TTL_MS);
71      const to = new URL(MOVED_PATH, there);
72      to.searchParams.set('token', handoff);
73      to.searchParams.set('to', url.pathname + url.search);
74      return new Response(null, { status: 302, headers: { Location: to.href, 'Cache-Control': 'no-store' } });
75    }
76  }
77  return new Response(null, {
78    status: page ? 301 : 308,
79    headers: { Location: there + url.pathname + url.search },
80  });
81}

A path on this site, and nothing that leaves it: "/x", never "//host" or "/\host", which browsers read as another site.

85const sitePath = (to: string | null) => (to && /^\/(?![/\\])/.test(to) ? to : '/');

GET /jev/auth/moved?token=…&to=…, on the site: the handoff taken for a session here. A token that is unknown, used or expired signs nobody in; either way the visitor lands on the page they asked for.

90export async function arrived(request: Request, env: Env, accounts: Accounts = d1Accounts(env.DB)): Promise<Response> {
91  const url = new URL(request.url);
92  const headers = new Headers({ Location: sitePath(url.searchParams.get('to')), 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' });
93  if (request.method !== 'GET') return new Response('GET only', { status: 405, headers: { Allow: 'GET' } });
94  const handoff = url.searchParams.get('token');
95  if (handoff && /^[A-Za-z0-9_-]{43}$/.test(handoff)) {
96    const session = newSessionToken();
97    if (await accounts.takeHandoff(handoff, session, SESSION_TTL_MS)) headers.set('Set-Cookie', sessionCookie(session));
98  }
99  return new Response(null, { status: 302, headers });
100}