diff --git a/packages/chess/src/net/types.ts b/packages/chess/src/net/types.ts index 5741c1d..ebfb1a1 100644 --- a/packages/chess/src/net/types.ts +++ b/packages/chess/src/net/types.ts @@ -217,6 +217,45 @@ export interface ModifierProfileQueuedPayload { pendingVersion: number; } +// --------------------------------------------------------------------------- +// T2-ADR-2: Two-player consent flow (mirrors server/src/protocol.ts) +// --------------------------------------------------------------------------- + +/** + * Server → opponent only: a profile swap has been proposed and + * awaits consent. `expiresAt` is a unix-ms deadline after which + * the server auto-rejects with reason `"timeout"`. The proposing + * player does NOT receive this message — they get a + * `modifier-profile.queued` ack (shape reused from T2). + */ +export interface ModifierProfileProposalPendingPayload { + roomCode: string; + profile: ModifierProfileWire; + expiresAt: number; + proposer: Color; +} + +/** Why a pending proposal was cancelled without being promoted + * to the turn-boundary queue. */ +export type ModifierProfileRejectReason = + | "rejected" + | "timeout" + | "superseded"; + +/** Server → both: the current proposal was NOT applied. UIs + * clear any "proposal-pending" banner on receipt. */ +export interface ModifierProfileRejectedPayload { + roomCode: string; + reason: ModifierProfileRejectReason; +} + +/** Server → proposer only: opponent approved; the proposal is now + * in the T2 pending queue and will apply at the next turn + * boundary via `modifier-profile.updated`. */ +export interface ModifierProfileConsentReceivedPayload { + roomCode: string; +} + export interface ErrorPayload { code: string; message: string; @@ -249,6 +288,25 @@ export interface ModifierProfileUpdatePayload { version: number; } +/** + * Client → server: propose a profile swap for opponent consent + * (T2-ADR-2). Multiplayer only; solo clients use + * `modifier-profile.update` directly. Same staleness gate as + * `update`. + */ +export interface ModifierProfileProposePayload { + roomCode: string; + candidate: ModifierProfileWire; + version: number; +} + +/** Client → server (opponent of the proposer only): approve + * or reject the pending proposal. Self-consent is rejected. */ +export interface ModifierProfileConsentPayload { + roomCode: string; + decision: "approve" | "reject"; +} + export interface RoomJoinPayload { code: string; } @@ -281,6 +339,15 @@ export type ServerMessage = | MessageEnvelope<"room.joined", RoomJoinedPayload> | MessageEnvelope<"modifier-profile.updated", ModifierProfileUpdatedPayload> | MessageEnvelope<"modifier-profile.queued", ModifierProfileQueuedPayload> + | MessageEnvelope< + "modifier-profile.proposal-pending", + ModifierProfileProposalPendingPayload + > + | MessageEnvelope<"modifier-profile.rejected", ModifierProfileRejectedPayload> + | MessageEnvelope< + "modifier-profile.consent-received", + ModifierProfileConsentReceivedPayload + > | MessageEnvelope<"error", ErrorPayload>; export type ClientMessage = @@ -289,4 +356,6 @@ export type ClientMessage = | MessageEnvelope<"room.leave", Record> | MessageEnvelope<"game.move", GameMovePayload> | MessageEnvelope<"room.setPresets", RoomSetPresetsPayload> - | MessageEnvelope<"modifier-profile.update", ModifierProfileUpdatePayload>; + | MessageEnvelope<"modifier-profile.update", ModifierProfileUpdatePayload> + | MessageEnvelope<"modifier-profile.propose", ModifierProfileProposePayload> + | MessageEnvelope<"modifier-profile.consent", ModifierProfileConsentPayload>;