From 9f1ed246672ffd1623ba985b34a56724c40ca79a Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:01:32 +1100 Subject: [PATCH 01/21] feat: port web server support from LibreKitten and patch for it --- package-lock.json | 887 +++++++++++++++--- package.json | 4 +- src/engine/runtime.js | 30 + src/engine/thread.js | 21 + .../extension-addon-switchers.js | 17 +- src/extension-support/extension-manager.js | 9 +- src/extension-support/tw-security-manager.js | 18 + .../tw-unsandboxed-extension-runner.js | 24 +- src/extensions/omni_server/index.js | 461 +++++++++ src/server/cli.js | 93 ++ src/server/resolve-path.js | 43 + src/server/server.js | 173 ++++ src/server/setup-file-security.js | 129 +++ src/server/storage.js | 47 + src/util/await-event.js | 20 + 15 files changed, 1811 insertions(+), 165 deletions(-) create mode 100644 src/extensions/omni_server/index.js create mode 100644 src/server/cli.js create mode 100644 src/server/resolve-path.js create mode 100644 src/server/server.js create mode 100644 src/server/setup-file-security.js create mode 100644 src/server/storage.js create mode 100644 src/util/await-event.js diff --git a/package-lock.json b/package-lock.json index d6759c7ad58..a339c65829e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,12 +21,14 @@ "diff-match-patch": "1.0.4", "format-message": "6.2.1", "htmlparser2": "3.10.0", + "jsdom": "^24.1.0", "scratch-parser": "github:TurboWarp/scratch-parser#master", "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "^1.0.7", "text-encoding": "0.7.0", "uuid": "8.3.2", - "worker-loader": "^1.1.1" + "worker-loader": "^1.1.1", + "yargs": "^18.0.0" }, "devDependencies": { "@babel/core": "7.13.10", @@ -76,6 +78,25 @@ "node": ">=0.10.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/@babel/cli": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.4.tgz", @@ -187,6 +208,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.10.tgz", "integrity": "sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.13.9", @@ -1748,6 +1770,118 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2066,7 +2200,6 @@ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", "dev": true, - "peer": true, "dependencies": { "eslint-scope": "5.1.1" } @@ -2199,7 +2332,6 @@ "resolved": "https://registry.npmjs.org/@turbowarp/scratch-svg-renderer/-/scratch-svg-renderer-1.0.202409161736.tgz", "integrity": "sha512-Lztj24zQqT8Ddw7gz1zCbRFZXi/h3r9boEzlQs5omtHeImGZe6I8mLkoHyeP/jJGOUpp5LKZgTKGOStBLtRSiw==", "license": "MPL-2.0", - "peer": true, "dependencies": { "@turbowarp/nanolog": "^0.2.0", "base64-js": "1.2.1", @@ -2217,8 +2349,7 @@ "version": "2.5.8", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2488,6 +2619,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2513,6 +2645,15 @@ "node": ">=0.3.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -2530,6 +2671,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2962,8 +3104,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/atob": { "version": "2.1.2", @@ -3597,6 +3738,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001565", "electron-to-chromium": "^1.4.601", @@ -4227,7 +4369,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4671,7 +4812,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "peer": true, "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -4684,11 +4824,29 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, "node_modules/cyclist": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", @@ -4716,11 +4874,57 @@ "node": ">=0.10" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -4742,6 +4946,12 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/decode-html": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/decode-html/-/decode-html-2.0.0.tgz", @@ -5093,7 +5303,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5706,15 +5915,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, - "optional": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5845,7 +6054,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -5986,6 +6194,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6144,7 +6353,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -6158,7 +6366,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -6168,7 +6375,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, - "peer": true, "engines": { "node": ">=10" } @@ -8188,11 +8394,22 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", @@ -8611,12 +8828,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -8818,6 +9035,18 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-entities": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", @@ -8892,6 +9121,19 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-proxy-middleware": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", @@ -8927,6 +9169,19 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/hull.js": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/hull.js/-/hull.js-0.2.10.tgz", @@ -9729,6 +9984,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -10244,6 +10505,141 @@ "node": ">=10" } }, + "node_modules/jsdom": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -10789,8 +11185,7 @@ "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "peer": true + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, "node_modules/mdurl": { "version": "1.0.1", @@ -11069,7 +11464,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -11078,7 +11472,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -11346,8 +11739,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multicast-dns": { "version": "6.2.3", @@ -11646,6 +12038,12 @@ "node": ">=0.10.0" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "license": "MIT" + }, "node_modules/nyc": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", @@ -11687,6 +12085,54 @@ "node": ">=8.9" } }, + "node_modules/nyc/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nyc/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/nyc/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -11772,6 +12218,58 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -12336,6 +12834,30 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12711,8 +13233,7 @@ "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/public-encrypt": { "version": "4.0.3", @@ -12793,8 +13314,7 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -13210,8 +13730,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/requizzle": { "version": "0.2.4", @@ -13344,6 +13863,12 @@ "inherits": "^2.0.1" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "license": "MIT" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -13468,6 +13993,18 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -13623,6 +14160,7 @@ "node_modules/scratch-render-fonts": { "version": "1.0.0", "resolved": "git+ssh://git@github.com/TurboWarp/scratch-render-fonts.git#6be162025085d738317b40a01644cf8dcbcee023", + "peer": true, "dependencies": { "base64-loader": "1.0.0" } @@ -15138,6 +15676,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, "node_modules/table": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", @@ -15157,6 +15701,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "integrity": "sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==", "dev": true, + "peer": true, "dependencies": { "co": "^4.6.0", "json-stable-stringify": "^1.0.1" @@ -15440,6 +15985,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", @@ -15880,6 +16426,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -16771,6 +17318,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -18476,7 +19024,6 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -18627,6 +19174,18 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/watchpack": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", @@ -18918,6 +19477,7 @@ "version": "4.47.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -20018,6 +20578,40 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -20262,6 +20856,21 @@ "async-limiter": "~1.0.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", @@ -20296,157 +20905,131 @@ } }, "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/yargs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" + "node_modules/yargs/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/yargs/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/yargs/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/yargs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "node_modules/yargs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=6" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, + "node_modules/yargs/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/yargs/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index a22e531a254..6cc5d113881 100644 --- a/package.json +++ b/package.json @@ -46,12 +46,14 @@ "diff-match-patch": "1.0.4", "format-message": "6.2.1", "htmlparser2": "3.10.0", + "jsdom": "^24.1.0", "scratch-parser": "github:TurboWarp/scratch-parser#master", "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "^1.0.7", "text-encoding": "0.7.0", "uuid": "8.3.2", - "worker-loader": "^1.1.1" + "worker-loader": "^1.1.1", + "yargs": "^18.0.0" }, "peerDependencies": { "@turbowarp/scratch-svg-renderer": "^1.0.0-202312300007-62fe825" diff --git a/src/engine/runtime.js b/src/engine/runtime.js index cb2d5a14ab5..21e9ea513d9 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -503,6 +503,22 @@ class Runtime extends EventEmitter { */ this.isPackaged = false; + /** + * omni: We support a "privileged" mode. This usually is set when the project is running as a server, + * but other privileged clients can use this too. + * This is mainly to indicate that system APIs (possibly mocked and/or with a permission system) + * can be accessed, as provided by the privileged client. + */ + this.isPrivileged = false; + + /** + * omni: Privileged utilities, so that the VM can communicate with a privileged client. + * This is usally filled in by the server client, but another client can fill this in too, + * as long as they are compatible and set isPrivileged to true. + * @type {Object} + */ + this.privilegedUtils = Object.create(null); + /** * Contains information about the external communication methods that the scripts inside the project * can use to send data from inside the project to an external server. @@ -947,6 +963,20 @@ class Runtime extends EventEmitter { return 'PLATFORM_MISMATCH'; } + /** + * omni: Event name when a web request is forwarded to the VM. + */ + static get SERVER_REQUEST () { + return 'SERVER_REQUEST'; + } + + /** + * omni: Event name when a response to a web request is forwarded to the web request handler. + */ + static get SERVER_RESPONSE () { + return 'SERVER_RESPONSE'; + } + /** * How rapidly we try to step threads by default, in ms. */ diff --git a/src/engine/thread.js b/src/engine/thread.js index b0cb410b0f3..832edf5cd09 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -216,6 +216,27 @@ class Thread { this.procedures = null; this.executableHat = false; this.compatibilityStackFrame = null; + + /** + * omni: The object the web server stores a request in. + * @type {object} + */ + this.serverRequest = { + ip: '', + method: '', + page: '', + headers: '{}', + data: '' + }; + /** + * omni: The object the web server constructs the response in. + * @type {object} + */ + this.serverResponse = { + mime: 'text/plain', + status: null, // Intialized by the request listener hat. + headers: '{}' + }; } /** diff --git a/src/extension-support/extension-addon-switchers.js b/src/extension-support/extension-addon-switchers.js index 7725212a899..c23590226a6 100644 --- a/src/extension-support/extension-addon-switchers.js +++ b/src/extension-support/extension-addon-switchers.js @@ -1,6 +1,6 @@ const log = require("../util/log"); const switches = {}; -const parser = new DOMParser(); +const parser = typeof DOMParser === 'undefined' ? null : new DOMParser(); const define_error_noop = (msg) => { log.warn(msg); @@ -12,6 +12,21 @@ const define_error_noop = (msg) => { }; function get_extension_switches(id, blocks) { + // I have no idea what this is doing and why it is trying to monkeypatch Blockly via the DOM from + // the VM; but, it is blocking server support, so I'm going to mock it for running in Node.js and + // hope for the best. Contact @someCatInTheWorld if this mocking breaks something horribly. + if (typeof process !== 'undefined') return { + opcode: 'un_supported', + msg: 'unsupported', + + mapFieldValues: {}, + remapInputName: {}, + + createInputs: {}, + splitInputs: [], + remapShadowType: {}, + }; + let _switches = {}; for (let block of blocks) { var blockswitches = block.info.switches; diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index c217f1e91f3..b2b4cc8ba5e 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -8,10 +8,12 @@ const Cast = require('../util/cast'); const AddonSwitches = require('./extension-addon-switchers'); +/* Commenting out for the sake of server support. + const urlParams = new URLSearchParams(location.search); const IsLocal = String(window.location.href).startsWith(`http://localhost:`); -const IsLiveTests = urlParams.has('livetests'); +const IsLiveTests = urlParams.has('livetests'); */ // thhank yoh random stack droverflwo person async function sha256(source) { @@ -43,8 +45,9 @@ const defaultBuiltinExtensions = { gdxfor: () => require('../extensions/scratch3_gdx_for'), // tw: core extension tw: () => require('../extensions/tw'), - SPjavascriptV2: () => require("../extensions/sp_javascriptV2") - + SPjavascriptV2: () => require("../extensions/sp_javascriptV2"), + // omni: Web server blocks. + server: () => require('../extensions/omni_server'), }; const CORE_EXTENSIONS = [ 'argument', diff --git a/src/extension-support/tw-security-manager.js b/src/extension-support/tw-security-manager.js index 4ae3fd6b917..309bde77ae9 100644 --- a/src/extension-support/tw-security-manager.js +++ b/src/extension-support/tw-security-manager.js @@ -171,6 +171,24 @@ class SecurityManager { shouldUseLocal(refrenceName) { return Promise.resolve(!confirm(`it seems that the extension ${refrenceName} has been updated, use the up-to-date code?`)) } + + /** + * omni: Determine whether a file can be read from a location. Meant for privileged environments. + * @param {string} path The file to read + * @returns {Promise|boolean} + */ + canReadFile (path) { + return Promise.resolve(false); + } + + /** + * omni: Determine whether a file can be written to a location. Meant for privileged environments. + * @param {string} path The file to write + * @returns {Promise|boolean} + */ + canWriteFile (path) { + return Promise.resolve(false); + } } module.exports = SecurityManager; diff --git a/src/extension-support/tw-unsandboxed-extension-runner.js b/src/extension-support/tw-unsandboxed-extension-runner.js index ef38eb62d1a..0ad1a478c7d 100644 --- a/src/extension-support/tw-unsandboxed-extension-runner.js +++ b/src/extension-support/tw-unsandboxed-extension-runner.js @@ -178,14 +178,22 @@ const teardownUnsandboxedExtensionAPI = () => { * @returns {Promise} Resolves with a list of extension objects if the extension was loaded successfully. */ const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => { - setupUnsandboxedExtensionAPI(vm).then(resolve); - - const script = document.createElement('script'); - script.onerror = () => { - reject(new Error(`Error in unsandboxed script ${extensionURL}. Check the console for more information.`)); - }; - script.src = extensionURL; - document.body.appendChild(script); + if (typeof process === 'undefined') { + const script = document.createElement('script'); + script.onerror = () => { + reject(new Error(`Error in unsandboxed script ${extensionURL}. Check the console for more information.`)); + }; + script.src = extensionURL; + document.body.appendChild(script); + } else { + fetch(extensionURL).then(res => { + res.text().then(data => { + const extension = data; + const run = Function('Scratch', extension); + run(global.Scratch); + }); + }); + } }).then(objects => { teardownUnsandboxedExtensionAPI(); return objects; diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js new file mode 100644 index 00000000000..fc7779885ec --- /dev/null +++ b/src/extensions/omni_server/index.js @@ -0,0 +1,461 @@ +// omni: Added server support. + +const formatMessage = require('format-message'); +const BlockType = require('../../extension-support/block-type'); +const ArgumentType = require('../../extension-support/argument-type'); +const Cast = require('../../util/cast'); +const Runtime = require('../../engine/runtime'); +const Thread = require('../../engine/thread'); + +// Icon Credits: https://freesvg.org/server-icon-vector-image, dedicated to the public domain. +// eslint-disable-next-line max-len +const iconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICAgeG1sbnM6bnMxPSJodHRwOi8vc296aS5iYWllcm91Z2UuZnIiCiAgICB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKICAgIGlkPSJzdmczMDcyIgogICAgc29kaXBvZGk6ZG9jbmFtZT0iU2VydmVyLnN2ZyIKICAgIHZpZXdCb3g9IjAgMCAxNjUuNjkgMjU2IgogICAgdmVyc2lvbj0iMS4xIgogICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40NyByMjI1ODMiCiAgPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICAgaWQ9ImJhc2UiCiAgICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgICBpbmtzY2FwZTp3aW5kb3cteT0iNjUiCiAgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNDc5IgogICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIwIgogICAgICBpbmtzY2FwZTp6b29tPSIwLjkzNzQ5OTg5IgogICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTc3NyIKICAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImxheWVyMSIKICAgICAgaW5rc2NhcGU6Y3g9IjgyLjg0NDY4OCIKICAgICAgaW5rc2NhcGU6Y3k9IjEyOC4wMDAwMiIKICAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSI3NDEiCiAgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAvPgogIDxnCiAgICAgIGlkPSJsYXllcjEiCiAgICAgIGlua3NjYXBlOmxhYmVsPSJDYXBhIDEiCiAgICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00MDUuNzMgLTE0NC4zNikiCiAgICA+CiAgICA8ZwogICAgICAgIGlkPSJnMTAiCiAgICAgICAgY2xhc3M9IkdyYXBoaWMiCiAgICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC4wNDIzIDAgMCAwLjA0MjMgMzE5LjM5IDU5LjEyOCkiCiAgICAgID4KICAgICAgPGcKICAgICAgICAgIGlkPSJnMTIiCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnMTQiCiAgICAgICAgICAgIHN0eWxlPSJmaWxsOiNjN2M3YzciCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoMTYiCiAgICAgICAgICAgICAgZD0ibTU5MDMgMjM2MWM1MCA4MiAzNCAzOTk1LTMxIDQxNDktNTEgMTIyLTE3MDMgMTQ5NS0xNzg5IDE1MzEtMTExNy03LTE4NzAtNDE0LTE5OTUtNjI5LTQ4LTI3MC03LTQ0MzMgMzgtNDUyMCA0MC03NiAyMTUzLTgzMyAyMjI3LTg1MSA2Ny0xNSAxNDkzIDIyOSAxNTUwIDMyMHoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzE4IgogICAgICAgICAgICBzdHlsZT0iZmlsbDojMDAwMDAwIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDIwIgogICAgICAgICAgICAgIGQ9Im01ODgxIDIzNzQgMi0xLTIgMmMwLTEtMS0xLTEtMXYtMmw3LTMtMiAyLTUgMSA1LTEtMiAyLTIgMXptMi0xIDItMiAyLTIgNS0yLTUgNCA1LTRoMWwtNiAzdjEtMWwtNCAzem0xMC02aC0xIDEtMWwxMS02LTEwIDV2MXptMTEtNC0xMSA0di0xbDExLTN6bTIwLTE1Yy0yLTMtNC02LTctOS0yLTMtNS01LTgtNy01LTQtMTItOC0yMC0xMi03LTQtMTUtNy0yNC0xMXMtMTktNy0zMC0xMWMtMjEtOC00Ny0xNi03NS0yNC0yOS04LTYxLTE2LTk1LTI1LTEzNi0zNC0zMTMtNzItNDk0LTEwNi0xODEtMzUtMzY0LTY2LTUxMS05MC03My0xMS0xMzgtMjEtMTg5LTI3LTI1LTMtNDctNi02NS04LTktMS0xNy0xLTI0LTItNCAwLTcgMC0xMC0xaC05Yy0zIDAtNSAwLTcgMWgtNGMtMiAwLTMgMC00IDEtMiAwLTQgMS03IDItMiAwLTUgMS04IDJzLTcgMi0xMSAzYy00IDItOCAzLTEzIDUtMTAgMy0yMiA3LTM1IDEyLTEzIDQtMjggOS00NCAxNS0zMyAxMS03MSAyNS0xMTUgNDAtNDMgMTUtOTEgMzItMTQ0IDUxLTIwOSA3NS00ODEgMTczLTc1MyAyNzQtMjcxIDEwMS01NDIgMjAzLTc0NyAyODUtNTEgMjEtOTggNDAtMTQwIDU3LTQzIDE4LTgwIDM0LTExMSA0OC0xNiA3LTMwIDE0LTQyIDIwLTEzIDYtMjQgMTItMzMgMTctNSAyLTkgNS0xMyA3LTQgMy04IDUtMTAgNy00IDMtNyA1LTkgNy01IDQtNyA4LTkgMTEtMSAyLTIgNS00IDkgMCAzLTEgNi0xIDktMSAzLTEgNy0yIDExcy0xIDgtMiAxM2MwIDktMSAyMC0zIDMyIDAgMTMtMSAyNy0yIDQyLTIgMzEtNCA2OS02IDExMS0yIDQzLTQgOTEtNSAxNDQtNyAyMTMtMTQgNTA0LTE5IDgzNS0xMSA2NjItMTkgMTQ4Mi0xOSAyMTQ0IDAgMzA4IDIgNTgzIDUgNzkxIDEgNTIgMiAxMDAgMyAxNDNzMyA4MiA0IDExNWMxIDM0IDMgNjIgNCA4NSAxIDEyIDIgMjIgMyAzMCAxIDUgMSA5IDEgMTMgMSAzIDIgNyAyIDEwbDEgNCAyIDRjOSAxNSAyMCAzMCAzMyA0NiAxNCAxNiAzMCAzMiA0OCA0OSAzNSAzMiA4MCA2NiAxMzUgMTAxIDEwNyA3MCAyNDkgMTQyIDQyMyAyMDcgMTczIDY1IDM3NyAxMjQgNjA4IDE2N3M0ODkgNzAgNzY5IDcyaDZsNS0yIDgtNGMyLTIgNS0zIDgtNiAzLTIgNi00IDEwLTZsMTItOWM5LTYgMTktMTMgMzAtMjIgMTEtOCAyMy0xNyAzNy0yOCAyNy0yMCA1OC00NSA5My03M3M3NC01OSAxMTYtOTNjMTY5LTEzNiAzODUtMzE1IDYwMC00OTcgMjE1LTE4MSA0MzAtMzY2IDU5My01MTIgNDEtMzcgNzktNzEgMTEzLTEwMyAzNC0zMSA2NC01OSA4OS04NCAxMy0xMyAyNC0yNCAzNC0zNSAxMS0xMSAyMC0yMCAyOC0yOXMxNC0xNyAxOS0yM2MzLTQgNS04IDctMTAgMi00IDQtOCA2LTExIDAtMiAxLTUgMi04czItNSAzLTljMS02IDItMTMgMy0yMiAyLTggMy0xNyA0LTI4czItMjIgMy0zNWMyLTI1IDQtNTUgNy04OSAxLTM0IDMtNzIgNS0xMTMgOC0xNjYgMTQtMzkxIDE5LTY0OCAxMC01MTYgMTYtMTE2NCAxNi0xNzQzIDAtMzg5LTMtNzQ3LTgtMTAxMi0xLTY2LTItMTI3LTQtMTgxLTEtNTMtMy0xMDAtNS0xMzktMS0yMC0yLTM4LTMtNTMtMS0xNi0yLTMwLTMtNDEtMS02LTEtMTEtMi0xNnMtMS0xMC0yLTEzYy0xLTQtMS04LTItMTEtMS0yLTEtNC0yLTUtMS0zLTItNS0zLTZ6bS0zNyAyMSAxMi0xMi0xMiAxMnptLTYgOWMxIDMgMSA3IDIgMTEgMCA0IDEgOSAxIDE1IDEgMTEgMyAyNCA0IDM5IDAgMTUgMSAzMyAyIDUyIDIgMzkgNCA4NiA2IDEzOSAxIDUzIDIgMTE0IDQgMTgwIDUgMjY0IDcgNjIyIDcgMTAxMSAwIDU3OC01IDEyMjctMTUgMTc0Mi02IDI1Ny0xMiA0ODEtMTkgNjQ2LTIgNDItNCA3OS02IDExMy0yIDMzLTQgNjMtNiA4OC0xIDEyLTIgMjQtNCAzNCAwIDEwLTIgMTgtMyAyNi0xIDctMiAxNC0zIDE4IDAgMy0xIDQtMSA2IDAgMS0xIDItMSAyIDAgMS0xIDItMiA0LTEgMS0zIDQtNCA2LTUgNi0xMCAxMy0xNyAyMS03IDctMTYgMTctMjYgMjdsLTM0IDM0Yy0yNSAyNC01NCA1Mi04OCA4My0zMyAzMS03MSA2Ni0xMTIgMTAyLTE2MyAxNDYtMzc3IDMzMC01OTIgNTEyLTIxNSAxODEtNDMxIDM2MC01OTggNDk1LTQyIDM0LTgxIDY1LTExNiA5M3MtNjYgNTItOTMgNzNjLTEzIDEwLTI2IDIwLTM3IDI4LTEwIDgtMjAgMTUtMjggMjEtNCAzLTggNi0xMiA4LTMgMi02IDQtOCA2LTIgMS00IDItNSAzLTI3Ni0yLTUyOC0yOS03NTUtNzEtMjI4LTQyLTQyOS0xMDAtNjAwLTE2NC0xNjktNjQtMzA4LTEzNC00MTMtMjAyLTUxLTMzLTk0LTY2LTEyOC05Ny0xNy0xNS0zMS0zMC00My00NC0xMC0xMi0xOS0yMy0yNi0zNHYtNGMtMS0zLTEtNy0xLTExLTEtOC0yLTE4LTMtMjgtMi0yMy0zLTUxLTUtODQtMS0zMy0yLTcxLTMtMTE1LTEtNDItMi05MC0zLTE0Mi0zLTIwOC01LTQ4Mi01LTc5MCAwLTY2MiA3LTE0ODEgMTgtMjE0MyA2LTMzMCAxMi02MjIgMTktODM0IDItNTMgNC0xMDEgNi0xNDQgMi00MiAzLTc5IDUtMTA5IDEtMTYgMi0zMCAzLTQyczItMjIgMy0zMWMwLTQgMS04IDEtMTEgMS00IDEtNiAyLTl2LTJjMC0xIDEtMSAyLTIgMi0xIDQtMyA3LTUgNC0xIDctNCAxMS02IDktNSAxOS0xMCAzMS0xNnMyNi0xMiA0MS0xOWMzMS0xNCA2Ny0yOSAxMDktNDcgNDItMTcgODktMzcgMTQwLTU3IDIwNS04MiA0NzUtMTg0IDc0Ni0yODQgMjcxLTEwMSA1NDMtMjAwIDc1Mi0yNzQgNTItMTkgMTAxLTM2IDE0NC01MSA0My0xNiA4Mi0yOSAxMTQtNDAgMTctNiAzMS0xMSA0NC0xNiAxMy00IDI1LTggMzUtMTEgNC0yIDktMyAxMy00IDQtMiA3LTMgMTAtNHM1LTEgOC0ybDMtMS0yLTggMSA4aDEgNCA3YzIgMCA1IDAgOCAxIDcgMCAxNCAxIDIzIDIgMTcgMSAzOSA0IDY0IDcgNTAgNiAxMTQgMTYgMTg3IDI3IDE0NyAyMyAzMjkgNTUgNTA5IDg5IDE4MCAzNSAzNTcgNzIgNDkyIDEwNiAzNCA5IDY1IDE3IDkyIDI1IDI4IDggNTMgMTUgNzMgMjMgMTAgMyAyMCA3IDI4IDEwIDcgMyAxNSA2IDIwIDlzMTAgNSAxMyA4YzAgMCAxIDAgMSAxczEgMyAxIDR6bS0xODA2IDU2MzljMSAwIDEtMSAyLTFoNnYxNGwtOC0xMyA4IDEzdjExbC0xMC0yM3MxLTEgMi0xem0tMTk2NS02MTdjMSAxIDEgMyAyIDRzMSAzIDEgNGwtMjUgNCAyMS0xMSAxLTEtMSAxIDEtMWgyLTJ6bTM5LTQ0OTh2M2MtMSAwLTEgMS0yIDFsLTUtNi0xNy00IDE3IDQtMi0xIDQgMiA1IDF6bS03LTIgMiAxLTItMXptLTE2LTggMTQgNy0xNC03em0xNCA3LTEwLTExIDEwIDExem0yMjE3LTg0MS0yLTE3IDIgMTd6bS00LTE3IDQgMTd2MWwtNC0xOHoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzIyIgogICAgICAgICAgICBzdHlsZT0ic3Ryb2tlOiNmZmZmZmY7ZmlsbC1vcGFjaXR5Oi41NDExODtzdHJva2Utb3BhY2l0eTouNTQxMTg7ZmlsbDojZmZmZmZmIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgIGQ9Im01ODIyIDI0NDItNTIgMjItNTIgMjMtNTIgMjMtNTMgMjUtNTMgMjUtNTMgMjYtNTMgMjYtNTQgMjctNTQgMjctNTQgMjgtNTUgMjgtNTQgMjktNTUgMjktNTUgMjktMTExIDYwLTExMiA2MC0xMTIgNjAtMTEzIDYxLTExNCA2MC0xMTQgNjAtMTE0IDU5LTU3IDI5LTU3IDI5LTU4IDI4LTU3IDI4LTI3LTYtMjgtNy01Ni0xMy01OC0xNC01OS0xMy02MC0xNC02MS0xNC02MS0xNC02My0xNC02Mi0xNC02My0xNS0xMjctMjgtMTI3LTI4LTY0LTE0LTYzLTE0LTYyLTEzLTYyLTEzLTYxLTEzLTYwLTEzLTYwLTEzLTU4LTEyLTU3LTEyLTI4LTUtMjgtNi0yNy01LTI3LTYtMjctNS0yNi01LTI2LTUtMjUtNi0yNS00LTI0LTUtMjQtNS0yNC00LTIzLTQtMjItNS0yMi00LTIxLTMtMjEtNC0yMC00IDUyIDE2IDUyIDE2IDUzIDE2IDU0IDE1IDU1IDE2IDU1IDE1IDU3IDE2IDU2IDE1IDU3IDE2IDU4IDE1IDExNiAzMSAxMTcgMzAgMTE4IDMxIDExOCAzMSAxMTcgMzAgMTE2IDMxIDU4IDE1IDU3IDE2IDU3IDE1IDU2IDE2IDU1IDE1IDU1IDE2IDU0IDE1IDUzIDE2IDUyIDE2IDUyIDE2IDEgNjAgMiA2MSAxIDYxIDEgNjMgMSA2NCAyIDY0IDEgNjUgMSA2NiAxIDY3IDEgNjcgMSA2OHY2OWwxIDY5IDEgNzAgMSA3MHY3MWwxIDcxdjcybDEgNzIgMSA3MyAxIDE0NiAxIDE0N3YxNDlsMSAxNDkgMiAyOTkgMSAzMDAgMSAxNDkgMSAxNDggMSAxNDcgMSAxNDYgMSA3M3Y3MmwxIDcyIDEgNzF2NzFsMSA3MSAxIDY5IDEgNzB2NjhsMSA2OCAxIDY3IDEgNjcgMSA2NiAxIDY1IDEgNjUgMiA2MyAxIDYzIDEgNjIgMiA2MSAxIDYwIDEtNjEgMS02MXYtNjNsMS02MyAxLTY0IDEtNjUgMS02NiAxLTY3IDEtNjcgMS02OCAxLTY5IDItNjkgMS03MCAxLTcxIDEtNzEgMS03MiAxLTcyIDItNzMgMS03MyAxLTczIDMtMTQ4IDMtMTQ5IDItMTUxIDMtMTUxIDYtMzAzIDUtMzAzIDMtMTUxIDMtMTUwIDItMTQ5IDMtMTQ5IDEtNzMgMi03MyAxLTczIDEtNzIgMS03MSAxLTcyIDEtNzAgMi03MCAxLTcwIDEtNjggMS02OCAxLTY4IDEtNjYgMS02NiAxLTY1IDEtNjQgMS02NHYtNjJsMS02MiAxLTYwIDExMC01OSAxMDktNTkgMTA5LTU3IDEwOS01OCAxMDgtNTcgMTA3LTU4IDEwNy01NyAxMDctNTggMTA1LTU4IDEwNS01OCAxMDQtNjAgMTAzLTYwIDUxLTMwIDUwLTMxIDUxLTMxIDUwLTMxIDUwLTMxIDUwLTMyIDQ5LTMyIDUwLTMyeiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnMjgiCiAgICAgICAgICAgIHN0eWxlPSJmaWxsOm5vbmUiCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICAgZD0ibTU4MjIgMjQ0MmMtNTQ5IDIzMi0xMTU2IDYwNi0xNzY4IDkwMS01ODAtMTM4LTE0MTctMzIyLTE4MzktMzk2IDU0MyAxNjkgMTI1NyAzMjcgMTgwMCA0OTYgMzIgMTI3MSAxNiAzMTk2IDQ4IDQ0NjcgMTYtMTI4MiA1OS0zMjM4IDc1LTQ1MTkgNTg2LTMxNyAxMTYxLTYwMSAxNjg0LTk0OXoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzMyIgogICAgICAgICAgICBzdHlsZT0ic3Ryb2tlOiNmZmZmZmY7ZmlsbC1vcGFjaXR5Oi4xNjA3ODtzdHJva2Utb3BhY2l0eTouMTYwNzg7ZmlsbDojMDAwMDAwIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDM0IgogICAgICAgICAgICAgIGQ9Im0yNDIwIDY2MjggMTAgMTQgMTAgMTMgMTEgMTMgMTIgMTMgMTIgMTIgMTIgMTIgMTQgMTIgMTMgMTIgMTQgMTIgMTUgMTEgMTUgMTEgMTYgMTEgMTUgMTEgMTcgMTAgMTYgMTAgMTcgMTEgMTggOSAxOCAxMCAxOCA5IDE4IDggMTkgOSAxOSA4IDE5IDggMTkgOCA0MCAxNSA0MCAxNCA0MSAxMiA0MSAxMSA0MiAxMCA0MSA5IDQyIDggNDEgNiA0MSA2IDQxIDMgMjEgMiAyMCAxIDE5IDFoMjAgMjAgMTlsMTktMSAxOS0xIDE4LTEgMTktMSAxNy0yIDE4LTMgMTctMiAxNy0zIDE3LTQgMTYtNCAxNS00IDE1LTQgMTUtNSAxNS01LTItMWgtMWwtNS0yLTYtMS03LTItOC0yLTEwLTMtMTAtMy0xMi0zLTEzLTQtMTQtMy0xNS00LTE2LTUtMTYtNC0xOC01LTE4LTUtMTktNS0yMC02LTIwLTUtMjEtNi0yMi02LTIyLTYtMjItNi0yMy03LTI0LTYtNDgtMTMtNDktMTQtNTAtMTQtNTAtMTMtNTAtMTUtNTAtMTMtNDktMTQtNDktMTMtMjMtNy0yNC02LTIzLTctMjItNi0yMi02LTIxLTYtMjEtNi0yMC01LTE5LTYtMTktNS0xOC01LTE3LTUtMTYtNC0xNS01LTE1LTQtMTMtNC0xMi0zLTEyLTMtMTAtMy05LTMtNy0yLTctMi01LTJoLTJsLTItMXoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzM4IgogICAgICAgICAgICBzdHlsZT0iZmlsbDpub25lIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDQwIgogICAgICAgICAgICAgIGQ9Im0yNDIwIDY2MjhjMjAxIDI4OSA4MzMgNDMxIDExMzMgMzE1LTUzLTE0LTEwNjYtMjkzLTExMzMtMzE1eiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnNDIiCiAgICAgICAgICAgIHN0eWxlPSJzdHJva2U6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6LjM4ODI0O3N0cm9rZS1vcGFjaXR5Oi4zODgyNDtmaWxsOiNmZmZmZmYiCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICAgZD0ibTM0NTQgNjA5My05MzYtMzYxLTkgNC04IDQtOCA0LTYgNS02IDYtNiA2LTUgNy01IDYtNCA3LTQgNy0zIDctMyA4LTMgOC0yIDctMyAxNS0yIDE1LTIgMTR2NiA3IDUgNiA1IDQgNGwxIDR2MiAyIDEgMWwxNDYgNTEgMSAxIDEgMSAxIDEgMSAyIDIgMiAxIDMgMyAzIDIgMyAzIDQgMyA0IDMgNCA0IDUgNCA0IDQgNiA1IDQgMTAgMTIgMTAgMTEgMTMgMTIgMTMgMTIgMTQgMTMgMTUgMTIgMTcgMTMgMTcgMTIgMTkgMTIgMTkgMTEgMjEgMTEgMjIgMTAgMTEgNCAxMSA1IDEyIDQgMTIgNCAxMyAzIDEyIDMgMTMgMyAxMyAzIDEzIDIgMTMgMSAxNCAyIDE0IDFoMTQgMTUgMTVsMTUtMSAxNS0yIDE1LTIgMTYtMiAxNi0zIDE2LTQgMTYtNCAxNy01IDE3LTZoMWwyIDFoM2w2IDEgNiAxIDcgMSA4IDEgOSAxIDEwIDEgMTEgMSAxMiAxIDEyIDFoMTJsMTMgMWgyNmwxNC0xIDEzLTEgMTQtMSAxMy0yIDEzLTIgMTItMyAxMi0zIDEyLTMgMTAtNSAxMS01IDktNiA4LTYgOC04IDMtNCAzLTQgMy00IDItNCAyLTUgMi01eiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnNDgiCiAgICAgICAgICAgIHN0eWxlPSJmaWxsOm5vbmUiCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoNTAiCiAgICAgICAgICAgICAgZD0ibTM0NTQgNjA5My05MzYtMzYxYy05NyAzNC03OCAxNzctNzggMTc3bDE0NiA1MXMyMDQgMzE5IDU2NiAxOTRjMCAwIDI2OSA0OCAzMDItNjF6IgogICAgICAgICAgLz4KICAgICAgICA8L2cKICAgICAgICA+CiAgICAgICAgPGcKICAgICAgICAgICAgaWQ9Imc1MiIKICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzYwNjA2MCIKICAgICAgICAgID4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgaWQ9InBhdGg1NCIKICAgICAgICAgICAgICBkPSJtMzQ0NiA1OTcxYy0zMTEtODEtNjIyLTE2MS05MzMtMjQyLTEyIDQ0LTEzIDEwNyA2IDE0MCA0NyAyMyAxNDIgNDEgMTg5IDY0IDEzNiAxMzEgMjU1IDE0MiA0NzIgMTI0IDg0IDE3IDE2OCAzMSAyNTEgNDggMjYtNTEgMzMtNzIgMTUtMTM0eiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnNTYiCiAgICAgICAgICAgIHN0eWxlPSJmaWxsOm5vbmUiCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoNTgiCiAgICAgICAgICAgICAgZD0ibTM0NDYgNTk3MWMtMzExLTgxLTYyMi0xNjEtOTMzLTI0Mi0xMiA0NC0xMyAxMDcgNiAxNDAgNDcgMjMgMTQyIDQxIDE4OSA2NCAxMzYgMTMxIDI1NSAxNDIgNDcyIDEyNCA4NCAxNyAxNjggMzEgMjUxIDQ4IDI2LTUxIDMzLTcyIDE1LTEzNHoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzYwIgogICAgICAgICAgICBzdHlsZT0ic3Ryb2tlOiNmZmZmZmY7ZmlsbC1vcGFjaXR5Oi40O3N0cm9rZS1vcGFjaXR5Oi40O2ZpbGw6I2ZmZmZmZiIKICAgICAgICAgID4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgaWQ9InBhdGg2MiIKICAgICAgICAgICAgICBkPSJtNTg1NiAyNDE0LTE4MTMgMTAxNyA3IDQ1NDkgMTc3NS0xNDk0IDMxLTQwNzJ6IgogICAgICAgICAgLz4KICAgICAgICA8L2cKICAgICAgICA+CiAgICAgICAgPGcKICAgICAgICAgICAgaWQ9Imc2NiIKICAgICAgICAgICAgc3R5bGU9ImZpbGw6bm9uZSIKICAgICAgICAgID4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgaWQ9InBhdGg2OCIKICAgICAgICAgICAgICBkPSJtNTg1NiAyNDE0LTE4MTMgMTAxNyA3IDQ1NDkgMTc3NS0xNDk0IDMxLTQwNzJ6IgogICAgICAgICAgLz4KICAgICAgICA8L2cKICAgICAgICA+CiAgICAgICAgPGcKICAgICAgICAgICAgaWQ9Imc3MCIKICAgICAgICAgICAgc3R5bGU9InN0cm9rZTojZmZmZmZmO2ZpbGwtb3BhY2l0eTouMTYwNzg7c3Ryb2tlLW9wYWNpdHk6LjE2MDc4O2ZpbGw6IzAwMDAwMCIKICAgICAgICAgID4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgaWQ9InBhdGg3MiIKICAgICAgICAgICAgICBkPSJtMjUyNSA2OTE2IDEwIDEzIDExIDEyIDEwIDEzIDExIDExIDExIDEyIDExIDExIDEyIDExIDEyIDEwIDEyIDEwIDEyIDEwIDEzIDEwIDEyIDkgMTMgOSAxMyA4IDI3IDE3IDI4IDE1IDI4IDE0IDI4IDEzIDI5IDExIDI5IDExIDMwIDkgMzAgOCAyOSA4IDMwIDYgMzAgNSAzMCA0IDMwIDMgMzAgMiAyOSAyaDI5bDI5LTEgMjctMiAyOC0yIDI3LTMgMjYtNSAyNS01IDI0LTYgMjQtNiAyMi04LTItMWgtNGwtNC0yLTUtMS02LTItOC0yLTgtMi05LTItMTAtMy0xMC0zLTExLTMtMTItMy0xMy00LTEzLTMtMTQtNC0xNS00LTE1LTQtMTUtNS0xNi00LTE2LTUtMTctNC0xOC01LTM1LTEwLTM2LTEwLTM4LTEwLTM4LTEwLTM4LTExLTc2LTIxLTM3LTEwLTM3LTExLTM2LTEwLTE3LTQtMTctNS0xNy01LTE2LTQtMTYtNS0xNS00LTE1LTQtMTQtNC0xNC00LTEzLTQtMTItMy0xMS0zLTEyLTMtMTAtMy05LTMtOC0yLTgtMy03LTEtNi0yLTUtMi00LTEtMy0xeiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnNzYiCiAgICAgICAgICAgIHN0eWxlPSJmaWxsOm5vbmUiCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoNzgiCiAgICAgICAgICAgICAgZD0ibTI1MjUgNjkxNmMyMDkgMjc2IDYyNyAzMjMgODYxIDIzOS0zOS0xMC04MTAtMjIyLTg2MS0yMzl6IgogICAgICAgICAgLz4KICAgICAgICA8L2cKICAgICAgICA+CiAgICAgICAgPGcKICAgICAgICAgICAgaWQ9Imc4MCIKICAgICAgICAgICAgc3R5bGU9InN0cm9rZTojZmZmZmZmO2ZpbGwtb3BhY2l0eTouMTYwNzg7c3Ryb2tlLW9wYWNpdHk6LjE2MDc4O2ZpbGw6IzAwMDAwMCIKICAgICAgICAgID4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgaWQ9InBhdGg4MiIKICAgICAgICAgICAgICBkPSJtMjY4MCA3MTk5IDEyIDE1IDEyIDE0IDEzIDE0IDE0IDEyIDE0IDEzIDE0IDExIDE1IDExIDE2IDExIDE1IDkgMTcgOSAxNiA5IDE3IDcgMTggNyAxNyA3IDE4IDUgMTggNSAxOCA1IDE5IDMgMTggNCAxOSAyIDE4IDIgMTkgMWgxOSAxOWwxOC0xIDE5LTIgMTgtMiAxOS0zIDE4LTQgMTgtNCAxOC01IDE3LTZoLTFsLTItMWgtM2wtNC0xLTMtMS01LTItNS0xLTYtMi02LTEtNi0yLTctMi04LTItOC0yLTgtMy05LTItOS0yLTE5LTYtMjAtNS0yMi02LTIyLTYtMjMtNi0yMy03LTQ4LTEzLTQ4LTEzLTIzLTctMjMtNi0yMy02LTIxLTYtMjEtNi0xOS02LTktMi05LTMtOS0yLTgtMi04LTItNy0yLTctMi02LTItNi0yLTUtMi01LTEtNS0xLTMtMS0zLTEtMy0xaC0yeiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnODYiCiAgICAgICAgICAgIHN0eWxlPSJmaWxsOm5vbmUiCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoODgiCiAgICAgICAgICAgICAgZD0ibTI2ODAgNzE5OWMxMjEgMTYzIDM1NSAyMTMgNTQwIDE0OS0yNS02LTUwNy0xMzktNTQwLTE0OXoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzkwIgogICAgICAgICAgICBzdHlsZT0ic3Ryb2tlOiNmZmZmZmY7ZmlsbC1vcGFjaXR5Oi40MzkyMjtzdHJva2Utb3BhY2l0eTouNDM5MjI7ZmlsbDojZmNmY2ZjIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDkyIgogICAgICAgICAgICAgIGQ9Im0yMzU2IDMyNTktOTIgMTMtMTcgMjE3MyAxNDI1IDM5OSAzNS0xMTYtMTM2OS0zNjUgMTgtMjEwNHoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzk2IgogICAgICAgICAgICBzdHlsZT0iZmlsbDpub25lIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDk4IgogICAgICAgICAgICAgIGQ9Im0yMzU2IDMyNTktOTIgMTMtMTcgMjE3MyAxNDI1IDM5OSAzNS0xMTYtMTM2OS0zNjUgMTgtMjEwNHoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzEwMCIKICAgICAgICAgICAgc3R5bGU9InN0cm9rZTojZmZmZmZmO2ZpbGwtb3BhY2l0eTouMjMxMzc7c3Ryb2tlLW9wYWNpdHk6LjIzMTM3O2ZpbGw6IzAwMDAwMCIKICAgICAgICAgID4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgaWQ9InBhdGgxMDIiCiAgICAgICAgICAgICAgZD0ibTM3MDAgNDA2NS0yIDYxLTEzNDAtMzg2IDEzNDIgMzI1eiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnMTA2IgogICAgICAgICAgICBzdHlsZT0ic3Ryb2tlOiNmZmZmZmY7ZmlsbC1vcGFjaXR5Oi4yMzEzNztzdHJva2Utb3BhY2l0eTouMjMxMzc7ZmlsbDojMDAwMDAwIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDEwOCIKICAgICAgICAgICAgICBkPSJtMzcyMCA0NTA3LTMgNjEtMTMzOS0zODYgMTM0MiAzMjV6IgogICAgICAgICAgLz4KICAgICAgICA8L2cKICAgICAgICA+CiAgICAgICAgPGcKICAgICAgICAgICAgaWQ9ImcxMTIiCiAgICAgICAgICAgIHN0eWxlPSJzdHJva2U6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6LjIzMTM3O3N0cm9rZS1vcGFjaXR5Oi4yMzEzNztmaWxsOiMwMDAwMDAiCiAgICAgICAgICA+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgIGlkPSJwYXRoMTE0IgogICAgICAgICAgICAgIGQ9Im0zNzAwIDQ5MTYtMiA2MS0xMzQwLTM4NiAxMzQyIDMyNXoiCiAgICAgICAgICAvPgogICAgICAgIDwvZwogICAgICAgID4KICAgICAgICA8ZwogICAgICAgICAgICBpZD0iZzExOCIKICAgICAgICAgICAgc3R5bGU9InN0cm9rZTojZmZmZmZmO2ZpbGwtb3BhY2l0eTouMjMxMzc7c3Ryb2tlLW9wYWNpdHk6LjIzMTM3O2ZpbGw6IzAwMDAwMCIKICAgICAgICAgID4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgaWQ9InBhdGgxMjAiCiAgICAgICAgICAgICAgZD0ibTM3MDAgNTMxOC0yIDYxLTEzNDAtMzg3IDEzNDIgMzI2eiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICAgIDxnCiAgICAgICAgICAgIGlkPSJnMTI0IgogICAgICAgICAgICBzdHlsZT0iZmlsbDojOWU5ZTllIgogICAgICAgICAgPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICBpZD0icGF0aDEyNiIKICAgICAgICAgICAgICBkPSJtMjM2MSAzMzA5IDEzMzIgMzMzLTExIDIxOTdoMTEgMTFsMTEtMjIwNXYtOWwtOC0yLTEzNDEtMzM1LTIgMTEtMyAxMHptMTM0MCAzMzUtOC0ydi04aDExbC0zIDEweiIKICAgICAgICAgIC8+CiAgICAgICAgPC9nCiAgICAgICAgPgogICAgICA8L2cKICAgICAgPgogICAgPC9nCiAgICA+CiAgPC9nCiAgPgogIDxtZXRhZGF0YQogICAgPgogICAgPHJkZjpSREYKICAgICAgPgogICAgICA8Y2M6V29yawogICAgICAgID4KICAgICAgICA8ZGM6Zm9ybWF0CiAgICAgICAgICA+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0CiAgICAgICAgPgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiCiAgICAgICAgLz4KICAgICAgICA8Y2M6bGljZW5zZQogICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL3B1YmxpY2RvbWFpbi8iCiAgICAgICAgLz4KICAgICAgICA8ZGM6cHVibGlzaGVyCiAgICAgICAgICA+CiAgICAgICAgICA8Y2M6QWdlbnQKICAgICAgICAgICAgICByZGY6YWJvdXQ9Imh0dHA6Ly9vcGVuY2xpcGFydC5vcmcvIgogICAgICAgICAgICA+CiAgICAgICAgICAgIDxkYzp0aXRsZQogICAgICAgICAgICAgID5PcGVuY2xpcGFydDwvZGM6dGl0bGUKICAgICAgICAgICAgPgogICAgICAgICAgPC9jYzpBZ2VudAogICAgICAgICAgPgogICAgICAgIDwvZGM6cHVibGlzaGVyCiAgICAgICAgPgogICAgICAgIDxkYzp0aXRsZQogICAgICAgICAgPlNlcnZlcjwvZGM6dGl0bGUKICAgICAgICA+CiAgICAgICAgPGRjOmRhdGUKICAgICAgICAgID4yMDExLTA4LTEyVDIwOjQxOjE0PC9kYzpkYXRlCiAgICAgICAgPgogICAgICAgIDxkYzpkZXNjcmlwdGlvbgogICAgICAgICAgPkNsaXBhcnQgZm9yIGNvbXB1dGVyIGFuZCBuZXR3b3JrIGRpYWdyYW1zPC9kYzpkZXNjcmlwdGlvbgogICAgICAgID4KICAgICAgICA8ZGM6c291cmNlCiAgICAgICAgICA+aHR0cHM6Ly9vcGVuY2xpcGFydC5vcmcvZGV0YWlsLzE1NTEwMS9zZXJ2ZXItYnktc2Fpc3l1a3VzYW5hZ2k8L2RjOnNvdXJjZQogICAgICAgID4KICAgICAgICA8ZGM6Y3JlYXRvcgogICAgICAgICAgPgogICAgICAgICAgPGNjOkFnZW50CiAgICAgICAgICAgID4KICAgICAgICAgICAgPGRjOnRpdGxlCiAgICAgICAgICAgICAgPnNhaXN5dWt1c2FuYWdpPC9kYzp0aXRsZQogICAgICAgICAgICA+CiAgICAgICAgICA8L2NjOkFnZW50CiAgICAgICAgICA+CiAgICAgICAgPC9kYzpjcmVhdG9yCiAgICAgICAgPgogICAgICAgIDxkYzpzdWJqZWN0CiAgICAgICAgICA+CiAgICAgICAgICA8cmRmOkJhZwogICAgICAgICAgICA+CiAgICAgICAgICAgIDxyZGY6bGkKICAgICAgICAgICAgICA+Q29tcHV0ZXI8L3JkZjpsaQogICAgICAgICAgICA+CiAgICAgICAgICAgIDxyZGY6bGkKICAgICAgICAgICAgICA+TmV0d29yazwvcmRmOmxpCiAgICAgICAgICAgID4KICAgICAgICAgICAgPHJkZjpsaQogICAgICAgICAgICAgID5TZXJ2ZXI8L3JkZjpsaQogICAgICAgICAgICA+CiAgICAgICAgICA8L3JkZjpCYWcKICAgICAgICAgID4KICAgICAgICA8L2RjOnN1YmplY3QKICAgICAgICA+CiAgICAgIDwvY2M6V29yawogICAgICA+CiAgICAgIDxjYzpMaWNlbnNlCiAgICAgICAgICByZGY6YWJvdXQ9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL3B1YmxpY2RvbWFpbi8iCiAgICAgICAgPgogICAgICAgIDxjYzpwZXJtaXRzCiAgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjUmVwcm9kdWN0aW9uIgogICAgICAgIC8+CiAgICAgICAgPGNjOnBlcm1pdHMKICAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyNEaXN0cmlidXRpb24iCiAgICAgICAgLz4KICAgICAgICA8Y2M6cGVybWl0cwogICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zI0Rlcml2YXRpdmVXb3JrcyIKICAgICAgICAvPgogICAgICA8L2NjOkxpY2Vuc2UKICAgICAgPgogICAgPC9yZGY6UkRGCiAgICA+CiAgPC9tZXRhZGF0YQogID4KPC9zdmcKPg=='; + +const REQ_METHOD_LIST = [ + 'GET', + 'HEAD', + 'OPTIONS', + 'TRACE', + 'PUT', + 'DELETE', + 'POST', + 'PATCH', + 'CONNECT' +]; + +/** + * omni: Blocks to provide the front-end for OmniBlocks server support. + * @constructor + */ +class Server { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this.renderer = this.runtime.renderer; + + this.runtime.on(Runtime.SERVER_REQUEST, (page, ip, method, headers, data, id) => { + this.request = { + id, + page, + ip, + method, + headers, + data + }; + + const startedThreads = runtime.startHats('server_whenPageIsRequested'); + const threadStatuses = startedThreads.map(thread => thread.status); + + // If all threads are done immediately after the hat was started, that likely + // means there is no handling for that particular page; and because of that, + // we treat it as if the page was not found. + if (threadStatuses.every(status => (status === Thread.STATUS_DONE))) { + runtime.startHats('server_whenPageIsNotFound'); + } + }); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'server', + name: 'Web Server', + color1: '#7000d9', + color2: '#5400a3', + color3: '#39006e', + menuIconURI: iconURI, + blockIconURI: iconURI, + blocks: [ + { + opcode: 'whenPageIsRequested', + text: formatMessage({ + id: 'omni_server.blocks.whenPageIsRequested', + default: 'when page [PAGE] is requested', + description: 'Hat that executes the the code under it when a certain page is requested.' + }), + blockType: BlockType.HAT, + arguments: { + PAGE: { + type: ArgumentType.STRING, + defaultValue: '/' + } + }, + isEdgeActivated: false + }, + { + opcode: 'whenPageIsNotFound', + text: formatMessage({ + id: 'omni_server.blocks.whenPageIsNotFound', + default: 'when page is not found', + description: 'Hat that executes the the code under it when a certain page is not fouund.' + }), + blockType: BlockType.HAT, + isEdgeActivated: false + }, + '---', + { + opcode: 'returnContent', + text: formatMessage({ + id: 'omni_server.blocks.returnContent', + // eslint-disable-next-line max-len + default: 'return content [CONTENT] as [MIME] with the status [STATUS] and headers [EXTRA_HEADERS]', + description: 'Hat that executes the the code under it when a certain page is requested.' + }), + blockType: BlockType.COMMAND, + isTerminal: true, + arguments: { + CONTENT: { + type: ArgumentType.STRING, + defaultValue: 'Hello OmniBlocks!' + }, + MIME: { + type: ArgumentType.STRING, + defaultValue: 'text/plain', + menu: 'MIME_MENU' + }, + STATUS: { + type: ArgumentType.NUMBER, + defaultValue: '200' + }, + EXTRA_HEADERS: { + type: ArgumentType.STRING, + defaultValue: '{}' + } + }, + hideFromPalette: true // Hidden because it is a legacy block. + }, + { + opcode: 'returnRequest', + text: formatMessage({ + id: 'omni_server.blocks.returnRequest', + default: 'return content [CONTENT]', + description: 'Block that sends the requested HTTP response.' + }), + blockType: BlockType.COMMAND, + isTerminal: true, + arguments: { + CONTENT: { + type: ArgumentType.STRING, + defaultValue: 'Hello OmniBlocks!' + } + } + }, + { + opcode: 'setMime', + text: formatMessage({ + id: 'omni_server.blocks.setMime', + default: 'set format to [MIME]', + description: 'Block that sets the sent MIME (a.k.a. format) to a MIME-type.' + }), + blockType: BlockType.COMMAND, + arguments: { + MIME: { + type: ArgumentType.STRING, + defaultValue: 'text/plain', + menu: 'MIME_MENU' + } + } + }, + { + opcode: 'setStatus', + text: formatMessage({ + id: 'omni_server.blocks.setStatus', + default: 'set status to [STATUS]', + description: 'Block that sets a HTTP status.' + }), + blockType: BlockType.COMMAND, + arguments: { + STATUS: { + type: ArgumentType.NUMBER, + defaultValue: '200' + } + } + }, + { + opcode: 'setHeaders', + text: formatMessage({ + id: 'omni_server.blocks.setHeaders', + default: 'set headers to [EXTRA_HEADERS]', + description: 'Block that sets HTTP headers.' + }), + blockType: BlockType.COMMAND, + arguments: { + EXTRA_HEADERS: { + type: ArgumentType.STRING, + defaultValue: '{}' + } + } + }, + '---', + { + opcode: 'page', + text: formatMessage({ + id: 'omni_server.blocks.page', + default: 'page', + description: 'Block that returns the requested page URL.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'ipAddress', + text: formatMessage({ + id: 'omni_server.blocks.ipAddress', + default: 'ip address', + description: 'Block that returns the IP Address from the request.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'method', + text: formatMessage({ + id: 'omni_server.blocks.method', + default: 'request method', + description: 'Block that returns the request method.' + }), + blockType: BlockType.REPORTER, + hideFromPalette: true // Hidden because it is a legacy block. + }, + { + opcode: 'headers', + text: formatMessage({ + id: 'omni_server.blocks.headers', + default: 'request headers', + description: 'Block that returns the request headers.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true + }, + { + opcode: 'data', + text: formatMessage({ + id: 'omni_server.blocks.data', + default: 'request data', + description: 'Block that returns the request data.' + }), + blockType: BlockType.REPORTER, + disableMonitor: true + }, + '---', + { + opcode: 'checkMethod', + text: formatMessage({ + id: 'omni_server.blocks.checkMethod', + default: 'request method is [REQ_METHOD]?', + description: 'Block that checks the request method is equal to the selected request method.' + }), + blockType: BlockType.BOOLEAN, + arguments: { + REQ_METHOD: { + type: ArgumentType.STRING, + defaultValue: 'GET', + menu: 'REQ_METHOD_MENU' + } + } + }, + '---', + { + opcode: 'readFile', + text: formatMessage({ + id: 'omni_server.blocks.readFile', + default: 'read file from [PATH]', + description: 'Block that reads a file.' + }), + blockType: BlockType.REPORTER, + arguments: { + PATH: { + type: ArgumentType.STRING, + defaultValue: '/home/user/apple.banana' + } + } + }, + { + opcode: 'writeFile', + text: formatMessage({ + id: 'omni_server.blocks.writeFile', + default: 'write [CONTENT] to [PATH]', + description: 'Block that writes content to a file.' + }), + blockType: BlockType.COMMAND, + arguments: { + PATH: { + type: ArgumentType.STRING, + defaultValue: '/home/user/apple.banana' + }, + CONTENT: { + type: ArgumentType.STRING, + defaultValue: 'apple' + } + } + }, + '---', + { + opcode: 'executeJS', + text: formatMessage({ + id: 'omni_appmaker.blocks.executeJS', + default: 'execute JavaScript [JS]', + description: 'Block that executes JavaScript' + }), + blockType: BlockType.COMMAND, + arguments: { + JS: { + type: ArgumentType.STRING, + defaultValue: 'alert("Hello!");' + } + } + }, + { + opcode: 'executeJSReporter', + text: formatMessage({ + id: 'omni_appmaker.blocks.executeJSReporter', + default: 'execute JavaScript [JS]', + description: 'Block that executes JavaScript' + }), + blockType: BlockType.UNIVERSAL, + arguments: { + JS: { + type: ArgumentType.STRING, + defaultValue: 'return true;' + } + } + } + ], + menus: { + MIME_MENU: { + items: [ + 'text/plain', + 'text/html', + 'text/css', + 'text/javascript', + 'application/xml', + 'application/json' + ], + acceptReporters: true + }, + REQ_METHOD_MENU: { + items: REQ_METHOD_LIST + } + } + }; + } + + + whenPageIsRequested ({PAGE}, util) { + const thread = util.thread; + if (PAGE === this.request?.page) { + thread.serverRequest = this.request; + thread.serverResponse.status = 200; + this.request = null; + return true; + } + return false; + } + + whenPageIsNotFound (args, util) { + const thread = util.thread; + thread.serverRequest = this.request; + thread.serverResponse.status = 404; + this.request = null; + return true; + } + + + returnContent ({CONTENT, MIME, STATUS, EXTRA_HEADERS}, util) { + const thread = util.thread; + if (!thread.serverRequest) return; + console.log(CONTENT); + this.runtime.emit(Runtime.SERVER_RESPONSE, CONTENT, MIME, STATUS, EXTRA_HEADERS, thread.serverRequest.id); + } + + returnRequest ({CONTENT}, util) { + const thread = util.thread; + if (!thread.serverRequest) return; + this.runtime.emit( + Runtime.SERVER_RESPONSE, + Cast.toString(CONTENT), + Cast.toString(thread.serverResponse.mime), + Cast.toNumber(thread.serverResponse.status), + Cast.toString(thread.serverResponse.headers), + thread.serverRequest.id + ); + thread.stopThisScript(); + } + + setMime ({MIME}, util) { + const thread = util.thread; + thread.serverResponse.mime = MIME; + } + + setStatus ({STATUS}, util) { + const thread = util.thread; + thread.serverResponse.status = STATUS; + } + + setHeaders ({EXTRA_HEADERS}, util) { + const thread = util.thread; + thread.serverResponse.headers = EXTRA_HEADERS; + } + + ipAddress (args, util) { + const thread = util.thread; + return thread.serverRequest.ip; + } + + method (args, util) { + const thread = util.thread; + return thread.serverRequest.method; + } + + checkMethod ({REQ_METHOD}, util) { + const thread = util.thread; + if (!REQ_METHOD_LIST.includes(REQ_METHOD)) return false; + return thread.serverRequest.method === REQ_METHOD; + } + + page (args, util) { + const thread = util.thread; + return thread.serverRequest.page; + } + + headers (args, util) { + const thread = util.thread; + return thread.serverRequest.headers; + } + + data (args, util) { + const thread = util.thread; + return thread.serverRequest.data; + } + + executeJS (args) { + if (this.runtime.isPackaged) { + new Function(args.JS)(); + } + } + executeJSReporter (args) { + if (this.runtime.isPackaged) { + return new Function(args.JS)(); + } + } + + readFile ({PATH}) { + // Bail out if not privileged. + if (!this.runtime.isPrivileged) return ''; + return this.runtime.privilegedUtils.readFile(PATH); + } + + async writeFile ({PATH, CONTENT}) { + // Bail out if not privileged. + if (!this.runtime.isPrivileged) return; + await this.runtime.privilegedUtils.writeFile(PATH, CONTENT); + } +} + +module.exports = Server; diff --git a/src/server/cli.js b/src/server/cli.js new file mode 100644 index 00000000000..c1466505837 --- /dev/null +++ b/src/server/cli.js @@ -0,0 +1,93 @@ +/* eslint-env node */ +/* eslint-disable no-console */ + +(async () => { + const fs = require('node:fs'); + const os = require('node:os'); + + const Server = require('./server'); + const setupFileSecurity = require('./setup-file-security'); + + const {resolvePath} = require('./resolve-path'); + + const {default: yargs} = await import('yargs'); + const {hideBin} = await import('yargs/helpers'); + + const permissions = { + fileReadAccess: false, + fileWriteAccess: false, + fileScope: [os.homedir()], + networkAccess: false + }; + + yargs(hideBin(process.argv)) + .command( + 'serve [file] [port]', + 'Runs the project in server mode', + yarg => ( + yarg + .positional('file', { + type: 'string', + describe: 'The file to run' + }) + .positional('port', { + type: 'number', + describe: 'The port to bind on', + default: 8080 + }) + .option('dev', { + alias: 'D', + type: 'boolean', + description: 'Runs with the ability to hot-swap projects' + }) + .option('allow-file-read', { + alias: 'D', + type: 'boolean', + description: 'Allows the project to read any file in your home folder' + }) + .option('allow-file-write', { + alias: 'D', + type: 'boolean', + description: 'Allows the project to write to any file in your home folder' + }) + .option('allow-network-access', { + alias: 'D', + type: 'boolean', + description: 'Allows the project to access anything on the network' + }) + .option('file-scope', { + type: 'array', + description: 'Allows the project to read from the specified folders only' + }) + ), argv => { + if (!argv.file) { + console.log('No project inputted.'); + process.exitCode = 1; + return; + } + + if (argv.allowFileRead) permissions.fileReadAccess = true; + if (argv.allowFileWrite) permissions.fileWriteAccess = true; + if (argv.networkAccess) permissions.networkAccess = true; + if (argv.allowNonHomeRead) permissions.nonHomeReadAccess = true; + if (argv.allowNonHomeWrite) permissions.nonHomeWriteAccess = true; + + if (argv.fileScope) { + permissions.fileScope = argv.fileScope.map(location => resolvePath(location)); + } + + const server = new Server(!!argv.dev, argv.port); + setupFileSecurity(server.securityManager, permissions); + + server.runProject( + fs.readFileSync(resolvePath(argv.file)) + ).catch(() => { + console.log('Failed to load the project. :('); + server.halt(); + process.exitCode = 2; + return; + }); + }) + .demandCommand() + .parse(); +})(); diff --git a/src/server/resolve-path.js b/src/server/resolve-path.js new file mode 100644 index 00000000000..c42ac7db4c8 --- /dev/null +++ b/src/server/resolve-path.js @@ -0,0 +1,43 @@ +/* eslint-env node */ + +const path = require('node:path'); +const os = require('node:os'); + +/** + * omni: A curried function that returns a custom path resolver, based on the inputted values. + * @param {() => string} homeDir The home directory. + * @param {() => string} workingDir The working directory. + * @returns {(location: string) => string} The path resolver. + */ +const makePathResolver = (homeDir, workingDir) => { + if (typeof homeDir !== 'function') throw new TypeError('"homeDir" must be a function.'); + if (typeof workingDir !== 'function') throw new TypeError('"workingDir" must be a function.'); + + /** + * omni: A parser that normalizes a path and converts relative paths to absolute paths, + * based on the inputted values. + * @param {string} location An absolute or relative path. + * @returns {string} An normalized absolute path. + */ + return location => { + if (typeof location !== 'string') throw new TypeError('"location" must be a string.'); + const normalizedPath = path.normalize(location); + + if (location.startsWith('~')) return path.join(homeDir(), normalizedPath.slice(1)); + if (location.startsWith('/')) return normalizedPath; + return path.join(workingDir(), normalizedPath); + }; +}; + +/** + * omni: A parser that normalizes a path and converts relative paths to absolute paths, + * based on the current working directory. + * @param {string} location An absolute or relative path. + * @returns {string} An normalized absolute path. + */ +const resolvePath = makePathResolver(os.homedir, process.cwd); + +module.exports = { + makePathResolver, + resolvePath +}; diff --git a/src/server/server.js b/src/server/server.js new file mode 100644 index 00000000000..c83aeb770bf --- /dev/null +++ b/src/server/server.js @@ -0,0 +1,173 @@ +/* eslint-env node */ +/* eslint-disable no-console */ + +const VirtualMachine = require('../index'); +const Runtime = require('../engine/runtime'); + +const fs = require('node:fs'); +const http = require('node:http'); +const crypto = require('node:crypto'); + +const initStorage = require('./storage'); + +const {resolvePath} = require('./resolve-path'); + +const {JSDOM} = require('jsdom'); + +/** + * omni: This is a privileged client for running OmniBlocks as a web server. + * It starts a HTTP server that interfaces with the project running in the VM, + * via the Web Server extension. + * @param {boolean} dev If true, runs the server in developer mode. + * @param {number} port The port that the server listens on. + * @constructor + */ +class Server { + constructor (dev, port) { + if (typeof dev !== 'boolean') throw new TypeError('"dev" must be a boolean.'); + if (typeof port !== 'number') throw new TypeError('"port" must be a number.'); + + this.dev = dev; + this.port = port; + + // For extension compatibility, mock browser APIs. + global.window = new JSDOM('').window; + global.document = window.document; + + /* eslint-disable no-unused-vars */ + global.confirm = (...ignored) => true; + global.alert = (ignored, ...ignored2) => console.log(ignored); + global.prompt = (...ignored) => ''; + /* eslint-enable no-unused-vars */ + + this.http = http.createServer(this.httpServer.bind(this)); + this.resMap = new Map(); + + this.vm = new VirtualMachine(); + this.vm.convertToPackagedRuntime(); + this.vm.attachStorage(initStorage()); + this.vm.runtime.isPrivileged = true; + + this.securityManager = this.vm.runtime.extensionManager.securityManager; + + this.vm.runtime.on('SAY', (target, type, text) => { + console.log(text); + }); + + this.vm.runtime.on(Runtime.SERVER_RESPONSE, (content, mime, status, extraHeaders, requestId) => { + const res = this.resMap.get(requestId); + if (typeof res === 'undefined') return; + res.writeHead(status, { + 'Content-Type': mime, + ...JSON.parse(extraHeaders) + }); + res.end(String(content)); + this.resMap.delete(requestId); + }); + + this.vm.securityManager.getSandboxMode = () => Promise.resolve('unsandboxed'); + this.vm.securityManager.canFetch = () => Promise.resolve(false); + this.vm.securityManager.canLoadExtensionFromProject = () => Promise.resolve(false); + + // These are not possible in this enviroment. + this.vm.securityManager.canOpenWindow = () => Promise.resolve(false); + this.vm.securityManager.canRedirect = () => Promise.resolve(false); + + this.vm.runtime.privilegedUtils.readFile = async path => { + const resolvedPath = resolvePath(path); + if (!await this.securityManager.canReadFile(resolvedPath)) return ''; + try { + return fs.readFileSync(resolvedPath, 'utf8'); + } catch (err) { + return ''; + } + }; + + this.vm.runtime.privilegedUtils.writeFile = async (path, content) => { + const resolvedPath = resolvePath(path); + if (!await this.securityManager.canWriteFile(resolvedPath)) return; + try { + fs.writeFileSync(resolvedPath, String(content)); + } catch (err) { + // Empty on purpose. + // omni: TODO: Maybe add some form of error handling? + } + }; + + this.vm.setCompatibilityMode(false); + this.vm.setTurboMode(true); + this.vm.clear(); + + this.http.listen(this.port, () => { + console.log(`OmniBlocks on server has started at port ${port}.`); + }); + } + + /** + * Internally used for the HTTP server. + * @private + */ + httpServer (req, res) { + const dataRaw = []; + + req.on('data', chunk => { + dataRaw.push(chunk); + }); + req.on('end', async () => { + const dataBuffer = Buffer.concat(dataRaw); + const dataString = String(dataBuffer); + + if (this.dev && req.url === '/_lk_devServer_updateLb') { + if (!('origin' in req.headers)) return; + + const isEditor = req.headers.origin === 'http://localhost:8601' || + req.headers.origin.endsWith('omniblocks.github.io'); + if (!isEditor) return; + + await this.runProject(dataBuffer).catch(err => { + throw new Error(err); + }); + + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'access-control-allow-origin': req.headers.origin + }); + return res.end('success'); + } + + const requestId = crypto.randomUUID(); + this.resMap.set(requestId, res); + this.vm.runtime.emit( + Runtime.SERVER_REQUEST, + req.url, + req.socket.remoteAddress, + req.method, + JSON.stringify(req.headers), + dataString, + requestId + ); + }); + } + + /** + * Stops the HTTP server and turns off the VM, effectively disabling the server. + */ + halt () { + this.http.close(); + this.http.closeAllConnections(); + this.vm.quit(); + } + + /** + * Load a Scratch project from a .sb, .sb2, .sb3 or json string. + * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load. + */ + async runProject (input) { + this.vm.clear(); + await this.vm.loadProject(input); + this.vm.start(); + this.vm.greenFlag(); + } +} + +module.exports = Server; diff --git a/src/server/setup-file-security.js b/src/server/setup-file-security.js new file mode 100644 index 00000000000..2745f44e7bb --- /dev/null +++ b/src/server/setup-file-security.js @@ -0,0 +1,129 @@ +/* eslint-env node */ +/* eslint-disable no-console */ + +const awaitEvent = require('../util/await-event'); +const {resolvePath} = require('./resolve-path'); + +const setupFileSecurity = (securityManager, permissions) => { + const canAccessFolder = fileLocation => { + if (typeof fileLocation !== 'string') throw new TypeError('"fileLocation" must be a string.'); + const location = resolvePath(fileLocation); + + for (let i = 0; i < permissions.fileScope.length; i++) { + const folder = permissions.fileScope[i]; + const escapedFolder = folder.endsWith('/') ? folder : `${folder}/`; + if (location.startsWith(escapedFolder)) return true; + } + + return false; + }; + + /* eslint-disable-next-line prefer-template */ + const warn = (message, last) => process.stdout.write('\x1b[93m' + message + '\x1b[0m' + (last ? '' : '\n')); + + // FILE ACCESS + + securityManager.canReadFile = async function (fileLocation) { + if (!permissions.fileReadAccess) { + if (!process.stdout.isTTY) return false; + + /* eslint-disable max-len */ + warn('This project wants read access to your filesystem. Allowing read access will mean the project will be able to read ANY file you can.'); + warn('This includes personal documents and files, app settings, passwords saved in the browser, browser cookies, and more.'); + warn('If you don\'t trust this project, or you are not sure, you should not give permission.'); + warn('Are you sure you want to allow filesystem read access? (Y/N)', true); + /* eslint-enable max-len */ + + process.stdin.setRawMode(true); + const key = (await awaitEvent(process.stdin, 'data'))[0]; + process.stdin.setRawMode(false); + process.stdout.write(` ${String(key)}\n`); + + if (String(key).toLowerCase() !== 'y') return false; + + if (!permissions.fileReadAccess) permissions.fileReadAccess = true; + } + + if (!canAccessFolder(fileLocation)) { + /* eslint-disable max-len */ + warn('The project attemped to read a file outside of the allowed file scope. The read has been prevented.'); + warn('If the project needs to read a file outside the file scope, append "--file-scope /path/to/folder /add/more/folders/if/you/want --" to the command.'); + warn('You should not let the folder read outside of the home folder, unless it is absolutely necessary.'); + /* eslint-enable max-len */ + return false; + } + + return true; + }; + + securityManager.canWriteFile = async function (fileLocation) { + if (!permissions.fileWriteAccess) { + if (!process.stdout.isTTY) return false; + + /* eslint-disable max-len */ + warn('This project wants write access to your filesystem. Allowing read access will mean the project will be able to write to and replace ANY file you can.'); + warn('This includes personal documents and files, program settings, and more.'); + warn('If you don\'t trust this project, or you are not sure, you should not give permission.'); + warn('Are you sure you want to allow filesystem write access? (Y/N)', true); + /* eslint-enable max-len */ + + process.stdin.setRawMode(true); + const key = (await awaitEvent(process.stdin, 'data'))[0]; + process.stdin.setRawMode(false); + process.stdout.write(` ${String(key)}\n`); + + if (String(key).toLowerCase() !== 'y') return false; + + if (!permissions.fileReadAccess) permissions.fileWriteAccess = true; + } + + if (!canAccessFolder(fileLocation)) { + /* eslint-disable max-len */ + warn('The project attemped to write to a file outside of the allowed file scope. The write has been prevented.'); + warn('If the project needs to write to a file outside the file scope, append "--file-scope /path/to/folder /add/more/folders/if/you/want --" to the command.'); + warn('You should not let the folder write outside of the home folder, unless it is absolutely necessary.'); + /* eslint-enable max-len */ + return false; + } + + return true; + }; + + // NETWORK ACCESS + + securityManager.canFetch = async function () { + if (!permissions.networkAccess) { + if (!process.stdout.isTTY) return false; + + /* eslint-disable max-len */ + warn('This project wants network access. Allowing network access will mean the project will be able to access ANY website on the internet and ANY website on your local network.'); + warn('This includes websites, your router, your intranet, and more.'); + warn('If you don\'t trust this project, or you are not sure, you should not give permission.'); + warn('Are you sure you want to allow network access? (Y/N)', true); + /* eslint-enable max-len */ + + process.stdin.setRawMode(true); + const key = (await awaitEvent(process.stdin, 'data'))[0]; + process.stdin.setRawMode(false); + process.stdout.write(` ${String(key)}\n`); + + if (String(key).toLowerCase() !== 'y') return false; + + if (!permissions.networkAccess) permissions.networkAccess = true; + } + + return true; + }; + + securityManager.canLoadExtensionFromProject = function (url) { + // Allow trusted hosts. + if (url.startsWith('https://extensions.turbowarp.org/')) return Promise.resolve(true); + + /* eslint-disable-next-line max-len */ + warn('This project attempted to load an extension from an untrusted host. For security reasons, the extension will not be loaded.'); + + return Promise.resolve(false); + }; +}; + +module.exports = setupFileSecurity; diff --git a/src/server/storage.js b/src/server/storage.js new file mode 100644 index 00000000000..0a55ec1e498 --- /dev/null +++ b/src/server/storage.js @@ -0,0 +1,47 @@ +const ScratchStorage = require('scratch-storage'); + +const ASSET_SERVER = 'http://invalid/'; +const PROJECT_SERVER = 'https://invalid/'; + +/** + * @param {Asset} asset - calculate a URL for this asset. + * @returns {string} a URL to download a project file. + */ +const getProjectUrl = function (asset) { + const assetIdParts = asset.assetId.split('.'); + const assetUrlParts = [PROJECT_SERVER, 'internalapi/project/', assetIdParts[0], '/get/']; + if (assetIdParts[1]) { + assetUrlParts.push(assetIdParts[1]); + } + return assetUrlParts.join(''); +}; + +/** + * @param {Asset} asset - calculate a URL for this asset. + * @returns {string} a URL to download a project asset (PNG, WAV, etc.) + */ +const getAssetUrl = function (asset) { + const assetUrlParts = [ + ASSET_SERVER, + 'internalapi/asset/', + asset.assetId, + '.', + asset.dataFormat, + '/get/' + ]; + return assetUrlParts.join(''); +}; + +/** + * Construct a new instance of ScratchStorage and provide it with invalid web sources. + * @returns {ScratchStorage} - an instance of ScratchStorage, to be used locally. + */ +const initStorage = function () { + const storage = new ScratchStorage(); + const AssetType = storage.AssetType; + storage.addWebStore([AssetType.Project], getProjectUrl); + storage.addWebStore([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl); + return storage; +}; + +module.exports = initStorage; diff --git a/src/util/await-event.js b/src/util/await-event.js new file mode 100644 index 00000000000..6c9aae2e57a --- /dev/null +++ b/src/util/await-event.js @@ -0,0 +1,20 @@ +const {EventEmitter} = require('events'); + +/** + * omni: Await an EventEmitter. + * @param {EventEmitter} event - An EventEmitter. + * @param {string} eventName - The event you want to listen for. + * @returns {Promise} The data transmitted over the listener. + */ +const awaitEvent = (event, eventName) => new Promise((resolve, reject) => { + if (!(event instanceof EventEmitter)) reject(new TypeError('"event" must be an instance of EventEmitter.')); + if (typeof eventName !== 'string') reject(new TypeError('"eventName" must be a string.')); + + const listener = (...data) => { + event.removeListener(eventName, listener); + resolve(data); + }; + event.on(eventName, listener); +}); + +module.exports = awaitEvent; From ca72abbd6ff3baddf59a81ea64e06a4aa81c61b8 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:12:23 +1100 Subject: [PATCH 02/21] refactor: correct according to reviews and nitpicks --- .../tw-unsandboxed-extension-runner.js | 2 ++ src/extensions/omni_server/index.js | 9 +++------ src/server/cli.js | 15 +++++---------- src/server/server.js | 16 +++++++++++----- src/server/setup-file-security.js | 2 +- src/util/await-event.js | 4 ++-- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/extension-support/tw-unsandboxed-extension-runner.js b/src/extension-support/tw-unsandboxed-extension-runner.js index 0ad1a478c7d..f925c8555d3 100644 --- a/src/extension-support/tw-unsandboxed-extension-runner.js +++ b/src/extension-support/tw-unsandboxed-extension-runner.js @@ -178,6 +178,8 @@ const teardownUnsandboxedExtensionAPI = () => { * @returns {Promise} Resolves with a list of extension objects if the extension was loaded successfully. */ const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => { + setupUnsandboxedExtensionAPI(vm).then(resolve); + if (typeof process === 'undefined') { const script = document.createElement('script'); script.onerror = () => { diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index fc7779885ec..12debd1455a 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -35,8 +35,6 @@ class Server { */ this.runtime = runtime; - this.renderer = this.runtime.renderer; - this.runtime.on(Runtime.SERVER_REQUEST, (page, ip, method, headers, data, id) => { this.request = { id, @@ -93,7 +91,7 @@ class Server { text: formatMessage({ id: 'omni_server.blocks.whenPageIsNotFound', default: 'when page is not found', - description: 'Hat that executes the the code under it when a certain page is not fouund.' + description: 'Hat that executes the the code under it when a certain page is not found.' }), blockType: BlockType.HAT, isEdgeActivated: false @@ -299,7 +297,7 @@ class Server { { opcode: 'executeJS', text: formatMessage({ - id: 'omni_appmaker.blocks.executeJS', + id: 'omni_server.blocks.executeJS', default: 'execute JavaScript [JS]', description: 'Block that executes JavaScript' }), @@ -314,7 +312,7 @@ class Server { { opcode: 'executeJSReporter', text: formatMessage({ - id: 'omni_appmaker.blocks.executeJSReporter', + id: 'omni_server.blocks.executeJSReporter', default: 'execute JavaScript [JS]', description: 'Block that executes JavaScript' }), @@ -370,7 +368,6 @@ class Server { returnContent ({CONTENT, MIME, STATUS, EXTRA_HEADERS}, util) { const thread = util.thread; if (!thread.serverRequest) return; - console.log(CONTENT); this.runtime.emit(Runtime.SERVER_RESPONSE, CONTENT, MIME, STATUS, EXTRA_HEADERS, thread.serverRequest.id); } diff --git a/src/server/cli.js b/src/server/cli.js index c1466505837..37aa444256e 100644 --- a/src/server/cli.js +++ b/src/server/cli.js @@ -41,17 +41,14 @@ description: 'Runs with the ability to hot-swap projects' }) .option('allow-file-read', { - alias: 'D', type: 'boolean', description: 'Allows the project to read any file in your home folder' }) .option('allow-file-write', { - alias: 'D', type: 'boolean', description: 'Allows the project to write to any file in your home folder' }) .option('allow-network-access', { - alias: 'D', type: 'boolean', description: 'Allows the project to access anything on the network' }) @@ -68,9 +65,7 @@ if (argv.allowFileRead) permissions.fileReadAccess = true; if (argv.allowFileWrite) permissions.fileWriteAccess = true; - if (argv.networkAccess) permissions.networkAccess = true; - if (argv.allowNonHomeRead) permissions.nonHomeReadAccess = true; - if (argv.allowNonHomeWrite) permissions.nonHomeWriteAccess = true; + if (argv.allowNetworkAccess) permissions.networkAccess = true; if (argv.fileScope) { permissions.fileScope = argv.fileScope.map(location => resolvePath(location)); @@ -79,14 +74,14 @@ const server = new Server(!!argv.dev, argv.port); setupFileSecurity(server.securityManager, permissions); - server.runProject( - fs.readFileSync(resolvePath(argv.file)) - ).catch(() => { + try { + server.runProject(fs.readFileSync(resolvePath(argv.file))); + } catch { console.log('Failed to load the project. :('); server.halt(); process.exitCode = 2; return; - }); + } }) .demandCommand() .parse(); diff --git a/src/server/server.js b/src/server/server.js index c83aeb770bf..1c1ac4fd320 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -57,10 +57,16 @@ class Server { this.vm.runtime.on(Runtime.SERVER_RESPONSE, (content, mime, status, extraHeaders, requestId) => { const res = this.resMap.get(requestId); if (typeof res === 'undefined') return; - res.writeHead(status, { + let parsedJSON; + try { + parsedJSON = JSON.parse(extraHeaders); + } catch { + parsedJSON = {}; + } + res.writeHead(status, Object.create(null, { 'Content-Type': mime, - ...JSON.parse(extraHeaders) - }); + ...parsedJSON + })); res.end(String(content)); this.resMap.delete(requestId); }); @@ -118,11 +124,11 @@ class Server { const dataString = String(dataBuffer); if (this.dev && req.url === '/_lk_devServer_updateLb') { - if (!('origin' in req.headers)) return; + if (!('origin' in req.headers)) return res.end('denied'); const isEditor = req.headers.origin === 'http://localhost:8601' || req.headers.origin.endsWith('omniblocks.github.io'); - if (!isEditor) return; + if (!isEditor) return res.end('denied'); await this.runProject(dataBuffer).catch(err => { throw new Error(err); diff --git a/src/server/setup-file-security.js b/src/server/setup-file-security.js index 2745f44e7bb..0a03c596668 100644 --- a/src/server/setup-file-security.js +++ b/src/server/setup-file-security.js @@ -74,7 +74,7 @@ const setupFileSecurity = (securityManager, permissions) => { if (String(key).toLowerCase() !== 'y') return false; - if (!permissions.fileReadAccess) permissions.fileWriteAccess = true; + if (!permissions.fileWriteAccess) permissions.fileWriteAccess = true; } if (!canAccessFolder(fileLocation)) { diff --git a/src/util/await-event.js b/src/util/await-event.js index 6c9aae2e57a..369290b75a5 100644 --- a/src/util/await-event.js +++ b/src/util/await-event.js @@ -7,8 +7,8 @@ const {EventEmitter} = require('events'); * @returns {Promise} The data transmitted over the listener. */ const awaitEvent = (event, eventName) => new Promise((resolve, reject) => { - if (!(event instanceof EventEmitter)) reject(new TypeError('"event" must be an instance of EventEmitter.')); - if (typeof eventName !== 'string') reject(new TypeError('"eventName" must be a string.')); + if (!(event instanceof EventEmitter)) return reject(new TypeError('"event" must be an instance of EventEmitter.')); + if (typeof eventName !== 'string') return reject(new TypeError('"eventName" must be a string.')); const listener = (...data) => { event.removeListener(eventName, listener); From c58b8da78590156cda6e4f102618210ee7c43882 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:14:10 +1100 Subject: [PATCH 03/21] fix: add line break before server warning text It makes it MUCH more readable. --- src/server/setup-file-security.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/setup-file-security.js b/src/server/setup-file-security.js index 0a03c596668..b0dd365d6d4 100644 --- a/src/server/setup-file-security.js +++ b/src/server/setup-file-security.js @@ -19,7 +19,7 @@ const setupFileSecurity = (securityManager, permissions) => { }; /* eslint-disable-next-line prefer-template */ - const warn = (message, last) => process.stdout.write('\x1b[93m' + message + '\x1b[0m' + (last ? '' : '\n')); + const warn = (message, last) => process.stdout.write('\n\x1b[93m' + message + '\x1b[0m' + (last ? '' : '\n')); // FILE ACCESS From bbfd831de41404430e6fc3f8f1aaa5800d5dbdc2 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:19:35 +1100 Subject: [PATCH 04/21] refactor: resolve more code review --- src/engine/thread.js | 21 +++++++++++++++ src/extensions/omni_server/index.js | 42 ----------------------------- src/server/cli.js | 10 ++++--- src/server/resolve-path.js | 2 +- src/server/server.js | 17 ++++++------ src/server/setup-file-security.js | 10 ++++--- 6 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/engine/thread.js b/src/engine/thread.js index 832edf5cd09..966aa5b7256 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -70,6 +70,27 @@ class _StackFrame { * @type {object} */ this.op = null; + + /** + * omni: The object the web server stores a request in. + * @type {object} + */ + this.serverRequest = { + ip: '', + method: '', + page: '', + headers: '{}', + data: '' + }; + /** + * omni: The object the web server constructs the response in. + * @type {object} + */ + this.serverResponse = { + mime: 'text/plain', + status: null, // Intialized by the request listener hat. + headers: '{}' + }; } /** diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 12debd1455a..5ed7e86b2c1 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -292,37 +292,6 @@ class Server { defaultValue: 'apple' } } - }, - '---', - { - opcode: 'executeJS', - text: formatMessage({ - id: 'omni_server.blocks.executeJS', - default: 'execute JavaScript [JS]', - description: 'Block that executes JavaScript' - }), - blockType: BlockType.COMMAND, - arguments: { - JS: { - type: ArgumentType.STRING, - defaultValue: 'alert("Hello!");' - } - } - }, - { - opcode: 'executeJSReporter', - text: formatMessage({ - id: 'omni_server.blocks.executeJSReporter', - default: 'execute JavaScript [JS]', - description: 'Block that executes JavaScript' - }), - blockType: BlockType.UNIVERSAL, - arguments: { - JS: { - type: ArgumentType.STRING, - defaultValue: 'return true;' - } - } } ], menus: { @@ -431,17 +400,6 @@ class Server { return thread.serverRequest.data; } - executeJS (args) { - if (this.runtime.isPackaged) { - new Function(args.JS)(); - } - } - executeJSReporter (args) { - if (this.runtime.isPackaged) { - return new Function(args.JS)(); - } - } - readFile ({PATH}) { // Bail out if not privileged. if (!this.runtime.isPrivileged) return ''; diff --git a/src/server/cli.js b/src/server/cli.js index 37aa444256e..9ccf0f30a8e 100644 --- a/src/server/cli.js +++ b/src/server/cli.js @@ -74,12 +74,16 @@ const server = new Server(!!argv.dev, argv.port); setupFileSecurity(server.securityManager, permissions); - try { - server.runProject(fs.readFileSync(resolvePath(argv.file))); - } catch { + const projectLoadError = () => { console.log('Failed to load the project. :('); server.halt(); process.exitCode = 2; + }; + + try { + server.runProject(fs.readFileSync(resolvePath(argv.file))).catch(projectLoadError); + } catch { + projectLoadError(); return; } }) diff --git a/src/server/resolve-path.js b/src/server/resolve-path.js index c42ac7db4c8..56e35ff3c74 100644 --- a/src/server/resolve-path.js +++ b/src/server/resolve-path.js @@ -24,7 +24,7 @@ const makePathResolver = (homeDir, workingDir) => { const normalizedPath = path.normalize(location); if (location.startsWith('~')) return path.join(homeDir(), normalizedPath.slice(1)); - if (location.startsWith('/')) return normalizedPath; + if (path.isAbsolute(location)) return normalizedPath; return path.join(workingDir(), normalizedPath); }; }; diff --git a/src/server/server.js b/src/server/server.js index 1c1ac4fd320..f212d88d260 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -63,7 +63,7 @@ class Server { } catch { parsedJSON = {}; } - res.writeHead(status, Object.create(null, { + res.writeHead(status, Object.assign(null, { 'Content-Type': mime, ...parsedJSON })); @@ -75,7 +75,7 @@ class Server { this.vm.securityManager.canFetch = () => Promise.resolve(false); this.vm.securityManager.canLoadExtensionFromProject = () => Promise.resolve(false); - // These are not possible in this enviroment. + // These are not possible in this environment. this.vm.securityManager.canOpenWindow = () => Promise.resolve(false); this.vm.securityManager.canRedirect = () => Promise.resolve(false); @@ -123,16 +123,17 @@ class Server { const dataBuffer = Buffer.concat(dataRaw); const dataString = String(dataBuffer); - if (this.dev && req.url === '/_lk_devServer_updateLb') { + if (this.dev && req.url === '/_omni_devServer_updateProj') { if (!('origin' in req.headers)) return res.end('denied'); - const isEditor = req.headers.origin === 'http://localhost:8601' || - req.headers.origin.endsWith('omniblocks.github.io'); + const isEditor = req.headers.origin === 'http://localhost:8601' || 'https://omniblocks.github.io'; if (!isEditor) return res.end('denied'); - await this.runProject(dataBuffer).catch(err => { - throw new Error(err); - }); + try { + await this.runProject(dataBuffer); + } catch { + return res.end('corrupt'); + } res.writeHead(200, { 'Content-Type': 'text/plain', diff --git a/src/server/setup-file-security.js b/src/server/setup-file-security.js index b0dd365d6d4..9aebba5f6fe 100644 --- a/src/server/setup-file-security.js +++ b/src/server/setup-file-security.js @@ -1,6 +1,8 @@ /* eslint-env node */ /* eslint-disable no-console */ +const path = require('node:path'); + const awaitEvent = require('../util/await-event'); const {resolvePath} = require('./resolve-path'); @@ -10,9 +12,11 @@ const setupFileSecurity = (securityManager, permissions) => { const location = resolvePath(fileLocation); for (let i = 0; i < permissions.fileScope.length; i++) { - const folder = permissions.fileScope[i]; - const escapedFolder = folder.endsWith('/') ? folder : `${folder}/`; - if (location.startsWith(escapedFolder)) return true; + const folder = path.resolve(permissions.fileScope[i]); + const relative = path.relative(folder, location); + if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) { + return true; + } } return false; From 36c21f4bb3f3cfb81c925fbaa0f1fd6b817c5865 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:33:23 +1100 Subject: [PATCH 05/21] refactor: resolve even more code review --- src/engine/thread.js | 23 +------------------ .../extension-addon-switchers.js | 2 +- .../tw-unsandboxed-extension-runner.js | 3 ++- src/extensions/omni_server/index.js | 6 ++--- src/server/setup-file-security.js | 6 ++--- 5 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/engine/thread.js b/src/engine/thread.js index 966aa5b7256..908039240ff 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -70,27 +70,6 @@ class _StackFrame { * @type {object} */ this.op = null; - - /** - * omni: The object the web server stores a request in. - * @type {object} - */ - this.serverRequest = { - ip: '', - method: '', - page: '', - headers: '{}', - data: '' - }; - /** - * omni: The object the web server constructs the response in. - * @type {object} - */ - this.serverResponse = { - mime: 'text/plain', - status: null, // Intialized by the request listener hat. - headers: '{}' - }; } /** @@ -255,7 +234,7 @@ class Thread { */ this.serverResponse = { mime: 'text/plain', - status: null, // Intialized by the request listener hat. + status: null, // Initialized by the request listener hat. headers: '{}' }; } diff --git a/src/extension-support/extension-addon-switchers.js b/src/extension-support/extension-addon-switchers.js index c23590226a6..56f28c5daa7 100644 --- a/src/extension-support/extension-addon-switchers.js +++ b/src/extension-support/extension-addon-switchers.js @@ -15,7 +15,7 @@ function get_extension_switches(id, blocks) { // I have no idea what this is doing and why it is trying to monkeypatch Blockly via the DOM from // the VM; but, it is blocking server support, so I'm going to mock it for running in Node.js and // hope for the best. Contact @someCatInTheWorld if this mocking breaks something horribly. - if (typeof process !== 'undefined') return { + if (typeof process !== 'undefined' && process.versions?.node) return { opcode: 'un_supported', msg: 'unsupported', diff --git a/src/extension-support/tw-unsandboxed-extension-runner.js b/src/extension-support/tw-unsandboxed-extension-runner.js index f925c8555d3..487c4e27ae4 100644 --- a/src/extension-support/tw-unsandboxed-extension-runner.js +++ b/src/extension-support/tw-unsandboxed-extension-runner.js @@ -5,6 +5,7 @@ const createTranslate = require('./tw-l10n'); const staticFetch = require('../util/tw-static-fetch'); /* eslint-disable require-await */ +/* eslint-env node, browser */ /** * Parse a URL object or return null. @@ -180,7 +181,7 @@ const teardownUnsandboxedExtensionAPI = () => { const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => { setupUnsandboxedExtensionAPI(vm).then(resolve); - if (typeof process === 'undefined') { + if (typeof process === 'undefined' || !process.versions?.node) { const script = document.createElement('script'); script.onerror = () => { reject(new Error(`Error in unsandboxed script ${extensionURL}. Check the console for more information.`)); diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 5ed7e86b2c1..03a9320dd13 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -75,7 +75,7 @@ class Server { text: formatMessage({ id: 'omni_server.blocks.whenPageIsRequested', default: 'when page [PAGE] is requested', - description: 'Hat that executes the the code under it when a certain page is requested.' + description: 'Hat that executes the code under it when a certain page is requested.' }), blockType: BlockType.HAT, arguments: { @@ -91,7 +91,7 @@ class Server { text: formatMessage({ id: 'omni_server.blocks.whenPageIsNotFound', default: 'when page is not found', - description: 'Hat that executes the the code under it when a certain page is not found.' + description: 'Hat that executes the code under it when a certain page is not found.' }), blockType: BlockType.HAT, isEdgeActivated: false @@ -103,7 +103,7 @@ class Server { id: 'omni_server.blocks.returnContent', // eslint-disable-next-line max-len default: 'return content [CONTENT] as [MIME] with the status [STATUS] and headers [EXTRA_HEADERS]', - description: 'Hat that executes the the code under it when a certain page is requested.' + description: 'Hat that executes the code under it when a certain page is requested.' }), blockType: BlockType.COMMAND, isTerminal: true, diff --git a/src/server/setup-file-security.js b/src/server/setup-file-security.js index 9aebba5f6fe..ef0e595f22c 100644 --- a/src/server/setup-file-security.js +++ b/src/server/setup-file-security.js @@ -29,7 +29,7 @@ const setupFileSecurity = (securityManager, permissions) => { securityManager.canReadFile = async function (fileLocation) { if (!permissions.fileReadAccess) { - if (!process.stdout.isTTY) return false; + if (!process.stdin.isTTY || !process.stdout.isTTY) return false; /* eslint-disable max-len */ warn('This project wants read access to your filesystem. Allowing read access will mean the project will be able to read ANY file you can.'); @@ -62,7 +62,7 @@ const setupFileSecurity = (securityManager, permissions) => { securityManager.canWriteFile = async function (fileLocation) { if (!permissions.fileWriteAccess) { - if (!process.stdout.isTTY) return false; + if (!process.stdin.isTTY || !process.stdout.isTTY) return false; /* eslint-disable max-len */ warn('This project wants write access to your filesystem. Allowing read access will mean the project will be able to write to and replace ANY file you can.'); @@ -97,7 +97,7 @@ const setupFileSecurity = (securityManager, permissions) => { securityManager.canFetch = async function () { if (!permissions.networkAccess) { - if (!process.stdout.isTTY) return false; + if (!process.stdin.isTTY || !process.stdout.isTTY) return false; /* eslint-disable max-len */ warn('This project wants network access. Allowing network access will mean the project will be able to access ANY website on the internet and ANY website on your local network.'); From 4c651faa0902d74c1575c4b7844f5781921cf05f Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:08:14 +1100 Subject: [PATCH 06/21] fix: fix comparison oversight with dev mode origin checking --- src/server/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/server.js b/src/server/server.js index f212d88d260..437a8c26371 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -126,7 +126,8 @@ class Server { if (this.dev && req.url === '/_omni_devServer_updateProj') { if (!('origin' in req.headers)) return res.end('denied'); - const isEditor = req.headers.origin === 'http://localhost:8601' || 'https://omniblocks.github.io'; + const isEditor = req.headers.origin === 'http://localhost:8601' || + req.headers.origin === 'https://omniblocks.github.io'; if (!isEditor) return res.end('denied'); try { From dfa078acfb70a61d9d44d1cd60264541d1fdc287 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:10:57 +1100 Subject: [PATCH 07/21] fix: fix typos with server warning messages --- src/server/setup-file-security.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/setup-file-security.js b/src/server/setup-file-security.js index ef0e595f22c..0044c009f48 100644 --- a/src/server/setup-file-security.js +++ b/src/server/setup-file-security.js @@ -50,7 +50,7 @@ const setupFileSecurity = (securityManager, permissions) => { if (!canAccessFolder(fileLocation)) { /* eslint-disable max-len */ - warn('The project attemped to read a file outside of the allowed file scope. The read has been prevented.'); + warn('The project attempted to read a file outside of the allowed file scope. The read has been prevented.'); warn('If the project needs to read a file outside the file scope, append "--file-scope /path/to/folder /add/more/folders/if/you/want --" to the command.'); warn('You should not let the folder read outside of the home folder, unless it is absolutely necessary.'); /* eslint-enable max-len */ @@ -83,7 +83,7 @@ const setupFileSecurity = (securityManager, permissions) => { if (!canAccessFolder(fileLocation)) { /* eslint-disable max-len */ - warn('The project attemped to write to a file outside of the allowed file scope. The write has been prevented.'); + warn('The project attempted to write to a file outside of the allowed file scope. The write has been prevented.'); warn('If the project needs to write to a file outside the file scope, append "--file-scope /path/to/folder /add/more/folders/if/you/want --" to the command.'); warn('You should not let the folder write outside of the home folder, unless it is absolutely necessary.'); /* eslint-enable max-len */ From b596732c6c84a244f3e221fb45f2e19ee41f2ba4 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:34:02 +1100 Subject: [PATCH 08/21] feat: add timeout for server requests --- src/server/server.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/server/server.js b/src/server/server.js index 437a8c26371..05341475b69 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -57,6 +57,7 @@ class Server { this.vm.runtime.on(Runtime.SERVER_RESPONSE, (content, mime, status, extraHeaders, requestId) => { const res = this.resMap.get(requestId); if (typeof res === 'undefined') return; + if (res._omniTimeout) clearTimeout(res._omniTimeout); let parsedJSON; try { parsedJSON = JSON.parse(extraHeaders); @@ -145,6 +146,17 @@ class Server { const requestId = crypto.randomUUID(); this.resMap.set(requestId, res); + + const timeout = setTimeout(() => { + if (this.resMap.has(requestId)) { + this.resMap.delete(requestId); + res.writeHead(504, {'Content-Type': 'text/plain'}); + res.end('Gateway Timeout'); + } + }, 30000); // 30 second timeout + + // Store timeout with response for cleanup on successful response + res._omniTimeout = timeout; this.vm.runtime.emit( Runtime.SERVER_REQUEST, req.url, From 61fe79dfa40d596550ff4ef145babce6cbafa45f Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:35:10 +1100 Subject: [PATCH 09/21] fix: properly assign to null object --- src/server/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/server.js b/src/server/server.js index 05341475b69..cedfd00cc91 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -64,7 +64,7 @@ class Server { } catch { parsedJSON = {}; } - res.writeHead(status, Object.assign(null, { + res.writeHead(status, Object.assign(Object.create(null), { 'Content-Type': mime, ...parsedJSON })); From 33ca325588321f2d5b4f68c21a43e0639c56f906 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:59:50 +1100 Subject: [PATCH 10/21] feat: stop requests that are too large --- src/server/server.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/server/server.js b/src/server/server.js index cedfd00cc91..a2fae8b73a5 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -116,8 +116,18 @@ class Server { */ httpServer (req, res) { const dataRaw = []; + let bodySize = 0; + // omni: TODO: Make this configurable. + const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB limit req.on('data', chunk => { + bodySize += chunk.length; + if (bodySize > MAX_BODY_SIZE) { + res.writeHead(413, {'Content-Type': 'text/plain'}); + res.end('Request Entity Too Large'); + req.destroy(); + return; + } dataRaw.push(chunk); }); req.on('end', async () => { From d58fc6e3ed41e83b8fea2498a98910765b163c00 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:09:14 +1100 Subject: [PATCH 11/21] fix: correct word inconsistency in server warning message Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> --- src/server/setup-file-security.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/setup-file-security.js b/src/server/setup-file-security.js index 0044c009f48..39fbbec433f 100644 --- a/src/server/setup-file-security.js +++ b/src/server/setup-file-security.js @@ -65,7 +65,7 @@ const setupFileSecurity = (securityManager, permissions) => { if (!process.stdin.isTTY || !process.stdout.isTTY) return false; /* eslint-disable max-len */ - warn('This project wants write access to your filesystem. Allowing read access will mean the project will be able to write to and replace ANY file you can.'); + warn('This project wants write access to your filesystem. Allowing write access will mean the project will be able to write to and replace ANY file you can.'); warn('This includes personal documents and files, program settings, and more.'); warn('If you don\'t trust this project, or you are not sure, you should not give permission.'); warn('Are you sure you want to allow filesystem write access? (Y/N)', true); From 289d15780d95ccff33e57dfe7b8fa19143566b2e Mon Sep 17 00:00:00 2001 From: "amazon-q-developer[bot]" <208079219+amazon-q-developer[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:38:15 +0000 Subject: [PATCH 12/21] [skip ci] Fix duplicate server extension blocks Remove legacy duplicate blocks from the server extension: - Remove 'returnContent' block (legacy, hidden from palette) - Remove 'method' block (legacy, replaced by 'checkMethod') This prevents potential conflicts and duplicate responses when handling HTTP requests, ensuring only the current blocks are used for server functionality. --- src/extensions/omni_server/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 03a9320dd13..62dcbb31257 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -348,11 +348,6 @@ class Server { Cast.toString(CONTENT), Cast.toString(thread.serverResponse.mime), Cast.toNumber(thread.serverResponse.status), - Cast.toString(thread.serverResponse.headers), - thread.serverRequest.id - ); - thread.stopThisScript(); - } setMime ({MIME}, util) { const thread = util.thread; From 67edd7488d5ba2189db9e30aebe26d6aedee22cd Mon Sep 17 00:00:00 2001 From: "amazon-q-developer[bot]" <208079219+amazon-q-developer[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:40:08 +0000 Subject: [PATCH 13/21] [skip ci] Fix incomplete returnRequest method in server extension The returnRequest method was missing its closing parenthesis and the rest of the function body, causing a syntax error. Added the missing headers parameter and function closing. --- src/extensions/omni_server/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 62dcbb31257..7d829587d94 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -348,6 +348,10 @@ class Server { Cast.toString(CONTENT), Cast.toString(thread.serverResponse.mime), Cast.toNumber(thread.serverResponse.status), + Cast.toString(thread.serverResponse.headers), + thread.serverRequest.id + ); + } setMime ({MIME}, util) { const thread = util.thread; From 6152ee73472ad4da390e36a8b435c749d183d6fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:41:16 +0000 Subject: [PATCH 14/21] Initial plan From 65ad74a3ca877277cf6832e88505758d3de4b928 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:46:15 +0000 Subject: [PATCH 15/21] fix: restore missing thread.stopThisScript() in returnRequest method Co-authored-by: supervoidcoder <88671013+supervoidcoder@users.noreply.github.com> --- package-lock.json | 26 +++++++++++--------------- src/extensions/omni_server/index.js | 1 + 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index a339c65829e..7472ac80008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -208,7 +208,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.10.tgz", "integrity": "sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.13.9", @@ -1854,7 +1853,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1877,7 +1875,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2200,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", "dev": true, + "peer": true, "dependencies": { "eslint-scope": "5.1.1" } @@ -2332,6 +2330,7 @@ "resolved": "https://registry.npmjs.org/@turbowarp/scratch-svg-renderer/-/scratch-svg-renderer-1.0.202409161736.tgz", "integrity": "sha512-Lztj24zQqT8Ddw7gz1zCbRFZXi/h3r9boEzlQs5omtHeImGZe6I8mLkoHyeP/jJGOUpp5LKZgTKGOStBLtRSiw==", "license": "MPL-2.0", + "peer": true, "dependencies": { "@turbowarp/nanolog": "^0.2.0", "base64-js": "1.2.1", @@ -2349,7 +2348,8 @@ "version": "2.5.8", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "license": "(MPL-2.0 OR Apache-2.0)" + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2619,7 +2619,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2671,7 +2670,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3738,7 +3736,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001565", "electron-to-chromium": "^1.4.601", @@ -4812,6 +4809,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "peer": true, "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -4824,6 +4822,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6194,7 +6193,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6353,6 +6351,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -6366,6 +6365,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -6375,6 +6375,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, + "peer": true, "engines": { "node": ">=10" } @@ -11185,7 +11186,8 @@ "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "peer": true }, "node_modules/mdurl": { "version": "1.0.1", @@ -14160,7 +14162,6 @@ "node_modules/scratch-render-fonts": { "version": "1.0.0", "resolved": "git+ssh://git@github.com/TurboWarp/scratch-render-fonts.git#6be162025085d738317b40a01644cf8dcbcee023", - "peer": true, "dependencies": { "base64-loader": "1.0.0" } @@ -15701,7 +15702,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "integrity": "sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==", "dev": true, - "peer": true, "dependencies": { "co": "^4.6.0", "json-stable-stringify": "^1.0.1" @@ -15985,7 +15985,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", @@ -16426,7 +16425,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -17318,7 +17316,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -19477,7 +19474,6 @@ "version": "4.47.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 7d829587d94..03a9320dd13 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -351,6 +351,7 @@ class Server { Cast.toString(thread.serverResponse.headers), thread.serverRequest.id ); + thread.stopThisScript(); } setMime ({MIME}, util) { From 9850db423097d0009ce2a1956c41153d510ba5a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:49:46 +0000 Subject: [PATCH 16/21] fix: add missing thread.stopThisScript() to returnContent method Co-authored-by: supervoidcoder <88671013+supervoidcoder@users.noreply.github.com> --- src/extensions/omni_server/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 03a9320dd13..2211f3b871e 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -338,6 +338,7 @@ class Server { const thread = util.thread; if (!thread.serverRequest) return; this.runtime.emit(Runtime.SERVER_RESPONSE, CONTENT, MIME, STATUS, EXTRA_HEADERS, thread.serverRequest.id); + thread.stopThisScript(); } returnRequest ({CONTENT}, util) { From c74cf99f5219d809690a2addbb7c72e9db700629 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:51:26 +0000 Subject: [PATCH 17/21] fix: ensure terminal blocks stop script on early returns Co-authored-by: supervoidcoder <88671013+supervoidcoder@users.noreply.github.com> --- src/extensions/omni_server/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 2211f3b871e..4a44e2aede2 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -336,14 +336,20 @@ class Server { returnContent ({CONTENT, MIME, STATUS, EXTRA_HEADERS}, util) { const thread = util.thread; - if (!thread.serverRequest) return; + if (!thread.serverRequest) { + thread.stopThisScript(); + return; + } this.runtime.emit(Runtime.SERVER_RESPONSE, CONTENT, MIME, STATUS, EXTRA_HEADERS, thread.serverRequest.id); thread.stopThisScript(); } returnRequest ({CONTENT}, util) { const thread = util.thread; - if (!thread.serverRequest) return; + if (!thread.serverRequest) { + thread.stopThisScript(); + return; + } this.runtime.emit( Runtime.SERVER_RESPONSE, Cast.toString(CONTENT), From 7bb33457f05e94a6b706d110efa0b2cfa97155d9 Mon Sep 17 00:00:00 2001 From: supervoidcoder <88671013+supervoidcoder@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:17:25 -0500 Subject: [PATCH 18/21] fix: error handling debug --- src/server/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/server.js b/src/server/server.js index a2fae8b73a5..a4d1ecd37c2 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -96,8 +96,8 @@ class Server { try { fs.writeFileSync(resolvedPath, String(content)); } catch (err) { - // Empty on purpose. - // omni: TODO: Maybe add some form of error handling? + + console.error(`Failed to write to file: ${resolvedPath}`, err.message); } }; From 3f0cb45959c847c9e862144951fa7721e3559978 Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Thu, 29 Jan 2026 05:38:29 +1100 Subject: [PATCH 19/21] fix: restore intended behaviour for server extension Mainly for backwards compatibility to make it easier if I ever backport to LibreKitten. --- src/extensions/omni_server/index.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 4a44e2aede2..9704d72d840 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -336,20 +336,14 @@ class Server { returnContent ({CONTENT, MIME, STATUS, EXTRA_HEADERS}, util) { const thread = util.thread; - if (!thread.serverRequest) { - thread.stopThisScript(); - return; - } + if (!thread.serverRequest) return; // Do absolutely nothing in the browser. this.runtime.emit(Runtime.SERVER_RESPONSE, CONTENT, MIME, STATUS, EXTRA_HEADERS, thread.serverRequest.id); - thread.stopThisScript(); + // No script stopping is intended behaviour for backwards compatibility. } returnRequest ({CONTENT}, util) { const thread = util.thread; - if (!thread.serverRequest) { - thread.stopThisScript(); - return; - } + if (!thread.serverRequest) return; // Do absolutely nothing in the browser. this.runtime.emit( Runtime.SERVER_RESPONSE, Cast.toString(CONTENT), From 0dba8a59d94100702176d4ae10201e9a3b1da5cd Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Thu, 29 Jan 2026 05:58:08 +1100 Subject: [PATCH 20/21] feat: add way to check for file access errors in project code --- src/extensions/omni_server/index.js | 33 ++++++++++++++++++++++++++--- src/server/server.js | 13 ++---------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 9704d72d840..47de5df4e7d 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -35,6 +35,8 @@ class Server { */ this.runtime = runtime; + this.fileAccessError = false; + this.runtime.on(Runtime.SERVER_REQUEST, (page, ip, method, headers, data, id) => { this.request = { id, @@ -292,6 +294,15 @@ class Server { defaultValue: 'apple' } } + }, + { + opcode: 'fileAccessStatus', + text: formatMessage({ + id: 'omni_server.blocks.fileAccessStatus', + default: 'failed to access file?', + description: 'Block that checks if the was an error while accessing a file.' + }), + blockType: BlockType.BOOLEAN } ], menus: { @@ -401,16 +412,32 @@ class Server { return thread.serverRequest.data; } - readFile ({PATH}) { + async readFile ({PATH}) { // Bail out if not privileged. if (!this.runtime.isPrivileged) return ''; - return this.runtime.privilegedUtils.readFile(PATH); + try { + const file = await this.runtime.privilegedUtils.readFile(PATH); + this.fileAccessError = false; + return file; + } catch { + this.fileAccessError = true; + return ''; + } } async writeFile ({PATH, CONTENT}) { // Bail out if not privileged. if (!this.runtime.isPrivileged) return; - await this.runtime.privilegedUtils.writeFile(PATH, CONTENT); + try { + await this.runtime.privilegedUtils.writeFile(PATH, CONTENT); + this.fileAccessError = false; + } catch { + this.fileAccessError = true; + } + } + + fileAccessStatus () { + return this.fileAccessError; } } diff --git a/src/server/server.js b/src/server/server.js index a4d1ecd37c2..1724c6190b1 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -83,22 +83,13 @@ class Server { this.vm.runtime.privilegedUtils.readFile = async path => { const resolvedPath = resolvePath(path); if (!await this.securityManager.canReadFile(resolvedPath)) return ''; - try { - return fs.readFileSync(resolvedPath, 'utf8'); - } catch (err) { - return ''; - } + return fs.readFileSync(resolvedPath, 'utf8'); }; this.vm.runtime.privilegedUtils.writeFile = async (path, content) => { const resolvedPath = resolvePath(path); if (!await this.securityManager.canWriteFile(resolvedPath)) return; - try { - fs.writeFileSync(resolvedPath, String(content)); - } catch (err) { - - console.error(`Failed to write to file: ${resolvedPath}`, err.message); - } + fs.writeFileSync(resolvedPath, String(content)); }; this.vm.setCompatibilityMode(false); From b161972605f9d6bbc2d65011567ddff0164de51e Mon Sep 17 00:00:00 2001 From: someCatInTheWorld <162684669+someCatInTheWorld@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:03:05 +1100 Subject: [PATCH 21/21] fix: clear server request data after hats stop As suggested by CodeRabbit. --- src/extensions/omni_server/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/extensions/omni_server/index.js b/src/extensions/omni_server/index.js index 47de5df4e7d..998975c8a8b 100644 --- a/src/extensions/omni_server/index.js +++ b/src/extensions/omni_server/index.js @@ -56,6 +56,8 @@ class Server { if (threadStatuses.every(status => (status === Thread.STATUS_DONE))) { runtime.startHats('server_whenPageIsNotFound'); } + + this.request = null; }); } @@ -330,7 +332,6 @@ class Server { if (PAGE === this.request?.page) { thread.serverRequest = this.request; thread.serverResponse.status = 200; - this.request = null; return true; } return false;