UNPKG

@rocket.chat/apps-engine

Version:

The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.

127 lines (99 loc) 4.67 kB
import type { IParseAppPackageResult } from '@rocket.chat/apps-engine/server/compiler/IParseAppPackageResult.ts'; import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; import { require } from '../../lib/require.ts'; import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage.ts'; import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; import { Socket } from 'node:net'; const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; const ALLOWED_EXTERNAL_MODULES = ['uuid']; function prepareEnvironment() { // Deno does not behave equally to Node when it comes to piping content to a socket // So we intervene here const originalFinal = Socket.prototype._final; Socket.prototype._final = function _final(cb) { // Deno closes the readable stream in the Socket earlier than Node // The exact reason for that is yet unknown, so we'll need to simply delay the execution // which allows data to be read in a response setTimeout(() => originalFinal.call(this, cb), 1); }; } // As the apps are bundled, the only times they will call require are // 1. To require native modules // 2. To require external npm packages we may provide // 3. To require apps-engine files function buildRequire(): (module: string) => unknown { return (module: string): unknown => { if (ALLOWED_NATIVE_MODULES.includes(module)) { return require(`node:${module}`); } if (ALLOWED_EXTERNAL_MODULES.includes(module)) { return require(`npm:${module}`); } if (module.startsWith('@rocket.chat/apps-engine')) { // Our `require` function knows how to handle these return require(module); } throw new Error(`Module ${module} is not allowed`); }; } function wrapAppCode(code: string): (require: (module: string) => unknown) => Promise<Record<string, unknown>> { return new Function( 'require', ` const { Buffer } = require('buffer'); const exports = {}; const module = { exports }; const _error = console.error.bind(console); const _console = { log: _error, error: _error, debug: _error, info: _error, warn: _error, }; const result = (async (exports,module,require,Buffer,console,globalThis,Deno) => { ${code}; })(exports,module,require,Buffer,_console,undefined,undefined); return result.then(() => module.exports);`, ) as (require: (module: string) => unknown) => Promise<Record<string, unknown>>; } export default async function handleConstructApp(params: unknown): Promise<boolean> { if (!Array.isArray(params)) { throw new Error('Invalid params', { cause: 'invalid_param_type' }); } const [appPackage] = params as [IParseAppPackageResult]; if (!appPackage?.info?.id || !appPackage?.info?.classFile || !appPackage?.files) { throw new Error('Invalid params', { cause: 'invalid_param_type' }); } prepareEnvironment(); AppObjectRegistry.set('id', appPackage.info.id); const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]); const require = buildRequire(); const exports = await wrapAppCode(source)(require); // This is the same naive logic we've been using in the App Compiler // Applying the correct type here is quite difficult because of the dynamic nature of the code // deno-lint-ignore no-explicit-any const appClass = Object.values(exports)[0] as any; const logger = AppObjectRegistry.get('logger'); const app = new appClass(appPackage.info, logger, AppAccessorsInstance.getDefaultAppAccessors()); if (typeof app.getName !== 'function') { throw new Error('App must contain a getName function'); } if (typeof app.getNameSlug !== 'function') { throw new Error('App must contain a getNameSlug function'); } if (typeof app.getVersion !== 'function') { throw new Error('App must contain a getVersion function'); } if (typeof app.getID !== 'function') { throw new Error('App must contain a getID function'); } if (typeof app.getDescription !== 'function') { throw new Error('App must contain a getDescription function'); } if (typeof app.getRequiredApiVersion !== 'function') { throw new Error('App must contain a getRequiredApiVersion function'); } AppObjectRegistry.set('app', app); return true; }