The sign-up challenge is reachable from the app's own namespace

/account/challenge alone is a dead end for the iPhone app: it talks to
the compat namespace, which shares the sign-up handler but had no route
to ask what work was wanted. Sign-up would refuse for a missing
challenge with nowhere to go and get one.

Mounts the same handler under the compat prefix and lists it in the
namespace index, which is what detectCapabilities reads.

Refs #39

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-02 20:05:29 -04:00
co-authored by Claude Opus 4.8
parent 7040dce5f7
commit 48ce3b167c
3 changed files with 39 additions and 0 deletions
+7
View File
@@ -357,6 +357,13 @@ func configure(_ app: Application) async throws {
compat.post("signup") { req in
try await accounts.signup(req, clientIP: clientIP(req), now: unixNow())
}
// 🔴 The same challenge, served in the namespace the app actually asks
// in. `/account/challenge` alone would be unreachable from a client
// pointed at the compat prefix — it would sign up, be refused for a
// missing challenge, and have nowhere to go and get one.
compat.get("challenge") { req in
try await accounts.signupChallenge(req, now: unixNow())
}
compat.post("signin") { req in
try await accounts.signin(req, clientIP: clientIP(req), now: unixNow())
}
+1
View File
@@ -783,6 +783,7 @@ struct NamespaceIndex: Content {
"/flamenet/v1/messenger/me",
"/flamenet/v1/messenger/users",
"/flamenet/v1/messenger/signup",
"/flamenet/v1/messenger/challenge",
"/flamenet/v1/messenger/signin",
"/flamenet/v1/messenger/session",
"/flamenet/v1/messenger/account",
@@ -156,3 +156,34 @@ struct SignupProofOfWorkTests {
#expect(!issued.challenge.isEmpty)
}
}
/// The challenge has to be reachable from where the client actually asks.
///
/// 🔴 **`/account/challenge` alone is a dead end for the iPhone app.** It talks
/// to the compat namespace (`/api/flamenet/v1/messenger/…`), the same handler
/// the relay's own `/account/signup` uses. Mounting the puzzle in only one of
/// the two places produces the worst shape available: sign-up refused for a
/// missing challenge, and no route to go and get one.
@Suite("the challenge is reachable")
struct SignupChallengeReachabilityTests {
@Test("the compat namespace advertises the challenge beside sign-up")
func advertised() {
let routes = NamespaceIndex.current.routes
#expect(routes.contains("/flamenet/v1/messenger/challenge"),
"a client that reads the index cannot find the puzzle")
// ⚠️ Beside sign-up, not merely present: a client that can sign up but
// cannot ask what work is wanted is the case this whole suite is about.
#expect(routes.contains("/flamenet/v1/messenger/signup"))
}
/// ⚠️ The index is what `detectCapabilities` matches on, so a duplicate
/// entry is not cosmetic — it is a probe that can match twice and a list
/// that has been edited twice by two people who did not see each other.
@Test("the index lists each route once")
func noDuplicates() {
let routes = NamespaceIndex.current.routes
#expect(Set(routes).count == routes.count,
"duplicate route in the namespace index: \(routes)")
}
}