snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import { AirGappedRestriction, License } from '@rocket.chat/license';
import { Settings, Statistics } from '@rocket.chat/models';
import { i18n } from '../../../../server/lib/i18n';
import { notifyOnSettingChangedById } from '../../../../server/lib/notifyListener';
import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins';
import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates';
const updateRestrictionSetting = async (remainingDays: number) => {
await updateAuditedBySystem({
reason: 'updateRestrictionSetting',
})(Settings.updateValueById, 'Cloud_Workspace_AirGapped_Restrictions_Remaining_Days', remainingDays);
void notifyOnSettingChangedById('Cloud_Workspace_AirGapped_Restrictions_Remaining_Days');
};
const sendRocketCatWarningToAdmins = async (remainingDays: number) => {
const lastDayOrNoRestrictionsAtAll = remainingDays <= 0;
if (lastDayOrNoRestrictionsAtAll) {
return;
}
if (AirGappedRestriction.isWarningPeriod(remainingDays)) {
await sendMessagesToAdmins({
msgs: async ({ adminUser }) => ({
msg: i18n.t('AirGapped_Restriction_Warning', { lng: adminUser.language || 'en', remainingDays }),
}),
});
}
};
AirGappedRestriction.on('remainingDays', async ({ days }: { days: number }) => {
await updateRestrictionSetting(days);
await sendRocketCatWarningToAdmins(days);
});
License.onValidateLicense(async () => {
const token = await Statistics.findLastStatsToken();
void AirGappedRestriction.computeRestriction(token);
});
License.onRemoveLicense(async () => {
const token = await Statistics.findLastStatsToken();
void AirGappedRestriction.computeRestriction(token);
});

View File

@@ -0,0 +1,50 @@
import type { IAppStorageItem } from '@rocket.chat/apps/dist/server/storage/IAppStorageItem';
import { Apps } from '@rocket.chat/core-services';
import type { LicenseModule } from '@rocket.chat/core-typings';
import { License, type LicenseImp } from '@rocket.chat/license';
import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem';
type _canEnableAppDependencies = {
Apps: typeof Apps;
License: LicenseImp;
};
export const _canEnableApp = async ({ Apps, License }: _canEnableAppDependencies, app: IAppStorageItem): Promise<void> => {
if (!(await Apps.isInitialized())) {
throw new Error('apps-engine-not-initialized');
}
// Migrated apps were installed before the validation was implemented
// so they're always allowed to be enabled
if (app.migrated) {
return;
}
if (app.info.addon && !License.hasModule(app.info.addon as LicenseModule)) {
throw new Error('app-addon-not-valid');
}
const source = getInstallationSourceFromAppStorageItem(app);
switch (source) {
case 'private':
if (await License.shouldPreventAction('privateApps')) {
throw new Error('license-prevented');
}
break;
default:
if (await License.shouldPreventAction('marketplaceApps')) {
throw new Error('license-prevented');
}
const marketplaceInfo = app.marketplaceInfo?.[0];
if (marketplaceInfo?.isEnterpriseOnly && !License.hasValidLicense()) {
throw new Error('invalid-license');
}
break;
}
};
export const canEnableApp = async (app: IAppStorageItem): Promise<void> => _canEnableApp({ Apps, License }, app);

View File

@@ -0,0 +1,35 @@
import crypto from 'node:crypto';
import { License } from '@rocket.chat/license';
import { showErrorBox } from '../../../../server/lib/logger/showBox';
export function enforceFipsLicense(): void {
if (!crypto.getFips()) {
return;
}
if (License.hasModule('fips')) {
return;
}
const reason = !License.encryptedLicense
? 'No license was found in this workspace'
: (!License.hasValidLicense() && 'The license applied to this workspace is invalid or expired') ||
"The license applied to this workspace does not include the 'fips' module";
showErrorBox(
'FIPS LICENSE ERROR',
[
`${reason}.`,
'',
'This server is running in FIPS mode, which requires a valid license',
"including the 'fips' module.",
'',
'Provide your license through the ROCKETCHAT_LICENSE environment variable,',
'or contact Rocket.Chat to obtain one: https://www.rocket.chat/sales-contact',
].join('\n'),
);
process.exit(1);
}

View File

@@ -0,0 +1,34 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { Settings, Users } from '@rocket.chat/models';
type WizardSettings = Array<ISetting>;
export const getSeatsRequestLink = async (url: string, params?: Record<string, string>): Promise<string> => {
const workspaceId = await Settings.findOneById('Cloud_Workspace_Id');
const activeUsers = await Users.getActiveLocalUserCount();
const wizardSettings: WizardSettings = await Settings.findSetupWizardSettings().toArray();
const newUrl = new URL(url);
if (workspaceId?.value) {
newUrl.searchParams.append('workspaceId', String(workspaceId.value));
}
if (activeUsers) {
newUrl.searchParams.append('activeUsers', String(activeUsers));
}
wizardSettings
.filter(({ _id, value }) => ['Industry', 'Country', 'Size'].includes(_id) && value)
.forEach((setting) => {
newUrl.searchParams.append(setting._id.toLowerCase(), String(setting.value));
});
if (params) {
Object.entries(params).forEach(([key, value]) => {
newUrl.searchParams.append(key, String(value));
});
}
return newUrl.toString();
};

View File

@@ -0,0 +1,21 @@
import { Apps } from '@rocket.chat/core-services';
import type { LicenseAppSources } from '@rocket.chat/core-typings';
import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem';
export async function getAppCount(source: LicenseAppSources): Promise<number> {
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;
}

View File

@@ -0,0 +1,57 @@
import type { ILicense } from '@rocket.chat/core-services';
import { api, ServiceClassInternal } from '@rocket.chat/core-services';
import type { LicenseModule } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { guestPermissions } from '../authorization/guestPermissions';
import { resetEnterprisePermissions } from '../authorization/resetEnterprisePermissions';
export class LicenseService extends ServiceClassInternal implements ILicense {
protected name = 'license';
constructor() {
super();
License.onValidateLicense((): void => {
if (!License.hasValidLicense()) {
return;
}
void api.broadcast('authorization.guestPermissions', guestPermissions);
void resetEnterprisePermissions();
});
License.onModule((licenseModule) => {
void api.broadcast('license.module', licenseModule);
});
this.onEvent('license.actions', (preventedActions) => License.syncShouldPreventActionResults(preventedActions));
this.onEvent('license.sync', () => License.sync());
}
override async started(): Promise<void> {
if (!License.hasValidLicense()) {
return;
}
void api.broadcast('authorization.guestPermissions', guestPermissions);
await resetEnterprisePermissions();
}
hasModule(feature: LicenseModule): boolean {
return License.hasModule(feature);
}
hasValidLicense(): boolean {
return License.hasValidLicense();
}
getModules(): string[] {
return License.getModules();
}
getGuestPermissions(): string[] {
return guestPermissions;
}
}

View File

@@ -0,0 +1,28 @@
import { settingsRegistry } from '../../../../server/settings';
// The proper name for this group is Premium, but we can't change it because it's already in use and we will break the settings
// TODO: Keep this until next major updates
await settingsRegistry.addGroup('Enterprise', async function () {
await this.section('Enterprise', async function () {
await this.add('Enterprise_License', '', {
type: 'string',
i18nLabel: 'Premium_License',
alert: 'Premium_License_alert',
});
await this.add('Enterprise_License_Data', '', {
type: 'string',
hidden: true,
public: false,
});
await this.add('Enterprise_License_Status', '', {
readonly: true,
type: 'string',
i18nLabel: 'Status',
});
});
await this.add('Cloud_Workspace_AirGapped_Restrictions_Remaining_Days', -1, {
type: 'int',
readonly: true,
public: true,
});
});

View File

@@ -0,0 +1,180 @@
import { api } from '@rocket.chat/core-services';
import type { LicenseLimitKind } from '@rocket.chat/core-typings';
import { applyLicense, applyLicenseOrRemove, applyNewestLicense, License } from '@rocket.chat/license';
import { Subscriptions, Users, Settings, LivechatContacts } from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
import moment from 'moment';
import { getAppCount } from './lib/getAppCount';
import { callbacks } from '../../../../server/lib/callbacks';
import { syncWorkspace } from '../../../../server/lib/cloud/syncWorkspace';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { notifyOnSettingChangedById } from '../../../../server/lib/notifyListener';
import { settings } from '../../../../server/settings';
const applyStartupLicense = async (): Promise<void> => {
// Licenses can't be validated before the workspace URL is known; set it explicitly
// so the startup license is never left pending behind the Site_Url watcher.
await License.setWorkspaceUrl(settings.get<string>('Site_Url'));
await applyNewestLicense(License, settings.get<string>('Enterprise_License') ?? '', process.env.ROCKETCHAT_LICENSE ?? '');
};
export const startLicense = async () => {
settings.watch<string>('Site_Url', (value) => {
if (value) {
void License.setWorkspaceUrl(value);
}
});
License.onValidateLicense(async () => {
(await Settings.updateValueById('Enterprise_License', License.encryptedLicense)).modifiedCount &&
void notifyOnSettingChangedById('Enterprise_License');
(await Settings.updateValueById('Enterprise_License_Status', 'Valid')).modifiedCount &&
void notifyOnSettingChangedById('Enterprise_License_Status');
});
License.onInvalidateLicense(async () => {
(await Settings.updateValueById('Enterprise_License_Status', 'Invalid')).modifiedCount &&
void notifyOnSettingChangedById('Enterprise_License_Status');
});
License.onRemoveLicense(async () => {
(await Settings.updateValueById('Enterprise_License', '')).modifiedCount &&
void notifyOnSettingChangedById('Enterprise_License_Status');
(await Settings.updateValueById('Enterprise_License_Status', 'Invalid')).modifiedCount &&
void notifyOnSettingChangedById('Enterprise_License_Status');
});
/**
* This is a debounced function that will sync the workspace data to the cloud.
* it caches the context, waits for a second and then syncs the data.
*/
const syncByTriggerDebounced = (() => {
let timeout: NodeJS.Timeout | undefined;
const contexts: Set<string> = new Set();
return async (context: string) => {
contexts.add(context);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
timeout = undefined;
void syncByTrigger([...contexts]);
contexts.clear();
}, 1000);
};
})();
const syncByTrigger = async (contexts: string[]) => {
if (!License.encryptedLicense) {
return;
}
const existingData = wrapExceptions(() => JSON.parse(settings.get<string>('Enterprise_License_Data'))).catch(() => ({})) ?? {};
const date = new Date();
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
const period = `${year}-${month}-${day}`;
const [, , signed] = License.encryptedLicense.split('.');
// Check if this sync has already been done. Based on License, behavior.
if ([...contexts.values()].every((context) => existingData.signed === signed && existingData[context] === period)) {
return;
}
const obj = Object.fromEntries(contexts.map((context) => [context, period]));
(
await Settings.updateValueById(
'Enterprise_License_Data',
JSON.stringify({
...(existingData.signed === signed && existingData),
...existingData,
...obj,
signed,
}),
)
).modifiedCount && void notifyOnSettingChangedById('Enterprise_License_Data');
try {
await syncWorkspace();
} catch (error) {
console.error(error);
}
};
License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount());
License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount());
License.setLicenseLimitCounter('roomsPerGuest', async (context) =>
context?.userId ? Subscriptions.countByUserIdExceptType(context.userId, 'd') : 0,
);
License.setLicenseLimitCounter('privateApps', () => getAppCount('private'));
License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace'));
License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatContacts.countContactsOnPeriod(moment.utc().format('YYYY-MM')));
return new Promise<void>((resolve) => {
// When settings are loaded, apply the current license if there is one.
settings.onReady(async () => {
import('./airGappedRestrictions');
await applyStartupLicense();
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<string>('Enterprise_License', (license) => applyLicenseOrRemove(license, true));
callbacks.add('workspaceLicenseRemoved', () => License.remove());
callbacks.add('workspaceLicenseChanged', (updatedLicense) => applyLicense(updatedLicense, true));
License.onInstall(async () => void api.broadcast('license.actions', {} as Record<Partial<LicenseLimitKind>, boolean>));
License.onInvalidate(async () => void api.broadcast('license.actions', {} as Record<Partial<LicenseLimitKind>, boolean>));
License.onBehaviorTriggered('prevent_action', (context) => syncByTriggerDebounced(`prevent_action_${context.limit}`));
License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTriggerDebounced(`start_fair_policy_${context.limit}`));
License.onBehaviorTriggered('disable_modules', async (context) => syncByTriggerDebounced(`disable_modules_${context.limit}`));
License.onChange(() => api.broadcast('license.sync'));
License.onBehaviorToggled('prevent_action', (context) => {
if (!context.limit) {
return;
}
void api.broadcast('license.actions', {
[context.limit]: true,
} as Record<Partial<LicenseLimitKind>, boolean>);
});
License.onBehaviorToggled('allow_action', (context) => {
if (!context.limit) {
return;
}
void api.broadcast('license.actions', {
[context.limit]: false,
} as Record<Partial<LicenseLimitKind>, boolean>);
});
resolve();
});
});
};