(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 135 lines 3.6 kB view raw
1import type { APIContext } from "astro"; 2import { readFile } from "node:fs/promises"; 3import { join } from "node:path"; 4import { clearSessionCacheForCookie, getSession } from "./lib/api"; 5 6const API_PORT = process.env.API_PORT || 8081; 7const API_URL = process.env.API_URL || `http://localhost:${API_PORT}`; 8 9const PROXY_PATHS = [ 10 "/api/", 11 "/auth/", 12 "/oauth-client-metadata.json", 13 "/jwks.json", 14]; 15 16const CORS_HEADERS = { 17 "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 18 "Access-Control-Allow-Headers": 19 "Accept, Authorization, Content-Type, X-CSRF-Token, X-Session-Token", 20 "Access-Control-Expose-Headers": "Link", 21 "Access-Control-Allow-Credentials": "true", 22 "Access-Control-Max-Age": "300", 23}; 24 25function isExtensionOrigin(origin: string | null): origin is string { 26 if (!origin) return false; 27 return ( 28 origin.startsWith("chrome-extension://") || 29 origin.startsWith("moz-extension://") || 30 origin.startsWith("safari-web-extension://") 31 ); 32} 33 34export async function onRequest( 35 context: APIContext, 36 next: () => Promise<Response>, 37): Promise<Response> { 38 const { request, url, locals } = context; 39 40 if (url.pathname === "/favicon.ico") { 41 try { 42 const file = await readFile( 43 join(process.cwd(), "dist", "client", "favicon.ico"), 44 ); 45 return new Response(file, { 46 headers: { 47 "Content-Type": "image/x-icon", 48 "Cache-Control": "public, max-age=86400", 49 }, 50 }); 51 } catch { 52 /* ignore */ 53 } 54 } 55 56 const shouldProxy = PROXY_PATHS.some( 57 (p) => url.pathname.startsWith(p) || url.pathname === p.replace(/\/$/, ""), 58 ); 59 60 if (shouldProxy) { 61 const response = await proxyToBackend(request, url); 62 if (url.pathname === "/auth/logout") { 63 clearSessionCacheForCookie(request.headers.get("cookie") || ""); 64 } 65 return response; 66 } 67 68 const cookie = request.headers.get("cookie") || ""; 69 70 if (cookie.includes("margin_session")) { 71 locals.user = await getSession(cookie); 72 } else { 73 locals.user = null; 74 } 75 76 return next(); 77} 78 79async function proxyToBackend(request: Request, url: URL): Promise<Response> { 80 const origin = request.headers.get("origin"); 81 82 if (request.method === "OPTIONS" && isExtensionOrigin(origin)) { 83 return new Response(null, { 84 status: 204, 85 headers: { 86 "Access-Control-Allow-Origin": origin, 87 ...CORS_HEADERS, 88 }, 89 }); 90 } 91 92 const target = new URL(url.pathname + url.search, API_URL); 93 94 const headers = new Headers(request.headers); 95 const host = headers.get("host"); 96 headers.delete("host"); 97 headers.delete("origin"); 98 headers.delete("referer"); 99 if (host) { 100 headers.set("X-Forwarded-Host", host); 101 headers.set("X-Forwarded-Proto", url.protocol.replace(":", "")); 102 } 103 104 const init: RequestInit = { 105 method: request.method, 106 headers, 107 redirect: "manual", 108 }; 109 110 if (request.method !== "GET" && request.method !== "HEAD" && request.body) { 111 init.body = request.body; 112 // @ts-expect-error duplex is generic on RequestInit 113 init.duplex = "half"; 114 } 115 116 try { 117 const res = await fetch(target.toString(), init); 118 const responseHeaders = new Headers(res.headers); 119 120 if (isExtensionOrigin(origin)) { 121 responseHeaders.set("Access-Control-Allow-Origin", origin); 122 for (const [key, value] of Object.entries(CORS_HEADERS)) { 123 responseHeaders.set(key, value); 124 } 125 } 126 127 return new Response(res.body, { 128 status: res.status, 129 statusText: res.statusText, 130 headers: responseHeaders, 131 }); 132 } catch { 133 return new Response("Backend unavailable", { status: 502 }); 134 } 135}