import { jsonError } from "./common.js"; async function constructHTML(context: RequestContext) { const { pathname } = new URL(context.request.url); if (pathname.startsWith("/api/")) return await context.next(); if ( pathname.startsWith("/assets/") || ["/app.webmanifest", "/favicon.ico", "/robots.txt"].includes(pathname) || pathname.startsWith("/files/") ) return await context.env.ASSETS.fetch(context.request); return await context.next(); } async function generateTokenHash(token: string) { const hash = await crypto.subtle.digest( "SHA-512", new TextEncoder().encode(token), ); return btoa(String.fromCharCode(...new Uint8Array(hash))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); } async function setAuth(context: RequestContext) { const cookies = context.request.headers.get("cookie"); const auth = context.request.headers.get("authorization"); if (auth) { const jwtSegments = auth.replace("Bearer ", "").split("."); if (jwtSegments.length !== 3) return jsonError("Malformed token", 401); const { alg } = JSON.parse(atob(jwtSegments[0])); if (alg !== "HS256") return jsonError("Invalid token", 400); const key = await crypto.subtle.importKey( "raw", // @ts-expect-error Uint8Array.from( atob( context.env.JWT_SIGNING_KEY.replaceAll("-", "+").replaceAll("_", "/"), ), (m) => m.codePointAt(0), ), { hash: "SHA-256", name: "HMAC" }, false, ["verify"], ); if ( !(await crypto.subtle.verify( "HMAC", key, // @ts-expect-error Uint8Array.from( atob(jwtSegments[2].replaceAll("-", "+").replaceAll("_", "/")), (m) => m.codePointAt(0), ), new TextEncoder().encode(`${jwtSegments[0]}.${jwtSegments[1]}`), )) ) return jsonError("Token could not be verified", 401); const { jti: sessionToken }: { jti: string } = JSON.parse(jwtSegments[1]); const linkedSessionData = await context.env.DATA.get( `auth_${await generateTokenHash(sessionToken)}`, ); if (linkedSessionData) { context.data.current_user = JSON.parse(linkedSessionData); return await context.next(); } else return jsonError("Session is invalid or expired", 401); } if (!cookies) return await context.next(); const cookieList = cookies.split(/; /); for (const c of cookieList) { const [name, value] = c.split("="); if (name !== "_s") continue; const userData = await context.env.DATA.get( `auth_${await generateTokenHash(value)}`, ); if (userData) context.data.current_user = JSON.parse(userData); else context.request.headers.append( "set-cookie", "_s=; HttpOnly; Max-Age=0; Path=/; Secure;", ); break; } return await context.next(); } async function setBody(context: RequestContext) { if ( context.request.method === "POST" && !context.request.url.endsWith("/api/infractions/new") ) { if (context.request.headers.get("content-type") !== "application/json") return new Response('{"error":"Invalid content-type"}', { headers: { "content-type": "application/json", }, status: 400, }); let body: { [k: string]: any }; try { body = await context.request.json(); } catch { return new Response('{"error":"Invalid JSON"}', { headers: { "content-type": "application/json", }, status: 400, }); } context.data.body = body; } return await context.next(); } async function setHeaders(context: RequestContext) { const response = await context.next(); const rtvValues = [ "Aldaria", "Altadena", "DEMA", "Dragonborn", "Heaven, Iowa", "Hollywood", "Parkway East", "Parkway North", "Parkway West", "Tokyo", "Wintervale", ]; response.headers.set("Permissions-Policy", "clipboard-write=(self)"); response.headers.set("Referrer-Policy", "same-origin"); response.headers.set( "RTV", rtvValues[Math.round(Math.random() * (rtvValues.length - 1))], ); response.headers.set("X-Frame-Options", "SAMEORIGIN"); return response; } async function setTheme(context: RequestContext) { const cookies = context.request.headers.get("cookie"); if (!cookies) { context.data.theme = "dark"; return await context.next(); } const cookieList = cookies.split("; "); const themeCookie = cookieList.find((c) => c.startsWith("chakra-ui-color-mode"), ); const theme = themeCookie?.split("=").at(1); if (!theme || !["dark", "light"].includes(theme)) context.data.theme = "dark"; else context.data.theme = theme; return await context.next(); } export const onRequest = [ setAuth, setTheme, constructHTML, setBody, setHeaders ];