11import * as child_process from 'child_process' ;
22import * as fs from 'fs' ;
3+ import os from 'os' ;
34import * as path from 'path' ;
45import { type Readable , EventEmitter } from 'stream' ;
56import { inspect as utilInspect } from 'util' ;
@@ -20,6 +21,7 @@ import { AppConsole, type ILoggerStorageEntry } from '../../logging';
2021import type { AppAccessorManager , AppApiManager } from '../../managers' ;
2122import type { AppLogStorage , IAppStorageItem } from '../../storage' ;
2223import type { IRuntimeController } from '../IRuntimeController' ;
24+ import type { DenoConfigurationFileSchema } from './typings' ;
2325
2426const baseDebug = debugFactory ( 'appsEngine:runtime:deno' ) ;
2527
@@ -58,7 +60,7 @@ const COMMAND_PONG = '_zPONG';
5860
5961export const JSONRPC_METHOD_NOT_FOUND = - 32601 ;
6062
61- export function getRuntimeTimeout ( ) {
63+ function getRuntimeTimeout ( ) {
6264 const defaultTimeout = 30000 ;
6365 const envValue = isFinite ( process . env . APPS_ENGINE_RUNTIME_TIMEOUT as any )
6466 ? Number ( process . env . APPS_ENGINE_RUNTIME_TIMEOUT )
@@ -72,28 +74,27 @@ export function getRuntimeTimeout() {
7274 return envValue ;
7375}
7476
75- export function isValidOrigin ( accessor : string ) : accessor is ( typeof ALLOWED_ACCESSOR_METHODS ) [ number ] {
77+ function isValidOrigin ( accessor : string ) : accessor is ( typeof ALLOWED_ACCESSOR_METHODS ) [ number ] {
7678 return ALLOWED_ACCESSOR_METHODS . includes ( accessor as any ) ;
7779}
7880
79- export function getDenoConfigPath ( ) : string {
80- try {
81- // This path is relative to the compiled version of the Apps-Engine source
82- return require . resolve ( '../../../deno-runtime/deno.jsonc' ) ;
83- } catch {
84- // This path is relative to the original Apps-Engine files - used during tests
85- return require . resolve ( '../../../../deno-runtime/deno.jsonc' ) ;
86- }
81+ function getDenoConfigPath ( ) : string {
82+ return require . resolve ( '../../../../deno-runtime/deno.jsonc' ) ;
83+ }
84+
85+ function getDenoConfig ( ) : DenoConfigurationFileSchema {
86+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return, import/no-dynamic-require
87+ return require ( getDenoConfigPath ( ) ) ;
8788}
8889
8990/**
9091 * Resolves the absolute path to @rocket.chat/apps-engine's src/ directory.
9192 * Uses require.resolve so it works regardless of the runtime environment
9293 * (monorepo dev, Meteor bundle, standalone node_modules).
9394 */
94- export function getAppsEngineSourceDir ( ) : string {
95+ function getAppsEngineDir ( ) : string {
9596 const pkgJsonPath = require . resolve ( '@rocket.chat/apps-engine/package.json' ) ;
96- return path . join ( path . dirname ( pkgJsonPath ) , 'src' ) ;
97+ return path . dirname ( pkgJsonPath ) ;
9798}
9899
99100/**
@@ -104,22 +105,20 @@ export function getAppsEngineSourceDir(): string {
104105 *
105106 * Returns the path to the generated config file.
106107 */
107- export function generateRuntimeDenoConfig ( staticConfigPath : string , tempDir : string ) : string {
108- const staticConfig = JSON . parse ( fs . readFileSync ( staticConfigPath , 'utf8' ) ) ;
109- const appsEngineSrcUrl = `file://${ getAppsEngineSourceDir ( ) } /` ;
108+ function generateEphemeralDenoConfig ( targetPath : string , appsEnginePath : string , staticConfig : DenoConfigurationFileSchema ) : void {
109+ if ( ! targetPath . startsWith ( os . tmpdir ( ) ) ) {
110+ throw new Error ( `Temp directory "${ targetPath } " is not inside the system temp directory` ) ;
111+ }
110112
111113 const runtimeConfig = {
112114 ...staticConfig ,
113115 imports : {
114116 ...staticConfig . imports ,
115- '@rocket.chat/apps-engine/' : appsEngineSrcUrl ,
117+ '@rocket.chat/apps-engine/' : appsEnginePath ,
116118 } ,
117119 } ;
118120
119- const runtimeConfigPath = path . join ( tempDir , 'deno_runtime.jsonc' ) ;
120- fs . writeFileSync ( runtimeConfigPath , JSON . stringify ( runtimeConfig , null , '\t' ) ) ;
121-
122- return runtimeConfigPath ;
121+ fs . writeFileSync ( targetPath , JSON . stringify ( runtimeConfig , null , '\t' ) ) ;
123122}
124123
125124type AbortFunction = ( reason ?: any ) => void ;
@@ -154,10 +153,20 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
154153
155154 private readonly tempFilePath : string ;
156155
156+ private readonly denoBin = 'deno' ;
157+
157158 private readonly denoRuntimePath : string ;
158159
159160 private readonly denoConfigPath : string ;
160161
162+ private readonly denoEphemeralConfigPath : string ;
163+
164+ private readonly denoDir : string ;
165+
166+ private readonly appsEnginePath : string ;
167+
168+ private readonly packagePath : string ;
169+
161170 constructor (
162171 manager : AppManager ,
163172 // We need to keep the appSource around in case the Deno process needs to be restarted
@@ -167,8 +176,13 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
167176 super ( ) ;
168177
169178 this . tempFilePath = manager . getTempFilePath ( ) ;
170- this . denoRuntimePath = path . join ( this . tempFilePath , 'deno-runtime' , 'main.ts' ) ;
171179 this . denoConfigPath = getDenoConfigPath ( ) ;
180+ this . appsEnginePath = getAppsEngineDir ( ) ;
181+ this . denoEphemeralConfigPath = path . join ( this . tempFilePath , 'deno_ephemeral.jsonc' ) ;
182+
183+ this . denoRuntimePath = path . join ( this . tempFilePath , 'deno-runtime' , 'main.ts' ) ;
184+ this . denoDir = process . env . DENO_DIR ?? path . join ( this . denoConfigPath , '..' , '.deno-cache' ) ;
185+ this . packagePath = path . dirname ( this . denoConfigPath ) ;
172186
173187 /**
174188 * Deno 2.x refuses to run scripts inside the node_modules, so we create a symlink to the deno runtime files in the temp directory
@@ -182,6 +196,9 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
182196 }
183197 }
184198
199+ // Generate a runtime config with the resolved absolute path for @rocket .chat/apps-engine/
200+ generateEphemeralDenoConfig ( this . denoEphemeralConfigPath , this . appsEnginePath , getDenoConfig ( ) ) ;
201+
185202 this . debug = baseDebug . extend ( appPackage . info . id ) ;
186203 this . messenger = new ProcessMessenger ( ) ;
187204 this . livenessManager = new LivenessManager ( {
@@ -200,30 +217,15 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
200217
201218 public spawnProcess ( ) : void {
202219 try {
203- const denoExePath = 'deno' ;
204-
205- const denoWrapperPath = this . denoRuntimePath ;
206- // The apps package dir (where deno-runtime/ and .deno-cache/ live)
207- const appsEngineDir = path . dirname ( path . join ( this . denoConfigPath , '..' ) ) ;
208- const DENO_DIR = process . env . DENO_DIR ?? path . join ( appsEngineDir , '.deno-cache' ) ;
209- // When running in production, we're likely inside a node_modules which the Deno
210- // process must be able to read in order to include files that use NPM packages
211- const parentNodeModulesDir = path . dirname ( path . join ( appsEngineDir , '..' ) ) ;
212- // The definition/ source files from @rocket .chat/apps-engine that deno-runtime imports
213- const appsEngineSrcDir = getAppsEngineSourceDir ( ) ;
214-
215- const allowedDirs = [ appsEngineDir , parentNodeModulesDir , appsEngineSrcDir , this . tempFilePath ] ;
216-
217- // Generate a runtime config with the resolved absolute path for @rocket .chat/apps-engine/
218- const runtimeConfigPath = generateRuntimeDenoConfig ( this . denoConfigPath , this . tempFilePath ) ;
220+ const allowedDirs = [ this . appsEnginePath , this . packagePath , this . tempFilePath ] ;
219221
220222 const options = [
221223 'run' ,
222224 '--cached-only' ,
223- `--config=${ runtimeConfigPath } ` ,
225+ `--config=${ this . denoEphemeralConfigPath } ` ,
224226 `--allow-read=${ allowedDirs . join ( ',' ) } ` ,
225227 `--allow-env=${ ALLOWED_ENVIRONMENT_VARIABLES . join ( ',' ) } ` ,
226- denoWrapperPath ,
228+ this . denoRuntimePath ,
227229 '--subprocess' ,
228230 this . appPackage . info . id ,
229231 '--spawnId' ,
@@ -241,12 +243,12 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
241243 // We need to pass the PATH, otherwise the shell won't find the deno executable
242244 // But the runtime itself won't have access to the env var because of the parameters
243245 PATH : process . env . PATH ,
244- DENO_DIR ,
246+ DENO_DIR : this . denoDir ,
245247 } ,
246248 } ;
247249
248250 // SECURITY: We control the command, the arguments and the script that will be executed.
249- this . deno = child_process . spawn ( denoExePath , options , environment ) ;
251+ this . deno = child_process . spawn ( this . denoBin , options , environment ) ;
250252 this . messenger . setReceiver ( this . deno ) ;
251253 this . livenessManager . attach ( this . deno ) ;
252254
0 commit comments