Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

Update TUTORIAL.md: self-service dashboard actions are now built

Documents the two new dashboard buttons and the one-time share feature
shipped in the previous commit, and splits the former "menu of options"
into what's actually built (share page + QR) vs. still just an idea
(PAKE exchange, delegated Forgejo token minting).

+19 -8
+19 -8
TUTORIAL.md
··· 54 54 - **When a third party must relay it, use something that dies after one read** — a password manager's one-time-share feature (Bitwarden Send, 1Password Psst), or a self-hosted equivalent (`onetimesecret.com` is open source and self-hostable). Never a plain link with no expiry. 55 55 - **Rotate it if you're ever unsure it stayed private** — regenerating a Forgejo token is free; guessing whether a six-month-old paste is still up is not. 56 56 57 - See [Future: built-in token exchange](#future-built-in-token-exchange) at the bottom for ideas on `graft` doing this itself instead of leaning on outside tools. 57 + See [Built-in token exchange](#built-in-token-exchange) at the bottom — `graft` now has a one-time secret-share page for exactly this. 58 58 59 59 ## 3. Create the repo on Forgejo 60 60 ··· 214 214 215 215 **A patch/PR never shows up on the other side** — check that the corresponding repo isn't still empty on the destination. The first git sync has to land before patches/issues referencing that content can be mirrored. 216 216 217 - ## Future: built-in token exchange 217 + ## Self-service dashboard actions 218 + 219 + The dashboard (next to the federation heatmap) has two buttons and a footer link that do the manual work above through a browser instead: 220 + 221 + - **`+ Add peer`** (`/add-peer`) — join an existing series. Pick the federation from a dropdown; its Radicle side (RID, seed, explorer) is filled in automatically, never asked for — see "Adding a peer to an existing federation" above for why that's always safe. Paste the new peer's Forgejo details and token, confirm with the admin password, done. Live within one `sync_interval`, no restart. 222 + - **`+ New repo`** (`/new-repo`) — the "Adding a brand-new Radicle node" + fresh-repo path, as an inline onboarding page: it spells out the manual prerequisites (create the Forgejo repo, generate a scoped token, `rad init`, note the RID) and then takes a form with everything once you've done them. 223 + - **`Share a secret, once`** (`/share/new`, footer link) — the one-time-secret page from the list below. Anyone can create a share (it's the mechanism *for* someone who isn't the admin to safely hand a token over); only claiming requires the passcode. 224 + 225 + Both onboarding forms are gated by `admin_password` in `config.yaml` (defaults to `graft` — `graft` logs a warning at startup if it's still set to that; change it). Checked in constant time, rate-limited (5 wrong attempts per IP or 20 total across all IPs, per 15 minutes) — a wrong guess never reveals which field was wrong, and the limiter, not the password's strength, is what actually stands between a stranger and brute-forcing a short shared secret. 218 226 219 - Today, getting a token from the peer who generated it into `graft`'s `token_file` is entirely a human problem — see [Sharing a token safely](#sharing-a-token-safely) above. `graft` could own more of that instead of leaning on outside tools. None of this is built; it's a menu of options worth weighing against each other before picking one, roughly cheapest-to-build first: 227 + ## Built-in token exchange 228 + 229 + Two of the ideas from the token-sharing menu below are live; the rest are recorded for when they're worth it. 220 230 221 - - **Point at an existing one-time-secret tool.** Zero code: document `onetimesecret.com` (self-hostable, open source) as the recommended relay when a direct SSH handoff isn't possible. Lowest effort, but `graft` doesn't control the experience or leave an audit trail of who claimed a token and when. 231 + **Built**, both reachable from the dashboard (see above): 222 232 223 - - **A `graft token share` command, minimal version.** `graft` generates a random opaque ID and a separate 6-digit passcode, stores the token encrypted (with a key derived from the passcode, so the encrypted-at-rest copy alone is useless), and serves it once at `https://graft.cyberwild.org/share/<id>` — the page asks for the passcode before it reveals anything, and the record is deleted server-side the moment it's successfully viewed (a second visit gets "already claimed", not the secret). A short TTL (15 minutes) covers the case where it's never claimed at all. Passcode communicated over a *different* channel than the link — a phone call, a second messaging app — so a leaked link alone is still useless. Straightforward to build on top of the state store already in place; the main design care is making sure the "delete on view" step is atomic (one claim, ever, even under a race). 233 + - **One-time secret share.** `POST /share/new` encrypts whatever's pasted (AES-256-GCM), keyed from a passcode *and* the share's own random id together — the id is never stored in reversible form, only its hash, so a full database dump alone can never derive the key. `GET/POST /share/<id>` claims it: right passcode reveals it once and destroys the record immediately; five wrong passcodes destroy it too, fail-closed; 15 minutes unclaimed and it's gone regardless. 234 + - **QR code.** `/share/<id>/qr.png` — encodes the claim link only, not the passcode (still meant to travel over a separate channel); a convenience for scanning off someone's screen on a call rather than copy-pasting a link. 224 235 225 - - **QR code variant of the same page.** Same backend, but the sending admin displays a QR code on their own screen (in person, or screen-shared on a call) instead of sending a link at all — the receiving admin scans it with their phone. Puts the passcode in the URL fragment (`#passcode=...`) rather than a query param, so it decrypts client-side in the browser and never reaches `graft`'s own access logs. Nicer for a live handoff; no real advantage over the plain link for an async one. 236 + **Not built:** 226 237 227 - - **A wormhole-style PAKE exchange** (the protocol behind [`magic-wormhole`](https://github.com/magic-wormhole/magic-wormhole)): both admins type the same short human-readable code (`7-crossover-clarinet`) into a small CLI, and a key exchange derives a shared secret from the code without either the code or the token ever crossing the wire in the clear — `graft`'s own server, even if fully compromised, never sees the plaintext token at any point. The strongest guarantee of the options here, and the most to build (needs a relay endpoint speaking the handshake, and both sides need shell access rather than just a browser) — worth it mainly if token handoffs become frequent enough to justify the investment. 238 + - **A wormhole-style PAKE exchange** (the protocol behind [`magic-wormhole`](https://github.com/magic-wormhole/magic-wormhole)): both admins type the same short human-readable code into a small CLI, and a key exchange derives a shared secret from the code without either the code or the token ever crossing the wire in the clear — `graft`'s own server, even if fully compromised, never sees the plaintext token at any point. The strongest guarantee here, and the most to build (a relay endpoint speaking the handshake, and both sides need shell access rather than just a browser). Deliberately skipped for now: the one-time-share page above already closes the gap it was meant to close, at a fraction of the engineering cost, and re-litigating that trade only makes sense if token handoffs become frequent enough that the share page's residual risk (a compromised link *and* a brute-forced passcode, however rate-limited) stops being acceptable. 228 239 229 - - **Skip the handoff entirely.** The most elegant fix doesn't share a secret better — it avoids minting a long-lived one a human has to move at all. If Forgejo ever exposes a delegated token-creation flow (the peer's admin clicks a link, logs into *their own* Forgejo, and a token scoped to exactly `write:repository` + `write:issue` on exactly that repo is minted and handed to `graft` server-to-server), no plaintext secret ever needs to touch a clipboard, a chat window, or a QR code in the first place. Worth checking against Forgejo's OAuth2/application-token APIs before building any of the options above. 240 + - **Skip the handoff entirely.** The most elegant fix doesn't share a secret better — it avoids minting a long-lived one a human has to move at all. If Forgejo ever exposes a delegated token-creation flow (the peer's admin clicks a link, logs into *their own* Forgejo, and a token scoped to exactly `write:repository` + `write:issue` on exactly that repo is minted and handed to `graft` server-to-server), no plaintext secret ever needs to touch a clipboard, a chat window, or a QR code in the first place. This is genuinely the right long-term direction — everything built above, share page included, is a workaround for a token having to exist as a string a human copies, and this is the only option that makes that string never exist. It isn't built because it depends on Forgejo's own OAuth2/application-token APIs supporting a delegated, scope-limited minting flow that a third party can drive — worth checking directly against a current Forgejo instance before investing in it, since the answer decides whether this is a small integration or a real feature request upstream.