feat(meetings): rotate the join code when an invitee is removed - #64
Merged
ralyodio merged 1 commit intoAug 11, 2026
Merged
Conversation
Removing an invitee deleted their row and emailed them "your join code no
longer applies" — which was not true. The code is shared by the whole guest
list and was never rotated, so a removed invitee could still open the room
with the code from their original invitation. The email said one thing and
the system did another.
Now a PATCH that drops at least one invitee rotates the meeting's join_code
and emails the replacement to everyone still invited, which makes the
existing removal copy honest.
Notes on the edges:
- Rotation is skipped once the meeting has started. create_session() copies
the code onto the live sessions row, so rewriting the scheduled row would
not evict anyone — it would only claim a lockout that did not happen.
/api/sessions/{id}/regenerate-code is the lever for a running session.
- A failed rotation write does not fail the PATCH. The removal already
succeeded, and a stale code beats a half-applied edit; retained invitees
are simply not told the code changed.
- The update email hardcoded "Your join code (unchanged)", which would have
become a lie. It now switches on a codeChanged flag.
- Adding an invitee never rotates. Only removal revokes.
Because the code is shared, there is no way to revoke one person without
reissuing to everybody. That is inherent to a single shared code.
getUniqueJoinCode moved out of the create route into lib/join-code.ts so
both paths generate codes the same way, alongside a liveSessionExistsForCode
helper for the already-started check.
Also fixes the test service mock: it returned [] for unmatched tables, and
Boolean([]) is true, so the live-session probe would have read as "already
started" and silently suppressed every rotation under test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan169 finding(s) HIGH/CRITICAL: 16 | MEDIUM: 47 | LOW: 106
…and 119 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Aug 11, 2026
…#63) * docs(api): document the scheduled sessions API and its invitee tables The scheduled meetings feature shipped undocumented: docs/API.md covered neither the two tables nor any of the /api/scheduled-sessions endpoints. Adds the scheduled_sessions and scheduled_session_invitees schemas, the create/list/read/update/cancel endpoints, and the invitee-facing RSVP endpoints at /api/invite/[token]. Three things worth writing down rather than rediscovering: - PATCH takes `inviteeEmails` as the complete desired guest list, not a delta, and which email goes out depends on what the diff turns up. Omitting the field leaves the list alone; sending [] clears it. - Removing an invitee does not rotate the shared join_code, so it does not actually revoke access. Recorded as a known limitation instead of leaving removal to look like a lockout. - scheduled_session_invitees must not get a permissive anon policy — the dropped USING (true) pair leaked every invite_token to the browser. Notes why the table needs no browser-facing policy at all. Also corrects an assumption while documenting: scheduled_sessions.session_id is never written by any code path, so it stays NULL even after a meeting starts. The link to the live room is the join_code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(meetings): rotate the join code when an invitee is removed (#64) Removing an invitee deleted their row and emailed them "your join code no longer applies" — which was not true. The code is shared by the whole guest list and was never rotated, so a removed invitee could still open the room with the code from their original invitation. The email said one thing and the system did another. Now a PATCH that drops at least one invitee rotates the meeting's join_code and emails the replacement to everyone still invited, which makes the existing removal copy honest. Notes on the edges: - Rotation is skipped once the meeting has started. create_session() copies the code onto the live sessions row, so rewriting the scheduled row would not evict anyone — it would only claim a lockout that did not happen. /api/sessions/{id}/regenerate-code is the lever for a running session. - A failed rotation write does not fail the PATCH. The removal already succeeded, and a stale code beats a half-applied edit; retained invitees are simply not told the code changed. - The update email hardcoded "Your join code (unchanged)", which would have become a lie. It now switches on a codeChanged flag. - Adding an invitee never rotates. Only removal revokes. Because the code is shared, there is no way to revoke one person without reissuing to everybody. That is inherent to a single shared code. getUniqueJoinCode moved out of the create route into lib/join-code.ts so both paths generate codes the same way, alongside a liveSessionExistsForCode helper for the already-started check. Also fixes the test service mock: it returned [] for unmatched tables, and Boolean([]) is true, so the live-session probe would have read as "already started" and silently suppressed every rotation under test. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * style(docs): apply prettier to API.md CI runs format:check across **/*.md and the new scheduled-sessions section did not match prettier's table and code-fence formatting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: apply prettier to the scheduled-sessions route tests Same CI format:check gate as the previous commit; the rotation tests were added without a prettier pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #63 — review/merge that one first, since this builds on its docs and targets its branch. The base flips to
masterautomatically once #63 lands.The problem
Removing an invitee deleted their row and emailed them "Your join code for this meeting no longer applies." That was not true. The
join_codeis shared by the whole guest list and was never rotated, so a removed invitee could still open the room with the code from their original invitation. The email said one thing; the system did another.This is the limitation I documented in #63, now fixed rather than just described.
The fix
A
PATCHthat drops at least one invitee rotates the meeting'sjoin_codeand emails the replacement to everyone still invited. Adding an invitee never rotates.Because the code is shared, there's no way to revoke one person without reissuing to everybody — that's inherent to a single shared code, not something this PR could design around.
Edges worth a look in review
create_session()copies the code onto the livesessionsrow, so rewriting the scheduled row wouldn't evict anyone — it would only claim a lockout that didn't happen.POST /api/sessions/{id}/regenerate-codeis the lever for a running session. Covered by a test.codeChangedflag.Test-mock bug found on the way
The service mock returned
[]for unmatched tables, andBoolean([])istrue. The live-session probe would have read as "already started" and silently suppressed every rotation under test — the new tests would have passed while asserting nothing. RealmaybeSingle()returnsnull; the mock now matches.Verification
tsc --noEmitclean ·eslintclean on all changed files · 633 tests pass (65 files, 5 new) ·next buildcompiles.🤖 Generated with Claude Code