BROWSER_RELAY_PORT is gone and the second listener no longer starts. The Chrome
extension, api/browser/ and the /browser screen all stay on disk — this is going
to be extracted, and deleting it means writing it again.
Three things had to move together, and the middle one would have failed the boot
on its own:
server.tsx the listener, commented out with the variable name recorded
hono.ts the /api/browser mount, closed
registry.ts the 'browser' capability's claim on /browser, dropped
assertCapabilityTotality checks both directions: check 2 refuses to start on a
capability claiming a prefix nothing serves. Unmounting the router alone would
have left the registry describing it, and the server would not have come up.
The capability itself survives because it also claims /scrape, which shares
nothing with the relay — it launches its own headless chromium through playwright
and never speaks to the extension.
The comment in server.tsx carries the two facts that are not recoverable by
reading the remaining code. First, the port is an INPUT TO A CREDENTIAL:
relay-auth.ts derives each extension's token as HMAC(JWT_SECRET,
'officer-browser-relay-v1:${port}:${userId}:${salt}'), so bringing the relay back
on a different number silently invalidates every paired browser — reported by the
extension as "Relay not reachable", which SETUP.md blames on a wrong address,
port or token. Second, it cannot come back as a kernel-assigned port:0 like the
other sidecars: the extension is configured by hand and stores the value, so a
port that moves each restart breaks the pairing each restart.
Left alone deliberately: the /browser route in App.tsx, its Dock entry, and the
Settings → Browser Relay panel. They will not work against a closed endpoint.
Removing them is frontend work for the extraction, not part of switching the
listener off.
.env is down to PORT and POSTGRES_URL.
Not typechecked (empty node_modules, frozen installs). Every changed file parses;
the setup section was run and writes two variables.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
91 lines
6.5 KiB
Bash
91 lines
6.5 KiB
Bash
# What officer-setup writes. Everything below this block is optional, or is on its way out.
|
|
PORT=9000
|
|
POSTGRES_URL="postgres://postgres:password@localhost:5432/officer"
|
|
|
|
# ── Moving to the secret store ─────────────────────────────────────────────────────────────────
|
|
# Still REQUIRED — jwt.ts throws at module load without JWT_SECRET, and crypto.ts throws without
|
|
# VAULT_STORE_KEY — but officer-setup no longer writes either. They are moving into the SQLite key
|
|
# store (docs/secret-store.md), which is designed and not yet built, so an install made by the
|
|
# current script will not boot until it is. That is deliberate sequencing, not an oversight.
|
|
JWT_SECRET="<generate with: openssl rand -base64 32>"
|
|
|
|
# NOT Vaultwarden's, despite the name and where it used to sit — it is the platform's at-rest key,
|
|
# encrypting every secret column in Postgres: Headscale admin API keys, app-store service
|
|
# credentials, Jellyfin tokens, wallet node credentials, and the wallet seed envelope on top of the
|
|
# owner passphrase that seals it.
|
|
#
|
|
# CHANGING IT MAKES ALL OF THAT UNREADABLE AT ONCE, and for the seed that is unrecoverable: the
|
|
# passphrase opens the inner envelope and this is the outer one.
|
|
VAULT_STORE_KEY="<generate with: openssl rand -base64 32>"
|
|
|
|
# ── Optional ───────────────────────────────────────────────────────────────────────────────────
|
|
# Where Officer is reached from a browser. Read by origin validation, the task API host check, and
|
|
# the CalDAV iOS profile builder — which is the only one that hard-requires it, and demands https.
|
|
# PUBLIC_URL=https://officer.example.com
|
|
|
|
# Guards (CORS origin checks, rate limits, password-strength rules) are ON unless this is set to
|
|
# "dev" or "development". Unset is hardened, which is why officer-setup no longer writes it — set it
|
|
# by hand, on a local machine you trust, to develop. Note that `bun dev` does NOT set it: that script
|
|
# only loads this file, so `bun dev` against a production .env runs fully hardened.
|
|
# PUBLIC_BUILD_ENV=dev
|
|
|
|
# DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR were here until 2026-08-12 and are no longer read.
|
|
# The install root is derived as the parent of the working directory (src/servers/data-path.ts), so
|
|
# data/, capabilities/ and dockers/ follow from it; the owner's home comes from the OS. Three values
|
|
# that had to agree with each other and with the disk became one that cannot disagree.
|
|
|
|
# ── Sidecars ────────────────────────────────────────────────────────────────────────────────────
|
|
# Each sidecar owns its upstream's credentials; the platform API is only a thin auth+forward proxy
|
|
# and never sees them. An unset upstream URL is not fatal — the sidecar logs a warning at boot and
|
|
# answers 503 until it is set, so you can run Officer with any subset of these configured.
|
|
|
|
# Transmission (officer-transmission) is configured from the app, not from here — Transmission →
|
|
# Connection. The daemon URL, the optional RPC auth and the RPC path live in `service_connections`,
|
|
# with the password encrypted, so nothing outside the sidecar can read it.
|
|
|
|
# InvoiceShelf (officer-invoiceshelf) is configured from the app, not from here — Invoices → Connection.
|
|
# Instances, their Sanctum tokens and the company each one is pinned to live encrypted in
|
|
# `invoiceshelf_accounts`, so nothing outside the sidecar can read a token.
|
|
|
|
# slskd (officer-slskd) is configured from the app, not from here — Soulseek → Connection. The
|
|
# daemon URL and its API key live encrypted in `service_connections`; the sidecar injects the key as
|
|
# X-API-Key on every forwarded request.
|
|
|
|
# Vaultwarden (officer-vault). VAULT_STORE_KEY is at the top of this file — it is the platform's
|
|
# key, not Vaultwarden's, however much the name and its old position here suggested otherwise.
|
|
# VAULTWARDEN_URL=http://127.0.0.1:8222
|
|
|
|
# The Anthropic proxy (officer-anthropic-proxy) binds PORT + 1, derived rather than configured — see
|
|
# src/servers/officer-url.mjs. There is nothing to set. It holds no credential from this file either:
|
|
# the upstream token is the OAuth one `claude` writes to ~/.claude/.credentials.json, and the
|
|
# ANTHROPIC_API_KEY the agent presents to it is the proxy's own generated secret.
|
|
|
|
# ReClip — the self-hosted yt-dlp service the download-media capability talks to. Defaults to
|
|
# http://localhost:8899.
|
|
# RECLIP_URL=http://localhost:8899
|
|
|
|
# ── Headscale (/api/vpn) ────────────────────────────────────────────────────────────────────────
|
|
# These drive the /api/vpn router, NOT the officer-headscale sidecar. The sidecar deliberately reads
|
|
# neither, keeping its registered servers and their keys in Postgres so host env can never shadow
|
|
# one. Set these only if you use /api/vpn.
|
|
# HEADSCALE_URL=https://headscale.example.com
|
|
# HEADSCALE_API_KEY="<headscale admin api key>"
|
|
# HEADSCALE_USER=officer
|
|
|
|
# ── Bitcoin wallet (officer-wallet) ─────────────────────────────────────────────────────────────
|
|
# The chain data source is NOT here — it is configured from the app, at Wallet → Settings → Chain
|
|
# source, and stored per owner. Any Esplora-compatible API works (electrs, esplora, mempool.space);
|
|
# it defaults to the public mempool.space until you set one.
|
|
# WALLET_NETWORK=bitcoin # bitcoin | testnet | signet | regtest
|
|
#
|
|
# How long an unlocked wallet stays unlocked, in seconds. Default 900 (15 min). The root key is held
|
|
# in the sidecar's memory for exactly this long after an unlock, then wiped. Shorter is safer.
|
|
# WALLET_UNLOCK_TTL_SEC=900
|
|
#
|
|
# NOTE: seed material is encrypted with VAULT_STORE_KEY (above) on top of the owner passphrase that
|
|
# seals it. Both are required to spend. If you lose VAULT_STORE_KEY, every stored seed is
|
|
# unrecoverable — back up the mnemonics separately, offline.
|
|
|
|
# Immich (officer-photos) is configured from the app, not from here — Photos → Connection. Instances and
|
|
# their API keys live encrypted in `photos_config`, so the platform never sees a key.
|