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 #
- Republish the permission set.
lexicons/blue.checkmate.authFullAccess.jsonnow lists the new collection, but the record the PDS actually enforces is the published one atdid:plc:g2dztq6aggnn3tvimpebanu3/com.atproto.lexicon.schema/blue.checkmate.authFullAccess, which still grants onlyblue.checkmate.gameandblue.checkmate.challenge. Until it is updated, every decline write is refused. - Re-authenticate. Existing sessions hold a grant issued against the old permission set. Acceptable here because the app has no users yet.
Notes #
loadUserGamesandloadInvitationsrun 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.