Three tickets, one change: HAD-146, HAD-157, HAD-158.
An opponent's record is untrusted input, but verifyOpponentResult loaded their
claimed PGN into a fresh board and accepted any game-over position it found. A
fabricated two-move checkmate passed, and the client then wrote the resulting
loss into its own repo: a forged, permanent, public result signed by the victim.
verifyPgnExtends(game-logic.ts) requires a PGN to continue the game we already have. Same[FEN]starting header, and no divergence in move history up to the shorter length, so same-length rewrites are caught too.verifyOpponentResultmoves tosrc/lib/game-verify.tsand takes the current board, so no result is derived from a PGN that fails that check.reconcileCompletiononly accepts an agreed draw when our own record shows we offered one. The old comment claimed this check; the code never did it.applyOpponentMovereturnsapplied | stale | invalid | mismatchand setsintegrityError, which shows a banner, locks the board, and blocks every write.
Verified against a copy of the pre-fix code: the fabricated-checkmate test plus four store tests fail without this, and pass with it. 159 tests green.