Chess on the ATmosphere checkmate.blue

Show invitations waiting on you, and let them be dismissed #55

Merged checkmate.blue
targeting main
Merged opened by and assigned to checkmate.blue
targeting main

HAD-240 and HAD-241, built together: dismissing is meaningless without the list, and both need the same Constellation query and filter.

Invitations were invisible (HAD-240). A directed invite writes the game record in the inviter's repo. The invitee has nothing in their own repo until they accept, and the home page lists only repo: <your did> -- so an invitation appeared nowhere. The shared link was the only way in.

Constellation indexes the player DID fields, so the records are discoverable: /links?target=<my did>&collection=blue.checkmate.game&path=.black returns records living in other people's repos. findGamesNaming queries both seats and merges. A new "Waiting for you" section lists what comes back.

Anyone can write a record naming your DID, so isInvitationTo is strict: not our own record, no parentGameUri, status waiting, exactly one seat is ours, and the other seat is the repo owner. That last check is what stops a stranger forging an invitation that appears to be from someone else.

Dismissing (HAD-241). New blue.checkmate.challenge.status lexicon, written to the invitee's own repo, pointing at the game AT-URI. Decline-only by design: acceptance is already expressed by the child game record, and a second representation of the same fact drifts -- which is exactly what happened to challenge.status, still stuck on open (HAD-235). Anchored on the game rather than the challenge because handleRematch creates no challenge record.

listDeclinedGameUris is one read for the whole list rather than one per invitation, so the home page cost does not grow with the number of pending invites. Writes follow the established order: PDS first, local state only on success.

Deploy steps, both required before dismissing works #

  1. Republish the permission set. lexicons/blue.checkmate.authFullAccess.json now lists the new collection, but the record the PDS actually enforces is the published one at did:plc:g2dztq6aggnn3tvimpebanu3/com.atproto.lexicon.schema/blue.checkmate.authFullAccess, which still grants only blue.checkmate.game and blue.checkmate.challenge. Until it is updated, every decline write is refused.
  2. Re-authenticate. Existing sessions hold a grant issued against the old permission set. Acceptable here because the app has no users yet.

Notes #

  • loadUserGames and loadInvitations run in sequence, not in parallel: the invitation filter excludes games we already hold, so it needs that list first.
  • Spam is deliberately not solved. Filtering to accounts you follow would need app.bsky.graph.follow, outside the current scope; dismissal is the answer for now.
  • Tests cover the predicate (8 cases, including the forged-seat one), the merged two-seat query, and both decline helpers.

This pull request has no versions submitted yet.