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:
2026-08-12 23:43:39 +00:00
co-authored by Claude Opus 5
parent e72cae4830
commit 3c7f52ab77
23 changed files with 80 additions and 26 deletions
+1 -1
View File
@@ -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 {