Rename Tools → Theseus, trim /tlds/ self-ref, chipnet auth + state fixes

Site:
- Rename the "Tools" nav label to "Theseus" on every subpage. URL slug
  /tools/ is kept intact so bookmarks and cross-links survive; the page's
  own title + h1 rename to Theseus; homepage product card retitled.
- Remove the /tlds/ self-nav entry so its 7-item header matches every
  other subpage (was 8 items, causing the same "header rearranges" glitch
  Hephaestus had before).

Auth-proxy (fixes reported by a concurrent session working with a chipnet
wallet):
- verify.ts now auto-detects the CashAddr prefix (bitcoincash, bchtest,
  bchreg) instead of hardcoding "bitcoincash". Chipnet wallets sign the
  same message with the same key but display bchtest: addresses; the old
  code compared their claim against a re-encoded bitcoincash: version and
  always rejected. Same key material, different envelope.
- /challenge accepts any of the three known prefixes at the input gate.
- /verify accepts an optional `state` param in the request body — used as
  a fallback if the challenge record didn't carry one (which happens when
  a scripted caller runs the /verify step without passing state through
  /challenge). Ensures RFC 6749 §4.1.2 compliance: the OAuth callback URL
  always echoes state when the client provided one.
This commit is contained in:
Local Dev 2026-08-31 04:16:42 +02:00
parent abf21785bc
commit 9baa5db887
2 changed files with 33 additions and 7 deletions

View file

@ -109,8 +109,14 @@ app.post<{
Body: { cashaddr: string; state?: string; redirect_uri?: string };
}>("/challenge", async (req, reply) => {
const { cashaddr, state, redirect_uri } = req.body ?? {};
if (!cashaddr || !cashaddr.startsWith("bitcoincash:")) {
return reply.code(400).send({ error: "cashaddr must start with bitcoincash:" });
// Accept any CashAddr prefix the chain actually uses:
// bitcoincash: (mainnet)
// bchtest: (chipnet / testnet — what Silent Mode's whole stack runs on today)
// bchreg: (regtest)
if (!cashaddr || !/^(bitcoincash|bchtest|bchreg):/.test(cashaddr)) {
return reply.code(400).send({
error: "cashaddr must start with bitcoincash:, bchtest: or bchreg:",
});
}
const rec = createChallenge(cashaddr, state, redirect_uri);
const message = buildChallenge({
@ -123,10 +129,14 @@ app.post<{
});
// --- Signature verification & OAuth code mint ---
// `state` in the body is a fallback so scripted callers who bypass /challenge's
// state-passing (or use their own OAuth kickoff wrapper) can still get RFC 6749
// §4.1.2 compliant callbacks. If state was stored via /challenge it's used
// automatically; explicit body override wins if both are present.
app.post<{
Body: { nonce: string; signature: string };
Body: { nonce: string; signature: string; state?: string; redirect_uri?: string };
}>("/verify", async (req, reply) => {
const { nonce, signature } = req.body ?? {};
const { nonce, signature, state: stateOverride, redirect_uri: redirectOverride } = req.body ?? {};
if (!nonce || !signature) {
return reply.code(400).send({ error: "nonce and signature required" });
}
@ -142,11 +152,12 @@ app.post<{
const recovered = verifyBchSignedMessage(rec.cashaddr, message, signature);
if (!recovered) return reply.code(401).send({ error: "signature mismatch" });
const redirectUri = rec.redirectUri ?? FORGEJO_REDIRECT_URI;
const redirectUri = redirectOverride ?? rec.redirectUri ?? FORGEJO_REDIRECT_URI;
const code = mintAuthCode(recovered, CLIENT_ID, redirectUri);
const url = new URL(redirectUri);
url.searchParams.set("code", code);
if (rec.clientState) url.searchParams.set("state", rec.clientState);
const stateToEcho = stateOverride ?? rec.clientState;
if (stateToEcho) url.searchParams.set("state", stateToEcho);
return { redirect: url.toString() };
});

View file

@ -62,13 +62,28 @@ function magicHash(message: string): Uint8Array {
/**
* Verify a base64 recoverable-ECDSA signature against a message and cashaddr.
* Returns the recovered cashaddr on success, or null on any mismatch.
*
* The prefix is auto-detected from the claimed cashaddr, so both
* bitcoincash:qq... (mainnet)
* bchtest:qq... (chipnet / testnet)
* bchreg:qq... (regtest)
* work with identical key material. This matters because chipnet wallets
* (which the whole Silent Mode stack runs on today) sign the same message
* with the same key, but display their address with the bchtest: prefix.
*/
const KNOWN_PREFIXES = ["bitcoincash", "bchtest", "bchreg"] as const;
type Prefix = (typeof KNOWN_PREFIXES)[number];
export function verifyBchSignedMessage(
claimedCashaddr: string,
message: string,
signatureBase64: string,
prefix: "bitcoincash" | "bchtest" = "bitcoincash",
): string | null {
const prefixMatch = claimedCashaddr.match(/^([a-z]+):/);
if (!prefixMatch) return null;
const prefix = prefixMatch[1] as Prefix;
if (!KNOWN_PREFIXES.includes(prefix)) return null;
const sig = base64ToBin(signatureBase64);
if (typeof sig === "string" || sig.length !== 65) return null;