Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133import { isDid, isHandle } from "@atcute/lexicons/syntax";import { redirect, type RequestEvent } from "@sveltejs/kit";import type { Handle } from "@sveltejs/kit/hooks";import { createBobbinClient } from "#lib/api/client.ts";import { resolveRepoByRepoDid } from "#lib/api/repo.ts";import { signAvatarSrcs } from "#lib/server/avatar.ts";import { getConfig } from "#lib/server/config.ts";import { setBlobUrl } from "#lib/markup/paths.ts";import { themeAttribute, themeFromCookies } from "#lib/theme.ts";import { decodeOrEmpty } from "#lib/uri.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; }};
const firstSegment = (url: URL) => /^\/([^/]*)(.*)$/.exec(url.pathname) ?? [];
// links from the old appview: /tangled.sh/x means /tangled.org/x, and a handle// may be written with a leading @const legacyRedirect = (url: URL): Response | undefined => { const [, first = "", rest = ""] = firstSegment(url); const segment = first.replace(/^%40/i, "@"); const redirect = (status: number, handle: string) => new Response(null, { status, headers: { location: `/${handle}${rest}${url.search}` } }); if (segment === "tangled.sh" || segment === "@tangled.sh") return redirect(301, "tangled.org"); if (segment.startsWith("@") && isHandle(segment.slice(1))) return redirect(302, segment.slice(1)); return undefined;};
// a repo did permalink goes to the repo's url with whatever path follows, even one no// route matches. thrown, so a client navigation's data request gets it as dataconst repoDidRedirect = async (event: RequestEvent) => { const [, first = "", rest = ""] = firstSegment(event.url); // kit answers a malformed uri with a 400 once it routes, so don't throw before that const did = decodeOrEmpty(first); // git clones the permalink, that route sends it to the knot in one hop if (!isDid(did) || event.route.id === "/[handle]/info/refs") return; const ctx = createBobbinClient({ serviceUrl: getConfig().bobbinUrl, fetch: event.fetch }); const repo = await resolveRepoByRepoDid(ctx, did); if (repo) redirect(307, `/${repo.owner}/${repo.name}${rest}${event.url.search}`);};
export const handle: Handle = async ({ event, resolve }) => { setBlobUrl(getConfig().blobUrl); const legacy = legacyRedirect(event.url); if (legacy) return legacy; await repoDidRedirect(event);
// 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 { avatarUrl, avatarSecret, camoUrl } = getConfig();
const response = await resolve(event, { transformPageChunk: ({ html }) => { const page = html .replace("%tangled.theme%", theme) .replace("%tangled.avatar%", avatarUrl) .replace("%tangled.camo%", camoUrl); return avatarSecret ? signAvatarSrcs(page, avatarUrl, avatarSecret) : page; }, // 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);};