├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question-or-help-required.md └── workflows │ ├── deploy-docs.yml │ └── pull_request.yml ├── .gitignore ├── .np-config.json ├── .npmignore ├── .vscode └── settings.json ├── .zed └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── biome.json ├── bun.lock ├── example ├── README.md ├── ai │ ├── README.md │ └── index.ts ├── async │ ├── README.md │ └── index.ts ├── basic │ ├── README.md │ └── index.ts ├── browser │ ├── README.md │ ├── example-request.html │ ├── playground.html │ └── simple.html ├── custom-module │ ├── README.md │ ├── custom-module.js │ └── index.ts ├── function-call │ ├── README.md │ └── index.ts ├── run-tests │ ├── README.md │ └── index.ts ├── server │ ├── README.md │ ├── openapi.ts │ ├── server.ts │ ├── types.ts │ └── worker.ts ├── timer │ ├── README.md │ └── index.ts ├── typescript │ ├── README.md │ └── index.ts └── user-code │ ├── README.md │ └── index.ts ├── jsr.json ├── knip.json ├── loadtest.ts ├── package-lock.json ├── package.json ├── src ├── .DS_Store ├── adapter │ ├── fetch.test.ts │ └── fetch.ts ├── createTimeInterval.ts ├── createVirtualFileSystem.ts ├── getTypescriptSupport.ts ├── index.ts ├── loadAsyncQuickJs.ts ├── loadQuickJs.ts ├── modules │ ├── assert.js │ ├── buffer.js │ ├── events.js │ ├── fs.js │ ├── fs_promises.js │ ├── module.js │ ├── nodeCompatibility │ │ ├── headers.js │ │ ├── request.js │ │ └── response.js │ ├── path.js │ ├── process.js │ ├── punycode.js │ ├── querystring.js │ ├── string_decoder.js │ ├── timers.js │ ├── timers_promises.js │ ├── url.js │ └── util.js ├── sandbox │ ├── asyncVersion │ │ ├── createAsyncEvalCodeFunction.ts │ │ ├── createAsyncValidateCodeFunction.ts │ │ ├── executeAsyncSandboxFunction.ts │ │ ├── getAsyncModuleLoader.ts │ │ ├── loadWasmModule.ts │ │ ├── modulePathNormalizerAsync.ts │ │ ├── prepareAsyncNodeCompatibility.ts │ │ └── prepareAsyncSandbox.ts │ ├── expose │ │ ├── expose.ts │ │ ├── isES2015Class.ts │ │ └── isObject.ts │ ├── getMaxIntervalAmount.ts │ ├── getMaxTimeoutAmount.ts │ ├── handleEvalError.ts │ ├── handleToNative │ │ ├── handleToNative.ts │ │ └── serializer │ │ │ ├── index.ts │ │ │ ├── serializeArrayBuffer.ts │ │ │ ├── serializeBuffer.ts │ │ │ ├── serializeDate.ts │ │ │ ├── serializeError.ts │ │ │ ├── serializeHeaders.ts │ │ │ ├── serializeMap.ts │ │ │ ├── serializeSet.ts │ │ │ ├── serializeURLSearchParams.ts │ │ │ └── serializeUrl.ts │ ├── helper.ts │ ├── loadAsyncWasmModule.ts │ ├── provide │ │ ├── provideConsole.ts │ │ ├── provideEnv.ts │ │ ├── provideFs.ts │ │ ├── provideHttp.ts │ │ └── provideTimingFunctions.ts │ ├── setupFileSystem.ts │ └── syncVersion │ │ ├── createEvalCodeFunction.ts │ │ ├── createValidateCodeFunction.ts │ │ ├── executeSandboxFunction.ts │ │ ├── getModuleLoader.ts │ │ ├── loadWasmModule.ts │ │ ├── modulePathNormalizer.ts │ │ ├── prepareNodeCompatibility.ts │ │ └── prepareSandbox.ts ├── test-regression │ ├── 59-timeout.test.ts │ └── 68-crash-when-async-throws.test.ts ├── test │ ├── async │ │ ├── assert.test.ts │ │ ├── core-console.test.ts │ │ ├── core-return-values.test.ts │ │ ├── core-timeout.test.ts │ │ ├── core-timers.test.ts │ │ ├── core-validateCode.test.ts │ │ ├── data-exchange.test.ts │ │ ├── fsModule-directory.test.ts │ │ ├── fsModule-file.test.ts │ │ ├── fsModule-links.test.ts │ │ ├── fsModule-permissions.test.ts │ │ ├── nodeModules.test.ts │ │ ├── util-types.test.ts │ │ └── util.test.ts │ └── sync │ │ ├── assert.test.ts │ │ ├── core-console.test.ts │ │ ├── core-return-values.test.ts │ │ ├── core-timeout.test.ts │ │ ├── core-timers.test.ts │ │ ├── core-validateCode.test.ts │ │ ├── data-exchange.test.ts │ │ ├── fsModule-directory.test.ts │ │ ├── fsModule-file.test.ts │ │ ├── fsModule-links.test.ts │ │ ├── fsModule-permissions.test.ts │ │ ├── nodeModules.test.ts │ │ ├── util-types.test.ts │ │ └── util.test.ts └── types │ ├── CodeFunctionInput.ts │ ├── ErrorResponse.ts │ ├── LoadQuickJsOptions.ts │ ├── OkResponse.ts │ ├── OkResponseCheck.ts │ ├── Prettify.ts │ ├── RuntimeOptions.ts │ ├── SandboxEvalCode.ts │ ├── SandboxFunction.ts │ ├── SandboxOptions.ts │ ├── SandboxValidateCode.ts │ └── Serializer.ts ├── tsconfig.json ├── typedoc.json ├── typedoc.tsconfig.json ├── vendor.ts ├── vendor └── testrunner │ ├── testRunner.ts │ └── types.ts └── website ├── .vitepress ├── config.ts └── theme │ ├── components │ └── Playground.vue │ ├── index.ts │ └── style.css ├── blog ├── 2024-09-01-version-2.md ├── index.md └── release.md ├── docs ├── basic-understanding.md ├── credits.md ├── data-exchange.md ├── fetch.md ├── file-system.md ├── index.md ├── module-resolution │ ├── async-module-loader.md │ ├── custom-modules.md │ ├── index.md │ ├── node-compatibility.md │ ├── path-normalizer.md │ └── sync-module-loader.md ├── running-tests.md ├── runtime-options.md ├── sandboxed-code.md └── typescript-usage.md ├── imprint.md ├── index.md ├── playground.md ├── public ├── .DS_Store ├── .nojekyll ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── cookieconsent-init2.js ├── cookieconsent.css ├── cookieconsent2.js ├── example-request.html ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── logo.png ├── mstile-150x150.png ├── og.jpg ├── safari-pinned-tab.svg ├── site.webmanifest ├── use-case-ai.jpg ├── use-case-ssr.jpg └── use-case-user.jpg └── use-cases ├── ai-generated-code.md ├── index.md ├── serverside-rendering.md └── user-generated-code.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-or-help-required.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or Help required 3 | about: Ask a question and request help 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Update Website 2 | 3 | on: 4 | push: 5 | branches: 6 | # make sure this is the branch you are using 7 | - main 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: pages 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Build job 25 | deploydoc: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 32 | - name: Bun 33 | uses: oven-sh/setup-bun@v2 34 | with: 35 | bun-version: latest 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v4 38 | - name: Install dependencies 39 | run: bun install 40 | - name: Build the lib 41 | run: | 42 | bun run build 43 | - name: Build documentation 44 | run: | 45 | bun run docs:build 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v3 48 | with: 49 | path: docs 50 | 51 | # Deployment job 52 | deploy: 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | needs: deploydoc 57 | runs-on: ubuntu-latest 58 | name: Deploy 59 | steps: 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: 4 | pull_request: 5 | branches: ['main'] 6 | 7 | jobs: 8 | # Build job 9 | builddev: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 16 | - name: Bun 17 | uses: oven-sh/setup-bun@v2 18 | with: 19 | bun-version: latest 20 | - name: Install dependencies 21 | run: bun install 22 | - name: Lint 23 | run: | 24 | bun run lint 25 | - name: Build 26 | run: | 27 | bun run build 28 | - name: Test 29 | run: | 30 | bun run test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .tshy 133 | .tshy-build 134 | 135 | src/modules/build 136 | 137 | website/.vitepress/cache 138 | docs -------------------------------------------------------------------------------- /.np-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testScript": "test", 3 | "contents": "." 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .tshy 2 | .vscode 3 | .env 4 | .zed 5 | node_modules 6 | docs 7 | example 8 | src 9 | vendor 10 | bun* 11 | biome* 12 | website 13 | *.ts 14 | **/*.test.* 15 | 16 | !dist 17 | !package.json 18 | !README.md 19 | .github -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_on_save": "on", 3 | "code_actions_on_format": { 4 | "source.fixAll": true, 5 | "source.organizeImports.biome": true 6 | }, 7 | "formatter": "auto", 8 | "languages": { 9 | "TypeScript": { 10 | "code_actions_on_format": { 11 | "source.fixAll": true, 12 | "source.organizeImports.biome": true 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sebastian Wessel 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickJS - Execute JavaScript and TypeScript in a WebAssembly QuickJS Sandbox 2 | 3 | This TypeScript package allows you to safely execute **JavaScript AND TypeScript code** within a WebAssembly sandbox using the QuickJS engine. Perfect for isolating and running untrusted code securely, it leverages the lightweight and fast QuickJS engine compiled to WebAssembly, providing a robust environment for code execution. 4 | 5 | **[View the full documentation](https://sebastianwessel.github.io/quickjs/)** | **[Find examples in the repository](https://github.com/sebastianwessel/quickjs/tree/main/example)** | **[Online Playground](https://sebastianwessel.github.io/quickjs/playground.html)** 6 | 7 | ## Features 8 | 9 | - **Security**: Run untrusted JavaScript and TypeScript code in a safe, isolated environment. 10 | - **Basic Node.js modules**: Provides basic standard Node.js module support for common use cases. 11 | - **File System**: Can mount a virtual file system. 12 | - **Custom Node Modules**: Custom node modules are mountable. 13 | - **Fetch Client**: Can provide a fetch client to make http(s) calls. 14 | - **Test-Runner**: Includes a test runner and chai based `expect`. 15 | - **Performance**: Benefit from the lightweight and efficient QuickJS engine. 16 | - **Versatility**: Easily integrate with existing TypeScript projects. 17 | - **Simplicity**: User-friendly API for executing and managing JavaScript and TypeScript code in the sandbox. 18 | 19 | ## Basic Usage 20 | 21 | Here's a simple example of how to use the package: 22 | 23 | ```typescript 24 | import { type SandboxOptions, loadQuickJs } from '@sebastianwessel/quickjs' 25 | 26 | // General setup like loading and init of the QuickJS wasm 27 | // It is a ressource intensive job and should be done only once if possible 28 | const { runSandboxed } = await loadQuickJs() 29 | 30 | const options: SandboxOptions = { 31 | allowFetch: true, // inject fetch and allow the code to fetch data 32 | allowFs: true, // mount a virtual file system and provide node:fs module 33 | env: { 34 | MY_ENV_VAR: 'env var value', 35 | }, 36 | } 37 | 38 | const code = ` 39 | import { join } from 'path' 40 | 41 | const fn = async ()=>{ 42 | console.log(join('src','dist')) // logs "src/dist" on host system 43 | 44 | console.log(env.MY_ENV_VAR) // logs "env var value" on host system 45 | 46 | const url = new URL('https://example.com') 47 | 48 | const f = await fetch(url) 49 | 50 | return f.text() 51 | } 52 | 53 | export default await fn() 54 | ` 55 | 56 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 57 | 58 | console.log(result) // { ok: true, data: '\n\n[....]\n' } 59 | ``` 60 | 61 | **[View the full documentation](https://sebastianwessel.github.io/quickjs/)** 62 | 63 | **[Find examples in the repository](https://github.com/sebastianwessel/quickjs/tree/main/example)** 64 | 65 | ## Credits 66 | 67 | This lib is based on: 68 | 69 | - [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) 70 | - [quickjs-emscripten-sync](https://github.com/reearth/quickjs-emscripten-sync) 71 | - [memfs](https://github.com/streamich/memfs) 72 | - [Chai](https://www.chaijs.com) 73 | 74 | Tools used: 75 | 76 | - [Bun](https://bun.sh) 77 | - [Biome](https://biomejs.dev) 78 | - [Hono](https://hono.dev) 79 | - [poolifier-web-worker](https://github.com/poolifier/poolifier-web-worker) 80 | - [tshy](https://github.com/isaacs/tshy) 81 | - [autocannon](https://github.com/mcollina/autocannon) 82 | 83 | ## License 84 | 85 | This project is licensed under the MIT License. 86 | 87 | --- 88 | 89 | This package is ideal for developers looking to execute JavaScript code securely within a TypeScript application, ensuring both performance and safety with the QuickJS WebAssembly sandbox. 90 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "files": { 7 | "ignore": [ 8 | "dist", 9 | "node_modules", 10 | ".tshy", 11 | "**/build/**", 12 | "cookieconsent2.js", 13 | "**/cache/**", 14 | "cookieconsent-init2.js", 15 | "cookieconsent.css", 16 | "docs", 17 | ".tshy-build", 18 | "website/.vitepress/.temp" 19 | ] 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "complexity": { 26 | "noForEach": "warn", 27 | "noBannedTypes": "warn" 28 | }, 29 | "suspicious": { 30 | "noExplicitAny": "off" 31 | } 32 | } 33 | }, 34 | "formatter": { 35 | "enabled": true, 36 | "formatWithErrors": false, 37 | "attributePosition": "auto", 38 | "indentStyle": "tab", 39 | "indentWidth": 2, 40 | "lineWidth": 120, 41 | "lineEnding": "lf" 42 | }, 43 | "javascript": { 44 | "formatter": { 45 | "quoteStyle": "single", 46 | "arrowParentheses": "asNeeded", 47 | "bracketSameLine": false, 48 | "bracketSpacing": true, 49 | "jsxQuoteStyle": "double", 50 | "quoteProperties": "asNeeded", 51 | "semicolons": "asNeeded", 52 | "trailingCommas": "all", 53 | "lineWidth": 120, 54 | "indentWidth": 2, 55 | "indentStyle": "tab" 56 | } 57 | }, 58 | "json": { 59 | "formatter": { 60 | "trailingCommas": "none" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # QuickJS Examples 2 | 3 | Here are some examples on how you can use **[@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs)**. 4 | 5 | The examples are intended to run in [Bun](https://bun.sh), but can be easily adapted to run on other runtimes. 6 | 7 | ## Installation 8 | 9 | Before running any of the examples, ensure you have installed all necessary modules by running: 10 | 11 | ```sh 12 | bun i 13 | ``` 14 | 15 | ## Basic Example 16 | 17 | This is a basic example of how to use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs). You can test it out by running: 18 | 19 | ```sh 20 | bun run example:basic 21 | ``` 22 | 23 | from the root of this repository. 24 | 25 | ## Server Example 26 | 27 | The server example demonstrates how you can use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs) inside a web server. In this example, we run a web server that spawns workers on request using the [poolifier-web-worker](https://github.com/poolifier/poolifier-web-worker) package. Each worker runs its own QuickJS sandbox and executes the given code. 28 | 29 | You can test it out by running: 30 | 31 | ```sh 32 | bun run example:server 33 | ``` 34 | 35 | from the root of this repository. Once the server has started, open your browser and go to [http://localhost:3000/](http://localhost:3000/). You will see a simple OpenAPI (Swagger) UI. 36 | 37 | ## Run-Tests Example 38 | 39 | In the *run-tests* example, the usage of the included test runner is shown. You can test it out by running: 40 | 41 | ```sh 42 | bun run example:test 43 | ``` 44 | 45 | from the root of this repository. 46 | -------------------------------------------------------------------------------- /example/ai/README.md: -------------------------------------------------------------------------------- 1 | # Execute AI generated Code 2 | 3 | This example demonstrates how AI generated code can be executed with QuickJS. You can test it out by running: 4 | 5 | ```sh 6 | bun run example:ai 7 | ``` 8 | 9 | from the root of this repository. 10 | -------------------------------------------------------------------------------- /example/ai/index.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 3 | 4 | // The user instruction 5 | const USER_INSTRUCTION = 'I need the content of the title tag from https://purista.dev' 6 | 7 | // Set the LLM - here, Ollama with model Qwen 2.5 7b is used 8 | // biome-ignore lint/complexity/useLiteralKeys: 9 | const OPENAI_API_KEY = process.env['OPENAI_API_KEY'] ?? '' 10 | // biome-ignore lint/complexity/useLiteralKeys: 11 | const OPENAI_API_BASE = process.env['OPENAI_API_BASE'] ?? 'http://localhost:11434/v1' 12 | const MODEL = 'qwen2.5-coder:7b' //'gpt-4o' 13 | 14 | const client = new OpenAI({ 15 | apiKey: OPENAI_API_KEY, 16 | baseURL: OPENAI_API_BASE, 17 | }) 18 | 19 | const promptTemplate = ` 20 | Your task is to implement a function in javascript for the user instruction. 21 | 22 | 23 | %INSTRUCTION% 24 | 25 | 26 | Implementation details: 27 | - you are in a Node.JS environment 28 | - use ESM syntax with import statements 29 | - use only native node packages 30 | - never use external packages 31 | - the generated code must return the result with with default export 32 | - when a promise is returned via export default it must be explicit awaited 33 | - you can use the native fetch function 34 | 35 | Return only the javascript code without any thoughts or additional text. Always Return only as plain text. Do not use backticks or any format. 36 | 37 | 38 | Example: 39 | \`\`\`ts 40 | async myFunction()=> { 41 | const res = await fetch('https://example.com') 42 | if(!res.ok) { 43 | throw new Error('Failed to fetch example.com') 44 | } 45 | return res.json() 46 | } 47 | 48 | export default await myFunction() 49 | \`\`\` 50 | 51 | ` 52 | 53 | console.log('Generating code') 54 | 55 | const chatCompletion = await client.chat.completions.create({ 56 | messages: [{ role: 'user', content: promptTemplate.replace('%INSTRUCTION%', USER_INSTRUCTION) }], 57 | model: MODEL, 58 | }) 59 | 60 | const code = chatCompletion.choices[0].message.content?.replace(/^```[a-zA-Z]*\n?|```$/g, '') 61 | 62 | if (!code) { 63 | throw new Error('Failed to generate code') 64 | } 65 | 66 | const { runSandboxed } = await loadQuickJs() 67 | 68 | const options: SandboxOptions = { 69 | allowFetch: true, // inject fetch and allow the code to fetch data 70 | allowFs: true, // mount a virtual file system and provide node:fs module 71 | } 72 | 73 | const resultSandbox = await runSandboxed(async ({ evalCode }) => { 74 | console.log('Executing code') 75 | return await evalCode(code) 76 | }, options) 77 | 78 | let resultText = '' 79 | 80 | if (!resultSandbox.ok) { 81 | console.log('code', code) 82 | console.log() 83 | console.log('Code execution failed', resultSandbox.error) 84 | resultText = `The code execution failed with error ${resultSandbox.error.message}` 85 | } else { 86 | console.log('Code execution result', resultSandbox.data) 87 | resultText = `The code execution was successful. 88 | 89 | ${resultSandbox.data} 90 | 91 | ` 92 | } 93 | 94 | console.log('Generating final answer') 95 | 96 | const finalChatCompletion = await client.chat.completions.create({ 97 | messages: [ 98 | { 99 | role: 'user', 100 | content: `You task to create ab answer based on the the following actions. 101 | 102 | ## Action 1 103 | The user has provided the following instruction: 104 | 105 | 106 | ${USER_INSTRUCTION} 107 | 108 | 109 | ## Action 2 110 | An AI has generated a javascript code, based on the users instruction. 111 | 112 | ## Action 3 113 | The generated javascript code was executed. 114 | ${resultText} 115 | 116 | Give a friendly answer to the user, based on the actions. 117 | `, 118 | }, 119 | ], 120 | model: MODEL, 121 | }) 122 | 123 | console.log('======== Final Answer ========') 124 | console.log('') 125 | console.log(finalChatCompletion.choices[0].message.content) 126 | -------------------------------------------------------------------------------- /example/async/README.md: -------------------------------------------------------------------------------- 1 | # Async Example 2 | 3 | This is an example of how to use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs) with asynchronous variants. You can test it out by running: 4 | 5 | ```sh 6 | bun run example:async 7 | ``` 8 | 9 | from the root of this repository. 10 | -------------------------------------------------------------------------------- /example/async/index.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'node:path' 2 | import type { QuickJSAsyncContext } from 'quickjs-emscripten-core' 3 | import { type SandboxAsyncOptions, getAsyncModuleLoader, loadAsyncQuickJs } from '../../src/index.js' 4 | 5 | const { runSandboxed } = await loadAsyncQuickJs() 6 | 7 | const modulePathNormalizer = async (baseName: string, requestedName: string) => { 8 | // import from esm.sh 9 | if (requestedName.startsWith('esm.sh')) { 10 | return `https://${requestedName}` 11 | } 12 | 13 | // import from esm.sh 14 | if (requestedName.startsWith('https://esm.sh')) { 15 | return requestedName 16 | } 17 | 18 | // import within an esm.sh import 19 | if (requestedName.startsWith('/')) { 20 | return `https://esm.sh${requestedName}` 21 | } 22 | 23 | // relative import 24 | if (requestedName.startsWith('.')) { 25 | // relative import from esm.sh loaded module 26 | if (baseName.startsWith('https://esm.sh')) { 27 | return new URL(requestedName, baseName).toString() 28 | } 29 | 30 | // relative import from local import 31 | const parts = baseName.split('/') 32 | parts.pop() 33 | 34 | return resolve(`/${parts.join('/')}`, requestedName) 35 | } 36 | 37 | // unify module import name 38 | const moduleName = requestedName.replace('node:', '') 39 | 40 | return join('/node_modules', moduleName) 41 | } 42 | 43 | const getModuleLoader = (fs, runtimeOptions) => { 44 | const defaultLoader = getAsyncModuleLoader(fs, runtimeOptions) 45 | 46 | const loader = async (moduleName: string, context: QuickJSAsyncContext) => { 47 | console.log('fetching module', moduleName) 48 | 49 | if (!moduleName.startsWith('https://esm.sh')) { 50 | return defaultLoader(moduleName, context) 51 | } 52 | 53 | const response = await fetch(moduleName) 54 | if (!response.ok) { 55 | throw new Error(`Failed to load module ${moduleName}`) 56 | } 57 | const content = await response.text() 58 | return content 59 | } 60 | 61 | return loader 62 | } 63 | 64 | const options: SandboxAsyncOptions = { 65 | modulePathNormalizer, 66 | getModuleLoader, 67 | } 68 | 69 | const code = ` 70 | import * as React from 'esm.sh/react@15' 71 | import * as ReactDOMServer from 'esm.sh/react-dom@15/server' 72 | const e = React.createElement 73 | export default ReactDOMServer.renderToStaticMarkup( 74 | e('div', null, e('strong', null, 'Hello world!')) 75 | ) 76 | ` 77 | 78 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 79 | 80 | console.log(result) 81 | -------------------------------------------------------------------------------- /example/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic Example 2 | 3 | This is a basic example of how to use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs). You can test it out by running: 4 | 5 | ```sh 6 | bun run example:basic 7 | ``` 8 | 9 | from the root of this repository. 10 | -------------------------------------------------------------------------------- /example/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 2 | 3 | // General setup like loading and init of the QuickJS wasm 4 | // It is a ressource intensive job and should be done only once if possible 5 | const { runSandboxed } = await loadQuickJs() 6 | 7 | const options: SandboxOptions = { 8 | allowFetch: true, // inject fetch and allow the code to fetch data 9 | allowFs: true, // mount a virtual file system and provide node:fs module 10 | env: { 11 | MY_ENV_VAR: 'env var value', 12 | }, 13 | } 14 | 15 | const code = ` 16 | import { join } from 'path' 17 | 18 | const fn = async ()=>{ 19 | console.log(join('src','dist')) // logs "src/dist" on host system 20 | 21 | console.log(env.MY_ENV_VAR) // logs "env var value" on host system 22 | 23 | const url = new URL('https://example.com') 24 | 25 | const f = await fetch(url) 26 | 27 | return f.text() 28 | } 29 | 30 | const result = await fn() 31 | 32 | globalThis.step1 = result 33 | 34 | export default result 35 | ` 36 | 37 | const code2 = ` 38 | 39 | export default 'step 2' + step1 40 | ` 41 | 42 | const result = await runSandboxed(async ({ evalCode }) => { 43 | // run first call 44 | const result = await evalCode(code) 45 | 46 | console.log('step 1', result) 47 | 48 | // run second call 49 | return evalCode(code2) 50 | }, options) 51 | 52 | console.log(result) // { ok: true, data: '\n\n[....]\n' } 53 | -------------------------------------------------------------------------------- /example/browser/README.md: -------------------------------------------------------------------------------- 1 | # Browser Example 2 | 3 | An example on how to use this library witout any build step in the browser. 4 | 5 | The [simple Version](./simple.html) and the [Playground version](./playground.html) are available. 6 | -------------------------------------------------------------------------------- /example/browser/example-request.html: -------------------------------------------------------------------------------- 1 | 2 | @sebastianwessel/quickjs 3 | Hello World 4 | -------------------------------------------------------------------------------- /example/browser/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/custom-module/README.md: -------------------------------------------------------------------------------- 1 | # Using Custom Node Module 2 | 3 | This is a basic example of how to use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs). You can test it out by running: 4 | 5 | ```sh 6 | bun run example:module 7 | ``` 8 | 9 | from the root of this repository. -------------------------------------------------------------------------------- /example/custom-module/custom-module.js: -------------------------------------------------------------------------------- 1 | export const customFn = () => { 2 | return 'Hello from the custom module' 3 | } 4 | -------------------------------------------------------------------------------- /example/custom-module/index.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 4 | 5 | // General setup like loading and init of the QuickJS wasm 6 | // It is a ressource intensive job and should be done only once if possible 7 | const { runSandboxed } = await loadQuickJs() 8 | 9 | const __dirname = dirname(fileURLToPath(import.meta.url)) 10 | const customModuleHostLocation = join(__dirname, './custom-module.js') 11 | 12 | const options: SandboxOptions = { 13 | nodeModules: { 14 | // module name 15 | 'custom-module': { 16 | // key must be index.js, value file content of module 17 | 'index.js': await Bun.file(customModuleHostLocation).text(), 18 | }, 19 | }, 20 | mountFs: { 21 | src: { 22 | 'custom-relative.js': ` 23 | import { test } from './lib/sub-import.js' 24 | export const relativeImportFunction = ()=>test() 25 | `, 26 | lib: { 27 | 'sub-import.js': `export const test = ()=>'Hello from relative import function'`, 28 | }, 29 | }, 30 | 'text.txt': 'Some text file', 31 | }, 32 | allowFs: true, 33 | } 34 | 35 | const code = ` 36 | import { readFileSync } from 'node:fs' 37 | import { customFn } from 'custom-module' 38 | 39 | // the current code is virtual the file /src/index.js. 40 | // relative imports are relative to the current file location 41 | import { relativeImportFunction } from './custom-relative.js' 42 | 43 | const customModule = customFn() 44 | console.log('customModule:', customModule) 45 | 46 | const relativeImport = relativeImportFunction() 47 | console.log('relativeImport:', relativeImport) 48 | 49 | // node:fs is relative to cwd which is / = the root of the file system 50 | const fileContent = readFileSync('text.txt', 'utf8') 51 | 52 | export default { customModule, relativeImport, fileContent, cwd: process.cwd() } 53 | ` 54 | 55 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 56 | 57 | console.log(result) // { ok: true, data: 'Hello from the custom module' } 58 | -------------------------------------------------------------------------------- /example/function-call/README.md: -------------------------------------------------------------------------------- 1 | # Function Example 2 | 3 | This is an example of how to use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs). You can test it out by running: 4 | 5 | ```sh 6 | bun run example:function 7 | ``` 8 | 9 | from the root of this repository. 10 | 11 | The example shows how a function in the sandbox can be called from the host. 12 | -------------------------------------------------------------------------------- /example/function-call/index.ts: -------------------------------------------------------------------------------- 1 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 2 | 3 | // General setup like loading and init of the QuickJS wasm 4 | // It is a ressource intensive job and should be done only once if possible 5 | const { runSandboxed } = await loadQuickJs() 6 | 7 | const options: SandboxOptions = {} 8 | 9 | const code = ` 10 | 11 | globalThis.total = 0 12 | 13 | const sum = (input) => { 14 | globalThis.total = total + input 15 | return total 16 | } 17 | 18 | export default sum 19 | ` 20 | 21 | const result = await runSandboxed(async ({ evalCode }) => { 22 | const result = await evalCode(code) 23 | if (!result.ok) { 24 | console.error(result) 25 | throw new Error('Failed to evaluate code') 26 | } 27 | const fn = result.data as (input: number) => Promise 28 | 29 | for (let index = 0; index < 10; index++) { 30 | const total = await fn(index) 31 | console.log(index, total) 32 | } 33 | 34 | return 'DONE' 35 | }, options) 36 | 37 | console.log(result) 38 | -------------------------------------------------------------------------------- /example/run-tests/README.md: -------------------------------------------------------------------------------- 1 | # Run-Tests Example 2 | 3 | In the *run-tests* example, the usage of the included test runner is shown. You can test it out by running: 4 | 5 | ```sh 6 | bun run example:test 7 | ``` 8 | 9 | from the root of this repository. -------------------------------------------------------------------------------- /example/run-tests/index.ts: -------------------------------------------------------------------------------- 1 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 2 | 3 | // General setup like loading and init of the QuickJS wasm 4 | // It is a ressource intensive job and should be done only once if possible 5 | const { runSandboxed } = await loadQuickJs() 6 | 7 | const options: SandboxOptions = { 8 | allowFetch: true, // inject fetch and allow the code to fetch data 9 | allowFs: true, // mount a virtual file system and provide node:fs module 10 | env: { 11 | MY_ENV_VAR: 'env var value', 12 | }, 13 | enableTestUtils: true, 14 | } 15 | 16 | const code = ` 17 | import 'test' 18 | 19 | describe('mocha', ()=> { 20 | it('should work',()=>{ 21 | expect(true).to.be.true 22 | }) 23 | }) 24 | 25 | const testResult = await runTests(); 26 | 27 | export default testResult 28 | ` 29 | 30 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 31 | 32 | console.log(JSON.stringify(result, null, 2)) 33 | -------------------------------------------------------------------------------- /example/server/README.md: -------------------------------------------------------------------------------- 1 | ## Server Example 2 | 3 | The server example demonstrates how you can use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs) inside a web server. In this example, we run a web server that spawns workers on request using the [poolifier-web-worker](https://github.com/poolifier/poolifier-web-worker) package. Each worker runs its own QuickJS sandbox and executes the given code. 4 | 5 | You can test it out by running: 6 | 7 | ```sh 8 | bun run example:server 9 | ``` 10 | 11 | from the root of this repository. Once the server has started, open your browser and go to [http://localhost:3000/](http://localhost:3000/). You will see a simple OpenAPI (Swagger) UI. -------------------------------------------------------------------------------- /example/server/openapi.ts: -------------------------------------------------------------------------------- 1 | import { createRoute, z } from '@hono/zod-openapi' 2 | 3 | export const executeRoute = createRoute({ 4 | method: 'post', 5 | path: '/execute', 6 | request: { 7 | body: { 8 | content: { 9 | 'text/plain': { 10 | schema: z.string(), 11 | example: `import * as path from 'path' 12 | 13 | const fn = async ()=>{ 14 | console.log(path.join('src','dist')) 15 | 16 | const url = new URL('https://example.com') 17 | 18 | const f = await fetch(url) 19 | 20 | return f.text() 21 | } 22 | 23 | export default await fn() 24 | `, 25 | }, 26 | }, 27 | description: `The javascript code to execute. QuickJS supports ES2023 with async await pattern and top-level await. The javascript has access to the fetch function and to the path module node:path. The result must be exported with __export default__. 28 | Async functions must be exported with __export default await asyncFunction()__ 29 | 30 | `, 31 | }, 32 | }, 33 | responses: { 34 | 200: { 35 | content: { 36 | 'application/json': { 37 | schema: z.object({ 38 | ok: z.boolean(), 39 | isSyntaxError: z.boolean().optional(), 40 | error: z.any().optional(), 41 | data: z.any().optional(), 42 | }), 43 | example: { 44 | ok: true, 45 | data: 'the execution result', 46 | }, 47 | }, 48 | }, 49 | description: 'The result of the code execution', 50 | }, 51 | 400: { 52 | content: { 53 | 'application/json': { 54 | schema: z.object({ 55 | ok: z.boolean(), 56 | isSyntaxError: z.boolean().optional(), 57 | error: z.any().optional(), 58 | data: z.any().optional(), 59 | }), 60 | example: { 61 | ok: false, 62 | error: { 63 | name: 'SyntaxError', 64 | message: 'unexpected end of string', 65 | stack: ' at index.js:7:26\n', 66 | }, 67 | isSyntaxError: true, 68 | }, 69 | }, 70 | }, 71 | description: 'Returns how many FAQ pairs are inserted', 72 | }, 73 | 408: { 74 | content: { 75 | 'application/json': { 76 | schema: z.object({ 77 | ok: z.boolean(), 78 | isSyntaxError: z.boolean().optional(), 79 | error: z.any().optional(), 80 | data: z.any().optional(), 81 | }), 82 | example: { 83 | ok: false, 84 | error: { 85 | name: 'ExecutionTimeout', 86 | message: 'The script execution has exceeded the maximum allowed time limit.', 87 | }, 88 | isSyntaxError: true, 89 | }, 90 | }, 91 | }, 92 | description: 'The execution exceeded the maximum time limit', 93 | }, 94 | 429: { 95 | content: { 96 | 'text/plain': { 97 | schema: z.string(), 98 | example: 'stack size exceeded', 99 | }, 100 | }, 101 | description: 'Reject the request if there is no capacity left to execute code', 102 | }, 103 | 500: { 104 | content: { 105 | 'text/plain': { 106 | schema: z.string(), 107 | example: 'Internal server error', 108 | }, 109 | }, 110 | description: 'Returns some error on general failure', 111 | }, 112 | }, 113 | }) 114 | -------------------------------------------------------------------------------- /example/server/server.ts: -------------------------------------------------------------------------------- 1 | import { swaggerUI } from '@hono/swagger-ui' 2 | import { OpenAPIHono } from '@hono/zod-openapi' 3 | import type { StatusCode } from 'hono/utils/http-status' 4 | import { DynamicThreadPool, PoolEvents, availableParallelism } from 'poolifier-web-worker' 5 | import { executeRoute } from './openapi.js' 6 | import type { InputData, ResponseData } from './types.js' 7 | 8 | const workerFileURL = new URL('./worker.ts', import.meta.url) as any 9 | 10 | const dynamicPool = new DynamicThreadPool(0, availableParallelism(), workerFileURL, { 11 | errorEventHandler: console.error, 12 | enableTasksQueue: false, 13 | workerOptions: { type: 'module', smol: true }, 14 | }) 15 | 16 | dynamicPool.eventTarget?.addEventListener(PoolEvents.full, () => console.warn('Pool is full')) 17 | dynamicPool.eventTarget?.addEventListener(PoolEvents.ready, () => console.info('Pool is ready')) 18 | dynamicPool.eventTarget?.addEventListener(PoolEvents.busy, () => console.warn('Pool is busy')) 19 | dynamicPool.eventTarget?.addEventListener(PoolEvents.error, () => console.error('Pool error')) 20 | 21 | let count = 0 22 | 23 | const app = new OpenAPIHono() 24 | 25 | app.openapi(executeRoute, async c => { 26 | const content = await c.req.text() 27 | const id = `id_${count++}` 28 | 29 | if (dynamicPool.info.executingTasks + 1 > dynamicPool.info.maxSize) { 30 | return c.text('Too Many Requests', 429) 31 | } 32 | try { 33 | const res = await dynamicPool.execute({ 34 | id, 35 | content, 36 | }) 37 | 38 | let status: StatusCode = 200 39 | if (res?.result.ok === false) { 40 | if (res.result.isSyntaxError) { 41 | status = 400 42 | } 43 | if (res.result.error.name === 'ExecutionTimeout') { 44 | status = 408 45 | } 46 | } 47 | 48 | return c.json(res?.result, status) 49 | } catch (error) { 50 | const err = error as Error 51 | if (err.message.includes('stack size exceeded')) { 52 | console.error('POOL_ERROR', 'Too Many Requests') 53 | return c.text(err.message, 429) 54 | } 55 | 56 | console.error('POOL_ERROR', err) 57 | return c.text(err.message, 500) 58 | } 59 | }) 60 | 61 | app.get('/', swaggerUI({ url: '/doc' })) 62 | app.doc('/doc', { 63 | info: { 64 | title: 'QuickJS playground', 65 | version: 'v1', 66 | }, 67 | openapi: '3.1.0', 68 | }) 69 | 70 | export default app 71 | -------------------------------------------------------------------------------- /example/server/types.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorResponse } from '../../src/types/ErrorResponse.js' 2 | import type { OkResponse } from '../../src/types/OkResponse.js' 3 | 4 | export interface InputData { 5 | id: string 6 | content: string 7 | } 8 | 9 | export interface ResponseData { 10 | id: string 11 | result: OkResponse | ErrorResponse 12 | } 13 | -------------------------------------------------------------------------------- /example/server/worker.ts: -------------------------------------------------------------------------------- 1 | import { ThreadWorker } from 'poolifier-web-worker' 2 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 3 | import type { InputData, ResponseData } from './types.js' 4 | 5 | class MyThreadWorker extends ThreadWorker { 6 | runtime?: Awaited> 7 | 8 | constructor() { 9 | super(async (data?: InputData) => await this.process(data), { 10 | maxInactiveTime: 10_000, 11 | killBehavior: 'HARD', 12 | killHandler: () => { 13 | this.runtime = undefined 14 | }, 15 | }) 16 | } 17 | 18 | private async process(data?: InputData): Promise { 19 | if (!data?.content) { 20 | return { id: '', result: { ok: true, data: { ok: true } } } 21 | } 22 | if (!this.runtime) { 23 | this.runtime = await loadQuickJs() 24 | } 25 | 26 | const options: SandboxOptions = { 27 | executionTimeout: 10, 28 | allowFs: true, 29 | allowFetch: true, 30 | enableTestUtils: true, 31 | env: {}, 32 | transformTypescript: true, 33 | mountFs: { 34 | src: { 35 | 'test.ts': `export const testFn = (value: string): string => { 36 | console.log(value) 37 | return value 38 | }`, 39 | }, 40 | }, 41 | } 42 | 43 | return await this.runtime.runSandboxed(async sandbox => { 44 | const result = await sandbox.evalCode(data.content) 45 | 46 | if (!result.ok && result.error.name === 'ExecutionTimeout') { 47 | this.runtime = undefined 48 | } 49 | 50 | return { id: data.id, result } 51 | }, options) 52 | } 53 | } 54 | 55 | export default new MyThreadWorker() 56 | -------------------------------------------------------------------------------- /example/timer/README.md: -------------------------------------------------------------------------------- 1 | # Timer Example 2 | 3 | This is an example of how to use `setTimeout` and `setInterval`in [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs). You can test it out by running: 4 | 5 | ```sh 6 | bun run example:timer 7 | ``` 8 | 9 | from the root of this repository. 10 | -------------------------------------------------------------------------------- /example/timer/index.ts: -------------------------------------------------------------------------------- 1 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 2 | 3 | // General setup like loading and init of the QuickJS wasm 4 | // It is a ressource intensive job and should be done only once if possible 5 | const { runSandboxed } = await loadQuickJs() 6 | 7 | const options: SandboxOptions = { 8 | allowFetch: true, // inject fetch and allow the code to fetch data 9 | allowFs: true, // mount a virtual file system and provide node:fs module 10 | env: { 11 | MY_ENV_VAR: 'env var value', 12 | }, 13 | } 14 | 15 | const code = ` 16 | 17 | const t = 5_000 18 | 19 | setInterval(()=> console.log('interval') , 500) 20 | 21 | setTimeout(() => { 22 | // never executed 23 | console.log('timeout') 24 | }, t*2) 25 | 26 | export default await new Promise((resolve) => setTimeout(() => resolve('ok'), t)) 27 | ` 28 | 29 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 30 | 31 | console.log(result) 32 | 33 | await new Promise(() => {}) 34 | -------------------------------------------------------------------------------- /example/typescript/README.md: -------------------------------------------------------------------------------- 1 | # Typescript Example 2 | 3 | This is a basic example of how to use [@sebastianwessel/quickjs](https://github.com/sebastianwessel/quickjs) with typescript code. You can test it out by running: 4 | 5 | ```sh 6 | bun run example:typescript 7 | ``` 8 | 9 | from the root of this repository. 10 | -------------------------------------------------------------------------------- /example/typescript/index.ts: -------------------------------------------------------------------------------- 1 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 2 | 3 | // General setup like loading and init of the QuickJS wasm 4 | // It is a ressource intensive job and should be done only once if possible 5 | const { runSandboxed } = await loadQuickJs() 6 | 7 | const options: SandboxOptions = { 8 | // enable typescript transpile 9 | // typescript must be installed as peer dependency in the project 10 | transformTypescript: true, 11 | mountFs: { 12 | src: { 13 | 'custom.ts': 'export const custom = (input:string):string => input', 14 | }, 15 | }, 16 | } 17 | 18 | const code = ` 19 | import { join } from 'path' 20 | import { custom } from './custom.js' 21 | 22 | const example = (input: string):string => input 23 | 24 | export default { fn: example('Hello World'), custom: custom('Custom string') } 25 | ` 26 | 27 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 28 | 29 | console.log(result) 30 | -------------------------------------------------------------------------------- /example/user-code/README.md: -------------------------------------------------------------------------------- 1 | # Execute User Code 2 | 3 | This example demonstrates how user code can be executed with QuickJS. You can test it out by running: 4 | 5 | ```sh 6 | bun run example:user 7 | ``` 8 | 9 | from the root of this repository. 10 | -------------------------------------------------------------------------------- /example/user-code/index.ts: -------------------------------------------------------------------------------- 1 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 2 | 3 | const userGeneratedCode = ` 4 | 5 | console.log('getLastAlert guest', env.getLastAlert()) 6 | env.setLastAlert(new Date()) 7 | console.log('getLastAlert guest', env.getLastAlert()) 8 | 9 | return true` 10 | 11 | const logFileContent = `{"message":"some log message","errorCode":0,"dateTime":"2025-02-26T07:35:10Z"} 12 | {"message":"an error message","errorCode":1,"dateTime":"2025-02-26T07:40:00Z"}` 13 | 14 | let memory: Date = new Date(0) 15 | 16 | const options: SandboxOptions = { 17 | allowFetch: false, 18 | allowFs: true, 19 | transformTypescript: true, 20 | mountFs: { 21 | 'log.jsonl': logFileContent, 22 | src: { 23 | 'types.ts': `export type LogRow = { 24 | message: string 25 | errorCode: number 26 | dateTime: string 27 | } 28 | 29 | export type AlertDecisionFn = ( input: LogRow[] ) => boolean`, 30 | 'custom.ts': `import type { AlertDecisionFn } from './types.js' 31 | 32 | export const shouldAlert: AlertDecisionFn = (input) => { 33 | ${userGeneratedCode} 34 | }`, 35 | }, 36 | }, 37 | env: { 38 | setLastAlert: (input: Date) => { 39 | memory = input 40 | }, 41 | getLastAlert: () => { 42 | return memory 43 | }, 44 | }, 45 | } 46 | 47 | const { runSandboxed } = await loadQuickJs() 48 | 49 | // fixed code (guest: src/index.ts) 50 | const executionCode = `import { readFileSync } from 'node:fs' 51 | 52 | import { shouldAlert } from './custom.js' 53 | import type { LogRow } from './types.js' 54 | 55 | const main = () => { 56 | 57 | const logFileContent = readFileSync('log.jsonl', 'utf-8') 58 | const logs: LogRow[] = logFileContent.split('\\n').map(line => JSON.parse(line)) 59 | 60 | return shouldAlert(logs) 61 | } 62 | 63 | export default main() 64 | ` 65 | 66 | const resultSandbox = await runSandboxed(async ({ evalCode }) => { 67 | return await evalCode(executionCode) 68 | }, options) 69 | 70 | console.log(resultSandbox) 71 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://jsr.io/schema/config-file.v1.json", 3 | "name": "@sebastianwessel/quickjs", 4 | "version": "2.2.0", 5 | "description": "A TypeScript package to execute JavaScript and TypeScript code in a WebAssembly QuickJS sandbox", 6 | "keywords": ["quickjs", "sandbox", "typescript", "javascript", "webassembly"], 7 | "exports": "./dist/esm/index.js", 8 | "publish": { 9 | "include": ["dist/**/*.js", "dist/**/*.d.ts", "README.md", "package.json"], 10 | "exclude": [ 11 | "src", 12 | ".github", 13 | ".vscode", 14 | ".zed", 15 | "!dist", 16 | "!dist/**/*.js", 17 | "!dist/**/*.d.ts", 18 | ".tshy", 19 | "vendor", 20 | "docs" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": ["src/index.{js,ts}"], 3 | "project": ["**/*.{js,ts}", "!example/**", "!docs/**", "!**/*.test.{js,ts}", "!*.json", "!loadtest.ts", "!vendor/**"] 4 | } 5 | -------------------------------------------------------------------------------- /loadtest.ts: -------------------------------------------------------------------------------- 1 | import autocannon from 'autocannon' 2 | 3 | const promiseTestBody = `const fn = async ()=> { 4 | const x = await new Promise((resolve,reject)=>{ 5 | resolve('resolve') 6 | }) 7 | return x 8 | } 9 | 10 | export default await fn()` 11 | 12 | /* 13 | const fetchTestBody = `import * as path from 'node:path' 14 | import { writeFileSync, readFileSync } from 'node:fs' 15 | 16 | const fn = async ()=>{ 17 | // console.log(path.join('src','dist')) 18 | 19 | const url = new URL('https://example.com') 20 | 21 | const f = await fetch(url) 22 | 23 | const text = await f.text() 24 | 25 | writeFileSync('/test.html', 'stored: ' + text) 26 | 27 | const result = readFileSync('/test.html') 28 | 29 | 30 | return result 31 | } 32 | 33 | export default await fn()` 34 | */ 35 | 36 | autocannon( 37 | { 38 | url: 'http://127.0.0.1:3000/execute', 39 | duration: 60, 40 | method: 'POST', 41 | body: promiseTestBody, 42 | }, 43 | (_err, result) => { 44 | for (const [key, value] of Object.entries(result)) { 45 | if (Array.isArray(value)) { 46 | console.table(value) 47 | } else { 48 | console.log(key, value) 49 | } 50 | } 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/src/.DS_Store -------------------------------------------------------------------------------- /src/adapter/fetch.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock } from 'bun:test' 2 | import { type GetFetchAdapterOptions, getDefaultFetchAdapter } from './fetch.js' 3 | 4 | describe('core - fetch adapter', () => { 5 | it('should block disallowed hosts', async () => { 6 | const adapterOptions: GetFetchAdapterOptions = { 7 | disallowedHosts: ['example.com'], 8 | } 9 | const fetchAdapter = getDefaultFetchAdapter(adapterOptions) 10 | 11 | const response = await fetchAdapter('http://example.com') 12 | 13 | expect(response.status).toBe(403) 14 | expect(response.statusText).toBe('FORBIDDEN') 15 | }) 16 | 17 | it('should allow allowed hosts', async () => { 18 | const adapterOptions: GetFetchAdapterOptions = { 19 | allowedHosts: ['example.com'], 20 | } 21 | const fetchAdapter = getDefaultFetchAdapter(adapterOptions) 22 | 23 | const response = await fetchAdapter('http://example.com') 24 | 25 | expect(response.status).not.toBe(403) 26 | }) 27 | 28 | it('should respect timeout', async () => { 29 | const adapterOptions: GetFetchAdapterOptions = { 30 | timeout: 1000, 31 | } 32 | const fetchAdapter = getDefaultFetchAdapter(adapterOptions) 33 | 34 | try { 35 | await fetchAdapter('http://example.com', { signal: AbortSignal.timeout(50) }) 36 | } catch (error) { 37 | expect((error as Error).name).toBe('AbortError') 38 | } 39 | }) 40 | 41 | it('should apply rate limiting', async () => { 42 | const adapterOptions: GetFetchAdapterOptions = { 43 | rateLimitPoints: 1, 44 | rateLimitDuration: 1, 45 | } 46 | const fetchAdapter = getDefaultFetchAdapter(adapterOptions) 47 | 48 | const response1 = await fetchAdapter('http://example.com') 49 | expect(response1.status).not.toBe(429) 50 | 51 | const response2 = await fetchAdapter('http://example.com') 52 | expect(response2.status).toBe(429) 53 | expect(response2.statusText).toBe('TOO MANY REQUESTS') 54 | }) 55 | 56 | it('should not enforce CORS policy by default', async () => { 57 | const adapterOptions: GetFetchAdapterOptions = {} 58 | const fetchAdapter = getDefaultFetchAdapter(adapterOptions) 59 | 60 | // Mocking fetch to return a response without CORS headers 61 | global.fetch = Object.assign(mock().mockResolvedValue(new Response('', { status: 200, statusText: 'OK' })), { 62 | preconnect: async () => {}, 63 | }) 64 | 65 | const response = await fetchAdapter('http://example.com') 66 | expect(response.status).toBe(200) 67 | expect(response.statusText).toBe('OK') 68 | }) 69 | 70 | it('should enforce CORS policy if enabled', async () => { 71 | const adapterOptions: GetFetchAdapterOptions = { 72 | corsCheck: true, 73 | } 74 | const fetchAdapter = getDefaultFetchAdapter(adapterOptions) 75 | 76 | // Mocking fetch to return a response without CORS headers 77 | global.fetch = Object.assign(mock().mockResolvedValue(new Response('', { status: 200, statusText: 'OK' })), { 78 | preconnect: async () => {}, 79 | }) 80 | 81 | const response = await fetchAdapter('http://example.com') 82 | expect(response.status).toBe(403) 83 | expect(response.statusText).toBe('FORBIDDEN') 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /src/createTimeInterval.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilizes the standard setInterval to work properly with javascript `using` statement. 3 | * Because of this, the interval gets automatically cleared on dispose. 4 | */ 5 | export const createTimeInterval = (...params: Parameters) => { 6 | const interval: { 7 | id: ReturnType | undefined 8 | [Symbol.dispose]: () => void 9 | clear: () => void 10 | } = { 11 | id: setInterval(...params), 12 | clear: function () { 13 | if (this.id) { 14 | clearInterval(this.id) 15 | } 16 | this.id = undefined 17 | }, 18 | [Symbol.dispose]: function () { 19 | if (this.id) { 20 | clearInterval(this.id) 21 | } 22 | this.id = undefined 23 | }, 24 | } 25 | return interval 26 | } 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * QuickJS sandbox for JavaScript/Typescript applications 3 | * 4 | * This TypeScript package allows you to safely execute JavaScript code within a WebAssembly sandbox using the QuickJS engine. 5 | * Perfect for isolating and running untrusted code securely, it leverages the lightweight and fast QuickJS engine compiled to WebAssembly, providing a robust environment for code execution. 6 | * 7 | * @author Sebastian Wessel 8 | * @copyright Sebastian Wessel 9 | * @license MIT 10 | * @link https://sebastianwessel.github.io/quickjs/ 11 | * @link https://github.com/sebastianwessel/quickjs 12 | * 13 | * @example 14 | * ```typescript 15 | * import { type SandboxOptions, loadQuickJs } from '@sebastianwessel/quickjs' 16 | * 17 | * // General setup like loading and init of the QuickJS wasm 18 | * // It is a ressource intensive job and should be done only once if possible 19 | * const { runSandboxed } = await loadQuickJs() 20 | * 21 | * const options: SandboxOptions = { 22 | * allowFetch: true, // inject fetch and allow the code to fetch data 23 | * allowFs: true, // mount a virtual file system and provide node:fs module 24 | * env: { 25 | * MY_ENV_VAR: 'env var value', 26 | * }, 27 | * } 28 | * 29 | * const code = ` 30 | * import { join } from 'path' 31 | * 32 | * const fn = async ()=>{ 33 | * console.log(join('src','dist')) // logs "src/dist" on host system 34 | * 35 | * console.log(env.MY_ENV_VAR) // logs "env var value" on host system 36 | * 37 | * const url = new URL('https://example.com') 38 | * 39 | * const f = await fetch(url) 40 | * 41 | * return f.text() 42 | * } 43 | * 44 | * export default await fn() 45 | * ` 46 | * 47 | * const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 48 | * 49 | * console.log(result) // { ok: true, data: '\n\n[....]\n' } 50 | * ``` 51 | * 52 | * 53 | * @module 54 | */ 55 | 56 | export * from './loadQuickJs.js' 57 | export * from './loadAsyncQuickJs.js' 58 | 59 | export * from './adapter/fetch.js' 60 | 61 | export * from './createVirtualFileSystem.js' 62 | export * from './getTypescriptSupport.js' 63 | 64 | export * from './types/CodeFunctionInput.js' 65 | export * from './types/ErrorResponse.js' 66 | export * from './types/LoadQuickJsOptions.js' 67 | export * from './types/OkResponse.js' 68 | export * from './types/OkResponseCheck.js' 69 | export * from './types/Prettify.js' 70 | export * from './types/RuntimeOptions.js' 71 | export * from './types/SandboxEvalCode.js' 72 | export * from './types/SandboxFunction.js' 73 | export * from './types/SandboxOptions.js' 74 | export * from './types/SandboxValidateCode.js' 75 | export * from './types/Serializer.js' 76 | 77 | export * from './sandbox/handleToNative/serializer/index.js' 78 | export * from './sandbox/handleToNative/handleToNative.js' 79 | export * from './sandbox/expose/expose.js' 80 | 81 | export * from './sandbox/syncVersion/getModuleLoader.js' 82 | export * from './sandbox/syncVersion/modulePathNormalizer.js' 83 | 84 | export * from './sandbox/asyncVersion/getAsyncModuleLoader.js' 85 | export * from './sandbox/asyncVersion/modulePathNormalizerAsync.js' 86 | -------------------------------------------------------------------------------- /src/loadAsyncQuickJs.ts: -------------------------------------------------------------------------------- 1 | import { Scope, shouldInterruptAfterDeadline } from 'quickjs-emscripten-core' 2 | import { getTypescriptSupport } from './getTypescriptSupport.js' 3 | import { getAsyncModuleLoader } from './sandbox/asyncVersion/getAsyncModuleLoader.js' 4 | import { modulePathNormalizerAsync } from './sandbox/asyncVersion/modulePathNormalizerAsync.js' 5 | 6 | import { executeAsyncSandboxFunction } from './sandbox/asyncVersion/executeAsyncSandboxFunction.js' 7 | import { prepareAsyncNodeCompatibility } from './sandbox/asyncVersion/prepareAsyncNodeCompatibility.js' 8 | import { prepareAsyncSandbox } from './sandbox/asyncVersion/prepareAsyncSandbox.js' 9 | import { loadAsyncWasmModule } from './sandbox/loadAsyncWasmModule.js' 10 | import { setupFileSystem } from './sandbox/setupFileSystem.js' 11 | import type { LoadAsyncQuickJsOptions } from './types/LoadQuickJsOptions.js' 12 | 13 | import type { AsyncSandboxFunction } from './types/SandboxFunction.js' 14 | import type { SandboxAsyncOptions } from './types/SandboxOptions.js' 15 | 16 | /** 17 | * Loads the QuickJS async module and returns a sandbox execution function. 18 | * @param loadOptions - Options for loading the QuickJS module. Defaults to '@jitl/quickjs-ng-wasmfile-release-asyncify'. 19 | * @returns An object containing the runSandboxed function and the loaded module. 20 | */ 21 | export const loadAsyncQuickJs = async ( 22 | loadOptions: LoadAsyncQuickJsOptions = '@jitl/quickjs-ng-wasmfile-release-asyncify', 23 | ) => { 24 | const module = await loadAsyncWasmModule(loadOptions) 25 | 26 | /** 27 | * Provides a new sandbox and executes the given function. 28 | * When the function has been finished, the sandbox gets disposed and can longer be used. 29 | * 30 | * @param sandboxedFunction 31 | * @param sandboxOptions 32 | * @returns 33 | */ 34 | const runSandboxed = async ( 35 | sandboxedFunction: AsyncSandboxFunction, 36 | sandboxOptions: SandboxAsyncOptions = {}, 37 | ): Promise => { 38 | const scope = new Scope() 39 | 40 | const ctx = scope.manage(module.newContext()) 41 | 42 | if (sandboxOptions.executionTimeout) { 43 | ctx.runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + sandboxOptions.executionTimeout)) 44 | } 45 | 46 | if (sandboxOptions.maxStackSize) { 47 | ctx.runtime.setMaxStackSize(sandboxOptions.maxStackSize) 48 | } 49 | 50 | if (sandboxOptions.memoryLimit) { 51 | ctx.runtime.setMemoryLimit(sandboxOptions.memoryLimit) 52 | } 53 | 54 | // Virtual File System, 55 | const fs = setupFileSystem(sandboxOptions) 56 | 57 | // TypeScript Support: 58 | const { transpileVirtualFs, transpileFile } = await getTypescriptSupport( 59 | sandboxOptions.transformTypescript, 60 | sandboxOptions.typescriptImportFile, 61 | sandboxOptions.transformCompilerOptions, 62 | ) 63 | // if typescript support is enabled, transpile all ts files in file system 64 | transpileVirtualFs(fs) 65 | 66 | // JS Module Loader 67 | const moduleLoader = sandboxOptions.getModuleLoader 68 | ? sandboxOptions.getModuleLoader(fs, sandboxOptions) 69 | : getAsyncModuleLoader(fs, sandboxOptions) 70 | 71 | ctx.runtime.setModuleLoader(moduleLoader, sandboxOptions.modulePathNormalizer ?? modulePathNormalizerAsync) 72 | 73 | // Register Globals to be more Node.js compatible 74 | await prepareAsyncNodeCompatibility(ctx, sandboxOptions) 75 | 76 | // Prepare the Sandbox 77 | // Expose Data and Functions to Client 78 | prepareAsyncSandbox(ctx, scope, sandboxOptions, fs) 79 | 80 | // Run the given Function 81 | const result = await executeAsyncSandboxFunction({ 82 | ctx, 83 | fs, 84 | scope, 85 | sandboxOptions, 86 | sandboxedFunction, 87 | transpileFile, 88 | }) 89 | 90 | scope.dispose() 91 | return result 92 | } 93 | 94 | return { runSandboxed, module } 95 | } 96 | -------------------------------------------------------------------------------- /src/loadQuickJs.ts: -------------------------------------------------------------------------------- 1 | import { Scope, shouldInterruptAfterDeadline } from 'quickjs-emscripten-core' 2 | import { getTypescriptSupport } from './getTypescriptSupport.js' 3 | import { getModuleLoader } from './sandbox/syncVersion/getModuleLoader.js' 4 | import { modulePathNormalizer } from './sandbox/syncVersion/modulePathNormalizer.js' 5 | 6 | import { setupFileSystem } from './sandbox/setupFileSystem.js' 7 | import { executeSandboxFunction } from './sandbox/syncVersion/executeSandboxFunction.js' 8 | import { prepareNodeCompatibility } from './sandbox/syncVersion/prepareNodeCompatibility.js' 9 | import { prepareSandbox } from './sandbox/syncVersion/prepareSandbox.js' 10 | import type { LoadQuickJsOptions } from './types/LoadQuickJsOptions.js' 11 | 12 | import { loadWasmModule } from './sandbox/syncVersion/loadWasmModule.js' 13 | import type { SandboxFunction } from './types/SandboxFunction.js' 14 | import type { SandboxOptions } from './types/SandboxOptions.js' 15 | 16 | /** 17 | * Loads the QuickJS module and returns a sandbox execution function. 18 | * @param loadOptions - Options for loading the QuickJS module. Defaults to '@jitl/quickjs-ng-wasmfile-release-sync'. 19 | * @returns An object containing the runSandboxed function and the loaded module. 20 | */ 21 | export const loadQuickJs = async (loadOptions: LoadQuickJsOptions = '@jitl/quickjs-ng-wasmfile-release-sync') => { 22 | const module = await loadWasmModule(loadOptions) 23 | 24 | /** 25 | * Provides a new sandbox and executes the given function. 26 | * When the function has been finished, the sandbox gets disposed and can longer be used. 27 | * 28 | * @param sandboxedFunction 29 | * @param sandboxOptions 30 | * @returns 31 | */ 32 | const runSandboxed = async ( 33 | sandboxedFunction: SandboxFunction, 34 | sandboxOptions: SandboxOptions = {}, 35 | ): Promise => { 36 | try { 37 | const scope = new Scope() 38 | 39 | const ctx = scope.manage(module.newContext()) 40 | 41 | if (sandboxOptions.executionTimeout) { 42 | ctx.runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + sandboxOptions.executionTimeout)) 43 | } 44 | 45 | if (sandboxOptions.maxStackSize) { 46 | ctx.runtime.setMaxStackSize(sandboxOptions.maxStackSize) 47 | } 48 | 49 | if (sandboxOptions.memoryLimit) { 50 | ctx.runtime.setMemoryLimit(sandboxOptions.memoryLimit) 51 | } 52 | 53 | // Virtual File System, 54 | const fs = setupFileSystem(sandboxOptions) 55 | 56 | // TypeScript Support: 57 | const { transpileVirtualFs, transpileFile } = await getTypescriptSupport( 58 | sandboxOptions.transformTypescript, 59 | sandboxOptions.typescriptImportFile, 60 | sandboxOptions.transformCompilerOptions, 61 | ) 62 | // if typescript support is enabled, transpile all ts files in file system 63 | transpileVirtualFs(fs) 64 | 65 | // JS Module Loader 66 | const moduleLoader = sandboxOptions.getModuleLoader 67 | ? sandboxOptions.getModuleLoader(fs, sandboxOptions) 68 | : getModuleLoader(fs, sandboxOptions) 69 | ctx.runtime.setModuleLoader(moduleLoader, sandboxOptions.modulePathNormalizer ?? modulePathNormalizer) 70 | 71 | // Register Globals to be more Node.js compatible 72 | prepareNodeCompatibility(ctx, sandboxOptions) 73 | 74 | // Prepare the Sandbox 75 | // Expose Data and Functions to Client 76 | prepareSandbox(ctx, scope, sandboxOptions, fs) 77 | 78 | // Run the given Function 79 | const result = await executeSandboxFunction({ 80 | ctx, 81 | fs, 82 | scope, 83 | sandboxOptions, 84 | sandboxedFunction, 85 | transpileFile, 86 | }) 87 | 88 | scope.dispose() 89 | return result 90 | } catch (error) { 91 | throw error instanceof Error ? error : new Error('Internal Error') 92 | } 93 | } 94 | 95 | return { runSandboxed, module } 96 | } 97 | -------------------------------------------------------------------------------- /src/modules/assert.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | const assert = { 3 | fail(actual, expected, message, operator, stackStartFunction) { 4 | throw new Error(message || \`Expected \${actual} \${operator} \${expected}\`); 5 | }, 6 | ok(value, message) { 7 | if (!value) throw new Error(message || 'Assertion failed'); 8 | }, 9 | equal(actual, expected, message) { 10 | if (actual != expected) throw new Error(message || \`Expected \${actual} to equal \${expected}\`); 11 | }, 12 | notEqual(actual, expected, message) { 13 | if (actual == expected) throw new Error(message || \`Expected \${actual} to not equal \${expected}\`); 14 | }, 15 | deepEqual(actual, expected, message) { 16 | if (!isDeepEqual(actual, expected)) throw new Error(message || \`Expected \${actual} to deeply equal \${expected}\`); 17 | }, 18 | notDeepEqual(actual, expected, message) { 19 | if (isDeepEqual(actual, expected)) throw new Error(message || \`Expected \${actual} to not deeply equal \${expected}\`); 20 | }, 21 | strictEqual(actual, expected, message) { 22 | if (actual !== expected) throw new Error(message || \`Expected \${actual} to strictly equal \${expected}\`); 23 | }, 24 | notStrictEqual(actual, expected, message) { 25 | if (actual === expected) throw new Error(message || \`Expected \${actual} to not strictly equal \${expected}\`); 26 | } 27 | }; 28 | 29 | function isDeepEqual(a, b) { 30 | if (a === b) return true; 31 | 32 | if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false; 33 | 34 | const keysA = Object.keys(a); 35 | const keysB = Object.keys(b); 36 | 37 | if (keysA.length !== keysB.length) return false; 38 | 39 | for (const key of keysA) { 40 | if (!keysB.includes(key) || !isDeepEqual(a[key], b[key])) return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | export default assert; 47 | ` 48 | -------------------------------------------------------------------------------- /src/modules/buffer.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | import { TextEncoder, TextDecoder } from 'node:util' 3 | 4 | class Buffer extends Uint8Array { 5 | constructor(arg, encoding) { 6 | if (typeof arg === 'number') { 7 | super(arg); 8 | } else if (typeof arg === 'string') { 9 | super(Buffer._stringToBytes(arg, encoding)); 10 | } else if (arg instanceof ArrayBuffer) { 11 | super(new Uint8Array(arg)); 12 | } else if (Array.isArray(arg) || arg instanceof Uint8Array) { 13 | super(arg); 14 | } else { 15 | throw new TypeError('Invalid argument type'); 16 | } 17 | } 18 | 19 | static from(arg, encoding) { 20 | if (typeof arg === 'string') { 21 | return new Buffer(Buffer._stringToBytes(arg, encoding)); 22 | } else if (Array.isArray(arg) || arg instanceof Uint8Array) { 23 | return new Buffer(arg); 24 | } else if (arg instanceof ArrayBuffer) { 25 | return new Buffer(new Uint8Array(arg)); 26 | } else { 27 | throw new TypeError('Invalid argument type'); 28 | } 29 | } 30 | 31 | static alloc(size) { 32 | return new Buffer(size); 33 | } 34 | 35 | static allocUnsafe(size) { 36 | return new Buffer(size); 37 | } 38 | 39 | static concat(buffers, totalLength) { 40 | if (!Array.isArray(buffers)) { 41 | throw new TypeError('Argument must be an array of Buffers'); 42 | } 43 | if (buffers.length === 0) { 44 | return Buffer.alloc(0); 45 | } 46 | if (totalLength === undefined) { 47 | totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0); 48 | } 49 | const result = Buffer.allocUnsafe(totalLength); 50 | let offset = 0; 51 | for (const buf of buffers) { 52 | result.set(buf, offset); 53 | offset += buf.length; 54 | } 55 | return result; 56 | } 57 | 58 | toString(encoding) { 59 | if (encoding === 'base64') { 60 | return Buffer._bytesToBase64(this); 61 | } else { 62 | return Buffer._bytesToString(this, encoding); 63 | } 64 | } 65 | 66 | static _stringToBytes(string, encoding) { 67 | if (encoding === 'base64') { 68 | return Buffer._base64ToBytes(string); 69 | } else { 70 | return new TextEncoder().encode(string); 71 | } 72 | } 73 | 74 | static _bytesToString(bytes, encoding) { 75 | if (encoding === 'utf-8' || encoding === undefined) { 76 | return new TextDecoder(encoding).decode(bytes); 77 | } else { 78 | throw new TypeError('Unsupported encoding'); 79 | } 80 | } 81 | 82 | static _base64ToBytes(base64) { 83 | const binaryString = atob(base64); 84 | const length = binaryString.length; 85 | const bytes = new Uint8Array(length); 86 | for (let i = 0; i < length; i++) { 87 | bytes[i] = binaryString.charCodeAt(i); 88 | } 89 | return bytes; 90 | } 91 | 92 | static _bytesToBase64(bytes) { 93 | let binary = ''; 94 | for (let i = 0; i < bytes.length; i++) { 95 | binary += String.fromCharCode(bytes[i]); 96 | } 97 | return btoa(binary); 98 | } 99 | } 100 | 101 | globalThis.Buffer = Buffer 102 | 103 | export { Buffer } 104 | 105 | export default Buffer; 106 | ` 107 | -------------------------------------------------------------------------------- /src/modules/events.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | /* esm.sh - eventemitter3@5.0.1 */ 3 | var E=Object.create;var d=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,A=Object.prototype.hasOwnProperty;var k=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports);var P=(n,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of O(e))!A.call(n,i)&&i!==t&&d(n,i,{get:()=>e[i],enumerable:!(s=L(e,i))||s.enumerable});return n};var N=(n,e,t)=>(t=n!=null?E(C(n)):{},P(e||!n||!n.__esModule?d(t,"default",{value:n,enumerable:!0}):t,n));var m=k((q,x)=>{"use strict";var S=Object.prototype.hasOwnProperty,l="~";function _(){}Object.create&&(_.prototype=Object.create(null),new _().__proto__||(l=!1));function T(n,e,t){this.fn=n,this.context=e,this.once=t||!1}function w(n,e,t,s,i){if(typeof t!="function")throw new TypeError("The listener must be a function");var u=new T(t,s||n,i),o=l?l+e:e;return n._events[o]?n._events[o].fn?n._events[o]=[n._events[o],u]:n._events[o].push(u):(n._events[o]=u,n._eventsCount++),n}function y(n,e){--n._eventsCount===0?n._events=new _:delete n._events[e]}function c(){this._events=new _,this._eventsCount=0}c.prototype.eventNames=function(){var e=[],t,s;if(this._eventsCount===0)return e;for(s in t=this._events)S.call(t,s)&&e.push(l?s.slice(1):s);return Object.getOwnPropertySymbols?e.concat(Object.getOwnPropertySymbols(t)):e};c.prototype.listeners=function(e){var t=l?l+e:e,s=this._events[t];if(!s)return[];if(s.fn)return[s.fn];for(var i=0,u=s.length,o=new Array(u);i { 8 | const parts = baseName.split('/') 9 | parts.pop() 10 | const parent = parts.join('/') 11 | 12 | return fileName => { 13 | console.log('custom require', fileName) 14 | const filePath = resolve('/',parent, fileName) 15 | return readFileSync(filePath) 16 | } 17 | } 18 | 19 | export const isBuiltin = moduleName => 20 | builtinModules.some(module => module.replace('node:', '') === moduleName.replace('node:', '')) 21 | 22 | export const register = () => { 23 | console.error('node:module method register is not available in sandbox') 24 | throw new Error('node:module method register is not available in sandbox') 25 | } 26 | 27 | export const syncBuiltinESMExports = () => { 28 | console.error('node:module method syncBuiltinESMExports is not available in sandbox') 29 | throw new Error('node:module method syncBuiltinESMExports is not available in sandbox') 30 | } 31 | 32 | export default { builtinModules, createRequire, isBuiltin, register, syncBuiltinESMExports } 33 | ` 34 | -------------------------------------------------------------------------------- /src/modules/nodeCompatibility/headers.js: -------------------------------------------------------------------------------- 1 | export default `class Headers { 2 | constructor(init) { 3 | this.map = {}; 4 | 5 | if (init instanceof Headers) { 6 | init.forEach((value, name) => { 7 | this.append(name, value); 8 | }); 9 | } else if (init) { 10 | Object.getOwnPropertyNames(init).forEach(name => { 11 | this.append(name, init[name]); 12 | }); 13 | } 14 | } 15 | 16 | append(name, value) { 17 | name = name.toLowerCase(); 18 | if (this.map[name]) { 19 | this.map[name].push(value); 20 | } else { 21 | this.map[name] = [value]; 22 | } 23 | } 24 | 25 | delete(name) { 26 | delete this.map[name.toLowerCase()]; 27 | } 28 | 29 | get(name) { 30 | name = name.toLowerCase(); 31 | return this.map[name] ? this.map[name][0] : null; 32 | } 33 | 34 | getAll(name) { 35 | return this.map[name.toLowerCase()] || []; 36 | } 37 | 38 | has(name) { 39 | return this.map.hasOwnProperty(name.toLowerCase()); 40 | } 41 | 42 | set(name, value) { 43 | this.map[name.toLowerCase()] = [value]; 44 | } 45 | 46 | forEach(callback, thisArg) { 47 | Object.getOwnPropertyNames(this.map).forEach(name => { 48 | this.map[name].forEach(value => { 49 | callback.call(thisArg, value, name, this); 50 | }); 51 | }); 52 | } 53 | } 54 | 55 | globalThis.Headers = Headers; 56 | ` 57 | -------------------------------------------------------------------------------- /src/modules/nodeCompatibility/request.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | export default {} 3 | ` 4 | -------------------------------------------------------------------------------- /src/modules/nodeCompatibility/response.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | export default {} 3 | ` 4 | -------------------------------------------------------------------------------- /src/modules/querystring.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | function e(e, n) { return Object.prototype.hasOwnProperty.call(e, n) } var n = function (n, r, t, o) { r = r || "&", t = t || "="; var a = {}; if ("string" != typeof n || 0 === n.length) { return a; } var u = /\+/g; n = n.split(r); var c = 1e3; o && "number" == typeof o.maxKeys && (c = o.maxKeys); var i = n.length; c > 0 && i > c && (i = c); for (var s = 0; s < i; ++s) { var p, f, d, y, m = n[s].replace(u, "%20"), l = m.indexOf(t); l >= 0 ? (p = m.substr(0, l), f = m.substr(l + 1)) : (p = m, f = ""), d = decodeURIComponent(p), y = decodeURIComponent(f), e(a, d) ? Array.isArray(a[d]) ? a[d].push(y) : a[d] = [a[d], y] : a[d] = y; } return a }, r = function (e) { switch (typeof e) { case "string": return e; case "boolean": return e ? "true" : "false"; case "number": return isFinite(e) ? e : ""; default: return "" } }, t = function (e, n, t, o) { return n = n || "&", t = t || "=", null === e && (e = void 0), "object" == typeof e ? Object.keys(e).map((function (o) { var a = encodeURIComponent(r(o)) + t; return Array.isArray(e[o]) ? e[o].map((function (e) { return a + encodeURIComponent(r(e)) })).join(n) : a + encodeURIComponent(r(e[o])) })).join(n) : o ? encodeURIComponent(r(o)) + t + encodeURIComponent(r(e)) : "" }, o = {}; o.decode = o.parse = n, o.encode = o.stringify = t; o.decode; o.encode; o.parse; o.stringify; 3 | 4 | o.decode; 5 | o.encode; 6 | o.parse; 7 | o.stringify; 8 | 9 | var decode = o.decode; 10 | var encode = o.encode; 11 | var parse = o.parse; 12 | var stringify = o.stringify; 13 | 14 | export { decode, o as default, encode, parse, stringify }; 15 | ` 16 | -------------------------------------------------------------------------------- /src/modules/timers.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | export setTimeout 3 | export clearTimeout 4 | export setInterval 5 | export clearInterval 6 | 7 | export default {setTimeout, clearTimeout, setInterval, clearInterval } 8 | ` 9 | -------------------------------------------------------------------------------- /src/modules/timers_promises.js: -------------------------------------------------------------------------------- 1 | export default '' 2 | -------------------------------------------------------------------------------- /src/modules/url.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | 3 | export class URL { 4 | constructor(url, base) { 5 | const parser = __parseURL(url,base) 6 | this.href = parser.href; 7 | this.protocol = parser.protocol; 8 | this.host = parser.host; 9 | this.hostname = parser.hostname; 10 | this.port = parser.port; 11 | this.pathname = parser.pathname; 12 | this.search = parser.search; 13 | this.searchParams = new URLSearchParams(parser.search) 14 | this.hash = parser.hash; 15 | this.username = parser.username; 16 | this.password = parser.password; 17 | this.origin = parser.protocol+'//'+parser.host; 18 | } 19 | 20 | toString() { 21 | return this.href; 22 | } 23 | 24 | toJSON() { 25 | return this.href; 26 | } 27 | } 28 | 29 | 30 | 31 | 32 | export class URLSearchParams { 33 | constructor(init = '') { 34 | this.params = {}; 35 | 36 | if (typeof init === 'string') { 37 | this._parseFromString(init); 38 | } else if (init instanceof URLSearchParams) { 39 | init.forEach((value, key) => this.append(key, value)); 40 | } else if (typeof init === 'object') { 41 | Object.keys(init).forEach(key => this.append(key, init[key])); 42 | } 43 | } 44 | 45 | _parseFromString(query) { 46 | if (query.startsWith('?')) { 47 | query = query.slice(1); 48 | } 49 | query.split('&').forEach(pair => { 50 | const [key, value] = pair.split('=').map(decodeURIComponent); 51 | this.append(key, value); 52 | }); 53 | } 54 | 55 | append(key, value) { 56 | if (!this.params[key]) { 57 | this.params[key] = []; 58 | } 59 | this.params[key].push(value); 60 | } 61 | 62 | delete(key) { 63 | delete this.params[key]; 64 | } 65 | 66 | get(key) { 67 | return this.params[key] ? this.params[key][0] : null; 68 | } 69 | 70 | getAll(key) { 71 | return this.params[key] || []; 72 | } 73 | 74 | has(key) { 75 | return this.params.hasOwnProperty(key); 76 | } 77 | 78 | set(key, value) { 79 | this.params[key] = [value]; 80 | } 81 | 82 | toString() { 83 | return Object.keys(this.params) 84 | .map(key => this.params[key] 85 | .map(value => encodeURIComponent(key)+'='+encodeURIComponent(value)) 86 | .join('&')) 87 | .join('&'); 88 | } 89 | 90 | forEach(callback, thisArg) { 91 | Object.keys(this.params).forEach(key => { 92 | this.params[key].forEach(value => { 93 | callback.call(thisArg, value, key, this); 94 | }); 95 | }); 96 | } 97 | } 98 | 99 | globalThis.URLSearchParams = URLSearchParams; 100 | globalThis.URL = URL; 101 | 102 | 103 | export default { URLSearchParams, URL } 104 | ` 105 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/createAsyncEvalCodeFunction.ts: -------------------------------------------------------------------------------- 1 | import type { Scope } from 'quickjs-emscripten-core' 2 | import { createTimeInterval } from '../../createTimeInterval.js' 3 | import type { CodeFunctionAsyncInput } from '../../types/CodeFunctionInput.js' 4 | import type { OkResponse } from '../../types/OkResponse.js' 5 | import type { SandboxEvalCode } from '../../types/SandboxEvalCode.js' 6 | import { getMaxIntervalAmount } from '../getMaxIntervalAmount.js' 7 | import { getMaxTimeoutAmount } from '../getMaxTimeoutAmount.js' 8 | import { handleEvalError } from '../handleEvalError.js' 9 | import { handleToNative } from '../handleToNative/handleToNative.js' 10 | import { provideTimingFunctions } from '../provide/provideTimingFunctions.js' 11 | 12 | export const createAsyncEvalCodeFunction = (input: CodeFunctionAsyncInput, scope: Scope): SandboxEvalCode => { 13 | const { ctx, sandboxOptions, transpileFile } = input 14 | return async (code, filename = '/src/index.js', evalOptions?) => { 15 | const eventLoopinterval = createTimeInterval(() => ctx.runtime.executePendingJobs(), 0) 16 | 17 | let timeoutId: ReturnType | undefined 18 | 19 | const { dispose: disposeTimer } = provideTimingFunctions(ctx, { 20 | maxTimeoutCount: getMaxTimeoutAmount(sandboxOptions), 21 | maxIntervalCount: getMaxIntervalAmount(sandboxOptions), 22 | }) 23 | 24 | const disposeStep = () => { 25 | if (timeoutId) { 26 | clearTimeout(timeoutId) 27 | } 28 | eventLoopinterval?.clear() 29 | disposeTimer() 30 | } 31 | 32 | try { 33 | const jsCode = transpileFile(code) 34 | const evalResult = await ctx.evalCodeAsync(jsCode, filename, { 35 | strict: true, 36 | strip: false, 37 | backtraceBarrier: true, 38 | ...evalOptions, 39 | type: 'module', 40 | }) 41 | 42 | const handle = scope.manage(ctx.unwrapResult(evalResult)) 43 | 44 | const native = handleToNative(ctx, handle, scope) 45 | 46 | const result = await Promise.race([ 47 | (async () => { 48 | const res = await native 49 | return res.default 50 | })(), 51 | new Promise((_resolve, reject) => { 52 | if (sandboxOptions.executionTimeout) { 53 | timeoutId = setTimeout(() => { 54 | const err = new Error('The script execution has exceeded the maximum allowed time limit.') 55 | err.name = 'ExecutionTimeout' 56 | reject(err) 57 | }, sandboxOptions.executionTimeout) 58 | } 59 | }), 60 | ]) 61 | 62 | return { ok: true, data: result } as OkResponse 63 | } catch (err) { 64 | return handleEvalError(err) 65 | } finally { 66 | disposeStep() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/createAsyncValidateCodeFunction.ts: -------------------------------------------------------------------------------- 1 | import type { CodeFunctionAsyncInput } from '../../types/CodeFunctionInput.js' 2 | import type { OkResponseCheck } from '../../types/OkResponseCheck.js' 3 | import type { SandboxValidateCode } from '../../types/SandboxValidateCode.js' 4 | import { handleEvalError } from '../handleEvalError.js' 5 | 6 | export const createAsyncValidateCodeFunction = (input: CodeFunctionAsyncInput): SandboxValidateCode => { 7 | const { ctx } = input 8 | return async (code, filename = '/src/index.js', evalOptions?) => { 9 | try { 10 | ctx 11 | .unwrapResult( 12 | await ctx.evalCodeAsync(code, filename, { 13 | strict: true, 14 | strip: true, 15 | backtraceBarrier: true, 16 | ...evalOptions, 17 | type: 'module', 18 | compileOnly: true, 19 | }), 20 | ) 21 | .dispose() 22 | return { ok: true } as OkResponseCheck 23 | } catch (err) { 24 | return handleEvalError(err) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/executeAsyncSandboxFunction.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { QuickJSAsyncContext, Scope } from 'quickjs-emscripten-core' 3 | import type { CodeFunctionAsyncInput } from '../../types/CodeFunctionInput.js' 4 | import type { AsyncSandboxFunction } from '../../types/SandboxFunction.js' 5 | import type { SandboxAsyncOptions } from '../../types/SandboxOptions.js' 6 | import { createAsyncEvalCodeFunction } from './createAsyncEvalCodeFunction.js' 7 | import { createAsyncValidateCodeFunction } from './createAsyncValidateCodeFunction.js' 8 | 9 | export const executeAsyncSandboxFunction = async (input: { 10 | ctx: QuickJSAsyncContext 11 | fs: IFs 12 | scope: Scope 13 | sandboxOptions: SandboxAsyncOptions 14 | sandboxedFunction: AsyncSandboxFunction 15 | transpileFile: (input: string) => string 16 | }) => { 17 | const { ctx, sandboxOptions, sandboxedFunction, fs, transpileFile } = input 18 | const opt: CodeFunctionAsyncInput = { ctx, sandboxOptions, transpileFile } 19 | const evalCode = createAsyncEvalCodeFunction(opt, input.scope) 20 | const validateCode = createAsyncValidateCodeFunction(opt) 21 | return await sandboxedFunction({ ctx, evalCode, validateCode, mountedFs: fs }) 22 | } 23 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/getAsyncModuleLoader.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { JSModuleLoaderAsync } from 'quickjs-emscripten-core' 3 | 4 | import { join } from 'node:path' 5 | 6 | import type { RuntimeOptions } from '../../types/RuntimeOptions.js' 7 | 8 | export const getAsyncModuleLoader = (fs: IFs, _runtimeOptions: RuntimeOptions) => { 9 | const moduleLoader: JSModuleLoaderAsync = (inputName, _context) => { 10 | let name = inputName 11 | 12 | // if it does not exist 13 | if (!fs.existsSync(name)) { 14 | // try to add the .js extension 15 | if (fs.existsSync(`${name}.js`)) { 16 | name = `${name}.js` 17 | } else { 18 | return { error: new Error(`Module '${inputName}' not installed or available`) } 19 | } 20 | } 21 | 22 | // if it is a folder, we need to use the index.js file 23 | if (fs.lstatSync(name).isDirectory()) { 24 | name = join(name, 'index.js') 25 | if (!fs.existsSync(name)) { 26 | return { error: new Error(`Module '${inputName}' not installed or available`) } 27 | } 28 | } 29 | 30 | // workaround: as we can not provide the real import.meta.url functionality, we replace it dynamically with the current value string 31 | const value = fs.readFileSync(name)?.toString().replaceAll('import.meta.url', `'file://${name}'`) 32 | 33 | if (!value) { 34 | return { error: new Error(`Module '${name}' not installed or available`) } 35 | } 36 | return { value } 37 | } 38 | 39 | return moduleLoader 40 | } 41 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/loadWasmModule.ts: -------------------------------------------------------------------------------- 1 | import { type QuickJSAsyncWASMModule, newQuickJSAsyncWASMModuleFromVariant } from 'quickjs-emscripten-core' 2 | import type { LoadAsyncQuickJsOptions } from '../../types/LoadQuickJsOptions.js' 3 | 4 | /** 5 | * Loads the webassembly file and prepares sandbox creation 6 | * @param loadOptions 7 | * @returns 8 | */ 9 | export const loadWasmModule = async (loadOptions: LoadAsyncQuickJsOptions): Promise => { 10 | try { 11 | if (typeof loadOptions === 'string') { 12 | return await newQuickJSAsyncWASMModuleFromVariant(import(loadOptions)) 13 | } 14 | return loadOptions as QuickJSAsyncWASMModule 15 | } catch (error) { 16 | throw new Error('Failed to load webassembly', { cause: error }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/modulePathNormalizerAsync.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'node:path' 2 | import type { JSModuleNormalizerAsync } from 'quickjs-emscripten-core' 3 | 4 | export const modulePathNormalizerAsync: JSModuleNormalizerAsync = async (baseName: string, requestedName: string) => { 5 | // relative import 6 | if (requestedName.startsWith('.')) { 7 | const parts = baseName.split('/') 8 | parts.pop() 9 | 10 | return resolve(`/${parts.join('/')}`, requestedName) 11 | } 12 | 13 | // module import 14 | const moduleName = requestedName.replace('node:', '') 15 | return join('/node_modules', moduleName) 16 | } 17 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/prepareAsyncNodeCompatibility.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext } from 'quickjs-emscripten-core' 2 | import type { SandboxAsyncOptions } from '../../types/SandboxOptions.js' 3 | 4 | export const prepareAsyncNodeCompatibility = async (vm: QuickJSAsyncContext, sandboxOptions: SandboxAsyncOptions) => { 5 | vm.unwrapResult( 6 | await vm.evalCodeAsync( 7 | ` 8 | import 'node:buffer'; 9 | import 'node:util'; 10 | import 'node:url'; 11 | import '@node_compatibility/headers'; 12 | import '@node_compatibility/request'; 13 | import '@node_compatibility/response'; 14 | ${sandboxOptions.enableTestUtils ? "import 'test'" : ''} 15 | `, 16 | undefined, 17 | { type: 'module' }, 18 | ), 19 | ).dispose() 20 | } 21 | -------------------------------------------------------------------------------- /src/sandbox/asyncVersion/prepareAsyncSandbox.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { QuickJSAsyncContext, Scope } from 'quickjs-emscripten-core' 3 | import type { SandboxBaseOptions } from '../../types/SandboxOptions.js' 4 | import { provideConsole } from '../provide/provideConsole.js' 5 | import { provideEnv } from '../provide/provideEnv.js' 6 | import { provideFs } from '../provide/provideFs.js' 7 | import { provideHttp } from '../provide/provideHttp.js' 8 | 9 | export const prepareAsyncSandbox = ( 10 | ctx: QuickJSAsyncContext, 11 | scope: Scope, 12 | sandboxOptions: SandboxBaseOptions, 13 | fs: IFs, 14 | ) => { 15 | provideFs(ctx, scope, sandboxOptions, fs) 16 | provideConsole(ctx, scope, sandboxOptions) 17 | provideEnv(ctx, scope, sandboxOptions) 18 | provideHttp(ctx, scope, sandboxOptions, { fs: sandboxOptions.allowFs ? fs : undefined }) 19 | } 20 | -------------------------------------------------------------------------------- /src/sandbox/expose/isES2015Class.ts: -------------------------------------------------------------------------------- 1 | const classRegex = new RegExp(/^class\s/) 2 | 3 | export function isES2015Class(cls: any): cls is new (...args: any[]) => any { 4 | return typeof cls === 'function' && classRegex.test(Function.prototype.toString.call(cls)) 5 | } 6 | -------------------------------------------------------------------------------- /src/sandbox/expose/isObject.ts: -------------------------------------------------------------------------------- 1 | // biome-ignore lint/complexity/noBannedTypes: 2 | export function isObject(value: any): value is object | Function { 3 | return typeof value === 'function' || (typeof value === 'object' && value !== null) 4 | } 5 | -------------------------------------------------------------------------------- /src/sandbox/getMaxIntervalAmount.ts: -------------------------------------------------------------------------------- 1 | import type { SandboxBaseOptions } from '../index.js' 2 | 3 | export const getMaxIntervalAmount = (runtimeOptions: SandboxBaseOptions) => { 4 | if (!runtimeOptions.maxIntervalCount || runtimeOptions.maxIntervalCount <= 0) { 5 | return 10 6 | } 7 | return runtimeOptions.maxIntervalCount 8 | } 9 | -------------------------------------------------------------------------------- /src/sandbox/getMaxTimeoutAmount.ts: -------------------------------------------------------------------------------- 1 | import type { SandboxBaseOptions } from '../index.js' 2 | 3 | export const getMaxTimeoutAmount = (runtimeOptions: SandboxBaseOptions) => { 4 | if (!runtimeOptions.maxTimeoutCount || runtimeOptions.maxTimeoutCount <= 0) { 5 | return 10 6 | } 7 | return runtimeOptions.maxTimeoutCount 8 | } 9 | -------------------------------------------------------------------------------- /src/sandbox/handleEvalError.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorResponse } from '../types/ErrorResponse.js' 2 | 3 | export const handleEvalError = (err: unknown): ErrorResponse => { 4 | return err instanceof Error 5 | ? { 6 | ok: false, 7 | error: { 8 | name: err.name, 9 | message: err.message, 10 | stack: err.stack, 11 | }, 12 | isSyntaxError: err.name === 'SyntaxError', 13 | } 14 | : { 15 | ok: false, 16 | error: { 17 | name: 'UnknownError', 18 | message: typeof err === 'string' ? err : 'An unknown error occurred.', 19 | stack: '', 20 | }, 21 | isSyntaxError: false, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/index.ts: -------------------------------------------------------------------------------- 1 | import type { Serializer } from '../../../types/Serializer.js' 2 | import { serializeArrayBuffer } from './serializeArrayBuffer.js' 3 | import { serializeBuffer } from './serializeBuffer.js' 4 | import { serializeDate } from './serializeDate.js' 5 | import { serializeError } from './serializeError.js' 6 | import { serializeHeaders } from './serializeHeaders.js' 7 | import { serializeMap } from './serializeMap.js' 8 | import { serializeSet } from './serializeSet.js' 9 | import { serializeURLSearchParams } from './serializeURLSearchParams.js' 10 | import { serializeUrl } from './serializeUrl.js' 11 | 12 | export const serializer = new Map() 13 | 14 | /** 15 | * Add a new Serializer 16 | */ 17 | export const addSerializer = (name: string, fn: Serializer) => serializer.set(name, fn) 18 | 19 | /** 20 | * Get a Serializer by constructor name 21 | */ 22 | export const getSerializer = (name: string) => serializer.get(name) 23 | 24 | addSerializer('Buffer', serializeBuffer) 25 | addSerializer('ArrayBuffer', serializeArrayBuffer) 26 | addSerializer('Map', serializeMap) 27 | addSerializer('Set', serializeSet) 28 | addSerializer('Date', serializeDate) 29 | addSerializer('Headers', serializeHeaders) 30 | addSerializer('URL', serializeUrl) 31 | addSerializer('URLSearchParams', serializeURLSearchParams) 32 | 33 | // Errors 34 | addSerializer('AssertionError', serializeError) 35 | addSerializer('Error', serializeError) 36 | addSerializer('EvalError', serializeError) 37 | addSerializer('RangeError', serializeError) 38 | addSerializer('ReferenceError', serializeError) 39 | addSerializer('SyntaxError', serializeError) 40 | addSerializer('SystemError', serializeError) 41 | addSerializer('TypeError', serializeError) 42 | addSerializer('URIError', serializeError) 43 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeArrayBuffer.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | 4 | export const serializeArrayBuffer: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 5 | const b = ctx.getArrayBuffer(handle) 6 | return Uint8Array.from(b.value) 7 | } 8 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeBuffer.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeBuffer: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 7 | let b: Buffer | undefined 8 | ctx 9 | .newFunction('', value => { 10 | const v = handleToNative(ctx, value) 11 | b = Buffer.from(v) 12 | }) 13 | .consume(f => { 14 | call( 15 | ctx, 16 | 'internal/serializer/serializeBuffer.js', 17 | `(b,fn) => { 18 | fn(b.buffer) 19 | }`, 20 | undefined, 21 | handle, 22 | f, 23 | ).dispose() 24 | }) 25 | return b 26 | } 27 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeDate.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeDate: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 7 | const d = new Date() 8 | ctx 9 | .newFunction('', value => { 10 | const v = handleToNative(ctx, value) 11 | d.setTime(v) 12 | }) 13 | .consume(f => { 14 | call( 15 | ctx, 16 | 'internal/serializer/serializeDate.js', 17 | `(d,fn) => { 18 | fn(d.getTime()) 19 | }`, 20 | undefined, 21 | handle, 22 | f, 23 | ).dispose() 24 | }) 25 | return d 26 | } 27 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeError.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeError: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 7 | const d: Error = new Error() 8 | 9 | ctx 10 | .newFunction('serializeError', (value, name) => { 11 | const v = handleToNative(ctx, value) 12 | const n = handleToNative(ctx, name) 13 | Object.defineProperties(d, v) 14 | d.name = n 15 | }) 16 | .consume(f => { 17 | call( 18 | ctx, 19 | 'internal/serializer/serializeError.js', 20 | `(d,fn) => { 21 | fn(Object.getOwnPropertyDescriptors(d),d.name) 22 | }`, 23 | undefined, 24 | handle, 25 | f, 26 | ).dispose() 27 | }) 28 | return d 29 | } 30 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeHeaders.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeHeaders: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 7 | const h = new Headers() 8 | ctx 9 | .newFunction('', (value, name) => { 10 | const v = handleToNative(ctx, value) 11 | const n = handleToNative(ctx, name) 12 | h.append(n, v) 13 | }) 14 | .consume(f => { 15 | call( 16 | ctx, 17 | 'internal/serializer/serializeHeaders.js', 18 | `(h, fn) => { 19 | h.forEach(fn) 20 | }`, 21 | undefined, 22 | handle, 23 | f, 24 | ).dispose() 25 | }) 26 | return h 27 | } 28 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeMap.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeMap: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 7 | const m = new Map() 8 | ctx 9 | .newFunction('', (key, value) => { 10 | const k = handleToNative(ctx, key) 11 | const v = handleToNative(ctx, value) 12 | m.set(k, v) 13 | }) 14 | .consume(f => { 15 | call( 16 | ctx, 17 | 'internal/serializer/serializeMap.js', 18 | `(m,fn) => { 19 | for(const [key,value] of m.entries()) 20 | fn(key,value) 21 | }`, 22 | undefined, 23 | handle, 24 | f, 25 | ).dispose() 26 | }) 27 | return m 28 | } 29 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeSet.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeSet: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 7 | const s = new Set() 8 | ctx 9 | .newFunction('', value => { 10 | const v = handleToNative(ctx, value) 11 | s.add(v) 12 | }) 13 | .consume(f => { 14 | call( 15 | ctx, 16 | 'internal/serializer/serializeSet.js', 17 | `(s,fn) => { 18 | s.forEach(fn) 19 | }`, 20 | undefined, 21 | handle, 22 | f, 23 | ).dispose() 24 | }) 25 | return s 26 | } 27 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeURLSearchParams.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeURLSearchParams: Serializer = ( 7 | ctx: QuickJSContext | QuickJSAsyncContext, 8 | handle: QuickJSHandle, 9 | ) => { 10 | const h = new URLSearchParams() 11 | ctx 12 | .newFunction('', (value, name) => { 13 | const v = handleToNative(ctx, value) 14 | const n = handleToNative(ctx, name) 15 | h.append(n, v) 16 | }) 17 | .consume(f => { 18 | call( 19 | ctx, 20 | 'internal/serializer/serializeUrlSearchParams.js', 21 | `(h, fn) => { 22 | h.forEach(fn) 23 | }`, 24 | undefined, 25 | handle, 26 | f, 27 | ).dispose() 28 | }) 29 | return h 30 | } 31 | -------------------------------------------------------------------------------- /src/sandbox/handleToNative/serializer/serializeUrl.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | import type { Serializer } from '../../../types/Serializer.js' 3 | import { call } from '../../helper.js' 4 | import { handleToNative } from '../handleToNative.js' 5 | 6 | export const serializeUrl: Serializer = (ctx: QuickJSContext | QuickJSAsyncContext, handle: QuickJSHandle) => { 7 | let d: URL | undefined 8 | ctx 9 | .newFunction('', value => { 10 | const v = handleToNative(ctx, value) 11 | d = new URL(v) 12 | }) 13 | .consume(f => { 14 | call( 15 | ctx, 16 | 'internal/serializer/serializeUrl.js', 17 | `(d,fn) => { 18 | fn(d.toJSON()) 19 | }`, 20 | undefined, 21 | handle, 22 | f, 23 | ).dispose() 24 | }) 25 | return d 26 | } 27 | URLSearchParams 28 | -------------------------------------------------------------------------------- /src/sandbox/helper.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | 3 | export const call = ( 4 | ctx: QuickJSContext | QuickJSAsyncContext, 5 | fileName: string, 6 | code: string, 7 | that: QuickJSHandle | undefined, 8 | ...args: QuickJSHandle[] 9 | ) => { 10 | const fnHandle = ctx.unwrapResult(ctx.evalCode(code, fileName)) 11 | try { 12 | const callHandle = ctx.unwrapResult(ctx.callFunction(fnHandle, that || ctx.undefined, ...args)) 13 | fnHandle.dispose() 14 | return callHandle 15 | } catch (error) { 16 | console.error(error) 17 | const e = new Error((error as Error).message ?? 'Function call failed in serialization') 18 | e.cause = error 19 | throw e 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/sandbox/loadAsyncWasmModule.ts: -------------------------------------------------------------------------------- 1 | import { type QuickJSAsyncWASMModule, newQuickJSAsyncWASMModuleFromVariant } from 'quickjs-emscripten-core' 2 | import type { LoadAsyncQuickJsOptions } from '../types/LoadQuickJsOptions.js' 3 | 4 | /** 5 | * Loads the webassembly file and prepares sandbox creation 6 | * @param loadOptions 7 | * @returns 8 | */ 9 | export const loadAsyncWasmModule = async (loadOptions: LoadAsyncQuickJsOptions): Promise => { 10 | try { 11 | if (typeof loadOptions === 'string') { 12 | return await newQuickJSAsyncWASMModuleFromVariant(import(loadOptions)) 13 | } 14 | return loadOptions as QuickJSAsyncWASMModule 15 | } catch (error) { 16 | throw new Error('Failed to load webassembly', { cause: error }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/sandbox/provide/provideConsole.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, Scope } from 'quickjs-emscripten-core' 2 | import type { RuntimeOptions } from '../../types/RuntimeOptions.js' 3 | import { expose } from '../expose/expose.js' 4 | 5 | /** 6 | * Provide all Node.js console methods 7 | */ 8 | export const provideConsole = (ctx: QuickJSContext | QuickJSAsyncContext, scope: Scope, options: RuntimeOptions) => { 9 | const logger = { 10 | log: (...params) => console.log(...params), 11 | error: (...params) => console.error(...params), 12 | warn: (...params) => console.warn(...params), 13 | info: (...params) => console.info(...params), 14 | debug: (...params) => console.debug(...params), 15 | trace: (...params) => console.trace(...params), 16 | assert: (...params) => console.assert(...params), 17 | count: (...params) => console.count(...params), 18 | countReset: (...params) => console.countReset(...params), 19 | dir: (...params) => console.dir(...params), 20 | dirxml: (...params) => console.dirxml(...params), 21 | group: (...params) => console.group(...params), 22 | groupCollapsed: (...params) => console.groupCollapsed(...params), 23 | groupEnd: () => console.groupEnd(), 24 | table: (...params) => console.table(...params), 25 | time: (...params) => console.time(...params), 26 | timeEnd: (...params) => console.timeEnd(...params), 27 | timeLog: (...params) => console.timeLog(...params), 28 | clear: () => { 29 | throw new Error('console.clear is disabled for security reasons by default') 30 | }, 31 | ...options.console, 32 | } 33 | 34 | expose(ctx, scope, { 35 | console: { 36 | log: logger.log, 37 | error: logger.error, 38 | warn: logger.warn, 39 | info: logger.info, 40 | debug: logger.debug, 41 | trace: logger.trace, 42 | assert: logger.assert, 43 | count: logger.count, 44 | countReset: logger.countReset, 45 | dir: logger.dir, 46 | dirxml: logger.dirxml, 47 | group: logger.group, 48 | groupCollapsed: logger.groupCollapsed, 49 | groupEnd: logger.groupEnd, 50 | table: logger.table, 51 | time: logger.time, 52 | timeEnd: logger.timeEnd, 53 | timeLog: logger.timeLog, 54 | clear: logger.clear, 55 | }, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/sandbox/provide/provideEnv.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext, Scope } from 'quickjs-emscripten-core' 2 | import type { RuntimeOptions } from '../../types/RuntimeOptions.js' 3 | import { expose } from '../expose/expose.js' 4 | 5 | export const provideEnv = (ctx: QuickJSContext | QuickJSAsyncContext, scope: Scope, options: RuntimeOptions) => { 6 | expose(ctx, scope, { 7 | env: options.env ?? {}, 8 | process: { 9 | env: options.env ?? { NODE_DEBUG: 'true' }, 10 | cwd: () => '/', 11 | }, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/sandbox/provide/provideHttp.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { QuickJSAsyncContext, QuickJSContext, Scope } from 'quickjs-emscripten-core' 3 | import { getDefaultFetchAdapter } from '../../adapter/fetch.js' 4 | import type { RuntimeOptions } from '../../types/RuntimeOptions.js' 5 | import { expose } from '../expose/expose.js' 6 | 7 | /** 8 | * Provide http related functions 9 | */ 10 | export const provideHttp = ( 11 | ctx: QuickJSContext | QuickJSAsyncContext, 12 | scope: Scope, 13 | options: RuntimeOptions, 14 | input?: { fs?: IFs | undefined }, 15 | ) => { 16 | const injectUnsupported = 17 | (name: string) => 18 | () => { 19 | throw new Error( 20 | `Not supported: ${name} has been disabled for security reasons or is not supported by the runtime`, 21 | ) as T 22 | } 23 | 24 | let fetchFunction: typeof fetch = Object.assign(injectUnsupported('fetch'), { 25 | preconnect: async (_url: string | URL, _options?: any) => { 26 | return 27 | }, 28 | }) 29 | 30 | if (options.allowFetch) { 31 | const adapter = options.fetchAdapter ?? getDefaultFetchAdapter({ fs: input?.fs }) 32 | fetchFunction = Object.assign(adapter, { 33 | preconnect: async (_url: string | URL, _options?: any) => { 34 | return 35 | }, 36 | }) 37 | } 38 | 39 | expose(ctx, scope, { 40 | __parseURL: (input: string | URL, base?: string | URL | undefined) => { 41 | const url = new URL(input, base) 42 | return { 43 | href: url.href, 44 | origin: url.origin, 45 | protocol: url.protocol, 46 | username: url.username, 47 | password: url.password, 48 | host: url.host, 49 | hostname: url.hostname, 50 | port: url.port, 51 | pathname: url.pathname, 52 | hash: url.hash, 53 | search: url.search, 54 | } 55 | }, 56 | fetch: fetchFunction, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /src/sandbox/provide/provideTimingFunctions.ts: -------------------------------------------------------------------------------- 1 | import { type QuickJSAsyncContext, type QuickJSContext, Scope } from 'quickjs-emscripten-core' 2 | 3 | export const provideTimingFunctions = ( 4 | ctx: QuickJSContext | QuickJSAsyncContext, 5 | max: { 6 | maxTimeoutCount: number 7 | maxIntervalCount: number 8 | }, 9 | ) => { 10 | const scope = new Scope() 11 | 12 | const timeouts = new Map>() 13 | let timeoutCounter = 0 14 | 15 | const intervals = new Map>() 16 | let intervalCounter = 0 17 | 18 | const _setTimeout = ctx.newFunction('setTimeout', (vmFnHandle, timeoutHandle) => { 19 | const currentCounter = timeoutCounter++ 20 | if (timeouts.size + 1 > max.maxTimeoutCount) { 21 | throw new Error( 22 | `Client tries to use setTimeout, which exceeds the limit of max ${max.maxTimeoutCount} concurrent running timeout functions`, 23 | ) 24 | } 25 | 26 | const vmFnHandleCopy = vmFnHandle.dup() 27 | scope.manage(vmFnHandleCopy) 28 | const timeout = timeoutHandle ? ctx.dump(timeoutHandle) : undefined 29 | 30 | const timeoutID = setTimeout(() => { 31 | const t = timeouts.get(currentCounter) 32 | if (t) { 33 | clearTimeout(t) 34 | timeouts.delete(currentCounter) 35 | } 36 | ctx.callFunction(vmFnHandleCopy, ctx.undefined) 37 | }, timeout) 38 | 39 | timeouts.set(currentCounter, timeoutID) 40 | 41 | return ctx.newNumber(currentCounter) 42 | }) 43 | 44 | scope.manage(_setTimeout) 45 | ctx.setProp(ctx.global, 'setTimeout', _setTimeout) 46 | 47 | const _clearTimeout = ctx.newFunction('clearTimeout', timeoutHandle => { 48 | const id: number = ctx.dump(timeoutHandle) 49 | timeoutHandle.dispose() 50 | 51 | const t = timeouts.get(id) 52 | if (t) { 53 | clearTimeout(t) 54 | timeouts.delete(id) 55 | } 56 | }) 57 | 58 | scope.manage(_clearTimeout) 59 | ctx.setProp(ctx.global, 'clearTimeout', _clearTimeout) 60 | 61 | const _setInterval = ctx.newFunction('setInterval', (vmFnHandle, intervalHandle) => { 62 | const currentCounter = intervalCounter++ 63 | if (intervals.size + 1 > max.maxIntervalCount) { 64 | throw new Error( 65 | `Client tries to use setInterval, which exceeds the limit of max ${max.maxIntervalCount} concurrent running interval functions`, 66 | ) 67 | } 68 | const vmFnHandleCopy = vmFnHandle.dup() 69 | scope.manage(vmFnHandleCopy) 70 | const interval = ctx.dump(intervalHandle) 71 | 72 | const intervalID = setInterval(() => { 73 | ctx.callFunction(vmFnHandleCopy, ctx.undefined) 74 | }, interval) 75 | 76 | intervals.set(currentCounter, intervalID) 77 | 78 | return ctx.newNumber(currentCounter) 79 | }) 80 | 81 | scope.manage(_setInterval) 82 | ctx.setProp(ctx.global, 'setInterval', _setInterval) 83 | 84 | const _clearInterval = ctx.newFunction('clearInterval', intervalHandle => { 85 | const id: number = ctx.dump(intervalHandle) 86 | intervalHandle.dispose() 87 | 88 | const t = intervals.get(id) 89 | if (t) { 90 | clearInterval(t) 91 | intervals.delete(id) 92 | } 93 | }) 94 | 95 | scope.manage(_clearInterval) 96 | ctx.setProp(ctx.global, 'clearInterval', _clearInterval) 97 | 98 | const dispose = () => { 99 | for (const [_key, value] of timeouts) { 100 | clearTimeout(value) 101 | } 102 | timeouts.clear() 103 | timeoutCounter = 0 104 | 105 | for (const [_key, value] of intervals) { 106 | clearInterval(value) 107 | } 108 | intervals.clear() 109 | intervalCounter = 0 110 | 111 | scope.dispose() 112 | } 113 | 114 | return { dispose } 115 | } 116 | -------------------------------------------------------------------------------- /src/sandbox/setupFileSystem.ts: -------------------------------------------------------------------------------- 1 | import { type IFs, Volume } from 'memfs' 2 | import { createVirtualFileSystem } from '../createVirtualFileSystem.js' 3 | import type { SandboxBaseOptions } from '../types/SandboxOptions.js' 4 | 5 | export const setupFileSystem = (runtimeOptions: SandboxBaseOptions): IFs => { 6 | if (runtimeOptions.mountFs && runtimeOptions.mountFs instanceof Volume) { 7 | return runtimeOptions.mountFs 8 | } 9 | return createVirtualFileSystem(runtimeOptions).fs 10 | } 11 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/createEvalCodeFunction.ts: -------------------------------------------------------------------------------- 1 | import type { Scope } from 'quickjs-emscripten-core' 2 | import { createTimeInterval } from '../../createTimeInterval.js' 3 | import type { CodeFunctionInput } from '../../types/CodeFunctionInput.js' 4 | import type { OkResponse } from '../../types/OkResponse.js' 5 | import type { SandboxEvalCode } from '../../types/SandboxEvalCode.js' 6 | import { getMaxIntervalAmount } from '../getMaxIntervalAmount.js' 7 | import { getMaxTimeoutAmount } from '../getMaxTimeoutAmount.js' 8 | import { handleEvalError } from '../handleEvalError.js' 9 | import { handleToNative } from '../handleToNative/handleToNative.js' 10 | import { provideTimingFunctions } from '../provide/provideTimingFunctions.js' 11 | 12 | export const createEvalCodeFunction = (input: CodeFunctionInput, scope: Scope): SandboxEvalCode => { 13 | const { ctx, sandboxOptions, transpileFile } = input 14 | return async (code, filename = '/src/index.js', evalOptions?) => { 15 | const eventLoopinterval = createTimeInterval(() => ctx.runtime.executePendingJobs(), 0) 16 | 17 | let timeoutId: ReturnType | undefined 18 | 19 | const { dispose: disposeTimer } = provideTimingFunctions(ctx, { 20 | maxTimeoutCount: getMaxTimeoutAmount(sandboxOptions), 21 | maxIntervalCount: getMaxIntervalAmount(sandboxOptions), 22 | }) 23 | 24 | const disposeStep = () => { 25 | if (timeoutId) { 26 | clearTimeout(timeoutId) 27 | } 28 | eventLoopinterval?.clear() 29 | disposeTimer() 30 | } 31 | 32 | try { 33 | const jsCode = transpileFile(code) 34 | const evalResult = ctx.evalCode(jsCode, filename, { 35 | strict: true, 36 | strip: false, 37 | backtraceBarrier: true, 38 | ...evalOptions, 39 | type: 'module', 40 | }) 41 | 42 | const handle = scope.manage(ctx.unwrapResult(evalResult)) 43 | 44 | const native = handleToNative(ctx, handle, scope) 45 | const result = await Promise.race([ 46 | (async () => { 47 | const res = await native 48 | return res.default 49 | })(), 50 | new Promise((_resolve, reject) => { 51 | if (sandboxOptions.executionTimeout) { 52 | timeoutId = setTimeout(() => { 53 | const err = new Error('The script execution has exceeded the maximum allowed time limit.') 54 | err.name = 'ExecutionTimeout' 55 | reject(err) 56 | }, sandboxOptions.executionTimeout) 57 | } 58 | }), 59 | ]) 60 | 61 | return { ok: true, data: result } as OkResponse 62 | } catch (err) { 63 | return handleEvalError(err) 64 | } finally { 65 | disposeStep() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/createValidateCodeFunction.ts: -------------------------------------------------------------------------------- 1 | import type { CodeFunctionInput } from '../../types/CodeFunctionInput.js' 2 | import type { OkResponseCheck } from '../../types/OkResponseCheck.js' 3 | import type { SandboxValidateCode } from '../../types/SandboxValidateCode.js' 4 | import { handleEvalError } from '../handleEvalError.js' 5 | 6 | export const createValidateCodeFunction = (input: CodeFunctionInput): SandboxValidateCode => { 7 | const { ctx } = input 8 | return async (code, filename = '/src/index.js', evalOptions?) => { 9 | try { 10 | ctx 11 | .unwrapResult( 12 | ctx.evalCode(code, filename, { 13 | strict: true, 14 | strip: true, 15 | backtraceBarrier: true, 16 | ...evalOptions, 17 | type: 'module', 18 | compileOnly: true, 19 | }), 20 | ) 21 | .dispose() 22 | return { ok: true } as OkResponseCheck 23 | } catch (err) { 24 | return handleEvalError(err) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/executeSandboxFunction.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { QuickJSContext, Scope } from 'quickjs-emscripten-core' 3 | import type { CodeFunctionInput } from '../../types/CodeFunctionInput.js' 4 | import type { SandboxFunction } from '../../types/SandboxFunction.js' 5 | import type { SandboxOptions } from '../../types/SandboxOptions.js' 6 | import { createEvalCodeFunction } from './createEvalCodeFunction.js' 7 | import { createValidateCodeFunction } from './createValidateCodeFunction.js' 8 | 9 | export const executeSandboxFunction = async (input: { 10 | ctx: QuickJSContext 11 | fs: IFs 12 | scope: Scope 13 | sandboxOptions: SandboxOptions 14 | sandboxedFunction: SandboxFunction 15 | transpileFile: (input: string) => string 16 | }) => { 17 | const { ctx, sandboxOptions, sandboxedFunction, fs, transpileFile } = input 18 | const opt: CodeFunctionInput = { ctx, sandboxOptions, transpileFile } 19 | const evalCode = createEvalCodeFunction(opt, input.scope) 20 | const validateCode = createValidateCodeFunction(opt) 21 | return await sandboxedFunction({ ctx, evalCode, validateCode, mountedFs: fs }) 22 | } 23 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/getModuleLoader.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { JSModuleLoader } from 'quickjs-emscripten-core' 3 | 4 | import { join } from 'node:path' 5 | 6 | import type { RuntimeOptions } from '../../types/RuntimeOptions.js' 7 | 8 | export const getModuleLoader = (fs: IFs, _runtimeOptions: RuntimeOptions) => { 9 | const moduleLoader: JSModuleLoader = (inputName, _context) => { 10 | let name = inputName 11 | 12 | // if it does not exist 13 | if (!fs.existsSync(name)) { 14 | // try to add the .js extension 15 | if (fs.existsSync(`${name}.js`)) { 16 | name = `${name}.js` 17 | } else { 18 | return { error: new Error(`Module '${inputName}' not installed or available`) } 19 | } 20 | } 21 | 22 | // if it is a folder, we need to use the index.js file 23 | if (fs.lstatSync(name).isDirectory()) { 24 | name = join(name, 'index.js') 25 | if (!fs.existsSync(name)) { 26 | return { error: new Error(`Module '${inputName}' not installed or available`) } 27 | } 28 | } 29 | 30 | // workaround: as we can not provide the real import.meta.url functionality, we replace it dynamically with the current value string 31 | const value = fs.readFileSync(name)?.toString().replaceAll('import.meta.url', `'file://${name}'`) 32 | 33 | if (!value) { 34 | return { error: new Error(`Module '${name}' not installed or available`) } 35 | } 36 | return { value } 37 | } 38 | 39 | return moduleLoader 40 | } 41 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/loadWasmModule.ts: -------------------------------------------------------------------------------- 1 | import { type QuickJSWASMModule, newQuickJSWASMModuleFromVariant } from 'quickjs-emscripten-core' 2 | import type { LoadQuickJsOptions } from '../../types/LoadQuickJsOptions.js' 3 | 4 | /** 5 | * Loads the webassembly file and prepares sandbox creation 6 | * @param loadOptions 7 | * @returns 8 | */ 9 | export const loadWasmModule = async (loadOptions: LoadQuickJsOptions): Promise => { 10 | try { 11 | if (typeof loadOptions === 'string') { 12 | return await newQuickJSWASMModuleFromVariant(import(loadOptions)) 13 | } 14 | return loadOptions as QuickJSWASMModule 15 | } catch (error) { 16 | console.error(error) 17 | throw new Error('Failed to load webassembly', { cause: error }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/modulePathNormalizer.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'node:path' 2 | import type { JSModuleNormalizer } from 'quickjs-emscripten-core' 3 | 4 | export const modulePathNormalizer: JSModuleNormalizer = (baseName: string, requestedName: string) => { 5 | // relative import 6 | if (requestedName.startsWith('.')) { 7 | const parts = baseName.split('/') 8 | parts.pop() 9 | 10 | return resolve(`/${parts.join('/')}`, requestedName) 11 | } 12 | 13 | // module import 14 | const moduleName = requestedName.replace('node:', '') 15 | return join('/node_modules', moduleName) 16 | } 17 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/prepareNodeCompatibility.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSContext } from 'quickjs-emscripten-core' 2 | import type { SandboxOptions } from '../../types/SandboxOptions.js' 3 | 4 | export const prepareNodeCompatibility = (vm: QuickJSContext, sandboxOptions: SandboxOptions) => { 5 | vm.unwrapResult( 6 | vm.evalCode( 7 | ` 8 | import 'node:buffer'; 9 | import 'node:util'; 10 | import 'node:url'; 11 | import '@node_compatibility/headers'; 12 | import '@node_compatibility/request'; 13 | import '@node_compatibility/response'; 14 | ${sandboxOptions.enableTestUtils ? "import 'test'" : ''} 15 | `, 16 | undefined, 17 | { type: 'module' }, 18 | ), 19 | ).dispose() 20 | } 21 | -------------------------------------------------------------------------------- /src/sandbox/syncVersion/prepareSandbox.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { QuickJSContext, Scope } from 'quickjs-emscripten-core' 3 | import type { SandboxBaseOptions } from '../../types/SandboxOptions.js' 4 | import { provideEnv } from '../provide/provideEnv.js' 5 | import { provideFs } from '../provide/provideFs.js' 6 | import { provideHttp } from '../provide/provideHttp.js' 7 | import { provideConsole } from './../provide/provideConsole.js' 8 | 9 | export const prepareSandbox = (ctx: QuickJSContext, scope: Scope, sandboxOptions: SandboxBaseOptions, fs: IFs) => { 10 | provideFs(ctx, scope, sandboxOptions, fs) 11 | provideConsole(ctx, scope, sandboxOptions) 12 | provideEnv(ctx, scope, sandboxOptions) 13 | provideHttp(ctx, scope, sandboxOptions, { fs: sandboxOptions.allowFs ? fs : undefined }) 14 | } 15 | -------------------------------------------------------------------------------- /src/test-regression/59-timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../loadQuickJs.js' 3 | import type { OkResponse } from '../types/OkResponse.js' 4 | 5 | describe('bugfix - #59', () => { 6 | it('does not throw when setTimeout is called with one argument', async () => { 7 | const runtime = await loadQuickJs() 8 | 9 | const code = ` 10 | export default await new Promise((resolve) => setTimeout(()=>resolve(true))) 11 | ` 12 | 13 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code)) 14 | expect(result.ok).toBeTrue() 15 | expect((result as OkResponse).data).toBeTrue() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/test-regression/68-crash-when-async-throws.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../loadQuickJs.js' 3 | import type { ErrorResponse } from '../types/ErrorResponse.js' 4 | import type { OkResponse } from '../types/OkResponse.js' 5 | import type { SandboxOptions } from '../types/SandboxOptions.js' 6 | 7 | describe('bugfix - #68', () => { 8 | it('does not crash when async function resolves', async () => { 9 | const runtime = await loadQuickJs() 10 | 11 | const getExternalData = async (input: string) => { 12 | return { myValue: input } 13 | } 14 | 15 | const options: SandboxOptions = { 16 | allowFetch: false, 17 | env: { 18 | getExternalData, 19 | }, 20 | } 21 | 22 | const code = ` 23 | const fn = async ()=>{ 24 | const data = await env.getExternalData('some-id') 25 | return data.myValue 26 | } 27 | 28 | export default await fn() 29 | ` 30 | 31 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code), options) 32 | expect(result.ok).toBeTrue() 33 | expect((result as OkResponse).data).toBe('some-id') 34 | }) 35 | 36 | it('does not crash when async function throws', async () => { 37 | const runtime = await loadQuickJs('@jitl/quickjs-ng-wasmfile-debug-sync') 38 | 39 | const getExternalData = async (_input: string) => { 40 | throw new Error('test') 41 | } 42 | 43 | const options: SandboxOptions = { 44 | allowFetch: false, 45 | env: { 46 | getExternalData, 47 | }, 48 | } 49 | 50 | const code = ` 51 | const fn = async ()=>{ 52 | const data = await env.getExternalData('some-id') 53 | return data.myValue 54 | } 55 | 56 | export default await fn() 57 | ` 58 | 59 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code), options) 60 | expect(result.ok).toBeFalse() 61 | expect((result as ErrorResponse).error.name).toBe('Error') 62 | expect((result as ErrorResponse).error.message).toBe('test') 63 | }) 64 | 65 | it('maps Error type correctly', async () => { 66 | const runtime = await loadQuickJs('@jitl/quickjs-ng-wasmfile-debug-sync') 67 | 68 | const getExternalData = async (_input: string) => { 69 | throw new TypeError('test') 70 | } 71 | 72 | const options: SandboxOptions = { 73 | allowFetch: false, 74 | env: { 75 | getExternalData, 76 | }, 77 | } 78 | 79 | const code = ` 80 | const fn = async ()=>{ 81 | const data = await env.getExternalData('some-id') 82 | return data.myValue 83 | } 84 | 85 | export default await fn() 86 | ` 87 | 88 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code), options) 89 | expect(result.ok).toBeFalse() 90 | expect((result as ErrorResponse).error.name).toBe('TypeError') 91 | expect((result as ErrorResponse).error.message).toBe('test') 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /src/test/async/core-return-values.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | import type { ErrorResponse } from '../../types/ErrorResponse.js' 4 | import type { OkResponse } from '../../types/OkResponse.js' 5 | 6 | describe('async - core - return values', () => { 7 | let runtime: Awaited> 8 | 9 | beforeAll(async () => { 10 | runtime = await loadAsyncQuickJs() 11 | }) 12 | 13 | const execute = async (code: string) => { 14 | return await runtime.runSandboxed(async ({ evalCode }) => evalCode(code)) 15 | } 16 | 17 | it('returns string', async () => { 18 | const code = ` 19 | export default 'some valid code' 20 | ` 21 | 22 | const result = await execute(code) 23 | expect(result).toStrictEqual({ 24 | ok: true, 25 | data: 'some valid code', 26 | }) 27 | }) 28 | 29 | it('returns number', async () => { 30 | const code = ` 31 | export default 42 32 | ` 33 | 34 | const result = await execute(code) 35 | expect(result).toStrictEqual({ 36 | ok: true, 37 | data: 42, 38 | }) 39 | }) 40 | 41 | it('returns undefined', async () => { 42 | const code = ` 43 | export default undefined 44 | ` 45 | 46 | const result = await execute(code) 47 | expect(result).toStrictEqual({ 48 | ok: true, 49 | data: undefined, 50 | }) 51 | }) 52 | 53 | it('returns true', async () => { 54 | const code = ` 55 | export default true 56 | ` 57 | 58 | const result = await execute(code) 59 | expect(result).toStrictEqual({ 60 | ok: true, 61 | data: true, 62 | }) 63 | }) 64 | 65 | it('returns false', async () => { 66 | const code = ` 67 | export default false 68 | ` 69 | 70 | const result = await execute(code) 71 | expect(result).toStrictEqual({ 72 | ok: true, 73 | data: false, 74 | }) 75 | }) 76 | 77 | it('returns null', async () => { 78 | const code = ` 79 | export default null 80 | ` 81 | 82 | const result = await execute(code) 83 | expect(result).toStrictEqual({ 84 | ok: true, 85 | data: null, 86 | }) 87 | }) 88 | 89 | it('returns object', async () => { 90 | const code = ` 91 | export default { 92 | stringValue: 'Hello', 93 | numberValue: 42, 94 | trueValue: true, 95 | falseValue: false, 96 | undefined: undefined, 97 | nullValue: null, 98 | nestedObject: { 99 | value: true 100 | }, 101 | } 102 | ` 103 | 104 | const result = await execute(code) 105 | expect(result).toStrictEqual({ 106 | ok: true, 107 | data: { 108 | stringValue: 'Hello', 109 | numberValue: 42, 110 | trueValue: true, 111 | falseValue: false, 112 | undefined: undefined, 113 | nullValue: null, 114 | nestedObject: { 115 | value: true, 116 | }, 117 | }, 118 | }) 119 | }) 120 | 121 | it('returns a function', async () => { 122 | const code = ` 123 | export default () => 'hello world' 124 | ` 125 | 126 | const result = await runtime.runSandboxed(async ({ evalCode }) => { 127 | const res = (await evalCode(code)) as OkResponse<() => string> 128 | 129 | const y = res.data 130 | return y() 131 | }) 132 | 133 | expect(result).toEqual('hello world') 134 | }) 135 | 136 | it('returns an error', async () => { 137 | const code = ` 138 | export default new Error('error') 139 | ` 140 | 141 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code)) 142 | expect(result.ok).toBeTrue() 143 | expect((result as OkResponse).data).toEqual(new Error('error')) 144 | }) 145 | 146 | it('throws an error', async () => { 147 | const code = ` 148 | throw new Error('custom error') 149 | export default 'x' 150 | ` 151 | 152 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code)) 153 | expect(result.ok).toBeFalse() 154 | expect((result as ErrorResponse).error.message).toEqual('custom error') 155 | expect((result as ErrorResponse).error.name).toEqual('Error') 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /src/test/async/core-timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | 4 | describe('async - core - timeout', () => { 5 | let runtime: Awaited> 6 | 7 | beforeAll(async () => { 8 | runtime = await loadAsyncQuickJs() 9 | }) 10 | 11 | const runCode = async (code: string, options: { executionTimeout?: number } = {}) => { 12 | return await runtime.runSandboxed(async ({ evalCode }) => { 13 | return await evalCode(code) 14 | }, options) 15 | } 16 | 17 | it('terminates execution when global timeout is reached', async () => { 18 | const code = ` 19 | while(true){} 20 | export default 'ok' 21 | ` 22 | 23 | await expect(runCode(code, { executionTimeout: 1 })).rejects.toThrow('interrupted') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/test/async/core-timers.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('async - core - timers', () => { 6 | let runtime: Awaited> 7 | 8 | beforeAll(async () => { 9 | runtime = await loadAsyncQuickJs() 10 | }) 11 | 12 | const runCode = async (code: string) => { 13 | return await runtime.runSandboxed(async ({ evalCode }) => { 14 | return await evalCode(code) 15 | }) 16 | } 17 | 18 | it('setTimeout works correctly', async () => { 19 | const code = ` 20 | export default await new Promise((resolve) => { 21 | setTimeout(() => { 22 | resolve('timeout reached') 23 | }, 1_000) 24 | }) 25 | ` 26 | 27 | const result = await runCode(code) 28 | expect((result as OkResponse).data).toBe('timeout reached') 29 | }) 30 | 31 | it('clearTimeout works correctly', async () => { 32 | const code = ` 33 | export default await new Promise((resolve,reject) => { 34 | const timeout = setTimeout(() => { 35 | reject('timeout reached') 36 | }, 100) 37 | 38 | clearTimeout(timeout) 39 | 40 | setTimeout(()=>{ 41 | resolve('timeout cleared') 42 | }, 500) 43 | }) 44 | ` 45 | 46 | const result = await runCode(code) 47 | expect(result.ok).toBeTrue() 48 | expect((result as OkResponse).data).toBe('timeout cleared') 49 | }) 50 | 51 | it('setInterval works correctly', async () => { 52 | const code = ` 53 | export default await new Promise((resolve) => { 54 | let count = 0 55 | const interval = setInterval(() => { 56 | count++ 57 | if (count === 3) { 58 | clearInterval(interval) 59 | resolve('interval reached') 60 | } 61 | }, 10) 62 | }) 63 | ` 64 | 65 | const result = await runCode(code) 66 | expect(result.ok).toBeTrue() 67 | expect((result as OkResponse).data).toBe('interval reached') 68 | }) 69 | 70 | it('clearInterval works correctly', async () => { 71 | const code = ` 72 | export default await new Promise((resolve) => { 73 | let count = 0 74 | const interval = setInterval(() => { 75 | count++ 76 | }, 100) 77 | setTimeout(() => { 78 | clearInterval(interval) 79 | resolve(count) 80 | }, 500) 81 | }) 82 | ` 83 | 84 | const result = await runCode(code) 85 | expect(result.ok).toBeTrue() 86 | // The exact count can vary depending on timing precision, 87 | // but it should be around 3 if intervals are 100ms and we clear after 500ms. 88 | expect((result as OkResponse).data).toBeGreaterThanOrEqual(3) 89 | expect((result as OkResponse).data).toBeLessThanOrEqual(5) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/test/async/core-validateCode.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | import type { ErrorResponse } from '../../types/ErrorResponse.js' 4 | import type { OkResponseCheck } from '../../types/OkResponseCheck.js' 5 | 6 | describe('async - core - validateCode', () => { 7 | let runtime: Awaited> 8 | 9 | beforeAll(async () => { 10 | runtime = await loadAsyncQuickJs() 11 | }) 12 | 13 | const runValidation = async (code: string) => { 14 | return await runtime.runSandboxed(async ({ validateCode }) => { 15 | return await validateCode(code) 16 | }) 17 | } 18 | 19 | it('returns ok true when the code is valid', async () => { 20 | const code = ` 21 | const value = 'some valid code' 22 | export default value 23 | ` 24 | 25 | const result = (await runValidation(code)) as OkResponseCheck 26 | expect(result).toStrictEqual({ 27 | ok: true, 28 | }) 29 | }) 30 | 31 | it('returns ok false and the error when the code is invalid', async () => { 32 | const code = ` 33 | const value = 'missing string end 34 | export default value 35 | ` 36 | 37 | const result = (await runValidation(code)) as ErrorResponse 38 | expect(result).toStrictEqual({ 39 | ok: false, 40 | error: { 41 | message: 'unexpected end of string', 42 | name: 'SyntaxError', 43 | stack: expect.any(String), 44 | }, 45 | isSyntaxError: true, 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/test/async/fsModule-directory.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('async - node:fs - directory', () => { 6 | let runtime: Awaited> 7 | const testDirPath = '/testDir' 8 | const tempDirPrefix = '/tmpDir' 9 | 10 | beforeAll(async () => { 11 | runtime = await loadAsyncQuickJs() 12 | }) 13 | 14 | const runCode = async (code: string) => { 15 | return await runtime.runSandboxed( 16 | async ({ evalCode }) => { 17 | return await evalCode(code) 18 | }, 19 | { allowFs: true }, 20 | ) 21 | } 22 | 23 | it('can create and read a directory synchronously', async () => { 24 | const code = ` 25 | import { mkdirSync, readdirSync } from 'node:fs' 26 | 27 | mkdirSync('${testDirPath}') 28 | const files = readdirSync('/') 29 | 30 | export default files.includes('testDir') 31 | ` 32 | 33 | const result = await runCode(code) 34 | expect(result.ok).toBeTrue() 35 | expect((result as OkResponse).data).toBe(true) 36 | }) 37 | 38 | it('can create and remove a directory synchronously', async () => { 39 | const code = ` 40 | import { mkdirSync, rmdirSync, readdirSync } from 'node:fs' 41 | 42 | mkdirSync('${testDirPath}') 43 | rmdirSync('${testDirPath}') 44 | const files = readdirSync('/') 45 | 46 | export default !files.includes('testDir') 47 | ` 48 | 49 | const result = await runCode(code) 50 | expect(result.ok).toBeTrue() 51 | expect((result as OkResponse).data).toBe(true) 52 | }) 53 | 54 | it('can create a temporary directory synchronously', async () => { 55 | const code = ` 56 | import { mkdtempSync } from 'node:fs' 57 | 58 | const tempDir = mkdtempSync('${tempDirPrefix}') 59 | const isTempDir = tempDir.startsWith('${tempDirPrefix}') 60 | 61 | export default isTempDir 62 | ` 63 | 64 | const result = await runCode(code) 65 | expect(result.ok).toBeTrue() 66 | expect((result as OkResponse).data).toBe(true) 67 | }) 68 | 69 | it('can create and read a directory asynchronously', async () => { 70 | const code = ` 71 | import { mkdir, readdir } from 'node:fs/promises' 72 | 73 | await mkdir('${testDirPath}') 74 | const files = await readdir('/') 75 | 76 | export default files.includes('testDir') 77 | ` 78 | 79 | const result = await runCode(code) 80 | expect(result.ok).toBeTrue() 81 | expect((result as OkResponse).data).toBe(true) 82 | }) 83 | 84 | it('can create and remove a directory asynchronously', async () => { 85 | const code = ` 86 | import { mkdir, rmdir, readdir } from 'node:fs/promises' 87 | 88 | await mkdir('${testDirPath}') 89 | await rmdir('${testDirPath}') 90 | const files = await readdir('/') 91 | 92 | export default !files.includes('testDir') 93 | ` 94 | 95 | const result = await runCode(code) 96 | expect(result.ok).toBeTrue() 97 | expect((result as OkResponse).data).toBe(true) 98 | }) 99 | 100 | it('can create a temporary directory asynchronously', async () => { 101 | const code = ` 102 | import { mkdtemp } from 'node:fs/promises' 103 | 104 | const tempDir = await mkdtemp('${tempDirPrefix}') 105 | const isTempDir = tempDir.startsWith('${tempDirPrefix}') 106 | 107 | export default isTempDir 108 | ` 109 | 110 | const result = await runCode(code) 111 | expect(result.ok).toBeTrue() 112 | expect((result as OkResponse).data).toBe(true) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /src/test/async/fsModule-file.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('async - node:fs - file', () => { 6 | let runtime: Awaited> 7 | const testFilePath = '/test.txt' 8 | const testFileContent = 'example content' 9 | 10 | beforeAll(async () => { 11 | runtime = await loadAsyncQuickJs() 12 | }) 13 | 14 | const runCode = async (code: string) => { 15 | return await runtime.runSandboxed( 16 | async ({ evalCode }) => { 17 | return await evalCode(code) 18 | }, 19 | { allowFs: true }, 20 | ) 21 | } 22 | 23 | it('can write and read a file synchronously', async () => { 24 | const code = ` 25 | import { writeFileSync, readFileSync } from 'node:fs' 26 | 27 | writeFileSync('${testFilePath}', '${testFileContent}') 28 | const fileContent = readFileSync('${testFilePath}', 'utf-8') 29 | 30 | export default fileContent 31 | ` 32 | 33 | const result = await runCode(code) 34 | expect(result.ok).toBeTrue() 35 | expect((result as OkResponse).data).toBe(testFileContent) 36 | }) 37 | 38 | it('can append to a file', async () => { 39 | const appendedContent = ' - appended text' 40 | const code = ` 41 | import { writeFileSync, appendFileSync, readFileSync } from 'node:fs' 42 | 43 | writeFileSync('${testFilePath}', '${testFileContent}') 44 | appendFileSync('${testFilePath}', '${appendedContent}') 45 | const fileContent = readFileSync('${testFilePath}', 'utf-8') 46 | 47 | export default fileContent 48 | ` 49 | 50 | const result = await runCode(code) 51 | expect(result.ok).toBeTrue() 52 | expect((result as OkResponse).data).toBe(testFileContent + appendedContent) 53 | }) 54 | 55 | it('can check file existence', async () => { 56 | const code = ` 57 | import { writeFileSync, existsSync } from 'node:fs' 58 | 59 | writeFileSync('${testFilePath}', '${testFileContent}') 60 | const fileExists = existsSync('${testFilePath}') 61 | 62 | export default fileExists 63 | ` 64 | 65 | const result = await runCode(code) 66 | expect(result.ok).toBeTrue() 67 | expect((result as OkResponse).data).toBe(true) 68 | }) 69 | 70 | it('can read and write file asynchronously', async () => { 71 | const code = ` 72 | import { writeFile, readFile } from 'node:fs/promises' 73 | 74 | await writeFile('${testFilePath}', '${testFileContent}') 75 | const fileContent = await readFile('${testFilePath}', 'utf-8') 76 | 77 | export default fileContent 78 | ` 79 | 80 | const result = await runCode(code) 81 | expect(result.ok).toBeTrue() 82 | expect((result as OkResponse).data).toBe(testFileContent) 83 | }) 84 | 85 | it('can rename a file', async () => { 86 | const newFilePath = '/renamed.txt' 87 | const code = ` 88 | import { writeFileSync, renameSync, readFileSync } from 'node:fs' 89 | 90 | writeFileSync('${testFilePath}', '${testFileContent}') 91 | renameSync('${testFilePath}', '${newFilePath}') 92 | const fileContent = readFileSync('${newFilePath}', 'utf-8') 93 | 94 | export default fileContent 95 | ` 96 | 97 | const result = await runCode(code) 98 | expect(result.ok).toBeTrue() 99 | expect((result as OkResponse).data).toBe(testFileContent) 100 | }) 101 | 102 | it('can delete a file', async () => { 103 | const code = ` 104 | import { writeFileSync, unlinkSync, existsSync } from 'node:fs' 105 | 106 | writeFileSync('${testFilePath}', '${testFileContent}') 107 | unlinkSync('${testFilePath}') 108 | const fileExists = existsSync('${testFilePath}') 109 | 110 | export default fileExists 111 | ` 112 | 113 | const result = await runCode(code) 114 | expect(result.ok).toBeTrue() 115 | expect((result as OkResponse).data).toBe(false) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /src/test/async/util-types.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('async - node:util - types', () => { 6 | let runtime: Awaited> 7 | const typesToTest = [ 8 | { method: 'isAnyArrayBuffer', value: 'new ArrayBuffer(10)', expected: true }, 9 | { method: 'isAnyArrayBuffer', value: 'new SharedArrayBuffer(10)', expected: true }, 10 | { method: 'isArrayBufferView', value: 'new Uint8Array(10)', expected: true }, 11 | { method: 'isArgumentsObject', value: '(function() { return arguments; })()', expected: true }, 12 | { method: 'isArrayBuffer', value: 'new ArrayBuffer(10)', expected: true }, 13 | { method: 'isAsyncFunction', value: 'async function() {}', expected: true }, 14 | { method: 'isBigInt64Array', value: 'new BigInt64Array(10)', expected: true }, 15 | { method: 'isBigUint64Array', value: 'new BigUint64Array(10)', expected: true }, 16 | { method: 'isBooleanObject', value: 'new Boolean(true)', expected: true }, 17 | { method: 'isBoxedPrimitive', value: 'new String("test")', expected: true }, 18 | { method: 'isDataView', value: 'new DataView(new ArrayBuffer(10))', expected: true }, 19 | { method: 'isDate', value: 'new Date()', expected: true }, 20 | { method: 'isFloat32Array', value: 'new Float32Array(10)', expected: true }, 21 | { method: 'isFloat64Array', value: 'new Float64Array(10)', expected: true }, 22 | { method: 'isGeneratorFunction', value: 'function*() {}', expected: true }, 23 | { method: 'isGeneratorObject', value: '(function*() {})()', expected: true }, 24 | { method: 'isInt8Array', value: 'new Int8Array(10)', expected: true }, 25 | { method: 'isInt16Array', value: 'new Int16Array(10)', expected: true }, 26 | { method: 'isInt32Array', value: 'new Int32Array(10)', expected: true }, 27 | { method: 'isMap', value: 'new Map()', expected: true }, 28 | { method: 'isMapIterator', value: 'new Map().entries()', expected: true }, 29 | { method: 'isNativeError', value: 'new Error()', expected: true }, 30 | { method: 'isNumberObject', value: 'new Number(10)', expected: true }, 31 | { method: 'isPromise', value: 'new Promise(() => {})', expected: true }, 32 | { method: 'isRegExp', value: '/test/', expected: true }, 33 | { method: 'isSet', value: 'new Set()', expected: true }, 34 | { method: 'isSetIterator', value: 'new Set().entries()', expected: true }, 35 | { method: 'isSharedArrayBuffer', value: 'new SharedArrayBuffer(10)', expected: true }, 36 | { method: 'isStringObject', value: 'new String("test")', expected: true }, 37 | { method: 'isSymbolObject', value: 'Object(Symbol("test"))', expected: true }, 38 | { method: 'isTypedArray', value: 'new Uint8Array(10)', expected: true }, 39 | { method: 'isUint8Array', value: 'new Uint8Array(10)', expected: true }, 40 | { method: 'isUint8ClampedArray', value: 'new Uint8ClampedArray(10)', expected: true }, 41 | { method: 'isUint16Array', value: 'new Uint16Array(10)', expected: true }, 42 | { method: 'isUint32Array', value: 'new Uint32Array(10)', expected: true }, 43 | { method: 'isWeakMap', value: 'new WeakMap()', expected: true }, 44 | { method: 'isWeakSet', value: 'new WeakSet()', expected: true }, 45 | ] 46 | 47 | beforeAll(async () => { 48 | runtime = await loadAsyncQuickJs() 49 | }) 50 | 51 | const runCode = async (code: string) => { 52 | return await runtime.runSandboxed(async ({ evalCode }) => { 53 | return await evalCode(code) 54 | }) 55 | } 56 | 57 | for (const { method, value, expected } of typesToTest) { 58 | it(`${method} works correctly`, async () => { 59 | const code = ` 60 | import { types } from 'node:util' 61 | const result = types.${method}(${value}) 62 | export default result 63 | ` 64 | 65 | const result = await runCode(code) 66 | expect(result.ok).toBeTrue() 67 | expect((result as OkResponse).data).toBe(expected) 68 | }) 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /src/test/async/util.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('async - node:util - base', () => { 6 | let runtime: Awaited> 7 | 8 | beforeAll(async () => { 9 | runtime = await loadAsyncQuickJs() 10 | }) 11 | 12 | const runCode = async (code: string) => { 13 | return await runtime.runSandboxed(async ({ evalCode }) => { 14 | return await evalCode(code) 15 | }) 16 | } 17 | 18 | it('promisify works correctly', async () => { 19 | const code = ` 20 | function callbackFunction(arg, callback) { 21 | if (arg === 'error') { 22 | callback(new Error('Test error')) 23 | } else { 24 | callback(null, 'Test success') 25 | } 26 | } 27 | 28 | import { promisify } from 'node:util' 29 | const promiseFunction = promisify(callbackFunction) 30 | 31 | async function testPromisify() { 32 | const successResult = await promiseFunction('success') 33 | if (successResult !== 'Test success') { 34 | throw new Error('Promisify test failed for success case') 35 | } 36 | 37 | try { 38 | await promiseFunction('error') 39 | } catch (error) { 40 | if (error.message !== 'Test error') { 41 | throw new Error('Promisify test failed for error case') 42 | } 43 | } 44 | 45 | return 'Test passed' 46 | } 47 | 48 | export default await testPromisify() 49 | ` 50 | 51 | const result = await runCode(code) 52 | expect(result.ok).toBeTrue() 53 | expect((result as OkResponse).data).toBe('Test passed') 54 | }) 55 | 56 | it('callbackify works correctly', async () => { 57 | const code = ` 58 | async function asyncFunction(arg) { 59 | if (arg === 'error') { 60 | throw new Error('Test error') 61 | } else { 62 | return 'Test success' 63 | } 64 | } 65 | 66 | import { callbackify } from 'node:util' 67 | const callbackFunction = callbackify(asyncFunction) 68 | 69 | function testCallbackify() { 70 | return new Promise((resolve, reject) => { 71 | callbackFunction('success', (err, result) => { 72 | if (err || result !== 'Test success') { 73 | reject(new Error('Callbackify test failed for success case')) 74 | } else { 75 | callbackFunction('error', (err, result) => { 76 | if (!err || err.message !== 'Test error') { 77 | reject(new Error('Callbackify test failed for error case')) 78 | } else { 79 | resolve('Test passed') 80 | } 81 | }) 82 | } 83 | }) 84 | }) 85 | } 86 | 87 | export default await testCallbackify() 88 | ` 89 | 90 | const result = await runCode(code) 91 | expect(result.ok).toBeTrue() 92 | expect((result as OkResponse).data).toBe('Test passed') 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /src/test/sync/core-return-values.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | import type { ErrorResponse } from '../../types/ErrorResponse.js' 4 | import type { OkResponse } from '../../types/OkResponse.js' 5 | 6 | describe('sync - core - return values', () => { 7 | let runtime: Awaited> 8 | 9 | beforeAll(async () => { 10 | runtime = await loadQuickJs() 11 | }) 12 | 13 | const execute = async (code: string): Promise => { 14 | return await runtime.runSandboxed(async ({ evalCode }) => evalCode(code)) 15 | } 16 | 17 | it('returns string', async () => { 18 | const code = ` 19 | export default 'some valid code' 20 | ` 21 | 22 | const result = await execute(code) 23 | expect(result).toStrictEqual({ 24 | ok: true, 25 | data: 'some valid code', 26 | }) 27 | }) 28 | 29 | it('returns number', async () => { 30 | const code = ` 31 | export default 42 32 | ` 33 | 34 | const result = await execute(code) 35 | expect(result).toStrictEqual({ 36 | ok: true, 37 | data: 42, 38 | }) 39 | }) 40 | 41 | it('returns undefined', async () => { 42 | const code = ` 43 | export default undefined 44 | ` 45 | 46 | const result = await execute(code) 47 | expect(result).toStrictEqual({ 48 | ok: true, 49 | data: undefined, 50 | }) 51 | }) 52 | 53 | it('returns true', async () => { 54 | const code = ` 55 | export default true 56 | ` 57 | 58 | const result = await execute(code) 59 | expect(result).toStrictEqual({ 60 | ok: true, 61 | data: true, 62 | }) 63 | }) 64 | 65 | it('returns false', async () => { 66 | const code = ` 67 | export default false 68 | ` 69 | 70 | const result = await execute(code) 71 | expect(result).toStrictEqual({ 72 | ok: true, 73 | data: false, 74 | }) 75 | }) 76 | 77 | it('returns null', async () => { 78 | const code = ` 79 | export default null 80 | ` 81 | 82 | const result = await execute(code) 83 | expect(result).toStrictEqual({ 84 | ok: true, 85 | data: null, 86 | }) 87 | }) 88 | 89 | it('returns object', async () => { 90 | const code = ` 91 | export default { 92 | stringValue: 'Hello', 93 | numberValue: 42, 94 | trueValue: true, 95 | falseValue: false, 96 | undefined: undefined, 97 | nullValue: null, 98 | nestedObject: { 99 | value: true 100 | }, 101 | } 102 | ` 103 | 104 | const result = await execute(code) 105 | expect(result).toStrictEqual({ 106 | ok: true, 107 | data: { 108 | stringValue: 'Hello', 109 | numberValue: 42, 110 | trueValue: true, 111 | falseValue: false, 112 | undefined: undefined, 113 | nullValue: null, 114 | nestedObject: { 115 | value: true, 116 | }, 117 | }, 118 | }) 119 | }) 120 | 121 | it('returns a function', async () => { 122 | const code = ` 123 | export default () => 'hello world' 124 | ` 125 | 126 | const result = await runtime.runSandboxed(async ({ evalCode }) => { 127 | const res = await evalCode(code) 128 | 129 | const y = (res as OkResponse<() => string>).data 130 | return y() 131 | }) 132 | 133 | expect(result).toEqual('hello world') 134 | }) 135 | 136 | it('returns an error', async () => { 137 | const code = ` 138 | export default new Error('error') 139 | ` 140 | 141 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code)) 142 | expect(result.ok).toBeTrue() 143 | expect((result as OkResponse).data).toEqual(new Error('error')) 144 | }) 145 | 146 | it('throws an error', async () => { 147 | const code = ` 148 | throw new Error('custom error') 149 | export default 'x' 150 | ` 151 | 152 | const result = await runtime.runSandboxed(async ({ evalCode }) => evalCode(code)) 153 | expect(result.ok).toBeFalse() 154 | expect((result as ErrorResponse).error.message).toEqual('custom error') 155 | expect((result as ErrorResponse).error.name).toEqual('Error') 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /src/test/sync/core-timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | 4 | describe('sync - core - timeout', () => { 5 | let runtime: Awaited> 6 | 7 | beforeAll(async () => { 8 | runtime = await loadQuickJs() 9 | }) 10 | 11 | const runCode = async (code: string, options: { executionTimeout?: number } = {}) => { 12 | return await runtime.runSandboxed(async ({ evalCode }) => { 13 | return await evalCode(code) 14 | }, options) 15 | } 16 | 17 | it('terminates execution when global timeout is reached', async () => { 18 | const code = ` 19 | while(true){} 20 | export default 'ok' 21 | ` 22 | 23 | await expect(runCode(code, { executionTimeout: 1 })).rejects.toThrow('interrupted') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/test/sync/core-timers.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('sync - core - timers', () => { 6 | let runtime: Awaited> 7 | 8 | beforeAll(async () => { 9 | runtime = await loadQuickJs() 10 | }) 11 | 12 | const runCode = async (code: string) => { 13 | return await runtime.runSandboxed(async ({ evalCode }) => { 14 | return await evalCode(code) 15 | }) 16 | } 17 | 18 | it('setTimeout works correctly', async () => { 19 | const code = ` 20 | export default await new Promise((resolve) => { 21 | setTimeout(() => { 22 | resolve('timeout reached') 23 | }, 1_000) 24 | }) 25 | ` 26 | 27 | const result = await runCode(code) 28 | expect((result as OkResponse).data).toBe('timeout reached') 29 | }) 30 | 31 | it('clearTimeout works correctly', async () => { 32 | const code = ` 33 | export default await new Promise((resolve,reject) => { 34 | const timeout = setTimeout(() => { 35 | reject('timeout reached') 36 | }, 100) 37 | 38 | clearTimeout(timeout) 39 | 40 | setTimeout(()=>{ 41 | resolve('timeout cleared') 42 | }, 500) 43 | }) 44 | ` 45 | 46 | const result = await runCode(code) 47 | expect(result.ok).toBeTrue() 48 | expect((result as OkResponse).data).toBe('timeout cleared') 49 | }) 50 | 51 | it('setInterval works correctly', async () => { 52 | const code = ` 53 | export default await new Promise((resolve) => { 54 | let count = 0 55 | const interval = setInterval(() => { 56 | count++ 57 | if (count === 3) { 58 | clearInterval(interval) 59 | resolve('interval reached') 60 | } 61 | }, 10) 62 | }) 63 | ` 64 | 65 | const result = await runCode(code) 66 | expect(result.ok).toBeTrue() 67 | expect((result as OkResponse).data).toBe('interval reached') 68 | }) 69 | 70 | it('clearInterval works correctly', async () => { 71 | const code = ` 72 | export default await new Promise((resolve) => { 73 | let count = 0 74 | const interval = setInterval(() => { 75 | count++ 76 | }, 100) 77 | setTimeout(() => { 78 | clearInterval(interval) 79 | resolve(count) 80 | }, 500) 81 | }) 82 | ` 83 | 84 | const result = await runCode(code) 85 | expect(result.ok).toBeTrue() 86 | // The exact count can vary depending on timing precision, 87 | // but it should be around 3 if intervals are 100ms and we clear after 500ms. 88 | expect((result as OkResponse).data).toBeGreaterThanOrEqual(3) 89 | expect((result as OkResponse).data).toBeLessThanOrEqual(5) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/test/sync/core-validateCode.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | import type { ErrorResponse } from '../../types/ErrorResponse.js' 4 | import type { OkResponseCheck } from '../../types/OkResponseCheck.js' 5 | 6 | describe('sync - core - validateCode', () => { 7 | let runtime: Awaited> 8 | 9 | beforeAll(async () => { 10 | runtime = await loadQuickJs() 11 | }) 12 | 13 | const runValidation = async (code: string) => { 14 | return await runtime.runSandboxed(async ({ validateCode }) => { 15 | return await validateCode(code) 16 | }) 17 | } 18 | 19 | it('returns ok true when the code is valid', async () => { 20 | const code = ` 21 | const value = 'some valid code' 22 | export default value 23 | ` 24 | 25 | const result = (await runValidation(code)) as OkResponseCheck 26 | expect(result).toStrictEqual({ 27 | ok: true, 28 | }) 29 | }) 30 | 31 | it('returns ok false and the error when the code is invalid', async () => { 32 | const code = ` 33 | const value = 'missing string end 34 | export default value 35 | ` 36 | 37 | const result = (await runValidation(code)) as ErrorResponse 38 | expect(result).toStrictEqual({ 39 | ok: false, 40 | error: { 41 | message: 'unexpected end of string', 42 | name: 'SyntaxError', 43 | stack: expect.any(String), 44 | }, 45 | isSyntaxError: true, 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/test/sync/fsModule-directory.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('sync - node:fs - directory', () => { 6 | let runtime: Awaited> 7 | const testDirPath = '/testDir' 8 | const tempDirPrefix = '/tmpDir' 9 | 10 | beforeAll(async () => { 11 | runtime = await loadQuickJs() 12 | }) 13 | 14 | const runCode = async (code: string) => { 15 | return await runtime.runSandboxed( 16 | async ({ evalCode }) => { 17 | return await evalCode(code) 18 | }, 19 | { allowFs: true }, 20 | ) 21 | } 22 | 23 | it('can create and read a directory synchronously', async () => { 24 | const code = ` 25 | import { mkdirSync, readdirSync } from 'node:fs' 26 | 27 | mkdirSync('${testDirPath}') 28 | const files = readdirSync('/') 29 | 30 | export default files.includes('testDir') 31 | ` 32 | 33 | const result = await runCode(code) 34 | expect(result.ok).toBeTrue() 35 | expect((result as OkResponse).data).toBe(true) 36 | }) 37 | 38 | it('can create and remove a directory synchronously', async () => { 39 | const code = ` 40 | import { mkdirSync, rmdirSync, readdirSync } from 'node:fs' 41 | 42 | mkdirSync('${testDirPath}') 43 | rmdirSync('${testDirPath}') 44 | const files = readdirSync('/') 45 | 46 | export default !files.includes('testDir') 47 | ` 48 | 49 | const result = await runCode(code) 50 | expect(result.ok).toBeTrue() 51 | expect((result as OkResponse).data).toBe(true) 52 | }) 53 | 54 | it('can create a temporary directory synchronously', async () => { 55 | const code = ` 56 | import { mkdtempSync } from 'node:fs' 57 | 58 | const tempDir = mkdtempSync('${tempDirPrefix}') 59 | const isTempDir = tempDir.startsWith('${tempDirPrefix}') 60 | 61 | export default isTempDir 62 | ` 63 | 64 | const result = await runCode(code) 65 | expect(result.ok).toBeTrue() 66 | expect((result as OkResponse).data).toBe(true) 67 | }) 68 | 69 | it('can create and read a directory asynchronously', async () => { 70 | const code = ` 71 | import { mkdir, readdir } from 'node:fs/promises' 72 | 73 | await mkdir('${testDirPath}') 74 | const files = await readdir('/') 75 | 76 | export default files.includes('testDir') 77 | ` 78 | 79 | const result = await runCode(code) 80 | expect(result.ok).toBeTrue() 81 | expect((result as OkResponse).data).toBe(true) 82 | }) 83 | 84 | it('can create and remove a directory asynchronously', async () => { 85 | const code = ` 86 | import { mkdir, rmdir, readdir } from 'node:fs/promises' 87 | 88 | await mkdir('${testDirPath}') 89 | await rmdir('${testDirPath}') 90 | const files = await readdir('/') 91 | 92 | export default !files.includes('testDir') 93 | ` 94 | 95 | const result = await runCode(code) 96 | expect(result.ok).toBeTrue() 97 | expect((result as OkResponse).data).toBe(true) 98 | }) 99 | 100 | it('can create a temporary directory asynchronously', async () => { 101 | const code = ` 102 | import { mkdtemp } from 'node:fs/promises' 103 | 104 | const tempDir = await mkdtemp('${tempDirPrefix}') 105 | const isTempDir = tempDir.startsWith('${tempDirPrefix}') 106 | 107 | export default isTempDir 108 | ` 109 | 110 | const result = await runCode(code) 111 | expect(result.ok).toBeTrue() 112 | expect((result as OkResponse).data).toBe(true) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /src/test/sync/fsModule-file.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('sync - node:fs - file', () => { 6 | let runtime: Awaited> 7 | const testFilePath = '/test.txt' 8 | const testFileContent = 'example content' 9 | 10 | beforeAll(async () => { 11 | runtime = await loadQuickJs() 12 | }) 13 | 14 | const runCode = async (code: string) => { 15 | return await runtime.runSandboxed( 16 | async ({ evalCode }) => { 17 | return await evalCode(code) 18 | }, 19 | { allowFs: true }, 20 | ) 21 | } 22 | 23 | it('can write and read a file synchronously', async () => { 24 | const code = ` 25 | import { writeFileSync, readFileSync } from 'node:fs' 26 | 27 | writeFileSync('${testFilePath}', '${testFileContent}') 28 | const fileContent = readFileSync('${testFilePath}', 'utf-8') 29 | 30 | export default fileContent 31 | ` 32 | 33 | const result = await runCode(code) 34 | expect(result.ok).toBeTrue() 35 | expect((result as OkResponse).data).toBe(testFileContent) 36 | }) 37 | 38 | it('can append to a file', async () => { 39 | const appendedContent = ' - appended text' 40 | const code = ` 41 | import { writeFileSync, appendFileSync, readFileSync } from 'node:fs' 42 | 43 | writeFileSync('${testFilePath}', '${testFileContent}') 44 | appendFileSync('${testFilePath}', '${appendedContent}') 45 | const fileContent = readFileSync('${testFilePath}', 'utf-8') 46 | 47 | export default fileContent 48 | ` 49 | 50 | const result = await runCode(code) 51 | expect(result.ok).toBeTrue() 52 | expect((result as OkResponse).data).toBe(testFileContent + appendedContent) 53 | }) 54 | 55 | it('can check file existence', async () => { 56 | const code = ` 57 | import { writeFileSync, existsSync } from 'node:fs' 58 | 59 | writeFileSync('${testFilePath}', '${testFileContent}') 60 | const fileExists = existsSync('${testFilePath}') 61 | 62 | export default fileExists 63 | ` 64 | 65 | const result = await runCode(code) 66 | expect(result.ok).toBeTrue() 67 | expect((result as OkResponse).data).toBe(true) 68 | }) 69 | 70 | it('can read and write file asynchronously', async () => { 71 | const code = ` 72 | import { writeFile, readFile } from 'node:fs/promises' 73 | 74 | await writeFile('${testFilePath}', '${testFileContent}') 75 | const fileContent = await readFile('${testFilePath}', 'utf-8') 76 | 77 | export default fileContent 78 | ` 79 | 80 | const result = await runCode(code) 81 | expect(result.ok).toBeTrue() 82 | expect((result as OkResponse).data).toBe(testFileContent) 83 | }) 84 | 85 | it('can rename a file', async () => { 86 | const newFilePath = '/renamed.txt' 87 | const code = ` 88 | import { writeFileSync, renameSync, readFileSync } from 'node:fs' 89 | 90 | writeFileSync('${testFilePath}', '${testFileContent}') 91 | renameSync('${testFilePath}', '${newFilePath}') 92 | const fileContent = readFileSync('${newFilePath}', 'utf-8') 93 | 94 | export default fileContent 95 | ` 96 | 97 | const result = await runCode(code) 98 | expect(result.ok).toBeTrue() 99 | expect((result as OkResponse).data).toBe(testFileContent) 100 | }) 101 | 102 | it('can delete a file', async () => { 103 | const code = ` 104 | import { writeFileSync, unlinkSync, existsSync } from 'node:fs' 105 | 106 | writeFileSync('${testFilePath}', '${testFileContent}') 107 | unlinkSync('${testFilePath}') 108 | const fileExists = existsSync('${testFilePath}') 109 | 110 | export default fileExists 111 | ` 112 | 113 | const result = await runCode(code) 114 | expect(result.ok).toBeTrue() 115 | expect((result as OkResponse).data).toBe(false) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /src/test/sync/util-types.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('sync - node:util - types', () => { 6 | let runtime: Awaited> 7 | const typesToTest = [ 8 | { method: 'isAnyArrayBuffer', value: 'new ArrayBuffer(10)', expected: true }, 9 | { method: 'isAnyArrayBuffer', value: 'new SharedArrayBuffer(10)', expected: true }, 10 | { method: 'isArrayBufferView', value: 'new Uint8Array(10)', expected: true }, 11 | { method: 'isArgumentsObject', value: '(function() { return arguments; })()', expected: true }, 12 | { method: 'isArrayBuffer', value: 'new ArrayBuffer(10)', expected: true }, 13 | { method: 'isAsyncFunction', value: 'async function() {}', expected: true }, 14 | { method: 'isBigInt64Array', value: 'new BigInt64Array(10)', expected: true }, 15 | { method: 'isBigUint64Array', value: 'new BigUint64Array(10)', expected: true }, 16 | { method: 'isBooleanObject', value: 'new Boolean(true)', expected: true }, 17 | { method: 'isBoxedPrimitive', value: 'new String("test")', expected: true }, 18 | { method: 'isDataView', value: 'new DataView(new ArrayBuffer(10))', expected: true }, 19 | { method: 'isDate', value: 'new Date()', expected: true }, 20 | { method: 'isFloat32Array', value: 'new Float32Array(10)', expected: true }, 21 | { method: 'isFloat64Array', value: 'new Float64Array(10)', expected: true }, 22 | { method: 'isGeneratorFunction', value: 'function*() {}', expected: true }, 23 | { method: 'isGeneratorObject', value: '(function*() {})()', expected: true }, 24 | { method: 'isInt8Array', value: 'new Int8Array(10)', expected: true }, 25 | { method: 'isInt16Array', value: 'new Int16Array(10)', expected: true }, 26 | { method: 'isInt32Array', value: 'new Int32Array(10)', expected: true }, 27 | { method: 'isMap', value: 'new Map()', expected: true }, 28 | { method: 'isMapIterator', value: 'new Map().entries()', expected: true }, 29 | { method: 'isNativeError', value: 'new Error()', expected: true }, 30 | { method: 'isNumberObject', value: 'new Number(10)', expected: true }, 31 | { method: 'isPromise', value: 'new Promise(() => {})', expected: true }, 32 | { method: 'isRegExp', value: '/test/', expected: true }, 33 | { method: 'isSet', value: 'new Set()', expected: true }, 34 | { method: 'isSetIterator', value: 'new Set().entries()', expected: true }, 35 | { method: 'isSharedArrayBuffer', value: 'new SharedArrayBuffer(10)', expected: true }, 36 | { method: 'isStringObject', value: 'new String("test")', expected: true }, 37 | { method: 'isSymbolObject', value: 'Object(Symbol("test"))', expected: true }, 38 | { method: 'isTypedArray', value: 'new Uint8Array(10)', expected: true }, 39 | { method: 'isUint8Array', value: 'new Uint8Array(10)', expected: true }, 40 | { method: 'isUint8ClampedArray', value: 'new Uint8ClampedArray(10)', expected: true }, 41 | { method: 'isUint16Array', value: 'new Uint16Array(10)', expected: true }, 42 | { method: 'isUint32Array', value: 'new Uint32Array(10)', expected: true }, 43 | { method: 'isWeakMap', value: 'new WeakMap()', expected: true }, 44 | { method: 'isWeakSet', value: 'new WeakSet()', expected: true }, 45 | ] 46 | 47 | beforeAll(async () => { 48 | runtime = await loadQuickJs() 49 | }) 50 | 51 | const runCode = async (code: string) => { 52 | return await runtime.runSandboxed(async ({ evalCode }) => { 53 | return await evalCode(code) 54 | }) 55 | } 56 | 57 | for (const { method, value, expected } of typesToTest) { 58 | it(`${method} works correctly`, async () => { 59 | const code = ` 60 | import { types } from 'node:util' 61 | const result = types.${method}(${value}) 62 | export default result 63 | ` 64 | 65 | const result = await runCode(code) 66 | expect(result.ok).toBeTrue() 67 | expect((result as OkResponse).data).toBe(expected) 68 | }) 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /src/test/sync/util.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'bun:test' 2 | import { loadQuickJs } from '../../loadQuickJs.js' 3 | import type { OkResponse } from '../../types/OkResponse.js' 4 | 5 | describe('sync - node:util - base', () => { 6 | let runtime: Awaited> 7 | 8 | beforeAll(async () => { 9 | runtime = await loadQuickJs() 10 | }) 11 | 12 | const runCode = async (code: string) => { 13 | return await runtime.runSandboxed(async ({ evalCode }) => { 14 | return await evalCode(code) 15 | }) 16 | } 17 | 18 | it('promisify works correctly', async () => { 19 | const code = ` 20 | function callbackFunction(arg, callback) { 21 | if (arg === 'error') { 22 | callback(new Error('Test error')) 23 | } else { 24 | callback(null, 'Test success') 25 | } 26 | } 27 | 28 | import { promisify } from 'node:util' 29 | const promiseFunction = promisify(callbackFunction) 30 | 31 | async function testPromisify() { 32 | const successResult = await promiseFunction('success') 33 | if (successResult !== 'Test success') { 34 | throw new Error('Promisify test failed for success case') 35 | } 36 | 37 | try { 38 | await promiseFunction('error') 39 | } catch (error) { 40 | if (error.message !== 'Test error') { 41 | throw new Error('Promisify test failed for error case') 42 | } 43 | } 44 | 45 | return 'Test passed' 46 | } 47 | 48 | export default await testPromisify() 49 | ` 50 | 51 | const result = await runCode(code) 52 | expect(result.ok).toBeTrue() 53 | expect((result as OkResponse).data).toBe('Test passed') 54 | }) 55 | 56 | it('callbackify works correctly', async () => { 57 | const code = ` 58 | async function asyncFunction(arg) { 59 | if (arg === 'error') { 60 | throw new Error('Test error') 61 | } else { 62 | return 'Test success' 63 | } 64 | } 65 | 66 | import { callbackify } from 'node:util' 67 | const callbackFunction = callbackify(asyncFunction) 68 | 69 | function testCallbackify() { 70 | return new Promise((resolve, reject) => { 71 | callbackFunction('success', (err, result) => { 72 | if (err || result !== 'Test success') { 73 | reject(new Error('Callbackify test failed for success case')) 74 | } else { 75 | callbackFunction('error', (err, result) => { 76 | if (!err || err.message !== 'Test error') { 77 | reject(new Error('Callbackify test failed for error case')) 78 | } else { 79 | resolve('Test passed') 80 | } 81 | }) 82 | } 83 | }) 84 | }) 85 | } 86 | 87 | export default await testCallbackify() 88 | ` 89 | 90 | const result = await runCode(code) 91 | expect(result.ok).toBeTrue() 92 | expect((result as OkResponse).data).toBe('Test passed') 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /src/types/CodeFunctionInput.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncContext, QuickJSContext } from 'quickjs-emscripten-core' 2 | import type { SandboxAsyncOptions, SandboxOptions } from './SandboxOptions.js' 3 | 4 | export type CodeFunctionInput = { 5 | ctx: QuickJSContext 6 | sandboxOptions: SandboxOptions 7 | transpileFile: (input: string) => string 8 | } 9 | 10 | export type CodeFunctionAsyncInput = { 11 | ctx: QuickJSAsyncContext 12 | sandboxOptions: SandboxAsyncOptions 13 | transpileFile: (input: string) => string 14 | } 15 | -------------------------------------------------------------------------------- /src/types/ErrorResponse.ts: -------------------------------------------------------------------------------- 1 | export type ErrorResponse = { 2 | ok: false 3 | error: { 4 | name: string 5 | message: string 6 | stack?: string 7 | } 8 | isSyntaxError?: boolean 9 | } 10 | -------------------------------------------------------------------------------- /src/types/LoadQuickJsOptions.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSAsyncWASMModule, QuickJSWASMModule } from 'quickjs-emscripten-core' 2 | import type { Prettify } from './Prettify.js' 3 | 4 | export type LoadQuickJsOptions = Prettify 5 | 6 | export type LoadAsyncQuickJsOptions = Prettify 7 | -------------------------------------------------------------------------------- /src/types/OkResponse.ts: -------------------------------------------------------------------------------- 1 | export type OkResponse = { 2 | ok: true 3 | data: T 4 | } 5 | -------------------------------------------------------------------------------- /src/types/OkResponseCheck.ts: -------------------------------------------------------------------------------- 1 | export type OkResponseCheck = { 2 | ok: true 3 | } 4 | -------------------------------------------------------------------------------- /src/types/Prettify.ts: -------------------------------------------------------------------------------- 1 | export type Prettify = { 2 | [K in keyof T]: T[K] 3 | } & {} 4 | -------------------------------------------------------------------------------- /src/types/RuntimeOptions.ts: -------------------------------------------------------------------------------- 1 | import type { IFs, NestedDirectoryJSON } from 'memfs' 2 | import type { default as TS } from 'typescript' 3 | 4 | export type RuntimeOptions = { 5 | /** 6 | * The maximum time in seconds a script can run. 7 | * Unset or set to 0 for unlimited execution time. 8 | */ 9 | executionTimeout?: number 10 | /** 11 | * Mount a virtual file system 12 | * @link https://github.com/streamich/memfs 13 | */ 14 | mountFs?: NestedDirectoryJSON | IFs 15 | /** 16 | * Mount custom node_modules in a virtual file system 17 | * @link https://github.com/streamich/memfs 18 | */ 19 | nodeModules?: NestedDirectoryJSON 20 | /** 21 | * Enable file capabilities 22 | * If enabled, the package node:fs becomes available 23 | */ 24 | allowFs?: boolean 25 | /** 26 | * Allow code to make http(s) calls. 27 | * When enabled, the global fetch will be available 28 | */ 29 | allowFetch?: boolean 30 | /** 31 | * The custom fetch adapter provided as host function in the QuickJS runtime 32 | */ 33 | fetchAdapter?: typeof fetch 34 | /** 35 | * Includes test framework 36 | * If enabled, the packages chai and mocha become available 37 | * They are registered global 38 | */ 39 | enableTestUtils?: boolean 40 | /** 41 | * Per default, the console log inside of QuickJS is passed to the host console log. 42 | * Here, you can customize the handling and provide your own logging methods. 43 | */ 44 | console?: { 45 | log?: (message?: unknown, ...optionalParams: unknown[]) => void 46 | error?: (message?: unknown, ...optionalParams: unknown[]) => void 47 | warn?: (message?: unknown, ...optionalParams: unknown[]) => void 48 | info?: (message?: unknown, ...optionalParams: unknown[]) => void 49 | debug?: (message?: unknown, ...optionalParams: unknown[]) => void 50 | trace?: (message?: unknown, ...optionalParams: unknown[]) => void 51 | assert?: (condition?: boolean, ...data: unknown[]) => void 52 | count?: (label?: string) => void 53 | countReset?: (label?: string) => void 54 | dir?: (item?: unknown, options?: object) => void 55 | dirxml?: (...data: unknown[]) => void 56 | group?: (...label: unknown[]) => void 57 | groupCollapsed?: (...label: unknown[]) => void 58 | groupEnd?: () => void 59 | table?: (tabularData?: unknown, properties?: string[]) => void 60 | time?: (label?: string) => void 61 | timeEnd?: (label?: string) => void 62 | timeLog?: (label?: string, ...data: unknown[]) => void 63 | clear?: () => void 64 | } 65 | /** 66 | * Key-value list of ENV vars, which should be available in QuickJS 67 | * It is not limited to primitives like string and numbers. 68 | * Objects, arrays and functions can be provided as well. 69 | * 70 | * @example 71 | * ```js 72 | * // in config 73 | * { 74 | * env: { 75 | * My_ENV: 'my var' 76 | * } 77 | * } 78 | * 79 | * // inside of QuickJS 80 | * console.log(env.My_ENV) // outputs: my var 81 | * ``` 82 | */ 83 | env?: Record 84 | /** 85 | * The object is synchronized between host and guest system. 86 | * This means, the values on the host, can be set by the guest system 87 | */ 88 | dangerousSync?: Record 89 | /** 90 | * Transpile all typescript files to javascript file in mountFs 91 | * Requires dependency typescript to be installed 92 | */ 93 | transformTypescript?: boolean 94 | /** 95 | * The Typescript compiler options for transpiling files from typescript to JavaScript 96 | */ 97 | transformCompilerOptions?: TS.CompilerOptions 98 | } 99 | -------------------------------------------------------------------------------- /src/types/SandboxEvalCode.ts: -------------------------------------------------------------------------------- 1 | import type { ContextEvalOptions } from 'quickjs-emscripten-core' 2 | import type { ErrorResponse } from './ErrorResponse.js' 3 | import type { OkResponse } from './OkResponse.js' 4 | import type { Prettify } from './Prettify.js' 5 | 6 | export type SandboxEvalCode = ( 7 | code: string, 8 | filename?: string, 9 | options?: Prettify>, 10 | ) => Promise | ErrorResponse> 11 | -------------------------------------------------------------------------------- /src/types/SandboxFunction.ts: -------------------------------------------------------------------------------- 1 | import type { IFs } from 'memfs' 2 | import type { QuickJSAsyncContext, QuickJSContext } from 'quickjs-emscripten-core' 3 | import type { SandboxEvalCode } from './SandboxEvalCode.js' 4 | import type { SandboxValidateCode } from './SandboxValidateCode.js' 5 | 6 | export type SandboxFunction = (sandbox: { 7 | ctx: QuickJSContext 8 | evalCode: SandboxEvalCode 9 | validateCode: SandboxValidateCode 10 | mountedFs: IFs 11 | }) => Promise 12 | 13 | export type AsyncSandboxFunction = (sandbox: { 14 | ctx: QuickJSAsyncContext 15 | evalCode: SandboxEvalCode 16 | validateCode: SandboxValidateCode 17 | mountedFs: IFs 18 | }) => Promise 19 | -------------------------------------------------------------------------------- /src/types/SandboxValidateCode.ts: -------------------------------------------------------------------------------- 1 | import type { ContextEvalOptions } from 'quickjs-emscripten-core' 2 | import type { ErrorResponse } from './ErrorResponse.js' 3 | import type { OkResponseCheck } from './OkResponseCheck.js' 4 | import type { Prettify } from './Prettify.js' 5 | 6 | export type SandboxValidateCode = ( 7 | code: string, 8 | filename?: string, 9 | options?: Prettify< 10 | Omit & { 11 | executionTimeout?: number 12 | maxStackSize?: number 13 | memoryLimit?: number 14 | } 15 | >, 16 | ) => Promise 17 | -------------------------------------------------------------------------------- /src/types/Serializer.ts: -------------------------------------------------------------------------------- 1 | import type { QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core' 2 | 3 | export type Serializer = (ctx: QuickJSContext, handle: QuickJSHandle) => any 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "ES2022", 5 | "module": "es2022", 6 | "moduleResolution": "node", 7 | "moduleDetection": "force", 8 | "allowJs": true, 9 | 10 | "allowImportingTsExtensions": false, 11 | "verbatimModuleSyntax": false, 12 | "noEmit": false, 13 | 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": false, 18 | 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | 23 | "esModuleInterop": true, 24 | "allowSyntheticDefaultImports": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "resolveJsonModule": true, 27 | "isolatedModules": true, 28 | "declaration": true 29 | }, 30 | "exclude": ["./dist/**"] 31 | } 32 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "name": "@sebastianwessel/quickjs", 4 | "entryPoints": ["./src/index.ts"], 5 | "plugin": ["typedoc-plugin-markdown"], 6 | "out": "website/api", 7 | 8 | "hideGenerator": true, 9 | "includeVersion": true, 10 | "excludeInternal": true, 11 | "excludeExternals": true, 12 | "excludePrivate": true, 13 | "excludeProtected": false, 14 | "exclude": [ 15 | "**/**/*.(test).ts", 16 | "**/*+(index|.spec|.e2e|.test).ts", 17 | "dist/**", 18 | "lib/**", 19 | "example", 20 | "node_modules", 21 | "**/node_modules", 22 | "vendor", 23 | "loadtest.ts" 24 | ], 25 | "gitRevision": "main", 26 | "tsconfig": "typedoc.tsconfig.json" 27 | } 28 | -------------------------------------------------------------------------------- /typedoc.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "ES2022", 5 | "module": "es2022", 6 | "moduleResolution": "node", 7 | "moduleDetection": "force", 8 | "allowJs": true, 9 | 10 | "allowImportingTsExtensions": false, 11 | "verbatimModuleSyntax": false, 12 | "noEmit": false, 13 | 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": false, 18 | 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | 23 | "esModuleInterop": true, 24 | "allowSyntheticDefaultImports": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "resolveJsonModule": true, 27 | "isolatedModules": true, 28 | "declaration": true 29 | }, 30 | "exclude": ["./dist/**", "./example", "./vendor", "loadtest.ts", "**/node_modules"] 31 | } 32 | -------------------------------------------------------------------------------- /vendor.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path' 2 | 3 | const testRunnerResult = await Bun.build({ 4 | entrypoints: ['./vendor/testrunner/testRunner.ts'], 5 | format: 'esm', 6 | minify: true, 7 | }) 8 | 9 | for (const res of testRunnerResult.outputs) { 10 | const content = await res.text() 11 | 12 | Bun.write(join('src', 'modules', 'build', 'test-lib.js'), content) 13 | 14 | console.info('test lib generated') 15 | } 16 | -------------------------------------------------------------------------------- /vendor/testrunner/types.ts: -------------------------------------------------------------------------------- 1 | // testTypes.ts 2 | 3 | /** 4 | * Represents a test function that can be synchronous or asynchronous. 5 | */ 6 | export type TestFunction = () => void | Promise 7 | 8 | /** 9 | * Represents a hook function that can be synchronous or asynchronous. 10 | */ 11 | export type HookFunction = () => void | Promise 12 | 13 | /** 14 | * Represents a test with its title and function. 15 | */ 16 | export interface Test { 17 | title: string 18 | fn: TestFunction 19 | } 20 | 21 | /** 22 | * Represents a hook with its function. 23 | */ 24 | export interface Hook { 25 | fn: HookFunction 26 | } 27 | 28 | /** 29 | * Represents the result of an individual hook execution. 30 | */ 31 | export interface HookResult { 32 | title: string // The title or description of the hook. 33 | passed: boolean // Indicates whether the hook passed or failed. 34 | error?: any // An optional error object if the hook failed. 35 | } 36 | 37 | /** 38 | * Represents the result of an individual test execution. 39 | */ 40 | export interface TestResult { 41 | title: string // The title or description of the test. 42 | passed: boolean // Indicates whether the test passed or failed. 43 | error?: any // An optional error object if the test failed. 44 | } 45 | 46 | /** 47 | * Represents the result of a test suite execution, including its hooks, tests, and nested suites. 48 | */ 49 | export interface SuiteResult { 50 | title: string // The title or description of the suite. 51 | beforeAll: HookResult[] // Results of beforeAll hooks. 52 | beforeEach: HookResult[] // Results of beforeEach hooks. 53 | tests: TestResult[] // Results of individual tests. 54 | afterEach: HookResult[] // Results of afterEach hooks. 55 | afterAll: HookResult[] // Results of afterAll hooks. 56 | suites: SuiteResult[] // Results of nested suites. 57 | passed: boolean // Indicates whether the test passed or failed. 58 | passedSuites: number 59 | failedSuites: number 60 | passedTests: number 61 | failedTests: number 62 | totalTests: number 63 | totalSuites: number 64 | } 65 | 66 | /** 67 | * Represents a test suite, including its hooks, tests, and nested suites. 68 | */ 69 | export interface Suite { 70 | title: string // The title or description of the suite. 71 | tests: Test[] // Array of test functions in the suite. 72 | beforeAll: Hook[] // Array of beforeAll hooks. 73 | afterAll: Hook[] // Array of afterAll hooks. 74 | beforeEach: Hook[] // Array of beforeEach hooks. 75 | afterEach: Hook[] // Array of afterEach hooks. 76 | suites: Suite[] // Array of nested suites. 77 | } 78 | -------------------------------------------------------------------------------- /website/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from 'vitepress' 2 | import DefaultTheme from 'vitepress/theme' 3 | // https://vitepress.dev/guide/custom-theme 4 | import { h } from 'vue' 5 | import './style.css' 6 | import Playground from './components/Playground.vue' 7 | 8 | export default { 9 | extends: DefaultTheme, 10 | Layout: () => { 11 | return h(DefaultTheme.Layout, null, { 12 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 13 | }) 14 | }, 15 | enhanceApp({ app, router, siteData }) { 16 | // ... 17 | app.component('Playground', Playground) 18 | }, 19 | } satisfies Theme 20 | -------------------------------------------------------------------------------- /website/blog/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Blog 4 | description: QuickJS Blog 5 | --- -------------------------------------------------------------------------------- /website/blog/release.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Announcing QuickJS 1.0.0" 3 | description: "Version 1.0 of the QuickJS package is released. A TypeScript package that allows you to safely execute JavaScript code within a WebAssembly sandbox using the QuickJS engine." 4 | date: 2024-07-07 12:00:00 5 | categories: release 6 | --- 7 | 8 | ## Announcing QuickJS 1.0.0 9 | 10 | We are excited to announce the release of **QuickJS 1.0.0**, a TypeScript package that allows you to safely execute JavaScript code within a WebAssembly sandbox using the QuickJS engine. Perfect for isolating and running untrusted code securely, QuickJS leverages the lightweight and fast QuickJS engine compiled to WebAssembly, providing a robust environment for code execution. 11 | 12 | ### Key Features 13 | 14 | - **Security**: Run untrusted JavaScript code in a safe, isolated environment. 15 | - **File System**: Can mount a virtual file system. 16 | - **Custom Node Modules**: Custom node modules are mountable. 17 | - **Fetch Client**: Can provide a fetch client to make http(s) calls. 18 | - **Test-Runner**: Includes a test runner and chai-based `expect`. 19 | - **Performance**: Benefit from the lightweight and efficient QuickJS engine. 20 | - **Versatility**: Easily integrate with existing TypeScript projects. 21 | - **Simplicity**: User-friendly API for executing and managing JavaScript code in the sandbox. 22 | 23 | ### Full Documentation and Examples 24 | 25 | - **[View the full documentation](https://sebastianwessel.github.io/quickjs/)** 26 | - **[Find examples in the repository](https://github.com/sebastianwessel/quickjs/tree/main/example)** 27 | 28 | ### Basic Usage Example 29 | 30 | Here's a simple example of how to use the package: 31 | 32 | ```typescript 33 | import { quickJS } from '@sebastianwessel/quickjs' 34 | 35 | // General setup like loading and init of the QuickJS wasm 36 | // It is a resource-intensive job and should be done only once if possible 37 | const { createRuntime } = await quickJS() 38 | 39 | // Create a runtime instance each time a js code should be executed 40 | const { evalCode } = await createRuntime({ 41 | allowFetch: true, // inject fetch and allow the code to fetch data 42 | allowFs: true, // mount a virtual file system and provide node:fs module 43 | env: { 44 | MY_ENV_VAR: 'env var value' 45 | }, 46 | }) 47 | 48 | const result = await evalCode(` 49 | import { join } as path from 'path' 50 | 51 | const fn = async () => { 52 | console.log(join('src','dist')) // logs "src/dist" on host system 53 | 54 | console.log(env.MY_ENV_VAR) // logs "env var value" on host system 55 | 56 | const url = new URL('https://example.com') 57 | 58 | const f = await fetch(url) 59 | 60 | return f.text() 61 | } 62 | 63 | export default await fn() 64 | `) 65 | 66 | console.log(result) // { ok: true, data: '\n\n[....]\n' } 67 | ``` 68 | 69 | ### Credits 70 | 71 | This library is based on: 72 | 73 | - [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) 74 | - [quickjs-emscripten-sync](https://github.com/reearth/quickjs-emscripten-sync) 75 | - [memfs](https://github.com/streamich/memfs) 76 | 77 | Tools used: 78 | 79 | - [Bun](https://bun.sh) 80 | - [Biome](https://biomejs.dev) 81 | - [Hono](https://hono.dev) 82 | - [poolifier-web-worker](https://github.com/poolifier/poolifier-web-worker) 83 | - [tshy](https://github.com/isaacs/tshy) 84 | - [autocannon](https://github.com/mcollina/autocannon) 85 | 86 | ### License 87 | 88 | This project is licensed under the MIT License. 89 | 90 | --- 91 | 92 | QuickJS 1.0.0 is ideal for developers looking to execute JavaScript code securely within a TypeScript application, ensuring both performance and safety with the QuickJS WebAssembly sandbox. Try it out and let us know what you think! 93 | 94 | [Check out QuickJS on GitHub](https://github.com/sebastianwessel/quickjs) 95 | -------------------------------------------------------------------------------- /website/docs/basic-understanding.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Understanding 3 | description: Get a basic understanding on how to the QuickJS module works 4 | order: 10 5 | --- 6 | 7 | # Basic Understanding 8 | 9 | This documentation provides essential information to help you avoid common pitfalls when working with QuickJS WebAssembly runtime. The terms "host" and "guest" are used to describe your main application and the QuickJS runtime, respectively. 10 | 11 | ::: warning 12 | As the QuickJS sandbox is running via WebAssembly, the JavaScript eventloop gets blocked by the WebAssembly execution. 13 | ::: 14 | 15 | ## Synchronous Execution 16 | 17 | ### 🚫 Blocking the JavaScript Event Loop 18 | 19 | When the `evalCode` method is called on the host, the event loop of the host system is blocked until the method returns. 20 | 21 | Here is an example of how the host system can be blocked 🔥: 22 | 23 | ```typescript 24 | import { loadQuickJs } from '@sebastianwessel/quickjs' 25 | 26 | const { runSandboxed } = await loadQuickJs() 27 | 28 | setInterval(() => { 29 | console.log('y') 30 | }, 2000) 31 | 32 | console.log('start') 33 | 34 | const code = ` 35 | const fn = () => new Promise(() => { 36 | while(true) { 37 | } 38 | }) 39 | export default await fn() 40 | ` 41 | 42 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code)) 43 | ``` 44 | 45 | You might expect that this code does not block the host system, but it does, even with `await evalCode`. The host system must wait for the guest system to return a value. In this example, the value is never returned because of the endless while-loop. 46 | 47 | ### ⏳ Setting Execution Timeouts 48 | 49 | **❗ Set Execution Timeouts if Possible** 50 | It is highly recommended to set a default timeout value to avoid blocking the host system indefinitely. The execution timeout can be set in the options of `runSandboxed`. Setting the `executionTimeout` to `0` or `undefined` disables the execution timeout. 51 | 52 | Timeout values are in milliseconds. 53 | 54 | **Please see: [Runtime Options - Execution Limits](./runtime-options.md)** 55 | 56 | ### 👷 Workers and Threads 57 | 58 | It is **highly recommended** to run the guest system in separate workers or threads rather than the main thread. This approach has several critical benefits: 59 | 60 | 1. It ensures that the main event loop is not blocked. 61 | 2. Multiple workers can boost performance. 62 | 3. The host application can terminate a single worker anytime. If the guest system exceeds the maximum runtime, restarting the worker ensures a clean state. 63 | 64 | ## Asynchronous Behavior 65 | 66 | The provided QuickJS WebAssembly runtime does not have an internal event loop like a regular runtime. Instead, the host system must trigger the loop for any provided promises. This library starts an interval on the host that triggers `executePendingJobs` in QuickJS. The interval is automatically stopped and removed when no longer needed. 67 | 68 | When a promise is provided by the host and used by the client, the client executes until it reaches the promise. If the promise is not settled, the QuickJS runtime pauses execution. Once the promise is settled, the host needs to call `executePendingJobs` to instruct QuickJS to resume execution. 69 | -------------------------------------------------------------------------------- /website/docs/credits.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Credits 3 | description: Credits to other projects 4 | order: 90000 5 | --- 6 | 7 | # Credits 8 | 9 | This lib is mainly based on: 10 | 11 | - [QuickJS](https://bellard.org/quickjs/) 12 | - [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) 13 | - [memfs](https://github.com/streamich/memfs) 14 | - [Chai](https://www.chaijs.com) 15 | - [node-rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) 16 | 17 | Tools used: 18 | 19 | - [Bun](https://bun.sh) 20 | - [Biome](https://biomejs.dev) 21 | - [Hono](https://hono.dev) 22 | - [poolifier-web-worker](https://github.com/poolifier/poolifier-web-worker) 23 | - [tshy](https://github.com/isaacs/tshy) 24 | - [autocannon](https://github.com/mcollina/autocannon) 25 | -------------------------------------------------------------------------------- /website/docs/file-system.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: File System 3 | description: Learn, how you can create and mount your own virtual file system into the QuickJS runtime 4 | order: 50 5 | --- 6 | 7 | # Custom File System 8 | 9 | Every QuickJS sandbox has its own virtual file system. The file system is based on [memfs](https://github.com/streamich/memfs). It holds the `node_modules` and allows for the inclusion of custom files in the script running in the QuickJS sandbox. 10 | 11 | For detailed information on providing a custom node module, please refer to the documentation: [Custom Node Modules](./module-resolution/custom-modules.md). 12 | 13 | The code provided to the `evalCode` function is treated as file `src/index.js`. This means when you use relative files, they are relative to `src/index.js`. 14 | 15 | ## Providing Files 16 | 17 | To provide a custom file system, a nested structure is used. The structure is mounted below `/`. 18 | 19 | ### Example 20 | 21 | ```typescript 22 | const options:SandboxOptions = { 23 | mountFs: { 24 | src: { 25 | 'custom.js': `export const relativeImportFunction = ()=>'Hello from relative import function'`, 26 | }, 27 | 'fileInRoot.txt': 'Some text content' 28 | }, 29 | }; 30 | ``` 31 | 32 | In this example, a JavaScript file is added to the virtual file system at `/src/custom.js`, and a text file is added to the root `/fileInRoot.txt`. 33 | 34 | ## Importing Files 35 | 36 | JavaScript files can be imported as usual. Importing files is possible even if the [runtime option](./runtime-options.md) `allowFs` is set to `false`. This option only refers to the functions and methods of the `fs` package. Regular js imports are not effected. 37 | 38 | This means, the provided js file from the example above, can be used like this: 39 | 40 | ```js 41 | import { customFn } from './custom.js' 42 | 43 | // [...] 44 | ``` 45 | 46 | ## Direct File Access 47 | 48 | To use file handling methods from `node:fs`, the [runtime option](./runtime-options.md) `allowFs` must be set to `true`. If `allowFs` is not enabled, every method from `node:fs` will throw an error. For security reasons, the `allowFs` option is set to `false` by default. 49 | 50 | Currently, only basic file operations on text files are supported. For more information, see [Node compatibility - node:fs](module-resolution/node-compatibility.md). 51 | -------------------------------------------------------------------------------- /website/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | description: QuickJS is a small and embeddable JavaScript engine. 4 | order: 0 5 | --- 6 | 7 | # Usage 8 | 9 | ## Installation 10 | 11 | Install the package using npm, yarn, or bun: 12 | 13 | ::: code-group 14 | 15 | ```sh [npm] 16 | npm install @sebastianwessel/quickjs 17 | ``` 18 | 19 | ```sh [bun] 20 | bun add @sebastianwessel/quickjs 21 | ``` 22 | 23 | ```sh [yarn] 24 | yarn add @sebastianwessel/quickjs 25 | ``` 26 | 27 | ::: 28 | 29 | This package is also available at [jsr.io/@sebastianwessel/quickjs](https://jsr.io/@sebastianwessel/quickjs) 30 | 31 | ### QuickJS Wasm Variant 32 | 33 | This library does not include the QuickJS wasm file. It must be installed separat. 34 | 35 | The most straight forward variant is `@jitl/quickjs-ng-wasmfile-release-sync` 36 | 37 | ::: code-group 38 | 39 | ```sh [npm] 40 | npm install @jitl/quickjs-ng-wasmfile-release-sync 41 | ``` 42 | 43 | ```sh [bun] 44 | bun add @jitl/quickjs-ng-wasmfile-release-sync 45 | ``` 46 | 47 | ```sh [yarn] 48 | yarn add @jitl/quickjs-ng-wasmfile-release-sync 49 | ``` 50 | 51 | ::: 52 | 53 | Please see [github.com/justjake/quickjs-emscripten](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten-core/README.md) to find the variant which fits best for your needs. 54 | 55 | ## Backend Usage 56 | 57 | Here's a simple example of how to use the package: 58 | 59 | ```typescript 60 | import { type SandboxOptions, loadQuickJs } from '../../src/index.js' 61 | 62 | // General setup like loading and init of the QuickJS wasm 63 | // It is a ressource intensive job and should be done only once if possible 64 | const { runSandboxed } = await loadQuickJs() 65 | 66 | const options: SandboxOptions = { 67 | allowFetch: true, // inject fetch and allow the code to fetch data 68 | allowFs: true, // mount a virtual file system and provide node:fs module 69 | env: { 70 | MY_ENV_VAR: 'env var value', 71 | }, 72 | } 73 | 74 | const code = ` 75 | import { join } from 'path' 76 | 77 | const fn = async ()=>{ 78 | console.log(join('src','dist')) // logs "src/dist" on host system 79 | 80 | console.log(env.MY_ENV_VAR) // logs "env var value" on host system 81 | 82 | const url = new URL('https://example.com') 83 | 84 | const f = await fetch(url) 85 | 86 | return f.text() 87 | } 88 | 89 | export default await fn() 90 | ` 91 | 92 | const result = await runSandboxed(async ({ evalCode }) => { 93 | return evalCode(code) 94 | }, options) 95 | 96 | console.log(result) 97 | // { ok: true, data: '\n\n[....]\n' } 98 | ``` 99 | 100 | ### Cloudflare Workers 101 | 102 | Cloudflare workers have some limitations regarding bundling. The developers of the underlaying quickjs-emscripten library, already solved this. 103 | 104 | [github.com/justjake/quickjs-emscripten/tree/main/examples/cloudflare-workers](https://github.com/justjake/quickjs-emscripten/tree/main/examples/cloudflare-workers) 105 | 106 | This library will be aligned soon, to support cloudflare as well. 107 | 108 | ## Usage in Browser 109 | 110 | Here is the most minimal example on how to use this library in the browser. 111 | You need to ensure, that the webassembly file can be loaded correctly. Therefore, you need to add this as parameter to the `quickJS` call. 112 | 113 | Using `fetch`is possible, but there are the same restrictions as in any other browser usage (CORS & co). 114 | 115 | ```html 116 | 117 | 118 | 133 | ``` 134 | 135 | Please see the examples in the repository. 136 | -------------------------------------------------------------------------------- /website/docs/module-resolution/custom-modules.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Modules 3 | description: Learn, how you can provide your own custom node modules to the QuickJS runtime 4 | order: 4040 5 | --- 6 | 7 | # Custom Node Modules 8 | 9 | This library allows the use of custom Node.js packages within the QuickJS runtime. Node modules are loaded into a virtual file system, which is then exposed to the client system. This ensures complete isolation from the underlying host system. 10 | 11 | ## Preparing a Custom Module 12 | 13 | To work around some of these limitations, the custom Node.js module should be a single ESM file, including all dependencies (except Node.js core modules). 14 | 15 | Tools like [Bun](https://bun.sh) and [esbuild](https://esbuild.github.io) make this process easy for many modules. 16 | 17 | ### Bun 18 | 19 | Bun is used in the development of this library. Working examples are available in the repository. 20 | 21 | To create a bundled module as a single file with dependencies, you need an entry file: 22 | 23 | ```typescript 24 | // entry.ts 25 | 26 | // Import the module or functionality to bundle 27 | import { expect } from 'chai'; 28 | 29 | // Optional custom code 30 | 31 | export { expect }; 32 | ``` 33 | 34 | Here, only the `expect` part gets bundled, but you can also use `export * from 'chai'` to include everything. 35 | 36 | A configuration file is recommended to repeat the build step easily: 37 | 38 | ```typescript 39 | // build.ts 40 | const testRunnerResult = await Bun.build({ 41 | entrypoints: ['./entry.ts'], 42 | format: 'esm', 43 | minify: true, 44 | outdir: './vendor' 45 | }); 46 | ``` 47 | 48 | Build the custom module file with `bun ./build.ts`. The generated file should be in `./vendor`. 49 | 50 | For more information, visit the [official Bun website](https://bun.sh/docs/bundler). 51 | 52 | ### Esbuild 53 | 54 | Using esbuild works similarly to Bun. An entry file is required, and a config file is recommended. 55 | 56 | The entry file will be the same as for Bun. The config file needs to be adapted (with a `.mjs` extension): 57 | 58 | ```typescript 59 | // build.mjs 60 | import * as esbuild from 'esbuild'; 61 | 62 | await esbuild.build({ 63 | entryPoints: ['./entry.ts'], 64 | bundle: true, 65 | outfile: './vendor/my-package.js', 66 | }); 67 | ``` 68 | 69 | Build the custom module file with `node ./build.mjs`. The generated file should be in `./vendor`. 70 | 71 | For more information, visit the [official esbuild website](https://esbuild.github.io/getting-started/). 72 | 73 | ## Using a Custom Module 74 | 75 | A virtual file system is used to provide Node.js modules to the client system. To provide custom modules, a nested structure is used. 76 | 77 | The root key is the package name, and the child key represents the index file, which should be `index.js` by default. The custom module itself is provided as a raw string. 78 | 79 | Example: 80 | 81 | ```typescript 82 | import { join } from 'node:path'; 83 | import { dirname } from 'node:path'; 84 | import { fileURLToPath } from 'node:url'; 85 | import { type SandboxOptions, loadQuickJs } from '@sebastianwessel/quickjs'; 86 | 87 | // General setup, such as loading and initializing the QuickJS WASM 88 | // This is a resource-intensive job and should be done only once if possible 89 | const { runSandboxed } = await loadQuickJs() 90 | 91 | const __dirname = dirname(fileURLToPath(import.meta.url)); 92 | const customModuleHostLocation = join(__dirname, './custom.js'); 93 | 94 | const options:SandboxOptions = { 95 | nodeModules: { 96 | // Module name 97 | 'custom-module': { 98 | // Key must be index.js, value is the file content of the module 99 | 'index.js': await Bun.file(customModuleHostLocation).text(), 100 | }, 101 | }, 102 | }; 103 | 104 | const code = ` 105 | import { customFn } from 'custom-module'; 106 | 107 | const result = customFn(); 108 | 109 | console.log(result); 110 | 111 | export default result; 112 | ` 113 | 114 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 115 | 116 | 117 | console.log(result); // { ok: true, data: 'Hello from the custom module' } 118 | ``` 119 | -------------------------------------------------------------------------------- /website/docs/module-resolution/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Modules 3 | description: Learn, how you can provide your own custom node modules to the QuickJS runtime 4 | order: 40 5 | --- 6 | 7 | # Using Modules 8 | 9 | JavaScript and TypeScript code often relies on modules to provide functionality. In general, modules can be used normally with the `import` statement. 10 | 11 | This library includes a set of modules intended to be as compatible as possible with the Node.js API. However, full compatibility is not guaranteed. In particular, not all Node.js modules are available. For more details, see [Node Compatibility](./node-compatibility.md). 12 | 13 | ## Path Normalizer 14 | 15 | The path normalizer determines the location of a module to be imported. 16 | 17 | It takes two input parameters: 18 | 1. The location of the file containing the import statement. 19 | 2. The requested import path. 20 | 21 | This allows it to resolve relative paths and rewrite imports as needed. 22 | 23 | The normalized path is then passed as input to the module loader. 24 | 25 | ## Module Loaders 26 | 27 | Module loaders are responsible for retrieving the content (source code) of an imported module. 28 | 29 | ### Default (Synchronous) 30 | 31 | The standard runtime does not support asynchronous module loading. This means that asynchronous functions like `fetch` cannot be used within the module loader. 32 | 33 | As a result, dynamic imports at runtime—e.g., from [esm.sh](https://esm.sh/) or similar services—are not possible. 34 | 35 | Additionally, the standard module loader is a simple implementation that reads modules from the virtual file system. 36 | 37 | #### Limitations 38 | 39 | - Modules do not have access to native functions. 40 | - Modules must be plain JavaScript. 41 | - Relative imports within a single module are not supported (though modules can still import other modules). 42 | - The `package.json` file is not used: 43 | - Determining the root file via `package.json` is not supported. 44 | - Module (sub-)dependencies are not automatically installed or handled. 45 | - Modules with multiple exports defined in `package.json` must be managed manually. 46 | - Only a small subset of Node.js core modules is available, so not every module will work out of the box. 47 | 48 | These limitations mean that all required modules must be available beforehand, and the module file structure must be straightforward. 49 | 50 | ### Asynchronous 51 | 52 | This library supports asynchronous module loading — see [github.com/justjake/quickjs-emscripten](https://github.com/justjake/quickjs-emscripten?tab=readme-ov-file#asyncify). 53 | 54 | The asynchronous variant allows the module loader to function asynchronously, enabling dynamic dependency loading at runtime from sources such as [esm.sh](https://esm.sh/) or similar services. 55 | -------------------------------------------------------------------------------- /website/docs/sandboxed-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Code in the Sandbox 3 | description: Compile and execute javascript or typescript code in the sandbox 4 | order: 20 5 | --- 6 | 7 | # Code in the Sandbox 8 | 9 | ## Execute JavaScript in the Sandbox 10 | 11 | The most common use case is, to execute a give JavaScript code in the QuickJS webassembly sandbox. 12 | The `evalCode` function described below, is intend to be used, when a given JavaScript code should be executed and optional a result value is returned. 13 | It is recommended, to always return a value, for better validation on the host side, that the code was executed as expected. 14 | 15 | In the sandbox, the executed code "lives" in `/src/index.js`. If custom files are added via configuration, source files should be placed below `/src`. Nested directories are supported. 16 | 17 | ```typescript 18 | import { type SandboxOptions, loadQuickJs } from '@sebastianwessel/quickjs' 19 | 20 | const { runSandboxed } = await loadQuickJs() 21 | 22 | const options:SandboxOptions = { 23 | allowFetch: true, // inject fetch and allow the code to fetch data 24 | allowFs: true, // mount a virtual file system and provide node:fs module 25 | env: { 26 | MY_ENV_VAR: 'env var value' 27 | }, 28 | } 29 | 30 | 31 | const code = ` 32 | import { join } as path from 'path' 33 | 34 | const fn = async ()=>{ 35 | console.log(join('src','dist')) // logs "src/dist" on host system 36 | 37 | console.log(env.MY_ENV_VAR) // logs "env var value" on host system 38 | 39 | const url = new URL('https://example.com') 40 | 41 | const f = await fetch(url) 42 | 43 | return f.text() 44 | } 45 | 46 | export default await fn() 47 | ` 48 | const result = await runSandboxed(async ({ evalCode }) => evalCode(code), options) 49 | 50 | console.log(result) // { ok: true, data: '\n\n[....]\n' } 51 | ``` 52 | 53 | The `evalCode` functions returns a unified result object, and does not throw. 54 | 55 | For further information, it is hight recommended to [read "Basic Understanding"](./basic-understanding.md). 56 | 57 | ## Only Validation Check of the Code 58 | 59 | There are use cases, where the given JavaScript code should not be executed, but validated for technical correctness. 60 | To only test the code, without executing the code, the function `validateCode` should be used. 61 | 62 | ```typescript 63 | import { loadQuickJs } from '@sebastianwessel/quickjs' 64 | 65 | const { runSandboxed } = await loadQuickJs() 66 | 67 | const code = ` 68 | const value ='missing string end 69 | export default value 70 | ` 71 | 72 | const result = await runSandboxed(async ({ validateCode }) => validateCode(code)) 73 | 74 | console.log(result) 75 | //{ 76 | // ok: false, 77 | // error: { 78 | // message: 'unexpected end of string', 79 | // name: 'SyntaxError', 80 | // stack: ' at /src/index.js:2:1\n', 81 | // }, 82 | // isSyntaxError: true, 83 | //} 84 | ``` 85 | 86 | The `validateCode` functions returns a unified result object, and does not throw. 87 | 88 | For further information, it is hight recommended to [read "Basic Understanding"](./basic-understanding.md). -------------------------------------------------------------------------------- /website/docs/typescript-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Typescript Code 3 | description: Execute Typescript code in the QuickJS sandbox 4 | order: 70 5 | --- 6 | 7 | ## Execute TypeScript in the Sandbox 8 | 9 | Executing TypeScript in the sandboxed runtime, is similar to executing JavaScript. An additional transpile step will be applied to the given code. Additionally each file below `/src` with file extension`.ts` in the custom file system will be transpiled. 10 | 11 | The TypeScript code is only transpiled, but not type-checked! 12 | If checking types is required, it should be done and handled, before using this library. 13 | 14 | **Requirements:** 15 | 16 | - optional dependency package `typescript` must be installed on the host system 17 | - `createRuntime` option `transformTypescript` must be set to `true` 18 | 19 | Example: 20 | 21 | ```typescript 22 | import { quickJS } from '@sebastianwessel/quickjs' 23 | 24 | const { createRuntime } = await quickJS() 25 | 26 | const options:SandboxOptions = { 27 | transformTypescript: true, // [!code ++] enable typescript support 28 | mountFs: { 29 | src: { 30 | 'test.ts': `export const testFn = (value: string): string => { 31 | console.log(value) 32 | return value 33 | }`, 34 | }, 35 | }, 36 | } 37 | 38 | 39 | const result = await evalCode(` 40 | import { testFn } from './test.js' 41 | 42 | const t = (value:string):number=>value.length 43 | 44 | console.log(t('abc')) 45 | 46 | export default testFn('hello') 47 | `) 48 | 49 | console.log(result) // { ok: true, data: 'hello' } 50 | // console log on host: 51 | // 3 52 | // hello 53 | ``` 54 | 55 | ## Browser 56 | 57 | The Typescript package will be imported automatically when it is required. The import defaults to `typescript`, which is expected in a backend environment. 58 | The import can be changed in the [Runtime Options](./runtime-options.md) via the `typescriptImportFile` option. 59 | 60 | ## Compiler Options 61 | 62 | For most use cases, the default compiler options should fit the requirements. 63 | 64 | ```ts 65 | const compilerOptions: TS.CompilerOptions = { 66 | module: 99, // ESNext 67 | target: 99, // ES2023 68 | allowJs: true, 69 | skipLibCheck: true, 70 | esModuleInterop: true, 71 | strict: false, 72 | allowSyntheticDefaultImports: true, 73 | ...options, 74 | } 75 | ``` 76 | 77 | If there is a need to use custom settings, please see section `transformCompilerOptions` in [Runtime Options](./runtime-options.md). 78 | -------------------------------------------------------------------------------- /website/imprint.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Imprint 3 | description: Imprint 4 | --- 5 | 6 | 21 | 22 | # Imprint 23 | 24 | 25 | 26 | **Sebastian Wessel** 27 | 28 | Finkenweg 4B, 29 | 63755 Alzenau 30 | Germany 31 | 32 | [mail@sebastianwessel.de](mailto:mail@sebastianwessel.de) 33 | -------------------------------------------------------------------------------- /website/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # https://vitepress.dev/reference/default-theme-home-page 4 | layout: home 5 | title: QuickJS Sandbox - Execute JavaScript and TypeScript code safe and secure 6 | 7 | 8 | hero: 9 | name: "QuickJS Sandbox" 10 | text: "Execute JavaScript and TypeScript code safe and secure" 11 | tagline: Try out now! 12 | image: /logo.png 13 | actions: 14 | - theme: brand 15 | text: 📒 Documentation 16 | link: /docs 17 | - theme: alt 18 | text: 🔥 Use Cases 19 | link: /use-cases 20 | - theme: alt 21 | text: Try Out Online 22 | link: /playground.md 23 | 24 | features: 25 | - title: AI Generated Code 26 | details: Execute AI generated code in a sandboxed environment. 27 | link: /use-cases/ai-generated-code 28 | icon: 🤖 29 | linkText: Read more 30 | - title: User Provided Code 31 | details: Execute custom code which is provided by users. 32 | link: /use-cases/user-generated-code 33 | icon: 👨 34 | linkText: Read more 35 | - title: Serverside Rendering (SSR) 36 | details: Render HTML in a secure sandbox 37 | link: /use-cases/serverside-rendering 38 | icon: 👨‍🎨 39 | linkText: Read more 40 | --- 41 | 42 | -------------------------------------------------------------------------------- /website/playground.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Playground 3 | description: Try out the QuickJS sandbox in your browser online. 4 | aside: false 5 | --- 6 | 7 | # Playground 8 | 9 | Try out the QuickJS sandbox in your browser online. 10 | 11 | -------------------------------------------------------------------------------- /website/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/.DS_Store -------------------------------------------------------------------------------- /website/public/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/.nojekyll -------------------------------------------------------------------------------- /website/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/apple-touch-icon.png -------------------------------------------------------------------------------- /website/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /website/public/example-request.html: -------------------------------------------------------------------------------- 1 | 2 | @sebastianwessel/quickjs 3 | Hello World 4 | -------------------------------------------------------------------------------- /website/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/favicon-32x32.png -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/logo.png -------------------------------------------------------------------------------- /website/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/mstile-150x150.png -------------------------------------------------------------------------------- /website/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/og.jpg -------------------------------------------------------------------------------- /website/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /website/public/use-case-ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/use-case-ai.jpg -------------------------------------------------------------------------------- /website/public/use-case-ssr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/use-case-ssr.jpg -------------------------------------------------------------------------------- /website/public/use-case-user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianwessel/quickjs/6e049e4ec6e27a40b7873538e8b182497a3fa35e/website/public/use-case-user.jpg -------------------------------------------------------------------------------- /website/use-cases/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | title: QuickJS Use Cases 5 | description: A closer look at how QuickJS can be used in different scenarios. 6 | 7 | hero: 8 | name: "Use Cases" 9 | text: "A closer look at how QuickJS can be used in different scenarios." 10 | tagline: Choose a use case to learn more. 11 | image: /logo.png 12 | 13 | 14 | features: 15 | - title: Execute AI Generated Code 16 | details: Execute AI generated code in a sandboxed environment. 17 | link: /use-cases/ai-generated-code 18 | icon: 🤖 19 | linkText: Read more 20 | - title: Execute User Provided Code 21 | details: Execute custom code which is provided by users. 22 | link: /use-cases/user-generated-code 23 | icon: 👨 24 | linkText: Read more 25 | - title: Serverside Rendering (SSR) 26 | details: Render HTML in a secure sandbox 27 | link: /use-cases/serverside-rendering 28 | icon: 👨‍🎨 29 | linkText: Read more 30 | --- 31 | 32 | --------------------------------------------------------------------------------