isolated-function 0.0.3

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2024 microlink.io <hello@microlink.io> (microlink.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # isolated-function
2
+
3
+ ![Last version](https://img.shields.io/github/tag/Kikobeats/isolated-function.svg?style=flat-square)
4
+ [![Coverage Status](https://img.shields.io/coveralls/Kikobeats/isolated-function.svg?style=flat-square)](https://coveralls.io/github/Kikobeats/isolated-function)
5
+ [![NPM Status](https://img.shields.io/npm/dm/isolated-function.svg?style=flat-square)](https://www.npmjs.org/package/isolated-function)
6
+
7
+ **Highlights**
8
+
9
+ - Based on [v8-sandbox](https://github.com/fulcrumapp/v8-sandbox).
10
+ - Auto install npm dependencies.
11
+ - Timeout support.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install isolated-function --save
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```js
22
+ const isoaltedFunction = require('isolated-function')
23
+
24
+ /* This function will run in a sandbox, in a separate process */
25
+ const sum = isoaltedFunction((y, z) => y + z)
26
+
27
+ /* Interact with it as usual from your main code */
28
+ const result = await sum(3, 2)
29
+
30
+ console.log(result)
31
+ ```
32
+
33
+ You can also use `require' for external dependencies:
34
+
35
+ ```js
36
+ const isEmoji = isoaltedFunction(emoji => {
37
+ const isEmoji = require('is-standard-emoji')
38
+ return isEmoji(emoji)
39
+ })
40
+
41
+ await isEmoji('🙌') // => true
42
+ await isEmoji('foo') // => false
43
+ ```
44
+
45
+ The dependencies are bundled with the source code into a single file that is executed in the sandbox.
46
+
47
+ It's intentionally not possible to expose any Node.js objects or functions directly to the sandbox (such as `process`, or filesystem). This makes it slightly harder to integrate into a project, but has the benefit of guaranteed isolation.
48
+
49
+ ## API
50
+
51
+ ### isoaltedFunction(snippet, [options])
52
+
53
+ #### snippet
54
+
55
+ *Required*<br>
56
+ Type: `string`
57
+
58
+ The source code to run.
59
+
60
+ #### options
61
+
62
+ ##### foo
63
+
64
+ Type: `boolean`<br>
65
+ Default: `false`
66
+
67
+ Lorem ipsum.
68
+
69
+ ## License
70
+
71
+ **isolated-function** © [Kiko Beats](https://kikobeats.com), released under the [MIT](https://github.com/Kikobeats/isolated-function/blob/master/LICENSE.md) License.<br>
72
+ Authored and maintained by Kiko Beats with help from [contributors](https://github.com/Kikobeats/isolated-function/contributors).
73
+
74
+ > [kikobeats.com](https://kikobeats.com) · GitHub [@Kiko Beats](https://github.com/Kikobeats) · X [@Kikobeats](https://x.com/Kikobeats)
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "isolated-function",
3
+ "description": "Runs untrusted code in a Node.js v8 sandbox.",
4
+ "homepage": "https://github.com/Kikobeats/isolated-function",
5
+ "version": "0.0.3",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "author": {
11
+ "email": "hello@microlink.io",
12
+ "name": "microlink.io",
13
+ "url": "https://microlink.io"
14
+ },
15
+ "contributors": [
16
+ {
17
+ "name": "Kiko Beats",
18
+ "email": "josefrancisco.verdu@gmail.com"
19
+ }
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Kikobeats/isolated-function.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/Kikobeats/isolated-function/issues"
27
+ },
28
+ "keywords": [
29
+ "isolated",
30
+ "javascript",
31
+ "js",
32
+ "sandbox",
33
+ "v8"
34
+ ],
35
+ "dependencies": {
36
+ "acorn": "~8.12.1",
37
+ "acorn-walk": "~8.3.3",
38
+ "ensure-error": "~3.0.1",
39
+ "esbuild": "~0.23.1",
40
+ "process-stats": "~3.7.10",
41
+ "serialize-error": "8",
42
+ "tinyspawn": "~1.3.2",
43
+ "v8-sandbox": "~3.2.12"
44
+ },
45
+ "devDependencies": {
46
+ "@commitlint/cli": "latest",
47
+ "@commitlint/config-conventional": "latest",
48
+ "@ksmithut/prettier-standard": "latest",
49
+ "ava": "latest",
50
+ "c8": "latest",
51
+ "ci-publish": "latest",
52
+ "finepack": "latest",
53
+ "git-authors-cli": "latest",
54
+ "github-generate-release": "latest",
55
+ "nano-staged": "latest",
56
+ "simple-git-hooks": "latest",
57
+ "standard": "latest",
58
+ "standard-version": "latest"
59
+ },
60
+ "engines": {
61
+ "node": ">= 18"
62
+ },
63
+ "files": [
64
+ "src"
65
+ ],
66
+ "scripts": {
67
+ "clean": "rm -rf node_modules",
68
+ "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
69
+ "coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
70
+ "lint": "standard",
71
+ "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
72
+ "pretest": "npm run lint",
73
+ "release": "standard-version -a",
74
+ "release:github": "github-generate-release",
75
+ "release:tags": "git push --follow-tags origin HEAD:master",
76
+ "test": "c8 ava"
77
+ },
78
+ "license": "MIT",
79
+ "commitlint": {
80
+ "extends": [
81
+ "@commitlint/config-conventional"
82
+ ],
83
+ "rules": {
84
+ "body-max-line-length": [
85
+ 0
86
+ ]
87
+ }
88
+ },
89
+ "nano-staged": {
90
+ "*.js": [
91
+ "prettier-standard",
92
+ "standard --fix"
93
+ ],
94
+ "package.json": [
95
+ "finepack"
96
+ ]
97
+ },
98
+ "simple-git-hooks": {
99
+ "commit-msg": "npx commitlint --edit",
100
+ "pre-commit": "npx nano-staged"
101
+ }
102
+ }
package/src/compile.js ADDED
@@ -0,0 +1,76 @@
1
+ 'use strict'
2
+
3
+ const { execSync } = require('child_process')
4
+ const walk = require('acorn-walk')
5
+ const esbuild = require('esbuild')
6
+ const fs = require('fs/promises')
7
+ const { tmpdir } = require('os')
8
+ const acorn = require('acorn')
9
+ const $ = require('tinyspawn')
10
+ const path = require('path')
11
+
12
+ const generateTemplate = require('./template')
13
+
14
+ const packageManager = (() => {
15
+ try {
16
+ execSync('which pnpm').toString().trim()
17
+ return { init: 'pnpm init', install: 'pnpm install' }
18
+ } catch {
19
+ return { init: 'npm init --yes', install: 'npm install' }
20
+ }
21
+ })()
22
+
23
+ // Function to detect require and import statements using acorn
24
+ const detectDependencies = code => {
25
+ const dependencies = new Set()
26
+
27
+ // Parse the code into an AST
28
+ const ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module' })
29
+
30
+ // Traverse the AST to find require and import statements
31
+ walk.simple(ast, {
32
+ CallExpression (node) {
33
+ if (
34
+ node.callee.name === 'require' &&
35
+ node.arguments.length === 1 &&
36
+ node.arguments[0].type === 'Literal'
37
+ ) {
38
+ dependencies.add(node.arguments[0].value)
39
+ }
40
+ },
41
+ ImportDeclaration (node) {
42
+ dependencies.add(node.source.value)
43
+ }
44
+ })
45
+
46
+ return Array.from(dependencies)
47
+ }
48
+
49
+ module.exports = async snippet => {
50
+ const tmp = await fs.mkdtemp(path.join(tmpdir(), 'compile-'))
51
+ await fs.mkdir(tmp, { recursive: true })
52
+
53
+ const template = generateTemplate(snippet)
54
+
55
+ const entryFile = path.join(tmp, 'index.js')
56
+ await fs.writeFile(entryFile, template)
57
+
58
+ const dependencies = detectDependencies(template)
59
+ await $(packageManager.init, { cwd: tmp })
60
+ await $(`${packageManager.install} ${dependencies.join(' ')}`, { cwd: tmp })
61
+
62
+ const result = await esbuild.build({
63
+ entryPoints: [entryFile],
64
+ bundle: true,
65
+ write: false,
66
+ platform: 'node'
67
+ })
68
+
69
+ const bundledCode = result.outputFiles[0].text
70
+
71
+ await fs.rm(tmp, { recursive: true })
72
+
73
+ return bundledCode
74
+ }
75
+
76
+ module.exports.detectDependencies = detectDependencies
package/src/index.js ADDED
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ const { Sandbox } = require('v8-sandbox')
4
+ const { deserializeError } = require('serialize-error')
5
+
6
+ const compile = require('./compile')
7
+
8
+ module.exports = (snippet, { timeout = 10000, globals = {} } = {}) => {
9
+ if (typeof snippet !== 'function') {
10
+ throw new TypeError('Expected a function')
11
+ }
12
+
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
31
+ }
32
+ }
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const SERIALIZE_ERROR = require('./serialize-error')
4
+
5
+ const DISALLOW_INTERNALS = [
6
+ '_dispatch',
7
+ 'base64ToBuffer',
8
+ 'bufferToBase64',
9
+ 'define',
10
+ 'defineAsync',
11
+ 'dispatch',
12
+ 'httpRequest',
13
+ 'info',
14
+ 'setResult'
15
+ ]
16
+
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
+ })()
26
+
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
+ }
39
+
40
+ module.exports = generateTemplate
@@ -0,0 +1,107 @@
1
+ // serialize-error@8.1.0
2
+ module.exports = `(() => {
3
+ 'use strict'
4
+
5
+ const commonProperties = [
6
+ { property: 'name', enumerable: false },
7
+ { property: 'message', enumerable: false },
8
+ { property: 'stack', enumerable: false },
9
+ { property: 'code', enumerable: true }
10
+ ]
11
+
12
+ const isCalled = Symbol('.toJSON called')
13
+
14
+ const toJSON = from => {
15
+ from[isCalled] = true
16
+ const json = from.toJSON()
17
+ delete from[isCalled]
18
+ return json
19
+ }
20
+
21
+ const destroyCircular = ({
22
+ from,
23
+ seen,
24
+ to_,
25
+ forceEnumerable,
26
+ maxDepth,
27
+ depth
28
+ }) => {
29
+ const to = to_ || (Array.isArray(from) ? [] : {})
30
+
31
+ seen.push(from)
32
+
33
+ if (depth >= maxDepth) {
34
+ return to
35
+ }
36
+
37
+ if (typeof from.toJSON === 'function' && from[isCalled] !== true) {
38
+ return toJSON(from)
39
+ }
40
+
41
+ for (const [key, value] of Object.entries(from)) {
42
+ if (typeof Buffer === 'function' && Buffer.isBuffer(value)) {
43
+ to[key] = '[object Buffer]'
44
+ continue
45
+ }
46
+
47
+ if (typeof value === 'function') {
48
+ continue
49
+ }
50
+
51
+ if (!value || typeof value !== 'object') {
52
+ to[key] = value
53
+ continue
54
+ }
55
+
56
+ if (!seen.includes(from[key])) {
57
+ depth++
58
+
59
+ to[key] = destroyCircular({
60
+ from: from[key],
61
+ seen: seen.slice(),
62
+ forceEnumerable,
63
+ maxDepth,
64
+ depth
65
+ })
66
+ continue
67
+ }
68
+
69
+ to[key] = '[Circular]'
70
+ }
71
+
72
+ for (const { property, enumerable } of commonProperties) {
73
+ if (typeof from[property] === 'string') {
74
+ Object.defineProperty(to, property, {
75
+ value: from[property],
76
+ enumerable: forceEnumerable ? true : enumerable,
77
+ configurable: true,
78
+ writable: true
79
+ })
80
+ }
81
+ }
82
+
83
+ return to
84
+ }
85
+
86
+ return (value, options = {}) => {
87
+ const { maxDepth = Number.POSITIVE_INFINITY } = options
88
+
89
+ if (typeof value === 'object' && value !== null) {
90
+ return destroyCircular({
91
+ from: value,
92
+ seen: [],
93
+ forceEnumerable: true,
94
+ maxDepth,
95
+ depth: 0
96
+ })
97
+ }
98
+
99
+ // People sometimes throw things besides Error objects…
100
+ if (typeof value === 'function') {
101
+ return \`[Function: \${value.name || 'anonymous'}]\`
102
+ }
103
+
104
+ return value
105
+ }
106
+
107
+ })()`