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>
134 lines
4.8 KiB
Bash
134 lines
4.8 KiB
Bash
#!/bin/bash
|
|
# =============================================================================
|
|
# officer-setup — the environment file
|
|
# =============================================================================
|
|
#
|
|
# Definitions only.
|
|
#
|
|
# ── No secrets are written here ──
|
|
#
|
|
# Every encryption and signing key lives in the secret store — a 0600 SQLite file
|
|
# at $OFFICER_ROOT/secrets/officer-keys.db, one key per purpose, created on first
|
|
# use. See docs/secret-store.md and the Secrets section of officer-setup.sh.
|
|
#
|
|
# So this file holds no credential except POSTGRES_URL, which is a connection
|
|
# string to a database bound to loopback.
|
|
#
|
|
# ── Derived, not asked ──
|
|
#
|
|
# DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR are gone too, and this time nothing
|
|
# replaces them. The platform derives the install root as the parent of its own
|
|
# working directory, so data/, capabilities/ and dockers/ follow from the layout
|
|
# on disk, and the owner's home comes from the OS. They were three environment
|
|
# variables that had to agree with each other and with the directory tree.
|
|
|
|
[[ -n "${OFFICER_SETUP_ENV_LOADED:-}" ]] && return 0
|
|
OFFICER_SETUP_ENV_LOADED=1
|
|
|
|
env_file() { echo "$(platform_dir)/.env"; }
|
|
|
|
env_exists() { [[ -f "$(env_file)" ]]; }
|
|
|
|
# One value out of an existing .env, without sourcing it — the file holds
|
|
# secrets and arbitrary shell would run as root.
|
|
env_get() {
|
|
[[ -r "$(env_file)" ]] || return 0
|
|
awk -F= -v k="$1" '
|
|
$1 == k {
|
|
v = substr($0, index($0, "=") + 1)
|
|
gsub(/^"|"$/, "", v)
|
|
print v
|
|
exit
|
|
}' "$(env_file)"
|
|
}
|
|
|
|
write_env() {
|
|
local dest
|
|
dest="$(env_file)"
|
|
|
|
[[ -f "$dest" ]] && cp -a "$dest" "${dest}.before-officer-setup"
|
|
|
|
# Restrictive from the moment it exists rather than chmod'd afterwards, so the
|
|
# secrets are never briefly world-readable. Restored straight after: umask is
|
|
# not scoped to a function, and leaving it at 077 would quietly make every file
|
|
# a later section creates owner-only.
|
|
local prior_umask
|
|
prior_umask="$(umask)"
|
|
umask 077
|
|
cat >"$dest" <<ENVF
|
|
# Written by officer-setup.
|
|
#
|
|
# Everything Officer reads at runtime. Kept at 0600 and owned by ${USERNAME}: it
|
|
# holds the token-signing secret and the database credential.
|
|
#
|
|
# EVERY NAME IS PREFIXED \`OFFICERDEV_\`. Bun auto-loads this file into every
|
|
# process started in the platform directory, so a bare PORT or POSTGRES_URL
|
|
# reaches the sidecars, the tools they spawn and anything an agent runs from
|
|
# here — names half the software in the world already uses. Prefixed, they
|
|
# cannot be picked up by accident.
|
|
#
|
|
# Every consumer reads \`OFFICERDEV_X ?? X\`, so an older unprefixed file still
|
|
# works. \`bun run fix:env\` converts one in place.
|
|
|
|
OFFICERDEV_PORT="${ENV_PORT}"
|
|
|
|
# Where Officer is reached from a browser. Not derivable — see the section.
|
|
OFFICERDEV_PUBLIC_URL="${ENV_PUBLIC_URL}"
|
|
|
|
OFFICERDEV_POSTGRES_URL="${POSTGRES_URL}"
|
|
|
|
ENVF
|
|
|
|
umask "$prior_umask"
|
|
|
|
chown "${USERNAME}:$(user_group)" "$dest"
|
|
chmod 600 "$dest"
|
|
return 0
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# A sensible default for PUBLIC_URL
|
|
# -----------------------------------------------------------------------------
|
|
#
|
|
# localhost is the wrong default on a machine with a tailnet, and quietly so:
|
|
# it works from the machine itself and from nowhere else, so the mistake shows up
|
|
# on the first phone, not during setup.
|
|
#
|
|
# The tailnet is where Officer is actually reached — it is the perimeter the
|
|
# whole security model rests on — so its address is the honest default.
|
|
#
|
|
# The SHORT MagicDNS name — `officer-dev`, not `officer-dev.ts.example.dev` and
|
|
# not the raw 100.x address. All three resolve inside the tailnet; the short one
|
|
# is the one anybody actually types, and PUBLIC_URL ends up baked into the page's
|
|
# OpenGraph tags by `bun gen:index`, so it is read by people as well as machines.
|
|
#
|
|
# It relies on the tailnet's search domain, which every Tailscale client sets when
|
|
# MagicDNS is on. A device that has somehow lost it resolves the FQDN and not the
|
|
# short name — the fix there is to type the longer one, not to default to it.
|
|
#
|
|
# Falls back to localhost when there is no tailnet, which is correct rather than
|
|
# merely tolerable: a machine with no private network has no other address that
|
|
# is any better a guess.
|
|
tailnet_hostname() {
|
|
local dns ip
|
|
dns="$(tailscale status --json 2>/dev/null | grep -oP '"DNSName":\s*"\K[^"]+' | head -1)"
|
|
dns="${dns%.}" # MagicDNS reports it fully qualified, with a trailing dot
|
|
dns="${dns%%.*}" # and we want the short name
|
|
if [[ -n "$dns" ]]; then
|
|
echo "$dns"
|
|
return 0
|
|
fi
|
|
ip="$(tailscale ip -4 2>/dev/null | head -1)"
|
|
[[ -n "$ip" ]] && echo "$ip"
|
|
}
|
|
|
|
default_public_url() {
|
|
local host
|
|
host="$(tailnet_hostname)"
|
|
if [[ -n "$host" ]]; then
|
|
echo "http://${host}:${1}"
|
|
else
|
|
echo "http://localhost:${1}"
|
|
fi
|
|
}
|