feat: add compiled project protection and GitHub Pages deployment
- promote compiled packaging as a primary product feature - add VM access and configurable JavaScript obfuscation - support global debug protection and self-defending options - package compiled artifacts with integrity verification - update SB3C branding, documentation, and dependency URLs - fix standalone builds and production service worker caching - configure GitHub Pages deployment workflow
This commit is contained in:
23 files changed
+1693
-112
No files matched your search
@@ -9,9 +9,15 @@ concurrency:
|
||||
group: "deploy"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -21,6 +27,7 @@ jobs:
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Test
|
||||
@@ -36,6 +43,8 @@ jobs:
|
||||
run: |
|
||||
npm run build-standalone-prod
|
||||
cp dist/standalone.html web
|
||||
- name: Configure GitHub Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
@@ -45,10 +54,8 @@ jobs:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# TurboWarp Packager
|
||||
# SB3C Packager
|
||||
|
||||
https://packager.turbowarp.org/
|
||||
An independent modified version of [TurboWarp Packager](https://github.com/TurboWarp/packager), focused on ahead-of-time compiled `.sb3c` projects, VM-access protection, reproducible JavaScript obfuscation, and package integrity verification.
|
||||
|
||||
Source: https://github.com/LuYifei2011/tw-packager
|
||||
|
||||
Website: https://luyifei2011.github.io/tw-packager/
|
||||
|
||||
Converts Scratch projects into HTML files, zip archives, or executable programs for Windows, macOS, and Linux.
|
||||
|
||||
Compilation and obfuscation are not encryption or DRM. They can make casual inspection, copying, and automated analysis more expensive, but the player must still receive executable JavaScript. A determined user can inspect, instrument, or modify it, so projects must never contain private keys, long-term secrets, or client-side security decisions.
|
||||
|
||||
## Development
|
||||
|
||||
Install dependencies:
|
||||
@@ -57,6 +63,8 @@ Note that npm is a very buggy piece of software and our dependency tree is very
|
||||
|
||||
The packager is deployed as a simple static website. You can host it anywhere by just copying the `dist` folder after a build.
|
||||
|
||||
This fork includes `.github/workflows/deploy.yml` for GitHub Pages. In the repository's **Settings → Pages**, set **Source** to **GitHub Actions**. A push to `master`, or a manual run of “Deploy to GitHub Pages”, will test the project, build the production website and standalone version, upload the Pages artifact, and deploy it to `https://luyifei2011.github.io/tw-packager/`.
|
||||
|
||||
We use GitHub Actions and GitHub Pages to manage our deployment. If you want to do this too:
|
||||
|
||||
- Fork the repository on GitHub and push your changes.
|
||||
@@ -103,6 +111,8 @@ npm run build-node-prod
|
||||
|
||||
<!-- Make sure to also update COPYRIGHT_NOTICE in src/packager/brand.js -->
|
||||
|
||||
Copyright (C) 2021-2024 Thomas Weber
|
||||
SB3C Packager modifications Copyright (C) 2026 SB3C Packager contributors
|
||||
|
||||
Based on TurboWarp Packager, Copyright (C) 2021-2024 Thomas Weber
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
+57
-3
@@ -3,7 +3,7 @@
|
||||
## Installing
|
||||
|
||||
```bash
|
||||
npm install --save-exact @turbowarp/packager
|
||||
npm install --save-exact @luyifei2011/tw-packager
|
||||
```
|
||||
|
||||
We suggest that you use `--save-exact` (or, with yarn, `--exact`) to make sure you always install the same version. This is important because we don't promise API compatibility across even minor updates.
|
||||
@@ -34,7 +34,7 @@ The Node.js module as published on npm is not intended to work in a browser rega
|
||||
|
||||
Large assets such as Electron binaries are not stored in this repository and will be downloaded from a remote server on demand. While we aren't actively removing old files, we can't promise they will exist forever. Downloads are validated with a SHA-256 checksum and cached locally.
|
||||
|
||||
Large assets are cached in `node_modules/@turbowarp/packager/.packager-cache`. You may want to periodically clean this folder.
|
||||
Large assets are cached in `node_modules/@luyifei2011/tw-packager/.packager-cache`. You may want to periodically clean this folder.
|
||||
|
||||
## Using the API
|
||||
|
||||
@@ -43,7 +43,7 @@ See demo.js or demo-simple.js for a full example.
|
||||
First, you can import the module like this:
|
||||
|
||||
```js
|
||||
const Packager = require('@turbowarp/packager');
|
||||
const Packager = require('@luyifei2011/tw-packager');
|
||||
```
|
||||
|
||||
### Load a project
|
||||
@@ -112,6 +112,60 @@ Some options expect an image as an argument. In the Node.js module, there is a s
|
||||
packager.options.app.icon = new Packager.Image('image/png', fs.readFileSync('icon.png'));
|
||||
```
|
||||
|
||||
### Ahead-of-time compilation and protection
|
||||
|
||||
The packager can compile a project into a `.sb3c` package before embedding it. VM member-access protection is enabled by default when this mode is selected; `javascript-obfuscator` can be added as a second stage.
|
||||
|
||||
```js
|
||||
packager.options.compiler.aot.enabled = true;
|
||||
packager.options.compiler.aot.obfuscate = true;
|
||||
packager.options.compiler.aot.javascriptObfuscator = true;
|
||||
packager.options.compiler.aot.shortIds = true;
|
||||
packager.options.compiler.aot.seed = 'public-reproducible-seed';
|
||||
|
||||
Object.assign(packager.options.compiler.aot.javascriptObfuscatorOptions, {
|
||||
identifierNamesGenerator: 'mangled-shuffled',
|
||||
stringArray: true,
|
||||
stringArrayEncoding: 'rc4',
|
||||
stringArrayThreshold: 0.75,
|
||||
controlFlowFlattening: true,
|
||||
controlFlowFlatteningThreshold: 0.5,
|
||||
deadCodeInjection: false,
|
||||
numbersToExpressions: true,
|
||||
splitStrings: true,
|
||||
splitStringsChunkLength: 8,
|
||||
selfDefending: true,
|
||||
debugProtection: true,
|
||||
debugProtectionInterval: 3000
|
||||
});
|
||||
```
|
||||
|
||||
`renameGlobals`, `renameProperties`, and source maps are intentionally unavailable for compiled factories because they can break VM bindings or the artifact format. Self-defending mode applies to each independently evaluated compiled factory and must be the final factory-source transformation. Debug protection is different: the packager generates one global player sidecar, so entry scripts and procedures do not each install their own guard or interval. An interval of `0` runs the guard once; repeating values must be at least 1000 ms, and 2000–4000 ms is the practical range suggested by JavaScript Obfuscator.
|
||||
|
||||
These transformations are deterrence, not encryption or a security boundary. They raise the cost of casual inspection and automated analysis, but executable JavaScript and the accessor still reach the user's machine. Never include private keys, long-term secrets, authorization logic, or other client-side trust decisions in a project.
|
||||
|
||||
For release builds, keep providers and secrets outside the serializable options object. `compiledProjectHooks` supports project-field seed selection, local or remote seed derivation, custom stages, `javascript-obfuscator` options, and integrity signing:
|
||||
|
||||
```js
|
||||
packager.compiledProjectHooks = {
|
||||
seedMaterial: ({project}) => ({
|
||||
extensions: project.extensions,
|
||||
targetNames: project.targets.map(target => target.name)
|
||||
}),
|
||||
deriveSeed: async ({materialSha256}) => {
|
||||
const {seed, keyId} = await seedService.derive({materialSha256});
|
||||
return {seed, keyId, derivationId: 'release-hmac-v1'};
|
||||
},
|
||||
javascriptObfuscatorOptions: {compact: true},
|
||||
signManifest,
|
||||
// Source for the verifier embedded in the generated player. It must be a
|
||||
// self-contained async function accepting {payload, manifest, signature}.
|
||||
signatureVerifierSource: verifyManifest.toString()
|
||||
};
|
||||
```
|
||||
|
||||
The generated package includes a SHA-256 integrity manifest. When VM member-access protection is enabled, the matching accessor sidecar is embedded separately in the player and injected through the compiled artifact's declared binding. The artifact and accessor must always be published together; the packager handles this automatically.
|
||||
|
||||
Note that a Packager is a single-use object; you must make a new Packager each time you want to package a project.
|
||||
|
||||
Now you can finally actually package the project.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// You should use require('@turbowarp/packager') instead
|
||||
// You should use require('@luyifei2011/tw-packager') instead
|
||||
// We use a strange require() in this demo because we use this to test the API internally
|
||||
const Packager = require('../dist/packager');
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fetch = require('cross-fetch').default;
|
||||
|
||||
// You should use require('@turbowarp/packager') instead
|
||||
// You should use require('@luyifei2011/tw-packager') instead
|
||||
// We use a strange require() in this demo because we use this to test the API internally
|
||||
const Packager = require('../dist/packager');
|
||||
|
||||
|
||||
Generated
+724
-57
File diff suppressed because it is too large.
Load diff
+11
-9
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@turbowarp/packager",
|
||||
"name": "@luyifei2011/tw-packager",
|
||||
"version": "3.13.0",
|
||||
"description": "Converts Scratch projects into HTML files, zip archives, or executable programs for Windows, macOS, and Linux.",
|
||||
"description": "Compiles, protects, and packages Scratch projects as HTML, archives, or desktop applications.",
|
||||
"main": "dist/packager.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve",
|
||||
@@ -17,18 +17,18 @@
|
||||
"prepublishOnly": "npm run test && npm run build-node-prod && node src/build/nodejs/update-readme.js node",
|
||||
"postpublish": "node src/build/nodejs/update-readme.js restore"
|
||||
},
|
||||
"author": "GarboMuffin",
|
||||
"homepage": "https://github.com/TurboWarp/packager#readme",
|
||||
"author": "LuYifei2011",
|
||||
"homepage": "https://luyifei2011.github.io/tw-packager/",
|
||||
"keywords": [
|
||||
"scratch",
|
||||
"packager"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/TurboWarp/packager/issues"
|
||||
"url": "https://github.com/LuYifei2011/tw-packager/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TurboWarp/packager.git"
|
||||
"url": "git+https://github.com/LuYifei2011/tw-packager.git"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
@@ -52,11 +52,13 @@
|
||||
"fastestsmallesttextencoderdecoder": "^1.0.22",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"javascript-obfuscator": "^5.5.0",
|
||||
"jest": "^27.4.5",
|
||||
"rimraf": "^2.7.1",
|
||||
"scratch-audio": "github:TurboWarp/scratch-audio#develop",
|
||||
"scratch-render": "github:TurboWarp/scratch-render#develop",
|
||||
"scratch-vm": "github:TurboWarp/scratch-vm#develop",
|
||||
"scratch-audio": "git+https://github.com/TurboWarp/scratch-audio.git#develop",
|
||||
"scratch-render": "git+https://github.com/TurboWarp/scratch-render.git#develop",
|
||||
"scratch-vm": "git+https://github.com/LuYifei2011/scratch-vm.git#compiled-project-artifacts",
|
||||
"sb3c-vm-obfuscator": "git+https://github.com/LuYifei2011/sb3c-vm-obfuscator.git#main",
|
||||
"style-loader": "^2.0.0",
|
||||
"svelte": "^3.44.3",
|
||||
"svelte-loader": "^3.1.2",
|
||||
|
||||
@@ -4,8 +4,13 @@ const patchParser = (parser) => {
|
||||
const originalParseCommentOptions = parser.parseCommentOptions;
|
||||
parser.parseCommentOptions = function (...args) {
|
||||
const result = originalParseCommentOptions.call(this, ...args);
|
||||
result.options.webpackMode = 'eager';
|
||||
return result;
|
||||
return {
|
||||
...result,
|
||||
options: {
|
||||
...(result.options || {}),
|
||||
webpackMode: 'eager'
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class GenerateServiceWorkerPlugin {
|
||||
const workerSource = workerFile.source.source().toString();
|
||||
const newSource = workerSource
|
||||
.replace('__IS_PRODUCTION__', JSON.stringify(process.env.NODE_ENV === 'production'))
|
||||
.replace('[/* __ASSETS__ */]', stringifiedAssets)
|
||||
.replace(/["']__ASSETS__["']/, stringifiedAssets)
|
||||
.replace('__CACHE_NAME__', JSON.stringify(`p4-${hash}`));
|
||||
compilation.updateAsset(SW_NAME, new RawSource(newSource));
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = source => `var self = typeof globalThis === 'undefined' ? this : globalThis;\n${source}`;
|
||||
+101
-2
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"p4": {
|
||||
"description1": {
|
||||
"string": "Converts Scratch projects into HTML files, zip archives, or executable programs for Windows, macOS, and Linux."
|
||||
"string": "Compiles and protects Scratch projects, then packages them as HTML, zip archives, or applications for Windows, macOS, and Linux."
|
||||
},
|
||||
"basedOn": {
|
||||
"string": "This independent project is modified from the open-source {upstream}."
|
||||
},
|
||||
"basedOn-upstream": {
|
||||
"string": "TurboWarp Packager"
|
||||
},
|
||||
"description2": {
|
||||
"string": "If you just want an easy way to embed a project into your website, you may be interested in {embedding}."
|
||||
@@ -13,7 +19,7 @@
|
||||
"string": "Report bugs, give feedback, and suggest ideas {onScratch} or {onGitHub}."
|
||||
},
|
||||
"disclaimer": {
|
||||
"string": "This site is not affiliated with Scratch, the Scratch Team, or the Scratch Foundation."
|
||||
"string": "This is not an official TurboWarp service and is not affiliated with Scratch, the Scratch Team, or the Scratch Foundation."
|
||||
},
|
||||
"description3-on": {
|
||||
"string": "on {brand}",
|
||||
@@ -412,6 +418,99 @@
|
||||
"enableCompiler": {
|
||||
"string": "Enable Compiler"
|
||||
},
|
||||
"aotCompile": {
|
||||
"string": "Enable compiled project mode"
|
||||
},
|
||||
"compiledProtection": {
|
||||
"string": "Compiled Project Protection"
|
||||
},
|
||||
"compiledProtectionDescription": {
|
||||
"string": "A featured build mode of SB3C Packager: compile the block graph ahead of time, optionally replace VM member accesses, and apply additional JavaScript transformations."
|
||||
},
|
||||
"obfuscateCompiledProject": {
|
||||
"string": "Obfuscate compiled project"
|
||||
},
|
||||
"javascriptObfuscator": {
|
||||
"string": "Apply JavaScript Obfuscator to compiled factories"
|
||||
},
|
||||
"javascriptObfuscatorOptions": {
|
||||
"string": "JavaScript Obfuscator options"
|
||||
},
|
||||
"compactOutput": {
|
||||
"string": "Compact output"
|
||||
},
|
||||
"simplifyCode": {
|
||||
"string": "Simplify and transform expressions"
|
||||
},
|
||||
"identifierNamesGenerator": {
|
||||
"string": "Identifier style"
|
||||
},
|
||||
"stringArray": {
|
||||
"string": "Move string literals into encoded arrays"
|
||||
},
|
||||
"stringArrayEncoding": {
|
||||
"string": "String encoding"
|
||||
},
|
||||
"encodingNone": {
|
||||
"string": "No additional encoding"
|
||||
},
|
||||
"transformationThreshold": {
|
||||
"string": "Transformation probability (0–1)"
|
||||
},
|
||||
"controlFlowFlattening": {
|
||||
"string": "Control-flow flattening"
|
||||
},
|
||||
"deadCodeInjection": {
|
||||
"string": "Dead-code injection"
|
||||
},
|
||||
"splitStrings": {
|
||||
"string": "Split string literals"
|
||||
},
|
||||
"splitStringsChunkLength": {
|
||||
"string": "String chunk length"
|
||||
},
|
||||
"numbersToExpressions": {
|
||||
"string": "Convert numbers to expressions"
|
||||
},
|
||||
"transformObjectKeys": {
|
||||
"string": "Transform object keys"
|
||||
},
|
||||
"unicodeEscapeSequence": {
|
||||
"string": "Use Unicode escape sequences"
|
||||
},
|
||||
"disableConsoleOutput": {
|
||||
"string": "Disable console output"
|
||||
},
|
||||
"selfDefending": {
|
||||
"string": "Self-defending output"
|
||||
},
|
||||
"selfDefendingWarning": {
|
||||
"string": "Self-defending output must not be reformatted or minified again. It increases output size and can make compatibility failures harder to diagnose."
|
||||
},
|
||||
"debugProtection": {
|
||||
"string": "Developer Tools debug protection"
|
||||
},
|
||||
"debugProtectionInterval": {
|
||||
"string": "Repeat interval in milliseconds (0 means once)"
|
||||
},
|
||||
"debugProtectionGlobal": {
|
||||
"string": "The anti-debugging guard is generated once as a global player script instead of being duplicated in every compiled procedure."
|
||||
},
|
||||
"debugProtectionWarning": {
|
||||
"string": "Debug protection can freeze or severely disrupt browser Developer Tools, especially with a repeating interval. Use 2000–4000 ms only when necessary and test the packaged project carefully."
|
||||
},
|
||||
"javascriptObfuscatorPerformance": {
|
||||
"string": "Control-flow flattening, dead-code injection, RC4 strings, and self-defending output can substantially increase build time, file size, startup time, and runtime overhead."
|
||||
},
|
||||
"obfuscationSeed": {
|
||||
"string": "Obfuscation seed"
|
||||
},
|
||||
"derivedFromProject": {
|
||||
"string": "Derived from project when empty"
|
||||
},
|
||||
"aotCompileWarning": {
|
||||
"string": "Compilation and obfuscation only raise the cost of casual reading, copying, and automated analysis. They do not encrypt or completely hide the code: the player must receive executable JavaScript and a determined user can inspect, instrument, or modify it. Do not embed secrets, private keys, or security decisions in the project."
|
||||
},
|
||||
"warpTimer": {
|
||||
"string": "Warp Timer"
|
||||
},
|
||||
|
||||
+36
-3
@@ -142,6 +142,37 @@
|
||||
"version": "版本",
|
||||
"versionHelp": "更新作品版本是完全可选的,不会影响作品的。根据环境的不同,它可能会出现在关于页面等地方。格式应该像 X.Y.Z 这样",
|
||||
"warpTimer": "循环计时器",
|
||||
"aotCompile": "打包前编译项目",
|
||||
"compiledProtection": "编译项目保护(特色功能)",
|
||||
"compiledProtectionDescription": "SB3C Packager 的特色构建模式:预先编译积木图,并可替换 VM 成员访问、继续进行 JavaScript 变换。",
|
||||
"obfuscateCompiledProject": "混淆编译项目",
|
||||
"javascriptObfuscator": "对编译后的 factory 使用 JavaScript Obfuscator",
|
||||
"javascriptObfuscatorOptions": "JavaScript Obfuscator 选项",
|
||||
"compactOutput": "紧凑输出",
|
||||
"simplifyCode": "简化并变换表达式",
|
||||
"identifierNamesGenerator": "标识符样式",
|
||||
"stringArray": "将字符串字面量移入编码数组",
|
||||
"stringArrayEncoding": "字符串编码",
|
||||
"encodingNone": "不额外编码",
|
||||
"transformationThreshold": "变换概率(0–1)",
|
||||
"controlFlowFlattening": "控制流平坦化",
|
||||
"deadCodeInjection": "死代码注入",
|
||||
"splitStrings": "拆分字符串字面量",
|
||||
"splitStringsChunkLength": "字符串分块长度",
|
||||
"numbersToExpressions": "将数字转换为表达式",
|
||||
"transformObjectKeys": "变换对象键",
|
||||
"unicodeEscapeSequence": "使用 Unicode 转义序列",
|
||||
"disableConsoleOutput": "禁用控制台输出",
|
||||
"selfDefending": "自我保护输出",
|
||||
"selfDefendingWarning": "自我保护输出不能再次格式化或压缩;它会增大产物,也会让兼容性问题更难诊断。",
|
||||
"debugProtection": "开发者工具防调试",
|
||||
"debugProtectionInterval": "重复间隔(毫秒,0 表示只执行一次)",
|
||||
"debugProtectionGlobal": "防调试守卫只生成一份全局播放器脚本,不会重复注入每个编译入口和自定义过程。",
|
||||
"debugProtectionWarning": "防调试可能冻结或严重干扰浏览器开发者工具,定时重复时风险更高。仅在必要时使用 2000–4000 毫秒,并充分测试打包结果。",
|
||||
"javascriptObfuscatorPerformance": "控制流平坦化、死代码注入、RC4 字符串和自我保护可能显著增加构建时间、文件大小、启动时间和运行开销。",
|
||||
"obfuscationSeed": "混淆种子",
|
||||
"derivedFromProject": "留空时从项目派生",
|
||||
"aotCompileWarning": "编译和混淆只能提高直接阅读、复制及自动分析的成本,并不是加密,也无法完全隐藏代码:播放器必须获得可执行 JavaScript,有意分析者仍可检查、插桩或修改它。不要把密钥、私钥或安全判断写进项目。",
|
||||
"windowControls": "窗口控制栏",
|
||||
"zip": "Zip 压缩包(适合嵌入网站)",
|
||||
"zip-one-asset": "Zip 压缩包,把所有素材打包在一个文件中(不推荐)"
|
||||
@@ -152,12 +183,14 @@
|
||||
"cannotAccessProject": "无法访问作品令牌。这通常意味着此作品未共享、不存在或 ID 无效,或者您的网络正在屏蔽 turbowarp.org。",
|
||||
"cannotAccessProjectCaching": "如果作品刚刚分享,这条消息可能需要几分钟才消失。",
|
||||
"close": "关闭",
|
||||
"description1": "将 Scratch 作品转换为 HTML 文件、ZIP 压缩包或 Windows、MacOS 和 Linux 的可执行文件。",
|
||||
"description1": "编译并保护 Scratch 作品,再将其打包为 HTML、ZIP 压缩包或 Windows、macOS 和 Linux 应用程序。",
|
||||
"basedOn": "这是一个基于开源 {upstream} 修改的独立项目。",
|
||||
"basedOn-upstream": "TurboWarp Packager",
|
||||
"description2": "如果您只是想以一种简单的方式将 Scratch 官网作品嵌入您的网站,您可能会对 {embedding} 感兴趣。 ",
|
||||
"description2-embedding": "网站嵌入",
|
||||
"description3": "在 {onScratch} 或者 {onGitHub} 报告错误,提供反馈,并提出建议。",
|
||||
"description3-on": "{brand}",
|
||||
"disclaimer": "此网站不属于 Scratch、Scratch 团队或 Scratch 基金会。",
|
||||
"disclaimer": "这不是 TurboWarp 官方服务,也不属于 Scratch、Scratch 团队或 Scratch 基金会。",
|
||||
"documentation": "更多资源和帮助",
|
||||
"donate": "捐赠",
|
||||
"error": "错误",
|
||||
@@ -210,4 +243,4 @@
|
||||
"light": "明亮主题",
|
||||
"system": "使用系统主题"
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-10
@@ -14,7 +14,6 @@
|
||||
import {
|
||||
APP_NAME,
|
||||
FEEDBACK_PRIMARY,
|
||||
FEEDBACK_SECONDARY,
|
||||
ACCENT_COLOR,
|
||||
SOURCE_CODE,
|
||||
WEBSITE,
|
||||
@@ -156,16 +155,11 @@
|
||||
</p>
|
||||
<p>
|
||||
<ComplexMessage
|
||||
message={$_('p4.description3')}
|
||||
message={$_('p4.basedOn')}
|
||||
values={{
|
||||
// These placeholders are named this way for legacy reasons.
|
||||
onScratch: {
|
||||
text: $_('p4.description3-on').replace('{brand}', FEEDBACK_PRIMARY.name),
|
||||
href: FEEDBACK_PRIMARY.link
|
||||
},
|
||||
onGitHub: {
|
||||
text: $_('p4.description3-on').replace('{brand}', FEEDBACK_SECONDARY.name),
|
||||
href: FEEDBACK_SECONDARY.link
|
||||
upstream: {
|
||||
text: $_('p4.basedOn-upstream'),
|
||||
href: 'https://github.com/TurboWarp/packager'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -331,6 +331,11 @@
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
.suboptions {
|
||||
border-left: 3px solid #855cd6;
|
||||
margin: 10px 0 10px 8px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Section
|
||||
@@ -452,6 +457,161 @@
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
accent="#855CD6"
|
||||
reset={() => resetOptions(['compiler.aot'])}
|
||||
>
|
||||
<div>
|
||||
<h2>{$_('options.compiledProtection')}</h2>
|
||||
<p>{$_('options.compiledProtectionDescription')}</p>
|
||||
<div class="option">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.enabled}>
|
||||
{$_('options.aotCompile')}
|
||||
</label>
|
||||
</div>
|
||||
{#if $options.compiler.aot.enabled}
|
||||
<div class="suboptions" transition:slide|local>
|
||||
<div class="option">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.obfuscate}>
|
||||
{$_('options.obfuscateCompiledProject')}
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscator}>
|
||||
{$_('options.javascriptObfuscator')}
|
||||
</label>
|
||||
</div>
|
||||
<label class="option">
|
||||
{$_('options.obfuscationSeed')}
|
||||
<input type="text" bind:value={$options.compiler.aot.seed} placeholder={$_('options.derivedFromProject')}>
|
||||
</label>
|
||||
|
||||
{#if $options.compiler.aot.javascriptObfuscator}
|
||||
<details open>
|
||||
<summary>{$_('options.javascriptObfuscatorOptions')}</summary>
|
||||
<label class="option">
|
||||
<input type="checkbox"
|
||||
checked={$options.compiler.aot.javascriptObfuscatorOptions.selfDefending ||
|
||||
$options.compiler.aot.javascriptObfuscatorOptions.compact}
|
||||
disabled={$options.compiler.aot.javascriptObfuscatorOptions.selfDefending}
|
||||
on:change={(event) => {
|
||||
$options.compiler.aot.javascriptObfuscatorOptions.compact = event.target.checked;
|
||||
$options = $options;
|
||||
}}>
|
||||
{$_('options.compactOutput')}
|
||||
</label>
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.simplify}>
|
||||
{$_('options.simplifyCode')}
|
||||
</label>
|
||||
<label class="option">
|
||||
{$_('options.identifierNamesGenerator')}
|
||||
<select bind:value={$options.compiler.aot.javascriptObfuscatorOptions.identifierNamesGenerator}>
|
||||
<option value="hexadecimal">hexadecimal</option>
|
||||
<option value="mangled">mangled</option>
|
||||
<option value="mangled-shuffled">mangled-shuffled</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.stringArray}>
|
||||
{$_('options.stringArray')}
|
||||
</label>
|
||||
{#if $options.compiler.aot.javascriptObfuscatorOptions.stringArray}
|
||||
<label class="option nested">
|
||||
{$_('options.stringArrayEncoding')}
|
||||
<select bind:value={$options.compiler.aot.javascriptObfuscatorOptions.stringArrayEncoding}>
|
||||
<option value="none">{$_('options.encodingNone')}</option>
|
||||
<option value="base64">base64</option>
|
||||
<option value="rc4">rc4</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="option nested">
|
||||
{$_('options.transformationThreshold')}
|
||||
<input type="number" min="0" max="1" step="0.05"
|
||||
bind:value={$options.compiler.aot.javascriptObfuscatorOptions.stringArrayThreshold}>
|
||||
</label>
|
||||
{/if}
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.controlFlowFlattening}>
|
||||
{$_('options.controlFlowFlattening')}
|
||||
</label>
|
||||
{#if $options.compiler.aot.javascriptObfuscatorOptions.controlFlowFlattening}
|
||||
<label class="option nested">
|
||||
{$_('options.transformationThreshold')}
|
||||
<input type="number" min="0" max="1" step="0.05"
|
||||
bind:value={$options.compiler.aot.javascriptObfuscatorOptions.controlFlowFlatteningThreshold}>
|
||||
</label>
|
||||
{/if}
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.deadCodeInjection}>
|
||||
{$_('options.deadCodeInjection')}
|
||||
</label>
|
||||
{#if $options.compiler.aot.javascriptObfuscatorOptions.deadCodeInjection}
|
||||
<label class="option nested">
|
||||
{$_('options.transformationThreshold')}
|
||||
<input type="number" min="0" max="1" step="0.05"
|
||||
bind:value={$options.compiler.aot.javascriptObfuscatorOptions.deadCodeInjectionThreshold}>
|
||||
</label>
|
||||
{/if}
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.splitStrings}>
|
||||
{$_('options.splitStrings')}
|
||||
</label>
|
||||
{#if $options.compiler.aot.javascriptObfuscatorOptions.splitStrings}
|
||||
<label class="option nested">
|
||||
{$_('options.splitStringsChunkLength')}
|
||||
<input type="number" min="1" step="1"
|
||||
bind:value={$options.compiler.aot.javascriptObfuscatorOptions.splitStringsChunkLength}>
|
||||
</label>
|
||||
{/if}
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.numbersToExpressions}>
|
||||
{$_('options.numbersToExpressions')}
|
||||
</label>
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.transformObjectKeys}>
|
||||
{$_('options.transformObjectKeys')}
|
||||
</label>
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.unicodeEscapeSequence}>
|
||||
{$_('options.unicodeEscapeSequence')}
|
||||
</label>
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.disableConsoleOutput}>
|
||||
{$_('options.disableConsoleOutput')}
|
||||
</label>
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.selfDefending}>
|
||||
{$_('options.selfDefending')}
|
||||
</label>
|
||||
{#if $options.compiler.aot.javascriptObfuscatorOptions.selfDefending}
|
||||
<p class="warning">{$_('options.selfDefendingWarning')}</p>
|
||||
{/if}
|
||||
<label class="option">
|
||||
<input type="checkbox" bind:checked={$options.compiler.aot.javascriptObfuscatorOptions.debugProtection}>
|
||||
{$_('options.debugProtection')}
|
||||
</label>
|
||||
{#if $options.compiler.aot.javascriptObfuscatorOptions.debugProtection}
|
||||
<label class="option nested">
|
||||
{$_('options.debugProtectionInterval')}
|
||||
<input type="number" min="0" step="1000"
|
||||
bind:value={$options.compiler.aot.javascriptObfuscatorOptions.debugProtectionInterval}>
|
||||
</label>
|
||||
<p>{$_('options.debugProtectionGlobal')}</p>
|
||||
<p class="warning">{$_('options.debugProtectionWarning')}</p>
|
||||
{/if}
|
||||
</details>
|
||||
<p>{$_('options.javascriptObfuscatorPerformance')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="warning">{$_('options.aotCompileWarning')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
accent="#9966FF"
|
||||
reset={() => {
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
// These will be replaced at build-time by generate-service-worker-plugin.js
|
||||
const ASSETS = [/* __ASSETS__ */];
|
||||
// Use a string placeholder because production minification removes comments
|
||||
// before GenerateServiceWorkerPlugin replaces this value.
|
||||
const ASSETS = '__ASSETS__';
|
||||
const CACHE_NAME = __CACHE_NAME__;
|
||||
const IS_PRODUCTION = __IS_PRODUCTION__;
|
||||
|
||||
|
||||
+11
-9
@@ -30,23 +30,25 @@ If this is set to an empty string, the link is removed.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
APP_NAME: 'TurboWarp Packager',
|
||||
WEBSITE: 'https://packager.turbowarp.org/',
|
||||
COPYRIGHT_NOTICE: `Copyright (C) 2021-2024 Thomas Weber
|
||||
APP_NAME: 'SB3C Packager',
|
||||
WEBSITE: 'https://luyifei2011.github.io/tw-packager/',
|
||||
COPYRIGHT_NOTICE: `SB3C Packager modifications Copyright (C) 2026 SB3C Packager contributors
|
||||
|
||||
Based on TurboWarp Packager, Copyright (C) 2021-2024 Thomas Weber
|
||||
|
||||
The Source Code Form of this program is available under the terms of the
|
||||
Mozilla Public License, v. 2.0. You can obtain a copy of the license at
|
||||
https://www.mozilla.org/en-US/MPL/2.0/.`,
|
||||
ACCENT_COLOR: '#ff4c4c',
|
||||
SOURCE_CODE: 'https://github.com/TurboWarp/packager',
|
||||
SOURCE_CODE: 'https://github.com/LuYifei2011/tw-packager',
|
||||
FEEDBACK_PRIMARY: {
|
||||
name: 'Scratch',
|
||||
link: 'https://scratch.mit.edu/users/GarboMuffin/#comments'
|
||||
name: 'GitHub',
|
||||
link: 'https://github.com/LuYifei2011/tw-packager/issues'
|
||||
},
|
||||
FEEDBACK_SECONDARY: {
|
||||
name: 'GitHub',
|
||||
link: 'https://github.com/TurboWarp/packager/issues'
|
||||
name: 'TurboWarp Packager',
|
||||
link: 'https://github.com/TurboWarp/packager'
|
||||
},
|
||||
DONATE: '',
|
||||
PRIVACY_POLICY: 'https://turbowarp.org/privacy.html',
|
||||
PRIVACY_POLICY: '',
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
const loadDependencies = async () => {
|
||||
const [vmModule, storageModule, protection] = await Promise.all([
|
||||
import(/* webpackChunkName: "compiled-project-builder" */ 'scratch-vm'),
|
||||
import(/* webpackChunkName: "compiled-project-builder" */ '@turbowarp/scratch-storage'),
|
||||
import(/* webpackChunkName: "compiled-project-protection" */ 'sb3c-vm-obfuscator')
|
||||
]);
|
||||
return {
|
||||
VM: vmModule.default || vmModule,
|
||||
Storage: storageModule.default || storageModule,
|
||||
protection
|
||||
};
|
||||
};
|
||||
|
||||
const configureExtensionSecurity = (vm) => {
|
||||
const securityManager = vm.extensionManager && vm.extensionManager.securityManager;
|
||||
if (!securityManager) return;
|
||||
securityManager.getSandboxMode = () => 'unsandboxed';
|
||||
securityManager.canLoadExtensionFromProject = () => true;
|
||||
};
|
||||
|
||||
const loadJavaScriptObfuscator = async supplied => {
|
||||
if (supplied) return supplied.default || supplied;
|
||||
const imported = await import(
|
||||
/* webpackChunkName: "compiled-project-debug-protection" */ 'javascript-obfuscator'
|
||||
);
|
||||
return imported.default || imported;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create one player-level anti-debugging script. Applying debug protection to
|
||||
* each compiled factory would duplicate guards and timer loops in every
|
||||
* procedure, so it is intentionally separated from factory obfuscation.
|
||||
* @param {object} options debug protection options
|
||||
* @param {number} [options.interval=0] repeat interval in milliseconds; 0 disables repetition
|
||||
* @param {object} [implementation] injected javascript-obfuscator implementation
|
||||
* @returns {object} protection stage
|
||||
*/
|
||||
export const createGlobalDebugProtectionStage = (options = {}, implementation) => ({
|
||||
id: 'tw-packager-global-debug-protection',
|
||||
version: 1,
|
||||
kind: 'runtime-sidecar',
|
||||
mutatesFactories: false,
|
||||
async transform ({artifact, seed}) {
|
||||
const interval = Number(options.interval || 0);
|
||||
if (!Number.isFinite(interval) || interval < 0 || (interval > 0 && interval < 1000)) {
|
||||
throw new Error('debugProtectionInterval must be 0 or at least 1000 milliseconds');
|
||||
}
|
||||
const obfuscator = await loadJavaScriptObfuscator(implementation);
|
||||
if (!obfuscator || typeof obfuscator.obfuscate !== 'function') {
|
||||
throw new Error('Invalid javascript-obfuscator implementation');
|
||||
}
|
||||
const result = obfuscator.obfuscate('void 0;', {
|
||||
compact: true,
|
||||
debugProtection: true,
|
||||
debugProtectionInterval: interval,
|
||||
renameGlobals: false,
|
||||
renameProperties: false,
|
||||
seed,
|
||||
selfDefending: Boolean(options.selfDefending),
|
||||
sourceMap: false,
|
||||
target: 'browser'
|
||||
});
|
||||
return {
|
||||
artifact,
|
||||
sidecars: [{
|
||||
name: 'debug-protection.js',
|
||||
mediaType: 'text/javascript',
|
||||
content: result.getObfuscatedCode(),
|
||||
execution: 'global-script'
|
||||
}],
|
||||
diagnostics: {interval}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const getJavaScriptObfuscatorOptions = aot => {
|
||||
const configured = {...(aot.javascriptObfuscatorOptions || {})};
|
||||
const encoding = configured.stringArrayEncoding;
|
||||
if (typeof encoding === 'string') {
|
||||
configured.stringArrayEncoding = encoding === 'none' ? [] : [encoding];
|
||||
}
|
||||
delete configured.debugProtection;
|
||||
delete configured.debugProtectionInterval;
|
||||
return configured;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compile and optionally protect a project before it is embedded in a packaged application.
|
||||
* @param {ArrayBuffer|Uint8Array} projectData original project bytes
|
||||
* @param {object} options compiler options
|
||||
* @param {object} [hooks] non-serializable build hooks for advanced callers
|
||||
* @param {Function} [hooks.seedMaterial] public seed material selector
|
||||
* @param {Function} [hooks.deriveSeed] local or remote seed provider
|
||||
* @param {Function} [hooks.signManifest] integrity manifest signer
|
||||
* @param {object} [hooks.javascriptObfuscatorOptions] javascript-obfuscator options
|
||||
* @param {Array<object>} [hooks.stages] custom protection stages
|
||||
* @param {object} [dependencies] injected dependencies for tests
|
||||
* @returns {Promise<object>} compiled package, accessor sidecar, and diagnostics
|
||||
*/
|
||||
export const compileProject = async (projectData, options, hooks = {}, dependencies) => {
|
||||
const {VM, Storage, protection} = dependencies || await loadDependencies();
|
||||
const vm = new VM();
|
||||
vm.attachStorage(new Storage());
|
||||
configureExtensionSecurity(vm);
|
||||
vm.setCompilerOptions({enabled: true, warpTimer: Boolean(options.warpTimer)});
|
||||
|
||||
try {
|
||||
await vm.loadProject(projectData);
|
||||
const aot = options.aot;
|
||||
let artifact = vm.exportCompiledProject();
|
||||
let sidecars = [];
|
||||
let diagnostics = {stages: []};
|
||||
|
||||
if (aot.obfuscate || aot.javascriptObfuscator || hooks.stages) {
|
||||
const configuredObfuscatorOptions = {
|
||||
...getJavaScriptObfuscatorOptions(aot),
|
||||
...(hooks.javascriptObfuscatorOptions || {})
|
||||
};
|
||||
const debugProtection = Boolean(
|
||||
(aot.javascriptObfuscatorOptions && aot.javascriptObfuscatorOptions.debugProtection) ||
|
||||
configuredObfuscatorOptions.debugProtection
|
||||
);
|
||||
const debugProtectionInterval = configuredObfuscatorOptions.debugProtectionInterval !== undefined ?
|
||||
configuredObfuscatorOptions.debugProtectionInterval :
|
||||
(aot.javascriptObfuscatorOptions && aot.javascriptObfuscatorOptions.debugProtectionInterval);
|
||||
delete configuredObfuscatorOptions.debugProtection;
|
||||
delete configuredObfuscatorOptions.debugProtectionInterval;
|
||||
const stages = hooks.stages || [
|
||||
...(aot.obfuscate ? [protection.createVmAccessStage({
|
||||
accessorName: aot.accessorName,
|
||||
shortIds: aot.shortIds
|
||||
})] : []),
|
||||
...(aot.javascriptObfuscator ? [protection.createJavaScriptObfuscatorStage({
|
||||
options: configuredObfuscatorOptions
|
||||
})] : []),
|
||||
...(aot.javascriptObfuscator && debugProtection ? [
|
||||
createGlobalDebugProtectionStage({
|
||||
interval: debugProtectionInterval,
|
||||
selfDefending: configuredObfuscatorOptions.selfDefending
|
||||
})
|
||||
] : [])
|
||||
];
|
||||
const protectOptions = {stages};
|
||||
if (hooks.seedMaterial) protectOptions.seedMaterial = hooks.seedMaterial;
|
||||
if (hooks.deriveSeed) {
|
||||
protectOptions.deriveSeed = hooks.deriveSeed;
|
||||
} else if (aot.seed) {
|
||||
protectOptions.deriveSeed = () => ({
|
||||
seed: aot.seed,
|
||||
derivationId: 'tw-packager-explicit-v1'
|
||||
});
|
||||
}
|
||||
const result = await protection.protectCompiledProject(
|
||||
vm.exportCompiledProjectBuildInput(),
|
||||
protectOptions
|
||||
);
|
||||
artifact = result.artifact;
|
||||
sidecars = result.sidecars;
|
||||
diagnostics = result.diagnostics;
|
||||
}
|
||||
|
||||
const packageData = await vm.saveCompiledProject('arraybuffer', {
|
||||
artifact,
|
||||
integrity: hooks.signManifest ? {signManifest: hooks.signManifest} : {}
|
||||
});
|
||||
const accessorSidecar = sidecars.find(sidecar => sidecar.bindingName) || null;
|
||||
return {packageData, artifact, accessorSidecar, sidecars, diagnostics};
|
||||
} finally {
|
||||
vm.quit();
|
||||
}
|
||||
};
|
||||
|
||||
export default compileProject;
|
||||
@@ -12,6 +12,7 @@ import {OutdatedPackagerError} from '../common/errors';
|
||||
import {darken} from './colors';
|
||||
import {Adapter} from './adapter';
|
||||
import encodeBigString from './encode-big-string';
|
||||
import compileProject from './compile-project';
|
||||
|
||||
const PROGRESS_LOADED_SCRIPTS = 0.1;
|
||||
|
||||
@@ -186,6 +187,9 @@ class Packager extends EventTarget {
|
||||
this.options = Packager.DEFAULT_OPTIONS();
|
||||
this.aborted = false;
|
||||
this.used = false;
|
||||
// Functions and provider credentials belong here rather than in serializable UI options.
|
||||
this.compiledProjectHooks = {};
|
||||
this.compiledProjectRuntime = null;
|
||||
}
|
||||
|
||||
abort () {
|
||||
@@ -286,6 +290,38 @@ class Packager extends EventTarget {
|
||||
this.script = texts.join('\n').replace(/<\/script>/g,"</scri'+'pt>");
|
||||
}
|
||||
|
||||
async prepareCompiledProject () {
|
||||
const aot = this.options.compiler.aot;
|
||||
if (!aot || !aot.enabled) return;
|
||||
this.dispatchEvent(new CustomEvent('compile-progress', {detail: {progress: 0}}));
|
||||
const result = await compileProject(
|
||||
this.project.arrayBuffer,
|
||||
this.options.compiler,
|
||||
this.compiledProjectHooks
|
||||
);
|
||||
if (this.compiledProjectHooks.signManifest && !this.compiledProjectHooks.signatureVerifierSource) {
|
||||
throw new Error('Signed compiled projects require compiledProjectHooks.signatureVerifierSource');
|
||||
}
|
||||
this.ensureNotAborted();
|
||||
this.project = {
|
||||
...this.project,
|
||||
arrayBuffer: result.packageData,
|
||||
type: 'sb3c'
|
||||
};
|
||||
this.compiledProjectRuntime = {
|
||||
accessorSource: result.accessorSidecar ? result.accessorSidecar.content : null,
|
||||
bindingName: result.accessorSidecar ? result.accessorSidecar.bindingName : null,
|
||||
signed: Boolean(this.compiledProjectHooks.signManifest),
|
||||
signatureVerifierSource: this.compiledProjectHooks.signatureVerifierSource || null,
|
||||
runtimeScripts: result.sidecars
|
||||
.filter(sidecar => sidecar.execution === 'global-script')
|
||||
.map(sidecar => ({name: sidecar.name, content: sidecar.content}))
|
||||
};
|
||||
this.dispatchEvent(new CustomEvent('compile-progress', {
|
||||
detail: {progress: 1, diagnostics: result.diagnostics}
|
||||
}));
|
||||
}
|
||||
|
||||
computeWindowSize () {
|
||||
let width = this.options.stageWidth;
|
||||
let height = this.options.stageHeight;
|
||||
@@ -1078,7 +1114,8 @@ cd "$(dirname "$0")"
|
||||
}`;
|
||||
} else {
|
||||
let src;
|
||||
if (this.project.type === 'blob' || this.options.target === 'zip-one-asset') {
|
||||
if (this.project.type === 'blob' || this.project.type === 'sb3c' ||
|
||||
this.options.target === 'zip-one-asset') {
|
||||
isZip = this.project.type !== 'blob';
|
||||
src = './project.zip';
|
||||
storageProgressStart = PROGRESS_FETCHED_COMPRESSED;
|
||||
@@ -1146,7 +1183,7 @@ cd "$(dirname "$0")"
|
||||
if (!file) {
|
||||
throw new Error('project.json is not in zip');
|
||||
}
|
||||
return file.async('arraybuffer');
|
||||
${this.project.type === 'sb3c' ? 'return data;' : "return file.async('arraybuffer');"}
|
||||
});` : `
|
||||
storage.addWebStore(
|
||||
[
|
||||
@@ -1241,6 +1278,8 @@ cd "$(dirname "$0")"
|
||||
}
|
||||
this.used = true;
|
||||
this.ensureNotAborted();
|
||||
await this.prepareCompiledProject();
|
||||
this.ensureNotAborted();
|
||||
await this.loadResources();
|
||||
this.ensureNotAborted();
|
||||
const html = encodeBigString`<!DOCTYPE html>
|
||||
@@ -1660,10 +1699,28 @@ cd "$(dirname "$0")"
|
||||
}
|
||||
</script>` : ''}
|
||||
${await this.generateGetProjectData()}
|
||||
${this.project.type === 'sb3c' ? this.compiledProjectRuntime.runtimeScripts
|
||||
.map(script => `<script data-sb3c-runtime=${JSON.stringify(script.name)}>${
|
||||
script.content.replace(/<\/script>/gi, "</scri'+'pt>")
|
||||
}</script>`)
|
||||
.join('\n') : ''}
|
||||
<script>
|
||||
const run = async () => {
|
||||
const projectData = await getProjectData();
|
||||
await scaffolding.loadProject(projectData);
|
||||
${this.project.type === 'sb3c' ? `
|
||||
let bindings = {};
|
||||
${this.compiledProjectRuntime.accessorSource ? `
|
||||
const accessorModule = await import(${JSON.stringify(
|
||||
`data:text/javascript;charset=utf-8,${encodeURIComponent(this.compiledProjectRuntime.accessorSource)}`
|
||||
)});
|
||||
bindings = {[accessorModule.bindingName]: accessorModule.accessor};` : ''}
|
||||
await scaffolding.loadCompiledProjectPackage(projectData, {
|
||||
bindings,
|
||||
requireIntegrity: true,
|
||||
requireSignature: ${this.compiledProjectRuntime.signed},
|
||||
${this.compiledProjectRuntime.signatureVerifierSource ?
|
||||
`verifyIntegritySignature: (${this.compiledProjectRuntime.signatureVerifierSource})` : ''}
|
||||
});` : 'await scaffolding.loadProject(projectData);'}
|
||||
setProgress(1);
|
||||
loadingScreen.hidden = true;
|
||||
if (${this.options.autoplay}) {
|
||||
@@ -1757,7 +1814,8 @@ Packager.usesUnsafeOptions = (options) => {
|
||||
const getUnsafeOptions = (options) => [
|
||||
options.custom,
|
||||
options.extensions,
|
||||
options.cloudVariables.unsafeCloudBehaviors
|
||||
options.cloudVariables.unsafeCloudBehaviors,
|
||||
options.compiler.aot.javascriptObfuscatorOptions.debugProtection
|
||||
];
|
||||
return JSON.stringify(getUnsafeOptions(defaultOptions)) !== JSON.stringify(getUnsafeOptions(options));
|
||||
};
|
||||
@@ -1813,7 +1871,36 @@ Packager.DEFAULT_OPTIONS = () => ({
|
||||
},
|
||||
compiler: {
|
||||
enabled: true,
|
||||
warpTimer: false
|
||||
warpTimer: false,
|
||||
aot: {
|
||||
enabled: false,
|
||||
obfuscate: true,
|
||||
javascriptObfuscator: false,
|
||||
shortIds: true,
|
||||
accessorName: '__vmAccess',
|
||||
seed: '',
|
||||
javascriptObfuscatorOptions: {
|
||||
compact: true,
|
||||
controlFlowFlattening: false,
|
||||
controlFlowFlatteningThreshold: 0.75,
|
||||
deadCodeInjection: false,
|
||||
deadCodeInjectionThreshold: 0.4,
|
||||
debugProtection: false,
|
||||
debugProtectionInterval: 0,
|
||||
disableConsoleOutput: false,
|
||||
identifierNamesGenerator: 'hexadecimal',
|
||||
numbersToExpressions: false,
|
||||
selfDefending: false,
|
||||
simplify: true,
|
||||
splitStrings: false,
|
||||
splitStringsChunkLength: 10,
|
||||
stringArray: true,
|
||||
stringArrayEncoding: 'none',
|
||||
stringArrayThreshold: 0.75,
|
||||
transformObjectKeys: false,
|
||||
unicodeEscapeSequence: false
|
||||
}
|
||||
}
|
||||
},
|
||||
packagedRuntime: true,
|
||||
target: 'html',
|
||||
|
||||
@@ -468,6 +468,20 @@ class Scaffolding extends EventTarget {
|
||||
});
|
||||
}
|
||||
|
||||
loadCompiledProjectPackage (data, options) {
|
||||
if (typeof this.vm.loadCompiledProjectPackage !== 'function') {
|
||||
return Promise.reject(new Error('This Scratch VM does not support compiled project packages'));
|
||||
}
|
||||
return this.vm.loadCompiledProjectPackage(data, options)
|
||||
.then(() => {
|
||||
this.vm.setCloudProvider(this.cloudManager);
|
||||
this.cloudManager.projectReady();
|
||||
this.renderer.draw();
|
||||
setTimeout(() => this.renderer.draw());
|
||||
if (this.shouldConnectPeripherals) this._connectPeripherals();
|
||||
});
|
||||
}
|
||||
|
||||
setUsername (username) {
|
||||
this._username = username;
|
||||
this.vm.postIOData('userData', {
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
<body>
|
||||
<main>
|
||||
<h1>TurboWarp Packager Referer</h1>
|
||||
<p>If you're seeing requests to your website with this page in its <code>Referer</code> header, that probably means someone is using a program they created with the <a href="/">TurboWarp Packager</a> to interact with your website.</p>
|
||||
<p>If you're seeing requests to your website with this page in its <code>Referer</code> header, that probably means someone is using a program they created with the <a href="./">SB3C Packager</a> to interact with your website.</p>
|
||||
<p hidden class="package-name-outer">The creator of the program set their program's package name to "<code class="package-name-value"></code>". They can set this to anything they want, so that may or may not be meaningful.</p>
|
||||
<script>
|
||||
var packageName = location.hash.substring(1);
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
compileProject,
|
||||
createGlobalDebugProtectionStage
|
||||
} from '../../src/packager/compile-project';
|
||||
|
||||
const makeDependencies = () => {
|
||||
const calls = {};
|
||||
class VM {
|
||||
constructor () {
|
||||
calls.vm = this;
|
||||
}
|
||||
|
||||
attachStorage (storage) {
|
||||
calls.storage = storage;
|
||||
}
|
||||
|
||||
setCompilerOptions (options) {
|
||||
calls.compilerOptions = options;
|
||||
}
|
||||
|
||||
loadProject (data) {
|
||||
calls.projectData = data;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
exportCompiledProject () {
|
||||
return {format: 'scratch-vm-compiled-project', requiredBindings: []};
|
||||
}
|
||||
|
||||
exportCompiledProjectBuildInput () {
|
||||
return {artifact: this.exportCompiledProject(), project: {targets: []}};
|
||||
}
|
||||
|
||||
saveCompiledProject (type, options) {
|
||||
calls.save = {type, options};
|
||||
return Promise.resolve(new ArrayBuffer(4));
|
||||
}
|
||||
|
||||
quit () {
|
||||
calls.quit = true;
|
||||
}
|
||||
}
|
||||
class Storage {}
|
||||
const protection = {
|
||||
createVmAccessStage: jest.fn(options => ({id: 'vm-access', options})),
|
||||
createJavaScriptObfuscatorStage: jest.fn(options => ({id: 'javascript', options})),
|
||||
protectCompiledProject: jest.fn(async (input, options) => {
|
||||
calls.protect = {input, options};
|
||||
return {
|
||||
artifact: {...input.artifact, requiredBindings: ['__vmAccess']},
|
||||
sidecars: [{
|
||||
name: 'vm-accessor.mjs',
|
||||
bindingName: '__vmAccess',
|
||||
mediaType: 'text/javascript',
|
||||
content: 'export const accessor = {};'
|
||||
}],
|
||||
diagnostics: {stages: []}
|
||||
};
|
||||
})
|
||||
};
|
||||
return {calls, dependencies: {VM, Storage, protection}};
|
||||
};
|
||||
|
||||
test('compileProject applies ordered protection stages and packages the external artifact', async () => {
|
||||
const {calls, dependencies} = makeDependencies();
|
||||
const signManifest = jest.fn();
|
||||
const result = await compileProject(new Uint8Array([1, 2, 3]), {
|
||||
warpTimer: true,
|
||||
aot: {
|
||||
obfuscate: true,
|
||||
javascriptObfuscator: true,
|
||||
shortIds: true,
|
||||
accessorName: '__vmAccess',
|
||||
seed: 'release-seed'
|
||||
}
|
||||
}, {
|
||||
signManifest,
|
||||
javascriptObfuscatorOptions: {compact: true}
|
||||
}, dependencies);
|
||||
|
||||
expect(calls.compilerOptions).toEqual({enabled: true, warpTimer: true});
|
||||
expect(calls.protect.options.stages.map(stage => stage.id)).toEqual(['vm-access', 'javascript']);
|
||||
expect(calls.protect.options.deriveSeed()).toEqual({
|
||||
seed: 'release-seed',
|
||||
derivationId: 'tw-packager-explicit-v1'
|
||||
});
|
||||
expect(calls.save.type).toBe('arraybuffer');
|
||||
expect(calls.save.options.artifact.requiredBindings).toEqual(['__vmAccess']);
|
||||
expect(calls.save.options.integrity.signManifest).toBe(signManifest);
|
||||
expect(result.accessorSidecar.bindingName).toBe('__vmAccess');
|
||||
expect(calls.quit).toBe(true);
|
||||
});
|
||||
|
||||
test('compileProject can create an unobfuscated compiled package', async () => {
|
||||
const {calls, dependencies} = makeDependencies();
|
||||
const result = await compileProject(new ArrayBuffer(0), {
|
||||
warpTimer: false,
|
||||
aot: {
|
||||
obfuscate: false,
|
||||
javascriptObfuscator: false,
|
||||
shortIds: true,
|
||||
accessorName: '__vmAccess',
|
||||
seed: ''
|
||||
}
|
||||
}, {}, dependencies);
|
||||
|
||||
expect(dependencies.protection.protectCompiledProject).not.toHaveBeenCalled();
|
||||
expect(calls.save.options.artifact.requiredBindings).toEqual([]);
|
||||
expect(result.accessorSidecar).toBe(null);
|
||||
expect(calls.quit).toBe(true);
|
||||
});
|
||||
|
||||
test('compileProject can apply JavaScript obfuscation without the VM accessor stage', async () => {
|
||||
const {calls, dependencies} = makeDependencies();
|
||||
await compileProject(new ArrayBuffer(0), {
|
||||
warpTimer: false,
|
||||
aot: {
|
||||
obfuscate: false,
|
||||
javascriptObfuscator: true,
|
||||
shortIds: true,
|
||||
accessorName: '__vmAccess',
|
||||
seed: ''
|
||||
}
|
||||
}, {}, dependencies);
|
||||
|
||||
expect(calls.protect.options.stages.map(stage => stage.id)).toEqual(['javascript']);
|
||||
expect(dependencies.protection.createVmAccessStage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('debug protection is a single global stage and is not duplicated in factory options', async () => {
|
||||
const {calls, dependencies} = makeDependencies();
|
||||
await compileProject(new ArrayBuffer(0), {
|
||||
warpTimer: false,
|
||||
aot: {
|
||||
obfuscate: true,
|
||||
javascriptObfuscator: true,
|
||||
shortIds: true,
|
||||
accessorName: '__vmAccess',
|
||||
seed: '',
|
||||
javascriptObfuscatorOptions: {
|
||||
debugProtection: true,
|
||||
debugProtectionInterval: 3000,
|
||||
selfDefending: true
|
||||
}
|
||||
}
|
||||
}, {}, dependencies);
|
||||
|
||||
expect(calls.protect.options.stages.map(stage => stage.id)).toEqual([
|
||||
'vm-access',
|
||||
'javascript',
|
||||
'tw-packager-global-debug-protection'
|
||||
]);
|
||||
const factoryOptions = dependencies.protection.createJavaScriptObfuscatorStage.mock.calls[0][0].options;
|
||||
expect(factoryOptions.debugProtection).toBeUndefined();
|
||||
expect(factoryOptions.debugProtectionInterval).toBeUndefined();
|
||||
});
|
||||
|
||||
test('global debug protection stage emits one executable sidecar', async () => {
|
||||
const obfuscate = jest.fn(() => ({getObfuscatedCode: () => 'protected();'}));
|
||||
const stage = createGlobalDebugProtectionStage({interval: 2500, selfDefending: true}, {obfuscate});
|
||||
const artifact = {format: 'scratch-vm-compiled-project'};
|
||||
const result = await stage.transform({artifact, seed: 'derived-seed'});
|
||||
|
||||
expect(obfuscate).toHaveBeenCalledWith('void 0;', expect.objectContaining({
|
||||
debugProtection: true,
|
||||
debugProtectionInterval: 2500,
|
||||
selfDefending: true,
|
||||
seed: 'derived-seed'
|
||||
}));
|
||||
expect(result.artifact).toBe(artifact);
|
||||
expect(result.sidecars).toEqual([expect.objectContaining({
|
||||
name: 'debug-protection.js',
|
||||
execution: 'global-script',
|
||||
content: 'protected();'
|
||||
})]);
|
||||
});
|
||||
|
||||
test('compileProject always disposes the compiler VM after failure', async () => {
|
||||
const {calls, dependencies} = makeDependencies();
|
||||
dependencies.protection.protectCompiledProject.mockRejectedValueOnce(new Error('stage failed'));
|
||||
await expect(compileProject(new ArrayBuffer(0), {
|
||||
warpTimer: false,
|
||||
aot: {
|
||||
obfuscate: true,
|
||||
javascriptObfuscator: false,
|
||||
shortIds: true,
|
||||
accessorName: '__vmAccess',
|
||||
seed: ''
|
||||
}
|
||||
}, {}, dependencies)).rejects.toThrow('stage failed');
|
||||
expect(calls.quit).toBe(true);
|
||||
});
|
||||
@@ -27,3 +27,9 @@ test('unsafe cloud behaviors', () => {
|
||||
options.cloudVariables.unsafeCloudBehaviors = true;
|
||||
expect(Packager.usesUnsafeOptions(options)).toBe(true);
|
||||
});
|
||||
|
||||
test('debug protection', () => {
|
||||
const options = Packager.DEFAULT_OPTIONS();
|
||||
options.compiler.aot.javascriptObfuscatorOptions.debugProtection = true;
|
||||
expect(Packager.usesUnsafeOptions(options)).toBe(true);
|
||||
});
|
||||
+74
-1
@@ -14,6 +14,70 @@ const base = {
|
||||
};
|
||||
const dist = path.resolve(__dirname, 'dist');
|
||||
const buildId = isProduction ? require('./src/build/generate-scaffolding-build-id') : null;
|
||||
const nodeCompatibilityAliases = {
|
||||
'node:crypto': require.resolve('crypto-browserify'),
|
||||
'node:buffer': require.resolve('buffer/'),
|
||||
// The GitHub package exports ESM but its current package metadata has no
|
||||
// Webpack 4-compatible main/module field.
|
||||
'sb3c-vm-obfuscator$': path.resolve(__dirname, 'node_modules', 'sb3c-vm-obfuscator', 'src', 'index.mjs'),
|
||||
// Use the self-contained browser distribution in both targets. The Node
|
||||
// entry pulls in ESM dependencies which Webpack 4 cannot parse directly.
|
||||
'javascript-obfuscator$': path.resolve(__dirname, 'node_modules', 'javascript-obfuscator', 'dist', 'index.browser.js')
|
||||
};
|
||||
const compiledProjectJavaScriptRules = [
|
||||
{
|
||||
// Webpack 4 treats .mjs as strict ESM and otherwise rejects named imports
|
||||
// from CommonJS dependencies used by Babel's AST packages.
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.(?:mjs|js)$/,
|
||||
loader: 'babel-loader',
|
||||
include: [/node_modules[\\/]scratch-[^\\/]+[\\/]src/],
|
||||
options: {
|
||||
babelrc: false,
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
type: 'javascript/auto',
|
||||
loader: 'babel-loader',
|
||||
include: [/node_modules[\\/]sb3c-vm-obfuscator[\\/]src/],
|
||||
options: {
|
||||
babelrc: false,
|
||||
// javascript/auto enables CJS named-import interop for Babel's parser
|
||||
// packages while preserving ESM so Webpack can bundle relative modules.
|
||||
presets: [['@babel/preset-env', {modules: false}]]
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /javascript-obfuscator[\\/]dist[\\/]index\.browser\.js$/,
|
||||
type: 'javascript/auto',
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
babelrc: false,
|
||||
presets: [['@babel/preset-env', {modules: false}]]
|
||||
}
|
||||
},
|
||||
{
|
||||
// Its UMD wrapper assumes a browser global named `self`.
|
||||
loader: path.resolve(__dirname, 'src', 'build', 'global-self-loader.js')
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const compiledProjectAudioRule = {
|
||||
test: /\.(?:mp3|wav)$/i,
|
||||
use: [{
|
||||
// The compiler only needs extension block definitions, not sample bytes.
|
||||
loader: path.resolve(__dirname, 'src', 'build', 'noop-loader.js')
|
||||
}]
|
||||
};
|
||||
|
||||
const getVersion = () => {
|
||||
if (process.env.VERSION) {
|
||||
@@ -140,7 +204,8 @@ const makeWebsite = () => ({
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
svelte: path.resolve('node_modules', 'svelte')
|
||||
svelte: path.resolve('node_modules', 'svelte'),
|
||||
...nodeCompatibilityAliases
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.svelte'],
|
||||
mainFields: ['svelte', 'browser', 'module', 'main']
|
||||
@@ -153,6 +218,8 @@ const makeWebsite = () => ({
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
...compiledProjectJavaScriptRules,
|
||||
compiledProjectAudioRule,
|
||||
{
|
||||
test: /\.png|\.svg$/i,
|
||||
use: isStandalone ? {
|
||||
@@ -219,6 +286,10 @@ const makeNode = () => ({
|
||||
entry: {
|
||||
packager: './src/packager/node/export.js'
|
||||
},
|
||||
resolve: {
|
||||
alias: nodeCompatibilityAliases,
|
||||
extensions: ['.mjs', '.js']
|
||||
},
|
||||
externals: {
|
||||
'@turbowarp/jszip': '@turbowarp/jszip',
|
||||
'@turbowarp/sbdl': '@turbowarp/sbdl',
|
||||
@@ -228,6 +299,8 @@ const makeNode = () => ({
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
...compiledProjectJavaScriptRules,
|
||||
compiledProjectAudioRule,
|
||||
{
|
||||
test: /\.png|\.svg$/i,
|
||||
use: 'file-loader'
|
||||
|
||||
Reference in new issue
Block a user