Files
pastilhasandClaude Opus 5 60ab531294 use the short tailnet name, not the FQDN
http://officer-dev:9000 rather than http://officer-dev.ts.pastilhas.dev:9000.
All three forms resolve inside the tailnet — short name, FQDN, raw 100.x — and
the short one is what anybody actually types. PUBLIC_URL is read by people too:
gen:index bakes it into the page's OpenGraph tags.

It depends on the tailnet's search domain, which every Tailscale client sets when
MagicDNS is on. A device that has lost it resolves the FQDN instead, and the
answer there is to type the longer one rather than to default everybody to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:05:04 +00:00

125 lines
4.3 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.
PORT="${ENV_PORT}"
# Where Officer is reached from a browser. Not derivable — see the section.
PUBLIC_URL="${ENV_PUBLIC_URL}"
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
}