From 49051b137554d0e5fbde8523787c0be79b04fa53 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 19 Jan 2026 20:42:16 -0300 Subject: [PATCH 01/26] wip: extract required fields from DDP connection in auth hooks --- .../ee/server/lib/deviceManagement/session.ts | 14 ++---- apps/meteor/server/hooks/sauMonitorHooks.ts | 46 +++++++++++++++++-- .../services/device-management/events.ts | 3 +- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/apps/meteor/ee/server/lib/deviceManagement/session.ts b/apps/meteor/ee/server/lib/deviceManagement/session.ts index c5b286c9dff2c..f5d8acb4c0843 100644 --- a/apps/meteor/ee/server/lib/deviceManagement/session.ts +++ b/apps/meteor/ee/server/lib/deviceManagement/session.ts @@ -32,14 +32,14 @@ const uaParser = async ( }; export const listenSessionLogin = () => { - return deviceManagementEvents.on('device-login', async ({ userId, connection }) => { + return deviceManagementEvents.on('device-login', async ({ userId, userAgent, loginToken, clientAddress }) => { const deviceEnabled = settings.get('Device_Management_Enable_Login_Emails'); if (!deviceEnabled) { return; } - if (connection.loginToken) { + if (loginToken) { return; } @@ -67,11 +67,7 @@ export const listenSessionLogin = () => { emails: [{ address: email }], } = user; - const userAgentString = - connection.httpHeaders instanceof Headers - ? (connection.httpHeaders.get('user-agent') ?? '') - : (connection.httpHeaders['user-agent'] ?? ''); - const { browser, os, device, cpu, app } = await uaParser(userAgentString); + const { browser, os, device, cpu, app } = await uaParser(userAgent); const mailData = { name, @@ -81,7 +77,7 @@ export const listenSessionLogin = () => { deviceInfo: `${device.type || t('Device_Management_Device_Unknown')} ${device.vendor || ''} ${device.model || ''} ${ cpu.architecture || '' }`, - ipInfo: connection.clientAddress, + ipInfo: clientAddress, userAgent: '', date: moment().format(String(dateFormat)), }; @@ -105,7 +101,7 @@ export const listenSessionLogin = () => { mailData.deviceInfo = `Desktop App ${cpu.architecture || ''}`; break; default: - mailData.userAgent = connection.httpHeaders['user-agent'] || ''; + mailData.userAgent = userAgent || ''; break; } diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 3137ab41237a1..15e839db2e6b6 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import type { ILoginAttempt } from '../../app/authentication/server/ILoginAttempt'; import { deviceManagementEvents } from '../services/device-management/events'; import { sauEvents } from '../services/sauMonitor/events'; +import { getClientAddress } from '../lib/getClientAddress'; Accounts.onLogin((info: ILoginAttempt) => { const { @@ -19,18 +20,43 @@ Accounts.onLogin((info: ILoginAttempt) => { } const { resume } = methodArguments.find((arg) => 'resume' in arg) ?? {}; + const loginToken = resume ? Accounts._hashLoginToken(resume) : ''; + // const instanceId = InstanceStatus.id(); + const userId = info.user._id; + // const connectionId = info.connection.id; + const clientAddress = getClientAddress(info.connection); + const userAgent = getHeader(httpHeaders, 'user-agent'); + // const host = getHeader(httpHeaders, 'host'); - const eventObject = { + const eventObject = { userId: info.user._id, connection: { ...info.connection, ...(resume && { loginToken: Accounts._hashLoginToken(resume) }), instanceId: InstanceStatus.id(), - httpHeaders: httpHeaders as IncomingHttpHeaders, + httpHeaders, }, }; + + // const loginEventObject = { + // userId, + // instanceId, + // userAgent, + // loginToken, + // connectionId, + // clientAddress, + // host, + // }; + // sauEvents.emit('accounts.login', eventObject); - deviceManagementEvents.emit('device-login', eventObject); + + const deviceLoginEventObject = { + userId, + userAgent, + loginToken, + clientAddress, + } + deviceManagementEvents.emit('device-login', deviceLoginEventObject); }); Accounts.onLogout((info) => { @@ -58,6 +84,18 @@ Meteor.onConnection((connection) => { Meteor.onConnection((connection) => { const { httpHeaders } = connection; - sauEvents.emit('socket.connected', { instanceId: InstanceStatus.id(), ...connection, httpHeaders: httpHeaders as IncomingHttpHeaders }); }); + +// TODO: extract this function to another file +const getHeader = (headers: unknown, key: string) : string => { + if (!headers) { + return ''; + } + + if (typeof (headers as any).get === 'function') { + return (headers as Headers).get(key) ?? ''; + } + + return (headers as Record)[key] || ''; +} diff --git a/apps/meteor/server/services/device-management/events.ts b/apps/meteor/server/services/device-management/events.ts index 5f99c63da472f..76d836d51bbab 100644 --- a/apps/meteor/server/services/device-management/events.ts +++ b/apps/meteor/server/services/device-management/events.ts @@ -1,6 +1,5 @@ -import type { ISocketConnectionLogged } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const deviceManagementEvents = new Emitter<{ - 'device-login': { userId: string; connection: ISocketConnectionLogged }; + 'device-login': { userId: string; userAgent: string, loginToken: string, clientAddress: string }; }>(); From 3ca9f1672c2df321bc85b3c826ce5483ad402f32 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 20 Jan 2026 15:24:57 -0300 Subject: [PATCH 02/26] wip: flag commit --- .../app/statistics/server/lib/SAUMonitor.ts | 57 +++++--------- apps/meteor/server/hooks/sauMonitorHooks.ts | 74 ++++++++----------- .../services/device-management/events.ts | 3 +- .../services/device-management/service.ts | 5 +- .../server/services/sauMonitor/events.ts | 4 +- ee/apps/ddp-streamer/src/DDPStreamer.ts | 12 ++- packages/core-services/src/events/Events.ts | 3 +- .../core-typings/src/DeviceLoginPayload.ts | 7 ++ .../core-typings/src/LoginSessionPayload.ts | 9 +++ packages/core-typings/src/index.ts | 3 + 10 files changed, 88 insertions(+), 89 deletions(-) create mode 100644 packages/core-typings/src/DeviceLoginPayload.ts create mode 100644 packages/core-typings/src/LoginSessionPayload.ts diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 3beb3639cf3b0..66b11d7a47fd7 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -1,4 +1,4 @@ -import type { ISession, ISessionDevice, ISocketConnectionLogged, IUser } from '@rocket.chat/core-typings'; +import type { ISession, ISessionDevice, IUser, LoginSessionPayload } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Logger } from '@rocket.chat/logger'; import { Sessions, Users, aggregates } from '@rocket.chat/models'; @@ -8,7 +8,6 @@ import UAParser from 'ua-parser-js'; import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; import { getMostImportantRole } from '../../../../lib/roles/getMostImportantRole'; -import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { sauEvents } from '../../../../server/services/sauMonitor/events'; type DateObj = { day: number; month: number; year: number }; @@ -111,7 +110,7 @@ export class SAUMonitorClass { return; } - sauEvents.on('accounts.login', async ({ userId, connection }) => { + sauEvents.on('accounts.login', async ({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload) => { if (!this.isRunning()) { return; } @@ -121,8 +120,8 @@ export class SAUMonitorClass { const mostImportantRole = getMostImportantRole(roles); const loginAt = new Date(); - const params = { userId, roles, mostImportantRole, loginAt, ...getDateObj() }; - await this._handleSession(connection, params); + const params = { roles, mostImportantRole, loginAt, ...getDateObj() }; + await this._handleSession({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }, params); }); sauEvents.on('accounts.logout', async ({ userId, connection }) => { @@ -157,10 +156,20 @@ export class SAUMonitorClass { } private async _handleSession( - connection: ISocketConnectionLogged, - params: Pick, + { userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host } : LoginSessionPayload, + params: Pick, ): Promise { - const data = this._getConnectionInfo(connection, params); + const data: Omit = { + userId, + loginToken, + ip: clientAddress, + host, + sessionId: connectionId, + instanceId, + type: 'session', + ...this._getUserAgentInfo(userAgent), + ...params, + } if (!data) { return; @@ -221,37 +230,7 @@ export class SAUMonitorClass { .join(''); } - private _getConnectionInfo( - connection: ISocketConnectionLogged, - params: Pick, - ): Omit | undefined { - if (!connection) { - return; - } - - const ip = getClientAddress(connection); - - const host = connection.httpHeaders?.host ?? ''; - - return { - type: 'session', - sessionId: connection.id, - instanceId: connection.instanceId, - ...(connection.loginToken && { loginToken: connection.loginToken }), - ip, - host, - ...this._getUserAgentInfo(connection), - ...params, - }; - } - - private _getUserAgentInfo(connection: ISocketConnectionLogged): { device: ISessionDevice } | undefined { - if (!connection?.httpHeaders?.['user-agent']) { - return; - } - - const uaString = connection.httpHeaders['user-agent']; - + private _getUserAgentInfo(uaString: string): { device: ISessionDevice } | undefined { // TODO define a type for "result" below // | UAParser.IResult // | { device: { type: string; model?: string }; browser: undefined; os: undefined; app: { name: string; version: string } } diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 15e839db2e6b6..619a62bf1b4d2 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -5,9 +5,10 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ILoginAttempt } from '../../app/authentication/server/ILoginAttempt'; +import { getClientAddress } from '../lib/getClientAddress'; import { deviceManagementEvents } from '../services/device-management/events'; import { sauEvents } from '../services/sauMonitor/events'; -import { getClientAddress } from '../lib/getClientAddress'; +import { LoginSessionPayload, DeviceLoginPayload } from '@rocket.chat/core-typings'; Accounts.onLogin((info: ILoginAttempt) => { const { @@ -20,42 +21,31 @@ Accounts.onLogin((info: ILoginAttempt) => { } const { resume } = methodArguments.find((arg) => 'resume' in arg) ?? {}; - const loginToken = resume ? Accounts._hashLoginToken(resume) : ''; - // const instanceId = InstanceStatus.id(); - const userId = info.user._id; - // const connectionId = info.connection.id; - const clientAddress = getClientAddress(info.connection); - const userAgent = getHeader(httpHeaders, 'user-agent'); - // const host = getHeader(httpHeaders, 'host'); + const loginToken = resume ? Accounts._hashLoginToken(resume) : ''; + const instanceId = InstanceStatus.id(); + const userId = info.user._id; + const connectionId = info.connection.id; + const clientAddress = getClientAddress(info.connection); + const userAgent = getHeader(httpHeaders, 'user-agent'); + const host = getHeader(httpHeaders, 'host'); - const eventObject = { - userId: info.user._id, - connection: { - ...info.connection, - ...(resume && { loginToken: Accounts._hashLoginToken(resume) }), - instanceId: InstanceStatus.id(), - httpHeaders, - }, + const loginEventObject: LoginSessionPayload = { + userId, + instanceId, + userAgent, + loginToken, + connectionId, + clientAddress, + host, }; + sauEvents.emit('accounts.login', loginEventObject); - // const loginEventObject = { - // userId, - // instanceId, - // userAgent, - // loginToken, - // connectionId, - // clientAddress, - // host, - // }; - // - sauEvents.emit('accounts.login', eventObject); - - const deviceLoginEventObject = { - userId, - userAgent, + const deviceLoginEventObject: DeviceLoginPayload = { + userId, + userAgent, loginToken, - clientAddress, - } + clientAddress, + }; deviceManagementEvents.emit('device-login', deviceLoginEventObject); }); @@ -88,14 +78,14 @@ Meteor.onConnection((connection) => { }); // TODO: extract this function to another file -const getHeader = (headers: unknown, key: string) : string => { - if (!headers) { - return ''; - } +const getHeader = (headers: unknown, key: string): string => { + if (!headers) { + return ''; + } - if (typeof (headers as any).get === 'function') { - return (headers as Headers).get(key) ?? ''; - } + if (typeof (headers as any).get === 'function') { + return (headers as Headers).get(key) ?? ''; + } - return (headers as Record)[key] || ''; -} + return (headers as Record)[key] || ''; +}; diff --git a/apps/meteor/server/services/device-management/events.ts b/apps/meteor/server/services/device-management/events.ts index 76d836d51bbab..1c3c94dba3b52 100644 --- a/apps/meteor/server/services/device-management/events.ts +++ b/apps/meteor/server/services/device-management/events.ts @@ -1,5 +1,6 @@ import { Emitter } from '@rocket.chat/emitter'; +import type { DeviceLoginPayload } from '@rocket.chat/core-typings'; export const deviceManagementEvents = new Emitter<{ - 'device-login': { userId: string; userAgent: string, loginToken: string, clientAddress: string }; + 'device-login': DeviceLoginPayload; }>(); diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index 75f3639088922..cf66d9e1d2125 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -1,5 +1,6 @@ import type { IDeviceManagementService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; +import type { LoginSessionPayload } from '@rocket.chat/core-typings'; import { deviceManagementEvents } from './events'; @@ -9,9 +10,9 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe constructor() { super(); - this.onEvent('accounts.login', async (data) => { + this.onEvent('accounts.login', async ({ userId, userAgent, loginToken, clientAddress }: LoginSessionPayload) => { // TODO need to add loginToken to data - deviceManagementEvents.emit('device-login', data); + deviceManagementEvents.emit('device-login', { userId, userAgent, loginToken, clientAddress }); }); } } diff --git a/apps/meteor/server/services/sauMonitor/events.ts b/apps/meteor/server/services/sauMonitor/events.ts index 44b971a91a70e..bf54cc2da14aa 100644 --- a/apps/meteor/server/services/sauMonitor/events.ts +++ b/apps/meteor/server/services/sauMonitor/events.ts @@ -1,8 +1,8 @@ -import type { ISocketConnection, ISocketConnectionLogged } from '@rocket.chat/core-typings'; +import type { ISocketConnection, LoginSessionPayload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const sauEvents = new Emitter<{ - 'accounts.login': { userId: string; connection: ISocketConnectionLogged }; + 'accounts.login': LoginSessionPayload; 'accounts.logout': { userId: string; connection: ISocketConnection }; 'socket.connected': ISocketConnection; 'socket.disconnected': ISocketConnection; diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 1ea6281d9fc0a..5c8647f5af4cc 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -15,6 +15,7 @@ import { proxy } from './proxy'; import { ListenersModule } from '../../../../apps/meteor/server/modules/listeners/listeners.module'; import type { NotificationsModule } from '../../../../apps/meteor/server/modules/notifications/notifications.module'; import { StreamerCentral } from '../../../../apps/meteor/server/modules/streamer/streamer.module'; +import { getClientAddress } from '../../../../apps/meteor/server/lib/getClientAddress'; const { PORT = 4000 } = process.env; @@ -168,7 +169,7 @@ export class DDPStreamer extends ServiceClass { } server.on(DDP_EVENTS.LOGGED, async (info: Client) => { const { userId, connection } = info; - + console.log(info) if (!userId) { throw new Error('User not logged in'); } @@ -182,7 +183,14 @@ export class DDPStreamer extends ServiceClass { server.emit('presence', { userId, connection }); - this.api?.broadcast('accounts.login', { userId, connection }); + this.api?.broadcast('accounts.login', { + userId, + connectionId: connection.id, + userAgent: connection.httpHeaders?.['user-agent'] || '', + clientAddress: getClientAddress(connection), + instanceId: nodeID, + host: connection.httpHeaders?.['host'] || '' + }); }); server.on(DDP_EVENTS.LOGGEDOUT, (info) => { diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index b80b6a7d90061..5ea3a75eea9e0 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -32,6 +32,7 @@ import type { ICustomUserStatus, IWebdavAccount, MessageAttachment, + LoginSessionPayload, } from '@rocket.chat/core-typings'; import type { ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling'; import type * as UiKit from '@rocket.chat/ui-kit'; @@ -57,7 +58,7 @@ export type EventSignatures = { 'room.video-conference': (params: { rid: string; callId: string }) => void; 'shutdown': (params: Record) => void; '$services.changed': (info: { localService: boolean }) => void; - 'accounts.login': (info: { userId: string; connection: ISocketConnection }) => void; + 'accounts.login': (info: LoginSessionPayload) => void; 'accounts.logout': (info: { userId: string; connection: ISocketConnection }) => void; 'authorization.guestPermissions': (permissions: string[]) => void; 'socket.connected': (connection: ISocketConnection) => void; diff --git a/packages/core-typings/src/DeviceLoginPayload.ts b/packages/core-typings/src/DeviceLoginPayload.ts new file mode 100644 index 0000000000000..780256e170811 --- /dev/null +++ b/packages/core-typings/src/DeviceLoginPayload.ts @@ -0,0 +1,7 @@ +export type DeviceLoginPayload = { + userId: string; + userAgent: string; + loginToken: string; + clientAddress: string; +} + diff --git a/packages/core-typings/src/LoginSessionPayload.ts b/packages/core-typings/src/LoginSessionPayload.ts new file mode 100644 index 0000000000000..0b048ef9cc118 --- /dev/null +++ b/packages/core-typings/src/LoginSessionPayload.ts @@ -0,0 +1,9 @@ +export type LoginSessionPayload = { + userId: string; + instanceId: string; + userAgent: string; + loginToken?: string; + connectionId: string; + clientAddress: string; + host: string; +}; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index befca6827c4cf..56899106bdb38 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -145,4 +145,7 @@ export * from './IAbacAttribute'; export * from './Abac'; export * from './ServerAudit/IAuditServerAbacAction'; +export * from './LoginSessionPayload'; +export * from './DeviceLoginPayload'; + export { schemas } from './Ajv'; From 0b0a9a2d9b109aada9b25be77954162d9766e632 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Wed, 21 Jan 2026 11:59:34 -0300 Subject: [PATCH 03/26] run linter and fix DeviceLoginPayload type issue --- .../app/statistics/server/lib/SAUMonitor.ts | 43 ++++++++++--------- apps/meteor/server/hooks/sauMonitorHooks.ts | 16 +++---- .../services/device-management/events.ts | 2 +- ee/apps/ddp-streamer/src/DDPStreamer.ts | 18 ++++---- packages/core-services/src/events/Events.ts | 2 +- .../core-typings/src/DeviceLoginPayload.ts | 11 +++-- .../core-typings/src/LoginSessionPayload.ts | 2 +- 7 files changed, 48 insertions(+), 46 deletions(-) diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 66b11d7a47fd7..a6ae679fc0702 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -110,19 +110,22 @@ export class SAUMonitorClass { return; } - sauEvents.on('accounts.login', async ({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload) => { - if (!this.isRunning()) { - return; - } + sauEvents.on( + 'accounts.login', + async ({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload) => { + if (!this.isRunning()) { + return; + } - const roles = await getUserRoles(userId); + const roles = await getUserRoles(userId); - const mostImportantRole = getMostImportantRole(roles); + const mostImportantRole = getMostImportantRole(roles); - const loginAt = new Date(); - const params = { roles, mostImportantRole, loginAt, ...getDateObj() }; - await this._handleSession({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }, params); - }); + const loginAt = new Date(); + const params = { roles, mostImportantRole, loginAt, ...getDateObj() }; + await this._handleSession({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }, params); + }, + ); sauEvents.on('accounts.logout', async ({ userId, connection }) => { if (!this.isRunning()) { @@ -156,20 +159,20 @@ export class SAUMonitorClass { } private async _handleSession( - { userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host } : LoginSessionPayload, + { userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload, params: Pick, ): Promise { - const data: Omit = { - userId, - loginToken, - ip: clientAddress, - host, - sessionId: connectionId, - instanceId, - type: 'session', + const data: Omit = { + userId, + loginToken, + ip: clientAddress, + host, + sessionId: connectionId, + instanceId, + type: 'session', ...this._getUserAgentInfo(userAgent), ...params, - } + }; if (!data) { return; diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 619a62bf1b4d2..13769a71f4a3e 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -1,5 +1,6 @@ import type { IncomingHttpHeaders } from 'http'; +import type { LoginSessionPayload, DeviceLoginPayload } from '@rocket.chat/core-typings'; import { InstanceStatus } from '@rocket.chat/instance-status'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; @@ -8,7 +9,6 @@ import type { ILoginAttempt } from '../../app/authentication/server/ILoginAttemp import { getClientAddress } from '../lib/getClientAddress'; import { deviceManagementEvents } from '../services/device-management/events'; import { sauEvents } from '../services/sauMonitor/events'; -import { LoginSessionPayload, DeviceLoginPayload } from '@rocket.chat/core-typings'; Accounts.onLogin((info: ILoginAttempt) => { const { @@ -30,13 +30,13 @@ Accounts.onLogin((info: ILoginAttempt) => { const host = getHeader(httpHeaders, 'host'); const loginEventObject: LoginSessionPayload = { - userId, - instanceId, - userAgent, - loginToken, - connectionId, - clientAddress, - host, + userId, + instanceId, + userAgent, + loginToken, + connectionId, + clientAddress, + host, }; sauEvents.emit('accounts.login', loginEventObject); diff --git a/apps/meteor/server/services/device-management/events.ts b/apps/meteor/server/services/device-management/events.ts index 1c3c94dba3b52..b40df77de2c83 100644 --- a/apps/meteor/server/services/device-management/events.ts +++ b/apps/meteor/server/services/device-management/events.ts @@ -1,5 +1,5 @@ -import { Emitter } from '@rocket.chat/emitter'; import type { DeviceLoginPayload } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; export const deviceManagementEvents = new Emitter<{ 'device-login': DeviceLoginPayload; diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 5c8647f5af4cc..822d6b588ffba 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -12,10 +12,10 @@ import { events, server } from './configureServer'; import { DDP_EVENTS } from './constants'; import { Autoupdate } from './lib/Autoupdate'; import { proxy } from './proxy'; +import { getClientAddress } from '../../../../apps/meteor/server/lib/getClientAddress'; import { ListenersModule } from '../../../../apps/meteor/server/modules/listeners/listeners.module'; import type { NotificationsModule } from '../../../../apps/meteor/server/modules/notifications/notifications.module'; import { StreamerCentral } from '../../../../apps/meteor/server/modules/streamer/streamer.module'; -import { getClientAddress } from '../../../../apps/meteor/server/lib/getClientAddress'; const { PORT = 4000 } = process.env; @@ -169,7 +169,7 @@ export class DDPStreamer extends ServiceClass { } server.on(DDP_EVENTS.LOGGED, async (info: Client) => { const { userId, connection } = info; - console.log(info) + if (!userId) { throw new Error('User not logged in'); } @@ -184,13 +184,13 @@ export class DDPStreamer extends ServiceClass { server.emit('presence', { userId, connection }); this.api?.broadcast('accounts.login', { - userId, - connectionId: connection.id, - userAgent: connection.httpHeaders?.['user-agent'] || '', - clientAddress: getClientAddress(connection), - instanceId: nodeID, - host: connection.httpHeaders?.['host'] || '' - }); + userId, + connectionId: connection.id, + userAgent: connection.httpHeaders?.['user-agent'] || '', + clientAddress: getClientAddress(connection), + instanceId: nodeID, + host: connection.httpHeaders?.host || '', + }); }); server.on(DDP_EVENTS.LOGGEDOUT, (info) => { diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index 5ea3a75eea9e0..e7d3349f83890 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -32,7 +32,7 @@ import type { ICustomUserStatus, IWebdavAccount, MessageAttachment, - LoginSessionPayload, + LoginSessionPayload, } from '@rocket.chat/core-typings'; import type { ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling'; import type * as UiKit from '@rocket.chat/ui-kit'; diff --git a/packages/core-typings/src/DeviceLoginPayload.ts b/packages/core-typings/src/DeviceLoginPayload.ts index 780256e170811..b7eb03fb8bab8 100644 --- a/packages/core-typings/src/DeviceLoginPayload.ts +++ b/packages/core-typings/src/DeviceLoginPayload.ts @@ -1,7 +1,6 @@ export type DeviceLoginPayload = { - userId: string; - userAgent: string; - loginToken: string; - clientAddress: string; -} - + userId: string; + userAgent: string; + loginToken?: string; + clientAddress: string; +}; diff --git a/packages/core-typings/src/LoginSessionPayload.ts b/packages/core-typings/src/LoginSessionPayload.ts index 0b048ef9cc118..0936592912452 100644 --- a/packages/core-typings/src/LoginSessionPayload.ts +++ b/packages/core-typings/src/LoginSessionPayload.ts @@ -1,5 +1,5 @@ export type LoginSessionPayload = { - userId: string; + userId: string; instanceId: string; userAgent: string; loginToken?: string; From 7bbe051eb08a9ff64ba11ccd8b9a8b1f069bef0f Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Wed, 21 Jan 2026 14:51:41 -0300 Subject: [PATCH 04/26] add LogoutSesionPayload type use --- apps/meteor/app/statistics/server/lib/SAUMonitor.ts | 5 ++--- apps/meteor/server/hooks/sauMonitorHooks.ts | 5 ++--- apps/meteor/server/services/sauMonitor/events.ts | 4 ++-- ee/apps/ddp-streamer/src/DDPStreamer.ts | 2 +- packages/core-services/src/events/Events.ts | 3 ++- packages/core-typings/src/LogoutSessionPayload.ts | 4 ++++ packages/core-typings/src/index.ts | 1 + 7 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 packages/core-typings/src/LogoutSessionPayload.ts diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index a6ae679fc0702..81df37b398a4e 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -1,4 +1,4 @@ -import type { ISession, ISessionDevice, IUser, LoginSessionPayload } from '@rocket.chat/core-typings'; +import type { ISession, ISessionDevice, IUser, LoginSessionPayload, LogoutSessionPayload } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Logger } from '@rocket.chat/logger'; import { Sessions, Users, aggregates } from '@rocket.chat/models'; @@ -127,7 +127,7 @@ export class SAUMonitorClass { }, ); - sauEvents.on('accounts.logout', async ({ userId, connection }) => { + sauEvents.on('accounts.logout', async ({ userId, sessionId }: LogoutSessionPayload) => { if (!this.isRunning()) { return; } @@ -137,7 +137,6 @@ export class SAUMonitorClass { return; } - const { id: sessionId } = connection; if (!sessionId) { logger.warn({ msg: "Received 'accounts.logout' event without 'sessionId'" }); return; diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 13769a71f4a3e..ce2432fd2d655 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -50,14 +50,13 @@ Accounts.onLogin((info: ILoginAttempt) => { }); Accounts.onLogout((info) => { - const { httpHeaders } = info.connection; - if (!info.user) { return; } + sauEvents.emit('accounts.logout', { userId: info.user._id, - connection: { instanceId: InstanceStatus.id(), ...info.connection, httpHeaders: httpHeaders as IncomingHttpHeaders }, + sessionId: info.connection.id, }); }); diff --git a/apps/meteor/server/services/sauMonitor/events.ts b/apps/meteor/server/services/sauMonitor/events.ts index bf54cc2da14aa..73b073704b1e6 100644 --- a/apps/meteor/server/services/sauMonitor/events.ts +++ b/apps/meteor/server/services/sauMonitor/events.ts @@ -1,9 +1,9 @@ -import type { ISocketConnection, LoginSessionPayload } from '@rocket.chat/core-typings'; +import type { ISocketConnection, LoginSessionPayload, LogoutSessionPayload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const sauEvents = new Emitter<{ 'accounts.login': LoginSessionPayload; - 'accounts.logout': { userId: string; connection: ISocketConnection }; + 'accounts.logout': LogoutSessionPayload; 'socket.connected': ISocketConnection; 'socket.disconnected': ISocketConnection; }>(); diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 822d6b588ffba..8c861255446c9 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -196,7 +196,7 @@ export class DDPStreamer extends ServiceClass { server.on(DDP_EVENTS.LOGGEDOUT, (info) => { const { userId, connection } = info; - this.api?.broadcast('accounts.logout', { userId, connection }); + this.api?.broadcast('accounts.logout', { userId, sessionId: connection.id }); this.updateConnections(); diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index e7d3349f83890..e16e72f339cdd 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -33,6 +33,7 @@ import type { IWebdavAccount, MessageAttachment, LoginSessionPayload, + LogoutSessionPayload, } from '@rocket.chat/core-typings'; import type { ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling'; import type * as UiKit from '@rocket.chat/ui-kit'; @@ -59,7 +60,7 @@ export type EventSignatures = { 'shutdown': (params: Record) => void; '$services.changed': (info: { localService: boolean }) => void; 'accounts.login': (info: LoginSessionPayload) => void; - 'accounts.logout': (info: { userId: string; connection: ISocketConnection }) => void; + 'accounts.logout': (info: LogoutSessionPayload) => void; 'authorization.guestPermissions': (permissions: string[]) => void; 'socket.connected': (connection: ISocketConnection) => void; 'socket.disconnected': (connection: ISocketConnection) => void; diff --git a/packages/core-typings/src/LogoutSessionPayload.ts b/packages/core-typings/src/LogoutSessionPayload.ts new file mode 100644 index 0000000000000..a4c0b55b80362 --- /dev/null +++ b/packages/core-typings/src/LogoutSessionPayload.ts @@ -0,0 +1,4 @@ +export type LogoutSessionPayload = { + userId?: string; + sessionId?: string; +}; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 56899106bdb38..233742926ef23 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -147,5 +147,6 @@ export * from './ServerAudit/IAuditServerAbacAction'; export * from './LoginSessionPayload'; export * from './DeviceLoginPayload'; +export * from './LogoutSessionPayload'; export { schemas } from './Ajv'; From d02ff3427a3372444d97fd860db069218b32544d Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Wed, 21 Jan 2026 19:04:50 -0300 Subject: [PATCH 05/26] add properties funneling on sau events --- .../app/statistics/server/lib/SAUMonitor.ts | 4 +-- apps/meteor/server/hooks/sauMonitorHooks.ts | 34 ++++++++----------- apps/meteor/server/lib/getHeader.ts | 11 ++++++ .../services/device-management/service.ts | 9 +++-- .../server/services/sauMonitor/events.ts | 8 ++--- .../server/services/sauMonitor/service.ts | 21 ++++++++---- ee/apps/ddp-streamer/src/DDPStreamer.ts | 12 ++----- packages/core-services/src/events/Events.ts | 6 ++-- 8 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 apps/meteor/server/lib/getHeader.ts diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 81df37b398a4e..3880a6e0384a6 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -111,7 +111,7 @@ export class SAUMonitorClass { } sauEvents.on( - 'accounts.login', + 'sau.accounts.login', async ({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload) => { if (!this.isRunning()) { return; @@ -127,7 +127,7 @@ export class SAUMonitorClass { }, ); - sauEvents.on('accounts.logout', async ({ userId, sessionId }: LogoutSessionPayload) => { + sauEvents.on('sau.accounts.logout', async ({ userId, sessionId }: LogoutSessionPayload) => { if (!this.isRunning()) { return; } diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index ce2432fd2d655..122ce09886852 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -1,12 +1,13 @@ import type { IncomingHttpHeaders } from 'http'; -import type { LoginSessionPayload, DeviceLoginPayload } from '@rocket.chat/core-typings'; +import type { LoginSessionPayload, LogoutSessionPayload, DeviceLoginPayload } from '@rocket.chat/core-typings'; import { InstanceStatus } from '@rocket.chat/instance-status'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ILoginAttempt } from '../../app/authentication/server/ILoginAttempt'; import { getClientAddress } from '../lib/getClientAddress'; +import { getHeader } from '../lib/getHeader'; import { deviceManagementEvents } from '../services/device-management/events'; import { sauEvents } from '../services/sauMonitor/events'; @@ -38,7 +39,7 @@ Accounts.onLogin((info: ILoginAttempt) => { clientAddress, host, }; - sauEvents.emit('accounts.login', loginEventObject); + sauEvents.emit('sau.accounts.login', loginEventObject); const deviceLoginEventObject: DeviceLoginPayload = { userId, @@ -54,16 +55,18 @@ Accounts.onLogout((info) => { return; } - sauEvents.emit('accounts.logout', { + const logoutEventObject: LogoutSessionPayload = { userId: info.user._id, - sessionId: info.connection.id, - }); + sessionId: info.connection.id, + }; + + sauEvents.emit('sau.accounts.logout', logoutEventObject); }); Meteor.onConnection((connection) => { connection.onClose(async () => { const { httpHeaders } = connection; - sauEvents.emit('socket.disconnected', { + sauEvents.emit('sau.socket.disconnected', { instanceId: InstanceStatus.id(), ...connection, httpHeaders: httpHeaders as IncomingHttpHeaders, @@ -73,18 +76,9 @@ Meteor.onConnection((connection) => { Meteor.onConnection((connection) => { const { httpHeaders } = connection; - sauEvents.emit('socket.connected', { instanceId: InstanceStatus.id(), ...connection, httpHeaders: httpHeaders as IncomingHttpHeaders }); + sauEvents.emit('sau.socket.connected', { + instanceId: InstanceStatus.id(), + ...connection, + httpHeaders: httpHeaders as IncomingHttpHeaders, + }); }); - -// TODO: extract this function to another file -const getHeader = (headers: unknown, key: string): string => { - if (!headers) { - return ''; - } - - if (typeof (headers as any).get === 'function') { - return (headers as Headers).get(key) ?? ''; - } - - return (headers as Record)[key] || ''; -}; diff --git a/apps/meteor/server/lib/getHeader.ts b/apps/meteor/server/lib/getHeader.ts new file mode 100644 index 0000000000000..cd8a7eaa781a6 --- /dev/null +++ b/apps/meteor/server/lib/getHeader.ts @@ -0,0 +1,11 @@ +export const getHeader = (headers: unknown, key: string): string => { + if (!headers) { + return ''; + } + + if (typeof (headers as any).get === 'function') { + return (headers as Headers).get(key) ?? ''; + } + + return (headers as Record)[key] || ''; +}; diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index cf66d9e1d2125..0592473a97997 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -1,8 +1,9 @@ import type { IDeviceManagementService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { LoginSessionPayload } from '@rocket.chat/core-typings'; import { deviceManagementEvents } from './events'; +import { getClientAddress } from '../../lib/getClientAddress'; +import { getHeader } from '../../lib/getHeader'; export class DeviceManagementService extends ServiceClassInternal implements IDeviceManagementService { protected name = 'device-management'; @@ -10,9 +11,11 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe constructor() { super(); - this.onEvent('accounts.login', async ({ userId, userAgent, loginToken, clientAddress }: LoginSessionPayload) => { + this.onEvent('accounts.login', async ({ userId, connection }) => { + const clientAddress = getClientAddress(connection); + const userAgent = getHeader(connection.httpHeaders, 'user-agent'); // TODO need to add loginToken to data - deviceManagementEvents.emit('device-login', { userId, userAgent, loginToken, clientAddress }); + deviceManagementEvents.emit('device-login', { userId, userAgent, clientAddress }); }); } } diff --git a/apps/meteor/server/services/sauMonitor/events.ts b/apps/meteor/server/services/sauMonitor/events.ts index 73b073704b1e6..8ffb3560a6e6d 100644 --- a/apps/meteor/server/services/sauMonitor/events.ts +++ b/apps/meteor/server/services/sauMonitor/events.ts @@ -2,8 +2,8 @@ import type { ISocketConnection, LoginSessionPayload, LogoutSessionPayload } fro import { Emitter } from '@rocket.chat/emitter'; export const sauEvents = new Emitter<{ - 'accounts.login': LoginSessionPayload; - 'accounts.logout': LogoutSessionPayload; - 'socket.connected': ISocketConnection; - 'socket.disconnected': ISocketConnection; + 'sau.accounts.login': LoginSessionPayload; + 'sau.accounts.logout': LogoutSessionPayload; + 'sau.socket.connected': ISocketConnection; + 'sau.socket.disconnected': ISocketConnection; }>(); diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index a0b36bb201633..724bf5f199ff1 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -2,8 +2,11 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { ISAUMonitorService } from '@rocket.chat/core-services'; +import { InstanceStatus } from '@rocket.chat/instance-status'; import { sauEvents } from './events'; +import { getClientAddress } from '../../lib/getClientAddress'; +import { getHeader } from '../../lib/getHeader'; export class SAUMonitorService extends ServiceClassInternal implements ISAUMonitorService { protected name = 'sau-monitor'; @@ -11,22 +14,28 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit constructor() { super(); - this.onEvent('accounts.login', async (data) => { - sauEvents.emit('accounts.login', data); + this.onEvent('accounts.login', async ({ userId, connection }) => { + const instanceId = InstanceStatus.id(); + const connectionId = connection.id; + const clientAddress = getClientAddress(connection); + const userAgent = getHeader(connection.httpHeaders, 'user-agent'); + const host = getHeader(connection.httpHeaders, 'host'); + + sauEvents.emit('sau.accounts.login', { userId, instanceId, connectionId, clientAddress, userAgent, host }); }); - this.onEvent('accounts.logout', async (data) => { - sauEvents.emit('accounts.logout', data); + this.onEvent('accounts.logout', async ({ userId, connection }) => { + sauEvents.emit('sau.accounts.logout', { userId, sessionId: connection.id }); }); this.onEvent('socket.disconnected', async (data) => { // console.log('socket.disconnected', data); - sauEvents.emit('socket.disconnected', data); + sauEvents.emit('sau.socket.disconnected', data); }); this.onEvent('socket.connected', async (data) => { // console.log('socket.connected', data); - sauEvents.emit('socket.connected', data); + sauEvents.emit('sau.socket.connected', data); }); } } diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 8c861255446c9..1ea6281d9fc0a 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -12,7 +12,6 @@ import { events, server } from './configureServer'; import { DDP_EVENTS } from './constants'; import { Autoupdate } from './lib/Autoupdate'; import { proxy } from './proxy'; -import { getClientAddress } from '../../../../apps/meteor/server/lib/getClientAddress'; import { ListenersModule } from '../../../../apps/meteor/server/modules/listeners/listeners.module'; import type { NotificationsModule } from '../../../../apps/meteor/server/modules/notifications/notifications.module'; import { StreamerCentral } from '../../../../apps/meteor/server/modules/streamer/streamer.module'; @@ -183,20 +182,13 @@ export class DDPStreamer extends ServiceClass { server.emit('presence', { userId, connection }); - this.api?.broadcast('accounts.login', { - userId, - connectionId: connection.id, - userAgent: connection.httpHeaders?.['user-agent'] || '', - clientAddress: getClientAddress(connection), - instanceId: nodeID, - host: connection.httpHeaders?.host || '', - }); + this.api?.broadcast('accounts.login', { userId, connection }); }); server.on(DDP_EVENTS.LOGGEDOUT, (info) => { const { userId, connection } = info; - this.api?.broadcast('accounts.logout', { userId, sessionId: connection.id }); + this.api?.broadcast('accounts.logout', { userId, connection }); this.updateConnections(); diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index e16e72f339cdd..b80b6a7d90061 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -32,8 +32,6 @@ import type { ICustomUserStatus, IWebdavAccount, MessageAttachment, - LoginSessionPayload, - LogoutSessionPayload, } from '@rocket.chat/core-typings'; import type { ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling'; import type * as UiKit from '@rocket.chat/ui-kit'; @@ -59,8 +57,8 @@ export type EventSignatures = { 'room.video-conference': (params: { rid: string; callId: string }) => void; 'shutdown': (params: Record) => void; '$services.changed': (info: { localService: boolean }) => void; - 'accounts.login': (info: LoginSessionPayload) => void; - 'accounts.logout': (info: LogoutSessionPayload) => void; + 'accounts.login': (info: { userId: string; connection: ISocketConnection }) => void; + 'accounts.logout': (info: { userId: string; connection: ISocketConnection }) => void; 'authorization.guestPermissions': (permissions: string[]) => void; 'socket.connected': (connection: ISocketConnection) => void; 'socket.disconnected': (connection: ISocketConnection) => void; From 3d6bfed99378b774e3164959a74941695d83a983 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Thu, 22 Jan 2026 12:39:49 -0300 Subject: [PATCH 06/26] move types to sau folder, enhance socket.disconnected parameters --- .../app/statistics/server/lib/SAUMonitor.ts | 4 +-- apps/meteor/server/hooks/sauMonitorHooks.ts | 28 ++++++++++--------- .../server/services/sauMonitor/events.ts | 11 ++++++-- .../server/services/sauMonitor/service.ts | 3 +- packages/core-typings/src/index.ts | 7 +++-- .../src/{ => sau}/LoginSessionPayload.ts | 0 .../src/{ => sau}/LogoutSessionPayload.ts | 0 .../src/sau/SocketConnectedPayload.ts | 3 ++ .../src/sau/SocketDisconnectedPayload.ts | 4 +++ 9 files changed, 38 insertions(+), 22 deletions(-) rename packages/core-typings/src/{ => sau}/LoginSessionPayload.ts (100%) rename packages/core-typings/src/{ => sau}/LogoutSessionPayload.ts (100%) create mode 100644 packages/core-typings/src/sau/SocketConnectedPayload.ts create mode 100644 packages/core-typings/src/sau/SocketDisconnectedPayload.ts diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 3880a6e0384a6..968e13c1dcb7e 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -96,12 +96,12 @@ export class SAUMonitorClass { return; } - sauEvents.on('socket.disconnected', async ({ id, instanceId }) => { + sauEvents.on('sau.socket.disconnected', async ({ connectionId, instanceId }) => { if (!this.isRunning()) { return; } - await Sessions.closeByInstanceIdAndSessionId(instanceId, id); + await Sessions.closeByInstanceIdAndSessionId(instanceId, connectionId); }); } diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 122ce09886852..d81dbbf62ddfe 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -1,6 +1,10 @@ -import type { IncomingHttpHeaders } from 'http'; - -import type { LoginSessionPayload, LogoutSessionPayload, DeviceLoginPayload } from '@rocket.chat/core-typings'; +import type { + LoginSessionPayload, + LogoutSessionPayload, + SocketConnectedPayload, + SocketDisconnectedPayload, + DeviceLoginPayload, +} from '@rocket.chat/core-typings'; import { InstanceStatus } from '@rocket.chat/instance-status'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; @@ -65,20 +69,18 @@ Accounts.onLogout((info) => { Meteor.onConnection((connection) => { connection.onClose(async () => { - const { httpHeaders } = connection; - sauEvents.emit('sau.socket.disconnected', { + const socketDisconnectedEventObject: SocketDisconnectedPayload = { + connectionId: connection.id, instanceId: InstanceStatus.id(), - ...connection, - httpHeaders: httpHeaders as IncomingHttpHeaders, - }); + }; + sauEvents.emit('sau.socket.disconnected', socketDisconnectedEventObject); }); }); Meteor.onConnection((connection) => { - const { httpHeaders } = connection; - sauEvents.emit('sau.socket.connected', { - instanceId: InstanceStatus.id(), + const socketConnectedEventObject: SocketConnectedPayload = { + // Implement SocketConnectedPayload type in case of using the sau.socket.connected hook ...connection, - httpHeaders: httpHeaders as IncomingHttpHeaders, - }); + }; + sauEvents.emit('sau.socket.connected', socketConnectedEventObject); }); diff --git a/apps/meteor/server/services/sauMonitor/events.ts b/apps/meteor/server/services/sauMonitor/events.ts index 8ffb3560a6e6d..ab375c2206440 100644 --- a/apps/meteor/server/services/sauMonitor/events.ts +++ b/apps/meteor/server/services/sauMonitor/events.ts @@ -1,9 +1,14 @@ -import type { ISocketConnection, LoginSessionPayload, LogoutSessionPayload } from '@rocket.chat/core-typings'; +import type { + LoginSessionPayload, + LogoutSessionPayload, + SocketConnectedPayload, + SocketDisconnectedPayload, +} from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const sauEvents = new Emitter<{ 'sau.accounts.login': LoginSessionPayload; 'sau.accounts.logout': LogoutSessionPayload; - 'sau.socket.connected': ISocketConnection; - 'sau.socket.disconnected': ISocketConnection; + 'sau.socket.connected': SocketConnectedPayload; + 'sau.socket.disconnected': SocketDisconnectedPayload; }>(); diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index 724bf5f199ff1..da5fc6756047f 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -29,8 +29,7 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit }); this.onEvent('socket.disconnected', async (data) => { - // console.log('socket.disconnected', data); - sauEvents.emit('sau.socket.disconnected', data); + sauEvents.emit('sau.socket.disconnected', { instanceId: InstanceStatus.id(), connectionId: data.id }); }); this.onEvent('socket.connected', async (data) => { diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 233742926ef23..617f3fb5bf2ea 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -145,8 +145,11 @@ export * from './IAbacAttribute'; export * from './Abac'; export * from './ServerAudit/IAuditServerAbacAction'; -export * from './LoginSessionPayload'; export * from './DeviceLoginPayload'; -export * from './LogoutSessionPayload'; + +export * from './sau/LoginSessionPayload'; +export * from './sau/LogoutSessionPayload'; +export * from './sau/SocketDisconnectedPayload'; +export * from './sau/SocketConnectedPayload'; export { schemas } from './Ajv'; diff --git a/packages/core-typings/src/LoginSessionPayload.ts b/packages/core-typings/src/sau/LoginSessionPayload.ts similarity index 100% rename from packages/core-typings/src/LoginSessionPayload.ts rename to packages/core-typings/src/sau/LoginSessionPayload.ts diff --git a/packages/core-typings/src/LogoutSessionPayload.ts b/packages/core-typings/src/sau/LogoutSessionPayload.ts similarity index 100% rename from packages/core-typings/src/LogoutSessionPayload.ts rename to packages/core-typings/src/sau/LogoutSessionPayload.ts diff --git a/packages/core-typings/src/sau/SocketConnectedPayload.ts b/packages/core-typings/src/sau/SocketConnectedPayload.ts new file mode 100644 index 0000000000000..bd36e6c9bf9c6 --- /dev/null +++ b/packages/core-typings/src/sau/SocketConnectedPayload.ts @@ -0,0 +1,3 @@ +export type SocketConnectedPayload = { + // To implement in case we use the sau.socket.connected hook +}; diff --git a/packages/core-typings/src/sau/SocketDisconnectedPayload.ts b/packages/core-typings/src/sau/SocketDisconnectedPayload.ts new file mode 100644 index 0000000000000..3e91d474958e2 --- /dev/null +++ b/packages/core-typings/src/sau/SocketDisconnectedPayload.ts @@ -0,0 +1,4 @@ +export type SocketDisconnectedPayload = { + instanceId: string; + connectionId: string; +}; From 4aea9c5e0af84f5512571afb2dc04e292ec6785a Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Thu, 22 Jan 2026 13:06:51 -0300 Subject: [PATCH 07/26] prettier fix --- packages/core-typings/src/sau/SocketConnectedPayload.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core-typings/src/sau/SocketConnectedPayload.ts b/packages/core-typings/src/sau/SocketConnectedPayload.ts index bd36e6c9bf9c6..0aefbb0c0a125 100644 --- a/packages/core-typings/src/sau/SocketConnectedPayload.ts +++ b/packages/core-typings/src/sau/SocketConnectedPayload.ts @@ -1,3 +1,2 @@ -export type SocketConnectedPayload = { - // To implement in case we use the sau.socket.connected hook -}; +// Payload for sau.socket.connected event (to be extended when needed) +export type SocketConnectedPayload = {}; From 11eaf8cfceb8d8126033cd651d8ddec48f62a025 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Thu, 22 Jan 2026 13:09:16 -0300 Subject: [PATCH 08/26] edit logger message --- apps/meteor/app/statistics/server/lib/SAUMonitor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 968e13c1dcb7e..a9b8e403eef80 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -133,12 +133,12 @@ export class SAUMonitorClass { } if (!userId) { - logger.warn({ msg: "Received 'accounts.logout' event without 'userId'" }); + logger.warn({ msg: "Received 'sau.accounts.logout' event without 'userId'" }); return; } if (!sessionId) { - logger.warn({ msg: "Received 'accounts.logout' event without 'sessionId'" }); + logger.warn({ msg: "Received 'sau.accounts.logout' event without 'sessionId'" }); return; } From 668c2a3ce9fa9fdcae6e2a7934338056eb775175 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 23 Jan 2026 13:45:29 -0300 Subject: [PATCH 09/26] remove unreachable code --- apps/meteor/app/statistics/server/lib/SAUMonitor.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index a9b8e403eef80..842442c1cb266 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -173,10 +173,6 @@ export class SAUMonitorClass { ...params, }; - if (!data) { - return; - } - const searchTerm = this._getSearchTerm(data); await Sessions.createOrUpdate({ ...data, searchTerm }); From ba3990cc3601f1d706bf493503103abccc7f61d4 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 23 Jan 2026 16:09:04 -0300 Subject: [PATCH 10/26] add forgotten loginToken --- apps/meteor/server/services/sauMonitor/service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index da5fc6756047f..e62c77b24d1e0 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -16,12 +16,11 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit this.onEvent('accounts.login', async ({ userId, connection }) => { const instanceId = InstanceStatus.id(); - const connectionId = connection.id; const clientAddress = getClientAddress(connection); const userAgent = getHeader(connection.httpHeaders, 'user-agent'); const host = getHeader(connection.httpHeaders, 'host'); - sauEvents.emit('sau.accounts.login', { userId, instanceId, connectionId, clientAddress, userAgent, host }); + sauEvents.emit('sau.accounts.login', { userId, instanceId, connectionId: connection.id, loginToken: connection.loginToken, clientAddress, userAgent, host }); }); this.onEvent('accounts.logout', async ({ userId, connection }) => { From ecf59a719e88b89e477098b0f661dc88283664c2 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 23 Jan 2026 18:07:05 -0300 Subject: [PATCH 11/26] remove use of created types --- .../app/statistics/server/lib/SAUMonitor.ts | 41 ++++++++++------- apps/meteor/server/hooks/sauMonitorHooks.ts | 46 ++++--------------- .../services/device-management/events.ts | 3 +- .../services/device-management/service.ts | 8 ++-- .../server/services/sauMonitor/events.ts | 22 +++++---- .../server/services/sauMonitor/service.ts | 12 ++++- .../core-typings/src/DeviceLoginPayload.ts | 6 --- packages/core-typings/src/index.ts | 7 --- .../src/sau/LoginSessionPayload.ts | 9 ---- .../src/sau/LogoutSessionPayload.ts | 4 -- .../src/sau/SocketConnectedPayload.ts | 2 - .../src/sau/SocketDisconnectedPayload.ts | 4 -- 12 files changed, 61 insertions(+), 103 deletions(-) delete mode 100644 packages/core-typings/src/DeviceLoginPayload.ts delete mode 100644 packages/core-typings/src/sau/LoginSessionPayload.ts delete mode 100644 packages/core-typings/src/sau/LogoutSessionPayload.ts delete mode 100644 packages/core-typings/src/sau/SocketConnectedPayload.ts delete mode 100644 packages/core-typings/src/sau/SocketDisconnectedPayload.ts diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 842442c1cb266..d709c5e869431 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -1,4 +1,4 @@ -import type { ISession, ISessionDevice, IUser, LoginSessionPayload, LogoutSessionPayload } from '@rocket.chat/core-typings'; +import type { ISession, ISessionDevice, IUser } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Logger } from '@rocket.chat/logger'; import { Sessions, Users, aggregates } from '@rocket.chat/models'; @@ -31,6 +31,16 @@ const getUserRoles = mem( const isProdEnv = process.env.NODE_ENV === 'production'; +type HandleSessionArgs = { + userId: string; + instanceId: string; + userAgent: string; + loginToken?: string; + connectionId: string; + clientAddress: string; + host: string; +}; + /** * Server Session Monitor for SAU(Simultaneously Active Users) based on Meteor server sessions */ @@ -110,24 +120,21 @@ export class SAUMonitorClass { return; } - sauEvents.on( - 'sau.accounts.login', - async ({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload) => { - if (!this.isRunning()) { - return; - } + sauEvents.on('sau.accounts.login', async ({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }) => { + if (!this.isRunning()) { + return; + } - const roles = await getUserRoles(userId); + const roles = await getUserRoles(userId); - const mostImportantRole = getMostImportantRole(roles); + const mostImportantRole = getMostImportantRole(roles); - const loginAt = new Date(); - const params = { roles, mostImportantRole, loginAt, ...getDateObj() }; - await this._handleSession({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }, params); - }, - ); + const loginAt = new Date(); + const params = { roles, mostImportantRole, loginAt, ...getDateObj() }; + await this._handleSession({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }, params); + }); - sauEvents.on('sau.accounts.logout', async ({ userId, sessionId }: LogoutSessionPayload) => { + sauEvents.on('sau.accounts.logout', async ({ userId, sessionId }) => { if (!this.isRunning()) { return; } @@ -158,12 +165,12 @@ export class SAUMonitorClass { } private async _handleSession( - { userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload, + { userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: HandleSessionArgs, params: Pick, ): Promise { const data: Omit = { userId, - loginToken, + ...(loginToken && { loginToken }), ip: clientAddress, host, sessionId: connectionId, diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index d81dbbf62ddfe..1edc2781f50d3 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -1,10 +1,3 @@ -import type { - LoginSessionPayload, - LogoutSessionPayload, - SocketConnectedPayload, - SocketDisconnectedPayload, - DeviceLoginPayload, -} from '@rocket.chat/core-typings'; import { InstanceStatus } from '@rocket.chat/instance-status'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; @@ -28,30 +21,21 @@ Accounts.onLogin((info: ILoginAttempt) => { const { resume } = methodArguments.find((arg) => 'resume' in arg) ?? {}; const loginToken = resume ? Accounts._hashLoginToken(resume) : ''; const instanceId = InstanceStatus.id(); - const userId = info.user._id; - const connectionId = info.connection.id; const clientAddress = getClientAddress(info.connection); const userAgent = getHeader(httpHeaders, 'user-agent'); const host = getHeader(httpHeaders, 'host'); - const loginEventObject: LoginSessionPayload = { - userId, + sauEvents.emit('sau.accounts.login', { + userId: info.user._id, instanceId, userAgent, loginToken, - connectionId, + connectionId: info.connection.id, clientAddress, host, - }; - sauEvents.emit('sau.accounts.login', loginEventObject); + }); - const deviceLoginEventObject: DeviceLoginPayload = { - userId, - userAgent, - loginToken, - clientAddress, - }; - deviceManagementEvents.emit('device-login', deviceLoginEventObject); + deviceManagementEvents.emit('device-login', { userId: info.user._id, userAgent, loginToken, clientAddress }); }); Accounts.onLogout((info) => { @@ -59,28 +43,16 @@ Accounts.onLogout((info) => { return; } - const logoutEventObject: LogoutSessionPayload = { - userId: info.user._id, - sessionId: info.connection.id, - }; - - sauEvents.emit('sau.accounts.logout', logoutEventObject); + sauEvents.emit('sau.accounts.logout', { userId: info.user._id, sessionId: info.connection.id }); }); Meteor.onConnection((connection) => { connection.onClose(async () => { - const socketDisconnectedEventObject: SocketDisconnectedPayload = { - connectionId: connection.id, - instanceId: InstanceStatus.id(), - }; - sauEvents.emit('sau.socket.disconnected', socketDisconnectedEventObject); + sauEvents.emit('sau.socket.disconnected', { connectionId: connection.id, instanceId: InstanceStatus.id() }); }); }); Meteor.onConnection((connection) => { - const socketConnectedEventObject: SocketConnectedPayload = { - // Implement SocketConnectedPayload type in case of using the sau.socket.connected hook - ...connection, - }; - sauEvents.emit('sau.socket.connected', socketConnectedEventObject); + // in case of implementing a listener of this event, define the parameters type in services/sauMonitor/events.ts + sauEvents.emit('sau.socket.connected', { instanceId: InstanceStatus.id(), connectionId: connection.id }); }); diff --git a/apps/meteor/server/services/device-management/events.ts b/apps/meteor/server/services/device-management/events.ts index b40df77de2c83..a6a5c8b97c31b 100644 --- a/apps/meteor/server/services/device-management/events.ts +++ b/apps/meteor/server/services/device-management/events.ts @@ -1,6 +1,5 @@ -import type { DeviceLoginPayload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const deviceManagementEvents = new Emitter<{ - 'device-login': DeviceLoginPayload; + 'device-login': { userId: string; userAgent: string; loginToken: string; clientAddress: string }; }>(); diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index 0592473a97997..190c99d71dc91 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -12,10 +12,12 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe super(); this.onEvent('accounts.login', async ({ userId, connection }) => { - const clientAddress = getClientAddress(connection); - const userAgent = getHeader(connection.httpHeaders, 'user-agent'); // TODO need to add loginToken to data - deviceManagementEvents.emit('device-login', { userId, userAgent, clientAddress }); + deviceManagementEvents.emit('device-login', { + userId, + userAgent: getHeader(connection.httpHeaders, 'user-agent'), + clientAddress: getClientAddress(connection), + }); }); } } diff --git a/apps/meteor/server/services/sauMonitor/events.ts b/apps/meteor/server/services/sauMonitor/events.ts index ab375c2206440..05d1c5bc78784 100644 --- a/apps/meteor/server/services/sauMonitor/events.ts +++ b/apps/meteor/server/services/sauMonitor/events.ts @@ -1,14 +1,16 @@ -import type { - LoginSessionPayload, - LogoutSessionPayload, - SocketConnectedPayload, - SocketDisconnectedPayload, -} from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const sauEvents = new Emitter<{ - 'sau.accounts.login': LoginSessionPayload; - 'sau.accounts.logout': LogoutSessionPayload; - 'sau.socket.connected': SocketConnectedPayload; - 'sau.socket.disconnected': SocketDisconnectedPayload; + 'sau.accounts.login': { + userId: string; + instanceId: string; + connectionId: string; + loginToken: string; + clientAddress: string; + userAgent: string; + host: string; + }; + 'sau.accounts.logout': { userId: string; sessionId: string }; + 'sau.socket.connected': { instanceId: string; connectionId: string }; + 'sau.socket.disconnected': { instanceId: string; connectionId: string }; }>(); diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index e62c77b24d1e0..d6ca3534a91ac 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -20,7 +20,15 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit const userAgent = getHeader(connection.httpHeaders, 'user-agent'); const host = getHeader(connection.httpHeaders, 'host'); - sauEvents.emit('sau.accounts.login', { userId, instanceId, connectionId: connection.id, loginToken: connection.loginToken, clientAddress, userAgent, host }); + sauEvents.emit('sau.accounts.login', { + userId, + instanceId, + connectionId: connection.id, + loginToken: connection.loginToken, + clientAddress, + userAgent, + host, + }); }); this.onEvent('accounts.logout', async ({ userId, connection }) => { @@ -33,7 +41,7 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit this.onEvent('socket.connected', async (data) => { // console.log('socket.connected', data); - sauEvents.emit('sau.socket.connected', data); + sauEvents.emit('sau.socket.connected', { instanceId: InstanceStatus.id(), connectionId: data.id }); }); } } diff --git a/packages/core-typings/src/DeviceLoginPayload.ts b/packages/core-typings/src/DeviceLoginPayload.ts deleted file mode 100644 index b7eb03fb8bab8..0000000000000 --- a/packages/core-typings/src/DeviceLoginPayload.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type DeviceLoginPayload = { - userId: string; - userAgent: string; - loginToken?: string; - clientAddress: string; -}; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 46120b9a211e9..5e0522cd0cf2d 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -133,11 +133,4 @@ export * from './Abac'; export * from './ServerAudit/IAuditServerAbacAction'; export * from './ServerAudit/IAuditUserChangedEvent'; -export * from './DeviceLoginPayload'; - -export * from './sau/LoginSessionPayload'; -export * from './sau/LogoutSessionPayload'; -export * from './sau/SocketDisconnectedPayload'; -export * from './sau/SocketConnectedPayload'; - export { schemas } from './Ajv'; diff --git a/packages/core-typings/src/sau/LoginSessionPayload.ts b/packages/core-typings/src/sau/LoginSessionPayload.ts deleted file mode 100644 index 0936592912452..0000000000000 --- a/packages/core-typings/src/sau/LoginSessionPayload.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type LoginSessionPayload = { - userId: string; - instanceId: string; - userAgent: string; - loginToken?: string; - connectionId: string; - clientAddress: string; - host: string; -}; diff --git a/packages/core-typings/src/sau/LogoutSessionPayload.ts b/packages/core-typings/src/sau/LogoutSessionPayload.ts deleted file mode 100644 index a4c0b55b80362..0000000000000 --- a/packages/core-typings/src/sau/LogoutSessionPayload.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type LogoutSessionPayload = { - userId?: string; - sessionId?: string; -}; diff --git a/packages/core-typings/src/sau/SocketConnectedPayload.ts b/packages/core-typings/src/sau/SocketConnectedPayload.ts deleted file mode 100644 index 0aefbb0c0a125..0000000000000 --- a/packages/core-typings/src/sau/SocketConnectedPayload.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Payload for sau.socket.connected event (to be extended when needed) -export type SocketConnectedPayload = {}; diff --git a/packages/core-typings/src/sau/SocketDisconnectedPayload.ts b/packages/core-typings/src/sau/SocketDisconnectedPayload.ts deleted file mode 100644 index 3e91d474958e2..0000000000000 --- a/packages/core-typings/src/sau/SocketDisconnectedPayload.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type SocketDisconnectedPayload = { - instanceId: string; - connectionId: string; -}; From e76d829f1e7f75ad4a6610af43ec4b12b9dd079a Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 23 Jan 2026 19:13:42 -0300 Subject: [PATCH 12/26] add optional operator to loginToken --- apps/meteor/server/services/device-management/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/services/device-management/events.ts b/apps/meteor/server/services/device-management/events.ts index a6a5c8b97c31b..1b014f38caf49 100644 --- a/apps/meteor/server/services/device-management/events.ts +++ b/apps/meteor/server/services/device-management/events.ts @@ -1,5 +1,5 @@ import { Emitter } from '@rocket.chat/emitter'; export const deviceManagementEvents = new Emitter<{ - 'device-login': { userId: string; userAgent: string; loginToken: string; clientAddress: string }; + 'device-login': { userId: string; userAgent: string; loginToken?: string; clientAddress: string }; }>(); From 4ed51c65c627220f064979adcdb8a7c633db4a88 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 26 Jan 2026 10:40:27 -0300 Subject: [PATCH 13/26] add optional to loginToken --- apps/meteor/app/statistics/server/lib/SAUMonitor.ts | 2 +- apps/meteor/server/services/sauMonitor/events.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index d709c5e869431..59ab6e45ee448 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -176,7 +176,7 @@ export class SAUMonitorClass { sessionId: connectionId, instanceId, type: 'session', - ...this._getUserAgentInfo(userAgent), + ...(loginToken && this._getUserAgentInfo(userAgent) ), ...params, }; diff --git a/apps/meteor/server/services/sauMonitor/events.ts b/apps/meteor/server/services/sauMonitor/events.ts index 05d1c5bc78784..134092ca717f5 100644 --- a/apps/meteor/server/services/sauMonitor/events.ts +++ b/apps/meteor/server/services/sauMonitor/events.ts @@ -5,7 +5,7 @@ export const sauEvents = new Emitter<{ userId: string; instanceId: string; connectionId: string; - loginToken: string; + loginToken?: string; clientAddress: string; userAgent: string; host: string; From 7586e196b3dbd6ec5bf9cac3f32bcd7477d05d83 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 26 Jan 2026 12:05:12 -0300 Subject: [PATCH 14/26] modify instanceId param --- .../app/statistics/server/lib/SAUMonitor.ts | 2 +- .../server/services/sauMonitor/service.ts | 5 ++- packages/apps-engine/deno-runtime/deno.lock | 32 ++++++++++++++----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 59ab6e45ee448..d712b5b623e9e 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -176,7 +176,7 @@ export class SAUMonitorClass { sessionId: connectionId, instanceId, type: 'session', - ...(loginToken && this._getUserAgentInfo(userAgent) ), + ...(loginToken && this._getUserAgentInfo(userAgent)), ...params, }; diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index d6ca3534a91ac..feb9e3326b61b 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -15,14 +15,13 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit super(); this.onEvent('accounts.login', async ({ userId, connection }) => { - const instanceId = InstanceStatus.id(); const clientAddress = getClientAddress(connection); const userAgent = getHeader(connection.httpHeaders, 'user-agent'); const host = getHeader(connection.httpHeaders, 'host'); sauEvents.emit('sau.accounts.login', { userId, - instanceId, + instanceId: connection.instanceId, connectionId: connection.id, loginToken: connection.loginToken, clientAddress, @@ -36,7 +35,7 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit }); this.onEvent('socket.disconnected', async (data) => { - sauEvents.emit('sau.socket.disconnected', { instanceId: InstanceStatus.id(), connectionId: data.id }); + sauEvents.emit('sau.socket.disconnected', { instanceId: data.instanceId, connectionId: data.id }); }); this.onEvent('socket.connected', async (data) => { diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 6dbfd05882fe2..9923094073ef1 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -16,14 +16,9 @@ "npm:uuid@8.3.2": "npm:uuid@8.3.2" }, "jsr": { - "@std/bytes@1.0.6": { - "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" - }, - "@std/cli@1.0.13": { - "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" - }, + "@std/bytes@1.0.6": {}, + "@std/cli@1.0.13": {}, "@std/streams@1.0.16": { - "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", "dependencies": [ "jsr:@std/bytes@^1.0.6" ] @@ -108,7 +103,28 @@ "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", - "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" + "https://jsr.io/@std/bytes/1.0.6/_types.ts": "65db521a1e83079110f20ee8121218b7485afd9223634f03934a36e80304ddac", + "https://jsr.io/@std/bytes/1.0.6/concat.ts": "b10dc6291f8afb30f98aefd54002d21c961793c9aa18433301ab49d2a09976f1", + "https://jsr.io/@std/bytes/1.0.6/copy.ts": "e1c0262d3f0fa02b65cd923099d1f64ede3add3d79ca5b357e592c13aab26fae", + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef", + "https://jsr.io/@std/streams/1.0.16/_common.ts": "e0166ea71d2abef38b491ddbdf7de2604e0cf6031e7e4c2881c33447a6951111", + "https://jsr.io/@std/streams/1.0.16/buffer.ts": "4dea623b23426ae15d5f9ad3d5651752d9a5e61058aeb90818c51e894fc9c326", + "https://jsr.io/@std/streams/1.0.16/byte_slice_stream.ts": "f475afd6cb35b39a70c457b2b8a4918e64877410cabbcb274a622b3b5c0665eb", + "https://jsr.io/@std/streams/1.0.16/concat_readable_streams.ts": "dfb9c246d82f73e3c4dcf54045b4c3c105d2870360305ddb19fc76f2ac83be92", + "https://jsr.io/@std/streams/1.0.16/delimiter_stream.ts": "724f6dc8c8df89656382afad07e5464952c09b8381d9a0d019f50a871fbe9ab5", + "https://jsr.io/@std/streams/1.0.16/early_zip_readable_streams.ts": "2ae1c9ca40e1a5210591416eda32b24491fbe7499948dd020683537f1ccf062b", + "https://jsr.io/@std/streams/1.0.16/limited_bytes_transform_stream.ts": "bed6da5a1ca6f6a764b1bc6ef28e6592183feec278cd2709118214f65240a2d6", + "https://jsr.io/@std/streams/1.0.16/limited_transform_stream.ts": "4cd04ff24646929d5bcb01d759097cc4a094a965bd1fbb0f3b9fe2eed152a48b", + "https://jsr.io/@std/streams/1.0.16/merge_readable_streams.ts": "207a97f860d3587317bd3a773d0670a17145e655208060d58f3d2e948842f497", + "https://jsr.io/@std/streams/1.0.16/mod.ts": "64dac8b1ca68656b80813ed822832e43ccdf47475bfd6746c4fea0b8fca8fc95", + "https://jsr.io/@std/streams/1.0.16/text_delimiter_stream.ts": "9828ba9836910aa8c3350a9e24f70caa7dd3a033756b652cd7cc7542f965c207", + "https://jsr.io/@std/streams/1.0.16/text_line_stream.ts": "4ed2cca1ec311bf8437485417e53e5aac4bb5ac8fd05431b00a185d1e4d76a2d", + "https://jsr.io/@std/streams/1.0.16/to_array_buffer.ts": "0b1369f0d04e502e4174cd8757ed74ef04a09f4af2e850786dbef3bc164f70ac", + "https://jsr.io/@std/streams/1.0.16/to_blob.ts": "3de10d659bf38b11143b4c350a3fb688eda953803bbdd0e9f925c68aae48975e", + "https://jsr.io/@std/streams/1.0.16/to_json.ts": "9f75ae4e4b2d52f88245c0f11bbfc6a480e869c40922c6820e962cdc733786c9", + "https://jsr.io/@std/streams/1.0.16/to_text.ts": "79e8745d710b1f00071299bb607cbd73bbed4418d7236bc2786db6ad2eb032a9", + "https://jsr.io/@std/streams/1.0.16/to_transform_stream.ts": "7a63e524ad913073c2ea26a3f2d9df675685cabd0375c2ab207f231261628ae7", + "https://jsr.io/@std/streams/1.0.16/zip_readable_streams.ts": "ed90185e4986bd77ed897c67c61b9a20d5814619ad647eba288230a85f0767d0" }, "workspace": { "dependencies": [ From eac64ef225d02743dbaf5401d49841824bd2c739 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 26 Jan 2026 12:30:16 -0300 Subject: [PATCH 15/26] rollback deno.lock changes by accident --- packages/apps-engine/deno-runtime/deno.lock | 32 ++++++--------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 9923094073ef1..6dbfd05882fe2 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -16,9 +16,14 @@ "npm:uuid@8.3.2": "npm:uuid@8.3.2" }, "jsr": { - "@std/bytes@1.0.6": {}, - "@std/cli@1.0.13": {}, + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/cli@1.0.13": { + "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" + }, "@std/streams@1.0.16": { + "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", "dependencies": [ "jsr:@std/bytes@^1.0.6" ] @@ -103,28 +108,7 @@ "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", - "https://jsr.io/@std/bytes/1.0.6/_types.ts": "65db521a1e83079110f20ee8121218b7485afd9223634f03934a36e80304ddac", - "https://jsr.io/@std/bytes/1.0.6/concat.ts": "b10dc6291f8afb30f98aefd54002d21c961793c9aa18433301ab49d2a09976f1", - "https://jsr.io/@std/bytes/1.0.6/copy.ts": "e1c0262d3f0fa02b65cd923099d1f64ede3add3d79ca5b357e592c13aab26fae", - "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef", - "https://jsr.io/@std/streams/1.0.16/_common.ts": "e0166ea71d2abef38b491ddbdf7de2604e0cf6031e7e4c2881c33447a6951111", - "https://jsr.io/@std/streams/1.0.16/buffer.ts": "4dea623b23426ae15d5f9ad3d5651752d9a5e61058aeb90818c51e894fc9c326", - "https://jsr.io/@std/streams/1.0.16/byte_slice_stream.ts": "f475afd6cb35b39a70c457b2b8a4918e64877410cabbcb274a622b3b5c0665eb", - "https://jsr.io/@std/streams/1.0.16/concat_readable_streams.ts": "dfb9c246d82f73e3c4dcf54045b4c3c105d2870360305ddb19fc76f2ac83be92", - "https://jsr.io/@std/streams/1.0.16/delimiter_stream.ts": "724f6dc8c8df89656382afad07e5464952c09b8381d9a0d019f50a871fbe9ab5", - "https://jsr.io/@std/streams/1.0.16/early_zip_readable_streams.ts": "2ae1c9ca40e1a5210591416eda32b24491fbe7499948dd020683537f1ccf062b", - "https://jsr.io/@std/streams/1.0.16/limited_bytes_transform_stream.ts": "bed6da5a1ca6f6a764b1bc6ef28e6592183feec278cd2709118214f65240a2d6", - "https://jsr.io/@std/streams/1.0.16/limited_transform_stream.ts": "4cd04ff24646929d5bcb01d759097cc4a094a965bd1fbb0f3b9fe2eed152a48b", - "https://jsr.io/@std/streams/1.0.16/merge_readable_streams.ts": "207a97f860d3587317bd3a773d0670a17145e655208060d58f3d2e948842f497", - "https://jsr.io/@std/streams/1.0.16/mod.ts": "64dac8b1ca68656b80813ed822832e43ccdf47475bfd6746c4fea0b8fca8fc95", - "https://jsr.io/@std/streams/1.0.16/text_delimiter_stream.ts": "9828ba9836910aa8c3350a9e24f70caa7dd3a033756b652cd7cc7542f965c207", - "https://jsr.io/@std/streams/1.0.16/text_line_stream.ts": "4ed2cca1ec311bf8437485417e53e5aac4bb5ac8fd05431b00a185d1e4d76a2d", - "https://jsr.io/@std/streams/1.0.16/to_array_buffer.ts": "0b1369f0d04e502e4174cd8757ed74ef04a09f4af2e850786dbef3bc164f70ac", - "https://jsr.io/@std/streams/1.0.16/to_blob.ts": "3de10d659bf38b11143b4c350a3fb688eda953803bbdd0e9f925c68aae48975e", - "https://jsr.io/@std/streams/1.0.16/to_json.ts": "9f75ae4e4b2d52f88245c0f11bbfc6a480e869c40922c6820e962cdc733786c9", - "https://jsr.io/@std/streams/1.0.16/to_text.ts": "79e8745d710b1f00071299bb607cbd73bbed4418d7236bc2786db6ad2eb032a9", - "https://jsr.io/@std/streams/1.0.16/to_transform_stream.ts": "7a63e524ad913073c2ea26a3f2d9df675685cabd0375c2ab207f231261628ae7", - "https://jsr.io/@std/streams/1.0.16/zip_readable_streams.ts": "ed90185e4986bd77ed897c67c61b9a20d5814619ad647eba288230a85f0767d0" + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" }, "workspace": { "dependencies": [ From 9ce09a4a3b646288c598d06e22861da3dcade89d Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 26 Jan 2026 13:53:50 -0300 Subject: [PATCH 16/26] make code cleaner --- apps/meteor/server/services/sauMonitor/service.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index feb9e3326b61b..7e7534985701a 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -2,7 +2,6 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { ISAUMonitorService } from '@rocket.chat/core-services'; -import { InstanceStatus } from '@rocket.chat/instance-status'; import { sauEvents } from './events'; import { getClientAddress } from '../../lib/getClientAddress'; @@ -15,18 +14,14 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit super(); this.onEvent('accounts.login', async ({ userId, connection }) => { - const clientAddress = getClientAddress(connection); - const userAgent = getHeader(connection.httpHeaders, 'user-agent'); - const host = getHeader(connection.httpHeaders, 'host'); - sauEvents.emit('sau.accounts.login', { userId, instanceId: connection.instanceId, connectionId: connection.id, loginToken: connection.loginToken, - clientAddress, - userAgent, - host, + clientAddress: getClientAddress(connection), + userAgent: getHeader(connection.httpHeaders, 'user-agent'), + host: getHeader(connection.httpHeaders, 'host'), }); }); @@ -40,7 +35,7 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit this.onEvent('socket.connected', async (data) => { // console.log('socket.connected', data); - sauEvents.emit('sau.socket.connected', { instanceId: InstanceStatus.id(), connectionId: data.id }); + sauEvents.emit('sau.socket.connected', { instanceId: data.instanceId, connectionId: data.id }); }); } } From f86de1211a4d954139841a6e65fe65a94f03e261 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli <84046180+nazabucciarelli@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:34:46 -0300 Subject: [PATCH 17/26] Apply suggestion from @KevLehman Co-authored-by: Kevin Aleman --- apps/meteor/server/services/sauMonitor/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index 7e7534985701a..1b74d67cf6c3d 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -34,7 +34,6 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit }); this.onEvent('socket.connected', async (data) => { - // console.log('socket.connected', data); sauEvents.emit('sau.socket.connected', { instanceId: data.instanceId, connectionId: data.id }); }); } From 73403e22e64c699060244b58ba330fe624a6b0e6 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 26 Jan 2026 19:52:16 -0300 Subject: [PATCH 18/26] enhance getHeader method and add unit tests --- apps/meteor/server/hooks/sauMonitorHooks.ts | 2 +- apps/meteor/server/lib/getHeader.ts | 11 ----- .../services/device-management/service.ts | 2 +- .../server/services/sauMonitor/service.ts | 2 +- packages/apps-engine/deno-runtime/deno.lock | 32 ++++++++++---- packages/tools/src/getHeader.spec.ts | 42 +++++++++++++++++++ packages/tools/src/getHeader.ts | 22 ++++++++++ packages/tools/src/index.ts | 1 + 8 files changed, 92 insertions(+), 22 deletions(-) delete mode 100644 apps/meteor/server/lib/getHeader.ts create mode 100644 packages/tools/src/getHeader.spec.ts create mode 100644 packages/tools/src/getHeader.ts diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 1edc2781f50d3..aaaf5257a9dc1 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -1,10 +1,10 @@ import { InstanceStatus } from '@rocket.chat/instance-status'; +import { getHeader } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ILoginAttempt } from '../../app/authentication/server/ILoginAttempt'; import { getClientAddress } from '../lib/getClientAddress'; -import { getHeader } from '../lib/getHeader'; import { deviceManagementEvents } from '../services/device-management/events'; import { sauEvents } from '../services/sauMonitor/events'; diff --git a/apps/meteor/server/lib/getHeader.ts b/apps/meteor/server/lib/getHeader.ts deleted file mode 100644 index cd8a7eaa781a6..0000000000000 --- a/apps/meteor/server/lib/getHeader.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const getHeader = (headers: unknown, key: string): string => { - if (!headers) { - return ''; - } - - if (typeof (headers as any).get === 'function') { - return (headers as Headers).get(key) ?? ''; - } - - return (headers as Record)[key] || ''; -}; diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index 190c99d71dc91..a5aa69857c891 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -1,9 +1,9 @@ import type { IDeviceManagementService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; +import { getHeader } from '@rocket.chat/tools'; import { deviceManagementEvents } from './events'; import { getClientAddress } from '../../lib/getClientAddress'; -import { getHeader } from '../../lib/getHeader'; export class DeviceManagementService extends ServiceClassInternal implements IDeviceManagementService { protected name = 'device-management'; diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index 7e7534985701a..298ef2c286b1a 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -2,10 +2,10 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { ISAUMonitorService } from '@rocket.chat/core-services'; +import { getHeader } from '@rocket.chat/tools'; import { sauEvents } from './events'; import { getClientAddress } from '../../lib/getClientAddress'; -import { getHeader } from '../../lib/getHeader'; export class SAUMonitorService extends ServiceClassInternal implements ISAUMonitorService { protected name = 'sau-monitor'; diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 6dbfd05882fe2..9923094073ef1 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -16,14 +16,9 @@ "npm:uuid@8.3.2": "npm:uuid@8.3.2" }, "jsr": { - "@std/bytes@1.0.6": { - "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" - }, - "@std/cli@1.0.13": { - "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" - }, + "@std/bytes@1.0.6": {}, + "@std/cli@1.0.13": {}, "@std/streams@1.0.16": { - "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", "dependencies": [ "jsr:@std/bytes@^1.0.6" ] @@ -108,7 +103,28 @@ "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", - "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" + "https://jsr.io/@std/bytes/1.0.6/_types.ts": "65db521a1e83079110f20ee8121218b7485afd9223634f03934a36e80304ddac", + "https://jsr.io/@std/bytes/1.0.6/concat.ts": "b10dc6291f8afb30f98aefd54002d21c961793c9aa18433301ab49d2a09976f1", + "https://jsr.io/@std/bytes/1.0.6/copy.ts": "e1c0262d3f0fa02b65cd923099d1f64ede3add3d79ca5b357e592c13aab26fae", + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef", + "https://jsr.io/@std/streams/1.0.16/_common.ts": "e0166ea71d2abef38b491ddbdf7de2604e0cf6031e7e4c2881c33447a6951111", + "https://jsr.io/@std/streams/1.0.16/buffer.ts": "4dea623b23426ae15d5f9ad3d5651752d9a5e61058aeb90818c51e894fc9c326", + "https://jsr.io/@std/streams/1.0.16/byte_slice_stream.ts": "f475afd6cb35b39a70c457b2b8a4918e64877410cabbcb274a622b3b5c0665eb", + "https://jsr.io/@std/streams/1.0.16/concat_readable_streams.ts": "dfb9c246d82f73e3c4dcf54045b4c3c105d2870360305ddb19fc76f2ac83be92", + "https://jsr.io/@std/streams/1.0.16/delimiter_stream.ts": "724f6dc8c8df89656382afad07e5464952c09b8381d9a0d019f50a871fbe9ab5", + "https://jsr.io/@std/streams/1.0.16/early_zip_readable_streams.ts": "2ae1c9ca40e1a5210591416eda32b24491fbe7499948dd020683537f1ccf062b", + "https://jsr.io/@std/streams/1.0.16/limited_bytes_transform_stream.ts": "bed6da5a1ca6f6a764b1bc6ef28e6592183feec278cd2709118214f65240a2d6", + "https://jsr.io/@std/streams/1.0.16/limited_transform_stream.ts": "4cd04ff24646929d5bcb01d759097cc4a094a965bd1fbb0f3b9fe2eed152a48b", + "https://jsr.io/@std/streams/1.0.16/merge_readable_streams.ts": "207a97f860d3587317bd3a773d0670a17145e655208060d58f3d2e948842f497", + "https://jsr.io/@std/streams/1.0.16/mod.ts": "64dac8b1ca68656b80813ed822832e43ccdf47475bfd6746c4fea0b8fca8fc95", + "https://jsr.io/@std/streams/1.0.16/text_delimiter_stream.ts": "9828ba9836910aa8c3350a9e24f70caa7dd3a033756b652cd7cc7542f965c207", + "https://jsr.io/@std/streams/1.0.16/text_line_stream.ts": "4ed2cca1ec311bf8437485417e53e5aac4bb5ac8fd05431b00a185d1e4d76a2d", + "https://jsr.io/@std/streams/1.0.16/to_array_buffer.ts": "0b1369f0d04e502e4174cd8757ed74ef04a09f4af2e850786dbef3bc164f70ac", + "https://jsr.io/@std/streams/1.0.16/to_blob.ts": "3de10d659bf38b11143b4c350a3fb688eda953803bbdd0e9f925c68aae48975e", + "https://jsr.io/@std/streams/1.0.16/to_json.ts": "9f75ae4e4b2d52f88245c0f11bbfc6a480e869c40922c6820e962cdc733786c9", + "https://jsr.io/@std/streams/1.0.16/to_text.ts": "79e8745d710b1f00071299bb607cbd73bbed4418d7236bc2786db6ad2eb032a9", + "https://jsr.io/@std/streams/1.0.16/to_transform_stream.ts": "7a63e524ad913073c2ea26a3f2d9df675685cabd0375c2ab207f231261628ae7", + "https://jsr.io/@std/streams/1.0.16/zip_readable_streams.ts": "ed90185e4986bd77ed897c67c61b9a20d5814619ad647eba288230a85f0767d0" }, "workspace": { "dependencies": [ diff --git a/packages/tools/src/getHeader.spec.ts b/packages/tools/src/getHeader.spec.ts new file mode 100644 index 0000000000000..be52279b107ae --- /dev/null +++ b/packages/tools/src/getHeader.spec.ts @@ -0,0 +1,42 @@ +import { IncomingHttpHeaders } from 'http'; +import { getHeader } from './getHeader'; + +describe('getHeader', () => { + it('returns empty string when headers is undefined', () => { + expect(getHeader(undefined as unknown as IncomingHttpHeaders, 'origin')).toBe(''); + }); + + it('returns empty string when header does not exist', () => { + expect(getHeader({ origin: 'localhost:3000' }, 'host')).toBe(''); + }); + + it('returns the header value when it exists', () => { + expect(getHeader({ origin: 'localhost:3000' }, 'origin')).toBe('localhost:3000'); + }); + + it('returns empty string when headers is an empty object', () => { + expect(getHeader({}, 'origin')).toBe(''); + }); + + it('returns first value when header is an array', () => { + expect(getHeader({ 'x-forwarded-for': ['127.0.0.1', '10.0.0.1'] }, 'x-forwarded-for')).toBe('127.0.0.1'); + }); + + it('returns empty string when value is an empty array', () => { + expect(getHeader({ 'x-forwarded-for': [] }, 'x-forwarded-for')).toBe(''); + }); + + it('returns empty string when header value is undefined', () => { + expect(getHeader({ origin: undefined }, 'origin')).toBe(''); + }); + + it('works with IncomingHttpHeaders shape', () => { + const headers: IncomingHttpHeaders = { + host: 'localhost:3000', + connection: 'keep-alive', + }; + + expect(getHeader(headers, 'host')).toBe('localhost:3000'); + }); +}); + diff --git a/packages/tools/src/getHeader.ts b/packages/tools/src/getHeader.ts new file mode 100644 index 0000000000000..fb990d523b636 --- /dev/null +++ b/packages/tools/src/getHeader.ts @@ -0,0 +1,22 @@ +import type { IncomingHttpHeaders } from 'http'; + +type HeaderLike = IncomingHttpHeaders | Record; + +export const getHeader = (headers: HeaderLike, key: string): string => { + if (!headers) { + return ''; + } + + const value = headers[key]; + + if (Array.isArray(value)) { + return value[0] ?? ''; + } + + if (typeof value === 'string') { + return value; + } + + return ''; +}; + diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 5f60085511b71..7889c9caf5caa 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -15,3 +15,4 @@ export * from './isRecord'; export * from './validateEmail'; export * from './truncateString'; export * from './isTruthy'; +export * from './getHeader'; From 64cf87ccf2ebf0215c0acfef1989e240ee54ecd8 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 26 Jan 2026 21:45:01 -0300 Subject: [PATCH 19/26] rollback deno.lock (again) --- packages/apps-engine/deno-runtime/deno.lock | 32 ++++++--------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 9923094073ef1..6dbfd05882fe2 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -16,9 +16,14 @@ "npm:uuid@8.3.2": "npm:uuid@8.3.2" }, "jsr": { - "@std/bytes@1.0.6": {}, - "@std/cli@1.0.13": {}, + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/cli@1.0.13": { + "integrity": "5db2d95ab2dca3bca9fb6ad3c19908c314e93d6391c8b026725e4892d4615a69" + }, "@std/streams@1.0.16": { + "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", "dependencies": [ "jsr:@std/bytes@^1.0.6" ] @@ -103,28 +108,7 @@ "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", - "https://jsr.io/@std/bytes/1.0.6/_types.ts": "65db521a1e83079110f20ee8121218b7485afd9223634f03934a36e80304ddac", - "https://jsr.io/@std/bytes/1.0.6/concat.ts": "b10dc6291f8afb30f98aefd54002d21c961793c9aa18433301ab49d2a09976f1", - "https://jsr.io/@std/bytes/1.0.6/copy.ts": "e1c0262d3f0fa02b65cd923099d1f64ede3add3d79ca5b357e592c13aab26fae", - "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef", - "https://jsr.io/@std/streams/1.0.16/_common.ts": "e0166ea71d2abef38b491ddbdf7de2604e0cf6031e7e4c2881c33447a6951111", - "https://jsr.io/@std/streams/1.0.16/buffer.ts": "4dea623b23426ae15d5f9ad3d5651752d9a5e61058aeb90818c51e894fc9c326", - "https://jsr.io/@std/streams/1.0.16/byte_slice_stream.ts": "f475afd6cb35b39a70c457b2b8a4918e64877410cabbcb274a622b3b5c0665eb", - "https://jsr.io/@std/streams/1.0.16/concat_readable_streams.ts": "dfb9c246d82f73e3c4dcf54045b4c3c105d2870360305ddb19fc76f2ac83be92", - "https://jsr.io/@std/streams/1.0.16/delimiter_stream.ts": "724f6dc8c8df89656382afad07e5464952c09b8381d9a0d019f50a871fbe9ab5", - "https://jsr.io/@std/streams/1.0.16/early_zip_readable_streams.ts": "2ae1c9ca40e1a5210591416eda32b24491fbe7499948dd020683537f1ccf062b", - "https://jsr.io/@std/streams/1.0.16/limited_bytes_transform_stream.ts": "bed6da5a1ca6f6a764b1bc6ef28e6592183feec278cd2709118214f65240a2d6", - "https://jsr.io/@std/streams/1.0.16/limited_transform_stream.ts": "4cd04ff24646929d5bcb01d759097cc4a094a965bd1fbb0f3b9fe2eed152a48b", - "https://jsr.io/@std/streams/1.0.16/merge_readable_streams.ts": "207a97f860d3587317bd3a773d0670a17145e655208060d58f3d2e948842f497", - "https://jsr.io/@std/streams/1.0.16/mod.ts": "64dac8b1ca68656b80813ed822832e43ccdf47475bfd6746c4fea0b8fca8fc95", - "https://jsr.io/@std/streams/1.0.16/text_delimiter_stream.ts": "9828ba9836910aa8c3350a9e24f70caa7dd3a033756b652cd7cc7542f965c207", - "https://jsr.io/@std/streams/1.0.16/text_line_stream.ts": "4ed2cca1ec311bf8437485417e53e5aac4bb5ac8fd05431b00a185d1e4d76a2d", - "https://jsr.io/@std/streams/1.0.16/to_array_buffer.ts": "0b1369f0d04e502e4174cd8757ed74ef04a09f4af2e850786dbef3bc164f70ac", - "https://jsr.io/@std/streams/1.0.16/to_blob.ts": "3de10d659bf38b11143b4c350a3fb688eda953803bbdd0e9f925c68aae48975e", - "https://jsr.io/@std/streams/1.0.16/to_json.ts": "9f75ae4e4b2d52f88245c0f11bbfc6a480e869c40922c6820e962cdc733786c9", - "https://jsr.io/@std/streams/1.0.16/to_text.ts": "79e8745d710b1f00071299bb607cbd73bbed4418d7236bc2786db6ad2eb032a9", - "https://jsr.io/@std/streams/1.0.16/to_transform_stream.ts": "7a63e524ad913073c2ea26a3f2d9df675685cabd0375c2ab207f231261628ae7", - "https://jsr.io/@std/streams/1.0.16/zip_readable_streams.ts": "ed90185e4986bd77ed897c67c61b9a20d5814619ad647eba288230a85f0767d0" + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" }, "workspace": { "dependencies": [ From 6d3d051489fdc88eb3b7f8f65b1cd1f67119d1e8 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 26 Jan 2026 23:24:54 -0300 Subject: [PATCH 20/26] change hashLoginToken method --- apps/meteor/server/hooks/sauMonitorHooks.ts | 3 ++- packages/tools/src/getHeader.spec.ts | 4 ++-- packages/tools/src/getHeader.ts | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index aaaf5257a9dc1..0d73c8c288c81 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -1,3 +1,4 @@ +import { hashLoginToken } from '@rocket.chat/account-utils'; import { InstanceStatus } from '@rocket.chat/instance-status'; import { getHeader } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; @@ -19,7 +20,7 @@ Accounts.onLogin((info: ILoginAttempt) => { } const { resume } = methodArguments.find((arg) => 'resume' in arg) ?? {}; - const loginToken = resume ? Accounts._hashLoginToken(resume) : ''; + const loginToken = resume ? hashLoginToken(resume) : ''; const instanceId = InstanceStatus.id(); const clientAddress = getClientAddress(info.connection); const userAgent = getHeader(httpHeaders, 'user-agent'); diff --git a/packages/tools/src/getHeader.spec.ts b/packages/tools/src/getHeader.spec.ts index be52279b107ae..a3a20c43f1d9f 100644 --- a/packages/tools/src/getHeader.spec.ts +++ b/packages/tools/src/getHeader.spec.ts @@ -1,4 +1,5 @@ -import { IncomingHttpHeaders } from 'http'; +import type { IncomingHttpHeaders } from 'http'; + import { getHeader } from './getHeader'; describe('getHeader', () => { @@ -39,4 +40,3 @@ describe('getHeader', () => { expect(getHeader(headers, 'host')).toBe('localhost:3000'); }); }); - diff --git a/packages/tools/src/getHeader.ts b/packages/tools/src/getHeader.ts index fb990d523b636..f8f1e61416c1b 100644 --- a/packages/tools/src/getHeader.ts +++ b/packages/tools/src/getHeader.ts @@ -19,4 +19,3 @@ export const getHeader = (headers: HeaderLike, key: string): string => { return ''; }; - From cef0138cf0191a095ab4c9dd5703d6493a6d6814 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 27 Jan 2026 12:06:21 -0300 Subject: [PATCH 21/26] improve getHeader tool --- packages/tools/src/getHeader.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/getHeader.ts b/packages/tools/src/getHeader.ts index f8f1e61416c1b..92bbc53389163 100644 --- a/packages/tools/src/getHeader.ts +++ b/packages/tools/src/getHeader.ts @@ -1,12 +1,16 @@ import type { IncomingHttpHeaders } from 'http'; -type HeaderLike = IncomingHttpHeaders | Record; +type HeaderLike = IncomingHttpHeaders | Headers | Record; export const getHeader = (headers: HeaderLike, key: string): string => { if (!headers) { return ''; } + if (isHeadersType(headers)) { + return headers.get(key) ?? ''; + } + const value = headers[key]; if (Array.isArray(value)) { @@ -19,3 +23,8 @@ export const getHeader = (headers: HeaderLike, key: string): string => { return ''; }; + + +function isHeadersType(headers: HeaderLike): headers is Headers{ + return headers instanceof Headers; +} From 2361dad2366775835eb96c3a7c37cc97e5152d74 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 27 Jan 2026 12:36:49 -0300 Subject: [PATCH 22/26] add loginToken data to device-management hook --- apps/meteor/server/services/device-management/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index a5aa69857c891..27b905875fe35 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -12,11 +12,11 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe super(); this.onEvent('accounts.login', async ({ userId, connection }) => { - // TODO need to add loginToken to data deviceManagementEvents.emit('device-login', { userId, userAgent: getHeader(connection.httpHeaders, 'user-agent'), clientAddress: getClientAddress(connection), + loginToken: connection.loginToken, }); }); } From fa604e34627d3d6f21e8ccf9e39481ca7f179a10 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 27 Jan 2026 14:01:44 -0300 Subject: [PATCH 23/26] overload getHeader, update tests --- .../services/device-management/service.ts | 2 +- packages/tools/src/getHeader.spec.ts | 85 +++++++++++++------ packages/tools/src/getHeader.ts | 33 ++++--- 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index 27b905875fe35..35116064eeef7 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -16,7 +16,7 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe userId, userAgent: getHeader(connection.httpHeaders, 'user-agent'), clientAddress: getClientAddress(connection), - loginToken: connection.loginToken, + loginToken: connection.loginToken, }); }); } diff --git a/packages/tools/src/getHeader.spec.ts b/packages/tools/src/getHeader.spec.ts index a3a20c43f1d9f..4a96fa35e9b9b 100644 --- a/packages/tools/src/getHeader.spec.ts +++ b/packages/tools/src/getHeader.spec.ts @@ -3,40 +3,77 @@ import type { IncomingHttpHeaders } from 'http'; import { getHeader } from './getHeader'; describe('getHeader', () => { - it('returns empty string when headers is undefined', () => { - expect(getHeader(undefined as unknown as IncomingHttpHeaders, 'origin')).toBe(''); - }); + describe('default mode (string)', () => { + it('returns empty string when headers is undefined', () => { + expect(getHeader(undefined as unknown as IncomingHttpHeaders, 'origin')).toBe(''); + }); - it('returns empty string when header does not exist', () => { - expect(getHeader({ origin: 'localhost:3000' }, 'host')).toBe(''); - }); + it('returns empty string when header does not exist', () => { + expect(getHeader({ origin: 'localhost:3000' }, 'host')).toBe(''); + }); - it('returns the header value when it exists', () => { - expect(getHeader({ origin: 'localhost:3000' }, 'origin')).toBe('localhost:3000'); - }); + it('returns header value when it exists', () => { + expect(getHeader({ origin: 'localhost:3000' }, 'origin')).toBe('localhost:3000'); + }); - it('returns empty string when headers is an empty object', () => { - expect(getHeader({}, 'origin')).toBe(''); - }); + it('returns empty string when headers is empty object', () => { + expect(getHeader({}, 'origin')).toBe(''); + }); + + it('returns first value when header is array', () => { + expect(getHeader({ 'x-forwarded-for': ['127.0.0.1', '10.0.0.1'] }, 'x-forwarded-for')).toBe('127.0.0.1'); + }); + + it('returns empty string when value is empty array', () => { + expect(getHeader({ 'x-forwarded-for': [] }, 'x-forwarded-for')).toBe(''); + }); - it('returns first value when header is an array', () => { - expect(getHeader({ 'x-forwarded-for': ['127.0.0.1', '10.0.0.1'] }, 'x-forwarded-for')).toBe('127.0.0.1'); + it('returns empty string when value is undefined', () => { + expect(getHeader({ origin: undefined }, 'origin')).toBe(''); + }); }); - it('returns empty string when value is an empty array', () => { - expect(getHeader({ 'x-forwarded-for': [] }, 'x-forwarded-for')).toBe(''); + describe('asArray mode (string[])', () => { + it('returns empty array when headers is undefined', () => { + expect(getHeader(undefined as unknown as IncomingHttpHeaders, 'origin', true)).toEqual([]); + }); + + it('returns empty array when header does not exist', () => { + expect(getHeader({ origin: 'localhost:3000' }, 'host', true)).toEqual([]); + }); + + it('returns array with single value when header is string', () => { + expect(getHeader({ origin: 'localhost:3000' }, 'origin', true)).toEqual(['localhost:3000']); + }); + + it('returns array when header already array', () => { + expect(getHeader({ 'x-forwarded-for': ['127.0.0.1', '10.0.0.1'] }, 'x-forwarded-for', true)).toEqual(['127.0.0.1', '10.0.0.1']); + }); + + it('returns empty array when value is empty array', () => { + expect(getHeader({ 'x-forwarded-for': [] }, 'x-forwarded-for', true)).toEqual([]); + }); }); - it('returns empty string when header value is undefined', () => { - expect(getHeader({ origin: undefined }, 'origin')).toBe(''); + describe('IncomingHttpHeaders support', () => { + it('works with IncomingHttpHeaders', () => { + const headers: IncomingHttpHeaders = { + host: 'localhost:3000', + connection: 'keep-alive', + }; + + expect(getHeader(headers, 'host')).toBe('localhost:3000'); + expect(getHeader(headers, 'origin')).toBe(''); + }); }); - it('works with IncomingHttpHeaders shape', () => { - const headers: IncomingHttpHeaders = { - host: 'localhost:3000', - connection: 'keep-alive', - }; + describe('Headers support', () => { + it('works with Headers', () => { + const headers = new Headers(); + headers.set('host', 'localhost:3000'); - expect(getHeader(headers, 'host')).toBe('localhost:3000'); + expect(getHeader(headers, 'host')).toBe('localhost:3000'); + expect(getHeader(headers, 'origin')).toEqual(''); + }); }); }); diff --git a/packages/tools/src/getHeader.ts b/packages/tools/src/getHeader.ts index 92bbc53389163..82d050a8698f7 100644 --- a/packages/tools/src/getHeader.ts +++ b/packages/tools/src/getHeader.ts @@ -2,29 +2,34 @@ import type { IncomingHttpHeaders } from 'http'; type HeaderLike = IncomingHttpHeaders | Headers | Record; -export const getHeader = (headers: HeaderLike, key: string): string => { +export function getHeader(headers: HeaderLike, key: string): string; + +export function getHeader(headers: HeaderLike, key: string, asArray: true): string[]; + +export function getHeader(headers: HeaderLike, key: string, asArray = false): string | string[] { if (!headers) { - return ''; + return asArray ? [] : ''; } - if (isHeadersType(headers)) { - return headers.get(key) ?? ''; - } + let value: string | string[] | undefined; - const value = headers[key]; + if (isHeadersType(headers)) { + value = headers.get(key) ?? undefined; + } else { + value = headers[key]; + } if (Array.isArray(value)) { - return value[0] ?? ''; + return asArray ? value : (value[0] ?? ''); } if (typeof value === 'string') { - return value; + return asArray ? [value] : value; } - return ''; -}; - - -function isHeadersType(headers: HeaderLike): headers is Headers{ - return headers instanceof Headers; + return asArray ? [] : ''; } + +const isHeadersType = (headers: HeaderLike): headers is Headers => { + return headers instanceof Headers; +}; From 497e50d30121dde8ac3c0619a6d1cdb369fc1159 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 27 Jan 2026 15:14:34 -0300 Subject: [PATCH 24/26] remove getClientAddress, make getHeader generic --- apps/meteor/server/hooks/sauMonitorHooks.ts | 3 +- .../services/device-management/service.ts | 3 +- .../server/services/sauMonitor/service.ts | 3 +- packages/tools/src/getHeader.spec.ts | 38 ++++++------------- packages/tools/src/getHeader.ts | 30 +++------------ 5 files changed, 20 insertions(+), 57 deletions(-) diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 0d73c8c288c81..8e07e96f807a0 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -5,7 +5,6 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ILoginAttempt } from '../../app/authentication/server/ILoginAttempt'; -import { getClientAddress } from '../lib/getClientAddress'; import { deviceManagementEvents } from '../services/device-management/events'; import { sauEvents } from '../services/sauMonitor/events'; @@ -22,7 +21,7 @@ Accounts.onLogin((info: ILoginAttempt) => { const { resume } = methodArguments.find((arg) => 'resume' in arg) ?? {}; const loginToken = resume ? hashLoginToken(resume) : ''; const instanceId = InstanceStatus.id(); - const clientAddress = getClientAddress(info.connection); + const clientAddress = info.connection.clientAddress ?? getHeader(httpHeaders, 'x-real-ip'); const userAgent = getHeader(httpHeaders, 'user-agent'); const host = getHeader(httpHeaders, 'host'); diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index 35116064eeef7..5d9fa0ba9479f 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -3,7 +3,6 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import { getHeader } from '@rocket.chat/tools'; import { deviceManagementEvents } from './events'; -import { getClientAddress } from '../../lib/getClientAddress'; export class DeviceManagementService extends ServiceClassInternal implements IDeviceManagementService { protected name = 'device-management'; @@ -15,7 +14,7 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe deviceManagementEvents.emit('device-login', { userId, userAgent: getHeader(connection.httpHeaders, 'user-agent'), - clientAddress: getClientAddress(connection), + clientAddress: connection.clientAddress ?? getHeader(connection.httpHeaders, 'x-real-ip'), loginToken: connection.loginToken, }); }); diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index 03aacb7242793..70fcdd4b915be 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -5,7 +5,6 @@ import type { ISAUMonitorService } from '@rocket.chat/core-services'; import { getHeader } from '@rocket.chat/tools'; import { sauEvents } from './events'; -import { getClientAddress } from '../../lib/getClientAddress'; export class SAUMonitorService extends ServiceClassInternal implements ISAUMonitorService { protected name = 'sau-monitor'; @@ -19,7 +18,7 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit instanceId: connection.instanceId, connectionId: connection.id, loginToken: connection.loginToken, - clientAddress: getClientAddress(connection), + clientAddress: connection.clientAddress ?? getHeader(connection.httpHeaders, 'x-real-ip'), userAgent: getHeader(connection.httpHeaders, 'user-agent'), host: getHeader(connection.httpHeaders, 'host'), }); diff --git a/packages/tools/src/getHeader.spec.ts b/packages/tools/src/getHeader.spec.ts index 4a96fa35e9b9b..71589322fc79f 100644 --- a/packages/tools/src/getHeader.spec.ts +++ b/packages/tools/src/getHeader.spec.ts @@ -20,49 +20,35 @@ describe('getHeader', () => { expect(getHeader({}, 'origin')).toBe(''); }); - it('returns first value when header is array', () => { - expect(getHeader({ 'x-forwarded-for': ['127.0.0.1', '10.0.0.1'] }, 'x-forwarded-for')).toBe('127.0.0.1'); - }); - - it('returns empty string when value is empty array', () => { - expect(getHeader({ 'x-forwarded-for': [] }, 'x-forwarded-for')).toBe(''); - }); - it('returns empty string when value is undefined', () => { expect(getHeader({ origin: undefined }, 'origin')).toBe(''); }); }); - describe('asArray mode (string[])', () => { - it('returns empty array when headers is undefined', () => { - expect(getHeader(undefined as unknown as IncomingHttpHeaders, 'origin', true)).toEqual([]); - }); - - it('returns empty array when header does not exist', () => { - expect(getHeader({ origin: 'localhost:3000' }, 'host', true)).toEqual([]); + describe('generic array mode (string[])', () => { + it('returns the correct array when header is array', () => { + expect(getHeader({ 'x-forwarded-for': ['127.0.0.1', '10.0.0.1'] }, 'x-forwarded-for')).toStrictEqual([ + '127.0.0.1', + '10.0.0.1', + ]); }); - it('returns array with single value when header is string', () => { - expect(getHeader({ origin: 'localhost:3000' }, 'origin', true)).toEqual(['localhost:3000']); - }); - - it('returns array when header already array', () => { - expect(getHeader({ 'x-forwarded-for': ['127.0.0.1', '10.0.0.1'] }, 'x-forwarded-for', true)).toEqual(['127.0.0.1', '10.0.0.1']); + it('returns empty array when value is empty array', () => { + expect(getHeader({ 'x-forwarded-for': [] }, 'x-forwarded-for')).toStrictEqual([]); }); - it('returns empty array when value is empty array', () => { - expect(getHeader({ 'x-forwarded-for': [] }, 'x-forwarded-for', true)).toEqual([]); + it('returns string even when T is string[] (by design)', () => { + expect(getHeader({ origin: 'localhost:3000' }, 'origin')).toEqual('localhost:3000'); }); }); describe('IncomingHttpHeaders support', () => { it('works with IncomingHttpHeaders', () => { const headers: IncomingHttpHeaders = { - host: 'localhost:3000', connection: 'keep-alive', }; - expect(getHeader(headers, 'host')).toBe('localhost:3000'); + expect(getHeader(headers, 'connection')).toBe('keep-alive'); expect(getHeader(headers, 'origin')).toBe(''); }); }); @@ -73,7 +59,7 @@ describe('getHeader', () => { headers.set('host', 'localhost:3000'); expect(getHeader(headers, 'host')).toBe('localhost:3000'); - expect(getHeader(headers, 'origin')).toEqual(''); + expect(getHeader(headers, 'origin')).toBe(''); }); }); }); diff --git a/packages/tools/src/getHeader.ts b/packages/tools/src/getHeader.ts index 82d050a8698f7..bceaf6d364b07 100644 --- a/packages/tools/src/getHeader.ts +++ b/packages/tools/src/getHeader.ts @@ -2,34 +2,14 @@ import type { IncomingHttpHeaders } from 'http'; type HeaderLike = IncomingHttpHeaders | Headers | Record; -export function getHeader(headers: HeaderLike, key: string): string; - -export function getHeader(headers: HeaderLike, key: string, asArray: true): string[]; - -export function getHeader(headers: HeaderLike, key: string, asArray = false): string | string[] { +export const getHeader = (headers: HeaderLike, key: string): T => { if (!headers) { - return asArray ? [] : ''; - } - - let value: string | string[] | undefined; - - if (isHeadersType(headers)) { - value = headers.get(key) ?? undefined; - } else { - value = headers[key]; - } - - if (Array.isArray(value)) { - return asArray ? value : (value[0] ?? ''); + return '' as T; } - if (typeof value === 'string') { - return asArray ? [value] : value; + if (headers instanceof Headers) { + return (headers.get(key) ?? '') as T; } - return asArray ? [] : ''; -} - -const isHeadersType = (headers: HeaderLike): headers is Headers => { - return headers instanceof Headers; + return (headers[key] ?? '') as T; }; From 0700e172a4ebef02c373cd0e5a8468e90c2b228e Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 27 Jan 2026 16:11:30 -0300 Subject: [PATCH 25/26] change ?? by || --- apps/meteor/server/hooks/sauMonitorHooks.ts | 2 +- apps/meteor/server/services/device-management/service.ts | 2 +- apps/meteor/server/services/sauMonitor/service.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 8e07e96f807a0..f0ca4dd64e63a 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -21,7 +21,7 @@ Accounts.onLogin((info: ILoginAttempt) => { const { resume } = methodArguments.find((arg) => 'resume' in arg) ?? {}; const loginToken = resume ? hashLoginToken(resume) : ''; const instanceId = InstanceStatus.id(); - const clientAddress = info.connection.clientAddress ?? getHeader(httpHeaders, 'x-real-ip'); + const clientAddress = info.connection.clientAddress || getHeader(httpHeaders, 'x-real-ip'); const userAgent = getHeader(httpHeaders, 'user-agent'); const host = getHeader(httpHeaders, 'host'); diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index 5d9fa0ba9479f..ebe6d4ec3c5d4 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -14,7 +14,7 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe deviceManagementEvents.emit('device-login', { userId, userAgent: getHeader(connection.httpHeaders, 'user-agent'), - clientAddress: connection.clientAddress ?? getHeader(connection.httpHeaders, 'x-real-ip'), + clientAddress: connection.clientAddress || getHeader(connection.httpHeaders, 'x-real-ip'), loginToken: connection.loginToken, }); }); diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index 70fcdd4b915be..d4b48924caab4 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -18,7 +18,7 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit instanceId: connection.instanceId, connectionId: connection.id, loginToken: connection.loginToken, - clientAddress: connection.clientAddress ?? getHeader(connection.httpHeaders, 'x-real-ip'), + clientAddress: connection.clientAddress || getHeader(connection.httpHeaders, 'x-real-ip'), userAgent: getHeader(connection.httpHeaders, 'user-agent'), host: getHeader(connection.httpHeaders, 'host'), }); From 16167859f6a4ba7bb5489eea7ba2645bb51947e3 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 27 Jan 2026 17:42:56 -0300 Subject: [PATCH 26/26] move loginToken conditional upstream preventing device-login duplicated call --- .../ee/server/lib/deviceManagement/session.ts | 6 +----- apps/meteor/server/hooks/sauMonitorHooks.ts | 5 +++-- .../server/services/device-management/events.ts | 2 +- .../server/services/device-management/service.ts | 13 +++++++------ 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/meteor/ee/server/lib/deviceManagement/session.ts b/apps/meteor/ee/server/lib/deviceManagement/session.ts index f5d8acb4c0843..a613fbb0a11d0 100644 --- a/apps/meteor/ee/server/lib/deviceManagement/session.ts +++ b/apps/meteor/ee/server/lib/deviceManagement/session.ts @@ -32,17 +32,13 @@ const uaParser = async ( }; export const listenSessionLogin = () => { - return deviceManagementEvents.on('device-login', async ({ userId, userAgent, loginToken, clientAddress }) => { + return deviceManagementEvents.on('device-login', async ({ userId, userAgent, clientAddress }) => { const deviceEnabled = settings.get('Device_Management_Enable_Login_Emails'); if (!deviceEnabled) { return; } - if (loginToken) { - return; - } - const user = await Users.findOneByIdWithEmailAddress(userId, { projection: { 'name': 1, 'username': 1, 'emails': 1, 'settings.preferences.receiveLoginDetectionEmail': 1 }, }); diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index f0ca4dd64e63a..ca985869af877 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -35,7 +35,9 @@ Accounts.onLogin((info: ILoginAttempt) => { host, }); - deviceManagementEvents.emit('device-login', { userId: info.user._id, userAgent, loginToken, clientAddress }); + if (!loginToken) { + deviceManagementEvents.emit('device-login', { userId: info.user._id, userAgent, clientAddress }); + } }); Accounts.onLogout((info) => { @@ -53,6 +55,5 @@ Meteor.onConnection((connection) => { }); Meteor.onConnection((connection) => { - // in case of implementing a listener of this event, define the parameters type in services/sauMonitor/events.ts sauEvents.emit('sau.socket.connected', { instanceId: InstanceStatus.id(), connectionId: connection.id }); }); diff --git a/apps/meteor/server/services/device-management/events.ts b/apps/meteor/server/services/device-management/events.ts index 1b014f38caf49..488b2e958d435 100644 --- a/apps/meteor/server/services/device-management/events.ts +++ b/apps/meteor/server/services/device-management/events.ts @@ -1,5 +1,5 @@ import { Emitter } from '@rocket.chat/emitter'; export const deviceManagementEvents = new Emitter<{ - 'device-login': { userId: string; userAgent: string; loginToken?: string; clientAddress: string }; + 'device-login': { userId: string; userAgent: string; clientAddress: string }; }>(); diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index ebe6d4ec3c5d4..b3f1402bc786a 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -11,12 +11,13 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe super(); this.onEvent('accounts.login', async ({ userId, connection }) => { - deviceManagementEvents.emit('device-login', { - userId, - userAgent: getHeader(connection.httpHeaders, 'user-agent'), - clientAddress: connection.clientAddress || getHeader(connection.httpHeaders, 'x-real-ip'), - loginToken: connection.loginToken, - }); + if (!connection.loginToken) { + deviceManagementEvents.emit('device-login', { + userId, + userAgent: getHeader(connection.httpHeaders, 'user-agent'), + clientAddress: connection.clientAddress || getHeader(connection.httpHeaders, 'x-real-ip'), + }); + } }); } }