Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
TypeScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586import type { Handle } from "@sveltejs/kit/hooks";import { getConfig } from "#lib/server/config.ts";import { themeAttribute, themeFromCookies } from "#lib/theme.ts";
// node's fetch hands back the body decoded but keeps these, which would make the// browser decode it againconst passThrough = (res: Response) => { const headers = new Headers(res.headers); headers.delete("content-encoding"); headers.delete("content-length"); return new Response(res.body, { status: res.status, headers });};
// set on every response the app renders, unless a route set its own. none of them changes// what a page may load or run: nosniff only stops a script or stylesheet served under the// wrong type, the referrer policy is the browsers' default spelled out, and the frame// rules refuse being framed (nothing frames the app, and an embedded tangled page could// otherwise be clickjacked into a star, fork or settings change). a full content policy// and hsts need their own rolloutconst SECURITY_HEADERS: [string, string][] = [ ["x-content-type-options", "nosniff"], ["referrer-policy", "strict-origin-when-cross-origin"], ["x-frame-options", "DENY"], ["content-security-policy", "frame-ancestors 'none'"]];
const withSecurityHeaders = (res: Response): Response => { const missing = SECURITY_HEADERS.filter(([name]) => !res.headers.has(name)); if (missing.length === 0) return res; try { for (const [name, value] of missing) res.headers.set(name, value); return res; } catch { // an upstream fetch's response handed back as is has immutable headers const copy = new Response(res.body, res); for (const [name, value] of missing) copy.headers.set(name, value); return copy; }};
export const handle: Handle = async ({ event, resolve }) => { // Keep bobbin off the public network. In Cloudflare this is a Worker // service binding; the browser only ever sees the web origin. if (event.url.pathname === "/xrpc" || event.url.pathname.startsWith("/xrpc/")) { const bobbin = event.platform?.env?.BOBBIN; if (bobbin) { const upstream = new URL(event.request.url); upstream.hostname = "bobbin.internal"; return bobbin.fetch(new Request(upstream, event.request)); } }
if (event.url.pathname === "/sitemap.xml" || event.url.pathname.startsWith("/sitemaps/")) { const sitemap = event.platform?.env?.SITEMAP; if (sitemap) { const upstream = new URL(event.request.url); upstream.hostname = "sitemap.internal"; return sitemap.fetch(new Request(upstream, event.request)); } const { sitemapUrl } = getConfig(); if (sitemapUrl) { return passThrough( await fetch(`${sitemapUrl}${event.url.pathname}${event.url.search}`) ); } console.warn("[hooks.server] no SITEMAP binding or SITEMAP_URL; sitemap request unhandled"); }
// stamp the saved theme onto <html> so the first paint is already right const theme = themeAttribute(themeFromCookies(event.request.headers.get("cookie")));
const response = await resolve(event, { transformPageChunk: ({ html }) => html.replace("%tangled.theme%", theme), // sveltekit blocks atcute fetch handler from reading headers // because it assumes backend APIs might return sensitive headers. // this happens when during CSR we run a fetch that was the same // as one ran during SSR, so sveltekit tries to give that fetch // the data we already had. // so we allow these headers to have atcute function properly. filterSerializedResponseHeaders(name) { return name === "content-type" || name === "content-length"; } }); return withSecurityHeaders(response);};