From 3c7f52ab776acd708bb1f09ede32b1d5ec186213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 23:43:39 +0000 Subject: [PATCH] PORT is read in one place, and it has no default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit officer-url.mjs is now the only file in the tree that touches process.env.PORT. Twenty-two others read it and supplied their own default; a value with twenty-two sources is not configuration, it is twenty-two things to keep in sync, and they had already drifted three ways. It throws when PORT is unset rather than guessing. A default only covers the case where .env was never loaded — which is not a machine anyone wants running, because POSTGRES_URL is missing in the same breath. What the default bought was a process that starts, binds somewhere unexpected, and fails later for a reason that does not name the cause. Same posture as jwt.ts with JWT_SECRET. It is .mjs, not .ts, and that is the whole reason this could be one file. pm2 launches officer-pty with node (ecosystem.config.cjs) and everything else with bun; node cannot import TypeScript, so a .ts module would have left the pty sidecar holding the only surviving copy of the default — precisely the thing being removed. allowJs is already on, so the TS callers still get types. Verified both runtimes import it, and that PUBLIC_URL-style overrides still work. It also exports API_URL and OFFICER_API_URL, because nineteen sidecars were independently building `ws://127.0.0.1:${PORT}` and two more were building the http form. Those are one listener described in two protocols — no sidecar binds anything — so they belong beside the port rather than being rediscovered per file. server.tsx now takes PORT as a number, so Number(PORT) at the serve site is gone. Not typechecked (empty node_modules, frozen installs). Every edited file parses under `bun build --no-bundle`; node and bun both load the new module; the unset and non-numeric paths were exercised; the pm2 profile still loads. Co-Authored-By: Claude Opus 5 (1M context) --- src/server.tsx | 4 +- src/servers/api/chat/agent-panels-routes.ts | 3 +- src/servers/api/tasks/task-api-env.ts | 5 +- src/servers/officer-url.mjs | 54 +++++++++++++++++++++ src/servers/sidecar/caldav/index.ts | 2 +- src/servers/sidecar/claude/index.ts | 2 +- src/servers/sidecar/claude/user-instance.ts | 4 +- src/servers/sidecar/email/index.ts | 2 +- src/servers/sidecar/gitea/index.ts | 2 +- src/servers/sidecar/headscale/index.ts | 2 +- src/servers/sidecar/invoiceshelf/index.ts | 2 +- src/servers/sidecar/jellyfin/index.ts | 2 +- src/servers/sidecar/memos/index.ts | 2 +- src/servers/sidecar/music/index.ts | 2 +- src/servers/sidecar/notify/index.ts | 2 +- src/servers/sidecar/opencode/index.ts | 2 +- src/servers/sidecar/photos/index.ts | 2 +- src/servers/sidecar/pty/index.mjs | 2 +- src/servers/sidecar/slskd/index.ts | 2 +- src/servers/sidecar/transmission/index.ts | 2 +- src/servers/sidecar/vault/index.ts | 2 +- src/servers/sidecar/vnc/index.ts | 2 +- src/servers/sidecar/wallet/index.ts | 2 +- 23 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 src/servers/officer-url.mjs diff --git a/src/server.tsx b/src/server.tsx index aaec895c..9833f2ba 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -4,6 +4,7 @@ import { serve } from 'bun'; import { honoServer, PROTECTED_API_PREFIXES, UNPROTECTED_API_PREFIXES } from './servers/hono'; import { assertCapabilityTotality } from './servers/capabilities/totality'; import { assertInstallLayout } from './servers/data-path'; +import { PORT } from './servers/officer-url.mjs'; import { assertSecretsClosed } from './servers/os-user'; import { resolveHomeDir } from './servers/user-home'; import { resolveAuthToken } from './servers/auth-token'; @@ -23,7 +24,6 @@ import './servers/api/chat/opencode/sidecar-server'; // subscribe to the opencod import type { SidecarRegistration } from './servers/sidecar/registration-protocol'; import { toShellUsername } from './servers/data-path'; -const { PORT = '9000' } = process.env; // Build static file routes from public/ const publicRoutes: Record Response> = {}; @@ -250,7 +250,7 @@ async function upgradeWs( } const server = serve({ - port: Number(PORT), + port: PORT, idleTimeout: 60, maxRequestBodySize: 1024 * 1024 * 1024 * 50, // 50 GB routes: { diff --git a/src/servers/api/chat/agent-panels-routes.ts b/src/servers/api/chat/agent-panels-routes.ts index 9d6c2e48..4b165857 100644 --- a/src/servers/api/chat/agent-panels-routes.ts +++ b/src/servers/api/chat/agent-panels-routes.ts @@ -9,6 +9,7 @@ import { } from 'officerdb'; import { introduceAgentPanel } from '../agent-handoff/deliver'; import { logger } from './logger'; +import { OFFICER_API_URL } from '../../officer-url.mjs'; /** * The browser's half of the address book: name a panel, look up what a panel is, rename, remove. @@ -23,7 +24,7 @@ import { logger } from './logger'; /** A name has to survive being typed into a prompt and into a shell, so keep it boring. */ const NAME_RE = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$|^[a-z0-9]$/; -const API_ORIGIN = `http://127.0.0.1:${process.env.PORT ?? '9000'}`; +const API_ORIGIN = OFFICER_API_URL; export function registerAgentPanelRoutes(router: Hono): void { // GET /chat/agent-panels?dashboardId=… — the address book for one dashboard. diff --git a/src/servers/api/tasks/task-api-env.ts b/src/servers/api/tasks/task-api-env.ts index 912a250a..363441e3 100644 --- a/src/servers/api/tasks/task-api-env.ts +++ b/src/servers/api/tasks/task-api-env.ts @@ -1,6 +1,7 @@ import { sign } from '@@/jwt'; +import { PORT, OFFICER_API_URL } from '../../officer-url.mjs'; -const { PORT = '9000', PUBLIC_URL } = process.env; +const { PUBLIC_URL } = process.env; const PUBLIC_HOST = (() => { try { @@ -24,7 +25,7 @@ type TaskApiUser = { id: number; email: string; username?: string | null }; export async function buildTaskApiEnv(user: TaskApiUser): Promise> { const token = await sign({ id: user.id, email: user.email, username: user.username ?? '' }, '12h'); return { - OFFICER_API_URL: `http://127.0.0.1:${PORT}`, + OFFICER_API_URL, OFFICER_API_HOST: PUBLIC_HOST ?? `127.0.0.1:${PORT}`, OFFICER_AUTH_TOKEN: token, }; diff --git a/src/servers/officer-url.mjs b/src/servers/officer-url.mjs new file mode 100644 index 00000000..4fe93426 --- /dev/null +++ b/src/servers/officer-url.mjs @@ -0,0 +1,54 @@ +// Where the app is, and the only place that knows. +// +// ── Why this file is .mjs ── +// +// Every process is bun except `officer-pty`, which pm2 launches with node (see +// ecosystem.config.cjs). Node cannot import TypeScript, so a .ts module here would have left the pty +// sidecar with its own copy — which is exactly the thing this file exists to end. Plain JS is +// importable by both, and `allowJs` in tsconfig.json means the TS callers still get types. +// +// ── Why there is no default ── +// +// There were twenty-two, and they disagreed: 5000 in the app and nineteen sidecars, 9010 in +// user-instance.ts, 9000 in .env.example. Each was defensible where it was written and none was +// visible from the others. +// +// A default is a guess at a value that .env always supplies. The one case it covers — PORT genuinely +// unset — is not a machine anyone wants running: it means .env was not loaded, so POSTGRES_URL is +// missing too and nothing works anyway. What a default buys there is a process that starts, binds +// somewhere unexpected and fails later for a reason that does not name the cause. +// +// So: no default anywhere, and this throws. Same posture as jwt.ts with JWT_SECRET. + +const raw = process.env.PORT; + +if (!raw) { + throw new Error( + [ + 'PORT is not set.', + '', + 'Officer reads it from .env, which bun auto-loads from the working directory. If this is a pm2', + 'process, the `cwd` pin in ecosystem.profile.cjs is what puts it there — a PORT this empty', + 'usually means .env was never loaded at all, and POSTGRES_URL is missing too.', + '', + 'Set PORT in .env (officer-setup writes 9000), or start from the repo root.', + ].join('\n'), + ); +} + +const parsed = Number(raw); + +if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) { + throw new Error(`PORT must be an integer between 1 and 65535, but it is ${JSON.stringify(raw)}`); +} + +/** The port the app binds. Everything else addresses it; nothing else binds. */ +export const PORT = parsed; + +// The app serves HTTP and WebSocket on ONE listener, so these are the same port in two protocols. +// Sidecars append their own path — `/api/sidecar/register` for the registration socket. +// +// The env overrides are kept because they are explicit escape hatches rather than defaults: nothing +// sets either today, and a value that is present is a deliberate act rather than a guess. +export const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${PORT}`; +export const OFFICER_API_URL = process.env.OFFICER_API_URL ?? `http://127.0.0.1:${PORT}`; diff --git a/src/servers/sidecar/caldav/index.ts b/src/servers/sidecar/caldav/index.ts index f04ef86c..2d2cca5f 100644 --- a/src/servers/sidecar/caldav/index.ts +++ b/src/servers/sidecar/caldav/index.ts @@ -3,6 +3,7 @@ import { createSidecarConnector } from '../connect'; import { startRadicale, davPaths } from './radicale'; import { listCollections, listEvents, listContacts } from './collections'; import { DATA_PATH } from '../../data-path'; +import { API_URL } from '../../officer-url.mjs'; // The officer-caldav sidecar. Owns the whole CalDAV/CardDAV contract: it supervises Radicale, owns the // collection storage under DATA_PATH/dav, and exposes two very different doors. @@ -23,7 +24,6 @@ import { DATA_PATH } from '../../data-path'; // machine-facing interface. Same split officer-email already uses. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/claude/index.ts b/src/servers/sidecar/claude/index.ts index 39f861a5..3aedb303 100644 --- a/src/servers/sidecar/claude/index.ts +++ b/src/servers/sidecar/claude/index.ts @@ -2,8 +2,8 @@ import type { SidecarCommand, SidecarEvent } from '../protocol'; import { loadState, flushAndSave, acquireLock, releaseLock, getState } from './state'; import { startAnthropicProxy, getProxySecret, ensureProxySecret } from './proxy'; import { createSidecarConnector } from '../connect'; +import { API_URL } from '../../officer-url.mjs'; -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; // ── Startup ── diff --git a/src/servers/sidecar/claude/user-instance.ts b/src/servers/sidecar/claude/user-instance.ts index 455c8d20..aa91dbc9 100644 --- a/src/servers/sidecar/claude/user-instance.ts +++ b/src/servers/sidecar/claude/user-instance.ts @@ -18,6 +18,7 @@ import { createSidecarConnector } from '../connect'; import { sign } from '../../jwt'; import { getUserByEmail, getOwnerUser, getEmailAccounts } from 'officerdb'; import { DATA_PATH } from '../../data-path'; +import { API_URL, OFFICER_API_URL } from '../../officer-url.mjs'; // PM2 starts this sidecar with no user in its env, so resolve the owner from the database rather than // being told who to run as by the main server — one less thing that has to come from `officer` before @@ -70,9 +71,6 @@ const email = dbUser.email; // 9000 everywhere now, matching server.tsx and .env.example. 9010 was the old installer's default // (scripts/setup-old/setup.sh), which is why it was the only one of the three that ever matched a real // machine. -const OFFICER_PORT = process.env.PORT ?? '9000'; -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${OFFICER_PORT}`; -const OFFICER_API_URL = process.env.OFFICER_API_URL ?? `http://127.0.0.1:${OFFICER_PORT}`; const MCP_SERVER_SCRIPT = resolve(import.meta.dir, '../../mcp-tool-server.ts'); // Mint a long-lived JWT for this user so tools (e.g. gmail) can call back to dev-platform as them diff --git a/src/servers/sidecar/email/index.ts b/src/servers/sidecar/email/index.ts index f53323ff..783df087 100644 --- a/src/servers/sidecar/email/index.ts +++ b/src/servers/sidecar/email/index.ts @@ -4,8 +4,8 @@ import { initEmailIdle, stopEmailIdle } from './email-idle'; import { broadcastEmailNew } from './routes'; import { startEmailServer } from './http'; import { createSidecarConnector } from '../connect'; +import { API_URL } from '../../officer-url.mjs'; -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; // The sidecar used to reach BACK into the platform's queue over this socket to get a sync run — // enqueueViaWs / listJobsViaWs and a pending-response map. Syncs run in this process now diff --git a/src/servers/sidecar/gitea/index.ts b/src/servers/sidecar/gitea/index.ts index 713d2286..536c5339 100644 --- a/src/servers/sidecar/gitea/index.ts +++ b/src/servers/sidecar/gitea/index.ts @@ -16,6 +16,7 @@ import { normalizeBase, probe, } from './upstream'; +import { API_URL } from '../../officer-url.mjs'; // The officer-gitea sidecar. Owns the whole Gitea contract: the instance URL and the personal access // token. The platform side is a thin auth-gated forwarder holding no Gitea credentials. @@ -42,7 +43,6 @@ import { // sidecar from being a general-purpose SSRF hop into whatever else is on that host. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; // Everything under /api/v1 the UI legitimately needs. // diff --git a/src/servers/sidecar/headscale/index.ts b/src/servers/sidecar/headscale/index.ts index a99bf8fe..c2b6cc00 100644 --- a/src/servers/sidecar/headscale/index.ts +++ b/src/servers/sidecar/headscale/index.ts @@ -2,6 +2,7 @@ import type { SidecarCommand, SidecarEvent } from '../protocol'; import { createSidecarConnector } from '../connect'; import { handleOfficerRoute } from './routes'; import { MIN_VERSION_LABEL } from './version'; +import { API_URL } from '../../officer-url.mjs'; // The officer-headscale sidecar. Owns the whole Headscale contract for Officer: the registered servers and // their admin API keys, the >=0.29 version floor, and every multi-call composition the UI needs. The platform @@ -54,7 +55,6 @@ import { MIN_VERSION_LABEL } from './version'; // — the mistake the Soulseek panels made with 37 raw upstream calls. Every quirk is absorbed here. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/invoiceshelf/index.ts b/src/servers/sidecar/invoiceshelf/index.ts index f962efb2..08ec70bd 100644 --- a/src/servers/sidecar/invoiceshelf/index.ts +++ b/src/servers/sidecar/invoiceshelf/index.ts @@ -3,6 +3,7 @@ import { createSidecarConnector } from '../connect'; import { handleConfigRoute, noteProbe, probe } from './config'; import { handleOfficerRoute } from './routes'; import { getConfig } from './upstream'; +import { API_URL } from '../../officer-url.mjs'; // The officer-invoiceshelf sidecar. Owns the whole InvoiceShelf contract for Officer: the instance URL, the // Sanctum API token, and the `company` header that scopes every request. The platform API is a thin @@ -51,7 +52,6 @@ import { getConfig } from './upstream'; // update/*, installation/*, mail config, settings writes, ownership transfer — is deliberately unreachable. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/jellyfin/index.ts b/src/servers/sidecar/jellyfin/index.ts index f6c64385..27176a93 100644 --- a/src/servers/sidecar/jellyfin/index.ts +++ b/src/servers/sidecar/jellyfin/index.ts @@ -3,6 +3,7 @@ import { createSidecarConnector } from '../connect'; import { handleConfigRoute, noteProbe } from './config'; import { handleBytesRoute, handleOfficerRoute } from './routes'; import { getConfig, probe } from './upstream'; +import { API_URL } from '../../officer-url.mjs'; // The officer-jellyfin sidecar. Owns the whole Jellyfin contract for Officer: the instance URL, the access // token, the Jellyfin user it belongs to and the DeviceId its sessions are keyed by. The platform API is a @@ -43,7 +44,6 @@ import { getConfig, probe } from './upstream'; // general proxy, and why HLS forces it to keep Jellyfin's own paths. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/memos/index.ts b/src/servers/sidecar/memos/index.ts index f62fb645..d1582496 100644 --- a/src/servers/sidecar/memos/index.ts +++ b/src/servers/sidecar/memos/index.ts @@ -2,6 +2,7 @@ import type { SidecarCommand, SidecarEvent } from '../protocol'; import { createSidecarConnector } from '../connect'; import { getServiceConnection, saveServiceConnection, deleteServiceConnection, recordServiceProbe } from 'officerdb'; import { callMemos, getMemosConfig, invalidateMemosConfig, normalizeBase, probe } from './upstream'; +import { API_URL } from '../../officer-url.mjs'; // The officer-memos sidecar. Owns the whole Memos contract: the instance URL and the personal access // token. The platform side is a thin auth-gated forwarder holding no Memos credentials. @@ -22,7 +23,6 @@ import { callMemos, getMemosConfig, invalidateMemosConfig, normalizeBase, probe // SSRF hop into whatever else is on that host. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; // Everything under /api/v1 the UI legitimately needs. Auth routes are excluded on purpose: signin and // signout would mint or destroy sessions on the instance, and this sidecar authenticates with a stored diff --git a/src/servers/sidecar/music/index.ts b/src/servers/sidecar/music/index.ts index 09eebf59..9c04fdb2 100644 --- a/src/servers/sidecar/music/index.ts +++ b/src/servers/sidecar/music/index.ts @@ -39,6 +39,7 @@ import { type FavoriteKind, } from 'officerdb'; import { DATA_PATH } from '../../data-path'; +import { API_URL } from '../../officer-url.mjs'; // ── Per-user state validation ── @@ -114,7 +115,6 @@ const asKeys = (v: unknown): string[] | null => // `v` = per-album version stamp; unchanged `v` ⇒ nothing changed ⇒ the phone can skip re-downloading. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; // ── Audio-streaming HTTP server ── diff --git a/src/servers/sidecar/notify/index.ts b/src/servers/sidecar/notify/index.ts index ae06cce3..f9b931a3 100644 --- a/src/servers/sidecar/notify/index.ts +++ b/src/servers/sidecar/notify/index.ts @@ -5,6 +5,7 @@ import { handleDeviceRoute } from './devices'; import { resolveNotifyUser } from './resolve-user'; import { closeApnsSessions } from './apns'; import type { Notification, NotifyType } from './types'; +import { API_URL } from '../../officer-url.mjs'; // The officer-notify sidecar. The one place anything leaves this machine to tell the owner something. // @@ -26,7 +27,6 @@ import type { Notification, NotifyType } from './types'; // the visible string is composed here rather than sent by the producer. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; const VALID_TYPES: NotifyType[] = ['job', 'mail', 'agent', 'download', 'test']; diff --git a/src/servers/sidecar/opencode/index.ts b/src/servers/sidecar/opencode/index.ts index 347154f8..c2fabd74 100644 --- a/src/servers/sidecar/opencode/index.ts +++ b/src/servers/sidecar/opencode/index.ts @@ -9,6 +9,7 @@ import { createSessionLogStore } from '../claude/session-log'; import type { SidecarCommand, SidecarEvent } from '../protocol'; import type { RunnerMessage } from './serve-runner'; import { runOpenCodeTurnOnServe, killServeTurn, listRunningServeTurns, stopAllServeTurns } from './serve-runner'; +import { API_URL } from '../../officer-url.mjs'; // The OpenCode sidecar (officer-opencode). Same philosophy as officer-claude: a singleton process that // OWNS its runtime — here, an `opencode serve` — registers with the API server, and answers commands. It @@ -18,7 +19,6 @@ import { runOpenCodeTurnOnServe, killServeTurn, listRunningServeTurns, stopAllSe // CRUD only, with turns spawned as `opencode run --dir ` subprocesses — that path was deleted on // 2026-08-10 once the serve had streaming, mid-turn injection and interrupt working end to end. -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; const OPENCODE_BIN = process.env.OPENCODE_BIN || join(homedir(), '.opencode', 'bin', 'opencode'); const SERVE_CWD = join(DATA_PATH, 'opencode_server'); const HEALTH_TIMEOUT_MS = 20_000; diff --git a/src/servers/sidecar/photos/index.ts b/src/servers/sidecar/photos/index.ts index 655220e8..887ffa03 100644 --- a/src/servers/sidecar/photos/index.ts +++ b/src/servers/sidecar/photos/index.ts @@ -4,6 +4,7 @@ import { handleConfigRoute, noteProbe, probe } from './config'; import { handleLockedRoute } from './locked'; import { handleOfficerRoute } from './routes'; import { getConfig } from './upstream'; +import { API_URL } from '../../officer-url.mjs'; // The officer-photos sidecar. Owns the whole Immich contract for Officer: the instance URL and the API key. // The platform API is a thin auth-gated forwarder (src/servers/api/photos/router.ts) holding no Immich @@ -46,7 +47,6 @@ import { getConfig } from './upstream'; // ETag included. The administrative half of Immich's API is unreachable — see routes.ts for the list. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/pty/index.mjs b/src/servers/sidecar/pty/index.mjs index 7f1dd9f1..c641cd39 100644 --- a/src/servers/sidecar/pty/index.mjs +++ b/src/servers/sidecar/pty/index.mjs @@ -18,8 +18,8 @@ import * as store from './sessions.mjs'; import { startServer } from './server.mjs'; import 'dotenv/config'; +import { API_URL } from '../../officer-url.mjs'; -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; const REGISTER_URL = `${API_URL}/api/sidecar/register`; const RECONNECT_DELAYS = [200, 500, 1000, 2000, 4000, 8000, 15000]; diff --git a/src/servers/sidecar/slskd/index.ts b/src/servers/sidecar/slskd/index.ts index 46646eef..9378d21b 100644 --- a/src/servers/sidecar/slskd/index.ts +++ b/src/servers/sidecar/slskd/index.ts @@ -4,6 +4,7 @@ import { resetStaleSoulseekBrowses } from 'officerdb'; import { getSlskdConfig, stripHopByHop } from './upstream'; import { handleConfigRoute, probe } from './config'; import { handleOfficerRoute } from './officer'; +import { API_URL } from '../../officer-url.mjs'; // The officer-slskd sidecar. Same philosophy as officer-vault / officer-music: a singleton process that // registers with the API server and OWNS a contract — here, a reverse-proxy to a self-hosted slskd @@ -37,7 +38,6 @@ import { handleOfficerRoute } from './officer'; // (src/servers/sidecar/vault/index.ts) once the client needs real-time updates. SignalR carries its // credential as an `?access_token=` query param on the socket, so the key injection differs from HTTP. -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/transmission/index.ts b/src/servers/sidecar/transmission/index.ts index 0457fe5d..d6f04f56 100644 --- a/src/servers/sidecar/transmission/index.ts +++ b/src/servers/sidecar/transmission/index.ts @@ -4,6 +4,7 @@ import { handleConfigRoute } from './config'; import { handleOfficerRoute } from './routes'; import { probe } from './rpc'; import { getTransmissionConfig } from './upstream'; +import { API_URL } from '../../officer-url.mjs'; // The officer-transmission sidecar. Owns the whole Transmission contract for Officer: the daemon URL and // credentials, the X-Transmission-Session-Id CSRF handshake, and the translation from Transmission's @@ -45,7 +46,6 @@ import { getTransmissionConfig } from './upstream'; // platform, and which daemon to talk to is per-owner data. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/vault/index.ts b/src/servers/sidecar/vault/index.ts index c80b7e87..30d95351 100644 --- a/src/servers/sidecar/vault/index.ts +++ b/src/servers/sidecar/vault/index.ts @@ -2,6 +2,7 @@ import type { ServerWebSocket } from 'bun'; import type { SidecarCommand, SidecarEvent } from '../protocol'; import { createSidecarConnector } from '../connect'; import { getVaultBase, getVaultWsBase, stripHopByHop, redactPath } from './upstream'; +import { API_URL } from '../../officer-url.mjs'; // The officer-vault sidecar. Same philosophy as the other officer-* sidecars: a singleton process that // registers with the API server and OWNS a contract — here, a transparent reverse-proxy to a self-hosted @@ -21,7 +22,6 @@ import { getVaultBase, getVaultWsBase, stripHopByHop, redactPath } from './upstr // The server listens on a random loopback port, reported to the API on connect so it can route here. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number { diff --git a/src/servers/sidecar/vnc/index.ts b/src/servers/sidecar/vnc/index.ts index 2cbe5fa1..e0746db2 100644 --- a/src/servers/sidecar/vnc/index.ts +++ b/src/servers/sidecar/vnc/index.ts @@ -2,8 +2,8 @@ import type { SidecarCommand, SidecarEvent } from '../protocol'; import * as vncManager from './vnc-manager'; import { createSidecarConnector } from '../connect'; import { getOwnerHomeDir } from '@@/data-path'; +import { API_URL } from '../../officer-url.mjs'; -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; // ── Command handlers ── diff --git a/src/servers/sidecar/wallet/index.ts b/src/servers/sidecar/wallet/index.ts index 304aed85..d71deef7 100644 --- a/src/servers/sidecar/wallet/index.ts +++ b/src/servers/sidecar/wallet/index.ts @@ -6,6 +6,7 @@ import { handleOfficerRoute } from './routes'; import { getChainSource, getConfig, hasStoreKey } from './upstream'; import { lockAll } from './keys'; import { invalidateAll } from './resolve'; +import { API_URL } from '../../officer-url.mjs'; // The officer-wallet sidecar. A bitcoin wallet in the shape Zeus models one — several interchangeable // backends behind one interface — but server-side, with the key material held here and nowhere else. @@ -81,7 +82,6 @@ import { invalidateAll } from './resolve'; // WALLET_LOCKED from signing paths only; every read above keeps working. // ───────────────────────────────────────────────────────────────────────────────────────────────── -const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`; /** Grab an ephemeral free port by briefly binding one and releasing it. */ function getFreePort(): number {