diff --git a/.changeset/brave-snakes-scream.md b/.changeset/brave-snakes-scream.md new file mode 100644 index 000000000000..914f248cd821 --- /dev/null +++ b/.changeset/brave-snakes-scream.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where broadcasted events were published twice within the same instance diff --git a/.changeset/fluffy-monkeys-sing.md b/.changeset/fluffy-monkeys-sing.md new file mode 100644 index 000000000000..db93491b0ecd --- /dev/null +++ b/.changeset/fluffy-monkeys-sing.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Changed the name of the administration Logs page to "Records", implemented a tab layout in this page and added a new tab called "Analytic reports" that shows the most recent result of the statistics endpoint. diff --git a/.changeset/large-pandas-beam.md b/.changeset/large-pandas-beam.md new file mode 100644 index 000000000000..19f1eade9a9b --- /dev/null +++ b/.changeset/large-pandas-beam.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +New setting to automatically enable autotranslate when joining rooms diff --git a/.changeset/nice-chairs-add.md b/.changeset/nice-chairs-add.md new file mode 100644 index 000000000000..dfc9d763e1c0 --- /dev/null +++ b/.changeset/nice-chairs-add.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +--- + +Added `push` statistic, containing three bits. Each bit represents a boolean: +``` +1 1 1 +| | | +| | +- push enabled = 0b1 = 1 +| +--- push gateway enabled = 0b10 = 2 ++----- push gateway changed = 0b100 = 4 +``` diff --git a/.changeset/quiet-phones-reply.md b/.changeset/quiet-phones-reply.md new file mode 100644 index 000000000000..f2735e615491 --- /dev/null +++ b/.changeset/quiet-phones-reply.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Search users using full name too on share message modal diff --git a/.changeset/thirty-jokes-compete.md b/.changeset/thirty-jokes-compete.md new file mode 100644 index 000000000000..9d4095e7771b --- /dev/null +++ b/.changeset/thirty-jokes-compete.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +chore: Deprecate un-used meteor method for omnichannel analytics diff --git a/.changeset/thirty-pumpkins-fix.md b/.changeset/thirty-pumpkins-fix.md new file mode 100644 index 000000000000..11b92b064e15 --- /dev/null +++ b/.changeset/thirty-pumpkins-fix.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/tools': minor +'@rocket.chat/meteor': minor +--- + +Added option to select between two script engine options for the integrations diff --git a/.changeset/twelve-files-deny.md b/.changeset/twelve-files-deny.md new file mode 100644 index 000000000000..123bf0a7764b --- /dev/null +++ b/.changeset/twelve-files-deny.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/license': minor +'@rocket.chat/jwt': minor +'@rocket.chat/omnichannel-services': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/stream-hub-service': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/account-service': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/queue-worker': minor +'@rocket.chat/presence': minor +'@rocket.chat/meteor': minor +--- + +Implemented the License library, it is used to handle the functionality like expiration date, modules, limits, etc. +Also added a version v3 of the license, which contains an extended list of features. +v2 is still supported, since we convert it to v3 on the fly. diff --git a/.github/workflows/vulnerabilities-jira-integration.yml b/.github/workflows/vulnerabilities-jira-integration.yml new file mode 100644 index 000000000000..2daeb533937d --- /dev/null +++ b/.github/workflows/vulnerabilities-jira-integration.yml @@ -0,0 +1,22 @@ +name: Github vulnerabilities and jira board integration + +on: + schedule: + - cron: '0 1 * * *' + +jobs: + IntegrateSecurityVulnerabilities: + runs-on: ubuntu-latest + steps: + - name: "Github vulnerabilities and jira board integration" + uses: RocketChat/github-vulnerabilities-jira-integration@v0.3 + env: + JIRA_URL: https://rocketchat.atlassian.net/ + JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} + GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} + JIRA_EMAIL: security-team-accounts@rocket.chat + JIRA_PROJECT_ID: GJIT + UID_CUSTOMFIELD_ID: customfield_10059 + JIRA_COMPLETE_PHASE_ID: 31 + JIRA_START_PHASE_ID: 11 + diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 62a0476d9077..003baa57aa8b 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -15,6 +15,11 @@ RUN set -x \ && npm install sharp@0.30.4 \ && mv node_modules/sharp npm/node_modules/sharp \ # End hack for sharp + # Start hack for isolated-vm... + && rm -rf npm/node_modules/isolated-vm \ + && npm install isolated-vm@4.4.2 \ + && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ + # End hack for isolated-vm && cd npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force \ diff --git a/apps/meteor/app/api/server/default/info.ts b/apps/meteor/app/api/server/default/info.ts index b7806ab08f32..8297f90fffd9 100644 --- a/apps/meteor/app/api/server/default/info.ts +++ b/apps/meteor/app/api/server/default/info.ts @@ -8,7 +8,6 @@ API.default.addRoute( { async get() { const user = await getLoggedInUser(this.request); - return API.v1.success(await getServerInfo(user?._id)); }, }, diff --git a/apps/meteor/app/api/server/lib/getServerInfo.spec.ts b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts new file mode 100644 index 000000000000..ca55cfa33e3e --- /dev/null +++ b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const hasAllPermissionAsyncMock = sinon.stub(); +const getCachedSupportedVersionsTokenMock = sinon.stub(); + +const { getServerInfo } = proxyquire.noCallThru().load('./getServerInfo', { + '../../../utils/rocketchat.info': { + Info: { + version: '3.0.1', + }, + }, + '../../../authorization/server/functions/hasPermission': { + hasPermissionAsync: hasAllPermissionAsyncMock, + }, + '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken': { + getCachedSupportedVersionsToken: getCachedSupportedVersionsTokenMock, + }, + '../../../settings/server': { + settings: new Map(), + }, +}); +describe('#getServerInfo()', () => { + beforeEach(() => { + hasAllPermissionAsyncMock.reset(); + getCachedSupportedVersionsTokenMock.reset(); + }); + + it('should return only the version (without the patch info) when the user is not present', async () => { + expect(await getServerInfo(undefined)).to.be.eql({ version: '3.0' }); + }); + + it('should return only the version (without the patch info) when the user present but they dont have permission', async () => { + hasAllPermissionAsyncMock.resolves(false); + expect(await getServerInfo('userId')).to.be.eql({ version: '3.0' }); + }); + + it('should return the info object + the supportedVersions from the cloud when the request to the cloud was a success', async () => { + const signedJwt = 'signedJwt'; + hasAllPermissionAsyncMock.resolves(true); + getCachedSupportedVersionsTokenMock.resolves(signedJwt); + expect(await getServerInfo('userId')).to.be.eql({ info: { version: '3.0.1', supportedVersions: signedJwt } }); + }); + + it('should return the info object ONLY from the cloud when the request to the cloud was NOT a success', async () => { + hasAllPermissionAsyncMock.resolves(true); + getCachedSupportedVersionsTokenMock.rejects(); + expect(await getServerInfo('userId')).to.be.eql({ info: { version: '3.0.1' } }); + }); +}); diff --git a/apps/meteor/app/api/server/lib/getServerInfo.ts b/apps/meteor/app/api/server/lib/getServerInfo.ts index 39f4b82b350b..53ba3656babe 100644 --- a/apps/meteor/app/api/server/lib/getServerInfo.ts +++ b/apps/meteor/app/api/server/lib/getServerInfo.ts @@ -1,23 +1,37 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Info } from '../../../utils/rocketchat.info'; +import { + getCachedSupportedVersionsToken, + wrapPromise, +} from '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken'; +import { Info, minimumClientVersions } from '../../../utils/rocketchat.info'; -type ServerInfo = - | { - info: typeof Info; - } - | { - version: string | undefined; - }; +type ServerInfo = { + info?: typeof Info; + supportedVersions?: { signed: string }; + minimumClientVersions: typeof minimumClientVersions; + version: string; +}; const removePatchInfo = (version: string): string => version.replace(/(\d+\.\d+).*/, '$1'); export async function getServerInfo(userId?: string): Promise { - if (userId && (await hasPermissionAsync(userId, 'get-server-info'))) { - return { - info: Info, - }; - } + const hasPermissionToViewStatistics = userId && (await hasPermissionAsync(userId, 'view-statistics')); + const supportedVersionsToken = await wrapPromise(getCachedSupportedVersionsToken()); + return { version: removePatchInfo(Info.version), + + ...(hasPermissionToViewStatistics && { + info: { + ...Info, + }, + version: Info.version, + }), + + minimumClientVersions, + ...(supportedVersionsToken.success && + supportedVersionsToken.result && { + supportedVersions: { signed: supportedVersionsToken.result }, + }), }; } diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts index 02fc30763eeb..7be5b1fc13fe 100644 --- a/apps/meteor/app/api/server/v1/federation.ts +++ b/apps/meteor/app/api/server/v1/federation.ts @@ -1,7 +1,7 @@ import { Federation, FederationEE } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { isFederationVerifyMatrixIdProps } from '@rocket.chat/rest-typings'; -import { isEnterprise } from '../../../../ee/app/license/server'; import { API } from '../api'; API.v1.addRoute( @@ -14,7 +14,7 @@ API.v1.addRoute( async get() { const { matrixIds } = this.queryParams; - const federationService = isEnterprise() ? FederationEE : Federation; + const federationService = License.hasValidLicense() ? FederationEE : Federation; const results = await federationService.verifyMatrixIds(matrixIds); diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index 6ffd0005c764..e1ee82d72478 100644 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ b/apps/meteor/app/api/server/v1/voip/omnichannel.ts @@ -78,7 +78,6 @@ API.v1.addRoute( } try { - logger.debug(`Setting extension ${extension} for agent with id ${user._id}`); await Users.setExtension(user._id, extension); return API.v1.success(); } catch (e) { @@ -146,7 +145,6 @@ API.v1.addRoute( return API.v1.notFound(); } if (!user.extension) { - logger.debug(`User ${user._id} is not associated with any extension. Skipping`); return API.v1.success(); } diff --git a/apps/meteor/app/autotranslate/server/msTranslate.ts b/apps/meteor/app/autotranslate/server/msTranslate.ts index 3e9c9dbd8a35..f885a23b8e6b 100644 --- a/apps/meteor/app/autotranslate/server/msTranslate.ts +++ b/apps/meteor/app/autotranslate/server/msTranslate.ts @@ -87,7 +87,7 @@ class MsAutoTranslate extends AutoTranslate { if (this.supportedLanguages[target]) { return this.supportedLanguages[target]; } - const request = await fetch(this.apiEndPointUrl); + const request = await fetch(this.apiGetLanguages); if (!request.ok) { throw new Error(request.statusText); } diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index d65897b72094..2ad8ba29072a 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -1,50 +1,52 @@ -import type { SettingValue } from '@rocket.chat/core-typings'; import { Statistics, Users } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { statistics } from '../../../statistics/server'; +import { Info } from '../../../utils/rocketchat.info'; import { LICENSE_VERSION } from '../license'; -type WorkspaceRegistrationData = { +export type WorkspaceRegistrationData = { uniqueId: string; - workspaceId: SettingValue; - address: SettingValue; + workspaceId: string; + address: string; contactName: string; contactEmail: T; seats: number; - allowMarketing: SettingValue; - accountName: SettingValue; - organizationType: unknown; - industry: unknown; - orgSize: unknown; - country: unknown; - language: unknown; - agreePrivacyTerms: SettingValue; - website: SettingValue; - siteName: SettingValue; + + organizationType: string; + industry: string; + orgSize: string; + country: string; + language: string; + allowMarketing: string; + accountName: string; + agreePrivacyTerms: string; + website: string; + siteName: string; workspaceType: unknown; deploymentMethod: string; deploymentPlatform: string; - version: unknown; + version: string; licenseVersion: number; enterpriseReady: boolean; setupComplete: boolean; connectionDisable: boolean; - npsEnabled: SettingValue; + npsEnabled: string; + MAC: number; }; export async function buildWorkspaceRegistrationData(contactEmail: T): Promise> { const stats = (await Statistics.findLast()) || (await statistics.get()); - const address = settings.get('Site_Url'); - const siteName = settings.get('Site_Name'); - const workspaceId = settings.get('Cloud_Workspace_Id'); - const allowMarketing = settings.get('Allow_Marketing_Emails'); - const accountName = settings.get('Organization_Name'); - const website = settings.get('Website'); - const npsEnabled = settings.get('NPS_survey_enabled'); - const agreePrivacyTerms = settings.get('Cloud_Service_Agree_PrivacyTerms'); - const setupWizardState = settings.get('Show_Setup_Wizard'); + const address = settings.get('Site_Url'); + const siteName = settings.get('Site_Name'); + const workspaceId = settings.get('Cloud_Workspace_Id'); + const allowMarketing = settings.get('Allow_Marketing_Emails'); + const accountName = settings.get('Organization_Name'); + const website = settings.get('Website'); + const npsEnabled = settings.get('NPS_survey_enabled'); + const agreePrivacyTerms = settings.get('Cloud_Service_Agree_PrivacyTerms'); + const setupWizardState = settings.get('Show_Setup_Wizard'); const firstUser = await Users.getOldest({ projection: { name: 1, emails: 1 } }); const contactName = firstUser?.name || ''; @@ -61,22 +63,24 @@ export async function buildWorkspaceRegistrationData { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/clients`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + if (!payload) { + return undefined; + } + + return payload; +}; + export async function connectWorkspace(token: string) { - // shouldn't get here due to checking this on the method - // but this is just to double check if (!token) { - return new Error('Invalid token; the registration token is required.'); + throw new CloudWorkspaceConnectionError('Invalid registration token'); } - const redirectUri = getRedirectUri(); + try { + const redirectUri = getRedirectUri(); - const regInfo = { - email: settings.get('Organization_Email'), - client_name: settings.get('Site_Name'), - redirect_uris: [redirectUri], - }; + const body = { + email: settings.get('Organization_Email'), + client_name: settings.get('Site_Name'), + redirect_uris: [redirectUri], + }; - const cloudUrl = settings.get('Cloud_Url'); - let result; - try { - const request = await fetch(`${cloudUrl}/api/oauth/clients`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, - body: regInfo, - }); + const payload = await fetchRegistrationDataPayload({ token, body }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!payload) { + return false; } - result = await request.json(); - } catch (err: any) { + await saveRegistrationData(payload); + + return true; + } catch (err) { SystemLogger.error({ msg: 'Failed to Connect with Rocket.Chat Cloud', url: '/api/oauth/clients', @@ -45,12 +76,4 @@ export async function connectWorkspace(token: string) { return false; } - - if (!result) { - return false; - } - - await saveRegistrationData(result); - - return true; } diff --git a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts index 780aa5c67a99..61b3a77966e7 100644 --- a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts +++ b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts @@ -14,15 +14,15 @@ export async function finishOAuthAuthorization(code: string, state: string) { }); } - const cloudUrl = settings.get('Cloud_Url'); const clientId = settings.get('Cloud_Workspace_Client_Id'); const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); const scope = userScopes.join(' '); - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/oauth/token`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, params: new URLSearchParams({ @@ -35,11 +35,11 @@ export async function finishOAuthAuthorization(code: string, state: string) { }), }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err) { SystemLogger.error({ msg: 'Failed to finish OAuth authorization with Rocket.Chat Cloud', @@ -51,7 +51,7 @@ export async function finishOAuthAuthorization(code: string, state: string) { } const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + result.expires_in); + expiresAt.setSeconds(expiresAt.getSeconds() + payload.expires_in); const uid = Meteor.userId(); if (!uid) { @@ -65,11 +65,11 @@ export async function finishOAuthAuthorization(code: string, state: string) { { $set: { 'services.cloud': { - accessToken: result.access_token, + accessToken: payload.access_token, expiresAt, - scope: result.scope, - tokenType: result.token_type, - refreshToken: result.refresh_token, + scope: payload.scope, + tokenType: payload.token_type, + refreshToken: payload.refresh_token, }, }, }, diff --git a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts index 4a35c9834ba5..2c5d9dec77dc 100644 --- a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts +++ b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts @@ -5,16 +5,16 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; export async function getConfirmationPoll(deviceCode: string): Promise { - const cloudUrl = settings.get('Cloud_Url'); - - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/poll`, { params: { token: deviceCode } }); - if (!request.ok) { - throw new Error((await request.json()).error); + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/poll`, { params: { token: deviceCode } }); + + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to get confirmation poll from Rocket.Chat Cloud', @@ -25,9 +25,9 @@ export async function getConfirmationPoll(deviceCode: string): Promise>(userId, { projection: { 'services.cloud': 1 } }); - if (!user?.services?.cloud?.accessToken || !user?.services?.cloud?.refreshToken) { - return ''; - } - - const { accessToken, refreshToken, expiresAt } = user.services.cloud; - - const clientId = settings.get('Cloud_Workspace_Client_Id'); - if (!clientId) { - return ''; - } - - const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); - if (!clientSecret) { - return ''; - } - - const now = new Date(); - - if (now < expiresAt && !forceNew) { - return accessToken; - } - - const cloudUrl = settings.get('Cloud_Url'); - const redirectUri = getRedirectUri(); - - if (scope === '') { - scope = userScopes.join(' '); - } - - let authTokenResult; - try { - const request = await fetch(`${cloudUrl}/api/oauth/token`, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - method: 'POST', - params: new URLSearchParams({ - client_id: clientId, - client_secret: clientSecret, - refresh_token: refreshToken, - scope, - grant_type: 'refresh_token', - redirect_uri: redirectUri, - }), - }); - - if (!request.ok) { - throw new Error((await request.json()).error); - } - - authTokenResult = await request.json(); - } catch (err: any) { - SystemLogger.error({ - msg: 'Failed to get User AccessToken from Rocket.Chat Cloud', - url: '/api/oauth/token', - err, - }); - - if (err) { - if (err.message.includes('oauth_invalid_client_credentials')) { - SystemLogger.error('Server has been unregistered from cloud'); - await removeWorkspaceRegistrationInfo(); - } - - if (err.message.includes('unauthorized')) { - await userLoggedOut(userId); - } - } - - return ''; - } - - if (save) { - const willExpireAt = new Date(); - willExpireAt.setSeconds(willExpireAt.getSeconds() + authTokenResult.expires_in); - - await Users.updateOne( - { _id: user._id }, - { - $set: { - 'services.cloud': { - accessToken: authTokenResult.access_token, - expiresAt: willExpireAt, - }, - }, - }, - ); - } - - return authTokenResult.access_token; -} diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts index 2b731ef82757..b495e3342d4b 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts @@ -10,7 +10,7 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; * @param {boolean} save * @returns string */ -export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) { +export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true): Promise { const { workspaceRegistered } = await retrieveRegistrationStatus(); if (!workspaceRegistered) { @@ -22,10 +22,11 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save if (expires === null) { throw new Error('Cloud_Workspace_Access_Token_Expires_At is not set'); } + const now = new Date(); if (expires.value && now < expires.value && !forceNew) { - return settings.get('Cloud_Workspace_Access_Token'); + return settings.get('Cloud_Workspace_Access_Token'); } const accessToken = await getWorkspaceAccessTokenWithScope(scope); @@ -39,3 +40,46 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save return accessToken.token; } + +export class CloudWorkspaceAccessTokenError extends Error { + constructor() { + super('Could not get workspace access token'); + } +} + +export async function getWorkspaceAccessTokenOrThrow(forceNew = false, scope = '', save = true): Promise { + const token = await getWorkspaceAccessToken(forceNew, scope, save); + + if (!token) { + throw new CloudWorkspaceAccessTokenError(); + } + + return token; +} + +export const generateWorkspaceBearerHttpHeaderOrThrow = async ( + forceNew = false, + scope = '', + save = true, +): Promise<{ Authorization: string }> => { + const token = await getWorkspaceAccessTokenOrThrow(forceNew, scope, save); + return { + Authorization: `Bearer ${token}`, + }; +}; + +export const generateWorkspaceBearerHttpHeader = async ( + forceNew = false, + scope = '', + save = true, +): Promise<{ Authorization: string } | undefined> => { + const token = await getWorkspaceAccessToken(forceNew, scope, save); + + if (!token) { + return undefined; + } + + return { + Authorization: `Bearer ${token}`, + }; +}; diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts index 351b4cba20e5..88509902cb6d 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts @@ -26,12 +26,11 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { scope = workspaceScopes.join(' '); } - const cloudUrl = settings.get('Cloud_Url'); // eslint-disable-next-line @typescript-eslint/naming-convention const client_secret = settings.get('Cloud_Workspace_Client_Secret'); const redirectUri = getRedirectUri(); - let authTokenResult; + let payload; try { const body = new URLSearchParams(); body.append('client_id', client_id); @@ -40,12 +39,13 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { body.append('grant_type', 'client_credentials'); body.append('redirect_uri', redirectUri); - const result = await fetch(`${cloudUrl}/api/oauth/token`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/token`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'POST', body, }); - authTokenResult = await result.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to get Workspace AccessToken from Rocket.Chat Cloud', @@ -64,10 +64,10 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { } const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + authTokenResult.expires_in); + expiresAt.setSeconds(expiresAt.getSeconds() + payload.expires_in); tokenResponse.expiresAt = expiresAt; - tokenResponse.token = authTokenResult.access_token; + tokenResponse.token = payload.access_token; return tokenResponse; } diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts index 275e646e5343..2ab8a4b27a62 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts @@ -1,68 +1,94 @@ +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; import { callbacks } from '../../../../lib/callbacks'; +import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceLicenseError } from '../../../../lib/errors/CloudWorkspaceLicenseError'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { LICENSE_VERSION } from '../license'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; +const workspaceLicensePayloadSchema = v.object({ + version: v.number().required(), + address: v.string().required(), + license: v.string().required(), + updatedAt: v.string().format('date-time').required(), + modules: v.string().required(), + expireAt: v.string().format('date-time').required(), +}); + +const assertWorkspaceLicensePayload = compile(workspaceLicensePayloadSchema); + +const fetchCloudWorkspaceLicensePayload = async ({ token }: { token: string }): Promise> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/license`, { + headers: { + Authorization: `Bearer ${token}`, + }, + params: { + version: LICENSE_VERSION, + }, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + assertWorkspaceLicensePayload(payload); + + return payload; +}; + export async function getWorkspaceLicense(): Promise<{ updated: boolean; license: string }> { const currentLicense = await Settings.findOne('Cloud_Workspace_License'); + // it should never happen, since even if the license is not found, it will return an empty settings + if (!currentLicense?._updatedAt) { + throw new CloudWorkspaceLicenseError('Failed to retrieve current license'); + } - const cachedLicenseReturn = async () => { - const license = currentLicense?.value as string; + const fromCurrentLicense = async () => { + const license = currentLicense?.value as string | undefined; if (license) { await callbacks.run('workspaceLicenseChanged', license); } - return { updated: false, license }; + return { updated: false, license: license ?? '' }; }; - const token = await getWorkspaceAccessToken(); - if (!token) { - return cachedLicenseReturn(); - } - - let licenseResult; try { - const request = await fetch(`${settings.get('Cloud_Workspace_Registration_Client_Uri')}/license`, { - headers: { - Authorization: `Bearer ${token}`, - }, - params: { - version: LICENSE_VERSION, - }, - }); + const token = await getWorkspaceAccessToken(); + if (!token) { + return fromCurrentLicense(); + } + + const payload = await fetchCloudWorkspaceLicensePayload({ token }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (Date.parse(payload.updatedAt) <= currentLicense._updatedAt.getTime()) { + return fromCurrentLicense(); } - licenseResult = await request.json(); - } catch (err: any) { + await Settings.updateValueById('Cloud_Workspace_License', payload.license); + + await callbacks.run('workspaceLicenseChanged', payload.license); + + return { updated: true, license: payload.license }; + } catch (err) { SystemLogger.error({ msg: 'Failed to update license from Rocket.Chat Cloud', url: '/license', err, }); - return cachedLicenseReturn(); - } - - const remoteLicense = licenseResult; - - if (!currentLicense || !currentLicense._updatedAt) { - throw new Error('Failed to retrieve current license'); - } - - if (remoteLicense.updatedAt <= currentLicense._updatedAt) { - return cachedLicenseReturn(); + return fromCurrentLicense(); } - - await Settings.updateValueById('Cloud_Workspace_License', remoteLicense.license); - - await callbacks.run('workspaceLicenseChanged', remoteLicense.license); - - return { updated: true, license: remoteLicense.license }; } diff --git a/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts b/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts index db425d2e8a30..7ee02a5e5de4 100644 --- a/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts @@ -11,7 +11,7 @@ export async function reconnectWorkspace() { await Settings.updateValueById('Register_Server', true); - await syncWorkspace(true); + await syncWorkspace(); return true; } diff --git a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts index 2a04aa54cfe7..ce415d2aa983 100644 --- a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts +++ b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts @@ -15,16 +15,16 @@ export async function registerPreIntentWorkspaceWizard(): Promise { } const regInfo = await buildWorkspaceRegistrationData(email); - const cloudUrl = settings.get('Cloud_Url'); try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/pre-intent`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/pre-intent`, { + method: 'POST', body: regInfo, timeout: 10 * 1000, - method: 'POST', }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } return true; diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index af74fcd7d211..5f5df80d0d3d 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -10,7 +10,7 @@ import { syncWorkspace } from './syncWorkspace'; export async function startRegisterWorkspace(resend = false) { const { workspaceRegistered } = await retrieveRegistrationStatus(); if (workspaceRegistered || process.env.TEST_MODE) { - await syncWorkspace(true); + await syncWorkspace(); return true; } @@ -19,22 +19,21 @@ export async function startRegisterWorkspace(resend = false) { const regInfo = await buildWorkspaceRegistrationData(undefined); - const cloudUrl = settings.get('Cloud_Url'); - - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace`, { method: 'POST', body: regInfo, params: { resend, }, }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to register with Rocket.Chat Cloud', @@ -44,11 +43,11 @@ export async function startRegisterWorkspace(resend = false) { return false; } - if (!result) { + if (!payload) { return false; } - await Settings.updateValueById('Cloud_Workspace_Id', result.id); + await Settings.updateValueById('Cloud_Workspace_Id', payload.id); return true; } diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts index 3afe84c409ec..382478db61c7 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts @@ -7,22 +7,22 @@ import { buildWorkspaceRegistrationData } from './buildRegistrationData'; export async function startRegisterWorkspaceSetupWizard(resend = false, email: string): Promise { const regInfo = await buildWorkspaceRegistrationData(email); - const cloudUrl = settings.get('Cloud_Url'); - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/intent`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/intent`, { body: regInfo, method: 'POST', params: { resent: resend, }, }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to register workspace intent with Rocket.Chat Cloud', @@ -33,9 +33,9 @@ export async function startRegisterWorkspaceSetupWizard(resend = false, email: s throw err; } - if (!result) { + if (!payload) { throw new Error('Failed to fetch registration intent endpoint'); } - return result; + return payload; } diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.spec.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.spec.ts new file mode 100644 index 000000000000..183065fd92a6 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.spec.ts @@ -0,0 +1,23 @@ +import type { SignedSupportedVersions } from '@rocket.chat/server-cloud-communication'; + +import { supportedVersionsChooseLatest } from './supportedVersionsChooseLatest'; + +describe('supportedVersionsChooseLatest', () => { + test('should return the latest version', async () => { + const versionFromLicense: SignedSupportedVersions = { + signed: 'signed____', + timestamp: '2021-08-31T18:00:00.000Z', + versions: [], + }; + + const versionFromCloud: SignedSupportedVersions = { + signed: 'signed_------', + timestamp: '2021-08-31T19:00:00.000Z', + versions: [], + }; + + const result = await supportedVersionsChooseLatest(versionFromLicense, versionFromCloud); + + expect(result.timestamp).toBe(versionFromCloud.timestamp); + }); +}); diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts new file mode 100644 index 000000000000..f0683535de6b --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts @@ -0,0 +1,9 @@ +import type { SignedSupportedVersions } from '@rocket.chat/server-cloud-communication'; + +export const supportedVersionsChooseLatest = async (...tokens: (SignedSupportedVersions | undefined)[]) => { + const [token] = (tokens.filter(Boolean) as SignedSupportedVersions[]).sort((a, b) => { + return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); + }); + + return token; +}; diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts new file mode 100644 index 000000000000..577abd4383d0 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -0,0 +1,130 @@ +import type { SettingValue } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; +import { Settings } from '@rocket.chat/models'; +import type { SignedSupportedVersions, SupportedVersions } from '@rocket.chat/server-cloud-communication'; +import type { Response } from '@rocket.chat/server-fetch'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; + +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import { generateWorkspaceBearerHttpHeader } from '../getWorkspaceAccessToken'; +import { supportedVersionsChooseLatest } from './supportedVersionsChooseLatest'; + +declare module '@rocket.chat/license' { + interface ILicenseV3 { + supportedVersions?: SignedSupportedVersions; + } +} + +/** HELPERS */ + +export const wrapPromise = ( + promise: Promise, +): Promise< + | { + success: true; + result: T; + } + | { + success: false; + error: any; + } +> => + promise + .then((result) => ({ success: true, result } as const)) + .catch((error) => ({ + success: false, + error, + })); + +export const handleResponse = async (promise: Promise) => { + return wrapPromise( + (async () => { + const request = await promise; + if (!request.ok) { + if (request.size > 0) { + throw new Error((await request.json()).error); + } + throw new Error(request.statusText); + } + + return request.json(); + })(), + ); +}; + +const cacheValueInSettings = ( + key: string, + fn: () => Promise, +): (() => Promise) & { + reset: () => Promise; +} => { + const reset = async () => { + const value = await fn(); + + await Settings.updateValueById(key, value); + + return value; + }; + + return Object.assign( + async () => { + const storedValue = settings.get(key); + + if (storedValue) { + return storedValue; + } + + return reset(); + }, + { + reset, + }, + ); +}; + +/** CODE */ + +const getSupportedVersionsFromCloud = async () => { + if (process.env.CLOUD_SUPPORTED_VERSIONS_TOKEN) { + return { + success: true, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result: JSON.parse(process.env.CLOUD_SUPPORTED_VERSIONS!), + }; + } + + const headers = await generateWorkspaceBearerHttpHeader(); + + const response = await handleResponse( + fetch('https://releases.rocket.chat/v2/server/supportedVersions', { + headers, + }), + ); + + if (!response.success) { + SystemLogger.error({ + msg: 'Failed to communicate with Rocket.Chat Cloud', + url: 'https://releases.rocket.chat/v2/server/supportedVersions', + err: response.error, + }); + } + + return response; +}; + +const getSupportedVersionsToken = async () => { + /** + * Gets the supported versions from the license + * Gets the supported versions from the cloud + * Gets the latest version + * return the token + */ + + const [versionsFromLicense, response] = await Promise.all([License.getLicense(), getSupportedVersionsFromCloud()]); + + return (await supportedVersionsChooseLatest(versionsFromLicense?.supportedVersions, (response.success && response.result) || undefined)) + ?.signed; +}; + +export const getCachedSupportedVersionsToken = cacheValueInSettings('Cloud_Workspace_Supported_Versions_Token', getSupportedVersionsToken); diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts deleted file mode 100644 index c8a323e40f95..000000000000 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { NPS, Banner } from '@rocket.chat/core-services'; -import { Settings } from '@rocket.chat/models'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; - -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCreateNpsSurvey'; -import { settings } from '../../../settings/server'; -import { buildWorkspaceRegistrationData } from './buildRegistrationData'; -import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; -import { getWorkspaceLicense } from './getWorkspaceLicense'; -import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; - -export async function syncWorkspace(_reconnectCheck = false) { - const { workspaceRegistered } = await retrieveRegistrationStatus(); - if (!workspaceRegistered) { - return false; - } - - const info = await buildWorkspaceRegistrationData(undefined); - - const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri'); - - let result; - try { - const headers: Record = {}; - const token = await getWorkspaceAccessToken(true); - - if (token) { - headers.Authorization = `Bearer ${token}`; - } else { - return false; - } - - const request = await fetch(`${workspaceUrl}/client`, { - headers, - body: info, - method: 'POST', - }); - - if (!request.ok) { - throw new Error((await request.json()).error); - } - - result = await request.json(); - } catch (err: any) { - SystemLogger.error({ - msg: 'Failed to sync with Rocket.Chat Cloud', - url: '/client', - err, - }); - - return false; - } finally { - // aways fetch the license - await getWorkspaceLicense(); - } - - const data = result; - if (!data) { - return true; - } - - if (data.publicKey) { - await Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey); - } - - if (data.trial?.trialId) { - await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); - } - - if (data.nps) { - const { id: npsId, expireAt } = data.nps; - - const startAt = new Date(data.nps.startAt); - - await NPS.create({ - npsId, - startAt, - expireAt: new Date(expireAt), - createdBy: { - _id: 'rocket.cat', - username: 'rocket.cat', - }, - }); - - const now = new Date(); - - if (startAt.getFullYear() === now.getFullYear() && startAt.getMonth() === now.getMonth() && startAt.getDate() === now.getDate()) { - await getAndCreateNpsSurvey(npsId); - } - } - - // add banners - if (data.banners) { - for await (const banner of data.banners) { - const { createdAt, expireAt, startAt } = banner; - - await Banner.create({ - ...banner, - createdAt: new Date(createdAt), - expireAt: new Date(expireAt), - startAt: new Date(startAt), - }); - } - } - - return true; -} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts new file mode 100644 index 000000000000..26d98b4a7574 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts @@ -0,0 +1,116 @@ +import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; + +import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; +import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; +import { getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; +import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; +import { handleAnnouncementsOnWorkspaceSync, handleNpsOnWorkspaceSync } from './handleCommsSync'; +import { legacySyncWorkspace } from './legacySyncWorkspace'; + +const workspaceCommPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + nps: v.object({ + id: v.string().required(), + startAt: v.string().format('date-time').required(), + expireAt: v.string().format('date-time').required(), + }), + announcements: v.object({ + create: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + selector: v.object({ + roles: v.array(v.string()), + }), + platform: v.array(v.string().enum('web', 'mobile')).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + createdBy: v.string().enum('cloud', 'system').required(), + createdAt: v.string().format('date-time').required(), + dictionary: v.object({}).additional(v.object({}).additional(v.string())), + view: v.any(), + surface: v.string().enum('banner', 'modal').required(), + }), + ), + delete: v.array(v.string()), + }), +}); + +const assertWorkspaceCommPayload = compile(workspaceCommPayloadSchema); + +const fetchCloudAnnouncementsSync = async ({ + token, + data, +}: { + token: string; + data: Cloud.WorkspaceSyncRequestPayload; +}): Promise> => { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v3/comms/workspace`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: data, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + assertWorkspaceCommPayload(payload); + return payload; +}; + +export async function announcementSync() { + try { + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (!workspaceRegistered) { + throw new CloudWorkspaceRegistrationError('Workspace is not registered'); + } + + const token = await getWorkspaceAccessToken(true); + if (!token) { + throw new CloudWorkspaceAccessError('Workspace does not have a valid access token'); + } + + const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); + + const { nps, announcements } = await fetchCloudAnnouncementsSync({ + token, + data: workspaceRegistrationData, + }); + + if (nps) { + await handleNpsOnWorkspaceSync(nps); + } + + if (announcements) { + await handleAnnouncementsOnWorkspaceSync(announcements); + } + + return true; + } catch (err) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + url: '/sync', + err, + }); + } + + await legacySyncWorkspace(); +} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts new file mode 100644 index 000000000000..c8b07f8826cf --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/handleCommsSync.ts @@ -0,0 +1,65 @@ +import { NPS, Banner } from '@rocket.chat/core-services'; +import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; +import { CloudAnnouncements } from '@rocket.chat/models'; + +import { getAndCreateNpsSurvey } from '../../../../../server/services/nps/getAndCreateNpsSurvey'; + +export const handleNpsOnWorkspaceSync = async (nps: Exclude['nps'], undefined>) => { + const { id: npsId, expireAt } = nps; + + const startAt = new Date(nps.startAt); + + await NPS.create({ + npsId, + startAt, + expireAt: new Date(expireAt), + createdBy: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + }); + + const now = new Date(); + + if (startAt.getFullYear() === now.getFullYear() && startAt.getMonth() === now.getMonth() && startAt.getDate() === now.getDate()) { + await getAndCreateNpsSurvey(npsId); + } +}; + +export const handleBannerOnWorkspaceSync = async (banners: Exclude['banners'], undefined>) => { + for await (const banner of banners) { + const { createdAt, expireAt, startAt, inactivedAt, _updatedAt, ...rest } = banner; + + await Banner.create({ + ...rest, + createdAt: new Date(createdAt), + expireAt: new Date(expireAt), + startAt: new Date(startAt), + ...(inactivedAt && { inactivedAt: new Date(inactivedAt) }), + }); + } +}; + +const deserializeAnnouncement = (announcement: Serialized): Cloud.Announcement => ({ + ...announcement, + _updatedAt: new Date(announcement._updatedAt), + expireAt: new Date(announcement.expireAt), + startAt: new Date(announcement.startAt), + createdAt: new Date(announcement.createdAt), +}); + +export const handleAnnouncementsOnWorkspaceSync = async ( + announcements: Exclude['announcements'], undefined>, +) => { + const { create, delete: deleteIds } = announcements; + + if (deleteIds) { + await CloudAnnouncements.deleteMany({ _id: { $in: deleteIds } }); + } + + for await (const announcement of create.map(deserializeAnnouncement)) { + const { _id, ...rest } = announcement; + + await CloudAnnouncements.updateOne({ _id }, { $set: rest }, { upsert: true }); + } +}; diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts new file mode 100644 index 000000000000..3173e652afe5 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts @@ -0,0 +1,17 @@ +import { CloudWorkspaceAccessTokenError } from '../getWorkspaceAccessToken'; +import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken'; +import { announcementSync } from './announcementSync'; +import { syncCloudData } from './syncCloudData'; + +export async function syncWorkspace() { + try { + await syncCloudData(); + await announcementSync(); + } catch (error) { + if (error instanceof CloudWorkspaceAccessTokenError) { + // TODO: Remove License if there is no access token + } + } + + await getCachedSupportedVersionsToken.reset(); +} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts new file mode 100644 index 000000000000..d5f86fad8409 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts @@ -0,0 +1,182 @@ +import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; + +import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; +import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import type { WorkspaceRegistrationData } from '../buildRegistrationData'; +import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; +import { getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; +import { getWorkspaceLicense } from '../getWorkspaceLicense'; +import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; +import { handleBannerOnWorkspaceSync, handleNpsOnWorkspaceSync } from './handleCommsSync'; + +const workspaceClientPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + trial: v.object({ + trialing: v.boolean().required(), + trialID: v.string().required(), + endDate: v.string().format('date-time').required(), + marketing: v + .object({ + utmContent: v.string().required(), + utmMedium: v.string().required(), + utmSource: v.string().required(), + utmCampaign: v.string().required(), + }) + .required(), + DowngradesToPlan: v + .object({ + id: v.string().required(), + }) + .required(), + trialRequested: v.boolean().required(), + }), + nps: v.object({ + id: v.string().required(), + startAt: v.string().format('date-time').required(), + expireAt: v.string().format('date-time').required(), + }), + banners: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + platform: v.array(v.string()).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + roles: v.array(v.string()), + createdBy: v.object({ + _id: v.string().required(), + username: v.string(), + }), + createdAt: v.string().format('date-time').required(), + view: v.any(), + active: v.boolean(), + inactivedAt: v.string().format('date-time'), + snapshot: v.string(), + }), + ), + announcements: v.object({ + create: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + selector: v.object({ + roles: v.array(v.string()), + }), + platform: v.array(v.string().enum('web', 'mobile')).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + createdBy: v.string().enum('cloud', 'system').required(), + createdAt: v.string().format('date-time').required(), + dictionary: v.object({}).additional(v.object({}).additional(v.string())), + view: v.any(), + surface: v.string().enum('banner', 'modal').required(), + }), + ), + delete: v.array(v.string()), + }), +}); + +const assertWorkspaceClientPayload = compile(workspaceClientPayloadSchema); + +/** @deprecated */ +const fetchWorkspaceClientPayload = async ({ + token, + workspaceRegistrationData, +}: { + token: string; + workspaceRegistrationData: WorkspaceRegistrationData; +}): Promise | undefined> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/client`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: workspaceRegistrationData, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + if (!payload) { + return undefined; + } + + if (!assertWorkspaceClientPayload(payload)) { + throw new CloudWorkspaceConnectionError('Invalid response from Rocket.Chat Cloud'); + } + + return payload; +}; + +/** @deprecated */ +const consumeWorkspaceSyncPayload = async (result: Serialized) => { + if (result.publicKey) { + await Settings.updateValueById('Cloud_Workspace_PublicKey', result.publicKey); + } + + if (result.trial?.trialID) { + await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + } + + // add banners + if (result.banners) { + await handleBannerOnWorkspaceSync(result.banners); + } + + if (result.nps) { + await handleNpsOnWorkspaceSync(result.nps); + } +}; + +/** @deprecated */ +export async function legacySyncWorkspace() { + try { + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (!workspaceRegistered) { + throw new CloudWorkspaceRegistrationError('Workspace is not registered'); + } + + const token = await getWorkspaceAccessToken(true); + if (!token) { + throw new CloudWorkspaceAccessError('Workspace does not have a valid access token'); + } + + const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); + + const payload = await fetchWorkspaceClientPayload({ token, workspaceRegistrationData }); + + if (!payload) { + return true; + } + + await consumeWorkspaceSyncPayload(payload); + + return true; + } catch (err) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + url: '/client', + err, + }); + + return false; + } finally { + await getWorkspaceLicense(); + } +} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts new file mode 100644 index 000000000000..5f529a4892ec --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts @@ -0,0 +1,87 @@ +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; + +import { callbacks } from '../../../../../lib/callbacks'; +import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; +import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { settings } from '../../../../settings/server'; +import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; +import { getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; +import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; +import { legacySyncWorkspace } from './legacySyncWorkspace'; + +const workspaceSyncPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + license: v.string().required(), +}); + +const assertWorkspaceSyncPayload = compile(workspaceSyncPayloadSchema); + +const fetchWorkspaceSyncPayload = async ({ + token, + data, +}: { + token: string; + data: Cloud.WorkspaceSyncRequestPayload; +}): Promise> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/sync`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: data, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + assertWorkspaceSyncPayload(payload); + + return payload; +}; + +export async function syncCloudData() { + try { + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (!workspaceRegistered) { + throw new CloudWorkspaceRegistrationError('Workspace is not registered'); + } + + const token = await getWorkspaceAccessToken(true); + if (!token) { + throw new CloudWorkspaceAccessError('Workspace does not have a valid access token'); + } + + const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); + + const { license } = await fetchWorkspaceSyncPayload({ + token, + data: workspaceRegistrationData, + }); + + await callbacks.run('workspaceLicenseChanged', license); + + return true; + } catch (err) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + url: '/sync', + err, + }); + } + + await legacySyncWorkspace(); +} diff --git a/apps/meteor/app/cloud/server/functions/userLogout.ts b/apps/meteor/app/cloud/server/functions/userLogout.ts index 7dd4aa094535..386137ced604 100644 --- a/apps/meteor/app/cloud/server/functions/userLogout.ts +++ b/apps/meteor/app/cloud/server/functions/userLogout.ts @@ -26,10 +26,11 @@ export async function userLogout(userId: string): Promise { return ''; } - const cloudUrl = settings.get('Cloud_Url'); const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); const { refreshToken } = user.services.cloud; + + const cloudUrl = settings.get('Cloud_Url'); await fetch(`${cloudUrl}/api/oauth/revoke`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, diff --git a/apps/meteor/app/cloud/server/methods.ts b/apps/meteor/app/cloud/server/methods.ts index 89e7b99e7146..1d328d0c213e 100644 --- a/apps/meteor/app/cloud/server/methods.ts +++ b/apps/meteor/app/cloud/server/methods.ts @@ -108,7 +108,9 @@ Meteor.methods({ }); } - return syncWorkspace(); + await syncWorkspace(); + + return true; }, async 'cloud:connectWorkspace'(token) { check(token, String); diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index e1db46729011..5162fa54ad9c 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -1,114 +1,21 @@ import { Integrations, Users } from '@rocket.chat/models'; -import * as Models from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; -import { Livechat } from 'meteor/rocketchat:livechat'; -import moment from 'moment'; import _ from 'underscore'; -import { VM, VMScript } from 'vm2'; -import * as s from '../../../../lib/utils/stringUtils'; -import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { httpCall } from '../../../../server/lib/http/call'; import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { settings } from '../../../settings/server'; +import { IsolatedVMScriptEngine } from '../lib/isolated-vm/isolated-vm'; +import { VM2ScriptEngine } from '../lib/vm2/vm2'; import { incomingLogger } from '../logger'; import { addOutgoingIntegration } from '../methods/outgoing/addOutgoingIntegration'; import { deleteOutgoingIntegration } from '../methods/outgoing/deleteOutgoingIntegration'; -const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); +const vm2Engine = new VM2ScriptEngine(true); +const ivmEngine = new IsolatedVMScriptEngine(true); -export const forbiddenModelMethods = ['registerModel', 'getCollectionName']; - -const compiledScripts = {}; -function buildSandbox(store = {}) { - const httpAsync = async (method, url, options) => { - try { - return { - result: await httpCall(method, url, options), - }; - } catch (error) { - return { error }; - } - }; - - const sandbox = { - scriptTimeout(reject) { - return setTimeout(() => reject('timed out'), 3000); - }, - _, - s, - console, - moment, - Promise, - Livechat, - Store: { - set(key, val) { - store[key] = val; - return val; - }, - get(key) { - return store[key]; - }, - }, - HTTP: (method, url, options) => { - // TODO: deprecate, track and alert - return deasyncPromise(httpAsync(method, url, options)); - }, - // TODO: Export fetch as the non deprecated method - }; - Object.keys(Models) - .filter((k) => !forbiddenModelMethods.includes(k)) - .forEach((k) => { - sandbox[k] = Models[k]; - }); - return { store, sandbox }; -} - -function getIntegrationScript(integration) { - if (DISABLE_INTEGRATION_SCRIPTS) { - throw API.v1.failure('integration-scripts-disabled'); - } - - const compiledScript = compiledScripts[integration._id]; - if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { - return compiledScript.script; - } - - const script = integration.scriptCompiled; - const { sandbox, store } = buildSandbox(); - try { - incomingLogger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); - incomingLogger.debug(script); - - const vmScript = new VMScript(`${script}; Script;`, 'script.js'); - const vm = new VM({ - sandbox, - }); - - const ScriptClass = vm.run(vmScript); - - if (ScriptClass) { - compiledScripts[integration._id] = { - script: new ScriptClass(), - store, - _updatedAt: integration._updatedAt, - }; - - return compiledScripts[integration._id].script; - } - } catch (err) { - incomingLogger.error({ - msg: 'Error evaluating Script in Trigger', - integration: integration.name, - script, - err, - }); - throw API.v1.failure('error-evaluating-script'); - } - - incomingLogger.error({ msg: 'Class "Script" not in Trigger', integration: integration.name }); - throw API.v1.failure('class-script-not-found'); +function getEngine(integration) { + return integration.scriptEngine === 'isolated-vm' ? ivmEngine : vm2Engine; } async function createIntegration(options, user) { @@ -178,20 +85,9 @@ async function executeIntegrationRest() { emoji: this.integration.emoji, }; - if ( - !DISABLE_INTEGRATION_SCRIPTS && - this.integration.scriptEnabled && - this.integration.scriptCompiled && - this.integration.scriptCompiled.trim() !== '' - ) { - let script; - try { - script = getIntegrationScript(this.integration); - } catch (e) { - incomingLogger.error(e); - return API.v1.failure(e.message); - } + const scriptEngine = getEngine(this.integration); + if (scriptEngine.integrationHasValidScript(this.integration)) { this.request.setEncoding('utf8'); const content_raw = this.request.read(); @@ -216,37 +112,12 @@ async function executeIntegrationRest() { }, }; - try { - const { sandbox } = buildSandbox(compiledScripts[this.integration._id].store); - sandbox.script = script; - sandbox.request = request; - - const vm = new VM({ - timeout: 3000, - sandbox, - }); - - const result = await new Promise((resolve, reject) => { - process.nextTick(async () => { - try { - const scriptResult = await vm.run(` - new Promise((resolve, reject) => { - scriptTimeout(reject); - try { - resolve(script.process_incoming_request({ request: request })); - } catch(e) { - reject(e); - } - }).catch((error) => { throw new Error(error); }); - `); - - resolve(scriptResult); - } catch (e) { - reject(e); - } - }); - }); + const result = await scriptEngine.processIncomingRequest({ + integration: this.integration, + request, + }); + try { if (!result) { incomingLogger.debug({ msg: 'Process Incoming Request result of Trigger has no data', diff --git a/apps/meteor/app/integrations/server/lib/ScriptEngine.ts b/apps/meteor/app/integrations/server/lib/ScriptEngine.ts new file mode 100644 index 000000000000..e46984a893ef --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/ScriptEngine.ts @@ -0,0 +1,385 @@ +import type { + IUser, + IRoom, + IMessage, + IOutgoingIntegration, + IIncomingIntegration, + IIntegration, + IIntegrationHistory, +} from '@rocket.chat/core-typings'; +import type { Logger } from '@rocket.chat/logger'; +import type { serverFetch } from '@rocket.chat/server-fetch'; +import { wrapExceptions } from '@rocket.chat/tools'; + +import { incomingLogger, outgoingLogger } from '../logger'; +import type { IScriptClass, CompiledScript } from './definition'; +import { updateHistory } from './updateHistory'; + +type OutgoingRequestBaseData = { + token: IOutgoingIntegration['token']; + bot: false; + trigger_word: string; +}; + +type OutgoingRequestSendMessageData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + message_id: string; + timestamp: Date; + user_id: string; + user_name: string; + text: string; + siteUrl: string; + alias?: string; + bot?: boolean; + isEdited?: true; + tmid?: string; +}; + +type OutgoingRequestUploadedFileData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + message_id: string; + timestamp: Date; + user_id: string; + user_name: string; + text: string; + + user: IUser; + room: IRoom; + message: IMessage; + + alias?: string; + bot?: boolean; +}; + +type OutgoingRequestRoomCreatedData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + timestamp: Date; + user_id: string; + user_name: string; + owner: IUser; + room: IRoom; +}; + +type OutgoingRequestRoomData = OutgoingRequestBaseData & { + channel_id: string; + channel_name: string; + timestamp: Date; + user_id: string; + user_name: string; + owner: IUser; + room: IRoom; + bot?: boolean; +}; + +type OutgoingRequestUserCreatedData = OutgoingRequestBaseData & { + timestamp: Date; + user_id: string; + user_name: string; + user: IUser; + bot?: boolean; +}; + +type OutgoingRequestData = + | OutgoingRequestSendMessageData + | OutgoingRequestUploadedFileData + | OutgoingRequestRoomCreatedData + | OutgoingRequestRoomData + | OutgoingRequestUserCreatedData; + +type OutgoingRequest = { + params: Record; + method: 'POST'; + url: string; + data: OutgoingRequestData; + auth: undefined; + headers: Record; +}; + +type OutgoingRequestFromScript = { + url?: string; + headers?: Record; + method?: string; + message?: { + text?: string; + channel?: string; + attachments?: { + color?: string; + author_name?: string; + author_link?: string; + author_icon?: string; + title?: string; + title_link?: string; + text?: string; + fields?: { + title?: string; + value?: string; + short?: boolean; + }[]; + image_url?: string; + thumb_url?: string; + }[]; + }; + + auth?: string; + data?: Record; +}; + +type OutgoingRequestContext = { + integration: IOutgoingIntegration; + data: OutgoingRequestData; + historyId: IIntegrationHistory['_id']; + url: string; +}; + +type ProcessedOutgoingRequest = OutgoingRequest | OutgoingRequestFromScript; + +type OutgoingResponseContext = { + integration: IOutgoingIntegration; + request: ProcessedOutgoingRequest; + response: Awaited>; + content: string; + historyId: IIntegrationHistory['_id']; +}; + +type IncomingIntegrationRequest = { + url: { + hash: string | null | undefined; + search: string | null | undefined; + query: Record; + pathname: string | null | undefined; + path: string | null | undefined; + }; + url_raw: string; + url_params: Record; + content: Record; + content_raw: string; + headers: Record; + body: Record; + user: Pick, '_id' | 'name' | 'username'>; +}; + +export abstract class IntegrationScriptEngine { + protected compiledScripts: Record; + + public get disabled(): boolean { + return this.isDisabled(); + } + + public get incoming(): IsIncoming { + return this.isIncoming; + } + + constructor(private isIncoming: IsIncoming) { + this.compiledScripts = {}; + } + + public integrationHasValidScript(integration: IIntegration): boolean { + return Boolean(!this.disabled && integration.scriptEnabled && integration.scriptCompiled && integration.scriptCompiled.trim() !== ''); + } + + // PrepareOutgoingRequest will execute a script to build the request object that will be used for the actual integration request + // It may also return a message object to be sent to the room where the integration was triggered + public async prepareOutgoingRequest({ integration, data, historyId, url }: OutgoingRequestContext): Promise { + const request: OutgoingRequest = { + params: {}, + method: 'POST', + url, + data, + auth: undefined, + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', + }, + }; + + if (!(await this.hasScriptAndMethod(integration, 'prepare_outgoing_request'))) { + return request; + } + + return this.executeOutgoingScript(integration, 'prepare_outgoing_request', { request }, historyId); + } + + public async processOutgoingResponse({ + integration, + request, + response, + content, + historyId, + }: OutgoingResponseContext): Promise { + if (!(await this.hasScriptAndMethod(integration, 'process_outgoing_response'))) { + return; + } + + const sandbox = { + request, + response: { + error: null, + status_code: response.status, + content, + content_raw: content, + headers: Object.fromEntries(response.headers), + }, + }; + + const scriptResult = await this.executeOutgoingScript(integration, 'process_outgoing_response', sandbox, historyId); + + if (scriptResult === false) { + return scriptResult; + } + + if (scriptResult?.content) { + return scriptResult.content; + } + } + + public async processIncomingRequest({ + integration, + request, + }: { + integration: IIncomingIntegration; + request: IncomingIntegrationRequest; + }): Promise { + return this.executeIncomingScript(integration, 'process_incoming_request', { request }); + } + + protected get logger(): ReturnType { + if (this.isIncoming) { + return incomingLogger; + } + + return outgoingLogger; + } + + protected async executeOutgoingScript( + integration: IOutgoingIntegration, + method: keyof IScriptClass, + params: Record, + historyId: IIntegrationHistory['_id'], + ): Promise { + if (this.disabled) { + return; + } + + const script = await wrapExceptions(() => this.getIntegrationScript(integration)).suppress((e: any) => + updateHistory({ + historyId, + step: 'execute-script-getting-script', + error: true, + errorStack: e, + }), + ); + + if (!script) { + return; + } + + if (!script[method]) { + this.logger.error(`Method "${method}" not found in the Integration "${integration.name}"`); + await updateHistory({ historyId, step: `execute-script-no-method-${method}` }); + return; + } + + try { + await updateHistory({ historyId, step: `execute-script-before-running-${method}` }); + + const result = await this.runScriptMethod({ + integrationId: integration._id, + script, + method, + params, + }); + + this.logger.debug({ + msg: `Script method "${method}" result of the Integration "${integration.name}" is:`, + result, + }); + + return result; + } catch (err: any) { + await updateHistory({ + historyId, + step: `execute-script-error-running-${method}`, + error: true, + errorStack: err.stack.replace(/^/gm, ' '), + }); + this.logger.error({ + msg: 'Error running Script in the Integration', + integration: integration.name, + err, + }); + this.logger.debug({ + msg: 'Error running Script in the Integration', + integration: integration.name, + script: integration.scriptCompiled, + }); + } + } + + protected async executeIncomingScript( + integration: IIncomingIntegration, + method: keyof IScriptClass, + params: Record, + ): Promise { + if (!this.integrationHasValidScript(integration)) { + return; + } + + const script = await wrapExceptions(() => this.getIntegrationScript(integration)).catch((e) => { + this.logger.error(e); + throw e; + }); + + if (!script[method]) { + this.logger.error(`Method "${method}" not found in the Integration "${integration.name}"`); + return; + } + + return wrapExceptions(() => + this.runScriptMethod({ + integrationId: integration._id, + script, + method, + params, + }), + ).catch((err: any) => { + this.logger.error({ + msg: 'Error running Script in Trigger', + integration: integration.name, + script: integration.scriptCompiled, + err, + }); + throw new Error('error-running-script'); + }); + } + + protected async hasScriptAndMethod(integration: IIntegration, method: keyof IScriptClass): Promise { + const script = await this.getScriptSafely(integration); + return typeof script?.[method] === 'function'; + } + + protected async getScriptSafely(integration: IIntegration): Promise | undefined> { + if (this.disabled || integration.scriptEnabled !== true || !integration.scriptCompiled || integration.scriptCompiled.trim() === '') { + return; + } + + return wrapExceptions(() => this.getIntegrationScript(integration)).suppress(); + } + + protected abstract isDisabled(): boolean; + + protected abstract runScriptMethod({ + integrationId, + script, + method, + params, + }: { + integrationId: IIntegration['_id']; + script: IScriptClass; + method: keyof IScriptClass; + params: Record; + }): Promise; + + protected abstract getIntegrationScript(integration: IIntegration): Promise>; +} diff --git a/apps/meteor/app/integrations/server/lib/definition.ts b/apps/meteor/app/integrations/server/lib/definition.ts new file mode 100644 index 000000000000..b4d11b9f4e8b --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/definition.ts @@ -0,0 +1,19 @@ +import type { IIntegration } from '@rocket.chat/core-typings'; + +export interface IScriptClass { + prepare_outgoing_request?: (params: Record) => any; + process_outgoing_response?: (params: Record) => any; + process_incoming_request?: (params: Record) => any; +} + +export type FullScriptClass = Required; + +export type CompiledScript = { + script: Partial; + store: Record; + _updatedAt: IIntegration['_updatedAt']; +}; + +export type CompatibilityScriptResult = IScriptClass & { + availableFunctions: (keyof IScriptClass)[]; +}; diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts new file mode 100644 index 000000000000..1bbefb6a2ee7 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts @@ -0,0 +1,127 @@ +import { EventEmitter } from 'events'; + +import { serverFetch as fetch, Response } from '@rocket.chat/server-fetch'; +import ivm, { type Context } from 'isolated-vm'; + +import * as s from '../../../../../lib/utils/stringUtils'; + +const proxyObject = (obj: Record, forbiddenKeys: string[] = []): Record => { + return copyObject({ + isProxy: true, + get: (key: string) => { + if (forbiddenKeys.includes(key)) { + return undefined; + } + + const value = obj[key]; + + if (typeof value === 'function') { + return new ivm.Reference(async (...args: any[]) => { + const result = (obj[key] as any)(...args); + + if (result && result instanceof Promise) { + return new Promise(async (resolve, reject) => { + try { + const awaitedResult = await result; + resolve(makeTransferable(awaitedResult)); + } catch (e) { + reject(e); + } + }); + } + + return makeTransferable(result); + }); + } + + return makeTransferable(value); + }, + }); +}; + +const copyObject = (obj: Record | any[]): Record | any[] => { + if (Array.isArray(obj)) { + return obj.map((data) => copyData(data)); + } + + if (obj instanceof Response) { + return proxyObject(obj, ['clone']); + } + + if (isSemiTransferable(obj)) { + return obj; + } + + if (typeof obj[Symbol.iterator as any] === 'function') { + return copyObject(Array.from(obj as any)); + } + + if (obj instanceof EventEmitter) { + return {}; + } + + const keys = Object.keys(obj); + + return { + ...Object.fromEntries( + keys.map((key) => { + const data = obj[key]; + + if (typeof data === 'function') { + return [key, new ivm.Callback((...args: any[]) => obj[key](...args))]; + } + + return [key, copyData(data)]; + }), + ), + }; +}; + +// Transferable data can be passed to isolates directly +const isTransferable = (data: any): data is ivm.Transferable => { + const dataType = typeof data; + + if (data === ivm) { + return true; + } + + if (['null', 'undefined', 'string', 'number', 'boolean', 'function'].includes(dataType)) { + return true; + } + + if (dataType !== 'object') { + return false; + } + + return ( + data instanceof ivm.Isolate || + data instanceof ivm.Context || + data instanceof ivm.Script || + data instanceof ivm.ExternalCopy || + data instanceof ivm.Callback || + data instanceof ivm.Reference + ); +}; + +// Semi-transferable data can be copied with an ivm.ExternalCopy without needing any manipulation. +const isSemiTransferable = (data: any) => data instanceof ArrayBuffer; + +const copyData = | any[]>(data: T) => (isTransferable(data) ? data : copyObject(data)); +const makeTransferable = (data: any) => (isTransferable(data) ? data : new ivm.ExternalCopy(copyObject(data)).copyInto()); + +export const buildSandbox = (context: Context) => { + const { global: jail } = context; + jail.setSync('global', jail.derefInto()); + jail.setSync('ivm', ivm); + + jail.setSync('s', makeTransferable(s)); + jail.setSync('console', makeTransferable(console)); + + jail.setSync( + 'serverFetch', + new ivm.Reference(async (url: string, ...args: any[]) => { + const result = await fetch(url, ...args); + return makeTransferable(result); + }), + ); +}; diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/getCompatibilityScript.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/getCompatibilityScript.ts new file mode 100644 index 000000000000..77ce2475e8c2 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/getCompatibilityScript.ts @@ -0,0 +1,60 @@ +export const getCompatibilityScript = (customScript?: string) => ` + const Store = (function() { + const store = {}; + return { + set(key, val) { + store[key] = val; + return val; + }, + get(key) { + return store[key]; + }, + }; + })(); + + const reproxy = (reference) => { + return new Proxy(reference, { + get(target, p, receiver) { + if (target !== reference || p === 'then') { + return Reflect.get(target, p, receiver); + } + + const data = reference.get(p); + + if (typeof data === 'object' && data instanceof ivm.Reference && data.typeof === 'function') { + return (...args) => data.apply(undefined, args, { arguments: { copy: true }, result: { promise: true } }); + } + + return data; + } + }); + }; + + //url, options, allowSelfSignedCertificate + const fetch = async (...args) => { + const result = await serverFetch.apply(undefined, args, { arguments: { copy: true }, result: { promise: true } }); + + if (result && typeof result === 'object' && result.isProxy) { + return reproxy(result); + } + + return result; + }; + + ${customScript} + + (function() { + const instance = new Script(); + + const functions = { + ...(typeof instance['prepare_outgoing_request'] === 'function' ? { prepare_outgoing_request : (...args) => instance.prepare_outgoing_request(...args) } : {}), + ...(typeof instance['process_outgoing_response'] === 'function' ? { process_outgoing_response : (...args) => instance.process_outgoing_response(...args) } : {}), + ...(typeof instance['process_incoming_request'] === 'function' ? { process_incoming_request : (...args) => instance.process_incoming_request(...args) } : {}), + }; + + return { + ...functions, + availableFunctions: Object.keys(functions), + } + })(); +`; diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts new file mode 100644 index 000000000000..2c78b6d98a7c --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts @@ -0,0 +1,99 @@ +import type { IIntegration, ValueOf } from '@rocket.chat/core-typings'; +import { pick } from '@rocket.chat/tools'; +import ivm, { type Reference } from 'isolated-vm'; + +import { IntegrationScriptEngine } from '../ScriptEngine'; +import type { IScriptClass, CompatibilityScriptResult, FullScriptClass } from '../definition'; +import { buildSandbox } from './buildSandbox'; +import { getCompatibilityScript } from './getCompatibilityScript'; + +const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true', 'ivm'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); + +export class IsolatedVMScriptEngine extends IntegrationScriptEngine { + protected isDisabled(): boolean { + return DISABLE_INTEGRATION_SCRIPTS; + } + + protected async callScriptFunction( + scriptReference: Reference>, + ...params: Parameters> + ): Promise { + return scriptReference.applySync(undefined, params, { + arguments: { copy: true }, + result: { copy: true, promise: true }, + }); + } + + protected async runScriptMethod({ + script, + method, + params, + }: { + integrationId: IIntegration['_id']; + script: Partial; + method: keyof IScriptClass; + params: Record; + }): Promise { + const fn = script[method]; + + if (typeof fn !== 'function') { + throw new Error('integration-method-not-found'); + } + + return fn(params); + } + + protected async getIntegrationScript(integration: IIntegration): Promise> { + if (this.disabled) { + throw new Error('integration-scripts-disabled'); + } + + const compiledScript = this.compiledScripts[integration._id]; + if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { + return compiledScript.script; + } + + const script = integration.scriptCompiled; + try { + this.logger.info({ msg: 'Will evaluate the integration script', integration: pick(integration, 'name', '_id') }); + this.logger.debug(script); + + const isolate = new ivm.Isolate({ memoryLimit: 8 }); + + const ivmScript = await isolate.compileScript(getCompatibilityScript(script)); + + const ivmContext = isolate.createContextSync(); + buildSandbox(ivmContext); + + const ivmResult: Reference = await ivmScript.run(ivmContext, { + reference: true, + timeout: 3000, + }); + + const availableFunctions = await ivmResult.get('availableFunctions', { copy: true }); + const scriptFunctions = Object.fromEntries( + availableFunctions.map((functionName) => { + const fnReference = ivmResult.getSync(functionName, { reference: true }); + return [functionName, (...params: Parameters>) => this.callScriptFunction(fnReference, ...params)]; + }), + ) as Partial; + + this.compiledScripts[integration._id] = { + script: scriptFunctions, + store: {}, + _updatedAt: integration._updatedAt, + }; + + return scriptFunctions; + } catch (err: any) { + this.logger.error({ + msg: 'Error evaluating integration script', + integration: integration.name, + script, + err, + }); + + throw new Error('error-evaluating-script'); + } + } +} diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index b122b22ff355..b5050b8c4716 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -1,30 +1,25 @@ -import { Integrations, IntegrationHistory, Users, Rooms, Messages } from '@rocket.chat/models'; -import * as Models from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; +import { Integrations, Users, Rooms, Messages } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { wrapExceptions } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; import _ from 'underscore'; -import { VM, VMScript } from 'vm2'; -import { omit } from '../../../../lib/utils/omit'; -import * as s from '../../../../lib/utils/stringUtils'; -import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { httpCall } from '../../../../server/lib/http/call'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { settings } from '../../../settings/server'; import { outgoingEvents } from '../../lib/outgoingEvents'; -import { forbiddenModelMethods } from '../api/api'; import { outgoingLogger } from '../logger'; - -const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); +import { IsolatedVMScriptEngine } from './isolated-vm/isolated-vm'; +import { updateHistory } from './updateHistory'; +import { VM2ScriptEngine } from './vm2/vm2'; class RocketChatIntegrationHandler { constructor() { this.successResults = [200, 201, 202]; this.compiledScripts = {}; this.triggers = {}; + this.vm2Engine = new VM2ScriptEngine(false); + this.ivmEngine = new IsolatedVMScriptEngine(false); } addIntegration(record) { @@ -51,6 +46,10 @@ class RocketChatIntegrationHandler { } } + getEngine(integration) { + return integration.scriptEngine === 'isolated-vm' ? this.ivmEngine : this.vm2Engine; + } + removeIntegration(record) { for (const trigger of Object.values(this.triggers)) { delete trigger[record._id]; @@ -67,114 +66,6 @@ class RocketChatIntegrationHandler { return false; } - async updateHistory({ - historyId, - step, - integration, - event, - data, - triggerWord, - ranPrepareScript, - prepareSentMessage, - processSentMessage, - resultMessage, - finished, - url, - httpCallData, - httpError, - httpResult, - error, - errorStack, - }) { - const history = { - type: 'outgoing-webhook', - step, - }; - - // Usually is only added on initial insert - if (integration) { - history.integration = integration; - } - - // Usually is only added on initial insert - if (event) { - history.event = event; - } - - if (data) { - history.data = { ...data }; - - if (data.user) { - history.data.user = omit(data.user, 'services'); - } - - if (data.room) { - history.data.room = data.room; - } - } - - if (triggerWord) { - history.triggerWord = triggerWord; - } - - if (typeof ranPrepareScript !== 'undefined') { - history.ranPrepareScript = ranPrepareScript; - } - - if (prepareSentMessage) { - history.prepareSentMessage = prepareSentMessage; - } - - if (processSentMessage) { - history.processSentMessage = processSentMessage; - } - - if (resultMessage) { - history.resultMessage = resultMessage; - } - - if (typeof finished !== 'undefined') { - history.finished = finished; - } - - if (url) { - history.url = url; - } - - if (typeof httpCallData !== 'undefined') { - history.httpCallData = httpCallData; - } - - if (httpError) { - history.httpError = httpError; - } - - if (typeof httpResult !== 'undefined') { - history.httpResult = JSON.stringify(httpResult, null, 2); - } - - if (typeof error !== 'undefined') { - history.error = error; - } - - if (typeof errorStack !== 'undefined') { - history.errorStack = errorStack; - } - - if (historyId) { - await IntegrationHistory.updateOne({ _id: historyId }, { $set: history }); - return historyId; - } - - history._createdAt = new Date(); - - const _id = Random.id(); - - await IntegrationHistory.insertOne({ _id, ...history }); - - return _id; - } - // Trigger is the trigger, nameOrId is a string which is used to try and find a room, room is a room, message is a message, and data contains "user_name" if trigger.impersonateUser is truthful. async sendMessage({ trigger, nameOrId = '', room, message, data }) { let user; @@ -229,199 +120,6 @@ class RocketChatIntegrationHandler { return message; } - buildSandbox(store = {}) { - const httpAsync = async (method, url, options) => { - try { - return { - result: await httpCall(method, url, options), - }; - } catch (error) { - return { error }; - } - }; - - const sandbox = { - scriptTimeout(reject) { - return setTimeout(() => reject('timed out'), 3000); - }, - _, - s, - console, - moment, - Promise, - Store: { - set: (key, val) => { - store[key] = val; - }, - get: (key) => store[key], - }, - HTTP: (method, url, options) => { - // TODO: deprecate, track and alert - return deasyncPromise(httpAsync(method, url, options)); - }, - // TODO: Export fetch as the non deprecated method - }; - - Object.keys(Models) - .filter((k) => !forbiddenModelMethods.includes(k)) - .forEach((k) => { - sandbox[k] = Models[k]; - }); - - return { store, sandbox }; - } - - getIntegrationScript(integration) { - if (DISABLE_INTEGRATION_SCRIPTS) { - throw new Meteor.Error('integration-scripts-disabled'); - } - - const compiledScript = this.compiledScripts[integration._id]; - if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { - return compiledScript.script; - } - - const script = integration.scriptCompiled; - const { store, sandbox } = this.buildSandbox(); - - try { - outgoingLogger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); - outgoingLogger.debug(script); - - const vmScript = new VMScript(`${script}; Script;`, 'script.js'); - const vm = new VM({ - sandbox, - }); - - const ScriptClass = vm.run(vmScript); - - if (ScriptClass) { - this.compiledScripts[integration._id] = { - script: new ScriptClass(), - store, - _updatedAt: integration._updatedAt, - }; - - return this.compiledScripts[integration._id].script; - } - } catch (err) { - outgoingLogger.error({ - msg: 'Error evaluating Script in Trigger', - integration: integration.name, - script, - err, - }); - throw new Meteor.Error('error-evaluating-script'); - } - - outgoingLogger.error(`Class "Script" not in Trigger ${integration.name}:`); - throw new Meteor.Error('class-script-not-found'); - } - - hasScriptAndMethod(integration, method) { - if ( - DISABLE_INTEGRATION_SCRIPTS || - integration.scriptEnabled !== true || - !integration.scriptCompiled || - integration.scriptCompiled.trim() === '' - ) { - return false; - } - - let script; - try { - script = this.getIntegrationScript(integration); - } catch (e) { - return false; - } - - return typeof script[method] !== 'undefined'; - } - - async executeScript(integration, method, params, historyId) { - if (DISABLE_INTEGRATION_SCRIPTS) { - return; - } - - let script; - try { - script = this.getIntegrationScript(integration); - } catch (e) { - await this.updateHistory({ - historyId, - step: 'execute-script-getting-script', - error: true, - errorStack: e, - }); - return; - } - - if (!script[method]) { - outgoingLogger.error(`Method "${method}" no found in the Integration "${integration.name}"`); - await this.updateHistory({ historyId, step: `execute-script-no-method-${method}` }); - return; - } - - try { - const { sandbox } = this.buildSandbox(this.compiledScripts[integration._id].store); - sandbox.script = script; - sandbox.method = method; - sandbox.params = params; - - await this.updateHistory({ historyId, step: `execute-script-before-running-${method}` }); - - const vm = new VM({ - timeout: 3000, - sandbox, - }); - - const result = await new Promise((resolve, reject) => { - process.nextTick(async () => { - try { - const scriptResult = await vm.run(` - new Promise((resolve, reject) => { - scriptTimeout(reject); - try { - resolve(script[method](params)) - } catch(e) { - reject(e); - } - }).catch((error) => { throw new Error(error); }); - `); - - resolve(scriptResult); - } catch (e) { - reject(e); - } - }); - }); - - outgoingLogger.debug({ - msg: `Script method "${method}" result of the Integration "${integration.name}" is:`, - result, - }); - - return result; - } catch (err) { - await this.updateHistory({ - historyId, - step: `execute-script-error-running-${method}`, - error: true, - errorStack: err.stack.replace(/^/gm, ' '), - }); - outgoingLogger.error({ - msg: 'Error running Script in the Integration', - integration: integration.name, - err, - }); - outgoingLogger.debug({ - msg: 'Error running Script in the Integration', - integration: integration.name, - script: integration.scriptCompiled, - }); // Only output the compiled script if debugging is enabled, so the logs don't get spammed. - } - } - eventNameArgumentsToObject(...args) { const argObject = { event: args[0], @@ -680,6 +378,17 @@ class RocketChatIntegrationHandler { } } + // Ensure that any errors thrown by the script engine will contibue to be compatible with Meteor.Error + async wrapScriptEngineCall(getter) { + return wrapExceptions(getter).catch((error) => { + if (error instanceof Error) { + throw new Meteor.Error(error.message); + } + + throw error; + }); + } + async executeTriggerUrl(url, trigger, { event, message, room, owner, user }, theHistoryId, tries = 0) { if (!this.isTriggerEnabled(trigger)) { outgoingLogger.warn(`The trigger "${trigger.name}" is no longer enabled, stopping execution of it at try: ${tries}`); @@ -715,7 +424,7 @@ class RocketChatIntegrationHandler { return; } - const historyId = await this.updateHistory({ + const historyId = await updateHistory({ step: 'start-execute-trigger-url', integration: trigger, event, @@ -731,36 +440,32 @@ class RocketChatIntegrationHandler { } this.mapEventArgsToData(data, { trigger, event, message, room, owner, user }); - await this.updateHistory({ historyId, step: 'mapped-args-to-data', data, triggerWord: word }); + await updateHistory({ historyId, step: 'mapped-args-to-data', data, triggerWord: word }); outgoingLogger.info(`Will be executing the Integration "${trigger.name}" to the url: ${url}`); outgoingLogger.debug({ data }); - let opts = { - params: {}, - method: 'POST', - url, - data, - auth: undefined, - headers: { - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', - }, - }; + const scriptEngine = this.getEngine(trigger); - if (this.hasScriptAndMethod(trigger, 'prepare_outgoing_request')) { - opts = await this.executeScript(trigger, 'prepare_outgoing_request', { request: opts }, historyId); - } + const opts = await this.wrapScriptEngineCall(() => + scriptEngine.prepareOutgoingRequest({ + integration: trigger, + data, + url, + historyId, + }), + ); - await this.updateHistory({ historyId, step: 'after-maybe-ran-prepare', ranPrepareScript: true }); + await updateHistory({ historyId, step: 'after-maybe-ran-prepare', ranPrepareScript: true }); if (!opts) { - await this.updateHistory({ historyId, step: 'after-prepare-no-opts', finished: true }); + await updateHistory({ historyId, step: 'after-prepare-no-opts', finished: true }); return; } if (opts.message) { const prepareMessage = await this.sendMessage({ trigger, room, message: opts.message, data }); - await this.updateHistory({ + await updateHistory({ historyId, step: 'after-prepare-send-message', prepareSentMessage: prepareMessage, @@ -768,7 +473,7 @@ class RocketChatIntegrationHandler { } if (!opts.url || !opts.method) { - await this.updateHistory({ historyId, step: 'after-prepare-no-url_or_method', finished: true }); + await updateHistory({ historyId, step: 'after-prepare-no-url_or_method', finished: true }); return; } @@ -782,7 +487,7 @@ class RocketChatIntegrationHandler { opts.headers.Authorization = `Basic ${base64}`; } - await this.updateHistory({ + await updateHistory({ historyId, step: 'pre-http-call', url: opts.url, @@ -823,47 +528,42 @@ class RocketChatIntegrationHandler { } })(); - await this.updateHistory({ + await updateHistory({ historyId, step: 'after-http-call', httpError: null, httpResult: content, }); - if (this.hasScriptAndMethod(trigger, 'process_outgoing_response')) { - const sandbox = { + const responseContent = await this.wrapScriptEngineCall(() => + scriptEngine.processOutgoingResponse({ + integration: trigger, request: opts, - response: { - error: null, - status_code: res.status, // These values will be undefined to close issues #4175, #5762, and #5896 - content, - content_raw: content, - headers: Object.fromEntries(res.headers), - }, - }; - - const scriptResult = await this.executeScript(trigger, 'process_outgoing_response', sandbox, historyId); - - if (scriptResult && scriptResult.content) { - const resultMessage = await this.sendMessage({ - trigger, - room, - message: scriptResult.content, - data, - }); - await this.updateHistory({ - historyId, - step: 'after-process-send-message', - processSentMessage: resultMessage, - finished: true, - }); - return; - } + response: res, + content, + historyId, + }), + ); + + if (responseContent) { + const resultMessage = await this.sendMessage({ + trigger, + room, + message: responseContent, + data, + }); + await updateHistory({ + historyId, + step: 'after-process-send-message', + processSentMessage: resultMessage, + finished: true, + }); + return; + } - if (scriptResult === false) { - await this.updateHistory({ historyId, step: 'after-process-false-result', finished: true }); - return; - } + if (responseContent === false) { + await updateHistory({ historyId, step: 'after-process-false-result', finished: true }); + return; } // if the result contained nothing or wasn't a successful statusCode @@ -875,14 +575,14 @@ class RocketChatIntegrationHandler { }); if (res.status === 410) { - await this.updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); + await updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); outgoingLogger.error(`Disabling the Integration "${trigger.name}" because the status code was 401 (Gone).`); await Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } }); return; } if (res.status === 500) { - await this.updateHistory({ historyId, step: 'after-process-http-status-500', error: true }); + await updateHistory({ historyId, step: 'after-process-http-status-500', error: true }); outgoingLogger.error({ msg: `Error "500" for the Integration "${trigger.name}" to ${url}.`, content, @@ -893,7 +593,7 @@ class RocketChatIntegrationHandler { if (trigger.retryFailedCalls) { if (tries < trigger.retryCount && trigger.retryDelay) { - await this.updateHistory({ historyId, error: true, step: `going-to-retry-${tries + 1}` }); + await updateHistory({ historyId, error: true, step: `going-to-retry-${tries + 1}` }); let waitTime; @@ -912,7 +612,7 @@ class RocketChatIntegrationHandler { break; default: const er = new Error("The integration's retryDelay setting is invalid."); - await this.updateHistory({ + await updateHistory({ historyId, step: 'failed-and-retry-delay-is-invalid', error: true, @@ -926,10 +626,10 @@ class RocketChatIntegrationHandler { void this.executeTriggerUrl(url, trigger, { event, message, room, owner, user }, historyId, tries + 1); }, waitTime); } else { - await this.updateHistory({ historyId, step: 'too-many-retries', error: true }); + await updateHistory({ historyId, step: 'too-many-retries', error: true }); } } else { - await this.updateHistory({ + await updateHistory({ historyId, step: 'failed-and-not-configured-to-retry', error: true, @@ -943,7 +643,7 @@ class RocketChatIntegrationHandler { if (content && this.successResults.includes(res.status)) { if (data?.text || data?.attachments) { const resultMsg = await this.sendMessage({ trigger, room, message: data, data }); - await this.updateHistory({ + await updateHistory({ historyId, step: 'url-response-sent-message', resultMessage: resultMsg, @@ -954,7 +654,7 @@ class RocketChatIntegrationHandler { }) .catch(async (error) => { outgoingLogger.error(error); - await this.updateHistory({ + await updateHistory({ historyId, step: 'after-http-call', httpError: error, diff --git a/apps/meteor/app/integrations/server/lib/updateHistory.ts b/apps/meteor/app/integrations/server/lib/updateHistory.ts new file mode 100644 index 000000000000..9f7a3017108d --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/updateHistory.ts @@ -0,0 +1,96 @@ +import type { IIntegrationHistory, IIntegration, IMessage, AtLeast } from '@rocket.chat/core-typings'; +import { IntegrationHistory } from '@rocket.chat/models'; +import { Random } from '@rocket.chat/random'; + +import { omit } from '../../../../lib/utils/omit'; + +export const updateHistory = async ({ + historyId, + step, + integration, + event, + data, + triggerWord, + ranPrepareScript, + prepareSentMessage, + processSentMessage, + resultMessage, + finished, + url, + httpCallData, + httpError, + httpResult, + error, + errorStack, +}: { + historyId: IIntegrationHistory['_id']; + step: IIntegrationHistory['step']; + integration?: IIntegration; + event?: string; + triggerWord?: string; + ranPrepareScript?: boolean; + prepareSentMessage?: { channel: string; message: Partial }[]; + processSentMessage?: { channel: string; message: Partial }[]; + resultMessage?: { channel: string; message: Partial }[]; + finished?: boolean; + url?: string; + httpCallData?: Record; // ProcessedOutgoingRequest.data + httpError?: any; // null or whatever error type `fetch` may throw + httpResult?: string | null; + + error?: boolean; + errorStack?: any; // Error | Error['stack'] + + data?: Record; +}) => { + const { user: userData, room: roomData, ...fullData } = data || {}; + + const history: AtLeast = { + type: 'outgoing-webhook', + step, + + // Usually is only added on initial insert + ...(integration ? { integration } : {}), + // Usually is only added on initial insert + ...(event ? { event } : {}), + ...(fullData + ? { + data: { + ...fullData, + ...(userData ? { user: omit(userData, 'services') } : {}), + ...(roomData ? { room: roomData } : {}), + }, + } + : {}), + ...(triggerWord ? { triggerWord } : {}), + ...(typeof ranPrepareScript !== 'undefined' ? { ranPrepareScript } : {}), + ...(prepareSentMessage ? { prepareSentMessage } : {}), + ...(processSentMessage ? { processSentMessage } : {}), + ...(resultMessage ? { resultMessage } : {}), + ...(typeof finished !== 'undefined' ? { finished } : {}), + ...(url ? { url } : {}), + ...(typeof httpCallData !== 'undefined' ? { httpCallData } : {}), + ...(httpError ? { httpError } : {}), + ...(typeof httpResult !== 'undefined' ? { httpResult: JSON.stringify(httpResult, null, 2) } : {}), + ...(typeof error !== 'undefined' ? { error } : {}), + ...(typeof errorStack !== 'undefined' ? { errorStack } : {}), + }; + + if (historyId) { + await IntegrationHistory.updateOne({ _id: historyId }, { $set: history }); + return historyId; + } + + // Can't create a new history without there being an integration + if (!history.integration) { + throw new Error('error-invalid-integration'); + } + + history._createdAt = new Date(); + + const _id = Random.id(); + + await IntegrationHistory.insertOne({ _id, ...history } as IIntegrationHistory); + + return _id; +}; diff --git a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index d9c2db78b62e..398f81161279 100644 --- a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -1,19 +1,18 @@ import type { IUser, INewOutgoingIntegration, IOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; +import { pick } from '@rocket.chat/tools'; import { Babel } from 'meteor/babel-compiler'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { parseCSV } from '../../../../lib/utils/parseCSV'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { outgoingEvents } from '../../lib/outgoingEvents'; +import { isScriptEngineFrozen } from './validateScriptEngine'; const scopedChannels = ['all_public_channels', 'all_private_groups', 'all_direct_messages']; const validChannelChars = ['@', '#']; -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - function _verifyRequiredFields(integration: INewOutgoingIntegration | IUpdateOutgoingIntegration): void { if ( !integration.event || @@ -152,6 +151,7 @@ export const validateOutgoingIntegration = async function ( const integrationData: IOutgoingIntegration = { ...integration, + scriptEngine: integration.scriptEngine ?? 'isolated-vm', type: 'webhook-outgoing', channel: channels, userId: user._id, @@ -171,7 +171,13 @@ export const validateOutgoingIntegration = async function ( delete integrationData.triggerWords; } - if (!FREEZE_INTEGRATION_SCRIPTS && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + // Only compile the script if it is enabled and using a sandbox that is not frozen + if ( + !isScriptEngineFrozen(integrationData.scriptEngine) && + integration.scriptEnabled === true && + integration.script && + integration.script.trim() !== '' + ) { try { const babelOptions = Object.assign(Babel.getDefaultOptions({ runtime: false }), { compact: true, @@ -183,7 +189,7 @@ export const validateOutgoingIntegration = async function ( integrationData.scriptError = undefined; } catch (e) { integrationData.scriptCompiled = undefined; - integrationData.scriptError = e instanceof Error ? _.pick(e, 'name', 'message', 'stack') : undefined; + integrationData.scriptError = e instanceof Error ? pick(e, 'name', 'message', 'stack') : undefined; } } diff --git a/apps/meteor/app/integrations/server/lib/validateScriptEngine.ts b/apps/meteor/app/integrations/server/lib/validateScriptEngine.ts new file mode 100644 index 000000000000..c20dc9c59427 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/validateScriptEngine.ts @@ -0,0 +1,26 @@ +import type { IntegrationScriptEngine } from '@rocket.chat/core-typings'; +import { wrapExceptions } from '@rocket.chat/tools'; + +const FREEZE_INTEGRATION_SCRIPTS_VALUE = String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase(); +const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(FREEZE_INTEGRATION_SCRIPTS_VALUE); + +export const validateScriptEngine = (engine?: IntegrationScriptEngine) => { + if (FREEZE_INTEGRATION_SCRIPTS) { + throw new Error('integration-scripts-disabled'); + } + + const engineCode = engine === 'isolated-vm' ? 'ivm' : 'vm2'; + + if (engineCode === FREEZE_INTEGRATION_SCRIPTS_VALUE) { + if (engineCode === 'ivm') { + throw new Error('integration-scripts-isolated-vm-disabled'); + } + + throw new Error('integration-scripts-vm2-disabled'); + } + + return true; +}; + +export const isScriptEngineFrozen = (engine?: IntegrationScriptEngine) => + wrapExceptions(() => !validateScriptEngine(engine)).catch(() => true); diff --git a/apps/meteor/app/integrations/server/lib/vm2/buildSandbox.ts b/apps/meteor/app/integrations/server/lib/vm2/buildSandbox.ts new file mode 100644 index 000000000000..9ba74404cf26 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/vm2/buildSandbox.ts @@ -0,0 +1,88 @@ +import * as Models from '@rocket.chat/models'; +import moment from 'moment'; +import _ from 'underscore'; + +import * as s from '../../../../../lib/utils/stringUtils'; +import { deasyncPromise } from '../../../../../server/deasync/deasync'; +import { httpCall } from '../../../../../server/lib/http/call'; + +const forbiddenModelMethods: readonly (keyof typeof Models)[] = ['registerModel', 'getCollectionName']; + +type ModelName = Exclude; + +export type Vm2Sandbox = { + scriptTimeout: (reject: (reason?: any) => void) => ReturnType; + _: typeof _; + s: typeof s; + console: typeof console; + moment: typeof moment; + Promise: typeof Promise; + Store: { + set: IsIncoming extends true ? (key: string, value: any) => any : (key: string, value: any) => void; + get: (key: string) => any; + }; + HTTP: (method: string, url: string, options: Record) => unknown; +} & (IsIncoming extends true ? { Livechat: undefined } : never) & + Record; + +export const buildSandbox = ( + store: Record, + isIncoming?: IsIncoming, +): { + store: Record; + sandbox: Vm2Sandbox; +} => { + const httpAsync = async (method: string, url: string, options: Record) => { + try { + return { + result: await httpCall(method, url, options), + }; + } catch (error) { + return { error }; + } + }; + + const sandbox = { + scriptTimeout(reject: (reason?: any) => void) { + return setTimeout(() => reject('timed out'), 3000); + }, + _, + s, + console, + moment, + Promise, + // There's a small difference between the sandbox that is sent to incoming and to outgoing scripts + // Technically we could unify this but since we're deprecating vm2 anyway I'm keeping this old behavior here until the feature is removed completely + ...(isIncoming + ? { + Livechat: undefined, + Store: { + set: (key: string, val: any): any => { + store[key] = val; + return val; + }, + get: (key: string) => store[key], + }, + } + : { + Store: { + set: (key: string, val: any): void => { + store[key] = val; + }, + get: (key: string) => store[key], + }, + }), + HTTP: (method: string, url: string, options: Record) => { + // TODO: deprecate, track and alert + return deasyncPromise(httpAsync(method, url, options)); + }, + } as Vm2Sandbox; + + (Object.keys(Models) as ModelName[]) + .filter((k) => !forbiddenModelMethods.includes(k)) + .forEach((k) => { + sandbox[k] = Models[k]; + }); + + return { store, sandbox }; +}; diff --git a/apps/meteor/app/integrations/server/lib/vm2/vm2.ts b/apps/meteor/app/integrations/server/lib/vm2/vm2.ts new file mode 100644 index 000000000000..5f7519d69346 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/vm2/vm2.ts @@ -0,0 +1,111 @@ +import type { IIntegration } from '@rocket.chat/core-typings'; +import { VM, VMScript } from 'vm2'; + +import { IntegrationScriptEngine } from '../ScriptEngine'; +import type { IScriptClass } from '../definition'; +import { buildSandbox, type Vm2Sandbox } from './buildSandbox'; + +const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true', 'vm2'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase()); + +export class VM2ScriptEngine extends IntegrationScriptEngine { + protected isDisabled(): boolean { + return DISABLE_INTEGRATION_SCRIPTS; + } + + protected buildSandbox(store: Record = {}): { store: Record; sandbox: Vm2Sandbox } { + return buildSandbox(store, this.incoming); + } + + protected async runScriptMethod({ + integrationId, + script, + method, + params, + }: { + integrationId: IIntegration['_id']; + script: IScriptClass; + method: keyof IScriptClass; + params: Record; + }): Promise { + const { sandbox } = this.buildSandbox(this.compiledScripts[integrationId].store); + + const vm = new VM({ + timeout: 3000, + sandbox: { + ...sandbox, + script, + method, + params, + ...(this.incoming && 'request' in params ? { request: params.request } : {}), + }, + }); + + return new Promise((resolve, reject) => { + process.nextTick(async () => { + try { + const scriptResult = await vm.run(` + new Promise((resolve, reject) => { + scriptTimeout(reject); + try { + resolve(script[method](params)) + } catch(e) { + reject(e); + } + }).catch((error) => { throw new Error(error); }); + `); + + resolve(scriptResult); + } catch (e) { + reject(e); + } + }); + }); + } + + protected async getIntegrationScript(integration: IIntegration): Promise> { + if (this.disabled) { + throw new Error('integration-scripts-disabled'); + } + + const compiledScript = this.compiledScripts[integration._id]; + if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { + return compiledScript.script; + } + + const script = integration.scriptCompiled; + const { store, sandbox } = this.buildSandbox(); + + try { + this.logger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); + this.logger.debug(script); + + const vmScript = new VMScript(`${script}; Script;`, 'script.js'); + const vm = new VM({ + sandbox, + }); + + const ScriptClass = vm.run(vmScript); + + if (ScriptClass) { + this.compiledScripts[integration._id] = { + script: new ScriptClass(), + store, + _updatedAt: integration._updatedAt, + }; + + return this.compiledScripts[integration._id].script; + } + } catch (err) { + this.logger.error({ + msg: 'Error evaluating Script in Trigger', + integration: integration.name, + script, + err, + }); + throw new Error('error-evaluating-script'); + } + + this.logger.error({ msg: 'Class "Script" not in Trigger', integration: integration.name }); + throw new Error('class-script-not-found'); + } +} diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index bf84957ba8ea..45548a17a565 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -8,11 +8,10 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -32,6 +31,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn alias: Match.Maybe(String), emoji: Match.Maybe(String), scriptEnabled: Boolean, + scriptEngine: Match.Maybe(String), overrideDestinationChannelEnabled: Match.Maybe(Boolean), script: Match.Maybe(String), avatar: Match.Maybe(String), @@ -76,8 +76,8 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn }); } - if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); + if (integration.script?.trim()) { + validateScriptEngine(integration.scriptEngine ?? 'isolated-vm'); } const user = await Users.findOne({ username: integration.username }); @@ -90,6 +90,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn const integrationData: IIncomingIntegration = { ...integration, + scriptEngine: integration.scriptEngine ?? 'isolated-vm', type: 'webhook-incoming', channel: channels, overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled ?? false, @@ -99,7 +100,13 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn _createdBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }; - if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + // Only compile the script if it is enabled and using a sandbox that is not frozen + if ( + !isScriptEngineFrozen(integrationData.scriptEngine) && + integration.scriptEnabled === true && + integration.script && + integration.script.trim() !== '' + ) { try { let babelOptions = Babel.getDefaultOptions({ runtime: false }); babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index b865c72e0cca..5358e3233ce7 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -1,16 +1,16 @@ import type { IIntegration, INewIncomingIntegration, IUpdateIncomingIntegration } from '@rocket.chat/core-typings'; import { Integrations, Roles, Subscriptions, Users, Rooms } from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Babel } from 'meteor/babel-compiler'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -66,11 +66,20 @@ Meteor.methods({ }); } - if (FREEZE_INTEGRATION_SCRIPTS) { - if (currentIntegration.script?.trim() !== integration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); - } - } else { + const oldScriptEngine = currentIntegration.scriptEngine ?? 'vm2'; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); + } + + const isFrozen = isScriptEngineFrozen(scriptEngine); + + if (!isFrozen) { let scriptCompiled: string | undefined; let scriptError: Pick | undefined; @@ -165,11 +174,12 @@ Meteor.methods({ emoji: integration.emoji, alias: integration.alias, channel: channels, - ...(FREEZE_INTEGRATION_SCRIPTS + ...(isFrozen ? {} : { script: integration.script, scriptEnabled: integration.scriptEnabled, + scriptEngine, }), ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts index 9e5d29261b36..59879f99d475 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; +import { validateScriptEngine } from '../../lib/validateScriptEngine'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,8 +15,6 @@ declare module '@rocket.chat/ui-contexts' { } } -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - export const addOutgoingIntegration = async (userId: string, integration: INewOutgoingIntegration): Promise => { check( integration, @@ -29,6 +28,7 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu emoji: Match.Maybe(String), scriptEnabled: Boolean, script: Match.Maybe(String), + scriptEngine: Match.Maybe(String), urls: Match.Maybe([String]), event: Match.Maybe(String), triggerWords: Match.Maybe([String]), @@ -52,8 +52,8 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu throw new Meteor.Error('not_authorized'); } - if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); + if (integration.script?.trim()) { + validateScriptEngine(integration.scriptEngine ?? 'isolated-vm'); } const integrationData = await validateOutgoingIntegration(integration, userId); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 166badee823d..9e62561ebf9a 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -1,10 +1,12 @@ import type { IIntegration, INewOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings'; import { Integrations, Users } from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; +import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -16,8 +18,6 @@ declare module '@rocket.chat/ui-contexts' { } } -const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase()); - Meteor.methods({ async updateOutgoingIntegration(integrationId, _integration) { if (!this.userId) { @@ -53,10 +53,19 @@ Meteor.methods({ throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); } - if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim() !== currentIntegration.script?.trim()) { - throw new Meteor.Error('integration-scripts-disabled'); + const oldScriptEngine = currentIntegration.scriptEngine ?? 'vm2'; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); } + const isFrozen = isScriptEngineFrozen(scriptEngine); + await Integrations.updateOne( { _id: integrationId }, { @@ -74,11 +83,12 @@ Meteor.methods({ userId: integration.userId, urls: integration.urls, token: integration.token, - ...(FREEZE_INTEGRATION_SCRIPTS + ...(isFrozen ? {} : { script: integration.script, scriptEnabled: integration.scriptEnabled, + scriptEngine, ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), }), triggerWords: integration.triggerWords, @@ -90,7 +100,7 @@ Meteor.methods({ _updatedAt: new Date(), _updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }), }, - ...(FREEZE_INTEGRATION_SCRIPTS + ...(isFrozen ? {} : { $unset: { diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index 6dc477a2926f..ad632a3b7dfc 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -3,6 +3,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { await callbacks.run('beforeJoinDefaultChannels', user); @@ -11,6 +12,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: }).toArray(); for await (const room of defaultRooms) { if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user); // Add a subscription to this user await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), @@ -20,6 +22,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: userMentions: 1, groupMentions: 0, ...(room.favorite && { f: true }), + ...autoTranslateConfig, }); // Insert user joined message diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 660af823de9e..41000cda2038 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { AppEvents, Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; export const addUserToRoom = async function ( @@ -24,7 +25,7 @@ export const addUserToRoom = async function ( }); } - const userToBeAdded = typeof user !== 'string' ? user : await Users.findOneByUsername(user.replace('@', '')); + const userToBeAdded = typeof user === 'string' ? await Users.findOneByUsername(user.replace('@', '')) : await Users.findOneById(user._id); const roomDirectives = roomCoordinator.getRoomDirectives(room.t); if (!userToBeAdded) { @@ -70,6 +71,8 @@ export const addUserToRoom = async function ( await callbacks.run('beforeJoinRoom', userToBeAdded, room); } + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(userToBeAdded); + await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { ts: now, open: true, @@ -77,6 +80,7 @@ export const addUserToRoom = async function ( unread: 1, userMentions: 1, groupMentions: 0, + ...autoTranslateConfig, }); if (!userToBeAdded.username) { diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 192139f96b7c..30cf2a593700 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; @@ -8,6 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { createDirectRoom } from './createDirectRoom'; @@ -178,7 +180,9 @@ export const createRoom = async ( extra.ls = now; } - await Subscriptions.createWithRoomAndUser(room, member, extra); + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(member); + + await Subscriptions.createWithRoomAndUser(room, member, { ...extra, ...autoTranslateConfig }); } } diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index 52ccd0441e24..c541e5f7b2c3 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -7,7 +7,6 @@ import moment from 'moment'; import { closeBusinessHour } from '../../../../ee/app/livechat-enterprise/server/business-hour/Helper'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; -import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; export class BusinessHourManager { @@ -27,7 +26,6 @@ export class BusinessHourManager { async startManager(): Promise { await this.createCronJobsForWorkHours(); - businessHourLogger.debug('Cron jobs created, setting up callbacks'); this.setupCallbacks(); await this.cleanupDisabledDepartmentReferences(); await this.behavior.onStartBusinessHours(); diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index e61bb1621765..e96ccb4c7b89 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -59,7 +59,6 @@ export const openBusinessHourDefault = async (): Promise => { await Users.makeAgentsWithinBusinessHourAvailable(); } await Users.updateLivechatStatusBasedOnBusinessHours(); - businessHourLogger.debug('Done opening default business hours'); }; export const createDefaultBusinessHourIfNotExists = async (): Promise => { diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index d899f2717376..5d2730dba9a1 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -8,7 +8,6 @@ import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { - businessHourLogger.debug('opening single business hour'); return openBusinessHourDefault(); } @@ -23,7 +22,6 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp } async onStartBusinessHours(): Promise { - businessHourLogger.debug('Starting Single Business Hours'); return openBusinessHourDefault(); } diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 30900481c4e2..0419f1d02a1d 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -3,7 +3,6 @@ import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { Livechat } from '../lib/Livechat'; -import { callbackLogger } from '../lib/logger'; type IAfterSaveUserProps = { user: IUser; @@ -34,17 +33,12 @@ const handleAgentCreated = async (user: IUser) => { const handleDeactivateUser = async (user: IUser) => { if (wasAgent(user)) { - callbackLogger.debug({ - msg: 'Removing agent extension & making agent unavailable', - userId: user._id, - }); await Users.makeAgentUnavailableAndUnsetExtension(user._id); } }; const handleActivateUser = async (user: IUser) => { if (isAgent(user)) { - callbackLogger.debug('Adding agent', user._id); await Livechat.addAgent(user.username); } }; diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index ec584ec001d6..e92e6b4d940b 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -3,7 +3,6 @@ import { LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; -import { callbackLogger } from '../lib/logger'; callbacks.add( 'afterSaveMessage', @@ -13,7 +12,6 @@ callbacks.add( return message; } - callbackLogger.debug(`Calculating Omnichannel metrics for room ${room._id}`); // skips this callback if the message was edited if (!message || isEditedMessage(message)) { return message; @@ -43,7 +41,6 @@ callbacks.add( const isResponseTotal = room.metrics?.response?.total; if (agentLastReply === room.ts) { - callbackLogger.debug('Calculating: first message from agent'); // first response const firstResponseDate = now; const firstResponseTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000; @@ -66,7 +63,6 @@ callbacks.add( reactionTime, }; } else if (visitorLastQuery > agentLastReply) { - callbackLogger.debug('Calculating: visitor sent a message after agent'); // response, not first const responseTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000; const avgResponseTime = diff --git a/apps/meteor/app/livechat/server/lib/Analytics.js b/apps/meteor/app/livechat/server/lib/Analytics.js index 5f6e3469501e..28bed221afbf 100644 --- a/apps/meteor/app/livechat/server/lib/Analytics.js +++ b/apps/meteor/app/livechat/server/lib/Analytics.js @@ -43,8 +43,6 @@ export const Analytics = { const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - logger.debug(`getAgentOverviewData[${name}] -> Using timezone ${timezone} with date range ${from} - ${to}`); - if (!(moment(from).isValid() && moment(to).isValid())) { logger.error('livechat:getAgentOverviewData => Invalid dates'); return; @@ -79,8 +77,6 @@ export const Analytics = { const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); const isSameDay = from.diff(to, 'days') === 0; - logger.debug(`getAnalyticsChartData[${name}] -> Using timezone ${timezone} with date range ${from} - ${to}`); - if (!(moment(from).isValid() && moment(to).isValid())) { logger.error('livechat:getAnalyticsChartData => Invalid dates'); return; @@ -133,8 +129,6 @@ export const Analytics = { const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - logger.debug(`getAnalyticsOverviewData[${name}] -> Using timezone ${timezone} with date range ${from} - ${to}`); - if (!(moment(from).isValid() && moment(to).isValid())) { logger.error('livechat:getAnalyticsOverviewData => Invalid dates'); return; diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts index 0dd48a328fd1..f17015e52e79 100644 --- a/apps/meteor/app/livechat/server/lib/Departments.ts +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -12,7 +12,6 @@ class DepartmentHelperClass { const department = await LivechatDepartment.findOneById(departmentId); if (!department) { - this.logger.debug(`Department not found: ${departmentId}`); throw new Error('error-department-not-found'); } @@ -20,10 +19,8 @@ class DepartmentHelperClass { const ret = await LivechatDepartment.removeById(_id); if (ret.acknowledged !== true) { - this.logger.error(`Department record not removed: ${_id}. Result from db: ${ret}`); throw new Error('error-failed-to-delete-department'); } - this.logger.debug(`Department record removed: ${_id}`); const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId>( department._id, @@ -47,8 +44,6 @@ class DepartmentHelperClass { } }); - this.logger.debug(`Post-department-removal actions completed: ${_id}. Notifying callbacks with department and agentsIds`); - setImmediate(() => { void callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); }); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 597f38b71ec0..aed0061e808e 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -23,7 +23,6 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent const dbInquiry = await LivechatInquiry.findOneById(inquiry._id); if (!dbInquiry) { - logger.error(`Inquiry with id ${inquiry._id} not found`); throw new Error('inquiry-not-found'); } @@ -68,7 +67,6 @@ export const QueueManager: queueManager = { ); if (!(await checkServiceStatus({ guest, agent }))) { - logger.debug(`Cannot create room for visitor ${guest._id}. No online agents`); throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); } @@ -96,8 +94,6 @@ export const QueueManager: queueManager = { throw new Error('inquiry-not-found'); } - logger.debug(`Generated inquiry for visitor ${guest._id} with id ${inquiry._id} [Not queued]`); - await LivechatRooms.updateRoomCount(); await queueInquiry(inquiry, agent); @@ -114,7 +110,6 @@ export const QueueManager: queueManager = { async unarchiveRoom(archivedRoom) { if (!archivedRoom) { - logger.error('No room to unarchive'); throw new Error('no-room-to-unarchive'); } @@ -145,17 +140,13 @@ export const QueueManager: queueManager = { await LivechatRooms.unarchiveOneById(rid); const room = await LivechatRooms.findOneById(rid); if (!room) { - logger.debug(`Room with id ${rid} not found`); throw new Error('room-not-found'); } const inquiry = await LivechatInquiry.findOneById(await createLivechatInquiry({ rid, name, guest, message, extraData: { source } })); if (!inquiry) { - logger.error(`Inquiry for visitor ${guest._id} not found`); throw new Error('inquiry-not-found'); } - logger.debug(`Generated inquiry for visitor ${v._id} with id ${inquiry._id} [Not queued]`); - await queueInquiry(inquiry, defaultAgent); logger.debug(`Inquiry ${inquiry._id} queued`); diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 0e975ca06763..f2fd7010eb12 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -74,7 +74,7 @@ export const RoutingManager: Routing = { }, async setMethodNameAndStartQueue(name) { - logger.debug(`Changing default routing method from ${this.methodName} to ${name}`); + logger.info(`Changing default routing method from ${this.methodName} to ${name}`); if (!this.methods[name]) { logger.warn(`Cannot change routing method to ${name}. Selected Routing method does not exists. Defaulting to Manual_Selection`); this.methodName = 'Manual_Selection'; @@ -87,7 +87,6 @@ export const RoutingManager: Routing = { // eslint-disable-next-line @typescript-eslint/naming-convention registerMethod(name, Method) { - logger.debug(`Registering new routing method with name ${name}`); this.methods[name] = new Method(); }, @@ -188,7 +187,6 @@ export const RoutingManager: Routing = { const { servedBy } = room; if (servedBy) { - logger.debug(`Unassigning current agent for inquiry ${inquiry._id}`); await LivechatRooms.removeAgentByRoomId(rid); await this.removeAllRoomSubscriptions(room); await dispatchAgentDelegated(rid); @@ -254,7 +252,7 @@ export const RoutingManager: Routing = { await LivechatInquiry.takeInquiry(_id); const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent); - logger.debug(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); + logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); callbacks.runAsync('livechat.afterTakeInquiry', inq, agent); @@ -262,7 +260,6 @@ export const RoutingManager: Routing = { }, async transferRoom(room, guest, transferData) { - logger.debug(`Transfering room ${room._id} by ${transferData.transferredBy._id}`); if (transferData.departmentId) { logger.debug(`Transfering room ${room._id} to department ${transferData.departmentId}`); return forwardRoomToDepartment(room, guest, transferData); @@ -278,7 +275,6 @@ export const RoutingManager: Routing = { }, async delegateAgent(agent, inquiry) { - logger.debug(`Delegating Inquiry ${inquiry._id}`); const defaultAgent = await callbacks.run('livechat.beforeDelegateAgent', agent, { department: inquiry?.department, }); diff --git a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts index 94fae239b74c..9cd5de75a0f3 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts @@ -18,9 +18,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:getAgentOverviewData'(options) { - methodDeprecationLogger.warn( - 'The method "livechat:getAgentOverviewData" is deprecated and will be removed after version v7.0.0. Use "livechat/analytics/agent-overview" instead.', - ); + methodDeprecationLogger.method('livechat:getAgentOverviewData', '7.0.0', ' Use "livechat/analytics/agent-overview" instead.'); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { diff --git a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts index 48313f1ce67c..76b7f276d671 100644 --- a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts @@ -3,6 +3,7 @@ import type { ServerMethods, TranslationKey } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import { Livechat } from '../lib/Livechat'; @@ -18,6 +19,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:getAnalyticsOverviewData'(options) { + methodDeprecationLogger.method('livechat:getAnalyticsOverviewData', '7.0.0', ' Use "livechat/analytics/overview" instead.'); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/app/livechat/server/methods/removeTrigger.ts b/apps/meteor/app/livechat/server/methods/removeTrigger.ts index c403dcd3edac..69f3a4a2d80c 100644 --- a/apps/meteor/app/livechat/server/methods/removeTrigger.ts +++ b/apps/meteor/app/livechat/server/methods/removeTrigger.ts @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,6 +15,8 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:removeTrigger'(triggerId) { + methodDeprecationLogger.method('livechat:removeTrigger', '7.0.0'); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.ts b/apps/meteor/app/livechat/server/sendMessageBySMS.ts index ea220b24d149..2557fcdeb83d 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.ts +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.ts @@ -10,33 +10,27 @@ import { callbackLogger } from './lib/logger'; callbacks.add( 'afterSaveMessage', async (message, room) => { - callbackLogger.debug('Attempting to send SMS message'); // skips this callback if the message was edited if (isEditedMessage(message)) { - callbackLogger.debug('Message was edited, skipping SMS send'); return message; } if (!settings.get('SMS_Enabled')) { - callbackLogger.debug('SMS is not enabled, skipping SMS send'); return message; } // only send the sms by SMS if it is a livechat room with SMS set to true if (!(isOmnichannelRoom(room) && room.sms && room.v && room.v.token)) { - callbackLogger.debug('Room is not a livechat room, skipping SMS send'); return message; } // if the message has a token, it was sent from the visitor, so ignore it if (message.token) { - callbackLogger.debug('Message was sent from the visitor, skipping SMS send'); return message; } // if the message has a type means it is a special message (like the closing comment), so skips if (message.t) { - callbackLogger.debug('Message is a special message, skipping SMS send'); return message; } @@ -52,8 +46,9 @@ callbacks.add( const { location } = message; extraData = Object.assign({}, extraData, { location }); } + const service = settings.get('SMS_Service'); - const SMSService = await OmnichannelIntegration.getSmsService(settings.get('SMS_Service')); + const SMSService = await OmnichannelIntegration.getSmsService(service); if (!SMSService) { callbackLogger.debug('SMS Service is not configured, skipping SMS send'); @@ -63,14 +58,12 @@ callbacks.add( const visitor = await LivechatVisitors.getVisitorByToken(room.v.token, { projection: { phone: 1 } }); if (!visitor?.phone || visitor.phone.length === 0) { - callbackLogger.debug('Visitor does not have a phone number, skipping SMS send'); return message; } try { - callbackLogger.debug(`Message will be sent to ${visitor.phone[0].phoneNumber} through service ${settings.get('SMS_Service')}`); await SMSService.send(room.sms.from, visitor.phone[0].phoneNumber, message.msg, extraData); - callbackLogger.debug(`SMS message sent to ${visitor.phone[0].phoneNumber}`); + callbackLogger.debug(`SMS message sent to ${visitor.phone[0].phoneNumber} via ${service}`); } catch (e) { callbackLogger.error(e); } diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index f24f88975b22..f9fce509e39a 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -62,14 +62,12 @@ Meteor.startup(async () => { await createDefaultBusinessHourIfNotExists(); settings.watch('Livechat_enable_business_hours', async (value) => { - Livechat.logger.debug(`Changing business hour type to ${value}`); + Livechat.logger.info(`Changing business hour type to ${value}`); if (value) { await businessHourManager.startManager(); - Livechat.logger.debug(`Business hour manager started`); return; } await businessHourManager.stopManager(); - Livechat.logger.debug(`Business hour manager stopped`); }); settings.watch('Livechat_Routing_Method', (value) => { diff --git a/apps/meteor/app/oembed/server/providers.ts b/apps/meteor/app/oembed/server/providers.ts index e80e456c679b..d2d0f85d19ce 100644 --- a/apps/meteor/app/oembed/server/providers.ts +++ b/apps/meteor/app/oembed/server/providers.ts @@ -1,9 +1,5 @@ -import QueryString from 'querystring'; -import URL from 'url'; - -import type { OEmbedMeta, OEmbedUrlContent, ParsedUrl, OEmbedProvider } from '@rocket.chat/core-typings'; +import type { OEmbedMeta, OEmbedUrlContent, OEmbedProvider } from '@rocket.chat/core-typings'; import { camelCase } from 'change-case'; -import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { SystemLogger } from '../../../server/lib/logger/system'; @@ -16,10 +12,10 @@ class Providers { } static getConsumerUrl(provider: OEmbedProvider, url: string): string { - const urlObj = new URL.URL(provider.endPoint); + const urlObj = new URL(provider.endPoint); urlObj.searchParams.set('url', url); - return URL.format(urlObj); + return urlObj.toString(); } registerProvider(provider: OEmbedProvider): number { @@ -95,25 +91,20 @@ providers.registerProvider({ callbacks.add( 'oembed:beforeGetUrlContent', (data) => { - if (data.parsedUrl != null) { - const url = URL.format(data.parsedUrl); - const provider = providers.getProviderForUrl(url); - if (provider != null) { - const consumerUrl = Providers.getConsumerUrl(provider, url); - - const parsedConsumerUrl = URL.parse(consumerUrl, true); - _.extend(data.parsedUrl, parsedConsumerUrl); - - data.urlObj.port = parsedConsumerUrl.port; - data.urlObj.hostname = parsedConsumerUrl.hostname; - data.urlObj.pathname = parsedConsumerUrl.pathname; - data.urlObj.query = parsedConsumerUrl.query; - - delete data.urlObj.search; - delete data.urlObj.host; - } + if (!data.urlObj) { + return data; } - return data; + + const url = data.urlObj.toString(); + const provider = providers.getProviderForUrl(url); + + if (!provider) { + return data; + } + + const consumerUrl = Providers.getConsumerUrl(provider, url); + + return { ...data, urlObj: new URL(consumerUrl) }; }, callbacks.priority.MEDIUM, 'oembed-providers-before', @@ -123,13 +114,11 @@ const cleanupOembed = (data: { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; - parsedUrl: ParsedUrl; content: OEmbedUrlContent; }): { url: string; meta: Omit; headers: { [k: string]: string }; - parsedUrl: ParsedUrl; content: OEmbedUrlContent; } => { if (!data?.meta) { @@ -148,24 +137,17 @@ const cleanupOembed = (data: { callbacks.add( 'oembed:afterParseContent', (data) => { - if (!data?.url || !data.content?.body || !data.parsedUrl?.query) { + if (!data?.url || !data.content?.body) { return cleanupOembed(data); } - const queryString = typeof data.parsedUrl.query === 'string' ? QueryString.parse(data.parsedUrl.query) : data.parsedUrl.query; - - if (!queryString.url) { - return cleanupOembed(data); - } + const provider = providers.getProviderForUrl(data.url); - const { url: originalUrl } = data; - const provider = providers.getProviderForUrl(originalUrl); if (!provider) { return cleanupOembed(data); } - const { url } = queryString; - data.meta.oembedUrl = url; + data.meta.oembedUrl = data.url; try { const metas = JSON.parse(data.content.body); diff --git a/apps/meteor/app/oembed/server/server.ts b/apps/meteor/app/oembed/server/server.ts index 256722cdd3d4..79de0402043f 100644 --- a/apps/meteor/app/oembed/server/server.ts +++ b/apps/meteor/app/oembed/server/server.ts @@ -1,6 +1,3 @@ -import querystring from 'querystring'; -import URL from 'url'; - import type { OEmbedUrlContentResult, OEmbedUrlWithMetadata, IMessage, MessageAttachment, OEmbedMeta } from '@rocket.chat/core-typings'; import { isOEmbedUrlContentResult, isOEmbedUrlWithMetadata } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; @@ -11,7 +8,6 @@ import he from 'he'; import iconv from 'iconv-lite'; import ipRangeCheck from 'ip-range-check'; import jschardet from 'jschardet'; -import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { isURL } from '../../../lib/utils/isURL'; @@ -62,14 +58,7 @@ const toUtf8 = function (contentType: string, body: Buffer): string { return iconv.decode(body, getCharset(contentType, body)); }; -const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery, redirectCount = 5): Promise { - let urlObj: URL.UrlWithStringQuery; - if (typeof urlObjStr === 'string') { - urlObj = URL.parse(urlObjStr); - } else { - urlObj = urlObjStr; - } - +const getUrlContent = async (urlObj: URL, redirectCount = 5): Promise => { const portsProtocol = new Map( Object.entries({ 80: 'http:', @@ -78,34 +67,28 @@ const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery }), ); - const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']); const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; - if (parsedUrl.hostname && (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts))) { + if (urlObj.hostname && (ignoredHosts.includes(urlObj.hostname) || ipRangeCheck(urlObj.hostname, ignoredHosts))) { throw new Error('invalid host'); } const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; - if (safePorts.length > 0 && parsedUrl.port && !safePorts.includes(parsedUrl.port)) { + // checks if the URL port is in the safe ports list + if (safePorts.length > 0 && urlObj.port && !safePorts.includes(urlObj.port)) { throw new Error('invalid/unsafe port'); } - if (safePorts.length > 0 && !parsedUrl.port && !safePorts.some((port) => portsProtocol.get(port) === parsedUrl.protocol)) { + // if port is not detected, use protocol to verify instead + if (safePorts.length > 0 && !urlObj.port && !safePorts.some((port) => portsProtocol.get(port) === urlObj.protocol)) { throw new Error('invalid/unsafe port'); } const data = await callbacks.run('oembed:beforeGetUrlContent', { urlObj, - parsedUrl, }); - /* This prop is neither passed or returned by the callback, so I'll just comment it for now - if (data.attachments != null) { - return data; - } */ - - const url = URL.format(data.urlObj); - + const url = data.urlObj.toString(); const sizeLimit = 250000; log.debug(`Fetching ${url} following redirects ${redirectCount} times`); @@ -137,10 +120,10 @@ const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery log.debug('Obtained response from server with length of', totalSize); const buffer = Buffer.concat(chunks); + return { headers: Object.fromEntries(response.headers), body: toUtf8(response.headers.get('content-type') || 'text/plain', buffer), - parsedUrl, statusCode: response.status, }; }; @@ -150,19 +133,13 @@ const getUrlMeta = async function ( withFragment?: boolean, ): Promise { log.debug('Obtaining metadata for URL', url); - const urlObj = URL.parse(url); - if (withFragment != null) { - const queryStringObj = querystring.parse(urlObj.query || ''); - queryStringObj._escaped_fragment_ = ''; - urlObj.query = querystring.stringify(queryStringObj); - let path = urlObj.pathname; - if (urlObj.query != null) { - path += `?${urlObj.query}`; - urlObj.search = `?${urlObj.query}`; - } - urlObj.path = path; + const urlObj = new URL(url); + + if (withFragment) { + urlObj.searchParams.set('_escaped_fragment_', ''); } - log.debug('Fetching url content', urlObj.path); + + log.debug('Fetching url content', urlObj.toString()); let content: OEmbedUrlContentResult | undefined; try { content = await getUrlContent(urlObj, 5); @@ -174,7 +151,7 @@ const getUrlMeta = async function ( return; } - if (content.attachments != null) { + if (content.attachments) { return content; } @@ -221,7 +198,6 @@ const getUrlMeta = async function ( url, meta: metas, headers, - parsedUrl: content.parsedUrl, content, }); }; @@ -233,38 +209,25 @@ const getUrlMetaWithCache = async function ( log.debug('Getting oembed metadata for', url); const cache = await OEmbedCache.findOneById(url); - if (cache != null) { + if (cache) { log.debug('Found oembed metadata in cache for', url); return cache.data; } + const data = await getUrlMeta(url, withFragment); - if (data != null) { - try { - log.debug('Saving oembed metadata in cache for', url); - await OEmbedCache.createWithIdAndData(url, data); - } catch (_error) { - log.error({ msg: 'OEmbed duplicated record', url }); - } - return data; - } -}; -const hasOnlyContentLength = (obj: any): obj is { contentLength: string } => 'contentLength' in obj && Object.keys(obj).length === 1; -const hasOnlyContentType = (obj: any): obj is { contentType: string } => 'contentType' in obj && Object.keys(obj).length === 1; -const hasContentLengthAndContentType = (obj: any): obj is { contentLength: string; contentType: string } => - 'contentLength' in obj && 'contentType' in obj && Object.keys(obj).length === 2; - -const getRelevantHeaders = function (headersObj: { - [key: string]: string; -}): { contentLength: string } | { contentType: string } | { contentLength: string; contentType: string } | void { - const headers = { - ...(headersObj.contentLength && { contentLength: headersObj.contentLength }), - ...(headersObj.contentType && { contentType: headersObj.contentType }), - }; + if (!data) { + return; + } - if (hasOnlyContentLength(headers) || hasOnlyContentType(headers) || hasContentLengthAndContentType(headers)) { - return headers; + try { + log.debug('Saving oembed metadata in cache for', url); + await OEmbedCache.createWithIdAndData(url, data); + } catch (_error) { + log.error({ msg: 'OEmbed duplicated record', url }); } + + return data; }; const getRelevantMetaTags = function (metaObj: OEmbedMeta): Record | void { @@ -286,57 +249,71 @@ const insertMaxWidthInOembedHtml = (oembedHtml?: string): string | undefined => const rocketUrlParser = async function (message: IMessage): Promise { log.debug('Parsing message URLs'); - if (Array.isArray(message.urls)) { - log.debug('URLs found', message.urls.length); - - if ( - (message.attachments && message.attachments.length > 0) || - message.urls.filter((item) => !item.url.includes(settings.get('Site_Url'))).length > MAX_EXTERNAL_URL_PREVIEWS - ) { - log.debug('All URL ignored'); - return message; + + if (!Array.isArray(message.urls)) { + return message; + } + + log.debug('URLs found', message.urls.length); + + if ( + (message.attachments && message.attachments.length > 0) || + message.urls.filter((item) => !item.url.includes(settings.get('Site_Url'))).length > MAX_EXTERNAL_URL_PREVIEWS + ) { + log.debug('All URL ignored'); + return message; + } + + const attachments: MessageAttachment[] = []; + + let changed = false; + for await (const item of message.urls) { + if (item.ignoreParse === true) { + log.debug('URL ignored', item.url); + continue; } - const attachments: MessageAttachment[] = []; + if (!isURL(item.url)) { + continue; + } - let changed = false; - for await (const item of message.urls) { - if (item.ignoreParse === true) { - log.debug('URL ignored', item.url); - continue; - } - if (!isURL(item.url)) { - continue; - } - const data = await getUrlMetaWithCache(item.url); - if (data != null) { - if (isOEmbedUrlContentResult(data) && data.attachments) { - attachments.push(...data.attachments); - break; - } - if (isOEmbedUrlWithMetadata(data) && data.meta != null) { - item.meta = getRelevantMetaTags(data.meta) || {}; - if (item.meta?.oembedHtml) { - item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml) || ''; - } - } - if (data.headers != null) { - const headers = getRelevantHeaders(data.headers); - if (headers) { - item.headers = headers; - } - } - item.parsedUrl = data.parsedUrl; - changed = true; + const data = await getUrlMetaWithCache(item.url); + + if (!data) { + continue; + } + + if (isOEmbedUrlContentResult(data) && data.attachments) { + attachments.push(...data.attachments); + break; + } + + if (isOEmbedUrlWithMetadata(data) && data.meta) { + item.meta = getRelevantMetaTags(data.meta) || {}; + if (item.meta?.oembedHtml) { + item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml) || ''; } } - if (attachments.length > 0) { - await Messages.setMessageAttachments(message._id, attachments); + + if (data.headers?.contentLength) { + item.headers = { ...item.headers, contentLength: data.headers.contentLength }; } - if (changed === true) { - await Messages.setUrlsById(message._id, message.urls); + + if (data.headers?.contentType) { + item.headers = { ...item.headers, contentType: data.headers.contentType }; } + + changed = true; } + + if (attachments.length) { + await Messages.setMessageAttachments(message._id, attachments); + } + + if (changed === true) { + await Messages.setUrlsById(message._id, message.urls); + } + return message; }; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 8cfe45b42232..89b068c11341 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -27,7 +27,7 @@ import { } from '@rocket.chat/models'; import { MongoInternals } from 'meteor/mongo'; -import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; +import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server/getStatistics'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { getControl } from '../../../../server/lib/migrations'; @@ -41,8 +41,6 @@ import { getAppsStatistics } from './getAppsStatistics'; import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; -const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server']; - const getUserLanguages = async (totalUsers: number): Promise<{ [key: string]: number }> => { const result = await Users.getUserLanguages(); @@ -70,17 +68,29 @@ export const statistics = { const statistics = {} as IStats; const statsPms = []; + const fetchWizardSettingValue = async (settingName: string): Promise => { + return ((await Settings.findOne(settingName))?.value as T | undefined) ?? undefined; + }; + // Setup Wizard - statistics.wizard = {}; - await Promise.all( - wizardFields.map(async (field) => { - const record = await Settings.findOne(field); - if (record) { - const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); - statistics.wizard[wizardField] = record.value; - } - }), - ); + const [organizationType, industry, size, country, language, serverType, registerServer] = await Promise.all([ + fetchWizardSettingValue('Organization_Type'), + fetchWizardSettingValue('Industry'), + fetchWizardSettingValue('Size'), + fetchWizardSettingValue('Country'), + fetchWizardSettingValue('Language'), + fetchWizardSettingValue('Server_Type'), + fetchWizardSettingValue('Register_Server'), + ]); + statistics.wizard = { + organizationType, + industry, + size, + country, + language, + serverType, + registerServer, + }; // Version const uniqueID = await Settings.findOne('uniqueID'); @@ -507,6 +517,15 @@ export const statistics = { statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count'); statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); + const defaultGateway = (await Settings.findOneById('Push_gateway', { projection: { packageValue: 1 } }))?.packageValue; + + // one bit for each of the following: + const pushEnabled = settings.get('Push_enable') ? 1 : 0; + const pushGatewayEnabled = settings.get('Push_enable_gateway') ? 2 : 0; + const pushGatewayChanged = settings.get('Push_gateway') !== defaultGateway ? 4 : 0; + + statistics.push = pushEnabled | pushGatewayEnabled | pushGatewayChanged; + const defaultHomeTitle = (await Settings.findOneById('Layout_Home_Title'))?.packageValue; statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle; diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index 4609797e6bd2..a926f8540d27 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -48,13 +48,15 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) text: string, { selection, + skipFocus, }: { selection?: | { readonly start?: number; readonly end?: number } | ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number }); + skipFocus?: boolean; } = {}, ): void => { - focus(); + !skipFocus && focus(); const { selectionStart, selectionEnd } = input; const textAreaTxt = input.value; @@ -66,7 +68,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) if (selection) { if (!document.execCommand?.('insertText', false, text)) { input.value = textAreaTxt.substring(0, selectionStart) + text + textAreaTxt.substring(selectionStart); - focus(); + !skipFocus && focus(); } input.setSelectionRange(selection.start ?? 0, selection.end ?? text.length); } @@ -78,7 +80,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) triggerEvent(input, 'input'); triggerEvent(input, 'change'); - focus(); + !skipFocus && focus(); }; const insertText = (text: string): void => { @@ -260,7 +262,9 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const insertNewLine = (): void => insertText('\n'); - setText(Meteor._localStorage.getItem(storageID) ?? ''); + setText(Meteor._localStorage.getItem(storageID) ?? '', { + skipFocus: true, + }); // Gets the text that is connected to the cursor and replaces it with the given text const replaceText = (text: string, selection: { readonly start: number; readonly end: number }): void => { diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx index 96d11cf44cb7..f96c198865cc 100644 --- a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx @@ -18,7 +18,16 @@ const UserAndRoomAutoCompleteMultiple = ({ value, onChange, ...props }: UserAndR const debouncedFilter = useDebouncedValue(filter, 1000); const rooms = useUserSubscriptions( - useMemo(() => ({ open: { $ne: false }, lowerCaseName: new RegExp(escapeRegExp(debouncedFilter), 'i') }), [debouncedFilter]), + useMemo( + () => ({ + open: { $ne: false }, + $or: [ + { lowerCaseFName: new RegExp(escapeRegExp(debouncedFilter), 'i') }, + { lowerCaseName: new RegExp(escapeRegExp(debouncedFilter), 'i') }, + ], + }), + [debouncedFilter], + ), ).filter((room) => { if (!user) { return; diff --git a/apps/meteor/client/definitions/info.d.ts b/apps/meteor/client/definitions/info.d.ts index 2b66032f484a..43fa1fc53414 100644 --- a/apps/meteor/client/definitions/info.d.ts +++ b/apps/meteor/client/definitions/info.d.ts @@ -23,4 +23,9 @@ declare module '*.info' { tag?: string; branch?: string; }; + + export const minimumClientVersions: { + desktop: string; + mobile: string; + }; } diff --git a/apps/meteor/client/hooks/useLicense.ts b/apps/meteor/client/hooks/useLicense.ts index 99b7e5e3461c..0f568d9bd5cc 100644 --- a/apps/meteor/client/hooks/useLicense.ts +++ b/apps/meteor/client/hooks/useLicense.ts @@ -1,13 +1,23 @@ import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; export const useLicense = (): UseQueryResult> => { const getLicenses = useEndpoint('GET', '/v1/licenses.get'); + const canViewLicense = usePermission('view-privileged-setting'); - return useQuery(['licenses', 'getLicenses'], () => getLicenses(), { - staleTime: Infinity, - keepPreviousData: true, - }); + return useQuery( + ['licenses', 'getLicenses'], + () => { + if (!canViewLicense) { + throw new Error('unauthorized api call'); + } + return getLicenses(); + }, + { + staleTime: Infinity, + keepPreviousData: true, + }, + ); }; diff --git a/apps/meteor/client/hooks/useRegistrationStatus.ts b/apps/meteor/client/hooks/useRegistrationStatus.ts index 8b091459291b..9260d672bec5 100644 --- a/apps/meteor/client/hooks/useRegistrationStatus.ts +++ b/apps/meteor/client/hooks/useRegistrationStatus.ts @@ -1,13 +1,23 @@ import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; export const useRegistrationStatus = (): UseQueryResult> => { const getRegistrationStatus = useEndpoint('GET', '/v1/cloud.registrationStatus'); + const canViewregistrationStatus = usePermission('manage-cloud'); - return useQuery(['getRegistrationStatus'], () => getRegistrationStatus(), { - keepPreviousData: true, - staleTime: Infinity, - }); + return useQuery( + ['getRegistrationStatus'], + () => { + if (!canViewregistrationStatus) { + throw new Error('unauthorized api call'); + } + return getRegistrationStatus(); + }, + { + keepPreviousData: true, + staleTime: Infinity, + }, + ); }; diff --git a/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx b/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx index 69afd3c2667a..593bd784be90 100644 --- a/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx +++ b/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx @@ -1,21 +1,14 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { SidebarSection } from '@rocket.chat/fuselage'; import type { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ComponentType, ReactElement } from 'react'; +import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '../../contexts/VideoConfContext'; import type { useAvatarTemplate } from '../hooks/useAvatarTemplate'; import type { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; -import OmnichannelSection from '../sections/OmnichannelSection'; import SideBarItemTemplateWithData from './SideBarItemTemplateWithData'; -const sections: { - [key: string]: ComponentType; -} = { - Omnichannel: OmnichannelSection, -}; - type RoomListRowProps = { extended: boolean; t: ReturnType; @@ -44,10 +37,7 @@ const RoomListRow = ({ data, item }: { data: RoomListRowProps; item: ISubscripti ); if (typeof item === 'string') { - const Section = sections[item]; - return Section ? ( -
- ) : ( + return ( {t(item)} diff --git a/apps/meteor/client/sidebar/Sidebar.tsx b/apps/meteor/client/sidebar/Sidebar.tsx index ae333bbdb2a1..9c7634872ed4 100644 --- a/apps/meteor/client/sidebar/Sidebar.tsx +++ b/apps/meteor/client/sidebar/Sidebar.tsx @@ -4,12 +4,16 @@ import { useSessionStorage } from '@rocket.chat/fuselage-hooks'; import { useLayout, useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; +import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnabled'; import SidebarRoomList from './RoomList'; import SidebarFooter from './footer'; import SidebarHeader from './header'; +import OmnichannelSection from './sections/OmnichannelSection'; import StatusDisabledSection from './sections/StatusDisabledSection'; const Sidebar = () => { + const showOmnichannel = useOmnichannelEnabled(); + const sidebarViewMode = useUserPreference('sidebarViewMode'); const sidebarHideAvatar = !useUserPreference('sidebarDisplayAvatar'); const { sidebar } = useLayout(); @@ -38,6 +42,7 @@ const Sidebar = () => { > {presenceDisabled && !bannerDismissed && setBannerDismissed(true)} />} + {showOmnichannel && } diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx index 248b91418739..b0b20972d346 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx @@ -19,12 +19,14 @@ it('should not show upgrade item if has license and not have trial', async () => workspaceRegistered: false, } as any, })) + .withPermission('view-privileged-setting') + .withPermission('manage-cloud') .build(), }); await waitFor(() => !!(result.all.length > 1)); - expect(result.current).toEqual([]); + expect(result.current.length).toEqual(1); }); it('should return an upgrade item if not have license or if have a trial', async () => { @@ -42,10 +44,13 @@ it('should return an upgrade item if not have license or if have a trial', async workspaceRegistered: false, } as any, })) + .withPermission('view-privileged-setting') + .withPermission('manage-cloud') .build(), }); - await waitFor(() => !!result.current[0]); + // Workspace admin is also expected to be here + await waitFor(() => result.current.length > 1); expect(result.current[0]).toEqual( expect.objectContaining({ diff --git a/apps/meteor/client/sidebar/hooks/useRoomList.ts b/apps/meteor/client/sidebar/hooks/useRoomList.ts index 436c7c1dc71d..fa5dfd2797cb 100644 --- a/apps/meteor/client/sidebar/hooks/useRoomList.ts +++ b/apps/meteor/client/sidebar/hooks/useRoomList.ts @@ -92,7 +92,6 @@ export const useRoomList = (): Array => { }); const groups = new Map(); - showOmnichannel && groups.set('Omnichannel', []); incomingCall.size && groups.set('Incoming Calls', incomingCall); showOmnichannel && inquiries.enabled && queue.length && groups.set('Incoming_Livechats', queue); showOmnichannel && omnichannel.size && groups.set('Open_Livechats', omnichannel); diff --git a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx index 542fa05c54ab..e7dec5f3506a 100644 --- a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx +++ b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx @@ -1,15 +1,13 @@ -import type { Box } from '@rocket.chat/fuselage'; import { Sidebar } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useLayout, useRoute, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; import React, { memo } from 'react'; import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; import { useOmnichannelShowQueueLink } from '../../hooks/omnichannel/useOmnichannelShowQueueLink'; import { OmniChannelCallDialPad, OmnichannelCallToggle, OmnichannelLivechatToggle } from './actions'; -const OmnichannelSection = (props: typeof Box): ReactElement => { +const OmnichannelSection = () => { const t = useTranslation(); const isCallEnabled = useIsCallEnabled(); const isCallReady = useIsCallReady(); @@ -34,7 +32,7 @@ const OmnichannelSection = (props: typeof Box): ReactElement => { // The className is a paliative while we make TopBar.ToolBox optional on fuselage return ( - + {t('Omnichannel')} {showOmnichannelQueueLink && ( @@ -56,6 +54,4 @@ const OmnichannelSection = (props: typeof Box): ReactElement => { ); }; -export default Object.assign(memo(OmnichannelSection), { - size: 56, -}); +export default memo(OmnichannelSection); diff --git a/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx b/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx index e3e1f474cb91..a75e66ec1e4b 100644 --- a/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx +++ b/apps/meteor/client/views/admin/cloud/RegisterWorkspace.tsx @@ -57,9 +57,9 @@ const RegisterWorkspace = () => { - - {isWorkspaceRegistered && t('RegisterWorkspace_NotRegistered_Subtitle')} - {!isWorkspaceRegistered && t('RegisterWorkspace_Registered_Description')} + + {!isWorkspaceRegistered && t('RegisterWorkspace_NotRegistered_Subtitle')} + {isWorkspaceRegistered && t('RegisterWorkspace_Registered_Description')} diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx index ebb92b040c83..98aa3a7073ff 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx @@ -265,6 +265,7 @@ export default { totalCustomRoles: 0, totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, + push: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx index 222f31f88334..a6ef0c8e9289 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx @@ -295,6 +295,7 @@ export default { totalCustomRoles: 0, totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, + push: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx index 14a6cac8633d..da49ee88fa6b 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx @@ -243,6 +243,7 @@ export default { totalCustomRoles: 0, totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, + push: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js b/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js index 94bbd156b86c..ae4d4fa411b5 100644 --- a/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js +++ b/apps/meteor/client/views/admin/integrations/IncomingWebhookForm.js @@ -1,4 +1,4 @@ -import { Field, TextInput, Box, ToggleSwitch, Icon, TextAreaInput, FieldGroup, Margins } from '@rocket.chat/fuselage'; +import { Field, TextInput, Box, ToggleSwitch, Icon, TextAreaInput, FieldGroup, Margins, Select } from '@rocket.chat/fuselage'; import { useAbsoluteUrl, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo, useCallback } from 'react'; @@ -11,7 +11,8 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat const absoluteUrl = useAbsoluteUrl(); - const { enabled, channel, username, name, alias, avatar, emoji, scriptEnabled, script, overrideDestinationChannelEnabled } = formValues; + const { enabled, channel, username, name, alias, avatar, emoji, scriptEnabled, script, scriptEngine, overrideDestinationChannelEnabled } = + formValues; const { handleEnabled, @@ -24,6 +25,7 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat handleScriptEnabled, handleOverrideDestinationChannelEnabled, handleScript, + handleScriptEngine, } = formHandlers; const url = absoluteUrl(`hooks/${extraData._id}/${extraData.token}`); @@ -42,6 +44,14 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat url, }); + const scriptEngineOptions = useMemo( + () => [ + ['vm2', t('Script_Engine_vm2')], + ['isolated-vm', t('Script_Engine_isolated_vm')], + ], + [t], + ); + const hilightedExampleJson = useHighlightedCode('json', JSON.stringify(exampleData, null, 2)); return ( @@ -172,6 +182,18 @@ export default function IncomingWebhookForm({ formValues, formHandlers, extraDat ), [t, scriptEnabled, handleScriptEnabled], )} + {useMemo( + () => ( + + {t('Script_Engine')} + + + + {t('Script_Engine_Description')} + + ), + [scriptEngine, scriptEngineOptions, handleScriptEngine, t], + )} {useMemo( () => ( diff --git a/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js b/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js index cbe3c3e5377d..e785f63ca29d 100644 --- a/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/edit/EditIncomingWebhook.js @@ -17,6 +17,7 @@ const getInitialValue = (data) => { avatar: data.avatar ?? '', emoji: data.emoji ?? '', scriptEnabled: data.scriptEnabled, + scriptEngine: data.scriptEngine ?? 'vm2', overrideDestinationChannelEnabled: data.overrideDestinationChannelEnabled, script: data.script, }; diff --git a/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js b/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js index 1734f32968c9..383b9209519d 100644 --- a/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/edit/EditOutgoingWebhook.js @@ -24,6 +24,7 @@ const getInitialValue = (data) => { avatar: data.avatar ?? '', emoji: data.emoji ?? '', scriptEnabled: data.scriptEnabled ?? false, + scriptEngine: data.scriptEngine ?? 'vm2', script: data.script ?? '', retryFailedCalls: data.retryFailedCalls ?? true, retryCount: data.retryCount ?? 5, diff --git a/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js b/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js index 019dc6d0d730..7b4e0880e57f 100644 --- a/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/new/NewIncomingWebhook.js @@ -15,6 +15,7 @@ const initialState = { avatar: '', emoji: '', scriptEnabled: false, + scriptEngine: 'isolated-vm', overrideDestinationChannelEnabled: false, script: '', }; diff --git a/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js b/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js index 818082f5f5de..153dc4c6eb7f 100644 --- a/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js +++ b/apps/meteor/client/views/admin/integrations/new/NewOutgoingWebhook.js @@ -23,6 +23,7 @@ const defaultData = { avatar: '', emoji: '', scriptEnabled: false, + scriptEngine: 'isolated-vm', script: '', retryFailedCalls: true, retryCount: 6, diff --git a/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx b/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx index f870dbc52776..326cc3382b4b 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx @@ -10,7 +10,7 @@ type DateRangePickerProps = Omit, 'onChange'> & { onChange(range: { start: string; end: string }): void; }; -const formatToDateInput = (date: Moment) => date.format('YYYY-MM-DD'); +const formatToDateInput = (date: Moment) => date.locale('en').format('YYYY-MM-DD'); const todayDate = formatToDateInput(moment()); diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index fa418b986cc1..a25bea5affaa 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -70,8 +70,8 @@ declare module '@rocket.chat/ui-contexts' { pattern: '/admin/registration/:page?'; }; 'admin-view-logs': { - pathname: '/admin/logs'; - pattern: '/admin/logs'; + pathname: '/admin/records'; + pattern: '/admin/records'; }; 'federation-dashboard': { pathname: '/admin/federation'; @@ -193,7 +193,7 @@ registerAdminRoute('/registration/:page?', { component: lazy(() => import('./cloud/CloudRoute')), }); -registerAdminRoute('/logs', { +registerAdminRoute('/records', { name: 'admin-view-logs', component: lazy(() => import('./viewLogs/ViewLogsRoute')), }); diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index c397e28e6db1..50a3284b5ed1 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -112,8 +112,8 @@ export const { permissionGranted: (): boolean => hasPermission('run-import'), }, { - href: '/admin/logs', - i18nLabel: 'Logs', + href: '/admin/records', + i18nLabel: 'Records', icon: 'post', permissionGranted: (): boolean => hasPermission('view-logs'), }, diff --git a/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx new file mode 100644 index 000000000000..7771298ceb73 --- /dev/null +++ b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx @@ -0,0 +1,38 @@ +import { Box, Icon, Skeleton } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { useAnalyticsObject } from './hooks/useAnalyticsObject'; + +const AnalyticsReports = () => { + const t = useTranslation(); + + const { data, isLoading, isSuccess, isError } = useAnalyticsObject(); + + return ( + <> + + + + + + {t('How_and_why_we_collect_usage_data')} + + {t('Analytics_page_briefing')} + + + {isSuccess &&
{JSON.stringify(data, null, '\t')}
} + {isError && t('Something_went_wrong_try_again_later')} + {isLoading && ( + <> + + + + + )} +
+ + ); +}; + +export default AnalyticsReports; diff --git a/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx b/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx index a75c22da19b0..4463fec8f5bf 100644 --- a/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx +++ b/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx @@ -1,18 +1,30 @@ +import { Tabs } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import Page from '../../../components/Page'; +import AnalyticsReports from './AnalyticsReports'; import ServerLogs from './ServerLogs'; const ViewLogsPage = (): ReactElement => { const t = useTranslation(); + const [tab, setTab] = useState('Logs'); + return ( - + - + + setTab('Logs')} selected={tab === 'Logs'}> + {t('Logs')} + + setTab('Analytics')} selected={tab === 'Analytics'}> + {t('Analytic_reports')} + + + {tab === 'Logs' ? : } ); diff --git a/apps/meteor/client/views/admin/viewLogs/hooks/useAnalyticsObject.ts b/apps/meteor/client/views/admin/viewLogs/hooks/useAnalyticsObject.ts new file mode 100644 index 000000000000..8aad0e605964 --- /dev/null +++ b/apps/meteor/client/views/admin/viewLogs/hooks/useAnalyticsObject.ts @@ -0,0 +1,8 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +export const useAnalyticsObject = () => { + const getAnalytics = useEndpoint('GET', '/v1/statistics'); + + return useQuery(['analytics'], () => getAnalytics({}), { staleTime: 10 * 60 * 1000 }); +}; diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index e051b69db8fa..65dd4cb1e396 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -1,3 +1,4 @@ +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; import { useSetting } from '@rocket.chat/ui-contexts'; import { format } from 'date-fns'; @@ -16,9 +17,12 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const hasValidLicense = licensesData?.licenses.some((license) => license.modules.length > 0) ?? false; const hadExpiredTrials = cloudWorkspaceHadTrial ?? false; - const trialLicense = licensesData?.licenses?.find(({ meta }) => meta?.trial); - const isTrial = licensesData?.licenses?.every(({ meta }) => meta?.trial) ?? false; - const trialEndDate = trialLicense?.meta ? format(new Date(trialLicense.meta.trialEnd), 'yyyy-MM-dd') : undefined; + const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[]; + + const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial); + const isTrial = Boolean(trialLicense); + const trialEndDateStr = trialLicense?.information?.visualExpiration || trialLicense?.meta?.trialEnd || trialLicense?.cloudMeta?.trialEnd; + const trialEndDate = trialEndDateStr ? format(new Date(trialEndDateStr), 'yyyy-MM-dd') : undefined; const upgradeTabType = getUpgradeTabType({ registered, diff --git a/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx b/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx index 8a1a81d9f64a..5ade384582c2 100644 --- a/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx +++ b/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx @@ -1,7 +1,7 @@ import type { ILivechatTrigger } from '@rocket.chat/core-typings'; import { IconButton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useRoute, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; import GenericModal from '../../../components/GenericModal'; @@ -13,7 +13,7 @@ const TriggersRow = ({ _id, name, description, enabled, reload }: TriggersRowPro const t = useTranslation(); const setModal = useSetModal(); const triggersRoute = useRoute('omnichannel-triggers'); - const deleteTrigger = useMethod('livechat:removeTrigger'); + const deleteTrigger = useEndpoint('DELETE', '/v1/livechat/triggers/:_id', { _id }); const dispatchToastMessage = useToastMessageDispatch(); const handleClick = useMutableCallback(() => { @@ -35,7 +35,7 @@ const TriggersRow = ({ _id, name, description, enabled, reload }: TriggersRowPro e.stopPropagation(); const onDeleteTrigger = async () => { try { - await deleteTrigger(_id); + await deleteTrigger(); dispatchToastMessage({ type: 'success', message: t('Trigger_removed') }); reload(); } catch (error) { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index de9a96dc43b4..da598c00be11 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -141,7 +141,7 @@ const MessageBox = ({ [chat, storageID], ); - const autofocusRef = useMessageBoxAutoFocus(); + const autofocusRef = useMessageBoxAutoFocus(!isMobile); const useEmojis = useUserPreference('useEmojis'); diff --git a/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts b/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts index 5ea6db79a869..b8efd9391f87 100644 --- a/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts +++ b/apps/meteor/client/views/room/composer/messageBox/hooks/useMessageBoxAutoFocus.ts @@ -1,13 +1,13 @@ import type { Ref } from 'react'; -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; /** * if the user is types outside the message box and its not actually typing in any input field * then the message box should be focused * @returns callbackRef to bind the logic to the message box */ -export const useMessageBoxAutoFocus = (): Ref => { - const ref = useRef(null); +export const useMessageBoxAutoFocus = (enabled: boolean): Ref => { + const ref = useRef(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -43,5 +43,22 @@ export const useMessageBoxAutoFocus = (): Ref => { }; }, []); - return ref; + return useCallback( + (node: HTMLElement | null) => { + if (!node) { + return; + } + + ref.current = node; + + if (!enabled) { + return; + } + + if (ref.current) { + ref.current.focus(); + } + }, + [enabled, ref], + ); }; diff --git a/apps/meteor/ee/app/api-enterprise/server/index.ts b/apps/meteor/ee/app/api-enterprise/server/index.ts index 6af539bda36c..7a528a4ec2f4 100644 --- a/apps/meteor/ee/app/api-enterprise/server/index.ts +++ b/apps/meteor/ee/app/api-enterprise/server/index.ts @@ -1,5 +1,5 @@ -import { onLicense } from '../../license/server'; +import { License } from '@rocket.chat/license'; -await onLicense('canned-responses', async () => { +await License.onLicense('canned-responses', async () => { await import('./canned-responses'); }); diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.js b/apps/meteor/ee/app/authorization/server/validateUserRoles.js deleted file mode 100644 index fe8e3410bc01..000000000000 --- a/apps/meteor/ee/app/authorization/server/validateUserRoles.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { isEnterprise, getMaxGuestUsers } from '../../license/server'; - -export const validateUserRoles = async function (userId, userData) { - if (!isEnterprise()) { - return; - } - - if (!userData.roles.includes('guest')) { - return; - } - - if (userData.roles.length >= 2) { - throw new Meteor.Error('error-guests-cant-have-other-roles', "Guest users can't receive any other role", { - method: 'insertOrUpdateUser', - field: 'Assign_role', - }); - } - - const guestCount = await Users.getActiveLocalGuestCount(userData._id); - if (guestCount >= getMaxGuestUsers()) { - throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', { - method: 'insertOrUpdateUser', - field: 'Assign_role', - }); - } -}; diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.ts b/apps/meteor/ee/app/authorization/server/validateUserRoles.ts new file mode 100644 index 000000000000..a07165b8c5d8 --- /dev/null +++ b/apps/meteor/ee/app/authorization/server/validateUserRoles.ts @@ -0,0 +1,43 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; +import { Users } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { i18n } from '../../../../server/lib/i18n'; + +export const validateUserRoles = async function (userData: Partial) { + if (!License.hasValidLicense()) { + return; + } + + const isGuest = Boolean(userData.roles?.includes('guest') && userData.roles.length === 1); + const currentUserData = userData._id ? await Users.findOneById(userData._id) : null; + const wasGuest = Boolean(currentUserData?.roles?.includes('guest') && currentUserData.roles.length === 1); + + if (currentUserData?.type === 'app') { + return; + } + + if (isGuest) { + if (wasGuest) { + return; + } + + if (await License.shouldPreventAction('guestUsers')) { + throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', { + method: 'insertOrUpdateUser', + field: 'Assign_role', + }); + } + + return; + } + + if (!wasGuest && userData._id) { + return; + } + + if (await License.shouldPreventAction('activeUsers')) { + throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); + } +}; diff --git a/apps/meteor/ee/app/canned-responses/server/index.ts b/apps/meteor/ee/app/canned-responses/server/index.ts index 47249c017b83..99254b037380 100644 --- a/apps/meteor/ee/app/canned-responses/server/index.ts +++ b/apps/meteor/ee/app/canned-responses/server/index.ts @@ -1,6 +1,6 @@ -import { onLicense } from '../../license/server'; +import { License } from '@rocket.chat/license'; -await onLicense('canned-responses', async () => { +await License.onLicense('canned-responses', async () => { const { createSettings } = await import('./settings'); await import('./permissions'); await import('./hooks/onRemoveAgentDepartment'); diff --git a/apps/meteor/ee/app/license/server/canEnableApp.ts b/apps/meteor/ee/app/license/server/canEnableApp.ts new file mode 100644 index 000000000000..72220e27acad --- /dev/null +++ b/apps/meteor/ee/app/license/server/canEnableApp.ts @@ -0,0 +1,25 @@ +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import { Apps } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; + +import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; + +export const canEnableApp = async (app: IAppStorageItem): Promise => { + if (!(await Apps.isInitialized())) { + return false; + } + + // Migrated apps were installed before the validation was implemented + // so they're always allowed to be enabled + if (app.migrated) { + return true; + } + + const source = getInstallationSourceFromAppStorageItem(app); + switch (source) { + case 'private': + return !(await License.shouldPreventAction('privateApps')); + default: + return !(await License.shouldPreventAction('marketplaceApps')); + } +}; diff --git a/apps/meteor/ee/app/license/server/decrypt.ts b/apps/meteor/ee/app/license/server/decrypt.ts deleted file mode 100644 index 62e34817aec6..000000000000 --- a/apps/meteor/ee/app/license/server/decrypt.ts +++ /dev/null @@ -1,10 +0,0 @@ -import crypto from 'crypto'; - -const publicKey = - 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; - -export default function decrypt(encrypted: string): string { - const decrypted = crypto.publicDecrypt(Buffer.from(publicKey, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); - - return decrypted.toString('utf-8'); -} diff --git a/apps/meteor/ee/app/license/server/getStatistics.ts b/apps/meteor/ee/app/license/server/getStatistics.ts index d7f81e416bfd..e8ff402ea1ca 100644 --- a/apps/meteor/ee/app/license/server/getStatistics.ts +++ b/apps/meteor/ee/app/license/server/getStatistics.ts @@ -1,10 +1,9 @@ import { log } from 'console'; import { Analytics } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { CannedResponse, OmnichannelServiceLevelAgreements, LivechatRooms, LivechatTag, LivechatUnit, Users } from '@rocket.chat/models'; -import { getModules, getTags, hasLicense } from './license'; - type ENTERPRISE_STATISTICS = GenericStats & Partial; type GenericStats = { @@ -28,8 +27,8 @@ type EEOnlyStats = { export async function getStatistics(): Promise { const genericStats: GenericStats = { - modules: getModules(), - tags: getTags().map(({ name }) => name), + modules: License.getModules(), + tags: License.getTags().map(({ name }) => name), seatRequests: await Analytics.getSeatRequestCount(), }; @@ -45,7 +44,7 @@ export async function getStatistics(): Promise { // These models are only available on EE license so don't import them inside CE license as it will break the build async function getEEStatistics(): Promise { - if (!hasLicense('livechat-enterprise')) { + if (!License.hasModule('livechat-enterprise')) { return; } diff --git a/apps/meteor/ee/app/license/server/index.ts b/apps/meteor/ee/app/license/server/index.ts index f7d83ed388b8..403922524fa8 100644 --- a/apps/meteor/ee/app/license/server/index.ts +++ b/apps/meteor/ee/app/license/server/index.ts @@ -2,6 +2,4 @@ import './settings'; import './methods'; import './startup'; -export { onLicense, overwriteClassOnLicense, isEnterprise, getMaxGuestUsers } from './license'; - export { getStatistics } from './getStatistics'; diff --git a/apps/meteor/ee/app/license/server/lib/getAppCount.ts b/apps/meteor/ee/app/license/server/lib/getAppCount.ts new file mode 100644 index 000000000000..a05813f596bb --- /dev/null +++ b/apps/meteor/ee/app/license/server/lib/getAppCount.ts @@ -0,0 +1,21 @@ +import { Apps } from '@rocket.chat/core-services'; +import type { LicenseAppSources } from '@rocket.chat/license'; + +import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; + +export async function getAppCount(source: LicenseAppSources): Promise { + if (!(await Apps.isInitialized())) { + return 0; + } + + const apps = await Apps.getApps({ enabled: true }); + + if (!apps || !Array.isArray(apps)) { + return 0; + } + + const storageItems = await Promise.all(apps.map((app) => Apps.getAppStorageItemById(app.id))); + const activeAppsFromSameSource = storageItems.filter((item) => item && getInstallationSourceFromAppStorageItem(item) === source); + + return activeAppsFromSameSource.length; +} diff --git a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts deleted file mode 100644 index b53b6512e2a1..000000000000 --- a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Apps } from '@rocket.chat/core-services'; - -import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import type { ILicense, LicenseAppSources } from '../../definition/ILicense'; - -export async function isUnderAppLimits(licenseAppsConfig: NonNullable, source: LicenseAppSources): Promise { - const apps = await Apps.getApps({ enabled: true }); - - if (!apps || !Array.isArray(apps)) { - return true; - } - - const storageItems = await Promise.all(apps.map((app) => Apps.getAppStorageItemById(app.id))); - const activeAppsFromSameSource = storageItems.filter((item) => item && getInstallationSourceFromAppStorageItem(item) === source); - - const configKey = `max${source.charAt(0).toUpperCase()}${source.slice(1)}Apps` as keyof typeof licenseAppsConfig; - const configLimit = licenseAppsConfig[configKey]; - - // If the workspace can install unlimited apps - // the config will be -1 - if (configLimit === -1) { - return true; - } - - return activeAppsFromSameSource.length < configLimit; -} diff --git a/apps/meteor/ee/app/license/server/license.internalService.ts b/apps/meteor/ee/app/license/server/license.internalService.ts index 047a67d323ff..9036a9b1848c 100644 --- a/apps/meteor/ee/app/license/server/license.internalService.ts +++ b/apps/meteor/ee/app/license/server/license.internalService.ts @@ -1,9 +1,9 @@ import type { ILicense } from '@rocket.chat/core-services'; import { api, ServiceClassInternal } from '@rocket.chat/core-services'; +import { License, type LicenseModule } from '@rocket.chat/license'; import { guestPermissions } from '../../authorization/lib/guestPermissions'; import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; -import { getModules, hasLicense, isEnterprise, onModule, onValidateLicenses } from './license'; export class LicenseService extends ServiceClassInternal implements ILicense { protected name = 'license'; @@ -11,8 +11,8 @@ export class LicenseService extends ServiceClassInternal implements ILicense { constructor() { super(); - onValidateLicenses((): void => { - if (!isEnterprise()) { + License.onValidateLicense((): void => { + if (!License.hasValidLicense()) { return; } @@ -20,13 +20,13 @@ export class LicenseService extends ServiceClassInternal implements ILicense { void resetEnterprisePermissions(); }); - onModule((licenseModule) => { + License.onModule((licenseModule) => { void api.broadcast('license.module', licenseModule); }); } async started(): Promise { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { return; } @@ -34,16 +34,16 @@ export class LicenseService extends ServiceClassInternal implements ILicense { await resetEnterprisePermissions(); } - hasLicense(feature: string): boolean { - return hasLicense(feature); + hasModule(feature: LicenseModule): boolean { + return License.hasModule(feature); } - isEnterprise(): boolean { - return isEnterprise(); + hasValidLicense(): boolean { + return License.hasValidLicense(); } getModules(): string[] { - return getModules(); + return License.getModules(); } getGuestPermissions(): string[] { diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts deleted file mode 100644 index fe0b22a0ee45..000000000000 --- a/apps/meteor/ee/app/license/server/license.ts +++ /dev/null @@ -1,467 +0,0 @@ -import { EventEmitter } from 'events'; - -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import { Apps } from '@rocket.chat/core-services'; -import { Users } from '@rocket.chat/models'; - -import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import type { ILicense } from '../definition/ILicense'; -import type { ILicenseTag } from '../definition/ILicenseTag'; -import type { BundleFeature } from './bundles'; -import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; -import decrypt from './decrypt'; -import { getTagColor } from './getTagColor'; -import { isUnderAppLimits } from './lib/isUnderAppLimits'; - -const EnterpriseLicenses = new EventEmitter(); - -interface IValidLicense { - valid?: boolean; - license: ILicense; -} - -let maxGuestUsers = 0; -let maxRoomsPerGuest = 0; -let maxActiveUsers = 0; - -class LicenseClass { - private url: string | null = null; - - private licenses: IValidLicense[] = []; - - private encryptedLicenses = new Set(); - - private tags = new Set(); - - private modules = new Set(); - - private appsConfig: NonNullable = { - maxPrivateApps: 3, - maxMarketplaceApps: 5, - }; - - private _validateExpiration(expiration: string): boolean { - return new Date() > new Date(expiration); - } - - private _validateURL(licenseURL: string, url: string): boolean { - licenseURL = licenseURL - .replace(/\./g, '\\.') // convert dots to literal - .replace(/\*/g, '.*'); // convert * to .* - const regex = new RegExp(`^${licenseURL}$`, 'i'); - - return !!regex.exec(url); - } - - private _setAppsConfig(license: ILicense): void { - // If the license is valid, no limit is going to be applied to apps installation for now - // This guarantees that upgraded workspaces won't be affected by the new limit right away - // and gives us time to propagate the new limit schema to all licenses - const { maxPrivateApps = -1, maxMarketplaceApps = -1 } = license.apps || {}; - - if (maxPrivateApps === -1 || maxPrivateApps > this.appsConfig.maxPrivateApps) { - this.appsConfig.maxPrivateApps = maxPrivateApps; - } - - if (maxMarketplaceApps === -1 || maxMarketplaceApps > this.appsConfig.maxMarketplaceApps) { - this.appsConfig.maxMarketplaceApps = maxMarketplaceApps; - } - } - - private _validModules(licenseModules: string[]): void { - licenseModules.forEach((licenseModule) => { - const modules = isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule]; - - modules.forEach((module) => { - this.modules.add(module); - EnterpriseLicenses.emit('module', { module, valid: true }); - EnterpriseLicenses.emit(`valid:${module}`); - }); - }); - } - - private _invalidModules(licenseModules: string[]): void { - licenseModules.forEach((licenseModule) => { - const modules = isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule]; - - modules.forEach((module) => { - EnterpriseLicenses.emit('module', { module, valid: false }); - EnterpriseLicenses.emit(`invalid:${module}`); - }); - }); - } - - private _addTags(license: ILicense): void { - // if no tag present, it means it is an old license, so try check for bundles and use them as tags - if (typeof license.tag === 'undefined') { - license.modules - .filter(isBundle) - .map(getBundleFromModule) - .forEach((tag) => tag && this._addTag({ name: tag, color: getTagColor(tag) })); - return; - } - - this._addTag(license.tag); - } - - private _addTag(tag: ILicenseTag): void { - // make sure to not add duplicated tag names - for (const addedTag of this.tags) { - if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { - return; - } - } - - this.tags.add(tag); - } - - addLicense(license: ILicense): void { - this.licenses.push({ - valid: undefined, - license, - }); - - this.validate(); - } - - lockLicense(encryptedLicense: string): void { - this.encryptedLicenses.add(encryptedLicense); - } - - isLicenseDuplicate(encryptedLicense: string): boolean { - if (this.encryptedLicenses.has(encryptedLicense)) { - return true; - } - - return false; - } - - hasModule(module: string): boolean { - return this.modules.has(module); - } - - hasAnyValidLicense(): boolean { - return this.licenses.some((item) => item.valid); - } - - getLicenses(): IValidLicense[] { - return this.licenses; - } - - getModules(): string[] { - return [...this.modules]; - } - - getTags(): ILicenseTag[] { - return [...this.tags]; - } - - getAppsConfig(): NonNullable { - return this.appsConfig; - } - - setURL(url: string): void { - this.url = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); - - this.validate(); - } - - validate(): void { - this.licenses = this.licenses.map((item) => { - const { license } = item; - - if (license.url) { - if (!this.url) { - return item; - } - if (!this._validateURL(license.url, this.url)) { - this.invalidate(item); - console.error(`#### License error: invalid url, licensed to ${license.url}, used on ${this.url}`); - this._invalidModules(license.modules); - return item; - } - } - - if (license.expiry && this._validateExpiration(license.expiry)) { - this.invalidate(item); - console.error(`#### License error: expired, valid until ${license.expiry}`); - this._invalidModules(license.modules); - return item; - } - - if (license.maxGuestUsers > maxGuestUsers) { - maxGuestUsers = license.maxGuestUsers; - } - - if (license.maxRoomsPerGuest > maxRoomsPerGuest) { - maxRoomsPerGuest = license.maxRoomsPerGuest; - } - - if (license.maxActiveUsers > maxActiveUsers) { - maxActiveUsers = license.maxActiveUsers; - } - - this._setAppsConfig(license); - - this._validModules(license.modules); - - this._addTags(license); - - console.log('#### License validated:', license.modules.join(', ')); - - item.valid = true; - return item; - }); - - EnterpriseLicenses.emit('validate'); - this.showLicenses(); - } - - invalidate(item: IValidLicense): void { - item.valid = false; - - EnterpriseLicenses.emit('invalidate'); - } - - async canAddNewUser(userCount = 1): Promise { - if (!maxActiveUsers) { - return true; - } - - return maxActiveUsers > (await Users.getActiveLocalUserCount()) + userCount; - } - - async canEnableApp(app: IAppStorageItem): Promise { - if (!(await Apps.isInitialized())) { - return false; - } - - // Migrated apps were installed before the validation was implemented - // so they're always allowed to be enabled - if (app.migrated) { - return true; - } - - return isUnderAppLimits(this.appsConfig, getInstallationSourceFromAppStorageItem(app)); - } - - showLicenses(): void { - if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') { - return; - } - - this.licenses - .filter((item) => item.valid) - .forEach((item) => { - const { license } = item; - - console.log('---- License enabled ----'); - console.log(' url ->', license.url); - console.log(' expiry ->', license.expiry); - console.log(' maxActiveUsers ->', license.maxActiveUsers); - console.log(' maxGuestUsers ->', license.maxGuestUsers); - console.log(' maxRoomsPerGuest ->', license.maxRoomsPerGuest); - console.log(' modules ->', license.modules.join(', ')); - console.log('-------------------------'); - }); - } -} - -const License = new LicenseClass(); - -export function addLicense(encryptedLicense: string): boolean { - if (!encryptedLicense || String(encryptedLicense).trim() === '' || License.isLicenseDuplicate(encryptedLicense)) { - return false; - } - - console.log('### New Enterprise License'); - - try { - const decrypted = decrypt(encryptedLicense); - if (!decrypted) { - return false; - } - - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - console.log('##### Raw license ->', decrypted); - } - - License.addLicense(JSON.parse(decrypted)); - License.lockLicense(encryptedLicense); - - return true; - } catch (e) { - console.error('##### Invalid license'); - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - console.error('##### Invalid raw license ->', encryptedLicense, e); - } - return false; - } -} - -export function validateFormat(encryptedLicense: string): boolean { - if (!encryptedLicense || String(encryptedLicense).trim() === '') { - return false; - } - - const decrypted = decrypt(encryptedLicense); - if (!decrypted) { - return false; - } - - return true; -} - -export function setURL(url: string): void { - License.setURL(url); -} - -export function hasLicense(feature: string): boolean { - return License.hasModule(feature); -} - -export function isEnterprise(): boolean { - return License.hasAnyValidLicense(); -} - -export function getMaxGuestUsers(): number { - return maxGuestUsers; -} - -export function getMaxRoomsPerGuest(): number { - return maxRoomsPerGuest; -} - -export function getMaxActiveUsers(): number { - return maxActiveUsers; -} - -export function getLicenses(): IValidLicense[] { - return License.getLicenses(); -} - -export function getModules(): string[] { - return License.getModules(); -} - -export function getTags(): ILicenseTag[] { - return License.getTags(); -} - -export function getAppsConfig(): NonNullable { - return License.getAppsConfig(); -} - -export async function canAddNewUser(userCount = 1): Promise { - return License.canAddNewUser(userCount); -} - -export async function canEnableApp(app: IAppStorageItem): Promise { - return License.canEnableApp(app); -} - -export function onLicense(feature: BundleFeature, cb: (...args: any[]) => void): void | Promise { - if (hasLicense(feature)) { - return cb(); - } - - EnterpriseLicenses.once(`valid:${feature}`, cb); -} - -function onValidFeature(feature: BundleFeature, cb: () => void): () => void { - EnterpriseLicenses.on(`valid:${feature}`, cb); - - if (hasLicense(feature)) { - cb(); - } - - return (): void => { - EnterpriseLicenses.off(`valid:${feature}`, cb); - }; -} - -function onInvalidFeature(feature: BundleFeature, cb: () => void): () => void { - EnterpriseLicenses.on(`invalid:${feature}`, cb); - - if (!hasLicense(feature)) { - cb(); - } - - return (): void => { - EnterpriseLicenses.off(`invalid:${feature}`, cb); - }; -} - -export function onToggledFeature( - feature: BundleFeature, - { - up, - down, - }: { - up?: () => Promise | void; - down?: () => Promise | void; - }, -): () => void { - let enabled = hasLicense(feature); - - const offValidFeature = onValidFeature(feature, () => { - if (!enabled) { - void up?.(); - enabled = true; - } - }); - - const offInvalidFeature = onInvalidFeature(feature, () => { - if (enabled) { - void down?.(); - enabled = false; - } - }); - - if (enabled) { - void up?.(); - } - - return (): void => { - offValidFeature(); - offInvalidFeature(); - }; -} - -export function onModule(cb: (...args: any[]) => void): void { - EnterpriseLicenses.on('module', cb); -} - -export function onValidateLicenses(cb: (...args: any[]) => void): void { - EnterpriseLicenses.on('validate', cb); -} - -export function onInvalidateLicense(cb: (...args: any[]) => void): void { - EnterpriseLicenses.on('invalidate', cb); -} - -export function flatModules(modulesAndBundles: string[]): string[] { - const bundles = modulesAndBundles.filter(isBundle); - const modules = modulesAndBundles.filter((x) => !isBundle(x)); - - const modulesFromBundles = bundles.map(getBundleModules).flat(); - - return modules.concat(modulesFromBundles); -} - -interface IOverrideClassProperties { - [key: string]: (...args: any[]) => any; -} - -type Class = { new (...args: any[]): any }; - -export async function overwriteClassOnLicense(license: BundleFeature, original: Class, overwrite: IOverrideClassProperties): Promise { - await onLicense(license, () => { - Object.entries(overwrite).forEach(([key, value]) => { - const originalFn = original.prototype[key]; - original.prototype[key] = function (...args: any[]): any { - return value.call(this, originalFn, ...args); - }; - }); - }); -} diff --git a/apps/meteor/ee/app/license/server/methods.ts b/apps/meteor/ee/app/license/server/methods.ts index 96978fad6d9a..39d14326a79a 100644 --- a/apps/meteor/ee/app/license/server/methods.ts +++ b/apps/meteor/ee/app/license/server/methods.ts @@ -1,10 +1,8 @@ +import { License, type ILicenseTag, type LicenseModule } from '@rocket.chat/license'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { ILicenseTag } from '../definition/ILicenseTag'; -import { getModules, getTags, hasLicense, isEnterprise } from './license'; - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -19,15 +17,15 @@ Meteor.methods({ 'license:hasLicense'(feature: string) { check(feature, String); - return hasLicense(feature); + return License.hasModule(feature as LicenseModule); }, 'license:getModules'() { - return getModules(); + return License.getModules(); }, 'license:getTags'() { - return getTags(); + return License.getTags(); }, 'license:isEnterprise'() { - return isEnterprise(); + return License.hasValidLicense(); }, }); diff --git a/apps/meteor/ee/app/license/server/settings.ts b/apps/meteor/ee/app/license/server/settings.ts index a5a07ba0200f..1bec7126ae85 100644 --- a/apps/meteor/ee/app/license/server/settings.ts +++ b/apps/meteor/ee/app/license/server/settings.ts @@ -1,8 +1,8 @@ +import { License } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { settings, settingsRegistry } from '../../../../app/settings/server'; -import { addLicense } from './license'; Meteor.startup(async () => { await settingsRegistry.addGroup('Enterprise', async function () { @@ -29,16 +29,24 @@ settings.watch('Enterprise_License', async (license) => { return; } - if (!addLicense(license)) { - await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); - return; + try { + if (!(await License.setLicense(license))) { + await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); + return; + } + } catch (_error) { + // do nothing } await Settings.updateValueById('Enterprise_License_Status', 'Valid'); }); if (process.env.ROCKETCHAT_LICENSE) { - addLicense(process.env.ROCKETCHAT_LICENSE); + try { + await License.setLicense(process.env.ROCKETCHAT_LICENSE); + } catch (_error) { + // do nothing + } Meteor.startup(async () => { if (settings.get('Enterprise_License')) { diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index 4a7a0776fc0d..d3523282d1e8 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,13 +1,28 @@ +import { License } from '@rocket.chat/license'; +import { Subscriptions, Users } from '@rocket.chat/models'; + import { settings } from '../../../../app/settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { addLicense, setURL } from './license'; +import { getAppCount } from './lib/getAppCount'; settings.watch('Site_Url', (value) => { if (value) { - setURL(value); + void License.setWorkspaceUrl(value); } }); -callbacks.add('workspaceLicenseChanged', (updatedLicense) => { - addLicense(updatedLicense); +callbacks.add('workspaceLicenseChanged', async (updatedLicense) => { + try { + await License.setLicense(updatedLicense); + } catch (_error) { + // Ignore + } }); + +License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); +License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); +License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); +License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); +License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); +// #TODO: Get real value +License.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts index 5839b717349d..a441e122ef99 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -1,10 +1,10 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { LivechatBusinessHours, LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; import moment from 'moment-timezone'; import { businessHourLogger } from '../../../../../app/livechat/server/lib/logger'; -import { isEnterprise } from '../../../license/server/license'; const getAllAgentIdsWithoutDepartment = async (): Promise => { // Fetch departments with agents excluding archived ones (disabled ones still can be tied to business hours) @@ -105,7 +105,7 @@ export const removeBusinessHourByAgentIds = async (agentIds: string[], businessH }; export const resetDefaultBusinessHourIfNeeded = async (): Promise => { - if (isEnterprise()) { + if (License.hasValidLicense()) { return; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts index 22379e27698d..6c4aac024ab0 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts @@ -44,7 +44,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior }, }); const businessHoursToOpen = await filterBusinessHoursThatMustBeOpened(activeBusinessHours); - bhLogger.debug({ + bhLogger.info({ msg: 'Starting Multiple Business Hours', totalBusinessHoursToOpen: businessHoursToOpen.length, top10BusinessHoursToOpen: businessHoursToOpen.slice(0, 10), @@ -153,7 +153,6 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior } async onRemoveDepartment(options: { department: ILivechatDepartment; agentsIds: string[] }): Promise { - bhLogger.debug(`onRemoveDepartment: department ${options.department._id} removed`); const { department, agentsIds } = options; if (!department || !agentsIds?.length) { return options; @@ -163,10 +162,6 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior async onDepartmentDisabled(department: ILivechatDepartment): Promise { if (!department.businessHourId) { - bhLogger.debug({ - msg: 'onDepartmentDisabled: department has no business hour', - departmentId: department._id, - }); return; } @@ -186,7 +181,6 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior // cleanup user's cache for default business hour and this business hour const defaultBH = await this.BusinessHourRepository.findOneDefaultBusinessHour(); if (!defaultBH) { - bhLogger.error('onDepartmentDisabled: default business hour not found'); throw new Error('Default business hour not found'); } await this.UsersRepository.closeAgentsBusinessHoursByBusinessHourIds([businessHour._id, defaultBH._id]); @@ -203,37 +197,22 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId); if (!businessHour) { - bhLogger.error({ - msg: 'onDepartmentDisabled: business hour not found', - businessHourId: department.businessHourId, - }); - throw new Error(`Business hour ${department.businessHourId} not found`); } } // start default business hour and this BH if needed if (!settings.get('Livechat_enable_business_hours')) { - bhLogger.debug(`onDepartmentDisabled: business hours are disabled. skipping`); return; } const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([businessHour, defaultBH]); for await (const bh of businessHourToOpen) { - bhLogger.debug({ - msg: 'onDepartmentDisabled: opening business hour', - businessHourId: bh._id, - }); await openBusinessHour(bh, false); } await Users.updateLivechatStatusBasedOnBusinessHours(); await businessHourManager.restartCronJobsIfNecessary(); - - bhLogger.debug({ - msg: 'onDepartmentDisabled: successfully processed department disabled event', - departmentId: department._id, - }); } async onDepartmentArchived(department: Pick): Promise { @@ -253,11 +232,6 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior } async onNewAgentCreated(agentId: string): Promise { - bhLogger.debug({ - msg: 'Executing onNewAgentCreated for agent', - agentId, - }); - await this.applyAnyOpenBusinessHourToAgent(agentId); await Users.updateLivechatStatusBasedOnBusinessHours([agentId]); @@ -293,11 +267,6 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT); if (isDefaultBHActive?._id) { await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId); - - bhLogger.debug({ - msg: 'Business hour status check passed for agent. Found default business hour to be active', - agentId, - }); return; } @@ -330,11 +299,6 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT); if (isDefaultBHActive?._id) { await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId); - - bhLogger.debug({ - msg: 'Business hour status check passed for agentId. Found default business hour to be active and agent has no departments with non-default business hours', - agentId, - }); return; } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.ts index 8babfec041c7..903fb8fd6928 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.ts @@ -13,7 +13,6 @@ callbacks.add( projection: { departmentAncestors: 1 }, }); if (!room) { - cbLogger.debug('Skipping callback. No room found'); return options; } await LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id); @@ -22,14 +21,12 @@ callbacks.add( projection: { ancestors: 1 }, }); if (!department) { - cbLogger.debug('Skipping callback. No department found'); return options; } const { departmentAncestors } = room; const { ancestors } = department; if (!ancestors && !departmentAncestors) { - cbLogger.debug('Skipping callback. No ancestors found for department'); return options; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts index d8f5878f01bb..cb6993b38aec 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts @@ -9,12 +9,10 @@ let timer = 0; const scheduleInquiry = async (inquiry: any): Promise => { if (!inquiry?._id) { - cbLogger.debug('Skipping callback. No inquiry provided'); return; } if (!inquiry?._updatedAt || !inquiry?._createdAt) { - cbLogger.debug('Skipping callback. Inquiry doesnt have timestamps'); return; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts index 99e69acd8ffe..11a9593b0c37 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts @@ -11,12 +11,10 @@ let autoCloseOnHoldChatTimeout = 0; const handleAfterOnHold = async (room: Pick): Promise => { const { _id: rid } = room; if (!rid) { - cbLogger.debug('Skipping callback. No room provided'); return; } if (!autoCloseOnHoldChatTimeout || autoCloseOnHoldChatTimeout <= 0) { - cbLogger.debug('Skipping callback. Autoclose on hold disabled by setting'); return; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts index c631656a2a07..0263efa5fe39 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts @@ -8,7 +8,6 @@ type IRoom = Pick; const handleAfterOnHoldChatResumed = async (room: IRoom): Promise => { if (!room?._id) { - cbLogger.debug('Skipping callback. No room provided'); return room; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts index be732be66297..26e176b03eb5 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts @@ -5,7 +5,6 @@ import { callbacks } from '../../../../../lib/callbacks'; import { cbLogger } from '../lib/logger'; const afterRemoveDepartment = async (options: { department: ILivechatDepartmentRecord; agentsId: ILivechatAgent['_id'][] }) => { - cbLogger.debug(`Performing post-department-removal actions in EE: ${options?.department?._id}. Removing department from forward list`); if (!options?.department) { cbLogger.warn('No department found in options', options); return options; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts index 1035a2f03286..cecf6b5e7f4a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts @@ -3,7 +3,6 @@ import { LivechatRooms } from '@rocket.chat/models'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; settings.watch('Livechat_abandoned_rooms_action', (value) => { if (!value || value === 'none') { @@ -14,7 +13,6 @@ settings.watch('Livechat_abandoned_rooms_action', (value) => { 'livechat:afterReturnRoomAsInquiry', ({ room }: { room: IOmnichannelRoom }) => { if (!room?._id || !room?.omnichannel?.predictedVisitorAbandonmentAt) { - cbLogger.debug('Skipping callback. No room or no visitor abandonment info'); return; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.ts index ee83facae6d4..c5e55e030b65 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.ts @@ -7,12 +7,10 @@ callbacks.add( 'livechat.afterTakeInquiry', async (inquiry) => { if (!settings.get('Livechat_waiting_queue')) { - cbLogger.debug('Skipping callback. Waiting queue disabled by setting'); return inquiry; } if (!inquiry) { - cbLogger.debug('Skipping callback. No inquiry provided'); return null; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts index ef3261b83482..3c96cad39b72 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts @@ -21,7 +21,6 @@ callbacks.add( 'livechat.applyDepartmentRestrictions', async (originalQuery: FilterOperators = {}, { userId }: { userId?: string | null } = { userId: null }) => { if (!userId || !(await hasRoleAsync(userId, 'livechat-monitor'))) { - cbLogger.debug('Skipping callback. No user id provided or user is not a monitor'); return originalQuery; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts index bde3b6d9e31a..1a18b92dc94d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts @@ -3,7 +3,6 @@ import { LivechatDepartment } from '@rocket.chat/models'; import type { FilterOperators } from 'mongodb'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; import { getUnitsFromUser } from '../lib/units'; export const restrictQuery = async (originalQuery: FilterOperators = {}) => { @@ -27,7 +26,6 @@ export const restrictQuery = async (originalQuery: FilterOperators = {}) => { - cbLogger.debug('Applying room query restrictions'); return restrictQuery(originalQuery); }, callbacks.priority.HIGH, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts index 4dd7a032e717..6d0c05fd39ed 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts @@ -3,7 +3,6 @@ import { LivechatDepartment } from '@rocket.chat/models'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; callbacks.add( 'livechat.applySimultaneousChatRestrictions', @@ -16,7 +15,6 @@ callbacks.add( }) )?.maxNumberSimultaneousChat || 0; if (departmentLimit > 0) { - cbLogger.debug(`Applying department filters. Max chats per department ${departmentLimit}`); return { $match: { 'queueInfo.chats': { $gte: Number(departmentLimit) } } }; } } @@ -49,8 +47,6 @@ callbacks.add( : // dummy filter meaning: don't match anything { _id: '' }; - cbLogger.debug(`Applying agent & global filters. Max number of chats allowed to all agents by setting: ${maxChatsPerSetting}`); - return { $match: { $or: [agentFilter, globalFilter] } }; }, callbacks.priority.HIGH, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.ts index 2503411d46c4..5a16b7014bd6 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.ts @@ -3,19 +3,16 @@ import { LivechatDepartment } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; callbacks.add( 'livechat.beforeForwardRoomToDepartment', async (options) => { const { room, transferData } = options; if (!room || !transferData) { - cbLogger.debug('Skipping callback. No room provided'); return options; } const { departmentId } = room; if (!departmentId) { - cbLogger.debug('Skipping callback. No department provided'); return options; } const { department: departmentToTransfer } = transferData; @@ -23,7 +20,6 @@ callbacks.add( projection: { departmentsAllowedToForward: 1 }, }); if (!currentDepartment) { - cbLogger.debug('Skipping callback. Current department does not exists'); return options; } const { departmentsAllowedToForward } = currentDepartment; @@ -31,7 +27,6 @@ callbacks.add( !departmentsAllowedToForward?.length || (Array.isArray(departmentsAllowedToForward) && departmentsAllowedToForward.includes(departmentToTransfer._id)); if (isAllowedToTransfer) { - cbLogger.debug(`Callback success. Room ${room._id} can be forwarded to department ${departmentToTransfer._id}`); return options; } throw new Meteor.Error('error-forwarding-department-target-not-allowed', 'The forwarding to the target department is not allowed.'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts index 21c38793734c..e6d68df1aa96 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts @@ -3,7 +3,6 @@ import { LivechatPriority, OmnichannelServiceLevelAgreements } from '@rocket.cha import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; type Props = { sla?: string; @@ -14,7 +13,6 @@ type Props = { const beforeNewInquiry = async (extraData: Props) => { const { sla: slaSearchTerm, priority: prioritySearchTerm, ...props } = extraData; if (!slaSearchTerm && !prioritySearchTerm) { - cbLogger.debug('Skipping callback. No sla or priority provided'); return extraData; } @@ -54,7 +52,6 @@ const beforeNewInquiry = async (extraData: Props) => { changes.priorityId = priority._id; changes.priorityWeight = priority.sortItem; } - cbLogger.debug('Callback success. Queue timing properties added to inquiry', changes); return { ...props, ...changes }; }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts index 6d2071406260..69c3914cb4d8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts @@ -16,7 +16,6 @@ callbacks.add( async (inquiry, agent) => { // check here if department has fallback before queueing if (inquiry?.department && !(await online(inquiry.department, true, true))) { - cbLogger.debug('No agents online on selected department. Inquiry will use fallback department'); const department = await LivechatDepartment.findOneById>( inquiry.department, { @@ -25,11 +24,10 @@ callbacks.add( ); if (!department) { - cbLogger.debug('No department found. Skipping'); return inquiry; } if (department.fallbackForwardDepartment) { - cbLogger.debug( + cbLogger.info( `Inquiry ${inquiry._id} will be moved from department ${department._id} to fallback department ${department.fallbackForwardDepartment}`, ); // update visitor @@ -41,31 +39,24 @@ callbacks.add( inquiry = (await LivechatInquiry.setDepartmentByInquiryId(inquiry._id, department.fallbackForwardDepartment)) ?? inquiry; // update room await LivechatRooms.setDepartmentByRoomId(inquiry.rid, department.fallbackForwardDepartment); - cbLogger.debug(`Inquiry ${inquiry._id} moved. Continue normal queue process`); - } else { - cbLogger.debug('No fallback department configured. Skipping'); } } if (!settings.get('Livechat_waiting_queue')) { - cbLogger.debug('Skipping callback. Waiting queue disabled by setting'); return inquiry; } if (!inquiry) { - cbLogger.debug('Skipping callback. No inquiry provided'); return inquiry; } const { _id, status, department } = inquiry; if (status !== 'ready') { - cbLogger.debug(`Skipping callback. Inquiry ${_id} is not ready`); return inquiry; } if (agent && (await allowAgentSkipQueue(agent))) { - cbLogger.debug(`Skipping callback. Agent ${agent.agentId} can skip queue`); return inquiry; } @@ -79,7 +70,6 @@ callbacks.add( }); if (inq) { await dispatchInquiryPosition(inq); - cbLogger.debug(`Callback success. Inquiry ${_id} position has been notified`); } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts index 7f566260cf04..55adb05cbd1c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts @@ -27,23 +27,20 @@ const validateMaxChats = async ({ }; }) => { if (!inquiry?._id || !agent?.agentId) { - cbLogger.debug('Callback with error. No inquiry or agent provided'); throw new Error('No inquiry or agent provided'); } const { agentId } = agent; if (!(await Livechat.checkOnlineAgents(undefined, agent))) { - cbLogger.debug('Callback with error. provided agent is not online'); throw new Error('Provided agent is not online'); } if (!settings.get('Livechat_waiting_queue')) { - cbLogger.debug('Skipping callback. Disabled by setting'); return agent; } if (await allowAgentSkipQueue(agent)) { - cbLogger.debug(`Callback success. Agent ${agent.agentId} can skip queue`); + cbLogger.info(`Chat can be taken by Agent ${agentId}: agent can skip queue`); return agent; } @@ -55,25 +52,23 @@ const validateMaxChats = async ({ }); if (maxNumberSimultaneousChat === 0) { - cbLogger.debug(`Callback success. Agent ${agentId} max number simultaneous chats on range`); + cbLogger.debug(`Chat can be taken by Agent ${agentId}: max number simultaneous chats on range`); return agent; } const user = await Users.getAgentAndAmountOngoingChats(agentId); if (!user) { - cbLogger.debug('Callback with error. No valid agent found'); throw new Error('No valid agent found'); } const { queueInfo: { chats = 0 } = {} } = user; const maxChats = typeof maxNumberSimultaneousChat === 'number' ? maxNumberSimultaneousChat : parseInt(maxNumberSimultaneousChat, 10); if (maxChats <= chats) { - cbLogger.debug('Callback with error. Agent reached max amount of simultaneous chats'); await callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry); throw new Error('error-max-number-simultaneous-chats-reached'); } - cbLogger.debug(`Callback success. Agent ${agentId} can take inquiry ${inquiry._id}`); + cbLogger.debug(`Agent ${agentId} can take inquiry ${inquiry._id}`); return agent; }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts index f11d3b9514d6..d27f317d3b18 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts @@ -2,7 +2,6 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; const handleOnAgentAssignmentFailed = async ( room: IOmnichannelRoom, @@ -18,25 +17,21 @@ const handleOnAgentAssignmentFailed = async ( }, ) => { if (!inquiry || !room) { - cbLogger.debug('Skipping callback. No inquiry or room provided'); return; } if (!settings.get('Livechat_waiting_queue')) { - cbLogger.debug('Skipping callback. Queue disabled by setting'); return; } const { forwardingToDepartment: { oldDepartmentId } = {}, forwardingToDepartment } = options; if (!forwardingToDepartment) { - cbLogger.debug('Skipping callback. Room not being forwarded to department'); return; } const { department: newDepartmentId } = inquiry; if (!newDepartmentId || !oldDepartmentId || newDepartmentId === oldDepartmentId) { - cbLogger.debug('Skipping callback. New and old departments are the same'); return; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts index e68148ee825d..4e76f396617b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts @@ -1,7 +1,6 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; -import { callbackLogger } from '../../../../../app/livechat/server/lib/logger'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; @@ -17,22 +16,17 @@ const onCloseLivechat = async (params: LivechatCloseCallbackParams) => { room: { _id: roomId }, } = params; - callbackLogger.debug(`[onCloseLivechat] clearing onHold related data for room ${roomId}`); - await Promise.all([ LivechatRooms.unsetOnHoldByRoomId(roomId), Subscriptions.unsetOnHoldByRoomId(roomId), AutoCloseOnHoldScheduler.unscheduleRoom(roomId), ]); - callbackLogger.debug(`[onCloseLivechat] clearing onHold related data for room ${roomId} completed`); - if (!settings.get('Livechat_waiting_queue')) { return params; } const { departmentId } = room || {}; - callbackLogger.debug(`[onCloseLivechat] dispatching waiting queue status for department ${departmentId}`); debouncedDispatchWaitingQueueStatus(departmentId); return params; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.ts index 2d4dc52f9a32..ef252c820ee4 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.ts @@ -1,26 +1,22 @@ import { LivechatDepartment } from '@rocket.chat/models'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; callbacks.add( 'livechat.onLoadForwardDepartmentRestrictions', async (options) => { const { departmentId } = options; if (!departmentId) { - cbLogger.debug('Skipping callback. No departmentId provided'); return options; } const department = await LivechatDepartment.findOneById(departmentId, { projection: { departmentsAllowedToForward: 1 }, }); if (!department) { - cbLogger.debug('Skipping callback. Invalid department provided'); return options; } - const { departmentsAllowedToForward, _id } = department; + const { departmentsAllowedToForward } = department; if (!departmentsAllowedToForward) { - cbLogger.debug(`Skipping callback. Department ${_id} doesnt allow forwarding to other departments`); return options; } return Object.assign({ restrictions: { _id: { $in: departmentsAllowedToForward } } }, options); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onSaveVisitorInfo.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onSaveVisitorInfo.ts index 2e1b879458f8..9b3f79f6ce3f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onSaveVisitorInfo.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onSaveVisitorInfo.ts @@ -4,7 +4,6 @@ import { OmnichannelServiceLevelAgreements } from '@rocket.chat/models'; import { callbacks } from '../../../../../lib/callbacks'; import { removePriorityFromRoom, updateRoomPriority } from '../api/lib/priorities'; import { removeRoomSLA, updateRoomSLA } from '../api/lib/sla'; -import { cbLogger } from '../lib/logger'; const updateSLA = async (room: IOmnichannelRoom, user: Required>, slaId?: string) => { if (!slaId) { @@ -37,19 +36,13 @@ callbacks.add( const { slaId: newSlaId, priorityId: newPriorityId } = room; if (oldSlaId === newSlaId && oldPriorityId === newPriorityId) { - cbLogger.debug('No changes in SLA or Priority'); return room; } if (oldSlaId === newSlaId && oldPriorityId !== newPriorityId) { - cbLogger.debug(`Updating Priority for room ${room._id}, from ${oldPriorityId} to ${newPriorityId}`); await updatePriority(room, user, newPriorityId); } else if (oldSlaId !== newSlaId && oldPriorityId === newPriorityId) { - cbLogger.debug(`Updating SLA for room ${room._id}, from ${oldSlaId} to ${newSlaId}`); await updateSLA(room, user, newSlaId); } else { - cbLogger.debug( - `Updating SLA and Priority for room ${room._id}, from ${oldSlaId} to ${newSlaId} and from ${oldPriorityId} to ${newPriorityId}`, - ); await Promise.all([updateSLA(room, user, newSlaId), updatePriority(room, user, newPriorityId)]); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onTransferFailure.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onTransferFailure.ts index bd33cfe12bbd..a667d4524926 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onTransferFailure.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onTransferFailure.ts @@ -21,10 +21,8 @@ const onTransferFailure = async ( return false; } - cbLogger.debug(`Attempting to transfer room ${room._id} using fallback departments`); const { departmentId } = transferData; if (!departmentId) { - cbLogger.debug(`No departmentId found in transferData`); return false; } @@ -39,14 +37,12 @@ const onTransferFailure = async ( return false; } - cbLogger.debug(`Fallback department ${department.fallbackForwardDepartment} found for department ${department._id}. Redirecting`); // TODO: find enabled not archived here const fallbackDepartment = await LivechatDepartment.findOneById(department.fallbackForwardDepartment, { projection: { name: 1, _id: 1 }, }); if (!fallbackDepartment) { - cbLogger.debug(`Fallback department ${department.fallbackForwardDepartment} not found`); return false; } @@ -80,6 +76,7 @@ const onTransferFailure = async ( ); } + cbLogger.info(`Fallback department ${department.fallbackForwardDepartment} found for department ${department._id}. Chat transfered`); return forwardSuccess; }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts index 4a16316cb93e..4039754a609c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts @@ -18,27 +18,23 @@ let autoTransferTimeout = 0; const handleAfterTakeInquiryCallback = async (inquiry: any = {}): Promise => { const { rid } = inquiry; if (!rid?.trim()) { - cbLogger.debug('Skipping callback. Invalid room id'); return; } if (!autoTransferTimeout || autoTransferTimeout <= 0) { - cbLogger.debug('Skipping callback. No auto transfer timeout or invalid value from setting'); return inquiry; } const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, autoTransferredAt: 1, autoTransferOngoing: 1 } }); if (!room) { - cbLogger.debug(`Skipping callback. Room ${rid} not found`); return inquiry; } if (room.autoTransferredAt || room.autoTransferOngoing) { - cbLogger.debug(`Skipping callback. Room ${room._id} already being transfered or not found`); return inquiry; } - cbLogger.debug(`Callback success. Room ${room._id} will be scheduled to be auto transfered after ${autoTransferTimeout} seconds`); + cbLogger.info(`Room ${room._id} will be scheduled to be auto transfered after ${autoTransferTimeout} seconds`); await AutoTransferChatScheduler.scheduleRoom(rid, autoTransferTimeout as number); return inquiry; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/index.ts index 13ebdd6a3521..13676e7cbedb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/index.ts @@ -1,3 +1,4 @@ +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import './methods/addMonitor'; @@ -25,11 +26,10 @@ import './hooks/onTransferFailure'; import './lib/routing/LoadBalancing'; import './lib/routing/LoadRotation'; import './lib/AutoCloseOnHoldScheduler'; -import { onLicense } from '../../license/server'; import './business-hour'; import { createDefaultPriorities } from './priorities'; -await onLicense('livechat-enterprise', async () => { +await License.onLicense('livechat-enterprise', async () => { require('./api'); require('./hooks'); await import('./startup'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index 0efda85e25a4..c59c27282501 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -26,7 +26,6 @@ class AutoCloseOnHoldSchedulerClass { public async init(): Promise { if (this.running) { - this.logger.debug('Already running'); return; } @@ -38,7 +37,7 @@ class AutoCloseOnHoldSchedulerClass { await this.scheduler.start(); this.running = true; - this.logger.debug('Started'); + this.logger.info('Service started'); } public async scheduleRoom(roomId: string, timeout: number, comment: string): Promise { @@ -75,7 +74,6 @@ class AutoCloseOnHoldSchedulerClass { comment, }; - this.logger.debug(`Closing room ${roomId}`); await Livechat.closeRoom(payload); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index 7e52cc266c04..9d4590836ac9 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -28,7 +28,6 @@ class AutoTransferChatSchedulerClass { public async init(): Promise { if (this.running) { - this.logger.debug('Already running'); return; } @@ -40,7 +39,7 @@ class AutoTransferChatSchedulerClass { await this.scheduler.start(); this.running = true; - this.logger.debug('Started'); + this.logger.info('Service started'); } private async getSchedulerUser(): Promise { @@ -58,7 +57,6 @@ class AutoTransferChatSchedulerClass { this.scheduler.define(jobName, this.executeJob.bind(this)); await this.scheduler.schedule(when, jobName, { roomId }); await LivechatRooms.setAutoTransferOngoingById(roomId); - this.logger.debug(`Scheduled room ${roomId} to be transferred in ${timeout} seconds`); } public async unscheduleRoom(roomId: string): Promise { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts index af517312348d..08ea48910f02 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts @@ -17,7 +17,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { OmnichannelQueueInactivityMonitor } from './QueueInactivityMonitor'; import { updateInquiryQueueSla } from './SlaHelper'; import { memoizeDebounce } from './debounceByParams'; -import { logger, helperLogger } from './logger'; +import { logger } from './logger'; type QueueInfo = { message: { @@ -124,7 +124,6 @@ const dispatchWaitingQueueStatus = async (department?: string) => { return; } - helperLogger.debug(`Updating statuses for queue ${department || 'Public'}`); const queue = await LivechatInquiry.getCurrentSortedQueueAsync({ department, queueSortBy: getInquirySortMechanismSetting(), @@ -186,12 +185,10 @@ export const updatePredictedVisitorAbandonment = async () => { export const updateQueueInactivityTimeout = async () => { const queueTimeout = settings.get('Livechat_max_queue_wait_time'); if (queueTimeout <= 0) { - logger.debug('QueueInactivityTimer: Disabling scheduled closing'); await OmnichannelQueueInactivityMonitor.stop(); return; } - logger.debug('QueueInactivityTimer: Updating estimated inactivity time for queued items'); await LivechatInquiry.getQueuedInquiries({ projection: { _updatedAt: 1 } }).forEach((inq) => { const aggregatedDate = moment(inq._updatedAt).add(queueTimeout, 'minutes'); try { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts index 83a2963a54d8..ec228e420abd 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts @@ -1,4 +1,5 @@ import type { IOmnichannelBusinessUnit, IOmnichannelServiceLevelAgreements, LivechatDepartmentDTO } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users, LivechatDepartment as LivechatDepartmentRaw, @@ -14,7 +15,6 @@ import { updateDepartmentAgents } from '../../../../../app/livechat/server/lib/H import { callbacks } from '../../../../../lib/callbacks'; import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../../server/lib/roles/removeUserFromRoles'; -import { hasLicense } from '../../../license/server/license'; import { updateSLAInquiries } from './Helper'; import { removeSLAFromRooms } from './SlaHelper'; @@ -195,7 +195,7 @@ export const LivechatEnterprise = { const department = _id ? await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null; - if (!hasLicense('livechat-enterprise')) { + if (!License.hasModule('livechat-enterprise')) { const totalDepartments = await LivechatDepartmentRaw.countTotal(); if (!department && totalDepartments >= 1) { throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { @@ -279,6 +279,6 @@ export const LivechatEnterprise = { }, async isDepartmentCreationAvailable() { - return hasLicense('livechat-enterprise') || (await LivechatDepartmentRaw.countTotal()) === 0; + return License.hasModule('livechat-enterprise') || (await LivechatDepartmentRaw.countTotal()) === 0; }, }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts index d7ad14251d3f..df975a3690be 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts @@ -69,6 +69,7 @@ class OmnichannelQueueInactivityMonitorClass { } await this.scheduler.start(); + this.logger.info('Service started'); this.running = true; } @@ -108,21 +109,18 @@ class OmnichannelQueueInactivityMonitorClass { async closeRoom({ attrs: { data } }: any = {}): Promise { const { inquiryId } = data; const inquiry = await LivechatInquiryRaw.findOneById(inquiryId); - this.logger.debug(`Processing inquiry item ${inquiryId}`); if (!inquiry || inquiry.status !== 'queued') { - this.logger.debug(`Skipping inquiry ${inquiryId}. Invalid or not queued anymore`); return; } const room = await LivechatRooms.findOneById(inquiry.rid); if (!room) { - this.logger.error(`Error: unable to find room ${inquiry.rid} for inquiry ${inquiryId} to close in queue inactivity monitor`); + this.logger.error(`Unable to find room ${inquiry.rid} for inquiry ${inquiryId} to close in queue inactivity monitor`); return; } await Promise.all([this.closeRoomAction(room), this.stopInquiry(inquiryId)]); - - this.logger.debug(`Running successful. Closed inquiry ${inquiry._id} because of inactivity`); + this.logger.info(`Closed room ${inquiry.rid} for inquiry ${inquiryId} due to inactivity`); } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts index 8947ecf62081..824296d1e673 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts @@ -33,7 +33,6 @@ export class VisitorInactivityMonitor { } async start() { - this.logger.debug('Starting'); await this._startMonitoring(); this._initializeMessageCache(); const cat = await Users.findOneById('rocket.cat'); @@ -50,18 +49,17 @@ export class VisitorInactivityMonitor { const everyMinute = '* * * * *'; await this.scheduler.add(this._name, everyMinute, async () => this.handleAbandonedRooms()); this._started = true; - this.logger.debug('Started'); + this.logger.info('Service started'); } async stop() { if (!this.isRunning()) { - this.logger.debug('Not running'); return; } await this.scheduler.remove(this._name); this._started = false; - this.logger.debug('Stopped'); + this.logger.info('Service stopped'); } isRunning() { @@ -73,9 +71,7 @@ export class VisitorInactivityMonitor { } async _getDepartmentAbandonedCustomMessage(departmentId: string) { - this.logger.debug(`Getting department abandoned custom message for department ${departmentId}`); if (this.messageCache.has(departmentId)) { - this.logger.debug(`Using cached department abandoned custom message for department ${departmentId}`); return this.messageCache.get(departmentId); } const department = await LivechatDepartment.findOneById>( @@ -83,16 +79,14 @@ export class VisitorInactivityMonitor { { projection: { _id: 1, abandonedRoomsCloseCustomMessage: 1 } }, ); if (!department) { - this.logger.debug(`Department ${departmentId} not found`); + this.logger.error(`Department ${departmentId} not found`); return; } - this.logger.debug(`Setting department abandoned custom message for department ${departmentId}`); this.messageCache.set(department._id, department.abandonedRoomsCloseCustomMessage); return department.abandonedRoomsCloseCustomMessage; } async closeRooms(room: IOmnichannelRoom) { - this.logger.debug(`Closing room ${room._id}`); let comment = await this.getDefaultAbandonedCustomMessage('close', room.v._id); if (room.departmentId) { comment = (await this._getDepartmentAbandonedCustomMessage(room.departmentId)) || comment; @@ -102,29 +96,24 @@ export class VisitorInactivityMonitor { room, user: this.user, }); - this.logger.debug(`Room ${room._id} closed`); + this.logger.info(`Room ${room._id} closed`); } async placeRoomOnHold(room: IOmnichannelRoom) { - this.logger.debug(`Placing room ${room._id} on hold`); - const comment = await this.getDefaultAbandonedCustomMessage('on-hold', room.v._id); const result = await Promise.allSettled([ OmnichannelEEService.placeRoomOnHold(room, comment, this.user), LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id), ]); - this.logger.debug(`Room ${room._id} placed on hold`); const rejected = result.filter(isPromiseRejectedResult).map((r) => r.reason); if (rejected.length) { this.logger.error({ msg: 'Error placing room on hold', error: rejected }); - throw new Error('Error placing room on hold. Please check logs for more details.'); } } async handleAbandonedRooms() { - this.logger.debug('Handling abandoned rooms'); const action = settings.get('Livechat_abandoned_rooms_action'); if (!action || action === 'none') { return; @@ -135,12 +124,12 @@ export class VisitorInactivityMonitor { await LivechatRooms.findAbandonedOpenRooms(new Date(), extraQuery).forEach((room) => { switch (action) { case 'close': { - this.logger.debug(`Closing room ${room._id}`); + this.logger.info(`Closing room ${room._id}`); promises.push(this.closeRooms(room)); break; } case 'on-hold': { - this.logger.debug(`Placing room ${room._id} on hold`); + this.logger.info(`Placing room ${room._id} on hold`); promises.push(this.placeRoomOnHold(room)); break; } @@ -153,7 +142,6 @@ export class VisitorInactivityMonitor { if (errors.length) { this.logger.error({ msg: `Error while removing priority from ${errors.length} rooms`, reason: errors[0] }); - this.logger.debug({ msg: 'Rejection results', errors }); } this._initializeMessageCache(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts index e9911ffe246c..3ef9633267c8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts @@ -54,8 +54,6 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE ]); await callbacks.run('livechat:afterOnHold', room); - - this.logger.debug(`Room ${room._id} set on hold successfully`); } async resumeRoomOnHold( @@ -105,8 +103,6 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE ]); await callbacks.run('livechat:afterOnHoldChatResumed', room); - - this.logger.debug(`Room ${room._id} resumed successfully`); } private async attemptToAssignRoomToServingAgentElseQueueIt({ @@ -134,7 +130,7 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE return; } catch (e) { - this.logger.debug(`Agent ${servingAgent._id} is not available to take the inquiry ${inquiry._id}`, e); + this.logger.error(`Agent ${servingAgent._id} is not available to take the inquiry ${inquiry._id}`, e); if (clientAction) { // if the action was triggered by the client, we should throw the error // so the client can handle it and show the error message to the user @@ -142,20 +138,15 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE } } - this.logger.debug(`Attempting to queue inquiry ${inquiry._id}`); - await this.removeCurrentAgentFromRoom({ room, inquiry }); const { _id: inquiryId } = inquiry; const newInquiry = await LivechatInquiry.findOneById(inquiryId); if (!newInquiry) { - this.logger.error(`No inquiry found for id ${inquiryId}`); throw new Error('error-invalid-inquiry'); } await queueInquiry(newInquiry); - - this.logger.debug('Room queued successfully'); } private async removeCurrentAgentFromRoom({ @@ -178,7 +169,5 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE ]); await dispatchAgentDelegated(roomId); - - this.logger.debug(`Current agent removed from room ${room._id} successfully`); } } diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.ts b/apps/meteor/ee/app/message-read-receipt/server/index.ts index a7682e4165f0..bb405c0eaffd 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/index.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/index.ts @@ -1,5 +1,5 @@ -import { onLicense } from '../../license/server'; +import { License } from '@rocket.chat/license'; -await onLicense('message-read-receipt', async () => { +await License.onLicense('message-read-receipt', async () => { await import('./hooks'); }); diff --git a/apps/meteor/ee/app/settings/server/settings.ts b/apps/meteor/ee/app/settings/server/settings.ts index afb5a4378ec8..76ce7c15155a 100644 --- a/apps/meteor/ee/app/settings/server/settings.ts +++ b/apps/meteor/ee/app/settings/server/settings.ts @@ -1,17 +1,17 @@ import type { ISetting, SettingValue } from '@rocket.chat/core-typings'; +import { License, type LicenseModule } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { settings, SettingsEvents } from '../../../../app/settings/server'; import { use } from '../../../../app/settings/server/Middleware'; -import { isEnterprise, hasLicense, onValidateLicenses } from '../../license/server/license'; export function changeSettingValue(record: ISetting): SettingValue { if (!record.enterprise) { return record.value; } - if (!isEnterprise()) { + if (!License.hasValidLicense()) { return record.invalidValue; } @@ -20,7 +20,7 @@ export function changeSettingValue(record: ISetting): SettingValue { } for (const moduleName of record.modules) { - if (!hasLicense(moduleName)) { + if (!License.hasModule(moduleName as LicenseModule)) { return record.invalidValue; } } @@ -58,5 +58,5 @@ async function updateSettings(): Promise { Meteor.startup(async () => { await updateSettings(); - onValidateLicenses(updateSettings); + License.onValidateLicense(updateSettings); }); diff --git a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts index a28e459e57fb..f5524ee026dc 100644 --- a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts +++ b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts @@ -1,11 +1,11 @@ import type { ILivechatAgent, ILivechatVisitor, IVoipRoomClosingInfo, IUser, IVoipRoom } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import type { IOmniRoomClosingMessage } from '../../../../../server/services/omnichannel-voip/internalTypes'; import { OmnichannelVoipService } from '../../../../../server/services/omnichannel-voip/service'; -import { overwriteClassOnLicense } from '../../../license/server'; import { calculateOnHoldTimeForRoom } from '../lib/calculateOnHoldTimeForRoom'; -await overwriteClassOnLicense('voip-enterprise', OmnichannelVoipService, { +await License.overwriteClassOnLicense('voip-enterprise', OmnichannelVoipService, { async getRoomClosingData( _originalFn: ( closer: ILivechatVisitor | ILivechatAgent, diff --git a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts index a1492d39a013..c7d76b093c3b 100644 --- a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts +++ b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts @@ -1,9 +1,8 @@ +import type { LicenseModule } from '@rocket.chat/license'; import { useMethod, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { BundleFeature } from '../../app/license/server/bundles'; - -export const useHasLicenseModule = (licenseName: BundleFeature): 'loading' | boolean => { +export const useHasLicenseModule = (licenseName: LicenseModule): 'loading' | boolean => { const method = useMethod('license:getModules'); const uid = useUserId(); diff --git a/apps/meteor/ee/client/lib/onToggledFeature.ts b/apps/meteor/ee/client/lib/onToggledFeature.ts index 86ab08723745..ae2e4ad9f4a8 100644 --- a/apps/meteor/ee/client/lib/onToggledFeature.ts +++ b/apps/meteor/ee/client/lib/onToggledFeature.ts @@ -1,11 +1,11 @@ +import type { LicenseModule } from '@rocket.chat/license'; import { QueryObserver } from '@tanstack/react-query'; import { queryClient } from '../../../client/lib/queryClient'; -import type { BundleFeature } from '../../app/license/server/bundles'; import { fetchFeatures } from './fetchFeatures'; export const onToggledFeature = ( - feature: BundleFeature, + feature: LicenseModule, { up, down, diff --git a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts index eb029d91f537..b4a45b49dda5 100644 --- a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts +++ b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts @@ -8,6 +8,7 @@ export type SeatCapProps = { }; export const useSeatsCap = (): SeatCapProps | undefined => { + // #TODO: Stop using this endpoint const fetch = useEndpoint('GET', '/v1/licenses.maxActiveUsers'); const result = useQuery(['/v1/licenses.maxActiveUsers'], () => fetch()); diff --git a/apps/meteor/ee/server/api/api.ts b/apps/meteor/ee/server/api/api.ts index f152a94ebcbf..ee2049bb70ae 100644 --- a/apps/meteor/ee/server/api/api.ts +++ b/apps/meteor/ee/server/api/api.ts @@ -1,7 +1,8 @@ +import { License } from '@rocket.chat/license'; + import { API } from '../../../app/api/server/api'; import type { NonEnterpriseTwoFactorOptions, Options } from '../../../app/api/server/definition'; import { use } from '../../../app/settings/server/Middleware'; -import { isEnterprise } from '../../app/license/server/license'; // Overwrites two factor method to enforce 2FA check for enterprise APIs when // no license was provided to prevent abuse on enterprise APIs. @@ -10,7 +11,7 @@ const isNonEnterpriseTwoFactorOptions = (options?: Options): options is NonEnter !!options && 'forceTwoFactorAuthenticationForNonEnterprise' in options && Boolean(options.forceTwoFactorAuthenticationForNonEnterprise); API.v1.processTwoFactor = use(API.v1.processTwoFactor, ([params, ...context], next) => { - if (isNonEnterpriseTwoFactorOptions(params.options) && !isEnterprise()) { + if (isNonEnterpriseTwoFactorOptions(params.options) && !License.hasValidLicense()) { const options: NonEnterpriseTwoFactorOptions = { ...params.options, twoFactorOptions: { diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index 5d21b20f2038..2c8f8c5ca605 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -1,8 +1,8 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../app/api/server/api'; -import { hasLicense } from '../../app/license/server/license'; type GetMessageReadReceiptsProps = { messageId: IMessage['_id']; @@ -24,7 +24,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - if (!hasLicense('message-read-receipt')) { + if (!License.hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index ab8d72164a97..cfd657a1f0e9 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,17 +1,9 @@ +import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; -import type { ILicense } from '../../app/license/definition/ILicense'; -import { getLicenses, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; - -function licenseTransform(license: ILicense): ILicense { - return { - ...license, - modules: flatModules(license.modules), - }; -} API.v1.addRoute( 'licenses.get', @@ -22,9 +14,8 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const licenses = getLicenses() - .filter(({ valid }) => valid) - .map(({ license }) => licenseTransform(license)); + const license = License.getUnmodifiedLicenseAndModules(); + const licenses = license ? [license] : []; return API.v1.success({ licenses }); }, @@ -45,7 +36,7 @@ API.v1.addRoute( } const { license } = this.bodyParams; - if (!validateFormat(license)) { + if (!(await License.validateFormat(license))) { return API.v1.failure('Invalid license'); } @@ -61,7 +52,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const maxActiveUsers = getMaxActiveUsers() || null; + const maxActiveUsers = License.getMaxActiveUsers() || null; const activeUsers = await Users.getActiveLocalUserCount(); return API.v1.success({ maxActiveUsers, activeUsers }); @@ -74,7 +65,7 @@ API.v1.addRoute( { authOrAnonRequired: true }, { get() { - const isEnterpriseEdtion = isEnterprise(); + const isEnterpriseEdtion = License.hasValidLicense(); return API.v1.success({ isEnterprise: isEnterpriseEdtion }); }, }, diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index 712e7583b709..c10c32c3ee1a 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -1,11 +1,11 @@ import type { IRole } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Roles } from '@rocket.chat/models'; import Ajv from 'ajv'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; import { settings } from '../../../app/settings/server/index'; -import { isEnterprise } from '../../app/license/server'; import { insertRoleAsync } from '../lib/roles/insertRole'; import { updateRole } from '../lib/roles/updateRole'; @@ -96,7 +96,7 @@ API.v1.addRoute( { authRequired: true }, { async post() { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } @@ -154,7 +154,7 @@ API.v1.addRoute( const role = await Roles.findOne(roleId); - if (!isEnterprise() && !role?.protected) { + if (!License.hasValidLicense() && !role?.protected) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/api/sessions.ts b/apps/meteor/ee/server/api/sessions.ts index 41c30aba401b..cdd454fd5bee 100644 --- a/apps/meteor/ee/server/api/sessions.ts +++ b/apps/meteor/ee/server/api/sessions.ts @@ -1,4 +1,5 @@ import type { IUser, ISession, DeviceManagementSession, DeviceManagementPopulatedSession } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users, Sessions } from '@rocket.chat/models'; import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -7,7 +8,6 @@ import Ajv from 'ajv'; import { API } from '../../../app/api/server/api'; import { getPaginationItems } from '../../../app/api/server/helpers/getPaginationItems'; import { Notifications } from '../../../app/notifications/server'; -import { hasLicense } from '../../app/license/server/license'; const ajv = new Ajv({ coerceTypes: true }); @@ -85,7 +85,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsPaginateProps }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -108,7 +108,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -127,7 +127,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async post() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -153,7 +153,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsPaginateProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -193,7 +193,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -212,7 +212,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['logout-device-management'] }, { async post() { - if (!hasLicense('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index 96247e704545..fc436b8229cf 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -1,9 +1,9 @@ import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import { License } from '@rocket.chat/license'; import { API } from '../../../../../app/api/server'; import type { SuccessResult } from '../../../../../app/api/server/definition'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import { getAppsConfig } from '../../../../app/license/server/license'; import type { AppsRestApi } from '../rest'; type AppsCountResult = { @@ -23,7 +23,7 @@ export const appsCountHandler = (apiManager: AppsRestApi) => const manager = apiManager._manager as AppManager; const apps = manager.get({ enabled: true }); - const { maxMarketplaceApps, maxPrivateApps } = getAppsConfig(); + const { maxMarketplaceApps, maxPrivateApps } = License.getAppsConfig(); return API.v1.success({ totalMarketplaceEnabled: apps.filter((app) => getInstallationSourceFromAppStorageItem(app.getStorageItem()) === 'marketplace') diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 1203d0d8c911..f356f3e45a18 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -3,6 +3,7 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; import type { IUser, IMessage } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; @@ -17,7 +18,7 @@ import { settings } from '../../../../app/settings/server'; import { Info } from '../../../../app/utils/rocketchat.info'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; -import { canEnableApp, isEnterprise } from '../../../app/license/server/license'; +import { canEnableApp } from '../../../app/license/server/canEnableApp'; import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest'; import { appEnableCheck } from '../marketplace/appEnableCheck'; import { notifyAppInstall } from '../marketplace/appInstall'; @@ -1149,7 +1150,7 @@ export class AppsRestApi { const storedApp = prl.getStorageItem(); const { installationSource, marketplaceInfo } = storedApp; - if (!isEnterprise() && installationSource === AppInstallationSource.MARKETPLACE) { + if (!License.hasValidLicense() && installationSource === AppInstallationSource.MARKETPLACE) { try { const baseUrl = orchestrator.getMarketplaceUrl() as string; const headers = getDefaultHeaders(); diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index c21508cbc626..9e4d6f00e7f0 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -19,7 +19,7 @@ import { } from '../../../app/apps/server/converters'; import { AppThreadsConverter } from '../../../app/apps/server/converters/threads'; import { settings, settingsRegistry } from '../../../app/settings/server'; -import { canEnableApp } from '../../app/license/server/license'; +import { canEnableApp } from '../../app/license/server/canEnableApp'; import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; diff --git a/apps/meteor/ee/server/configuration/ldap.ts b/apps/meteor/ee/server/configuration/ldap.ts index 40815e213b0c..5f1a84557d70 100644 --- a/apps/meteor/ee/server/configuration/ldap.ts +++ b/apps/meteor/ee/server/configuration/ldap.ts @@ -1,18 +1,18 @@ import type { IImportUser, ILDAPEntry, IUser } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../../app/settings/server'; import { callbacks } from '../../../lib/callbacks'; import type { LDAPConnection } from '../../../server/lib/ldap/Connection'; import { logger } from '../../../server/lib/ldap/Logger'; -import { onLicense } from '../../app/license/server'; import { LDAPEEManager } from '../lib/ldap/Manager'; import { LDAPEE } from '../sdk'; import { addSettings, ldapIntervalValuesToCronMap } from '../settings/ldap'; Meteor.startup(async () => { - await onLicense('ldap-enterprise', async () => { + await License.onLicense('ldap-enterprise', async () => { await addSettings(); // Configure background sync cronjob diff --git a/apps/meteor/ee/server/configuration/oauth.ts b/apps/meteor/ee/server/configuration/oauth.ts index 984670af6003..aa66a46caf69 100644 --- a/apps/meteor/ee/server/configuration/oauth.ts +++ b/apps/meteor/ee/server/configuration/oauth.ts @@ -1,11 +1,11 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { Roles } from '@rocket.chat/models'; import { capitalize } from '@rocket.chat/string-helpers'; import { settings } from '../../../app/settings/server'; import { callbacks } from '../../../lib/callbacks'; -import { onLicense } from '../../app/license/server'; import { OAuthEEManager } from '../lib/oauth/Manager'; interface IOAuthUserService { @@ -54,7 +54,7 @@ function getChannelsMap(channelsMap: string): Record | undefined { } } -await onLicense('oauth-enterprise', () => { +await License.onLicense('oauth-enterprise', () => { callbacks.add('afterProcessOAuthUser', async (auth: IOAuthUserService) => { auth.serviceName = capitalize(auth.serviceName); const settings = getOAuthSettings(auth.serviceName); diff --git a/apps/meteor/ee/server/configuration/outlookCalendar.ts b/apps/meteor/ee/server/configuration/outlookCalendar.ts index cf36ddeb0cab..67c8d7945030 100644 --- a/apps/meteor/ee/server/configuration/outlookCalendar.ts +++ b/apps/meteor/ee/server/configuration/outlookCalendar.ts @@ -1,11 +1,11 @@ import { Calendar } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/outlookCalendar'; Meteor.startup(() => - onLicense('outlook-calendar', async () => { + License.onLicense('outlook-calendar', async () => { addSettings(); await Calendar.setupNextNotification(); diff --git a/apps/meteor/ee/server/configuration/saml.ts b/apps/meteor/ee/server/configuration/saml.ts index 1e50fc7160b5..96dca07829c6 100644 --- a/apps/meteor/ee/server/configuration/saml.ts +++ b/apps/meteor/ee/server/configuration/saml.ts @@ -1,13 +1,13 @@ +import { License } from '@rocket.chat/license'; import { Roles, Users } from '@rocket.chat/models'; import type { ISAMLUser } from '../../../app/meteor-accounts-saml/server/definition/ISAMLUser'; import { SAMLUtils } from '../../../app/meteor-accounts-saml/server/lib/Utils'; import { settings } from '../../../app/settings/server'; import { ensureArray } from '../../../lib/utils/arrayUtils'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/saml'; -await onLicense('saml-enterprise', () => { +await License.onLicense('saml-enterprise', () => { SAMLUtils.events.on('mapUser', async ({ profile, userObject }: { profile: Record; userObject: ISAMLUser }) => { const roleAttributeName = settings.get('SAML_Custom_Default_role_attribute_name') as string; const roleAttributeSync = settings.get('SAML_Custom_Default_role_attribute_sync'); @@ -67,4 +67,4 @@ await onLicense('saml-enterprise', () => { }); // For setting creation we add the listener first because the event is emmited during startup -SAMLUtils.events.on('addSettings', (name: string): void | Promise => onLicense('saml-enterprise', () => addSettings(name))); +SAMLUtils.events.on('addSettings', (name: string): void | Promise => License.onLicense('saml-enterprise', () => addSettings(name))); diff --git a/apps/meteor/ee/server/configuration/videoConference.ts b/apps/meteor/ee/server/configuration/videoConference.ts index a9debed01b19..035110904840 100644 --- a/apps/meteor/ee/server/configuration/videoConference.ts +++ b/apps/meteor/ee/server/configuration/videoConference.ts @@ -1,16 +1,16 @@ import { VideoConf } from '@rocket.chat/core-services'; import type { IRoom, IUser, VideoConference } from '@rocket.chat/core-typings'; import { VideoConferenceStatus } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { videoConfTypes } from '../../../server/lib/videoConfTypes'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/video-conference'; Meteor.startup(async () => { - await onLicense('videoconference-enterprise', async () => { + await License.onLicense('videoconference-enterprise', async () => { await addSettings(); videoConfTypes.registerVideoConferenceType( diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 9b56239ad046..f5b385c9a805 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -1,5 +1,3 @@ -import './startup'; - import '../app/license/server/index'; import '../app/api-enterprise/server/index'; import '../app/authorization/server/index'; diff --git a/apps/meteor/ee/server/lib/EnterpriseCheck.ts b/apps/meteor/ee/server/lib/EnterpriseCheck.ts index 8bccfed59071..ca8cd1e25b10 100644 --- a/apps/meteor/ee/server/lib/EnterpriseCheck.ts +++ b/apps/meteor/ee/server/lib/EnterpriseCheck.ts @@ -41,7 +41,7 @@ export const EnterpriseCheck: ServiceSchema = { async started(): Promise { setInterval(async () => { try { - const hasLicense = await this.broker.call('license.hasLicense', ['scalability']); + const hasLicense = await this.broker.call('license.hasValidLicense', ['scalability']); if (hasLicense) { checkFails = 0; return; diff --git a/apps/meteor/ee/server/lib/syncUserRoles.ts b/apps/meteor/ee/server/lib/syncUserRoles.ts index e38f9de3c310..f3a380a8f228 100644 --- a/apps/meteor/ee/server/lib/syncUserRoles.ts +++ b/apps/meteor/ee/server/lib/syncUserRoles.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import type { IUser, IRole, AtLeast } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; import { addUserRolesAsync } from '../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../server/lib/roles/removeUserFromRoles'; -import { canAddNewUser } from '../../app/license/server/license'; type setUserRolesOptions = { // If specified, the function will not add nor remove any role that is not on this list. @@ -72,7 +72,7 @@ export async function syncUserRoles( } const wasGuest = existingRoles.length === 1 && existingRoles[0] === 'guest'; - if (wasGuest && !(await canAddNewUser())) { + if (wasGuest && (await License.shouldPreventAction('activeUsers'))) { throw new Error('error-license-user-limit-reached'); } diff --git a/apps/meteor/ee/server/local-services/instance/service.ts b/apps/meteor/ee/server/local-services/instance/service.ts index 0fc4fd33a9b1..5bb755ee347f 100644 --- a/apps/meteor/ee/server/local-services/instance/service.ts +++ b/apps/meteor/ee/server/local-services/instance/service.ts @@ -96,6 +96,12 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe events: { broadcast(ctx: any) { const { eventName, streamName, args } = ctx.params; + const { nodeID } = ctx; + + const fromLocalNode = nodeID === InstanceStatus.id(); + if (fromLocalNode) { + return; + } const instance = StreamerCentral.instances[streamName]; if (!instance) { @@ -137,7 +143,7 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe await InstanceStatus.registerInstance('rocket.chat', instance); - const hasLicense = await License.hasLicense('scalability'); + const hasLicense = await License.hasModule('scalability'); if (!hasLicense) { return; } diff --git a/apps/meteor/ee/server/methods/getReadReceipts.ts b/apps/meteor/ee/server/methods/getReadReceipts.ts index a30eec300c41..78fe8a4d967e 100644 --- a/apps/meteor/ee/server/methods/getReadReceipts.ts +++ b/apps/meteor/ee/server/methods/getReadReceipts.ts @@ -1,11 +1,11 @@ import type { ReadReceipt as ReadReceiptType, IMessage } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../app/authorization/server/functions/canAccessRoom'; -import { hasLicense } from '../../app/license/server/license'; import { ReadReceipt } from '../lib/message-read-receipt/ReadReceipt'; declare module '@rocket.chat/ui-contexts' { @@ -17,7 +17,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async getReadReceipts({ messageId }) { - if (!hasLicense('message-read-receipt')) { + if (!License.hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature', { method: 'getReadReceipts' }); } diff --git a/apps/meteor/ee/server/models/raw/LivechatRooms.ts b/apps/meteor/ee/server/models/raw/LivechatRooms.ts index 5c3bbb1296e0..b39e3d9eacfa 100644 --- a/apps/meteor/ee/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/ee/server/models/raw/LivechatRooms.ts @@ -11,7 +11,6 @@ import type { FindCursor, UpdateResult, Document, FindOptions, Db, Collection, F import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { LivechatRoomsRaw } from '../../../../server/models/raw/LivechatRooms'; -import { queriesLogger } from '../../../app/livechat-enterprise/server/lib/logger'; import { addQueryRestrictionsToRoomsModel } from '../../../app/livechat-enterprise/server/lib/query.helper'; declare module '@rocket.chat/model-typings' { @@ -271,25 +270,14 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo ], }; const update = { $set: { departmentAncestors: [unitId] } }; - queriesLogger.debug({ msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - association step`, query, update }); - const associationResult = await this.updateMany(query, update); - queriesLogger.debug({ msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - association step`, result: associationResult }); + await this.updateMany(query, update); const queryToDisassociateOldRoomsConnectedToUnit = { departmentAncestors: unitId, departmentId: { $nin: departments }, }; const updateToDisassociateRooms = { $unset: { departmentAncestors: 1 } }; - queriesLogger.debug({ - msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - disassociation step`, - query: queryToDisassociateOldRoomsConnectedToUnit, - update: updateToDisassociateRooms, - }); - const disassociationResult = await this.updateMany(queryToDisassociateOldRoomsConnectedToUnit, updateToDisassociateRooms); - queriesLogger.debug({ - msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - disassociation step`, - result: disassociationResult, - }); + await this.updateMany(queryToDisassociateOldRoomsConnectedToUnit, updateToDisassociateRooms); } async removeUnitAssociationFromRooms(unitId: string): Promise { @@ -297,9 +285,7 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo departmentAncestors: unitId, }; const update = { $unset: { departmentAncestors: 1 } }; - queriesLogger.debug({ msg: `LivechatRoomsRawEE.removeUnitAssociationFromRooms`, query, update }); - const result = await this.updateMany(query, update); - queriesLogger.debug({ msg: `LivechatRoomsRawEE.removeUnitAssociationFromRooms`, result }); + await this.updateMany(query, update); } async updateDepartmentAncestorsById(rid: string, departmentAncestors?: string[]) { @@ -314,7 +300,6 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo async update(...args: Parameters) { const [query, ...restArgs] = args; const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.update', query: restrictedQuery }); return super.update(restrictedQuery, ...restArgs); } @@ -328,14 +313,12 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo return super.updateOne(query, update, opts); } const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateOne', query: restrictedQuery }); return super.updateOne(restrictedQuery, update, opts); } async updateMany(...args: Parameters) { const [query, ...restArgs] = args; const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateMany', query: restrictedQuery }); return super.updateMany(restrictedQuery, ...restArgs); } diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts index b49cbb959df1..180b145e4352 100644 --- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts @@ -4,7 +4,6 @@ import { LivechatUnitMonitors, LivechatDepartment, LivechatRooms } from '@rocket import type { FindOptions, Filter, FindCursor, Db, FilterOperators, UpdateResult, DeleteResult, Document, UpdateFilter } from 'mongodb'; import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; -import { queriesLogger } from '../../../app/livechat-enterprise/server/lib/logger'; import { getUnitsFromUser } from '../../../app/livechat-enterprise/server/lib/units'; const addQueryRestrictions = async (originalQuery: Filter = {}) => { @@ -40,7 +39,6 @@ export class LivechatUnitRaw extends BaseRaw implement options: FindOptions, ): Promise> { const query = await addQueryRestrictions(originalQuery); - queriesLogger.debug({ msg: 'LivechatUnit.find', query }); return this.col.find(query, options) as FindCursor; } @@ -50,7 +48,6 @@ export class LivechatUnitRaw extends BaseRaw implement options: FindOptions, ): Promise { const query = await addQueryRestrictions(originalQuery); - queriesLogger.debug({ msg: 'LivechatUnit.findOne', query }); return this.col.findOne(query, options); } @@ -60,7 +57,6 @@ export class LivechatUnitRaw extends BaseRaw implement options: FindOptions, ): Promise { const query = await addQueryRestrictions(originalQuery); - queriesLogger.debug({ msg: 'LivechatUnit.update', query }); return this.col.updateOne(query, update, options); } diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 580f4c025e07..4fd8433358ca 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -1,4 +1,4 @@ -import { onLicense } from '../../app/license/server/license'; +import { License } from '@rocket.chat/license'; // To facilitate our lives with the stream // Collection will be registered on CE too @@ -8,7 +8,7 @@ import('./OmnichannelServiceLevelAgreements'); import('./AuditLog'); import('./ReadReceipts'); -await onLicense('livechat-enterprise', () => { +await License.onLicense('livechat-enterprise', () => { import('./CannedResponse'); import('./LivechatTag'); import('./LivechatUnit'); diff --git a/apps/meteor/ee/server/startup/apps/trialExpiration.ts b/apps/meteor/ee/server/startup/apps/trialExpiration.ts index 1c214ba0a406..eec50e91b7dd 100644 --- a/apps/meteor/ee/server/startup/apps/trialExpiration.ts +++ b/apps/meteor/ee/server/startup/apps/trialExpiration.ts @@ -1,10 +1,10 @@ +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onInvalidateLicense } from '../../../app/license/server/license'; import { Apps } from '../../apps'; Meteor.startup(() => { - onInvalidateLicense(() => { + License.onInvalidateLicense(() => { void Apps.disableApps(); }); }); diff --git a/apps/meteor/ee/server/startup/audit.ts b/apps/meteor/ee/server/startup/audit.ts index 441429e51b22..c38794a7582e 100644 --- a/apps/meteor/ee/server/startup/audit.ts +++ b/apps/meteor/ee/server/startup/audit.ts @@ -1,7 +1,8 @@ -import { onLicense } from '../../app/license/server'; +import { License } from '@rocket.chat/license'; + import { createPermissions } from '../lib/audit/startup'; -await onLicense('auditing', async () => { +await License.onLicense('auditing', async () => { await import('../lib/audit/methods'); await createPermissions(); diff --git a/apps/meteor/ee/server/startup/deviceManagement.ts b/apps/meteor/ee/server/startup/deviceManagement.ts index a9a1c805f72d..2ad5fd3b8a4f 100644 --- a/apps/meteor/ee/server/startup/deviceManagement.ts +++ b/apps/meteor/ee/server/startup/deviceManagement.ts @@ -1,8 +1,9 @@ -import { onToggledFeature } from '../../app/license/server/license'; +import { License } from '@rocket.chat/license'; + import { addSettings } from '../settings/deviceManagement'; let stopListening: (() => void) | undefined; -onToggledFeature('device-management', { +License.onToggledFeature('device-management', { up: async () => { const { createPermissions, createEmailTemplates } = await import('../lib/deviceManagement/startup'); const { listenSessionLogin } = await import('../lib/deviceManagement/session'); diff --git a/apps/meteor/ee/server/startup/engagementDashboard.ts b/apps/meteor/ee/server/startup/engagementDashboard.ts index 2fc393379bf3..ca5dda577bb0 100644 --- a/apps/meteor/ee/server/startup/engagementDashboard.ts +++ b/apps/meteor/ee/server/startup/engagementDashboard.ts @@ -1,8 +1,7 @@ +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onToggledFeature } from '../../app/license/server/license'; - -onToggledFeature('engagement-dashboard', { +License.onToggledFeature('engagement-dashboard', { up: () => Meteor.startup(async () => { const { prepareAnalytics, attachCallbacks } = await import('../lib/engagementDashboard/startup'); diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index f4e2452ec806..5731ca0d1deb 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -1,17 +1,14 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; -import { getMaxRoomsPerGuest } from '../../app/license/server/license'; callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - const totalSubscriptions = await Subscriptions.countByUserId(user._id); - - if (totalSubscriptions >= getMaxRoomsPerGuest()) { + if (await License.shouldPreventAction('roomsPerGuest', { userId: user._id })) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/apps/meteor/ee/server/startup/seatsCap.ts b/apps/meteor/ee/server/startup/seatsCap.ts index b390539ad6b1..f6d42823cb97 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { throttle } from 'underscore'; @@ -6,7 +7,6 @@ import { throttle } from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; import { validateUserRoles } from '../../app/authorization/server/validateUserRoles'; -import { canAddNewUser, getMaxActiveUsers, onValidateLicenses } from '../../app/license/server/license'; import { createSeatsLimitBanners, disableDangerBannerDiscardingDismissal, @@ -22,7 +22,7 @@ callbacks.add( return; } - if (!(await canAddNewUser())) { + if (await License.shouldPreventAction('activeUsers')) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -33,7 +33,7 @@ callbacks.add( callbacks.add( 'beforeUserImport', async ({ userCount }) => { - if (!(await canAddNewUser(userCount))) { + if (await License.shouldPreventAction('activeUsers', {}, userCount)) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -52,7 +52,7 @@ callbacks.add( return; } - if (!(await canAddNewUser())) { + if (await License.shouldPreventAction('activeUsers')) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -62,37 +62,13 @@ callbacks.add( callbacks.add( 'validateUserRoles', - async (userData: Partial) => { - const isGuest = userData.roles?.includes('guest'); - if (isGuest) { - await validateUserRoles(Meteor.userId(), userData); - return; - } - - if (!userData._id) { - return; - } - - const currentUserData = await Users.findOneById(userData._id); - if (currentUserData?.type === 'app') { - return; - } - - const wasGuest = currentUserData?.roles?.length === 1 && currentUserData.roles.includes('guest'); - if (!wasGuest) { - return; - } - - if (!(await canAddNewUser())) { - throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); - } - }, + async (userData: Partial) => validateUserRoles(userData), callbacks.priority.MEDIUM, 'check-max-user-seats', ); const handleMaxSeatsBanners = throttle(async function _handleMaxSeatsBanners() { - const maxActiveUsers = getMaxActiveUsers(); + const maxActiveUsers = License.getMaxActiveUsers(); if (!maxActiveUsers) { await disableWarningBannerDiscardingDismissal(); @@ -137,5 +113,5 @@ Meteor.startup(async () => { await handleMaxSeatsBanners(); - onValidateLicenses(handleMaxSeatsBanners); + License.onValidateLicense(handleMaxSeatsBanners); }); diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 5288b9a8e10e..37aec21bfe56 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -1,8 +1,8 @@ import { api } from '@rocket.chat/core-services'; +import { License } from '@rocket.chat/license'; import { isRunningMs } from '../../../server/lib/isRunningMs'; import { FederationService } from '../../../server/services/federation/service'; -import { isEnterprise, onLicense } from '../../app/license/server'; import { LicenseService } from '../../app/license/server/license.internalService'; import { OmnichannelEE } from '../../app/livechat-enterprise/server/services/omnichannel.internalService'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; @@ -26,13 +26,13 @@ if (!isRunningMs()) { let federationService: FederationService; void (async () => { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { federationService = await FederationService.createFederationService(); api.registerService(federationService); } })(); -await onLicense('federation', async () => { +await License.onLicense('federation', async () => { const federationServiceEE = new FederationServiceEE(); if (federationService) { api.destroyService(federationService); diff --git a/apps/meteor/ee/server/startup/upsell.ts b/apps/meteor/ee/server/startup/upsell.ts index c9e4c513276c..b31bf0635060 100644 --- a/apps/meteor/ee/server/startup/upsell.ts +++ b/apps/meteor/ee/server/startup/upsell.ts @@ -1,20 +1,13 @@ +import { License } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { onValidateLicenses, getLicenses } from '../../app/license/server/license'; - const handleHadTrial = (): void => { - getLicenses().forEach(({ valid, license }): void => { - if (!valid) { - return; - } - - if (license.meta?.trial) { - void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); - } - }); + if (License.getLicense()?.information.trial) { + void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + } }; Meteor.startup(() => { - onValidateLicenses(handleHadTrial); + License.onValidateLicense(handleHadTrial); }); diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index 0af2cee0c377..8ac29d191576 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,6 +1,5 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; - -import type { LicenseAppSources } from '../../ee/app/license/definition/ILicense'; +import type { LicenseAppSources } from '@rocket.chat/license'; /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 46a27357f546..9c7333a355b3 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -1,5 +1,3 @@ -import type { UrlWithParsedQuery } from 'url'; - import type { IMessage, IRoom, @@ -10,7 +8,6 @@ import type { ILivechatInquiryRecord, ILivechatVisitor, VideoConference, - ParsedUrl, OEmbedMeta, OEmbedUrlContent, Username, @@ -167,24 +164,13 @@ type ChainedCallbackSignatures = { BusinessHourBehaviorClass: { new (): IBusinessHourBehavior }; }; 'renderMessage': (message: T) => T; - 'oembed:beforeGetUrlContent': (data: { - urlObj: Omit & { host?: unknown; search?: unknown }; - parsedUrl: ParsedUrl; - }) => { - urlObj: UrlWithParsedQuery; - parsedUrl: ParsedUrl; + 'oembed:beforeGetUrlContent': (data: { urlObj: URL }) => { + urlObj: URL; }; - 'oembed:afterParseContent': (data: { - url: string; - meta: OEmbedMeta; - headers: { [k: string]: string }; - parsedUrl: ParsedUrl; - content: OEmbedUrlContent; - }) => { + 'oembed:afterParseContent': (data: { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; content: OEmbedUrlContent }) => { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; - parsedUrl: ParsedUrl; content: OEmbedUrlContent; }; 'livechat.beforeListTags': () => ILivechatTag[]; diff --git a/apps/meteor/lib/callbacks/callbacksBase.ts b/apps/meteor/lib/callbacks/callbacksBase.ts index e6681df78321..405cc5da80e6 100644 --- a/apps/meteor/lib/callbacks/callbacksBase.ts +++ b/apps/meteor/lib/callbacks/callbacksBase.ts @@ -170,7 +170,7 @@ export class Callbacks< this.setCallbacks(hook, hooks); } - run(hook: Hook, ...args: Parameters): void; + run(hook: Hook, ...args: Parameters): Promise; run( hook: Hook, diff --git a/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts b/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts new file mode 100644 index 000000000000..4cea63a01f09 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceAccessError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceAccessError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts b/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts new file mode 100644 index 000000000000..8b4edcf8f588 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceConnectionError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceConnectionError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceError.ts b/apps/meteor/lib/errors/CloudWorkspaceError.ts new file mode 100644 index 000000000000..d843c42ea520 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceError.ts @@ -0,0 +1,6 @@ +export class CloudWorkspaceError extends Error { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts b/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts new file mode 100644 index 000000000000..96c9a28be82c --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceLicenseError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceLicenseError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts b/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts new file mode 100644 index 000000000000..aecec757acee --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceRegistrationError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceRegistrationError.name; + } +} diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 9e68456a78c8..8bf3e3fe886a 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -246,7 +246,9 @@ "@rocket.chat/i18n": "workspace:^", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/instance-status": "workspace:^", + "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "next", + "@rocket.chat/license": "workspace:^", "@rocket.chat/log-format": "workspace:^", "@rocket.chat/logger": "workspace:^", "@rocket.chat/logo": "^0.31.27", @@ -263,6 +265,7 @@ "@rocket.chat/presence": "workspace:^", "@rocket.chat/random": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/server-cloud-communication": "workspace:^", "@rocket.chat/server-fetch": "workspace:^", "@rocket.chat/sha256": "workspace:^", "@rocket.chat/string-helpers": "next", @@ -286,7 +289,7 @@ "@xmldom/xmldom": "^0.8.8", "adm-zip": "0.5.10", "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", + "ajv-formats": "~2.1.1", "apn": "2.2.0", "archiver": "^3.1.1", "asterisk-manager": "^0.2.0", @@ -347,6 +350,7 @@ "imap": "^0.8.19", "ip-range-check": "^0.2.0", "is-svg": "^4.3.2", + "isolated-vm": "4.4.2", "jquery": "^3.6.0", "jschardet": "^3.0.0", "jsdom": "^16.7.0", @@ -413,6 +417,7 @@ "stream-buffers": "^3.0.2", "strict-uri-encode": "^2.0.0", "string-strip-html": "^7.0.3", + "suretype": "~2.4.1", "tar-stream": "^1.6.2", "textarea-caret": "^3.1.0", "tinykeys": "^1.4.0", @@ -468,5 +473,11 @@ }, "installConfig": { "hoistingLimits": "workspaces" + }, + "rocketchat": { + "minimumClientVersions": { + "desktop": "3.9.6", + "mobile": "4.39.0" + } } } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 197d38d603f0..94ce81c32284 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -415,6 +415,7 @@ "Also_send_to_channel": "Also send to channel", "Always_open_in_new_window": "Always Open in New Window", "Always_show_thread_replies_in_main_channel": "Always show thread replies in main channel", + "Analytic_reports": "Analytic reports", "Analytics": "Analytics", "Analytics_Description": "See how users interact with your workspace.", "Analytics_features_enabled": "Features Enabled", @@ -423,6 +424,7 @@ "Analytics_features_users_Description": "Tracks custom events related to actions related to users (password reset times, profile picture change, etc).", "Analytics_Google": "Google Analytics", "Analytics_Google_id": "Tracking ID", + "Analytics_page_briefing": "Rocket.Chat collects anonymous usage data to identify how many instances are deployed and to improve the product for all users. We take your privacy seriously, so the usage data is encrypted and stored securely.", "Analyze_practical_usage": "Analyze practical usage statistics about users, messages and channels", "and": "and", "And_more": "And {{length}} more", @@ -713,6 +715,8 @@ "AutoTranslate_DeepL": "DeepL", "AutoTranslate_Enabled": "Enable Auto-Translate", "AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the `auto-translate` permission to have all messages automatically translated into their selected language. Fees may apply.", + "AutoTranslate_AutoEnableOnJoinRoom": "Auto-Translate for non-default language members", + "AutoTranslate_AutoEnableOnJoinRoom_Description": "If enabled, whenever a user with a language preference different than the workspace default joins a room, it will be automatically translated for them.", "AutoTranslate_Google": "Google", "AutoTranslate_Microsoft": "Microsoft", "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key", @@ -2471,6 +2475,7 @@ "Hospitality_Businness": "Hospitality Business", "hours": "hours", "Hours": "Hours", + "How_and_why_we_collect_usage_data": "How and why we collect usage data", "How_friendly_was_the_chat_agent": "How friendly was the chat agent?", "How_knowledgeable_was_the_chat_agent": "How knowledgeable was the chat agent?", "How_long_to_wait_after_agent_goes_offline": "How Long to Wait After Agent Goes Offline", @@ -2610,6 +2615,8 @@ "Integration_Incoming_WebHook": "Incoming WebHook Integration", "Integration_New": "New Integration", "integration-scripts-disabled": "Integration Scripts are Disabled", + "integration-scripts-isolated-vm-disabled": "The \"Secure Sandbox\" may not be used on new or modified scripts.", + "integration-scripts-vm2-disabled": "The \"Compatible Sandbox\" may not be used on new or modified scripts.", "Integration_Outgoing_WebHook": "Outgoing WebHook Integration", "Integration_Outgoing_WebHook_History": "Outgoing WebHook Integration History", "Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "Data Passed to Integration", @@ -4163,6 +4170,7 @@ "Receive_Login_Detection_Emails_Description": "Receive an email each time a new login is detected on your account.", "Recent_Import_History": "Recent Import History", "Record": "Record", + "Records": "Records", "recording": "recording", "Redirect_URI": "Redirect URI", "Redirect_URL_does_not_match": "Redirect URL does not match", @@ -4520,6 +4528,10 @@ "Screen_Share": "Screen Share", "Script": "Script", "Script_Enabled": "Script Enabled", + "Script_Engine": "Script Sandbox", + "Script_Engine_Description": "Older scripts may require the compatible sandbox to run properly, but all new scripts should try to use the secure sandbox instead.", + "Script_Engine_vm2": "Compatible Sandbox (Deprecated)", + "Script_Engine_isolated_vm": "Secure Sandbox", "Search": "Search", "Searchable": "Searchable", "Search_Apps": "Search apps", @@ -5849,7 +5861,7 @@ "onboarding.component.form.action.confirm": "Confirm", "onboarding.component.form.termsAndConditions": "I agree with <1>Terms and Conditions and <3>Privacy Policy", "onboarding.component.emailCodeFallback": "Didn’t receive email? <1>Resend or <3>Change email", - "onboarding.page.form.title": "Let's <1>Launch Your Workspace", + "onboarding.page.form.title": "Let's launch your workspace", "onboarding.page.awaitingConfirmation.title": "Awaiting confirmation", "onboarding.page.awaitingConfirmation.subtitle": "We have sent you an email to {{emailAddress}} with a confirmation link. Please verify that the security code below matches the one in the email.", "onboarding.page.emailConfirmed.title": "Email Confirmed!", @@ -5876,10 +5888,8 @@ "onboarding.page.requestTrial.subtitle": "Try our best Enterprise Edition plan for 30 days for free", "onboarding.page.magicLinkEmail.title": "We emailed you a login link", "onboarding.page.magicLinkEmail.subtitle": "Click the link in the email we just sent you to sign in to your workspace. <1>The link will expire in 30 minutes.", - "onboarding.page.organizationInfoPage.title": "A few more details...", - "onboarding.page.organizationInfoPage.subtitle": "These will help us to personalize your workspace.", "onboarding.form.adminInfoForm.title": "Admin Info", - "onboarding.form.adminInfoForm.subtitle": "We need this to create an admin profile inside your workspace", + "onboarding.form.adminInfoForm.subtitle": "We need this information to create an admin profile for your workspace.", "onboarding.form.adminInfoForm.fields.fullName.label": "Full name", "onboarding.form.adminInfoForm.fields.fullName.placeholder": "First and last name", "onboarding.form.adminInfoForm.fields.username.label": "Username", @@ -5890,7 +5900,7 @@ "onboarding.form.adminInfoForm.fields.password.placeholder": "Create password", "onboarding.form.adminInfoForm.fields.keepPosted.label": "Keep me posted about Rocket.Chat updates", "onboarding.form.organizationInfoForm.title": "Organization Info", - "onboarding.form.organizationInfoForm.subtitle": "Please, bear with us. This info will help us personalize your workspace", + "onboarding.form.organizationInfoForm.subtitle": "We need to know who you are.", "onboarding.form.organizationInfoForm.fields.organizationName.label": "Organization name", "onboarding.form.organizationInfoForm.fields.organizationName.placeholder": "Organization name", "onboarding.form.organizationInfoForm.fields.organizationType.label": "Organization type", @@ -5901,17 +5911,16 @@ "onboarding.form.organizationInfoForm.fields.organizationSize.placeholder": "Select", "onboarding.form.organizationInfoForm.fields.country.label": "Country", "onboarding.form.organizationInfoForm.fields.country.placeholder": "Select", - "onboarding.form.registeredServerForm.title": "Register Your Server", + "onboarding.form.registeredServerForm.title": "Register your workspace", "onboarding.form.registeredServerForm.included.push": "Mobile push notifications", "onboarding.form.registeredServerForm.included.externalProviders": "Integration with external providers (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Access to marketplace apps", - "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud account email", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "To register your server, we need to connect it to your cloud account. If you already have one - we will link it automatically. Otherwise, a new account will be created", - "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Please enter your Email", + "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Admin email", + "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Insert your email to continue", "onboarding.form.registeredServerForm.keepInformed": "Keep me informed about news and events", "onboarding.form.registeredServerForm.registerLater": "Register later", "onboarding.form.registeredServerForm.notConnectedToInternet": "The server is not connected to the internet, so you’ll have to do an offline registration for this workspace.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "By registering I agree to receive relevant product and security updates", + "onboarding.form.registeredServerForm.registrationEngagement": "Registration allows automatic license updates, notifications of critical vulnerabilities and access to Rocket.Chat Cloud services. No sensitive workspace data is shared; statistics sent to Rocket.Chat is made visible to you within the administration area.", "onboarding.form.standaloneServerForm.title": "Standalone Server Confirmation", "onboarding.form.standaloneServerForm.servicesUnavailable": "Some of the services will be unavailable or will require manual setup", "onboarding.form.standaloneServerForm.publishOwnApp": "In order to send push notitications you need to compile and publish your own app to Google Play and App Store", @@ -6051,4 +6060,4 @@ "Filter_by_room": "Filter by room type", "Filter_by_visibility": "Filter by visibility", "Theme_Appearence": "Theme Appearence" -} \ No newline at end of file +} diff --git a/apps/meteor/packages/rocketchat-version/plugin/compile-version.js b/apps/meteor/packages/rocketchat-version/plugin/compile-version.js index c283af960e67..20b26b9cdcf0 100644 --- a/apps/meteor/packages/rocketchat-version/plugin/compile-version.js +++ b/apps/meteor/packages/rocketchat-version/plugin/compile-version.js @@ -1,6 +1,8 @@ import { exec } from 'child_process'; import os from 'os'; import util from 'util'; +import path from 'path'; +import fs from 'fs'; const execAsync = util.promisify(exec); @@ -24,6 +26,9 @@ class VersionCompiler { }; output.marketplaceApiVersion = require('@rocket.chat/apps-engine/package.json').version.replace(/^[^0-9]/g, ''); + const minimumClientVersions = + JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' }))?.rocketchat + ?.minimumClientVersions || {}; try { const result = await execAsync("git log --pretty=format:'%H%n%ad%n%an%n%s' -n 1"); const data = result.stdout.split('\n'); @@ -55,7 +60,8 @@ class VersionCompiler { // no branch } - output = `exports.Info = ${JSON.stringify(output, null, 4)};`; + output = `exports.Info = ${JSON.stringify(output, null, 4)}; + exports.minimumClientVersions = ${JSON.stringify(minimumClientVersions, null, 4)};`; file.addJavaScript({ data: output, diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index 44302ae9ff91..d71190cb0b6d 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -25,18 +25,10 @@ const language = settings.get('Language') || 'en'; const t = (s: string): string => i18n.t(s, { lng: language }); async function getGuestByEmail(email: string, name: string, department = ''): Promise { - logger.debug(`Attempt to register a guest for ${email} on department: ${department}`); const guest = await LivechatVisitors.findOneGuestByEmailAddress(email); if (guest) { - logger.debug(`Guest with email ${email} found with id ${guest._id}`); if (guest.department !== department) { - logger.debug({ - msg: 'Switching departments for guest', - guest, - previousDepartment: guest.department, - newDepartment: department, - }); if (!department) { await LivechatVisitors.removeDepartmentById(guest._id); delete guest.department; @@ -48,10 +40,6 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr return guest; } - logger.debug({ - msg: 'Creating a new Omnichannel guest for visitor with email', - email, - }); const userId = await LivechatTyped.registerGuest({ token: Random.id(), name: name || email, @@ -60,7 +48,6 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr }); const newGuest = await LivechatVisitors.findOneById(userId); - logger.debug(`Guest ${userId} for visitor ${email} created`); if (newGuest) { return newGuest; } @@ -111,7 +98,7 @@ async function uploadAttachment(attachmentParam: Attachment, rid: string, visito } export async function onEmailReceived(email: ParsedMail, inbox: string, department = ''): Promise { - logger.debug(`New email conversation received on inbox ${inbox}. Will be assigned to department ${department}`); + logger.info(`New email conversation received on inbox ${inbox}. Will be assigned to department ${department}`); if (!email.from?.value?.[0]?.address) { return; } @@ -119,19 +106,13 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme const references = typeof email.references === 'string' ? [email.references] : email.references; const initialRef = [email.messageId, email.inReplyTo].filter(Boolean) as string[]; const thread = (references?.length ? references : []).flatMap((t: string) => t.split(',')).concat(initialRef); - - logger.debug(`Received new email conversation with thread ${thread} on inbox ${inbox} from ${email.from.value[0].address}`); - - logger.debug(`Fetching guest for visitor ${email.from.value[0].address}`); const guest = await getGuestByEmail(email.from.value[0].address, email.from.value[0].name, department); if (!guest) { - logger.debug(`No visitor found for ${email.from.value[0].address}`); + logger.error(`No visitor found for ${email.from.value[0].address}`); return; } - logger.debug(`Guest ${guest._id} obtained. Attempting to find or create a room on department ${department}`); - let room: IOmnichannelRoom | null = await LivechatRooms.findOneByVisitorTokenAndEmailThreadAndDepartment( guest.token, thread, @@ -146,7 +127,6 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme }); if (room?.closedAt) { - logger.debug(`Room ${room?._id} is closed. Reopening`); room = await QueueManager.unarchiveRoom(room); } @@ -166,8 +146,6 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme const rid = room?._id ?? Random.id(); const msgId = Random.id(); - logger.debug(`Sending email message to room ${rid} for visitor ${guest._id}. Conversation assigned to department ${department}`); - Livechat.sendMessage({ guest, message: { @@ -242,7 +220,7 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme try { attachments.push(await uploadAttachment(attachment, rid, guest.token)); } catch (err) { - Livechat.logger.error({ msg: 'Error uploading attachment from email', err }); + logger.error({ msg: 'Error uploading attachment from email', err }); } } @@ -259,7 +237,7 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme room && (await LivechatRooms.updateEmailThreadByRoomId(room._id, thread)); }) .catch((err) => { - Livechat.logger.error({ + logger.error({ msg: 'Error receiving email', err, }); diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts index dccf8315acd3..61ca75aa65d4 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -75,7 +75,7 @@ async function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): Promi ...mail, }) .then((info) => { - logger.info('Message sent: %s', info.messageId); + logger.info({ msg: 'Message sent', info }); return info; }) .catch(async (err) => { @@ -92,7 +92,6 @@ async function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): Promi slashCommands.add({ command: 'sendEmailAttachment', callback: async ({ command, params }: SlashCommandCallbackParams<'sendEmailAttachment'>) => { - logger.debug('sendEmailAttachment command: ', command, params); if (command !== 'sendEmailAttachment' || !Match.test(params, String)) { return; } @@ -318,7 +317,6 @@ export async function sendTestEmailToInbox(emailInboxRecord: IEmailInbox, user: throw new Error('user-without-verified-email'); } - logger.info(`Sending testing email to ${address}`); void sendEmail(inbox, { to: address, subject: 'Test of inbox configuration', diff --git a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts new file mode 100644 index 000000000000..13540246f0e6 --- /dev/null +++ b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts @@ -0,0 +1,28 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; + +export const getSubscriptionAutotranslateDefaultConfig = async ( + user: IUser, +): Promise< + | { + autoTranslate: boolean; + autoTranslateLanguage: string; + } + | undefined +> => { + const [autoEnableSetting, languageSetting] = await Promise.all([ + Settings.findOneById('AutoTranslate_AutoEnableOnJoinRoom'), + Settings.findOneById('Language'), + ]); + const { language: userLanguage } = user.settings?.preferences || {}; + + if (!autoEnableSetting?.value) { + return; + } + + if (!userLanguage || userLanguage === 'default' || languageSetting?.value === userLanguage) { + return; + } + + return { autoTranslate: true, autoTranslateLanguage: userLanguage }; +}; diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 5579261911f5..09edca701540 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -9,9 +9,10 @@ import './importPackages'; import '../imports/startup/server'; import '../app/lib/server/startup'; +import '../ee/server/startup'; +import './startup'; import '../ee/server'; import './lib/pushConfig'; -import './startup'; import './configuration/accounts_meld'; import './configuration/ldap'; import './methods/OEmbedCacheCleanup'; diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts index cbbafccfe60b..acba1bed406b 100644 --- a/apps/meteor/server/methods/addAllUserToRoom.ts +++ b/apps/meteor/server/methods/addAllUserToRoom.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { settings } from '../../app/settings/server'; import { callbacks } from '../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../lib/getSubscriptionAutotranslateDefaultConfig'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -55,6 +56,7 @@ Meteor.methods({ continue; } await callbacks.run('beforeJoinRoom', user, room); + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user); await Subscriptions.createWithRoomAndUser(room, user, { ts: now, open: true, @@ -62,6 +64,7 @@ Meteor.methods({ unread: 1, userMentions: 1, groupMentions: 0, + ...autoTranslateConfig, }); await Message.saveSystemMessage('uj', rid, user.username || '', user, { ts: now }); await callbacks.run('afterJoinRoom', user, room); diff --git a/apps/meteor/server/models/CloudAnnouncements.ts b/apps/meteor/server/models/CloudAnnouncements.ts new file mode 100644 index 000000000000..4f6692d67fc9 --- /dev/null +++ b/apps/meteor/server/models/CloudAnnouncements.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { CloudAnnouncementsRaw } from './raw/CloudAnnouncements'; + +registerModel('ICloudAnnouncementsModel', new CloudAnnouncementsRaw(db)); diff --git a/apps/meteor/server/models/raw/CloudAnnouncements.ts b/apps/meteor/server/models/raw/CloudAnnouncements.ts new file mode 100644 index 000000000000..21b4304b2bd5 --- /dev/null +++ b/apps/meteor/server/models/raw/CloudAnnouncements.ts @@ -0,0 +1,11 @@ +import type { Cloud } from '@rocket.chat/core-typings'; +import type { ICloudAnnouncementsModel } from '@rocket.chat/model-typings'; +import type { Db } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class CloudAnnouncementsRaw extends BaseRaw implements ICloudAnnouncementsModel { + constructor(db: Db) { + super(db, 'cloud_announcements'); + } +} diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 14b26e0f188f..d355d1febd16 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -68,3 +68,4 @@ import './Imports'; import './AppsTokens'; import './CronHistory'; import './Migrations'; +import './CloudAnnouncements'; diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index 99863305f7c1..6918d40af871 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -39,7 +39,7 @@ export class Authorization extends ServiceClass implements IAuthorization { } async started(): Promise { - if (!(await License.isEnterprise())) { + if (!(await License.hasValidLicense())) { return; } diff --git a/apps/meteor/server/services/banner/service.ts b/apps/meteor/server/services/banner/service.ts index 4dc0dbbec494..d20b9e780875 100644 --- a/apps/meteor/server/services/banner/service.ts +++ b/apps/meteor/server/services/banner/service.ts @@ -26,7 +26,7 @@ export class BannerService extends ServiceClassInternal implements IBannerServic return true; } - async create(doc: Optional): Promise { + async create(doc: Optional): Promise { const bannerId = doc._id || uuidv4(); doc.view.appId = 'banner-core'; diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index 532bb5d245e9..3e492a4d6514 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -32,10 +32,8 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn // handle agent disconnections this.onEvent('watch.pbxevents', async ({ data }) => { - this.logger.debug(`Get event watch.pbxevents on service`); const extension = data.agentExtension; if (!extension) { - this.logger.debug(`No agent extension associated with the event. Skipping`); return; } switch (data.event) { @@ -53,12 +51,12 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn this.logger.info(`Processing hangup event for call with agent on extension ${extension}`); const agent = await Users.findOneByExtension(extension); if (!agent) { - this.logger.debug(`No agent found with extension ${extension}. Event won't proceed`); + this.logger.error(`No agent found with extension ${extension}. Event won't proceed`); return; } const currentRoom = await VoipRoom.findOneByAgentId(agent._id); if (!currentRoom) { - this.logger.debug(`No active call found for agent ${agent._id}`); + this.logger.error(`No active call found for agent ${agent._id}`); return; } this.logger.debug(`Notifying agent ${agent._id} of hangup on room ${currentRoom._id}`); @@ -69,7 +67,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn this.logger.info(`Processing disconnection event for agent with extension ${extension}`); const agent = await Users.findOneByExtension(extension); if (!agent) { - this.logger.debug(`No agent found with extension ${extension}. Event won't proceed`); + this.logger.error(`No agent found with extension ${extension}. Event won't proceed`); // this should not even be possible, but just in case return; } @@ -96,8 +94,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn const { _id, department: departmentId } = guest; const newRoomAt = new Date(); - this.logger.debug(`Creating Voip room for visitor ${_id}`); - /** * This is a peculiar case for outbound. In case of outbound, * the room is created as soon as the remote use accepts a call. @@ -182,7 +178,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn _updatedAt: newRoomAt, }; - this.logger.debug(`Room created for visitor ${_id}`); return (await VoipRoom.insertOne(room)).insertedId; } @@ -234,11 +229,9 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn direction: IVoipRoom['direction'], options: FindOptions = {}, ): Promise { - this.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`); let room = await VoipRoom.findOneById(rid, options); let newRoom = false; if (room && !room.open) { - this.logger.debug(`Last room for visitor ${guest._id} closed. Creating new one`); room = null; } if (room == null) { @@ -246,10 +239,8 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn const roomId = await this.createVoipRoom(rid, name, agent, guest, direction); room = await VoipRoom.findOneVoipRoomById(roomId); newRoom = true; - this.logger.debug(`Room obtained for visitor ${guest._id} -> ${room?._id}`); } if (!room) { - this.logger.debug(`Visitor ${guest._id} trying to access another visitor's room`); throw new Error('cannot-access-room'); } return { @@ -281,7 +272,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly' = 'voip-call-wrapup', options?: { comment?: string; tags?: string[] }, ): Promise { - this.logger.debug(`Attempting to close room ${room._id}`); if (!room || room.t !== 'v' || !room.open) { return false; } @@ -298,8 +288,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn // For now, this data will be appended as a metric on room closing await this.setCallWaitingQueueTimers(room); - this.logger.debug(`Room ${room._id} closed and timers set`); - this.logger.debug(`Room ${room._id} was closed at ${closeInfo.closedAt} (duration ${closeInfo.callDuration})`); await VoipRoom.closeByRoomId(room._id, closeInfo); return true; @@ -452,8 +440,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn }, }; - this.logger.debug(`Handling event ${event} on room ${room._id}`); - if ( isVoipRoom(room) && room.open && @@ -461,7 +447,6 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn // Check if call exists by looking if we have pbx events of it (await PbxEvents.findOneByUniqueId(room.callUniqueId)) ) { - this.logger.debug(`Room is valid. Sending event ${event}`); await sendMessage(user, message, room); } else { this.logger.warn({ msg: 'Invalid room type or event type', type: room.t, event }); diff --git a/apps/meteor/server/services/omnichannel/queue.ts b/apps/meteor/server/services/omnichannel/queue.ts index 684c10161a94..cbedf1cdcdec 100644 --- a/apps/meteor/server/services/omnichannel/queue.ts +++ b/apps/meteor/server/services/omnichannel/queue.ts @@ -20,24 +20,23 @@ export class OmnichannelQueue implements IOmnichannelQueue { } async start() { - queueLogger.debug('Starting queue'); if (this.running) { - queueLogger.debug('Queue already running'); return; } const activeQueues = await this.getActiveQueues(); queueLogger.debug(`Active queues: ${activeQueues.length}`); - this.running = true; + + queueLogger.info('Service started'); return this.execute(); } async stop() { - queueLogger.debug('Stopping queue'); await LivechatInquiry.unlockAll(); this.running = false; + queueLogger.info('Service stopped'); } private async getActiveQueues() { @@ -62,7 +61,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { const queue = await this.nextQueue(); const queueDelayTimeout = this.delay(); - queueLogger.debug(`Executing queue ${queue || 'Public'} with timeout of ${queueDelayTimeout}`); + queueLogger.info(`Executing queue ${queue || 'Public'} with timeout of ${queueDelayTimeout}`); setTimeout(this.checkQueue.bind(this, queue), queueDelayTimeout); } diff --git a/apps/meteor/server/settings/message.ts b/apps/meteor/server/settings/message.ts index b0cda60fe60a..17dd1f7b230d 100644 --- a/apps/meteor/server/settings/message.ts +++ b/apps/meteor/server/settings/message.ts @@ -245,6 +245,14 @@ export const createMessageSettings = () => public: true, }); + await this.add('AutoTranslate_AutoEnableOnJoinRoom', false, { + type: 'boolean', + group: 'Message', + section: 'AutoTranslate', + public: true, + enableQuery: [{ _id: 'AutoTranslate_Enabled', value: true }], + }); + await this.add('AutoTranslate_ServiceProvider', 'google-translate', { type: 'select', group: 'Message', diff --git a/apps/meteor/server/settings/setup-wizard.ts b/apps/meteor/server/settings/setup-wizard.ts index b5cdb4f6a4b1..62da3f1471cf 100644 --- a/apps/meteor/server/settings/setup-wizard.ts +++ b/apps/meteor/server/settings/setup-wizard.ts @@ -1204,6 +1204,13 @@ export const createSetupWSettings = () => secret: true, }); + await this.add('Cloud_Workspace_Supported_Versions_Token', '', { + type: 'string', + hidden: true, + readonly: true, + secret: true, + }); + await this.add('Cloud_Url', 'https://cloud.rocket.chat', { type: 'string', hidden: true, diff --git a/apps/meteor/server/startup/migrations/v278.ts b/apps/meteor/server/startup/migrations/v278.ts index 57986fd1064f..068d86499ff9 100644 --- a/apps/meteor/server/startup/migrations/v278.ts +++ b/apps/meteor/server/startup/migrations/v278.ts @@ -1,7 +1,7 @@ +import { License } from '@rocket.chat/license'; import { Banners, Settings } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; -import { isEnterprise } from '../../../ee/app/license/server'; import { addMigration } from '../../lib/migrations'; addMigration({ @@ -16,7 +16,7 @@ addMigration({ const LDAPEnabled = settings.get('LDAP_Enable'); const SAMLEnabled = settings.get('SAML_Custom_Default'); - const isEE = isEnterprise(); + const isEE = License.hasValidLicense(); if (!isEE && (isCustomOAuthEnabled || LDAPEnabled || SAMLEnabled)) { return; diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index f6093053fde6..7cfa089326b2 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -40,6 +40,8 @@ test.describe.serial('message-actions', () => { await page.locator('[data-qa-id="edit-message"]').click(); await page.locator('[name="msg"]').fill('this message was edited'); await page.keyboard.press('Enter'); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('this message was edited'); }); test('expect message is deleted', async ({ page }) => { @@ -47,6 +49,9 @@ test.describe.serial('message-actions', () => { await poHomeChannel.content.openLastMessageMenu(); await page.locator('[data-qa-id="delete-message"]').click(); await page.locator('#modal-root .rcx-button-group--align-end .rcx-button--danger').click(); + await expect(poHomeChannel.content.lastUserMessage.locator('[data-qa-type="message-body"]:has-text("Message to delete")')).toHaveCount( + 0, + ); }); test('expect quote the message', async ({ page }) => { @@ -64,6 +69,9 @@ test.describe.serial('message-actions', () => { await poHomeChannel.content.sendMessage('Message to star'); await poHomeChannel.content.openLastMessageMenu(); await page.locator('[data-qa-id="star-message"]').click(); + await page.getByRole('button').and(page.getByTitle('Options')).click(); + await page.locator('[data-key="starred-messages"]').click(); + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('Message to star'); }); test('expect copy the message', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/threads.spec.ts b/apps/meteor/tests/e2e/threads.spec.ts index 6d6b8ee6f122..d9e181d2706b 100644 --- a/apps/meteor/tests/e2e/threads.spec.ts +++ b/apps/meteor/tests/e2e/threads.spec.ts @@ -104,6 +104,7 @@ test.describe.serial('Threads', () => { await page.locator('[data-qa-id="edit-message"]').click(); await page.locator('[name="msg"]').last().fill('this message was edited'); await page.keyboard.press('Enter'); + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('this message was edited'); }); test('expect quote the thread message', async ({ page }) => { @@ -118,6 +119,9 @@ test.describe.serial('Threads', () => { test('expect star the thread message', async ({ page }) => { await poHomeChannel.content.openLastThreadMessageMenu(); await page.locator('[data-qa-id="star-message"]').click(); + await page.getByRole('button').and(page.getByTitle('Options')).click(); + await page.locator('[data-key="starred-messages"]').click(); + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('this is a message for reply'); }); test('expect copy the message', async ({ page }) => { diff --git a/apps/meteor/tests/end-to-end/api/00-autotranslate.js b/apps/meteor/tests/end-to-end/api/00-autotranslate.js index 52adb69f17c7..48bb021ce388 100644 --- a/apps/meteor/tests/end-to-end/api/00-autotranslate.js +++ b/apps/meteor/tests/end-to-end/api/00-autotranslate.js @@ -1,9 +1,12 @@ import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, describe, after, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage } from '../../data/chat.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; +import { createRoom } from '../../data/rooms.helper'; +import { password } from '../../data/user'; +import { createUser, login } from '../../data/users.helper.js'; describe('AutoTranslate', function () { this.retries(0); @@ -314,5 +317,130 @@ describe('AutoTranslate', function () { .end(done); }); }); + describe('Autoenable setting', () => { + let userA; + let userB; + let credA; + let credB; + let channel; + + const createChannel = async (members, cred) => + (await createRoom({ type: 'c', members, name: `channel-test-${Date.now()}`, credentials: cred })).body.channel; + + const setLanguagePref = async (language, cred) => { + await request + .post(api('users.setPreferences')) + .set(cred) + .send({ data: { language } }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }; + + const getSub = async (roomId, cred) => + ( + await request + .get(api('subscriptions.getOne')) + .set(cred) + .query({ + roomId, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').and.to.be.an('object'); + }) + ).body.subscription; + + before(async () => { + await updateSetting('AutoTranslate_Enabled', true); + await updateSetting('AutoTranslate_AutoEnableOnJoinRoom', true); + await updateSetting('Language', 'pt-BR'); + + channel = await createChannel(); + userA = await createUser(); + userB = await createUser(); + + credA = await login(userA.username, password); + credB = await login(userB.username, password); + + await setLanguagePref('en', credB); + }); + + after(async () => { + await updateSetting('AutoTranslate_AutoEnableOnJoinRoom', false); + await updateSetting('AutoTranslate_Enabled', false); + await updateSetting('Language', ''); + }); + + it("should do nothing if the user hasn't changed his language preference", async () => { + const sub = await getSub(channel._id, credentials); + expect(sub).to.not.have.property('autoTranslate'); + expect(sub).to.not.have.property('autoTranslateLanguage'); + }); + + it("should do nothing if the user changed his language preference to be the same as the server's", async () => { + await setLanguagePref('pt-BR', credA); + + const channel = await createChannel(undefined, credA); + const sub = await getSub(channel._id, credA); + expect(sub).to.not.have.property('autoTranslate'); + expect(sub).to.not.have.property('autoTranslateLanguage'); + }); + + it('should enable autotranslate with the correct language when creating a new room', async () => { + await setLanguagePref('en', credA); + + const channel = await createChannel(undefined, credA); + const sub = await getSub(channel._id, credA); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate for all the members added to the room upon creation', async () => { + const channel = await createChannel([userA.username, userB.username]); + const subA = await getSub(channel._id, credA); + expect(subA).to.have.property('autoTranslate'); + expect(subA).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + + const subB = await getSub(channel._id, credB); + expect(subB).to.have.property('autoTranslate'); + expect(subB).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate with the correct language when joining a room', async () => { + await request + .post(api('channels.join')) + .set(credA) + .send({ + roomId: channel._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const sub = await getSub(channel._id, credA); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate with the correct language when added to a room', async () => { + await request + .post(api('channels.invite')) + .set(credentials) + .send({ + roomId: channel._id, + userId: userB._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const sub = await getSub(channel._id, credB); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js index 7525fd6ab443..e9fec42e4b66 100644 --- a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js +++ b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js @@ -24,6 +24,7 @@ describe('miscellaneous', function () { .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { + expect(res.body).to.have.property('version').and.to.be.a('string'); expect(res.body.info).to.have.property('version').and.to.be.a('string'); expect(res.body.info).to.have.property('build').and.to.be.an('object'); expect(res.body.info).to.have.property('commit').and.to.be.an('object'); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index cccc3eb27738..e875be80fd3b 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -29,8 +29,8 @@ describe('[Groups]', function () { before((done) => getCredentials(done)); - before('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ @@ -46,10 +46,21 @@ describe('[Groups]', function () { expect(res.body).to.have.nested.property('group.msgs', 0); group._id = res.body.group._id; group.name = res.body.group.name; + }); + }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomId: group._id, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(200); }); - describe('[/groups.create]', () => { + + describe('/groups.create', () => { let guestUser; let room; @@ -60,80 +71,89 @@ describe('[Groups]', function () { await deleteUser(guestUser); }); - it('should not add guest users to more rooms than defined in the license', async function () { - // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, - // ideally we should have a single CI job that adds a license and runs both CE and EE tests. - if (!process.env.IS_EE) { - this.skip(); - } - const promises = []; + describe('guest users', () => { + it('should not add guest users to more rooms than defined in the license', async function () { + // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, + // ideally we should have a single CI job that adds a license and runs both CE and EE tests. + if (!process.env.IS_EE) { + this.skip(); + } + const promises = []; + + for (let i = 0; i < maxRoomsPerGuest; i++) { + promises.push( + createRoom({ + type: 'p', + name: `channel.test.${Date.now()}-${Math.random()}`, + members: [guestUser.username], + }), + ); + } + await Promise.all(promises); - for (let i = 0; i < maxRoomsPerGuest; i++) { - promises.push( - createRoom({ - type: 'p', + await request + .post(api('groups.create')) + .set(credentials) + .send({ name: `channel.test.${Date.now()}-${Math.random()}`, members: [guestUser.username], - }), - ); - } - await Promise.all(promises); + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + room = res.body.group; + }); - request - .post(api('groups.create')) - .set(credentials) - .send({ - name: `channel.test.${Date.now()}-${Math.random()}`, - members: [guestUser.username], - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - room = res.body.group; - }) - .then(() => { - request - .get(api('groups.members')) - .set(credentials) - .query({ - roomId: room._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('members').and.to.be.an('array'); - expect(res.body.members).to.have.lengthOf(1); - }); - }); + await request + .get(api('groups.members')) + .set(credentials) + .query({ + roomId: room._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body.members).to.have.lengthOf(1); + }); + }); }); - }); - describe('/groups.create (encrypted)', () => { - it('should create a new encrypted group', async () => { - await request - .post(api('groups.create')) - .set(credentials) - .send({ - name: `encrypted-${apiPrivateChannelName}`, - extraData: { - encrypted: true, - }, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group.name', `encrypted-${apiPrivateChannelName}`); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', 0); - expect(res.body).to.have.nested.property('group.encrypted', true); - }); + + describe('validate E2E rooms', () => { + it('should create a new encrypted group', async () => { + await request + .post(api('groups.create')) + .set(credentials) + .send({ + name: `encrypted-${apiPrivateChannelName}`, + extraData: { + encrypted: true, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group.name', `encrypted-${apiPrivateChannelName}`); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + expect(res.body).to.have.nested.property('group.encrypted', true); + }); + }); }); - it('should create the encrypted room by default', async () => { - await updateSetting('E2E_Enabled_Default_PrivateRooms', true); - try { + describe('E2E enabled by default', () => { + before(async () => { + await Promise.all([updateSetting('E2E_Enable', true), updateSetting('E2E_Enabled_Default_PrivateRooms', true)]); + }); + + after(async () => { + await Promise.all([updateSetting('E2E_Enable', false), updateSetting('E2E_Enabled_Default_PrivateRooms', false)]); + }); + + it('should create the encrypted room by default', async () => { await request .post(api('groups.create')) .set(credentials) @@ -149,30 +169,43 @@ describe('[Groups]', function () { expect(res.body).to.have.nested.property('group.msgs', 0); expect(res.body).to.have.nested.property('group.encrypted', true); }); - } finally { - await updateSetting('E2E_Enabled_Default_PrivateRooms', false); - } + }); }); }); - describe('[/groups.info]', () => { + + describe('/groups.info', () => { let testGroup = {}; let groupMessage = {}; - it('creating new group...', (done) => { - request + + const newGroupInfoName = `info-private-channel-test-${Date.now()}`; + + before('creating new group...', async () => { + await request .post(api('groups.create')) .set(credentials) .send({ - name: apiPrivateChannelName, + name: newGroupInfoName, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { testGroup = res.body.group; + }); + }); + + after('deleting group...', async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: newGroupInfoName, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(200); }); - it('should return group basic structure', (done) => { - request + + it('should return group basic structure', async () => { + await request .get(api('groups.info')) .set(credentials) .query({ @@ -183,14 +216,14 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); + expect(res.body).to.have.nested.property('group.name', newGroupInfoName); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.msgs', 0); - }) - .end(done); + }); }); - it('sending a message...', (done) => { - request + + it('sending a message...', async () => { + await request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -204,11 +237,11 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); groupMessage = res.body.message; - }) - .end(done); + }); }); - it('REACTing with last message', (done) => { - request + + it('REACTing with last message', async () => { + await request .post(api('chat.react')) .set(credentials) .send({ @@ -219,11 +252,11 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - it('STARring last message', (done) => { - request + + it('STARring last message', async () => { + await request .post(api('chat.starMessage')) .set(credentials) .send({ @@ -233,11 +266,11 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - it('PINning last message', (done) => { - request + + it('PINning last message', async () => { + await request .post(api('chat.pinMessage')) .set(credentials) .send({ @@ -247,11 +280,11 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - it('should return group structure with "lastMessage" object including pin, reaction and star(should be an array) infos', (done) => { - request + + it('should return group structure with "lastMessage" object including pin, reaction and star(should be an array) infos', async () => { + await request .get(api('groups.info')) .set(credentials) .query({ @@ -269,11 +302,10 @@ describe('[Groups]', function () { expect(group.lastMessage).to.have.property('pinnedAt').and.to.be.a('string'); expect(group.lastMessage).to.have.property('pinnedBy').and.to.be.an('object'); expect(group.lastMessage).to.have.property('starred').and.to.be.an('array'); - }) - .end(done); + }); }); - it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY', (done) => { - request + it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY', async () => { + await request .get(api('groups.messages')) .set(credentials) .query({ @@ -288,11 +320,11 @@ describe('[Groups]', function () { const lastMessage = messages.filter((message) => message._id === groupMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); - }) - .end(done); + }); }); - it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY even requested with count and offset params', (done) => { - request + + it('should return all groups messages where the last message of array should have the "star" array with USERS star ONLY even requested with count and offset params', async () => { + await request .get(api('groups.messages')) .set(credentials) .query({ @@ -309,178 +341,161 @@ describe('[Groups]', function () { const lastMessage = messages.filter((message) => message._id === groupMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); - }) - .end(done); + }); }); }); - it('/groups.invite', async () => { - const roomInfo = await getRoomInfo(group._id); - return request - .post(api('groups.invite')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); - }); - }); + describe('/groups.invite', async () => { + let roomInfo = {}; - it('/groups.addModerator', (done) => { - request - .post(api('groups.addModerator')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); + before(async () => { + roomInfo = await getRoomInfo(group._id); + }); - it('/groups.removeModerator', (done) => { - request - .post(api('groups.removeModerator')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + it('should invite user to group', async () => { + await request + .post(api('groups.invite')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); + }); + }); }); - it('/groups.addOwner', (done) => { - request - .post(api('groups.addOwner')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.addModerator', () => { + it('should make user a moderator', (done) => { + request + .post(api('groups.addModerator')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.removeOwner', (done) => { - request - .post(api('groups.removeOwner')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.removeModerator', () => { + it('should remove user from moderator', (done) => { + request + .post(api('groups.removeModerator')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.addLeader', (done) => { - request - .post(api('groups.addLeader')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.a.property('success', true); - }) - .end(done); + describe('/groups.addOwner', () => { + it('should add user as owner', (done) => { + request + .post(api('groups.addOwner')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.removeLeader', (done) => { - request - .post(api('groups.removeLeader')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.removeOwner', () => { + it('should remove user from owner', (done) => { + request + .post(api('groups.removeOwner')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.kick', (done) => { - request - .post(api('groups.kick')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.addLeader', () => { + it('should add user as leader', (done) => { + request + .post(api('groups.addLeader')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }) + .end(done); + }); }); - it('/groups.invite', async () => { - const roomInfo = await getRoomInfo(group._id); - - return request - .post(api('groups.invite')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); - }); + describe('/groups.removeLeader', () => { + it('should remove user from leader', (done) => { + request + .post(api('groups.removeLeader')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.addOwner', (done) => { - request - .post(api('groups.addOwner')) - .set(credentials) - .send({ - roomId: group._id, - userId: 'rocket.cat', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.kick', () => { + it('should remove user from group', (done) => { + request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: group._id, + userId: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); describe('/groups.setDescription', () => { @@ -623,114 +638,124 @@ describe('[Groups]', function () { }); }); - it('/groups.archive', (done) => { - request - .post(api('groups.archive')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.archive', () => { + it('should archive the group', (done) => { + request + .post(api('groups.archive')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.unarchive', (done) => { - request - .post(api('groups.unarchive')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.unarchive', () => { + it('should unarchive the group', (done) => { + request + .post(api('groups.unarchive')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.close', (done) => { - request - .post(api('groups.close')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); + describe('/groups.close', () => { + it('should close the group', (done) => { + request + .post(api('groups.close')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); - it('/groups.close', (done) => { - request - .post(api('groups.close')) - .set(credentials) - .send({ - roomName: apiPrivateChannelName, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', `The private group, ${apiPrivateChannelName}, is already closed to the sender`); - }) - .end(done); + it('should return an error when trying to close a private group that is already closed', (done) => { + request + .post(api('groups.close')) + .set(credentials) + .send({ + roomName: apiPrivateChannelName, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', `The private group, ${apiPrivateChannelName}, is already closed to the sender`); + }) + .end(done); + }); }); - it('/groups.open', (done) => { - request - .post(api('groups.open')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.open', () => { + it('should open the group', (done) => { + request + .post(api('groups.open')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it('/groups.list', (done) => { - request - .get(api('groups.list')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('count'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('groups').and.to.be.an('array'); - }) - .end(done); - }); + describe('/groups.list', () => { + it('should list the groups the caller is part of', (done) => { + request + .get(api('groups.list')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('groups').and.to.be.an('array'); + }) + .end(done); + }); - it('/groups.list should return a list of zero length if not a member of any group', async () => { - const user = await createUser(); - const newCreds = await login(user.username, password); - request - .get(api('groups.list')) - .set(newCreds) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('count').and.to.equal(0); - expect(res.body).to.have.property('total').and.to.equal(0); - expect(res.body).to.have.property('groups').and.to.be.an('array').and.that.has.lengthOf(0); - }); + it('should return a list of zero length if not a member of any group', async () => { + const user = await createUser(); + const newCreds = await login(user.username, password); + await request + .get(api('groups.list')) + .set(newCreds) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count').and.to.equal(0); + expect(res.body).to.have.property('total').and.to.equal(0); + expect(res.body).to.have.property('groups').and.to.be.an('array').and.that.has.lengthOf(0); + }); + }); }); - describe('[/groups.online]', () => { + describe('/groups.online', () => { const createUserAndChannel = async (setAsOnline = true) => { const testUser = await createUser(); const testUserCredentials = await login(testUser.username, password); @@ -812,7 +837,7 @@ describe('[Groups]', function () { const { room } = await createUserAndChannel(); - return request + await request .get(api('groups.online')) .set(outsiderCredentials) .query(`query={"_id": "${room._id}"}`) @@ -823,6 +848,7 @@ describe('[Groups]', function () { }); }); }); + describe('/groups.members', () => { it('should return group members when searching by roomId', (done) => { request @@ -864,7 +890,7 @@ describe('[Groups]', function () { }); }); - describe('[/groups.files]', async () => { + describe('/groups.files', async () => { await testFileUploads('groups.files', group); }); @@ -899,192 +925,204 @@ describe('[Groups]', function () { }); }); - it('/groups.counters', (done) => { - request - .get(api('groups.counters')) - .set(credentials) - .query({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('joined', true); - expect(res.body).to.have.property('members'); - expect(res.body).to.have.property('unreads'); - expect(res.body).to.have.property('unreadsFrom'); - expect(res.body).to.have.property('msgs'); - expect(res.body).to.have.property('latest'); - expect(res.body).to.have.property('userMentions'); - }) - .end(done); + describe('/groups.counters', () => { + it('should return group counters', (done) => { + request + .get(api('groups.counters')) + .set(credentials) + .query({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('joined', true); + expect(res.body).to.have.property('members'); + expect(res.body).to.have.property('unreads'); + expect(res.body).to.have.property('unreadsFrom'); + expect(res.body).to.have.property('msgs'); + expect(res.body).to.have.property('latest'); + expect(res.body).to.have.property('userMentions'); + }) + .end(done); + }); }); - it('/groups.rename', async () => { - const roomInfo = await getRoomInfo(group._id); + describe('/groups.rename', async () => { + let roomInfo; + before(async () => { + roomInfo = await getRoomInfo(group._id); + }); - return request - .post(api('groups.rename')) - .set(credentials) - .send({ - roomId: group._id, - name: `EDITED${apiPrivateChannelName}`, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', `EDITED${apiPrivateChannelName}`); - expect(res.body).to.have.nested.property('group.t', 'p'); - expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); - }); + it('should return the group rename with an additional message', async () => { + await request + .post(api('groups.rename')) + .set(credentials) + .send({ + roomId: group._id, + name: `EDITED${apiPrivateChannelName}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', `EDITED${apiPrivateChannelName}`); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', roomInfo.group.msgs + 1); + }); + }); }); describe('/groups.getIntegrations', () => { let integrationCreatedByAnUser; - let userCredentials; let createdGroup; - before((done) => { - createRoom({ name: `test-integration-group-${Date.now()}`, type: 'p' }).end((err, res) => { - createdGroup = res.body.group; - createUser().then((createdUser) => { - const user = createdUser; - login(user.username, password).then((credentials) => { - userCredentials = credentials; - updatePermission('manage-incoming-integrations', ['user']).then(() => { - updatePermission('manage-own-incoming-integrations', ['user']).then(() => { - createIntegration( - { - type: 'webhook-incoming', - name: 'Incoming test', - enabled: true, - alias: 'test', - username: 'rocket.cat', - scriptEnabled: false, - overrideDestinationChannelEnabled: true, - channel: `#${createdGroup.name}`, - }, - userCredentials, - ).then((integration) => { - integrationCreatedByAnUser = integration; - done(); - }); - }); - }); - }); - }); - }); + + before(async () => { + const resRoom = await createRoom({ name: `test-integration-group-${Date.now()}`, type: 'p' }); + + createdGroup = resRoom.body.group; + + const user = await createUser(); + + const userCredentials = await login(user.username, password); + + await Promise.all([ + updatePermission('manage-incoming-integrations', ['user']), + updatePermission('manage-own-incoming-integrations', ['user']), + ]); + + integrationCreatedByAnUser = await createIntegration( + { + type: 'webhook-incoming', + name: 'Incoming test', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: false, + overrideDestinationChannelEnabled: true, + channel: `#${createdGroup.name}`, + }, + userCredentials, + ); }); - after((done) => { - removeIntegration(integrationCreatedByAnUser._id, 'incoming').then(done); + after(async () => { + await removeIntegration(integrationCreatedByAnUser._id, 'incoming'); + + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-outgoing-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + updatePermission('manage-own-outgoing-integrations', ['admin']), + ]); }); - it('should return the list of integrations of create group and it should contain the integration created by user when the admin DOES have the permission', (done) => { - updatePermission('manage-incoming-integrations', ['admin']).then(() => { - request - .get(api('groups.getIntegrations')) - .set(credentials) - .query({ - roomId: createdGroup._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( - (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, - ); - expect(integrationCreated).to.be.an('object'); - expect(integrationCreated._id).to.be.equal(integrationCreatedByAnUser._id); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - }) - .end(done); - }); + it('should return the list of integrations of create group and it should contain the integration created by user when the admin DOES have the permission', async () => { + await updatePermission('manage-incoming-integrations', ['admin']); + + await request + .get(api('groups.getIntegrations')) + .set(credentials) + .query({ + roomId: createdGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + const integrationCreated = res.body.integrations.find( + (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, + ); + expect(integrationCreated).to.be.an('object'); + expect(integrationCreated._id).to.be.equal(integrationCreatedByAnUser._id); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + }); }); - it('should return the list of integrations created by the user only', (done) => { - updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - updatePermission('manage-incoming-integrations', []).then(() => { - request - .get(api('groups.getIntegrations')) - .set(credentials) - .query({ - roomId: createdGroup._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( - (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, - ); - expect(integrationCreated).to.be.equal(undefined); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - }) - .end(done); + it('should return the list of integrations created by the user only', async () => { + await Promise.all([ + updatePermission('manage-own-incoming-integrations', ['admin']), + updatePermission('manage-incoming-integrations', []), + ]); + + await request + .get(api('groups.getIntegrations')) + .set(credentials) + .query({ + roomId: createdGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + const integrationCreated = res.body.integrations.find( + (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, + ); + expect(integrationCreated).to.be.equal(undefined); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); }); - }); }); - it('should return unauthorized error when the user does not have any integrations permissions', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-own-outgoing-integrations', []).then(() => { - request - .get(api('groups.getIntegrations')) - .set(credentials) - .query({ - roomId: createdGroup._id, - }) - .expect('Content-Type', 'application/json') - .expect(403) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'unauthorized'); - }) - .end(done); - }); - }); + it('should return unauthorized error when the user does not have any integrations permissions', async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', []), + updatePermission('manage-outgoing-integrations', []), + updatePermission('manage-own-incoming-integrations', []), + updatePermission('manage-own-outgoing-integrations', []), + ]); + + await request + .get(api('groups.getIntegrations')) + .set(credentials) + .query({ + roomId: createdGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'unauthorized'); }); - }); }); }); - it('/groups.setReadOnly', (done) => { - request - .post(api('groups.setReadOnly')) - .set(credentials) - .send({ - roomId: group._id, - readOnly: true, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe('/groups.setReadOnly', () => { + it('should set the group as read only', (done) => { + request + .post(api('groups.setReadOnly')) + .set(credentials) + .send({ + roomId: group._id, + readOnly: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); - it.skip('/groups.leave', (done) => { - request - .post(api('groups.leave')) - .set(credentials) - .send({ - roomId: group._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + describe.skip('/groups.leave', () => { + it('should allow the user to leave the group', (done) => { + request + .post(api('groups.leave')) + .set(credentials) + .send({ + roomId: group._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); }); describe('/groups.setAnnouncement', () => { @@ -1123,39 +1161,101 @@ describe('[Groups]', function () { }); describe('/groups.setType', () => { - it('should change the type of the group to a channel', (done) => { - request + let roomTypeId; + + before(async () => { + await request + .post(api('groups.create')) + .set(credentials) + .send({ + name: `channel.type.${Date.now()}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + + roomTypeId = res.body.group._id; + }); + }); + + after(async () => { + await request + .post(api('channels.delete')) + .set(credentials) + .send({ + roomId: roomTypeId, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it('should change the type of the group to a channel', async () => { + await request .post(api('groups.setType')) .set(credentials) .send({ - roomId: group._id, + roomId: roomTypeId, type: 'c', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + expect(res.body).to.have.nested.property('group.t', 'c'); + }); }); }); - describe('/groups.setCustomFields:', () => { + describe('/groups.setCustomFields', () => { let cfchannel; - it('create group with customFields', (done) => { + let groupWithoutCustomFields; + + before('create group with customFields', async () => { const customFields = { field0: 'value0' }; - request + + await request .post(api('groups.create')) .set(credentials) .send({ name: `channel.cf.${Date.now()}`, customFields, }) - .end((err, res) => { + .expect((res) => { cfchannel = res.body.group; - done(); }); + + await request + .post(api('groups.create')) + .set(credentials) + .send({ + name: `channel.cf.${Date.now()}`, + }) + .expect((res) => { + groupWithoutCustomFields = res.body.group; + }); + }); + + after('delete group with customFields', async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: cfchannel.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: groupWithoutCustomFields.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); }); + it('get customFields using groups.info', (done) => { request .get(api('groups.info')) @@ -1173,7 +1273,7 @@ describe('[Groups]', function () { }); it('change customFields', async () => { const customFields = { field9: 'value9' }; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ @@ -1206,39 +1306,14 @@ describe('[Groups]', function () { }) .end(done); }); - it('delete group with customFields', (done) => { - request - .post(api('groups.delete')) - .set(credentials) - .send({ - roomName: cfchannel.name, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - it('create group without customFields', (done) => { - request - .post(api('groups.create')) - .set(credentials) - .send({ - name: `channel.cf.${Date.now()}`, - }) - .end((err, res) => { - cfchannel = res.body.group; - done(); - }); - }); + it('set customFields with one nested field', async () => { const customFields = { field1: 'value1' }; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomId: cfchannel._id, + roomId: groupWithoutCustomFields._id, customFields, }) .expect('Content-Type', 'application/json') @@ -1246,7 +1321,7 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.name', groupWithoutCustomFields.name); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.customFields.field1', 'value1'); }); @@ -1254,11 +1329,11 @@ describe('[Groups]', function () { it('set customFields with multiple nested fields', async () => { const customFields = { field2: 'value2', field3: 'value3', field4: 'value4' }; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomName: cfchannel.name, + roomName: groupWithoutCustomFields.name, customFields, }) .expect('Content-Type', 'application/json') @@ -1266,7 +1341,7 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.name', groupWithoutCustomFields.name); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.customFields.field2', 'value2'); expect(res.body).to.have.nested.property('group.customFields.field3', 'value3'); @@ -1277,11 +1352,11 @@ describe('[Groups]', function () { it('set customFields to empty object', async () => { const customFields = {}; - return request + await request .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomName: cfchannel.name, + roomName: groupWithoutCustomFields.name, customFields, }) .expect('Content-Type', 'application/json') @@ -1289,7 +1364,7 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('group._id'); - expect(res.body).to.have.nested.property('group.name', cfchannel.name); + expect(res.body).to.have.nested.property('group.name', groupWithoutCustomFields.name); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.not.nested.property('group.customFields.field2', 'value2'); expect(res.body).to.have.not.nested.property('group.customFields.field3', 'value3'); @@ -1303,7 +1378,7 @@ describe('[Groups]', function () { .post(api('groups.setCustomFields')) .set(credentials) .send({ - roomName: cfchannel.name, + roomName: groupWithoutCustomFields.name, customFields, }) .expect('Content-Type', 'application/json') @@ -1313,37 +1388,25 @@ describe('[Groups]', function () { }) .end(done); }); - it('delete group with empty customFields', (done) => { - request - .post(api('groups.delete')) - .set(credentials) - .send({ - roomName: cfchannel.name, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); }); describe('/groups.delete', () => { let testGroup; - it('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.test.${Date.now()}`, }) - .end((err, res) => { + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { testGroup = res.body.group; - done(); }); }); - it('/groups.delete', (done) => { + + it('should delete group', (done) => { request .post(api('groups.delete')) .set(credentials) @@ -1357,7 +1420,8 @@ describe('[Groups]', function () { }) .end(done); }); - it('/groups.info', (done) => { + + it('should return group not found', (done) => { request .get(api('groups.info')) .set(credentials) @@ -1376,18 +1440,31 @@ describe('[Groups]', function () { describe('/groups.roles', () => { let testGroup; - it('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.roles.test.${Date.now()}`, }) - .end((err, res) => { + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { testGroup = res.body.group; - done(); }); }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: testGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + it('/groups.invite', (done) => { request .post(api('groups.invite')) @@ -1451,18 +1528,31 @@ describe('[Groups]', function () { describe('/groups.moderators', () => { let testGroup; - it('/groups.create', (done) => { - request + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.roles.test.${Date.now()}`, }) - .end((err, res) => { + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { testGroup = res.body.group; - done(); }); }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: testGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + it('/groups.invite', (done) => { request .post(api('groups.invite')) @@ -1503,17 +1593,35 @@ describe('[Groups]', function () { describe('/groups.setEncrypted', () => { let testGroup; - it('/groups.create', (done) => { - request + + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group.encrypted.test.${Date.now()}`, }) - .end((err, res) => { + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + testGroup = res.body.group; - done(); }); + + await updateSetting('E2E_Enable', true); + }); + + after(async () => { + await updateSetting('E2E_Enable', false); + + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: testGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(200); }); it('should return an error when passing no boolean param', (done) => { @@ -1584,16 +1692,28 @@ describe('[Groups]', function () { }); describe('/groups.convertToTeam', () => { - before((done) => { - request + let newGroup; + + before(async () => { + await request .post(api('groups.create')) .set(credentials) .send({ name: `group-${Date.now()}` }) .expect(200) .expect((response) => { - this.newGroup = response.body.group; + newGroup = response.body.group; + }); + }); + + after(async () => { + await request + .post(api('groups.delete')) + .set(credentials) + .send({ + roomName: newGroup.name, }) - .then(() => done()); + .expect('Content-Type', 'application/json') + .expect(200); }); it('should fail to convert group if lacking edit-room permission', (done) => { @@ -1602,7 +1722,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(403) .expect((res) => { expect(res.body).to.have.a.property('success', false); @@ -1618,7 +1738,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(403) .expect((res) => { expect(res.body).to.have.a.property('success', false); @@ -1634,7 +1754,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(200) .expect((res) => { expect(res.body).to.have.a.property('success', true); @@ -1652,7 +1772,7 @@ describe('[Groups]', function () { request .post(api('groups.convertToTeam')) .set(credentials) - .send({ roomId: this.newGroup._id }) + .send({ roomId: newGroup._id }) .expect(400) .expect((res) => { expect(res.body).to.have.a.property('success', false); @@ -1693,6 +1813,7 @@ describe('[Groups]', function () { expect(res.body).to.have.property('success', true); }); }); + after(async () => { await updateSetting('UI_Use_Real_Name', false); diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index d7ccb734071b..d3dff1f3d805 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index d7ccb734071b..d3dff1f3d805 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 5250e48bf106..19fef1639db5 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -25,15 +25,23 @@ COPY ./packages/ui-contexts/dist packages/ui-contexts/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist -COPY ./packages/instance-status/package.json packages/instance-status/package.json -COPY ./packages/instance-status/dist packages/instance-status/dist - COPY ./packages/logger/package.json packages/logger/package.json COPY ./packages/logger/dist packages/logger/dist +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + +COPY ./packages/instance-status/package.json packages/instance-status/package.json +COPY ./packages/instance-status/dist packages/instance-status/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 95fb836e9f27..2c3c22a998c3 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist @@ -31,9 +42,6 @@ COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist COPY ./packages/tools/package.json packages/tools/package.json COPY ./packages/tools/dist packages/tools/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index f85c45246f29..9a056c4fde3d 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -22,9 +22,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json COPY ./packages/ui-contexts/dist packages/ui-contexts/dist diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 95fb836e9f27..2c3c22a998c3 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -19,9 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist @@ -31,9 +42,6 @@ COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist COPY ./packages/tools/package.json packages/tools/package.json COPY ./packages/tools/dist packages/tools/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index c06115c887f5..d3dff1f3d805 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -19,12 +19,20 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist COPY ./packages/logger/package.json packages/logger/package.json COPY ./packages/logger/dist packages/logger/dist +COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/packages/license/.eslintrc.json b/ee/packages/license/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/ee/packages/license/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/license/__tests__/MockedLicenseBuilder.ts b/ee/packages/license/__tests__/MockedLicenseBuilder.ts new file mode 100644 index 000000000000..316261744da5 --- /dev/null +++ b/ee/packages/license/__tests__/MockedLicenseBuilder.ts @@ -0,0 +1,209 @@ +import { LicenseImp } from '../src'; +import type { ILicenseTag } from '../src/definition/ILicenseTag'; +import type { ILicenseV3 } from '../src/definition/ILicenseV3'; +import type { LicenseLimit } from '../src/definition/LicenseLimit'; +import type { LicenseModule } from '../src/definition/LicenseModule'; +import type { LicensePeriod, Timestamp } from '../src/definition/LicensePeriod'; +import { encrypt } from '../src/token'; + +export class MockedLicenseBuilder { + information: { + id?: string; + autoRenew: boolean; + visualExpiration: Timestamp; + notifyAdminsAt?: Timestamp; + notifyUsersAt?: Timestamp; + trial: boolean; + offline: boolean; + createdAt: Timestamp; + grantedBy: { + method: 'manual' | 'self-service' | 'sales' | 'support' | 'reseller'; + seller?: string; + }; + grantedTo?: { + name?: string; + company?: string; + email?: string; + }; + legalText?: string; + notes?: string; + tags?: ILicenseTag[]; + }; + + validation: { + serverUrls: { + value: string; + type: 'url' | 'regex' | 'hash'; + }[]; + + serverVersions?: { + value: string; + }[]; + + serverUniqueId?: string; + cloudWorkspaceId?: string; + validPeriods: LicensePeriod[]; + legalTextAgreement?: { + type: 'required' | 'not-required' | 'accepted'; + acceptedVia?: 'cloud'; + }; + + statisticsReport: { + required: boolean; + allowedStaleInDays?: number; + }; + }; + + constructor() { + this.information = { + autoRenew: true, + // expires in 1 year + visualExpiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(), + // 15 days before expiration + notifyAdminsAt: new Date(new Date().setDate(new Date().getDate() + 15)).toISOString(), + // 30 days before expiration + notifyUsersAt: new Date(new Date().setDate(new Date().getDate() + 30)).toISOString(), + trial: false, + offline: false, + createdAt: new Date().toISOString(), + grantedBy: { + method: 'manual', + seller: 'Rocket.Cat', + }, + tags: [ + { + name: 'Test', + color: 'blue', + }, + ], + }; + + this.validation = { + serverUrls: [ + { + value: 'localhost:3000', + type: 'url', + }, + ], + serverVersions: [ + { + value: '3.0.0', + }, + ], + + serverUniqueId: '1234567890', + cloudWorkspaceId: '1234567890', + + validPeriods: [ + { + invalidBehavior: 'disable_modules', + modules: ['livechat-enterprise'], + validFrom: new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString(), + validUntil: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(), + }, + ], + + statisticsReport: { + required: true, + allowedStaleInDays: 30, + }, + }; + } + + public resetValidPeriods(): this { + this.validation.validPeriods = []; + return this; + } + + public withValidPeriod(period: LicensePeriod): this { + this.validation.validPeriods.push(period); + return this; + } + + public withGrantedTo(grantedTo: { name?: string; company?: string; email?: string }): this { + this.information.grantedTo = grantedTo; + return this; + } + + grantedModules: { + module: LicenseModule; + }[]; + + limits: { + activeUsers?: LicenseLimit[]; + guestUsers?: LicenseLimit[]; + roomsPerGuest?: LicenseLimit<'prevent_action'>[]; + privateApps?: LicenseLimit[]; + marketplaceApps?: LicenseLimit[]; + monthlyActiveContacts?: LicenseLimit[]; + }; + + cloudMeta?: Record; + + public withServerUrls(urls: { value: string; type: 'url' | 'regex' | 'hash' }): this { + this.validation.serverUrls = this.validation.serverUrls ?? []; + this.validation.serverUrls.push(urls); + return this; + } + + public withServerVersions(versions: { value: string }): this { + this.validation.serverVersions = this.validation.serverVersions ?? []; + this.validation.serverVersions.push(versions); + return this; + } + + public withGratedModules(modules: LicenseModule[]): this { + this.grantedModules = this.grantedModules ?? []; + this.grantedModules.push(...modules.map((module) => ({ module }))); + return this; + } + + withNoGratedModules(modules: LicenseModule[]): this { + this.grantedModules = this.grantedModules ?? []; + this.grantedModules = this.grantedModules.filter(({ module }) => !modules.includes(module)); + return this; + } + + public withLimits(key: K, value: ILicenseV3['limits'][K]): this { + this.limits = this.limits ?? {}; + this.limits[key] = value; + return this; + } + + public build(): ILicenseV3 { + return { + version: '3.0', + information: this.information, + validation: this.validation, + grantedModules: [...new Set(this.grantedModules)], + limits: { + activeUsers: [], + guestUsers: [], + roomsPerGuest: [], + privateApps: [], + marketplaceApps: [], + monthlyActiveContacts: [], + ...this.limits, + }, + cloudMeta: this.cloudMeta, + }; + } + + public sign(): Promise { + return encrypt(this.build()); + } +} + +export const getReadyLicenseManager = async () => { + const license = new LicenseImp(); + await license.setWorkspaceUrl('http://localhost:3000'); + await license.setWorkspaceUrl('http://localhost:3000'); + + license.setLicenseLimitCounter('activeUsers', () => 0); + license.setLicenseLimitCounter('guestUsers', () => 0); + license.setLicenseLimitCounter('roomsPerGuest', async () => 0); + license.setLicenseLimitCounter('privateApps', () => 0); + license.setLicenseLimitCounter('marketplaceApps', () => 0); + license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); + return license; +}; diff --git a/ee/packages/license/__tests__/emitter.spec.ts b/ee/packages/license/__tests__/emitter.spec.ts new file mode 100644 index 000000000000..4c7c5a8255d1 --- /dev/null +++ b/ee/packages/license/__tests__/emitter.spec.ts @@ -0,0 +1,66 @@ +/** + * @jest-environment node + */ + +import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder'; + +describe('Event License behaviors', () => { + it('should call the module as they are enabled/disabled', async () => { + const license = await getReadyLicenseManager(); + const validFn = jest.fn(); + const invalidFn = jest.fn(); + + license.onValidFeature('livechat-enterprise', validFn); + license.onInvalidFeature('livechat-enterprise', invalidFn); + + const mocked = await new MockedLicenseBuilder(); + const oldToken = await mocked.sign(); + + const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign(); + + // apply license + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.hasModule('livechat-enterprise')).toBe(false); + + await expect(validFn).not.toBeCalled(); + await expect(invalidFn).toBeCalledTimes(1); + + // apply license containing livechat-enterprise module + + validFn.mockClear(); + invalidFn.mockClear(); + + await expect(license.setLicense(newToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(true); + + await expect(validFn).toBeCalledTimes(1); + await expect(invalidFn).toBeCalledTimes(0); + + // apply the old license again + + validFn.mockClear(); + invalidFn.mockClear(); + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(false); + await expect(validFn).toBeCalledTimes(0); + await expect(invalidFn).toBeCalledTimes(1); + }); + + it('should call `onValidateLicense` when a valid license is applied', async () => { + const license = await getReadyLicenseManager(); + const fn = jest.fn(); + + license.onValidateLicense(fn); + + const mocked = await new MockedLicenseBuilder(); + const token = await mocked.sign(); + + await expect(license.setLicense(token)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(fn).toBeCalledTimes(1); + }); +}); diff --git a/ee/packages/license/__tests__/setLicense.spec.ts b/ee/packages/license/__tests__/setLicense.spec.ts new file mode 100644 index 000000000000..962f591750ad --- /dev/null +++ b/ee/packages/license/__tests__/setLicense.spec.ts @@ -0,0 +1,103 @@ +/** + * @jest-environment node + */ + +import { LicenseImp } from '../src'; +import { DuplicatedLicenseError } from '../src/errors/DuplicatedLicenseError'; +import { InvalidLicenseError } from '../src/errors/InvalidLicenseError'; +import { NotReadyForValidation } from '../src/errors/NotReadyForValidation'; +import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder'; + +// Same license used on ci tasks so no I didnt leak it +const VALID_LICENSE = + 'WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY='; + +describe('License set license procedures', () => { + describe('Invalid formats', () => { + it('by default it should have no license', async () => { + const license = new LicenseImp(); + + expect(license.hasValidLicense()).toBe(false); + expect(license.getLicense()).toBeUndefined(); + }); + + it('should throw an error if the license applied is empty', async () => { + const license = new LicenseImp(); + await expect(license.setLicense('')).rejects.toThrow(InvalidLicenseError); + }); + + it('should throw an error if the license applied is invalid', async () => { + const license = new LicenseImp(); + await expect(license.setLicense('invalid')).rejects.toThrow(InvalidLicenseError); + }); + }); + + it('should throw an error if the license is duplicated', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(DuplicatedLicenseError); + }); + + it('should keep a valid license if a new invalid license is applied', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.setLicense('invalid')).rejects.toThrow(InvalidLicenseError); + await expect(license.hasValidLicense()).toBe(true); + }); + + describe('Pending cases', () => { + it('should return an error if the license is not ready for validation yet - missing workspace url', async () => { + const license = new LicenseImp(); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(NotReadyForValidation); + }); + + it('should return an error if the license is not ready for validation yet - missing counters', async () => { + const license = new LicenseImp(); + await license.setWorkspaceUrl('http://localhost:3000'); + + expect(license.getWorkspaceUrl()).toBe('localhost:3000'); + + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(NotReadyForValidation); + + await expect(license.hasValidLicense()).toBe(false); + }); + + it('should return a valid license if the license is ready for validation', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + }); + }); + + describe('License V3', () => { + it('should return a valid license if the license is ready for validation', async () => { + const license = await getReadyLicenseManager(); + const token = await new MockedLicenseBuilder().sign(); + + await expect(license.setLicense(token)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + }); + + it('should accept new licenses', async () => { + const license = await getReadyLicenseManager(); + const mocked = await new MockedLicenseBuilder(); + const oldToken = await mocked.sign(); + + const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign(); + + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.hasModule('livechat-enterprise')).toBe(false); + + await expect(license.setLicense(newToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(true); + }); + }); +}); diff --git a/ee/packages/license/babel.config.json b/ee/packages/license/babel.config.json new file mode 100644 index 000000000000..e154c0813530 --- /dev/null +++ b/ee/packages/license/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-typescript"], + "plugins": [ + [ + "transform-inline-environment-variables", + { + "include": ["LICENSE_PUBLIC_KEY_V3"] + } + ] + ] +} diff --git a/ee/packages/license/jest.config.ts b/ee/packages/license/jest.config.ts new file mode 100644 index 000000000000..21121603f6e0 --- /dev/null +++ b/ee/packages/license/jest.config.ts @@ -0,0 +1,16 @@ +export default { + preset: 'ts-jest', + errorOnDeprecated: true, + modulePathIgnorePatterns: ['/dist/'], + testMatch: ['**/**.spec.ts'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, + // transformIgnorePatterns: ['!node_modules/jose'], + moduleNameMapper: { + '\\.css$': 'identity-obj-proxy', + '^jose$': require.resolve('jose'), + }, + collectCoverage: true, + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], +}; diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json new file mode 100644 index 000000000000..f6a1e7a2b7d5 --- /dev/null +++ b/ee/packages/license/package.json @@ -0,0 +1,47 @@ +{ + "name": "@rocket.chat/license", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@babel/cli": "^7.23.0", + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.22.20", + "@babel/preset-typescript": "^7.23.0", + "@swc/core": "^1.3.66", + "@swc/jest": "^0.2.26", + "@types/babel__core": "^7", + "@types/babel__preset-env": "^7", + "@types/jest": "~29.5.3", + "@types/ws": "^8.5.5", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "jest-environment-jsdom": "~29.6.1", + "jest-websocket-mock": "^2.4.0", + "ts-jest": "~29.0.5", + "typescript": "^5.2.2" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "testunit": "jest", + "build": "npm run build:types && npm run build:js", + "build:types": "tsc --emitDeclarationOnly", + "build:js": "babel src --out-dir dist --extensions \".ts,.tsx\" --source-maps inline", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + }, + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/jwt": "workspace:^", + "@rocket.chat/logger": "workspace:^" + } +} diff --git a/apps/meteor/ee/app/license/definition/ILicenseTag.ts b/ee/packages/license/src/definition/ILicenseTag.ts similarity index 100% rename from apps/meteor/ee/app/license/definition/ILicenseTag.ts rename to ee/packages/license/src/definition/ILicenseTag.ts diff --git a/apps/meteor/ee/app/license/definition/ILicense.ts b/ee/packages/license/src/definition/ILicenseV2.ts similarity index 93% rename from apps/meteor/ee/app/license/definition/ILicense.ts rename to ee/packages/license/src/definition/ILicenseV2.ts index 7ac4bafdc7b5..57d921a24907 100644 --- a/apps/meteor/ee/app/license/definition/ILicense.ts +++ b/ee/packages/license/src/definition/ILicenseV2.ts @@ -1,6 +1,6 @@ import type { ILicenseTag } from './ILicenseTag'; -export interface ILicense { +export interface ILicenseV2 { url: string; expiry: string; maxActiveUsers: number; diff --git a/ee/packages/license/src/definition/ILicenseV3.ts b/ee/packages/license/src/definition/ILicenseV3.ts new file mode 100644 index 000000000000..d3a2d7f572a3 --- /dev/null +++ b/ee/packages/license/src/definition/ILicenseV3.ts @@ -0,0 +1,64 @@ +import type { ILicenseTag } from './ILicenseTag'; +import type { LicenseLimit } from './LicenseLimit'; +import type { LicenseModule } from './LicenseModule'; +import type { LicensePeriod, Timestamp } from './LicensePeriod'; + +export interface ILicenseV3 { + version: '3.0'; + information: { + id?: string; + autoRenew: boolean; + visualExpiration: Timestamp; + notifyAdminsAt?: Timestamp; + notifyUsersAt?: Timestamp; + trial: boolean; + offline: boolean; + createdAt: Timestamp; + grantedBy: { + method: 'manual' | 'self-service' | 'sales' | 'support' | 'reseller'; + seller?: string; + }; + grantedTo?: { + name?: string; + company?: string; + email?: string; + }; + legalText?: string; + notes?: string; + tags?: ILicenseTag[]; + }; + validation: { + serverUrls: { + value: string; + type: 'url' | 'regex' | 'hash'; + }[]; + serverVersions?: { + value: string; + }[]; + serverUniqueId?: string; + cloudWorkspaceId?: string; + validPeriods: LicensePeriod[]; + legalTextAgreement?: { + type: 'required' | 'not-required' | 'accepted'; + acceptedVia?: 'cloud'; + }; + statisticsReport: { + required: boolean; + allowedStaleInDays?: number; + }; + }; + grantedModules: { + module: LicenseModule; + }[]; + limits: { + activeUsers?: LicenseLimit[]; + guestUsers?: LicenseLimit[]; + roomsPerGuest?: LicenseLimit<'prevent_action'>[]; + privateApps?: LicenseLimit[]; + marketplaceApps?: LicenseLimit[]; + monthlyActiveContacts?: LicenseLimit[]; + }; + cloudMeta?: Record; +} + +export type LicenseLimitKind = keyof ILicenseV3['limits']; diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts new file mode 100644 index 000000000000..b6d52bbfa8c5 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseBehavior.ts @@ -0,0 +1,8 @@ +import type { LicenseModule } from './LicenseModule'; + +export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules'; + +export type BehaviorWithContext = { + behavior: LicenseBehavior; + modules?: LicenseModule[]; +}; diff --git a/ee/packages/license/src/definition/LicenseLimit.ts b/ee/packages/license/src/definition/LicenseLimit.ts new file mode 100644 index 000000000000..40e5a62f597a --- /dev/null +++ b/ee/packages/license/src/definition/LicenseLimit.ts @@ -0,0 +1,7 @@ +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +export type LicenseLimit = { + max: number; + behavior: T; +} & (T extends 'disable_modules' ? { behavior: T; modules: LicenseModule[] } : { behavior: T }); diff --git a/ee/packages/license/src/definition/LicenseModule.ts b/ee/packages/license/src/definition/LicenseModule.ts new file mode 100644 index 000000000000..8ecebba1983b --- /dev/null +++ b/ee/packages/license/src/definition/LicenseModule.ts @@ -0,0 +1,18 @@ +export type LicenseModule = + | 'auditing' + | 'canned-responses' + | 'ldap-enterprise' + | 'livechat-enterprise' + | 'voip-enterprise' + | 'omnichannel-mobile-enterprise' + | 'engagement-dashboard' + | 'push-privacy' + | 'scalability' + | 'teams-mention' + | 'saml-enterprise' + | 'oauth-enterprise' + | 'device-management' + | 'federation' + | 'videoconference-enterprise' + | 'message-read-receipt' + | 'outlook-calendar'; diff --git a/ee/packages/license/src/definition/LicensePeriod.ts b/ee/packages/license/src/definition/LicensePeriod.ts new file mode 100644 index 000000000000..d9bae6198fde --- /dev/null +++ b/ee/packages/license/src/definition/LicensePeriod.ts @@ -0,0 +1,13 @@ +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +export type Timestamp = string; + +export type LicensePeriod = { + validFrom?: Timestamp; + validUntil?: Timestamp; + invalidBehavior: LicenseBehavior; +} & ({ validFrom: Timestamp } | { validUntil: Timestamp }) & + ({ invalidBehavior: 'disable_modules'; modules: LicenseModule[] } | { invalidBehavior: Exclude }); + +export type LicensePeriodBehavior = Exclude; diff --git a/ee/packages/license/src/definition/LimitContext.ts b/ee/packages/license/src/definition/LimitContext.ts new file mode 100644 index 000000000000..a2c44744bd75 --- /dev/null +++ b/ee/packages/license/src/definition/LimitContext.ts @@ -0,0 +1,5 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import type { LicenseLimitKind } from './ILicenseV3'; + +export type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts new file mode 100644 index 000000000000..65851a79c7eb --- /dev/null +++ b/ee/packages/license/src/deprecated.ts @@ -0,0 +1,38 @@ +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseManager } from './license'; +import { getModules } from './modules'; + +const getLicenseLimit = (license: ILicenseV3 | undefined, kind: LicenseLimitKind) => { + if (!license) { + return; + } + + const limitList = license.limits[kind]; + if (!limitList?.length) { + return; + } + + return Math.min(...limitList.map(({ max }) => max)); +}; + +// #TODO: Remove references to those functions + +export function getMaxActiveUsers(this: LicenseManager) { + return getLicenseLimit(this.getLicense(), 'activeUsers') ?? 0; +} + +export function getAppsConfig(this: LicenseManager) { + return { + maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? -1, + maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? -1, + }; +} + +export function getUnmodifiedLicenseAndModules(this: LicenseManager) { + if (this.valid && this.unmodifiedLicense) { + return { + license: this.unmodifiedLicense, + modules: getModules.call(this), + }; + } +} diff --git a/ee/packages/license/src/errors/DuplicatedLicenseError.ts b/ee/packages/license/src/errors/DuplicatedLicenseError.ts new file mode 100644 index 000000000000..70b962d53105 --- /dev/null +++ b/ee/packages/license/src/errors/DuplicatedLicenseError.ts @@ -0,0 +1,6 @@ +export class DuplicatedLicenseError extends Error { + constructor(message = 'Duplicated license') { + super(message); + this.name = 'DuplicatedLicense'; + } +} diff --git a/ee/packages/license/src/errors/InvalidLicenseError.ts b/ee/packages/license/src/errors/InvalidLicenseError.ts new file mode 100644 index 000000000000..a1eb328acd46 --- /dev/null +++ b/ee/packages/license/src/errors/InvalidLicenseError.ts @@ -0,0 +1,6 @@ +export class InvalidLicenseError extends Error { + constructor(message = 'Invalid license') { + super(message); + this.name = 'InvalidLicenseError'; + } +} diff --git a/ee/packages/license/src/errors/NotReadyForValidation.ts b/ee/packages/license/src/errors/NotReadyForValidation.ts new file mode 100644 index 000000000000..ccb99e054500 --- /dev/null +++ b/ee/packages/license/src/errors/NotReadyForValidation.ts @@ -0,0 +1,6 @@ +export class NotReadyForValidation extends Error { + constructor(message = 'Not ready for validation') { + super(message); + this.name = 'NotReadyForValidation'; + } +} diff --git a/ee/packages/license/src/events/deprecated.ts b/ee/packages/license/src/events/deprecated.ts new file mode 100644 index 000000000000..8ebfe4729292 --- /dev/null +++ b/ee/packages/license/src/events/deprecated.ts @@ -0,0 +1,12 @@ +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { hasModule } from '../modules'; + +// #TODO: Remove this onLicense handler +export function onLicense(this: LicenseManager, feature: LicenseModule, cb: (...args: any[]) => void): void | Promise { + if (hasModule.call(this, feature)) { + return cb(); + } + + this.once(`valid:${feature}`, cb); +} diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts new file mode 100644 index 000000000000..9d4025e4bce3 --- /dev/null +++ b/ee/packages/license/src/events/emitter.ts @@ -0,0 +1,30 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; + +export function moduleValidated(this: LicenseManager, module: LicenseModule) { + try { + this.emit('module', { module, valid: true }); + this.emit(`valid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module added event', error }); + } +} + +export function moduleRemoved(this: LicenseManager, module: LicenseModule) { + try { + this.emit('module', { module, valid: false }); + this.emit(`invalid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module removed event', error }); + } +} + +export function limitReached(this: LicenseManager, limitKind: LicenseLimitKind) { + try { + this.emit(`limitReached:${limitKind}`); + } catch (error) { + logger.error({ msg: 'Error running limit reached event', error }); + } +} diff --git a/ee/packages/license/src/events/listeners.ts b/ee/packages/license/src/events/listeners.ts new file mode 100644 index 000000000000..d6e9fb016f2c --- /dev/null +++ b/ee/packages/license/src/events/listeners.ts @@ -0,0 +1,75 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { hasModule } from '../modules'; + +export function onValidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) { + this.on(`valid:${feature}`, cb); + + if (hasModule.call(this, feature)) { + cb(); + } + + return (): void => { + this.off(`valid:${feature}`, cb); + }; +} + +export function onInvalidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) { + this.on(`invalid:${feature}`, cb); + + if (!hasModule.call(this, feature)) { + cb(); + } + + return (): void => { + this.off(`invalid:${feature}`, cb); + }; +} + +export function onToggledFeature( + this: LicenseManager, + feature: LicenseModule, + { up, down }: { up?: () => Promise | void; down?: () => Promise | void }, +): () => void { + let enabled = hasModule.call(this, feature); + + const offValidFeature = onValidFeature.bind(this)(feature, () => { + if (!enabled) { + void up?.(); + enabled = true; + } + }); + + const offInvalidFeature = onInvalidFeature.bind(this)(feature, () => { + if (enabled) { + void down?.(); + enabled = false; + } + }); + + if (enabled) { + void up?.(); + } + + return (): void => { + offValidFeature(); + offInvalidFeature(); + }; +} + +export function onModule(this: LicenseManager, cb: (...args: any[]) => void) { + this.on('module', cb); +} + +export function onValidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { + this.on('validate', cb); +} + +export function onInvalidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { + this.on('invalidate', cb); +} + +export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: (...args: any[]) => void) { + this.on(`limitReached:${limitKind}`, cb); +} diff --git a/ee/packages/license/src/events/overwriteClassOnLicense.ts b/ee/packages/license/src/events/overwriteClassOnLicense.ts new file mode 100644 index 000000000000..00a690d8f413 --- /dev/null +++ b/ee/packages/license/src/events/overwriteClassOnLicense.ts @@ -0,0 +1,26 @@ +import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; +import { onLicense } from './deprecated'; + +interface IOverrideClassProperties { + [key: string]: (...args: any[]) => any; +} + +type Class = { new (...args: any[]): any }; + +export async function overwriteClassOnLicense( + this: LicenseManager, + + license: LicenseModule, + original: Class, + overwrite: IOverrideClassProperties, +): Promise { + await onLicense.call(this, license, () => { + Object.entries(overwrite).forEach(([key, value]) => { + const originalFn = original.prototype[key]; + original.prototype[key] = function (...args: any[]): any { + return value.call(this, originalFn, ...args); + }; + }); + }); +} diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts new file mode 100644 index 000000000000..9dbd94db53ed --- /dev/null +++ b/ee/packages/license/src/index.ts @@ -0,0 +1,106 @@ +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { LimitContext } from './definition/LimitContext'; +import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; +import { onLicense } from './events/deprecated'; +import { + onInvalidFeature, + onInvalidateLicense, + onLimitReached, + onModule, + onToggledFeature, + onValidFeature, + onValidateLicense, +} from './events/listeners'; +import { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; +import { LicenseManager } from './license'; +import { getModules, hasModule } from './modules'; +import { getTags } from './tags'; +import { getCurrentValueForLicenseLimit, setLicenseLimitCounter } from './validation/getCurrentValueForLicenseLimit'; +import { validateFormat } from './validation/validateFormat'; + +export * from './definition/ILicenseTag'; +export * from './definition/ILicenseV2'; +export * from './definition/ILicenseV3'; +export * from './definition/LicenseBehavior'; +export * from './definition/LicenseLimit'; +export * from './definition/LicenseModule'; +export * from './definition/LicensePeriod'; +export * from './definition/LimitContext'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +interface License { + validateFormat: typeof validateFormat; + hasModule: typeof hasModule; + getModules: typeof getModules; + getTags: typeof getTags; + overwriteClassOnLicense: typeof overwriteClassOnLicense; + setLicenseLimitCounter: typeof setLicenseLimitCounter; + getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; + isLimitReached: (action: T, context?: Partial>) => Promise; + onValidFeature: typeof onValidFeature; + onInvalidFeature: typeof onInvalidFeature; + onToggledFeature: typeof onToggledFeature; + onModule: typeof onModule; + onValidateLicense: typeof onValidateLicense; + onInvalidateLicense: typeof onInvalidateLicense; + onLimitReached: typeof onLimitReached; + + // Deprecated: + onLicense: typeof onLicense; + // Deprecated: + getMaxActiveUsers: typeof getMaxActiveUsers; + // Deprecated: + getAppsConfig: typeof getAppsConfig; + // Deprecated: + getUnmodifiedLicenseAndModules: typeof getUnmodifiedLicenseAndModules; +} + +export class LicenseImp extends LicenseManager implements License { + validateFormat = validateFormat; + + hasModule = hasModule; + + getModules = getModules; + + getTags = getTags; + + overwriteClassOnLicense = overwriteClassOnLicense; + + public setLicenseLimitCounter = setLicenseLimitCounter; + + getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit; + + public async isLimitReached(action: T, context?: Partial>) { + return this.shouldPreventAction(action, context, 0); + } + + onValidFeature = onValidFeature; + + onInvalidFeature = onInvalidFeature; + + onToggledFeature = onToggledFeature; + + onModule = onModule; + + onValidateLicense = onValidateLicense; + + onInvalidateLicense = onInvalidateLicense; + + onLimitReached = onLimitReached; + + // Deprecated: + onLicense = onLicense; + + // Deprecated: + getMaxActiveUsers = getMaxActiveUsers; + + // Deprecated: + getAppsConfig = getAppsConfig; + + // Deprecated: + getUnmodifiedLicenseAndModules = getUnmodifiedLicenseAndModules; +} + +const license = new LicenseImp(); + +export { license as License }; diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts new file mode 100644 index 000000000000..36744585d59f --- /dev/null +++ b/ee/packages/license/src/license.spec.ts @@ -0,0 +1,42 @@ +import { MockedLicenseBuilder, getReadyLicenseManager } from '../__tests__/MockedLicenseBuilder'; + +it('should not prevent if there is no license', async () => { + const license = await getReadyLicenseManager(); + const result = await license.shouldPreventAction('activeUsers'); + expect(result).toBe(false); +}); + +it('should not prevent if the counter is under the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 5); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); +}); + +it('should prevent if the counter is equal or over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 11); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); +}); diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts new file mode 100644 index 000000000000..2fb25b0e3b4f --- /dev/null +++ b/ee/packages/license/src/license.ts @@ -0,0 +1,230 @@ +import { Emitter } from '@rocket.chat/emitter'; + +import type { ILicenseV2 } from './definition/ILicenseV2'; +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { LicenseModule } from './definition/LicenseModule'; +import type { LimitContext } from './definition/LimitContext'; +import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; +import { InvalidLicenseError } from './errors/InvalidLicenseError'; +import { NotReadyForValidation } from './errors/NotReadyForValidation'; +import { logger } from './logger'; +import { invalidateAll, replaceModules } from './modules'; +import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; +import { showLicense } from './showLicense'; +import { replaceTags } from './tags'; +import { decrypt } from './token'; +import { convertToV3 } from './v2/convertToV3'; +import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; +import { getModulesToDisable } from './validation/getModulesToDisable'; +import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; +import { isReadyForValidation } from './validation/isReadyForValidation'; +import { runValidation } from './validation/runValidation'; +import { validateFormat } from './validation/validateFormat'; + +export class LicenseManager extends Emitter< + Record<`limitReached:${LicenseLimitKind}` | `${'invalid' | 'valid'}:${LicenseModule}`, undefined> & { + validate: undefined; + invalidate: undefined; + module: { module: LicenseModule; valid: boolean }; + } +> { + dataCounters = new Map) => Promise>(); + + pendingLicense = ''; + + modules = new Set(); + + private workspaceUrl: string | undefined; + + private _license: ILicenseV3 | undefined; + + private _unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; + + private _valid: boolean | undefined; + + private _inFairPolicy: boolean | undefined; + + private _lockedLicense: string | undefined; + + public get license(): ILicenseV3 | undefined { + return this._license; + } + + public get unmodifiedLicense(): ILicenseV2 | ILicenseV3 | undefined { + return this._unmodifiedLicense; + } + + public get valid(): boolean | undefined { + return this._valid; + } + + public get inFairPolicy(): boolean { + return Boolean(this._inFairPolicy); + } + + public async setWorkspaceUrl(url: string) { + this.workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); + + if (hasPendingLicense.call(this)) { + await applyPendingLicense.call(this); + } + } + + public getWorkspaceUrl() { + return this.workspaceUrl; + } + + private clearLicenseData(): void { + this._license = undefined; + this._unmodifiedLicense = undefined; + this._inFairPolicy = undefined; + this._valid = false; + this._lockedLicense = undefined; + clearPendingLicense.call(this); + } + + private async setLicenseV3(newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3): Promise { + const hadValidLicense = this.hasValidLicense(); + this.clearLicenseData(); + + try { + this._unmodifiedLicense = originalLicense || newLicense; + this._license = newLicense; + + await this.validateLicense(); + + this._lockedLicense = encryptedLicense; + } finally { + if (hadValidLicense && !this.hasValidLicense()) { + this.emit('invalidate'); + invalidateAll.call(this); + } + } + } + + private async setLicenseV2(newLicense: ILicenseV2, encryptedLicense: string): Promise { + return this.setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); + } + + private isLicenseDuplicated(encryptedLicense: string): boolean { + return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense); + } + + private async validateLicense(): Promise { + if (!this._license) { + throw new InvalidLicenseError(); + } + + if (!isReadyForValidation.call(this)) { + throw new NotReadyForValidation(); + } + + // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license + const validationResult = await runValidation.call(this, this._license, [ + 'invalidate_license', + 'prevent_installation', + 'start_fair_policy', + 'disable_modules', + ]); + + this.processValidationResult(validationResult); + } + + public async setLicense(encryptedLicense: string): Promise { + if (!(await validateFormat(encryptedLicense))) { + throw new InvalidLicenseError(); + } + + if (this.isLicenseDuplicated(encryptedLicense)) { + // If there is a pending license but the user is trying to revert to the license that is currently active + if (hasPendingLicense.call(this) && !isPendingLicense.call(this, encryptedLicense)) { + // simply remove the pending license + clearPendingLicense.call(this); + throw new Error('Invalid license 1'); + } + + throw new DuplicatedLicenseError(); + } + + if (!isReadyForValidation.call(this)) { + // If we can't validate the license data yet, but is a valid license string, store it to validate when we can + setPendingLicense.call(this, encryptedLicense); + throw new NotReadyForValidation(); + } + + logger.info('New Enterprise License'); + try { + const decrypted = JSON.parse(await decrypt(encryptedLicense)); + + logger.debug({ msg: 'license', decrypted }); + + if (!encryptedLicense.startsWith('RCV3_')) { + await this.setLicenseV2(decrypted, encryptedLicense); + return true; + } + await this.setLicenseV3(decrypted, encryptedLicense); + + return true; + } catch (e) { + logger.error('Invalid license'); + + logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); + + throw new InvalidLicenseError(); + } + } + + private processValidationResult(result: BehaviorWithContext[]): void { + if (!this._license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { + return; + } + + this._valid = true; + this._inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); + + if (this._license.information.tags) { + replaceTags(this._license.information.tags); + } + + const disabledModules = getModulesToDisable(result); + const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); + + replaceModules.call( + this, + modulesToEnable.map(({ module }) => module), + ); + logger.log({ msg: 'License validated', modules: modulesToEnable }); + + this.emit('validate'); + showLicense.call(this, this._license, this._valid); + } + + public hasValidLicense(): boolean { + return Boolean(this.getLicense()); + } + + public getLicense(): ILicenseV3 | undefined { + if (this._valid && this._license) { + return this._license; + } + } + + public async shouldPreventAction( + action: T, + context?: Partial>, + newCount = 1, + ): Promise { + const license = this.getLicense(); + if (!license) { + return false; + } + + const currentValue = (await getCurrentValueForLicenseLimit.call(this, action, context)) + newCount; + return Boolean( + license.limits[action] + ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) + .some(({ max }) => max < currentValue), + ); + } +} diff --git a/ee/packages/license/src/logger.ts b/ee/packages/license/src/logger.ts new file mode 100644 index 000000000000..120b08691c6c --- /dev/null +++ b/ee/packages/license/src/logger.ts @@ -0,0 +1,3 @@ +import { Logger } from '@rocket.chat/logger'; + +export const logger = new Logger('License'); diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts new file mode 100644 index 000000000000..7570ec525fc7 --- /dev/null +++ b/ee/packages/license/src/modules.ts @@ -0,0 +1,50 @@ +import type { LicenseModule } from './definition/LicenseModule'; +import { moduleRemoved, moduleValidated } from './events/emitter'; +import type { LicenseManager } from './license'; + +export function notifyValidatedModules(this: LicenseManager, licenseModules: LicenseModule[]) { + licenseModules.forEach((module) => { + this.modules.add(module); + moduleValidated.call(this, module); + }); +} + +export function notifyInvalidatedModules(this: LicenseManager, licenseModules: LicenseModule[]) { + licenseModules.forEach((module) => { + moduleRemoved.call(this, module); + this.modules.delete(module); + }); +} + +export function invalidateAll(this: LicenseManager) { + notifyInvalidatedModules.call(this, [...this.modules]); + this.modules.clear(); +} + +export function getModules(this: LicenseManager) { + return [...this.modules]; +} + +export function hasModule(this: LicenseManager, module: LicenseModule) { + return this.modules.has(module); +} + +export function replaceModules(this: LicenseManager, newModules: LicenseModule[]) { + for (const moduleName of newModules) { + if (this.modules.has(moduleName)) { + continue; + } + + this.modules.add(moduleName); + moduleValidated.call(this, moduleName); + } + + for (const moduleName of this.modules) { + if (newModules.includes(moduleName)) { + continue; + } + + moduleRemoved.call(this, moduleName); + this.modules.delete(moduleName); + } +} diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts new file mode 100644 index 000000000000..2c2140044336 --- /dev/null +++ b/ee/packages/license/src/pendingLicense.ts @@ -0,0 +1,32 @@ +import type { LicenseManager } from './license'; +import { logger } from './logger'; + +export function setPendingLicense(this: LicenseManager, encryptedLicense: string) { + this.pendingLicense = encryptedLicense; + if (this.pendingLicense) { + logger.info('Storing license as pending validation.'); + } +} + +export function applyPendingLicense(this: LicenseManager) { + if (this.pendingLicense) { + logger.info('Applying pending license.'); + this.setLicense(this.pendingLicense); + } +} + +export function hasPendingLicense(this: LicenseManager) { + return Boolean(this.pendingLicense); +} + +export function isPendingLicense(this: LicenseManager, encryptedLicense: string) { + return !!this.pendingLicense && this.pendingLicense === encryptedLicense; +} + +export function clearPendingLicense(this: LicenseManager) { + if (this.pendingLicense) { + logger.info('Removing pending license.'); + } + + this.pendingLicense = ''; +} diff --git a/ee/packages/license/src/showLicense.ts b/ee/packages/license/src/showLicense.ts new file mode 100644 index 000000000000..3dda60a43b76 --- /dev/null +++ b/ee/packages/license/src/showLicense.ts @@ -0,0 +1,27 @@ +import type { ILicenseV3 } from './definition/ILicenseV3'; +import type { LicenseManager } from './license'; +import { getModules } from './modules'; + +export function showLicense(this: LicenseManager, license: ILicenseV3 | undefined, valid: boolean | undefined) { + if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') { + return; + } + + if (!license || !valid) { + return; + } + + const { + validation: { serverUrls, validPeriods }, + limits, + } = license; + + const modules = getModules.call(this); + + console.log('---- License enabled ----'); + console.log(' url ->', JSON.stringify(serverUrls)); + console.log(' periods ->', JSON.stringify(validPeriods)); + console.log(' limits ->', JSON.stringify(limits)); + console.log(' modules ->', modules.join(', ')); + console.log('-------------------------'); +} diff --git a/ee/packages/license/src/tags.ts b/ee/packages/license/src/tags.ts new file mode 100644 index 000000000000..ca2639678475 --- /dev/null +++ b/ee/packages/license/src/tags.ts @@ -0,0 +1,23 @@ +import type { ILicenseTag } from './definition/ILicenseTag'; + +export const tags = new Set(); + +export const addTag = (tag: ILicenseTag) => { + // make sure to not add duplicated tag names + for (const addedTag of tags) { + if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { + return; + } + } + + tags.add(tag); +}; + +export const replaceTags = (newTags: ILicenseTag[]) => { + tags.clear(); + for (const tag of newTags) { + addTag(tag); + } +}; + +export const getTags = () => [...tags]; diff --git a/ee/packages/license/src/token.ts b/ee/packages/license/src/token.ts new file mode 100644 index 000000000000..2a9836a48303 --- /dev/null +++ b/ee/packages/license/src/token.ts @@ -0,0 +1,55 @@ +import crypto from 'crypto'; + +import { verify, sign, getPairs } from '@rocket.chat/jwt'; + +import type { ILicenseV3 } from './definition/ILicenseV3'; + +const PUBLIC_LICENSE_KEY_V2 = + 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; + +const PUBLIC_LICENSE_KEY_V3 = process.env.PUBLIC_LICENSE_KEY_V3 || PUBLIC_LICENSE_KEY_V2; + +let TEST_KEYS: [string, string] | undefined = undefined; + +export async function decrypt(encrypted: string): Promise { + if (process.env.NODE_ENV === 'test') { + if (encrypted.startsWith('RCV3_')) { + const jwt = encrypted.substring(5); + + TEST_KEYS = TEST_KEYS ?? (await getPairs()); + + if (!TEST_KEYS) { + throw new Error('Missing PUBLIC_LICENSE_KEY_V3'); + } + + const [spki] = TEST_KEYS; + + const [payload] = await verify(jwt, spki); + return JSON.stringify(payload); + } + } + + // handle V3 + if (encrypted.startsWith('RCV3_')) { + const jwt = encrypted.substring(5); + const [payload] = await verify(jwt, PUBLIC_LICENSE_KEY_V3); + + return JSON.stringify(payload); + } + + const decrypted = crypto.publicDecrypt(Buffer.from(PUBLIC_LICENSE_KEY_V2, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); + + return decrypted.toString('utf-8'); +} + +export async function encrypt(license: ILicenseV3): Promise { + if (process.env.NODE_ENV !== 'test') { + throw new Error('This function should only be used in tests'); + } + + TEST_KEYS = TEST_KEYS ?? (await getPairs()); + + const [, pkcs8] = TEST_KEYS; + + return `RCV3_${await sign(license, pkcs8)}`; +} diff --git a/apps/meteor/ee/app/license/server/bundles.ts b/ee/packages/license/src/v2/bundles.ts similarity index 100% rename from apps/meteor/ee/app/license/server/bundles.ts rename to ee/packages/license/src/v2/bundles.ts diff --git a/ee/packages/license/src/v2/convertToV3.ts b/ee/packages/license/src/v2/convertToV3.ts new file mode 100644 index 000000000000..7586f54c8c54 --- /dev/null +++ b/ee/packages/license/src/v2/convertToV3.ts @@ -0,0 +1,114 @@ +/** + * FromV2ToV3 + * Transform a License V2 into a V3 representation. + */ + +import type { ILicenseV2 } from '../definition/ILicenseV2'; +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { LicenseModule } from '../definition/LicenseModule'; +import { isBundle, getBundleFromModule, getBundleModules } from './bundles'; +import { getTagColor } from './getTagColor'; + +export const convertToV3 = (v2: ILicenseV2): ILicenseV3 => { + return { + version: '3.0', + information: { + autoRenew: false, + visualExpiration: new Date(Date.parse(v2.meta?.trialEnd || v2.expiry)).toISOString(), + trial: v2.meta?.trial || false, + offline: false, + createdAt: new Date().toISOString(), + grantedBy: { + method: 'manual', + seller: 'V2', + }, + // if no tag present, it means it is an old license, so try check for bundles and use them as tags + tags: v2.tag + ? [v2.tag] + : [ + ...(v2.modules.filter(isBundle).map(getBundleFromModule).filter(Boolean) as string[]).map((tag) => ({ + name: tag, + color: getTagColor(tag), + })), + ], + }, + validation: { + serverUrls: [ + { + value: v2.url, + type: 'url', + }, + ], + validPeriods: [ + { + validUntil: new Date(Date.parse(v2.expiry)).toISOString(), + invalidBehavior: 'invalidate_license', + }, + ], + statisticsReport: { + required: true, + }, + }, + grantedModules: [ + ...new Set( + v2.modules + .map((licenseModule) => (isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule])) + .reduce((prev, curr) => [...prev, ...curr], []) + .map((licenseModule) => ({ module: licenseModule as LicenseModule })), + ), + ], + limits: { + ...(v2.maxActiveUsers + ? { + activeUsers: [ + { + max: v2.maxActiveUsers, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.maxGuestUsers + ? { + guestUsers: [ + { + max: v2.maxGuestUsers, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.maxRoomsPerGuest + ? { + roomsPerGuest: [ + { + max: v2.maxRoomsPerGuest, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.apps?.maxPrivateApps + ? { + privateApps: [ + { + max: v2.apps.maxPrivateApps, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.apps?.maxMarketplaceApps + ? { + marketplaceApps: [ + { + max: v2.apps.maxMarketplaceApps, + behavior: 'prevent_action', + }, + ], + } + : {}), + }, + cloudMeta: v2.meta, + }; +}; diff --git a/apps/meteor/ee/app/license/server/getTagColor.ts b/ee/packages/license/src/v2/getTagColor.ts similarity index 100% rename from apps/meteor/ee/app/license/server/getTagColor.ts rename to ee/packages/license/src/v2/getTagColor.ts diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts new file mode 100644 index 000000000000..88cedc6c7bc9 --- /dev/null +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -0,0 +1,40 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; +import { applyPendingLicense, hasPendingLicense } from '../pendingLicense'; + +type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; + +export function setLicenseLimitCounter( + this: LicenseManager, + limitKey: T, + fn: (context?: LimitContext) => Promise | number, +) { + this.dataCounters.set(limitKey, fn as (context?: LimitContext) => Promise); + + if (hasPendingLicense.call(this) && hasAllDataCounters.call(this)) { + void applyPendingLicense.call(this); + } +} + +export async function getCurrentValueForLicenseLimit( + this: LicenseManager, + limitKey: T, + context?: Partial>, +): Promise { + const counterFn = this.dataCounters.get(limitKey); + if (!counterFn) { + logger.error({ msg: 'Unable to validate license limit due to missing data counter.', limitKey }); + throw new Error('Unable to validate license limit due to missing data counter.'); + } + + return counterFn(context as LimitContext | undefined); +} + +export function hasAllDataCounters(this: LicenseManager) { + return ( + ['activeUsers', 'guestUsers', 'roomsPerGuest', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[] + ).every((limitKey) => this.dataCounters.has(limitKey)); +} diff --git a/ee/packages/license/src/validation/getModulesToDisable.ts b/ee/packages/license/src/validation/getModulesToDisable.ts new file mode 100644 index 000000000000..d42426e8af26 --- /dev/null +++ b/ee/packages/license/src/validation/getModulesToDisable.ts @@ -0,0 +1,15 @@ +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseModule } from '../definition/LicenseModule'; + +const filterValidationResult = (result: BehaviorWithContext[], expectedBehavior: LicenseBehavior) => + result.filter(({ behavior }) => behavior === expectedBehavior) as BehaviorWithContext[]; + +export const getModulesToDisable = (validationResult: BehaviorWithContext[]): LicenseModule[] => { + return [ + ...new Set([ + ...filterValidationResult(validationResult, 'disable_modules') + .map(({ modules }) => modules || []) + .flat(), + ]), + ]; +}; diff --git a/ee/packages/license/src/validation/getResultingBehavior.ts b/ee/packages/license/src/validation/getResultingBehavior.ts new file mode 100644 index 000000000000..47e2d91b8b89 --- /dev/null +++ b/ee/packages/license/src/validation/getResultingBehavior.ts @@ -0,0 +1,20 @@ +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseLimit } from '../definition/LicenseLimit'; +import type { LicensePeriod } from '../definition/LicensePeriod'; + +export const getResultingBehavior = (data: LicenseLimit | LicensePeriod | Partial): BehaviorWithContext => { + const behavior = 'invalidBehavior' in data ? data.invalidBehavior : data.behavior; + + switch (behavior) { + case 'disable_modules': + return { + behavior, + modules: ('modules' in data && data.modules) || [], + }; + + default: + return { + behavior, + } as BehaviorWithContext; + } +}; diff --git a/ee/packages/license/src/validation/isBehaviorsInResult.ts b/ee/packages/license/src/validation/isBehaviorsInResult.ts new file mode 100644 index 000000000000..7e6ed89db8ec --- /dev/null +++ b/ee/packages/license/src/validation/isBehaviorsInResult.ts @@ -0,0 +1,4 @@ +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; + +export const isBehaviorsInResult = (result: BehaviorWithContext[], expectedBehaviors: LicenseBehavior[]) => + result.some(({ behavior }) => expectedBehaviors.includes(behavior)); diff --git a/ee/packages/license/src/validation/isReadyForValidation.ts b/ee/packages/license/src/validation/isReadyForValidation.ts new file mode 100644 index 000000000000..aa763bf7f353 --- /dev/null +++ b/ee/packages/license/src/validation/isReadyForValidation.ts @@ -0,0 +1,7 @@ +import type { LicenseManager } from '../license'; +import { hasAllDataCounters } from './getCurrentValueForLicenseLimit'; + +// Can only validate licenses once the workspace URL and the data counter functions are set +export function isReadyForValidation(this: LicenseManager) { + return Boolean(this.getWorkspaceUrl() && hasAllDataCounters.call(this)); +} diff --git a/ee/packages/license/src/validation/runValidation.spec.ts b/ee/packages/license/src/validation/runValidation.spec.ts new file mode 100644 index 000000000000..98797c86cd27 --- /dev/null +++ b/ee/packages/license/src/validation/runValidation.spec.ts @@ -0,0 +1,38 @@ +/** + * @jest-environment node + */ + +import { MockedLicenseBuilder, getReadyLicenseManager } from '../../__tests__/MockedLicenseBuilder'; +import { runValidation } from './runValidation'; + +describe('Validation behaviors', () => { + it('should return a behavior if the license period is invalid', async () => { + const licenseManager = await getReadyLicenseManager(); + + // two days ago + const validFrom = new Date(new Date().setDate(new Date().getDate() - 2)); + // one day ago + const validUntil = new Date(new Date().setDate(new Date().getDate() - 1)); + + const license = await new MockedLicenseBuilder().resetValidPeriods().withValidPeriod({ + validFrom: validFrom.toISOString(), + validUntil: validUntil.toISOString(), + invalidBehavior: 'disable_modules', + modules: ['livechat-enterprise'], + }); + + await expect( + runValidation.call(licenseManager, await license.build(), [ + 'invalidate_license', + 'prevent_installation', + 'start_fair_policy', + 'disable_modules', + ]), + ).resolves.toStrictEqual([ + { + behavior: 'disable_modules', + modules: ['livechat-enterprise'], + }, + ]); + }); +}); diff --git a/ee/packages/license/src/validation/runValidation.ts b/ee/packages/license/src/validation/runValidation.ts new file mode 100644 index 000000000000..9cb623b8eae0 --- /dev/null +++ b/ee/packages/license/src/validation/runValidation.ts @@ -0,0 +1,22 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { LicenseBehavior, BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseManager } from '../license'; +import { validateLicenseLimits } from './validateLicenseLimits'; +import { validateLicensePeriods } from './validateLicensePeriods'; +import { validateLicenseUrl } from './validateLicenseUrl'; + +export async function runValidation( + this: LicenseManager, + license: ILicenseV3, + behaviorsToValidate: LicenseBehavior[] = [], +): Promise { + const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate.length || behaviorsToValidate.includes(behavior); + + return [ + ...new Set([ + ...validateLicenseUrl.call(this, license, shouldValidateBehavior), + ...validateLicensePeriods(license, shouldValidateBehavior), + ...(await validateLicenseLimits.call(this, license, shouldValidateBehavior)), + ]), + ]; +} diff --git a/ee/packages/license/src/validation/validateFormat.ts b/ee/packages/license/src/validation/validateFormat.ts new file mode 100644 index 000000000000..a8c2488cd9fc --- /dev/null +++ b/ee/packages/license/src/validation/validateFormat.ts @@ -0,0 +1,16 @@ +import { InvalidLicenseError } from '../errors/InvalidLicenseError'; +import { decrypt } from '../token'; + +export const validateFormat = async (encryptedLicense: string): Promise => { + if (!encryptedLicense || String(encryptedLicense).trim() === '') { + throw new InvalidLicenseError('Empty license'); + } + + try { + await decrypt(encryptedLicense); + } catch (e) { + throw new InvalidLicenseError(); + } + + return true; +}; diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts new file mode 100644 index 000000000000..168effe6a250 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -0,0 +1,39 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; +import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit'; +import { getResultingBehavior } from './getResultingBehavior'; + +export async function validateLicenseLimits( + this: LicenseManager, + license: ILicenseV3, + behaviorFilter: (behavior: LicenseBehavior) => boolean, +): Promise { + const { limits } = license; + + const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; + return ( + await Promise.all( + limitKeys.map(async (limitKey) => { + // Filter the limit list before running any query in the database so we don't end up loading some value we won't use. + const limitList = limits[limitKey]?.filter(({ behavior, max }) => max >= 0 && behaviorFilter(behavior)); + if (!limitList?.length) { + return []; + } + + const currentValue = await getCurrentValueForLicenseLimit.call(this, limitKey); + return limitList + .filter(({ max }) => max < currentValue) + .map((limit) => { + logger.error({ + msg: 'Limit validation failed', + kind: limitKey, + limit, + }); + return getResultingBehavior(limit); + }); + }), + ) + ).flat(); +} diff --git a/ee/packages/license/src/validation/validateLicensePeriods.ts b/ee/packages/license/src/validation/validateLicensePeriods.ts new file mode 100644 index 000000000000..5b3fae433e38 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicensePeriods.ts @@ -0,0 +1,38 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { Timestamp } from '../definition/LicensePeriod'; +import { logger } from '../logger'; +import { getResultingBehavior } from './getResultingBehavior'; + +export const isPeriodInvalid = (from: Timestamp | undefined, until: Timestamp | undefined) => { + const now = new Date(); + + if (from && now < new Date(from)) { + return true; + } + + if (until && now > new Date(until)) { + return true; + } + + return false; +}; + +export const validateLicensePeriods = ( + license: ILicenseV3, + behaviorFilter: (behavior: LicenseBehavior) => boolean, +): BehaviorWithContext[] => { + const { + validation: { validPeriods }, + } = license; + + return validPeriods + .filter(({ validFrom, validUntil, invalidBehavior }) => behaviorFilter(invalidBehavior) && isPeriodInvalid(validFrom, validUntil)) + .map((period) => { + logger.error({ + msg: 'Period validation failed', + period, + }); + return getResultingBehavior(period); + }); +}; diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts new file mode 100644 index 000000000000..55cd076c4378 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -0,0 +1,59 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseManager } from '../license'; +import { logger } from '../logger'; +import { getResultingBehavior } from './getResultingBehavior'; + +export const validateUrl = (licenseURL: string, url: string) => { + licenseURL = licenseURL + .replace(/\./g, '\\.') // convert dots to literal + .replace(/\*/g, '.*'); // convert * to .* + const regex = new RegExp(`^${licenseURL}$`, 'i'); + + return !!regex.exec(url); +}; + +export function validateLicenseUrl( + this: LicenseManager, + license: ILicenseV3, + behaviorFilter: (behavior: LicenseBehavior) => boolean, +): BehaviorWithContext[] { + if (!behaviorFilter('invalidate_license')) { + return []; + } + + const { + validation: { serverUrls }, + } = license; + + const workspaceUrl = this.getWorkspaceUrl(); + + if (!workspaceUrl) { + logger.error('Unable to validate license URL without knowing the workspace URL.'); + return [getResultingBehavior({ behavior: 'invalidate_license' })]; + } + + return serverUrls + .filter((url) => { + switch (url.type) { + case 'regex': + // #TODO + break; + case 'hash': + // #TODO + break; + case 'url': + return !validateUrl(url.value, workspaceUrl); + } + + return false; + }) + .map((url) => { + logger.error({ + msg: 'Url validation failed', + url, + workspaceUrl, + }); + return getResultingBehavior({ behavior: 'invalidate_license' }); + }); +} diff --git a/ee/packages/license/tsconfig.json b/ee/packages/license/tsconfig.json new file mode 100644 index 000000000000..539d1c0af1b8 --- /dev/null +++ b/ee/packages/license/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 802a6e15d0eb..ce21e963911b 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -78,7 +78,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT async started(): Promise { try { - this.shouldWork = await licenseService.hasLicense('scalability'); + this.shouldWork = await licenseService.hasModule('scalability'); } catch (e: unknown) { // ignore } @@ -222,7 +222,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } let file = message.files?.map((v) => ({ _id: v._id, name: v.name })).find((file) => file.name === attachment.title); if (!file) { - this.log.debug(`File ${attachment.title} not found in room ${message.rid}!`); + this.log.warn(`File ${attachment.title} not found in room ${message.rid}!`); // For some reason, when an image is uploaded from clipboard, it doesn't have a file :( // So, we'll try to get the FILE_ID from the `title_link` prop which has the format `/file-upload/FILE_ID/FILE_NAME` using a regex const fileId = attachment.title_link?.match(/\/file-upload\/(.*)\/.*/)?.[1]; @@ -236,7 +236,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } if (!file) { - this.log.error(`File ${attachment.title} not found in room ${message.rid}!`); + this.log.warn(`File ${attachment.title} not found in room ${message.rid}!`); // ignore attachments without file files.push({ name: attachment.title, buffer: null }); continue; diff --git a/ee/packages/omnichannel-services/src/QueueWorker.ts b/ee/packages/omnichannel-services/src/QueueWorker.ts index 141cb937f475..bfb69362fac6 100644 --- a/ee/packages/omnichannel-services/src/QueueWorker.ts +++ b/ee/packages/omnichannel-services/src/QueueWorker.ts @@ -35,7 +35,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { async started(): Promise { try { - this.shouldWork = await License.hasLicense('scalability'); + this.shouldWork = await License.hasModule('scalability'); } catch (e: unknown) { // ignore } diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 9011dab086b6..fdb6a16393b3 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -28,6 +28,9 @@ "files": [ "/dist" ], + "volta": { + "extends": "../../../package.json" + }, "dependencies": { "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index 238cd445def4..fb656fc3e158 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -65,7 +65,7 @@ export class Presence extends ServiceClass implements IPresence { try { await Settings.updateValueById('Presence_broadcast_disabled', false); - this.hasLicense = await License.hasLicense('scalability'); + this.hasLicense = await License.hasModule('scalability'); } catch (e: unknown) { // ignore } diff --git a/package.json b/package.json index f9029efb8b66..962e42d48c6e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/chart.js": "^2.9.37", "@types/js-yaml": "^4.0.5", "husky": "^7.0.4", - "turbo": "^1.10.13" + "turbo": "~1.10.14" }, "workspaces": [ "apps/*", diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 4cce8aebe07b..3492cc1f77bf 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -30,6 +30,9 @@ "files": [ "/dist" ], + "volta": { + "extends": "../../package.json" + }, "dependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/core-services/src/lib/Api.ts b/packages/core-services/src/lib/Api.ts index 66806dc54fde..f0b5e67594c2 100644 --- a/packages/core-services/src/lib/Api.ts +++ b/packages/core-services/src/lib/Api.ts @@ -46,7 +46,11 @@ export class Api implements IApiService { } async broadcast(event: T, ...args: Parameters): Promise { - return this.broker?.broadcast(event, ...args); + if (!this.broker) { + throw new Error(`No broker set to broadcast: ${event}`); + } + + return this.broker.broadcast(event, ...args); } async broadcastToServices( diff --git a/packages/core-services/src/types/IBannerService.ts b/packages/core-services/src/types/IBannerService.ts index 1035bdd59510..50b8ab08275c 100644 --- a/packages/core-services/src/types/IBannerService.ts +++ b/packages/core-services/src/types/IBannerService.ts @@ -2,7 +2,7 @@ import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typing export interface IBannerService { getBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise; - create(banner: Optional): Promise; + create(banner: Optional): Promise; dismiss(userId: string, bannerId: string): Promise; discardDismissal(bannerId: string): Promise; getById(bannerId: string): Promise; diff --git a/packages/core-services/src/types/ILicense.ts b/packages/core-services/src/types/ILicense.ts index 7b89a006bfc0..c9247f8887ce 100644 --- a/packages/core-services/src/types/ILicense.ts +++ b/packages/core-services/src/types/ILicense.ts @@ -1,9 +1,9 @@ import type { IServiceClass } from './ServiceClass'; export interface ILicense extends IServiceClass { - hasLicense(feature: string): boolean; + hasModule(feature: string): boolean; - isEnterprise(): boolean; + hasValidLicense(): boolean; getModules(): string[]; diff --git a/packages/core-typings/.eslintrc.json b/packages/core-typings/.eslintrc.json index 56a6f6602e33..44d74e043bc4 100644 --- a/packages/core-typings/.eslintrc.json +++ b/packages/core-typings/.eslintrc.json @@ -8,5 +8,8 @@ } } ], - "ignorePatterns": ["**/dist"] + "ignorePatterns": ["**/dist"], + "rules": { + "@typescript-eslint/no-empty-interface": "off" + } } diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 2d0b0d734897..2b673be0f857 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,13 +1,7 @@ { + "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "version": "6.4.0-rc.4", - "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", - "eslint": "~8.45.0", - "mongodb": "^4.17.1", - "prettier": "~2.8.8", - "typescript": "~5.2.2" - }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", @@ -26,6 +20,13 @@ "@rocket.chat/message-parser": "next", "@rocket.chat/ui-kit": "^0.32.1" }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "eslint": "~8.45.0", + "mongodb": "^4.17.1", + "prettier": "~2.8.8", + "typescript": "~5.2.2" + }, "volta": { "extends": "../../package.json" } diff --git a/packages/core-typings/src/IIntegration.ts b/packages/core-typings/src/IIntegration.ts index 6b99424264b2..cffff75767f4 100644 --- a/packages/core-typings/src/IIntegration.ts +++ b/packages/core-typings/src/IIntegration.ts @@ -1,6 +1,8 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IUser } from './IUser'; +export type IntegrationScriptEngine = 'vm2' | 'isolated-vm'; + export interface IIncomingIntegration extends IRocketChatRecord { type: 'webhook-incoming'; _createdBy: Pick | null; @@ -22,6 +24,8 @@ export interface IIncomingIntegration extends IRocketChatRecord { alias?: string; avatar?: string; emoji?: string; + + scriptEngine?: IntegrationScriptEngine; } export type OutgoingIntegrationEvent = @@ -65,6 +69,8 @@ export interface IOutgoingIntegration extends IRocketChatRecord { alias?: string; avatar?: string; emoji?: string; + + scriptEngine?: IntegrationScriptEngine; } export type IIntegration = IIncomingIntegration | IOutgoingIntegration; diff --git a/packages/core-typings/src/IIntegrationHistory.ts b/packages/core-typings/src/IIntegrationHistory.ts index 6297cd7d74a0..6594d611fb49 100644 --- a/packages/core-typings/src/IIntegrationHistory.ts +++ b/packages/core-typings/src/IIntegrationHistory.ts @@ -1,3 +1,4 @@ +import type { IMessage } from './IMessage'; import type { IRocketChatRecord } from './IRocketChatRecord'; export interface IIntegrationHistory extends IRocketChatRecord { @@ -17,10 +18,10 @@ export interface IIntegrationHistory extends IRocketChatRecord { finished: boolean; triggerWord?: string; - prepareSentMessage?: string; - processSentMessage?: string; + prepareSentMessage?: { channel: string; message: Partial }[]; + processSentMessage?: { channel: string; message: Partial }[]; url?: string; - httpCallData?: string; + httpCallData?: Record; httpError?: any; httpResult?: string; error?: any; diff --git a/packages/core-typings/src/IOembed.ts b/packages/core-typings/src/IOembed.ts index c540cb893817..0b781aa07fc8 100644 --- a/packages/core-typings/src/IOembed.ts +++ b/packages/core-typings/src/IOembed.ts @@ -1,9 +1,5 @@ -import type Url from 'url'; - import type { MessageAttachment } from './IMessage'; -export type ParsedUrl = Pick; - export type OEmbedMeta = { [key: string]: string; } & { @@ -12,8 +8,7 @@ export type OEmbedMeta = { }; export type OEmbedUrlContent = { - urlObj: Url.UrlWithParsedQuery; - parsedUrl: ParsedUrl; + urlObj: URL; headers: { [k: string]: string }; body: string; statusCode: number; @@ -27,7 +22,6 @@ export type OEmbedProvider = { export type OEmbedUrlContentResult = { headers: { [key: string]: string }; body: string; - parsedUrl: Pick; statusCode: number; attachments?: MessageAttachment[]; }; @@ -38,7 +32,6 @@ export type OEmbedUrlWithMetadata = { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; - parsedUrl: Pick; content: OEmbedUrlContent; }; diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 2ea8115a727c..6bbc2da81b74 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -6,7 +6,15 @@ import type { ITeamStats } from './ITeam'; export interface IStats { _id: string; - wizard: Record; + wizard: { + organizationType?: string; + industry?: string; + size?: string; + country?: string; + language?: string; + serverType?: string; + registerServer?: boolean; + }; uniqueId: string; installedAt?: string; version?: string; @@ -203,6 +211,7 @@ export interface IStats { totalCustomRoles: number; totalWebRTCCalls: number; uncaughtExceptionsCount: number; + push: number; matrixFederation: { enabled: boolean; }; diff --git a/packages/core-typings/src/cloud/Announcement.ts b/packages/core-typings/src/cloud/Announcement.ts new file mode 100644 index 000000000000..3d891daf132f --- /dev/null +++ b/packages/core-typings/src/cloud/Announcement.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { IRocketChatRecord } from '../IRocketChatRecord'; +import { type UiKitPayload } from '../UIKit'; + +type TargetPlatform = 'web' | 'mobile'; + +type Dictionary = { + [lng: string]: { + [key: string]: string; + }; +}; + +type Creator = 'cloud' | 'system'; + +export interface Announcement extends IRocketChatRecord { + selector?: { + roles?: string[]; + }; + platform: TargetPlatform[]; + expireAt: Date; + startAt: Date; + createdBy: Creator; + createdAt: Date; + dictionary?: Dictionary; + view: UiKitPayload; + surface: 'banner' | 'modal'; +} diff --git a/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts b/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts new file mode 100644 index 000000000000..fff1db8f1b99 --- /dev/null +++ b/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface NpsSurveyAnnouncement { + id: string; + startAt: Date; + expireAt: Date; +} diff --git a/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts b/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts new file mode 100644 index 000000000000..7e81e1b47599 --- /dev/null +++ b/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface WorkspaceLicensePayload { + version: number; + address: string; + license: string; + updatedAt: Date; + modules: string; + expireAt: Date; +} diff --git a/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts new file mode 100644 index 000000000000..fb95cfa4553c --- /dev/null +++ b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { IBanner } from '../IBanner'; +import type { Announcement } from './Announcement'; +import type { NpsSurveyAnnouncement } from './NpsSurveyAnnouncement'; + +export interface WorkspaceSyncPayload { + workspaceId: string; + publicKey?: string; + trial?: { + trialing: boolean; + trialID: string; + endDate: Date; + marketing: { + utmContent: string; + utmMedium: string; + utmSource: string; + utmCampaign: string; + }; + DowngradesToPlan: { + id: string; + }; + trialRequested: boolean; + }; + /** @deprecated */ + nps?: NpsSurveyAnnouncement; + /** @deprecated */ + banners?: IBanner[]; +} + +export interface WorkspaceSyncRequestPayload { + uniqueId: string; + workspaceId: string; + seats: number; + MAC: number; // Need to align on the property + address: string; + siteName: string; + deploymentMethod: string; + deploymentPlatform: string; + version: string; + licenseVersion: number; + connectionDisable: boolean; +} + +export interface WorkspaceSyncResponse { + workspaceId: string; + publicKey: string; + license: unknown; +} + +export interface WorkspaceCommsRequestPayload { + npsEnabled: boolean; + deploymentMethod: string; + deploymentPlatform: string; + version: string; +} +export interface WorkspaceCommsResponsePayload { + nps?: NpsSurveyAnnouncement | null; // Potentially consolidate into announcements + announcements?: { + create: Announcement[]; + delete: Announcement['_id'][]; + }; +} diff --git a/packages/core-typings/src/cloud/index.ts b/packages/core-typings/src/cloud/index.ts new file mode 100644 index 000000000000..da0565a215ed --- /dev/null +++ b/packages/core-typings/src/cloud/index.ts @@ -0,0 +1,10 @@ +export { Announcement } from './Announcement'; +export { NpsSurveyAnnouncement } from './NpsSurveyAnnouncement'; +export { WorkspaceLicensePayload } from './WorkspaceLicensePayload'; +export { + WorkspaceSyncPayload, + WorkspaceSyncRequestPayload, + WorkspaceSyncResponse, + WorkspaceCommsRequestPayload, + WorkspaceCommsResponsePayload, +} from './WorkspaceSyncPayload'; diff --git a/packages/core-typings/src/ee/ILicense/ILicense.ts b/packages/core-typings/src/ee/ILicense/ILicense.ts deleted file mode 100644 index 8490ab1d7cbe..000000000000 --- a/packages/core-typings/src/ee/ILicense/ILicense.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ILicenseTag } from './ILicenseTag'; - -export interface ILicense { - url: string; - expiry: string; - maxActiveUsers: number; - modules: string[]; - maxGuestUsers: number; - maxRoomsPerGuest: number; - tag?: ILicenseTag; - meta?: { - trial: boolean; - trialEnd: string; - workspaceId: string; - }; - apps?: { - maxPrivateApps: number; - maxMarketplaceApps: number; - }; -} diff --git a/packages/core-typings/src/ee/ILicense/ILicenseTag.ts b/packages/core-typings/src/ee/ILicense/ILicenseTag.ts deleted file mode 100644 index 2f11fdebd5db..000000000000 --- a/packages/core-typings/src/ee/ILicense/ILicenseTag.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ILicenseTag { - name: string; - color: string; -} diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 8cd004dd09f1..de36606e7f90 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -39,7 +39,6 @@ export * from './IUserSession'; export * from './IUserStatus'; export * from './IUser'; -export * from './ee/ILicense/ILicense'; export * from './ee/IAuditLog'; export * from './import'; @@ -135,3 +134,5 @@ export * from './ICustomOAuthConfig'; export * from './IModerationReport'; export * from './CustomFieldMetadata'; + +export * as Cloud from './cloud'; diff --git a/packages/jwt/.eslintrc.json b/packages/jwt/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/packages/jwt/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/jwt/__tests__/jwt.spec.ts b/packages/jwt/__tests__/jwt.spec.ts new file mode 100644 index 000000000000..302cfdf22c96 --- /dev/null +++ b/packages/jwt/__tests__/jwt.spec.ts @@ -0,0 +1,90 @@ +import { generateKeyPair, exportPKCS8, exportSPKI } from 'jose'; + +import { sign, verify } from '../src/index'; + +it('should sign and verify a jwt with RS256', async () => { + const { publicKey, privateKey } = await generateKeyPair('RS256'); + const spki = await exportSPKI(publicKey); + const pkcs8 = await exportPKCS8(privateKey); + + const licenseV3 = { + information: { + id: '64d28d096400df50b6ace670', + autoRenew: true, + createdAt: '2023-08-08T18:44:25.719+0000', + visualExpiration: '2024-09-08T18:44:25.719+0000', + notifyAdminsAt: '2024-09-01T18:44:25.719+0000', + notifyUsersAt: '2024-09-05T18:44:25.719+0000', + trial: false, + offline: false, + grantedBy: { method: 'manual', seller: 'john.rocketseed@rocket.chat' }, + grantedTo: { name: 'Alice Clientseed', company: 'Client', email: 'alice.clientseed@client.com' }, + legalText: "This license can't be used for reselling", + notes: 'Plan Premium', + tags: [{ name: 'Enterprise', color: '#CCCCCC' }], + }, + validation: { + serverUrls: [{ value: 'https://localhost:3000', type: 'url' }], + serverVersions: [{ value: '6.4' }], + cloudWorkspaceId: 'alks-a9sj0diba09shdiasodjha9s0diha9s9duabsiuhdai0sdh0a9hs09da09s8d09a80s9d8', + serverUniqueId: '64d28d096400df50b6ace670', + validUntil: '2024-09-18T18:44:25.719+0000', + validFrom: '2024-07-08T18:44:25.719+0000', + installationAllowedUntil: '2024-07-09T18:44:25.719+0000', + legalTextAgreement: { type: 'accepted', acceptedVia: 'cloud' }, + statisticsReport: { required: true, allowedStaleInDays: 5 }, + }, + grantedModules: [ + { module: 'auditing' }, + { module: 'canned-responses' }, + { module: 'ldap-enterprise' }, + { module: 'livechat-enterprise' }, + { module: 'voip-enterprise' }, + { module: 'omnichannel-mobile-enterprise' }, + { module: 'engagement-dashboard' }, + { module: 'push-privacy' }, + { module: 'scalability' }, + { module: 'teams-mention' }, + { module: 'saml-enterprise' }, + { module: 'oauth-enterprise' }, + { module: 'device-management' }, + { module: 'federation' }, + { module: 'videoconference-enterprise' }, + { module: 'message-read-receipt' }, + { module: 'outlook-calendar' }, + ], + limits: { + activeUsers: [ + { max: 500, behavior: 'start_fair_policy' }, + { max: 1000, behavior: 'prevent_action' }, + { max: 1100, behavior: 'invalidate_license' }, + ], + guestUsers: [ + { max: 200, behavior: 'start_fair_policy' }, + { max: 400, behavior: 'prevent_action' }, + { max: 500, behavior: 'invalidate_license' }, + ], + roomsPerGuest: [ + { max: 5, behavior: 'start_fair_policy' }, + { max: 10, behavior: 'prevent_action' }, + ], + privateApps: [ + { max: 5, behavior: 'start_fair_policy' }, + { max: 10, behavior: 'prevent_action' }, + { max: 11, behavior: 'invalidate_license' }, + ], + marketplaceApps: [ + { max: 5, behavior: 'start_fair_policy' }, + { max: 10, behavior: 'prevent_action' }, + { max: 11, behavior: 'invalidate_license' }, + ], + }, + cloudMeta: { lastStatisticId: '64d28d096400df50b6ace671' }, + }; + + const token = await sign(licenseV3, pkcs8); + const [payload, protectedHeader] = await verify(token, spki); + + expect(protectedHeader).toEqual({ alg: 'RS256', typ: 'JWT' }); + expect(payload).toEqual(licenseV3); +}); diff --git a/packages/jwt/jest.config.js b/packages/jwt/jest.config.js new file mode 100644 index 000000000000..6231bde11685 --- /dev/null +++ b/packages/jwt/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/packages/jwt/package.json b/packages/jwt/package.json new file mode 100644 index 000000000000..b6be368917c3 --- /dev/null +++ b/packages/jwt/package.json @@ -0,0 +1,31 @@ +{ + "name": "@rocket.chat/jwt", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@types/jest": "~29.5.3", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "ts-jest": "^29.1.1", + "typescript": "~5.2.2" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "testunit": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../package.json" + }, + "dependencies": { + "jose": "^4.14.4" + } +} diff --git a/packages/jwt/src/index.ts b/packages/jwt/src/index.ts new file mode 100644 index 000000000000..3508471f9d81 --- /dev/null +++ b/packages/jwt/src/index.ts @@ -0,0 +1,29 @@ +import { SignJWT, importPKCS8, jwtVerify, importSPKI, generateKeyPair, exportSPKI, exportPKCS8 } from 'jose'; +import type { JWTPayload } from 'jose'; + +export async function sign(keyObject: object, pkcs8: string, alg = 'RS256') { + const privateKey = await importPKCS8(pkcs8, alg); + + const token = await new SignJWT(keyObject as JWTPayload).setProtectedHeader({ alg, typ: 'JWT' }).sign(privateKey); + + return token; +} + +export async function verify(jwt: string, spki: string, alg = 'RS256') { + const publicKey = await importSPKI(spki, alg); + + const { payload, protectedHeader } = await jwtVerify(jwt, publicKey, {}); + + return [payload, protectedHeader]; +} + +export async function getPairs(): Promise<[string, string]> { + if (process.env.NODE_ENV !== 'test') { + throw new Error('This function should only be used in tests'); + } + const { publicKey, privateKey } = await generateKeyPair('RS256'); + const spki = await exportSPKI(publicKey); + const pkcs8 = await exportPKCS8(privateKey); + + return [spki, pkcs8]; +} diff --git a/packages/jwt/tsconfig.json b/packages/jwt/tsconfig.json new file mode 100644 index 000000000000..f236186070e8 --- /dev/null +++ b/packages/jwt/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "module": "commonjs", + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/livechat/src/components/App/App.tsx b/packages/livechat/src/components/App/App.tsx index 0ca3f1b6b366..cfaa52b94999 100644 --- a/packages/livechat/src/components/App/App.tsx +++ b/packages/livechat/src/components/App/App.tsx @@ -75,7 +75,26 @@ type AppState = { poppedOut: boolean; }; -// eslint-disable-next-line react/prefer-stateless-function +export type ScreenPropsType = { + notificationsEnabled: boolean; + minimized: boolean; + expanded: boolean; + windowed: boolean; + sound: unknown; + alerts: unknown; + modal: unknown; + nameDefault: string; + emailDefault: string; + departmentDefault: string; + onEnableNotifications: () => unknown; + onDisableNotifications: () => unknown; + onMinimize: () => unknown; + onRestore: () => unknown; + onOpenWindow: () => unknown; + onDismissAlert: () => unknown; + dismissNotification: () => void; +}; + export class App extends Component { state = { initialized: false, diff --git a/packages/livechat/src/components/Header/index.js b/packages/livechat/src/components/Header/index.tsx similarity index 67% rename from packages/livechat/src/components/Header/index.js rename to packages/livechat/src/components/Header/index.tsx index 669b46571b0e..9764c3c2ce37 100644 --- a/packages/livechat/src/components/Header/index.js +++ b/packages/livechat/src/components/Header/index.tsx @@ -1,12 +1,41 @@ +import type { ComponentChildren, Ref } from 'preact'; import { toChildArray } from 'preact'; +import type { JSXInternal } from 'preact/src/jsx'; import { createClassName } from '../../helpers/createClassName'; import styles from './styles.scss'; -export const Header = ({ children, theme: { color: backgroundColor, fontColor: color } = {}, className, post, large, style, ...props }) => ( +type HeaderProps = { + children?: ComponentChildren; + theme?: { + color?: string; + fontColor?: string; + }; + className?: string; + post?: ComponentChildren; + large?: boolean; + style?: JSXInternal.CSSProperties; + ref?: Ref; + onClick?: JSXInternal.DOMAttributes['onClick']; +}; + +type HeaderComponentProps = { + children?: ComponentChildren; + className?: string; +}; + +export const Header = ({ + children, + theme: { color: backgroundColor, fontColor: color } = {}, + className, + post, + large, + style, + ...props +}: HeaderProps) => (
{children} @@ -14,25 +43,25 @@ export const Header = ({ children, theme: { color: backgroundColor, fontColor: c
); -export const Picture = ({ children, className = undefined, ...props }) => ( +export const Picture = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const Content = ({ children, className = undefined, ...props }) => ( +export const Content = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const Title = ({ children, className = undefined, ...props }) => ( +export const Title = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const SubTitle = ({ children, className = undefined, ...props }) => ( +export const SubTitle = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
(
); -export const Actions = ({ children, className = undefined, ...props }) => ( +export const Actions = ({ children, className = undefined, ...props }: HeaderComponentProps) => ( ); -export const Action = ({ children, className = undefined, ...props }) => ( +export const Action = ({ children, className = undefined, ...props }: HeaderComponentProps & { onClick?: () => void }) => ( ); -export const Post = ({ children, className = undefined, ...props }) => ( +export const Post = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
); -export const CustomField = ({ children, className = undefined, ...props }) => ( +export const CustomField = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
{children}
diff --git a/packages/livechat/src/components/Screen/Header.js b/packages/livechat/src/components/Screen/Header.tsx similarity index 54% rename from packages/livechat/src/components/Screen/Header.js rename to packages/livechat/src/components/Screen/Header.tsx index 9f9e41810c7b..671a5b343b21 100644 --- a/packages/livechat/src/components/Screen/Header.js +++ b/packages/livechat/src/components/Screen/Header.tsx @@ -1,6 +1,8 @@ -import { Component } from 'preact'; -import { withTranslation } from 'react-i18next'; +import type { ComponentChildren } from 'preact'; +import { useRef } from 'preact/hooks'; +import { useTranslation, withTranslation } from 'react-i18next'; +import type { Agent } from '../../definitions/agents'; import MinimizeIcon from '../../icons/arrowDown.svg'; import RestoreIcon from '../../icons/arrowUp.svg'; import NotificationsEnabledIcon from '../../icons/bell.svg'; @@ -11,70 +13,84 @@ import { Avatar } from '../Avatar'; import Header from '../Header'; import Tooltip from '../Tooltip'; -class ScreenHeader extends Component { - largeHeader = () => { - const { agent } = this.props; - return !!(agent && agent.email && agent.phone); +type screenHeaderProps = { + alerts: { id: string; children: ComponentChildren; [key: string]: unknown }[]; + agent: Agent; + notificationsEnabled: boolean; + minimized: boolean; + expanded: boolean; + windowed: boolean; + onDismissAlert?: (id?: string) => void; + onEnableNotifications: () => unknown; + onDisableNotifications: () => unknown; + onMinimize: () => unknown; + onRestore: () => unknown; + onOpenWindow: () => unknown; + queueInfo: { + spot: number; }; + title: string; +}; - headerTitle = (t) => { - const { agent, queueInfo, title } = this.props; - if (agent && agent.name) { +const ScreenHeader = ({ + alerts, + agent, + notificationsEnabled, + minimized, + expanded, + windowed, + onDismissAlert, + onEnableNotifications, + onDisableNotifications, + onMinimize, + onRestore, + onOpenWindow, + queueInfo, + title, +}: screenHeaderProps) => { + const { t } = useTranslation(); + const headerRef = useRef(null); + + const largeHeader = () => { + return !!(agent?.email && agent.phone); + }; + + const headerTitle = () => { + if (agent?.name) { return agent.name; } - if (queueInfo && queueInfo.spot && queueInfo.spot > 0) { + if (queueInfo?.spot && queueInfo.spot > 0) { return t('waiting_queue'); } return title; }; - render = ({ - alerts, - agent, - notificationsEnabled, - minimized, - expanded, - windowed, - onDismissAlert, - onEnableNotifications, - onDisableNotifications, - onMinimize, - onRestore, - onOpenWindow, - t, - }) => ( + return (
- {alerts && - alerts.map((alert) => ( - - {alert.children} - - ))} + {alerts?.map((alert) => ( + + {alert.children} + + ))} } - large={this.largeHeader()} + large={largeHeader()} > - {agent && agent.avatar && ( + {agent?.avatar && ( - + )} - {this.headerTitle(t)} - {agent && agent.email && {agent.email}} - {agent && agent.phone && {agent.phone}} + {headerTitle()} + {agent?.email && {agent.email}} + {agent?.phone && {agent.phone}} @@ -108,6 +124,6 @@ class ScreenHeader extends Component {
); -} +}; export default withTranslation()(ScreenHeader); diff --git a/packages/livechat/src/components/Tooltip/index.js b/packages/livechat/src/components/Tooltip/index.js index 2f5368d8729c..3b2d36f3609a 100644 --- a/packages/livechat/src/components/Tooltip/index.js +++ b/packages/livechat/src/components/Tooltip/index.js @@ -98,7 +98,7 @@ export class TooltipContainer extends Component { } } -export const TooltipTrigger = ({ children, content, placement }) => ( +export const TooltipTrigger = ({ children, content, placement = '' }) => ( {({ showTooltip, hideTooltip }) => toChildArray(children).map((child, index) => diff --git a/packages/livechat/src/definitions/agents.d.ts b/packages/livechat/src/definitions/agents.d.ts new file mode 100644 index 000000000000..da1b81242574 --- /dev/null +++ b/packages/livechat/src/definitions/agents.d.ts @@ -0,0 +1,13 @@ +// TODO: Fully type agents in livechat +export type Agent = { + name?: string; + status?: string; + email?: string; + phone?: string; + username: string; + avatar?: { + description: string; + src: string; + }; + [key: string]: unknown; +}; diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 23e77ff1de29..a1874b144347 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -79,3 +79,4 @@ export * from './models/IAuditLogModel'; export * from './models/ICronHistoryModel'; export * from './models/IMigrationsModel'; export * from './models/IModerationReportsModel'; +export * from './models/ICloudAnnouncementsModel'; diff --git a/packages/model-typings/src/models/IBannersModel.ts b/packages/model-typings/src/models/IBannersModel.ts index 62f33ef5d3b2..4fe496bb954c 100644 --- a/packages/model-typings/src/models/IBannersModel.ts +++ b/packages/model-typings/src/models/IBannersModel.ts @@ -1,10 +1,10 @@ -import type { BannerPlatform, IBanner } from '@rocket.chat/core-typings'; +import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typings'; import type { Document, FindCursor, FindOptions, UpdateResult, InsertOneResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; export interface IBannersModel extends IBaseModel { - create(doc: IBanner): Promise>; + create(doc: Optional): Promise>; findActiveByRoleOrId(roles: string[], platform: BannerPlatform, bannerId?: string, options?: FindOptions): FindCursor; diff --git a/packages/model-typings/src/models/ICloudAnnouncementsModel.ts b/packages/model-typings/src/models/ICloudAnnouncementsModel.ts new file mode 100644 index 000000000000..672ff8c316a0 --- /dev/null +++ b/packages/model-typings/src/models/ICloudAnnouncementsModel.ts @@ -0,0 +1,6 @@ +import type { Cloud } from '@rocket.chat/core-typings'; + +import type { IBaseModel } from './IBaseModel'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ICloudAnnouncementsModel extends IBaseModel {} diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index c0ce51f79f45..1ee2a432c3df 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -372,7 +372,7 @@ export interface IUsersModel extends IBaseModel { getUsersToSendOfflineEmail(userIds: string[]): FindCursor>; countActiveUsersByService(service: string, options?: FindOptions): Promise; getActiveLocalUserCount(): Promise; - getActiveLocalGuestCount(): Promise; + getActiveLocalGuestCount(exceptions?: IUser['_id'] | IUser['_id'][]): Promise; removeOlderResumeTokensByUserId(userId: string, fromDate: Date): Promise; findAllUsersWithPendingAvatar(): FindCursor; updateCustomFieldsById(userId: string, customFields: Record): Promise; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index e1cf91f1b0ee..1e83fe72b93e 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -78,6 +78,7 @@ import type { ICronHistoryModel, IMigrationsModel, IModerationReportsModel, + ICloudAnnouncementsModel, } from '@rocket.chat/model-typings'; import { proxify } from './proxify'; @@ -170,3 +171,4 @@ export const AuditLog = proxify('IAuditLogModel'); export const CronHistory = proxify('ICronHistoryModel'); export const Migrations = proxify('IMigrationsModel'); export const ModerationReports = proxify('IModerationReportsModel'); +export const CloudAnnouncements = proxify('ICloudAnnouncementsModel'); diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 8b6f60f294b3..9da7694d28b9 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -26,6 +26,7 @@ "dependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/license": "workspace:^", "@rocket.chat/message-parser": "next", "@rocket.chat/ui-kit": "^0.32.1", "ajv": "^8.11.0", diff --git a/packages/rest-typings/src/default/index.ts b/packages/rest-typings/src/default/index.ts index b3aa5d3aa535..0be60fc4413b 100644 --- a/packages/rest-typings/src/default/index.ts +++ b/packages/rest-typings/src/default/index.ts @@ -1,36 +1,38 @@ // eslint-disable-next-line @typescript-eslint/naming-convention export interface DefaultEndpoints { '/info': { - GET: () => - | { - info: { - build: { - arch: string; - cpus: number; - date: string; - freeMemory: number; - nodeVersion: string; - osRelease: string; - platform: string; - totalMemory: number; - }; - commit: { - author?: string; - branch?: string; - date?: string; - hash?: string; - subject?: string; - tag?: string; - }; - marketplaceApiVersion: string; - version: string; - tag?: string; - branch?: string; - }; - } - | { - version: string | undefined; - }; + GET: () => { + info: { + build: { + arch: string; + cpus: number; + date: string; + freeMemory: number; + nodeVersion: string; + osRelease: string; + platform: string; + totalMemory: number; + }; + commit: { + author?: string; + branch?: string; + date?: string; + hash?: string; + subject?: string; + tag?: string; + }; + marketplaceApiVersion: string; + version: string; + tag?: string; + branch?: string; + }; + supportedVersions?: { signed: string }; + minimumClientVersions: { + desktop: string; + mobile: string; + }; + version: string | undefined; + }; }; '/ecdh_proxy/initEncryptedSession': { POST: () => void; diff --git a/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts b/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts index 3690d0672ce2..914739d000a0 100644 --- a/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts +++ b/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts @@ -21,7 +21,7 @@ const AutotranslateSaveSettingsParamsPostSchema = { enum: ['autoTranslate', 'autoTranslateLanguage'], }, value: { - type: ['boolean', 'string'], + anyOf: [{ type: 'boolean' }, { type: 'string' }], }, defaultLanguage: { type: 'string', diff --git a/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts b/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts index a63d37da07ba..a6009fe20d85 100644 --- a/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts +++ b/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts @@ -11,7 +11,7 @@ const FederationVerifyMatrixIdPropsSchema = { properties: { matrixIds: { type: 'array', - items: [{ type: 'string' }], + items: { type: 'string' }, uniqueItems: true, }, }, diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts index e9ef650656cd..249a12096729 100644 --- a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts +++ b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts @@ -1,4 +1,4 @@ -import type { OutgoingIntegrationEvent } from '@rocket.chat/core-typings'; +import type { OutgoingIntegrationEvent, IntegrationScriptEngine } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; const ajv = new Ajv(); @@ -16,6 +16,7 @@ export type IntegrationsCreateProps = alias?: string; avatar?: string; emoji?: string; + scriptEngine?: IntegrationScriptEngine; } | { type: 'webhook-outgoing'; @@ -44,6 +45,7 @@ export type IntegrationsCreateProps = alias?: string; avatar?: string; emoji?: string; + scriptEngine?: IntegrationScriptEngine; }; const integrationsCreateSchema = { @@ -96,6 +98,10 @@ const integrationsCreateSchema = { type: 'string', nullable: true, }, + scriptEngine: { + type: 'string', + nullable: true, + }, }, required: ['type', 'username', 'channel', 'scriptEnabled', 'name', 'enabled'], additionalProperties: false, @@ -196,6 +202,10 @@ const integrationsCreateSchema = { type: 'string', nullable: true, }, + scriptEngine: { + type: 'string', + nullable: true, + }, }, required: ['type', 'username', 'channel', 'event', 'scriptEnabled', 'name', 'enabled'], additionalProperties: false, diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index c6d102a967e4..96c67e2654bb 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicense } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -24,7 +24,7 @@ export const isLicensesAddProps = ajv.compile(licensesAddProps export type LicensesEndpoints = { '/v1/licenses.get': { - GET: () => { licenses: Array }; + GET: () => { licenses: Array }; }; '/v1/licenses.add': { POST: (params: licensesAddProps) => void; diff --git a/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts b/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts index 69b1d85f22a5..48b859a7899b 100644 --- a/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts +++ b/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts @@ -10,6 +10,7 @@ type ReportHistoryProps = { export type ReportHistoryPropsGET = PaginatedRequest; const reportHistoryPropsSchema = { + type: 'object', properties: { latest: { type: 'string', diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 083dab00753d..bebea2856861 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -2550,6 +2550,10 @@ const GETLivechatRoomsParamsSchema = { type: 'string', nullable: true, }, + query: { + type: 'string', + nullable: true, + }, fields: { type: 'string', nullable: true, @@ -2582,12 +2586,16 @@ const GETLivechatRoomsParamsSchema = { nullable: true, }, open: { - type: ['string', 'boolean'], - nullable: true, + anyOf: [ + { type: 'string', nullable: true }, + { type: 'boolean', nullable: true }, + ], }, onhold: { - type: ['string', 'boolean'], - nullable: true, + anyOf: [ + { type: 'string', nullable: true }, + { type: 'boolean', nullable: true }, + ], }, tags: { type: 'array', @@ -3112,7 +3120,7 @@ const POSTLivechatAppearanceParamsSchema = { type: 'string', }, value: { - type: ['string', 'boolean', 'number'], + anyOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'number' }], }, }, required: ['_id', 'value'], diff --git a/packages/server-cloud-communication/.eslintrc.json b/packages/server-cloud-communication/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/packages/server-cloud-communication/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/server-cloud-communication/package.json b/packages/server-cloud-communication/package.json new file mode 100644 index 000000000000..52a3ff801dac --- /dev/null +++ b/packages/server-cloud-communication/package.json @@ -0,0 +1,27 @@ +{ + "name": "@rocket.chat/server-cloud-communication", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@rocket.chat/license": "workspace:^", + "@types/jest": "~29.5.3", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "ts-jest": "~29.0.5", + "typescript": "~5.1.6" + }, + "volta": { + "extends": "../../package.json" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./src/index.ts", + "types": "./src/index.ts", + "files": [ + "/dist" + ] +} diff --git a/packages/server-cloud-communication/src/definitions/index.ts b/packages/server-cloud-communication/src/definitions/index.ts new file mode 100644 index 000000000000..d554aa538059 --- /dev/null +++ b/packages/server-cloud-communication/src/definitions/index.ts @@ -0,0 +1,40 @@ +type Dictionary = { [lng: string]: Record }; + +type Message = { + remainingDays: number; + title: 'message_token'; + subtitle: 'message_token'; + description: 'message_token'; + type: 'info' | 'alert' | 'error'; + params: Record & { + instance_ws_name: string; + instance_domain: string; + remaining_days: number; + }; + link: string; +}; + +type Version = { + version: string; + expiration: Date; + messages?: Message[]; +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface SupportedVersions { + timestamp: string; + messages?: Message[]; + versions: Version[]; + exceptions?: { + domain: string; + uniqueId: string; + messages?: Message[]; + versions: Version[]; + }; + i18n?: Dictionary; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface SignedSupportedVersions extends SupportedVersions { + signed: string; // SerializedJWT; +} diff --git a/packages/server-cloud-communication/src/index.ts b/packages/server-cloud-communication/src/index.ts new file mode 100644 index 000000000000..382400b0c72c --- /dev/null +++ b/packages/server-cloud-communication/src/index.ts @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { SupportedVersions, SignedSupportedVersions } from './definitions'; + +export { SupportedVersions, SignedSupportedVersions }; diff --git a/packages/server-cloud-communication/tsconfig.json b/packages/server-cloud-communication/tsconfig.json new file mode 100644 index 000000000000..e2be47cf5499 --- /dev/null +++ b/packages/server-cloud-communication/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 261823100d0a..b8bc90d9cb54 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,3 +1,4 @@ export * from './pick'; export * from './timezone'; export * from './stream'; +export * from './wrapExceptions'; diff --git a/packages/tools/src/wrapExceptions.ts b/packages/tools/src/wrapExceptions.ts new file mode 100644 index 000000000000..bd830a92bfeb --- /dev/null +++ b/packages/tools/src/wrapExceptions.ts @@ -0,0 +1,46 @@ +const isPromise = (value: unknown): value is Promise => !!value && value instanceof Promise; + +export function wrapExceptions( + getter: () => T, +): { + catch: (errorWrapper: (error: any) => T) => T; + suppress: (errorWrapper?: (error: any) => void) => T | undefined; +}; +export function wrapExceptions( + getter: () => Promise, +): { + catch: (errorWrapper: (error: any) => T | Awaited) => Promise; + suppress: (errorWrapper?: (error: any) => void) => Promise; +}; +export function wrapExceptions(getter: () => T) { + const doCatch = (errorWrapper: (error: any) => T | Awaited): T => { + try { + const value = getter(); + if (isPromise(value)) { + return value.catch(errorWrapper) as T; + } + + return value; + } catch (error) { + return errorWrapper(error); + } + }; + + const doSuppress = (errorWrapper?: (error: any) => void) => { + try { + const value = getter(); + if (isPromise(value)) { + return value.catch((error) => errorWrapper?.(error)); + } + + return value; + } catch (error) { + errorWrapper?.(error); + } + }; + + return { + catch: doCatch, + suppress: doSuppress, + }; +} diff --git a/yarn.lock b/yarn.lock index 7d7b23691683..b915bb0f2e2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,6 +966,33 @@ __metadata: languageName: node linkType: hard +"@babel/cli@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/cli@npm:7.23.0" + dependencies: + "@jridgewell/trace-mapping": ^0.3.17 + "@nicolo-ribaudo/chokidar-2": 2.1.8-no-fsevents.3 + chokidar: ^3.4.0 + commander: ^4.0.1 + convert-source-map: ^2.0.0 + fs-readdir-recursive: ^1.1.0 + glob: ^7.2.0 + make-dir: ^2.1.0 + slash: ^2.0.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + dependenciesMeta: + "@nicolo-ribaudo/chokidar-2": + optional: true + chokidar: + optional: true + bin: + babel: ./bin/babel.js + babel-external-helpers: ./bin/babel-external-helpers.js + checksum: beeb189560bf9c4ea951ef637eefa5214654678fb09c4aaa6695921037059c1e1553c610fe95fbd19a9cdfd9f5598a812fc13df40a6b9a9ea899e43fc6c42052 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -975,20 +1002,20 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.10, @babel/code-frame@npm:^7.22.5, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": - version: 7.22.10 - resolution: "@babel/code-frame@npm:7.22.10" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.10, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": + version: 7.22.13 + resolution: "@babel/code-frame@npm:7.22.13" dependencies: - "@babel/highlight": ^7.22.10 + "@babel/highlight": ^7.22.13 chalk: ^2.4.2 - checksum: 89a06534ad19759da6203a71bad120b1d7b2ddc016c8e07d4c56b35dea25e7396c6da60a754e8532a86733092b131ae7f661dbe6ba5d165ea777555daa2ed3c9 + checksum: 22e342c8077c8b77eeb11f554ecca2ba14153f707b85294fcf6070b6f6150aae88a7b7436dd88d8c9289970585f3fe5b9b941c5aa3aa26a6d5a8ef3f292da058 languageName: node linkType: hard -"@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.22.5, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9": - version: 7.22.9 - resolution: "@babel/compat-data@npm:7.22.9" - checksum: bed77d9044ce948b4327b30dd0de0779fa9f3a7ed1f2d31638714ed00229fa71fc4d1617ae0eb1fad419338d3658d0e9a5a083297451e09e73e078d0347ff808 +"@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.22.20, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9": + version: 7.22.20 + resolution: "@babel/compat-data@npm:7.22.20" + checksum: efedd1d18878c10fde95e4d82b1236a9aba41395ef798cbb651f58dbf5632dbff475736c507b8d13d4c8f44809d41c0eb2ef0d694283af9ba5dd8339b6dab451 languageName: node linkType: hard @@ -1016,7 +1043,30 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.7.5, @babel/core@npm:~7.22.10, @babel/core@npm:~7.22.9": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.23.0, @babel/core@npm:^7.7.5": + version: 7.23.0 + resolution: "@babel/core@npm:7.23.0" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.22.13 + "@babel/generator": ^7.23.0 + "@babel/helper-compilation-targets": ^7.22.15 + "@babel/helper-module-transforms": ^7.23.0 + "@babel/helpers": ^7.23.0 + "@babel/parser": ^7.23.0 + "@babel/template": ^7.22.15 + "@babel/traverse": ^7.23.0 + "@babel/types": ^7.23.0 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: cebd9b48dbc970a7548522f207f245c69567e5ea17ebb1a4e4de563823cf20a01177fe8d2fe19b6e1461361f92fa169fd0b29f8ee9d44eeec84842be1feee5f2 + languageName: node + linkType: hard + +"@babel/core@npm:~7.22.10, @babel/core@npm:~7.22.9": version: 7.22.10 resolution: "@babel/core@npm:7.22.10" dependencies: @@ -1053,15 +1103,15 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.22.10, @babel/generator@npm:^7.7.2": - version: 7.22.10 - resolution: "@babel/generator@npm:7.22.10" +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.22.10, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.7.2": + version: 7.23.0 + resolution: "@babel/generator@npm:7.23.0" dependencies: - "@babel/types": ^7.22.10 + "@babel/types": ^7.23.0 "@jridgewell/gen-mapping": ^0.3.2 "@jridgewell/trace-mapping": ^0.3.17 jsesc: ^2.5.1 - checksum: 59a79730abdff9070692834bd3af179e7a9413fa2ff7f83dff3eb888765aeaeb2bfc7b0238a49613ed56e1af05956eff303cc139f2407eda8df974813e486074 + checksum: 8efe24adad34300f1f8ea2add420b28171a646edc70f2a1b3e1683842f23b8b7ffa7e35ef0119294e1901f45bfea5b3dc70abe1f10a1917ccdfb41bed69be5f1 languageName: node linkType: hard @@ -1083,35 +1133,35 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.13.0, @babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.22.10, @babel/helper-compilation-targets@npm:^7.22.5, @babel/helper-compilation-targets@npm:^7.22.6": - version: 7.22.10 - resolution: "@babel/helper-compilation-targets@npm:7.22.10" +"@babel/helper-compilation-targets@npm:^7.13.0, @babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.22.10, @babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.22.5, @babel/helper-compilation-targets@npm:^7.22.6": + version: 7.22.15 + resolution: "@babel/helper-compilation-targets@npm:7.22.15" dependencies: "@babel/compat-data": ^7.22.9 - "@babel/helper-validator-option": ^7.22.5 + "@babel/helper-validator-option": ^7.22.15 browserslist: ^4.21.9 lru-cache: ^5.1.1 semver: ^6.3.1 - checksum: f6f1896816392bcff671bbe6e277307729aee53befb4a66ea126e2a91eda78d819a70d06fa384c74ef46c1595544b94dca50bef6c78438d9ffd31776dafbd435 + checksum: ce85196769e091ae54dd39e4a80c2a9df1793da8588e335c383d536d54f06baf648d0a08fc873044f226398c4ded15c4ae9120ee18e7dfd7c639a68e3cdc9980 languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.17.6, @babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-create-class-features-plugin@npm:7.22.5" +"@babel/helper-create-class-features-plugin@npm:^7.17.6, @babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/helper-create-class-features-plugin@npm:7.22.15" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 "@babel/helper-environment-visitor": ^7.22.5 "@babel/helper-function-name": ^7.22.5 - "@babel/helper-member-expression-to-functions": ^7.22.5 + "@babel/helper-member-expression-to-functions": ^7.22.15 "@babel/helper-optimise-call-expression": ^7.22.5 - "@babel/helper-replace-supers": ^7.22.5 + "@babel/helper-replace-supers": ^7.22.9 "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - "@babel/helper-split-export-declaration": ^7.22.5 - semver: ^6.3.0 + "@babel/helper-split-export-declaration": ^7.22.6 + semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0 - checksum: f1e91deae06dbee6dd956c0346bca600adfbc7955427795d9d8825f0439a3c3290c789ba2b4a02a1cdf6c1a1bd163dfa16d3d5e96b02a8efb639d2a774e88ed9 + checksum: 52c500d8d164abb3a360b1b7c4b8fff77bc4a5920d3a2b41ae6e1d30617b0dc0b972c1f5db35b1752007e04a748908b4a99bc872b73549ae837e87dcdde005a3 languageName: node linkType: hard @@ -1161,20 +1211,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-environment-visitor@npm:7.22.5" - checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1 +"@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5": + version: 7.22.20 + resolution: "@babel/helper-environment-visitor@npm:7.22.20" + checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-function-name@npm:7.22.5" +"@babel/helper-function-name@npm:^7.22.5, @babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" dependencies: - "@babel/template": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a + "@babel/template": ^7.22.15 + "@babel/types": ^7.23.0 + checksum: e44542257b2d4634a1f979244eb2a4ad8e6d75eb6761b4cfceb56b562f7db150d134bc538c8e6adca3783e3bc31be949071527aa8e3aab7867d1ad2d84a26e10 languageName: node linkType: hard @@ -1187,36 +1237,36 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-member-expression-to-functions@npm:7.22.5" +"@babel/helper-member-expression-to-functions@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" dependencies: - "@babel/types": ^7.22.5 - checksum: 4bd5791529c280c00743e8bdc669ef0d4cd1620d6e3d35e0d42b862f8262bc2364973e5968007f960780344c539a4b9cf92ab41f5b4f94560a9620f536de2a39 + "@babel/types": ^7.23.0 + checksum: 494659361370c979ada711ca685e2efe9460683c36db1b283b446122596602c901e291e09f2f980ecedfe6e0f2bd5386cb59768285446530df10c14df1024e75 languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.12.13, @babel/helper-module-imports@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-module-imports@npm:7.22.5" +"@babel/helper-module-imports@npm:^7.12.13, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/helper-module-imports@npm:7.22.15" dependencies: - "@babel/types": ^7.22.5 - checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad + "@babel/types": ^7.22.15 + checksum: ecd7e457df0a46f889228f943ef9b4a47d485d82e030676767e6a2fdcbdaa63594d8124d4b55fd160b41c201025aec01fc27580352b1c87a37c9c6f33d116702 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.22.9": - version: 7.22.9 - resolution: "@babel/helper-module-transforms@npm:7.22.9" +"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.22.9, @babel/helper-module-transforms@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-module-transforms@npm:7.23.0" dependencies: - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-module-imports": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-module-imports": ^7.22.15 "@babel/helper-simple-access": ^7.22.5 "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 peerDependencies: "@babel/core": ^7.0.0 - checksum: 2751f77660518cf4ff027514d6f4794f04598c6393be7b04b8e46c6e21606e11c19f3f57ab6129a9c21bacdf8b3ffe3af87bb401d972f34af2d0ffde02ac3001 + checksum: 6e2afffb058cf3f8ce92f5116f710dda4341c81cfcd872f9a0197ea594f7ce0ab3cb940b0590af2fe99e60d2e5448bfba6bca8156ed70a2ed4be2adc8586c891 languageName: node linkType: hard @@ -1256,17 +1306,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.16.7, @babel/helper-replace-supers@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-replace-supers@npm:7.22.5" +"@babel/helper-replace-supers@npm:^7.16.7, @babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9": + version: 7.22.20 + resolution: "@babel/helper-replace-supers@npm:7.22.20" dependencies: - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-member-expression-to-functions": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-member-expression-to-functions": ^7.22.15 "@babel/helper-optimise-call-expression": ^7.22.5 - "@babel/template": ^7.22.5 - "@babel/traverse": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: af29deff6c6dc3fa2d1a517390716aa3f4d329855e8689f1d5c3cb07c1b898e614a5e175f1826bb58e9ff1480e6552885a71a9a0ba5161787aaafa2c79b216cc + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a0008332e24daedea2e9498733e3c39b389d6d4512637e000f96f62b797e702ee24a407ccbcd7a236a551590a38f31282829a8ef35c50a3c0457d88218cae639 languageName: node linkType: hard @@ -1288,7 +1337,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.22.5, @babel/helper-split-export-declaration@npm:^7.22.6": +"@babel/helper-split-export-declaration@npm:^7.22.6": version: 7.22.6 resolution: "@babel/helper-split-export-declaration@npm:7.22.6" dependencies: @@ -1304,17 +1353,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-validator-identifier@npm:7.22.5" - checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.16.7, @babel/helper-validator-option@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-validator-option@npm:7.22.5" - checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3 +"@babel/helper-validator-option@npm:^7.16.7, @babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/helper-validator-option@npm:7.22.15" + checksum: 68da52b1e10002a543161494c4bc0f4d0398c8fdf361d5f7f4272e95c45d5b32d974896d44f6a0ea7378c9204988879d73613ca683e13bd1304e46d25ff67a8d languageName: node linkType: hard @@ -1329,58 +1378,58 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/helpers@npm:7.22.10" +"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.22.10, @babel/helpers@npm:^7.23.0": + version: 7.23.1 + resolution: "@babel/helpers@npm:7.23.1" dependencies: - "@babel/template": ^7.22.5 - "@babel/traverse": ^7.22.10 - "@babel/types": ^7.22.10 - checksum: 3b1219e362df390b6c5d94b75a53fc1c2eb42927ced0b8022d6a29b833a839696206b9bdad45b4805d05591df49fc16b6fb7db758c9c2ecfe99e3e94cb13020f + "@babel/template": ^7.22.15 + "@babel/traverse": ^7.23.0 + "@babel/types": ^7.23.0 + checksum: acfc345102045c24ea2a4d60e00dcf8220e215af3add4520e2167700661338e6a80bd56baf44bb764af05ec6621101c9afc315dc107e18c61fa6da8acbdbb893 languageName: node linkType: hard -"@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/highlight@npm:7.22.10" +"@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.22.13": + version: 7.22.20 + resolution: "@babel/highlight@npm:7.22.20" dependencies: - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 chalk: ^2.4.2 js-tokens: ^4.0.0 - checksum: f714a1e1a72dd9d72f6383f4f30fd342e21a8df32d984a4ea8f5eab691bb6ba6db2f8823d4b4cf135d98869e7a98925b81306aa32ee3c429f8cfa52c75889e1b + checksum: 84bd034dca309a5e680083cd827a766780ca63cef37308404f17653d32366ea76262bd2364b2d38776232f2d01b649f26721417d507e8b4b6da3e4e739f6d134 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.10, @babel/parser@npm:^7.22.5": - version: 7.22.10 - resolution: "@babel/parser@npm:7.22.10" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.10, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/parser@npm:7.23.0" bin: parser: ./bin/babel-parser.js - checksum: af51567b7d3cdf523bc608eae057397486c7fa6c2e5753027c01fe5c36f0767b2d01ce3049b222841326cc5b8c7fda1d810ac1a01af0a97bb04679e2ef9f7049 + checksum: 453fdf8b9e2c2b7d7b02139e0ce003d1af21947bbc03eb350fb248ee335c9b85e4ab41697ddbdd97079698de825a265e45a0846bb2ed47a2c7c1df833f42a354 languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.5" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0 - checksum: 1e353a060fb2cd8f1256d28cd768f16fb02513f905b9b6d656fb0242c96c341a196fa188b27c2701506a6e27515359fbcc1a5ca7fa8b9b530cf88fbd137baefc + checksum: 8910ca21a7ec7c06f7b247d4b86c97c5aa15ef321518f44f6f490c5912fdf82c605aaa02b90892e375d82ccbedeadfdeadd922c1b836c9dd4c596871bf654753 languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.22.5" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - "@babel/plugin-transform-optional-chaining": ^7.22.5 + "@babel/plugin-transform-optional-chaining": ^7.22.15 peerDependencies: "@babel/core": ^7.13.0 - checksum: 16e7a5f3bf2f2ac0ca032a70bf0ebd7e886d84dbb712b55c0643c04c495f0f221fbcbca14b5f8f8027fa6c87a3dafae0934022ad2b409384af6c5c356495b7bd + checksum: fbefedc0da014c37f1a50a8094ce7dbbf2181ae93243f23d6ecba2499b5b20196c2124d6a4dfe3e9e0125798e80593103e456352a4beb4e5c6f7c75efb80fdac languageName: node linkType: hard @@ -1798,9 +1847,9 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.22.10" +"@babel/plugin-transform-async-generator-functions@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.22.15" dependencies: "@babel/helper-environment-visitor": ^7.22.5 "@babel/helper-plugin-utils": ^7.22.5 @@ -1808,7 +1857,7 @@ __metadata: "@babel/plugin-syntax-async-generators": ^7.8.4 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 87d77b66fda05b42450aa285fa031aa3963c52aab00190f95f6c3ddefbed683035c1f314347c888f8406fba5d436b888ff75b5e36b8ab23afd4ca4c3f086f88c + checksum: fad98786b446ce63bde0d14a221e2617eef5a7bbca62b49d96f16ab5e1694521234cfba6145b830fbf9af16d60a8a3dbf148e8694830bd91796fe333b0599e73 languageName: node linkType: hard @@ -1836,14 +1885,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.12.12, @babel/plugin-transform-block-scoping@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-block-scoping@npm:7.22.10" +"@babel/plugin-transform-block-scoping@npm:^7.12.12, @babel/plugin-transform-block-scoping@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/plugin-transform-block-scoping@npm:7.23.0" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b1d06f358dedcb748a57e5feea4b9285c60593fb2912b921f22898c57c552c78fe18128678c8f84dd4ea1d4e5aebede8783830b24cd63f22c30261156d78bc77 + checksum: 0cfe925cc3b5a3ad407e2253fab3ceeaa117a4b291c9cb245578880872999bca91bd83ffa0128ae9ca356330702e1ef1dcb26804f28d2cef678239caf629f73e languageName: node linkType: hard @@ -1859,35 +1908,35 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-class-static-block@npm:7.22.5" +"@babel/plugin-transform-class-static-block@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-class-static-block@npm:7.22.11" dependencies: - "@babel/helper-create-class-features-plugin": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.22.11 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-class-static-block": ^7.14.5 peerDependencies: "@babel/core": ^7.12.0 - checksum: bc48b92dbaf625a14f2bf62382384eef01e0515802426841636ae9146e27395d068c7a8a45e9e15699491b0a01d990f38f179cbc9dc89274a393f85648772f12 + checksum: 69f040506fad66f1c6918d288d0e0edbc5c8a07c8b4462c1184ad2f9f08995d68b057126c213871c0853ae0c72afc60ec87492049dfacb20902e32346a448bcb languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.22.6": - version: 7.22.6 - resolution: "@babel/plugin-transform-classes@npm:7.22.6" +"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-classes@npm:7.22.15" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-compilation-targets": ^7.22.6 + "@babel/helper-compilation-targets": ^7.22.15 "@babel/helper-environment-visitor": ^7.22.5 "@babel/helper-function-name": ^7.22.5 "@babel/helper-optimise-call-expression": ^7.22.5 "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-replace-supers": ^7.22.5 + "@babel/helper-replace-supers": ^7.22.9 "@babel/helper-split-export-declaration": ^7.22.6 globals: ^11.1.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8380e855c01033dbc7460d9acfbc1fc37c880350fa798c2de8c594ef818ade0e4c96173ec72f05f2a4549d8d37135e18cb62548352d51557b45a0fb4388d2f3f + checksum: d3f4d0c107dd8a3557ea3575cc777fab27efa92958b41e4a9822f7499725c1f554beae58855de16ddec0a7b694e45f59a26cea8fbde4275563f72f09c6e039a0 languageName: node linkType: hard @@ -1903,14 +1952,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-destructuring@npm:7.22.10" +"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/plugin-transform-destructuring@npm:7.23.0" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 011707801bd0029fd4f0523d24d06fdc0cbe8c9da280d75728f76713d639c4dc976e1b56a1ba7bff25468f86867efb71c9b4cac81140adbdd0abf2324b19a8bb + checksum: cd6dd454ccc2766be551e4f8a04b1acc2aa539fa19e5c7501c56cc2f8cc921dd41a7ffb78455b4c4b2f954fcab8ca4561ba7c9c7bd5af9f19465243603d18cc3 languageName: node linkType: hard @@ -1937,15 +1986,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.22.5" +"@babel/plugin-transform-dynamic-import@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-dynamic-import": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 186a6d59f36eb3c5824739fc9c22ed0f4ca68e001662aa3a302634346a8b785cb9579b23b0c158f4570604d697d19598ca09b58c60a7fa2894da1163c4eb1907 + checksum: 78fc9c532210bf9e8f231747f542318568ac360ee6c27e80853962c984283c73da3f8f8aebe83c2096090a435b356b092ed85de617a156cbe0729d847632be45 languageName: node linkType: hard @@ -1961,15 +2010,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.22.5" +"@babel/plugin-transform-export-namespace-from@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-export-namespace-from": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3d197b788758044983c96b9c49bed4b456055f35a388521a405968db0f6e2ffb6fd59110e3931f4dcc5e126ae9e5e00e154a0afb47a7ea359d8d0dea79f480d7 + checksum: 73af5883a321ed56a4bfd43c8a7de0164faebe619287706896fc6ee2f7a4e69042adaa1338c0b8b4bdb9f7e5fdceb016fb1d40694cb43ca3b8827429e8aac4bf languageName: node linkType: hard @@ -1985,14 +2034,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.12.1, @babel/plugin-transform-for-of@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-for-of@npm:7.22.5" +"@babel/plugin-transform-for-of@npm:^7.12.1, @babel/plugin-transform-for-of@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-for-of@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: d7b8d4db010bce7273674caa95c4e6abd909362866ce297e86a2ecaa9ae636e05d525415811db9b3c942155df7f3651d19b91dd6c41f142f7308a97c7cb06023 + checksum: f395ae7bce31e14961460f56cf751b5d6e37dd27d7df5b1f4e49fec1c11b6f9cf71991c7ffbe6549878591e87df0d66af798cf26edfa4bfa6b4c3dba1fb2f73a languageName: node linkType: hard @@ -2009,15 +2058,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-json-strings@npm:7.22.5" +"@babel/plugin-transform-json-strings@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-json-strings@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-json-strings": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 4e00b902487a670b6c8948f33f9108133fd745cf9d1478aca515fb460b9b2f12e137988ebc1663630fb82070a870aed8b0c1aa4d007a841c18004619798f255c + checksum: 50665e5979e66358c50e90a26db53c55917f78175127ac2fa05c7888d156d418ffb930ec0a109353db0a7c5f57c756ce01bfc9825d24cbfd2b3ec453f2ed8cba languageName: node linkType: hard @@ -2032,15 +2081,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.22.5" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 18748e953c08f64885f18c224eac58df10a13eac4d845d16b5d9b6276907da7ca2530dfebe6ed41cdc5f8a75d9db3e36d8eb54ddce7cd0364af1cab09b435302 + checksum: c664e9798e85afa7f92f07b867682dee7392046181d82f5d21bae6f2ca26dfe9c8375cdc52b7483c3fc09a983c1989f60eff9fbc4f373b0c0a74090553d05739 languageName: node linkType: hard @@ -2067,30 +2116,30 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5" +"@babel/plugin-transform-modules-commonjs@npm:^7.22.15, @babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.0" dependencies: - "@babel/helper-module-transforms": ^7.22.5 + "@babel/helper-module-transforms": ^7.23.0 "@babel/helper-plugin-utils": ^7.22.5 "@babel/helper-simple-access": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa + checksum: 7fb25997194053e167c4207c319ff05362392da841bd9f42ddb3caf9c8798a5d203bd926d23ddf5830fdf05eddc82c2810f40d1287e3a4f80b07eff13d1024b5 languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.22.5" +"@babel/plugin-transform-modules-systemjs@npm:^7.22.11": + version: 7.23.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.0" dependencies: "@babel/helper-hoist-variables": ^7.22.5 - "@babel/helper-module-transforms": ^7.22.5 + "@babel/helper-module-transforms": ^7.23.0 "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 04f4178589543396b3c24330a67a59c5e69af5e96119c9adda730c0f20122deaff54671ebbc72ad2df6495a5db8a758bd96942de95fba7ad427de9c80b1b38c8 + checksum: 2d481458b22605046badea2317d5cc5c94ac3031c2293e34c96f02063f5b02af0979c4da6a8fbc67cc249541575dc9c6d710db6b919ede70b7337a22d9fd57a7 languageName: node linkType: hard @@ -2129,42 +2178,42 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.22.5" +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e6a059169d257fc61322d0708edae423072449b7c33de396261e68dee582aec5396789a1c22bce84e5bd88a169623c2e750b513fc222930979e6accd52a44bf2 + checksum: 167babecc8b8fe70796a7b7d34af667ebbf43da166c21689502e5e8cc93180b7a85979c77c9f64b7cce431b36718bd0a6df9e5e0ffea4ae22afb22cfef886372 languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.22.5" +"@babel/plugin-transform-numeric-separator@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-numeric-separator": ^7.10.4 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9e7837d4eae04f211ebaa034fe5003d2927b6bf6d5b9dc09f2b1183c01482cdde5a75b8bd5c7ff195c2abc7b923339eb0b2a9d27cb78359d38248a3b2c2367c4 + checksum: af064d06a4a041767ec396a5f258103f64785df290e038bba9f0ef454e6c914f2ac45d862bbdad8fac2c7ad47fa4e95356f29053c60c100a0160b02a995fe2a3 languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.22.5" +"@babel/plugin-transform-object-rest-spread@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.22.15" dependencies: - "@babel/compat-data": ^7.22.5 - "@babel/helper-compilation-targets": ^7.22.5 + "@babel/compat-data": ^7.22.9 + "@babel/helper-compilation-targets": ^7.22.15 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-transform-parameters": ^7.22.5 + "@babel/plugin-transform-parameters": ^7.22.15 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3b5e091f0dc67108f2e41ed5a97e15bbe4381a19d9a7eea80b71c7de1d8169fd28784e1e41a3d2ad12709ab212e58fc481282a5bb65d591fae7b443048de3330 + checksum: 62197a6f12289c1c1bd57f3bed9f0f765ca32390bfe91e0b5561dd94dd9770f4480c4162dec98da094bc0ba99d2c2ebba68de47c019454041b0b7a68ba2ec66d languageName: node linkType: hard @@ -2180,39 +2229,39 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.22.5" +"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b0e8b4233ff06b5c9d285257f49c5bd441f883189b24282e6200f9ebdf5db29aeeebbffae57fbbcd5df9f4387b3e66e5d322aaae5652a78e89685ddbae46bbd1 + checksum: f17abd90e1de67c84d63afea29c8021c74abb2794d3a6eeafb0bbe7372d3db32aefca386e392116ec63884537a4a2815d090d26264d259bacc08f6e3ed05294c languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.22.10, @babel/plugin-transform-optional-chaining@npm:^7.22.5": - version: 7.22.10 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.22.10" +"@babel/plugin-transform-optional-chaining@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.0" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 "@babel/plugin-syntax-optional-chaining": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 522d6214bb9f6ede8a2fc56a873e791aabd62f0b3be78fb8e62ca801a9033bcadabfb77aec6739f0e67f0f15f7c739c08bafafd66d3676edf1941fe6429cebcd + checksum: f702634f2b97e5260dbec0d4bde05ccb6f4d96d7bfa946481aeacfa205ca846cb6e096a38312f9d51fdbdac1f258f211138c5f7075952e46a5bf8574de6a1329 languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.12.1, @babel/plugin-transform-parameters@npm:^7.20.7, @babel/plugin-transform-parameters@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-parameters@npm:7.22.5" +"@babel/plugin-transform-parameters@npm:^7.12.1, @babel/plugin-transform-parameters@npm:^7.20.7, @babel/plugin-transform-parameters@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-parameters@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b44f89cf97daf23903776ba27c2ab13b439d80d8c8a95be5c476ab65023b1e0c0e94c28d3745f3b60a58edc4e590fa0cd4287a0293e51401ca7d29a2ddb13b8e + checksum: 541188bb7d1876cad87687b5c7daf90f63d8208ae83df24acb1e2b05020ad1c78786b2723ca4054a83fcb74fb6509f30c4cacc5b538ee684224261ad5fb047c1 languageName: node linkType: hard @@ -2228,17 +2277,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.5" +"@babel/plugin-transform-private-property-in-object@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.11" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-create-class-features-plugin": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.22.11 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-private-property-in-object": ^7.14.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9ac019fb2772f3af6278a7f4b8b14b0663accb3fd123d87142ceb2fbc57fd1afa07c945d1329029b026b9ee122096ef71a3f34f257a9e04cf4245b87298c38b4 + checksum: 4d029d84901e53c46dead7a46e2990a7bc62470f4e4ca58a0d063394f86652fd58fe4eea1eb941da3669cd536b559b9d058b342b59300026346b7a2a51badac8 languageName: node linkType: hard @@ -2403,17 +2452,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-typescript@npm:7.22.5" +"@babel/plugin-transform-typescript@npm:^7.22.15, @babel/plugin-transform-typescript@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/plugin-transform-typescript@npm:7.22.15" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-create-class-features-plugin": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.22.15 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-typescript": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: d12f1ca1ef1f2a54432eb044d2999705d1205ebe211c2a7f05b12e8eb2d2a461fd7657b5486b2f2f1efe7c0c0dc8e80725b767073d40fe4ae059a7af057b05e4 + checksum: c5d96cdbf0e1512707aa1c1e3ac6b370a25fd9c545d26008ce44eb13a47bd7fd67a1eb799c98b5ccc82e33a345fda55c0055e1fe3ed97646ed405dd13020b226 languageName: node linkType: hard @@ -2464,16 +2513,16 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": - version: 7.22.10 - resolution: "@babel/preset-env@npm:7.22.10" +"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.22.20, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": + version: 7.22.20 + resolution: "@babel/preset-env@npm:7.22.20" dependencies: - "@babel/compat-data": ^7.22.9 - "@babel/helper-compilation-targets": ^7.22.10 + "@babel/compat-data": ^7.22.20 + "@babel/helper-compilation-targets": ^7.22.15 "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-option": ^7.22.5 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.22.5 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.22.5 + "@babel/helper-validator-option": ^7.22.15 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.22.15 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.22.15 "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2 "@babel/plugin-syntax-async-generators": ^7.8.4 "@babel/plugin-syntax-class-properties": ^7.12.13 @@ -2494,41 +2543,41 @@ __metadata: "@babel/plugin-syntax-top-level-await": ^7.14.5 "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6 "@babel/plugin-transform-arrow-functions": ^7.22.5 - "@babel/plugin-transform-async-generator-functions": ^7.22.10 + "@babel/plugin-transform-async-generator-functions": ^7.22.15 "@babel/plugin-transform-async-to-generator": ^7.22.5 "@babel/plugin-transform-block-scoped-functions": ^7.22.5 - "@babel/plugin-transform-block-scoping": ^7.22.10 + "@babel/plugin-transform-block-scoping": ^7.22.15 "@babel/plugin-transform-class-properties": ^7.22.5 - "@babel/plugin-transform-class-static-block": ^7.22.5 - "@babel/plugin-transform-classes": ^7.22.6 + "@babel/plugin-transform-class-static-block": ^7.22.11 + "@babel/plugin-transform-classes": ^7.22.15 "@babel/plugin-transform-computed-properties": ^7.22.5 - "@babel/plugin-transform-destructuring": ^7.22.10 + "@babel/plugin-transform-destructuring": ^7.22.15 "@babel/plugin-transform-dotall-regex": ^7.22.5 "@babel/plugin-transform-duplicate-keys": ^7.22.5 - "@babel/plugin-transform-dynamic-import": ^7.22.5 + "@babel/plugin-transform-dynamic-import": ^7.22.11 "@babel/plugin-transform-exponentiation-operator": ^7.22.5 - "@babel/plugin-transform-export-namespace-from": ^7.22.5 - "@babel/plugin-transform-for-of": ^7.22.5 + "@babel/plugin-transform-export-namespace-from": ^7.22.11 + "@babel/plugin-transform-for-of": ^7.22.15 "@babel/plugin-transform-function-name": ^7.22.5 - "@babel/plugin-transform-json-strings": ^7.22.5 + "@babel/plugin-transform-json-strings": ^7.22.11 "@babel/plugin-transform-literals": ^7.22.5 - "@babel/plugin-transform-logical-assignment-operators": ^7.22.5 + "@babel/plugin-transform-logical-assignment-operators": ^7.22.11 "@babel/plugin-transform-member-expression-literals": ^7.22.5 "@babel/plugin-transform-modules-amd": ^7.22.5 - "@babel/plugin-transform-modules-commonjs": ^7.22.5 - "@babel/plugin-transform-modules-systemjs": ^7.22.5 + "@babel/plugin-transform-modules-commonjs": ^7.22.15 + "@babel/plugin-transform-modules-systemjs": ^7.22.11 "@babel/plugin-transform-modules-umd": ^7.22.5 "@babel/plugin-transform-named-capturing-groups-regex": ^7.22.5 "@babel/plugin-transform-new-target": ^7.22.5 - "@babel/plugin-transform-nullish-coalescing-operator": ^7.22.5 - "@babel/plugin-transform-numeric-separator": ^7.22.5 - "@babel/plugin-transform-object-rest-spread": ^7.22.5 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.22.11 + "@babel/plugin-transform-numeric-separator": ^7.22.11 + "@babel/plugin-transform-object-rest-spread": ^7.22.15 "@babel/plugin-transform-object-super": ^7.22.5 - "@babel/plugin-transform-optional-catch-binding": ^7.22.5 - "@babel/plugin-transform-optional-chaining": ^7.22.10 - "@babel/plugin-transform-parameters": ^7.22.5 + "@babel/plugin-transform-optional-catch-binding": ^7.22.11 + "@babel/plugin-transform-optional-chaining": ^7.22.15 + "@babel/plugin-transform-parameters": ^7.22.15 "@babel/plugin-transform-private-methods": ^7.22.5 - "@babel/plugin-transform-private-property-in-object": ^7.22.5 + "@babel/plugin-transform-private-property-in-object": ^7.22.11 "@babel/plugin-transform-property-literals": ^7.22.5 "@babel/plugin-transform-regenerator": ^7.22.10 "@babel/plugin-transform-reserved-words": ^7.22.5 @@ -2542,7 +2591,7 @@ __metadata: "@babel/plugin-transform-unicode-regex": ^7.22.5 "@babel/plugin-transform-unicode-sets-regex": ^7.22.5 "@babel/preset-modules": 0.1.6-no-external-plugins - "@babel/types": ^7.22.10 + "@babel/types": ^7.22.19 babel-plugin-polyfill-corejs2: ^0.4.5 babel-plugin-polyfill-corejs3: ^0.8.3 babel-plugin-polyfill-regenerator: ^0.5.2 @@ -2550,7 +2599,7 @@ __metadata: semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 4145a660a7b05e21e6d8b6cdf348c6931238abb15282a258bdb5e04cd3cca9356dc120ecfe0d1b977819ade4aac50163127c86db2300227ff60392d24daa0b7c + checksum: 99357a5cb30f53bacdc0d1cd6dff0f052ea6c2d1ba874d969bba69897ef716e87283e84a59dc52fb49aa31fd1b6f55ed756c64c04f5678380700239f6030b881 languageName: node linkType: hard @@ -2596,7 +2645,22 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.12.7, @babel/preset-typescript@npm:~7.22.5": +"@babel/preset-typescript@npm:^7.12.7, @babel/preset-typescript@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/preset-typescript@npm:7.23.0" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-validator-option": ^7.22.15 + "@babel/plugin-syntax-jsx": ^7.22.5 + "@babel/plugin-transform-modules-commonjs": ^7.23.0 + "@babel/plugin-transform-typescript": ^7.22.15 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3d5fce83e83f11c07e0ea13542bca181abb3b482b8981ec9c64e6add9d7beed3c54d063dc4bc9fd383165c71114a245abef89a289680833c5a8552fe3e7c4407 + languageName: node + linkType: hard + +"@babel/preset-typescript@npm:~7.22.5": version: 7.22.5 resolution: "@babel/preset-typescript@npm:7.22.5" dependencies: @@ -2660,43 +2724,43 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.12.7, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": - version: 7.22.5 - resolution: "@babel/template@npm:7.22.5" +"@babel/template@npm:^7.12.7, @babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" dependencies: - "@babel/code-frame": ^7.22.5 - "@babel/parser": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3 + "@babel/code-frame": ^7.22.13 + "@babel/parser": ^7.22.15 + "@babel/types": ^7.22.15 + checksum: 1f3e7dcd6c44f5904c184b3f7fe280394b191f2fed819919ffa1e529c259d5b197da8981b6ca491c235aee8dbad4a50b7e31304aa531271cb823a4a24a0dd8fd languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.22.10, @babel/traverse@npm:^7.22.5": - version: 7.22.10 - resolution: "@babel/traverse@npm:7.22.10" +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.22.10, @babel/traverse@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/traverse@npm:7.23.0" dependencies: - "@babel/code-frame": ^7.22.10 - "@babel/generator": ^7.22.10 - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-function-name": ^7.22.5 + "@babel/code-frame": ^7.22.13 + "@babel/generator": ^7.23.0 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 "@babel/helper-hoist-variables": ^7.22.5 "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/parser": ^7.22.10 - "@babel/types": ^7.22.10 + "@babel/parser": ^7.23.0 + "@babel/types": ^7.23.0 debug: ^4.1.0 globals: ^11.1.0 - checksum: 9f7b358563bfb0f57ac4ed639f50e5c29a36b821a1ce1eea0c7db084f5b925e3275846d0de63bde01ca407c85d9804e0efbe370d92cd2baaafde3bd13b0f4cdb + checksum: 0b17fae53269e1af2cd3edba00892bc2975ad5df9eea7b84815dab07dfec2928c451066d51bc65b4be61d8499e77db7e547ce69ef2a7b0eca3f96269cb43a0b0 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.22.10 - resolution: "@babel/types@npm:7.22.10" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.23.0 + resolution: "@babel/types@npm:7.23.0" dependencies: "@babel/helper-string-parser": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 to-fast-properties: ^2.0.0 - checksum: 095c4f4b7503fa816e4094113f0ec2351ef96ff32012010b771693066ff628c7c664b21c6bd3fb93aeb46fe7c61f6b3a3c9e4ed0034d6a2481201c417371c8af + checksum: 215fe04bd7feef79eeb4d33374b39909ce9cad1611c4135a4f7fdf41fe3280594105af6d7094354751514625ea92d0875aba355f53e86a92600f290e77b0e604 languageName: node linkType: hard @@ -4524,6 +4588,13 @@ __metadata: languageName: node linkType: hard +"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3": + version: 2.1.8-no-fsevents.3 + resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3" + checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344 + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -7937,7 +8008,7 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-in-js@npm:next, @rocket.chat/css-in-js@npm:~0.31.26-dev.19": +"@rocket.chat/css-in-js@npm:next": version: 0.31.26-dev.19 resolution: "@rocket.chat/css-in-js@npm:0.31.26-dev.19" dependencies: @@ -7950,6 +8021,15 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/css-in-js@npm:~0.31.26-dev.19": + version: 0.31.26-dev.23 + resolution: "@rocket.chat/css-in-js@npm:0.31.26-dev.23" + dependencies: + "@rocket.chat/memo": ^0.31.25 + checksum: 6d71bd0f232c8ea3fc2711347064ddd14925b1c2b8713f6d7649b98679455029a53ee41d08b98d010da3ea4789afa21a15901a92efef61dee7b32d6965157445 + languageName: node + linkType: hard + "@rocket.chat/css-supports@npm:^0.31.25": version: 0.31.25 resolution: "@rocket.chat/css-supports@npm:0.31.25" @@ -7959,12 +8039,12 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-supports@npm:~0.31.26-dev.19": - version: 0.31.26-dev.19 - resolution: "@rocket.chat/css-supports@npm:0.31.26-dev.19" +"@rocket.chat/css-supports@npm:~0.31.26-dev.19, @rocket.chat/css-supports@npm:~0.31.26-dev.23": + version: 0.31.26-dev.23 + resolution: "@rocket.chat/css-supports@npm:0.31.26-dev.23" dependencies: - "@rocket.chat/memo": ~0.31.26-dev.19 - checksum: c689ccca04901b128c8993e7475d89ca1e49d01efac9bb9641a0a35bba4237d36da48204cd26b39e92b8d98f24ff85df40e516fd0e421beaaf7c10a8308536ea + "@rocket.chat/memo": ~0.31.26-dev.23 + checksum: a4f25562df67214b1c92c85a1cd16eb03fc2aea385f48cdde42ad0053b9e03a92ca9e3486d1387c7a31cf68f47fa888825f31acae8f4700ee2b9f03495286a12 languageName: node linkType: hard @@ -8365,6 +8445,19 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/jwt@workspace:^, @rocket.chat/jwt@workspace:packages/jwt": + version: 0.0.0-use.local + resolution: "@rocket.chat/jwt@workspace:packages/jwt" + dependencies: + "@types/jest": ~29.5.3 + eslint: ~8.45.0 + jest: ~29.6.1 + jose: ^4.14.4 + ts-jest: ^29.1.1 + typescript: ~5.2.2 + languageName: unknown + linkType: soft + "@rocket.chat/layout@npm:next": version: 0.32.0-dev.312 resolution: "@rocket.chat/layout@npm:0.32.0-dev.312" @@ -8377,6 +8470,34 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/license@workspace:^, @rocket.chat/license@workspace:ee/packages/license": + version: 0.0.0-use.local + resolution: "@rocket.chat/license@workspace:ee/packages/license" + dependencies: + "@babel/cli": ^7.23.0 + "@babel/core": ^7.23.0 + "@babel/preset-env": ^7.22.20 + "@babel/preset-typescript": ^7.23.0 + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/jwt": "workspace:^" + "@rocket.chat/logger": "workspace:^" + "@rocket.chat/server-cloud-communication": "workspace:^" + "@swc/core": ^1.3.66 + "@swc/jest": ^0.2.26 + "@types/babel__core": ^7 + "@types/babel__preset-env": ^7 + "@types/jest": ~29.5.3 + "@types/ws": ^8.5.5 + babel-plugin-transform-inline-environment-variables: ^0.4.4 + eslint: ~8.45.0 + jest: ~29.6.1 + jest-environment-jsdom: ~29.6.1 + jest-websocket-mock: ^2.4.0 + ts-jest: ~29.0.5 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@rocket.chat/livechat@workspace:^, @rocket.chat/livechat@workspace:packages/livechat": version: 0.0.0-use.local resolution: "@rocket.chat/livechat@workspace:packages/livechat" @@ -8520,13 +8641,20 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/memo@npm:next, @rocket.chat/memo@npm:~0.31.26-dev.19": +"@rocket.chat/memo@npm:next": version: 0.31.26-dev.19 resolution: "@rocket.chat/memo@npm:0.31.26-dev.19" checksum: 387c29643c0d725b2e2d3b79eeebf2ed3ac2fa518178d2836913dddf48f2aa72e80b277d54c77ac0498c144324cdfd3449bae883895c316fbb43c7dbbfcb3993 languageName: node linkType: hard +"@rocket.chat/memo@npm:~0.31.26-dev.19, @rocket.chat/memo@npm:~0.31.26-dev.23": + version: 0.31.26-dev.23 + resolution: "@rocket.chat/memo@npm:0.31.26-dev.23" + checksum: 68301161d87ba25347f1d2ab85c139ba86c5fdd1101f41678808c19ba461772814f4bff048a30e4aefd08978fe2feb952c541bddc0beb6bc3cd190bd7852393b + languageName: node + linkType: hard + "@rocket.chat/message-parser@npm:next": version: 0.32.0-dev.377 resolution: "@rocket.chat/message-parser@npm:0.32.0-dev.377" @@ -8587,7 +8715,9 @@ __metadata: "@rocket.chat/i18n": "workspace:^" "@rocket.chat/icons": ^0.32.0 "@rocket.chat/instance-status": "workspace:^" + "@rocket.chat/jwt": "workspace:^" "@rocket.chat/layout": next + "@rocket.chat/license": "workspace:^" "@rocket.chat/livechat": "workspace:^" "@rocket.chat/log-format": "workspace:^" "@rocket.chat/logger": "workspace:^" @@ -8606,6 +8736,7 @@ __metadata: "@rocket.chat/presence": "workspace:^" "@rocket.chat/random": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/server-cloud-communication": "workspace:^" "@rocket.chat/server-fetch": "workspace:^" "@rocket.chat/sha256": "workspace:^" "@rocket.chat/string-helpers": next @@ -8711,7 +8842,7 @@ __metadata: "@xmldom/xmldom": ^0.8.8 adm-zip: 0.5.10 ajv: ^8.11.0 - ajv-formats: ^2.1.1 + ajv-formats: ~2.1.1 apn: 2.2.0 archiver: ^3.1.1 asterisk-manager: ^0.2.0 @@ -8796,6 +8927,7 @@ __metadata: imap: ^0.8.19 ip-range-check: ^0.2.0 is-svg: ^4.3.2 + isolated-vm: 4.4.2 jest: ~29.6.1 jquery: ^3.6.0 jschardet: ^3.0.0 @@ -8885,6 +9017,7 @@ __metadata: stylelint: ^14.9.1 stylelint-order: ^5.0.0 supertest: ^6.2.3 + suretype: ~2.4.1 tar-stream: ^1.6.2 template-file: ^6.0.1 textarea-caret: ^3.1.0 @@ -9261,6 +9394,7 @@ __metadata: "@rocket.chat/apps-engine": 1.41.0-alpha.290 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/license": "workspace:^" "@rocket.chat/message-parser": next "@rocket.chat/ui-kit": ^0.32.1 "@types/jest": ~29.5.3 @@ -9288,6 +9422,19 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/server-cloud-communication@workspace:^, @rocket.chat/server-cloud-communication@workspace:packages/server-cloud-communication": + version: 0.0.0-use.local + resolution: "@rocket.chat/server-cloud-communication@workspace:packages/server-cloud-communication" + dependencies: + "@rocket.chat/license": "workspace:^" + "@types/jest": ~29.5.3 + eslint: ~8.45.0 + jest: ~29.6.1 + ts-jest: ~29.0.5 + typescript: ~5.1.6 + languageName: unknown + linkType: soft + "@rocket.chat/server-fetch@workspace:^, @rocket.chat/server-fetch@workspace:packages/server-fetch": version: 0.0.0-use.local resolution: "@rocket.chat/server-fetch@workspace:packages/server-fetch" @@ -9392,13 +9539,13 @@ __metadata: linkType: hard "@rocket.chat/stylis-logical-props-middleware@npm:~0.31.26-dev.19": - version: 0.31.26-dev.19 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.26-dev.19" + version: 0.31.26-dev.23 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.26-dev.23" dependencies: - "@rocket.chat/css-supports": ~0.31.26-dev.19 + "@rocket.chat/css-supports": ~0.31.26-dev.23 peerDependencies: stylis: 4.0.10 - checksum: 893bd48b6cc320ee7a970cda019e08b00c299c51562cf74f14e925bd4f613fc0c9448de876c3aa6b651bfc060a42097ccdd5a2dee0769a9a05cfe32eaff684f3 + checksum: b2fbfad3b2f4dedd9023b30d4cdc51e76ae76faeeca5819cf697e896c02fd4bb2dde5bbc428b377d77f32011fd8cc82c6d98a84d66b93056ef981c13aee1dc67 languageName: node linkType: hard @@ -14112,7 +14259,7 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:^2.1.1": +"ajv-formats@npm:^2.1.1, ajv-formats@npm:~2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" dependencies: @@ -14158,7 +14305,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -15001,6 +15148,21 @@ __metadata: languageName: node linkType: hard +"awesome-ajv-errors@npm:^1.0.1": + version: 1.0.1 + resolution: "awesome-ajv-errors@npm:1.0.1" + dependencies: + chalk: ^4.1.0 + jsonpointer: ^4.1.0 + jsonpos: ^1.1.0 + leven: ^3.1.0 + terminal-link: ^2.1.1 + peerDependencies: + ajv: ^6 || ^7 + checksum: 1653f6dcebaf4913341e9ad5722aaa772bc1eddd623c11c58434d958c11bddc8f06f470c8ce6f04f269b45e296c4328455151e90cd0bb6892c6f1629753730d8 + languageName: node + linkType: hard + "aws-sdk@npm:^2.1363.0": version: 2.1363.0 resolution: "aws-sdk@npm:2.1363.0" @@ -15270,6 +15432,13 @@ __metadata: languageName: node linkType: hard +"babel-plugin-transform-inline-environment-variables@npm:^0.4.4": + version: 0.4.4 + resolution: "babel-plugin-transform-inline-environment-variables@npm:0.4.4" + checksum: fa361287411301237fd8ce332aff4f8e8ccb8db30e87a2ddc7224c8bf7cd792eda47aca24dc2e09e70bce4c027bc8cbe22f4999056be37a25d2472945df21ef5 + languageName: node + linkType: hard + "babel-polyfill@npm:^6.2.0": version: 6.26.0 resolution: "babel-polyfill@npm:6.26.0" @@ -16794,7 +16963,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -17144,6 +17313,13 @@ __metadata: languageName: node linkType: hard +"code-error-fragment@npm:0.0.230": + version: 0.0.230 + resolution: "code-error-fragment@npm:0.0.230" + checksum: 6c5e800d6d70b30938cc85a2fc2c6069f028eadb58bceb65716b995ce6228c99906302f2c438ba50115fd81a1ee15dd95dc7d317b16a6c590e311ac7e50613f3 + languageName: node + linkType: hard + "code-point-at@npm:^1.0.0": version: 1.1.0 resolution: "code-point-at@npm:1.1.0" @@ -17336,7 +17512,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.0.0, commander@npm:^4.1.1": +"commander@npm:^4.0.0, commander@npm:^4.0.1, commander@npm:^4.1.1": version: 4.1.1 resolution: "commander@npm:4.1.1" checksum: d7b9913ff92cae20cb577a4ac6fcc121bd6223319e54a40f51a14740a681ad5c574fd29a57da478a5f234a6fa6c52cbf0b7c641353e03c648b1ae85ba670b977 @@ -21943,6 +22119,13 @@ __metadata: languageName: node linkType: hard +"fs-readdir-recursive@npm:^1.1.0": + version: 1.1.0 + resolution: "fs-readdir-recursive@npm:1.1.0" + checksum: 29d50f3d2128391c7fc9fd051c8b7ea45bcc8aa84daf31ef52b17218e20bfd2bd34d02382742801954cc8d1905832b68227f6b680a666ce525d8b6b75068ad1e + languageName: node + linkType: hard + "fs-write-stream-atomic@npm:^1.0.8": version: 1.0.10 resolution: "fs-write-stream-atomic@npm:1.0.10" @@ -25272,6 +25455,15 @@ __metadata: languageName: node linkType: hard +"isolated-vm@npm:4.4.2": + version: 4.4.2 + resolution: "isolated-vm@npm:4.4.2" + dependencies: + node-gyp: latest + checksum: 86d12d96f90ceef74a3fc096439c71b0c115235ae3053d600eb8f7c678443d9ca3c8a2805dcd7f97463d11eb7d2e667868946b90e377a3e6d50fdd4085506fbc + languageName: node + linkType: hard + "isomorphic-unfetch@npm:^3.1.0": version: 3.1.0 resolution: "isomorphic-unfetch@npm:3.1.0" @@ -26011,10 +26203,10 @@ __metadata: languageName: node linkType: hard -"jose@npm:^4.11.1": - version: 4.12.0 - resolution: "jose@npm:4.12.0" - checksum: 09e67611768127ab54b6b507401de4b1f87e1e285cf2c2fc917e931e001b7e584c90081b421f483f13a6eec4fc44936e4a5f4b8ae2d59928061e886e35d33fa2 +"jose@npm:^4.11.1, jose@npm:^4.14.4": + version: 4.14.6 + resolution: "jose@npm:4.14.6" + checksum: eae81a234e7bf1446b1bd80722b3462b014e3835b155c3a7799c1c5043163a53a0dc28d347004151b031e6b7b863403aabf8814d9cc217ce21f8c2f3ebd4b335 languageName: node linkType: hard @@ -26280,6 +26472,16 @@ __metadata: languageName: node linkType: hard +"json-to-ast@npm:^2.1.0": + version: 2.1.0 + resolution: "json-to-ast@npm:2.1.0" + dependencies: + code-error-fragment: 0.0.230 + grapheme-splitter: ^1.0.4 + checksum: 1e9b051505b218573b39f3fec9054d75772413aefc2fee3e763d9033276664faa7eec26b945a71f70b9ce29685b2f13259df7dd3243e15eacf4672c62d5ba7ce + languageName: node + linkType: hard + "json5@npm:^0.5.0": version: 0.5.1 resolution: "json5@npm:0.5.1" @@ -26353,6 +26555,13 @@ __metadata: languageName: node linkType: hard +"jsonpointer@npm:^4.1.0": + version: 4.1.0 + resolution: "jsonpointer@npm:4.1.0" + checksum: ffc3e8937380989934676b339718d3213ecf5f6b7ce637b1ce5669a22f45dc61a86463e28abbe8c743d62f87ae790253c50cce0f586cb8e7623a21a7f811a444 + languageName: node + linkType: hard + "jsonpointer@npm:^5.0.0": version: 5.0.0 resolution: "jsonpointer@npm:5.0.0" @@ -26360,6 +26569,15 @@ __metadata: languageName: node linkType: hard +"jsonpos@npm:^1.1.0": + version: 1.1.0 + resolution: "jsonpos@npm:1.1.0" + dependencies: + json-to-ast: ^2.1.0 + checksum: 00a11fff623e74e1b14d10dcda2846e25ccdf0a12ade911fcbc8a75b82f4d33429c22dd57b6f7d2fd8a8eb07bc6435f2c3e412cb7ec09f2c8f63f19381742483 + languageName: node + linkType: hard + "jsonwebtoken@npm:^8.1.0, jsonwebtoken@npm:^8.5.1": version: 8.5.1 resolution: "jsonwebtoken@npm:8.5.1" @@ -27850,6 +28068,13 @@ __metadata: languageName: node linkType: hard +"meta-types@npm:^1.1.0": + version: 1.1.1 + resolution: "meta-types@npm:1.1.1" + checksum: 4dc31cf2eca16529ea8fc317e7d21cf8e88d85a64bc7894c8a00cf7395c1ac2d56d6655767c0a8ec02c80b8916555cf2968fad14629b06c5fefd00cb9b731b40 + languageName: node + linkType: hard + "meteor-blaze-tools@npm:^1.2.0, meteor-blaze-tools@npm:^1.2.4": version: 1.5.0 resolution: "meteor-blaze-tools@npm:1.5.0" @@ -34092,7 +34317,7 @@ __metadata: "@types/chart.js": ^2.9.37 "@types/js-yaml": ^4.0.5 husky: ^7.0.4 - turbo: ^1.10.13 + turbo: ~1.10.14 languageName: unknown linkType: soft @@ -36371,13 +36596,13 @@ __metadata: languageName: node linkType: hard -"supports-hyperlinks@npm:^2.2.0": - version: 2.2.0 - resolution: "supports-hyperlinks@npm:2.2.0" +"supports-hyperlinks@npm:^2.0.0, supports-hyperlinks@npm:^2.2.0": + version: 2.3.0 + resolution: "supports-hyperlinks@npm:2.3.0" dependencies: has-flag: ^4.0.0 supports-color: ^7.0.0 - checksum: aef04fb41f4a67f1bc128f7c3e88a81b6cf2794c800fccf137006efe5bafde281da3e42e72bf9206c2fcf42e6438f37e3a820a389214d0a88613ca1f2d36076a + checksum: 9ee0de3c8ce919d453511b2b1588a8205bd429d98af94a01df87411391010fe22ca463f268c84b2ce2abad019dfff8452aa02806eeb5c905a8d7ad5c4f4c52b8 languageName: node linkType: hard @@ -36388,6 +36613,17 @@ __metadata: languageName: node linkType: hard +"suretype@npm:~2.4.1": + version: 2.4.1 + resolution: "suretype@npm:2.4.1" + dependencies: + ajv: ^6.11.0 + awesome-ajv-errors: ^1.0.1 + meta-types: ^1.1.0 + checksum: f7562a8c1faa68e8daa3969e4488948eb72397eb5f9b4dbaed28c5ce1eb58e0701cca6352166834cde947662b2e30aeefb6f24d31904f773ee9bf65dd98810d1 + languageName: node + linkType: hard + "svg-arc-to-cubic-bezier@npm:^3.0.0, svg-arc-to-cubic-bezier@npm:^3.2.0": version: 3.2.0 resolution: "svg-arc-to-cubic-bezier@npm:3.2.0" @@ -36674,6 +36910,16 @@ __metadata: languageName: node linkType: hard +"terminal-link@npm:^2.1.1": + version: 2.1.1 + resolution: "terminal-link@npm:2.1.1" + dependencies: + ansi-escapes: ^4.2.1 + supports-hyperlinks: ^2.0.0 + checksum: ce3d2cd3a438c4a9453947aa664581519173ea40e77e2534d08c088ee6dda449eabdbe0a76d2a516b8b73c33262fedd10d5270ccf7576ae316e3db170ce6562f + languageName: node + linkType: hard + "terser-webpack-plugin@npm:^1.4.3": version: 1.4.5 resolution: "terser-webpack-plugin@npm:1.4.5" @@ -37231,6 +37477,39 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:^29.1.1": + version: 29.1.1 + resolution: "ts-jest@npm:29.1.1" + dependencies: + bs-logger: 0.x + fast-json-stable-stringify: 2.x + jest-util: ^29.0.0 + json5: ^2.2.3 + lodash.memoize: 4.x + make-error: 1.x + semver: ^7.5.3 + yargs-parser: ^21.0.1 + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402 + languageName: node + linkType: hard + "ts-jest@npm:~29.0.5": version: 29.0.5 resolution: "ts-jest@npm:29.0.5" @@ -37453,58 +37732,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-darwin-64@npm:1.10.13" +"turbo-darwin-64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-darwin-64@npm:1.10.14" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-darwin-arm64@npm:1.10.13" +"turbo-darwin-arm64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-darwin-arm64@npm:1.10.14" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-linux-64@npm:1.10.13" +"turbo-linux-64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-linux-64@npm:1.10.14" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-linux-arm64@npm:1.10.13" +"turbo-linux-arm64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-linux-arm64@npm:1.10.14" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-windows-64@npm:1.10.13" +"turbo-windows-64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-windows-64@npm:1.10.14" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-windows-arm64@npm:1.10.13" +"turbo-windows-arm64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-windows-arm64@npm:1.10.14" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:^1.10.13": - version: 1.10.13 - resolution: "turbo@npm:1.10.13" +"turbo@npm:~1.10.14": + version: 1.10.14 + resolution: "turbo@npm:1.10.14" dependencies: - turbo-darwin-64: 1.10.13 - turbo-darwin-arm64: 1.10.13 - turbo-linux-64: 1.10.13 - turbo-linux-arm64: 1.10.13 - turbo-windows-64: 1.10.13 - turbo-windows-arm64: 1.10.13 + turbo-darwin-64: 1.10.14 + turbo-darwin-arm64: 1.10.14 + turbo-linux-64: 1.10.14 + turbo-linux-arm64: 1.10.14 + turbo-windows-64: 1.10.14 + turbo-windows-arm64: 1.10.14 dependenciesMeta: turbo-darwin-64: optional: true @@ -37520,7 +37799,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 0c000c671534c8c80270c6d1fc77646df0e44164c0db561a85b3fefadd4bda6d5920626d067abb09af38613024e3984fb8d8bc5be922dae6236eda6aab9447a2 + checksum: 219d245bb5cc32a9f76b136b81e86e179228d93a44cab4df3e3d487a55dd2688b5b85f4d585b66568ac53166145352399dd2d7ed0cd47f1aae63d08beb814ebb languageName: node linkType: hard @@ -37707,7 +37986,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.2.2": +"typescript@npm:^5.2.2, typescript@npm:~5.2.2": version: 5.2.2 resolution: "typescript@npm:5.2.2" bin: @@ -37717,7 +37996,17 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@~5.2.2#~builtin": +"typescript@npm:~5.1.6": + version: 5.1.6 + resolution: "typescript@npm:5.1.6" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: b2f2c35096035fe1f5facd1e38922ccb8558996331405eb00a5111cc948b2e733163cc22fab5db46992aba7dd520fff637f2c1df4996ff0e134e77d3249a7350 + languageName: node + linkType: hard + +"typescript@patch:typescript@^5.2.2#~builtin, typescript@patch:typescript@~5.2.2#~builtin": version: 5.2.2 resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f456af" bin: @@ -37727,6 +38016,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~5.1.6#~builtin": + version: 5.1.6 + resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=f456af" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 21e88b0a0c0226f9cb9fd25b9626fb05b4c0f3fddac521844a13e1f30beb8f14e90bd409a9ac43c812c5946d714d6e0dee12d5d02dfc1c562c5aacfa1f49b606 + languageName: node + linkType: hard + "ua-parser-js@npm:^1.0.35": version: 1.0.35 resolution: "ua-parser-js@npm:1.0.35"