isolated-function 0.1.36 → 0.1.38

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
@@ -19,6 +19,7 @@
19
19
  - [Install](#install)
20
20
  - [Quickstart](#quickstart)
21
21
  - [Minimal privilege execution](#minimal-privilege-execution)
22
+ - [Granting specific permissions](#granting-specific-permissions)
22
23
  - [Auto install dependencies](#auto-install-dependencies)
23
24
  - [Execution profiling](#execution-profiling)
24
25
  - [Resource limits](#resource-limits)
@@ -32,6 +33,7 @@
32
33
  - [throwError](#throwerror)
33
34
  - [timeout](#timeout)
34
35
  - [tmpdir](#tmpdir)
36
+ - [allow](#allow)
35
37
  - [=\> (fn(\[...args\]), teardown())](#-fnargs-teardown)
36
38
  - [fn](#fn)
37
39
  - [teardown](#teardown)
@@ -89,6 +91,28 @@ If you exceed your limit, an error will occur. Any of the following interaction
89
91
  - File system access
90
92
  - WASI
91
93
 
94
+ ### Granting specific permissions
95
+
96
+ You can grant specific permissions to the isolated function using the `allow` option:
97
+
98
+ ```js
99
+ const [fn, teardown] = isolatedFunction(
100
+ () => {
101
+ const { execSync } = require('child_process')
102
+ return execSync('echo hello').toString().trim()
103
+ },
104
+ {
105
+ allow: ['child-process']
106
+ }
107
+ )
108
+
109
+ const { value } = await fn()
110
+ console.log(value) // 'hello'
111
+ await teardown()
112
+ ```
113
+
114
+ See [#allow](#allow) to know more.
115
+
92
116
  ### Auto install dependencies
93
117
 
94
118
  The hosted code is parsed for detecting `require`/`import` calls and install these dependencies:
@@ -279,6 +303,38 @@ const tmpdir = async () => {
279
303
  }
280
304
  ```
281
305
 
306
+ ##### allow
307
+
308
+ Type: `string[]`<br>
309
+ Default: `[]`
310
+
311
+ An array of permissions to grant to the isolated function based on [Node.js Options](https://nodejs.org/api/cli.html#options)
312
+
313
+ When empty, the function runs with minimal privileges and will throw an error if it attempts to access restricted resources. Available permissions are:
314
+
315
+ - `addons`
316
+ - `child-process`
317
+ - `fs-read`
318
+ - `fs-write`
319
+ - `inspector`
320
+ - `net`
321
+ - `wasi`
322
+ - `worker`
323
+
324
+ Example:
325
+
326
+ ```js
327
+ const [fn, cleanup] = isolatedFunction(
328
+ async () => {
329
+ const http = require('node:http')
330
+ // Network request code here
331
+ },
332
+ {
333
+ allow: ['net']
334
+ }
335
+ )
336
+ ```
337
+
282
338
  ### => (fn([...args]), teardown())
283
339
 
284
340
  #### fn
package/package.json CHANGED
@@ -2,7 +2,8 @@
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.1.36",
5
+ "version": "0.1.38",
6
+ "types": "src/index.d.ts",
6
7
  "main": "src/index.js",
7
8
  "exports": {
8
9
  ".": "./src/index.js"
@@ -55,7 +56,8 @@
55
56
  "nano-staged": "latest",
56
57
  "simple-git-hooks": "latest",
57
58
  "standard": "latest",
58
- "standard-version": "latest"
59
+ "standard-version": "latest",
60
+ "tsd": "latest"
59
61
  },
60
62
  "engines": {
61
63
  "node": ">= 20"
@@ -67,7 +69,7 @@
67
69
  "clean": "rm -rf node_modules",
68
70
  "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
69
71
  "coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
70
- "lint": "standard",
72
+ "lint": "standard && tsd",
71
73
  "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
72
74
  "pretest": "npm run lint",
73
75
  "release": "standard-version -a",
@@ -101,5 +103,8 @@
101
103
  "simple-git-hooks": {
102
104
  "commit-msg": "npx commitlint --edit",
103
105
  "pre-commit": "npx nano-staged"
106
+ },
107
+ "tsd": {
108
+ "directory": "test"
104
109
  }
105
110
  }
@@ -3,7 +3,7 @@
3
3
  const esbuild = require('esbuild')
4
4
 
5
5
  const MINIFY = (() => {
6
- return process.env.ISOLATED_FUNCTIONS_MINIFY !== 'false'
6
+ return process.env.ISOLATED_FUNCTIONS_MINIFY === 'false'
7
7
  ? {}
8
8
  : {
9
9
  minifyWhitespace: true,
@@ -4,35 +4,37 @@ const fs = require('fs/promises')
4
4
  const path = require('path')
5
5
 
6
6
  const transformDependencies = require('./transform-dependencies')
7
- const detectDependencies = require('./detect-dependencies')
8
7
  const installDependencies = require('./install-dependencies')
9
- const generateTemplate = require('../template')
10
- const { duration } = require('../debug')
8
+ const detectDependencies = require('./detect-dependencies')
9
+ const { debug, duration } = require('../debug')
10
+ const template = require('../template')
11
11
  const build = require('./build')
12
12
 
13
13
  const tmpdirDefault = async () => {
14
+ const duration = debug.duration()
14
15
  const cwd = await fs.mkdtemp(path.join(require('os').tmpdir(), 'compile-'))
15
16
  await fs.mkdir(cwd, { recursive: true })
16
17
  const cleanup = () => fs.rm(cwd, { recursive: true, force: true })
18
+ duration('tmpdir', { cwd })
17
19
  return { cwd, cleanup }
18
20
  }
19
21
 
20
22
  module.exports = async (snippet, tmpdir = tmpdirDefault) => {
21
- const compiledTemplate = generateTemplate(snippet)
22
- const dependencies = detectDependencies(compiledTemplate)
23
- let content = transformDependencies(compiledTemplate)
24
- let cleanupPromise
23
+ let content = template(snippet)
24
+ const { cwd, cleanup } = await tmpdir()
25
25
 
26
+ const dependencies = detectDependencies(content)
26
27
  if (dependencies.length) {
27
- const { cwd, cleanup } = await duration('tmpdir', tmpdir)
28
+ content = transformDependencies(content)
28
29
  await duration('npm:install', () => installDependencies({ dependencies, cwd }), {
29
30
  dependencies
30
31
  })
31
- const result = await duration('esbuild', () => build({ content, cwd }))
32
- content = result.outputFiles[0].text
33
- cleanupPromise = duration('tmpDir:cleanup', cleanup)
34
32
  }
35
33
 
34
+ const result = await duration('esbuild', () => build({ content, cwd }))
35
+ content = result.outputFiles[0].text
36
+ const cleanupPromise = duration('tmpDir:cleanup', cleanup)
37
+
36
38
  return { content, cleanupPromise }
37
39
  }
38
40
 
@@ -1,9 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { execSync } = require('child_process')
4
- const { writeFile } = require('fs/promises')
5
4
  const $ = require('tinyspawn')
6
- const path = require('path')
7
5
 
8
6
  const install = (() => {
9
7
  try {
@@ -17,6 +15,5 @@ const install = (() => {
17
15
  })()
18
16
 
19
17
  module.exports = async ({ dependencies, cwd }) => {
20
- await writeFile(path.join(cwd, 'package.json'), '{}')
21
18
  return $(`${install} ${dependencies.join(' ')}`, { cwd })
22
19
  }
@@ -3,6 +3,24 @@
3
3
  const walk = require('acorn-walk')
4
4
  const acorn = require('acorn')
5
5
 
6
+ /**
7
+ * Transforms dependency module names by removing version specifiers.
8
+ *
9
+ * Parses JavaScript code and walks through the AST to find all require() calls
10
+ * and import declarations. Extracts module names from strings that include
11
+ * version information (e.g., 'is-emoji@1.0.0' becomes 'is-emoji').
12
+ *
13
+ * Handles both:
14
+ * - Scoped packages: '@scope/package@1.0.0' → '@scope/package'
15
+ * - Regular packages: 'package@1.0.0' → 'package'
16
+ *
17
+ * This transformation is necessary because the dependency strings include
18
+ * version specifiers for installation tracking, but require/import statements
19
+ * should only reference the base module name.
20
+ *
21
+ * @param {string} code - JavaScript code containing require() or import statements
22
+ * @returns {string} Transformed code with version specifiers removed from dependencies
23
+ */
6
24
  module.exports = code => {
7
25
  const ast = acorn.parse(code, { ecmaVersion: 2023, sourceType: 'module' })
8
26
 
package/src/index.d.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Profiling information about the isolated function execution
3
+ */
4
+ export interface Profiling {
5
+ /** Memory usage in bytes */
6
+ memory: number
7
+ /** Execution duration in milliseconds */
8
+ duration: number
9
+ }
10
+
11
+ /**
12
+ * Logging information captured from the isolated function
13
+ */
14
+ export interface Logging {
15
+ log?: unknown[][]
16
+ info?: unknown[][]
17
+ debug?: unknown[][]
18
+ warn?: unknown[][]
19
+ error?: unknown[][]
20
+ }
21
+
22
+ /**
23
+ * Successful execution result
24
+ */
25
+ export interface SuccessResult<T = unknown> {
26
+ isFulfilled: true
27
+ value: T
28
+ profiling: Profiling
29
+ logging: Logging
30
+ }
31
+
32
+ /**
33
+ * Failed execution result
34
+ */
35
+ export interface FailureResult {
36
+ isFulfilled: false
37
+ value: Error
38
+ profiling: Profiling
39
+ logging: Logging
40
+ }
41
+
42
+ /**
43
+ * Execution result (success or failure)
44
+ */
45
+ export type ExecutionResult<T = unknown> = SuccessResult<T> | FailureResult
46
+
47
+ /**
48
+ * Options for creating an isolated function
49
+ */
50
+ export interface IsolatedFunctionOptions {
51
+ /** Temporary directory configuration */
52
+ tmpdir?: () => Promise<{ cwd: string; cleanup: () => Promise<void> }>
53
+ /** Execution timeout in milliseconds */
54
+ timeout?: number
55
+ /** Memory limit in megabytes */
56
+ memory?: number
57
+ /** When false, errors are returned instead of thrown */
58
+ throwError?: boolean
59
+ /** Array of resources to allow access to */
60
+ allow?: string[]
61
+ }
62
+
63
+ /**
64
+ * Cleanup function to release resources
65
+ */
66
+ export type Cleanup = () => Promise<void>
67
+
68
+ /**
69
+ * Isolated function that can be executed with arguments
70
+ */
71
+ export type IsolatedFn<T = unknown> = (
72
+ ...args: unknown[]
73
+ ) => Promise<SuccessResult<T> | FailureResult>
74
+
75
+ /**
76
+ * Creates an isolated function that runs untrusted code in a separate Node.js process.
77
+ *
78
+ * @param snippet - The function or code string to run in isolation
79
+ * @param options - Configuration options for the isolated function
80
+ * @returns A tuple containing the isolated function and a cleanup function
81
+ *
82
+ * @example
83
+ * ```js
84
+ * const [sum, cleanup] = isolatedFunction((a, b) => a + b, {
85
+ * memory: 128,
86
+ * timeout: 10000
87
+ * })
88
+ *
89
+ * const { value } = await sum(3, 2)
90
+ * console.log(value) // 5
91
+ * await cleanup()
92
+ * ```
93
+ *
94
+ * @example
95
+ * ```js
96
+ * // With error handling
97
+ * const [fn, cleanup] = isolatedFunction(() => {
98
+ * throw new Error('oh no!')
99
+ * }, { throwError: false })
100
+ *
101
+ * const result = await fn()
102
+ * if (!result.isFulfilled) {
103
+ * console.error(result.value)
104
+ * }
105
+ * await cleanup()
106
+ * ```
107
+ */
108
+ declare function isolatedFunction<T = unknown>(
109
+ snippet: Function | string,
110
+ options?: IsolatedFunctionOptions
111
+ ): [IsolatedFn<T>, Cleanup]
112
+
113
+ export default isolatedFunction
package/src/index.js CHANGED
@@ -49,8 +49,8 @@ module.exports = (snippet, { tmpdir, timeout, memory, throwError = true, allow =
49
49
  const { isFulfilled, value, profiling, logging } = JSON.parse(stdout)
50
50
  profiling.duration = duration()
51
51
  debug('node', {
52
- duration: `${Math.round(profiling.duration / 100)}s`,
53
- memory: `${Math.round(profiling.memory / (1024 * 1024))}MiB`
52
+ memory: `${Math.round(profiling.memory / (1024 * 1024))}MiB`,
53
+ duration: `${Math.round(profiling.duration / 100)}s`
54
54
  })
55
55
 
56
56
  return isFulfilled
@@ -2,23 +2,23 @@
2
2
 
3
3
  const SERIALIZE_ERROR = require('./serialize-error')
4
4
 
5
- module.exports = snippet => `
6
- const args = JSON.parse(process.argv[2])
5
+ module.exports = snippet => `;(send => {
6
+ process.stdout.write = function () {}
7
+ const respond = (isFulfilled, value, logs = {}) => send(JSON.stringify({isFulfilled, logging: logs, value, profiling: {memory: process.memoryUsage().rss}}))
7
8
 
8
- /* https://github.com/Kikobeats/null-prototype-object */
9
- const logging = new (/* @__PURE__ */ (() => { let e = function(){}; return e.prototype = Object.create(null), Object.freeze(e.prototype), e })());
9
+ return Promise.resolve().then(async () => {
10
+ const args = JSON.parse(process.argv[2])
10
11
 
11
- for (const method of ['log', 'info', 'debug', 'warn', 'error']) {
12
- console[method] = function (...args) {
13
- logging[method] === undefined ? logging[method] = [args] : logging[method].push(args)
12
+ /* https://github.com/Kikobeats/null-prototype-object */
13
+ const logging = new (/* @__PURE__ */ (() => { let e = function(){}; return e.prototype = Object.create(null), Object.freeze(e.prototype), e })());
14
+ for (const method of ['log', 'info', 'debug', 'warn', 'error']) {
15
+ console[method] = function (...args) {
16
+ logging[method] === undefined ? logging[method] = [args] : logging[method].push(args)
17
+ }
14
18
  }
15
- }
16
19
 
17
- ;(async (send) => {
18
- process.stdout.write = function () {}
19
20
  let value
20
21
  let isFulfilled
21
-
22
22
  try {
23
23
  value = await (${snippet.toString()})(...args)
24
24
  isFulfilled = true
@@ -26,11 +26,8 @@ module.exports = snippet => `
26
26
  value = ${SERIALIZE_ERROR}(error)
27
27
  isFulfilled = false
28
28
  } finally {
29
- send(JSON.stringify({
30
- isFulfilled,
31
- logging,
32
- value,
33
- profiling: { memory: process.memoryUsage().rss }
34
- }))
29
+ respond(isFulfilled, value, logging)
35
30
  }
36
- })(process.stdout.write.bind(process.stdout))`
31
+ })
32
+ .catch(e => respond(false, ${SERIALIZE_ERROR}(e)))
33
+ })(process.stdout.write.bind(process.stdout))`