@yufengtadian/freedom-cli 1.13.2 → 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.
Files changed (58) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +74 -21
  3. package/lib/agents.js +182 -52
  4. package/lib/build.js +119 -42
  5. package/lib/cli.js +106 -9
  6. package/lib/release.js +22 -6
  7. package/lib/security.js +525 -45
  8. package/lib/shell.js +288 -64
  9. package/lib/toolchain.js +339 -0
  10. package/lib/utils.js +51 -0
  11. package/lib/verify.js +85 -30
  12. package/lib/webkit.js +45 -0
  13. package/package.json +4 -3
  14. package/shell/darwin-arm64/freedom-shell +0 -0
  15. package/shell/linux-x64/freedom-shell +0 -0
  16. package/shell/win-x64/freedom-shell.exe +0 -0
  17. package/skill/freedom/SKILL.md +3 -3
  18. package/templates/desktop/backend/desktop.mjs +16 -6
  19. package/templates/desktop/freedom.config.js +4 -1
  20. package/templates/go/go.mod +3 -0
  21. package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
  22. package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
  23. package/templates/go/pkg/freedom/anti_debug_windows.go +136 -12
  24. package/templates/go/pkg/freedom/assets_embed.go +1 -0
  25. package/templates/go/pkg/freedom/backend_proc.go +5 -5
  26. package/templates/go/pkg/freedom/events_windows.go +5 -5
  27. package/templates/go/pkg/freedom/freedom.go +17 -3
  28. package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
  29. package/templates/go/pkg/freedom/resources.go +59 -1
  30. package/templates/go/pkg/freedom/security.go +475 -41
  31. package/templates/go/pkg/freedom/shutdown.go +55 -0
  32. package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
  33. package/templates/go/pkg/freedom/store.go +3 -3
  34. package/templates/go/pkg/freedom/syscap_windows.go +33 -33
  35. package/templates/go/pkg/freedom/sysint_windows.go +2 -2
  36. package/templates/go/pkg/freedom/tray_windows.go +18 -18
  37. package/templates/go/pkg/freedom/updater.go +3 -3
  38. package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
  39. package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
  40. package/templates/go/third_party/webview_go/LICENSE +22 -0
  41. package/templates/go/third_party/webview_go/README.md +50 -0
  42. package/templates/go/third_party/webview_go/glue.c +36 -0
  43. package/templates/go/third_party/webview_go/go.mod +3 -0
  44. package/templates/go/third_party/webview_go/go.sum +0 -0
  45. package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
  46. package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  47. package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
  48. package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
  49. package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
  50. package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
  51. package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
  52. package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
  53. package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
  54. package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
  55. package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
  56. package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
  57. package/templates/go/third_party/webview_go/webview.cc +1 -0
  58. package/templates/go/third_party/webview_go/webview.go +332 -0
@@ -0,0 +1,339 @@
1
+ 'use strict';
2
+
3
+ // 工具链自动装配扩展:探测 Go / Rust / C++ 是否就位,缺失时给出安装动作(--apply 才真跑),
4
+ // 并把镜像源等编译配置优化写进各工具自己的配置位。
5
+ //
6
+ // 「检测到配置好就不去检测」:探测要 spawn 子进程跑 --version,三平台冷启动累计几百毫秒,
7
+ // 故 ok 结论进缓存(按 PATH 指纹 + TTL 失效)。失败结论**永不入缓存**——刚装好的工具必须
8
+ // 当场被认出来,否则用户装完再跑一次会说「没装」,那是比慢得多的缺陷。
9
+ //
10
+ // 安装动作默认只打印不执行:这些命令要联网、要管理员权限、会改系统,属高危面(见 AGENTS.md
11
+ // 铁律 8 口径),必须 --apply 显式授权。
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const CACHE_VERSION = 1;
19
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
20
+
21
+ const SPECS = [
22
+ {
23
+ key: 'go',
24
+ name: 'Go 工具链',
25
+ why: '编译 Freedom 壳与内嵌 Go 后端(freedom shell build / cmd/freedom build)',
26
+ probes: [{ exe: 'go', args: ['version'], re: /go(\d+\.\d+(?:\.\d+)?)/ }],
27
+ manual: 'https://go.dev/dl/',
28
+ install: {
29
+ win32: [{ cmd: 'winget', args: ['install', '-e', '--id', 'GoLang.Go'] }],
30
+ darwin: [{ cmd: 'brew', args: ['install', 'go'] }],
31
+ linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'golang-go'] }],
32
+ },
33
+ },
34
+ {
35
+ key: 'rust',
36
+ name: 'Rust 工具链',
37
+ why: '编译 examples/multiproc 的 Rust 后端(rustc -O 单文件,不走 cargo)',
38
+ probes: [
39
+ { exe: 'rustc', args: ['--version'], re: /rustc\s+(\d+\.\d+(?:\.\d+)?)/ },
40
+ { exe: 'rustc', args: ['--version'], re: /rustc\s+(\d+\.\d+(?:\.\d+)?)/, homeRelative: true },
41
+ ],
42
+ manual: 'https://rustup.rs/',
43
+ install: {
44
+ win32: [{ cmd: 'winget', args: ['install', '-e', '--id', 'Rustlang.Rustup'] }],
45
+ darwin: [{ cmd: 'brew', args: ['install', 'rust'] }],
46
+ linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'rustc'] }],
47
+ },
48
+ },
49
+ {
50
+ key: 'cpp',
51
+ name: 'C/C++ 工具链',
52
+ why: 'cgo 依赖:Linux WebKitGTK 壳与 Windows WebView2 层需要宿主 C 编译器',
53
+ probes: [
54
+ { exe: 'cl', args: [], re: /Version\s+(\d+\.\d+)/, okNonZero: true },
55
+ { exe: 'g++', args: ['--version'], re: /(\d+\.\d+(?:\.\d+)?)/ },
56
+ { exe: 'clang++', args: ['--version'], re: /(\d+\.\d+(?:\.\d+)?)/ },
57
+ ],
58
+ manual: 'https://visualstudio.microsoft.com/visual-cpp-build-tools/',
59
+ install: {
60
+ win32: [{
61
+ cmd: 'winget',
62
+ args: ['install', '-e', '--id', 'Microsoft.VisualStudio.2022.BuildTools',
63
+ '--override', '"--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"'],
64
+ }],
65
+ darwin: [{ cmd: 'xcode-select', args: ['--install'] }],
66
+ linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'build-essential', 'pkg-config'] }],
67
+ },
68
+ },
69
+ ];
70
+
71
+ function toolchainHome(opts = {}) {
72
+ return opts.home || process.env.FREEDOM_TOOLCHAIN_HOME || os.homedir();
73
+ }
74
+
75
+ function cacheFile(opts) {
76
+ return path.join(toolchainHome(opts), '.freedom', 'toolchain-cache.json');
77
+ }
78
+
79
+ function findInPath(exe, env) {
80
+ const dirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
81
+ const exts = env.FREEDOM_HOST_PLATFORM === 'win32' || process.platform === 'win32'
82
+ ? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').concat([''])
83
+ : [''];
84
+ for (const dir of dirs) {
85
+ for (const ext of exts) {
86
+ const full = path.join(dir, exe + ext);
87
+ try { if (fs.existsSync(full) && fs.statSync(full).isFile()) return full; } catch (e) { /* 无权访问的 PATH 项:跳过 */ }
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ // PATH 指纹:工具可见性只取决于 PATH 变了没有,变了就必须重探(新装工具通常在 PATH 尾部)。
94
+ function pathFingerprint(env) {
95
+ let h = 0;
96
+ const s = String(env.PATH || '');
97
+ for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
98
+ return h.toString(16);
99
+ }
100
+
101
+ function readCache(opts) {
102
+ try {
103
+ const raw = JSON.parse(fs.readFileSync(cacheFile(opts), 'utf8'));
104
+ if (raw && raw.version === CACHE_VERSION && raw.host === hostPlatform(opts)) return raw;
105
+ } catch (e) { /* 无缓存 / 损坏 / 跨平台搬运:一律当未命中 */ }
106
+ return { version: CACHE_VERSION, host: hostPlatform(opts), pathHash: null, tools: {} };
107
+ }
108
+
109
+ function writeCache(opts, cache) {
110
+ const file = cacheFile(opts);
111
+ fs.mkdirSync(path.dirname(file), { recursive: true });
112
+ fs.writeFileSync(file, JSON.stringify(cache, null, 2) + '\n');
113
+ }
114
+
115
+ function hostPlatform(opts) {
116
+ return (opts.env && opts.env.FREEDOM_HOST_PLATFORM) || process.platform;
117
+ }
118
+
119
+ // 单个工具链探测:返回 { ok, version, exe, reason }。
120
+ function resolveProbe(p, o) {
121
+ if (p.homeRelative) {
122
+ // rustup 的默认安装位不进 PATH 时也要认出来(装完没重开终端是常态)。
123
+ const bin = path.join(o.env.CARGO_HOME || path.join(toolchainHome(o), '.cargo'), 'bin');
124
+ const exts = hostPlatform(o) === 'win32' ? ['.exe', ''] : [''];
125
+ for (const ext of exts) {
126
+ const full = path.join(bin, p.exe + ext);
127
+ try { if (fs.statSync(full).isFile()) return full; } catch (e) { /* 不存在 */ }
128
+ }
129
+ return null;
130
+ }
131
+ return findInPath(p.exe, o.env);
132
+ }
133
+
134
+ function probe(spec, opts) {
135
+ const o = Object.assign({ env: process.env, exec: spawnSync }, opts);
136
+ const reasons = [];
137
+ for (const p of spec.probes) {
138
+ const resolved = resolveProbe(p, o);
139
+ if (!resolved) continue;
140
+ let r;
141
+ try { r = o.exec(resolved, p.args, { encoding: 'utf8', timeout: 8000 }); } catch (e) { reasons.push(`${resolved}: 无法启动`); continue; }
142
+ const text = `${(r && r.stdout) || ''}${(r && r.stderr) || ''}`;
143
+ const m = text.match(p.re);
144
+ if (m && (r.status === 0 || p.okNonZero)) return { ok: true, version: m[1], exe: resolved };
145
+ if (r && r.error) { reasons.push(`${resolved}: ${r.error.code || r.error.message}`); continue; }
146
+ // 找得到可执行但跑不通(cl 未加载 vcvarsall 环境是最常见的一种):记原因再试下一个候选。
147
+ reasons.push(`${resolved}: 可执行但输出无法识别(MSVC 需先加载 vcvarsall 环境)`);
148
+ }
149
+ return { ok: false, reason: reasons.length ? reasons[reasons.length - 1] : 'PATH 中未找到可执行' };
150
+ }
151
+
152
+ // status:命中缓存的 ok 记录直接跳过 spawn。
153
+ function status(opts = {}) {
154
+ const o = Object.assign({ env: process.env, refresh: false }, opts);
155
+ const cache = readCache(o);
156
+ const hash = pathFingerprint(o.env);
157
+ const stale = cache.pathHash !== hash;
158
+ const rows = [];
159
+ for (const spec of SPECS) {
160
+ const hit = !o.refresh && !stale && cache.tools[spec.key] && cache.tools[spec.key].ok
161
+ && (Date.now() - cache.tools[spec.key].at) < CACHE_TTL_MS;
162
+ if (hit) {
163
+ rows.push({ key: spec.key, name: spec.name, why: spec.why, source: 'cache', cached: true, ...cache.tools[spec.key] });
164
+ continue;
165
+ }
166
+ const res = probe(spec, o);
167
+ rows.push({ key: spec.key, name: spec.name, why: spec.why, source: 'probe', cached: false, ...res });
168
+ if (res.ok) cache.tools[spec.key] = { ok: true, version: res.version, exe: res.exe, at: Date.now() };
169
+ else delete cache.tools[spec.key]; // 失败不入缓存:下次仍探
170
+ }
171
+ cache.pathHash = hash;
172
+ writeCache(o, cache);
173
+ return { rows, skipped: rows.filter((r) => r.cached).length };
174
+ }
175
+
176
+ function installPlan(spec, opts) {
177
+ const list = spec.install[hostPlatform(opts)] || [];
178
+ return list.map((s) => [s.cmd].concat(s.args).join(' '));
179
+ }
180
+
181
+ // install:默认只打印计划;--apply 才真跑(联网 + 提权 + 改系统,属高危动作)。
182
+ function install(targets, opts = {}) {
183
+ const o = Object.assign({ env: process.env, apply: false, exec: spawnSync }, opts);
184
+ const want = resolveTargets(targets);
185
+ const st = status(Object.assign({}, o, { refresh: o.apply ? true : false }));
186
+ const out = [];
187
+ for (const spec of want) {
188
+ const row = st.rows.find((r) => r.key === spec.key);
189
+ if (row.ok && !o.force) {
190
+ out.push({ key: spec.key, action: 'skip', version: row.version, note: '已就位' });
191
+ continue;
192
+ }
193
+ const cmds = installPlan(spec, o);
194
+ if (!cmds.length) { out.push({ key: spec.key, action: 'manual', note: `本平台无自动安装命令,请手动获取:${spec.manual}` }); continue; }
195
+ if (!o.apply) { out.push({ key: spec.key, action: 'plan', commands: cmds, manual: spec.manual }); continue; }
196
+ const ran = [];
197
+ for (const line of cmds) {
198
+ const [cmd, ...args] = splitCmd(line);
199
+ let r;
200
+ try { r = o.exec(cmd, args, { stdio: 'inherit', env: o.env }); } catch (e) { r = { status: -1, error: e }; }
201
+ ran.push({ command: line, status: r ? r.status : -1 });
202
+ if (!r || r.status !== 0) break; // 前置步骤失败就别往下跑
203
+ }
204
+ out.push({ key: spec.key, action: 'applied', ran });
205
+ }
206
+ return { rows: out, dryRun: !o.apply };
207
+ }
208
+
209
+ function resolveTargets(raw) {
210
+ const keys = !raw || !raw.length || raw === 'missing' ? SPECS.map((s) => s.key) : String(raw).split(',').map((s) => s.trim()).filter(Boolean);
211
+ return SPECS.filter((s) => keys.includes(s.key));
212
+ }
213
+
214
+ function splitCmd(line) {
215
+ const m = String(line).match(/(?:[^\s"']+|"[^"]*")+/g) || [];
216
+ return m.map((t) => t.replace(/^"|"$/g, ''));
217
+ }
218
+
219
+ // ---- 配置优化:只写各工具自己的配置位,幂等 + .bak,缺省只打印 diff ----
220
+
221
+ // 大陆网络提示:locale 或时区命中才提议换镜像(不误伤海外机器),也可 FREEDOM_CN_MIRROR=1/0 强制。
222
+ function mainlandHint(env) {
223
+ const force = env.FREEDOM_CN_MIRROR;
224
+ if (force === '0' || force === 'false') return false;
225
+ if (force === '1' || force === 'true') return true;
226
+ const loc = `${env.LANG || ''}${env.LC_ALL || ''}${env.LC_CTYPE || ''}`;
227
+ const tz = env.TZ || '';
228
+ return /zh[-_]CN|chinese/i.test(loc) || /Asia\/(Shanghai|Chongqing|Harbin|Urumqi|Beijing|Kashgar)/i.test(tz);
229
+ }
230
+
231
+ const GO_PROXY = 'https://goproxy.cn,direct';
232
+ const CARGO_SOURCE = {
233
+ 'source.crates-io': { 'replace-with': 'rsproxy-sparse' },
234
+ 'source.rsproxy-sparse': { registry: 'sparse+https://rsproxy.cn/index/' },
235
+ };
236
+
237
+ // cargo 的 config.toml:表名可含点([source.crates-io]),按表头整块替换/追加。
238
+ function upsertTomlTable(text, header, fields) {
239
+ const lines = String(text).split(/\r?\n/);
240
+ const block = [header];
241
+ for (const [k, v] of Object.entries(fields)) block.push(`${k} = ${JSON.stringify(v)}`);
242
+ const start = lines.findIndex((l) => l.trim() === header);
243
+ if (start < 0) {
244
+ while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
245
+ return { text: lines.concat('', block).join('\n') + '\n', had: false };
246
+ }
247
+ let end = start + 1;
248
+ while (end < lines.length && !/^\s*\[/.test(lines[end])) end++;
249
+ const out = lines.slice(0, start).concat(block, lines.slice(end));
250
+ return { text: out.join('\n').replace(/\n*$/, '\n'), had: true };
251
+ }
252
+
253
+ function cargoConfigFile(env, home) {
254
+ const root = env.CARGO_HOME ? env.CARGO_HOME : path.join(home, '.cargo');
255
+ return path.join(root, 'config.toml');
256
+ }
257
+
258
+ // optimize:返回每个工具链的配置动作。apply=false 只回报将要写什么。
259
+ function optimize(opts = {}) {
260
+ const o = Object.assign({ env: process.env, apply: false, exec: spawnSync }, opts);
261
+ const home = toolchainHome(o);
262
+ const actions = [];
263
+ const st = status(Object.assign({}, o, { refresh: false }));
264
+
265
+ const go = st.rows.find((r) => r.key === 'go');
266
+ if (!mainlandHint(o.env)) {
267
+ actions.push({ key: 'go', action: 'skip', note: '非大陆网络环境提示,保留官方 GOPROXY' });
268
+ } else if (!go || !go.ok) {
269
+ actions.push({ key: 'go', action: 'blocked', note: '未检测到 Go,先 freedom toolchain install go' });
270
+ } else {
271
+ const cur = runCapture(o.exec, go.exe, ['env', 'GOPROXY']);
272
+ if (cur && cur.includes('goproxy.cn')) {
273
+ actions.push({ key: 'go', action: 'skip', note: `GOPROXY 已是镜像(${cur})` });
274
+ } else {
275
+ const cmdLine = `${go.exe} env -w GOPROXY=${GO_PROXY}`;
276
+ if (!o.apply) { actions.push({ key: 'go', action: 'plan', commands: [cmdLine], from: cur }); }
277
+ else {
278
+ const r = o.exec(go.exe, ['env', '-w', `GOPROXY=${GO_PROXY}`], { env: o.env, encoding: 'utf8' });
279
+ actions.push({ key: 'go', action: 'applied', to: GO_PROXY, status: r ? r.status : -1, error: r && r.error ? String(r.error.message) : null });
280
+ }
281
+ }
282
+ }
283
+
284
+ const rust = st.rows.find((r) => r.key === 'rust');
285
+ if (!mainlandHint(o.env)) {
286
+ actions.push({ key: 'rust', action: 'skip', note: '非大陆网络环境提示,不改 cargo 源' });
287
+ } else if (!rust || !rust.ok) {
288
+ actions.push({ key: 'rust', action: 'blocked', note: '未检测到 Rust,先 freedom toolchain install rust' });
289
+ } else {
290
+ const file = cargoConfigFile(o.env, home);
291
+ let text = '';
292
+ try { text = fs.readFileSync(file, 'utf8'); } catch (e) { /* 首次创建 */ }
293
+ if (/rsproxy\.cn/.test(text)) {
294
+ actions.push({ key: 'rust', action: 'skip', note: `cargo 源已是 rsproxy(${file})`, file });
295
+ } else {
296
+ let next = text;
297
+ for (const [header, fields] of Object.entries(CARGO_SOURCE)) next = upsertTomlTable(next, `[${header}]`, fields).text;
298
+ if (!o.apply) actions.push({ key: 'rust', action: 'plan', file, writes: next });
299
+ else {
300
+ fs.mkdirSync(path.dirname(file), { recursive: true });
301
+ if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
302
+ fs.writeFileSync(file, next);
303
+ actions.push({ key: 'rust', action: 'applied', file, backup: fs.existsSync(`${file}.bak`) ? `${file}.bak` : null });
304
+ }
305
+ }
306
+ }
307
+
308
+ const cpp = st.rows.find((r) => r.key === 'cpp');
309
+ actions.push({
310
+ key: 'cpp',
311
+ action: 'report',
312
+ ok: !!(cpp && cpp.ok),
313
+ note: cpp && cpp.ok
314
+ ? `已就位:${cpp.exe}(cgo 可直接使用,无用户配置需改)`
315
+ : `${cpp && cpp.reason ? cpp.reason + ';' : ''}Windows 需经 vcvarsall 注入环境(freedom build 已在子进程内处理),或安装 Build Tools`,
316
+ });
317
+ return { rows: actions, dryRun: !o.apply };
318
+ }
319
+
320
+ function runCapture(exec, exe, args) {
321
+ try {
322
+ const r = exec(exe, args, { encoding: 'utf8', timeout: 8000 });
323
+ if (!r || r.status !== 0) return null;
324
+ return String(r.stdout || '').trim();
325
+ } catch (e) {
326
+ return null;
327
+ }
328
+ }
329
+
330
+ // 清理缓存(测试与「重装后重探」用)。
331
+ function clearCache(opts = {}) {
332
+ try { fs.unlinkSync(cacheFile(opts)); return true; } catch (e) { return false; }
333
+ }
334
+
335
+ module.exports = {
336
+ SPECS, status, install, optimize, probe, mainlandHint, upsertTomlTable,
337
+ cacheFile, clearCache, findInPath, pathFingerprint, installPlan, splitCmd, cargoConfigFile,
338
+ GO_PROXY, CACHE_VERSION, CACHE_TTL_MS,
339
+ };
package/lib/utils.js CHANGED
@@ -25,6 +25,9 @@ function goTemplateDir() {
25
25
  }
26
26
 
27
27
  function shellDir() {
28
+ // 壳目录可经 env 覆写(离线/测试注入假壳;与 FREEDOM_SHELL_REPO 等既有旋钮同族)。
29
+ const override = process.env.FREEDOM_SHELL_DIR;
30
+ if (override) return path.resolve(override);
28
31
  return path.join(PKG_ROOT, 'shell');
29
32
  }
30
33
 
@@ -81,6 +84,53 @@ function nativePlatform() {
81
84
  return `${plat}-${arch}`;
82
85
  }
83
86
 
87
+ // 平台别名归一化:把口语写法(win / mac / linux / windows / osx / ubuntu)与架构同义词
88
+ // (x86_64 / amd64 / aarch64)统一到 ALL_PLATFORMS 的 key,无法识别返回 null。
89
+ // 缺架构时的默认值与历史 build.js keyMap 一致:win→x64、darwin→arm64(已不支持 Intel Mac)、
90
+ // linux→x64。
91
+ const PLAT_FAMILY = {
92
+ win: 'win', win32: 'win', win64: 'win', windows: 'win', pc: 'win',
93
+ mac: 'darwin', macos: 'darwin', 'mac-os': 'darwin', 'mac-os-x': 'darwin',
94
+ osx: 'darwin', darwin: 'darwin', apple: 'darwin',
95
+ linux: 'linux', lin: 'linux', ubuntu: 'linux', debian: 'linux',
96
+ };
97
+ const PLAT_ARCH = {
98
+ x64: 'x64', x86_64: 'x64', 'x86-64': 'x64', amd64: 'x64',
99
+ arm64: 'arm64', aarch64: 'arm64', 'aarch-64': 'arm64',
100
+ };
101
+ const PLAT_DEFAULT_ARCH = { win: 'x64', darwin: 'arm64', linux: 'x64' };
102
+
103
+ function normalizePlatform(raw) {
104
+ if (typeof raw !== 'string') return null;
105
+ const s = raw.trim().toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-');
106
+ if (!s) return null;
107
+ if (ALL_PLATFORMS.includes(s)) return s;
108
+ // 从右往左试两种切法(架构名自身可能带分隔符,如 x86-64;族名也一样,如 mac-os-x):
109
+ // 先按最后一个分隔符切,切不出架构再按倒数第二个切,还不行就整串当族名。
110
+ let fam = s;
111
+ let arch;
112
+ const cuts = [];
113
+ const last = s.lastIndexOf('-');
114
+ if (last > 0) {
115
+ cuts.push([last, s.slice(last + 1)]);
116
+ const prev = s.lastIndexOf('-', last - 1);
117
+ if (prev > 0) cuts.push([prev, s.slice(prev + 1)]);
118
+ }
119
+ for (const [at, tail] of cuts) {
120
+ if (PLAT_ARCH[tail]) {
121
+ arch = PLAT_ARCH[tail];
122
+ fam = s.slice(0, at);
123
+ break;
124
+ }
125
+ }
126
+ const family = PLAT_FAMILY[fam];
127
+ if (!family) return null;
128
+ const key = `${family}-${arch || PLAT_DEFAULT_ARCH[family]}`;
129
+ // 归一后仍须落在支持矩阵里:darwin-x64(Intel Mac 已停止支持)之类的组合
130
+ // 不能因为"语法像对的"就放行。
131
+ return ALL_PLATFORMS.includes(key) ? key : null;
132
+ }
133
+
84
134
  function localShellPath(plat) {
85
135
  return path.join(shellDir(), plat, SHELL_EXE_NAME[plat] || 'freedom-shell');
86
136
  }
@@ -134,6 +184,7 @@ module.exports = {
134
184
  isMacPlat,
135
185
  platformExeName,
136
186
  nativePlatform,
187
+ normalizePlatform,
137
188
  localShellPath,
138
189
  tutorialFile,
139
190
  loadConfig,
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 { APP_BIN_MAGIC, decryptApp, buildIntegrity } = require('./security');
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 容器头校验(FRDM2)
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, APP_BIN_MAGIC.length).toString('latin1');
126
- if (head !== APP_BIN_MAGIC) {
127
- fail('app.bin 容器', `容器头不是 ${APP_BIN_MAGIC}(实际 "${head}"),文件损坏、非本工具产物或旧版产物`);
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 容器', `${APP_BIN_MAGIC} 头有效(${fmtSize(bin.length)})`);
130
- // 真解一次容器:认证失败 = 产物被篡改或 name 与容器不匹配;同时拿到 backend 供后端检查。
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 };
@@ -226,13 +280,14 @@ function collectTree(dir, depth, out, maxDepth) {
226
280
  return out;
227
281
  }
228
282
  for (const e of entries) {
283
+ const abs = path.join(dir, e.name);
284
+ const stat = safeStat(abs);
229
285
  if (e.name === '.integrity') {
230
- // 完整性清单内容为敏感校验值,仅标注存在与大小,不展开内容
231
- out.push({ rel: e.name, size: 0, isDir: false, depth, hidden: true });
286
+ // 清单内容是不对外展开的校验值,但大小必须照实报:写死 0 会让产物自检打印
287
+ // 「.integrity (0 B)」,在高模式安检语境下读起来像清单是空的,反而制造误判。
288
+ out.push({ rel: e.name, size: stat ? stat.size : 0, isDir: false, depth, hidden: true });
232
289
  continue;
233
290
  }
234
- const abs = path.join(dir, e.name);
235
- const stat = safeStat(abs);
236
291
  if (e.isDirectory()) {
237
292
  out.push({ rel: e.name, size: 0, isDir: true, depth });
238
293
  if (depth < maxDepth) collectTree(abs, depth + 1, out, maxDepth);
package/lib/webkit.js ADDED
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ // Linux 构建的 WebKitGTK 依赖名选择。
4
+ //
5
+ // 背景:webview_go 的 cgo 依赖名在本地补丁副本(third_party/webview_go)里拆成了
6
+ // webkit2_40.go / webkit2_41.go 两个互斥标签文件。Ubuntu 24.04 / Debian 13 已移除
7
+ // webkit2gtk-4.0,只保留同一套 C API 的 webkit2gtk-4.1 —— 沿用 4.0 会让 `go build`
8
+ // 在新发行版上直接失败。
9
+ //
10
+ // 这里做的事:探测 pkg-config 实际装了哪个,缺 4.0 且有 4.1 时给 go 命令加
11
+ // `-tags webkit2_41`;否则返回原始环境(老发行版与 CI 产物基线零变化)。
12
+ // webkitgtk-6.0 是另一套 API,不能这样桥接(需上游适配)。
13
+
14
+ const { spawnSync } = require('child_process');
15
+
16
+ function pkgConfigExists(mod, exec = spawnSync) {
17
+ try {
18
+ return exec('pkg-config', ['--exists', mod], { stdio: 'ignore' }).status === 0;
19
+ } catch (e) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ // 需要 4.1 标签吗:仅 Linux、仅当 4.0 不可用而 4.1 可用。
25
+ function needsWebkit241(env = process.env, exec = spawnSync) {
26
+ if (env.FREEDOM_WEBKIT_FORCE_41 === '1') return true;
27
+ const platform = env.FREEDOM_HOST_PLATFORM || process.platform;
28
+ if (platform !== 'linux') return false;
29
+ if (pkgConfigExists('webkit2gtk-4.0', exec)) return false;
30
+ return pkgConfigExists('webkit2gtk-4.1', exec);
31
+ }
32
+
33
+ // 把标签并入 GOFLAGS(go 命令原生识别 GOFLAGS,故子进程与人工命令行都吃到)。
34
+ // 已有 GOFLAGS 时追加,不覆盖。
35
+ function applyWebkitTags(env = process.env, exec = spawnSync) {
36
+ const out = Object.assign({}, env);
37
+ if (!needsWebkit241(env, exec)) return { env: out, tags: [] };
38
+ const tags = ['webkit2_41'];
39
+ const existing = (env.GOFLAGS || '').trim();
40
+ const merged = existing ? `${existing} -tags=${tags.join(',')}` : `-tags=${tags.join(',')}`;
41
+ out.GOFLAGS = merged;
42
+ return { env: out, tags };
43
+ }
44
+
45
+ module.exports = { needsWebkit241, applyWebkitTags, pkgConfigExists };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yufengtadian/freedom-cli",
3
- "version": "1.13.2",
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",
@@ -15,7 +15,7 @@
15
15
  "frontend",
16
16
  "cross-platform"
17
17
  ],
18
- "license": "MIT",
18
+ "license": "SEE LICENSE IN LICENSE",
19
19
  "publishConfig": {
20
20
  "access": "public"
21
21
  },
@@ -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
@@ -121,9 +121,9 @@ freedom titlebar <native|frameless> | icon <path> | security <mode>
121
121
  freedom shell list|download <plat>|build <plat>
122
122
  freedom keygen # 生成自更新 ed25519 密钥对(私钥只在发布方)
123
123
  freedom manifest --artifact <产物> --url <下载地址> [--version x] [--notes y]
124
- freedom agents # Agent 集成支持矩阵(本机证据判定)
125
- freedom skill install --agent all [--dry-run]
126
- freedom mcp install --agent <key> [--config <path> --format json|toml|yaml]
124
+ freedom agents # Agent 集成支持矩阵(本机足迹判定「是否安装」,未安装不写入)
125
+ freedom skill install --agent all [--dry-run] [--force]
126
+ freedom mcp install --agent <key> [--config <path> --format json|toml|toml-aot|yaml] [--force]
127
127
  freedom mcp serve # stdio MCP 服务(一般由 agent 拉起,不手敲)
128
128
  freedom update | version | tutorial | help
129
129
  ```