From 3a9865107cc9660cdd32b109895cadf53a3b0fac Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 7 Aug 2026 16:23:18 -0300 Subject: [PATCH 1/2] fix(federation): endpoints rejecting valid requests --- .changeset/real-zoos-cover.md | 10 ++++++++++ .../federation-matrix/src/api/_matrix/client/media.ts | 8 +++++--- .../src/api/_matrix/client/rooms-messaging.ts | 4 +++- .../federation-matrix/src/api/_matrix/transactions.ts | 7 +++++-- 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 .changeset/real-zoos-cover.md diff --git a/.changeset/real-zoos-cover.md b/.changeset/real-zoos-cover.md new file mode 100644 index 0000000000000..90b7215728e4e --- /dev/null +++ b/.changeset/real-zoos-cover.md @@ -0,0 +1,10 @@ +--- +'@rocket.chat/federation-matrix': patch +'@rocket.chat/meteor': patch +--- + +Fixes federation endpoints rejecting valid requests, which broke: + +- room history backfill +- image thumbnails +- room message pagination diff --git a/ee/packages/federation-matrix/src/api/_matrix/client/media.ts b/ee/packages/federation-matrix/src/api/_matrix/client/media.ts index e465acaa6143f..e3530b269f55d 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/client/media.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/client/media.ts @@ -22,10 +22,12 @@ const isMediaParamsProps = ajv.compile(MediaParamsSchema); const ThumbnailQuerySchema = { type: 'object', properties: { - width: { oneOf: [{ type: 'number' }, { type: 'string' }] }, - height: { oneOf: [{ type: 'number' }, { type: 'string' }] }, + // union type lists rather than `oneOf`: ajvQuery coerces between number and string, so both + // `oneOf` branches would match a numeric value and fail validation + width: { type: ['number', 'string'] }, + height: { type: ['number', 'string'] }, method: { type: 'string', enum: ['crop', 'scale'] }, - timeout_ms: { oneOf: [{ type: 'number' }, { type: 'string' }] }, + timeout_ms: { type: ['number', 'string'] }, }, }; diff --git a/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts b/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts index d724fe859f16d..3ebb047904288 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts @@ -57,7 +57,9 @@ const MessagesQuerySchema = { from: { type: 'string' }, to: { type: 'string' }, dir: { type: 'string', enum: ['b', 'f'] }, - limit: { oneOf: [{ type: 'number' }, { type: 'string' }] }, + // a union type list rather than `oneOf`: ajvQuery coerces between number and string, so both + // `oneOf` branches would match a numeric value and fail validation + limit: { type: ['number', 'string'] }, filter: { type: 'string' }, }, }; diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index d0b001cdc111a..3881118cfd87b 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -279,7 +279,10 @@ const BackfillQuerySchema = { description: 'Maximum number of events to retrieve', }, v: { - oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + // a string branch here would be redundant: ajvQuery coerces a single `?v=` into a + // one-element array, and in a `oneOf` both branches would match and fail validation + type: 'array', + items: { type: 'string' }, description: 'Event ID(s) to backfill from', }, }, @@ -289,7 +292,7 @@ const BackfillQuerySchema = { const isBackfillQueryProps = ajvQuery.compile<{ limit: number; - v: string | string[]; + v: string[]; }>(BackfillQuerySchema); const BackfillResponseSchema = { From 34633738b2dcd03f2ed93860f4b7cf853f602f2a Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 7 Aug 2026 16:39:59 -0300 Subject: [PATCH 2/2] fix(federation): send_join wrong endpoint schema --- .changeset/real-zoos-cover.md | 1 + .../src/api/_matrix/send-join.ts | 144 +++--------------- 2 files changed, 22 insertions(+), 123 deletions(-) diff --git a/.changeset/real-zoos-cover.md b/.changeset/real-zoos-cover.md index 90b7215728e4e..ea52fed9b8e6e 100644 --- a/.changeset/real-zoos-cover.md +++ b/.changeset/real-zoos-cover.md @@ -8,3 +8,4 @@ Fixes federation endpoints rejecting valid requests, which broke: - room history backfill - image thumbnails - room message pagination +- accepting an invite from another homeserver diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index c65fc73ff83e4..f127559ef3c4e 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -1,4 +1,3 @@ -import type { EventID } from '@rocket.chat/federation-sdk'; import { federationSDK } from '@rocket.chat/federation-sdk'; import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; @@ -29,12 +28,6 @@ const TimestampSchema = { description: 'Unix timestamp in milliseconds', }; -const DepthSchema = { - type: 'number', - minimum: 0, - description: 'Event depth', -}; - const ServerNameSchema = { type: 'string', description: 'Matrix server name', @@ -51,137 +44,42 @@ const SendJoinParamsSchema = { const isSendJoinParamsProps = ajv.compile(SendJoinParamsSchema); -const EventHashSchema = { - type: 'object', - properties: { - sha256: { - type: 'string', - description: 'SHA256 hash of the event', - }, - }, - required: ['sha256'], -}; - -const EventSignatureSchema = { - type: 'object', - description: 'Event signatures by server and key ID', -}; - -const MembershipEventContentSchema = { - type: 'object', - properties: { - membership: { - type: 'string', - enum: ['join', 'leave', 'invite', 'ban', 'knock'], - description: 'Membership state', - }, - displayname: { - type: 'string', - nullable: true, - }, - avatar_url: { - type: 'string', - nullable: true, - }, - join_authorised_via_users_server: { - type: 'string', - nullable: true, - }, - is_direct: { - type: 'boolean', - nullable: true, - }, - reason: { - type: 'string', - description: 'Reason for membership change', - nullable: true, - }, - }, - required: ['membership'], -}; - -const EventBaseSchema = { +const SendJoinEventSchema = { type: 'object', properties: { type: { type: 'string', - description: 'Event type', - }, - content: { - type: 'object', - description: 'Event content', + const: 'm.room.member', }, - sender: UsernameSchema, - room_id: RoomIdSchema, - origin_server_ts: TimestampSchema, - depth: DepthSchema, - prev_events: { - type: 'array', - items: { - type: 'string', - }, - description: 'Previous events in the room', + state_key: { + ...UsernameSchema, + description: 'Matrix user ID of the joining member', }, - auth_events: { - type: 'array', - items: { - type: 'string', - }, - description: 'Authorization events', + sender: { + ...UsernameSchema, + description: 'Matrix user ID of the joining member', }, origin: { - type: 'string', - description: 'Origin server', - }, - hashes: { - ...EventHashSchema, - nullable: true, + ...ServerNameSchema, + description: 'The name of the joining homeserver', }, - signatures: { - ...EventSignatureSchema, - nullable: true, - }, - unsigned: { - type: 'object', - description: 'Unsigned data', - nullable: true, - }, - }, - required: ['type', 'content', 'sender', 'room_id', 'origin_server_ts', 'depth', 'prev_events', 'auth_events', 'origin'], -}; - -const SendJoinEventSchema = { - type: 'object', - allOf: [ - EventBaseSchema, - { + origin_server_ts: TimestampSchema, + content: { type: 'object', properties: { - type: { + membership: { type: 'string', - const: 'm.room.member', + const: 'join', }, - content: { - type: 'object', - allOf: [ - MembershipEventContentSchema, - { - type: 'object', - properties: { - membership: { - type: 'string', - const: 'join', - }, - }, - required: ['membership'], - }, - ], + join_authorised_via_users_server: { + ...UsernameSchema, + description: 'User ID of a resident server member authorizing the join into a restricted room', }, - state_key: UsernameSchema, }, - required: ['type', 'content', 'state_key'], + required: ['membership'], }, - ], + }, + required: ['type', 'state_key', 'sender', 'origin', 'origin_server_ts', 'content'], }; const isSendJoinEventProps = ajv.compile(SendJoinEventSchema); @@ -235,7 +133,7 @@ export const getMatrixSendJoinRoutes = () => { const { roomId, stateKey } = c.req.param(); const body = await c.req.json(); - const response = await federationSDK.sendJoin(roomId, stateKey as EventID, body); + const response = await federationSDK.sendJoin(roomId, stateKey, body); return { body: response,