Skip to content

Commit 38d07be

Browse files
committed
refactor(deno-runtime): extract sandbox config from construct.ts
1 parent 7e1b19b commit 38d07be

2 files changed

Lines changed: 57 additions & 7 deletions

File tree

packages/apps/deno-runtime/handlers/app/construct.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,39 @@ import { RequestContext } from '../../lib/requestContext';
99
const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs'];
1010
const 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

62106
export default async function handleConstructApp(request: RequestContext): Promise<boolean> {

packages/apps/deno-runtime/main.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import * as Messenger from './lib/messenger';
77
import { stdoutTransport } from './lib/transports/stdoutTransport';
88
import { decoder } from './lib/codec';
99
import { Logger } from './lib/logger';
10-
1110
import slashcommandHandler from './handlers/slashcommand-handler';
1211
import videoConferenceHandler from './handlers/videoconference-handler';
1312
import apiHandler from './handlers/api-handler';
13+
import { setSandboxGlobals, setSandboxRequire } from './handlers/app/construct';
1414
import handleApp from './handlers/app/handler';
1515
import handleScheduler from './handlers/scheduler-handler';
1616
import registerErrorListeners from './error-handlers';
@@ -144,6 +144,12 @@ function prepareEnvironment() {
144144
// This runtime communicates with the Apps-Engine host through stdout
145145
Messenger.setTransport(stdoutTransport);
146146

147+
// Deno's sandbox `require` is a createRequire shim that resolves compiled
148+
// apps-engine paths; the eval shell additionally needs a `Buffer` and a
149+
// shadowed `Deno` (→ undefined) bound by name.
150+
setSandboxRequire(require);
151+
setSandboxGlobals({ Buffer, Deno: undefined });
152+
147153
registerErrorListeners();
148154

149155
// Process-global side effect; doing it once at startup is cleaner than inside construct

0 commit comments

Comments
 (0)