From 7a032bc277b474fe7c293c36f98baaa774e40652 Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Sat, 11 Apr 2026 15:53:20 -0400 Subject: [PATCH] fix: enforce HP visibility on player-emitted rolls, add membership check to request-state Co-Authored-By: Claude Sonnet 4.6 --- server/src/routes/initiative.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/server/src/routes/initiative.ts b/server/src/routes/initiative.ts index dae767b..f826ba0 100644 --- a/server/src/routes/initiative.ts +++ b/server/src/routes/initiative.ts @@ -54,15 +54,16 @@ export function stripHp(combats: CombatState[]): CombatState[] { })); } -// Broadcast full state to sender, HP-stripped to everyone else in the room. +// Broadcast full state to sender (if DM), HP-stripped to everyone else in the room. function broadcast( io: Server, socket: Socket, campaignId: number, - combats: CombatState[] + combats: CombatState[], + callerIsDM: boolean ): void { socket.to(`campaign:${campaignId}`).emit("initiative:updated", stripHp(combats)); - socket.emit("initiative:updated", combats); + socket.emit("initiative:updated", callerIsDM ? combats : stripHp(combats)); } async function checkDM(socket: Socket, campaignId: number): Promise { @@ -113,7 +114,8 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { "SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?", [campaignId, userId] ); - const dm = rows.length > 0 && rows[0].role === "dm"; + if (rows.length === 0) return; // not a member of this campaign + const dm = rows[0].role === "dm"; const combats = await loadCombats(campaignId); socket.emit("initiative:state", dm ? combats : stripHp(combats)); } catch (err) { @@ -152,7 +154,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { }; const updated = [...combats, newCombat]; await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); } @@ -203,7 +205,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { }; }); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, dm); } catch (err) { console.error("[initiative]", err); } @@ -237,7 +239,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { c.id !== data.combatId ? c : { ...c, enemy_roll: rollValue } ); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); } @@ -262,7 +264,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { }; }); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); } @@ -288,7 +290,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { }; }); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); } @@ -322,7 +324,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { }; }); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); } @@ -351,7 +353,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { } ); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); } @@ -374,7 +376,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { } ); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); } @@ -391,7 +393,7 @@ export function registerInitiativeHandlers(io: Server, socket: Socket): void { const combats = await loadCombats(data.campaignId); const updated = combats.filter((c) => c.id !== data.combatId); await saveCombats(data.campaignId, updated); - broadcast(io, socket, data.campaignId, updated); + broadcast(io, socket, data.campaignId, updated, true); } catch (err) { console.error("[initiative]", err); }