exflower-license-server 1.4.0 → 1.4.2

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 (3) hide show
  1. package/db.js +3 -2
  2. package/package.json +2 -1
  3. package/updateSync.js +139 -0
package/db.js CHANGED
@@ -260,9 +260,10 @@ function setSetting(key, value) {
260
260
  .run(key, value);
261
261
  }
262
262
 
263
- // Seed demo data if empty
263
+ // Seed demo data if empty(仅显式开启:SEED_DEMO=1。生产部署不应自带有效演示 license——
264
+ // key 会打印在日志里,且客户并不知情)
264
265
  const count = db.prepare('SELECT COUNT(*) as c FROM companies').get().c;
265
- if (count === 0) {
266
+ if (count === 0 && process.env.SEED_DEMO === '1') {
266
267
  const { generateLicenseKey } = require('./utils');
267
268
  const demoKey = generateLicenseKey();
268
269
  db.prepare(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exflower-license-server",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "ExFlower License Server - 在线授权验证服务",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "routes.js",
13
13
  "utils.js",
14
14
  "activation.js",
15
+ "updateSync.js",
15
16
  "seed-templates.js",
16
17
  "central-public.pem",
17
18
  "migrate-excard-conventions.js",
package/updateSync.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 客户端版本 manifest 同步 + 后台拉取安装包(PLAN-CDN-DISTRIBUTION S1)
3
+ *
4
+ * 实测(2026-09-05):Vercel Blob 境内直连仅 ~60KB/s,客户端直连 CDN 不可行;
5
+ * 客户服务器(云主机)拉取 ~317KB/s。因此 Blob 只作中央源——本模块把 manifest
6
+ * 同步进 client_versions.remote_url 后,后台把最新包拉到本地 updates/ 目录
7
+ * (.part 断点续传 + sha512 校验后转正)。yml 永远优先本地文件(buildLatestYml),
8
+ * 客户端体验与纯本地托管完全一致;文件未就绪时 yml 才指向 remote_url 兜底。
9
+ * 同步/下载失败均维持本地现状(内网/气隙部署不受影响)。
10
+ */
11
+
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { queryAll, queryGet, queryRun, getSetting, setSetting, DATA_DIR } = require('./db');
16
+
17
+ const CENTRAL_URL = () => (process.env.CENTRAL_PRIMARY_URL || 'https://license.exflower.pro').replace(/\/$/, '');
18
+ const SYNC_INTERVAL_MS = 60 * 60 * 1000; // 每小时兜底
19
+ const STALE_MS = 55 * 60 * 1000; // /api/updates 被请求时超过即懒拉
20
+ const DOWNLOAD_CONCURRENCY = 1; // 串行下载,别挤爆小水管
21
+
22
+ const PLATFORMS = ['win', 'mac-arm64', 'mac-x64'];
23
+ const UPDATES_DIR = path.join(DATA_DIR, 'updates');
24
+
25
+ let _inflight = null;
26
+
27
+ async function syncNow() {
28
+ if (_inflight) return _inflight;
29
+ _inflight = (async () => {
30
+ try {
31
+ const r = await fetch(`${CENTRAL_URL()}/api/client-manifest`, { signal: AbortSignal.timeout(15000) });
32
+ if (!r.ok) return false; // 中央尚未发布(404)或故障——维持本地
33
+ const m = await r.json();
34
+ if (!m.success || !m.version || !Array.isArray(m.files)) return false;
35
+ for (const f of m.files) {
36
+ if (!PLATFORMS.includes(f.platform)) continue;
37
+ const existing = queryGet('SELECT id FROM client_versions WHERE platform = ? AND version = ?', [f.platform, m.version]);
38
+ if (existing) {
39
+ queryRun('UPDATE client_versions SET remote_url = ?, file_size = ?, sha512 = ? WHERE id = ?',
40
+ [f.url, f.size, f.sha512, existing.id]);
41
+ } else {
42
+ queryRun(`INSERT INTO client_versions (id, platform, version, file_path, file_name, file_size, sha512, release_notes, is_latest, remote_url)
43
+ VALUES (?, ?, ?, '', ?, ?, ?, '', 0, ?)`,
44
+ [crypto.randomUUID(), f.platform, m.version, String(f.url).split('/').pop(), f.size, f.sha512, f.url]);
45
+ }
46
+ queryRun('UPDATE client_versions SET is_latest = 0 WHERE platform = ?', [f.platform]);
47
+ queryRun('UPDATE client_versions SET is_latest = 1 WHERE platform = ? AND version = ?', [f.platform, m.version]);
48
+ }
49
+ setSetting('manifest_synced_at', new Date().toISOString());
50
+ console.log(`[UpdateSync] 同步中央 manifest v${m.version}(${m.files.length} 个平台)`);
51
+ _kickDownloads();
52
+ return true;
53
+ } catch (e) {
54
+ console.warn('[UpdateSync] 同步失败(维持本地版本):', e.message);
55
+ return false;
56
+ } finally {
57
+ _inflight = null;
58
+ }
59
+ })();
60
+ return _inflight;
61
+ }
62
+
63
+ // ── 后台下载:把有 remote_url 且本地无文件的最新行拉到 updates/<platform>/ ──
64
+
65
+ let _downloading = false;
66
+
67
+ function _fileReady(row) {
68
+ return row.file_path && fs.existsSync(row.file_path) && fs.statSync(row.file_path).size === row.file_size;
69
+ }
70
+
71
+ async function _downloadRow(row) {
72
+ const dir = path.join(UPDATES_DIR, row.platform);
73
+ fs.mkdirSync(dir, { recursive: true });
74
+ const finalPath = path.join(dir, row.file_name);
75
+ const partPath = finalPath + '.part';
76
+ // 断点续传:.part 有多大就从哪接着要
77
+ const have = fs.existsSync(partPath) ? fs.statSync(partPath).size : 0;
78
+ const headers = have > 0 ? { Range: `bytes=${have}-` } : {};
79
+ const r = await fetch(row.remote_url, { headers, signal: AbortSignal.timeout(30 * 60 * 1000) });
80
+ if (!r.ok && r.status !== 206) throw new Error(`HTTP ${r.status}`);
81
+ if (have > 0 && r.status !== 206) {
82
+ // 服务端不支持 Range:重来
83
+ fs.unlinkSync(partPath);
84
+ return _downloadRow(row);
85
+ }
86
+ const out = fs.createWriteStream(partPath, { flags: 'a' });
87
+ const reader = r.body.getReader();
88
+ for (;;) {
89
+ const { done, value } = await reader.read();
90
+ if (done) break;
91
+ if (!out.write(value)) await new Promise(res => out.once('drain', res));
92
+ }
93
+ await new Promise(res => out.end(res));
94
+ // 校验:size + sha512 都过才转正
95
+ const got = fs.statSync(partPath).size;
96
+ if (got !== row.file_size) throw new Error(`大小不符 ${got} != ${row.file_size}`);
97
+ const h = crypto.createHash('sha512');
98
+ for await (const chunk of fs.createReadStream(partPath)) h.update(chunk);
99
+ if (h.digest('base64') !== row.sha512) throw new Error('sha512 不符');
100
+ fs.renameSync(partPath, finalPath);
101
+ queryRun('UPDATE client_versions SET file_path = ? WHERE id = ?', [finalPath, row.id]);
102
+ console.log(`[UpdateSync] ${row.platform} v${row.version} 下载完成并校验通过(${(got / 1048576).toFixed(0)}MB)`);
103
+ }
104
+
105
+ async function _kickDownloads() {
106
+ if (_downloading) return;
107
+ _downloading = true;
108
+ try {
109
+ const rows = queryAll(
110
+ "SELECT * FROM client_versions WHERE is_latest = 1 AND remote_url IS NOT NULL AND remote_url != ''"
111
+ );
112
+ for (const row of rows) {
113
+ if (_fileReady(row)) continue;
114
+ const tag = `${row.platform} v${row.version}`;
115
+ try {
116
+ console.log(`[UpdateSync] 开始后台下载 ${tag}`);
117
+ await _downloadRow(row);
118
+ } catch (e) {
119
+ // 保留 .part 下轮续传;不阻塞其他平台
120
+ console.warn(`[UpdateSync] ${tag} 下载中断(下轮续传): ${e.message}`);
121
+ }
122
+ }
123
+ } finally {
124
+ _downloading = false;
125
+ }
126
+ }
127
+
128
+ /** 客户端来检查更新前调用:manifest 过期则先拉一次(失败不阻塞,用本地旧数据) */
129
+ async function ensureFresh() {
130
+ const last = getSetting('manifest_synced_at');
131
+ if (!last || Date.now() - new Date(last).getTime() > STALE_MS) await syncNow();
132
+ }
133
+
134
+ function startUpdateSync() {
135
+ syncNow();
136
+ setInterval(syncNow, SYNC_INTERVAL_MS).unref();
137
+ }
138
+
139
+ module.exports = { startUpdateSync, ensureFresh, syncNow };