slskd: scaffold reverse-proxy sidecar
Add officer-slskd, a singleton sidecar that reverse-proxies to a self-hosted slskd (Soulseek) instance and reports its loopback port to the API on connect. All slskd knowledge (URL + API key) lives in the sidecar; the platform is a thin auth+forward proxy for /api/slskd/* and holds no slskd credentials. Mirrors the officer-vault pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { getSlskdServerUrl } from './sidecar-server';
|
||||
|
||||
// Thin reverse-proxy for /api/slskd/*. 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-slskd sidecar, which OWNS the slskd contract and injects the
|
||||
// slskd API key. The platform holds no slskd credentials.
|
||||
//
|
||||
// This is a catch-all with no routes of its own: the full /api/slskd/* contract (slskd's own API, e.g.
|
||||
// /api/v0/searches, /api/v0/transfers) is documented at the top of the sidecar's fetch handler —
|
||||
// src/servers/sidecar/slskd/index.ts.
|
||||
|
||||
export const slskdRouter = createRouter();
|
||||
|
||||
const PREFIX = '/api/slskd';
|
||||
|
||||
slskdRouter.all('/*', async (ctx) => {
|
||||
const baseUrl = getSlskdServerUrl();
|
||||
if (!baseUrl) return ctx.text('slskd 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;
|
||||
const range = ctx.req.header('range');
|
||||
if (range) headers['Range'] = range;
|
||||
|
||||
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('[slskd] proxy fetch failed', { target, error: String(err) });
|
||||
return ctx.text('slskd sidecar unreachable', 502);
|
||||
}
|
||||
|
||||
return new Response(upstream.body, { status: upstream.status, headers: new Headers(upstream.headers) });
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
|
||||
// The officer-slskd sidecar starts its slskd reverse-proxy on a random loopback port and reports it here
|
||||
// on connect. We remember it so `/api/slskd/*` always forwards to the current sidecar. The platform holds
|
||||
// NO knowledge of slskd itself (URL, API key) — only where the sidecar is listening.
|
||||
|
||||
let serverPort: number | null = null;
|
||||
|
||||
sidecar.on('slskd:server', (msg) => {
|
||||
const port = (msg as { port?: number }).port;
|
||||
if (typeof port !== 'number') return;
|
||||
serverPort = port;
|
||||
console.log(`[slskd] sidecar proxy registered on port ${port}`);
|
||||
});
|
||||
|
||||
/** Base URL of the sidecar's HTTP proxy, or null if the sidecar hasn't reported in yet. */
|
||||
export function getSlskdServerUrl(): string | null {
|
||||
return serverPort ? `http://127.0.0.1:${serverPort}` : null;
|
||||
}
|
||||
Reference in New Issue
Block a user