...
 
Commits (3)
{
"name": "@rocket.chat/apps-engine",
"version": "1.4.2",
"version": "1.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
{
"name": "@rocket.chat/apps-engine",
"version": "1.4.2",
"version": "1.5.0",
"description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.",
"main": "index",
"typings": "index",
......
......@@ -14,6 +14,10 @@ export enum AppStatus {
* An attempt to enable it again will fail, as it needs to be updated.
*/
COMPILER_ERROR_DISABLED = 'compiler_error_disabled',
/**
* The App was disable due to its license being invalid
*/
INVALID_LICENSE_DISABLED = 'invalid_license_disabled',
/** The App was disabled due to an unrecoverable error being thrown. */
ERROR_DISABLED = 'error_disabled',
/** The App was manually disabled by a user. */
......@@ -40,6 +44,7 @@ export class AppStatusUtilsDef {
case AppStatus.ERROR_DISABLED:
case AppStatus.MANUALLY_DISABLED:
case AppStatus.INVALID_SETTINGS_DISABLED:
case AppStatus.INVALID_LICENSE_DISABLED:
case AppStatus.DISABLED:
return true;
default:
......
......@@ -3,4 +3,10 @@ import { IAppAuthorInfo } from './IAppAuthorInfo';
import { IAppInfo } from './IAppInfo';
import { RocketChatAssociationModel, RocketChatAssociationRecord } from './RocketChatAssociations';
export { AppMethod, IAppAuthorInfo, IAppInfo, RocketChatAssociationModel, RocketChatAssociationRecord };
export {
AppMethod,
IAppAuthorInfo,
IAppInfo,
RocketChatAssociationModel,
RocketChatAssociationRecord,
};
......@@ -4,6 +4,7 @@ import { IGetAppsFilter } from './IGetAppsFilter';
import {
AppAccessorManager,
AppApiManager,
AppLicenseManager,
AppListenerManager,
AppSettingsManager,
AppSlashCommandManager,
......@@ -14,6 +15,8 @@ import { AppLogStorage, AppStorage, IAppStorageItem } from './storage';
import { AppStatus, AppStatusUtils } from '../definition/AppStatus';
import { AppMethod } from '../definition/metadata';
import { InvalidLicenseError } from './errors';
import { IMarketplaceInfo } from './marketplace';
export class AppManager {
public static Instance: AppManager;
......@@ -31,6 +34,7 @@ export class AppManager {
private readonly commandManager: AppSlashCommandManager;
private readonly apiManager: AppApiManager;
private readonly settingsManager: AppSettingsManager;
private readonly licenseManager: AppLicenseManager;
private isLoaded: boolean;
......@@ -67,6 +71,7 @@ export class AppManager {
this.commandManager = new AppSlashCommandManager(this);
this.apiManager = new AppApiManager(this);
this.settingsManager = new AppSettingsManager(this);
this.licenseManager = new AppLicenseManager(this);
this.isLoaded = false;
AppManager.Instance = this;
......@@ -112,6 +117,10 @@ export class AppManager {
return this.commandManager;
}
public getLicenseManager(): AppLicenseManager {
return this.licenseManager;
}
/** Gets the api manager's instance. */
public getApiManager(): AppApiManager {
return this.apiManager;
......@@ -222,7 +231,9 @@ export class AppManager {
for (const rl of this.apps.values()) {
if (AppStatusUtils.isDisabled(rl.getStatus())) {
continue;
} else if (rl.getStatus() === AppStatus.INITIALIZED) {
}
if (rl.getStatus() === AppStatus.INITIALIZED) {
this.listenerManager.unregisterListeners(rl);
this.commandManager.unregisterCommands(rl.getID());
this.apiManager.unregisterApis(rl.getID());
......@@ -361,7 +372,7 @@ export class AppManager {
return true;
}
public async add(zipContentsBase64d: string, enable = true): Promise<AppFabricationFulfillment> {
public async add(zipContentsBase64d: string, enable = true, marketplaceInfo?: IMarketplaceInfo): Promise<AppFabricationFulfillment> {
const aff = new AppFabricationFulfillment();
const result = await this.getParser().parseZip(this.getCompiler(), zipContentsBase64d);
......@@ -382,10 +393,13 @@ export class AppManager {
languageContent: result.languageContent,
settings: {},
implemented: result.implemented.getValues(),
marketplaceInfo,
});
if (!created) {
throw new Error('Failed to create the App, the storage did not return it.');
aff.setStorageError('Failed to create the App, the storage did not return it.');
return aff;
}
// Now that is has all been compiled, let's get the
......@@ -548,6 +562,51 @@ export class AppManager {
return rl;
}
public async updateAppsMarketplaceInfo(appsOverview: Array<{ latest: IMarketplaceInfo }>): Promise<void> {
try {
appsOverview.forEach(({ latest: appInfo }) => {
if (!appInfo.subscriptionInfo) {
return;
}
const app = this.apps.get(appInfo.id);
if (!app) {
return;
}
const appStorageItem = app.getStorageItem();
const subscriptionInfo = appStorageItem.marketplaceInfo && appStorageItem.marketplaceInfo.subscriptionInfo;
if (subscriptionInfo && subscriptionInfo.startDate === appInfo.subscriptionInfo.startDate) {
return;
}
appStorageItem.marketplaceInfo.subscriptionInfo = appInfo.subscriptionInfo;
this.storage.update(appStorageItem).catch(console.error); // TODO: Figure out something better
});
} catch (err) {
// Errors here are not important
}
const queue = [] as Array<Promise<void>>;
this.apps.forEach((app) => queue.push(app.validateLicense().catch((error) => {
if (!(error instanceof InvalidLicenseError)) {
console.error(error);
return;
}
this.commandManager.unregisterCommands(app.getID());
this.apiManager.unregisterApis(app.getID());
return app.setStatus(AppStatus.INVALID_LICENSE_DISABLED);
})));
await Promise.all(queue);
}
/**
* Goes through the entire loading up process. WARNING: Do not use. ;)
*
......@@ -603,20 +662,29 @@ export class AppManager {
const envRead = this.getAccessorManager().getEnvironmentRead(storageItem.id);
try {
await app.validateLicense();
await app.call(AppMethod.INITIALIZE, configExtend, envRead);
result = true;
await app.setStatus(AppStatus.INITIALIZED, silenceStatus);
result = true;
} catch (e) {
let status = AppStatus.ERROR_DISABLED;
if (e.name === 'NotEnoughMethodArgumentsError') {
console.warn('Please report the following error:');
}
if (e instanceof InvalidLicenseError) {
status = AppStatus.INVALID_LICENSE_DISABLED;
}
console.error(e);
this.commandManager.unregisterCommands(storageItem.id);
this.apiManager.unregisterApis(storageItem.id);
result = false;
await app.setStatus(AppStatus.ERROR_DISABLED, silenceStatus);
await app.setStatus(status, silenceStatus);
}
if (saveToDb) {
......@@ -657,19 +725,27 @@ export class AppManager {
let enable: boolean;
try {
await app.validateLicense();
enable = await app.call(AppMethod.ONENABLE,
this.getAccessorManager().getEnvironmentRead(storageItem.id),
this.getAccessorManager().getConfigurationModify(storageItem.id)) as boolean;
await app.setStatus(isManual ? AppStatus.MANUALLY_ENABLED : AppStatus.AUTO_ENABLED, silenceStatus);
} catch (e) {
enable = false;
let status = AppStatus.ERROR_DISABLED;
if (e.name === 'NotEnoughMethodArgumentsError') {
console.warn('Please report the following error:');
}
if (e instanceof InvalidLicenseError) {
status = AppStatus.INVALID_LICENSE_DISABLED;
}
console.error(e);
await app.setStatus(AppStatus.ERROR_DISABLED, silenceStatus);
await app.setStatus(status, silenceStatus);
}
if (enable) {
......
......@@ -8,11 +8,14 @@ import { AppMethod, IAppAuthorInfo, IAppInfo } from '../definition/metadata';
import { AppManager } from './AppManager';
import { NotEnoughMethodArgumentsError } from './errors';
import { AppConsole } from './logging';
import { AppLicenseValidationResult } from './marketplace/license';
import { IAppStorageItem } from './storage';
export class ProxiedApp implements IApp {
private previousStatus: AppStatus;
private latestLicenseValidationResult: AppLicenseValidationResult;
constructor(private readonly manager: AppManager,
private storageItem: IAppStorageItem,
private readonly app: App,
......@@ -143,4 +146,16 @@ export class ProxiedApp implements IApp {
public getAccessors(): IAppAccessors {
return this.app.getAccessors();
}
public getLatestLicenseValidationResult(): AppLicenseValidationResult {
return this.latestLicenseValidationResult;
}
public validateLicense(): Promise<void> {
const { marketplaceInfo } = this.getStorageItem();
this.latestLicenseValidationResult = new AppLicenseValidationResult();
return this.manager.getLicenseManager().validate(this.latestLicenseValidationResult, marketplaceInfo);
}
}
import { ISetting } from '../../definition/settings';
export interface IInternalBridge {
getUsernamesOfRoomById(roomId: string): Array<string>;
getWorkspacePublicKey(): Promise<ISetting>;
}
......@@ -4,4 +4,6 @@ export interface IUserBridge {
getById(id: string, appId: string): Promise<IUser>;
getByUsername(username: string, appId: string): Promise<IUser>;
getActiveUserCount(): Promise<number>;
}
import { IAppInfo } from '../../definition/metadata';
import { AppLicenseValidationResult } from '../marketplace/license';
import { ProxiedApp } from '../ProxiedApp';
import { ICompilerError } from './ICompilerError';
......@@ -8,13 +8,17 @@ export class AppFabricationFulfillment {
public app: ProxiedApp;
public implemented: { [int: string]: boolean };
public compilerErrors: Array<ICompilerError>;
public licenseValidationResult: AppLicenseValidationResult;
public storageError: string;
constructor() {
this.compilerErrors = new Array<ICompilerError>();
this.licenseValidationResult = new AppLicenseValidationResult();
}
public setAppInfo(information: IAppInfo): void {
this.info = information;
this.licenseValidationResult.setAppId(information.id);
}
public getAppInfo(): IAppInfo {
......@@ -44,4 +48,20 @@ export class AppFabricationFulfillment {
public getCompilerErrors(): Array<ICompilerError> {
return this.compilerErrors;
}
public setStorageError(errorMessage: string): void {
this.storageError = errorMessage;
}
public getStorageError(): string {
return this.storageError;
}
public hasStorageError(): boolean {
return !!this.storageError;
}
public getLicenseValidationResult(): AppLicenseValidationResult {
return this.licenseValidationResult;
}
}
import { AppLicenseValidationResult } from '../marketplace/license/AppLicenseValidationResult';
export class InvalidLicenseError extends Error {
public constructor(public readonly validationResult: AppLicenseValidationResult) {
super('Invalid app license');
}
}
import { CommandAlreadyExistsError } from './CommandAlreadyExistsError';
import { CommandHasAlreadyBeenTouchedError } from './CommandHasAlreadyBeenTouchedError';
import { CompilerError } from './CompilerError';
import { InvalidLicenseError } from './InvalidLicenseError';
import { MustContainFunctionError } from './MustContainFunctionError';
import { MustExtendAppError } from './MustExtendAppError';
import { NotEnoughMethodArgumentsError } from './NotEnoughMethodArgumentsError';
......@@ -16,4 +17,5 @@ export {
MustExtendAppError,
NotEnoughMethodArgumentsError,
RequiredApiVersionError,
InvalidLicenseError,
};
import { AppManager } from '../AppManager';
import { IUserBridge } from '../bridges';
import { InvalidLicenseError } from '../errors';
import { IMarketplaceInfo } from '../marketplace';
import { AppLicenseValidationResult } from '../marketplace/license';
import { Crypto } from '../marketplace/license';
enum LicenseVersion {
v1 = 1,
}
export class AppLicenseManager {
private readonly crypto: Crypto;
private readonly userBridge: IUserBridge;
constructor(private readonly manager: AppManager) {
this.crypto = new Crypto(this.manager.getBridges().getInternalBridge());
this.userBridge = this.manager.getBridges().getUserBridge();
}
public async validate(validationResult: AppLicenseValidationResult, appMarketplaceInfo?: IMarketplaceInfo): Promise<void> {
if (!appMarketplaceInfo || !appMarketplaceInfo.subscriptionInfo) {
return;
}
validationResult.setValidated(true);
const { id: appId, subscriptionInfo } = appMarketplaceInfo;
let license;
try {
license = await this.crypto.decryptLicense(subscriptionInfo.license.license) as any;
} catch (err) {
validationResult.addError('publicKey', err.message);
throw new InvalidLicenseError(validationResult);
}
if (license.appId !== appId) {
validationResult.addError('appId', `License hasn't been issued for this app`);
}
switch (license.version) {
case LicenseVersion.v1:
await this.validateV1(appMarketplaceInfo, license, validationResult);
break;
}
}
private async validateV1(appMarketplaceInfo: IMarketplaceInfo, license: any, validationResult: AppLicenseValidationResult): Promise<void> {
const renewal = new Date(license.renewalDate);
const expire = new Date(license.expireDate);
const now = new Date();
if (expire < now) {
validationResult.addError('expire', 'License is no longer valid');
}
const currentActiveUsers = await this.userBridge.getActiveUserCount();
if (license.maxSeats < currentActiveUsers) {
validationResult.addError('maxSeats', 'License does not accomodate the currently active users');
}
if (validationResult.hasErrors) {
throw new InvalidLicenseError(validationResult);
}
if (renewal < now) {
validationResult.addWarning('renewal', 'License has expired and needs to be renewed');
}
if (license.seats < currentActiveUsers) {
validationResult.addWarning(
'seats',
`The license for the app "${
appMarketplaceInfo.name
}" does not have enough seats to accommodate the current amount of active users. Please increase the number of seats`,
);
}
}
}
import { AppAccessorManager} from './AppAccessorManager';
import { AppApiManager } from './AppApiManager';
import { AppLicenseManager } from './AppLicenseManager';
import { AppListenerManager } from './AppListenerManager';
import { AppSettingsManager } from './AppSettingsManager';
import { AppSlashCommandManager } from './AppSlashCommandManager';
export {
AppAccessorManager,
AppLicenseManager,
AppListenerManager,
AppSettingsManager,
AppSlashCommandManager,
......
export interface IAppLicenseMetadata {
license: string;
version: number;
expireDate: Date;
}
import { IAppInfo } from '../../definition/metadata';
import { IMarketplacePricingPlan } from './IMarketplacePricingPlan';
import { IMarketplaceSimpleBundleInfo } from './IMarketplaceSimpleBundleInfo';
import { IMarketplaceSubscriptionInfo } from './IMarketplaceSubscriptionInfo';
import { MarketplacePurchaseType } from './MarketplacePurchaseType';
export interface IMarketplaceInfo extends IAppInfo {
categories: Array<string>;
status: string;
reviewedNote?: string;
rejectionNote?: string;
isVisible: boolean;
isPurchased: boolean;
isSubscribed: boolean;
isBundled: boolean;
createdDate: string;
modifiedDate: string;
price: number;
subscriptionInfo?: IMarketplaceSubscriptionInfo;
puchaseType: MarketplacePurchaseType;
pricingPlans?: Array<IMarketplacePricingPlan>;
bundledIn?: Array<IMarketplaceSimpleBundleInfo>;
}
import { IMarketplacePricingTier } from './IMarketplacePricingTier';
import { MarketplacePricingStrategy } from './MarketplacePricingStrategy';
export interface IMarketplacePricingPlan {
id: string;
enabled: boolean;
price: number;
isPerSeat: boolean;
strategy: MarketplacePricingStrategy;
tiers?: Array<IMarketplacePricingTier>;
}
export interface IMarketplacePricingTier {
perUnit: boolean;
minimum: number;
maximum: number;
price: number;
}
export interface IMarketplaceSimpleBundleInfo {
bundleId: string;
bundleName: string;
}
import { IAppLicenseMetadata } from './IAppLicenseMetadata';
import { MarketplaceSubscriptionStatus } from './MarketplaceSubscriptionStatus';
import { MarketplaceSubscriptionType } from './MarketplaceSubscriptionType';
export interface IMarketplaceSubscriptionInfo {
seats: number;
maxSeats: number;
startDate: string;
periodEnd: string;
isSubscripbedViaBundle: boolean;
endDate?: string;
typeOf: MarketplaceSubscriptionType;
status: MarketplaceSubscriptionStatus;
license: IAppLicenseMetadata;
}
export enum MarketplacePricingStrategy {
PricingStrategyOnce = 'once',
PricingStrategyMonthly = 'monthly',
PricingStrategyYearly = 'yearly',
}
export enum MarketplacePurchaseType {
PurchaseTypeBuy = 'buy',
PurchaseTypeSubscription = 'subscription',
}
export enum MarketplaceSubscriptionStatus {
// PurchaseSubscriptionStatusTrialing is when the subscription is in the trial phase
PurchaseSubscriptionStatusTrialing = 'trialing',
// PurchaseSubscriptionStatusActive is when the subscription is active and being billed for
PurchaseSubscriptionStatusActive = 'active',
// PurchaseSubscriptionStatusCanceled is when the subscription is inactive due to being canceled
PurchaseSubscriptionStatusCanceled = 'canceled',
// PurchaseSubscriptionStatusPastDue is when the subscription was active but is now past due as a result of incorrect billing information
PurchaseSubscriptionStatusPastDue = 'pastDue',
}
export enum MarketplaceSubscriptionType {
SubscriptionTypeApp = 'app',
SubscriptionTypeService = 'service',
}
import { IAppLicenseMetadata } from './IAppLicenseMetadata';
import { IMarketplaceInfo } from './IMarketplaceInfo';
import { IMarketplacePricingPlan } from './IMarketplacePricingPlan';
import { IMarketplacePricingTier } from './IMarketplacePricingTier';
import { IMarketplaceSimpleBundleInfo } from './IMarketplaceSimpleBundleInfo';
import { IMarketplaceSubscriptionInfo } from './IMarketplaceSubscriptionInfo';
export {
IAppLicenseMetadata,
IMarketplaceInfo,
IMarketplacePricingPlan,
IMarketplacePricingTier,
IMarketplaceSimpleBundleInfo,
IMarketplaceSubscriptionInfo,
};
export class AppLicenseValidationResult {
private errors: {[key: string]: string} = {};
private warnings: {[key: string]: string} = {};
private validated: boolean = false;
private appId: string;
public addError(field: string, message: string): void {
this.errors[field] = message;
}
public addWarning(field: string, message: string): void {
this.warnings[field] = message;
}
public get hasErrors(): boolean {
return !!Object.keys(this.errors).length;
}
public get hasWarnings(): boolean {
return !!Object.keys(this.warnings).length;
}
public get hasBeenValidated(): boolean {
return this.validated;
}
public setValidated(validated: boolean): void {
this.validated = validated;
}
public setAppId(appId: string): void {
this.appId = appId;
}
public getAppId(): string {
return this.appId;
}
public getErrors(): object {
return this.errors;
}
public getWarnings(): object {
return this.warnings;
}
public toJSON(): object {
return {
errors: this.errors,
warnings: this.warnings,
};
}
}
import { publicDecrypt } from 'crypto';
import { IInternalBridge } from '../../bridges';
export class Crypto {
constructor(private readonly internalBridge: IInternalBridge) {}
public async decryptLicense(content: string): Promise<object> {
const publicKeySetting = await this.internalBridge.getWorkspacePublicKey();
if (!publicKeySetting || !publicKeySetting.value) {
throw new Error('Public key not available, cannot decrypt'); // TODO: add custom error?
}
const decoded = publicDecrypt(publicKeySetting.value, Buffer.from(content, 'base64'));
let license;
try {
license = JSON.parse(decoded.toString());
} catch (error) {
throw new Error('Invalid license provided');
}
return license;
}
}
import { AppLicenseValidationResult } from './AppLicenseValidationResult';
import { Crypto } from './Crypto';
export {
AppLicenseValidationResult,
Crypto,
};
import { AppStatus } from '../../definition/AppStatus';
import { IAppInfo } from '../../definition/metadata';
import { ISetting } from '../../definition/settings';
import { IMarketplaceInfo } from '../marketplace';
export interface IAppStorageItem {
_id?: string;
......@@ -14,4 +15,5 @@ export interface IAppStorageItem {
languageContent: { [key: string]: object };
settings: { [id: string]: ISetting };
implemented: { [int: string]: boolean };
marketplaceInfo?: IMarketplaceInfo;
}
import { ISetting } from '../../../src/definition/settings';
import { IInternalBridge } from '../../../src/server/bridges';
export class TestsInternalBridge implements IInternalBridge {
public getUsernamesOfRoomById(roomId: string): Array<string> {
throw new Error('Method not implemented.');
}
public getWorkspacePublicKey(): Promise<ISetting> {
throw new Error('Method not implemented.');
}
}
......@@ -10,4 +10,8 @@ export class TestsUserBridge implements IUserBridge {
public getByUsername(username: string, appId: string): Promise<IUser> {
throw new Error('Method not implemented.');
}
public getActiveUserCount(): Promise<number> {
throw new Error('Method not implemented.');
}
}