put every http sidecar on the proxy factory
createSidecarProxy arrived with the wallet but nothing else moved onto it, so five sidecars still carried their own copy of the same two files: a sidecar-server.ts that remembered a port announced as `<name>:server`, and a router.ts that forwarded the subpath. Byte for byte identical once the app name was normalised away — which is exactly what the factory's own header said it existed to end. headscale, transmission, invoiceshelf, slskd and music are now wallet-shaped: create the proxy, export the router and the URL getter. 386 lines deleted against 163 added, and the five feature directories go from ~70 lines each to ~18. Two deviations were real and moved INTO the factory rather than being dropped, because both are HTTP concerns rather than app knowledge: - Range and If-None-Match are now forwarded for every sidecar. music needed both (seeking, and ETag revalidation returning a cheap 304 instead of a cover image) and slskd needed Range. Forwarding them everywhere costs nothing and removes the reason to hand-roll. - timeoutSeconds, used only by music at 1800. A from-scratch reindex holds the proxied connection open for minutes with no bytes flowing, which the 60s idle timeout would drop. It applies to the whole prefix — the proxy must not know which of a sidecar's routes are slow. The five side-effect imports in hono.ts are gone with them: the port listener now registers when createSidecarProxy runs inside the router this file already imports. Vault keeps its hand-rolled pair and its side-effect import — it is off-limits by standing instruction, and is the one sidecar this commit deliberately does not touch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,54 +1,16 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { getInvoiceshelfServerUrl } from './sidecar-server';
|
||||
import { createSidecarProxy } from '../../sidecar/create-proxy';
|
||||
|
||||
// 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.
|
||||
// /api/invoiceshelf/* — auth, then forward to officer-invoiceshelf. No routes of its own and no invoiceshelf knowledge:
|
||||
// this file must never grow app logic.
|
||||
//
|
||||
// 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.
|
||||
// The sidecar owns the InvoiceShelf contract and holds its credentials.
|
||||
|
||||
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<string, string> = {};
|
||||
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) });
|
||||
const proxy = createSidecarProxy({
|
||||
name: 'invoiceshelf',
|
||||
prefix: '/api/invoiceshelf',
|
||||
});
|
||||
|
||||
export const invoiceshelfRouter = proxy.router;
|
||||
|
||||
/** Base URL of the sidecar's HTTP server, or null if it hasn't reported in yet. */
|
||||
export const getInvoiceshelfServerUrl = proxy.getHttpUrl;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
|
||||
// The officer-invoiceshelf sidecar starts its HTTP server on a random loopback port and reports it here on
|
||||
// connect. We remember it so `/api/invoiceshelf/*` always forwards to the current sidecar. The platform holds
|
||||
// NO knowledge of InvoiceShelf itself — not its URL, not its API token, and not which company it acts as.
|
||||
|
||||
let serverPort: number | null = null;
|
||||
|
||||
sidecar.on('invoiceshelf:server', (msg) => {
|
||||
const port = (msg as { port?: number }).port;
|
||||
if (typeof port !== 'number') return;
|
||||
serverPort = port;
|
||||
console.log(`[invoiceshelf] sidecar registered on port ${port}`);
|
||||
});
|
||||
|
||||
/** Base URL of the sidecar's HTTP server, or null if the sidecar hasn't reported in yet. */
|
||||
export function getInvoiceshelfServerUrl(): string | null {
|
||||
return serverPort ? `http://127.0.0.1:${serverPort}` : null;
|
||||
}
|
||||
Reference in New Issue
Block a user