PORT is read in one place, and it has no default
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
@@ -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 <cwd>` 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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user