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 +21 -0
- package/README.md +74 -0
- package/package.json +102 -0
- package/src/compile.js +76 -0
- package/src/index.js +32 -0
- package/src/template/index.js +40 -0
- package/src/template/serialize-error.js +107 -0
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
|
+

|
|
4
|
+
[](https://coveralls.io/github/Kikobeats/isolated-function)
|
|
5
|
+
[](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
|
+
})()`
|