(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136import type { APIContext } from "astro";import { readFile } from "node:fs/promises";import { join } from "node:path";import { clearSessionCacheForCookie, getSession } from "./lib/api";
const API_PORT = process.env.API_PORT || 8081;const API_URL = process.env.API_URL || `http://localhost:${API_PORT}`;
const PROXY_PATHS = [ "/api/", "/auth/", "/oauth-client-metadata.json", "/jwks.json",];
const CORS_HEADERS = { "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, X-CSRF-Token, X-Session-Token", "Access-Control-Expose-Headers": "Link", "Access-Control-Allow-Credentials": "true", "Access-Control-Max-Age": "300",};
function isExtensionOrigin(origin: string | null): origin is string { if (!origin) return false; return ( origin.startsWith("chrome-extension://") || origin.startsWith("moz-extension://") || origin.startsWith("safari-web-extension://") );}
export async function onRequest( context: APIContext, next: () => Promise<Response>,): Promise<Response> { const { request, url, locals } = context;
if (url.pathname === "/favicon.ico") { try { const file = await readFile( join(process.cwd(), "dist", "client", "favicon.ico"), ); return new Response(file, { headers: { "Content-Type": "image/x-icon", "Cache-Control": "public, max-age=86400", }, }); } catch { /* ignore */ } }
const shouldProxy = PROXY_PATHS.some( (p) => url.pathname.startsWith(p) || url.pathname === p.replace(/\/$/, ""), );
if (shouldProxy) { const response = await proxyToBackend(request, url); if (url.pathname === "/auth/logout") { clearSessionCacheForCookie(request.headers.get("cookie") || ""); } return response; }
const cookie = request.headers.get("cookie") || "";
if (cookie.includes("margin_session")) { locals.user = await getSession(cookie); } else { locals.user = null; }
return next();}
async function proxyToBackend(request: Request, url: URL): Promise<Response> { const origin = request.headers.get("origin");
if (request.method === "OPTIONS" && isExtensionOrigin(origin)) { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": origin, ...CORS_HEADERS, }, }); }
const target = new URL(url.pathname + url.search, API_URL);
const headers = new Headers(request.headers); const host = headers.get("host"); headers.delete("host"); headers.delete("origin"); headers.delete("referer"); if (host) { headers.set("X-Forwarded-Host", host); headers.set("X-Forwarded-Proto", url.protocol.replace(":", "")); }
const init: RequestInit = { method: request.method, headers, redirect: "manual", };
if (request.method !== "GET" && request.method !== "HEAD" && request.body) { init.body = request.body; // @ts-expect-error duplex is generic on RequestInit init.duplex = "half"; }
try { const res = await fetch(target.toString(), init); const responseHeaders = new Headers(res.headers);
if (isExtensionOrigin(origin)) { responseHeaders.set("Access-Control-Allow-Origin", origin); for (const [key, value] of Object.entries(CORS_HEADERS)) { responseHeaders.set(key, value); } }
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: responseHeaders, }); } catch { return new Response("Backend unavailable", { status: 502 }); }}