diff --git a/packages/homeserver/src/events/EventStagingArea.ts b/packages/homeserver/src/events/EventStagingArea.ts deleted file mode 100644 index bba736c02..000000000 --- a/packages/homeserver/src/events/EventStagingArea.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { EventTypeArray } from '../validation/pipelines'; -import { RoomState } from './roomState'; - -export class EventStagingArea { - private events: Map = new Map(); - private pendingEvents: Map = new Map(); - private processedEvents: Set = new Set(); - private missingAuthEvents: Map> = new Map(); // event -> missing auth events - private missingPrevEvents: Map> = new Map(); // event -> missing prev events - - private roomId: string; - // private eventFetcher: EventFetcher; - private roomState: RoomState; - - constructor(roomId: string, context: any) { - this.roomId = roomId; - // this.eventFetcher = new EventFetcher(context); - this.roomState = new RoomState(roomId); - console.info(`EventStagingArea initialized for room ${roomId}`); - } - - public async addEvents(events: EventTypeArray, context: any): Promise { - for (const { eventId, event } of events) { - const roomId = event.room_id.split(':')[0]; - if (roomId !== this.roomId) { - console.warn(`Ignoring event from wrong room: ${eventId}`); - continue; - } - - this.events.set(eventId, event); - this.pendingEvents.set(eventId, event); - } - - await this.processEvents(context); - } - - private async processEvents(context: any): Promise { - const pendingEventIds = Array.from(this.pendingEvents.keys()); - console.debug(`Processing ${pendingEventIds.length} pending events`); - - for (const eventId of pendingEventIds) { - const event = this.pendingEvents.get(eventId); - if (!event) continue; - - const authEventIds = this.getAuthEventIds(event); - const missingAuthEventIds = this.getMissingEventIds(authEventIds); - if (missingAuthEventIds.length > 0) { - this.missingAuthEvents.set(eventId, new Set(missingAuthEventIds)); - } - - const prevEventIds = this.getPrevEventIds(event); - const missingPrevEventIds = this.getMissingEventIds(prevEventIds); - if (missingPrevEventIds.length > 0) { - this.missingPrevEvents.set(eventId, new Set(missingPrevEventIds)); - } - - this.processedEvents.add(eventId); - } - - await this.moveEventsToRoomState(); - } - - private async moveEventsToRoomState(): Promise { - const readyEvents: any[] = []; - - for (const [eventId, event] of this.pendingEvents.entries()) { - if ( - this.missingAuthEvents.has(eventId) || - this.missingPrevEvents.has(eventId) - ) { - continue; - } - - readyEvents.push(event); - this.pendingEvents.delete(eventId); - } - - readyEvents.sort((a, b) => { - const depthA = a.depth || 0; - const depthB = b.depth || 0; - return depthA - depthB; - }); - - for (const event of readyEvents) { - await this.roomState.addEvent(event); - console.debug(`Added event ${event.event_id} to room state`); - } - - console.info(`Moved ${readyEvents.length} events to room state`); - } - - private getMissingEventIds(eventIds: string[]): string[] { - return eventIds.filter(id => - !this.events.has(id) && - !this.roomState.getEvent(id) - ); - } - - private getAuthEventIds(event: any): string[] { - if (!event.auth_events || !Array.isArray(event.auth_events)) { - return []; - } - - return event.auth_events.map((authEvent: any) => - Array.isArray(authEvent) ? authEvent[0] : authEvent - ); - } - - private getPrevEventIds(event: any): string[] { - if (!event.prev_events || !Array.isArray(event.prev_events)) { - return []; - } - - return event.prev_events.map((prevEvent: any) => - Array.isArray(prevEvent) ? prevEvent[0] : prevEvent - ); - } - - public getAllEvents(): any[] { - return Array.from(this.events.values()); - } - - public getPendingEvents(): any[] { - return Array.from(this.pendingEvents.values()); - } - - public getRoomState(): RoomState { - return this.roomState; - } - - public getStats(): Record { - return { - totalEvents: this.events.size, - pendingEvents: this.pendingEvents.size, - processedEvents: this.processedEvents.size, - eventsWithMissingAuthDeps: this.missingAuthEvents.size, - eventsWithMissingPrevDeps: this.missingPrevEvents.size - }; - } -} \ No newline at end of file diff --git a/packages/homeserver/src/events/handleMatrixEvents.ts b/packages/homeserver/src/events/handleMatrixEvents.ts deleted file mode 100644 index fe27b3c4f..000000000 --- a/packages/homeserver/src/events/handleMatrixEvents.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { getErrorMessage } from "../utils/get-error-message"; -import { stagingArea, type StagingEvent } from "./stagingArea"; - -/** - * Handle incoming Matrix protocol events - * This function demonstrates how to use the staging area to process Matrix events - * - * @param events The events to process - * @param context Context object containing configuration and services - * @returns Result of the processing - */ -export async function handleMatrixEvents(events: any[], context: any) { - try { - console.info(`Processing ${events.length} incoming Matrix events`); - - // Convert the raw events to StagingEvents - const stagingEvents: StagingEvent[] = events.map(event => ({ - eventId: event.event_id, - event, - originServer: event.origin || context.config.name, - roomId: event.room_id, - })); - - // Add the events to the staging area for processing - // This will handle downloading any missing dependency events - const results = await stagingArea.addEvents(stagingEvents, context); - - // Return the processing results - return { - success: true, - failed_count: 0, - results: [] - }; - } catch (error) { - console.error(`Error handling Matrix events: ${getErrorMessage(error)}`); - return { - success: false, - error: getErrorMessage(error) - }; - } -} - -/** - * Example of how to use the handleMatrixEvents function - */ -export async function processMatrixEventsExample(events: unknown[], serverName: string, context: unknown) { - const processingContext = { - ...(context as any), - config: { - name: serverName, - }, - }; - - // Process the events - await handleMatrixEvents(events, processingContext); - - return { - success: true, - results: [] - }; -} \ No newline at end of file diff --git a/packages/homeserver/src/events/roomState.spec.ts b/packages/homeserver/src/events/roomState.spec.ts deleted file mode 100644 index db2a6495f..000000000 --- a/packages/homeserver/src/events/roomState.spec.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { beforeEach, describe, expect, test } from 'bun:test'; -import { RoomState } from './roomState'; - -// TODO: Need to migrate this tests to services -describe.skip('RoomState', () => { - let roomState: RoomState; - const roomId = '!test:example.org'; - - beforeEach(() => { - roomState = new RoomState(roomId); - }); - - function createEvent(overrides: Record = {}): any { - return { - event_id: `$${Date.now()}-${Math.floor(Math.random() * 1000)}`, - room_id: roomId, - sender: '@user1:example.org', - origin_server_ts: Date.now(), - type: 'm.room.message', - content: { - body: 'Test message', - msgtype: 'm.text' - }, - depth: 1, - prev_events: [], - auth_events: [], - ...overrides - }; - } - - function createStateEvent(type: string, stateKey: string, content: any, overrides: Record = {}): any { - return createEvent({ - type, - state_key: stateKey, - content, - ...overrides - }); - } - - async function addCreateRoomEvent(): Promise { - const createEvent = createStateEvent('m.room.create', '', { - creator: '@user1:example.org', - room_version: '6' - }); - await roomState.addEvent(createEvent); - return createEvent; - } - - async function addPowerLevelsEvent(users: Record = {}): Promise { - const plEvent = createStateEvent('m.room.power_levels', '', { - users: { - '@user1:example.org': 100, - ...users - }, - users_default: 0, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50 - }); - await roomState.addEvent(plEvent); - return plEvent; - } - - test('should add a valid event', async () => { - const event = createEvent(); - const result = await roomState.addEvent(event); - expect(result).toBe(true); - expect(roomState.getEvent(event.event_id)).toEqual(event); - }); - - // Test rejecting events with wrong room_id - test('should reject events with incorrect room_id', async () => { - const event = createEvent({ room_id: '!wrong:example.org' }); - const result = await roomState.addEvent(event); - expect(result).toBe(false); - expect(roomState.getEvent(event.event_id)).toBeNull(); - }); - - // Test handling duplicate events - test('should handle duplicate events', async () => { - const event = createEvent(); - await roomState.addEvent(event); - const result = await roomState.addEvent(event); - expect(result).toBe(true); // Should succeed but not add again - }); - - // Test power level validation - test('should enforce power level validation for state events', async () => { - await addCreateRoomEvent(); - await addPowerLevelsEvent(); - - // User2 can't send state events (requires PL 50) - const event = createStateEvent('m.room.name', '', { name: 'Test Room' }, { - sender: '@user2:example.org' - }); - const result = await roomState.addEvent(event); - expect(result).toBe(false); - }); - - // Test users can send events they have power level for - test('should allow events when user has sufficient power level', async () => { - await addCreateRoomEvent(); - await addPowerLevelsEvent({ - '@user2:example.org': 60 - }); - - // User2 can now send state events - const event = createStateEvent('m.room.name', '', { name: 'Test Room' }, { - sender: '@user2:example.org' - }); - const result = await roomState.addEvent(event); - expect(result).toBe(true); - }); - - // Test membership changes - test('should track membership changes', async () => { - await addCreateRoomEvent(); - - // Add join event - const joinEvent = createStateEvent('m.room.member', '@user2:example.org', { - membership: 'join' - }); - await roomState.addEvent(joinEvent); - - expect(roomState.isUserJoined('@user2:example.org')).toBe(true); - expect(roomState.getJoinedMembers()).toContain('@user2:example.org'); - - // Add ban event - const banEvent = createStateEvent('m.room.member', '@user2:example.org', { - membership: 'ban' - }); - await roomState.addEvent(banEvent); - - expect(roomState.isUserJoined('@user2:example.org')).toBe(false); - expect(roomState.getBannedMembers()).toContain('@user2:example.org'); - }); - - // Test banned users can't send events - test('should reject events from banned users', async () => { - await addCreateRoomEvent(); - - // Ban user2 - const banEvent = createStateEvent('m.room.member', '@user2:example.org', { - membership: 'ban' - }); - await roomState.addEvent(banEvent); - - // Try to send message as banned user - const msgEvent = createEvent({ - sender: '@user2:example.org' - }); - const result = await roomState.addEvent(msgEvent); - expect(result).toBe(false); - }); - - // Test DAG management - test('should maintain forward and backward extremities', async () => { - const event1 = createEvent(); - await roomState.addEvent(event1); - - expect(roomState.getForwardExtremities()).toContain(event1.event_id); - - // Add an event that references the first one - const event2 = createEvent({ - prev_events: [[event1.event_id, {}]] - }); - await roomState.addEvent(event2); - - // Forward extremities should update - expect(roomState.getForwardExtremities()).not.toContain(event1.event_id); - expect(roomState.getForwardExtremities()).toContain(event2.event_id); - - // Reference a non-existent event to create backward extremity - const missingEventId = '$missing:example.org'; - const event3 = createEvent({ - prev_events: [[missingEventId, {}]] - }); - await roomState.addEvent(event3); - - expect(roomState.getBackwardExtremities()).toContain(missingEventId); - }); - - // Test state resolution - test('should resolve state correctly', async () => { - const createEvent = await addCreateRoomEvent(); - const plEvent = await addPowerLevelsEvent(); - - // Add a room name event - const nameEvent = createStateEvent('m.room.name', '', { name: 'First Name' }); - await roomState.addEvent(nameEvent); - - // Add a newer room name event - const nameEvent2 = createStateEvent('m.room.name', '', { name: 'Second Name' }, { - origin_server_ts: Date.now() + 1000 - }); - await roomState.addEvent(nameEvent2); - - // Resolve state from all events - const allEvents = [createEvent.event_id, plEvent.event_id, nameEvent.event_id, nameEvent2.event_id]; - const resolvedState = roomState.resolveState(allEvents); - - // Should have 3 state events (create, power_levels, and the newer name) - expect(resolvedState.length).toBe(3); - const resolvedName = resolvedState.find(e => e.type === 'm.room.name'); - expect(resolvedName.content.name).toBe('Second Name'); - }); - - // Test depth tracking - test('should track event depths', async () => { - const event1 = createEvent({ depth: 1 }); - await roomState.addEvent(event1); - - const event2 = createEvent({ - depth: 2, - event_id: `$${Date.now()}-${Math.floor(Math.random() * 1000)}-event2` - }); - await roomState.addEvent(event2); - - // Add a small delay to ensure unique timestamp in event_id - await new Promise(resolve => setTimeout(resolve, 5)); - - const event3 = createEvent({ - depth: 2, - event_id: `$${Date.now()}-${Math.floor(Math.random() * 1000)}-event3` - }); - await roomState.addEvent(event3); - - expect(roomState.getMaxDepth()).toBe(2); - expect(roomState.getEventsAtDepth(2).length).toBe(2); - }); - - // Test auth chain - test('should return success when computing auth chains correctly', async () => { - const roomCreateEvent = await addCreateRoomEvent(); - const plEvent = await addPowerLevelsEvent(); - - // Create an event that references the create and power level events in its auth chain - const messageEvent = createEvent({ - event_id: '$test:example.org', - type: 'm.room.message', - sender: '@alice:example.org', - content: {}, - auth_events: [ - [roomCreateEvent.event_id, {}], - [plEvent.event_id, {}] - ], - prev_events: [] - }); - await roomState.addEvent(messageEvent); - - const authChain = roomState.getAuthChain(messageEvent.event_id); - expect(authChain.size).toBe(3); // The event itself plus the two auth events - expect(authChain.has(roomCreateEvent.event_id)).toBe(true); - expect(authChain.has(plEvent.event_id)).toBe(true); - }); - - // Test child event tracking - test('should track child events', async () => { - const event1 = createEvent(); - await roomState.addEvent(event1); - - const event2 = createEvent({ - prev_events: [[event1.event_id, {}]] - }); - await roomState.addEvent(event2); - - const children = roomState.getChildEvents(event1.event_id); - expect(children).toContain(event2.event_id); - }); -}); \ No newline at end of file diff --git a/packages/homeserver/src/events/roomState.ts b/packages/homeserver/src/events/roomState.ts deleted file mode 100644 index beef9164d..000000000 --- a/packages/homeserver/src/events/roomState.ts +++ /dev/null @@ -1,427 +0,0 @@ -import type { EventType } from "../validation/pipelines"; - -export interface PowerLevels { - ban?: number; - kick?: number; - invite?: number; - redact?: number; - events?: Record; - state_default?: number; - events_default?: number; - users_default?: number; - users?: Record; - notifications?: Record; -} - -export class RoomState { - private eventMap: Map = new Map(); - private stateEvents: Map = new Map(); - private powerLevels: PowerLevels | null = null; - - private authEvents: Map> = new Map(); - private prevEvents: Map> = new Map(); - private forwardExtremities: Set = new Set(); - private backwardExtremities: Set = new Set(); - - private eventDepths: Map = new Map(); - private depthToEvents: Map> = new Map(); - private maxDepth = 0; - - private roomVersion = "10"; - private roomCreator: string | null = null; - private joinedMembers: Set = new Set(); - private invitedMembers: Set = new Set(); - private bannedMembers: Set = new Set(); - - private roomId: string; - - constructor(roomId: string) { - this.roomId = roomId; - console.info(`RoomState initialized for room ${roomId}`); - } - - public async addEvent({ eventId, event }: EventType): Promise { - if (event.room_id !== this.roomId) { - console.warn(`Attempted to add event from room ${event.room_id} to room ${this.roomId}`); - return false; - } - - if (this.eventMap.has(eventId)) { - console.debug(`Event ${eventId} already exists in room state`); - return true; - } - - try { - await this.validateEventAgainstAuthChain(event); - this.checkAndMarkBackwardExtremities(event); - this.eventMap.set(eventId, event); - - const depth = event.depth || 0; - this.eventDepths.set(eventId, depth); - if (!this.depthToEvents.has(depth)) { - this.depthToEvents.set(depth, new Set()); - } - this.depthToEvents.get(depth)?.add(eventId); - - if (depth > this.maxDepth) { - this.maxDepth = depth; - } - - this.trackAuthChain(event); - this.trackPrevEvents(event); - this.updateForwardExtremities(event); - - if (this.isStateEvent(event)) { - await this.processStateEvent(event); - } - - if (event.type === "m.room.member") { - this.processMembershipChange(event); - } - - console.debug(`Added event ${eventId} to room ${this.roomId}`); - return true; - } catch (error: any) { - console.warn(`Failed to add event ${eventId}: ${error.message}`); - console.debug(event); - console.debug(error); - return false; - } - } - - private processMembershipChange(event: any): void { - if (event.type !== "m.room.member" || !event.state_key) { - return; - } - - const userId = event.state_key; - const membership = event.content?.membership; - - this.joinedMembers.delete(userId); - this.invitedMembers.delete(userId); - this.bannedMembers.delete(userId); - - if (membership === "join") { - this.joinedMembers.add(userId); - } else if (membership === "invite") { - this.invitedMembers.add(userId); - } else if (membership === "ban") { - this.bannedMembers.add(userId); - } - - if (membership === "join" && !this.roomCreator) { - const createEvent = this.getStateEvent("m.room.create", ""); - if (createEvent?.content?.creator === userId) { - this.roomCreator = userId; - } - } - } - - private async processStateEvent(event: any): Promise { - const stateKey = `${event.type}|${event.state_key}`; - - if (event.type === "m.room.power_levels") { - this.powerLevels = event.content; - } else if (event.type === "m.room.create") { - this.roomVersion = event.content?.room_version; - } - - this.stateEvents.set(stateKey, event); - } - - private trackAuthChain(event: any): void { - const authEventIds = this.getAuthEventIds(event); - this.authEvents.set(event.event_id, new Set(authEventIds)); - } - - private trackPrevEvents(event: any): void { - const prevEventIds = this.getPrevEventIds(event); - this.prevEvents.set(event.event_id, new Set(prevEventIds)); - } - - private async validateEventAgainstAuthChain(event: any): Promise { - const authEventIds = this.getAuthEventIds(event); - - for (const authId of authEventIds) { - if (!this.eventMap.has(authId)) { - // In a real implementation, we would fetch missing auth events - console.warn(`Auth event ${authId} not found for event ${event.event_id}`); - this.backwardExtremities.add(authId); - } - } - - // Implement auth rules according to Matrix spec - // https://matrix.org/docs/spec/server_server/latest#checks-performed-on-receipt-of-a-pdu - - this.validateBasicEventRules(event); - - if (this.powerLevels) { - this.validateEventAgainstPowerLevels(event); - } - - if (this.isStateEvent(event)) { - await this.validateStateEvent(event); - } - } - - private async validateStateEvent(event: any): Promise { - // TODO: - // 1. If this is a redaction, check the sender has permission to redact - // 2. If this changes membership, check the sender has permission for that specific change - // 3. Validate the state transition is allowed by Matrix spec - - if (event.type === "m.room.power_levels") { - const currentPowerLevels = this.powerLevels; - if (currentPowerLevels) { - const senderPower = this.getUserPowerLevel(event.sender); - - const newUsers = event.content?.users || {}; - const currentUsers = currentPowerLevels.users || {}; - - for (const [userId, newLevel] of Object.entries(newUsers)) { - const currentLevel = currentUsers[userId] || currentPowerLevels.users_default || 0; - if ((newLevel as number) > senderPower || currentLevel > senderPower) { - throw new Error("Cannot change power levels higher than your own"); - } - } - } - } - } - - private validateEventAgainstPowerLevels(event: any): void { - const senderPower = this.getUserPowerLevel(event.sender); - - if (this.isStateEvent(event)) { - const requiredPower = this.getRequiredPowerLevelForState(event.type); - if (senderPower < requiredPower) { - throw new Error(`Sender power level ${senderPower} is lower than required level ${requiredPower} for state event ${event.type}`); - } - } else { - const requiredPower = this.getRequiredPowerLevelForEvent(event.type); - if (senderPower < requiredPower) { - throw new Error(`Sender power level ${senderPower} is lower than required level ${requiredPower} for event ${event.type}`); - } - } - } - - private getUserPowerLevel(userId: string): number { - if (!this.powerLevels) { - return 0; - } - - return this.powerLevels.users?.[userId] ?? this.powerLevels.users_default ?? 0; - } - - private getRequiredPowerLevelForState(eventType: string): number { - if (!this.powerLevels) { - return 50; // Default in Matrix spec - } - - return this.powerLevels.events?.[eventType] ?? this.powerLevels.state_default ?? 50; - } - - private getRequiredPowerLevelForEvent(eventType: string): number { - if (!this.powerLevels) { - return 0; // Default in Matrix spec - } - - return this.powerLevels.events?.[eventType] ?? this.powerLevels.events_default ?? 0; - } - - private getAuthEventIds(event: any): string[] { - if (!event.auth_events || !Array.isArray(event.auth_events)) { - return []; - } - return event.auth_events.map((authEvent: any) => - Array.isArray(authEvent) ? authEvent[0] : authEvent - ); - } - - private getPrevEventIds(event: any): string[] { - if (!event.prev_events || !Array.isArray(event.prev_events)) { - return []; - } - return event.prev_events.map((prevEvent: any) => - Array.isArray(prevEvent) ? prevEvent[0] : prevEvent - ); - } - - private validateBasicEventRules(event: any): void { - if (!event.event_id) { - throw new Error("Event missing event_id"); - } - - if (!event.room_id) { - throw new Error("Event missing room_id"); - } - - if (!event.sender) { - throw new Error("Event missing sender"); - } - - if (!event.origin_server_ts) { - throw new Error("Event missing origin_server_ts"); - } - - if (event.room_id !== this.roomId) { - throw new Error(`Event room_id ${event.room_id} does not match expected ${this.roomId}`); - } - - const now = Date.now(); - const eventTime = event.origin_server_ts; - - if (eventTime > now + 5 * 60 * 1000) { // 5 minutes in the future - throw new Error("Event timestamp too far in the future"); - } - - if (this.bannedMembers.has(event.sender)) { - throw new Error("Sender is banned from the room"); - } - } - - private checkAndMarkBackwardExtremities(event: any): void { - const prevEventIds = this.getPrevEventIds(event); - - for (const prevEventId of prevEventIds) { - if (!this.eventMap.has(prevEventId)) { - console.debug(`Adding backward extremity: ${prevEventId}`); - this.backwardExtremities.add(prevEventId); - } - } - } - - private updateForwardExtremities(event: any): void { - const prevEventIds = this.getPrevEventIds(event); - - // If this event has no prev_events, it's a new forward extremity - if (prevEventIds.length === 0) { - this.forwardExtremities.add(event.event_id); - return; - } - - for (const prevEventId of prevEventIds) { - this.forwardExtremities.delete(prevEventId); - } - - this.forwardExtremities.add(event.event_id); - } - - private isStateEvent(event: any): boolean { - return event.type && event.hasOwn('state_key'); - } - - public resolveState(eventIds: string[]): any[] { - // TODO: use the appropriate state resolution algorithm based on the room version - - // Simplified algorithm: - // 1. Collect all state events referenced by the given event IDs - // 2. For each (type, state_key), pick the most recent event by origin_server_ts - - const stateMap = new Map(); - - for (const eventId of eventIds) { - const event = this.eventMap.get(eventId); - if (event && this.isStateEvent(event)) { - const key = `${event.type}|${event.state_key}`; - const existing = stateMap.get(key); - - if (!existing || event.origin_server_ts > existing.origin_server_ts) { - stateMap.set(key, event); - } - } - } - - return Array.from(stateMap.values()); - } - - public getStateEvents(): any[] { - return Array.from(this.stateEvents.values()); - } - - public getStateEvent(type: string, stateKey: string): any | null { - const key = `${type}|${stateKey}`; - return this.stateEvents.get(key) || null; - } - - public getForwardExtremities(): string[] { - return Array.from(this.forwardExtremities); - } - - public getBackwardExtremities(): string[] { - return Array.from(this.backwardExtremities); - } - - public getEvent(eventId: string): any | null { - return this.eventMap.get(eventId) || null; - } - - public getAllEvents(): any[] { - return Array.from(this.eventMap.values()); - } - - public getEventsAtDepth(depth: number): any[] { - const eventIds = this.depthToEvents.get(depth) || new Set(); - return Array.from(eventIds).map(id => this.eventMap.get(id)).filter(Boolean); - } - - public getMaxDepth(): number { - return this.maxDepth; - } - - public getRoomId(): string { - return this.roomId; - } - - public getRoomVersion(): string { - return this.roomVersion; - } - - public getJoinedMembers(): string[] { - return Array.from(this.joinedMembers); - } - - public getInvitedMembers(): string[] { - return Array.from(this.invitedMembers); - } - - public getBannedMembers(): string[] { - return Array.from(this.bannedMembers); - } - - public isUserJoined(userId: string): boolean { - return this.joinedMembers.has(userId); - } - - public getAuthChain(eventId: string): Set { - const result = new Set(); - const toProcess = [eventId]; - - while (toProcess.length > 0) { - const currentId = toProcess.pop()!; - result.add(currentId); - - const authEvents = this.authEvents.get(currentId); - if (authEvents) { - for (const authId of authEvents) { - if (!result.has(authId)) { - toProcess.push(authId); - } - } - } - } - - return result; - } - - public getChildEvents(eventId: string): string[] { - const children: string[] = []; - - for (const [id, prevEvents] of this.prevEvents.entries()) { - if (prevEvents.has(eventId)) { - children.push(id); - } - } - - return children; - } -} \ No newline at end of file diff --git a/packages/homeserver/src/events/stagingArea.spec.ts b/packages/homeserver/src/events/stagingArea.spec.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/homeserver/src/events/stagingArea.ts b/packages/homeserver/src/events/stagingArea.ts deleted file mode 100644 index 182c48ceb..000000000 --- a/packages/homeserver/src/events/stagingArea.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { BaseEventType } from "../validation/schemas/event-schemas"; - -export type StagingEvent = { - eventId: BaseEventType["event_id"]; - event: BaseEventType; -}; - -const getFirst = (set: Set): T | undefined => Array.from(set)[0]; - -export class StagingArea { - private missingEventsQueue = new Set(); - private processingQueue = new Set(); - private context: any; - - constructor() { - this.startBackgroundProcessors(); - } - - private startBackgroundProcessors() { - setImmediate(() => this.startMissingEventProcessor()); - setImmediate(() => this.startEventProcessor()); - } - - private async startMissingEventProcessor() { - while (this.missingEventsQueue.size) { - const eventId = this.missingEventsQueue.values().next().value; - if (!eventId) { - break; - } - - this.missingEventsQueue.delete(eventId); - - const event = await this.getEventFromDatabase(eventId); - if (event) { - await this.processNewEvent({ eventId, event: event.event }); - } - } - } - - private async startEventProcessor() { - } - - private addToProcessingQueue(stagingEvent: StagingEvent) { - const exists = Array.from(this.processingQueue).some(item => item.eventId === stagingEvent.eventId); - if (!exists) this.processingQueue.add(stagingEvent); - } - - private async checkForMissingDependencies(event: any): Promise { - const authEvents = event?.auth_events ?? []; - const prevEvents = event?.prev_events ?? []; - const deps = [...authEvents, ...prevEvents]; - - const missingDeps: string[] = []; - for (const depId of deps) { - if (typeof depId === "string" && !(await this.getEventFromDatabase(depId))) { - missingDeps.push(depId); - } - } - return missingDeps; - } - - private async getEventFromDatabase(eventId: string): Promise { - if (this.context.mongo.getEventById) { - const event = await this.context.mongo.getEventById(eventId); - return event ? { eventId: event._id, event: event.event } : null; - } - - return null; - } - - private async processNewEvent(stagingEvent: StagingEvent) { - const { eventId, event } = stagingEvent; - - const missingDeps = await this.checkForMissingDependencies(event); - - if (missingDeps.length === 0) { - console.debug(`Event ${eventId} has all dependencies, adding to processing queue`); - return this.addToProcessingQueue(stagingEvent); - } - - console.debug(`Event ${eventId} has ${missingDeps.length} missing dependencies`); - missingDeps.map((depId) => this.missingEventsQueue.add(depId)); - } - - public async addEvents(events: StagingEvent[], context: any) { - this.context = context; - console.debug(`Adding ${events.length} events to staging area`); - for (const event of events) { - await this.processNewEvent(event); - } - } -} - -export const stagingArea = new StagingArea(); diff --git a/packages/homeserver/src/fixtures/ContextBuilder.ts b/packages/homeserver/src/fixtures/ContextBuilder.ts deleted file mode 100644 index 7b1cff4ca..000000000 --- a/packages/homeserver/src/fixtures/ContextBuilder.ts +++ /dev/null @@ -1,207 +0,0 @@ -import Elysia from "elysia"; -import Crypto from "node:crypto"; - -import type { EventBase } from "@hs/core/src/events/eventBase"; -import { createSignedEvent } from "@hs/core/src/events/utils/createSignedEvent"; -import { authorizationHeaders, generateId } from "../authentication"; -import { toUnpaddedBase64 } from "../binaryData"; -import { type SigningKey, generateKeyPairsFromString } from "../keys"; -import type { EventStore } from "../plugins/mongodb"; -import { createRoom } from "../procedures/createRoom"; - -export function createMediaId(length: number) { - const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - let result = ""; - for (let i = 0; i < length; i++) { - const randomIndex = Crypto.randomInt(0, characters.length); - result += characters[randomIndex]; - } - return result; -} - -class MockedRoom { - public events: EventStore[] = []; - constructor( - public roomId: string, - events: EventStore[], - ) { - for (const event of events) { - this.events.push(event); - } - } -} - -export class ContextBuilder { - private config: any; - private mongo: any; - private mutex: any; - private signingSeed: any; - - private events: Map = new Map(); - - private localRemoteSigningKeys: Map = new Map(); - private remoteRemoteSigningKeys: Map = new Map(); - - constructor(private name: string) {} - static create(name: string) { - return new ContextBuilder(name); - } - - public withName(name: string) { - this.name = name; - return this; - } - - public withEvent(roomId: string, event: EventBase) { - const arr = this.events.get(roomId) || []; - arr.push({ - _id: generateId(event), - event, - }); - this.events.set(roomId, arr); - return this; - } - - public withConfig(config: any) { - this.config = config; - return this; - } - - public withMongo(mongo: any) { - this.mongo = mongo; - return this; - } - - public withMutex(mutex: any) { - this.mutex = mutex; - return this; - } - - public withSigningKey(signingSeed: string) { - this.signingSeed = signingSeed; - return this; - } - - public withLocalSigningKey(remote: string, signingKey: SigningKey) { - this.localRemoteSigningKeys.set(remote, signingKey); - return this; - } - - public withRemoteSigningKey(remote: string, signingKey: SigningKey) { - this.remoteRemoteSigningKeys.set(remote, signingKey); - return this; - } - - public async build(): Promise<{ - signature: SigningKey; - name: string; - app: Elysia; - instance: ContextBuilder; - makeRequest: (method: 'GET' | 'POST' | 'PUT' | 'DELETE', uri: string, body: unknown) => Promise; - createRoom: (sender: string, ...members: string[]) => Promise; - }> { - const signature = await generateKeyPairsFromString(this.signingSeed); - - const config = { - path: "./config.json", - signingKeyPath: "./keys/ed25519.signing.key", - port: 8080, - signingKey: [signature], - name: this.name, - version: "org.matrix.msc3757.10", - }; - const app = new Elysia() - .decorate("mongo", { - getValidPublicKeyFromLocal: async (origin: string, key: string) => { - const signingKey = this.localRemoteSigningKeys.get(origin); - if (!signingKey) { - return; - } - return toUnpaddedBase64(signingKey.publicKey); - }, - getOldestStagedEvent: async (roomId: string) => { - return this.events.get(roomId)?.[0]; - }, - getEventsByRoomAndEventIds: async (roomId: string, eventIds: string[]) => { - return ( - this.events - .get(roomId) - ?.filter((event) => eventIds.includes(event._id)) ?? [] - ); - }, - createStagingEvent: async (event: EventBase) => { - const id = generateId(event); - this.events.get(event.room_id)?.push({ - _id: id, - event, - staged: true, - }); - return id; - }, - createEvent: async (event: EventBase) => { - const id = generateId(event); - this.events.get(event.room_id)?.push({ - _id: id, - event, - }); - }, - }) - .decorate("config", config); - - const makeRequest = async (method: 'GET' | 'POST' | 'PUT' | 'DELETE', uri: string, body: unknown) => { - const signingName = this.name; - const domain = "localhost"; - - return new Request(`https://${domain}${uri}`, { - headers: { - authorization: await authorizationHeaders( - signingName, - signature, - domain, - method, - uri, - body as any, - ), - "content-type": "application/json", - }, - method, - body: body && JSON.stringify(body) as any, - }); - }; - - return { - signature, - name: this.name, - app, - instance: this, - makeRequest, - createRoom: async (sender: string, ...members: string[]) => { - const { roomId, events } = await createRoom( - [ - `@${sender}:${config.name}`, - ...members.map((member) => `@${member}:${config.name}`), - ], - createSignedEvent(config.signingKey[0], config.name), - `!${createMediaId(18)}:${config.name}`, - ); - - for (const { event } of events) { - this.withEvent(roomId, event); - } - return new MockedRoom(roomId, events); - }, - }; - } -} - -export const rc1 = ContextBuilder.create("rc1").withSigningKey( - "ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8", -); - -export const hs1 = ContextBuilder.create("hs1").withSigningKey( - "ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw", -); - -export const hs2 = ContextBuilder.create("hs2").withSigningKey( - "ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw", -); diff --git a/packages/homeserver/src/mutex/Mutex.ts b/packages/homeserver/src/mutex/Mutex.ts deleted file mode 100644 index 78851cc4c..000000000 --- a/packages/homeserver/src/mutex/Mutex.ts +++ /dev/null @@ -1,34 +0,0 @@ -export class Mutex { - private map: Map = new Map(); - - public async request(scope: string, fail: true): Promise; - - public async request(scope: string): Promise; - public async request(scope: string, fail?: true) { - if (this.map.has(scope)) { - if (fail) { - throw new Error("Mutex already locked"); - } - return false; - } - - const lock = new Lock(this, scope, () => this.map.delete(scope)); - this.map.set(scope, true); - return lock; - } -} - -export class Lock { - constructor( - protected m: Mutex, - public scope: string, - private unlock: () => void, - ) {} - public async release() { - this.unlock(); - } - - [Symbol.dispose]() { - this.release(); - } -} diff --git a/packages/homeserver/src/mutex/mutext.spec.ts b/packages/homeserver/src/mutex/mutext.spec.ts deleted file mode 100644 index f5367d151..000000000 --- a/packages/homeserver/src/mutex/mutext.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; -import { Mutex, type Lock } from "./Mutex"; - -describe("Mutex", () => { - let mutex: Mutex; - let lock: Lock | false; - - beforeEach(() => { - mutex = new Mutex(); - }); - - afterEach(() => { - if (lock) { - lock.release(); - } - }); - - it("should grant a lock if the scope is not already locked", async () => { - const scope = "test-scope"; - lock = await mutex.request(scope); - expect(lock).toBeTruthy(); - }); - - it("should not grant a lock if the scope is already locked", async () => { - const scope = "test-scope"; - await mutex.request(scope); - const secondLock = await mutex.request(scope); - expect(secondLock).toBeFalsy(); - }); - - it("should release a lock and allow re-locking the same scope", async () => { - const scope = "test-scope"; - lock = await mutex.request(scope); - expect(lock).not.toBeFalse(); - - await (lock as Lock).release(); - - const newLock = await mutex.request(scope); - expect(newLock).toBeTruthy(); - }); -}); diff --git a/packages/homeserver/src/plugins/config.ts b/packages/homeserver/src/plugins/config.ts deleted file mode 100644 index d42d4ed97..000000000 --- a/packages/homeserver/src/plugins/config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Elysia from "elysia"; -import type { InferContext } from "elysia"; -import type { SigningKey } from "../keys"; - -export interface Config { - path: string; - signingKeyPath: string; - port: number; - signingKey: SigningKey[]; - name: string; - version: string; - tls: { - cert: string; - key: string; - } -} - -export const routerWithConfig = new Elysia().decorate("config", {} as Config); - -export type Context = InferContext; diff --git a/packages/homeserver/src/plugins/isConfigContext.ts b/packages/homeserver/src/plugins/isConfigContext.ts deleted file mode 100644 index 5aa3a9761..000000000 --- a/packages/homeserver/src/plugins/isConfigContext.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Context } from "./config"; - -export const isConfigContext = ( - context: T, -): context is T & Context => "config" in context; diff --git a/packages/homeserver/src/plugins/isKeysContext.ts b/packages/homeserver/src/plugins/isKeysContext.ts deleted file mode 100644 index ee28a56b4..000000000 --- a/packages/homeserver/src/plugins/isKeysContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Context } from "./keys"; - -export const isKeysContext = ( - context: T, -): context is T & Context => "keys" in context; - diff --git a/packages/homeserver/src/plugins/isMongodbContext.ts b/packages/homeserver/src/plugins/isMongodbContext.ts deleted file mode 100644 index 7b72dc12a..000000000 --- a/packages/homeserver/src/plugins/isMongodbContext.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Context } from "./mongodb"; - -export const isMongodbContext = ( - context: T, -): context is T & Context => "mongo" in context; diff --git a/packages/homeserver/src/plugins/keys.ts b/packages/homeserver/src/plugins/keys.ts deleted file mode 100644 index 68494fdac..000000000 --- a/packages/homeserver/src/plugins/keys.ts +++ /dev/null @@ -1,233 +0,0 @@ -import Elysia from "elysia"; -import type { Collection, Db, FindOptions, WithoutId } from "mongodb"; - -import type { InferContext } from "@bogeychan/elysia-logger"; -import { makeRequest } from "../makeRequest"; -import { type Key } from "./mongodb"; - -import { type V2KeyQueryBody } from "@hs/core/src/query"; -import { getSignaturesFromRemote, signJson } from "../signJson"; -import type { Config } from "./config"; - -type OnlyKey = Omit, "_createdAt">; - -class KeysManager { - private readonly keysCollection!: Collection; - - private config!: Config; - - private constructor(db: Db, config: Config) { - this.keysCollection = db.collection("keys"); - this.config = config; - } - - static createPlugin(db: Db, config: Config) { - const _manager = new KeysManager(db, config); - - return { - query: _manager.query.bind(_manager), - }; - } - - storeKeys(key: Key) { - return this.keysCollection.insertOne({ ...key, _createdAt: new Date() }); - } - - getLocalKeysForServer( - serverName: string, - keyId?: string, - validUntil?: number, - opts?: FindOptions, - ) { - return this.keysCollection.find( - { - server_name: serverName, - ...(keyId && { [`verify_keys.${keyId}`]: { $exists: true } }), - ...(validUntil && { valid_until_ts: { $lt: validUntil } }), - }, - opts, - ); - } - - getLocalKeysForServerList( - serverName: string, - keyId?: string, - validUntil?: number, - opts?: FindOptions, - ) { - return this.getLocalKeysForServer( - serverName, - keyId, - validUntil, - opts, - ).toArray(); - } - - shouldRefetchKeys( - keys: (Omit & { _createdAt?: Date })[], - validUntil?: number, - ) { - return ( - keys.length === 0 || - keys.every((key) => - validUntil - ? key.valid_until_ts < validUntil - : ((key._createdAt?.getTime() || Date.now()) + key.valid_until_ts) / - 2 < - Date.now(), - ) - ); - } - - async getRemoteKeysForServer(serverName: string): Promise { - const response = await makeRequest({ - signingName: this.config.name, - method: "GET", - domain: serverName, - uri: "/_matrix/key/v2/server", - }); - - // TODO(deb): check what this does; - const [signature] = await getSignaturesFromRemote(response as any, serverName); - - if (!signature) { - throw new Error("no signature found"); - } - - return response as OnlyKey; - } - - async fetchAllkeysForServerName( - serverName: string, - keyId?: string, - validUntil: number = Date.now(), - ): Promise { - const keys = await this.getLocalKeysForServerList(serverName, keyId); - - console.log({ msg: "cached keys", serverName, value: keys }); - - if (!this.shouldRefetchKeys(keys, validUntil)) { - console.log({ msg: "cache validated", serverName, value: keys }); - return keys; - } - - console.log({ msg: "cache invalidated", serverName }); - - const remoteKey = await (async () => { - try { - console.log({ msg: 'fetching from remote', serverName }); - const remoteKey = await this.getRemoteKeysForServer(serverName); - return remoteKey; - } catch (err) { - console.log({ - msg: "failed to fetch remote keys", - serverName, - value: err, - }); - } - })(); - - if (remoteKey) { - console.log({ msg: "remote key", serverName, value: remoteKey }); - - void this.storeKeys(remoteKey as Key); - } else { - return keys; - } - - // if (!this.shouldRefetchKeys([remoteKey], validUntil)) { - // return []; // expired even from remote server? likely for a custom minimum_valid_until_ts criteria. ok to return nothing. - // } - - if (keyId) { - let found = false; - - const keys = Object.keys(remoteKey.verify_keys).reduce( - (accum, key) => { - if (key === keyId) { - found = true; - accum[key] = remoteKey.verify_keys[key]; - } - - return accum; - }, - {} as Key["verify_keys"], - ); - - remoteKey.verify_keys = keys; - - console.log({ - msg: "after filter remote key", - serverName, - value: remoteKey, - }); - - if (!found) { - return []; - } - } - - return [remoteKey as Key]; - } - - async signNotaryResponseKey(keys: Key) { - const { signatures, _id, _createdAt, ...all } = keys; - - const signed = await signJson( - all, - this.config.signingKey[0], - this.config.name, - ); - - return { - ...signed, - signatures: { - ...signed.signatures, - ...signatures, - }, - }; - } - - async query(request: V2KeyQueryBody) { - const servers = Object.entries(request.server_keys); - - const response: { server_keys: Key[] } = { server_keys: [] }; - - if (servers.length === 0) { - return response; - } - - for (const [serverName, _query] of servers) { - const keys = Object.entries(_query); - if (keys.length === 0) { - // didn't ask for any specific keys - const keys = await this.fetchAllkeysForServerName(serverName); - response.server_keys = response.server_keys.concat(keys); - continue; - } - - for (const [ - keyId, - { minimum_valid_until_ts: minimumValidUntilTs }, - ] of keys) { - const keys = await this.fetchAllkeysForServerName( - serverName, - keyId === 'undefined' ? undefined : keyId, - minimumValidUntilTs, - ); - response.server_keys = response.server_keys.concat(keys); - } - } - - return { - server_keys: await Promise.all( - response.server_keys.map(this.signNotaryResponseKey.bind(this)), - ), - }; - } -} - -export const routerWithKeyManager = (db: Db, config: Config) => - new Elysia().decorate("keys", KeysManager.createPlugin(db, config)); - -export type Context = InferContext>; diff --git a/packages/homeserver/src/plugins/mongodb.ts b/packages/homeserver/src/plugins/mongodb.ts deleted file mode 100644 index d2e100207..000000000 --- a/packages/homeserver/src/plugins/mongodb.ts +++ /dev/null @@ -1,257 +0,0 @@ -import type { InferContext } from "elysia"; -import Elysia from "elysia"; -import type { Db, WithId } from "mongodb"; - -import type { EventBase } from "@hs/core/src/events/eventBase"; -import type { ServerKey } from "@hs/core/src/server"; -import { generateId } from "../authentication"; - -export type Key = WithId & { _createdAt: Date }; - -interface Room { - _id: string; - state: EventBase[]; -} - -export const routerWithMongodb = (db: Db) => - new Elysia().decorate( - "mongo", - (() => { - const eventsCollection = db.collection("events"); - const keysCollection = db.collection("keys"); - const roomsCollection = db.collection("rooms"); - - const getLastEvent = async (roomId: string) => { - return eventsCollection.findOne( - { "event.room_id": roomId }, - { sort: { "event.depth": -1 } }, - ); - }; - - const upsertRoom = async (roomId: string, state: EventBase[]) => { - await roomsCollection.findOneAndUpdate( - { _id: roomId }, - { - $set: { - _id: roomId, - state, - }, - }, - { upsert: true }, - ); - }; - - const getEventsByRoomAndEventIds = async (roomId: string, eventIds: string[]) => { - return eventsCollection - .find({ "event.room_id": roomId, "event._id": { $in: eventIds } }) - .toArray(); - }; - - const getEventById = async (eventId: string) => { - return eventsCollection.findOne({ _id: eventId }); - }; - - const getEventsByIds = async (eventIds: string[]) => { - return eventsCollection.find({ _id: { $in: eventIds } }).toArray(); - }; - - const getDeepEarliestAndLatestEvents = async ( - roomId: string, - earliest_events: string[], - latest_events: string[], - ) => { - const depths = await eventsCollection - .find( - { - _id: { $in: [...earliest_events, ...latest_events] }, - "event.room_id": roomId, - }, - { projection: { "event.depth": 1 } }, - ) - .toArray() - .then((events) => events.map((event) => event.event.depth)); - - if (depths.length === 0) { - return []; - } - - const minDepth = Math.min(...depths); - const maxDepth = Math.max(...depths); - - return [minDepth, maxDepth]; - }; - - const getMissingEventsByDeep = async ( - roomId: string, - minDepth: number, - maxDepth: number, - limit: number, - ) => { - const events = await eventsCollection - .find( - { - "event.room_id": roomId, - "event.depth": { $gte: minDepth, $lte: maxDepth }, - }, - { limit: limit, sort: { "event.depth": 1 } }, - ) - .map((event) => event.event) - .toArray(); - - return events; - }; - - const getAuthEvents = async (roomId: string) => { - return eventsCollection - .find( - { - "event.room_id": roomId, - $or: [ - { - "event.type": { - $in: [ - "m.room.create", - "m.room.power_levels", - "m.room.join_rules", - ], - }, - }, - { - // Lots of room members, when including the join ones it fails the auth check - "event.type": "m.room.member", - "event.content.membership": "invite", - }, - ], - }, - { - projection: { - _id: 1, - }, - }, - ) - .toArray(); - }; - - const getRoomVersion = async (roomId: string) => { - const createRoomEvent = await eventsCollection.findOne({ "event.room_id": roomId, "event.type": "m.room.create" }, { projection: { "event.content.room_version": 1 } }); - return (createRoomEvent?.event.content as any)?.room_version ?? null; - }; - - const getValidPublicKeyFromLocal = async ( - origin: string, - key: string, - ): Promise => { - const server = await keysCollection.findOne({ - name: origin, - }); - if (!server) { - return; - } - const [, publicKey] = - Object.entries((server as any).keys).find( - ([protocolAndVersion, value]) => - protocolAndVersion === key && (value as any).validUntil > Date.now(), - ) ?? []; - return (publicKey as any)?.key; - }; - - const storePublicKey = async ( - origin: string, - key: string, - value: string, - validUntil: number, - ) => { - await keysCollection.findOneAndUpdate( - { name: origin }, - { - $set: { - keys: { - [key]: { - key: value, - validUntil, - }, - }, - }, - }, - { upsert: true }, - ); - }; - - const createStagingEvent = async (event: EventBase) => { - const id = generateId(event); - await eventsCollection.insertOne({ - _id: id, - event, - staged: true, - }); - - return id; - }; - - const createEvent = async (event: EventBase) => { - const id = generateId(event); - await eventsCollection.insertOne({ - _id: id, - event, - }); - return id; - }; - - const upsertEvent = async (event: EventBase) => { - const id = generateId(event); - await eventsCollection.updateOne( - { _id: id }, - { $set: { _id: id, event } }, - { upsert: true } - ); - - return id; - }; - - const removeEventFromStaged = async (roomId: string, id: string) => { - await eventsCollection.updateOne( - { _id: id, "event.room_id": roomId }, - { $unset: { staged: 1 } }, - ); - }; - - const getOldestStagedEvent = async (roomId: string) => { - return eventsCollection.findOne( - { staged: true, "event.room_id": roomId }, - { sort: { "event.origin_server_ts": 1 } }, - ); - }; - - return { - serversCollection: keysCollection, - getValidPublicKeyFromLocal, - storePublicKey, - - eventsCollection, - getDeepEarliestAndLatestEvents, - getMissingEventsByDeep, - getLastEvent, - getAuthEvents, - getRoomVersion, - getEventById, - getEventsByIds, - - removeEventFromStaged, - getEventsByRoomAndEventIds, - getOldestStagedEvent, - createStagingEvent, - createEvent, - upsertEvent, - upsertRoom, - }; - })(), - ); - -export type Context = InferContext>; - -export type EventStore = { - _id: string; - event: EventBase; - staged?: true; - outlier?: true; -}; diff --git a/packages/homeserver/src/plugins/mutex.ts b/packages/homeserver/src/plugins/mutex.ts deleted file mode 100644 index f89c9f17f..000000000 --- a/packages/homeserver/src/plugins/mutex.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Elysia, { type InferContext } from "elysia"; -import { Mutex } from "../mutex/Mutex"; - -export const routerWithMutex = new Elysia().decorate("mutex", new Mutex()); - -export const isMutexContext = ( - context: T, -): context is T & Context => "mutex" in context; - -type Context = InferContext; diff --git a/packages/homeserver/src/plugins/validateHeaderSignature.spec.ts b/packages/homeserver/src/plugins/validateHeaderSignature.spec.ts deleted file mode 100644 index c0313ba4c..000000000 --- a/packages/homeserver/src/plugins/validateHeaderSignature.spec.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { clearMocks, mock } from "bun-bagel"; -import { afterEach, beforeAll, describe, expect, it } from "bun:test"; - -import Elysia from "elysia"; -import { authorizationHeaders } from "../authentication"; -import { toUnpaddedBase64 } from "../binaryData"; -import { type SigningKey, generateKeyPairsFromString } from "../keys"; -import { signJson } from "../signJson"; -import { validateHeaderSignature } from "./validateHeaderSignature"; - -describe("validateHeaderSignature getting public key from local", () => { - let app: Elysia; - let signature: SigningKey; - - beforeAll(async () => { - signature = await generateKeyPairsFromString( - "ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8", - ); - - app = new Elysia() - .decorate("config", { - path: "./config.json", - signingKeyPath: "./keys/ed25519.signing.key", - port: 8080, - signingKey: [ - await generateKeyPairsFromString( - "ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U", - ), - ], - name: "synapse2", - version: "org.matrix.msc3757.10", - }) - .decorate("mongo", { - getValidPublicKeyFromLocal: async () => { - return toUnpaddedBase64(signature.publicKey); - }, - storePublicKey: async () => { - return; - }, - eventsCollection: { - findOne: async () => { - return; - }, - findOneAndUpdate: async () => { - return; - }, - }, - serversCollection: { - findOne: async () => { - return; - }, - } as any, - }) - .onBeforeHandle(validateHeaderSignature) - .get("/", () => "") - .post("/", () => ""); - }); - - it("Should reject if no authorization header", async () => { - const resp = await app.handle(new Request("http://localhost/")); - expect(resp.status).toBe(401); - }); - it("Should reject if invalid authorization header is provided", async () => { - const resp = await app.handle( - new Request("http://localhost/", { - headers: { - authorization: "Bearer invalid", - }, - }), - ); - expect(resp.status).toBe(401); - }); - - it("Should reject if the origin is not the same as the config.name", async () => { - const resp = await app.handle( - new Request("http://localhost/", { - headers: { - authorization: "Bearer invalid", - }, - }), - ); - expect(resp.status).toBe(401); - }); - - it("Should pass if authorization header is valid with no body", async () => { - const authorizationHeader = await authorizationHeaders( - "synapse1", - signature, - "synapse2", - "GET", - "/", - ); - - const resp = await app.handle( - new Request("http://localhost/", { - headers: { - authorization: authorizationHeader, - }, - }), - ); - expect(resp.status).toBe(200); - }); - - it("Should pass if authorization header is valid with body", async () => { - const authorizationHeader = await authorizationHeaders( - "synapse1", - signature, - "synapse2", - "POST", - "/", - { - test: 1, - }, - ); - - const resp = await app.handle( - new Request("http://localhost/", { - headers: { - authorization: authorizationHeader, - "content-type": "application/json", - }, - body: JSON.stringify({ - test: 1, - }), - method: "POST", - }), - ); - console.log("RESP->", await resp.text()); - expect(resp.status).toBe(200); - }); - - it("Should reject if the body is different from the signature", async () => { - const authorizationHeader = await authorizationHeaders( - "synapse1", - signature, - "synapse2", - "POST", - "/", - { - test: 1, - }, - ); - - const resp = await app.handle( - new Request("http://localhost/", { - headers: { - authorization: authorizationHeader, - "content-type": "application/json", - }, - body: JSON.stringify({ - test: 2, - }), - method: "POST", - }), - ); - expect(resp.status).toBe(401); - }); -}); - -describe("validateHeaderSignature getting public key from remote", () => { - let app: Elysia; - let signature: SigningKey; - afterEach(() => { - clearMocks(); - }); - - beforeAll(async () => { - signature = await generateKeyPairsFromString( - "ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8", - ); - - app = new Elysia() - .decorate("config", { - path: "./config.json", - signingKeyPath: "./keys/ed25519.signing.key", - port: 8080, - signingKey: [ - await generateKeyPairsFromString( - "ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U", - ), - ], - name: "synapse2", - version: "org.matrix.msc3757.10", - }) - .decorate("mongo", { - getValidPublicKeyFromLocal: async () => { - return; - }, - storePublicKey: async () => { - return; - }, - eventsCollection: { - findOne: async () => { - return; - }, - findOneAndUpdate: async () => { - return; - }, - }, - serversCollection: { - findOne: async () => { - return { - name: "synapse1", - }; - }, - } as any, - }) - .onBeforeHandle(validateHeaderSignature) - .get("/", () => "") - .post("/", () => ""); - }); - - // TODO: Rewrite this test to call a route that can validate the signature - it.skip("Should pass if authorization header is valid with no body synapse2 requesting from synapse1", async () => { - const result = await signJson( - { - old_verify_keys: {}, - server_name: "synapse1", - valid_until_ts: new Date().getTime() + 1000, - verify_keys: { - "ed25519:a_yNbw": { - key: toUnpaddedBase64(signature.publicKey), - }, - }, - }, - signature, - "synapse1", - ); - - mock("https://synapse1/_matrix/key/v2/server", { data: result }); - - const authorizationHeader = await authorizationHeaders( - "synapse1", - signature, - "synapse2", - "GET", - "/", - ); - - const resp = await app.handle( - new Request("http://localhost/", { - headers: { - authorization: authorizationHeader, - }, - }), - ); - - expect(resp.status).toBe(200); - }); - - it("Should reject if authorization header is expired requesting from synapse1 (synapse2 delivered an already expired key)", async () => { - const result = await signJson( - { - old_verify_keys: {}, - server_name: "synapse1", - valid_until_ts: new Date().getTime() - 1000, - verify_keys: { - "ed25519:a_yNbw": { - key: toUnpaddedBase64(signature.publicKey), - }, - }, - }, - signature, - "synapse1", - ); - - mock("https://synapse1:8448/_matrix/key/v2/server", { data: result }); - - const authorizationHeader = await authorizationHeaders( - "synapse1", - signature, - "synapse2", - "GET", - "/", - ); - - const resp = await app.handle( - new Request("http://localhost/", { - headers: { - authorization: authorizationHeader, - }, - }), - ); - - expect(resp.status).toBe(401); - }); -}); diff --git a/packages/homeserver/src/plugins/validateHeaderSignature.ts b/packages/homeserver/src/plugins/validateHeaderSignature.ts deleted file mode 100644 index 65c7ea2ea..000000000 --- a/packages/homeserver/src/plugins/validateHeaderSignature.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Context } from "elysia"; -import { - extractSignaturesFromHeader, - validateAuthorizationHeader, -} from "../authentication"; -import { - getSignaturesFromRemote, - isValidAlgorithm, - verifyJsonSignature, -} from "../signJson"; -import { isConfigContext } from "./isConfigContext"; -import { isMongodbContext } from "./isMongodbContext"; -import { - getPublicKeyFromRemoteServer, - makeGetPublicKeyFromServerProcedure, -} from "../procedures/getPublicKeyFromServer"; -import { makeRequest } from "../makeRequest"; -import { ForbiddenError, UnknownTokenError } from "../errors"; -import { extractURIfromURL } from "../helpers/url"; - -export interface OriginOptions { - /** - * If the API doesn't compliant with RFC6750 - * The key for extracting the token is configurable - */ - extract: { - /** - * Determined which fields to be identified as Bearer token - * - * @default access_token - */ - body?: string; - /** - * Determined which fields to be identified as Bearer token - * - * @default access_token - */ - query?: string; - /** - * Determined which type of Authentication should be Bearer token - * - * @default Bearer - */ - header?: string; - }; -} - -export const validateHeaderSignature = async ({ - headers: { authorization }, - request, - body, - ...context -}: Context) => { - if (!isConfigContext(context)) { - throw new Error("No config context"); - } - if (!isMongodbContext(context)) { - throw new Error("No mongodb context"); - } - if (!authorization) { - throw new UnknownTokenError("No authorization header"); - } - - try { - const origin = extractSignaturesFromHeader(authorization); - // TODO: not sure if we should throw an error if the origin is not the same as the config.name - // or if we should just act as a proxy - if (origin.destination !== context.config.name) { - throw new Error("Invalid destination"); - } - - const getPublicKeyFromServer = makeGetPublicKeyFromServerProcedure( - context.mongo.getValidPublicKeyFromLocal, - () => - getPublicKeyFromRemoteServer( - origin.origin, - origin.destination, - origin.key, - ), - - context.mongo.storePublicKey, - ); - - const publickey = await getPublicKeyFromServer(origin.origin, origin.key); - const url = new URL(request.url); - if ( - !(await validateAuthorizationHeader( - origin.origin, - publickey, - origin.destination, - request.method, - extractURIfromURL(url), - origin.signature, - body as any, - )) - ) { - throw new Error("Invalid signature"); - } - // return { - // get origin() { - // return origin; - // }, - // }; - } catch (error) { - if (error instanceof ForbiddenError) { - throw error; - } - console.log("ERROR->", error); - if (error instanceof Error) { - throw new UnknownTokenError(error.message); - } - } -}; - -export default validateHeaderSignature; diff --git a/packages/homeserver/src/procedures/makeJoin.ts b/packages/homeserver/src/procedures/makeJoin.ts index 4ad539091..a1931307b 100644 --- a/packages/homeserver/src/procedures/makeJoin.ts +++ b/packages/homeserver/src/procedures/makeJoin.ts @@ -1,6 +1,6 @@ import { roomMemberEvent } from "@hs/core/src/events/m.room.member"; import { IncompatibleRoomVersionError, NotFoundError } from "../errors"; -import type { EventStore } from "../plugins/mongodb"; +import type { EventStore } from "../models/event.model"; // "method":"GET", // "url":"http://rc1:443/_matrix/federation/v1/make_join/%21kwkcWPpOXEJvlcollu%3Arc1/%40admin%3Ahs1?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=org.matrix.msc3757.10&ver=org.matrix.msc3757.11", @@ -45,7 +45,7 @@ export const makeJoinEventBuilder = join_rules: authEventsMap.get("m.room.join_rules")!._id, }, prev_events: [lastEvent._id], - depth: lastEvent.event.depth + 1, + depth: (lastEvent.event.depth ?? 0) + 1, origin, ts: Date.now(), }); diff --git a/packages/homeserver/src/procedures/processPDU.ts b/packages/homeserver/src/procedures/processPDU.ts deleted file mode 100644 index e5929feb1..000000000 --- a/packages/homeserver/src/procedures/processPDU.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { EventBase } from "@hs/core/src/events/eventBase"; -import type { HashedEvent } from "../authentication"; -import type { EventStore } from "../plugins/mongodb"; -import type { SignedJson } from "../signJson"; - -export const processPDUsByRoomId = async ( - roomId: string, - pdus: SignedJson>[], - validatePdu: (pdu: SignedJson>) => Promise, - getEventsByRoomAndEventIds: (roomId: string, eventIds: string[]) => Promise, - createStagingEvent: (event: EventBase) => Promise, - createEvent: (event: EventBase) => Promise, - processMissingEvents: (roomId: string) => Promise, - generateId: (pdu: SignedJson>) => string, -) => { - const resultPDUs = {} as { - [key: string]: Record; - }; - for (const pdu of pdus) { - const pid = generateId(pdu); - try { - await validatePdu(pdu); - resultPDUs[pid] = {}; - - const events = await getEventsByRoomAndEventIds(roomId, pdu.prev_events); - - const missing = pdu.prev_events.filter( - (event) => !events.find((e) => e._id === event), - ); - - if (!missing.length) { - await createStagingEvent(pdu); - } else { - await createEvent(pdu); - } - } catch (error) { - resultPDUs[pid] = { error } as any; - } - void (async () => { - while (await processMissingEvents(roomId)); - })(); - } - - return { - pdus: resultPDUs, - }; -}; diff --git a/packages/homeserver/src/services/event.service.ts b/packages/homeserver/src/services/event.service.ts index 1e4eff0cc..f03aeec31 100644 --- a/packages/homeserver/src/services/event.service.ts +++ b/packages/homeserver/src/services/event.service.ts @@ -14,7 +14,7 @@ import { KeyRepository } from "../repositories/key.repository"; import { RoomRepository } from "../repositories/room.repository"; import { signEvent } from "../signEvent"; import { checkSignAndHashes } from "../utils/checkSignAndHashes"; -import { eventSchemas } from "../validation/schemas/event-schemas"; +import { eventSchemas } from "../utils/event-schemas"; import { ConfigService } from "./config.service"; type ValidationResult = { diff --git a/packages/homeserver/src/services/profiles.service.ts b/packages/homeserver/src/services/profiles.service.ts index 0e20990c6..011239f61 100644 --- a/packages/homeserver/src/services/profiles.service.ts +++ b/packages/homeserver/src/services/profiles.service.ts @@ -1,12 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; +import type { EventStore as MongoEventStore } from '../models/event.model'; import { makeJoinEventBuilder } from '../procedures/makeJoin'; import { ConfigService } from './config.service'; import { EventService } from './event.service'; import { RoomService } from './room.service'; -// Import EventStore from plugins/mongodb for type compatibility with makeJoinEventBuilder -import type { EventStore as MongoEventStore } from '../plugins/mongodb'; - @Injectable() export class ProfilesService { private readonly logger = new Logger(ProfilesService.name); @@ -99,4 +97,4 @@ export class ProfilesService { auth_chain: [], }; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/packages/homeserver/src/utils/README.md b/packages/homeserver/src/utils/README.md deleted file mode 100644 index c9676fef8..000000000 --- a/packages/homeserver/src/utils/README.md +++ /dev/null @@ -1,25 +0,0 @@ -```mermaid -flowchart TD - A[POST /_matrix/federation/v1/send/txn] --> B[Receive Event] - - B --> C[Canonicalize Event] - - C --> D1[Verify SHA-256 Hash] - C --> D2[Verify Ed25519 Signature] - B --> E[Fetch Auth Events] - - D1 --> F[Wait for Hash and Signature] - D2 --> F - - E --> G[Validate Auth Event Signatures and Hashes] - G --> H[Apply Room Auth Rules] - - F --> I{Valid Event and Auth Chain?} - H --> I - - I -->|Yes| J[Persist Event and Update State] - I -->|No| K[Reject Event] - - J --> L[Send Success Response] - K --> M[Send Error Response] -``` \ No newline at end of file diff --git a/packages/homeserver/src/validation/schemas/event-schemas.ts b/packages/homeserver/src/utils/event-schemas.ts similarity index 100% rename from packages/homeserver/src/validation/schemas/event-schemas.ts rename to packages/homeserver/src/utils/event-schemas.ts diff --git a/packages/homeserver/src/utils/extractOrigin.ts b/packages/homeserver/src/utils/extractOrigin.ts deleted file mode 100644 index a66222467..000000000 --- a/packages/homeserver/src/utils/extractOrigin.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const extractOrigin = (matrixId: string): string => { - return matrixId.split(':').pop() as string; -}; \ No newline at end of file diff --git a/packages/homeserver/src/utils/sendTransactionDTO.ts b/packages/homeserver/src/utils/sendTransactionDTO.ts deleted file mode 100644 index 809a780f8..000000000 --- a/packages/homeserver/src/utils/sendTransactionDTO.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { t } from "elysia"; - -export const SendTransactionBodyDTO = t.Object({ - pdus: t.Array(t.Any(), { - description: "Protocol Data Units (PDUs) for the transaction" - }), - edus: t.Array(t.Any(), { - description: "Ephemeral Data Units (EDUs) for the transaction" - }) -}, { - description: "Transaction data for federation requests" -}); - -export const SendTransactionParamsDTO = t.Object({ - txnId: t.String({ - description: "The transaction ID" - }) -}, { - description: "Transaction data for federation requests" -}); - -export const SendTransactionResponseDTO = { - 200: t.Object({ - pdus: t.Optional(t.Record(t.String(), t.Object({ - error: t.Optional(t.String()), - }))), - }, { - description: "Successful transaction processing response" - }), - - 400: t.Object({ - errcode: t.String({ - description: "Matrix error code", - examples: ["M_UNKNOWN", "M_INVALID"] - }), - error: t.String({ - description: "Human-readable error message" - }) - }, { - description: "Error response when request cannot be processed" - }) -}; - -export const SendTransactionDTO = { - body: SendTransactionBodyDTO, - params: SendTransactionParamsDTO, - response: SendTransactionResponseDTO, -}; \ No newline at end of file diff --git a/packages/homeserver/src/utils/sendTransactionV2.ts b/packages/homeserver/src/utils/sendTransactionV2.ts deleted file mode 100644 index b31561e6f..000000000 --- a/packages/homeserver/src/utils/sendTransactionV2.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Elysia } from "elysia"; -import { generateId } from "../authentication"; -import { validateMatrixEvent } from "../validation/EventValidationPipeline"; -import type { Event as MatrixEvent } from "../validation/validators/EventValidators"; - -async function processPDU(pdu: MatrixEvent["event"], pduResults: Record, txnId: string) { - const eventId = generateId(pdu); - - try { - const result = await validateMatrixEvent(pdu, txnId, eventId); - if (!result.success && result.error) { - pduResults[eventId] = { - error: `${result.error.code}: ${result.error.message}` - }; - console.error(`Validation failed for PDU ${eventId}: ${result.error.message}`); - } else { - console.debug(`Successfully validated PDU ${eventId}`); - // TODO: Persist the event on LRU cache and database - // TODO: Make this as part of the validation pipeline - } - } catch (error) { - const errorMessage = error instanceof Error - ? error.message - : String(error); - pduResults[eventId] = { error: errorMessage }; - console.error(`Error processing PDU: ${errorMessage}`); - } - - return pduResults; -} - -async function processPDUs(pdus: MatrixEvent["event"][], txnId: string): Promise> { - if (pdus.length === 0) { - console.debug("No PDUs to process"); - return {}; - } - - const pduResults: Record = {}; - await Promise.all(pdus.map(pdu => processPDU(pdu, pduResults, txnId))); - - return pduResults; -} - -export const sendTransactionRoute = new Elysia() - .put("/send/:txnId", async ({ params, body, set }) => { - const { txnId } = params; - const { pdus = [], edus = [] } = body as { pdus?: MatrixEvent["event"][], edus?: any[] }; - - const pduResults = await processPDUs(pdus, txnId); - console.debug(`PDU results: ${JSON.stringify(pduResults)}`); - - set.status = 200; - return { - pdus: pduResults, - } - // }, SendTransactionDTO); - }); \ No newline at end of file diff --git a/packages/homeserver/src/validation/EventValidationPipeline.ts b/packages/homeserver/src/validation/EventValidationPipeline.ts deleted file mode 100644 index 46672dcd9..000000000 --- a/packages/homeserver/src/validation/EventValidationPipeline.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ValidationResult } from './ValidationResult'; -import { ParallelValidation, Pipeline } from './Validator'; -import type { - AuthorizedEvent, - CanonicalizedEvent, - Event, -} from './validators/EventValidators'; -import { - canonicalizeEvent, - fetchAuthEvents, - validateEventHash -} from './validators/event'; - -/** - * Creates a complete validation pipeline for Matrix federation events - * following the flowchart: - * - * 1. Canonicalize the event - * 2. In parallel: - * a. Validate hash and signature - * b. Fetch auth events - * 3. Validate auth event chain - * 4. Validate room auth rules - * 5. Event is ready for persistence - */ -export function createEventValidationPipeline() { - const hashAndSignatureValidation = new ParallelValidation() - .add(validateEventHash) - // .add(validateEventSignature); - - return new Pipeline() - .add(canonicalizeEvent) - .add(hashAndSignatureValidation) - .add(fetchAuthEvents) - // .add(validateAuthChain) - // .add(validateRoomRules); -} - -/** - * Validates a Matrix event - * - * @param event The event to validate - * @param txnId The transaction ID - * @param eventId The event ID - * @returns A validation result - */ -export async function validateMatrixEvent( - eventData: any, - txnId: string, - eventId: string -): Promise> { - const pipeline = createEventValidationPipeline(); - - const event: Event = { - event: eventData - }; - - const result = await pipeline.validate(event, txnId, eventId); - - if (result.success) { - console.debug(`Validation success for event ${eventId}`); - } else { - console.warn(`Validation failed for event ${eventId}: ${result.error?.message}`); - } - - return result; -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/ValidationResult.ts b/packages/homeserver/src/validation/ValidationResult.ts deleted file mode 100644 index 03a19a5bf..000000000 --- a/packages/homeserver/src/validation/ValidationResult.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface ValidationResult { - success: boolean; - event?: T; - error?: { - code: string; - message: string; - }; -} - -export function success(event: T): ValidationResult { - return { success: true, event }; -} - -export function failure(code: string, message: string): ValidationResult { - return { success: false, error: { code, message } }; -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/Validator.ts b/packages/homeserver/src/validation/Validator.ts deleted file mode 100644 index 02435fc9e..000000000 --- a/packages/homeserver/src/validation/Validator.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { ValidationResult } from './ValidationResult'; - -/** - * Base interface for all event validators - */ -export interface Validator { - validate(event: T, txnId: string, eventId: string): Promise>; -} - -/** - * Sequential validator that runs multiple validators in order - */ -export class Pipeline implements Validator { - private steps: Validator[] = []; - - constructor(steps: Validator[] = []) { - this.steps = steps; - } - - add(validator: Validator): this { - this.steps.push(validator); - return this; - } - - async validate(event: T, txnId: string, eventId: string): Promise { - let currentEvent = event; - - for (const validator of this.steps) { - const result = await validator.validate(currentEvent, txnId, eventId); - if (!result.success) { - return result; - } - currentEvent = result.event; - } - - return { success: true, event: currentEvent }; - } -} - -/** - * Runs multiple validators in parallel and combines their results - */ -export class ParallelValidation implements Validator { - private validators: Validator[] = []; - - constructor(validators: Validator[] = []) { - this.validators = validators; - } - - add(validator: Validator): this { - this.validators.push(validator); - return this; - } - - async validate(event: T, txnId: string, eventId: string): Promise> { - const results = await Promise.all( - this.validators.map(validator => validator.validate(event, txnId, eventId)) - ); - - const failure = results.find(result => !result.success); - if (failure) { - return failure; - } - - return { success: true, event }; - } -} - -/** - * Utility to create a functional validator from a function - */ -export function createValidator( - fn: (this: any, event: T, txnId: string, eventId: string) => Promise> -): Validator { - const validator = { - validate: fn, - }; - - validator.validate = validator.validate.bind(validator); - - return validator; -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/decorators/pipeline.decorator.ts b/packages/homeserver/src/validation/decorators/pipeline.decorator.ts deleted file mode 100644 index 3606720fa..000000000 --- a/packages/homeserver/src/validation/decorators/pipeline.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function Pipeline() { - return any>(target: T): T => { - let instance: any; - - const SingletonClass = class extends target { - constructor(...args: any[]) { - if (instance) { - super(...args); - Object.assign(this, instance); - } else { - super(...args); - instance = this; - } - } - }; - - return SingletonClass as T; - }; -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/decorators/validator.decorator.ts b/packages/homeserver/src/validation/decorators/validator.decorator.ts deleted file mode 100644 index e70548557..000000000 --- a/packages/homeserver/src/validation/decorators/validator.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function Validator() { - return any>(target: T): T => { - let instance: any; - - const SingletonClass = class extends target { - constructor(...args: any[]) { - if (instance) { - super(...args); - Object.assign(this, instance); - } else { - super(...args); - instance = this; - } - } - }; - - return SingletonClass as T; - }; -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/errors/drop-event.error.ts b/packages/homeserver/src/validation/errors/drop-event.error.ts deleted file mode 100644 index 9c78f232a..000000000 --- a/packages/homeserver/src/validation/errors/drop-event.error.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PipelineBaseError } from "./pipeline-base.error"; - -/** - * The event is fundamentally broken and should not be stored, not propagated, and not processed at all. - * Should be ignored completely — like it never arrived. - */ -export class DropEventError extends PipelineBaseError {} diff --git a/packages/homeserver/src/validation/errors/pipeline-base.error.ts b/packages/homeserver/src/validation/errors/pipeline-base.error.ts deleted file mode 100644 index 7f191c785..000000000 --- a/packages/homeserver/src/validation/errors/pipeline-base.error.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class PipelineBaseError extends Error { - constructor(message: string) { - super(message); - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/errors/redact.error.ts b/packages/homeserver/src/validation/errors/redact.error.ts deleted file mode 100644 index 507848add..000000000 --- a/packages/homeserver/src/validation/errors/redact.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PipelineBaseError } from "./pipeline-base.error"; - -/** - * The event fails hash validation, meaning its content may be tampered with. - * Should redact the event (keep only allowed fields like type, room_id, sender, etc.), but still process it. - */ -export class RedactEventError extends PipelineBaseError { - constructor(message: string) { - super(message); - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/errors/reject.error.ts b/packages/homeserver/src/validation/errors/reject.error.ts deleted file mode 100644 index b39001bc5..000000000 --- a/packages/homeserver/src/validation/errors/reject.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PipelineBaseError } from "./pipeline-base.error"; - -/** - * The event is well-formed, but it's not allowed according to Matrix rules (authorization failure based on auth_events, prev_events, etc.). - * Do not apply the event to the room, but store it as rejected — and return an error in the /send response. - */ -export class RejectEventError extends PipelineBaseError { - constructor(message: string) { - super(message); - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/errors/soft-fail.error.ts b/packages/homeserver/src/validation/errors/soft-fail.error.ts deleted file mode 100644 index 5518e431b..000000000 --- a/packages/homeserver/src/validation/errors/soft-fail.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PipelineBaseError } from "./pipeline-base.error"; - -/** - * The event is well-formed, but it's not allowed according to Matrix rules (authorization failure based on auth_events, prev_events, etc.). - * Store the event in the DAG, but do not advance the current room state with it. - */ -export class SoftFailEventError extends PipelineBaseError { - constructor(message: string) { - super(message); - } -} diff --git a/packages/homeserver/src/validation/events/index.ts b/packages/homeserver/src/validation/events/index.ts deleted file mode 100644 index aca09b08c..000000000 --- a/packages/homeserver/src/validation/events/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ValidationResult } from '../ValidationResult'; -import { success } from '../ValidationResult'; -import type { AuthorizedEvent } from '../validators/EventValidators'; - -const eventValidators: Record Promise> = {}; - -export function registerEventHandler( - eventType: string, - handler: (event: AuthorizedEvent, eventId: string) => Promise -): void { - eventValidators[eventType] = handler; - console.info(`Registered validator for ${eventType}`); -} - -export async function validateEventByType( - event: AuthorizedEvent, - eventId: string -): Promise { - const eventType = event.event.type; - - if (!eventValidators[eventType]) { - console.debug(`No specific validator registered for event type ${eventType}, using default validation`); - return success(event); - } - - console.debug(`Dispatching ${eventType} event ${eventId} to specific validator`); - return eventValidators[eventType](event, eventId); -} - -export const eventTypeValidator = validateEventByType; diff --git a/packages/homeserver/src/validation/events/m.room.create.ts b/packages/homeserver/src/validation/events/m.room.create.ts deleted file mode 100644 index cd8d5b385..000000000 --- a/packages/homeserver/src/validation/events/m.room.create.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { failure, success, type ValidationResult } from '../ValidationResult'; -import type { AuthorizedEvent } from '../validators/EventValidators'; -import { registerEventHandler } from './index'; - -export async function validateCreateEvent( - event: AuthorizedEvent, - eventId: string -): Promise { - try { - const { event: rawEvent } = event; - - // Create events must have state_key = "" - if (rawEvent.state_key !== '') { - console.error(`Create event ${eventId} has invalid state_key: '${rawEvent.state_key}'`); - return failure('M_INVALID_PARAM', 'Create events must have an empty state_key'); - } - - // Create events must be the first event in the room - no auth events - const authEvents = event.authorizedEvent.auth_event_objects || []; - if (authEvents.length > 0) { - console.error(`Create event ${eventId} has ${authEvents.length} auth events which is not allowed`); - return failure('M_FORBIDDEN', 'Create events must not have auth events'); - } - - // Basic content validation - const content = rawEvent.content || {}; - - // creator is required - if (!content.creator) { - console.error(`Create event ${eventId} is missing required creator field`); - return failure('M_MISSING_PARAM', 'Create events must specify a creator'); - } - - // creator must match sender - if (content.creator !== rawEvent.sender) { - console.error(`Create event ${eventId} has creator (${content.creator}) that doesn't match sender (${rawEvent.sender})`); - return failure('M_INVALID_PARAM', 'Create event creator must match sender'); - } - - // room_version is required (or defaults to "1") - if (content.room_version && typeof content.room_version !== 'string') { - console.error(`Create event ${eventId} has invalid room_version: ${content.room_version}`); - return failure('M_INVALID_PARAM', 'room_version must be a string'); - } - - // Validate room version is supported - const roomVersion = content.room_version || '1'; - const supportedVersions = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; - if (!supportedVersions.includes(roomVersion)) { - console.warn(`Create event ${eventId} uses unsupported room version: ${roomVersion}`); - // We don't fail on this because we might want to still process these events - // but we log a warning - } - - // Validate predefined_state_events if present - if (content.predefined_state_events) { - if (!Array.isArray(content.predefined_state_events)) { - console.error(`Create event ${eventId} has invalid predefined_state_events (not an array)`); - return failure('M_INVALID_PARAM', 'predefined_state_events must be an array'); - } - - // Check each predefined state event - for (const stateEvent of content.predefined_state_events) { - if (!stateEvent.type || !stateEvent.state_key || !stateEvent.content) { - console.error(`Create event ${eventId} has invalid predefined state event: missing required fields`); - return failure('M_INVALID_PARAM', 'Predefined state events must have type, state_key and content'); - } - } - } - - // Validate predecessor if present - if (content.predecessor) { - if (typeof content.predecessor !== 'object') { - console.error(`Create event ${eventId} has invalid predecessor (not an object)`); - return failure('M_INVALID_PARAM', 'predecessor must be an object'); - } - - if (!content.predecessor.room_id || typeof content.predecessor.room_id !== 'string') { - console.error(`Create event ${eventId} has invalid predecessor room_id`); - return failure('M_INVALID_PARAM', 'predecessor.room_id must be a string'); - } - - if (!content.predecessor.event_id || typeof content.predecessor.event_id !== 'string') { - console.error(`Create event ${eventId} has invalid predecessor event_id`); - return failure('M_INVALID_PARAM', 'predecessor.event_id must be a string'); - } - } - - console.debug(`Create event ${eventId} passed validation`); - return success(event); - - } catch (error: any) { - console.error(`Error validating create event ${eventId}: ${error.message || String(error)}`); - return failure('M_UNKNOWN', `Error validating create event: ${error.message || String(error)}`); - } -} - -export function registerCreateValidator(): void { - registerEventHandler('m.room.create', validateCreateEvent); -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/events/m.room.join_rules.ts b/packages/homeserver/src/validation/events/m.room.join_rules.ts deleted file mode 100644 index ccc7bbdc0..000000000 --- a/packages/homeserver/src/validation/events/m.room.join_rules.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { failure, success, type ValidationResult } from '../ValidationResult'; -import type { AuthorizedEvent } from '../validators/EventValidators'; -import { registerEventHandler } from './index'; - -enum JoinRule { - PUBLIC = 'public', - KNOCK = 'knock', - INVITE = 'invite', - PRIVATE = 'private', - RESTRICTED = 'restricted' -} - -export async function validateJoinRules( - event: AuthorizedEvent, - eventId: string -): Promise { - try { - const { event: rawEvent } = event; - - // Join rules must have state_key = "" - if (rawEvent.state_key !== '') { - console.error(`Join rules event ${eventId} has invalid state_key: '${rawEvent.state_key}'`); - return failure('M_INVALID_PARAM', 'Join rules events must have an empty state_key'); - } - - // Check for required auth events in auth_event_objects - const { auth_event_objects } = event.authorizedEvent; - if (!auth_event_objects || auth_event_objects.length === 0) { - console.error(`Join rules event ${eventId} is missing required auth events`); - return failure('M_MISSING_AUTH_EVENTS', 'Join rules events must have auth events'); - } - - let createEvent = null; - let powerLevels = null; - let senderMembership = null; - - // Find the required auth events - for (const authObj of auth_event_objects) { - if (!authObj.event) continue; - - if (authObj.event.type === 'm.room.create' && authObj.event.state_key === '') { - createEvent = authObj.event; - } - else if (authObj.event.type === 'm.room.power_levels' && authObj.event.state_key === '') { - powerLevels = authObj.event; - } - else if (authObj.event.type === 'm.room.member' && authObj.event.state_key === rawEvent.sender) { - senderMembership = authObj.event; - } - } - - // Always require create event - if (!createEvent) { - console.error(`Join rules event ${eventId} missing required m.room.create event`); - return failure('M_MISSING_AUTH_EVENTS', 'Join rules event must reference the room create event'); - } - - // Check that the sender is in the room - if (!senderMembership) { - console.error(`Join rules event ${eventId} missing sender's membership event`); - return failure('M_MISSING_AUTH_EVENTS', 'Join rules events must reference the sender\'s membership'); - } - - if (senderMembership.content?.membership !== 'join') { - console.error(`Join rules event ${eventId} sender is not joined to the room`); - return failure('M_FORBIDDEN', 'Sender must be joined to the room to set join rules'); - } - - // Basic content validation - const content = rawEvent.content || {}; - - // join_rule is required - if (!content.join_rule) { - console.error(`Join rules event ${eventId} is missing required join_rule field`); - return failure('M_MISSING_PARAM', 'Join rules events must specify a join_rule value'); - } - - // join_rule must be a valid value - if (!Object.values(JoinRule).includes(content.join_rule)) { - console.error(`Join rules event ${eventId} has invalid join_rule value: ${content.join_rule}`); - return failure('M_INVALID_PARAM', - `Invalid join_rule value: ${content.join_rule}. Must be one of: ${Object.values(JoinRule).join(', ')}`); - } - - // For restricted join rule, validate allow rules - if (content.join_rule === JoinRule.RESTRICTED) { - if (!content.allow || !Array.isArray(content.allow) || content.allow.length === 0) { - console.error(`Join rules event ${eventId} with restricted join_rule is missing required allow rules`); - return failure('M_MISSING_PARAM', 'Restricted rooms must specify allow rules'); - } - - // Validate each allow rule - for (const rule of content.allow) { - if (!rule.type) { - console.error(`Join rules event ${eventId} has allow rule missing required type field`); - return failure('M_MISSING_PARAM', 'Allow rules must specify a type'); - } - - if (rule.type === 'm.room_membership') { - if (!rule.room_id) { - console.error(`Join rules event ${eventId} has m.room_membership rule missing required room_id`); - return failure('M_MISSING_PARAM', 'Room membership rules must specify a room_id'); - } - } else { - console.warn(`Join rules event ${eventId} has unknown allow rule type: ${rule.type}`); - // Don't fail on unknown types, just warn - } - } - } - - // Check if user has permission to change join rules - if (powerLevels) { - const eventPowerLevel = powerLevels.content?.events?.['m.room.join_rules'] ?? - powerLevels.content?.state_default ?? - 50; // default for state events - - const userPowerLevel = powerLevels.content?.users?.[rawEvent.sender] ?? - powerLevels.content?.users_default ?? - 0; - - // Only the room creator is allowed to set initial join rules without having existing power - const isCreator = rawEvent.sender === createEvent.content?.creator; - - if (!isCreator && userPowerLevel < eventPowerLevel) { - console.error(`Join rules event ${eventId} sender has insufficient power: ${userPowerLevel} < ${eventPowerLevel}`); - return failure('M_FORBIDDEN', 'Sender does not have permission to change join rules'); - } - } - - console.debug(`Join rules event ${eventId} passed validation`); - return success(event); - - } catch (error: any) { - console.error(`Error validating join rules event ${eventId}: ${error.message || String(error)}`); - return failure('M_UNKNOWN', `Error validating join rules event: ${error.message || String(error)}`); - } -} - -export function registerJoinRulesValidator(): void { - registerEventHandler('m.room.join_rules', validateJoinRules); -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/events/m.room.member.ts b/packages/homeserver/src/validation/events/m.room.member.ts deleted file mode 100644 index 86ae2cd78..000000000 --- a/packages/homeserver/src/validation/events/m.room.member.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { failure, success, type ValidationResult } from '../ValidationResult'; -import type { AuthorizedEvent } from '../validators/EventValidators'; -import { registerEventHandler } from './index'; - -enum Membership { - JOIN = 'join', - LEAVE = 'leave', - INVITE = 'invite', - BAN = 'ban', - KNOCK = 'knock' -} - -export async function validateMemberEvent( - event: AuthorizedEvent, - eventId: string -): Promise { - try { - const { event: rawEvent } = event; - - // Member events must have a state_key (the user ID being affected) - if (rawEvent.state_key === undefined || rawEvent.state_key === '') { - console.error(`Member event ${eventId} has invalid state_key: '${rawEvent.state_key}'`); - return failure('M_INVALID_PARAM', 'Member events must have a non-empty state_key'); - } - - // Basic content validation - const content = rawEvent.content || {}; - - // membership is required - if (!content.membership) { - console.error(`Member event ${eventId} is missing required membership field`); - return failure('M_MISSING_PARAM', 'Member events must specify a membership value'); - } - - // membership must be a valid value - if (!Object.values(Membership).includes(content.membership)) { - console.error(`Member event ${eventId} has invalid membership value: ${content.membership}`); - return failure('M_INVALID_PARAM', - `Invalid membership value: ${content.membership}. Must be one of: ${Object.values(Membership).join(', ')}`); - } - - // Check for required auth events in auth_event_objects - const { auth_event_objects } = event.authorizedEvent; - if (!auth_event_objects || auth_event_objects.length === 0) { - console.error(`Member event ${eventId} is missing required auth events`); - return failure('M_MISSING_AUTH_EVENTS', 'Member events must have auth events'); - } - - let createEvent = null; - let joinRules = null; - let powerLevels = null; - let previousMembership = null; - - // Find the required auth events - for (const authObj of auth_event_objects) { - if (!authObj.event) continue; - - if (authObj.event.type === 'm.room.create' && authObj.event.state_key === '') { - createEvent = authObj.event; - } - else if (authObj.event.type === 'm.room.join_rules' && authObj.event.state_key === '') { - joinRules = authObj.event; - } - else if (authObj.event.type === 'm.room.power_levels' && authObj.event.state_key === '') { - powerLevels = authObj.event; - } - else if (authObj.event.type === 'm.room.member' && authObj.event.state_key === rawEvent.state_key) { - previousMembership = authObj.event; - } - } - - // Always require create event - if (!createEvent) { - console.error(`Member event ${eventId} missing required m.room.create event`); - return failure('M_MISSING_AUTH_EVENTS', 'Member event must reference the room create event'); - } - - // Additional auth checks by membership type - if (content.membership === Membership.JOIN) { - // For join-only or invite-only rooms, we need join rules - if (!joinRules) { - console.warn(`Join event ${eventId} missing join_rules event`); - // Don't fail but warn - } - else if (joinRules.content?.join_rule === 'invite' || joinRules.content?.join_rule === 'private') { - // For invite-only or private rooms, check that user was invited - if (rawEvent.sender !== createEvent.content?.creator) { - // The original creator can always join without invite - if (!previousMembership || previousMembership.content?.membership !== Membership.INVITE) { - console.error(`Join event ${eventId} for invite-only room without proper invitation`); - return failure('M_FORBIDDEN', 'Cannot join invite-only room without invitation'); - } - } - } - } - else if (content.membership === Membership.INVITE) { - // Invites require power level check - if (!powerLevels) { - console.warn(`Invite event ${eventId} missing power_levels event`); - // Don't fail but warn - } - else { - // Check that user has permission to invite - const invitePowerLevel = powerLevels.content?.invite ?? 50; - const userPowerLevel = (powerLevels.content?.users?.[rawEvent.sender] ?? - powerLevels.content?.users_default ?? 0); - - if (userPowerLevel < invitePowerLevel) { - console.error(`Invite event ${eventId} sender ${rawEvent.sender} has insufficient power: ${userPowerLevel} < ${invitePowerLevel}`); - return failure('M_FORBIDDEN', 'Sender does not have sufficient power level to invite'); - } - } - } - - // Validate invite events - if (content.membership === Membership.INVITE) { - // Check if this is a third-party invite - if (content.third_party_invite) { - // Validate third_party_invite structure - if (typeof content.third_party_invite !== 'object') { - console.error(`Member event ${eventId} has invalid third_party_invite (not an object)`); - return failure('M_INVALID_PARAM', 'third_party_invite must be an object'); - } - - // Must have signed section - if (!content.third_party_invite.signed || typeof content.third_party_invite.signed !== 'object') { - console.error(`Member event ${eventId} has invalid third_party_invite.signed (missing or not an object)`); - return failure('M_INVALID_PARAM', 'third_party_invite.signed must be an object'); - } - - // The signed section needs key fields - const { signed } = content.third_party_invite; - if (!signed.mxid || !signed.token || !signed.signatures) { - console.error(`Member event ${eventId} has invalid third_party_invite.signed (missing required fields)`); - return failure('M_MISSING_PARAM', 'third_party_invite.signed must contain mxid, token, and signatures'); - } - - // The mxid should match the state_key - if (signed.mxid !== rawEvent.state_key) { - console.error(`Member event ${eventId} has third_party_invite.signed.mxid (${signed.mxid}) that doesn't match state_key (${rawEvent.state_key})`); - return failure('M_INVALID_PARAM', 'third_party_invite.signed.mxid must match the state_key'); - } - } - } - - // Check join_authorised_via_users_server if present - if (content.join_authorised_via_users_server) { - if (typeof content.join_authorised_via_users_server !== 'string') { - console.error(`Member event ${eventId} has invalid join_authorised_via_users_server (not a string)`); - return failure('M_INVALID_PARAM', 'join_authorised_via_users_server must be a string'); - } - - // Should be a valid server name - if (!content.join_authorised_via_users_server.includes('.')) { - console.warn(`Member event ${eventId} has suspicious join_authorised_via_users_server value: ${content.join_authorised_via_users_server}`); - // We don't fail on this, just warn - } - } - - console.debug(`Member event ${eventId} passed validation`); - return success(event); - - } catch (error: any) { - console.error(`Error validating member event ${eventId}: ${error.message || String(error)}`); - return failure('M_UNKNOWN', `Error validating member event: ${error.message || String(error)}`); - } -} - -export function registerMemberValidator(): void { - registerEventHandler('m.room.member', validateMemberEvent); -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/events/m.room.message.ts b/packages/homeserver/src/validation/events/m.room.message.ts deleted file mode 100644 index 2c0900a53..000000000 --- a/packages/homeserver/src/validation/events/m.room.message.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { failure, success, type ValidationResult } from '../ValidationResult'; -import type { AuthorizedEvent } from '../validators/EventValidators'; -import { registerEventHandler } from './index'; - -export async function validateRoomMessage( - event: AuthorizedEvent, - eventId: string -): Promise { - try { - // Messages must have auth events - if (!event.authorizedEvent.auth_event_objects || - event.authorizedEvent.auth_event_objects.length === 0) { - console.error(`Message ${eventId} is missing required auth events`); - return failure('M_MISSING_AUTH_EVENTS', 'Messages must have auth events'); - } - - // Check for required auth events in auth_event_objects - const { auth_event_objects } = event.authorizedEvent; - let createEvent = null; - let powerLevels = null; - let senderMembership = null; - - // Find the required auth events - for (const authObj of auth_event_objects) { - if (!authObj.event) continue; - - if (authObj.event.type === 'm.room.create' && authObj.event.state_key === '') { - createEvent = authObj.event; - } - else if (authObj.event.type === 'm.room.power_levels' && authObj.event.state_key === '') { - powerLevels = authObj.event; - } - else if (authObj.event.type === 'm.room.member' && authObj.event.state_key === event.event.sender) { - senderMembership = authObj.event; - } - } - - // 1. Check for create event - if (!createEvent) { - console.error(`Message ${eventId} missing required m.room.create event`); - return failure('M_MISSING_AUTH_EVENTS', 'Message must reference the room create event'); - } - - // 2. Check for power levels - if (!powerLevels) { - console.error(`Message ${eventId} missing required m.room.power_levels event`); - return failure('M_MISSING_AUTH_EVENTS', 'Message must reference the room power levels'); - } - - // 3. Check for sender's membership - if (!senderMembership) { - console.error(`Message ${eventId} missing sender's membership event`); - return failure('M_MISSING_AUTH_EVENTS', 'Message must reference the sender\'s membership'); - } - - // Check that sender is actually in the room - if (senderMembership.content?.membership !== 'join') { - console.error(`Message ${eventId} sender is not joined to the room`); - return failure('M_FORBIDDEN', 'Sender must be joined to the room to send messages'); - } - - // 4. Check that user has permission to send messages based on power levels - const eventPowerLevel = (powerLevels.content?.events?.['m.room.message'] ?? - powerLevels.content?.events_default ?? - 0); - - const userPowerLevel = (powerLevels.content?.users?.[event.event.sender] ?? - powerLevels.content?.users_default ?? - 0); - - if (userPowerLevel < eventPowerLevel) { - console.error(`Message ${eventId} sender has insufficient power level: ${userPowerLevel} < ${eventPowerLevel}`); - return failure('M_FORBIDDEN', 'Sender does not have sufficient power level to send messages'); - } - - // Validate basic message structure - if (!event.event.content) { - console.error(`Message ${eventId} missing content`); - return failure('M_MISSING_PARAM', 'Message must have content'); - } - - // Validate msgtype if present - const msgtype = event.event.content.msgtype; - if (msgtype) { - const validTypes = ['m.text', 'm.emote', 'm.notice', 'm.image', 'm.file', 'm.audio', 'm.video', 'm.location']; - if (!validTypes.includes(msgtype) && !msgtype.startsWith('m.')) { - console.warn(`Message ${eventId} has non-standard msgtype: ${msgtype}`); - // We don't fail on this, just warn - } - } - - console.debug(`Message ${eventId} passed validation`); - return success(event); - - } catch (error: any) { - console.error(`Error validating message ${eventId}: ${error.message || String(error)}`); - return failure('M_UNKNOWN', `Error validating message: ${error.message || String(error)}`); - } -} - -export function registerMessageValidator(): void { - registerEventHandler('m.room.message', validateRoomMessage); -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/events/m.room.power_levels.ts b/packages/homeserver/src/validation/events/m.room.power_levels.ts deleted file mode 100644 index 30acf97ed..000000000 --- a/packages/homeserver/src/validation/events/m.room.power_levels.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { failure, success, type ValidationResult } from '../ValidationResult'; -import type { AuthorizedEvent } from '../validators/EventValidators'; -import { registerEventHandler } from './index'; - -export async function validatePowerLevels( - event: AuthorizedEvent, - eventId: string -): Promise { - try { - const { event: rawEvent } = event; - - // Power levels must have state_key = "" - if (rawEvent.state_key !== '') { - console.error(`Power levels event ${eventId} has invalid state_key: '${rawEvent.state_key}'`); - return failure('M_INVALID_PARAM', 'Power levels events must have an empty state_key'); - } - - // Check for required auth events in auth_event_objects - const { auth_event_objects } = event.authorizedEvent; - if (!auth_event_objects || auth_event_objects.length === 0) { - console.error(`Power levels event ${eventId} is missing required auth events`); - return failure('M_MISSING_AUTH_EVENTS', 'Power levels events must have auth events'); - } - - let createEvent = null; - let previousPowerLevels = null; - let senderMembership = null; - - // Find the required auth events - for (const authObj of auth_event_objects) { - if (!authObj.event) continue; - - if (authObj.event.type === 'm.room.create' && authObj.event.state_key === '') { - createEvent = authObj.event; - } - else if (authObj.event.type === 'm.room.power_levels' && authObj.event.state_key === '') { - previousPowerLevels = authObj.event; - } - else if (authObj.event.type === 'm.room.member' && authObj.event.state_key === rawEvent.sender) { - senderMembership = authObj.event; - } - } - - // Always require create event - if (!createEvent) { - console.error(`Power levels event ${eventId} missing required m.room.create event`); - return failure('M_MISSING_AUTH_EVENTS', 'Power levels event must reference the room create event'); - } - - // Check that the sender is in the room - if (!senderMembership) { - console.error(`Power levels event ${eventId} missing sender's membership event`); - return failure('M_MISSING_AUTH_EVENTS', 'Power levels events must reference the sender\'s membership'); - } - - if (senderMembership.content?.membership !== 'join') { - console.error(`Power levels event ${eventId} sender is not joined to the room`); - return failure('M_FORBIDDEN', 'Sender must be joined to the room to set power levels'); - } - - // Basic content validation - const content = rawEvent.content || {}; - - // Ensure power levels are numbers - const numericalFields = [ - 'ban', 'kick', 'redact', 'invite', - 'events_default', 'state_default', 'users_default' - ]; - - for (const field of numericalFields) { - if (content[field] !== undefined && - (typeof content[field] !== 'number' || !Number.isInteger(content[field]))) { - console.error(`Power levels event ${eventId} has invalid ${field}: ${content[field]}`); - return failure('M_INVALID_PARAM', `${field} must be an integer`); - } - } - - // Check events and users sections - if (content.events && typeof content.events !== 'object') { - console.error(`Power levels event ${eventId} has invalid events field (not an object)`); - return failure('M_INVALID_PARAM', 'events must be an object'); - } - - if (content.users && typeof content.users !== 'object') { - console.error(`Power levels event ${eventId} has invalid users field (not an object)`); - return failure('M_INVALID_PARAM', 'users must be an object'); - } - - // Check that all event power levels are integers - if (content.events) { - for (const [eventType, powerLevel] of Object.entries(content.events)) { - if (typeof powerLevel !== 'number' || !Number.isInteger(powerLevel)) { - console.error(`Power levels event ${eventId} has invalid power level for ${eventType}: ${powerLevel}`); - return failure('M_INVALID_PARAM', 'Event power levels must be integers'); - } - } - } - - // Check that all user power levels are integers - if (content.users) { - for (const [userId, powerLevel] of Object.entries(content.users)) { - if (typeof powerLevel !== 'number' || !Number.isInteger(powerLevel)) { - console.error(`Power levels event ${eventId} has invalid power level for ${userId}: ${powerLevel}`); - return failure('M_INVALID_PARAM', 'User power levels must be integers'); - } - } - } - - // Check if user has permission to change power levels - // (they need power above the required power to send power level events) - if (previousPowerLevels) { - const eventPowerLevel = previousPowerLevels.content?.events?.['m.room.power_levels'] ?? - previousPowerLevels.content?.state_default ?? - 50; // default for state events - - const userPowerLevel = previousPowerLevels.content?.users?.[rawEvent.sender] ?? - previousPowerLevels.content?.users_default ?? - 0; - - // Only the room creator is allowed to set initial power levels without having existing power - const isCreator = rawEvent.sender === createEvent.content?.creator; - - if (!isCreator && userPowerLevel < eventPowerLevel) { - console.error(`Power levels event ${eventId} sender has insufficient power: ${userPowerLevel} < ${eventPowerLevel}`); - return failure('M_FORBIDDEN', 'Sender does not have permission to change power levels'); - } - - // Verify that users can't change power levels of others with more power than themselves - if (content.users) { - for (const [userId, newPowerLevel] of Object.entries(content.users)) { - const currentPowerLevel = previousPowerLevels.content?.users?.[userId] ?? - previousPowerLevels.content?.users_default ?? - 0; - - // Can't change power levels of users with higher power - if (userId !== rawEvent.sender && // users can demote themselves - currentPowerLevel > userPowerLevel && - currentPowerLevel !== newPowerLevel) { - console.error(`Power levels event ${eventId} tries to change power of ${userId} from ${currentPowerLevel} to ${newPowerLevel}, but sender only has ${userPowerLevel}`); - return failure('M_FORBIDDEN', 'Cannot change power levels of users with higher power than yourself'); - } - } - } - } - - console.debug(`Power levels event ${eventId} passed validation`); - return success(event); - - } catch (error: any) { - console.error(`Error validating power levels event ${eventId}: ${error.message || String(error)}`); - return failure('M_UNKNOWN', `Error validating power levels event: ${error.message || String(error)}`); - } -} - -export function registerPowerLevelsValidator(): void { - registerEventHandler('m.room.power_levels', validatePowerLevels); -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/pipelines/index.ts b/packages/homeserver/src/validation/pipelines/index.ts deleted file mode 100644 index e98b3ba93..000000000 --- a/packages/homeserver/src/validation/pipelines/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface IPipeline { - validate(events: T, context: any): Promise; -} - -export class SequentialPipeline implements IPipeline { - private steps: IPipeline[] = []; - - constructor(steps: IPipeline[] = []) { - this.steps = steps; - } - - add(validator: IPipeline) { - this.steps.push(validator); - return this; - } - - async validate(events: T, context: any): Promise { - let result: T = events; - - for await (const validator of this.steps) { - try { - result = await validator.validate(result, context); - } catch (error: unknown) { - console.error(error); - throw error; - } - } - - return result; - } -} - -export { StagingAreaPipeline } from './stagingAreaPipeline'; -export { SynchronousEventReceptionPipeline } from './synchronousEventReceptionPipeline'; -export type { EventType, EventTypeArray } from './synchronousEventReceptionPipeline'; - diff --git a/packages/homeserver/src/validation/pipelines/stagingAreaPipeline.ts b/packages/homeserver/src/validation/pipelines/stagingAreaPipeline.ts deleted file mode 100644 index c92001b57..000000000 --- a/packages/homeserver/src/validation/pipelines/stagingAreaPipeline.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { EventStagingArea } from "../../events/EventStagingArea"; -import type { StagingEvent } from "../../events/stagingArea"; -import { Pipeline } from "../decorators/pipeline.decorator"; -// import { -// EventAuthChainValidator, -// MissingEventDownloader, -// RoomStateValidator -// } from "../validators"; -import { type EventTypeArray, type IPipeline, SequentialPipeline } from "./index"; - -/** - * Validates and processes events through a staging area - * - * This pipeline: - * 1. Downloads any missing events referenced by the incoming events - * 2. Validates event auth chains against Matrix rules - * 3. Validates events against the room's current state - * 4. Processes events into room-specific staging areas - * 5. Saves validated events to the database - */ -@Pipeline() -export class StagingAreaPipeline { - private validationPipeline: IPipeline; - private stagingAreas: Map = new Map(); - - constructor() { - this.validationPipeline = this.createValidationPipeline(); - } - - private createValidationPipeline(): IPipeline { - return new SequentialPipeline() - // .add(new MissingEventDownloader()) - // .add(new EventAuthChainValidator()) - // .add(new RoomStateValidator()); - } - - async validate(events: EventTypeArray, context: any): Promise { - // if (!events || events.length === 0) { - // console.warn("No events to validate"); - // return []; - // } - - // console.debug(`Validating ${events.length} events in staging area pipeline`); - - // const validatedEvents = await this.validationPipeline.validate(events, context); - // const successfulEvents = validatedEvents.filter(e => !e.error); - - // if (successfulEvents.length > 0) { - // const eventsByRoom = this.groupEventsByRoom(successfulEvents); - // for (const [roomId, roomEvents] of Object.entries(eventsByRoom)) { - // await this.processRoomEvents(roomId, roomEvents, context); - // } - // await this.saveValidatedEvents(successfulEvents.map(e => e.event), context); - // } - - // return validatedEvents; - } - - private groupEventsByRoom(events: EventTypeArray): Record { - const eventsByRoom: Record = {}; - - for (const { eventId, event } of events) { - const parts = event.room_id.split(':'); - const roomId = parts[0]; - - if (!roomId) continue; - - if (!eventsByRoom[roomId]) { - eventsByRoom[roomId] = []; - } - - eventsByRoom[roomId].push({ eventId, event }); - } - - return eventsByRoom; - } - - private async processRoomEvents(roomId: string, events: EventTypeArray, context: any): Promise { - let stagingArea = this.stagingAreas.get(roomId); - if (!stagingArea) { - stagingArea = new EventStagingArea(roomId, context); - this.stagingAreas.set(roomId, stagingArea); - console.debug(`Created new staging area for room ${roomId}`); - } - - await stagingArea.addEvents(events, context); - const stats = stagingArea.getStats(); - console.debug(`Staging area for room ${roomId}: ${JSON.stringify(stats)}`); - } - - async saveValidatedEvents(events: any[], context: any) { - if (!context.mongo?.createEvent) { - console.warn('No createEvent function provided'); - return; - } - - console.debug(`Saving ${events.length} validated events to database`); - for (const event of events) { - try { - await context.mongo.createEvent(event); - console.debug(`Saved event: ${event.event_id || 'unknown'}`); - } catch (error) { - console.error(`Failed to save validated event: ${error}`); - } - } - } - - public getRoomState(roomId: string): any | null { - const stagingArea = this.stagingAreas.get(roomId); - if (!stagingArea) return null; - - return stagingArea.getRoomState(); - } - - public async downloadAndProcessEvents(events: StagingEvent[], context: any): Promise<{ - downloadedEvents: { eventId: string, event: any }[] - }> { - // const processedEvents = await this.validate(events, context); - // const successfulEvents = processedEvents.filter(e => !e.error); - // if (successfulEvents.length > 0) { - // console.info(`Processing ${successfulEvents.length} downloaded events`); - // await this.saveValidatedEvents(successfulEvents.map(e => e.event), context); - // } - - return { - downloadedEvents: [] - }; - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/pipelines/synchronousEventReceptionPipeline.ts b/packages/homeserver/src/validation/pipelines/synchronousEventReceptionPipeline.ts deleted file mode 100644 index ec261e5d9..000000000 --- a/packages/homeserver/src/validation/pipelines/synchronousEventReceptionPipeline.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { IPipeline } from "."; -import { SequentialPipeline } from "."; -import { Pipeline } from "../decorators/pipeline.decorator"; -import type { roomV10Type } from "../schemas/room-v10.type"; -import { - EventFormatValidator, - EventHashesAndSignaturesValidator, - EventTypeSpecificValidator -} from "../validators"; - -export type EventType = { - eventId: string; - event: roomV10Type; - error?: { - errcode: string; - error: string; - } -} - -export type EventTypeArray = EventType[]; - -@Pipeline() -export class SynchronousEventReceptionPipeline implements IPipeline { - private pipeline: IPipeline; - - constructor() { - this.pipeline = this.createPipeline(); - } - - private createPipeline(): IPipeline { - return new SequentialPipeline() - .add(new EventFormatValidator()) - .add(new EventTypeSpecificValidator()) - .add(new EventHashesAndSignaturesValidator()) - // .add(new OutlierDetectionValidator()); // TODO: Think there's a better place for this - } - - async validate(events: EventTypeArray, context: any): Promise { - return await this.pipeline.validate(events, context); - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/schemas/room-v10.type.ts b/packages/homeserver/src/validation/schemas/room-v10.type.ts deleted file mode 100644 index 7c73ff283..000000000 --- a/packages/homeserver/src/validation/schemas/room-v10.type.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { z } from "zod"; - -const eventId = z.string().regex(/^\$.+$/, "Invalid event_id format"); -const userId = z.string().regex(/^@.+:.+$/, "Invalid sender format"); -const roomId = z.string().regex(/^!.+:.+$/, "Invalid room_id format"); - -const hashesSchema = z.record(z.literal('sha256'), z.string()); - -export const roomV10Schema = z.object({ - auth_events: z.array(eventId), - content: z.record(z.unknown()), - depth: z.number().int(), - hashes: hashesSchema, - origin_server_ts: z.number().int(), - origin: z.string(), - prev_events: z.array(eventId), - redacts: eventId.optional(), - room_id: roomId, - sender: userId, - signatures: z.record(z.record(z.string())), - type: z.string(), - unsigned: z.record(z.unknown()).optional(), - state_key: z.string().optional(), -}); - -export type roomV10Type = z.infer; \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/EventFormatValidator.ts b/packages/homeserver/src/validation/validators/EventFormatValidator.ts deleted file mode 100644 index 439a02d69..000000000 --- a/packages/homeserver/src/validation/validators/EventFormatValidator.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { z } from 'zod'; -import type { Config } from '../../plugins/config'; -import { Validator } from '../decorators/validator.decorator'; -import type { EventTypeArray, IPipeline } from '../pipelines'; -import { eventSchemas } from '../schemas/event-schemas'; - -async function getCachedRoomVersion(roomId: string, context: any): Promise { - // TODO: Should load from an injected repository instead of passing in context - if (!context.mongo?.getRoomVersion) { - console.warn('No getRoomVersion method available'); - return null; - } - - try { - return await context.mongo.getRoomVersion(roomId); - } catch (error) { - console.error(`Error getting cached room version: ${error}`); - return null; - } -} - -async function getRoomVersionFromOriginServer(origin: string, roomId: string, config: Config): Promise { - console.debug(`Fetching room version from origin server ${origin} for room ${roomId}`); - return null; -} - -async function extractRoomVersion(event: any, context: any): Promise { - if (event.type === 'm.room.create' && event.state_key === '') { - const roomVersion = event.content?.room_version; - if (roomVersion) { - console.debug(`Extracted room version ${roomVersion} from create event`); - return roomVersion; - } - } - - const cachedRoomVersion = await getCachedRoomVersion(event.room_id, context); - if (cachedRoomVersion) { - console.debug(`Using cached room version ${cachedRoomVersion} for room ${event.room_id}`); - return cachedRoomVersion; - } - - if (event.origin) { - const originRoomVersion = await getRoomVersionFromOriginServer(event.origin, event.room_id, context.config); - if (originRoomVersion) { - console.debug(`Using origin server room version ${originRoomVersion} for room ${event.room_id}`); - return originRoomVersion; - } - } - - console.warn(`Could not determine room version for ${event.room_id}, using default version 11`); - return "11"; -} - -function getEventSchema(roomVersion: string, eventType: string): z.ZodSchema { - const versionSchemas = eventSchemas[roomVersion]; - if (!versionSchemas) { - throw new Error(`Unsupported room version: ${roomVersion}`); - } - - const schema = versionSchemas[eventType] || versionSchemas.default; - if (!schema) { - throw new Error(`No schema available for event type ${eventType} in room version ${roomVersion}`); - } - - return schema; -} - -@Validator() -export class EventFormatValidator implements IPipeline { - async validate(events: EventTypeArray, context: any): Promise { - const response: EventTypeArray = []; - - for (const event of events) { - const eventId = event.eventId; - const eventType = event.event.type; - - console.debug(`Validating format for event ${eventId} of type ${eventType}`); - - try { - const roomVersion = await extractRoomVersion(event.event, context); - if (!roomVersion) { - console.error(`Could not determine room version for event ${eventId}`); - response.push({ - eventId, - error: { - errcode: 'M_UNKNOWN_ROOM_VERSION', - error: 'Could not determine room version for event' - }, - event: event.event - }); - continue; - } - - const eventSchema = getEventSchema(roomVersion, eventType); - const validationResult = eventSchema.safeParse(event.event); - if (!validationResult.success) { - const formattedErrors = JSON.stringify(validationResult.error.format()); - console.error(`Event ${eventId} failed schema validation: ${formattedErrors}`); - response.push({ - eventId, - error: { - errcode: 'M_SCHEMA_VALIDATION_FAILED', - error: `Schema validation failed: ${formattedErrors}` - }, - event: event.event - }); - continue; - } - - console.debug(`Event ${eventId} passed schema validation for room version ${roomVersion}`); - response.push({ - eventId, - event: event.event - }); - } catch (error: any) { - const errorMessage = error?.message || String(error); - console.error(`Error validating format for ${eventId}: ${errorMessage}`); - response.push({ - eventId, - error: { - errcode: 'M_FORMAT_VALIDATION_ERROR', - error: `Error validating format: ${errorMessage}` - }, - event: event.event - }); - } - } - - return response; - } -} diff --git a/packages/homeserver/src/validation/validators/EventHashesAndSignaturesValidator.ts b/packages/homeserver/src/validation/validators/EventHashesAndSignaturesValidator.ts deleted file mode 100644 index 14da5ecd3..000000000 --- a/packages/homeserver/src/validation/validators/EventHashesAndSignaturesValidator.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MatrixError } from '../../errors'; -import { getPublicKeyFromRemoteServer, makeGetPublicKeyFromServerProcedure } from '../../procedures/getPublicKeyFromServer'; -import { checkSignAndHashes } from '../../utils/checkSignAndHashes'; -import { Validator } from '../decorators/validator.decorator'; -import type { EventTypeArray, IPipeline } from '../pipelines'; - -@Validator() -export class EventHashesAndSignaturesValidator implements IPipeline { - async validate(events: EventTypeArray, context: any): Promise { - const response: EventTypeArray = []; - - for (const event of events) { - const getPublicKeyFromServer = makeGetPublicKeyFromServerProcedure( - context.mongo.getValidPublicKeyFromLocal, - (origin, key) => getPublicKeyFromRemoteServer(origin, context.config.name, key), - context.mongo.storePublicKey, - ); - - const eventId = event.eventId; - - try{ - await checkSignAndHashes(event.event, event.event.origin, getPublicKeyFromServer); - response.push({ eventId, event: event.event }); - } catch (error: any) { - console.error(error); - response.push({ - eventId, - error: { - errcode: error instanceof MatrixError ? error.errcode : 'M_UNKNOWN', - error: error.message - }, - event: event.event - }); - } - } - - return response; - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/EventTypeSpecificValidator.ts b/packages/homeserver/src/validation/validators/EventTypeSpecificValidator.ts deleted file mode 100644 index 38ca3831e..000000000 --- a/packages/homeserver/src/validation/validators/EventTypeSpecificValidator.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Validator } from '../decorators/validator.decorator'; -import type { EventTypeArray, IPipeline } from '../pipelines'; - -@Validator() -export class EventTypeSpecificValidator implements IPipeline { - async validate(events: EventTypeArray , _context: any): Promise { - const response: EventTypeArray = []; - - for (const event of events) { - try { - const eventId = event?.eventId; - const eventType = event?.event.type; - - console.debug(`Validating event type specific rules for ${eventId} of type ${eventType}`); - - if (eventType === 'm.room.create') { - const errors = this.validateCreateEvent(event.event); - - if (errors.length > 0) { - console.error(`Create event ${eventId} validation failed: ${errors.join(', ')}`); - response.push({ - eventId, - error: { - errcode: 'M_INVALID_CREATE_EVENT', - error: `Create event validation failed: ${errors[0]}` - }, - event: event.event - }); - continue; - } - } else { - const errors = this.validateNonCreateEvent(event.event); - - if (errors.length > 0) { - console.error(`Event ${eventId} validation failed: ${errors.join(', ')}`); - response.push({ - eventId, - error: { - errcode: 'M_INVALID_EVENT', - error: `Event validation failed: ${errors[0]}` - }, - event: event.event - }); - continue; - } - } - - console.debug(`Event ${eventId} passed type-specific validation`); - response.push({ - eventId, - event: event.event - }); - } catch (error: any) { - const eventId = event?.eventId || 'unknown'; - console.error(`Error in type-specific validation for ${eventId}: ${error.message || String(error)}`); - response.push({ - eventId, - error: { - errcode: 'M_TYPE_VALIDATION_ERROR', - error: `Error in type-specific validation: ${error.message || String(error)}` - }, - event: event.event - }); - } - } - - return response; - } - - private validateCreateEvent(event: any): string[] { - const errors: string[] = []; - - if (event.prev_events && event.prev_events.length > 0) { - errors.push('Create event must not have prev_events'); - } - - if (event.room_id && event.sender) { - const roomDomain = this.extractDomain(event.room_id); - const senderDomain = this.extractDomain(event.sender); - - if (roomDomain !== senderDomain) { - errors.push(`Room ID domain (${roomDomain}) does not match sender domain (${senderDomain})`); - } - } - - if (event.auth_events && event.auth_events.length > 0) { - errors.push('Create event must not have auth_events'); - } - - if (!event.content || !event.content.room_version) { - errors.push('Create event must specify a room_version'); - } else { - const validRoomVersions = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; - if (!validRoomVersions.includes(event.content.room_version)) { - errors.push(`Unsupported room version: ${event.content.room_version}`); - } - } - - return errors; - } - - private validateNonCreateEvent(event: any): string[] { - const errors: string[] = []; - - if (!event.prev_events || !Array.isArray(event.prev_events) || event.prev_events.length === 0) { - errors.push('Event must reference previous events (prev_events)'); - } - - // TODO: Add DB room check - if (event.room_id) { - // const roomIdRegex = /^![\w-]+:[\w.-]+\.\w+$/; - // if (!roomIdRegex.test(event.room_id)) { - // errors.push(`Invalid room_id format: ${event.room_id}`); - // } - } - - return errors; - } - - private extractDomain(id: string): string { - const parts = id.split(':'); - return parts.length > 1 ? parts[1] : ''; - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/EventValidators.ts b/packages/homeserver/src/validation/validators/EventValidators.ts deleted file mode 100644 index b28861870..000000000 --- a/packages/homeserver/src/validation/validators/EventValidators.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { EventBase as CoreEventBase } from '@hs/core/src/events/eventBase'; - -export interface Event extends Partial { - event: { - type: string; - room_id: string; - sender: string; - content: Record; - origin_server_ts: number; - [key: string]: any; - } -} - -export interface CanonicalizedEvent extends Event { - canonicalizedEvent: { - canonical: boolean; - canonicalJson?: string; - } -} - -export interface AuthorizedEvent extends CanonicalizedEvent { - authorizedEvent: { - auth_events: string[]; - signatures: Record>; - hashes: Record; - auth_event_objects?: Event[]; - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/OutlierDetectionValidator.ts b/packages/homeserver/src/validation/validators/OutlierDetectionValidator.ts deleted file mode 100644 index 9ae0cb7d4..000000000 --- a/packages/homeserver/src/validation/validators/OutlierDetectionValidator.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Validator } from '../decorators/validator.decorator'; -import type { EventType, EventTypeArray, IPipeline } from '../pipelines'; - -@Validator() -export class OutlierDetectionValidator implements IPipeline { - async validate(events: (EventType & { type: string, state_key: string, event_id: string })[], context: any): Promise { - const pdus: Array> = []; - const edus: Array> = []; - - for (const event of events) { - try { - const eventId = event?.event_id; - console.debug(`Checking for outlier status for event ${eventId}`); - - if (event.type === 'm.room.create' && event?.state_key === '') { - console.debug(`Event ${eventId} is a create event, not an outlier`); - pdus.push({ [eventId]: {} }); - continue; - } - - const isReferenced = await this.isEventReferenced(event, context); - const hasKnownParents = await this.hasKnownParents(event, context); - const isOutlier = !isReferenced || !hasKnownParents; - - if (isOutlier) { - console.info(`Event ${eventId} identified as an outlier (referenced=${isReferenced}, known_parents=${hasKnownParents})`); - - // Mark event as an outlier but don't reject it - // This is important information for further processing - pdus.push({ - [eventId]: { - is_outlier: true - } - }); - } else { - console.debug(`Event ${eventId} is part of the main event graph`); - pdus.push({ [eventId]: {} }); - } - } catch (error: any) { - const eventId = event?.event_id || 'unknown'; - console.error(`Error during outlier detection for ${eventId}: ${error.message || String(error)}`); - pdus.push({ - [eventId]: { - errcode: 'M_OUTLIER_DETECTION_ERROR', - error: `Error during outlier detection: ${error.message || String(error)}` - } - }); - } - } - - return pdus as unknown as EventTypeArray; - } - - private async isEventReferenced(event: any, context: any): Promise { - if (!event.event_id || !context.mongo?.isEventReferenced) { - return false; - } - - try { - return await context.mongo.isEventReferenced(event.event_id, event.room_id); - } catch (error) { - console.warn(`Error checking if event is referenced: ${error}`); - return false; - } - } - - private async hasKnownParents(event: any, context: any): Promise { - if (!event.prev_events || !event.prev_events.length || !context.mongo?.areEventsInMainDAG) { - return false; - } - - try { - const prevEventIds = event.prev_events.map((pe: any) => - Array.isArray(pe) ? pe[0] : pe - ); - - return await context.mongo.areEventsInMainDAG(prevEventIds, event.room_id); - } catch (error) { - console.warn(`Error checking if event has known parents: ${error}`); - return false; - } - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/RoomStateValidator.ts b/packages/homeserver/src/validation/validators/RoomStateValidator.ts deleted file mode 100644 index 388d2c6ca..000000000 --- a/packages/homeserver/src/validation/validators/RoomStateValidator.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { RoomState } from "../../events/roomState"; -import type { EventType, EventTypeArray, IPipeline } from "../pipelines"; - -/** - * Validates events against room state to ensure they comply with Matrix protocol rules - * - * This validator ensures that: - * 1. Events are associated with valid rooms - * 2. Events comply with the current state of their rooms - * 3. Events can be properly applied to their room state - */ -export class RoomStateValidator implements IPipeline { - private roomStates: Map = new Map(); - - async validate(events: EventTypeArray, context: any): Promise { - console.log(`Validating ${events.length} events against room state`); - - const eventsByRoom = this.groupEventsByRoom(events); - - const validatedEvents: EventTypeArray = []; - for (const [roomId, roomEvents] of eventsByRoom.entries()) { - const roomValidatedEvents = await this.validateRoomEvents(roomId, roomEvents, context); - validatedEvents.push(...roomValidatedEvents); - } - - return validatedEvents; - } - - private groupEventsByRoom(events: EventTypeArray): Map { - const eventsByRoom = new Map(); - - for (const eventData of events) { - const { eventId, event } = eventData; - - const roomId = this.extractRoomId(event.room_id); - - if (!roomId) { - console.warn(`Event ${eventId} has invalid room ID: ${event.room_id}`); - continue; - } - - if (!eventsByRoom.has(roomId)) { - eventsByRoom.set(roomId, []); - } - eventsByRoom.get(roomId)!.push(eventData); - } - - return eventsByRoom; - } - - private extractRoomId(roomIdString: string): string | null { - if (!roomIdString) return null; - - const parts = roomIdString.split(':'); - return parts[0] || null; - } - - private async validateRoomEvents( - roomId: string, - roomEvents: EventTypeArray, - context: any - ): Promise { - const validatedEvents: EventTypeArray = []; - const roomState = await this.getRoomState(roomId, context); - - for (const eventData of roomEvents) { - const { eventId, event } = eventData; - - try { - const isValid = await this.validateEventAgainstRoomState({ eventId, event }, roomState); - - if (isValid) { - validatedEvents.push(eventData); - console.debug(`Event ${eventId} passed room state validation`); - } else { - validatedEvents.push({ - eventId, - event, - error: { - errcode: "M_FAILED_ROOM_STATE_VALIDATION", - error: "Event does not comply with room state rules" - } - }); - console.warn(`Event ${eventId} failed room state validation`); - } - } catch (error: any) { - validatedEvents.push({ - eventId, - event, - error: { - errcode: "M_FAILED_ROOM_STATE_VALIDATION", - error: `Validation error: ${error.message}` - } - }); - console.warn(`Exception validating event ${eventId}: ${error.message}`); - } - } - - return validatedEvents; - } - - private async getRoomState(roomId: string, context: any): Promise { - let roomState = this.roomStates.get(roomId); - - if (!roomState) { - roomState = new RoomState(roomId); - this.roomStates.set(roomId, roomState); - console.debug(`Created new RoomState for room ${roomId}`); - - await this.loadRoomStateFromDatabase(roomState, roomId, context); - } - - return roomState; - } - - private async loadRoomStateFromDatabase( - roomState: RoomState, - roomId: string, - context: any - ): Promise { - if (!context.mongo?.getRoomState) { - console.debug(`No database connection available to load room state for ${roomId}`); - return; - } - - try { - const storedState = await context.mongo.getRoomState(roomId); - - if (storedState) { - console.debug(`Loading stored state for room ${roomId}`); - await this.initializeRoomStateFromStorage(roomState, storedState); - console.debug(`Successfully loaded stored state for room ${roomId}`); - } else { - console.debug(`No stored state found for room ${roomId}`); - } - } catch (error: any) { - console.warn(`Failed to load room state for ${roomId}: ${error.message}`); - } - } - - private async initializeRoomStateFromStorage( - roomState: RoomState, - storedState: any - ): Promise { - const createEvent = storedState.stateEvents?.find((e: any) => e.type === 'm.room.create'); - if (createEvent) { - await roomState.addEvent(createEvent); - } - - const powerLevelsEvent = storedState.stateEvents?.find((e: any) => e.type === 'm.room.power_levels'); - if (powerLevelsEvent) { - await roomState.addEvent(powerLevelsEvent); - } - - if (storedState.stateEvents) { - for (const event of storedState.stateEvents) { - if (event.type !== 'm.room.create' && event.type !== 'm.room.power_levels') { - await roomState.addEvent(event); - } - } - } - - if (storedState.forwardExtremities) { - for (const eventId of storedState.forwardExtremities) { - const event = storedState.events?.find((e: any) => e.event_id === eventId); - if (event) { - await roomState.addEvent(event); - } - } - } - } - - private async validateEventAgainstRoomState( - event: EventType, - roomState: RoomState - ): Promise { - const tempRoomState = new RoomState(roomState.getRoomId()); - const stateEvents = roomState.getStateEvents(); - for (const stateEvent of stateEvents) { - await tempRoomState.addEvent(stateEvent); - } - - return await tempRoomState.addEvent(event); - } -} \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/event/AuthChainValidator.ts b/packages/homeserver/src/validation/validators/event/AuthChainValidator.ts deleted file mode 100644 index 89fbc4cbb..000000000 --- a/packages/homeserver/src/validation/validators/event/AuthChainValidator.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getErrorMessage } from '../../../utils/get-error-message'; -import { failure, success } from '../../ValidationResult'; -import { createValidator } from '../../Validator'; -import type { AuthorizedEvent } from '../EventValidators'; - -/** - * Validates the auth event chain - * - * Each auth event must be cryptographically valid and form a valid chain. - * This validator ensures the integrity of the auth chain. - */ -export const validateAuthChain = createValidator(async (event, _, eventId) => { - try { - if (!event.authorizedEvent.auth_event_objects) { - console.warn(`Event ${eventId} missing auth_event_objects`); - return failure('M_MISSING_AUTH_EVENTS', 'Event missing auth event objects'); - } - - // For development purposes, we'll assume the auth chain is valid - // In production, you would implement proper validation of the auth chain - console.debug(`Auth chain considered valid for event ${eventId} (development mode)`); - return success(event); - - } catch (error) { - console.error(`Error validating auth chain: ${getErrorMessage(error)}`); - return failure('M_INVALID_AUTH_CHAIN', `Error validating auth chain: ${getErrorMessage(error)}`); - } -}); \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/event/AuthEventsValidator.ts b/packages/homeserver/src/validation/validators/event/AuthEventsValidator.ts deleted file mode 100644 index 9bf283c4c..000000000 --- a/packages/homeserver/src/validation/validators/event/AuthEventsValidator.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Collection, MongoClient } from 'mongodb'; -import { generateId } from '../../../authentication'; -import { makeRequest } from '../../../makeRequest'; -import { extractOrigin } from '../../../utils/extractOrigin'; -import { getErrorMessage } from '../../../utils/get-error-message'; -import { getServerName } from '../../../utils/serverConfig'; -import { failure, success } from '../../ValidationResult'; -import { createValidator } from '../../Validator'; -import type { AuthorizedEvent, CanonicalizedEvent } from '../EventValidators'; - -interface StoredEvent { - _id: string; - event: unknown; -} - -let client: MongoClient | null = null; -let eventsCollection: Collection | null = null; - -async function ensureDbConnection() { - if (!client) { - const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017'; - client = new MongoClient(mongoUri); - await client.connect(); - - const db = client.db('matrix'); - eventsCollection = db.collection('events'); - } - - if (!eventsCollection) { - throw new Error('Failed to initialize events collection'); - } - - return { eventsCollection }; -} - -/** - * Fetches and validates auth events - * - * Matrix events are authorized based on a chain of previous events. - * This validator fetches those auth events and prepares them for validation. - */ -export const fetchAuthEvents = createValidator(async (event, txnId, eventId) => { - try { - console.debug(`Fetching auth events for event ${eventId}`); - - const { eventsCollection } = await ensureDbConnection(); - - const authEventIds = event.event.auth_events || []; - if (authEventIds.length === 0) { - console.warn(`Event ${eventId} has no auth events`); - return failure('M_MISSING_AUTH_EVENTS', 'Event has no auth events'); - } - - console.debug(`Checking for locally available auth events: ${authEventIds.join(', ')}`); - const existingEvents = await eventsCollection.find({ - _id: { $in: authEventIds } - }).toArray(); - - const existingEventMap = new Map(existingEvents.map(e => [e._id, e])); - - const missingEventIds = authEventIds.filter((id: string) => !existingEventMap.has(id)); - - if (missingEventIds.length === 0) { - console.debug(`All auth events found locally for ${eventId}`); - - const authEventObjects = existingEvents.map(storedEvent => ({ - event: storedEvent.event - })); - - return success({ - event: event.event, - canonicalizedEvent: event.canonicalizedEvent, - authorizedEvent: { - auth_events: authEventIds, - auth_event_objects: authEventObjects, - signatures: event.event.signatures, - hashes: event.event.hashes - } - }); - } - - console.debug(`Need to fetch ${missingEventIds.length} missing auth events from remote: ${missingEventIds.join(', ')}`); - - const origin = extractOrigin(event.event.sender); - const localServerName = getServerName(); - const roomId = event.event.room_id; - - try { - const response = await makeRequest({ - method: 'POST', - domain: origin, - uri: `/_matrix/federation/v1/get_missing_events/${roomId}`, - body: { - earliest_events: [], - latest_events: missingEventIds, - limit: missingEventIds.length, - min_depth: 0 - }, - signingName: localServerName - }) as { events: unknown[] }; - - if (!response.events || !Array.isArray(response.events) || response.events.length === 0) { - console.warn(`No events returned from ${origin} for auth events: ${missingEventIds.join(', ')}`); - return failure('M_MISSING_AUTH_EVENTS', 'Remote server did not return required auth events'); - } - - // TODO: Validate the events before storing them - await Promise.all(response.events.map(async (fetchedEvent) => { - const fetchedEventId = generateId(fetchedEvent as object); - await eventsCollection.updateOne( - { _id: fetchedEventId }, - { $set: { event: fetchedEvent } }, - { upsert: true } - ); - - existingEventMap.set(fetchedEventId, { _id: fetchedEventId, event: fetchedEvent }); - })); - - const stillMissingIds = authEventIds.filter((id: string) => !existingEventMap.has(id)); - - if (stillMissingIds.length > 0) { - console.warn(`Still missing ${stillMissingIds.length} auth events after fetching: ${stillMissingIds.join(', ')}`); - return failure('M_MISSING_AUTH_EVENTS', `Failed to retrieve all required auth events: ${stillMissingIds.join(', ')}`); - } - - const allAuthEventObjects = authEventIds.map((id: string) => { - const storedEvent = existingEventMap.get(id); - return { - event: storedEvent!.event - }; - }); - - console.debug(`Successfully fetched all auth events for ${eventId}`); - - return success({ - event: event.event, - canonicalizedEvent: event.canonicalizedEvent, - authorizedEvent: { - auth_events: authEventIds, - auth_event_objects: allAuthEventObjects, - signatures: event.event.signatures, - hashes: event.event.hashes - } - }); - - } catch (networkError) { - console.error(`Network error fetching auth events from ${origin}: ${getErrorMessage(networkError)}`); - return failure('M_FAILED_TO_FETCH_AUTH', `Failed to fetch auth events: ${getErrorMessage(networkError)}`); - } - } catch (error) { - console.error(`Failed to fetch auth events for ${eventId}: ${getErrorMessage(error)}`); - return failure('M_MISSING_AUTH_EVENTS', `Failed to fetch auth events: ${getErrorMessage(error)}`); - } -}); \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/event/CanonicalizeEvent.ts b/packages/homeserver/src/validation/validators/event/CanonicalizeEvent.ts deleted file mode 100644 index e9ec12b6f..000000000 --- a/packages/homeserver/src/validation/validators/event/CanonicalizeEvent.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { encodeCanonicalJson } from '../../../signJson'; -import { getErrorMessage } from '../../../utils/get-error-message'; -import { failure, success } from '../../ValidationResult'; -import { createValidator } from '../../Validator'; -import type { CanonicalizedEvent, Event } from '../EventValidators'; - -/** - * Canonicalizes event format to prepare for validation - * - * Matrix spec defines a canonical form for JSON which is used for signing: - * - Deterministic property order (lexicographically by property name) - * - No whitespace or line breaks - * - Unescaped string literals (no \n, \r, \t, etc.) - * - * See: https://spec.matrix.org/v1.9/server-server-api/#canonical-json - */ -export const canonicalizeEvent = createValidator(async (event, txnId, eventId) => { - try { - const { event: eventData } = event; - - // 1. Validate required event fields according to Matrix spec - if (!eventData.type || typeof eventData.type !== 'string') { - return failure('M_INVALID_EVENT', 'Event must have a valid type'); - } - - if (!eventData.room_id || typeof eventData.room_id !== 'string') { - return failure('M_INVALID_EVENT', 'Event must have a valid room_id'); - } - - if (!eventData.sender || typeof eventData.sender !== 'string') { - return failure('M_INVALID_EVENT', 'Event must have a valid sender'); - } - - if (!eventData.content || typeof eventData.content !== 'object') { - return failure('M_INVALID_EVENT', 'Event must have valid content'); - } - - if (!eventData.origin_server_ts || typeof eventData.origin_server_ts !== 'number') { - return failure('M_INVALID_EVENT', 'Event must have a valid origin_server_ts'); - } - - const { signatures, unsigned, ...eventWithoutSignatures } = eventData; - - let canonicalJsonStr: string; - try { - canonicalJsonStr = encodeCanonicalJson(eventWithoutSignatures); - } catch (error) { - return failure('M_INVALID_EVENT', `Event could not be canonicalized: ${getErrorMessage(error)}`); - } - - return success({ - event: eventData, - canonicalizedEvent: { - canonical: true, - canonicalJson: canonicalJsonStr - } - }); - } catch (error) { - return failure('M_INVALID_EVENT', `Failed to canonicalize event: ${getErrorMessage(error)}`); - } -}); \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/event/EventHashValidator.ts b/packages/homeserver/src/validation/validators/event/EventHashValidator.ts deleted file mode 100644 index 5e6724839..000000000 --- a/packages/homeserver/src/validation/validators/event/EventHashValidator.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { computeHash } from '../../../authentication'; -import { getErrorMessage } from '../../../utils/get-error-message'; -import { failure, success } from '../../ValidationResult'; -import { createValidator } from '../../Validator'; -import type { CanonicalizedEvent } from '../EventValidators'; - -/** - * Validates the event hash against the canonical event - * - * Matrix events are cryptographically hashed to ensure integrity. - * This validator directly mirrors the logic in checkSignAndHashes.ts - */ -export const validateEventHash = createValidator(async (event, _, eventId) => { - try { - const { event: eventData } = event; - - if (!eventData.hashes || !eventData.hashes.sha256) { - console.warn(`Event ${eventId} missing required hash`); - return failure('M_MISSING_HASH', 'Event is missing required sha256 hash'); - } - - const [algorithm, hash] = computeHash(eventData); - const expectedHash = eventData.hashes[algorithm]; - - if (hash !== expectedHash) { - console.warn(`Hash validation failed for event ${eventId}, expected: ${expectedHash}, got: ${hash}`); - return failure('M_INVALID_HASH', 'Event hash validation failed - hashes do not match'); - } - - return success(event); - } catch (error) { - console.error(`Error validating event hash: ${getErrorMessage(error)}`); - return failure('M_INVALID_HASH', `Error validating event hash: ${getErrorMessage(error)}`); - } -}); diff --git a/packages/homeserver/src/validation/validators/event/EventSignatureValidator.ts b/packages/homeserver/src/validation/validators/event/EventSignatureValidator.ts deleted file mode 100644 index fd92d8e4b..000000000 --- a/packages/homeserver/src/validation/validators/event/EventSignatureValidator.ts +++ /dev/null @@ -1,108 +0,0 @@ -import nacl from 'tweetnacl'; -import { getPublicKeyFromRemoteServer, makeGetPublicKeyFromServerProcedure } from '../../../procedures/getPublicKeyFromServer'; -import { EncryptionValidAlgorithm } from '../../../signJson'; -import { extractOrigin } from '../../../utils/extractOrigin'; -import { getValidPublicKeyFromLocal, storePublicKey } from '../../../utils/keyStore'; -import { getServerName } from '../../../utils/serverConfig'; -import { failure, success } from '../../ValidationResult'; -import { createValidator } from '../../Validator'; -import type { CanonicalizedEvent } from '../EventValidators'; - -/** - * Validates the event signatures - * - * Matrix events are cryptographically signed by their originating servers. - * This validator verifies the cryptographic signatures match the canonical form. - * - * Prerequisites: - * - CanonicalizeEvent must have run first to provide canonicalJson - */ -export const validateEventSignature = createValidator(async (event, _, eventId) => { - try { - if (!event.canonicalizedEvent.canonicalJson) { - console.warn(`Event ${eventId} missing canonicalJson from CanonicalizeEvent validator`); - return failure('M_MISSING_CANONICAL_JSON', 'Event missing canonicalJson'); - } - - if (!event.event.signatures) { - console.warn(`Event ${eventId} missing signatures`); - return failure('M_MISSING_SIGNATURES', 'Event is missing required signatures'); - } - - const serverName = extractOrigin(event.event.sender); - const serverSignatures = event.event.signatures[serverName]; - if (!serverSignatures || Object.keys(serverSignatures).length === 0) { - console.warn(`Missing/empty signatures from origin server ${serverName}`); - return failure('M_INVALID_SIGNATURE', `Event is missing signature from origin server ${serverName}`); - } - - const keyId = Object.keys(serverSignatures).find(key => key.includes(':')); - if (!keyId) { - console.warn(`No valid signature key format found for ${serverName}`); - return failure('M_INVALID_SIGNATURE', 'Invalid signature key format'); - } - - const signatureValue = serverSignatures[keyId]; - if (!signatureValue) { - console.warn(`Signature value missing for key ${keyId}`); - return failure('M_INVALID_SIGNATURE', 'Signature value missing'); - } - - const [algorithmStr, version] = keyId.split(':'); - - if (algorithmStr !== EncryptionValidAlgorithm.ed25519) { - console.warn(`Unsupported signature algorithm: ${algorithmStr}`); - return failure('M_INVALID_SIGNATURE', `Unsupported signature algorithm: ${algorithmStr}`); - } - - try { - const localServerName = getServerName(); - - const getPublicKeyFromServer = makeGetPublicKeyFromServerProcedure( - getValidPublicKeyFromLocal, - (origin, key) => getPublicKeyFromRemoteServer(origin, localServerName, key), - storePublicKey - ); - - const publicKeyB64 = await getPublicKeyFromServer(serverName, keyId); - if (!publicKeyB64) { - console.warn(`Public key not found for ${serverName}:${keyId}`); - return failure('M_INVALID_SIGNATURE', 'Public key not found'); - } - - let publicKeyBytes: Uint8Array; - let signatureBytes: Uint8Array; - - try { - publicKeyBytes = Uint8Array.from(Buffer.from(publicKeyB64, 'base64')); - signatureBytes = Uint8Array.from(Buffer.from(signatureValue, 'base64')); - } catch (decodeError) { - console.error(`Failed to decode Base64: ${decodeError}`); - return failure('M_INVALID_SIGNATURE', 'Failed to decode Base64 key or signature'); - } - - const canonicalJson = event.canonicalizedEvent.canonicalJson; - - const isValid = nacl.sign.detached.verify( - new TextEncoder().encode(canonicalJson), - signatureBytes, - publicKeyBytes - ); - - if (!isValid) { - console.warn(`Signature verification failed for ${eventId}`); - return failure('M_INVALID_SIGNATURE', 'Signature verification failed'); - } - - console.info(`Successfully verified signature for event ${eventId}`); - return success(event); - - } catch (error: unknown) { - console.error(`Error during signature verification: ${String(error)}`); - return failure('M_INVALID_SIGNATURE', `Error during signature verification: ${String(error)}`); - } - } catch (error: unknown) { - console.error(`Error validating event signatures: ${String(error)}`); - return failure('M_INVALID_SIGNATURE', `Error validating signatures: ${String(error)}`); - } -}); \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/event/RoomRulesValidator.ts b/packages/homeserver/src/validation/validators/event/RoomRulesValidator.ts deleted file mode 100644 index df13f8601..000000000 --- a/packages/homeserver/src/validation/validators/event/RoomRulesValidator.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { failure, success } from '../../ValidationResult'; -import { createValidator } from '../../Validator'; -import type { AuthorizedEvent } from '../EventValidators'; - -/** - * Validates room auth rules - * - * Matrix rooms have specific authorization rules that determine - * who can send what types of events. This validator enforces these rules. - */ -export const validateRoomRules = createValidator(async (event, txnId, eventId) => { - try { - console.debug(`Validating room rules for event ${eventId}`); - - // Implementation would apply room auth rules - // In a real implementation, this would: - // 1. Check the sender's permissions in the room - // 2. Apply state-dependent rules based on event type - // 3. Validate against the Matrix spec's auth rules section - - const isValid = true; - - if (isValid) { - console.debug(`Room rules validation passed for event ${eventId}`); - return success(event); - } - - console.warn(`Room rules validation failed for event ${eventId}`); - return failure('M_UNAUTHORIZED', 'Event failed room authorization rules'); - } catch (error: any) { - console.error(`Error validating room rules for ${eventId}: ${error.message || String(error)}`); - return failure('M_UNAUTHORIZED', `Error validating room rules: ${error.message || String(error)}`); - } -}); \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/event/index.ts b/packages/homeserver/src/validation/validators/event/index.ts deleted file mode 100644 index f9937053b..000000000 --- a/packages/homeserver/src/validation/validators/event/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './EventHashValidator'; -export * from './EventSignatureValidator'; -export * from './RoomRulesValidator'; -export * from './AuthChainValidator'; -export * from './CanonicalizeEvent'; -export * from './AuthEventsValidator'; \ No newline at end of file diff --git a/packages/homeserver/src/validation/validators/index.ts b/packages/homeserver/src/validation/validators/index.ts deleted file mode 100644 index a6a869e1b..000000000 --- a/packages/homeserver/src/validation/validators/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './EventFormatValidator'; -export * from './EventHashesAndSignaturesValidator'; -export * from './EventTypeSpecificValidator'; -export * from './OutlierDetectionValidator'; -export * from './RoomStateValidator'; -