@yufengtadian/freedom-cli 1.13.3 → 1.14.0-preview
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/lib/build.js +98 -36
- package/lib/cli.js +6 -2
- package/lib/release.js +22 -6
- package/lib/security.js +525 -45
- package/lib/shell.js +129 -9
- package/lib/verify.js +80 -26
- package/package.json +3 -2
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/pkg/freedom/freedom.go +4 -1
- package/templates/go/pkg/freedom/resources.go +7 -0
- package/templates/go/pkg/freedom/security.go +423 -39
package/lib/shell.js
CHANGED
|
@@ -176,6 +176,78 @@ function hasShell(plat) {
|
|
|
176
176
|
return fs.existsSync(localShellPath(plat));
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// ---- 发布代际预检(npm publish 前置门)----
|
|
180
|
+
// 随包壳是三平台预编译二进制、不随 git 入库;改了框架源却忘了重新同步壳,就会发出
|
|
181
|
+
// 「新 CLI + 旧壳」的混合代 tarball——FRDM3 断代下新 CLI 产的产物被旧壳直接拒跑
|
|
182
|
+
// (1.14.0-preview 发布前实测:三只壳的生成时间全部早于当时 security.go 的改动时间)。
|
|
183
|
+
// 判据取 mtime:壳文件的时间是「最后一次 download/build 同步」的证据,框架源比它新即可疑。
|
|
184
|
+
// 盲区如实说明:全新克隆里壳刚下载完必然最新,此时本门只证明「同步之后没再改过源」。
|
|
185
|
+
const PUBLISH_PLATFORMS = ['win-x64', 'darwin-arm64', 'linux-x64'];
|
|
186
|
+
|
|
187
|
+
// newestSourceMtime 遍历将编进壳的框架源(*.go / go.mod / go.sum,排除 _test.go)。
|
|
188
|
+
function newestSourceMtime(dir) {
|
|
189
|
+
let newest = 0;
|
|
190
|
+
let file = null;
|
|
191
|
+
const stack = [dir];
|
|
192
|
+
while (stack.length) {
|
|
193
|
+
const d = stack.pop();
|
|
194
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
195
|
+
const p = path.join(d, e.name);
|
|
196
|
+
if (e.isDirectory()) {
|
|
197
|
+
stack.push(p);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!/\.(go|mod|sum)$/.test(e.name) || e.name.endsWith('_test.go')) continue;
|
|
201
|
+
const t = fs.statSync(p).mtimeMs;
|
|
202
|
+
if (t > newest) {
|
|
203
|
+
newest = t;
|
|
204
|
+
file = p;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { newest, file };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function checkBundledShells(opts = {}) {
|
|
212
|
+
const plats = opts.platforms || PUBLISH_PLATFORMS;
|
|
213
|
+
const tplDir = opts.goTemplateDir || goTemplateDir();
|
|
214
|
+
const shellsDir = opts.shellsDir || shellDir();
|
|
215
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
216
|
+
const rel = (p, base) => path.relative(base, p).split(path.sep).join('/');
|
|
217
|
+
const problems = [];
|
|
218
|
+
const src = newestSourceMtime(tplDir);
|
|
219
|
+
if (!src.newest) problems.push(`预检无法判定代际:${tplDir} 下找不到 *.go 框架源`);
|
|
220
|
+
for (const plat of plats) {
|
|
221
|
+
const exe = path.join(shellsDir, plat, SHELL_EXE_NAME[plat] || 'freedom-shell');
|
|
222
|
+
if (!fs.existsSync(exe)) {
|
|
223
|
+
problems.push(`${plat}: 缺随包壳 ${rel(exe, shellsDir)}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const t = fs.statSync(exe).mtimeMs;
|
|
227
|
+
if (src.newest && t < src.newest) {
|
|
228
|
+
problems.push(`${plat}: 壳生成于 ${iso(t)},早于框架源最新改动 ${iso(src.newest)}(${rel(src.file, tplDir)})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return problems;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function preflightBundledShells(opts = {}) {
|
|
235
|
+
const problems = checkBundledShells(opts);
|
|
236
|
+
if (!problems.length) {
|
|
237
|
+
process.stdout.write('[freedom] 发布预检通过:随包壳不早于框架源\n');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// 走 stderr + 退出码而非抛错:本函数由 npm 的 prepublishOnly 直接调用,抛错会带一串
|
|
241
|
+
// 对用户无用的堆栈,把「该重编哪几只壳」这句关键信息挤到屏幕外。
|
|
242
|
+
process.stderr.write(
|
|
243
|
+
'freedom: 随包壳与框架源代际不一致,禁止发布(发出即成混合代 tarball,新 CLI 的产物会被旧壳拒跑):\n - ' +
|
|
244
|
+
problems.join('\n - ') +
|
|
245
|
+
'\n处置:先推 tag 让 CI 产壳,再 freedom shell download <plat> 覆盖 shell/<plat>' +
|
|
246
|
+
'(本机有工具链时 freedom shell build <plat>),然后重跑发布。\n'
|
|
247
|
+
);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
179
251
|
// ---- 代理支持(修复:本地无 linux/mac 壳时自动下载必失败的历史根因) ----
|
|
180
252
|
// 背景:GitHub Releases 资产重定向到 S3,Node 内置 fetch 直连在受限网络下
|
|
181
253
|
// body 下载会卡死(可拿到 200 响应头但 arrayBuffer 永远不结束)。本机若跑
|
|
@@ -387,7 +459,16 @@ async function fetchShellDirect(plat, url, dest) {
|
|
|
387
459
|
|
|
388
460
|
// 本地用 Go 编译指定平台壳(需要 Go + 该平台编译环境)。
|
|
389
461
|
// Windows 产物以 GUI 子系统编译(-H windowsgui),运行时无 cmd 黑窗。
|
|
390
|
-
|
|
462
|
+
//
|
|
463
|
+
// opts.dest:产物落点(默认为包内通用壳位置)。high 模式传临时路径,编出的是
|
|
464
|
+
// 本应用专属壳(Tier B),不得覆盖通用壳缓存。
|
|
465
|
+
// opts.inject:{-X 符号路径: 值} 映射,用于把信任锚编进壳(见 lib/security.js shellInject)。
|
|
466
|
+
// 值必须是命令行安全字符集——这里拼进单个 -ldflags 字符串,含空格即串味。
|
|
467
|
+
// opts.stageGoFiles:{文件名: Go 源码} —— 编壳时**本次构建专属**的 Go 源文件(Tier B 的
|
|
468
|
+
// keyslot 装配码),经 `go build -overlay` 虚拟进 pkg/freedom。刻意不落进 templates/go:
|
|
469
|
+
// 那是随 npm 包分发的公共树,写进去一次就等于把某个产物的主密钥编进之后所有壳(含通用壳),
|
|
470
|
+
// 还会撞上 CI 的 Template mirror 门。overlay 让它只活在这次构建的这一个进程里。
|
|
471
|
+
function buildShell(plat, opts = {}) {
|
|
391
472
|
plat = requirePlatform(plat);
|
|
392
473
|
// 本机只能编译本机平台(webview_go 依赖系统 WebView 框架,无法交叉编译)。
|
|
393
474
|
// 这道判断刻意前置于 Go 探测:平台不匹配是与工具有关的硬事实,先报工具有关
|
|
@@ -407,34 +488,71 @@ function buildShell(plat) {
|
|
|
407
488
|
'或手动获取 https://go.dev/dl/ ,或改用 freedom shell download 拉取预编译壳。'
|
|
408
489
|
);
|
|
409
490
|
}
|
|
410
|
-
const dest = localShellPath(plat);
|
|
491
|
+
const dest = opts.dest ? path.resolve(opts.dest) : localShellPath(plat);
|
|
411
492
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
412
493
|
|
|
413
494
|
const buildDir = goTemplateDir();
|
|
414
495
|
// 剥离符号与调试信息(-s -w)+ 抹掉构建期绝对路径(-trimpath):
|
|
415
496
|
// 通用壳里编译进了 high 模式的密钥派生逻辑,函数名/DWARF 是给逆向者的地图。
|
|
416
497
|
const ldflags = ['-s', '-w'];
|
|
498
|
+
for (const [sym, val] of Object.entries(opts.inject || {})) {
|
|
499
|
+
if (!/^[A-Za-z0-9_./:-]+$/.test(val)) {
|
|
500
|
+
throw new Error(`-X 注入值含非法字符(仅允许字母数字与 _./:-):${sym}`);
|
|
501
|
+
}
|
|
502
|
+
ldflags.push('-X', `${sym}=${val}`);
|
|
503
|
+
}
|
|
417
504
|
if (plat.startsWith('win')) ldflags.push('-H', 'windowsgui');
|
|
418
505
|
// Linux:新发行版只有 webkit2gtk-4.1,需要 -tags webkit2_41(探测与注入见 lib/webkit.js)。
|
|
419
506
|
const webkit = applyWebkitTags(process.env);
|
|
420
507
|
if (webkit.tags.length) {
|
|
421
508
|
process.stdout.write(`[freedom] 本机 WebKitGTK 仅有 4.1,构建加 -tags ${webkit.tags.join(',')}\n`);
|
|
422
509
|
}
|
|
423
|
-
|
|
510
|
+
// 本次构建专属的 Go 源文件经 overlay 虚拟进壳的包目录(见函数注释)。
|
|
511
|
+
const staged = stageOverlayFiles(buildDir, opts.stageGoFiles);
|
|
512
|
+
const args = ['build', '-trimpath'];
|
|
513
|
+
if (staged.overlayPath) args.push(`-overlay=${staged.overlayPath}`);
|
|
514
|
+
args.push('-ldflags', ldflags.join(' '), '-o', dest, '.');
|
|
515
|
+
const build = spawnSync('go', args, {
|
|
424
516
|
cwd: buildDir,
|
|
425
517
|
encoding: 'utf8',
|
|
426
518
|
env: Object.assign({ CGO_ENABLED: '1' }, webkit.env),
|
|
427
519
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
520
|
+
try {
|
|
521
|
+
if (build.error || build.status !== 0) {
|
|
522
|
+
const hint = plat.startsWith('linux') && /webkit2gtk-4\.0/.test(`${build.stdout}${build.stderr}`)
|
|
523
|
+
? `\n提示:本机未装 webkit2gtk-4.0/4.1 开发库。Ubuntu/Debian 执行 ` +
|
|
524
|
+
`sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev(老发行版用 4.0)后重试。`
|
|
525
|
+
: '';
|
|
526
|
+
throw new Error(`Go 编译失败:\n${build.stdout}\n${build.stderr}${hint}`);
|
|
527
|
+
}
|
|
528
|
+
} finally {
|
|
529
|
+
staged.cleanup();
|
|
434
530
|
}
|
|
435
531
|
return dest;
|
|
436
532
|
}
|
|
437
533
|
|
|
534
|
+
// stageOverlayFiles 把 {文件名: 源码} 写成临时文件并产出一份 go command 认的 overlay 清单
|
|
535
|
+
// (Replace: 虚拟路径 → 真实路径)。返回 { overlayPath, cleanup };无文件时 overlayPath 为 null。
|
|
536
|
+
// 虚拟路径必须落在目标包目录下,否则不会被视为该包的源文件。
|
|
537
|
+
function stageOverlayFiles(buildDir, files) {
|
|
538
|
+
const entries = Object.entries(files || {});
|
|
539
|
+
if (!entries.length) return { overlayPath: null, cleanup() {} };
|
|
540
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'freedom-stage-'));
|
|
541
|
+
const replace = {};
|
|
542
|
+
for (const [name, src] of entries) {
|
|
543
|
+
if (!/^keyslot_[0-9a-f]{6}\.go$/.test(name)) {
|
|
544
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
545
|
+
throw new Error(`internal: 不接受的壳源文件名 ${name}(只允许 keyslot_<tag>.go)`);
|
|
546
|
+
}
|
|
547
|
+
const real = path.join(dir, name);
|
|
548
|
+
fs.writeFileSync(real, src, 'utf8');
|
|
549
|
+
replace[path.join(buildDir, 'pkg', 'freedom', name)] = real;
|
|
550
|
+
}
|
|
551
|
+
const overlayPath = path.join(dir, 'overlay.json');
|
|
552
|
+
fs.writeFileSync(overlayPath, JSON.stringify({ Replace: replace }), 'utf8');
|
|
553
|
+
return { overlayPath, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
|
|
554
|
+
}
|
|
555
|
+
|
|
438
556
|
function nativePlatformKey() {
|
|
439
557
|
// 已废弃:统一使用 utils.nativePlatform(B44),本函数保留仅作内部兜底,勿再调用。
|
|
440
558
|
return nativePlatform();
|
|
@@ -450,4 +568,6 @@ module.exports = {
|
|
|
450
568
|
detectShellFormat,
|
|
451
569
|
expectedFormat,
|
|
452
570
|
validateLocalShell,
|
|
571
|
+
checkBundledShells,
|
|
572
|
+
preflightBundledShells,
|
|
453
573
|
};
|
package/lib/verify.js
CHANGED
|
@@ -9,8 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
12
13
|
const { isWinPlat, platformExeName } = require('./utils');
|
|
13
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
APP_BIN_MAGIC,
|
|
16
|
+
APP_BIN_MAGIC3,
|
|
17
|
+
decryptAppV3,
|
|
18
|
+
loadProductKey,
|
|
19
|
+
verifyIntegrityV3,
|
|
20
|
+
signingKeyPath,
|
|
21
|
+
rawPubHexFromKey,
|
|
22
|
+
sha256hex,
|
|
23
|
+
} = require('./security');
|
|
14
24
|
|
|
15
25
|
// ---- 产物定位 ----
|
|
16
26
|
|
|
@@ -67,8 +77,66 @@ function fmtSize(n) {
|
|
|
67
77
|
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
// checkSignedProduct —— FRDM3 high 产物的签名链复核,返回解密载荷(失败返回 null)。
|
|
81
|
+
//
|
|
82
|
+
// 三步与壳的启动校验同源同序(先比锚、再验签、后解密),verify 绿即启动必绿:
|
|
83
|
+
// 1) .integrity 用发布方公钥验签,并把 appBin/identity/salt 与真实产物逐项比对;
|
|
84
|
+
// 2) claims.self 与 exe 本体哈希比对(壳被替换或 build 后改写 exe 即红);
|
|
85
|
+
// 3) 用每产物主密钥真解一次容器(认证失败 = 内容被篡改)。
|
|
86
|
+
// 信任锚来自项目内的发布方私钥(freedom keygen 资产):私钥不在就无从判真伪,
|
|
87
|
+
// 按红处理而不是跳过——"没钥匙所以当作没问题"正是降级攻击要走的那一步。
|
|
88
|
+
function checkSignedProduct({ projectDir, resDir, bin, appName, exePath, fail, okc }) {
|
|
89
|
+
let manifest;
|
|
90
|
+
try {
|
|
91
|
+
manifest = JSON.parse(fs.readFileSync(path.join(resDir, '.integrity'), 'utf8'));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
fail('签名清单', `.integrity 读取/解析失败:${e.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const privPath = signingKeyPath(projectDir);
|
|
97
|
+
if (!fs.existsSync(privPath)) {
|
|
98
|
+
fail('签名清单', `缺信任锚(发布方私钥 ${privPath} 不存在):无法判定清单真伪,请先 freedom keygen 或用同钥匙重新构建`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
let anchorPubHex;
|
|
102
|
+
try {
|
|
103
|
+
anchorPubHex = rawPubHexFromKey(crypto.createPrivateKey(fs.readFileSync(privPath)));
|
|
104
|
+
} catch (e) {
|
|
105
|
+
fail('签名清单', `发布方私钥不可用:${e.message}`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
let claims;
|
|
109
|
+
try {
|
|
110
|
+
claims = verifyIntegrityV3({ manifest, anchorPubHex, name: appName, appBin: bin });
|
|
111
|
+
okc('签名清单', `ed25519 验签通过,且清单与 app.bin/应用标识/容器盐逐项吻合(built ${claims.built})`);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
fail('签名清单', e.message);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (claims.self) {
|
|
117
|
+
try {
|
|
118
|
+
const got = sha256hex(fs.readFileSync(exePath));
|
|
119
|
+
if (got !== claims.self.toLowerCase()) {
|
|
120
|
+
fail('exe 自绑定', `exe 哈希与清单不符(清单 ${claims.self.slice(0, 8)}…,实际 ${got.slice(0, 8)}…):壳被替换,或 build 之后改写过 exe(含事后补做代码签名)`);
|
|
121
|
+
} else {
|
|
122
|
+
okc('exe 自绑定', `exe 哈希与签名清单一致(${got.slice(0, 8)}…)`);
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
fail('exe 自绑定', e.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const p = decryptAppV3(loadProductKey(projectDir, appName), appName, bin);
|
|
130
|
+
okc('容器解密', `认证通过,html ${fmtSize(p.html.length)} / config ${fmtSize(p.config.length)}`);
|
|
131
|
+
return p;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
fail('容器解密', e.message);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
70
138
|
// 单平台产物校验,返回 [{ name, pass, detail }]。
|
|
71
|
-
function checkPlatformProduct({ targetDir, plat, appName, hasBackend }) {
|
|
139
|
+
function checkPlatformProduct({ projectDir, targetDir, plat, appName, hasBackend }) {
|
|
72
140
|
const checks = [];
|
|
73
141
|
const fail = (name, detail) => checks.push({ name, pass: false, detail });
|
|
74
142
|
const okc = (name, detail) => checks.push({ name, pass: true, detail });
|
|
@@ -117,34 +185,20 @@ function checkPlatformProduct({ targetDir, plat, appName, hasBackend }) {
|
|
|
117
185
|
}
|
|
118
186
|
if (!hasIntegrity) fail('完整性清单', '缺失 .integrity');
|
|
119
187
|
else okc('完整性清单', '.integrity 存在');
|
|
120
|
-
// app.bin
|
|
188
|
+
// app.bin 容器代际校验。FRDM2 及其以前一律红:壳侧同样拒收旧代际(其主密钥与清单
|
|
189
|
+
// 密钥都随 npm 包公开,人人可伪造),verify 放行就会出现"自检绿、启动红"。
|
|
121
190
|
let bin = Buffer.alloc(0);
|
|
122
191
|
try {
|
|
123
192
|
bin = fs.readFileSync(path.join(resDir, 'app.bin'));
|
|
124
193
|
} catch (e) { /* 读取失败按不通过处理 */ }
|
|
125
|
-
const head = bin.subarray(0,
|
|
126
|
-
if (head
|
|
127
|
-
fail('app.bin 容器',
|
|
194
|
+
const head = bin.subarray(0, APP_BIN_MAGIC3.length).toString('latin1');
|
|
195
|
+
if (head === APP_BIN_MAGIC) {
|
|
196
|
+
fail('app.bin 容器', `${APP_BIN_MAGIC} 属旧代际(密钥在公开源里,清单可被任意伪造):请用 freedom-cli 1.14.0-preview 及以上重新 build(预览代走 preview 通道:npm i -D @yufengtadian/freedom-cli@preview)`);
|
|
197
|
+
} else if (head !== APP_BIN_MAGIC3) {
|
|
198
|
+
fail('app.bin 容器', `容器头不是 ${APP_BIN_MAGIC3}(实际 "${head}"),文件损坏、非本工具产物或旧代产物`);
|
|
128
199
|
} else {
|
|
129
|
-
okc('app.bin 容器', `${
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
securePayload = decryptApp(appName, bin);
|
|
133
|
-
okc('容器解密', `认证通过,html ${fmtSize(securePayload.html.length)} / config ${fmtSize(securePayload.config.length)}`);
|
|
134
|
-
} catch (e) {
|
|
135
|
-
fail('容器解密', e.message);
|
|
136
|
-
}
|
|
137
|
-
// .integrity 与容器一致(防整体替换:清单值由构建期容器盐派生密钥签名)
|
|
138
|
-
if (hasIntegrity) {
|
|
139
|
-
try {
|
|
140
|
-
const list = JSON.parse(fs.readFileSync(path.join(resDir, '.integrity'), 'utf8'));
|
|
141
|
-
const want = buildIntegrity(appName, bin).appBin;
|
|
142
|
-
if (list.appBin !== want) fail('完整性清单', '.integrity 与 app.bin 不一致(产物可能被替换)');
|
|
143
|
-
else okc('完整性清单', '.integrity 与 app.bin 一致');
|
|
144
|
-
} catch (e) {
|
|
145
|
-
fail('完整性清单', e.message);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
200
|
+
okc('app.bin 容器', `${APP_BIN_MAGIC3} 头有效(${fmtSize(bin.length)})`);
|
|
201
|
+
securePayload = checkSignedProduct({ projectDir, resDir, bin, appName, exePath, fail, okc });
|
|
148
202
|
}
|
|
149
203
|
} else {
|
|
150
204
|
if (hasIntegrity) {
|
|
@@ -209,7 +263,7 @@ async function verifyProduct(projectDir, opts = {}) {
|
|
|
209
263
|
|
|
210
264
|
const targets = findProducts(dir, cfg)
|
|
211
265
|
.filter((t) => !opts.platform || t.plat === opts.platform)
|
|
212
|
-
.map((t) => ({ ...t, checks: checkPlatformProduct({ targetDir: t.dir, plat: t.plat, appName, hasBackend: hasBackendDir }) }));
|
|
266
|
+
.map((t) => ({ ...t, checks: checkPlatformProduct({ projectDir: dir, targetDir: t.dir, plat: t.plat, appName, hasBackend: hasBackendDir }) }));
|
|
213
267
|
|
|
214
268
|
const ok = targets.length > 0 && targets.every((t) => t.checks.every((c) => c.pass));
|
|
215
269
|
return { ok, targets, appName };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yufengtadian/freedom-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0-preview",
|
|
4
4
|
"description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"postinstall": "node postinstall.js"
|
|
36
|
+
"postinstall": "node postinstall.js",
|
|
37
|
+
"prepublishOnly": "node -e \"require('./lib/shell').preflightBundledShells()\""
|
|
37
38
|
},
|
|
38
39
|
"engines": {
|
|
39
40
|
"node": ">=18"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -251,7 +251,10 @@ func (a *App) Run() {
|
|
|
251
251
|
var se *secureFatalError
|
|
252
252
|
if errors.As(err, &se) {
|
|
253
253
|
fmt.Printf("freedom: 安全模式资源校验失败,拒绝运行:%v\n", err)
|
|
254
|
-
|
|
254
|
+
// 退出码必须非零:Run 返回 nil 会让"拒跑"与"正常退出"在脚本与 CI 眼里同形,
|
|
255
|
+
// 而这条分支存在的意义正是"不能让它悄悄跑起来"。此处尚无 defer 注册
|
|
256
|
+
// (cleanupSecureBackend 在下一行才 defer),os.Exit 不会跳过清理。
|
|
257
|
+
os.Exit(exitSecureFatal)
|
|
255
258
|
}
|
|
256
259
|
fmt.Printf("freedom: warning: %v\n", err)
|
|
257
260
|
}
|
|
@@ -34,6 +34,11 @@ func secureFatal(err error) error {
|
|
|
34
34
|
return &secureFatalError{err: err}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// exitSecureFatal 是 high 模式拒绝运行时的进程退出码(见 freedom.go Run)。
|
|
38
|
+
// 取 70 —— BSD sysexits 的 EX_SOFTWARE:"这份产物本身不可信",与 1(一般失败)、
|
|
39
|
+
// 130(Ctrl+C 收尾)区分开,发布脚本据此认出这一具体事件。
|
|
40
|
+
const exitSecureFatal = 70
|
|
41
|
+
|
|
37
42
|
// runtimeBackend 描述 config.json 中的后端进程配置(任意语言,经 stdio NDJSON 桥接)。
|
|
38
43
|
type runtimeBackend struct {
|
|
39
44
|
Command string `json:"command"`
|
|
@@ -106,6 +111,8 @@ func (a *App) loadRuntimeConfig() error {
|
|
|
106
111
|
return secureFatal(fmt.Errorf("后端源码解密落地失败:%w", err))
|
|
107
112
|
}
|
|
108
113
|
a.secureBackendDir = bdir
|
|
114
|
+
// 落盘完成即擦:载荷缓存会活到进程结束,后端源码没有理由跟着一起常驻。
|
|
115
|
+
scrubSecureBackendPayload(p)
|
|
109
116
|
}
|
|
110
117
|
a.applyRuntimeConfig(&rc)
|
|
111
118
|
// high 模式强制关闭 WebView 开发者工具,防止前端源码经 devtools 直接查看。
|