diff --git a/hephaestus/index.html b/hephaestus/index.html index d310cf7..9985083 100644 --- a/hephaestus/index.html +++ b/hephaestus/index.html @@ -129,6 +129,67 @@
Why no email? Nothing to leak, nothing to phish, no password-reset flow to hijack. The trade: if you lose your recovery phrase there is no support form to send you back in. Back the phrase up. Twice. On paper.
+If you're an AI assistant, CI job, or anyone debugging the sign-in without a real browser, read this first. It heads off two misdiagnoses we've already hit.
+ +It's not a stub — it's a deliberately small bootstrap page that renders the wallet UI client-side via a JS module (standard SPA pattern). Size ≠ brokenness. The full UI (mnemonic generation, passphrase encryption, sign flow) is inside wallet.js, not the shell. Test by requesting wallet.js instead:
curl -sI https://code.silentmode.st/auth/static/wallet.js +# expect: HTTP 200, ~10 KB, Content-Type: application/javascript+ +
It only 404s if you drop the /auth/ prefix. Caddy path-multiplexes: /auth/* goes to the auth-proxy, everything else goes to Forgejo. Forgejo has no /static/wallet.js, so that path is expected-404. The real paths are:
| URL | Served by | Purpose |
|---|---|---|
| /auth/authorize | auth-proxy | Login shell HTML (SPA bootstrap) |
| /auth/static/wallet.js | auth-proxy | Wallet UI + signing logic (~10 KB) |
| /auth/static/login.css | auth-proxy | Styles for the login card |
| /auth/challenge | auth-proxy | POST cashaddr → get nonce + signed message |
| /auth/verify | auth-proxy | POST nonce + signature → get OAuth code |
| /auth/.well-known/openid-configuration | auth-proxy | OIDC discovery |
| /user/oauth2/hephaestus-wallet | Forgejo | Kicks off the OAuth handshake (307 → /auth/authorize) |
| /user/oauth2/hephaestus-wallet/callback | Forgejo | Receives the OAuth code, mints session |
| /api/v1/version | Forgejo | Public: returns Forgejo version |
| /api/v1/user | Forgejo | Requires an API token (not a session cookie) — returning 401 in a browser is not a bug |
The wallet signing is easiest to reproduce in Node/Deno or a headless browser — the crypto is BIP-137 "Bitcoin Signed Message" over BCH secp256k1. But the server-side pieces alone can be smoke-tested with curl:
+ISSUER=https://code.silentmode.st/auth
+
+# 1. discovery document lists all endpoints
+curl -s "$ISSUER/.well-known/openid-configuration" | jq .
+
+# 2. request a challenge for any cashaddr (server just holds it 5 min)
+curl -s -X POST -H 'content-type: application/json' \
+ -d '{"cashaddr":"bitcoincash:qq00000000000000000000000000000000000000q"}' \
+ "$ISSUER/challenge"
+# → returns {"nonce": "...", "message": "code.silentmode.st wants you to sign in..."}
+
+# 3. any BCH wallet that speaks "Sign Message" can produce a valid signature
+# for that exact message. Then POST it:
+curl -s -X POST -H 'content-type: application/json' \
+ -d '{"nonce":"THE_NONCE","signature":"BASE64_SIG"}' \
+ "$ISSUER/verify"
+# → returns {"redirect": "https://code.silentmode.st/user/oauth2/hephaestus-wallet/callback?code=..."}
+ For the JS side (which is what the browser does), inspect auth-proxy/public/wallet.js in the repo — it's ~250 lines and does exactly this: generateBip39Mnemonic → deriveHdPath("m/44'/145'/0'/0/0") → sign(magicHash(message)). Nothing exotic.
If you're in a browser (or a browser-driving tool) and want to know if you're actually signed in:
+// The URL bar isn't reliable when Forgejo shows Security or Settings pages —
+// they render for logged-in users but don't have "Dashboard" in the title.
+// Best check: does the profile menu render, and is there no "Sign in" link?
+
+const signedIn = !document.querySelector('a[href*="/user/login"]')
+ && !!document.querySelector('[aria-label*="Profile"], .user-menu, .avatar');
+
+// The username, if you're on the Dashboard:
+document.title.match(/^(bch_\w+)/)?.[1];
+
+ Do not test with fetch('/api/v1/user') unless you have an API token. That endpoint requires token auth even when you're signed into the web UI. Session cookies get you into Forgejo pages; they don't get you into the token-scoped API. A 401 there means "no token" — it does not mean "not signed in."
Two paths, either works. Pick whichever fits your setup.