fix(frontend): scope Call-to-Play event keys by author
Security audit finding Codex #14 ("cross-author event-ID collisions can suppress Call-to-Play entries"). The Rust side guarantees that an event nonce is unique within one authenticated author's history and preserves `author_id` on every projected event, but the frontend reducer deduplicated the merged view with `new Map(events.map(e => [e.id, e]))` and tracked chat messages by `event.id` alone. A peer could therefore publish, say, a Respond event reusing the nonce of another user's Create event and make that call vanish from every viewer, or shadow other users' chat messages. `eventKeyOf` now builds `author_id + NUL + id` and is used for view deduplication, event ordering ties, message deduplication and the message id that CtpChat uses as its React key. Nomination ids (`call_id`) were already creator scoped and are unchanged. Test plan: `just frontend-test`. The new test feeds a Create from Alice and a Respond from Bob sharing one nonce and expects both to apply, then two same-nonce messages from different authors and expects two distinct messages. Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
This commit is contained in:
2 files changed
+36
-5
No files matched your search
@@ -54,6 +54,27 @@ const create = (
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test('events from different authors never collapse on a shared nonce', () => {
|
||||
const respond = event('create', 'Bob', { Respond: { ready_at: null } }, NOW + 1);
|
||||
const [nomination] = reduceCallToPlayEvents([create(), respond], NOW + 2);
|
||||
assert(nomination, 'the creator event must survive a colliding nonce from another author');
|
||||
assertEquals(nomination.creator, 'Alice', 'creator');
|
||||
assertEquals(Object.keys(nomination.participants).length, 2, 'both participants applied');
|
||||
|
||||
const aliceMessage = event('m1', 'Alice', { SendMessage: { text: 'hi' } }, NOW + 3);
|
||||
const bobMessage = event('m1', 'Bob', { SendMessage: { text: 'yo' } }, NOW + 4);
|
||||
const [withMessages] = reduceCallToPlayEvents(
|
||||
[create(), respond, aliceMessage, bobMessage],
|
||||
NOW + 5,
|
||||
);
|
||||
assert(withMessages, 'call should exist');
|
||||
assertEquals(withMessages.messages.length, 2, 'messages with a shared nonce are distinct');
|
||||
assert(
|
||||
withMessages.messages[0].id !== withMessages.messages[1].id,
|
||||
'message keys are author scoped',
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test('play-now call starts with its creator ready', () => {
|
||||
const [nomination] = reduceCallToPlayEvents([create()], NOW);
|
||||
assert(nomination, 'call should exist');
|
||||
|
||||
Reference in new issue
Block a user