isolated-function 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,9 @@
6
6
 
7
7
  **Highlights**
8
8
 
9
- - Based on [v8-sandbox](https://github.com/fulcrumapp/v8-sandbox).
9
+ - Based in [Node.js Permission Model](https://nodejs.org/api/permissions.html#permission-model)
10
10
  - Auto install npm dependencies.
11
+ - Memory limit support.
11
12
  - Timeout support.
12
13
 
13
14
  ## Install
@@ -19,10 +20,10 @@ npm install isolated-function --save
19
20
  ## Usage
20
21
 
21
22
  ```js
22
- const isoaltedFunction = require('isolated-function')
23
+ const isolatedFunction = require('isolated-function')
23
24
 
24
25
  /* This function will run in a sandbox, in a separate process */
25
- const sum = isoaltedFunction((y, z) => y + z)
26
+ const sum = isolatedFunction((y, z) => y + z)
26
27
 
27
28
  /* Interact with it as usual from your main code */
28
29
  const result = await sum(3, 2)
@@ -33,7 +34,7 @@ console.log(result)
33
34
  You can also use `require' for external dependencies:
34
35
 
35
36
  ```js
36
- const isEmoji = isoaltedFunction(emoji => {
37
+ const isEmoji = isolatedFunction(emoji => {
37
38
  const isEmoji = require('is-standard-emoji')
38
39
  return isEmoji(emoji)
39
40
  })
@@ -48,7 +49,7 @@ It's intentionally not possible to expose any Node.js objects or functions direc
48
49
 
49
50
  ## API
50
51
 
51
- ### isoaltedFunction(snippet, [options])
52
+ ### isolatedFunction(snippet, [options])
52
53
 
53
54
  #### snippet
54
55
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "isolated-function",
3
3
  "description": "Runs untrusted code in a Node.js v8 sandbox.",
4
4
  "homepage": "https://github.com/Kikobeats/isolated-function",
5
- "version": "0.0.3",
5
+ "version": "0.0.5",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js"
@@ -33,14 +33,13 @@
33
33
  "v8"
34
34
  ],
35
35
  "dependencies": {
36
+ "@kikobeats/time-span": "~1.0.5",
36
37
  "acorn": "~8.12.1",
37
38
  "acorn-walk": "~8.3.3",
38
39
  "ensure-error": "~3.0.1",
39
40
  "esbuild": "~0.23.1",
40
- "process-stats": "~3.7.10",
41
41
  "serialize-error": "8",
42
- "tinyspawn": "~1.3.2",
43
- "v8-sandbox": "~3.2.12"
42
+ "tinyspawn": "~1.3.2"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@commitlint/cli": "latest",
package/src/compile.js CHANGED
@@ -11,6 +11,8 @@ const path = require('path')
11
11
 
12
12
  const generateTemplate = require('./template')
13
13
 
14
+ const MINIFY = process.env.ISOLATED_FUNCTIONS_MINIFY !== 'false'
15
+
14
16
  const packageManager = (() => {
15
17
  try {
16
18
  execSync('which pnpm').toString().trim()
@@ -46,31 +48,35 @@ const detectDependencies = code => {
46
48
  return Array.from(dependencies)
47
49
  }
48
50
 
49
- module.exports = async snippet => {
50
- const tmp = await fs.mkdtemp(path.join(tmpdir(), 'compile-'))
51
- await fs.mkdir(tmp, { recursive: true })
51
+ const getTmp = async content => {
52
+ const cwd = await fs.mkdtemp(path.join(tmpdir(), 'compile-'))
53
+ await fs.mkdir(cwd, { recursive: true })
52
54
 
53
- const template = generateTemplate(snippet)
55
+ const filepath = path.join(cwd, 'index.js')
56
+ await fs.writeFile(filepath, content)
54
57
 
55
- const entryFile = path.join(tmp, 'index.js')
56
- await fs.writeFile(entryFile, template)
58
+ const cleanup = () => fs.rm(cwd, { recursive: true, force: true })
59
+ return { filepath, cwd, content, cleanup }
60
+ }
57
61
 
58
- const dependencies = detectDependencies(template)
59
- await $(packageManager.init, { cwd: tmp })
60
- await $(`${packageManager.install} ${dependencies.join(' ')}`, { cwd: tmp })
62
+ module.exports = async snippet => {
63
+ const tmp = await getTmp(generateTemplate(snippet))
64
+ const dependencies = detectDependencies(tmp.content)
65
+ await $(packageManager.init, { cwd: tmp.cwd })
66
+ await $(`${packageManager.install} ${dependencies.join(' ')}`, {
67
+ cwd: tmp.cwd
68
+ })
61
69
 
62
70
  const result = await esbuild.build({
63
- entryPoints: [entryFile],
71
+ entryPoints: [tmp.filepath],
64
72
  bundle: true,
73
+ minify: MINIFY,
65
74
  write: false,
66
75
  platform: 'node'
67
76
  })
68
77
 
69
- const bundledCode = result.outputFiles[0].text
70
-
71
- await fs.rm(tmp, { recursive: true })
72
-
73
- return bundledCode
78
+ await tmp.cleanup()
79
+ return getTmp(result.outputFiles[0].text)
74
80
  }
75
81
 
76
82
  module.exports.detectDependencies = detectDependencies
package/src/index.js CHANGED
@@ -1,32 +1,78 @@
1
1
  'use strict'
2
2
 
3
- const { Sandbox } = require('v8-sandbox')
4
3
  const { deserializeError } = require('serialize-error')
4
+ const timeSpan = require('@kikobeats/time-span')()
5
+ const $ = require('tinyspawn')
6
+ const path = require('path')
5
7
 
6
8
  const compile = require('./compile')
7
9
 
8
- module.exports = (snippet, { timeout = 10000, globals = {} } = {}) => {
9
- if (typeof snippet !== 'function') {
10
- throw new TypeError('Expected a function')
11
- }
10
+ const createError = ({ name, message, ...props }) => {
11
+ const error = new Error(message)
12
+ error.name = name
13
+ Object.assign(error, props)
14
+ return error
15
+ }
16
+
17
+ const flags = ({ filename, memory }) => {
18
+ const flags = ['--experimental-permission', `--allow-fs-read=${filename}`]
19
+ if (memory) flags.push(`--max-old-space-size=${memory}`)
20
+ return flags.join(' ')
21
+ }
22
+
23
+ module.exports = (snippet, { timeout = 0, memory } = {}) => {
24
+ if (typeof snippet !== 'function') throw new TypeError('Expected a function')
25
+ const compilePromise = compile(snippet)
26
+
27
+ const fn = async (...args) => {
28
+ let duration
29
+ try {
30
+ const { filepath } = await compilePromise
12
31
 
13
- const sandbox = new Sandbox({
14
- httpEnabled: false,
15
- timersEnabled: true,
16
- debug: true
17
- })
18
- const compiling = compile(snippet)
19
- const initializing = sandbox.initialize()
20
-
21
- return async (...args) => {
22
- const [code] = await Promise.all([compiling, initializing])
23
- const { error, value } = await sandbox.execute({
24
- code,
25
- timeout,
26
- globals: { ...globals, arguments: args }
27
- })
28
- await sandbox.shutdown()
29
- if (error) throw require('ensure-error')(deserializeError(error))
30
- return value
32
+ const cwd = path.dirname(filepath)
33
+ const filename = path.basename(filepath)
34
+ const cmd = `node ${flags({ filename, memory })} ${filename} ${JSON.stringify(args)}`
35
+
36
+ duration = timeSpan()
37
+ const { stdout } = await $(cmd, {
38
+ cwd,
39
+ timeout,
40
+ killSignal: 'SIGKILL'
41
+ })
42
+ const { isFulfilled, value, profiling } = JSON.parse(stdout)
43
+ profiling.duration = duration()
44
+ if (isFulfilled) return [value, profiling]
45
+ throw deserializeError(value)
46
+ } catch (error) {
47
+ if (error.signalCode === 'SIGTRAP') {
48
+ throw createError({
49
+ name: 'MemoryError',
50
+ message: 'Out of memory',
51
+ profiling: { duration: duration() }
52
+ })
53
+ }
54
+
55
+ if (error.signalCode === 'SIGKILL') {
56
+ throw createError({
57
+ name: 'TimeoutError',
58
+ message: 'Execution timed out',
59
+ profiling: { duration: duration() }
60
+ })
61
+ }
62
+
63
+ if (error.code === 'ERR_ACCESS_DENIED') {
64
+ throw createError({
65
+ name: 'PermissionError',
66
+ message: `Access to '${error.permission}' has been restricted`,
67
+ profiling: { duration: duration() }
68
+ })
69
+ }
70
+
71
+ throw error
72
+ }
31
73
  }
74
+
75
+ const cleanup = async () => (await compilePromise).cleanup
76
+
77
+ return [fn, cleanup]
32
78
  }
@@ -2,39 +2,26 @@
2
2
 
3
3
  const SERIALIZE_ERROR = require('./serialize-error')
4
4
 
5
- const DISALLOW_INTERNALS = [
6
- '_dispatch',
7
- 'base64ToBuffer',
8
- 'bufferToBase64',
9
- 'define',
10
- 'defineAsync',
11
- 'dispatch',
12
- 'httpRequest',
13
- 'info',
14
- 'setResult'
15
- ]
5
+ const generateTemplate = snippet => `
6
+ const args = JSON.parse(process.argv[2])
16
7
 
17
- const generateTemplate = snippet => {
18
- const value = (() => {
19
- return `await (() => {
20
- let ${DISALLOW_INTERNALS.join(',')};
21
- globalThis = { clearTimeout, setTimeout }
22
- globalThis.global = globalThis
23
- return (${snippet.toString()})(...arguments)
24
- })()`
25
- })()
8
+ ;(async () => {
9
+ let value
10
+ let isFulfilled
26
11
 
27
- const template = `;(async () => {
28
- try {
29
- setResult({
30
- value: ${value}
31
- })
32
- } catch(error) {
33
- setResult({ error: ${SERIALIZE_ERROR}(error) })
34
- }
35
- })()`
36
-
37
- return template
38
- }
12
+ try {
13
+ value = await (${snippet.toString()})(...args)
14
+ isFulfilled = true
15
+ } catch (error) {
16
+ value = ${SERIALIZE_ERROR}(error)
17
+ isFulfilled = false
18
+ } finally {
19
+ console.log(JSON.stringify({
20
+ isFulfilled,
21
+ value,
22
+ profiling: { memory: process.memoryUsage().rss }
23
+ }))
24
+ }
25
+ })()`
39
26
 
40
27
  module.exports = generateTemplate