Problem
ServerDraftAdapter never adopts the game token the server issues when a draft transitions into a match, so it keeps using the draft token for the match session.
ServerMessage::DraftMatchStart carries a player_token (crates/server-core/src/protocol.rs:599-606), and the server populates it from the seat's game token, not its draft token:
// crates/phase-server/src/main.rs:3086-3090
let msg = ServerMessage::DraftMatchStart {
…
player_token: player.game_token.clone(),
The client destructures that field but never assigns it (client/src/adapter/server-draft-adapter.ts:572-593):
case "DraftMatchStart": {
const data = msg.data as {
match_id: string;
round: number;
game_code: string;
player_token: string; // <-- declared
your_player: PlayerId;
opponent_name: string;
};
this.phase = "match";
this.activeMatchId = data.match_id;
this._playerId = data.your_player;
this._gameCode = data.game_code;
// no `this.draftToken = data.player_token`
Both sibling handlers in the same switch do perform that assignment — :528 and :547 each do this.draftToken = data.player_token;. DraftMatchStart is the odd one out, which is what makes this read as an omission rather than a design choice.
this.draftToken is what the adapter sends for session identity, e.g. ReconnectDraft at :828.
Impact
Any server-hosted draft match operation authenticated by the seat's game token is submitted with the draft token instead. It surfaced while fixing #6941: once ClientMessage::Interaction exists and the server derives the acting seat from the authenticated token, a drafted match's interaction submissions authenticate with the wrong token and are rejected as an unknown token. #6941's fix therefore restores interactions for ordinary WebSocket multiplayer but not for server-hosted draft matches, which stay blocked on this.
The defect predates #6941 — it is only newly visible because #6941 adds the first code path that derives a seat from the token on the interaction channel.
Fix
Assign the token in the DraftMatchStart handler, matching the two sibling cases:
this.draftToken = data.player_token;
Client-side only. Worth checking at the same time whether draftToken is the right field name to hold a game token, or whether the adapter should carry the two identities separately — the current single field means "draft token, except after a match starts, when it means game token", which is exactly the kind of implicit state change that produced this bug.
Coverage note
client/src/adapter/__tests__/server-draft-adapter.test.ts already constructs DraftMatchStart messages in six tests (:155, :210, :242, :276, :359, :499), none of which assert on the resulting token. A regression test should assert that after DraftMatchStart, a subsequent authenticated send carries data.player_token and not the earlier draft token.
Related: #6941, #6947.
Problem
ServerDraftAdapternever adopts the game token the server issues when a draft transitions into a match, so it keeps using the draft token for the match session.ServerMessage::DraftMatchStartcarries aplayer_token(crates/server-core/src/protocol.rs:599-606), and the server populates it from the seat's game token, not its draft token:The client destructures that field but never assigns it (
client/src/adapter/server-draft-adapter.ts:572-593):Both sibling handlers in the same switch do perform that assignment —
:528and:547each dothis.draftToken = data.player_token;.DraftMatchStartis the odd one out, which is what makes this read as an omission rather than a design choice.this.draftTokenis what the adapter sends for session identity, e.g.ReconnectDraftat:828.Impact
Any server-hosted draft match operation authenticated by the seat's game token is submitted with the draft token instead. It surfaced while fixing #6941: once
ClientMessage::Interactionexists and the server derives the acting seat from the authenticated token, a drafted match's interaction submissions authenticate with the wrong token and are rejected as an unknown token. #6941's fix therefore restores interactions for ordinary WebSocket multiplayer but not for server-hosted draft matches, which stay blocked on this.The defect predates #6941 — it is only newly visible because #6941 adds the first code path that derives a seat from the token on the interaction channel.
Fix
Assign the token in the
DraftMatchStarthandler, matching the two sibling cases:Client-side only. Worth checking at the same time whether
draftTokenis the right field name to hold a game token, or whether the adapter should carry the two identities separately — the current single field means "draft token, except after a match starts, when it means game token", which is exactly the kind of implicit state change that produced this bug.Coverage note
client/src/adapter/__tests__/server-draft-adapter.test.tsalready constructsDraftMatchStartmessages in six tests (:155,:210,:242,:276,:359,:499), none of which assert on the resulting token. A regression test should assert that afterDraftMatchStart, a subsequent authenticated send carriesdata.player_tokenand not the earlier draft token.Related: #6941, #6947.