Files
petal/.env.example
T
prosolis 69bf3ffde1 Close the door the edge gate used to hold
A security review of the whole repo. The queries were already scoped, the
OIDC flow already did state and nonce and PKCE, the session tokens were
already stored as hashes. What it found was mostly the seam between the
code and the deployment — and one place where the deployment quietly
undid the code.

The one that matters: with any AUTHENTIK_* variable missing, Petal fell
back to resolving every request to the single `local` user. That is right
on a laptop and a catastrophe on a public host, and Phase 16 removed the
Traefik basic-auth gate that used to stand behind the mistake. A typo in
the client secret would have served her journals to the open internet and
said so only in a log line nobody reads. It now refuses to start, guarded
by default for any BASE_URL that isn't loopback.

Then the one that would have been fixed and wasn't: stored images now
serve under `default-src 'none'; sandbox`, so an SVG pasted into a
document can't run as a page on Petal's own origin. Traefik's
customresponseheaders *overwrites*, so the CSP declared in the compose
labels would have silently replaced that per-route policy in production.
The whole header block moved into the binary, where a route can tighten
its own and a test can prove it; only HSTS stays at the edge, where TLS
actually terminates.

The rest, smaller:

  - PETAL_ALLOWED_SUBS empty means everyone authentik authenticates, and
    authentik here fronts half a dozen applications. Still legal, now
    said out loud every boot, and set in both env examples.
  - LLM failures relayed err.Error() to the browser, which carries the
    address of the inference box on the far side of the VPN. Logged
    instead; the client only ever rendered "the helper is resting".
  - Exports scheme-check their links. Escaping makes a URL safe to sit
    in an attribute and says nothing about following it, and an export
    is the one artifact here meant to leave. Writing the test found the
    markdown image src, which I'd missed reading it.
  - The draft rescue is namespaced per account and cleared on sign-out.
    Everything else in localStorage is a preference; this is her unsaved
    writing, sitting in a profile two people share.
  - /auth/logout is POST-only. With SameSite=Lax a GET route lets any
    page on the internet sign her out mid-draft.
  - Image uploads get a per-account allowance and the TTS cache a size
    cap. Both share the encrypted volume the database is on, and a full
    disk is SQLite failing to write, not a feature degrading.
  - The session cookie takes the __Host- prefix over https, so nothing
    else under parodia.dev can plant one. Old cookies still resolve;
    nobody is signed out to get there.
  - npm audit: linkify-it and postcss.

Verified: go build, go vet, the full Go suite, tsc, 195 frontend tests,
npm audit clean. The startup guard and both CSPs checked against a
running server rather than only asserted.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
2026-07-27 18:24:47 -07:00

77 lines
3.6 KiB
Bash

# Petal — copy to .env and fill in. All values have local-dev defaults; override as needed.
# Server
PORT=8080
BASE_URL=http://localhost:8080
# Database (SQLite, pure-Go modernc — no cgo)
DATABASE_PATH=./data/petal.db
# On-disk store for images pasted/dropped/inserted in the editor
IMAGE_DIR=./data/images
# DreamDict's built dictionary (French, European Portuguese, Spanish, Mandarin),
# opened read-only beside petal.db. Optional: with no file here, word lookups use
# the embedded English/Chinese datasets, which is how a laptop checkout runs.
# Build one with `go run ./cmd/dictimport` in the dreamdict repo.
DICT_PATH=./data/dict.db
# LLM
LLM_BACKEND=vllm # vllm | ollama
LLM_ENDPOINT=http://localhost:8000 # vLLM :8000, Ollama :11434
LLM_MODEL= # checkpoint model (small/fast). Never hardcoded.
LLM_CHAT_MODEL= # Ask Petal model (Mandarin-native). Falls back to LLM_MODEL if empty.
LLM_TIMEOUT=30s
# Read-aloud (TTS). Off unless TTS_ENDPOINT is set — when empty, the /api/tts route
# is not mounted and the frontend falls back to the browser's Web Speech API.
# Backed by a local Piper HTTP server (python3 -m piper.http_server).
# Each Piper HTTP server loads ONE voice, so English and Chinese need separate
# instances (different ports). zh is only routed when both TTS_ENDPOINT_ZH and
# TTS_VOICE_ZH are set; otherwise Chinese falls back to Web Speech.
TTS_ENDPOINT= # e.g. http://127.0.0.1:5005 — empty disables server TTS
TTS_ENDPOINT_ZH= # e.g. http://127.0.0.1:5006 — Chinese Piper instance
TTS_VOICE_EN=en_US-amy-medium # Piper voice id for English
TTS_VOICE_ZH=zh_CN-huayan-medium # Piper voice id for Chinese
TTS_PATH=/ # path Piper serves synthesis on: "/" up to piper-tts 1.5, "/synthesize" from 1.6.0
TTS_CACHE_DIR=./data/tts # on-disk store for synthesized clips (content-addressed)
TTS_TIMEOUT=15s
TTS_AUDIO_FORMAT=mp3 # mp3 | opus | wav — mp3/opus transcode Piper's WAV via ffmpeg
# --- Auth (Authentik OIDC) ---
#
# Login turns on only when the issuer, client id and secret are all set. Leave
# them commented out for local development and Petal runs as the single
# hardcoded `local` user, exactly as it did before auth landed.
#
# That fallback is scoped to development on purpose. With a BASE_URL naming
# anything but localhost, Petal refuses to start rather than run open — see
# PETAL_REQUIRE_AUTH — because the fallback on a reachable host means every
# anonymous visitor is the `local` user, with full read and write over every
# document in the database.
#
# AUTHENTIK_URL is the issuer of the Petal provider in Authentik (the value of
# its "OpenID Configuration Issuer" field). The redirect URI to register there
# is BASE_URL + /auth/callback.
# AUTHENTIK_URL=https://auth.parodia.dev/application/o/petal/
# AUTHENTIK_CLIENT_ID=petal
# AUTHENTIK_CLIENT_SECRET=
#
# Who may sign in: comma-separated OIDC subject ids and/or email addresses.
# Empty = anyone Authentik authenticates, which is right for a single-household
# instance and wrong the moment the IdP serves a wider audience than Petal. An
# empty list is warned about at every boot rather than assumed either way.
# PETAL_ALLOWED_SUBS=her@example.com,me@example.com
#
# Whether a missing OIDC configuration is fatal. Defaults to false for a
# loopback BASE_URL and true for anything else, so neither a laptop nor a
# deployment normally has to name it.
# PETAL_REQUIRE_AUTH=true
# --- Deferred (not wired in the local-dev build) ---
# Copyleaks (Tier-2 plagiarism) — deferred; needs a public webhook
# COPYLEAKS_ENABLED=false
# COPYLEAKS_API_KEY=
# COPYLEAKS_EMAIL=