Skip to content

Add multi-user chat sharing foundation#18

Merged
TraderSamwise merged 2 commits into
masterfrom
feat/multi-user-chat-foundation
May 24, 2026
Merged

Add multi-user chat sharing foundation#18
TraderSamwise merged 2 commits into
masterfrom
feat/multi-user-chat-foundation

Conversation

@TraderSamwise

@TraderSamwise TraderSamwise commented May 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • document the multi-user chat ownership, invite, relay authorization, and prefixing model
  • add relay sharing state primitives for invites, participants, mode detection, and guest route allowlisting
  • include focused sharing state tests in the root vitest suite

Verification

  • yarn --cwd relay typecheck
  • yarn vitest run relay/src/sharing.test.ts
  • pre-push: yarn typecheck && yarn lint && yarn test

Summary by CodeRabbit

  • New Features

    • Multi-user chat with shareable session invites sent via email
    • Guest access management with email-verified invite acceptance
    • Automatic display of speaker names when multiple users are active
    • Automatic chat mode transitions between single and multi sessions
    • Owner-controlled guest participant removal
  • Documentation

    • Added comprehensive multi-user chat specification, workflows, and security invariants
  • Tests

    • Added coverage for invite creation/acceptance, authorization rules, mode transitions, and display behavior
  • Chores

    • Test discovery expanded to include relay tests

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7399e60e-7846-408a-a376-0c3cfb2f8080

📥 Commits

Reviewing files that changed from the base of the PR and between 3aaec10 and 8e91959.

📒 Files selected for processing (3)
  • docs/multi-user-chat.md
  • relay/src/sharing.test.ts
  • relay/src/sharing.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/multi-user-chat.md

📝 Walkthrough

Walkthrough

This PR adds a multi-user chat MVP: a docs spec, a relay-side sharing module with invite token creation/acceptance, share state persistence and normalization, guest request gating and display helpers, Vitest tests covering major flows, and test discovery configuration updates.

Changes

Multi-User Chat Sharing

Layer / File(s) Summary
MVP Specification
docs/multi-user-chat.md
Defines the ownership model (relay as session owner with guests connecting via owner's relay), the invite flow (relay-generated hashed tokens, email acceptance), speaker-name prefixing in multi-user mode, authorization boundaries (guests limited to shared-session routes), trusted actor metadata injection, and security invariants around token handling, request routing, and emergency lockdown.
Data Model & State Persistence
relay/src/sharing.ts
Exports types/interfaces and constants for shares, invites, participants, and state; implements emptySharingState, loadSharingState, saveSharingState, normalizeSharingState, and queries getShareChatMode, activeParticipants, and findShareForSession.
Invite Creation & Acceptance
relay/src/sharing.ts
Implements createShareInvite to generate random tokens, store only hashed tokens with TTL, and attach invites to shares; acceptShareInvite validates token hashes, enforces optional email checks, activates guest participants, marks invites accepted, and updates share versioning; includes invite lookup and sanitization helpers.
Participant Removal & Mode Downgrade
relay/src/sharing.ts
Implements removeShareParticipant to mark non-owner participants removed, update timestamps/version, and cause mode downgrade when appropriate while preserving normalized state.
Request Authorization & Display Helpers
relay/src/sharing.ts
Implements isSharedRelayRequestAllowed with method/path allowlists and optional sessionId scoping; implements actorDisplayPrefix to sanitize and format display prefixes with fallbacks.
Tests & Configuration
relay/src/sharing.test.ts, vitest.config.ts
Adds Vitest tests for invite lifecycle, acceptance/rejection cases, participant removal and mode transitions, guest relay request authorization and session scoping, and actorDisplayPrefix behavior; updates Vitest config to include relay/src/**/*.test.ts for test discovery.

Sequence Diagram(s)

sequenceDiagram
  participant OwnerCLI
  participant RelayDO
  participant EmailService
  participant GuestBrowser
  participant Daemon
  OwnerCLI->>RelayDO: createShareInvite(owner, sessionId, inviteEmail)
  RelayDO->>EmailService: sendInviteLink(hashed token metadata)
  GuestBrowser->>RelayDO: acceptInvite(link -> Clerk auth, token)
  RelayDO->>RelayDO: validate token hash, activate guest participant
  RelayDO->>Daemon: proxy guest request with injected trusted actor metadata
Loading

🐰 A relay shared, an invite baked,
Hashed tokens safe, no guest forsaken.
Two voices speak, the mode's alive—
Welcome aboard the sharing hive! 🎉

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add multi-user chat sharing foundation' accurately summarizes the main change—introducing the foundational infrastructure for multi-user chat sharing including documentation, state management, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multi-user-chat-foundation

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
relay/src/sharing.test.ts (2)

145-148: 💤 Low value

Consider adding edge cases for display prefix sanitization.

The current test covers basic whitespace normalization and empty string handling. Consider adding test cases for:

  • Whitespace-only strings (e.g., " ")
  • Names with special characters or Unicode
  • Very long display names

However, if the current coverage aligns with your "focused tests" goal, this is acceptable as-is.

🧪 Optional additional test cases
it("sanitizes actor display prefixes", () => {
  expect(actorDisplayPrefix({ userId: "u", displayName: "  Sam   Teady  ", role: "owner" })).toBe("[Sam Teady]:");
  expect(actorDisplayPrefix({ userId: "u", displayName: "", role: "guest" })).toBe("[User]:");
  // Additional edge cases:
  expect(actorDisplayPrefix({ userId: "u", displayName: "   ", role: "guest" })).toBe("[User]:");
  expect(actorDisplayPrefix({ userId: "u", displayName: "A".repeat(100), role: "owner" })).toBe(`[${"A".repeat(100)}]:`);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/src/sharing.test.ts` around lines 145 - 148, Add tests to cover edge
cases for actorDisplayPrefix: include a whitespace-only displayName (e.g., "  
") to assert it falls back to "[User]:", a very long displayName (e.g.,
"A".repeat(100)) to confirm it preserves long names, and optionally a name with
special/Unicode characters to ensure sanitization doesn't strip them. Update the
existing test block calling actorDisplayPrefix to add these extra expect
assertions so whitespace-only, Unicode/special characters, and long-name
behaviors are explicitly validated.

96-118: 💤 Low value

Consider extracting the userId to a variable for consistency.

The hardcoded string "user_guest" on line 113 could be extracted to a constant (e.g., from the actor object defined earlier). This would make the test more maintainable and reduce the risk of typos.

♻️ Optional refactor
     const accepted = await acceptShareInvite(created.state, {
       token: created.token.token,
       actor: {
         userId: "user_guest",
         displayName: "Alex",
         email: "alex@example.com",
         role: "guest",
       },
     });
 
-    const removed = removeShareParticipant(accepted.state, accepted.share.id, "user_guest");
+    const guestUserId = "user_guest";
+    const accepted = await acceptShareInvite(created.state, {
+      token: created.token.token,
+      actor: {
+        userId: guestUserId,
+        displayName: "Alex",
+        email: "alex@example.com",
+        role: "guest",
+      },
+    });
+
+    const removed = removeShareParticipant(accepted.state, accepted.share.id, guestUserId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/src/sharing.test.ts` around lines 96 - 118, Extract the repeated
"user_guest" literal into a local variable and use it throughout the test to
avoid duplication and typos: define const guestId = "user_guest" (or reuse
actor.userId if you prefer) near where the actor is declared, then replace
occurrences in acceptShareInvite actor.userId and the removeShareParticipant
call and the final assertions; this affects the actor object used with
acceptShareInvite, the removeShareParticipant(accepted.state, accepted.share.id,
...), and the participant access removed.share!.participants[guestId].status,
while leaving createShareInvite, acceptShareInvite, removeShareParticipant, and
getShareChatMode unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/multi-user-chat.md`:
- Around line 41-43: The docs state invites can be "pending, accepted, revoked,
or expired" but the persisted model only stores statuses as
`pending|accepted|revoked` and prunes expired pending invites during
normalization; update the documentation (in the multi-user-chat description of
`invites`) to match the actual model by removing "expired" from the persisted
statuses and adding a sentence that expired pending invites are pruned during
normalization (or alternatively implement explicit `expired` persistence in the
invite normalization logic if you prefer to keep the doc wording); reference the
`invites` field and the normalization logic that enforces
`pending|accepted|revoked` so the contract stays consistent.

In `@relay/src/sharing.ts`:
- Around line 211-214: The route check permits paths like "/attachments/../..."
allowing traversal; update the check in the handler that uses method and path
(and the helper normalizePath) to operate on a canonicalized,
dot-segment-resolved path before matching: first resolve/canonicalize the
incoming path (e.g., using a POSIX path normalization or URL pathname resolution
used consistently in normalizePath), reject any path that still contains ".." or
whose resolved pathname does not start with the exact "/attachments" segment
boundary (accept only "/attachments" or "/attachments/" or "/attachments/<rest>"
where "<rest>" cannot climb up), and then apply the GET/POST + prefix check
using the resolvedPath; also fix normalizePath to properly remove dot segments
for relative inputs so both the approval branch and other uses rely on the same
canonicalized path.
- Around line 164-175: The current assignment
share.participants[participant.userId] can overwrite an owner record with role
"guest"; change it to first check const existing =
share.participants[participant.userId] and if existing?.role === "owner" then
merge updates instead of replacing (preserve existing.role and joinedAt, update
email/status/lastSeenAt as appropriate and keep owner-specific fields),
otherwise assign the new participant object; keep the invite mutations
(invite.status, invite.acceptedAt, invite.acceptedByUserId) as-is and use
actor.userId to locate the participant.

---

Nitpick comments:
In `@relay/src/sharing.test.ts`:
- Around line 145-148: Add tests to cover edge cases for actorDisplayPrefix:
include a whitespace-only displayName (e.g., "   ") to assert it falls back to
"[User]:", a very long displayName (e.g., "A".repeat(100)) to confirm it
preserves long names, and optionally a name with special/Unicode characters to
ensure sanitization doesn't strip them. Update the existing test block calling
actorDisplayPrefix to add these extra expect assertions so whitespace-only,
Unicode/special characters, and long-name behaviors are explicitly validated.
- Around line 96-118: Extract the repeated "user_guest" literal into a local
variable and use it throughout the test to avoid duplication and typos: define
const guestId = "user_guest" (or reuse actor.userId if you prefer) near where
the actor is declared, then replace occurrences in acceptShareInvite
actor.userId and the removeShareParticipant call and the final assertions; this
affects the actor object used with acceptShareInvite, the
removeShareParticipant(accepted.state, accepted.share.id, ...), and the
participant access removed.share!.participants[guestId].status, while leaving
createShareInvite, acceptShareInvite, removeShareParticipant, and
getShareChatMode unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e02d177-6bf9-4326-a60f-a9e77cf51a1c

📥 Commits

Reviewing files that changed from the base of the PR and between b4a5fc5 and 3aaec10.

📒 Files selected for processing (4)
  • docs/multi-user-chat.md
  • relay/src/sharing.test.ts
  • relay/src/sharing.ts
  • vitest.config.ts

Comment thread docs/multi-user-chat.md
Comment thread relay/src/sharing.ts
Comment thread relay/src/sharing.ts Outdated
@TraderSamwise

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@TraderSamwise

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant