16 Commits

Author SHA1 Message Date
Josh Creek f628ccfd35 feat(auth): wire production Steam sign-in and the client login flow
newAPIService never supplied SteamLogin, so POST /v1/session/steam
always returned 503 auth_unavailable in production. The only
implementation was cmd/testkit-api's fake, which derives an identity
from the ticket string itself and accepts anything -- so the passing
integration path was neither deployable nor secure. On the client side
the game started with an empty token and a loopback base URL, and no
production code called configure() or login_steam(); the menu entered
matchmaking directly, so every request failed ERR_UNAUTHORIZED before
reaching the network.

Add a real ISteamUserAuth/AuthenticateUserTicket adapter behind an
interface, so the production login path is testable with only the Valve
call stubbed. It rejects family-shared copies (the account playing does
not own the app) and, by default, VAC- or publisher-banned accounts, and
refuses malformed tickets locally rather than forwarding them.

Crucially it separates our faults from the player's: a Valve outage or a
revoked publisher key returns 503, not 401. Answering 401 would tell a
legitimate player their login failed and send them to fix an account
that is fine while the real fault went unnoticed. A banned identity now
returns 403 rather than a misleading 503.

Sign-in is configuration-gated on the publisher key and App ID: without
them the endpoint keeps returning 503, since silently accepting an
unverified ticket would be worse than refusing to authenticate. A
returning player keeps the player ID they already had, so ratings,
penalties and bans follow the account rather than the session.

Client side: acquire a web-API ticket through GodotSteam's async
signal -- requesting one returns a handle, not a ticket -- using the
existing dynamic-call pattern so stock Godot still parses the project.
The endpoint is configurable for release builds, and matchmaking
completes sign-in before it will queue.

Verified against real PostgreSQL; 232 Godot tests pass.
2026-09-05 10:57:50 +01:00
Josh Creek 801fca7cb0 fix(matchmaking): make regional RTT evidence obtainable end to end
domain.validCandidate hard-requires a non-empty PredictedRTT map, but
CreateQueueTicket persisted an empty one and the only endpoint that
could fill it returned 503 in every real binary, because Service.Probe
was assigned nowhere outside api tests. No client-created ticket could
ever be selected by the matcher. The Godot client had no probe method at
all, so even a wired backend was unreachable from the game.

Four distinct defects had to be fixed for this path to work:

Nothing issued the nonce ProbeProvider was meant to compare against, so
the contract could not be satisfied even in principle. Add
POST /v1/probes/{region}/challenge, backed by a durable single-use
challenge -- durable because any replica may serve the answer for a
challenge another replica issued. RTT is the interval between issuing
and receiving, so no client-reported latency reaches placement.

CreateQueueTicket marshalled a nil map to JSON `null`, a JSONB scalar
rather than an object, and jsonb_set rejects that with "cannot set path
in scalar". RecordProbe would have failed at runtime even once wired.
Persist an object, and normalise non-object values in the update for
rows already written.

A nil ProbeRecorder made the handler report success while persisting
nothing, which silently leaves the ticket unmatchable. That is a
misconfiguration, not a successful probe; it now returns 503.

A successful probe updated PostgreSQL only. The candidate inserted at
enqueue time carries an empty RTT map, and the Redis keyspace has its
TTL continually refreshed, so the stale entry need never repair itself.
Refresh that player's projection after the probe commits.

Client side: add the challenge/answer round trip and have the
matchmaking screen collect evidence before creating a ticket, since
queueing first produces a search that can never match. Probing every
region fully is not required -- placement uses whichever regions
answered -- but queueing with none is refused rather than silently
stalling.

New integration test drives the real enqueue and probe paths and then
asks the actual matcher predicate, rather than hand-building a candidate
the way the unit tests do -- which is exactly why they missed this.

Also make the integration schema reset drop the whole public schema: the
enumerated table list silently broke with each new migration.
2026-09-05 10:49:28 +01:00
Josh Creek 510138eb0a feat(audio): wire menu click feedback 2026-09-01 23:19:48 +01:00
Josh Creek b15d19eec2 feat(multiplayer): explain queue and connection health 2026-09-01 23:11:37 +01:00
Josh Creek 27017043f9 feat(multiplayer): explain allocation lifecycle 2026-09-01 22:25:54 +01:00
Josh Creek fd1a4d9577 feat(multiplayer): show authoritative proposal countdown 2026-09-01 22:23:33 +01:00
Josh Creek c08c761af3 fix(multiplayer): recover requeued tickets 2026-09-01 22:06:30 +01:00
Josh Creek 30a5a89164 fix(multiplayer): project complete queue lifecycle 2026-09-01 21:55:19 +01:00
Josh Creek 6ac2d0fbb1 fix(multiplayer): project accepted queue state 2026-09-01 21:54:10 +01:00
Josh Creek 9240cd4b27 feat(multiplayer): preserve authoritative queue wait 2026-09-01 21:45:07 +01:00
Josh Creek d85c8b8194 feat(multiplayer): expose action retry in matchmaking UI 2026-09-01 17:03:31 +01:00
Josh Creek e1807d2492 fix: handle matchmaking session expiry 2026-08-31 22:47:31 +01:00
Josh Creek 3e94960f0a fix: retry queue creation idempotently 2026-08-31 22:46:45 +01:00
Josh Creek fddbebb33b feat: display backend ranked profile 2026-08-31 22:44:21 +01:00
Josh Creek 66a67c931d feat: add participant-scoped proposal recovery 2026-08-31 22:41:26 +01:00
Josh Creek 47550aefae feat: add matchmaking queue UI 2026-08-31 22:39:29 +01:00