Self-service federation onboarding + one-time secret sharing
New internal/admin package, wired into the dashboard:
- POST /share/new + GET/POST /share/<id>: a one-time secret share.
AES-256-GCM key derived from the share's own high-entropy id plus a
6-digit passcode, neither of which is ever stored together — only the
id's hash goes in the database, so a full DB dump alone can never
derive the decryption key. Destroyed on first successful claim, after
5 wrong passcodes, or after 15 minutes, whichever comes first. QR code
(pure-Go, github.com/skip2/go-qrcode) encodes the claim link only —
the passcode is still meant to travel over a separate channel.
- GET/POST /add-peer: join an existing federation. Picks up the
series' Radicle side (RID, seed, explorer) automatically — nothing
Radicle-related is ever user-supplied for this flow, since Radicle
already replicates to whatever's seeding that RID.
- GET/POST /new-repo: start a repo graft has never seen, with the
manual prerequisite steps (create the Forgejo repo + token, rad init,
note the RID) spelled out inline before the form.
Both onboarding forms are gated by a single shared admin password
(config.yaml's admin_password, defaulting to "graft" with a startup log
warning when left at that default) checked in constant time, backed by
a rate limiter — 5 failures per IP or 20 globally per 15-minute window,
independent of X-Forwarded-For accuracy. A submitted token is written to
its own chmod-600 file under the same directory static pairs already
use, following the existing token_file convention exactly; only the
path is ever persisted to the database.
Submissions land in a new dynamic_repo table and go live on the very
next sync pass — no restart. cmd/sync/main.go now tracks all runtime
registration (syncers, topology, AI-detection state) behind a mutex,
since it's written from the ticker goroutine and read from HTTP handler
goroutines; each pass checks for newly-approved dynamic repos, builds a
RepoSyncer for each, and rebuilds the dashboard's topology and
Authorized-Integration detection to include them.
CSP's form-action tightened from 'none' to 'self' — the new pages are
plain HTML forms posting back to graft itself; 'none' would have
silently blocked every one of them.
Verified: 9 functional tests (share round-trip, one-time destruction,
max-attempts destruction, wrong-password rejection, rate limiting
blocking even a correct password once tripped, successful add-peer/
new-repo with real DB row + token file content/permissions, QR
generation, clean GET rendering) plus a live production round trip
(share create/claim/QR, wrong-password rejection leaving no trace) and
a Playwright pass confirming zero CSP violations on all three new pages.