Files
petal/deploy/petal.env.example
T
prosolis 1cf207d73f Phase 16: Petal authenticates for itself
Petal is now an OIDC client in its own right rather than trusting a header
from the proxy. The Phase-0 Resolver seam was the only integration point:
main.go picks the session store when Authentik is configured and the static
local user otherwise, and no handler or query moved for either.

internal/auth gains three pieces. session.go issues an opaque cookie token
and stores only its SHA-256, so a database copy yields nothing usable; the
30-day expiry slides on every request, throttled to one write an hour, and
logout deletes the row rather than just the cookie. oidc.go runs the
authorization-code flow with state, nonce and PKCE, and discovers the
provider lazily and on retry — an Authentik outage should block new logins
without stopping Petal booting or invalidating live sessions. users.go
provisions accounts from the token's claims and gates them on an allowlist
that matches emails as well as subject ids, since a subject is an opaque
uuid that doesn't exist until someone has already logged in once.

Migration 0010 lands sessions, images and users.pair_lang together. The
images table closes the capability-URL hole the Phase-0 audit flagged: a
hash was previously enough to fetch anyone's picture. Rows are keyed
(name, user_id) so one file can have several owners and deduplication
survives; a stranger gets 404 rather than 403, the cache header drops to
private, and files already on disk are claimed at startup or every image
already pasted into a document would 404.

On the frontend a single 401 interceptor feeds a warm bilingual sign-in
overlay, drawn over a still-visible editor because nothing has been taken
away. Behind it is the part that matters: a save that comes back 401
stashes its body to localStorage before anything else and stops the
auto-save loop, and reopening that document after signing in merges the
draft back and saves it. An expired session must not cost writing.

Writing the round-trip test against a stub identity provider turned up a
real bug: the one-shot state/nonce/PKCE cookies were cleared in a defer,
which runs after the redirect has written the response header, so the
clearing Set-Cookie was silently dropped and they lingered for their full
ten minutes.

Also swaps the emoji favicon for a drawn sakura, which renders as Petal's
own rose palette everywhere instead of whatever each platform's font
decides, and doubles as the app tile in Authentik.

Migration 0010 verified against a VACUUM INTO copy of the live millenia
database: counts intact, FTS still matching, the one existing image
claimed.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
2026-07-27 07:21:32 -07:00

71 lines
3.3 KiB
Bash

# Petal — production environment for the parodia.dev VPS.
# Copy to the stack directory as `.env` (docker-compose.yml reads it via
# env_file) and fill in the model names. Values the image already fixes
# (PORT, DATABASE_PATH, IMAGE_DIR, TTS_CACHE_DIR, TTS endpoints) are set in
# docker-compose.yml, not here.
# --- Routing -----------------------------------------------------------------
# Must match the DNS A record and the Traefik Host() rule.
PETAL_HOST=petal.parodia.dev
# Absolute origin the app knows itself by. Phase 16's OIDC redirect URI is
# built from this, so it has to be the real public HTTPS origin.
BASE_URL=https://petal.parodia.dev
# The companion's bedtime nag and the night theme read the container clock.
TZ=Europe/Lisbon
# The container runs as this uid/gid so it can write the ./data bind mount.
# Set both to the output of `id -u` / `id -g` for the account owning the stack
# directory. Wrong values show up as "unable to open database file (14)".
PETAL_UID=1001
PETAL_GID=1001
# --- Interim edge gate (delete when Phase 16 auth lands) ---------------------
# Petal has no authentication of its own yet — StaticResolver hands every
# request the same local user — so Traefik holds the door with basic auth until
# the OIDC flow exists. user:bcrypt-hash, as produced by:
# htpasswd -nbB petal 'your-password'
# /api/health is deliberately exempt (its own router) so monitoring still works.
PETAL_BASIC_AUTH=
# --- LLM (millenia, over headscale) ------------------------------------------
# The only cross-VPN dependency. Petal degrades warmly when it's unreachable:
# spell check, gloss, garden, search, export and read-aloud all keep working and
# the status bar shows 小助手在休息 · Petal's helper is resting.
#
# 100.64.0.2 is millenia on the headscale network. vLLM must be bound to that
# interface (NOT 0.0.0.0 — this host is public); see deploy/README.md.
LLM_BACKEND=vllm
LLM_ENDPOINT=http://100.64.0.2:8000
LLM_MODEL=
LLM_CHAT_MODEL=
# 30s is the local-network default. Over WAN + VPN, with the voice and
# collocation passes sending a whole document, that truncates real work — the
# request is a hard deadline on Complete, and a timeout surfaces as the same
# warm 502 as an unreachable model. 90s leaves headroom without letting a
# genuinely wedged backend hang the pass forever.
LLM_TIMEOUT=90s
# --- Read-aloud (Piper sidecars) ---------------------------------------------
# Endpoints are wired in docker-compose.yml; these pick the voice each sidecar
# loads. Changing one means recreating that container so it downloads the model.
TTS_VOICE_EN=en_US-amy-medium
TTS_VOICE_ZH=zh_CN-huayan-medium
TTS_AUDIO_FORMAT=mp3
TTS_TIMEOUT=15s
# --- Auth (Authentik OIDC) ---------------------------------------------------
# Authentik already runs on this host. Set all three and Petal authenticates
# for itself; leave any unset and it falls back to the single `local` user
# (which on a public host means the Traefik basic-auth gate must stay).
#
# AUTHENTIK_URL is the provider's issuer, and the redirect URI to register in
# Authentik is https://petal.parodia.dev/auth/callback.
# AUTHENTIK_URL=https://auth.parodia.dev/application/o/petal/
# AUTHENTIK_CLIENT_ID=petal
# AUTHENTIK_CLIENT_SECRET=
#
# Who may sign in: comma-separated subject ids and/or emails. Empty = anyone
# Authentik authenticates, which is wider than this instance wants.
# PETAL_ALLOWED_SUBS=