import { createRouter } from '../../create-router'; import { getHeadscaleServerUrl } from './sidecar-server'; // Thin reverse-proxy for /api/headscale/*. The platform's ONLY job here is AUTH + FORWARDING: this router // mounts under the protected /api tree (userMiddleware upstream authenticates the owner), then forwards the // subpath + query + body to the officer-headscale sidecar, which OWNS the Headscale contract and holds the // admin API key. // // A catch-all with no routes of its own. Unlike /api/slskd this proxies nothing of the upstream's own // surface — the sidecar exposes only Officer-owned routes under /_officer/, because Headscale's REST shape // differs across releases and version handling belongs in the sidecar, not the browser. The full contract // is documented at the top of src/servers/sidecar/headscale/index.ts. It is opaque from here: this file // must never grow Headscale logic. export const headscaleRouter = createRouter(); const PREFIX = '/api/headscale'; headscaleRouter.all('/*', async (ctx) => { const baseUrl = getHeadscaleServerUrl(); if (!baseUrl) return ctx.text('headscale sidecar not available', 503); const url = new URL(ctx.req.url); const subpath = url.pathname.slice(PREFIX.length) || '/'; const target = `${baseUrl}${subpath}${url.search}`; const method = ctx.req.method; const headers: Record = {}; const contentType = ctx.req.header('content-type'); if (contentType) headers['Content-Type'] = contentType; // Forward the authenticated user id so the sidecar can serve its Officer-owned routes. The sidecar binds // loopback only, so this header is trusted. headers['X-Officer-User'] = String(ctx.get('user').id); const hasBody = method !== 'GET' && method !== 'HEAD'; let upstream: Response; try { upstream = await fetch(target, { method, headers, body: hasBody ? await ctx.req.arrayBuffer() : undefined, }); } catch (err) { console.error('[headscale] proxy fetch failed', { target, error: String(err) }); return ctx.text('headscale sidecar unreachable', 502); } return new Response(upstream.body, { status: upstream.status, headers: new Headers(upstream.headers) }); });