Skip to content

Draft adapter drops DraftMatchStart.player_token, so drafted matches authenticate with the draft token #6948

Description

@matthewevans

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions