import { createRouter } from '../../create-router'; import { getInvoiceshelfServerUrl } from './sidecar-server'; // Thin reverse-proxy for /api/invoiceshelf/*. 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-invoiceshelf sidecar, which OWNS the InvoiceShelf contract and holds // the API token. // // A catch-all with no routes of its own. The sidecar exposes only Officer-owned routes under /_officer/, // against an allow-list of resources — the administrative half of InvoiceShelf's API is unreachable by // design. The full contract is documented at the top of src/servers/sidecar/invoiceshelf/index.ts. It is // opaque from here: this file must never grow InvoiceShelf logic. // // Note which headers are forwarded, and which are NOT. Origin and Referer are deliberately dropped: if either // reaches InvoiceShelf, its statefulApi() middleware switches from token auth to session+CSRF and every // request 419s. The browser sets them on same-origin XHR, so passing them through would break the sidecar in // a way that looks like an auth bug. export const invoiceshelfRouter = createRouter(); const PREFIX = '/api/invoiceshelf'; invoiceshelfRouter.all('/*', async (ctx) => { const baseUrl = getInvoiceshelfServerUrl(); if (!baseUrl) return ctx.text('invoiceshelf 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('[invoiceshelf] proxy fetch failed', { target, error: String(err) }); return ctx.text('invoiceshelf sidecar unreachable', 502); } return new Response(upstream.body, { status: upstream.status, headers: new Headers(upstream.headers) }); });