Files
platform/src/servers/officer-url.mjs
T
pastilhasandClaude Opus 5 b9524ee4d5 prefix the env with OFFICERDEV_, and keep reading the bare names
PORT, PUBLIC_URL and POSTGRES_URL are names half the software in the world
uses. Bun auto-loads .env into EVERY process started in the platform directory,
so a bare PORT reaches the sidecars, the tools they spawn and anything an agent
runs from here — and a bare POSTGRES_URL reaches things with no business
holding a database credential.

Four parts.

officer-setup now writes OFFICERDEV_-prefixed names, with the reasoning in the
generated file so whoever opens it later knows why.

Every consumer reads `OFFICERDEV_X ?? X` — eleven sites across the databases,
the DAV router, officer-url.mjs, os-user-postgres and two scripts. The bare
name is the FALLBACK, never the source, which is what makes this safe to ship
before every machine has been converted.

`bun run fix:env` converts a file in place. Idempotent, preserves comments and
`export`, leaves the value untouched including an `=` inside it, and refuses to
touch names belonging to other software — prefixing PATH or NODE_ENV does not
rename a variable, it deletes it.

And it runs on every restart from the server settings screen. There rather than
at boot because a restart is the one moment the file can be rewritten and take
effect in the same breath: the process about to start is the one that reads it.
Non-fatal, since the fallback means the worst case is that nothing changed.

gen-index reads either spelling out of the file directly, because it is run by
hand as often as by the setup and a bare shell has no .env loaded.

Verified by converting this machine's own .env: PORT, PUBLIC_URL and the
database all still resolve, gen:index still finds its URL, 1152 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:46:00 +00:00

76 lines
3.9 KiB
JavaScript

// 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.OFFICERDEV_PORT ?? 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}`;
// ── The Anthropic proxy: PORT + 1, derived, not configured ──
//
// This is the ONE sidecar that binds a fixed port. Every other one binds `port: 0`, lets the kernel
// choose, and reports what it got back over the registration socket — which works because their
// consumer is the platform.
//
// The proxy cannot do that. Its consumer is `claude`, spawned by a DIFFERENT pm2 process
// (officer-agent), which needs ANTHROPIC_BASE_URL at spawn time and has no channel to ask the proxy
// what port it landed on. Two independent processes with nothing between them have to agree in
// advance, so the number has to be predictable rather than discovered.
//
// It was ANTHROPIC_PROXY_PORT, defaulting to 5051 in four separate files: the one that binds it and
// three that guessed the same constant to find it. 5051 was chosen against nothing and could collide
// with anything the owner installs later, with the symptom being chat failing while the rest of the
// platform looked healthy.
//
// PORT + 1 keeps the predictability and removes both problems. There is no variable to set, no second
// number to keep in agreement with the first, and the pair moves together when the install moves.
export const ANTHROPIC_PROXY_PORT = PORT + 1;
export const ANTHROPIC_PROXY_URL = `http://127.0.0.1:${ANTHROPIC_PROXY_PORT}`;