docs(hephaestus.x): §12 for parallel sessions + dots-are-OK note in §4

Parallel session assumed (a) Forgejo usernames can't contain dots, and (b) a
wallet mnemonic has to be pasted into a browser to sign in. Both wrong — the
docs now show that.

- §4 gets an 'Allowed characters' subsection listing what Forgejo actually
  accepts (letters, digits, hyphens, underscores, dots). game.x works as-is
  as a username or an org name.
- New §12 'Working with other sessions' — the wallet-vs-PAT split, a full
  five-step recipe for delegating a namespace to a parallel session
  (create org / bootstrap user / add to Owners / mint PAT / clone+push),
  a scriptable programmatic sign-in template so mnemonics never touch a
  browser, and a scope-per-session-shape table.
- TOC + §13 (Help+source) renumbered.
This commit is contained in:
Local Dev 2026-09-20 01:57:46 +02:00
parent cf2995c73c
commit f34535721f

View file

@ -163,6 +163,7 @@
<li><a href="#storage">Where files live</a></li>
<li><a href="#selfhost">Self-hosting</a></li>
<li><a href="#gotchas">Known gotchas</a></li>
<li><a href="#sessions">Working with other sessions</a></li>
<li><a href="#help">Help + source</a></li>
</ol>
</aside>
@ -217,6 +218,18 @@
<h4>From the UI</h4>
<p><a href="https://code.silentmode.st/user/settings">Settings → Profile → Username</a>. Change the field, save. All your repo URLs move to <code>/&lt;new-name&gt;/…</code> and the old URLs 307-redirect for a while so bookmarks survive.</p>
<h4>Allowed characters</h4>
<p>Forgejo names — usernames, org names, repo names — accept letters (mixed case), digits, hyphens, underscores, <b>and dots</b>. So all of these are valid:</p>
<ul>
<li><code>alice</code></li>
<li><code>game.x</code> <span class="pill">dots OK</span></li>
<li><code>Game.X</code></li>
<li><code>game-x</code></li>
<li><code>Silent_Mode</code></li>
<li><code>bch_qp3hvzy09lzcm46qm4cd</code></li>
</ul>
<p>Rejected: whitespace, slashes, and reserved routes like <code>assets</code>, <code>login</code>, <code>api</code>. If you're picking a name for an on-chain <code>.x</code> project, the natural mapping (<code>game.x</code> repo owner) works — no need to substitute a hyphen.</p>
<h4>From the admin API (if you're the operator)</h4>
<pre><span class="cm"># The endpoint expects FORM-encoded, not JSON. If you send JSON it silently returns 422 "NewName required".</span>
curl -X POST -H "Authorization: token $TOKEN" \
@ -346,7 +359,105 @@ open https://your-domain.example/user/oauth2/hephaestus-wallet</pre>
<li><b>Docker Hub rate limits</b> can bite <code>docker compose build</code> if you're unauthenticated. Log in once with <code>docker login</code> against Docker Hub before running the stack build.</li>
</ul>
<h2 id="help">12. Help + source</h2>
<h2 id="sessions">12. Working with other sessions</h2>
<p>Any tool, colleague, or parallel Claude session that needs to push to Hephaestus goes through the same short workflow. Once you internalize the wallet-vs-PAT split, everything else is one API call at a time.</p>
<h3>Wallet vs Personal Access Token — when to use each</h3>
<p>The wallet is for <b>bootstrapping the account</b> once, and for <b>recovering</b> if a PAT ever leaks. Nothing else. Every git operation, every API call, every docker push, from every session, from every day after the first — runs on a PAT.</p>
<table>
<tr><th>Task</th><th>Auth</th></tr>
<tr><td>Create the user account for the first time</td><td>Wallet sign-in</td></tr>
<tr><td>Push, pull, clone, edit repo settings</td><td>PAT (<code>write:repository</code>)</td></tr>
<tr><td>Docker push / pull to the OCI registry</td><td>PAT (<code>write:package</code>)</td></tr>
<tr><td>Create repos under an org, manage members</td><td>PAT (<code>write:organization</code>)</td></tr>
<tr><td>Revoke a compromised PAT</td><td>Any PAT with <code>write:user</code>, or admin</td></tr>
<tr><td>Rotate ownership (transfer everything to a new wallet)</td><td>Wallet sign-in on the new address + admin transfer</td></tr>
</table>
<div class="note info"><b>Never share a wallet mnemonic across sessions.</b> A parallel session doesn't need it. Give them a PAT with the narrowest scope that works, and revoke it independently when they're done.</div>
<h3>Bootstrapping a new namespace for another session</h3>
<p>Say a parallel session needs to push code to <code>game.x/checkers</code>. Full recipe, five steps, no browser required for the human at all:</p>
<pre><span class="cm"># 1. Admin creates the org (one API call, no wallet involvement)</span>
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"game.x","visibility":"public"}' \
https://code.silentmode.st/api/v1/orgs
<span class="cm"># 2. Whoever owns the game.x wallet signs in ONCE to bootstrap their user.
# Either via browser (open /user/oauth2/hephaestus-wallet)
# or programmatically (see next subsection — mnemonic never touches a browser).
# Result: a user account named `bch_&lt;first-20-of-cashaddr&gt;`.</span>
<span class="cm"># 3. Admin adds that user to game.x/Owners</span>
OWNERS_ID=$(curl -sk -H "Authorization: token $ADMIN_TOKEN" \
https://code.silentmode.st/api/v1/orgs/game.x/teams | \
python -c "import sys,json; \
print(next(t['id'] for t in json.load(sys.stdin) if t['name']=='Owners'))")
curl -X PUT -H "Authorization: token $ADMIN_TOKEN" \
https://code.silentmode.st/api/v1/teams/$OWNERS_ID/members/bch_&lt;addr&gt;
<span class="cm"># 4. That user generates a PAT with write:repository scope
# at Settings → Applications → Generate New Token.
# Hand only the PAT to the parallel session.</span>
<span class="cm"># 5. Session clones + pushes using the PAT as HTTPS password</span>
git clone https://code.silentmode.st/game.x/checkers.git
cd checkers && ... && git push</pre>
<h3>Programmatic wallet sign-in (mnemonic stays local)</h3>
<p>If the wallet owner wants to bootstrap without ever opening a browser — for a headless workstation, a CI job, or an AI session that reads its wallet from disk — the whole sign-in flow is scriptable. The mnemonic sits in a local <code>wallets.json</code>, never gets pasted anywhere, never enters a chat log.</p>
<p>Sketch (Node + <a href="https://github.com/bitauth/libauth">@bitauth/libauth</a>):</p>
<pre>import { deriveHdPath, deriveHdPrivateNodeFromSeed, deriveSeedFromBip39Mnemonic,
encodeCashAddress, hash160, hash256, secp256k1, utf8ToBin, binToBase64,
CashAddressType } from "@bitauth/libauth";
import { readFileSync } from "node:fs";
const mnemonic = JSON.parse(readFileSync("wallets.json", "utf8")).main.seed;
const seed = deriveSeedFromBip39Mnemonic(mnemonic);
const root = deriveHdPrivateNodeFromSeed(seed);
const child = deriveHdPath(root, "m/44'/145'/0'/0/0");
const pub = secp256k1.derivePublicKeyCompressed(child.privateKey);
const cashaddr = encodeCashAddress({ prefix: "bchtest",
type: CashAddressType.p2pkh, payload: hash160(pub) }).address;
<span class="cm">// 1. kick off OIDC → get state + redirect_uri</span>
const kick = await fetch("https://code.silentmode.st/user/oauth2/hephaestus-wallet",
{ redirect: "manual" });
const url = new URL(kick.headers.get("location"));
const state = url.searchParams.get("state");
const redirect_uri = url.searchParams.get("redirect_uri");
<span class="cm">// 2. challenge → nonce + message</span>
const { nonce, message } = await (await fetch(
"https://code.silentmode.st/auth/challenge",
{ method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ cashaddr, state, redirect_uri }) })).json();
<span class="cm">// 3. sign — Bitcoin Signed Message (see PROTOCOL.md for the exact bytes)</span>
const signature = signBitcoinMessage(child.privateKey, message); <span class="cm">// see PROTOCOL.md</span>
<span class="cm">// 4. verify → callback URL</span>
const { redirect } = await (await fetch(
"https://code.silentmode.st/auth/verify",
{ method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ nonce, signature, state }) })).json();
<span class="cm">// 5. follow callback with a cookie-jar client → session cookie in hand
// → GET /user/settings/applications to mint a PAT for future use</span></pre>
<p>Reference implementations in the tree: <a href="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/auth-proxy/src/verify.ts"><code>auth-proxy/src/verify.ts</code></a> for byte-perfect signing, and <a href="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/PROTOCOL.md"><code>PROTOCOL.md</code></a> for the full wire format. Adapt for <code>bitcoincash:</code> / <code>bchreg:</code> by swapping the <code>prefix</code>.</p>
<h3>Sensible PAT scopes per session shape</h3>
<table>
<tr><th>Session shape</th><th>Scopes</th><th>Rationale</th></tr>
<tr><td>CI job pushing container images</td><td><code>write:package</code></td><td>Nothing else needed</td></tr>
<tr><td>Docs / content writer</td><td><code>write:repository</code></td><td>Push commits, edit metadata</td></tr>
<tr><td>Ops session managing an org</td><td><code>write:organization</code>, <code>write:repository</code></td><td>Create repos, adjust members</td></tr>
<tr><td>Analytics / read-only reader</td><td><code>read:repository</code>, <code>read:package</code></td><td>Zero write surface</td></tr>
<tr><td>Dedicated admin session</td><td>Admin account + <code>write:organization</code>, <code>write:user</code></td><td>Full backend</td></tr>
</table>
<p><b>Rotation habit:</b> issue one PAT per session, scope narrowly, revoke the moment the session ends or a machine changes hands. Every token has a name — use it (<code>ci-fly-x-deploy</code>, <code>session-2026-09-20</code>) so you know which one to revoke when things move.</p>
<h2 id="help">13. Help + source</h2>
<ul>
<li>Source: <a href="https://code.silentmode.st/silentmode/hephaestus">silentmode/hephaestus</a> (MIT)</li>
<li>Wire-format spec: <a href="https://code.silentmode.st/silentmode/hephaestus/src/branch/master/PROTOCOL.md">PROTOCOL.md</a></li>