@@ -9,6 +9,39 @@ import { RequestContext } from '../../lib/requestContext';
99const ALLOWED_NATIVE_MODULES = [ 'path' , 'url' , 'crypto' , 'buffer' , 'stream' , 'net' , 'http' , 'https' , 'zlib' , 'util' , 'punycode' , 'os' , 'querystring' , 'fs' ] ;
1010const ALLOWED_EXTERNAL_MODULES = [ 'uuid' ] ;
1111
12+ /**
13+ * A platform-dependent `require` used to resolve the modules an app is allowed
14+ * to load (native `node:` modules, a small allow-list of npm packages, and
15+ * apps-engine files). Each runtime injects its own via {@link setSandboxRequire}
16+ * — Node hands over its global `require`, Deno hands over its `createRequire`
17+ * shim that knows how to resolve compiled apps-engine paths.
18+ */
19+ type SandboxRequire = ( module : string ) => unknown ;
20+
21+ function defaultSandboxRequire ( ) : never {
22+ throw new Error ( 'No sandbox require has been injected; the runtime adapter must call setSandboxRequire() during bootstrap' ) ;
23+ }
24+
25+ let sandboxRequire : SandboxRequire = defaultSandboxRequire ;
26+
27+ export function setSandboxRequire ( newRequire : SandboxRequire ) : void {
28+ sandboxRequire = newRequire ;
29+ }
30+
31+ /**
32+ * Extra globals bound into the app's eval shell on top of the common ones
33+ * (`exports`, `module`, `require`, `console`, `globalThis`). Node needs none;
34+ * Deno injects a `Buffer` and shadows `Deno` with `undefined`. Injecting them
35+ * as data keeps the eval-shell skeleton single-source.
36+ */
37+ type SandboxGlobals = Record < string , unknown > ;
38+
39+ let sandboxGlobals : SandboxGlobals = { } ;
40+
41+ export function setSandboxGlobals ( globals : SandboxGlobals ) : void {
42+ sandboxGlobals = globals ;
43+ }
44+
1245// As the apps are bundled, the only times they will call require are
1346// 1. To require native modules
1447// 2. To require external npm packages we may provide
@@ -35,11 +68,20 @@ function buildRequire(): (module: string) => unknown {
3568 } ;
3669}
3770
38- function wrapAppCode ( code : string ) : ( require : ( module : string ) => unknown ) => Promise < Record < string , unknown > > {
39- return new Function (
71+ function wrapAppCode ( code : string ) : ( require : SandboxRequire ) => unknown ) => Promise < Record < string , unknown > > {
72+ const globals = sandboxGlobals ;
73+ // The common globals are bound by name; any platform-specific extras are
74+ // spread in by name from the injected `sandboxGlobals`, so the shell
75+ // skeleton stays identical across runtimes.
76+ const extraNames = Object . keys ( globals ) ;
77+ const extraParams = extraNames . length ? `,${ extraNames . join ( ',' ) } ` : '' ;
78+ const extraArgs = extraNames . map ( ( name ) => `,__globals[${ JSON . stringify ( name ) } ]` ) . join ( '' ) ;
79+
80+ // eslint-disable-next-line @typescript-eslint/no-implied-eval -- This is the reason we run in a separate process
81+ const fn = new Function (
4082 'require' ,
83+ '__globals' ,
4184 `
42- const { Buffer } = require('buffer');
4385 const exports = {};
4486 const module = { exports };
4587 const _error = console.error.bind(console);
@@ -51,12 +93,14 @@ function wrapAppCode(code: string): (require: (module: string) => unknown) => Pr
5193 warn: _error,
5294 };
5395
54- const result = (async (exports,module,require,Buffer, console,globalThis,Deno ) => {
96+ const result = (async (exports,module,require,console,globalThis${ extraParams } ) => {
5597 ${ code } ;
56- })(exports,module,require,Buffer, _console,undefined,undefined );
98+ })(exports,module,require,_console,undefined${ extraArgs } );
5799
58100 return result.then(() => module.exports);` ,
59- ) as ( require : ( module : string ) => unknown ) => Promise < Record < string , unknown > > ;
101+ ) as ( require : SandboxRequire , globals : SandboxGlobals ) => Promise < Record < string , unknown > > ;
102+
103+ return ( require : SandboxRequire ) => fn ( require , globals ) ;
60104}
61105
62106export default async function handleConstructApp ( request : RequestContext ) : Promise < boolean > {
0 commit comments