From 6a2ebe94d4c3158022ff310e8fe6530953ada8a9 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 16 Jul 2026 08:40:51 -0600 Subject: [PATCH] fix: review comments --- apps/meteor/ee/server/lib/license/startup.ts | 32 +++++-------------- .../lib/cloud/getWorkspaceAccessToken.ts | 4 +-- .../cloud/getWorkspaceAccessTokenWithScope.ts | 4 +-- .../meteor/server/lib/cloud/offlineLicense.ts | 16 ++-------- .../cloud/registerPreIntentWorkspaceWizard.ts | 6 ++-- .../server/lib/cloud/syncWorkspace/index.ts | 5 +-- apps/meteor/server/lib/cloud/userLogout.ts | 4 +-- .../lib/users/getAvatarSuggestionForUser.ts | 4 +-- .../server/lib/users/saveUser/saveNewUser.ts | 4 +-- apps/meteor/server/main.ts | 3 -- .../registerPreIntentWorkspaceWizard.spec.ts | 2 +- .../unit/server/lib/cloud/userLogout.spec.ts | 2 +- 12 files changed, 28 insertions(+), 58 deletions(-) diff --git a/apps/meteor/ee/server/lib/license/startup.ts b/apps/meteor/ee/server/lib/license/startup.ts index 9fe9f0cdf0ace..ec761a4805f78 100644 --- a/apps/meteor/ee/server/lib/license/startup.ts +++ b/apps/meteor/ee/server/lib/license/startup.ts @@ -12,24 +12,6 @@ import { callbacks } from '../../../../server/lib/callbacks'; import { syncWorkspace } from '../../../../server/lib/cloud/syncWorkspace'; import { SystemLogger } from '../../../../server/lib/logger/system'; -const logOfflineLicense = (() => { - let logged = false; - return () => { - if (!License.hasOfflineLicense()) { - logged = false; - return; - } - - if (!logged) { - // startup level so it is visible at the default Log_Level, like 'License installed' - SystemLogger.startup( - 'Offline license detected: outbound connections to Rocket.Chat Cloud services and the Rocket.Chat Push Gateway are disabled', - ); - logged = true; - } - }; -})(); - export const startLicense = async () => { settings.watch('Site_Url', (value) => { if (value) { @@ -144,12 +126,14 @@ export const startLicense = async () => { } } - logOfflineLicense(); - License.onValidateLicense(logOfflineLicense); - // Also run on invalidate/remove so the once-per-activation flag resets and a - // subsequently re-applied offline license is logged again. - License.onInvalidateLicense(logOfflineLicense); - License.onRemoveLicense(logOfflineLicense); + License.onInstall(() => { + if (License.hasOfflineLicense()) { + // startup level so it is visible at the default Log_Level, like 'License installed' + SystemLogger.startup( + 'Offline license detected: outbound connections to Rocket.Chat Cloud services and the Rocket.Chat Push Gateway are disabled', + ); + } + }); // After the current license is already loaded, watch the setting value to react to new licenses being applied. settings.change('Enterprise_License', (license) => applyLicenseOrRemove(license, true)); diff --git a/apps/meteor/server/lib/cloud/getWorkspaceAccessToken.ts b/apps/meteor/server/lib/cloud/getWorkspaceAccessToken.ts index 3ccffe7cb62af..e49913bd56f77 100644 --- a/apps/meteor/server/lib/cloud/getWorkspaceAccessToken.ts +++ b/apps/meteor/server/lib/cloud/getWorkspaceAccessToken.ts @@ -1,9 +1,9 @@ import type { IWorkspaceCredentials } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { WorkspaceCredentials } from '@rocket.chat/models'; import { getWorkspaceAccessTokenWithScope } from './getWorkspaceAccessTokenWithScope'; import { workspaceScopes } from './oauthScopes'; -import { hasOfflineLicense } from './offlineLicense'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { SystemLogger } from '../logger/system'; @@ -27,7 +27,7 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save return ''; } - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { return ''; } diff --git a/apps/meteor/server/lib/cloud/getWorkspaceAccessTokenWithScope.ts b/apps/meteor/server/lib/cloud/getWorkspaceAccessTokenWithScope.ts index 8b2579ff6d0ad..f49a738befc23 100644 --- a/apps/meteor/server/lib/cloud/getWorkspaceAccessTokenWithScope.ts +++ b/apps/meteor/server/lib/cloud/getWorkspaceAccessTokenWithScope.ts @@ -1,9 +1,9 @@ +import { License } from '@rocket.chat/license'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { getRedirectUri } from './getRedirectUri'; import { CloudWorkspaceAccessTokenError } from './getWorkspaceAccessToken'; import { workspaceScopes } from './oauthScopes'; -import { hasOfflineLicense } from './offlineLicense'; import { removeWorkspaceRegistrationInfo } from './removeWorkspaceRegistrationInfo'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { settings } from '../../../app/settings/server'; @@ -32,7 +32,7 @@ export async function getWorkspaceAccessTokenWithScope({ return tokenResponse; } - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { return tokenResponse; } diff --git a/apps/meteor/server/lib/cloud/offlineLicense.ts b/apps/meteor/server/lib/cloud/offlineLicense.ts index 2abbbad561566..7a3b5f52b01a4 100644 --- a/apps/meteor/server/lib/cloud/offlineLicense.ts +++ b/apps/meteor/server/lib/cloud/offlineLicense.ts @@ -2,24 +2,12 @@ import { License } from '@rocket.chat/license'; import { CloudOfflineLicenseError } from '../../../lib/errors/CloudOfflineLicenseError'; -/** - * Whether the applied license was issued for offline (air-gapped) workspaces. - * - * When this returns true the workspace must never initiate outbound connections - * to Rocket.Chat Cloud services (registration, sync, marketplace, telemetry) or - * to the Rocket.Chat Push Gateway. Calls must be suppressed at the source — not - * by relying on the requests failing. - */ -export function hasOfflineLicense(): boolean { - return License.hasOfflineLicense(); -} - /** * Guard for interactive cloud flows (registration, OAuth, billing). Background - * jobs should instead skip silently via {@link hasOfflineLicense}. + * jobs should instead skip silently by checking {@link License.hasOfflineLicense}. */ export function assertNotOfflineLicense(): void { - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { throw new CloudOfflineLicenseError('Cloud connectivity is disabled by the offline license applied to this workspace'); } } diff --git a/apps/meteor/server/lib/cloud/registerPreIntentWorkspaceWizard.ts b/apps/meteor/server/lib/cloud/registerPreIntentWorkspaceWizard.ts index 795bb605685c0..285a6fe755439 100644 --- a/apps/meteor/server/lib/cloud/registerPreIntentWorkspaceWizard.ts +++ b/apps/meteor/server/lib/cloud/registerPreIntentWorkspaceWizard.ts @@ -1,14 +1,14 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; -import { hasOfflineLicense } from './offlineLicense'; import { settings } from '../../../app/settings/server'; import { SystemLogger } from '../logger/system'; export async function registerPreIntentWorkspaceWizard(): Promise { - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { return false; } @@ -23,7 +23,7 @@ export async function registerPreIntentWorkspaceWizard(): Promise { // Re-validated at dispatch time: an offline license applied while the // registration data was being built must still suppress the request. - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { return false; } diff --git a/apps/meteor/server/lib/cloud/syncWorkspace/index.ts b/apps/meteor/server/lib/cloud/syncWorkspace/index.ts index 8d837843b06da..aa76435e2b18e 100644 --- a/apps/meteor/server/lib/cloud/syncWorkspace/index.ts +++ b/apps/meteor/server/lib/cloud/syncWorkspace/index.ts @@ -1,7 +1,8 @@ +import { License } from '@rocket.chat/license'; + import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError'; import { SystemLogger } from '../../logger/system'; import { CloudWorkspaceAccessTokenEmptyError, CloudWorkspaceAccessTokenError, isAbortError } from '../getWorkspaceAccessToken'; -import { hasOfflineLicense } from '../offlineLicense'; import { announcementSync } from './announcementSync'; import { legacySyncWorkspace } from './legacySyncWorkspace'; import { syncCloudData } from './syncCloudData'; @@ -13,7 +14,7 @@ import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/suppo * @throws {Error} - If there is an unexpected error during sync like a network error */ export async function syncWorkspace() { - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { SystemLogger.debug({ msg: 'Skipping cloud sync: workspace has an offline license', function: 'syncWorkspace' }); await getCachedSupportedVersionsToken.reset(); return; diff --git a/apps/meteor/server/lib/cloud/userLogout.ts b/apps/meteor/server/lib/cloud/userLogout.ts index 3bde8c90ac2da..e813c841d07b1 100644 --- a/apps/meteor/server/lib/cloud/userLogout.ts +++ b/apps/meteor/server/lib/cloud/userLogout.ts @@ -1,7 +1,7 @@ +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { hasOfflineLicense } from './offlineLicense'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { userLoggedOut } from './userLoggedOut'; import { settings } from '../../../app/settings/server'; @@ -20,7 +20,7 @@ export async function userLogout(userId: string): Promise { // Offline (air-gapped) licenses forbid the outbound token-revocation call, but // the local cloud credentials must still be destroyed on logout. - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { return userLoggedOut(userId); } diff --git a/apps/meteor/server/lib/users/getAvatarSuggestionForUser.ts b/apps/meteor/server/lib/users/getAvatarSuggestionForUser.ts index fd93683d90ce1..9e72ebf3fe489 100644 --- a/apps/meteor/server/lib/users/getAvatarSuggestionForUser.ts +++ b/apps/meteor/server/lib/users/getAvatarSuggestionForUser.ts @@ -1,11 +1,11 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import Gravatar from 'gravatar'; import { check } from 'meteor/check'; import { ServiceConfiguration } from 'meteor/service-configuration'; import { settings } from '../../../app/settings/server'; -import { hasOfflineLicense } from '../cloud/offlineLicense'; const avatarProviders = { facebook(user: IUser) { @@ -107,7 +107,7 @@ const avatarProviders = { // Offline (air-gapped) licenses suppress Gravatar lookups: every suggested // URL is fetched server-side below, and gravatar.com is not admin-configured // infrastructure (unlike OAuth provider avatars, which keep working). - if (hasOfflineLicense()) { + if (License.hasOfflineLicense()) { return avatars; } diff --git a/apps/meteor/server/lib/users/saveUser/saveNewUser.ts b/apps/meteor/server/lib/users/saveUser/saveNewUser.ts index 98e84afdbb560..9796dc75c7ba2 100644 --- a/apps/meteor/server/lib/users/saveUser/saveNewUser.ts +++ b/apps/meteor/server/lib/users/saveUser/saveNewUser.ts @@ -1,11 +1,11 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import Gravatar from 'gravatar'; import { Accounts } from 'meteor/accounts-base'; import { notifyOnUserChangeById } from '../../../../app/lib/server/lib/notifyListener'; import { validateEmailDomain } from '../../../../app/lib/server/lib/validateEmailDomain'; -import { hasOfflineLicense } from '../../cloud/offlineLicense'; import { setUserAvatar } from '../setUserAvatar'; import { handleBio } from './handleBio'; import { handleNickname } from './handleNickname'; @@ -73,7 +73,7 @@ export const saveNewUser = async function (userData: SaveUserData, sendPassword: // Offline (air-gapped) licenses suppress the default Gravatar fetch — a // default-on outbound call the workspace must never initiate on its own. - if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email && !hasOfflineLicense()) { + if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email && !License.hasOfflineLicense()) { const gravatarUrl = Gravatar.url(userData.email, { default: '404', size: '200', diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 89b1ee27d535a..4c725f62aa990 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -30,9 +30,6 @@ await Promise.all([configureServer(settings), registerServices(), startup()]); await startRocketChat(); -// Cron jobs start only after startRocketChat() has applied the license, so jobs -// that contact Rocket.Chat Cloud on boot (e.g. the usage report) respect the -// offline license flag from their very first run. setImmediate(() => { startCronJobs().catch((err) => { SystemLogger.error({ msg: 'Failed to start cron jobs', err }); diff --git a/apps/meteor/tests/unit/server/lib/cloud/registerPreIntentWorkspaceWizard.spec.ts b/apps/meteor/tests/unit/server/lib/cloud/registerPreIntentWorkspaceWizard.spec.ts index 90c3110dc69fb..9bf8f3f9fcc9c 100644 --- a/apps/meteor/tests/unit/server/lib/cloud/registerPreIntentWorkspaceWizard.spec.ts +++ b/apps/meteor/tests/unit/server/lib/cloud/registerPreIntentWorkspaceWizard.spec.ts @@ -11,10 +11,10 @@ const buildRegistrationDataStub = sinon.stub(); const { registerPreIntentWorkspaceWizard } = proxyquire .noCallThru() .load('../../../../../server/lib/cloud/registerPreIntentWorkspaceWizard.ts', { + '@rocket.chat/license': { License: { hasOfflineLicense: hasOfflineLicenseStub } }, '@rocket.chat/models': { Users: { getOldest: getOldestStub } }, '@rocket.chat/server-fetch': { serverFetch: fetchStub }, './buildRegistrationData': { buildWorkspaceRegistrationData: buildRegistrationDataStub }, - './offlineLicense': { hasOfflineLicense: hasOfflineLicenseStub }, '../../../app/settings/server': { settings: { get: sinon.stub().returns('https://cloud.rocket.chat') } }, '../logger/system': { SystemLogger: { error: sinon.stub() } }, }); diff --git a/apps/meteor/tests/unit/server/lib/cloud/userLogout.spec.ts b/apps/meteor/tests/unit/server/lib/cloud/userLogout.spec.ts index 980f7927e77d2..27e97256c1efd 100644 --- a/apps/meteor/tests/unit/server/lib/cloud/userLogout.spec.ts +++ b/apps/meteor/tests/unit/server/lib/cloud/userLogout.spec.ts @@ -11,9 +11,9 @@ const findOneByIdStub = sinon.stub(); const settingsGetStub = sinon.stub(); const { userLogout } = proxyquire.noCallThru().load('../../../../../server/lib/cloud/userLogout.ts', { + '@rocket.chat/license': { License: { hasOfflineLicense: hasOfflineLicenseStub } }, '@rocket.chat/models': { Users: { findOneById: findOneByIdStub } }, '@rocket.chat/server-fetch': { serverFetch: fetchStub }, - './offlineLicense': { hasOfflineLicense: hasOfflineLicenseStub }, './retrieveRegistrationStatus': { retrieveRegistrationStatus: retrieveRegistrationStatusStub }, './userLoggedOut': { userLoggedOut: userLoggedOutStub }, '../../../app/settings/server': { settings: { get: settingsGetStub } },