├── .babelrc ├── .circleci └── config.yml ├── .eslintrc.json ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierrc ├── .release-please-manifest.json ├── .sandworm.config.json ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LIBRARY.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cli ├── frontend │ ├── build │ │ ├── asset-manifest.json │ │ ├── index.html │ │ ├── logo.png │ │ └── static │ │ │ └── js │ │ │ ├── bundle.js │ │ │ └── bundle.js.map │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── index.html │ │ └── logo.png │ ├── scripts │ │ └── watch.js │ ├── src │ │ ├── components │ │ │ ├── Activity.jsx │ │ │ ├── ConfigModal.jsx │ │ │ ├── Modules.jsx │ │ │ ├── Permission.jsx │ │ │ ├── PermissionList.jsx │ │ │ ├── Permissions.jsx │ │ │ └── Title.jsx │ │ ├── context.js │ │ ├── hooks │ │ │ ├── useESCPress.js │ │ │ └── useLocalStorage.js │ │ ├── index.css │ │ ├── index.js │ │ ├── libraries.js │ │ └── library │ ├── tailwind.config.js │ └── yarn.lock ├── index.js └── screenshot.png ├── commitlint.config.js ├── dist ├── index.d.ts ├── index.js └── index.js.LICENSE.txt ├── logo-dark.png ├── logo-light.png ├── package-permissions.json ├── package.json ├── release-please-config.json ├── scripts ├── build-library-md.js └── build-library-min.js ├── src ├── index.js ├── library │ ├── builder.js │ ├── node.json │ ├── node.min.json │ ├── web.json │ └── web.min.json ├── logger.js ├── module.js ├── patch.js ├── platform.js ├── source.js ├── stack.js └── track.js ├── tests ├── configs │ ├── jest.capture.config.js │ ├── jest.enforce.config.js │ ├── jest.unit.jsdom.config.js │ ├── jest.unit.node.config.js │ └── playwright.config.js ├── node │ ├── Function.capture.test.js │ ├── Function.enforce.test.js │ ├── bind.capture.test.js │ ├── bind.enforce.test.js │ ├── child_process.capture.test.js │ ├── child_process.enforce.test.js │ ├── cluster.capture.test.js │ ├── cluster.enforce.test.js │ ├── dgram.capture.test.js │ ├── dgram.enforce.test.js │ ├── dns.capture.test.js │ ├── dns.enforce.test.js │ ├── eval.capture.test.js │ ├── eval.enforce.test.js │ ├── fetch.capture.test.js │ ├── fetch.enforce.test.js │ ├── fs.capture.test.js │ ├── fs.enforce.test.js │ ├── fs │ │ ├── async.js │ │ ├── promises.js │ │ ├── sync.js │ │ ├── test-dir │ │ │ └── .gitkeep │ │ └── test.txt │ ├── http.capture.test.js │ ├── http.enforce.test.js │ ├── http2.capture.test.js │ ├── http2.enforce.test.js │ ├── https.capture.test.js │ ├── https.enforce.test.js │ ├── inspector.capture.test.js │ ├── inspector.enforce.test.js │ ├── log.js │ ├── net.capture.test.js │ ├── net.enforce.test.js │ ├── os.capture.test.js │ ├── os.enforce.test.js │ ├── process.capture.test.js │ ├── process.enforce.test.js │ ├── timers.capture.test.js │ ├── timers.enforce.test.js │ ├── tls.capture.test.js │ ├── tls.enforce.test.js │ ├── trace_events.capture.test.js │ ├── trace_events.enforce.test.js │ ├── v8.capture.test.js │ ├── v8.enforce.test.js │ ├── vm.capture.test.js │ └── vm.enforce.test.js ├── unit │ ├── bundle-generator │ │ ├── package.json │ │ ├── src │ │ │ └── index.js │ │ └── yarn.lock │ ├── inline-nosources-cheap-source-map │ │ └── main.js │ ├── logger.test.js │ ├── module.test.js │ ├── nosources-cheap-source-map │ │ ├── main.js │ │ └── main.js.map │ ├── patch.test.js │ ├── platform.test.jsdom.js │ ├── platform.test.node.js │ ├── source.test.js │ ├── stack.test.js │ ├── track.test.js │ ├── track.test.jsdom.js │ └── track.test.node.js ├── utils.js └── web │ ├── AudioContext.test.js │ ├── BackgroundFetchManager.test.js │ ├── BackgroundTasks.test.js │ ├── Battery.test.js │ ├── Beacon.test.js │ ├── Bluetooth.test.js │ ├── Clipboard.test.js │ ├── CookieStore.test.js │ ├── CredentialsContainer.test.js │ ├── EventSource.test.js │ ├── Fetch.test.js │ ├── FileReader.test.js │ ├── FileReaderSync.test.js │ ├── FileSystem.test.js │ ├── Gamepad.test.js │ ├── Geolocation.test.js │ ├── HID.test.js │ ├── History.test.js │ ├── IDBFactory.test.js │ ├── ImageCapture.test.js │ ├── MIDI.test.js │ ├── MediaDevices.test.js │ ├── MediaRecorder.test.js │ ├── MediaStream.test.js │ ├── MessageChannel.test.js │ ├── Notification.test.js │ ├── PaymentRequest.test.js │ ├── PerformanceObserver.test.js │ ├── PeriodicSyncManager.test.js │ ├── Permissions.test.js │ ├── PresentationRequest.test.js │ ├── PushManager.test.js │ ├── ReportingObserver.test.js │ ├── Scheduler.test.js │ ├── Selection.test.js │ ├── Sensor.test.js │ ├── Share.test.js │ ├── SpeechRecognition.test.js │ ├── SpeechSynthesis.test.js │ ├── Storage.test.js │ ├── StorageAccess.test.js │ ├── StorageManager.test.js │ ├── SubtleCrypto.test.js │ ├── USB.test.js │ ├── Vibration.test.js │ ├── WakeLock.test.js │ ├── WebSocket.test.js │ ├── Worker.test.js │ ├── XMLHttpRequest.test.js │ └── setup.js ├── webpack.config.cjs └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "prettier" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 2020 8 | }, 9 | "plugins": [ 10 | "prettier", 11 | "jest" 12 | ], 13 | "rules": { 14 | "prettier/prettier": "error", 15 | "no-nested-ternary": "off" 16 | }, 17 | "globals": { 18 | "__non_webpack_require__": "readonly", 19 | "Sandworm": "readonly", 20 | "eval": "readonly", 21 | "window": "readonly", 22 | "self": "readonly", 23 | "document": "readonly", 24 | "navigator": "readonly", 25 | "Storage": "readonly", 26 | "AbsoluteOrientationSensor": "readonly", 27 | "Accelerometer": "readonly", 28 | "AmbientLightSensor": "readonly", 29 | "AudioContext": "readonly", 30 | "BackgroundFetchManager": "readonly", 31 | "Blob": "readonly", 32 | "Bluetooth": "readonly", 33 | "Clipboard": "readonly", 34 | "CredentialsContainer": "readonly", 35 | "ContentIndex": "readonly", 36 | "CookieStore": "readonly", 37 | "EventSource": "readonly", 38 | "FileReader": "readonly", 39 | "FileReaderSync": "readonly", 40 | "Geolocation": "readonly", 41 | "Gyroscope": "readonly", 42 | "GravitySensor": "readonly", 43 | "HID": "readonly", 44 | "History": "readonly", 45 | "ImageCapture": "readonly", 46 | "IDBFactory": "readonly", 47 | "LinearAccelerationSensor": "readonly", 48 | "Magnetometer": "readonly", 49 | "MediaDevices": "readonly", 50 | "MediaStream": "readonly", 51 | "MediaRecorder": "readonly", 52 | "Notification": "readonly", 53 | "OrientationSensor": "readonly", 54 | "PaymentRequest": "readonly", 55 | "PerformanceObserver": "readonly", 56 | "PeriodicSyncManager": "readonly", 57 | "Permissions": "readonly", 58 | "PresentationRequest": "readonly", 59 | "RelativeOrientationSensor": "readonly", 60 | "ReportingObserver": "readonly", 61 | "Scheduler": "readonly", 62 | "Selection": "readonly", 63 | "ServiceWorkerRegistration": "readonly", 64 | "webkitSpeechRecognition": "readonly", 65 | "SharedWorker": "readonly", 66 | "SpeechRecognition": "readonly", 67 | "StorageManager": "readonly", 68 | "SubtleCrypto": "readonly", 69 | "PushManager": "readonly", 70 | "USB": "readonly", 71 | "WakeLock": "readonly", 72 | "WebSocket": "readonly", 73 | "Worker": "readonly", 74 | "XMLHttpRequest": "readonly" 75 | }, 76 | "overrides": [ 77 | { 78 | "files": [ 79 | "tests/**/*.js" 80 | ], 81 | "rules": { 82 | // Allow using "self" in tests 83 | "no-restricted-globals": "off", 84 | "no-empty": "off", 85 | "no-new": "off", 86 | "global-require": "off", 87 | "no-console": "off", 88 | "import/no-extraneous-dependencies": "off" 89 | } 90 | } 91 | ], 92 | "env": { 93 | "jest": true 94 | }, 95 | "ignorePatterns": [ 96 | "**/node_modules/**/*.js", 97 | "**/nosources-cheap-source-map/*.js", 98 | "**/inline-nosources-cheap-source-map/*.js" 99 | ] 100 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sandworm-hq 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: gabidobo 7 | 8 | --- 9 | **Sandworm version** 10 | The library version you're using. 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior. 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Node (please complete the following information):** 22 | - Version [e.g. 14.17.3] 23 | - OS: [e.g. Ubuntu 22.04] 24 | 25 | **Browser (please complete the following information):** 26 | - Device: [e.g. iPhone6, laptop] 27 | - OS: [e.g. iOS8.1] 28 | - Browser [e.g. stock browser, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.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 | # Coverage info 14 | coverage 15 | 16 | # Test results 17 | junit*.xml 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Dependency directories 26 | node_modules/ 27 | jspm_packages/ 28 | 29 | # TypeScript cache 30 | *.tsbuildinfo 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional eslint cache 36 | .eslintcache 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Output of 'npm pack' 42 | *.tgz 43 | 44 | # Yarn Integrity file 45 | .yarn-integrity 46 | 47 | # dotenv environment variable files 48 | .env 49 | .env.development.local 50 | .env.test.local 51 | .env.production.local 52 | .env.local 53 | 54 | # yarn v2 55 | .yarn/cache 56 | .yarn/unplugged 57 | .yarn/build-state.yml 58 | .yarn/install-state.gz 59 | .pnp.* 60 | 61 | # Quick dev tests file 62 | test.js 63 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn build-library 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .husky/commit-msg 2 | .husky/pre-commit 3 | .gitbook.yaml 4 | .circleci 5 | .github 6 | .vscode 7 | coverage 8 | scripts 9 | src 10 | tests 11 | .babelrc 12 | .eslintrc.json 13 | .prettierrc 14 | junit*.xml 15 | test.js 16 | webpack.config.cjs 17 | commitlint.config.js 18 | .release-please-manifest.json 19 | release-please-config.json 20 | dist/node_modules 21 | cli/frontend/node_modules 22 | cli/frontend/yarn.lock 23 | docs 24 | *.md 25 | /*.png 26 | /cli/screenshot.png 27 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "bracketSpacing": false 8 | } 9 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.0.0" 3 | } -------------------------------------------------------------------------------- /.sandworm.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "aliases": [ 3 | {"path": "tests", "name": "test"} 4 | ], 5 | "ignoredModules": ["test"] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sandworm Security 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sandworm 6 | 7 | 8 | 9 |

 

10 | 11 | Easy auditing & sandboxing for your JavaScript dependencies 🪱 12 | 13 | --- 14 | 15 | [![NPM][npm-version-image]][npm-version-url] 16 | [![License][license-image]][license-url] 17 | [![CircleCI][ci-image]][ci-url] 18 | [![Maintainability][cc-image]][cc-url] 19 | [![Test Coverage][coverage-image]][coverage-url] 20 | 21 | ## TL;DR 22 | * Sandworm Guard intercepts all potentially harmful Node & browser APIs, like arbitrary code execution (`child_process.exec`) or network calls (`fetch`). It knows what packages are responsible for each call. 23 | * Simple obfuscation techniques can confuse static analysis tools, but Sandworm's dynamic analysis will always intercept risky calls at run time. 24 | * You can use Sandworm Guard to: 25 | * [audit your dependencies](https://docs.sandworm.dev/#getting-started), monitor activity and permissions, and see what your code is doing under the hood using the Inspector; 26 | * [generate a security profile](https://docs.sandworm.dev/test-framework-plugins) automatically from your test suite and do snapshot testing against it; 27 | * [secure your app against supply chain attacks](https://docs.sandworm.dev/#enforcing-permissions) by enforcing per-module permissions. 28 | * Install it as an `npm` module in your existing Node or browser app. 29 | * Works in Node v15+ and [modern browsers](https://browsersl.ist/#q=defaults). Beta support for browsers and sourcemaps. 30 | 31 | ### Getting Started 32 | 33 | Add the Sandworm init call as the very first line of your app: 34 | 35 | ```js 36 | require('@sandworm/guard').init({devMode: true}); // add `permissions: [...]` to enforce 37 | ``` 38 | 39 | Then launch the inspector tool with `npm run sandworm` or `yarn sandworm` to monitor activity and permissions. 40 | 41 | ### Documentation 42 | 43 | > [Read the full docs here](https://docs.sandworm.dev/guard). 44 | 45 | ### Get Involved 46 | 47 | * Have a support question? [Post it here](https://github.com/sandworm-hq/sandworm-guard-js/discussions/categories/q-a). 48 | * Have a feature request? [Post it here](https://github.com/sandworm-hq/sandworm-guard-js/discussions/categories/ideas). 49 | * Did you find a security issue? [See SECURITY.md](contributing/security.md). 50 | * Did you find a bug? [Post an issue](https://github.com/sandworm-hq/sandworm-guard-js/issues/new/choose). 51 | * Want to write some code? See [CONTRIBUTING.md](contributing/). 52 | 53 | [npm-version-image]: https://img.shields.io/npm/v/sandworm?style=flat-square 54 | [npm-version-url]: https://www.npmjs.com/package/sandworm 55 | [license-image]: https://img.shields.io/npm/l/sandworm?style=flat-square 56 | [license-url]: https://github.com/sandworm-hq/sandworm-guard-js/blob/main/LICENSE 57 | [ci-image]: https://img.shields.io/circleci/build/github/sandworm-hq/sandworm-guard-js?style=flat-square 58 | [ci-url]: https://app.circleci.com/pipelines/github/sandworm-hq/sandworm-guard-js 59 | [cc-image]: https://api.codeclimate.com/v1/badges/edff60f7f06bb0c589aa/maintainability 60 | [cc-url]: https://codeclimate.com/github/sandworm-hq/sandworm-guard-js/maintainability 61 | [coverage-image]: https://api.codeclimate.com/v1/badges/edff60f7f06bb0c589aa/test_coverage 62 | [coverage-url]: https://codeclimate.com/github/sandworm-hq/sandworm-guard-js/test_coverage 63 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Security is a top priority for Sandworm. Thanks for helping make Sandworm safe for everyone. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | The Sandworm team takes all security vulnerabilities seriously. Thank you for improving the security of our open source software. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. 8 | 9 | If you believe you have found a security vulnerability in any Sandworm-owned repository, please report it to us through coordinated disclosure. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Instead, please send an email to: 14 | 15 | ``` 16 | security@sandworm.dev 17 | ``` 18 | 19 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 20 | 21 | - The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 22 | - Full paths of source file(s) related to the manifestation of the issue 23 | - The location of the affected source code (tag/branch/commit or direct URL) 24 | - Any special configuration required to reproduce the issue 25 | - Step-by-step instructions to reproduce the issue 26 | - Proof-of-concept or exploit code (if possible) 27 | - Impact of the issue, including how an attacker might exploit the issue 28 | 29 | The lead maintainer will acknowledge your email within 24 hours and send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will endeavor to inform you of the progress toward a fix and full announcement and may ask for additional information or guidance. 30 | 31 | ## Disclosure Policy 32 | 33 | When the security team receives a security bug report, they will assign it to a primary handler. This person will coordinate the fix and release process, involving the following steps: 34 | 35 | - Confirm the problem and determine the affected versions. 36 | - Audit code to find any potential similar problems. 37 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. 38 | -------------------------------------------------------------------------------- /cli/frontend/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.js": "/static/js/bundle.js", 4 | "index.html": "/index.html", 5 | "bundle.js.map": "/static/js/bundle.js.map" 6 | }, 7 | "entrypoints": [ 8 | "static/js/bundle.js" 9 | ] 10 | } -------------------------------------------------------------------------------- /cli/frontend/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | Sandworm Inspector 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /cli/frontend/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandworm-hq/sandworm-guard-js/fe9895207eb98a76388992c5b1facb81b7c4281a/cli/frontend/build/logo.png -------------------------------------------------------------------------------- /cli/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sandworm-inspector", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "classnames": "^2.3.1", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "web-vitals": "^2.1.4" 13 | }, 14 | "main": "src/index.js", 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "watch": "node scripts/watch.js", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ], 27 | "ignorePatterns": [ 28 | "**/bundle.js" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "autoprefixer": "^10.4.7", 45 | "postcss": "^8.4.14", 46 | "react-scripts": "5.0.1", 47 | "tailwindcss": "^3.1.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cli/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /cli/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | Sandworm Inspector 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /cli/frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandworm-hq/sandworm-guard-js/fe9895207eb98a76388992c5b1facb81b7c4281a/cli/frontend/public/logo.png -------------------------------------------------------------------------------- /cli/frontend/scripts/watch.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development'; 2 | 3 | const fs = require('fs-extra'); 4 | const paths = require('react-scripts/config/paths'); 5 | const webpack = require('webpack'); 6 | const config = require('react-scripts/config/webpack.config')('development'); 7 | 8 | webpack(config).watch({}, (err, stats) => { 9 | if (err) { 10 | console.error(err); 11 | } else { 12 | copyPublicFolder(); 13 | } 14 | console.error( 15 | stats.toString({ 16 | chunks: false, 17 | colors: true, 18 | }), 19 | ); 20 | }); 21 | 22 | function copyPublicFolder() { 23 | fs.copySync(paths.appPublic, paths.appBuild, { 24 | dereference: true, 25 | filter: (file) => file !== paths.appHtml, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /cli/frontend/src/components/Activity.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import {useContext, useMemo, useState} from 'react'; 3 | import {DataContext} from '../context'; 4 | import Title from './Title'; 5 | 6 | const Activity = () => { 7 | const [selected, setSelected] = useState(null); 8 | const {history, selectedModule} = useContext(DataContext); 9 | const moduleHistory = useMemo( 10 | () => (history || []).filter((e) => e?.module === selectedModule), 11 | [history, selectedModule], 12 | ); 13 | 14 | return ( 15 | <> 16 | 17 | <p> 18 | {moduleHistory.length} event{moduleHistory.length > 1 && 's'} recorded: 19 | </p> 20 | <div className="flex flex-col gap-3 mt-3"> 21 | {moduleHistory.map(({uid, module, family, method, args, stack}) => { 22 | return ( 23 | <div key={uid} className="bg-zinc-600 rounded p-5 shadow text-xs w-full"> 24 | <div className="flex items-center w-full"> 25 | <div className="flex flex-col flex-grow min-w-0"> 26 | <div className="flex"> 27 | <pre className="inline">{module}</pre> 28 | <span className="ml-1">called</span> 29 | </div> 30 | <code 31 | lang="javascript" 32 | className="bg-zinc-700 rounded p-2 mt-2 overflow-auto block select-all max-h-40" 33 | > 34 | {family}.{method}( 35 | {(args || []).map((a) => JSON.stringify(a, null, 2)).join(', ')}) 36 | </code> 37 | </div> 38 | <div 39 | className="ml-3 flex-shrink-0 cursor-pointer" 40 | onClick={() => setSelected((prev) => (prev === uid ? null : uid))} 41 | > 42 | <i className="bi bi-chevron-down p-3" /> 43 | </div> 44 | </div> 45 | {selected === uid && ( 46 | <div className="mt-3 flex flex-col gap-3"> 47 | {stack.map( 48 | ({file, fileLine, fileColumn, mapping, mappingLine, mappingColumn, module}) => ( 49 | <div 50 | className={classNames('flex flex-col py-1 border-b border-zinc-500', { 51 | 'opacity-60': !module, 52 | })} 53 | > 54 | {module && <div className="font-bold text-zinc-300">{module}</div>} 55 | {mapping && ( 56 | <div> 57 | {mapping}:{mappingLine}:{mappingColumn} 58 | </div> 59 | )} 60 | <div className="text-zinc-400"> 61 | {file}:{fileLine}:{fileColumn} 62 | </div> 63 | </div> 64 | ), 65 | )} 66 | </div> 67 | )} 68 | </div> 69 | ); 70 | })} 71 | </div> 72 | </> 73 | ); 74 | }; 75 | 76 | export default Activity; 77 | -------------------------------------------------------------------------------- /cli/frontend/src/components/ConfigModal.jsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef, useState} from 'react'; 2 | 3 | const ConfigModal = ({onHide, permissions}) => { 4 | const [copyButtonText, setCopyButtonText] = useState('Copy'); 5 | const modalContent = useRef(null); 6 | useEffect(() => { 7 | if (modalContent.current && window.getSelection) { 8 | const selection = window.getSelection(); 9 | const range = document.createRange(); 10 | range.selectNodeContents(modalContent.current); 11 | selection.removeAllRanges(); 12 | selection.addRange(range); 13 | } 14 | }, [modalContent]); 15 | 16 | return ( 17 | <div 18 | onClick={() => onHide()} 19 | className="absolute left-0 top-0 right-0 bottom-0 bg-black bg-opacity-75 flex items-center justify-center" 20 | > 21 | <div 22 | onClick={(e) => { 23 | e.stopPropagation(); 24 | }} 25 | className="bg-zinc-800 rounded shadow-xl p-12 h-3/5 w-3/5 flex flex-col" 26 | > 27 | <div className="flex w-full"> 28 | <div className="flex-grow"> 29 | <div className="font-bold text-lg">Permissions JSON</div> 30 | <div className="text-sm">Based on captured app activity and user overrides.</div> 31 | </div> 32 | <div> 33 | <button 34 | onClick={() => { 35 | navigator.clipboard.writeText(JSON.stringify(permissions, null, 2)); 36 | setCopyButtonText('Copied!'); 37 | setTimeout(() => setCopyButtonText('Copy'), 2000); 38 | }} 39 | className="bg-sandworm-yellow text-black font-bold p-4 rounded" 40 | > 41 | {copyButtonText} 42 | </button> 43 | </div> 44 | </div> 45 | <pre 46 | className="flex-grow overflow-scroll select-all mt-5 bg-zinc-900 p-5 shadow-inner selection:bg-sandworm-yellow selection:text-black" 47 | ref={modalContent} 48 | > 49 | {JSON.stringify( 50 | Object.keys(permissions).map((module) => ({module, permissions: permissions[module]})), 51 | null, 52 | 2, 53 | )} 54 | </pre> 55 | </div> 56 | </div> 57 | ); 58 | }; 59 | 60 | export default ConfigModal; 61 | -------------------------------------------------------------------------------- /cli/frontend/src/components/Modules.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import {useState, useContext} from 'react'; 3 | import {DataContext} from '../context'; 4 | import useESCPress from '../hooks/useESCPress'; 5 | import ConfigModal from './ConfigModal'; 6 | 7 | const PERMISSION_PREVIEW_LIMIT = 3; 8 | 9 | const Modules = ({history, currentTab}) => { 10 | const {requiredPermissions, selectedModule, setSelectedModule, permissions} = 11 | useContext(DataContext); 12 | const [configModalVisible, setConfigModalVisible] = useState(false); 13 | useESCPress(() => setConfigModalVisible(false)); 14 | 15 | return ( 16 | <> 17 | <div className="flex-grow overflow-scroll shadow-inner"> 18 | {Object.keys(requiredPermissions).length === 0 && ( 19 | <div className="text-neutral-400">Listening to events from your app...</div> 20 | )} 21 | {Object.keys(requiredPermissions) 22 | .sort() 23 | .map((module) => { 24 | const modules = module.split('>'); 25 | return ( 26 | <div 27 | key={module} 28 | className={classNames( 29 | 'w-full py-3 px-5 bg-zinc-700 rounded shadow flex items-center mb-4 cursor-pointer border-2', 30 | { 31 | 'border-sandworm-yellow': selectedModule === module, 32 | 'border-zinc-900': selectedModule !== module, 33 | }, 34 | )} 35 | onClick={() => setSelectedModule(module)} 36 | > 37 | <div className="flex flex-col flex-grow gap-2"> 38 | <div className="font-bold">{modules[0]}</div> 39 | {modules.length > 1 && ( 40 | <div className="text-zinc-400 text-xs"> 41 | via{' '} 42 | {modules 43 | .slice(1) 44 | .map((p) => <code key={p}>{p}</code>) 45 | .reduce((prev, curr) => [prev, ' > ', curr])} 46 | </div> 47 | )} 48 | <div className="text-xs"> 49 | Using{' '} 50 | {requiredPermissions[module] 51 | .slice(0, PERMISSION_PREVIEW_LIMIT) 52 | .map((p) => <code key={p}>{p}</code>) 53 | .reduce((prev, curr) => [prev, ', ', curr])} 54 | {requiredPermissions[module].length > PERMISSION_PREVIEW_LIMIT && 55 | `, and ${requiredPermissions[module].length - PERMISSION_PREVIEW_LIMIT} more`} 56 | </div> 57 | </div> 58 | <div className="ml-3"> 59 | <i className="bi bi-chevron-right" /> 60 | </div> 61 | </div> 62 | ); 63 | })} 64 | </div> 65 | <button 66 | onClick={() => setConfigModalVisible(true)} 67 | className="mt-8 bg-sandworm-yellow text-black font-bold p-4 rounded" 68 | > 69 | Get Permissions JSON 70 | </button> 71 | {configModalVisible && ( 72 | <ConfigModal onHide={() => setConfigModalVisible(false)} permissions={permissions} /> 73 | )} 74 | </> 75 | ); 76 | }; 77 | 78 | export default Modules; 79 | -------------------------------------------------------------------------------- /cli/frontend/src/components/Permission.jsx: -------------------------------------------------------------------------------- 1 | function Permission({method, onClick, checked}) { 2 | return ( 3 | <div className="flex items-center"> 4 | <input readOnly type="checkbox" checked={checked} onClick={onClick} />{' '} 5 | <code 6 | onClick={onClick} 7 | className="text-sm ml-2 border-b border-dotted tooltip cursor-pointer select-none" 8 | > 9 | {method.name} 10 | </code> 11 | {method.url && ( 12 | <a href={method.url} target="_blank" rel="noopener noreferrer"> 13 | <i className="bi bi-box-arrow-up-right block ml-4 text-zinc-400 text-sm" /> 14 | </a> 15 | )} 16 | {method.description && ( 17 | <span className="text-xs ml-2 text-zinc-400">{method.description}</span> 18 | )} 19 | </div> 20 | ); 21 | } 22 | 23 | export default Permission; 24 | -------------------------------------------------------------------------------- /cli/frontend/src/components/PermissionList.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { DataContext } from '../context'; 3 | import { nodeLibrary, webLibrary } from '../libraries'; 4 | import Permission from './Permission'; 5 | 6 | function PermissionList({filters, selectedLibrary}) { 7 | const {permissions, selectedModule, updatePermissions} = useContext(DataContext); 8 | 9 | return ( 10 | <div className="flex flex-col gap-8"> 11 | {(selectedLibrary === 'web' ? webLibrary : nodeLibrary) 12 | .filter( 13 | (family) => 14 | !filters || filters.map((filter) => filter.split('.')[0]).includes(family.name), 15 | ) 16 | .map((family) => { 17 | return ( 18 | <div key={family.name}> 19 | <div className="mb-2 flex items-center"> 20 | <code className="font-bold">{family.name}</code> 21 | <i className="gg-external block ml-4 w-2 h-2 text-zinc-400" /> 22 | </div> 23 | <div className="flex flex-col gap-1"> 24 | {family.methods 25 | .filter( 26 | (method) => 27 | !filters || 28 | filters 29 | .map( 30 | (filter) => filter.split('.')[0] === family.name && filter.split('.')[1], 31 | ) 32 | .includes(method.name), 33 | ) 34 | .map((method) => { 35 | const methodSlug = `${family.name}.${method.name}`; 36 | const currentlyChecked = permissions[selectedModule]?.includes?.(methodSlug); 37 | return ( 38 | <Permission 39 | key={methodSlug} 40 | method={method} 41 | checked={currentlyChecked} 42 | onClick={() => updatePermissions(currentlyChecked, methodSlug)} 43 | /> 44 | ); 45 | })} 46 | </div> 47 | </div> 48 | ); 49 | })} 50 | </div> 51 | ); 52 | } 53 | 54 | export default PermissionList; 55 | -------------------------------------------------------------------------------- /cli/frontend/src/components/Permissions.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import {useEffect, useState, useContext} from 'react'; 3 | import PermissionList from './PermissionList'; 4 | import {nodeLibrary, webLibrary} from '../libraries'; 5 | import {DataContext} from '../context'; 6 | import Title from './Title'; 7 | 8 | const Permissions = () => { 9 | const [selectedLibrary, setSelectedLibrary] = useState(null); 10 | const { 11 | requiredPermissions, 12 | selectedModule, 13 | explicitlyRemovedPermissions, 14 | } = useContext(DataContext); 15 | 16 | useEffect(() => { 17 | if (!selectedLibrary && Object.values(requiredPermissions).length) { 18 | const sampleMethod = Object.values(requiredPermissions)[0][0]; 19 | const sampleFamily = sampleMethod.split('.')[0]; 20 | if (webLibrary.find((v) => v.name === sampleFamily)) { 21 | setSelectedLibrary('web'); 22 | } else if (nodeLibrary.find((v) => v.name === sampleFamily)) { 23 | setSelectedLibrary('node'); 24 | } 25 | } 26 | }, [requiredPermissions, selectedLibrary]); 27 | 28 | return ( 29 | <> 30 | <Title module={selectedModule} text="permissions" /> 31 | <ul className="flex gap-4 text-neutral-400"> 32 | <li 33 | className={classNames('cursor-pointer', { 34 | 'font-bold text-neutral-200 border-b-2': selectedLibrary === 'web', 35 | })} 36 | onClick={() => setSelectedLibrary('web')} 37 | > 38 | Web 39 | </li> 40 | <li 41 | className={classNames('cursor-pointer', { 42 | 'font-bold text-neutral-200 border-b-2': selectedLibrary === 'node', 43 | })} 44 | onClick={() => setSelectedLibrary('node')} 45 | > 46 | Node 47 | </li> 48 | </ul> 49 | <div className="w-full py-3 px-5 bg-zinc-700 rounded shadow items-center mt-4 mb-10"> 50 | <div className="mb-2">Required permissions:</div> 51 | <PermissionList 52 | filters={requiredPermissions[selectedModule]} 53 | selectedLibrary={selectedLibrary} 54 | /> 55 | </div> 56 | {explicitlyRemovedPermissions[selectedModule]?.length > 0 && ( 57 | <div className="mb-10 flex gap-3 bg-yellow-900 rounded shadow p-7 items-center"> 58 | <i className="bi bi-exclamation-triangle-fill text-yellow-500 block text-lg" /> 59 | <div className="text-sm"> 60 | Some required permissions have been explicitly removed. 61 | <br /> 62 | This may cause your app to not function as expected. 63 | </div> 64 | </div> 65 | )} 66 | <PermissionList selectedLibrary={selectedLibrary} /> 67 | </> 68 | ); 69 | }; 70 | 71 | export default Permissions; 72 | -------------------------------------------------------------------------------- /cli/frontend/src/components/Title.jsx: -------------------------------------------------------------------------------- 1 | const Title = ({module, text}) => { 2 | const modules = module.split('>'); 3 | return ( 4 | <div className="font-bold text-lg border border-sandworm-yellow mb-10 p-5 shadow rounded"> 5 | <code>{modules[0]}</code> {text} 6 | {modules.length > 1 && ( 7 | <div className="text-zinc-400 text-xs"> 8 | via{' '} 9 | {modules 10 | .slice(1) 11 | .map((p) => <code key={p}>{p}</code>) 12 | .reduce((prev, curr) => [prev, ' > ', curr])} 13 | </div> 14 | )} 15 | </div> 16 | ); 17 | }; 18 | 19 | export default Title; 20 | -------------------------------------------------------------------------------- /cli/frontend/src/context.js: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useMemo, useState} from 'react'; 2 | import useLocalStorage from './hooks/useLocalStorage'; 3 | 4 | export const DataContext = React.createContext(); 5 | 6 | export const DataProvider = ({children}) => { 7 | const [currentTab, setCurrentTab] = useLocalStorage('_sandworm_permissions', 'permissions'); 8 | const [history, setHistory] = useState([]); 9 | const [selectedModule, setSelectedModule] = useLocalStorage('_sandworm_module', null); 10 | const [requiredPermissions, setRequiredPermissions] = useState({}); 11 | const [explicitlyAddedPermissions, setExplicitlyAddedPermissions] = useState({}); 12 | const [explicitlyRemovedPermissions, setExplicitlyRemovedPermissions] = useState({}); 13 | 14 | useEffect(() => { 15 | const permissions = {}; 16 | history.forEach((event) => { 17 | if (!permissions[event.module]) { 18 | permissions[event.module] = []; 19 | } 20 | const eventType = `${event.family}.${event.method}`; 21 | if (!permissions[event.module].includes(eventType)) { 22 | permissions[event.module].push(eventType); 23 | } 24 | }); 25 | setRequiredPermissions(permissions); 26 | }, [history, setRequiredPermissions]); 27 | 28 | const permissions = useMemo(() => { 29 | return Object.keys(requiredPermissions).reduce( 30 | (acc, v) => ({ 31 | ...acc, 32 | [v]: requiredPermissions[v] 33 | .concat(explicitlyAddedPermissions[v] || []) 34 | .filter((val) => !(explicitlyRemovedPermissions[v] || []).includes(val)), 35 | }), 36 | {}, 37 | ); 38 | }, [requiredPermissions, explicitlyAddedPermissions, explicitlyRemovedPermissions]); 39 | 40 | const updatePermissions = useCallback( 41 | (currentlyChecked, methodSlug) => { 42 | if (currentlyChecked) { 43 | setExplicitlyAddedPermissions((prev) => ({ 44 | ...prev, 45 | [selectedModule]: (prev[selectedModule] || []).filter((v) => v !== methodSlug), 46 | })); 47 | if (requiredPermissions[selectedModule].includes(methodSlug)) { 48 | setExplicitlyRemovedPermissions((prev) => ({ 49 | ...prev, 50 | [selectedModule]: [...(prev[selectedModule] || []), methodSlug], 51 | })); 52 | } 53 | } else { 54 | setExplicitlyRemovedPermissions((prev) => ({ 55 | ...prev, 56 | [selectedModule]: (prev[selectedModule] || []).filter((v) => v !== methodSlug), 57 | })); 58 | if (!requiredPermissions[selectedModule].includes(methodSlug)) { 59 | setExplicitlyAddedPermissions((prev) => ({ 60 | ...prev, 61 | [selectedModule]: [...(prev[selectedModule] || []), methodSlug], 62 | })); 63 | } 64 | } 65 | }, 66 | [ 67 | requiredPermissions, 68 | selectedModule, 69 | setExplicitlyAddedPermissions, 70 | setExplicitlyRemovedPermissions, 71 | ], 72 | ); 73 | 74 | return ( 75 | <DataContext.Provider 76 | value={{ 77 | currentTab, 78 | setCurrentTab, 79 | history, 80 | setHistory, 81 | selectedModule, 82 | setSelectedModule, 83 | requiredPermissions, 84 | setRequiredPermissions, 85 | explicitlyAddedPermissions, 86 | setExplicitlyAddedPermissions, 87 | explicitlyRemovedPermissions, 88 | setExplicitlyRemovedPermissions, 89 | permissions, 90 | updatePermissions, 91 | }} 92 | > 93 | {children} 94 | </DataContext.Provider> 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /cli/frontend/src/hooks/useESCPress.js: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | 3 | function useESCPress(handler = () => {}) { 4 | const targetKeyCode = 27; 5 | // If pressed key is our target key then set to true 6 | function downHandler({keyCode}) { 7 | if (keyCode === targetKeyCode) { 8 | handler(); 9 | } 10 | } 11 | // Add event listeners 12 | useEffect(() => { 13 | window.addEventListener('keydown', downHandler); 14 | // Remove event listeners on cleanup 15 | return () => { 16 | window.removeEventListener('keydown', downHandler); 17 | }; 18 | // eslint-disable-next-line react-hooks/exhaustive-deps 19 | }, []); // Empty array ensures that effect is only run on mount and unmount 20 | } 21 | 22 | export default useESCPress; 23 | -------------------------------------------------------------------------------- /cli/frontend/src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | 3 | function useLocalStorage(key, initialValue) { 4 | // State to store our value 5 | // Pass initial state function to useState so logic is only executed once 6 | const [storedValue, setStoredValue] = useState(() => { 7 | if (typeof window === 'undefined') { 8 | return initialValue; 9 | } 10 | try { 11 | // Get from local storage by key 12 | const item = window.localStorage.getItem(key); 13 | // Parse stored json or if none return initialValue 14 | return item ? JSON.parse(item) : initialValue; 15 | } catch (error) { 16 | // If error also return initialValue 17 | console.log(error); 18 | return initialValue; 19 | } 20 | }); 21 | // Return a wrapped version of useState's setter function that ... 22 | // ... persists the new value to localStorage. 23 | const setValue = (value) => { 24 | try { 25 | // Allow value to be a function so we have same API as useState 26 | const valueToStore = value instanceof Function ? value(storedValue) : value; 27 | // Save state 28 | setStoredValue(valueToStore); 29 | // Save to local storage 30 | if (typeof window !== 'undefined') { 31 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 32 | } 33 | } catch (error) { 34 | // A more advanced implementation would handle the error case 35 | console.log(error); 36 | } 37 | }; 38 | return [storedValue, setValue]; 39 | } 40 | 41 | export default useLocalStorage; 42 | -------------------------------------------------------------------------------- /cli/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"); 6 | 7 | body { 8 | margin: 0; 9 | background-color: rgb(21, 21, 24); 10 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 11 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 12 | sans-serif; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | html, body, #root { 18 | height: 100%; 19 | } 20 | 21 | code { 22 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 23 | monospace; 24 | } 25 | -------------------------------------------------------------------------------- /cli/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, {useContext, useEffect} from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import Activity from './components/Activity'; 5 | import Modules from './components/Modules'; 6 | import Permissions from './components/Permissions'; 7 | import {DataContext, DataProvider} from './context'; 8 | 9 | import './index.css'; 10 | 11 | function App() { 12 | const {history, setHistory, currentTab, setCurrentTab, selectedModule} = useContext(DataContext); 13 | 14 | useEffect(() => { 15 | const evtSource = new EventSource('/events'); 16 | evtSource.onmessage = function (e) { 17 | const data = JSON.parse(e.data); 18 | console.log('Received data:', data); 19 | setHistory((history) => [...history, ...data]); 20 | }; 21 | return () => { 22 | evtSource.close(); 23 | }; 24 | }, [setHistory]); 25 | 26 | return ( 27 | <div className="w-full h-full max-h-full flex flex-col text-neutral-200 relative"> 28 | <div className="px-10 py-3 bg-[#434245] flex items-center"> 29 | <div className="flex-grow flex gap-6 items-center"> 30 | <img width={177} src="/logo.png" alt="Sandworm" /> 31 | <div className="bg-custom-light-gray py-2 px-3 text-sandworm-yellow-light uppercase text-xs rounded font-bold"> 32 | Inspector 33 | </div> 34 | </div> 35 | <div> 36 | <ul className="flex gap-10 text-neutral-300 text-xl"> 37 | <li 38 | className={classNames('cursor-pointer', { 39 | 'font-bold text-neutral-200 border-b-2': currentTab === 'permissions', 40 | })} 41 | onClick={() => setCurrentTab('permissions')} 42 | > 43 | Permissions 44 | </li> 45 | <li 46 | className={classNames('cursor-pointer', { 47 | 'font-bold text-neutral-200 border-b-2': currentTab === 'activity', 48 | })} 49 | onClick={() => setCurrentTab('activity')} 50 | > 51 | Activity 52 | </li> 53 | </ul> 54 | </div> 55 | </div> 56 | 57 | <div className="flex w-full flex-grow flex-shrink min-h-0"> 58 | <div className="w-1/2 bg-zinc-900 p-10 h-full flex flex-col"> 59 | <Modules history={history} currentTab={currentTab} /> 60 | </div> 61 | <div className="w-1/2 bg-zinc-800 p-10 h-full"> 62 | <div className="h-full overflow-scroll"> 63 | {currentTab === 'permissions' && selectedModule && <Permissions />} 64 | {currentTab === 'activity' && selectedModule && <Activity />} 65 | </div> 66 | </div> 67 | </div> 68 | </div> 69 | ); 70 | } 71 | 72 | const root = ReactDOM.createRoot(document.getElementById('root')); 73 | root.render( 74 | <React.StrictMode> 75 | <DataProvider> 76 | <App /> 77 | </DataProvider> 78 | </React.StrictMode>, 79 | ); 80 | -------------------------------------------------------------------------------- /cli/frontend/src/libraries.js: -------------------------------------------------------------------------------- 1 | import webLibraryData from './library/web.json'; 2 | import nodeLibraryData from './library/node.json'; 3 | import { buildNodeLibraryFrom, buildWebLibraryFrom } from './library/builder'; 4 | 5 | const processLibrary = (library) => 6 | library 7 | .reduce((acc, v) => { 8 | const family = acc.find((l) => l.name === v.name); 9 | if (family) { 10 | family.methods = family.methods 11 | .concat(v.methods) 12 | .filter((v, i, a) => a.findIndex((e) => e.name === v.name) === i); 13 | } else { 14 | acc.push(v); 15 | } 16 | return acc; 17 | }, []) 18 | .sort((a, b) => a.name.localeCompare(b.name)); 19 | 20 | export const webLibrary = processLibrary(buildWebLibraryFrom(webLibraryData)); 21 | export const nodeLibrary = processLibrary(buildNodeLibraryFrom(nodeLibraryData)); 22 | -------------------------------------------------------------------------------- /cli/frontend/src/library: -------------------------------------------------------------------------------- 1 | ../../../src/library/ -------------------------------------------------------------------------------- /cli/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'sandworm-yellow': '#FFCD53', 8 | 'sandworm-yellow-light': '#FFECC0', 9 | 'custom-light-gray': 'rgba(255, 255, 255, 0.1)', 10 | }, 11 | }, 12 | }, 13 | plugins: [], 14 | }; 15 | -------------------------------------------------------------------------------- /cli/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandworm-hq/sandworm-guard-js/fe9895207eb98a76388992c5b1facb81b7c4281a/cli/screenshot.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _default { 2 | export {init}; 3 | export {getHistory}; 4 | export {clearHistory}; 5 | export {SandwormError as Error}; 6 | } 7 | export default _default; 8 | 9 | /** Initialize Sandworm. */ 10 | declare function init({ 11 | loadSourceMaps, 12 | devMode, 13 | verbose, 14 | skipTracking, 15 | trackingIP, 16 | trackingPort, 17 | ignoreExtensions, 18 | trustedModules, 19 | permissions, 20 | aliases, 21 | onAccessDenied, 22 | }?: { 23 | /** 24 | * Set this to true to automatically load the sourcemap declared in the caller js file. 25 | * Alternatively, to load multiple sources and sourcemaps, set this to an object that has original 26 | * source file paths/urls as keys, and sourcemap file paths/urls as values. 27 | * Defaults to `true` in browsers, and `false` on Node. 28 | */ 29 | loadSourceMaps?: boolean; 30 | /** In dev mode, all calls are captured, allowed, and tracked to the inspector. 31 | * When dev mode is false, user-provided permissions are enforced and calls are not tracked. 32 | * Defaults to `false`. 33 | */ 34 | devMode?: boolean = false; 35 | /** The default logger level is `warn`; setting this to `true` lowers the level to `debug`. Defaults to `false`. */ 36 | verbose?: boolean = false; 37 | /** Set this to true to stop event tracking to the inspector in dev mode. Defaults to `false`. */ 38 | skipTracking?: boolean = false; 39 | /** The IP address for the inspector. */ 40 | trackingIP?: any; 41 | /** The port number for the inspector. */ 42 | trackingPort?: any; 43 | /** Ignore activity from browser extensions. Defaults to `true`. */ 44 | ignoreExtensions?: boolean = true; 45 | /** Utility or platform modules, that should be removed from a caller path. */ 46 | trustedModules?: any[] = []; 47 | /** Module permissions to enforce if dev mode is false. */ 48 | permissions?: Permission[] = []; 49 | /** An array describing the optional aliases to apply to root-level sources based on their path. */ 50 | aliases: Alias[] = []; 51 | /** An optional callback to be triggered on access errors, before throwing */ 52 | onAccessDenied: function; 53 | }): Promise<void>; 54 | /** Specifies a set of permissions to grant a module or a class of modules. */ 55 | declare interface Permission { 56 | /** The string module name to grant permissions to, or a RegExp to match multiple module names. */ 57 | module: string | RegExp; 58 | /** An array of string permissions to grant the specified module(s), formatted as `family.method`. */ 59 | permissions: string[]; 60 | } 61 | declare interface Alias { 62 | /** A path component shared between all source code files that should be matched by the alias. */ 63 | path: string; 64 | /** The alias name to apply. */ 65 | name: string; 66 | } 67 | /** In dev mode, returns the current call history. In production mode, returns an empty array. */ 68 | declare function getHistory(): any[]; 69 | /** In dev mode, clears the current call history. In production mode, does nothing. */ 70 | declare function clearHistory(): void; 71 | 72 | declare class SandwormError extends Error { 73 | constructor(message: any); 74 | } 75 | -------------------------------------------------------------------------------- /dist/index.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh <https://feross.org> 5 | * @license MIT 6 | */ 7 | 8 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */ 9 | 10 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 11 | -------------------------------------------------------------------------------- /logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandworm-hq/sandworm-guard-js/fe9895207eb98a76388992c5b1facb81b7c4281a/logo-dark.png -------------------------------------------------------------------------------- /logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandworm-hq/sandworm-guard-js/fe9895207eb98a76388992c5b1facb81b7c4281a/logo-light.png -------------------------------------------------------------------------------- /package-permissions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "module": "root", 4 | "permissions": [ 5 | "Function.Function", 6 | "fs.readFile" 7 | ] 8 | }, 9 | { 10 | "module": "source-map-js", 11 | "permissions": [ 12 | "Function.Function" 13 | ] 14 | } 15 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sandworm/guard", 3 | "version": "2.0.0", 4 | "description": "Easy auditing & sandboxing for your JavaScript dependencies 🪱", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/sandworm-hq/sandworm-guard-js.git" 10 | }, 11 | "scripts": { 12 | "dev": "webpack --watch --progress", 13 | "build": "webpack", 14 | "lint": "yarn eslint ./src/ ./tests/ ./scripts/ ./cli/", 15 | "test-node-unit": "jest --noStackTrace --config=tests/configs/jest.unit.node.config.js", 16 | "test-jsdom-unit": "jest --noStackTrace --config=tests/configs/jest.unit.jsdom.config.js", 17 | "test-unit": "yarn test-node-unit && yarn test-jsdom-unit", 18 | "test-node-capture": "yarn prepare-build-for-testing && jest --noStackTrace --config=tests/configs/jest.capture.config.js", 19 | "test-node-enforce": "yarn prepare-build-for-testing && jest --noStackTrace --config=tests/configs/jest.enforce.config.js", 20 | "test-web-capture": "yarn prepare-build-for-testing && npx playwright test --config=tests/configs/playwright.config.js", 21 | "test-web-all": "yarn test-web-capture && yarn test-jsdom-unit", 22 | "test-node-ce": "yarn test-node-enforce && yarn test-node-capture", 23 | "test-node-all": "yarn test-node-ce && yarn test-node-unit", 24 | "test-all": "yarn test-node-all && yarn test-web-all", 25 | "build-library-md": "node scripts/build-library-md.js", 26 | "build-library-min": "node scripts/build-library-min.js", 27 | "build-library": "yarn build-library-md && yarn build-library-min", 28 | "prepare-build-for-testing": "if [ ! -d \"dist/node_modules/@sandworm/audit\" ]; then mkdir -p dist/node_modules/@sandworm/audit && cd dist/node_modules/@sandworm/audit && ln ../../../index.js index.js && cd ../../../../; fi", 29 | "prepare": "husky install" 30 | }, 31 | "bin": "cli/index.js", 32 | "author": "Sandworm <dev@sandworm.dev>", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@babel/core": "^7.18.6", 36 | "@babel/preset-env": "^7.18.6", 37 | "@commitlint/cli": "^17.1.2", 38 | "@commitlint/config-conventional": "^17.1.0", 39 | "@playwright/test": "^1.23.4", 40 | "@types/jest": "^28.1.6", 41 | "babel-loader": "^8.2.5", 42 | "buffer": "^6.0.3", 43 | "eslint": "^8.2.0", 44 | "eslint-config-airbnb": "19.0.4", 45 | "eslint-config-prettier": "^8.5.0", 46 | "eslint-config-react-app": "^7.0.1", 47 | "eslint-plugin-import": "^2.25.3", 48 | "eslint-plugin-jest": "^26.6.0", 49 | "eslint-plugin-jsx-a11y": "^6.5.1", 50 | "eslint-plugin-prettier": "^4.2.1", 51 | "eslint-plugin-react": "^7.28.0", 52 | "eslint-plugin-react-hooks": "^4.3.0", 53 | "husky": "^8.0.0", 54 | "jest": "^29.0.3", 55 | "jest-environment-jsdom": "^29.0.3", 56 | "jest-junit": "^14.0.0", 57 | "playwright-watch": "^1.3.23", 58 | "prettier": "^2.7.1", 59 | "sandworm-jest": "^1.5.0", 60 | "webpack": "^5.73.0", 61 | "webpack-cli": "^4.10.0" 62 | }, 63 | "dependencies": { 64 | "source-map-js": "1.0.2" 65 | }, 66 | "keywords": [ 67 | "security", 68 | "audit", 69 | "dependencies", 70 | "supply-chain", 71 | "sandbox", 72 | "harden", 73 | "ses", 74 | "compartments" 75 | ], 76 | "homepage": "https://sandworm.dev" 77 | } 78 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "changelog-path": "CHANGELOG.md", 5 | "release-type": "node", 6 | "bump-minor-pre-major": false, 7 | "bump-patch-for-minor-pre-major": false, 8 | "draft": false, 9 | "prerelease": false 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /scripts/build-library-md.js: -------------------------------------------------------------------------------- 1 | const {createWriteStream} = require('fs'); 2 | const path = require('path'); 3 | 4 | const webLibrary = require('../src/library/web.json'); 5 | const nodeLibrary = require('../src/library/node.json'); 6 | const {buildNodeLibraryFrom, buildWebLibraryFrom} = require('../src/library/builder'); 7 | 8 | const filePath = path.join(__dirname, '..', 'LIBRARY.md'); 9 | const stream = createWriteStream(filePath, {flags: 'w'}); 10 | 11 | const writeLib = (library) => { 12 | stream.write('| Method | Description | Docs |\n'); 13 | stream.write('|---|---|---|\n'); 14 | library.forEach(({name: familyName, methods}) => 15 | methods.forEach(({name: methodName, description, url}) => { 16 | stream.write(`| \`${familyName}.${methodName}\` | ${description} | [Docs](${url}) |\n`); 17 | }), 18 | ); 19 | }; 20 | 21 | stream.write('# Sandworm.JS Library\n'); 22 | 23 | stream.write('## Node\n'); 24 | writeLib(buildNodeLibraryFrom(nodeLibrary)); 25 | 26 | stream.write('\n## Web\n'); 27 | writeLib(buildWebLibraryFrom(webLibrary)); 28 | 29 | stream.write('\n## `bind` calls\n'); 30 | stream.write( 31 | 'For each method listed above, Sandworm also intercepts `bind` calls. To allow `bind` calls with more than one argument, the `bind.args` permission is required. [Read more](/README.md#bind-calls).', 32 | ); 33 | 34 | stream.end(); 35 | -------------------------------------------------------------------------------- /scripts/build-library-min.js: -------------------------------------------------------------------------------- 1 | const {writeFileSync} = require('fs'); 2 | const path = require('path'); 3 | 4 | const webLibrary = require('../src/library/web.json'); 5 | const nodeLibrary = require('../src/library/node.json'); 6 | 7 | const webMinLibraryPath = path.join(__dirname, '..', 'src', 'library', 'web.min.json'); 8 | const nodeMinLibraryPath = path.join(__dirname, '..', 'src', 'library', 'node.min.json'); 9 | 10 | const cleanMethod = (method) => ({ 11 | ...method, 12 | description: undefined, 13 | url: undefined, 14 | }); 15 | 16 | [ 17 | {lib: webLibrary, output: webMinLibraryPath}, 18 | {lib: nodeLibrary, output: nodeMinLibraryPath}, 19 | ].forEach(({lib, output}) => { 20 | const minimized = lib.map((family) => ({ 21 | ...family, 22 | constructorDescription: undefined, 23 | constructorUrl: undefined, 24 | methods: family.methods?.map(cleanMethod), 25 | globalMethods: family.globalMethods?.map(cleanMethod), 26 | globalPropertyMethods: family.globalPropertyMethods?.map(cleanMethod), 27 | navigatorMethods: family.navigatorMethods?.map(cleanMethod), 28 | documentMethods: family.documentMethods?.map(cleanMethod), 29 | swrMethods: family.swrMethods?.map(cleanMethod), 30 | globalConstructors: family.globalConstructors?.map(cleanMethod), 31 | })); 32 | writeFileSync(output, JSON.stringify(minimized)); 33 | }); 34 | -------------------------------------------------------------------------------- /src/library/builder.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-restricted-globals 2 | const safeGlobal = typeof self !== 'undefined' ? self : {}; 3 | const safeNavigator = typeof navigator !== 'undefined' ? navigator : {}; 4 | const safeDocument = typeof document !== 'undefined' ? document : {}; 5 | 6 | const web = ({ 7 | name, 8 | globalConstructor, 9 | constructorDescription, 10 | constructorUrl, 11 | globalConstructors, 12 | methods, 13 | className, 14 | globalMethods, 15 | globalProperty, 16 | globalPropertyMethods, 17 | navigatorMethods, 18 | documentMethods, 19 | swrMethods, 20 | }) => 21 | [ 22 | methods && { 23 | name, 24 | methods, 25 | originalRoot: () => safeGlobal[className || name].prototype, 26 | available: typeof safeGlobal[className || name] !== 'undefined', 27 | }, 28 | globalMethods?.length && { 29 | name, 30 | methods: globalMethods, 31 | originalRoot: () => safeGlobal, 32 | available: typeof safeGlobal[globalMethods[0].name] !== 'undefined', 33 | }, 34 | globalPropertyMethods?.length && { 35 | name, 36 | methods: globalPropertyMethods, 37 | originalRoot: () => safeGlobal[globalProperty || className || name], 38 | available: typeof safeGlobal[globalProperty || className || name] !== 'undefined', 39 | }, 40 | navigatorMethods?.length && { 41 | name, 42 | methods: navigatorMethods, 43 | originalRoot: () => safeNavigator, 44 | available: typeof safeNavigator[navigatorMethods[0].name] !== 'undefined', 45 | }, 46 | documentMethods?.length && { 47 | name, 48 | methods: documentMethods, 49 | originalRoot: () => safeDocument, 50 | available: typeof safeDocument[documentMethods[0].name] !== 'undefined', 51 | }, 52 | swrMethods?.length && { 53 | name, 54 | methods: swrMethods, 55 | originalRoot: () => ServiceWorkerRegistration.prototype, 56 | available: typeof ServiceWorkerRegistration !== 'undefined', 57 | }, 58 | ...(globalConstructor 59 | ? [{name: className || name, description: constructorDescription, url: constructorUrl}] 60 | : globalConstructors || [] 61 | ).map((constructor) => ({ 62 | name, 63 | methods: [ 64 | { 65 | ...constructor, 66 | isConstructor: true, 67 | }, 68 | ], 69 | originalRoot: () => safeGlobal, 70 | available: typeof safeGlobal[constructor.name] !== 'undefined', 71 | })), 72 | ].filter((a) => !!a); 73 | 74 | const node = ({name, methods, globalMethod}) => { 75 | let module; 76 | if (!globalMethod) { 77 | try { 78 | // eslint-disable-next-line no-undef 79 | module = __non_webpack_require__(name); 80 | // eslint-disable-next-line no-empty 81 | } catch (error) {} 82 | } 83 | 84 | return { 85 | name, 86 | methods, 87 | originalRoot: () => (globalMethod ? global : module), 88 | available: globalMethod 89 | ? typeof global !== 'undefined' && typeof global[methods[0].name] !== 'undefined' 90 | : typeof module !== 'undefined', 91 | }; 92 | }; 93 | 94 | module.exports = { 95 | buildNodeLibraryFrom: (lib) => lib.map((family) => node(family)), 96 | buildWebLibraryFrom: (lib) => lib.reduce((acc, cur) => [...acc, ...web(cur)], []), 97 | }; 98 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | export const levels = ['debug', 'info', 'warn', 'error']; 2 | 3 | const logger = { 4 | level: 'warn', 5 | }; 6 | 7 | levels.forEach((level) => { 8 | logger[level] = (...args) => { 9 | if (logger.level && levels.indexOf(logger.level) <= levels.indexOf(level)) { 10 | // eslint-disable-next-line no-console 11 | console[level](...args); 12 | } 13 | }; 14 | }); 15 | 16 | export default logger; 17 | -------------------------------------------------------------------------------- /src/platform.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-restricted-globals 2 | const isWeb = typeof window !== 'undefined' || typeof self !== 'undefined'; 3 | const isNode = 4 | typeof process === 'object' && typeof process.versions === 'object' && !!process.versions.node; 5 | 6 | export const PLATFORMS = { 7 | NODE: 'NODE', 8 | WEB: 'WEB', 9 | UNKNOWN: 'UNKNOWN', 10 | }; 11 | 12 | export default () => { 13 | try { 14 | if (isWeb) { 15 | return PLATFORMS.WEB; 16 | } 17 | if (isNode) { 18 | return PLATFORMS.NODE; 19 | } 20 | 21 | return PLATFORMS.UNKNOWN; 22 | } catch (error) { 23 | return PLATFORMS.UNKNOWN; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/source.js: -------------------------------------------------------------------------------- 1 | import {SourceMapConsumer} from 'source-map-js'; 2 | 3 | let fs; 4 | try { 5 | fs = __non_webpack_require__('fs'); 6 | // eslint-disable-next-line no-empty 7 | } catch (error) {} 8 | 9 | const INLINE_SOURCEMAP_REGEX = /^data:application\/json[^,]+base64,/; 10 | const SOURCEMAP_REGEX = 11 | /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*(?:\*\/)[ \t]*$)/; 12 | const logger = console; 13 | 14 | export const getSourceFromUrl = async (url) => { 15 | try { 16 | const response = await fetch(url); 17 | 18 | return response.text(); 19 | } catch (error) { 20 | logger.error(error); 21 | return null; 22 | } 23 | }; 24 | 25 | export const getSourceFromPath = async (path) => { 26 | if (!fs) { 27 | return null; 28 | } 29 | 30 | return new Promise((resolve) => { 31 | fs.readFile(path, {encoding: 'utf8'}, (err, data) => { 32 | if (err) { 33 | logger.error(err); 34 | resolve(null); 35 | } 36 | 37 | resolve(data); 38 | }); 39 | }); 40 | }; 41 | 42 | export const getSource = (location) => { 43 | try { 44 | const url = new URL(location); 45 | return getSourceFromUrl(url); 46 | } catch (error) { 47 | return getSourceFromPath(location); 48 | } 49 | }; 50 | 51 | export const getSourceMapFromSource = async (location) => { 52 | try { 53 | logger.debug(`loading source from ${location}`); 54 | const source = await getSource(location); 55 | 56 | const sourceMapReferenceSearch = source?.match?.(SOURCEMAP_REGEX); 57 | 58 | if (sourceMapReferenceSearch?.[1]) { 59 | logger.debug('found sourcemap reference'); 60 | const sourceMapReference = sourceMapReferenceSearch[1]; 61 | let sourceMap; 62 | 63 | if (INLINE_SOURCEMAP_REGEX.test(sourceMapReference)) { 64 | logger.debug('loading inline sourcemap'); 65 | const rawData = sourceMapReference.slice(sourceMapReference.indexOf(',') + 1); 66 | if (typeof atob === 'function') { 67 | sourceMap = atob(rawData); 68 | } else { 69 | sourceMap = Buffer.from(rawData, 'base64').toString(); 70 | } 71 | } else { 72 | const sourceLocationParts = location.split('/'); 73 | sourceLocationParts.splice(-1, 1); 74 | const sourceMapLocation = [...sourceLocationParts, sourceMapReference].join('/'); 75 | logger.debug(`loading sourcemap from ${sourceMapLocation}`); 76 | sourceMap = await getSource(sourceMapLocation); 77 | } 78 | 79 | if (sourceMap) { 80 | const sourceMapConsumer = await new SourceMapConsumer(sourceMap); 81 | 82 | logger.debug('loaded sourcemap'); 83 | return sourceMapConsumer; 84 | } 85 | } 86 | 87 | logger.debug('sourcemap not available'); 88 | return null; 89 | } catch (error) { 90 | logger.error(error); 91 | return null; 92 | } 93 | }; 94 | 95 | export const getSourceMap = async (location) => { 96 | try { 97 | logger.debug(`loading sourcemap from ${location}`); 98 | const sourceMap = await getSource(location); 99 | 100 | if (sourceMap) { 101 | const sourceMapConsumer = await new SourceMapConsumer(sourceMap); 102 | return sourceMapConsumer; 103 | } 104 | 105 | logger.debug('sourcemap not available'); 106 | return null; 107 | } catch (error) { 108 | logger.error(error); 109 | return null; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/stack.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-cond-assign */ 2 | const lineParseCache = {}; 3 | const nixSlashes = (x) => x.replace(/\\/g, '/'); 4 | 5 | export const parseStackLine = (l) => { 6 | const line = (l || '').trim(); 7 | 8 | const cached = lineParseCache[line]; 9 | if (cached) { 10 | return cached; 11 | } 12 | 13 | let fileLineColumn = []; 14 | let name; 15 | let alias; 16 | let match; 17 | 18 | // Try to parse standard stack lines, like: 19 | // V8: "at eval (eval at <anonymous> (http://localhost:3000/static/js/bundle.js:196037:31), <anonymous>:1:1)" 20 | // V8: "at Object.exports.test (/Users/jason/code/sandworm/tests/node/prod/stack.test.js:10:11)" 21 | // Safari/FF: "init@http://localhost:3000/static/js/bundle.js:24496:21" 22 | if ( 23 | (match = line.match(/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls 24 | (match = line.match(/at (.+) \((.+)\)/)) || 25 | (line.slice(0, 3) !== 'at ' && (match = line.match(/(.*)@(.*)/))) 26 | ) { 27 | [, name] = match; 28 | fileLineColumn = ( 29 | match[2].match(/(.*):(\d+):(\d+)/) || 30 | match[2].match(/(.*):(\d+)/) || 31 | [] 32 | ).slice(1); 33 | // Try to parse stack lines with no caller info, like: 34 | // "at App.js:32:10" 35 | } else if ((match = line.match(/^(at\s+)*(.+):(\d+):(\d+)/))) { 36 | fileLineColumn = match.slice(2); 37 | } else { 38 | lineParseCache[line] = undefined; 39 | return undefined; 40 | } 41 | 42 | // Caller info sometimes includes an alias, like: 43 | // "at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)" 44 | // Try to extract the alias. 45 | if (name?.includes?.(' [as ')) { 46 | [name, alias] = name.slice(0, -1).split(' [as '); 47 | } 48 | 49 | lineParseCache[line] = { 50 | beforeParse: line, 51 | file: nixSlashes(fileLineColumn[0] || ''), 52 | line: parseInt(fileLineColumn[1] || '', 10) || undefined, 53 | column: parseInt(fileLineColumn[2] || '', 10) || undefined, 54 | name, 55 | alias, 56 | }; 57 | return lineParseCache[line]; 58 | }; 59 | 60 | export const currentStack = () => { 61 | // We want the whole stack trace. Temporarily disable the limit. 62 | const currentStackLimit = Error.stackTraceLimit; 63 | Error.stackTraceLimit = Infinity; 64 | const lines = (new Error().stack || '').split('\n'); 65 | Error.stackTraceLimit = currentStackLimit; 66 | const entries = lines 67 | .map(parseStackLine) 68 | .filter((x) => x !== undefined) 69 | // Augment every stack item with details about the call chain. 70 | // Who called the current method? Who did the current method call next? 71 | .map((value, index, original) => { 72 | const pre = original[index + 1]; 73 | const post = original[index - 1]; 74 | return { 75 | ...value, 76 | caller: pre ? pre.alias || pre.name : undefined, 77 | called: post ? post.alias || post.name : undefined, 78 | }; 79 | }); 80 | return entries; 81 | }; 82 | -------------------------------------------------------------------------------- /src/track.js: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | import platform, {PLATFORMS} from './platform'; 3 | 4 | let http; 5 | try { 6 | http = __non_webpack_require__('http'); 7 | // eslint-disable-next-line no-empty 8 | } catch (error) {} 9 | const hasXMLHTTPRequest = typeof XMLHttpRequest !== 'undefined'; 10 | 11 | if (!http && !hasXMLHTTPRequest) { 12 | logger.error('tracking disabled for this platform: no HTTP or XMLHttpRequest available'); 13 | } 14 | 15 | const originals = {}; 16 | let batch = []; 17 | let host = '127.0.0.1'; 18 | let port = 7071; 19 | let currentTimer; 20 | let skipTracking = false; 21 | 22 | export const setSkipTracking = (skipTrackingOption) => { 23 | skipTracking = !!skipTrackingOption; 24 | }; 25 | 26 | // Grab the original methods before we monkey patch them 27 | // so that tracking calls do not get tracked causing an infinite loop 28 | if (http) { 29 | originals.http = { 30 | request: http.request, 31 | }; 32 | } 33 | if (hasXMLHTTPRequest) { 34 | originals.xmlhttprequest = { 35 | XMLHttpRequest, 36 | open: XMLHttpRequest.prototype.open, 37 | send: XMLHttpRequest.prototype.send, 38 | setRequestHeader: XMLHttpRequest.prototype.setRequestHeader, 39 | }; 40 | } 41 | 42 | // Remove circular references from method invoke arguments getting 43 | // converted to JSON to be tracked 44 | export const getCircularReplacer = () => { 45 | const seen = new WeakSet(); 46 | return (key, value) => { 47 | if (typeof value === 'object' && value !== null) { 48 | if (seen.has(value)) { 49 | return undefined; 50 | } 51 | seen.add(value); 52 | } 53 | return value; 54 | }; 55 | }; 56 | 57 | export const sendBatch = () => { 58 | try { 59 | if (skipTracking || batch.length === 0) { 60 | return; 61 | } 62 | const currentBatch = [...batch]; 63 | const resetBatch = () => { 64 | batch = [...currentBatch, ...batch]; 65 | }; 66 | logger.debug('sending tracking...'); 67 | if (platform() === PLATFORMS.NODE && http) { 68 | const req = originals.http.request( 69 | { 70 | port, 71 | host, 72 | path: '/ingest', 73 | method: 'POST', 74 | headers: {'content-type': 'application/json'}, 75 | }, 76 | (res) => { 77 | if (res.statusCode !== 200) { 78 | logger.debug('error tracking call to inspector: got status', res.statusCode); 79 | resetBatch(); 80 | } 81 | }, 82 | ); 83 | req.on('error', (error) => { 84 | logger.debug('error tracking call to inspector:', error.message); 85 | resetBatch(); 86 | }); 87 | req.end(JSON.stringify(currentBatch, getCircularReplacer())); 88 | batch = []; 89 | } else if (hasXMLHTTPRequest) { 90 | const request = new originals.xmlhttprequest.XMLHttpRequest(); 91 | request.onreadystatechange = () => { 92 | if (request.readyState === 4) { 93 | if (request.status !== 200) { 94 | logger.debug('error tracking call to inspector:', request.statusText); 95 | resetBatch(); 96 | } 97 | } 98 | }; 99 | request.addEventListener('error', () => { 100 | logger.debug('error tracking call to inspector'); 101 | resetBatch(); 102 | }); 103 | originals.xmlhttprequest.open.call(request, 'POST', `http://${host}:${port}/ingest`, true); 104 | originals.xmlhttprequest.setRequestHeader.call( 105 | request, 106 | 'content-type', 107 | 'application/json;charset=UTF-8', 108 | ); 109 | originals.xmlhttprequest.send.call( 110 | request, 111 | JSON.stringify(currentBatch, getCircularReplacer()), 112 | ); 113 | batch = []; 114 | } 115 | } catch (error) { 116 | logger.debug('error tracking call to inspector:', error.message); 117 | logger.debug('attempted to track:', batch); 118 | } finally { 119 | currentTimer = null; 120 | } 121 | }; 122 | 123 | export const setTrackingServer = (hostOption, portOption) => { 124 | if (hostOption && typeof hostOption === 'string') { 125 | host = hostOption; 126 | } 127 | if (portOption && typeof portOption === 'number') { 128 | port = portOption; 129 | } 130 | }; 131 | 132 | export default (event) => { 133 | if (!event || typeof event !== 'object') { 134 | logger.warn('track: event is not an object'); 135 | return; 136 | } 137 | 138 | // Add to queue and debounce sending to server 139 | batch.push({...event}); 140 | 141 | if (!currentTimer) { 142 | currentTimer = setTimeout(sendBatch, 1000); 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /tests/configs/jest.unit.jsdom.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'dom', 3 | clearMocks: true, 4 | collectCoverage: true, 5 | coverageDirectory: 'coverage/jsdom', 6 | coveragePathIgnorePatterns: ['/tests/', '/node_modules/'], 7 | coverageProvider: 'babel', 8 | reporters: ['default', ['jest-junit', {outputName: 'junit-jsdom-unit.xml'}]], 9 | rootDir: '../../', 10 | testEnvironment: 'jsdom', 11 | testMatch: ['**/tests/unit/*.test.js', '**/tests/unit/*.test.jsdom.js'], 12 | }; 13 | -------------------------------------------------------------------------------- /tests/configs/jest.unit.node.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'node', 3 | clearMocks: true, 4 | collectCoverage: true, 5 | coverageDirectory: 'coverage/node', 6 | coveragePathIgnorePatterns: ['/tests/', '/node_modules/'], 7 | coverageProvider: 'babel', 8 | reporters: ['default', ['jest-junit', {outputName: 'junit-node-unit.xml'}]], 9 | rootDir: '../../', 10 | testEnvironment: 'node', 11 | testMatch: ['**/tests/unit/*.test.js', '**/tests/unit/*.test.node.js'], 12 | globalSetup: require.resolve('sandworm-jest/setup'), 13 | globalTeardown: require.resolve('sandworm-jest/teardown'), 14 | setupFilesAfterEnv: [require.resolve('sandworm-jest/setupFiles')], 15 | }; 16 | -------------------------------------------------------------------------------- /tests/configs/playwright.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const {devices} = require('@playwright/test'); 3 | 4 | const config = { 5 | forbidOnly: !!process.env.CI, 6 | retries: process.env.CI ? 2 : 0, 7 | workers: process.env.CI ? 2 : undefined, 8 | testDir: '../web/', 9 | testMatch: '*.test.js', 10 | use: { 11 | trace: 'on-first-retry', 12 | }, 13 | globalSetup: require.resolve('../web/setup'), 14 | projects: [ 15 | { 16 | name: 'chromium', 17 | use: {...devices['Desktop Chrome']}, 18 | }, 19 | { 20 | name: 'firefox', 21 | use: {...devices['Desktop Firefox']}, 22 | }, 23 | { 24 | name: 'webkit', 25 | use: {...devices['Desktop Safari']}, 26 | }, 27 | ], 28 | reporter: [['list'], ['junit', {outputFile: 'junit-web-capture.xml'}]], 29 | }; 30 | 31 | module.exports = config; 32 | -------------------------------------------------------------------------------- /tests/node/Function.capture.test.js: -------------------------------------------------------------------------------- 1 | const Sandworm = require('../../dist/index'); 2 | const {expectCallToMatch, loadSandworm} = require('../utils'); 3 | 4 | describe('Function', () => { 5 | beforeAll(loadSandworm); 6 | afterEach(() => Sandworm.clearHistory()); 7 | 8 | test('Function', async () => { 9 | const script = 'console.log("hello from eval")'; 10 | // eslint-disable-next-line no-new-func 11 | const func = new Function(script); 12 | func(); 13 | expectCallToMatch({family: 'Function', method: 'Function', firstArg: script}); 14 | 15 | // eslint-disable-next-line no-new-func 16 | const func2 = Function(`${script};`); 17 | func2(); 18 | expectCallToMatch({family: 'Function', method: 'Function', firstArg: `${script};`}); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/node/Function.enforce.test.js: -------------------------------------------------------------------------------- 1 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 2 | 3 | describe('enforce: Function', () => { 4 | beforeAll(loadSandwormInProductionMode); 5 | 6 | test('Function', () => { 7 | // eslint-disable-next-line no-new-func 8 | expectCallToThrow(() => new Function('console.log()')); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/node/bind.capture.test.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectNoCall, loadSandworm, expectCallToMatch} = require('../utils'); 4 | 5 | describe('bind', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('args', async () => { 10 | http.request.bind(this); 11 | expectNoCall(); 12 | 13 | http.request.bind(this, []); 14 | expectCallToMatch({family: 'bind', method: 'args'}); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/node/bind.enforce.test.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: bind', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('args', () => { 8 | http.request.bind(this); 9 | expectCallToThrow(() => http.request.bind(this, {})); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/node/child_process.capture.test.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const path = require('path'); 3 | const Sandworm = require('../../dist/index'); 4 | const {expectCallToMatch, loadSandworm} = require('../utils'); 5 | 6 | describe('child_process', () => { 7 | beforeAll(loadSandworm); 8 | afterEach(() => Sandworm.clearHistory()); 9 | 10 | test('exec', () => { 11 | const cp = childProcess.exec('echo "test"'); 12 | expect(cp).toBeInstanceOf(childProcess.ChildProcess); 13 | expectCallToMatch({ 14 | family: 'child_process', 15 | method: 'exec', 16 | firstArg: 'echo "test"', 17 | offset: 1, 18 | }); 19 | }); 20 | 21 | test('execFile', () => { 22 | const cp = childProcess.execFile('node', ['--version']); 23 | expect(cp).toBeInstanceOf(childProcess.ChildProcess); 24 | expectCallToMatch({ 25 | family: 'child_process', 26 | method: 'execFile', 27 | firstArg: 'node', 28 | secondArg: ['--version'], 29 | }); 30 | }); 31 | 32 | test('fork', () => { 33 | const scriptPath = path.join(__dirname, 'log.js'); 34 | const cp = childProcess.fork(scriptPath, ['test']); 35 | expect(cp).toBeInstanceOf(childProcess.ChildProcess); 36 | cp.kill(); 37 | expectCallToMatch({ 38 | family: 'child_process', 39 | method: 'fork', 40 | firstArg: scriptPath, 41 | secondArg: ['test'], 42 | }); 43 | }); 44 | 45 | test('spawn', () => { 46 | const cp = childProcess.spawn('node', ['--version']); 47 | expect(cp).toBeInstanceOf(childProcess.ChildProcess); 48 | expectCallToMatch({ 49 | family: 'child_process', 50 | method: 'spawn', 51 | firstArg: 'node', 52 | secondArg: ['--version'], 53 | }); 54 | }); 55 | 56 | test('execSync', () => { 57 | const stdout = childProcess.execSync('echo "test"'); 58 | expect(stdout.toString().trim()).toEqual('test'); 59 | expectCallToMatch({family: 'child_process', method: 'execSync', firstArg: 'echo "test"'}); 60 | }); 61 | 62 | test('execFileSync', () => { 63 | const stdout = childProcess.execFileSync('echo', ['test']); 64 | expect(stdout.toString().trim()).toBe('test'); 65 | expectCallToMatch({ 66 | family: 'child_process', 67 | method: 'execFileSync', 68 | firstArg: 'echo', 69 | secondArg: ['test'], 70 | }); 71 | }); 72 | 73 | test('spawnSync', () => { 74 | const cp = childProcess.spawnSync('echo', ['test']); 75 | expect(cp.stdout.toString().trim()).toBe('test'); 76 | expectCallToMatch({ 77 | family: 'child_process', 78 | method: 'spawnSync', 79 | firstArg: 'echo', 80 | secondArg: ['test'], 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/node/child_process.enforce.test.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const path = require('path'); 3 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 4 | 5 | describe('enforce: child_process', () => { 6 | beforeAll(loadSandwormInProductionMode); 7 | 8 | test('exec', () => { 9 | expectCallToThrow(() => childProcess.exec('echo "test"')); 10 | }); 11 | 12 | test('execFile', () => { 13 | expectCallToThrow(() => childProcess.execFile('node', ['--version'])); 14 | }); 15 | 16 | test('fork', () => { 17 | const scriptPath = path.join(__dirname, 'log.js'); 18 | expectCallToThrow(() => childProcess.fork(scriptPath, ['test'])); 19 | }); 20 | 21 | test('spawn', () => { 22 | expectCallToThrow(() => childProcess.spawn('node', ['--version'])); 23 | }); 24 | 25 | test('execSync', () => { 26 | expectCallToThrow(() => childProcess.execSync('echo "test"')); 27 | }); 28 | 29 | test('execFileSync', () => { 30 | expectCallToThrow(() => childProcess.execFileSync('echo', ['test'])); 31 | }); 32 | 33 | test('spawnSync', () => { 34 | expectCallToThrow(() => childProcess.spawnSync('echo', ['test'])); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/node/cluster.capture.test.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 4 | 5 | describe('cluster', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('fork', () => { 10 | if (cluster.isMaster) { 11 | cluster.fork(); 12 | expectCallToMatch({family: 'cluster', method: 'fork'}); 13 | Object.values(cluster.workers).forEach((worker) => worker.kill()); 14 | } 15 | }); 16 | 17 | // Calling disconnect like this throws on Node v18+ 18 | testif(!process.versions.node.startsWith('18'))('disconnect', () => { 19 | cluster.disconnect(); 20 | expectCallToMatch({family: 'cluster', method: 'disconnect'}); 21 | }); 22 | 23 | test('setupPrimary', () => { 24 | if (cluster.setupPrimary) { 25 | cluster.setupPrimary(); 26 | expectCallToMatch({family: 'cluster', method: 'setupPrimary'}); 27 | } 28 | }); 29 | 30 | test('setupMaster', () => { 31 | cluster.setupMaster(); 32 | expectCallToMatch({family: 'cluster', method: 'setupMaster'}); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/node/cluster.enforce.test.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const {loadSandwormInProductionMode, expectCallToThrow, testif} = require('../utils'); 3 | 4 | describe('enforce: cluster', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('fork', () => { 8 | expectCallToThrow(() => cluster.fork()); 9 | }); 10 | 11 | test('disconnect', () => { 12 | expectCallToThrow(() => cluster.disconnect()); 13 | }); 14 | 15 | testif(cluster.setupPrimary)('setupPrimary', () => { 16 | expectCallToThrow(() => cluster.setupPrimary()); 17 | }); 18 | 19 | test('setupMaster', () => { 20 | expectCallToThrow(() => cluster.setupMaster()); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/node/dgram.capture.test.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm} = require('../utils'); 4 | 5 | describe('dgram', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('disconnect', (done) => { 10 | const socket = dgram.createSocket({type: 'udp4'}); 11 | expect(socket).toBeInstanceOf(dgram.Socket); 12 | expectCallToMatch({family: 'dgram', method: 'createSocket'}); 13 | socket.close(done); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/node/dgram.enforce.test.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: dgram', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('disconnect', () => { 8 | expectCallToThrow(() => dgram.createSocket({type: 'udp4'})); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/node/dns.capture.test.js: -------------------------------------------------------------------------------- 1 | const dns = require('dns'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 4 | 5 | describe('dns', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('Resolver', () => { 10 | const resolver = new dns.Resolver(); 11 | expect(resolver).toBeInstanceOf(dns.Resolver); 12 | expectCallToMatch({family: 'dns', method: 'Resolver'}); 13 | }); 14 | 15 | test('getServers', () => { 16 | const servers = dns.getServers(); 17 | expect(Array.isArray(servers)).toBeTruthy(); 18 | expectCallToMatch({family: 'dns', method: 'getServers'}); 19 | }); 20 | 21 | test('lookup', (done) => { 22 | dns.lookup('localhost', (err, address) => { 23 | if (err) { 24 | done(err); 25 | } 26 | // Node v18+ returns IPv6 loopback 27 | expect(['127.0.0.1', '::1'].includes(address)).toBeTruthy(); 28 | done(); 29 | }); 30 | expectCallToMatch({family: 'dns', method: 'lookup', firstArg: 'localhost'}); 31 | }); 32 | 33 | test('lookupService', (done) => { 34 | dns.lookupService('127.0.0.1', 22, (err, hostname, service) => { 35 | if (err) { 36 | done(err); 37 | } 38 | expect(hostname).toEqual('localhost'); 39 | expect(service).toEqual('ssh'); 40 | done(); 41 | }); 42 | expectCallToMatch({family: 'dns', method: 'lookupService', firstArg: '127.0.0.1'}); 43 | }); 44 | 45 | test('resolve', () => { 46 | dns.resolve('localhost', () => {}); 47 | expectCallToMatch({family: 'dns', method: 'resolve', firstArg: 'localhost'}); 48 | }); 49 | 50 | test('resolve4', () => { 51 | dns.resolve4('localhost', () => {}); 52 | expectCallToMatch({family: 'dns', method: 'resolve4', firstArg: 'localhost'}); 53 | }); 54 | 55 | test('resolve6', () => { 56 | dns.resolve6('localhost', () => {}); 57 | expectCallToMatch({family: 'dns', method: 'resolve6', firstArg: 'localhost'}); 58 | }); 59 | 60 | test('resolveAny', () => { 61 | dns.resolveAny('localhost', () => {}); 62 | expectCallToMatch({family: 'dns', method: 'resolveAny', firstArg: 'localhost'}); 63 | }); 64 | 65 | testif(dns.resolveCaa)('resolveCaa', () => { 66 | dns.resolveCaa('localhost', () => {}); 67 | expectCallToMatch({family: 'dns', method: 'resolveCaa', firstArg: 'localhost'}); 68 | }); 69 | 70 | test('resolveCname', () => { 71 | dns.resolveCname('localhost', () => {}); 72 | expectCallToMatch({family: 'dns', method: 'resolveCname', firstArg: 'localhost'}); 73 | }); 74 | 75 | test('resolveMx', () => { 76 | dns.resolveMx('localhost', () => {}); 77 | expectCallToMatch({family: 'dns', method: 'resolveMx', firstArg: 'localhost'}); 78 | }); 79 | 80 | test('resolveNaptr', () => { 81 | dns.resolveNaptr('localhost', () => {}); 82 | expectCallToMatch({family: 'dns', method: 'resolveNaptr', firstArg: 'localhost'}); 83 | }); 84 | 85 | test('resolveNs', () => { 86 | dns.resolveNs('localhost', () => {}); 87 | expectCallToMatch({family: 'dns', method: 'resolveNs', firstArg: 'localhost'}); 88 | }); 89 | 90 | test('resolvePtr', () => { 91 | dns.resolvePtr('localhost', () => {}); 92 | expectCallToMatch({family: 'dns', method: 'resolvePtr', firstArg: 'localhost'}); 93 | }); 94 | 95 | test('resolveSoa', () => { 96 | dns.resolveSoa('localhost', () => {}); 97 | expectCallToMatch({family: 'dns', method: 'resolveSoa', firstArg: 'localhost'}); 98 | }); 99 | 100 | test('resolveSrv', () => { 101 | dns.resolveSrv('localhost', () => {}); 102 | expectCallToMatch({family: 'dns', method: 'resolveSrv', firstArg: 'localhost'}); 103 | }); 104 | 105 | test('resolveTxt', () => { 106 | dns.resolveTxt('localhost', () => {}); 107 | expectCallToMatch({family: 'dns', method: 'resolveTxt', firstArg: 'localhost'}); 108 | }); 109 | 110 | test('reverse', () => { 111 | dns.reverse('127.0.0.1', () => {}); 112 | expectCallToMatch({family: 'dns', method: 'reverse', firstArg: '127.0.0.1'}); 113 | }); 114 | 115 | test('setServers', () => { 116 | const servers = dns.getServers(); 117 | dns.setServers(servers); 118 | expectCallToMatch({index: 1, family: 'dns', method: 'setServers'}); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/node/dns.enforce.test.js: -------------------------------------------------------------------------------- 1 | const dns = require('dns'); 2 | const {loadSandwormInProductionMode, testif, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: dns', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('Resolver', () => { 8 | expectCallToThrow(() => new dns.Resolver()); 9 | }); 10 | 11 | test('getServers', () => { 12 | expectCallToThrow(() => dns.getServers()); 13 | }); 14 | 15 | test('lookup', () => { 16 | expectCallToThrow(() => dns.lookup('localhost', () => {})); 17 | }); 18 | 19 | test('lookupService', () => { 20 | expectCallToThrow(() => dns.lookupService('127.0.0.1', 22, () => {})); 21 | }); 22 | 23 | test('resolve', () => { 24 | expectCallToThrow(() => dns.resolve('localhost', () => {})); 25 | }); 26 | 27 | test('resolve4', () => { 28 | expectCallToThrow(() => dns.resolve4('localhost', () => {})); 29 | }); 30 | 31 | test('resolve6', () => { 32 | expectCallToThrow(() => dns.resolve6('localhost', () => {})); 33 | }); 34 | 35 | test('resolveAny', () => { 36 | expectCallToThrow(() => dns.resolveAny('localhost', () => {})); 37 | }); 38 | 39 | testif(dns.resolveCaa)('resolveCaa', () => { 40 | expectCallToThrow(() => dns.resolveCaa('localhost', () => {})); 41 | }); 42 | 43 | test('resolveCname', () => { 44 | expectCallToThrow(() => dns.resolveCname('localhost', () => {})); 45 | }); 46 | 47 | test('resolveMx', () => { 48 | expectCallToThrow(() => dns.resolveMx('localhost', () => {})); 49 | }); 50 | 51 | test('resolveNaptr', () => { 52 | expectCallToThrow(() => dns.resolveNaptr('localhost', () => {})); 53 | }); 54 | 55 | test('resolveNs', () => { 56 | expectCallToThrow(() => dns.resolveNs('localhost', () => {})); 57 | }); 58 | 59 | test('resolvePtr', () => { 60 | expectCallToThrow(() => dns.resolvePtr('localhost', () => {})); 61 | }); 62 | 63 | test('resolveSoa', () => { 64 | expectCallToThrow(() => dns.resolveSoa('localhost', () => {})); 65 | }); 66 | 67 | test('resolveSrv', () => { 68 | expectCallToThrow(() => dns.resolveSrv('localhost', () => {})); 69 | }); 70 | 71 | test('resolveTxt', () => { 72 | expectCallToThrow(() => dns.resolveTxt('localhost', () => {})); 73 | }); 74 | 75 | test('reverse', () => { 76 | expectCallToThrow(() => dns.reverse('127.0.0.1', () => {})); 77 | }); 78 | 79 | test('setServers', () => { 80 | expectCallToThrow(() => dns.setServers(['localhost'])); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /tests/node/eval.capture.test.js: -------------------------------------------------------------------------------- 1 | const Sandworm = require('../../dist/index'); 2 | const {expectCallToMatch, loadSandworm} = require('../utils'); 3 | 4 | describe('eval', () => { 5 | beforeAll(loadSandworm); 6 | afterEach(() => Sandworm.clearHistory()); 7 | 8 | test('eval', async () => { 9 | const script = 'console.log("hello from eval")'; 10 | // eslint-disable-next-line no-eval 11 | eval(script); 12 | expectCallToMatch({family: 'eval', method: 'eval', firstArg: script}); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/node/eval.enforce.test.js: -------------------------------------------------------------------------------- 1 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 2 | 3 | describe('enforce: eval', () => { 4 | beforeAll(loadSandwormInProductionMode); 5 | 6 | test('eval', () => { 7 | // eslint-disable-next-line no-eval 8 | expectCallToThrow(() => eval('console.log()')); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/node/fetch.capture.test.js: -------------------------------------------------------------------------------- 1 | const Sandworm = require('../../dist/index'); 2 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 3 | 4 | describe('fetch', () => { 5 | beforeAll(loadSandworm); 6 | afterEach(() => Sandworm.clearHistory()); 7 | 8 | testif(typeof fetch !== 'undefined')('fetch', async () => { 9 | await fetch('https://google.com'); 10 | expectCallToMatch({family: 'fetch', method: 'fetch', firstArg: 'https://google.com'}); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/node/fetch.enforce.test.js: -------------------------------------------------------------------------------- 1 | const {loadSandwormInProductionMode, testif, expectCallToThrow} = require('../utils'); 2 | 3 | describe('enforce: fetch', () => { 4 | beforeAll(loadSandwormInProductionMode); 5 | 6 | testif(typeof fetch !== 'undefined')('fetch', () => { 7 | expectCallToThrow(() => fetch('https://google.com')); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/node/fs.capture.test.js: -------------------------------------------------------------------------------- 1 | const v8 = require('v8'); 2 | // const fs = require('fs'); 3 | 4 | const Sandworm = require('../../dist/index'); 5 | const syncSuite = require('./fs/sync'); 6 | const asyncSuite = require('./fs/async'); 7 | const promisesSuite = require('./fs/promises'); 8 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 9 | 10 | describe('fs', () => { 11 | beforeAll(loadSandworm); 12 | afterEach(() => Sandworm.clearHistory()); 13 | asyncSuite(); 14 | syncSuite(); 15 | promisesSuite(); 16 | describe('v8', () => { 17 | testif(v8.getHeapCodeStatistics)('getHeapCodeStatistics', () => { 18 | v8.getHeapCodeStatistics(); 19 | expectCallToMatch({family: 'v8', method: 'getHeapCodeStatistics'}); 20 | }); 21 | testif(v8.getHeapSnapshot)('getHeapSnapshot', () => { 22 | v8.getHeapSnapshot(); 23 | expectCallToMatch({family: 'v8', method: 'getHeapSnapshot'}); 24 | }); 25 | test('getHeapSpaceStatistics', () => { 26 | v8.getHeapSpaceStatistics(); 27 | expectCallToMatch({family: 'v8', method: 'getHeapSpaceStatistics'}); 28 | }); 29 | test('getHeapStatistics', () => { 30 | v8.getHeapStatistics(); 31 | expectCallToMatch({family: 'v8', method: 'getHeapStatistics'}); 32 | }); 33 | // test('writeHeapSnapshot', () => { 34 | // const filename = v8.writeHeapSnapshot(); 35 | // expectCallToMatch({family: 'v8', method: 'writeHeapSnapshot'}); 36 | // fs.rmSync(filename); 37 | // }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/node/fs/test-dir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandworm-hq/sandworm-guard-js/fe9895207eb98a76388992c5b1facb81b7c4281a/tests/node/fs/test-dir/.gitkeep -------------------------------------------------------------------------------- /tests/node/fs/test.txt: -------------------------------------------------------------------------------- 1 | just for testing -------------------------------------------------------------------------------- /tests/node/http.capture.test.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm} = require('../utils'); 4 | 5 | describe('http', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('Agent', () => { 10 | const agent = new http.Agent(); 11 | expect(agent).toBeInstanceOf(http.Agent); 12 | expectCallToMatch({family: 'http', method: 'Agent'}); 13 | agent.destroy(); 14 | 15 | const agent2 = http.Agent(); 16 | expect(agent2).toBeInstanceOf(http.Agent); 17 | }); 18 | 19 | test('createServer', () => { 20 | const server = http.createServer(); 21 | server.close(); 22 | expect(server).toBeInstanceOf(http.Server); 23 | expectCallToMatch({family: 'http', method: 'createServer'}); 24 | }); 25 | 26 | test('get', () => { 27 | const req = http.get('http://google.com', () => {}); 28 | expect(req).toBeInstanceOf(http.ClientRequest); 29 | expectCallToMatch({family: 'http', method: 'get'}); 30 | req.end(); 31 | }); 32 | 33 | test('request', () => { 34 | const req = http.request('http://google.com', () => {}); 35 | expect(req).toBeInstanceOf(http.ClientRequest); 36 | expectCallToMatch({family: 'http', method: 'request'}); 37 | req.end(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/node/http.enforce.test.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: http', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('Agent', () => { 8 | expectCallToThrow(() => new http.Agent()); 9 | }); 10 | 11 | test('createServer', () => { 12 | expectCallToThrow(() => http.createServer()); 13 | }); 14 | 15 | test('get', () => { 16 | expectCallToThrow(() => http.get('http://google.com', () => {})); 17 | }); 18 | 19 | test('request', () => { 20 | expectCallToThrow(() => http.request('http://google.com', () => {})); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/node/http2.capture.test.js: -------------------------------------------------------------------------------- 1 | const http2 = require('http2'); 2 | const net = require('net'); 3 | const Sandworm = require('../../dist/index'); 4 | const {expectCallToMatch, loadSandworm} = require('../utils'); 5 | 6 | describe('http2', () => { 7 | beforeAll(loadSandworm); 8 | afterEach(() => Sandworm.clearHistory()); 9 | 10 | test('createServer', () => { 11 | const server = http2.createServer(); 12 | server.close(); 13 | expect(server).toBeInstanceOf(net.Server); 14 | expectCallToMatch({family: 'http2', method: 'createServer'}); 15 | }); 16 | 17 | test('createSecureServer', () => { 18 | const server = http2.createSecureServer(); 19 | server.close(); 20 | expect(server).toBeInstanceOf(net.Server); 21 | expectCallToMatch({family: 'http2', method: 'createSecureServer'}); 22 | }); 23 | 24 | test('connect', () => { 25 | const session = http2.connect('https://google.com'); 26 | session.close(); 27 | expectCallToMatch({family: 'http2', method: 'connect'}); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/node/http2.enforce.test.js: -------------------------------------------------------------------------------- 1 | const http2 = require('http2'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: http2', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('createServer', () => { 8 | expectCallToThrow(() => http2.createServer()); 9 | }); 10 | 11 | test('createSecureServer', () => { 12 | expectCallToThrow(() => http2.createSecureServer()); 13 | }); 14 | 15 | test('connect', () => { 16 | expectCallToThrow(() => http2.connect('https://google.com')); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/node/https.capture.test.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm} = require('../utils'); 4 | 5 | describe('https', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('Agent', () => { 10 | const agent = new https.Agent(); 11 | expect(agent).toBeInstanceOf(https.Agent); 12 | expectCallToMatch({family: 'https', method: 'Agent'}); 13 | agent.destroy(); 14 | 15 | const agent2 = https.Agent(); 16 | expect(agent2).toBeInstanceOf(https.Agent); 17 | }); 18 | 19 | test('createServer', () => { 20 | const server = https.createServer(); 21 | server.close(); 22 | expect(server).toBeInstanceOf(https.Server); 23 | expectCallToMatch({family: 'https', method: 'createServer'}); 24 | }); 25 | 26 | test('get', () => { 27 | https.get('https://google.com', () => {}).end(); 28 | expectCallToMatch({family: 'https', method: 'get'}); 29 | }); 30 | 31 | test('request', () => { 32 | https.request('https://google.com', () => {}).end(); 33 | expectCallToMatch({family: 'https', method: 'request'}); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/node/https.enforce.test.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: https', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('Agent', () => { 8 | expectCallToThrow(() => new https.Agent()); 9 | }); 10 | 11 | test('createServer', () => { 12 | expectCallToThrow(() => https.createServer()); 13 | }); 14 | 15 | test('get', () => { 16 | expectCallToThrow(() => https.get('https://google.com', () => {})); 17 | }); 18 | 19 | test('request', () => { 20 | expectCallToThrow(() => https.request('https://google.com', () => {})); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/node/inspector.capture.test.js: -------------------------------------------------------------------------------- 1 | const inspector = require('inspector'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm} = require('../utils'); 4 | 5 | describe('inspector', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('close', () => { 10 | inspector.close(); 11 | expectCallToMatch({family: 'inspector', method: 'close'}); 12 | }); 13 | 14 | test('open', () => { 15 | try { 16 | inspector.open(); 17 | } catch (error) {} 18 | expectCallToMatch({family: 'inspector', method: 'open'}); 19 | inspector.close(); 20 | }); 21 | 22 | test('url', () => { 23 | inspector.url(); 24 | expectCallToMatch({family: 'inspector', method: 'url'}); 25 | }); 26 | 27 | // test('waitForDebugger', () => { 28 | // try { 29 | // inspector.waitForDebugger(); 30 | // } catch (error) { 31 | // expectCallToMatch({family: 'inspector', method: 'waitForDebugger'}); 32 | // } 33 | // }); 34 | 35 | test('Session', () => { 36 | const session = new inspector.Session(); 37 | expectCallToMatch({family: 'inspector', method: 'Session'}); 38 | session.disconnect(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/node/inspector.enforce.test.js: -------------------------------------------------------------------------------- 1 | const inspector = require('inspector'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: inspector', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('close', () => { 8 | expectCallToThrow(() => inspector.close()); 9 | }); 10 | 11 | test('open', () => { 12 | expectCallToThrow(() => inspector.open()); 13 | }); 14 | 15 | test('url', () => { 16 | expectCallToThrow(() => inspector.url()); 17 | }); 18 | 19 | // test('waitForDebugger', () => { 20 | // try { 21 | // inspector.waitForDebugger(); 22 | // } catch (error) { 23 | // expectCallToMatch({family: 'inspector', method: 'waitForDebugger'}); 24 | // } 25 | // }); 26 | 27 | test('Session', () => { 28 | expectCallToThrow(() => new inspector.Session()); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/node/log.js: -------------------------------------------------------------------------------- 1 | console.log('test'); 2 | -------------------------------------------------------------------------------- /tests/node/net.capture.test.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm} = require('../utils'); 4 | 5 | describe('net', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('Server', () => { 10 | const server = new net.Server(); 11 | expect(server).toBeInstanceOf(net.Server); 12 | expectCallToMatch({family: 'net', method: 'Server'}); 13 | 14 | const server2 = net.Server(); 15 | expect(server2).toBeInstanceOf(net.Server); 16 | }); 17 | 18 | test('Socket', () => { 19 | const socket = new net.Socket(); 20 | expect(socket).toBeInstanceOf(net.Socket); 21 | expectCallToMatch({family: 'net', method: 'Socket'}); 22 | 23 | const socket2 = net.Socket(); 24 | expect(socket2).toBeInstanceOf(net.Socket); 25 | }); 26 | 27 | test('connect', () => { 28 | try { 29 | net.connect().on('error', () => {}); 30 | } catch (error) { 31 | } finally { 32 | expectCallToMatch({family: 'net', method: 'connect'}); 33 | } 34 | }); 35 | 36 | test('createConnection', () => { 37 | try { 38 | net.createConnection().on('error', () => {}); 39 | } catch (error) { 40 | } finally { 41 | expectCallToMatch({family: 'net', method: 'createConnection'}); 42 | } 43 | }); 44 | 45 | test('createServer', () => { 46 | const server = net.createServer(); 47 | expect(server).toBeInstanceOf(net.Server); 48 | expectCallToMatch({family: 'net', method: 'createServer'}); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/node/net.enforce.test.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: net', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('Server', () => { 8 | expectCallToThrow(() => new net.Server()); 9 | }); 10 | 11 | test('Socket', () => { 12 | expectCallToThrow(() => new net.Socket()); 13 | }); 14 | 15 | test('connect', () => { 16 | expectCallToThrow(() => net.connect()); 17 | }); 18 | 19 | test('createConnection', () => { 20 | expectCallToThrow(() => net.createConnection()); 21 | }); 22 | 23 | test('createServer', () => { 24 | expectCallToThrow(() => net.createServer()); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/node/os.capture.test.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 4 | 5 | describe('os', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('arch', () => { 10 | os.arch(); 11 | expectCallToMatch({family: 'os', method: 'arch'}); 12 | }); 13 | 14 | test('cpus', () => { 15 | os.cpus(); 16 | expectCallToMatch({family: 'os', method: 'cpus'}); 17 | }); 18 | 19 | test('endianness', () => { 20 | os.endianness(); 21 | expectCallToMatch({family: 'os', method: 'endianness'}); 22 | }); 23 | 24 | test('freemem', () => { 25 | os.freemem(); 26 | expectCallToMatch({family: 'os', method: 'freemem'}); 27 | }); 28 | 29 | test('getPriority', () => { 30 | os.getPriority(); 31 | expectCallToMatch({family: 'os', method: 'getPriority'}); 32 | }); 33 | 34 | test('homedir', () => { 35 | os.homedir(); 36 | expectCallToMatch({family: 'os', method: 'homedir'}); 37 | }); 38 | 39 | test('hostname', () => { 40 | os.hostname(); 41 | expectCallToMatch({family: 'os', method: 'hostname'}); 42 | }); 43 | 44 | test('loadavg', () => { 45 | os.loadavg(); 46 | expectCallToMatch({family: 'os', method: 'loadavg'}); 47 | }); 48 | 49 | test('networkInterfaces', () => { 50 | os.networkInterfaces(); 51 | expectCallToMatch({family: 'os', method: 'networkInterfaces'}); 52 | }); 53 | 54 | test('platform', () => { 55 | os.platform(); 56 | expectCallToMatch({family: 'os', method: 'platform'}); 57 | }); 58 | 59 | test('release', () => { 60 | os.release(); 61 | expectCallToMatch({family: 'os', method: 'release'}); 62 | }); 63 | 64 | test('setPriority', () => { 65 | os.setPriority(os.getPriority()); 66 | expectCallToMatch({family: 'os', method: 'setPriority'}); 67 | }); 68 | 69 | test('tmpdir', () => { 70 | os.tmpdir(); 71 | expectCallToMatch({family: 'os', method: 'tmpdir'}); 72 | }); 73 | 74 | test('totalmem', () => { 75 | os.totalmem(); 76 | expectCallToMatch({family: 'os', method: 'totalmem'}); 77 | }); 78 | 79 | test('type', () => { 80 | os.type(); 81 | expectCallToMatch({family: 'os', method: 'type'}); 82 | }); 83 | 84 | test('uptime', () => { 85 | os.uptime(); 86 | expectCallToMatch({family: 'os', method: 'uptime'}); 87 | }); 88 | 89 | test('userInfo', () => { 90 | os.userInfo(); 91 | expectCallToMatch({family: 'os', method: 'userInfo'}); 92 | }); 93 | 94 | testif(os.version)('version', () => { 95 | os.version(); 96 | expectCallToMatch({family: 'os', method: 'version'}); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /tests/node/os.enforce.test.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const {loadSandwormInProductionMode, testif, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: os', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('arch', () => { 8 | expectCallToThrow(() => os.arch()); 9 | }); 10 | 11 | test('cpus', () => { 12 | expectCallToThrow(() => os.cpus()); 13 | }); 14 | 15 | test('endianness', () => { 16 | expectCallToThrow(() => os.endianness()); 17 | }); 18 | 19 | test('freemem', () => { 20 | expectCallToThrow(() => os.freemem()); 21 | }); 22 | 23 | test('getPriority', () => { 24 | expectCallToThrow(() => os.getPriority()); 25 | }); 26 | 27 | test('homedir', () => { 28 | expectCallToThrow(() => os.homedir()); 29 | }); 30 | 31 | test('hostname', () => { 32 | expectCallToThrow(() => os.hostname()); 33 | }); 34 | 35 | test('loadavg', () => { 36 | expectCallToThrow(() => os.loadavg()); 37 | }); 38 | 39 | test('networkInterfaces', () => { 40 | expectCallToThrow(() => os.networkInterfaces()); 41 | }); 42 | 43 | test('platform', () => { 44 | expectCallToThrow(() => os.platform()); 45 | }); 46 | 47 | test('release', () => { 48 | expectCallToThrow(() => os.release()); 49 | }); 50 | 51 | test('setPriority', () => { 52 | expectCallToThrow(() => os.setPriority()); 53 | }); 54 | 55 | test('tmpdir', () => { 56 | expectCallToThrow(() => os.tmpdir()); 57 | }); 58 | 59 | test('totalmem', () => { 60 | expectCallToThrow(() => os.totalmem()); 61 | }); 62 | 63 | test('type', () => { 64 | expectCallToThrow(() => os.type()); 65 | }); 66 | 67 | test('uptime', () => { 68 | expectCallToThrow(() => os.uptime()); 69 | }); 70 | 71 | test('userInfo', () => { 72 | expectCallToThrow(() => os.userInfo()); 73 | }); 74 | 75 | testif(os.version)('version', () => { 76 | expectCallToThrow(() => os.version()); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/node/process.enforce.test.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const {loadSandwormInProductionMode, testif, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: os', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('abort', () => { 8 | expectCallToThrow(() => process.abort()); 9 | }); 10 | 11 | test('chdir', () => { 12 | expectCallToThrow(() => process.chdir('.')); 13 | }); 14 | 15 | test('cpuUsage', () => { 16 | expectCallToThrow(() => process.cpuUsage()); 17 | }); 18 | 19 | test('cwd', () => { 20 | expectCallToThrow(() => process.cwd()); 21 | }); 22 | 23 | test('disconnect', () => { 24 | expectCallToThrow(() => process.disconnect()); 25 | }); 26 | 27 | test('dlopen', () => { 28 | expectCallToThrow(() => process.dlopen()); 29 | }); 30 | 31 | test('emitWarning', () => { 32 | expectCallToThrow(() => process.emitWarning('test warning, all good')); 33 | }); 34 | 35 | test('exit', () => { 36 | expectCallToThrow(() => process.exit()); 37 | }); 38 | 39 | testif(process.getActiveResourcesInfo)('getActiveResourcesInfo', () => { 40 | expectCallToThrow(() => process.getActiveResourcesInfo()); 41 | }); 42 | 43 | test('getegid', () => { 44 | expectCallToThrow(() => process.getegid()); 45 | }); 46 | 47 | test('geteuid', () => { 48 | expectCallToThrow(() => process.geteuid()); 49 | }); 50 | 51 | test('getgid', () => { 52 | expectCallToThrow(() => process.getgid()); 53 | }); 54 | 55 | test('getgroups', () => { 56 | expectCallToThrow(() => process.getgroups()); 57 | }); 58 | 59 | test('getuid', () => { 60 | expectCallToThrow(() => process.getuid()); 61 | }); 62 | 63 | test('hasUncaughtExceptionCaptureCallback', () => { 64 | expectCallToThrow(() => process.hasUncaughtExceptionCaptureCallback()); 65 | }); 66 | 67 | test('hrtime', () => { 68 | expectCallToThrow(() => process.hrtime()); 69 | }); 70 | 71 | test('initgroups', () => { 72 | expectCallToThrow(() => process.initgroups()); 73 | }); 74 | 75 | test('kill', () => { 76 | expectCallToThrow(() => process.kill()); 77 | }); 78 | 79 | test('memoryUsage', () => { 80 | expectCallToThrow(() => process.memoryUsage()); 81 | }); 82 | 83 | testif(process.resourceUsage)('resourceUsage', () => { 84 | expectCallToThrow(() => process.resourceUsage()); 85 | }); 86 | 87 | test('send', () => { 88 | expectCallToThrow(() => process.send('test')); 89 | }); 90 | 91 | test('setegid', () => { 92 | expectCallToThrow(() => process.setegid(-1)); 93 | }); 94 | 95 | test('seteuid', () => { 96 | expectCallToThrow(() => process.seteuid(-1)); 97 | }); 98 | 99 | test('setgid', () => { 100 | expectCallToThrow(() => process.setgid(-1)); 101 | }); 102 | 103 | test('setgroups', () => { 104 | expectCallToThrow(() => process.setgroups([-1])); 105 | }); 106 | 107 | test('setuid', () => { 108 | expectCallToThrow(() => process.setuid(-1)); 109 | }); 110 | 111 | testif(process.setSourceMapsEnabled)('setSourceMapsEnabled', () => { 112 | expectCallToThrow(() => process.setSourceMapsEnabled(true)); 113 | }); 114 | 115 | test('setUncaughtExceptionCaptureCallback', () => { 116 | expectCallToThrow(() => process.setUncaughtExceptionCaptureCallback()); 117 | }); 118 | 119 | test('umask', () => { 120 | expectCallToThrow(() => process.umask()); 121 | }); 122 | 123 | test('uptime', () => { 124 | expectCallToThrow(() => process.uptime()); 125 | }); 126 | 127 | test('on', () => { 128 | expectCallToThrow(() => process.on('test', () => {})); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /tests/node/timers.capture.test.js: -------------------------------------------------------------------------------- 1 | const timers = require('timers'); 2 | 3 | let timersPromises; 4 | 5 | try { 6 | timersPromises = require('timers/promises'); 7 | } catch (error) {} 8 | 9 | const Sandworm = require('../../dist/index'); 10 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 11 | 12 | describe('timers', () => { 13 | beforeAll(loadSandworm); 14 | afterEach(() => Sandworm.clearHistory()); 15 | 16 | describe('timersAsync', () => { 17 | test('setImmediate', () => { 18 | timers.setImmediate(() => {}); 19 | expectCallToMatch({family: 'timers', method: 'setImmediate'}); 20 | }); 21 | 22 | test('setInterval', () => { 23 | const interval = timers.setInterval(() => {}, 100); 24 | expectCallToMatch({family: 'timers', method: 'setInterval'}); 25 | timers.clearInterval(interval); 26 | }); 27 | 28 | test('setTimeout', () => { 29 | const timeout = timers.setTimeout(() => {}, 100); 30 | expectCallToMatch({family: 'timers', method: 'setTimeout'}); 31 | timers.clearTimeout(timeout); 32 | }); 33 | 34 | test('clearImmediate', () => { 35 | const immediate = timers.setImmediate(() => {}); 36 | timers.clearImmediate(immediate); 37 | expectCallToMatch({family: 'timers', method: 'clearImmediate'}); 38 | }); 39 | 40 | test('clearInterval', () => { 41 | const interval = timers.setInterval(() => {}, 100); 42 | timers.clearInterval(interval); 43 | expectCallToMatch({family: 'timers', method: 'clearInterval'}); 44 | }); 45 | 46 | test('clearTimeout', () => { 47 | const timeout = timers.setTimeout(() => {}, 100); 48 | timers.clearTimeout(timeout); 49 | expectCallToMatch({family: 'timers', method: 'clearTimeout'}); 50 | }); 51 | }); 52 | 53 | if (timersPromises) { 54 | describe('timersPromises', () => { 55 | test('setImmediate', async () => { 56 | await timersPromises.setImmediate(''); 57 | expectCallToMatch({family: 'timers/promises', method: 'setImmediate'}); 58 | }); 59 | 60 | testif(timersPromises.setInterval)('setInterval', async () => { 61 | await timersPromises.setInterval(); 62 | expectCallToMatch({family: 'timers/promises', method: 'setInterval'}); 63 | }); 64 | 65 | test('setTimeout', async () => { 66 | await timersPromises.setTimeout(); 67 | expectCallToMatch({family: 'timers/promises', method: 'setTimeout'}); 68 | }); 69 | }); 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /tests/node/timers.enforce.test.js: -------------------------------------------------------------------------------- 1 | const timers = require('timers'); 2 | 3 | let timersPromises; 4 | 5 | try { 6 | timersPromises = require('timers/promises'); 7 | } catch (error) {} 8 | 9 | const {loadSandwormInProductionMode, expectCallToThrow, testif} = require('../utils'); 10 | 11 | describe('enforce: timers', () => { 12 | beforeAll(loadSandwormInProductionMode); 13 | 14 | describe('timersAsync', () => { 15 | test('setImmediate', () => { 16 | expectCallToThrow(() => timers.setImmediate(() => {})); 17 | }); 18 | 19 | test('setInterval', () => { 20 | expectCallToThrow(() => timers.setInterval(() => {}, 100)); 21 | }); 22 | 23 | test('setTimeout', () => { 24 | expectCallToThrow(() => timers.setTimeout(() => {}, 100)); 25 | }); 26 | 27 | test('clearImmediate', () => { 28 | expectCallToThrow(() => timers.clearImmediate(1)); 29 | }); 30 | 31 | test('clearInterval', () => { 32 | expectCallToThrow(() => timers.clearInterval(1)); 33 | }); 34 | 35 | test('clearTimeout', () => { 36 | expectCallToThrow(() => timers.clearTimeout(1)); 37 | }); 38 | }); 39 | 40 | if (timersPromises) { 41 | describe('timersPromises', () => { 42 | test('setImmediate', () => { 43 | expectCallToThrow(() => timersPromises.setImmediate('')); 44 | }); 45 | 46 | testif(timersPromises.setInterval)('setInterval', () => { 47 | expectCallToThrow(() => timersPromises.setInterval(() => {}, 100)); 48 | }); 49 | 50 | test('setTimeout', () => { 51 | expectCallToThrow(() => timersPromises.setTimeout(() => {}, 100)); 52 | }); 53 | }); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /tests/node/tls.capture.test.js: -------------------------------------------------------------------------------- 1 | const tls = require('tls'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm} = require('../utils'); 4 | 5 | describe('tls', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('Server', () => { 10 | const server = new tls.Server(); 11 | expect(server).toBeInstanceOf(tls.Server); 12 | expectCallToMatch({family: 'tls', method: 'Server'}); 13 | 14 | const server2 = tls.Server(); 15 | expect(server2).toBeInstanceOf(tls.Server); 16 | }); 17 | 18 | test('TLSSocket', () => { 19 | const socket = new tls.TLSSocket(); 20 | socket.destroy(); 21 | expectCallToMatch({family: 'tls', method: 'TLSSocket'}); 22 | }); 23 | 24 | test('checkServerIdentity', () => { 25 | try { 26 | tls.checkServerIdentity(); 27 | } catch (error) { 28 | } finally { 29 | expectCallToMatch({family: 'tls', method: 'checkServerIdentity'}); 30 | } 31 | }); 32 | 33 | test('connect', () => { 34 | const socket = tls.connect(443, 'google.com'); 35 | socket.on('error', () => {}); 36 | expectCallToMatch({family: 'tls', method: 'connect'}); 37 | }); 38 | 39 | test('createSecureContext', () => { 40 | tls.createSecureContext(); 41 | expectCallToMatch({family: 'tls', method: 'createSecureContext'}); 42 | }); 43 | 44 | test('createSecurePair', () => { 45 | try { 46 | tls.createSecurePair(); 47 | } catch (error) { 48 | } finally { 49 | expectCallToMatch({family: 'tls', method: 'createSecurePair'}); 50 | } 51 | }); 52 | 53 | test('createServer', () => { 54 | const server = tls.createServer(); 55 | expect(server).toBeInstanceOf(tls.Server); 56 | expectCallToMatch({family: 'tls', method: 'createServer'}); 57 | server.close(); 58 | }); 59 | 60 | test('getCiphers', () => { 61 | tls.getCiphers(); 62 | expectCallToMatch({family: 'tls', method: 'getCiphers'}); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/node/tls.enforce.test.js: -------------------------------------------------------------------------------- 1 | const tls = require('tls'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: tls', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('Server', () => { 8 | expectCallToThrow(() => new tls.Server()); 9 | }); 10 | 11 | test('TLSSocket', () => { 12 | expectCallToThrow(() => new tls.TLSSocket()); 13 | }); 14 | 15 | test('checkServerIdentity', () => { 16 | expectCallToThrow(() => tls.checkServerIdentity()); 17 | }); 18 | 19 | test('connect', () => { 20 | expectCallToThrow(() => tls.connect()); 21 | }); 22 | 23 | test('createSecureContext', () => { 24 | expectCallToThrow(() => tls.createSecureContext()); 25 | }); 26 | 27 | test('createSecurePair', () => { 28 | expectCallToThrow(() => tls.createSecurePair()); 29 | }); 30 | 31 | test('createServer', () => { 32 | expectCallToThrow(() => tls.createServer()); 33 | }); 34 | 35 | test('getCiphers', () => { 36 | expectCallToThrow(() => tls.getCiphers()); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/node/trace_events.capture.test.js: -------------------------------------------------------------------------------- 1 | const trace = require('trace_events'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm} = require('../utils'); 4 | 5 | describe('trace_events', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('createTracing', () => { 10 | const tracing = trace.createTracing({categories: ['node.perf']}); 11 | expectCallToMatch({family: 'trace_events', method: 'createTracing'}); 12 | tracing.disable(); 13 | }); 14 | 15 | test('getEnabledCategories', () => { 16 | trace.getEnabledCategories(); 17 | expectCallToMatch({family: 'trace_events', method: 'getEnabledCategories'}); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/node/trace_events.enforce.test.js: -------------------------------------------------------------------------------- 1 | const trace = require('trace_events'); 2 | const {loadSandwormInProductionMode, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: trace_events', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('createTracing', () => { 8 | expectCallToThrow(() => trace.createTracing({categories: ['node.perf']})); 9 | }); 10 | 11 | test('getEnabledCategories', () => { 12 | expectCallToThrow(() => trace.getEnabledCategories()); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/node/v8.capture.test.js: -------------------------------------------------------------------------------- 1 | const v8 = require('v8'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 4 | 5 | describe('v8', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('cachedDataVersionTag', () => { 10 | v8.cachedDataVersionTag(); 11 | expectCallToMatch({family: 'v8', method: 'cachedDataVersionTag'}); 12 | }); 13 | 14 | test('setFlagsFromString', () => { 15 | v8.setFlagsFromString('--notrace_gc'); 16 | expectCallToMatch({family: 'v8', method: 'setFlagsFromString'}); 17 | }); 18 | 19 | testif(v8.stopCoverage)('stopCoverage', () => { 20 | v8.stopCoverage(); 21 | expectCallToMatch({family: 'v8', method: 'stopCoverage'}); 22 | }); 23 | 24 | testif(v8.takeCoverage)('takeCoverage', () => { 25 | v8.takeCoverage(); 26 | expectCallToMatch({family: 'v8', method: 'takeCoverage'}); 27 | }); 28 | 29 | test('serialize', () => { 30 | v8.serialize(''); 31 | expectCallToMatch({family: 'v8', method: 'serialize'}); 32 | }); 33 | 34 | test('deserialize', () => { 35 | try { 36 | v8.deserialize(Buffer.from('')); 37 | } catch (error) { 38 | } finally { 39 | expectCallToMatch({family: 'v8', method: 'deserialize'}); 40 | } 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/node/v8.enforce.test.js: -------------------------------------------------------------------------------- 1 | const v8 = require('v8'); 2 | const {loadSandwormInProductionMode, testif, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: v8', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('cachedDataVersionTag', () => { 8 | expectCallToThrow(() => v8.cachedDataVersionTag()); 9 | }); 10 | 11 | test('setFlagsFromString', () => { 12 | expectCallToThrow(() => v8.setFlagsFromString('--harmony')); 13 | }); 14 | 15 | testif(v8.stopCoverage)('stopCoverage', () => { 16 | expectCallToThrow(() => v8.stopCoverage()); 17 | }); 18 | 19 | testif(v8.takeCoverage)('takeCoverage', () => { 20 | expectCallToThrow(() => v8.takeCoverage()); 21 | }); 22 | 23 | test('serialize', () => { 24 | expectCallToThrow(() => v8.serialize('')); 25 | }); 26 | 27 | test('deserialize', () => { 28 | expectCallToThrow(() => v8.deserialize(Buffer.from(''))); 29 | }); 30 | 31 | testif(v8.getHeapCodeStatistics)('getHeapCodeStatistics', () => { 32 | expectCallToThrow(() => v8.getHeapCodeStatistics()); 33 | }); 34 | 35 | testif(v8.getHeapSnapshot)('getHeapSnapshot', () => { 36 | expectCallToThrow(() => v8.getHeapSnapshot()); 37 | }); 38 | 39 | test('getHeapSpaceStatistics', () => { 40 | expectCallToThrow(() => v8.getHeapSpaceStatistics()); 41 | }); 42 | 43 | test('getHeapStatistics', () => { 44 | expectCallToThrow(() => v8.getHeapStatistics()); 45 | }); 46 | 47 | testif(v8.writeHeapSnapshot)('writeHeapSnapshot', () => { 48 | expectCallToThrow(() => v8.writeHeapSnapshot()); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/node/vm.capture.test.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'); 2 | const Sandworm = require('../../dist/index'); 3 | const {expectCallToMatch, loadSandworm, testif} = require('../utils'); 4 | 5 | describe('vm', () => { 6 | beforeAll(loadSandworm); 7 | afterEach(() => Sandworm.clearHistory()); 8 | 9 | test('Script', () => { 10 | new vm.Script(''); 11 | expectCallToMatch({family: 'vm', method: 'Script'}); 12 | }); 13 | 14 | testif(vm.SourceTextModule)('SourceTextModule', () => { 15 | const module = new vm.SourceTextModule(); 16 | expect(module).toBeInstanceOf(vm.SourceTextModule); 17 | expectCallToMatch({family: 'vm', method: 'SourceTextModule'}); 18 | 19 | const module2 = vm.SourceTextModule(); 20 | expect(module2).toBeInstanceOf(vm.SourceTextModule); 21 | }); 22 | 23 | testif(vm.SyntheticModule)('SyntheticModule', () => { 24 | const module = new vm.SyntheticModule(); 25 | expect(module).toBeInstanceOf(vm.SyntheticModule); 26 | expectCallToMatch({family: 'vm', method: 'SyntheticModule'}); 27 | 28 | const module2 = new vm.SyntheticModule(); 29 | expect(module2).toBeInstanceOf(vm.SyntheticModule); 30 | }); 31 | 32 | test('compileFunction', () => { 33 | vm.compileFunction(''); 34 | expectCallToMatch({family: 'vm', method: 'compileFunction'}); 35 | }); 36 | 37 | test('createContext', () => { 38 | vm.createContext(); 39 | expectCallToMatch({family: 'vm', method: 'createContext'}); 40 | }); 41 | 42 | test('isContext', () => { 43 | const context = vm.createContext(); 44 | expect(vm.isContext(context)).toBeTruthy(); 45 | expectCallToMatch({family: 'vm', method: 'isContext'}); 46 | }); 47 | 48 | testif(vm.measureMemory)('measureMemory', () => { 49 | vm.measureMemory(); 50 | expectCallToMatch({family: 'vm', method: 'measureMemory'}); 51 | }); 52 | 53 | test('runInContext', () => { 54 | const context = vm.createContext(); 55 | vm.runInContext('', context); 56 | expectCallToMatch({family: 'vm', method: 'runInContext'}); 57 | }); 58 | 59 | test('runInNewContext', () => { 60 | vm.runInNewContext(''); 61 | expectCallToMatch({family: 'vm', method: 'runInNewContext'}); 62 | }); 63 | 64 | test('runInThisContext', () => { 65 | vm.runInThisContext(''); 66 | expectCallToMatch({family: 'vm', method: 'runInThisContext'}); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/node/vm.enforce.test.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'); 2 | const {loadSandwormInProductionMode, testif, expectCallToThrow} = require('../utils'); 3 | 4 | describe('enforce: vm', () => { 5 | beforeAll(loadSandwormInProductionMode); 6 | 7 | test('Script', () => { 8 | expectCallToThrow(() => new vm.Script('')); 9 | }); 10 | 11 | testif(vm.SourceTextModule)('SourceTextModule', () => { 12 | expectCallToThrow(() => new vm.SourceTextModule()); 13 | }); 14 | 15 | testif(vm.SyntheticModule)('SyntheticModule', () => { 16 | expectCallToThrow(() => new vm.SyntheticModule()); 17 | }); 18 | 19 | test('compileFunction', () => { 20 | expectCallToThrow(() => vm.compileFunction('')); 21 | }); 22 | 23 | test('createContext', () => { 24 | expectCallToThrow(() => vm.createContext()); 25 | }); 26 | 27 | test('isContext', () => { 28 | expectCallToThrow(() => vm.isContext()); 29 | }); 30 | 31 | testif(vm.measureMemory)('measureMemory', () => { 32 | expectCallToThrow(() => vm.measureMemory()); 33 | }); 34 | 35 | test('runInContext', () => { 36 | expectCallToThrow(() => vm.runInContext('', {})); 37 | }); 38 | 39 | test('runInNewContext', () => { 40 | expectCallToThrow(() => vm.runInNewContext('')); 41 | }); 42 | 43 | test('runInThisContext', () => { 44 | expectCallToThrow(() => vm.runInThisContext('')); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/unit/bundle-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "yarn build-nosources-cheap-source-map && yarn build-inline-nosources-cheap-source-map", 8 | "build-nosources-cheap-source-map": "npx webpack --mode=development --devtool=nosources-cheap-source-map --output-path=../nosources-cheap-source-map", 9 | "build-inline-nosources-cheap-source-map": "npx webpack --mode=development --devtool=inline-nosources-cheap-source-map --output-path=../inline-nosources-cheap-source-map", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "Telepat <dev@telepat.io> (http://telepat.io/)", 14 | "license": "ISC", 15 | "dependencies": { 16 | "lodash": "^4.17.21", 17 | "webpack": "^5.74.0", 18 | "webpack-cli": "^4.10.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/unit/bundle-generator/src/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | function component() { 4 | const element = document.createElement('div'); 5 | element.innerHTML = _.join(['Hello', 'webpack'], ' '); 6 | return element; 7 | } 8 | 9 | document.body.appendChild(component()); 10 | -------------------------------------------------------------------------------- /tests/unit/logger.test.js: -------------------------------------------------------------------------------- 1 | const {default: logger} = require('../../src/logger'); 2 | 3 | describe('logger', () => { 4 | test('should properly log', () => { 5 | expect(logger.level).toBe('warn'); 6 | 7 | const debugSpy = jest.spyOn(console, 'debug'); 8 | const infoSpy = jest.spyOn(console, 'info'); 9 | const warnSpy = jest.spyOn(console, 'warn'); 10 | const errorSpy = jest.spyOn(console, 'error'); 11 | 12 | logger.level = 'debug'; 13 | logger.debug('test'); 14 | expect(debugSpy).toBeCalledWith('test'); 15 | expect(debugSpy).toBeCalledTimes(1); 16 | logger.info('test'); 17 | expect(infoSpy).toBeCalledTimes(1); 18 | logger.warn('test'); 19 | expect(warnSpy).toBeCalledTimes(1); 20 | logger.error('test'); 21 | expect(errorSpy).toBeCalledTimes(1); 22 | 23 | logger.level = 'info'; 24 | logger.debug('test'); 25 | expect(debugSpy).toBeCalledTimes(1); 26 | logger.info('test'); 27 | expect(infoSpy).toBeCalledTimes(2); 28 | logger.warn('test'); 29 | expect(warnSpy).toBeCalledTimes(2); 30 | logger.error('test'); 31 | expect(errorSpy).toBeCalledTimes(2); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/unit/platform.test.jsdom.js: -------------------------------------------------------------------------------- 1 | const {default: platform, PLATFORMS} = require('../../src/platform'); 2 | 3 | describe('platform', () => { 4 | test('should detect the node platform', () => { 5 | expect(platform()).toBe(PLATFORMS.WEB); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/unit/platform.test.node.js: -------------------------------------------------------------------------------- 1 | const {default: platform, PLATFORMS} = require('../../src/platform'); 2 | 3 | describe('platform', () => { 4 | test('should detect the node platform', () => { 5 | expect(platform()).toBe(PLATFORMS.NODE); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/unit/source.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-underscore-dangle 2 | global.__non_webpack_require__ = require; 3 | 4 | const path = require('path'); 5 | const {SourceMapConsumer} = require('source-map-js'); 6 | const { 7 | getSourceFromUrl, 8 | getSourceFromPath, 9 | getSource, 10 | getSourceMap, 11 | getSourceMapFromSource, 12 | } = require('../../src/source'); 13 | 14 | describe('source', () => { 15 | test('should get source from a url', async () => { 16 | global.fetch = jest.fn((url) => ({text: () => url})); 17 | 18 | const data = await getSourceFromUrl('https://google.com'); 19 | expect(global.fetch).toBeCalledWith('https://google.com'); 20 | expect(data).toBe('https://google.com'); 21 | }); 22 | 23 | test('should get source from a valid path', async () => { 24 | const data = await getSourceFromPath( 25 | path.resolve(__dirname, 'nosources-cheap-source-map', 'main.js'), 26 | ); 27 | expect(data.length).toBe(548767); 28 | }); 29 | 30 | test('should fail to get source from an invalid path', async () => { 31 | const nullData = await getSourceFromPath( 32 | path.resolve(__dirname, 'nosources-cheap-source-map', 'bogus.js'), 33 | ); 34 | expect(nullData).toBeNull(); 35 | }); 36 | 37 | test('should accept both urls and paths with `getSource`', async () => { 38 | global.fetch = jest.fn((url) => ({text: () => url.toString()})); 39 | 40 | const urlData = await getSource('https://google.com'); 41 | expect(global.fetch).toBeCalledWith(new URL('https://google.com')); 42 | expect(urlData).toBe('https://google.com/'); 43 | 44 | const pathData = await getSource( 45 | path.resolve(__dirname, 'nosources-cheap-source-map', 'main.js'), 46 | ); 47 | expect(pathData.length).toBe(548767); 48 | }); 49 | 50 | test('should load a valid sourcemap', async () => { 51 | const source = await getSourceMap( 52 | path.resolve(__dirname, 'nosources-cheap-source-map', 'main.js.map'), 53 | ); 54 | expect(source).toBeInstanceOf(SourceMapConsumer); 55 | }); 56 | 57 | test('should fail to load an invalid sourcemap', async () => { 58 | const nullSource = await getSourceMap( 59 | path.resolve(__dirname, 'nosources-cheap-source-map', 'bogus.js.map'), 60 | ); 61 | expect(nullSource).toBeNull(); 62 | }); 63 | 64 | test('should load an external sourcemap from a source file', async () => { 65 | const externalSource = await getSourceMapFromSource( 66 | path.resolve(__dirname, 'nosources-cheap-source-map', 'main.js'), 67 | ); 68 | expect(externalSource).toBeInstanceOf(SourceMapConsumer); 69 | }); 70 | 71 | test('should load an inline sourcemap from a source file', async () => { 72 | const internalSource = await getSourceMapFromSource( 73 | path.resolve(__dirname, 'inline-nosources-cheap-source-map', 'main.js'), 74 | ); 75 | expect(internalSource).toBeInstanceOf(SourceMapConsumer); 76 | }); 77 | 78 | test('should fail to load a sourcemap from an invalid source file', async () => { 79 | const nullSource = await getSourceMapFromSource( 80 | path.resolve(__dirname, 'nosources-cheap-source-map', 'bogus.js'), 81 | ); 82 | expect(nullSource).toBeNull(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/unit/track.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-underscore-dangle 2 | global.__non_webpack_require__ = require; 3 | 4 | const http = require('http'); 5 | 6 | const spy = jest.spyOn(http, 'request'); 7 | 8 | const { 9 | default: track, 10 | getCircularReplacer, 11 | sendBatch, 12 | setSkipTracking, 13 | } = require('../../src/track'); 14 | 15 | describe('track', () => { 16 | test('getCircularReplacer', async () => { 17 | const replacer = getCircularReplacer(); 18 | const dummy = {test: true}; 19 | expect(replacer(undefined, 1)).toBe(1); 20 | expect(replacer(undefined, 1)).toBe(1); 21 | expect(replacer(undefined, 'test')).toBe('test'); 22 | expect(replacer(undefined, 'test')).toBe('test'); 23 | expect(replacer(undefined, dummy)).toBe(dummy); 24 | expect(replacer(undefined, dummy)).toBeUndefined(); 25 | }); 26 | 27 | test('should skip empty batch', () => { 28 | // No events to send, should not call request 29 | sendBatch(); 30 | expect(spy).not.toBeCalled(); 31 | }); 32 | 33 | test('should skip invalid events', () => { 34 | track(); 35 | track(2); 36 | sendBatch(); 37 | expect(spy).not.toBeCalled(); 38 | }); 39 | 40 | test('should honor `skipTracking`', () => { 41 | setSkipTracking(true); 42 | track({}); 43 | sendBatch(); 44 | setSkipTracking(false); 45 | expect(spy).not.toBeCalled(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/unit/track.test.jsdom.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-underscore-dangle 2 | global.__non_webpack_require__ = require; 3 | 4 | const spy = jest.fn(); 5 | window.XMLHttpRequest = jest.fn().mockImplementation(() => ({ 6 | addEventListener: jest.fn(), 7 | })); 8 | window.XMLHttpRequest.prototype.open = jest.fn(); 9 | window.XMLHttpRequest.prototype.send = spy; 10 | window.XMLHttpRequest.prototype.setRequestHeader = jest.fn(); 11 | 12 | const {default: track, sendBatch} = require('../../src/track'); 13 | 14 | describe('track', () => { 15 | test('should track and send batch', () => { 16 | track({}); 17 | sendBatch(); 18 | expect(spy).toBeCalledTimes(1); 19 | }); 20 | 21 | test('should batch', async () => { 22 | track({}); 23 | // Request should not be sent immediately 24 | expect(spy).toBeCalledTimes(0); 25 | // Batch should go out one second later 26 | await new Promise((r) => { 27 | setTimeout(r, 1200); 28 | }); 29 | expect(spy).toBeCalledTimes(1); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/unit/track.test.node.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-underscore-dangle 2 | global.__non_webpack_require__ = require; 3 | 4 | const http = require('http'); 5 | 6 | jest.mock('http'); 7 | http.request.mockReturnValue({on: () => {}, end: () => {}}); 8 | const spy = jest.spyOn(http, 'request'); 9 | 10 | const {default: track, sendBatch, setTrackingServer} = require('../../src/track'); 11 | 12 | describe('track', () => { 13 | test('should apply updated tracking server info', () => { 14 | setTrackingServer('201.123.68.122', 9031); 15 | track({}); 16 | sendBatch(); 17 | expect(spy).toBeCalledWith( 18 | expect.objectContaining({ 19 | host: '201.123.68.122', 20 | port: 9031, 21 | }), 22 | expect.any(Function), 23 | ); 24 | }); 25 | 26 | test('should track and send batch', () => { 27 | track({}); 28 | sendBatch(); 29 | expect(spy).toBeCalledTimes(1); 30 | }); 31 | 32 | test('should batch', async () => { 33 | track({}); 34 | // Request should not be sent immediately 35 | expect(spy).toBeCalledTimes(0); 36 | // Batch should go out one second later 37 | await new Promise((r) => { 38 | setTimeout(r, 1200); 39 | }); 40 | expect(spy).toBeCalledTimes(1); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/web/AudioContext.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb} = require('../utils'); 3 | 4 | test.describe('AudioContext', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | }); 8 | 9 | test('AudioContext', async ({page}) => { 10 | await page.evaluate(() => new AudioContext()); 11 | await expectWebCallToMatch({family: 'AudioContext', method: 'AudioContext', page}); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/web/BackgroundFetchManager.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const { 3 | expectWebCallToMatch, 4 | serviceWorkersAvailable, 5 | serviceWorkerRegistrationHasFeature, 6 | loadSandwormOnWeb, 7 | } = require('../utils'); 8 | 9 | test.describe('BackgroundFetchManager', () => { 10 | test.beforeEach(async ({page}) => { 11 | await loadSandwormOnWeb(page); 12 | if ( 13 | !(await serviceWorkersAvailable(page)) || 14 | !(await serviceWorkerRegistrationHasFeature('backgroundFetch', page)) 15 | ) { 16 | test.skip('Background Fetch not available'); 17 | } 18 | }); 19 | 20 | test('fetch', async ({page}) => { 21 | await page.evaluate(async () => { 22 | const swReg = await navigator.serviceWorker.ready; 23 | await swReg.backgroundFetch.fetch('my-fetch', ['image.png']); 24 | }); 25 | await expectWebCallToMatch({ 26 | family: 'BackgroundFetchManager', 27 | method: 'fetch', 28 | firstArg: 'my-fetch', 29 | page, 30 | }); 31 | }); 32 | 33 | test('get', async ({page}) => { 34 | await page.evaluate(async () => { 35 | const swReg = await navigator.serviceWorker.ready; 36 | await swReg.backgroundFetch.get('my-fetch'); 37 | }); 38 | await expectWebCallToMatch({ 39 | family: 'BackgroundFetchManager', 40 | method: 'get', 41 | firstArg: 'my-fetch', 42 | page, 43 | }); 44 | }); 45 | 46 | test('getIds', async ({page}) => { 47 | await page.evaluate(async () => { 48 | const swReg = await navigator.serviceWorker.ready; 49 | await swReg.backgroundFetch.getIds(); 50 | }); 51 | await expectWebCallToMatch({ 52 | family: 'BackgroundFetchManager', 53 | method: 'getIds', 54 | page, 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/web/BackgroundTasks.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('BackgroundTasks', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('requestIdleCallback', page))) { 8 | test.skip('Background Tasks not available'); 9 | } 10 | }); 11 | 12 | test('requestIdleCallback', async ({page}) => { 13 | await page.evaluate(async () => { 14 | self.requestIdleCallback(() => {}); 15 | }); 16 | await expectWebCallToMatch({ 17 | family: 'BackgroundTasks', 18 | method: 'requestIdleCallback', 19 | page, 20 | }); 21 | }); 22 | 23 | test('cancelIdleCallback', async ({page}) => { 24 | await page.evaluate(async () => { 25 | self.cancelIdleCallback(1); 26 | }); 27 | await expectWebCallToMatch({ 28 | family: 'BackgroundTasks', 29 | method: 'cancelIdleCallback', 30 | firstArg: 1, 31 | page, 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/web/Battery.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Battery', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('getBattery', page))) { 8 | test.skip('Battery API not available'); 9 | } 10 | }); 11 | 12 | test('getBattery', async ({page}) => { 13 | await page.evaluate(async () => navigator.getBattery()); 14 | await expectWebCallToMatch({ 15 | family: 'Battery', 16 | method: 'getBattery', 17 | page, 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/web/Beacon.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Beacon', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('sendBeacon', page))) { 8 | test.skip('Beacon not available'); 9 | } 10 | }); 11 | 12 | test('sendBeacon', async ({page}) => { 13 | await page.evaluate(async () => { 14 | navigator.sendBeacon(''); 15 | }); 16 | await expectWebCallToMatch({ 17 | family: 'Beacon', 18 | method: 'sendBeacon', 19 | page, 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/web/Bluetooth.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Bluetooth', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('bluetooth', page))) { 8 | test.skip('Bluetooth not available'); 9 | } 10 | }); 11 | 12 | test('getAvailability', async ({page}) => { 13 | const availability = await page.evaluate(async () => navigator.bluetooth.getAvailability()); 14 | expect(typeof availability).toEqual('boolean'); 15 | await expectWebCallToMatch({ 16 | family: 'Bluetooth', 17 | method: 'getAvailability', 18 | page, 19 | }); 20 | }); 21 | 22 | test('getDevices', async ({page}) => { 23 | if (await hasNavigatorFeature('bluetooth', 'getDevices', page)) { 24 | try { 25 | await page.evaluate(async () => navigator.bluetooth.getDevices()); 26 | } catch (error) { 27 | } finally { 28 | await expectWebCallToMatch({ 29 | family: 'Bluetooth', 30 | method: 'getDevices', 31 | page, 32 | }); 33 | } 34 | } else { 35 | test.skip('Bluetooth.getDevices is not available'); 36 | } 37 | }); 38 | 39 | test('requestDevice', async ({page}) => { 40 | if (await hasNavigatorFeature('bluetooth', 'requestDevice', page)) { 41 | // Web Bluetooth is not supported on this platform. 42 | try { 43 | await page.evaluate(async () => 44 | navigator.bluetooth.requestDevice({acceptAllDevices: true}), 45 | ); 46 | } catch (error) { 47 | } finally { 48 | await expectWebCallToMatch({ 49 | family: 'Bluetooth', 50 | method: 'requestDevice', 51 | page, 52 | }); 53 | } 54 | } else { 55 | test.skip('Bluetooth.requestDevice is not available'); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/web/Clipboard.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Clipboard', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('clipboard', page))) { 8 | test.skip('Clipboard not available'); 9 | } 10 | }); 11 | 12 | test('read', async ({page, context}) => { 13 | if (await hasNavigatorFeature('clipboard', 'read', page)) { 14 | try { 15 | await context.grantPermissions(['clipboard-read']); 16 | } catch (error) { 17 | test.skip('Could not grant `clipboard-read` permissions'); 18 | return; 19 | } 20 | const type = await page.evaluate(async () => typeof navigator.clipboard.read()); 21 | expect(type).toEqual('object'); 22 | await expectWebCallToMatch({ 23 | family: 'Clipboard', 24 | method: 'read', 25 | page, 26 | }); 27 | } else { 28 | test.skip('Clipboard.read is not available'); 29 | } 30 | }); 31 | 32 | test('readText', async ({page, context}) => { 33 | try { 34 | await context.grantPermissions(['clipboard-read']); 35 | } catch (error) { 36 | test.skip('Could not grant `clipboard-read` permissions'); 37 | return; 38 | } 39 | const text = await page.evaluate(async () => navigator.clipboard.readText()); 40 | expect(typeof text).toEqual('string'); 41 | await expectWebCallToMatch({ 42 | family: 'Clipboard', 43 | method: 'readText', 44 | page, 45 | }); 46 | }); 47 | 48 | test('write', async ({page, context}) => { 49 | if (await hasNavigatorFeature('clipboard', 'write', page)) { 50 | try { 51 | await context.grantPermissions(['clipboard-write']); 52 | } catch (error) { 53 | test.skip('Could not grant `clipboard-write` permissions'); 54 | return; 55 | } 56 | await page.evaluate(async () => typeof navigator.clipboard.write([])); 57 | await expectWebCallToMatch({ 58 | family: 'Clipboard', 59 | method: 'write', 60 | page, 61 | }); 62 | } else { 63 | test.skip('Clipboard.write is not available'); 64 | } 65 | }); 66 | 67 | test('writeText', async ({page, context}) => { 68 | try { 69 | await context.grantPermissions(['clipboard-write', 'clipboard-read']); 70 | } catch (error) { 71 | test.skip('Could not grant `clipboard-write` permissions'); 72 | return; 73 | } 74 | await page.evaluate(async () => navigator.clipboard.writeText('sandworm test')); 75 | await expectWebCallToMatch({ 76 | family: 'Clipboard', 77 | method: 'writeText', 78 | page, 79 | }); 80 | 81 | const text = await page.evaluate(async () => navigator.clipboard.readText()); 82 | expect(text).toEqual('sandworm test'); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/web/CookieStore.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('CookieStore', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('cookieStore', page))) { 8 | test.skip('Cookie Store not available'); 9 | } 10 | }); 11 | 12 | test('get', async ({page}) => { 13 | const cookie = await page.evaluate(async () => self.cookieStore.get('cookie1')); 14 | expect(cookie).toBeNull(); 15 | await expectWebCallToMatch({ 16 | family: 'CookieStore', 17 | method: 'get', 18 | firstArg: 'cookie1', 19 | page, 20 | }); 21 | }); 22 | 23 | test('getAll', async ({page}) => { 24 | const cookies = await page.evaluate(async () => self.cookieStore.getAll()); 25 | expect(Array.isArray(cookies)).toBeTruthy(); 26 | await expectWebCallToMatch({ 27 | family: 'CookieStore', 28 | method: 'getAll', 29 | page, 30 | }); 31 | }); 32 | 33 | test('set', async ({page}) => { 34 | await page.evaluate(async () => self.cookieStore.set('cookie1', 'test')); 35 | const cookie = await page.evaluate(async () => self.cookieStore.get('cookie1')); 36 | expect(cookie).not.toBeNull(); 37 | expect(cookie.value).toEqual('test'); 38 | await expectWebCallToMatch({ 39 | family: 'CookieStore', 40 | method: 'set', 41 | page, 42 | }); 43 | }); 44 | 45 | test('delete', async ({page}) => { 46 | await page.evaluate(async () => self.cookieStore.delete('cookie1')); 47 | await expectWebCallToMatch({ 48 | family: 'CookieStore', 49 | method: 'delete', 50 | page, 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/web/CredentialsContainer.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('CredentialsContainer', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('credentials', page))) { 8 | test.skip('Credentials Container not available'); 9 | } 10 | }); 11 | 12 | test('get', async ({page}) => { 13 | try { 14 | await page.evaluate(async () => navigator.credentials.get()); 15 | } catch (error) {} 16 | await expectWebCallToMatch({ 17 | family: 'CredentialsContainer', 18 | method: 'get', 19 | page, 20 | }); 21 | }); 22 | 23 | test('create', async ({page}) => { 24 | try { 25 | await page.evaluate(async () => navigator.credentials.create()); 26 | } catch (error) {} 27 | await expectWebCallToMatch({ 28 | family: 'CredentialsContainer', 29 | method: 'create', 30 | page, 31 | }); 32 | }); 33 | 34 | test('preventSilentAccess', async ({page}) => { 35 | try { 36 | await page.evaluate(async () => navigator.credentials.preventSilentAccess()); 37 | } catch (error) {} 38 | await expectWebCallToMatch({ 39 | family: 'CredentialsContainer', 40 | method: 'preventSilentAccess', 41 | page, 42 | }); 43 | }); 44 | 45 | test('store', async ({page}) => { 46 | try { 47 | await page.evaluate(async () => navigator.credentials.store()); 48 | } catch (error) {} 49 | await expectWebCallToMatch({ 50 | family: 'CredentialsContainer', 51 | method: 'store', 52 | page, 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/web/EventSource.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb} = require('../utils'); 3 | 4 | test.describe('EventSource', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | }); 8 | 9 | test('EventSource', async ({page}) => { 10 | await page.evaluate(() => new EventSource('')); 11 | await expectWebCallToMatch({family: 'EventSource', method: 'EventSource', page}); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/web/Fetch.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('Fetch', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('fetch', page))) { 8 | test.skip('Fetch not available'); 9 | } 10 | }); 11 | 12 | test('fetch', async ({page}) => { 13 | const text = await page.evaluate(async () => { 14 | const response = await fetch('http://localhost:7070'); 15 | return response.text(); 16 | }); 17 | expect(text).toEqual('Hello World\n'); 18 | await expectWebCallToMatch({ 19 | family: 'Fetch', 20 | method: 'fetch', 21 | page, 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/web/FileReader.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb} = require('../utils'); 3 | 4 | test.describe('FileReader', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | }); 8 | 9 | test('FileReader', async ({page}) => { 10 | const readyState = await page.evaluate(() => { 11 | const reader = new FileReader(); 12 | return reader.readyState; 13 | }); 14 | expect(readyState).toEqual(0); 15 | await expectWebCallToMatch({family: 'FileReader', method: 'FileReader', page}); 16 | }); 17 | 18 | test('abort', async ({page}) => { 19 | await page.evaluate(() => { 20 | const reader = new FileReader(); 21 | reader.abort(); 22 | }); 23 | await expectWebCallToMatch({family: 'FileReader', method: 'abort', index: 1, page}); 24 | }); 25 | 26 | test('readAsArrayBuffer', async ({page}) => { 27 | await page.evaluate(() => { 28 | const reader = new FileReader(); 29 | reader.readAsArrayBuffer(new Blob()); 30 | }); 31 | await expectWebCallToMatch({family: 'FileReader', method: 'readAsArrayBuffer', index: 1, page}); 32 | }); 33 | 34 | test('readAsBinaryString', async ({page}) => { 35 | await page.evaluate(() => { 36 | const reader = new FileReader(); 37 | reader.readAsBinaryString(new Blob()); 38 | }); 39 | await expectWebCallToMatch({ 40 | family: 'FileReader', 41 | method: 'readAsBinaryString', 42 | index: 1, 43 | page, 44 | }); 45 | }); 46 | 47 | test('readAsDataURL', async ({page}) => { 48 | await page.evaluate(() => { 49 | const reader = new FileReader(); 50 | reader.readAsDataURL(new Blob()); 51 | }); 52 | await expectWebCallToMatch({ 53 | family: 'FileReader', 54 | method: 'readAsDataURL', 55 | index: 1, 56 | page, 57 | }); 58 | }); 59 | 60 | test('readAsText', async ({page}) => { 61 | await page.evaluate(() => { 62 | const reader = new FileReader(); 63 | reader.readAsText(new Blob()); 64 | }); 65 | await expectWebCallToMatch({ 66 | family: 'FileReader', 67 | method: 'readAsText', 68 | index: 1, 69 | page, 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/web/FileReaderSync.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const { 3 | expectWebWorkerCallToMatch, 4 | loadSandwormOnWeb, 5 | webWorkersAvailable, 6 | webWorkerHasFeature, 7 | startWebWorker, 8 | } = require('../utils'); 9 | 10 | test.describe('FileReaderSync', () => { 11 | test.beforeEach(async ({page}) => { 12 | await loadSandwormOnWeb(page); 13 | if ( 14 | !(await webWorkersAvailable(page)) || 15 | !(await webWorkerHasFeature('FileReaderSync', page)) 16 | ) { 17 | test.skip('FileReaderSync not available'); 18 | } 19 | }); 20 | 21 | test('FileReaderSync', async ({page}) => { 22 | const worker = await startWebWorker(page); 23 | await worker.evaluate(() => new FileReaderSync()); 24 | await expectWebWorkerCallToMatch({family: 'FileReaderSync', method: 'FileReaderSync', worker}); 25 | }); 26 | 27 | test('readAsArrayBuffer', async ({page}) => { 28 | const worker = await startWebWorker(page); 29 | await worker.evaluate(() => { 30 | const reader = new FileReaderSync(); 31 | reader.readAsArrayBuffer(new Blob()); 32 | }); 33 | await expectWebWorkerCallToMatch({ 34 | family: 'FileReaderSync', 35 | method: 'readAsArrayBuffer', 36 | index: 1, 37 | worker, 38 | }); 39 | }); 40 | 41 | test('readAsBinaryString', async ({page}) => { 42 | const worker = await startWebWorker(page); 43 | await worker.evaluate(() => { 44 | const reader = new FileReaderSync(); 45 | reader.readAsBinaryString(new Blob()); 46 | }); 47 | await expectWebWorkerCallToMatch({ 48 | family: 'FileReaderSync', 49 | method: 'readAsBinaryString', 50 | index: 1, 51 | worker, 52 | }); 53 | }); 54 | 55 | test('readAsDataURL', async ({page}) => { 56 | const worker = await startWebWorker(page); 57 | await worker.evaluate(() => { 58 | const reader = new FileReaderSync(); 59 | reader.readAsDataURL(new Blob()); 60 | }); 61 | await expectWebWorkerCallToMatch({ 62 | family: 'FileReaderSync', 63 | method: 'readAsDataURL', 64 | index: 1, 65 | worker, 66 | }); 67 | }); 68 | 69 | test('readAsText', async ({page}) => { 70 | const worker = await startWebWorker(page); 71 | await worker.evaluate(() => { 72 | const reader = new FileReaderSync(); 73 | reader.readAsText(new Blob()); 74 | }); 75 | await expectWebWorkerCallToMatch({ 76 | family: 'FileReaderSync', 77 | method: 'readAsText', 78 | index: 1, 79 | worker, 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /tests/web/FileSystem.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('FileSystem', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | 8 | // Playwright doesn't support the File System Access API yet 9 | // See https://github.com/microsoft/playwright/issues/8850 10 | test.skip('No Playwright support'); 11 | }); 12 | 13 | test('showOpenFilePicker', async ({page}) => { 14 | if (!(await hasGlobalFeature('showOpenFilePicker', page))) { 15 | test.skip('FileSystem.showOpenFilePicker not available'); 16 | } 17 | await page.evaluate(async () => self.showOpenFilePicker()); 18 | await expectWebCallToMatch({ 19 | family: 'FileSystem', 20 | method: 'showOpenFilePicker', 21 | page, 22 | }); 23 | }); 24 | 25 | test('showSaveFilePicker', async ({page}) => { 26 | if (!(await hasGlobalFeature('showSaveFilePicker', page))) { 27 | test.skip('FileSystem.showSaveFilePicker not available'); 28 | } 29 | await page.evaluate(async () => self.showSaveFilePicker()); 30 | await expectWebCallToMatch({ 31 | family: 'FileSystem', 32 | method: 'showSaveFilePicker', 33 | page, 34 | }); 35 | }); 36 | 37 | test('showDirectoryPicker', async ({page}) => { 38 | if (!(await hasGlobalFeature('showDirectoryPicker', page))) { 39 | test.skip('FileSystem.showDirectoryPicker not available'); 40 | } 41 | await page.evaluate(async () => self.showDirectoryPicker()); 42 | await expectWebCallToMatch({ 43 | family: 'FileSystem', 44 | method: 'showDirectoryPicker', 45 | page, 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/web/Gamepad.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Gamepad', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('getGamepads', page))) { 8 | test.skip('Gamepad API not available'); 9 | } 10 | }); 11 | 12 | test('getGamepads', async ({page}) => { 13 | const gamepads = await page.evaluate(async () => navigator.getGamepads()); 14 | expect(Array.isArray(gamepads)).toBeTruthy(); 15 | await expectWebCallToMatch({ 16 | family: 'Gamepad', 17 | method: 'getGamepads', 18 | page, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/web/Geolocation.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Geolocation', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('geolocation', page))) { 8 | test.skip('Geolocation API not available'); 9 | } 10 | }); 11 | 12 | test('clearWatch', async ({page}) => { 13 | await page.evaluate(async () => navigator.geolocation.clearWatch(0)); 14 | await expectWebCallToMatch({ 15 | family: 'Geolocation', 16 | method: 'clearWatch', 17 | page, 18 | }); 19 | }); 20 | 21 | test('getCurrentPosition', async ({page, context}) => { 22 | context.grantPermissions(['geolocation']); 23 | await page.evaluate(async () => navigator.geolocation.getCurrentPosition(() => {})); 24 | await expectWebCallToMatch({ 25 | family: 'Geolocation', 26 | method: 'getCurrentPosition', 27 | page, 28 | }); 29 | }); 30 | 31 | test('watchPosition', async ({page}) => { 32 | await page.evaluate(async () => { 33 | const watch = navigator.geolocation.watchPosition(() => {}); 34 | navigator.geolocation.clearWatch(watch); 35 | }); 36 | await expectWebCallToMatch({ 37 | family: 'Geolocation', 38 | method: 'watchPosition', 39 | page, 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/web/HID.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('HID', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('hid', page))) { 8 | test.skip('HID API not available'); 9 | } 10 | }); 11 | 12 | test('getDevices', async ({page}) => { 13 | await page.evaluate(async () => navigator.hid.getDevices()); 14 | await expectWebCallToMatch({ 15 | family: 'HID', 16 | method: 'getDevices', 17 | page, 18 | }); 19 | }); 20 | 21 | test('requestDevice', async ({page}) => { 22 | await page.evaluate(async () => navigator.hid.requestDevice({filters: [{vendorId: 1234}]})); 23 | await expectWebCallToMatch({ 24 | family: 'HID', 25 | method: 'requestDevice', 26 | page, 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/web/History.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('History', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('history', page))) { 8 | test.skip('History not available'); 9 | } 10 | }); 11 | 12 | test('back', async ({page}) => { 13 | await page.goto('http://localhost:7070#testing', { 14 | waitUntil: 'load', 15 | }); 16 | await page.evaluate(async () => self.history.back()); 17 | await expectWebCallToMatch({ 18 | family: 'History', 19 | method: 'back', 20 | page, 21 | }); 22 | }); 23 | 24 | test('forward', async ({page}) => { 25 | await page.evaluate(async () => self.history.forward()); 26 | await expectWebCallToMatch({ 27 | family: 'History', 28 | method: 'forward', 29 | page, 30 | }); 31 | }); 32 | 33 | test('go', async ({page}) => { 34 | await page.evaluate(async () => self.history.go(1)); 35 | await expectWebCallToMatch({ 36 | family: 'History', 37 | method: 'go', 38 | page, 39 | }); 40 | }); 41 | 42 | test('pushState', async ({page}) => { 43 | await page.evaluate(async () => self.history.pushState({}, '')); 44 | await expectWebCallToMatch({ 45 | family: 'History', 46 | method: 'pushState', 47 | page, 48 | }); 49 | }); 50 | 51 | test('replaceState', async ({page}) => { 52 | await page.evaluate(async () => self.history.replaceState({}, '')); 53 | await expectWebCallToMatch({ 54 | family: 'History', 55 | method: 'replaceState', 56 | page, 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/web/IDBFactory.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('IDBFactory', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('indexedDB', page))) { 8 | test.skip('IDBFactory not available'); 9 | } 10 | }); 11 | 12 | test('cmp', async ({page}) => { 13 | await page.evaluate(async () => self.indexedDB.cmp('a', 'b')); 14 | await expectWebCallToMatch({ 15 | family: 'IDBFactory', 16 | method: 'cmp', 17 | page, 18 | }); 19 | }); 20 | 21 | test('databases', async ({page}) => { 22 | if (!(await page.evaluate(() => 'databases' in self.indexedDB))) { 23 | test.skip('IDBFactory.databases not available'); 24 | } 25 | await page.evaluate(async () => self.indexedDB.databases()); 26 | await expectWebCallToMatch({ 27 | family: 'IDBFactory', 28 | method: 'databases', 29 | page, 30 | }); 31 | }); 32 | 33 | test('deleteDatabase', async ({page}) => { 34 | await page.evaluate(async () => self.indexedDB.deleteDatabase('')); 35 | await expectWebCallToMatch({ 36 | family: 'IDBFactory', 37 | method: 'deleteDatabase', 38 | page, 39 | }); 40 | }); 41 | 42 | test('open', async ({page}) => { 43 | await page.evaluate(async () => self.indexedDB.open('')); 44 | await expectWebCallToMatch({ 45 | family: 'IDBFactory', 46 | method: 'open', 47 | page, 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/web/ImageCapture.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('ImageCapture', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('ImageCapture', page))) { 8 | test.skip('ImageCapture API not available'); 9 | } 10 | }); 11 | 12 | test('ImageCapture', async ({page}) => { 13 | try { 14 | await page.evaluate(async () => new ImageCapture()); 15 | } catch (error) {} 16 | await expectWebCallToMatch({ 17 | family: 'ImageCapture', 18 | method: 'ImageCapture', 19 | page, 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/web/MIDI.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('MIDI', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('requestMIDIAccess', page))) { 8 | test.skip('MIDI API not available'); 9 | } 10 | }); 11 | 12 | test('requestMIDIAccess', async ({page}) => { 13 | try { 14 | const midiInputs = await page.evaluate( 15 | async () => (await navigator.requestMIDIAccess()).inputs, 16 | ); 17 | expect(midiInputs).toBeDefined(); 18 | } catch (error) {} 19 | await expectWebCallToMatch({ 20 | family: 'MIDI', 21 | method: 'requestMIDIAccess', 22 | page, 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/web/MediaDevices.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('MediaDevices', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('mediaDevices', page))) { 8 | test.skip('MediaDevices API not available'); 9 | } 10 | }); 11 | 12 | test('enumerateDevices', async ({page}) => { 13 | const devices = await page.evaluate(async () => navigator.mediaDevices.enumerateDevices()); 14 | expect(Array.isArray(devices)).toBeTruthy(); 15 | await expectWebCallToMatch({ 16 | family: 'MediaDevices', 17 | method: 'enumerateDevices', 18 | page, 19 | }); 20 | }); 21 | 22 | test('getDisplayMedia', async ({page, context}) => { 23 | try { 24 | await context.grantPermissions(['camera']); 25 | } catch (error) { 26 | test.skip('User media is not available'); 27 | } 28 | try { 29 | await page.evaluate(async () => navigator.mediaDevices.getDisplayMedia()); 30 | } catch (error) {} 31 | await expectWebCallToMatch({ 32 | family: 'MediaDevices', 33 | method: 'getDisplayMedia', 34 | page, 35 | }); 36 | }); 37 | 38 | test('getSupportedConstraints', async ({page}) => { 39 | await page.evaluate(async () => navigator.mediaDevices.getSupportedConstraints()); 40 | await expectWebCallToMatch({ 41 | family: 'MediaDevices', 42 | method: 'getSupportedConstraints', 43 | page, 44 | }); 45 | }); 46 | 47 | test('getUserMedia', async ({page, context}) => { 48 | try { 49 | await context.grantPermissions(['camera']); 50 | } catch (error) { 51 | test.skip('User media is not available'); 52 | } 53 | try { 54 | await page.evaluate(async () => navigator.mediaDevices.getUserMedia({video: true})); 55 | } catch (error) {} 56 | await expectWebCallToMatch({ 57 | family: 'MediaDevices', 58 | method: 'getUserMedia', 59 | page, 60 | }); 61 | }); 62 | 63 | test('selectAudioOutput', async ({page}) => { 64 | if (!(await hasNavigatorFeature('mediaDevices', 'selectAudioOutput', page))) { 65 | test.skip('MediaDevices.selectAudioOutput is not available'); 66 | } 67 | await page.evaluate(async () => navigator.mediaDevices.selectAudioOutput()); 68 | await expectWebCallToMatch({ 69 | family: 'MediaDevices', 70 | method: 'selectAudioOutput', 71 | page, 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/web/MediaRecorder.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('MediaRecorder', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('MediaRecorder', page))) { 8 | test.skip('MediaRecorder not available'); 9 | } 10 | }); 11 | 12 | test('MediaRecorder', async ({page}) => { 13 | const recorderState = await page.evaluate( 14 | async () => new MediaRecorder(new MediaStream()).state, 15 | ); 16 | expect(recorderState).toEqual('inactive'); 17 | await expectWebCallToMatch({ 18 | family: 'MediaRecorder', 19 | method: 'MediaRecorder', 20 | page, 21 | index: 1, 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/web/MediaStream.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('MediaStream', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('MediaStream', page))) { 8 | test.skip('MediaStream not available'); 9 | } 10 | }); 11 | 12 | test('MediaStream', async ({page}) => { 13 | const streamActive = await page.evaluate(async () => new MediaStream().active); 14 | expect(streamActive).toBeFalsy(); 15 | await expectWebCallToMatch({ 16 | family: 'MediaStream', 17 | method: 'MediaStream', 18 | page, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/web/MessageChannel.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('MessageChannel', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('MessageChannel', page))) { 8 | test.skip('MessageChannel not available'); 9 | } 10 | }); 11 | 12 | test('MessageChannel', async ({page}) => { 13 | const streamActive = await page.evaluate(async () => new MessageChannel().port1); 14 | expect(streamActive).toBeDefined(); 15 | await expectWebCallToMatch({ 16 | family: 'MessageChannel', 17 | method: 'MessageChannel', 18 | page, 19 | }); 20 | }); 21 | 22 | test('postMessage', async ({page}) => { 23 | await page.evaluate(async () => self.postMessage('')); 24 | await expectWebCallToMatch({ 25 | family: 'MessageChannel', 26 | method: 'postMessage', 27 | page, 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/web/Notification.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const { 3 | expectWebCallToMatch, 4 | loadSandwormOnWeb, 5 | hasGlobalFeature, 6 | serviceWorkersAvailable, 7 | serviceWorkerRegistrationHasFeature, 8 | } = require('../utils'); 9 | 10 | test.describe('Notification', () => { 11 | test.beforeEach(async ({page}) => { 12 | await loadSandwormOnWeb(page); 13 | if (!(await hasGlobalFeature('Notification', page))) { 14 | test.skip('Notification API not available'); 15 | } 16 | }); 17 | 18 | test('Notification', async ({page}) => { 19 | const notificationTitle = await page.evaluate(async () => new Notification('test').title); 20 | expect(notificationTitle).toEqual('test'); 21 | await expectWebCallToMatch({ 22 | family: 'Notification', 23 | method: 'Notification', 24 | firstArg: 'test', 25 | page, 26 | }); 27 | }); 28 | 29 | test('requestPermission', async ({page, context}) => { 30 | try { 31 | await context.grantPermissions(['notifications']); 32 | } catch (error) { 33 | test.skip('Could not grant notifications permission'); 34 | } 35 | 36 | if (!(await page.evaluate(() => 'requestPermission' in self))) { 37 | test.skip('Notification.requestPermission is not available'); 38 | } 39 | 40 | const permissionResult = await page.evaluate(async () => Notification.requestPermission()); 41 | expect(permissionResult).toEqual('granted'); 42 | await expectWebCallToMatch({ 43 | family: 'Notification', 44 | method: 'requestPermission', 45 | page, 46 | }); 47 | }); 48 | 49 | test('showNotification', async ({page, context}) => { 50 | try { 51 | await context.grantPermissions(['notifications']); 52 | } catch (error) { 53 | test.skip('Could not grant notifications permission'); 54 | } 55 | 56 | if ( 57 | !(await serviceWorkersAvailable(page)) || 58 | !(await serviceWorkerRegistrationHasFeature('showNotification', page)) 59 | ) { 60 | test.skip('Notification.showNotification not available'); 61 | } 62 | 63 | try { 64 | await page.evaluate(async () => { 65 | const swReg = await navigator.serviceWorker.ready; 66 | await swReg.showNotification('test'); 67 | }); 68 | } catch (error) {} 69 | await expectWebCallToMatch({ 70 | family: 'Notification', 71 | method: 'showNotification', 72 | firstArg: 'test', 73 | page, 74 | }); 75 | }); 76 | 77 | test('getNotifications', async ({page, context}) => { 78 | try { 79 | await context.grantPermissions(['notifications']); 80 | } catch (error) { 81 | test.skip('Could not grant notifications permission'); 82 | } 83 | 84 | if ( 85 | !(await serviceWorkersAvailable(page)) || 86 | !(await serviceWorkerRegistrationHasFeature('getNotifications', page)) 87 | ) { 88 | test.skip('Notification.getNotifications not available'); 89 | } 90 | 91 | const notifications = await page.evaluate(async () => { 92 | const swReg = await navigator.serviceWorker.ready; 93 | return swReg.getNotifications(); 94 | }); 95 | expect(Array.isArray(notifications)).toBeTruthy(); 96 | await expectWebCallToMatch({ 97 | family: 'Notification', 98 | method: 'getNotifications', 99 | page, 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /tests/web/PaymentRequest.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('PaymentRequest', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('PaymentRequest', page))) { 8 | test.skip('Payment API not available'); 9 | } 10 | }); 11 | 12 | test('PaymentRequest', async ({page}) => { 13 | const requestId = await page.evaluate(async () => { 14 | const request = new PaymentRequest( 15 | [ 16 | { 17 | supportedMethods: 'https://example.com/pay', 18 | }, 19 | ], 20 | { 21 | total: {label: 'Donation', amount: {currency: 'USD', value: '65.00'}}, 22 | displayItems: [ 23 | { 24 | label: 'Original donation amount', 25 | amount: {currency: 'USD', value: '65.00'}, 26 | }, 27 | ], 28 | shippingOptions: [ 29 | { 30 | id: 'standard', 31 | label: 'Standard shipping', 32 | amount: {currency: 'USD', value: '0.00'}, 33 | selected: true, 34 | }, 35 | ], 36 | }, 37 | {requestShipping: true}, 38 | ); 39 | 40 | return request.id; 41 | }); 42 | expect(requestId).toBeDefined(); 43 | await expectWebCallToMatch({ 44 | family: 'PaymentRequest', 45 | method: 'PaymentRequest', 46 | page, 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/web/PerformanceObserver.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('PerformanceObserver', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('PerformanceObserver', page))) { 8 | test.skip('PerformanceObserver not available'); 9 | } 10 | }); 11 | 12 | test('PerformanceObserver', async ({page}) => { 13 | const observer = await page.evaluate(async () => new PerformanceObserver(() => {})); 14 | expect(observer).toBeDefined(); 15 | await expectWebCallToMatch({ 16 | family: 'PerformanceObserver', 17 | method: 'PerformanceObserver', 18 | page, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/web/PeriodicSyncManager.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const { 3 | expectWebCallToMatch, 4 | loadSandwormOnWeb, 5 | serviceWorkersAvailable, 6 | serviceWorkerRegistrationHasFeature, 7 | } = require('../utils'); 8 | 9 | test.describe('PeriodicSyncManager', () => { 10 | test.beforeEach(async ({page}) => { 11 | await loadSandwormOnWeb(page); 12 | if ( 13 | !(await serviceWorkersAvailable(page)) || 14 | !(await serviceWorkerRegistrationHasFeature('periodicSync', page)) 15 | ) { 16 | test.skip('PeriodicSyncManager not available'); 17 | } 18 | }); 19 | 20 | test('register', async ({page}) => { 21 | await page.evaluate(async () => { 22 | const swReg = await navigator.serviceWorker.ready; 23 | swReg.periodicSync.register('test'); 24 | }); 25 | await expectWebCallToMatch({ 26 | family: 'PeriodicSyncManager', 27 | method: 'register', 28 | page, 29 | }); 30 | }); 31 | 32 | test('unregister', async ({page}) => { 33 | await page.evaluate(async () => { 34 | const swReg = await navigator.serviceWorker.ready; 35 | swReg.periodicSync.unregister('test'); 36 | }); 37 | await expectWebCallToMatch({ 38 | family: 'PeriodicSyncManager', 39 | method: 'unregister', 40 | page, 41 | }); 42 | }); 43 | 44 | test('getTags', async ({page}) => { 45 | try { 46 | await page.evaluate(async () => { 47 | const swReg = await navigator.serviceWorker.ready; 48 | swReg.periodicSync.getTags(); 49 | }); 50 | } catch (error) {} 51 | await expectWebCallToMatch({ 52 | family: 'PeriodicSyncManager', 53 | method: 'getTags', 54 | page, 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/web/Permissions.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Permissions', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('permissions', page))) { 8 | test.skip('Permissions API not available'); 9 | } 10 | }); 11 | 12 | test('query', async ({page}) => { 13 | const state = await page.evaluate( 14 | async () => (await navigator.permissions.query({name: 'geolocation'})).state, 15 | ); 16 | expect(state).toEqual('prompt'); 17 | await expectWebCallToMatch({ 18 | family: 'Permissions', 19 | method: 'query', 20 | page, 21 | }); 22 | }); 23 | 24 | test('revoke', async ({page}) => { 25 | if (!(await hasNavigatorFeature('permissions', 'revoke', page))) { 26 | test.skip('Permissions.revoke not available'); 27 | } 28 | const state = await page.evaluate( 29 | async () => (await navigator.permissions.revoke({name: 'geolocation'})).state, 30 | ); 31 | expect(state).toEqual('denied'); 32 | await expectWebCallToMatch({ 33 | family: 'Permissions', 34 | method: 'revoke', 35 | page, 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/web/PresentationRequest.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('PresentationRequest', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('PresentationRequest', page))) { 8 | test.skip('Presentation API not available'); 9 | } 10 | }); 11 | 12 | test('PresentationRequest', async ({page}) => { 13 | const isProperClass = await page.evaluate( 14 | async () => new PresentationRequest(['https://example.com']) instanceof PresentationRequest, 15 | ); 16 | expect(isProperClass).toBeTruthy(); 17 | await expectWebCallToMatch({ 18 | family: 'PresentationRequest', 19 | method: 'PresentationRequest', 20 | page, 21 | }); 22 | }); 23 | 24 | test('start', async ({page}) => { 25 | let error; 26 | try { 27 | await page.evaluate(async () => { 28 | const pres = new PresentationRequest(['https://example.com']); 29 | await pres.start(); 30 | }); 31 | } catch (err) { 32 | error = err; 33 | } 34 | expect(error).toBeDefined(); 35 | await expectWebCallToMatch({ 36 | family: 'PresentationRequest', 37 | method: 'start', 38 | page, 39 | index: 1, 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/web/PushManager.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const { 3 | expectWebCallToMatch, 4 | serviceWorkersAvailable, 5 | serviceWorkerRegistrationHasFeature, 6 | loadSandwormOnWeb, 7 | } = require('../utils'); 8 | 9 | test.describe('PushManager', () => { 10 | test.beforeEach(async ({page}) => { 11 | await loadSandwormOnWeb(page); 12 | if ( 13 | !(await serviceWorkersAvailable(page)) || 14 | !(await serviceWorkerRegistrationHasFeature('pushManager', page)) 15 | ) { 16 | test.skip('Push API not available'); 17 | } 18 | }); 19 | 20 | test('getSubscription', async ({page}) => { 21 | try { 22 | await page.evaluate(async () => { 23 | const swReg = await navigator.serviceWorker.ready; 24 | await swReg.pushManager.getSubscription(); 25 | }); 26 | } catch (error) {} 27 | await expectWebCallToMatch({ 28 | family: 'PushManager', 29 | method: 'getSubscription', 30 | page, 31 | }); 32 | }); 33 | 34 | test('hasPermission', async ({page}) => { 35 | if (!(await serviceWorkerRegistrationHasFeature('pushManager', 'hasPermission', page))) { 36 | test.skip('PushManager.hasPermission is not available'); 37 | } 38 | await page.evaluate(async () => { 39 | const swReg = await navigator.serviceWorker.ready; 40 | await swReg.pushManager.hasPermission(); 41 | }); 42 | await expectWebCallToMatch({ 43 | family: 'PushManager', 44 | method: 'hasPermission', 45 | page, 46 | }); 47 | }); 48 | 49 | test('permissionState', async ({page}) => { 50 | const state = await page.evaluate(async () => { 51 | const swReg = await navigator.serviceWorker.ready; 52 | return swReg.pushManager.permissionState({userVisibleOnly: true}); 53 | }); 54 | expect(state).toEqual('prompt'); 55 | await expectWebCallToMatch({ 56 | family: 'PushManager', 57 | method: 'permissionState', 58 | page, 59 | }); 60 | }); 61 | 62 | test('register', async ({page}) => { 63 | if (!(await serviceWorkerRegistrationHasFeature('pushManager', 'register', page))) { 64 | test.skip('PushManager.register is not available'); 65 | } 66 | await page.evaluate(async () => { 67 | const swReg = await navigator.serviceWorker.ready; 68 | return swReg.pushManager.register(); 69 | }); 70 | await expectWebCallToMatch({ 71 | family: 'PushManager', 72 | method: 'register', 73 | page, 74 | }); 75 | }); 76 | 77 | test('registrations', async ({page}) => { 78 | if (!(await serviceWorkerRegistrationHasFeature('pushManager', 'registrations', page))) { 79 | test.skip('PushManager.registrations is not available'); 80 | } 81 | await page.evaluate(async () => { 82 | const swReg = await navigator.serviceWorker.ready; 83 | return swReg.pushManager.registrations(); 84 | }); 85 | await expectWebCallToMatch({ 86 | family: 'PushManager', 87 | method: 'registrations', 88 | page, 89 | }); 90 | }); 91 | 92 | test('subscribe', async ({page}) => { 93 | await page.evaluate(async () => { 94 | const swReg = await navigator.serviceWorker.ready; 95 | // This hangs on firefox 96 | // Intentionally not awaiting the promise 97 | swReg.pushManager.subscribe(); 98 | }); 99 | await expectWebCallToMatch({ 100 | family: 'PushManager', 101 | method: 'subscribe', 102 | page, 103 | }); 104 | }); 105 | 106 | test('unregister', async ({page}) => { 107 | if (!(await serviceWorkerRegistrationHasFeature('pushManager', 'unregister', page))) { 108 | test.skip('PushManager.unregister is not available'); 109 | } 110 | await page.evaluate(async () => { 111 | const swReg = await navigator.serviceWorker.ready; 112 | return swReg.pushManager.unregister(''); 113 | }); 114 | await expectWebCallToMatch({ 115 | family: 'PushManager', 116 | method: 'unregister', 117 | page, 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/web/ReportingObserver.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('ReportingObserver', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('ReportingObserver', page))) { 8 | test.skip('ReportingObserver API not available'); 9 | } 10 | }); 11 | 12 | test('ReportingObserver', async ({page}) => { 13 | const type = await page.evaluate(async () => typeof new ReportingObserver(() => {}).observe); 14 | expect(type).toEqual('function'); 15 | await expectWebCallToMatch({ 16 | family: 'ReportingObserver', 17 | method: 'ReportingObserver', 18 | page, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/web/Scheduler.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('Scheduler', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('scheduler', page))) { 8 | test.skip('Scheduler API not available'); 9 | } 10 | }); 11 | 12 | test('postTask', async ({page}) => { 13 | await page.evaluate(async () => { 14 | self.scheduler.postTask(() => {}); 15 | }); 16 | await expectWebCallToMatch({ 17 | family: 'Scheduler', 18 | method: 'postTask', 19 | page, 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/web/Selection.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('Selection', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('getSelection', page))) { 8 | test.skip('Selection API not available'); 9 | } 10 | }); 11 | 12 | test('getSelection', async ({page}) => { 13 | const text = await page.evaluate(async () => self.getSelection().toString()); 14 | expect(text).toEqual(''); 15 | await expectWebCallToMatch({ 16 | family: 'Selection', 17 | method: 'getSelection', 18 | page, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/web/Sensor.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('Sensor', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | }); 8 | 9 | test('AbsoluteOrientationSensor', async ({page}) => { 10 | if (!(await hasGlobalFeature('AbsoluteOrientationSensor', page))) { 11 | test.skip('AbsoluteOrientationSensor not available'); 12 | } 13 | const activated = await page.evaluate(async () => new AbsoluteOrientationSensor().activated); 14 | expect(activated).toBeFalsy(); 15 | await expectWebCallToMatch({ 16 | family: 'Sensor', 17 | method: 'AbsoluteOrientationSensor', 18 | page, 19 | }); 20 | }); 21 | 22 | test('Accelerometer', async ({page}) => { 23 | if (!(await hasGlobalFeature('Accelerometer', page))) { 24 | test.skip('Accelerometer not available'); 25 | } 26 | const activated = await page.evaluate(async () => new Accelerometer().activated); 27 | expect(activated).toBeFalsy(); 28 | await expectWebCallToMatch({ 29 | family: 'Sensor', 30 | method: 'Accelerometer', 31 | page, 32 | }); 33 | }); 34 | 35 | test('AmbientLightSensor', async ({page}) => { 36 | if (!(await hasGlobalFeature('AmbientLightSensor', page))) { 37 | test.skip('AmbientLightSensor not available'); 38 | } 39 | const activated = await page.evaluate(async () => new AmbientLightSensor().activated); 40 | expect(activated).toBeFalsy(); 41 | await expectWebCallToMatch({ 42 | family: 'Sensor', 43 | method: 'AmbientLightSensor', 44 | page, 45 | }); 46 | }); 47 | 48 | test('GravitySensor', async ({page}) => { 49 | if (!(await hasGlobalFeature('GravitySensor', page))) { 50 | test.skip('GravitySensor not available'); 51 | } 52 | const activated = await page.evaluate(async () => new GravitySensor().activated); 53 | expect(activated).toBeFalsy(); 54 | await expectWebCallToMatch({ 55 | family: 'Sensor', 56 | method: 'GravitySensor', 57 | page, 58 | }); 59 | }); 60 | 61 | test('Gyroscope', async ({page}) => { 62 | if (!(await hasGlobalFeature('Gyroscope', page))) { 63 | test.skip('Gyroscope not available'); 64 | } 65 | const activated = await page.evaluate(async () => new Gyroscope().activated); 66 | expect(activated).toBeFalsy(); 67 | await expectWebCallToMatch({ 68 | family: 'Sensor', 69 | method: 'Gyroscope', 70 | page, 71 | }); 72 | }); 73 | 74 | test('LinearAccelerationSensor', async ({page}) => { 75 | if (!(await hasGlobalFeature('LinearAccelerationSensor', page))) { 76 | test.skip('LinearAccelerationSensor not available'); 77 | } 78 | const activated = await page.evaluate(async () => new LinearAccelerationSensor().activated); 79 | expect(activated).toBeFalsy(); 80 | await expectWebCallToMatch({ 81 | family: 'Sensor', 82 | method: 'LinearAccelerationSensor', 83 | page, 84 | }); 85 | }); 86 | 87 | test('Magnetometer', async ({page}) => { 88 | if (!(await hasGlobalFeature('Magnetometer', page))) { 89 | test.skip('Magnetometer not available'); 90 | } 91 | const activated = await page.evaluate(async () => new Magnetometer().activated); 92 | expect(activated).toBeFalsy(); 93 | await expectWebCallToMatch({ 94 | family: 'Sensor', 95 | method: 'Magnetometer', 96 | page, 97 | }); 98 | }); 99 | 100 | test('RelativeOrientationSensor', async ({page}) => { 101 | if (!(await hasGlobalFeature('RelativeOrientationSensor', page))) { 102 | test.skip('RelativeOrientationSensor not available'); 103 | } 104 | const activated = await page.evaluate(async () => new RelativeOrientationSensor().activated); 105 | expect(activated).toBeFalsy(); 106 | await expectWebCallToMatch({ 107 | family: 'Sensor', 108 | method: 'RelativeOrientationSensor', 109 | page, 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/web/Share.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Share', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('share', page))) { 8 | test.skip('Share API not available'); 9 | } 10 | }); 11 | 12 | test('share', async ({page}) => { 13 | // This crashes webkit 14 | try { 15 | await page.evaluate(async () => navigator.share()); 16 | } catch (error) {} 17 | await expectWebCallToMatch({ 18 | family: 'Share', 19 | method: 'share', 20 | page, 21 | }); 22 | }); 23 | 24 | test('canShare', async ({page}) => { 25 | const canShare = await page.evaluate(async () => navigator.canShare()); 26 | expect(canShare).toBeFalsy(); 27 | await expectWebCallToMatch({ 28 | family: 'Share', 29 | method: 'canShare', 30 | page, 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/web/SpeechRecognition.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('SpeechRecognition', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if ( 8 | !(await hasGlobalFeature('SpeechRecognition', page)) && 9 | !(await hasGlobalFeature('webkitSpeechRecognition', page)) 10 | ) { 11 | test.skip('SpeechRecognition API not available'); 12 | } 13 | }); 14 | 15 | test('SpeechRecognition', async ({page}) => { 16 | if (!(await hasGlobalFeature('SpeechRecognition', page))) { 17 | test.skip('SpeechRecognition not available'); 18 | } 19 | const text = await page.evaluate(async () => new SpeechRecognition().lang); 20 | expect(text).toEqual(''); 21 | await expectWebCallToMatch({ 22 | family: 'SpeechRecognition', 23 | method: 'SpeechRecognition', 24 | page, 25 | }); 26 | }); 27 | 28 | test('webkitSpeechRecognition', async ({page}) => { 29 | if (!(await hasGlobalFeature('webkitSpeechRecognition', page))) { 30 | test.skip('webkitSpeechRecognition not available'); 31 | } 32 | // eslint-disable-next-line new-cap 33 | const text = await page.evaluate(async () => new webkitSpeechRecognition().lang); 34 | expect(text).toEqual(''); 35 | await expectWebCallToMatch({ 36 | family: 'SpeechRecognition', 37 | method: 'webkitSpeechRecognition', 38 | page, 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/web/SpeechSynthesis.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('SpeechSynthesis', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('speechSynthesis', page))) { 8 | test.skip('SpeechSynthesis API not available'); 9 | } 10 | }); 11 | 12 | test('cancel', async ({page}) => { 13 | await page.evaluate(async () => self.speechSynthesis.cancel()); 14 | await expectWebCallToMatch({ 15 | family: 'SpeechSynthesis', 16 | method: 'cancel', 17 | page, 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/web/Storage.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('Storage', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('localStorage', page))) { 8 | test.skip('Storage API not available'); 9 | } 10 | }); 11 | 12 | test('getItem', async ({page}) => { 13 | const value = await page.evaluate(async () => self.localStorage.getItem('test')); 14 | expect(value).toBeNull(); 15 | await expectWebCallToMatch({ 16 | family: 'Storage', 17 | method: 'getItem', 18 | page, 19 | }); 20 | }); 21 | 22 | test('setItem', async ({page}) => { 23 | await page.evaluate(async () => self.localStorage.setItem('test', 'value')); 24 | const value = await page.evaluate(async () => self.localStorage.getItem('test')); 25 | expect(value).toEqual('value'); 26 | await expectWebCallToMatch({ 27 | family: 'Storage', 28 | method: 'setItem', 29 | page, 30 | }); 31 | }); 32 | 33 | test('removeItem', async ({page}) => { 34 | await page.evaluate(async () => self.localStorage.setItem('test', 'value')); 35 | await page.evaluate(async () => self.localStorage.removeItem('test')); 36 | const value = await page.evaluate(async () => self.localStorage.getItem('test')); 37 | expect(value).toBeNull(); 38 | await expectWebCallToMatch({ 39 | family: 'Storage', 40 | method: 'removeItem', 41 | page, 42 | index: 1, 43 | }); 44 | }); 45 | 46 | test('clear', async ({page}) => { 47 | await page.evaluate(async () => self.localStorage.clear()); 48 | await expectWebCallToMatch({ 49 | family: 'Storage', 50 | method: 'clear', 51 | page, 52 | }); 53 | }); 54 | 55 | test('key', async ({page}) => { 56 | await page.evaluate(async () => self.localStorage.key(0)); 57 | await expectWebCallToMatch({ 58 | family: 'Storage', 59 | method: 'key', 60 | page, 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/web/StorageAccess.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasDocumentFeature} = require('../utils'); 3 | 4 | test.describe('StorageAccess', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasDocumentFeature('requestStorageAccess', page))) { 8 | test.skip('StorageAccess API not available'); 9 | } 10 | }); 11 | 12 | test('requestStorageAccess', async ({page}) => { 13 | await page.evaluate(async () => document.requestStorageAccess()); 14 | await expectWebCallToMatch({ 15 | family: 'StorageAccess', 16 | method: 'requestStorageAccess', 17 | page, 18 | }); 19 | }); 20 | 21 | test('hasStorageAccess', async ({page}) => { 22 | await page.evaluate(async () => document.hasStorageAccess()); 23 | await expectWebCallToMatch({ 24 | family: 'StorageAccess', 25 | method: 'hasStorageAccess', 26 | page, 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/web/StorageManager.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('StorageManager', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('storage', page))) { 8 | test.skip('StorageManager API not available'); 9 | } 10 | }); 11 | 12 | test('estimate', async ({page}) => { 13 | if (!(await hasNavigatorFeature('storage', 'estimate', page))) { 14 | test.skip('StorageManager.estimate() not available'); 15 | } 16 | const usage = await page.evaluate(async () => (await navigator.storage.estimate()).usage); 17 | expect(usage).toEqual(0); 18 | await expectWebCallToMatch({ 19 | family: 'StorageManager', 20 | method: 'estimate', 21 | page, 22 | }); 23 | }); 24 | 25 | test('persist', async ({page, browser}) => { 26 | if (browser.browserType().name() === 'firefox') { 27 | test.skip('StorageManager.persist hangs on Firefox'); 28 | } 29 | await page.evaluate(async () => navigator.storage.persist()); 30 | await expectWebCallToMatch({ 31 | family: 'StorageManager', 32 | method: 'persist', 33 | page, 34 | }); 35 | }); 36 | 37 | test('persisted', async ({page}) => { 38 | const persisted = await page.evaluate(async () => navigator.storage.persisted()); 39 | expect(persisted).toBeFalsy(); 40 | await expectWebCallToMatch({ 41 | family: 'StorageManager', 42 | method: 'persisted', 43 | page, 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/web/SubtleCrypto.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb} = require('../utils'); 3 | 4 | test.describe('SubtleCrypto', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await page.evaluate(async () => 'crypto' in self && 'subtle' in self.crypto))) { 8 | test.skip('SubtleCrypto API not available'); 9 | } 10 | }); 11 | 12 | test('decrypt', async ({page}) => { 13 | try { 14 | await page.evaluate(async () => self.crypto.subtle.decrypt()); 15 | } catch (error) {} 16 | 17 | await expectWebCallToMatch({ 18 | family: 'SubtleCrypto', 19 | method: 'decrypt', 20 | page, 21 | }); 22 | }); 23 | 24 | test('deriveBits', async ({page}) => { 25 | try { 26 | await page.evaluate(async () => self.crypto.subtle.deriveBits()); 27 | } catch (error) {} 28 | 29 | await expectWebCallToMatch({ 30 | family: 'SubtleCrypto', 31 | method: 'deriveBits', 32 | page, 33 | }); 34 | }); 35 | 36 | test('deriveKey', async ({page}) => { 37 | try { 38 | await page.evaluate(async () => self.crypto.subtle.deriveKey()); 39 | } catch (error) {} 40 | 41 | await expectWebCallToMatch({ 42 | family: 'SubtleCrypto', 43 | method: 'deriveKey', 44 | page, 45 | }); 46 | }); 47 | 48 | test('digest', async ({page}) => { 49 | try { 50 | await page.evaluate(async () => self.crypto.subtle.digest()); 51 | } catch (error) {} 52 | 53 | await expectWebCallToMatch({ 54 | family: 'SubtleCrypto', 55 | method: 'digest', 56 | page, 57 | }); 58 | }); 59 | 60 | test('encrypt', async ({page}) => { 61 | try { 62 | await page.evaluate(async () => self.crypto.subtle.encrypt()); 63 | } catch (error) {} 64 | 65 | await expectWebCallToMatch({ 66 | family: 'SubtleCrypto', 67 | method: 'encrypt', 68 | page, 69 | }); 70 | }); 71 | 72 | test('exportKey', async ({page}) => { 73 | try { 74 | await page.evaluate(async () => self.crypto.subtle.exportKey()); 75 | } catch (error) {} 76 | 77 | await expectWebCallToMatch({ 78 | family: 'SubtleCrypto', 79 | method: 'exportKey', 80 | page, 81 | }); 82 | }); 83 | 84 | test('generateKey', async ({page}) => { 85 | try { 86 | await page.evaluate(async () => self.crypto.subtle.generateKey()); 87 | } catch (error) {} 88 | 89 | await expectWebCallToMatch({ 90 | family: 'SubtleCrypto', 91 | method: 'generateKey', 92 | page, 93 | }); 94 | }); 95 | 96 | test('importKey', async ({page}) => { 97 | try { 98 | await page.evaluate(async () => self.crypto.subtle.importKey()); 99 | } catch (error) {} 100 | 101 | await expectWebCallToMatch({ 102 | family: 'SubtleCrypto', 103 | method: 'importKey', 104 | page, 105 | }); 106 | }); 107 | 108 | test('sign', async ({page}) => { 109 | try { 110 | await page.evaluate(async () => self.crypto.subtle.sign()); 111 | } catch (error) {} 112 | 113 | await expectWebCallToMatch({ 114 | family: 'SubtleCrypto', 115 | method: 'sign', 116 | page, 117 | }); 118 | }); 119 | 120 | test('unwrapKey', async ({page}) => { 121 | try { 122 | await page.evaluate(async () => self.crypto.subtle.unwrapKey()); 123 | } catch (error) {} 124 | 125 | await expectWebCallToMatch({ 126 | family: 'SubtleCrypto', 127 | method: 'unwrapKey', 128 | page, 129 | }); 130 | }); 131 | 132 | test('verify', async ({page}) => { 133 | try { 134 | await page.evaluate(async () => self.crypto.subtle.verify()); 135 | } catch (error) {} 136 | 137 | await expectWebCallToMatch({ 138 | family: 'SubtleCrypto', 139 | method: 'verify', 140 | page, 141 | }); 142 | }); 143 | 144 | test('wrapKey', async ({page}) => { 145 | try { 146 | await page.evaluate(async () => self.crypto.subtle.wrapKey()); 147 | } catch (error) {} 148 | 149 | await expectWebCallToMatch({ 150 | family: 'SubtleCrypto', 151 | method: 'wrapKey', 152 | page, 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /tests/web/USB.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('USB', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('usb', page))) { 8 | test.skip('USB API not available'); 9 | } 10 | }); 11 | 12 | test('getDevices', async ({page}) => { 13 | const devices = await page.evaluate(async () => navigator.usb.getDevices()); 14 | expect(Array.isArray(devices)).toBeTruthy(); 15 | await expectWebCallToMatch({ 16 | family: 'USB', 17 | method: 'getDevices', 18 | page, 19 | }); 20 | }); 21 | 22 | test('requestDevice', async ({page}) => { 23 | try { 24 | await page.evaluate(async () => navigator.usb.requestDevice({filters: []})); 25 | } catch (error) {} 26 | await expectWebCallToMatch({ 27 | family: 'USB', 28 | method: 'requestDevice', 29 | page, 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/web/Vibration.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('Vibration', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('vibrate', page))) { 8 | test.skip('Vibration API not available'); 9 | } 10 | }); 11 | 12 | test('vibrate', async ({page}) => { 13 | await page.evaluate(async () => navigator.vibrate(1)); 14 | await expectWebCallToMatch({ 15 | family: 'Vibration', 16 | method: 'vibrate', 17 | firstArg: 1, 18 | page, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/web/WakeLock.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasNavigatorFeature} = require('../utils'); 3 | 4 | test.describe('WakeLock', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasNavigatorFeature('wakeLock', page))) { 8 | test.skip('WakeLock API not available'); 9 | } 10 | }); 11 | 12 | test('request', async ({page}) => { 13 | try { 14 | await page.evaluate(async () => navigator.wakeLock.request('screen')); 15 | } catch (error) {} 16 | await expectWebCallToMatch({ 17 | family: 'WakeLock', 18 | method: 'request', 19 | firstArg: 'screen', 20 | page, 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/web/WebSocket.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('WebSocket', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('WebSocket', page))) { 8 | test.skip('WebSocket API not available'); 9 | } 10 | }); 11 | 12 | test('WebSocket', async ({page}) => { 13 | const url = await page.evaluate(async () => new WebSocket(new URL('ws://localhost')).url); 14 | expect(url).toEqual('ws://localhost/'); 15 | await expectWebCallToMatch({ 16 | family: 'WebSocket', 17 | method: 'WebSocket', 18 | page, 19 | }); 20 | }); 21 | 22 | test('close', async ({page}) => { 23 | await page.evaluate(async () => { 24 | const socket = new WebSocket(new URL('ws://localhost')); 25 | socket.close(); 26 | }); 27 | await expectWebCallToMatch({ 28 | family: 'WebSocket', 29 | method: 'close', 30 | index: 1, 31 | page, 32 | }); 33 | }); 34 | 35 | test('send', async ({page}) => { 36 | try { 37 | await page.evaluate(async () => { 38 | const socket = new WebSocket(new URL('ws://localhost')); 39 | socket.send(''); 40 | socket.close(); 41 | }); 42 | } catch (error) {} 43 | await expectWebCallToMatch({ 44 | family: 'WebSocket', 45 | method: 'send', 46 | index: 1, 47 | page, 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/web/Worker.test.js: -------------------------------------------------------------------------------- 1 | const {test} = require('@playwright/test'); 2 | const {expectCallToMatch, loadSandwormOnWeb, webWorkersAvailable} = require('../utils'); 3 | 4 | test.describe('Worker', () => { 5 | test.beforeEach(async ({page}) => { 6 | // Worker API seems overridden by Playwright 7 | test.skip('Worker API not available'); 8 | 9 | await loadSandwormOnWeb(page); 10 | if (!(await webWorkersAvailable(page))) { 11 | test.skip('Worker API not available'); 12 | } 13 | }); 14 | 15 | test('Worker', async ({page}) => { 16 | await page.evaluate(() => new Worker('worker.js')); 17 | await expectCallToMatch({ 18 | family: 'Worker', 19 | method: 'Worker', 20 | firstArg: 'worker.js', 21 | page, 22 | }); 23 | }); 24 | 25 | test('postMessage', async ({page}) => { 26 | await page.evaluate(() => { 27 | const worker = new Worker('worker.js'); 28 | worker.postMessage('test'); 29 | }); 30 | await expectCallToMatch({ 31 | family: 'Worker', 32 | method: 'postMessage', 33 | firstArg: 'test', 34 | page, 35 | }); 36 | }); 37 | 38 | test('terminate', async ({page}) => { 39 | await page.evaluate(() => { 40 | const worker = new Worker('worker.js'); 41 | worker.terminate(); 42 | }); 43 | await expectCallToMatch({ 44 | family: 'Worker', 45 | method: 'terminate', 46 | page, 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/web/XMLHttpRequest.test.js: -------------------------------------------------------------------------------- 1 | const {test, expect} = require('@playwright/test'); 2 | const {expectWebCallToMatch, loadSandwormOnWeb, hasGlobalFeature} = require('../utils'); 3 | 4 | test.describe('XMLHttpRequest', () => { 5 | test.beforeEach(async ({page}) => { 6 | await loadSandwormOnWeb(page); 7 | if (!(await hasGlobalFeature('XMLHttpRequest', page))) { 8 | test.skip('XMLHttpRequest not available'); 9 | } 10 | }); 11 | 12 | test('XMLHttpRequest', async ({page}) => { 13 | const status = await page.evaluate(async () => new XMLHttpRequest().status); 14 | expect(status).toEqual(0); 15 | await expectWebCallToMatch({ 16 | family: 'XMLHttpRequest', 17 | method: 'XMLHttpRequest', 18 | page, 19 | }); 20 | }); 21 | 22 | test('abort', async ({page}) => { 23 | await page.evaluate(async () => { 24 | const request = new XMLHttpRequest(); 25 | request.abort(); 26 | }); 27 | await expectWebCallToMatch({ 28 | family: 'XMLHttpRequest', 29 | method: 'abort', 30 | page, 31 | index: 1, 32 | }); 33 | }); 34 | 35 | test('getAllResponseHeaders', async ({page}) => { 36 | await page.evaluate(async () => { 37 | const request = new XMLHttpRequest(); 38 | request.getAllResponseHeaders(); 39 | }); 40 | await expectWebCallToMatch({ 41 | family: 'XMLHttpRequest', 42 | method: 'getAllResponseHeaders', 43 | page, 44 | index: 1, 45 | }); 46 | }); 47 | 48 | test('getResponseHeader', async ({page}) => { 49 | await page.evaluate(async () => { 50 | const request = new XMLHttpRequest(); 51 | request.getResponseHeader(''); 52 | }); 53 | await expectWebCallToMatch({ 54 | family: 'XMLHttpRequest', 55 | method: 'getResponseHeader', 56 | page, 57 | index: 1, 58 | }); 59 | }); 60 | 61 | test('open', async ({page}) => { 62 | await page.evaluate(async () => { 63 | const request = new XMLHttpRequest(); 64 | request.open('GET', 'http://localhost'); 65 | }); 66 | await expectWebCallToMatch({ 67 | family: 'XMLHttpRequest', 68 | method: 'open', 69 | page, 70 | index: 1, 71 | }); 72 | }); 73 | 74 | test('overrideMimeType', async ({page}) => { 75 | await page.evaluate(async () => { 76 | const request = new XMLHttpRequest(); 77 | request.overrideMimeType('application/json'); 78 | }); 79 | await expectWebCallToMatch({ 80 | family: 'XMLHttpRequest', 81 | method: 'overrideMimeType', 82 | page, 83 | index: 1, 84 | }); 85 | }); 86 | 87 | test('send', async ({page}) => { 88 | try { 89 | await page.evaluate(async () => { 90 | const request = new XMLHttpRequest(); 91 | request.send('test'); 92 | }); 93 | } catch (error) {} 94 | await expectWebCallToMatch({ 95 | family: 'XMLHttpRequest', 96 | method: 'send', 97 | page, 98 | index: 1, 99 | }); 100 | }); 101 | 102 | test('setRequestHeader', async ({page}) => { 103 | try { 104 | await page.evaluate(async () => { 105 | const request = new XMLHttpRequest(); 106 | request.setRequestHeader('content-type', 'application/json'); 107 | }); 108 | } catch (error) {} 109 | await expectWebCallToMatch({ 110 | family: 'XMLHttpRequest', 111 | method: 'setRequestHeader', 112 | page, 113 | index: 1, 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /tests/web/setup.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const sandwormCode = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'index.js'), { 6 | encoding: 'utf-8', 7 | }); 8 | 9 | module.exports = async () => { 10 | const server = http.createServer((request, response) => { 11 | switch (request.url) { 12 | case '/': 13 | response.writeHead(200, {'Content-Type': 'text/html'}); 14 | response.end('Hello World\n'); 15 | break; 16 | case '/sw.js': 17 | response.writeHead(200, {'Content-Type': 'text/javascript'}); 18 | response.end(` 19 | self.addEventListener('activate', event => { 20 | console.log('service worker active'); 21 | }); 22 | `); 23 | break; 24 | case '/worker.js': 25 | response.writeHead(200, {'Content-Type': 'text/javascript'}); 26 | response.end(` 27 | ${sandwormCode} 28 | Sandworm.init({devMode:true, loadSourceMaps: false}); 29 | `); 30 | break; 31 | case '/sandworm.js': 32 | response.writeHead(200, {'Content-Type': 'text/javascript'}); 33 | response.end(sandwormCode); 34 | break; 35 | default: 36 | response.writeHead(404); 37 | response.end(); 38 | } 39 | }); 40 | 41 | server.on('error', (err) => { 42 | if (err.code !== 'EADDRINUSE') { 43 | console.error(err); 44 | } 45 | }); 46 | 47 | server.listen(7070); 48 | }; 49 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: path.resolve(__dirname, 'src/index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'index.js', 10 | library: 'Sandworm', 11 | libraryTarget: 'umd', 12 | libraryExport: 'default', 13 | globalObject: 'this', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js)$/, 19 | exclude: /node_modules/, 20 | use: 'babel-loader', 21 | resolve: { 22 | fullySpecified: false, 23 | fallback: { 24 | fs: false, 25 | path: false, 26 | buffer: require.resolve('buffer/'), 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | plugins: [ 33 | new webpack.ProvidePlugin({ 34 | Buffer: ['buffer', 'Buffer'], 35 | }), 36 | ], 37 | mode: 'production', 38 | }; 39 | --------------------------------------------------------------------------------