Commit Graph

71 Commits

Author SHA1 Message Date
Josh Creek 8b9ae35b43 test(domain): guard the ranked arena list against Godot registry drift
Task 8.20. `arena_registry.gd` is the documented single source of truth
for arenas, but `domain/ranked.go` keeps a hand-maintained mirror of its
floor-goal entries and nothing checked the two against each other --
ranked_test.go asserts the same three paths the production code
hardcodes, so both could drift together silently.

Drift is not hypothetical in either direction. The registry's own comment
anticipates flipping an elevated variant to random:true once a checkpoint
trained on that geometry is promoted, which ranked would then keep
excluding indefinitely. A rename or removal is worse: the allocator would
hand out a scene path that no longer exists, and the ranked server fails
to load its arena at match start -- after allocation, so it burns a real
match and a real server.

Keeping the two copies is deliberate rather than a wart: ranked arena
selection is server-authoritative and happens before any Godot process
exists. So this guards the relationship instead of removing it, the same
way the golden join-authorisation token guards the signing format. It
parses the registry and fails if the sets disagree either way, if
rotation order diverges from declaration order, or if a ranked path has
no scene behind it. The parser asserts it found both eligible and
ineligible entries, so a format change cannot make everything pass
vacuously. Verified against four drift scenarios.

The allocation-wiring half of 8.20 turned out to be already complete end
to end, with coverage at each hop; recorded in the task row rather than
rebuilt.

Add a Server Unit Tests workflow, because none of this would otherwise
run: the only Go tests CI executed were multiplayer-load's two load
tests, so ~24k lines of control plane gated nothing. Docker-free so it
can gate every push, and it vets the integration-tagged files too, since
those are excluded from the default build and could otherwise rot
uncompiled.

CLAUDE.md's CI section claimed two workflows and no unit-test job; there
were seven and now eight.
2026-09-05 15:05:49 +01:00
Josh Creek 5765532409 fix(allocator): publish signed assignment rosters before servers start
The root blocker (issue #14). The worker bound the provider allocation
and stopped. Service.PublishRoster and store.SaveVerifiedAssignmentRoster
both existed, fully tested, with zero non-test callers, and the
production allocator configured neither a roster store nor a signing
key. Nothing ever wrote the assignments table.

The allocated supervisor fetches a non-empty roster before it launches
the game child, so every real allocation failed at that fetch: no match
could reach ASSIGNMENT_READY or accept a player. Existing tests seeded
assignments directly, which is exactly why the missing hand-off went
unnoticed.

The worker now builds one join authorisation per durable participant,
signs each with the active key, and publishes them. Participants are
read through the same query SaveVerifiedAssignmentRoster re-validates
against, so the allocator cannot construct a roster the persistence
boundary would reject. The manifest commits to a digest over the whole
roster, so a server cannot be handed a truncated roster whose surviving
entries are each individually valid.

Persist the provider endpoint on the allocation: it arrived on the
provider response and was never stored, so a worker crashing between
allocating and publishing had no endpoint to recover and would have
stranded the match permanently. Republishing is idempotent, so that
crash now simply retries.

cmd/allocator refuses to start without key material rather than running
an allocator that binds allocations and silently strands every match.
The k8s allocator Deployment mounts the same key set the Fleet does, and
both now take the JSON key map so a rotation can publish several.

New integration test drives the real worker through to the supervisor's
own roster read path without seeding the assignments table. Verified it
fails with "assignments = 0, want 2" when the publish step is removed.
2026-09-05 10:42:31 +01:00
Josh Creek b8bcc1f3c1 feat(join-auth): add key-ID rotation to signed join authorisations
Prerequisite for wiring the allocator to publish rosters. The signing
key is a shared HMAC secret mounted into both the allocator and the
allocated game server; without a key ID, rotating it would invalidate
every authorisation already issued for an in-flight match, because a
server holding only the new key cannot verify a token signed with the
old one.

Add KeyID to JoinAuthorisation and append it to the canonical claim
bytes, so it is covered by the signature and cannot be repointed at a
different key than the one that actually signed. Allocated servers now
hold a set of currently-valid keys and select by ID: a rotation
publishes the new key alongside the old, and the old is dropped once no
live match can still reference it.

The key file becomes a JSON map of key ID to base64 key. A file of raw
key bytes is still accepted as a single key under the empty ID, which is
what an unrotated deployment and the kind fixture use.

Game/scripts/match_net.gd builds the canonical bytes independently, so
it changes in lockstep; the cross-language golden token in
test_match_net.gd is regenerated from the Go implementation and now
carries a key ID. Added tests cover accepting either key mid-rotation,
rejecting a retired key ID, and rejecting a token whose key ID was
swapped to name a key the server does hold.

Go suite and 223 Godot tests pass.
2026-09-05 10:36:06 +01:00
Josh Creek 4248e51c60 fix(server): enforce durable identity bans on session issuance and auth
banned_until and ban_reason have been in the schema since 0001, but no
production query ever read them -- grepping the tree found no reference
outside the migration itself. The only ban check was an in-memory map on
domain.TicketVerifier used by domain tests. Once real Steam login is
wired, a banned identity would keep full access through every existing
session until expiry and could obtain new ones.

Make the ban part of the durable authentication transaction rather than
a policy each login adapter must remember to re-implement:

- Session issuance inserts only when the identity exists and has no
  active ban, so a banned player cannot mint a session.
- Authentication joins the identity and rejects an active ban on every
  request, so a ban takes effect immediately on every replica rather
  than at session expiry.
- ApplyIdentityBan sets the ban and revokes that identity's sessions in
  one serializable transaction, closing the window where the ban is
  durable but another replica still accepts an issued session.

Bans are time-bounded and clearing one does not resurrect sessions the
ban revoked.

Tests cover enforcement across two independently constructed stores
standing in for two replicas, expiry/unban semantics, and -- separately,
because revocation would otherwise mask it -- that a ban applied without
revoking anything still blocks the next request.
2026-09-05 10:26:25 +01:00
Josh Creek d64920b0f9 fix(multiplayer): repair allocated compose verification 2026-09-04 16:38:10 +01:00
Josh Creek 2463713cde feat(multiplayer): persist live reconnect abandonments 2026-09-03 20:53:28 +01:00
Josh Creek 2e9da3032c fix(multiplayer): complete live results atomically 2026-09-03 13:29:31 +01:00
Josh Creek 8c28374eb4 fix(multiplayer): harden reconnect lifecycle fencing 2026-09-03 13:24:52 +01:00
Josh Creek a15368ed29 fix(multiplayer): fence client queue cancellation states 2026-09-03 00:15:41 +01:00
Josh Creek f8af212e3f fix(multiplayer): terminate proposal offenders atomically 2026-09-03 00:09:07 +01:00
Josh Creek aa446cfbfe fix(multiplayer): reconcile authoritative initial connections 2026-09-03 00:02:04 +01:00
Josh Creek 55b88a3aa5 fix(multiplayer): converge events through REST 2026-09-02 18:54:10 +01:00
Josh Creek 91658fbc13 fix(multiplayer): recover assignment handoff 2026-09-02 18:47:00 +01:00
Josh Creek ffc7856993 fix(multiplayer): align proposal participant contract 2026-09-01 22:52:52 +01:00
Josh Creek 51f6e19339 feat(multiplayer): expose ranked season countdown 2026-09-01 21:49:48 +01:00
Josh Creek eb3b685af0 feat(multiplayer): expose active ranked season 2026-09-01 21:34:57 +01:00
Josh Creek 701d7a9a2e fix(multiplayer): centralize arena path validation 2026-09-01 21:18:48 +01:00
Josh Creek 2ff348adf0 fix(multiplayer): persist arena identity on allocations 2026-09-01 21:16:42 +01:00
Josh Creek bd617dbf06 fix(multiplayer): validate ranked arena paths durably 2026-09-01 21:07:56 +01:00
Josh Creek ff80ab46b7 feat(multiplayer): rotate ranked arenas deterministically 2026-09-01 21:06:26 +01:00
Josh Creek 84372204fd feat(multiplayer): persist ranked arena allocations 2026-09-01 21:04:55 +01:00
Josh Creek 5630c5c8dc feat(multiplayer): harden ranked arena admission 2026-09-01 19:39:11 +01:00
Josh Creek 455055c67c feat(multiplayer): enforce proposal decline cooldowns 2026-09-01 19:30:45 +01:00
Josh Creek 9ae01ecc5a fix(multiplayer): propagate allocated playlist 2026-09-01 17:00:37 +01:00
Josh Creek b7642ad2be feat(multiplayer): model initial connect outcomes 2026-09-01 16:21:40 +01:00
Josh Creek 04b5d3b29d feat(multiplayer): verify ranked profile delivery 2026-09-01 15:29:49 +01:00
Josh Creek d081a72b9a feat(multiplayer): wire ranked profile runtime policy 2026-09-01 15:28:37 +01:00
Josh Creek 03ff8e485e feat: promote accepted proposals from API 2026-09-01 10:29:50 +01:00
Josh Creek eebab1bc19 feat: add workload-authenticated result API 2026-09-01 10:01:04 +01:00
Josh Creek def60169a8 feat: persist authenticated queue probe RTT 2026-09-01 09:40:25 +01:00
Josh Creek e25d61d80e feat: verify allocated join authorisations with hmac 2026-09-01 08:48:48 +01:00
Josh Creek 99680dbf6f fix: bind queue idempotency to compatibility 2026-08-31 22:51:00 +01:00
Josh Creek 66a67c931d feat: add participant-scoped proposal recovery 2026-08-31 22:41:26 +01:00
Josh Creek 4d3a8cc217 feat: add ban policy to ticket verification 2026-08-31 22:28:45 +01:00
Josh Creek ae164e625a fix: expire abandoned auth attempts 2026-08-31 22:22:33 +01:00
Josh Creek 9fe6c57b5d feat: model asynchronous Steam auth sessions 2026-08-31 22:21:31 +01:00
Josh Creek 3b208ae860 fix: bind queue candidates to their owner 2026-08-31 22:05:44 +01:00
Josh Creek b47c7dc3fa fix: enforce matchmaking compatibility boundaries 2026-08-31 22:04:57 +01:00
Josh Creek b2ee9ec92d feat: validate queue compatibility metadata 2026-08-31 21:57:40 +01:00
Josh Creek 7b6e22292a feat: sign reconnect authorisations 2026-08-31 21:44:27 +01:00
Josh Creek 229ded8613 feat: gate proposals on formed match policy 2026-08-31 21:38:47 +01:00
Josh Creek 4dcf98cbb0 feat: form matches from queue projections 2026-08-31 21:36:00 +01:00
Josh Creek 0f1cc17af6 feat: add authenticated queue recovery 2026-08-31 21:29:33 +01:00
Josh Creek 846663e320 feat: derive authoritative ranked tiers 2026-08-31 21:25:53 +01:00
Josh Creek 726fe1ce2e feat: gate assignment publication on allocation 2026-08-31 21:24:19 +01:00
Josh Creek 79e66c7a95 feat: validate workload-bound result credentials 2026-08-31 21:20:12 +01:00
Josh Creek caa875f15c fix: fence concurrent queue mutations 2026-08-31 21:09:58 +01:00
Josh Creek bc7136b2cb feat: add ranked season window policy 2026-08-31 21:08:10 +01:00
Josh Creek e13a64756f feat: add authoritative rating outcome scoring 2026-08-31 21:07:04 +01:00
Josh Creek 63332435d2 feat: classify match integrity separately from delivery 2026-08-31 21:05:54 +01:00