Skip to content

Conversation

@ggazzo
Copy link
Member

@ggazzo ggazzo commented Oct 2, 2025

… and EventID across services and schemas

Summary by CodeRabbit

  • Refactor
    • Strengthened identifier validation and standardized event schemas across federation, improving consistency for joins, invites, messaging, reactions, redactions, and room lifecycle flows.
    • Improved stability for creating rooms, direct messages, membership changes, and power-level updates.
  • Chores
    • Internal type alignment and controller adjustments to ensure consistent identifier handling without changing public APIs or UI behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Walkthrough

Replaces many string identifiers with branded types (RoomID, UserID, EventID) across services, controllers, schemas, and factories; changes one core type guard to return boolean; removes some explicit casts in staging emissions. No runtime control-flow changes.

Changes

Cohort / File(s) Summary
Core event predicate
packages/core/src/events/isRoomMemberEvent.ts
Changed isRoomMemberEvent from a TypeScript type predicate to return boolean; check logic (event.type === 'm.room.member') unchanged.
Federation SDK — Invite
packages/federation-sdk/src/services/invite.service.ts
Switched parameters and return properties to UserID/RoomID/EventID; updated processInvite signature to use RoomID/EventID.
Federation SDK — Message
packages/federation-sdk/src/services/message.service.ts
Public MessageService methods replaced many string IDs with RoomID/UserID/EventID; added imports for typed IDs.
Federation SDK — Profiles
packages/federation-sdk/src/services/profiles.service.ts
makeJoin, getMissingEvents, and eventAuth now accept/return branded ID types (RoomID, UserID, EventID, RoomVersion where applicable).
Federation SDK — Room
packages/federation-sdk/src/services/room.service.ts
Public RoomService APIs changed to accept/return branded IDs (RoomID, UserID, EventID); internal maps and helpers updated to typed keys.
Federation SDK — Send Join
packages/federation-sdk/src/services/send-join.service.ts
sendJoin parameter tightened to PduForType<'m.room.member'>; import and signature adjusted.
Federation SDK — Staging area
packages/federation-sdk/src/services/staging-area.service.ts
Removed several explicit TypeScript casts in emitted payloads (reason, state_key, content, topic); payload typings loosened.
Federation SDK — Event schemas
packages/federation-sdk/src/utils/event-schemas.ts
Replaced base/schema string fields with userIdSchema/roomIdSchema/eventIdSchema; refined state_key to userIdSchema or empty string.
Homeserver — Federation controllers
packages/homeserver/src/controllers/federation/...
Added RoomID/UserID imports and cast route params/query to branded IDs when calling services (invite, profiles, send-join, state, transactions).
Homeserver — Internal controllers
packages/homeserver/src/controllers/internal/...
Cast controller inputs to RoomID/UserID/EventID when invoking services or building events (direct-message, invite, message, room controllers).
Room — Factory and types
packages/room/src/manager/factory.ts, packages/room/src/types/_common.ts
Introduced roomIdSchema and userIdSchema (branded types); newCreateEvent now accepts UserID; roomId produced via roomIdSchema.parse.
Room — Event wrapper
packages/room/src/manager/event-wrapper.ts
Simplified stateKey getter implementation (single-line return); behavior unchanged.
Room — v3-11 schemas
packages/room/src/types/v3-11.ts
Replaced string fields with eventIdSchema/roomIdSchema/userIdSchema; state_key uses userIdSchema; several state events now use EmptyStateKey variant.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ggazzo
  • rodrigok

Poem

I stamped IDs on every trail,
Room and user, none go stale.
Events now wear tidy brands,
Typed fields held in careful hands.
Thump-thump—strong types lead the way. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes the core refactoring to replace primitive string identifiers with branded types (RoomID, UserID, etc.) and uses a standard “refactor:” prefix to indicate its nature, making it clear and directly relevant to the bulk of changes in the pull request.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/more-branded-types

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between effd3ad and 494e2d7.

📒 Files selected for processing (1)
  • packages/federation-sdk/src/utils/event-schemas.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/utils/event-schemas.ts (1)
packages/room/src/types/_common.ts (3)
  • userIdSchema (14-14)
  • roomIdSchema (10-10)
  • eventIdSchema (6-6)
🔇 Additional comments (6)
packages/federation-sdk/src/utils/event-schemas.ts (6)

1-5: LGTM! Branded type schemas imported correctly.

The import of eventIdSchema, roomIdSchema, and userIdSchema from @rocket.chat/federation-room aligns with the PR's objective to replace string types with branded types for improved type safety.


11-12: LGTM! Sender and room_id properly typed.

Replacing z.string() with userIdSchema and roomIdSchema provides compile-time type safety for these critical identifiers while maintaining runtime compatibility.


15-15: Past concern resolved: state_key remains optional.

The field correctly uses userIdSchema.or(z.literal('')).optional(), which:

  • Keeps the field optional (addressing the past review concern about breaking timeline events)
  • Allows either a UserID or empty string when present (appropriate for Matrix state events)
  • Provides type safety for state events

23-23: LGTM! Redacts field properly typed.

The redacts field now uses eventIdSchema.optional() instead of z.string().optional(), providing type safety for event redaction references.


35-35: LGTM! Creator field properly typed.

The creator field in the m.room.create event content now uses userIdSchema instead of z.string(), correctly representing that this field contains a user identifier.


44-44: LGTM! Member event state_key properly typed.

For m.room.member events, the state_key is always the user ID of the member being affected. Using userIdSchema instead of z.string() correctly enforces this semantic constraint and improves type safety.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@ggazzo ggazzo force-pushed the chore/more-branded-types branch 2 times, most recently from 2cc72ce to 3fb816f Compare October 2, 2025 02:47
@codecov-commenter
Copy link

codecov-commenter commented Oct 2, 2025

Codecov Report

❌ Patch coverage is 93.54839% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.84%. Comparing base (d57694f) to head (494e2d7).

Files with missing lines Patch % Lines
packages/room/src/manager/factory.ts 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #243      +/-   ##
==========================================
- Coverage   81.86%   81.84%   -0.02%     
==========================================
  Files          63       63              
  Lines        4709     4704       -5     
==========================================
- Hits         3855     3850       -5     
  Misses        854      854              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ggazzo ggazzo force-pushed the chore/more-branded-types branch from 3fb816f to 1e827f3 Compare October 2, 2025 02:50
@ggazzo ggazzo marked this pull request as ready for review October 2, 2025 14:17
Co-authored-by: Diego Sampaio <chinello@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/homeserver/src/controllers/internal/direct-message.controller.ts (1)

9-10: Integrate userIdSchema into DTO schema

Import userIdSchema from @rocket.chat/federation-room (it’s re-exported in packages/room/src/index.ts) and replace both t.String() fields with transforms that enforce the branded format:

+import { userIdSchema } from '@rocket.chat/federation-room';

 export const InternalCreateDirectMessageBodyDto = t.Object({
-	senderUserId: t.String(),
-	targetUserId: t.String(),
+	senderUserId: t.Transform(t.String())
+		.Decode((value) => {
+			const result = userIdSchema.safeParse(value);
+			if (!result.success) throw new Error('Invalid senderUserId format');
+			return result.data;
+		})
+		.Encode((value) => value),
+	targetUserId: t.Transform(t.String())
+		.Decode((value) => {
+			const result = userIdSchema.safeParse(value);
+			if (!result.success) throw new Error('Invalid targetUserId format');
+			return result.data;
+		})
+		.Encode((value) => value),
 });

This enforces UserID validation at the API boundary—no custom validator needed.

packages/homeserver/src/controllers/federation/transactions.controller.ts (1)

56-59: Validate event ID format before casting.

Consistent with other endpoints, validate the event ID before casting to the branded type.

+import { eventIdSchema } from '@rocket.chat/federation-room';
+
 		.get(
 			'/_matrix/federation/v1/event/:eventId',
 			async ({ params, set }) => {
+				const validatedEventId = eventIdSchema.parse(params.eventId);
 				const eventData = await eventService.getEventById(
-					params.eventId as EventID,
+					validatedEventId,
 				);
packages/homeserver/src/controllers/federation/profiles.controller.ts (1)

113-120: Validate ID formats and arrays before casting.

This endpoint casts roomId and two event ID arrays without validation. Array casts are particularly risky.

+import { eventIdSchema, roomIdSchema } from '@rocket.chat/federation-room';
+import { z } from 'zod';
+
 		.post(
 			'/_matrix/federation/v1/get_missing_events/:roomId',
 			async ({ params, body }) => {
+				const validatedRoomId = roomIdSchema.parse(params.roomId);
+				const validatedEarliestEvents = z.array(eventIdSchema).parse(body.earliest_events);
+				const validatedLatestEvents = z.array(eventIdSchema).parse(body.latest_events);
+
 				return profilesService.getMissingEvents(
-					params.roomId as RoomID,
-					body.earliest_events as EventID[],
-					body.latest_events as EventID[],
+					validatedRoomId,
+					validatedEarliestEvents,
+					validatedLatestEvents,
 					body.limit,
 					body.min_depth,
 				),
packages/homeserver/src/controllers/internal/room.controller.ts (1)

378-402: Validate IDs before casting in ban implementation.

The active ban implementation casts multiple IDs (roomId, userIdToBan, senderUserId) without validation. This is particularly important since these IDs are used to construct events that affect user permissions.

+import { roomIdSchema, userIdSchema } from '@rocket.chat/federation-room';
+
 				const { roomId, userIdToBan } = params;
 				const { senderUserId } = body;
+
+				const validatedRoomId = roomIdSchema.parse(roomId);
+				const validatedUserIdToBan = userIdSchema.parse(userIdToBan);
+				const validatedSenderUserId = userIdSchema.parse(senderUserId);
 
-				const room = await stateService.getFullRoomState(roomId as RoomID);
+				const room = await stateService.getFullRoomState(validatedRoomId);
 
 				// ... create event code ...
 
 				const membershipEvent =
 					PersistentEventFactory.newEvent<'m.room.member'>(
 						{
 							type: 'm.room.member',
 							content: { membership: 'ban' },
-							room_id: roomId as RoomID,
-							state_key: userIdToBan as UserID,
+							room_id: validatedRoomId,
+							state_key: validatedUserIdToBan,
 							auth_events: [],
 							depth: 0,
 							prev_events: [],
 							origin_server_ts: Date.now(),
-							sender: senderUserId as UserID,
+							sender: validatedSenderUserId,
 						},
🧹 Nitpick comments (1)
packages/federation-sdk/src/services/message.service.ts (1)

302-306: Return EventID-branded values

Now that these methods accept branded IDs, keeping the return types as Promise<string> drops the branding right away. Please return Promise<EventID> so callers preserve the stronger typing.

-	async sendReaction(
-		roomId: RoomID,
-		eventId: EventID,
-		emoji: string,
-		senderUserId: UserID,
-	): Promise<string> {
+	async sendReaction(
+		roomId: RoomID,
+		eventId: EventID,
+		emoji: string,
+		senderUserId: UserID,
+	): Promise<EventID> {-	async unsetReaction(
-		roomId: RoomID,
-		eventIdReactedTo: EventID,
-		_emoji: string,
-		senderUserId: UserID,
-	): Promise<string> {
+	async unsetReaction(
+		roomId: RoomID,
+		eventIdReactedTo: EventID,
+		_emoji: string,
+		senderUserId: UserID,
+	): Promise<EventID> {-	async updateMessage(
-		roomId: RoomID,
-		rawMessage: string,
-		formattedMessage: string,
-		senderUserId: UserID,
-		eventIdToReplace: EventID,
-	): Promise<string> {
+	async updateMessage(
+		roomId: RoomID,
+		rawMessage: string,
+		formattedMessage: string,
+		senderUserId: UserID,
+		eventIdToReplace: EventID,
+	): Promise<EventID> {-	async redactMessage(
-		roomId: RoomID,
-		eventIdToRedact: EventID,
-	): Promise<string> {
+	async redactMessage(
+		roomId: RoomID,
+		eventIdToRedact: EventID,
+	): Promise<EventID> {

Also applies to: 347-351, 379-385, 424-427

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d57694f and 1e827f3.

📒 Files selected for processing (22)
  • packages/core/src/events/isRoomMemberEvent.ts (1 hunks)
  • packages/federation-sdk/src/services/invite.service.ts (3 hunks)
  • packages/federation-sdk/src/services/message.service.ts (10 hunks)
  • packages/federation-sdk/src/services/profiles.service.ts (4 hunks)
  • packages/federation-sdk/src/services/room.service.ts (15 hunks)
  • packages/federation-sdk/src/services/send-join.service.ts (2 hunks)
  • packages/federation-sdk/src/services/staging-area.service.ts (3 hunks)
  • packages/federation-sdk/src/utils/event-schemas.ts (5 hunks)
  • packages/homeserver/src/controllers/federation/invite.controller.ts (2 hunks)
  • packages/homeserver/src/controllers/federation/profiles.controller.ts (6 hunks)
  • packages/homeserver/src/controllers/federation/rooms.controller.ts (1 hunks)
  • packages/homeserver/src/controllers/federation/send-join.controller.ts (2 hunks)
  • packages/homeserver/src/controllers/federation/state.controller.ts (3 hunks)
  • packages/homeserver/src/controllers/federation/transactions.controller.ts (2 hunks)
  • packages/homeserver/src/controllers/internal/direct-message.controller.ts (2 hunks)
  • packages/homeserver/src/controllers/internal/invite.controller.ts (3 hunks)
  • packages/homeserver/src/controllers/internal/message.controller.ts (5 hunks)
  • packages/homeserver/src/controllers/internal/room.controller.ts (12 hunks)
  • packages/room/src/manager/event-wrapper.ts (1 hunks)
  • packages/room/src/manager/factory.ts (3 hunks)
  • packages/room/src/types/_common.ts (1 hunks)
  • packages/room/src/types/v3-11.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (19)
packages/room/src/manager/factory.ts (3)
packages/room/src/manager/room-state.ts (1)
  • creator (21-31)
packages/room/src/types/_common.ts (2)
  • UserID (16-16)
  • roomIdSchema (10-10)
packages/room/src/manager/event-wrapper.ts (1)
  • roomId (78-80)
packages/homeserver/src/controllers/federation/send-join.controller.ts (3)
packages/room/src/manager/event-wrapper.ts (1)
  • roomId (78-80)
packages/room/src/types/_common.ts (2)
  • RoomID (12-12)
  • EventID (8-8)
packages/room/src/manager/v3.ts (1)
  • eventId (17-29)
packages/core/src/events/isRoomMemberEvent.ts (2)
packages/room/src/manager/event-wrapper.ts (1)
  • event (106-115)
packages/room/src/types/v3-11.ts (1)
  • Pdu (728-728)
packages/federation-sdk/src/services/send-join.service.ts (2)
packages/room/src/manager/event-wrapper.ts (1)
  • event (106-115)
packages/room/src/types/_common.ts (2)
  • EventID (8-8)
  • PduForType (22-22)
packages/federation-sdk/src/services/invite.service.ts (2)
packages/room/src/types/_common.ts (3)
  • UserID (16-16)
  • RoomID (12-12)
  • EventID (8-8)
packages/room/src/manager/v3.ts (1)
  • eventId (17-29)
packages/homeserver/src/controllers/internal/room.controller.ts (1)
packages/room/src/types/_common.ts (2)
  • UserID (16-16)
  • RoomID (12-12)
packages/homeserver/src/controllers/federation/invite.controller.ts (2)
packages/room/src/types/_common.ts (2)
  • RoomID (12-12)
  • EventID (8-8)
packages/room/src/manager/v3.ts (1)
  • eventId (17-29)
packages/federation-sdk/src/services/profiles.service.ts (2)
packages/room/src/manager/event-wrapper.ts (1)
  • roomId (78-80)
packages/room/src/types/_common.ts (4)
  • RoomID (12-12)
  • UserID (16-16)
  • PduForType (22-22)
  • EventID (8-8)
packages/federation-sdk/src/services/staging-area.service.ts (1)
packages/room/src/manager/event-wrapper.ts (1)
  • event (106-115)
packages/homeserver/src/controllers/internal/invite.controller.ts (3)
packages/room/src/manager/event-wrapper.ts (2)
  • roomId (78-80)
  • sender (82-84)
packages/room/src/types/_common.ts (2)
  • RoomID (12-12)
  • UserID (16-16)
packages/room/src/manager/power-level-event-wrapper.ts (1)
  • sender (190-196)
packages/homeserver/src/controllers/federation/transactions.controller.ts (1)
packages/room/src/types/_common.ts (1)
  • RoomID (12-12)
packages/homeserver/src/controllers/internal/direct-message.controller.ts (1)
packages/room/src/types/_common.ts (1)
  • UserID (16-16)
packages/room/src/types/v3-11.ts (1)
packages/room/src/types/_common.ts (3)
  • eventIdSchema (6-6)
  • roomIdSchema (10-10)
  • userIdSchema (14-14)
packages/homeserver/src/controllers/internal/message.controller.ts (2)
packages/room/src/manager/event-wrapper.ts (1)
  • roomId (78-80)
packages/room/src/types/_common.ts (3)
  • RoomID (12-12)
  • UserID (16-16)
  • EventID (8-8)
packages/federation-sdk/src/utils/event-schemas.ts (1)
packages/room/src/types/_common.ts (3)
  • userIdSchema (14-14)
  • roomIdSchema (10-10)
  • eventIdSchema (6-6)
packages/federation-sdk/src/services/message.service.ts (2)
packages/room/src/types/_common.ts (3)
  • RoomID (12-12)
  • UserID (16-16)
  • EventID (8-8)
packages/core/src/events/m.room.message.ts (1)
  • FileMessageContent (28-45)
packages/homeserver/src/controllers/federation/profiles.controller.ts (1)
packages/room/src/types/_common.ts (3)
  • UserID (16-16)
  • RoomID (12-12)
  • EventID (8-8)
packages/federation-sdk/src/services/room.service.ts (3)
packages/room/src/types/_common.ts (3)
  • UserID (16-16)
  • RoomID (12-12)
  • EventID (8-8)
packages/room/src/manager/event-wrapper.ts (2)
  • roomId (78-80)
  • sender (82-84)
packages/federation-sdk/src/index.ts (1)
  • EventID (23-23)
packages/homeserver/src/controllers/federation/state.controller.ts (1)
packages/room/src/types/_common.ts (2)
  • RoomID (12-12)
  • EventID (8-8)
🔇 Additional comments (7)
packages/federation-sdk/src/services/staging-area.service.ts (4)

237-237: LGTM! Type cast removal is appropriate.

Removing the explicit as string | undefined cast allows TypeScript to infer the type from the properly-typed schema. This aligns with the PR's goal of using branded types throughout the codebase.


248-248: LGTM! Unnecessary cast removed.

The removal of as string is appropriate here. For m.room.member events, state_key is always present and should now be properly typed (likely as UserID based on the PR's branded types refactor).


270-270: Confirm that content is always defined for m.room.topic events
I couldn’t locate the PDU type for room-topic events in @rocket.chat/federation-core. Please verify that its content field is declared as required (not optional); if it can be undefined, reintroduce optional chaining or add a runtime guard.


250-250: Verify external listeners handle full content

The SDK now emits the full event.event.content for m.room.member instead of an object with only membership. Ensure all consumers of homeserver.matrix.membership can process the complete content structure without breaking changes.

packages/homeserver/src/controllers/internal/direct-message.controller.ts (1)

1-1: LGTM: Import aligns with branded type migration.

The UserID import supports the controller's migration to strongly-typed IDs.

packages/federation-sdk/src/services/invite.service.ts (2)

38-47: LGTM! Service signature updates are appropriate.

The service method signatures have been correctly updated to use branded types (UserID, RoomID, EventID). This provides type safety at the service layer.

However, ensure that all calling code (controllers) validates IDs before passing them to these methods—validation comments have been left on the controller files.


143-148: LGTM! Process invite signature updated correctly.

The processInvite method signature now uses RoomID and EventID branded types, which improves type safety.

As with inviteUserToRoom, ensure controllers validate IDs before calling this method.

Comment on lines +68 to 70
export const isRoomMemberEvent = (event: Pdu): boolean => {
return event.type === 'm.room.member';
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify removal of type predicate doesn't break downstream type narrowing.

The function signature changed from a type predicate (event is RoomMemberEvent) to a plain boolean return. This removes compile-time type narrowing for callers who previously relied on TypeScript automatically treating event as RoomMemberEvent after this check.

While this may be intentional as part of the branded-types refactor, ensure that:

  1. Calling code that depended on the type predicate has been updated
  2. The Pdu type from @rocket.chat/federation-room now provides sufficient type safety that makes the predicate redundant or incompatible
  3. The simple event.type check is sufficient given that RoomMemberEvent has additional required fields (content.membership, state_key, etc.)

Run this script to find usages and assess impact:


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of isRoomMemberEvent to check if type narrowing was relied upon

# Search for isRoomMemberEvent usage with context
rg -n -C 5 'isRoomMemberEvent' --type ts

Length of output: 3739


🏁 Script executed:

#!/bin/bash
sed -n '130,180p' packages/core/src/utils/pruneEventDict.ts

Length of output: 1174


Restore type predicate on isRoomMemberEvent (packages/core/src/events/isRoomMemberEvent.ts:68)
Change the signature back to isRoomMemberEvent(event: Pdu): event is RoomMemberEvent so that TS correctly narrows callers (e.g. in pruneEventDict.ts) and drop the @ts-ignore annotations.

🤖 Prompt for AI Agents
In packages/core/src/events/isRoomMemberEvent.ts around lines 68 to 70, the
function currently returns boolean but should be a TypeScript type predicate to
allow proper narrowing; change the signature from (event: Pdu): boolean to
(event: Pdu): event is RoomMemberEvent, ensure RoomMemberEvent is imported or
referenced correctly, and remove any @ts-ignore annotations that were added
downstream (e.g., in pruneEventDict.ts) once callers can rely on the narrowed
type.

Comment on lines 12 to 17
return inviteService.processInvite(
body.event,
roomId,
eventId,
roomId as RoomID,
eventId as EventID,
body.room_version,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate ID formats before casting to branded types.

Similar to send-join.controller.ts, this code casts route parameters to RoomID and EventID without runtime validation. The as assertions bypass format checks that branded types are meant to enforce.

Apply the same validation pattern as suggested for send-join.controller.ts:

+import { eventIdSchema, roomIdSchema } from '@rocket.chat/federation-room';
+
 export const invitePlugin = (app: Elysia) => {
 	const inviteService = container.resolve(InviteService);
 	return app.put(
 		'/_matrix/federation/v2/invite/:roomId/:eventId',
 		async ({ body, params: { roomId, eventId } }) => {
+			const validatedRoomId = roomIdSchema.parse(roomId);
+			const validatedEventId = eventIdSchema.parse(eventId);
+
 			return inviteService.processInvite(
 				body.event,
-				roomId as RoomID,
-				eventId as EventID,
+				validatedRoomId,
+				validatedEventId,
 				body.room_version,
 			);
 		},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return inviteService.processInvite(
body.event,
roomId,
eventId,
roomId as RoomID,
eventId as EventID,
body.room_version,
);
import { eventIdSchema, roomIdSchema } from '@rocket.chat/federation-room';
export const invitePlugin = (app: Elysia) => {
const inviteService = container.resolve(InviteService);
return app.put(
'/_matrix/federation/v2/invite/:roomId/:eventId',
async ({ body, params: { roomId, eventId } }) => {
// Validate branded IDs at runtime
const validatedRoomId = roomIdSchema.parse(roomId);
const validatedEventId = eventIdSchema.parse(eventId);
return inviteService.processInvite(
body.event,
validatedRoomId,
validatedEventId,
body.room_version,
);
},
);
};
🤖 Prompt for AI Agents
In packages/homeserver/src/controllers/federation/invite.controller.ts around
lines 12 to 17, the code blindly casts route params to RoomID and EventID; add
runtime validation identical to send-join.controller.ts by checking the roomId
and eventId formats with the existing validator helpers (e.g., isValidRoomId /
isValidEventId or parse helpers) before casting; if validation fails, return a
400/BadRequest (with a brief message) instead of proceeding, and only then cast
to the branded types and call inviteService.processInvite with the validated
values.

Comment on lines +40 to +41
({ query: { user_id } }) =>
profilesService.queryProfile(user_id as UserID),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate user ID format before casting.

The queryProfile endpoint casts user_id from query parameters to UserID without validation.

+import { userIdSchema } from '@rocket.chat/federation-room';
+
 		.get(
 			'/_matrix/federation/v1/query/profile',
-			({ query: { user_id } }) =>
-				profilesService.queryProfile(user_id as UserID),
+			({ query: { user_id } }) => {
+				const validatedUserId = userIdSchema.parse(user_id);
+				return profilesService.queryProfile(validatedUserId);
+			},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
({ query: { user_id } }) =>
profilesService.queryProfile(user_id as UserID),
import { userIdSchema } from '@rocket.chat/federation-room';
// other imports and setup
router
.get(
'/_matrix/federation/v1/query/profile',
({ query: { user_id } }) => {
const validatedUserId = userIdSchema.parse(user_id);
return profilesService.queryProfile(validatedUserId);
},
)
// other endpoints
🤖 Prompt for AI Agents
In packages/homeserver/src/controllers/federation/profiles.controller.ts around
lines 40-41, the query handler directly casts query.user_id to UserID without
validating its format; instead, extract user_id from req.query as a string,
validate it with the project's existing user ID validator (or a canonical regex)
and if it fails return a 400 error response (with a short message), otherwise
cast to UserID and call profilesService.queryProfile(userId); ensure you handle
missing user_id similarly and keep types strict so the service only receives a
validated UserID.

Comment on lines +91 to 96
return profilesService.makeJoin(
roomId as RoomID,
userId as UserID,
ver ?? ['1'],
);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate ID formats before casting.

The makeJoin endpoint casts roomId and userId without validation. Additionally, ensure ver array contains valid room version strings.

+import { roomIdSchema, userIdSchema } from '@rocket.chat/federation-room';
+
 			async ({ params, query }) => {
 				const { roomId, userId } = params;
 				const { ver } = query;
+
+				const validatedRoomId = roomIdSchema.parse(roomId);
+				const validatedUserId = userIdSchema.parse(userId);
 
 				return profilesService.makeJoin(
-					roomId as RoomID,
-					userId as UserID,
+					validatedRoomId,
+					validatedUserId,
 					ver ?? ['1'],
 				);
 			},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return profilesService.makeJoin(
roomId as RoomID,
userId as UserID,
ver ?? ['1'],
);
},
// At the top of packages/homeserver/src/controllers/federation/profiles.controller.ts
import { roomIdSchema, userIdSchema } from '@rocket.chat/federation-room';
// Around lines 91–96
async ({ params, query }) => {
const { roomId, userId } = params;
const { ver } = query;
const validatedRoomId = roomIdSchema.parse(roomId);
const validatedUserId = userIdSchema.parse(userId);
return profilesService.makeJoin(
validatedRoomId,
validatedUserId,
ver ?? ['1'],
);
},
🤖 Prompt for AI Agents
In packages/homeserver/src/controllers/federation/profiles.controller.ts around
lines 91–96, do not blindly cast roomId and userId; validate their formats first
(use existing helpers like isValidRoomId/isValidUserId or a strict
regex/parser), return a 400/BadRequest when invalid, and only then cast to
RoomID/UserID; also validate the ver input is an array of allowed room version
strings (filter/validate against the supported versions list, default to ['1']
only if none valid) before passing to profilesService.makeJoin to avoid passing
malformed IDs or versions.

Comment on lines +176 to +179
const eventId = await roomService.joinUser(
roomId as RoomID,
userId as UserID,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation for ID parameters.

The joinUser endpoint casts both roomId and userId without any validation, creating inconsistency with other endpoints in this file.

+import { roomIdSchema, userIdSchema } from '@rocket.chat/federation-room';
+
 		.put(
 			'/internal/rooms/:roomId/join/:userId',
 			async ({ params }) => {
 				const { roomId, userId } = params;
+
+				const validatedRoomId = roomIdSchema.parse(roomId);
+				const validatedUserId = userIdSchema.parse(userId);
 
 				const eventId = await roomService.joinUser(
-					roomId as RoomID,
-					userId as UserID,
+					validatedRoomId,
+					validatedUserId,
 				);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const eventId = await roomService.joinUser(
roomId as RoomID,
userId as UserID,
);
// at the top of the file, alongside your other imports
import { roomIdSchema, userIdSchema } from '@rocket.chat/federation-room';
.put(
'/internal/rooms/:roomId/join/:userId',
async ({ params }) => {
const { roomId, userId } = params;
const validatedRoomId = roomIdSchema.parse(roomId);
const validatedUserId = userIdSchema.parse(userId);
const eventId = await roomService.joinUser(
validatedRoomId,
validatedUserId,
);
// …rest of handler…

Comment on lines +212 to +214
const room = await stateService.getFullRoomState(
params.roomId as RoomID,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate room ID before casting.

The getFullRoomState call casts params.roomId without validation.

+import { roomIdSchema } from '@rocket.chat/federation-room';
+
 				}
-				const room = await stateService.getFullRoomState(
-					params.roomId as RoomID,
-				);
+				const validatedRoomId = roomIdSchema.parse(params.roomId);
+				const room = await stateService.getFullRoomState(validatedRoomId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const room = await stateService.getFullRoomState(
params.roomId as RoomID,
);
import { roomIdSchema } from '@rocket.chat/federation-room';
}
const validatedRoomId = roomIdSchema.parse(params.roomId);
const room = await stateService.getFullRoomState(validatedRoomId);
🤖 Prompt for AI Agents
In packages/homeserver/src/controllers/internal/room.controller.ts around lines
212 to 214, the code casts params.roomId to RoomID without validating it;
validate params.roomId before casting (e.g., check it's present, a string, and
matches the RoomID format/regex or use the existing RoomID parsing/validation
helper), return or throw a 400/422 error if invalid, and only call
stateService.getFullRoomState with a safely validated/parsed RoomID value.

Comment on lines 453 to 458
return roomService.markRoomAsTombstone(
roomIdParse.data,
bodyParse.data.sender,
roomIdParse.data as RoomID,
bodyParse.data.sender as UserID,
bodyParse.data.reason,
bodyParse.data.replacementRoomId,
bodyParse.data.replacementRoomId as RoomID,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove redundant casts after validation.

Similar to other endpoints, this code validates with safeParse but then casts the already-validated data. Additionally, validate the sender field from the body.

+import { userIdSchema, roomIdSchema } from '@rocket.chat/federation-room';
+
 				}
+				const validatedSender = userIdSchema.parse(bodyParse.data.sender);
+				const validatedReplacementRoomId = bodyParse.data.replacementRoomId 
+					? roomIdSchema.parse(bodyParse.data.replacementRoomId)
+					: undefined;
+
 				return roomService.markRoomAsTombstone(
-					roomIdParse.data as RoomID,
-					bodyParse.data.sender as UserID,
+					roomIdParse.data,
+					validatedSender,
 					bodyParse.data.reason,
-					bodyParse.data.replacementRoomId as RoomID,
+					validatedReplacementRoomId,
 				);
🤖 Prompt for AI Agents
In packages/homeserver/src/controllers/internal/room.controller.ts around lines
453 to 458, the code is casting values after a successful safeParse and also
does not explicitly validate the body sender; remove the redundant casts and use
the already-validated parsed values (e.g., roomIdParse.data and bodyParse.data)
directly, and ensure the request body schema includes and validates sender (or
perform a separate safeParse on sender) so you pass a validated sender to
markRoomAsTombstone instead of casting.

Comment on lines 492 to 496
const resp = await inviteService.inviteUserToRoom(
userId,
roomId,
sender,
userId as UserID,
roomId as RoomID,
sender as UserID,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate IDs before casting.

The inviteUserToRoom endpoint casts all three ID parameters without validation.

+import { roomIdSchema, userIdSchema } from '@rocket.chat/federation-room';
+
 			async ({ params, body }) => {
 				const { roomId, userId } = params;
 				const { sender } = body;
+
+				const validatedUserId = userIdSchema.parse(userId);
+				const validatedRoomId = roomIdSchema.parse(roomId);
+				const validatedSender = userIdSchema.parse(sender);
 
 				const resp = await inviteService.inviteUserToRoom(
-					userId as UserID,
-					roomId as RoomID,
-					sender as UserID,
+					validatedUserId,
+					validatedRoomId,
+					validatedSender,
 				);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resp = await inviteService.inviteUserToRoom(
userId,
roomId,
sender,
userId as UserID,
roomId as RoomID,
sender as UserID,
);
// At the top of the file
import { roomIdSchema, userIdSchema } from '@rocket.chat/federation-room';
...
// In your async handler
async ({ params, body }) => {
const { roomId, userId } = params;
const { sender } = body;
// Validate IDs at runtime before calling the service
const validatedUserId = userIdSchema.parse(userId);
const validatedRoomId = roomIdSchema.parse(roomId);
const validatedSender = userIdSchema.parse(sender);
const resp = await inviteService.inviteUserToRoom(
validatedUserId,
validatedRoomId,
validatedSender,
);
// ...
}
🤖 Prompt for AI Agents
In packages/homeserver/src/controllers/internal/room.controller.ts around lines
492 to 496, the code directly casts userId, roomId and sender to UserID/RoomID
without validating them first; add input validation before casting: check that
each ID exists and matches the expected format (e.g., non-empty string and/or
regex/type-guard used by your ID types), return a 400/422 error response when
any ID is missing or malformed, and only then cast and call
inviteService.inviteUserToRoom. Use existing validation helpers if available or
add small guard functions (isValidUserID/isValidRoomID) and short-circuit on
failure with an appropriate error message.

Comment on lines 629 to 633
const EventPduTypeRoomAliases = z.object({
...PduNoContentStateEventSchema,
...PduNoContentEmptyStateKeyStateEventSchema,
type: z.literal('m.room.aliases'),
content: PduCanonicalAliasEventContentSchema,
});
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 2, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

m.room.aliases state_key must remain the server domain

Per the Matrix spec, m.room.aliases events carry the owning server’s domain in state_key (e.g., example.com). Forcing it to '' will make us reject every legitimate aliases event received over federation. Please revert to a schema that accepts arbitrary server names again—for example:

-const EventPduTypeRoomAliases = z.object({
-	...PduNoContentEmptyStateKeyStateEventSchema,
+const EventPduTypeRoomAliases = z.object({
+	...PduNoContentTimelineEventSchema,
+	state_key: z.string(),
 	type: z.literal('m.room.aliases'),
 	content: PduCanonicalAliasEventContentSchema,
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const EventPduTypeRoomAliases = z.object({
...PduNoContentStateEventSchema,
...PduNoContentEmptyStateKeyStateEventSchema,
type: z.literal('m.room.aliases'),
content: PduCanonicalAliasEventContentSchema,
});
const EventPduTypeRoomAliases = z.object({
...PduNoContentTimelineEventSchema,
state_key: z.string(),
type: z.literal('m.room.aliases'),
content: PduCanonicalAliasEventContentSchema,
});
🤖 Prompt for AI Agents
In packages/room/src/types/v3-11.ts around lines 629 to 633, the
EventPduTypeRoomAliases schema currently forces the state_key to an empty string
which is incorrect for m.room.aliases; change the schema so state_key accepts
arbitrary server domain strings again (e.g., z.string() or
z.string().nonempty(), optionally with a host/domain validation), instead of
''—update the spread or explicit property on the object to allow those server
names so federated aliases events are accepted.

Copy link
Member

Choose a reason for hiding this comment

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

this is incorrect.. based on the spec https://spec.matrix.org/v1.16/client-server-api/#mroomcanonical_alias canonical alias event should have empty state_key

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Matrix spec m.room.aliases event state_key server domain requirement

💡 Result:

Short answer: the m.room.aliases state event uses the homeserver domain as its state_key (the domain that "owns" the listed aliases), and the event must be sent/signed by that same server (i.e. the event sender’s domain MUST match the state_key). Clients/servers should treat the lists as advisory and validate aliases by resolving them. [1][2][3]

Sources:

  • m.room.aliases description (legacy spec): state_key is the homeserver domain which owns these aliases; client advice. [1]
  • Room v3/v4 auth rules: events of type m.room.aliases MUST have a state_key and the sender’s domain MUST match the state_key (otherwise reject). [2][3]

@sampaiodiego, you're absolutely correct about m.room.canonical_alias requiring an empty state_key—that change on line 618 is correct! ✓

However, my comment was specifically about m.room.aliases (lines 629-633 in the diff), which is a different event type with different requirements. According to the Matrix spec, m.room.aliases events must have the homeserver domain as the state_key (e.g., "example.com"), and the sender's domain must match that state_key. [1][2][3]

Summary:

  • m.room.canonical_alias → empty state_key (line 618 is correct)
  • m.room.aliases → server domain state_key (line 629 needs to allow non-empty strings)

The fix for m.room.aliases should be:

const EventPduTypeRoomAliases = z.object({
	...PduNoContentTimelineEventSchema,
	state_key: z.string(),
	type: z.literal('m.room.aliases'),
	content: PduCanonicalAliasEventContentSchema,
});

Note: m.room.aliases is a legacy/deprecated event type, but if the codebase still supports it, the schema must match the spec requirements for proper federation compatibility.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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.

3 participants