@heybox/hb-sdk 0.8.0 → 0.8.1-alpha.10
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/CHANGELOG.md +84 -0
- package/README.md +80 -7
- package/dist/cli-chunks/{build-PYCNacya.cjs → build-CtqIXxDL.cjs} +16 -10
- package/dist/cli-chunks/{context-m2W2XbL0.cjs → context-t57bMfbr.cjs} +17 -3
- package/dist/cli-chunks/{create-BdAg3WGA.cjs → create-BDOPSacf.cjs} +1 -1
- package/dist/cli-chunks/{dev-CyZuw7Yn.cjs → dev-VxjconYc.cjs} +628 -147
- package/dist/cli-chunks/{doctor-DU8rCfUF.cjs → doctor-BkfYdlnZ.cjs} +1 -1
- package/dist/cli-chunks/{index-MMW2ibQm.cjs → index-DDrytLTX.cjs} +87 -15
- package/dist/cli-chunks/{index-DATObqzK.cjs → index-lLaHzwfT.cjs} +2 -2
- package/dist/cli-chunks/{index.esm-BiAaAUFC.cjs → index.esm-Dy4qRhuo.cjs} +9 -8
- package/dist/cli-chunks/{login-B3TThMss.cjs → login-D2EX4R5_.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-BQj8YLI4.cjs → project-vite-CFhz2THK.cjs} +1 -1
- package/dist/cli-chunks/{remote-DNvI7tHH.cjs → remote-CfESZxO2.cjs} +41 -28
- package/dist/cli-chunks/{runtime-gate-BEFp1w_s.cjs → runtime-gate-BWb8QIcX.cjs} +0 -107
- package/dist/cli-chunks/{runtime-permission-env-CtL8rsjB.cjs → runtime-permission-env-BtTDFqEL.cjs} +304 -23
- package/dist/cli-chunks/{session-DjBkjaF8.cjs → session-Dkhwjqhm.cjs} +1 -1
- package/dist/cli-chunks/{skill-cR_wnaw2.cjs → skill-DOveC5jt.cjs} +83 -27
- package/dist/cli-chunks/{version-yEn1E2Bg.cjs → version-C4nE66sX.cjs} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/devtools/browser-dev-host/assets/browser-dev-host-BM9Qo_Ta.js +101 -0
- package/dist/devtools/browser-dev-host/assets/desktop-app-launch-C2I333Yn.js +6 -0
- package/dist/devtools/browser-dev-host/assets/index-Bnb6MMTv.js +567 -0
- package/dist/devtools/browser-dev-host/assets/index-D-aNERAr.css +1 -0
- package/dist/devtools/browser-dev-host/index.html +3 -3
- package/dist/index.cjs.js +1970 -1567
- package/dist/index.esm.js +1969 -1568
- package/dist/miniapp-publish.cjs.js +61 -0
- package/dist/miniapp-publish.esm.js +59 -1
- package/dist/protocol.cjs.js +64 -13
- package/dist/protocol.esm.js +64 -13
- package/dist/templates/vanilla-vite-js/README.md.ejs +1 -1
- package/dist/templates/vanilla-vite-js/package.json.ejs +1 -0
- package/dist/templates/vanilla-vite-js/vite.config.js +1 -1
- package/dist/vite.cjs.js +1333 -18
- package/dist/vite.esm.js +1334 -20
- package/package.json +9 -11
- package/skill/SKILL.md +11 -3
- package/skill/references/api-protocol.md +8 -4
- package/skill/references/api-root.md +328 -210
- package/skill/references/cli.md +11 -3
- package/skill/references/examples.md +13 -1
- package/skill/references/recipes.md +162 -0
- package/skill/references/safety-boundaries.md +9 -3
- package/skill/skill.json +5 -5
- package/types/core/client.d.ts +9 -1
- package/types/core/sdk.d.ts +6 -0
- package/types/core/singleton.d.ts +6 -0
- package/types/devtools/device-logs.d.ts +48 -0
- package/types/index.d.ts +5 -1
- package/types/miniapp-manifest/companion-directory.d.ts +2 -0
- package/types/miniapp-manifest/companion-executable.d.ts +4 -0
- package/types/miniapp-manifest/companion-types.d.ts +38 -0
- package/types/miniapp-manifest/companions.d.ts +17 -0
- package/types/miniapp-manifest/index.d.ts +2 -0
- package/types/miniapp-manifest/node.d.ts +9 -0
- package/types/miniapp-manifest/permissions.d.ts +1 -1
- package/types/miniapp-manifest/schema.d.ts +5 -1
- package/types/miniapp-publish/index.d.ts +17 -2
- package/types/modules/companion/index.d.ts +57 -0
- package/types/modules/environment/index.d.ts +70 -0
- package/types/modules/network/index.d.ts +1 -4
- package/types/modules/network/observability.d.ts +0 -1
- package/types/protocol/dev-session.d.ts +8 -1
- package/types/protocol.d.ts +1 -1
- package/types/vite/index.d.ts +11 -4
- package/dist/devtools/browser-dev-host/assets/browser-dev-host-TzYf9L6C.js +0 -99
- package/dist/devtools/browser-dev-host/assets/index-C5MZZDa5.js +0 -567
- package/dist/devtools/browser-dev-host/assets/index-P-ra4m1y.css +0 -1
- package/dist/devtools/browser-dev-host/assets/workbench-state-BwV7bm4n.js +0 -5
package/dist/vite.esm.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { readFileSync, existsSync,
|
|
1
|
+
import { readFileSync, existsSync, realpathSync, lstatSync, readdirSync, readlinkSync, mkdirSync, writeFileSync, renameSync, rmSync, unwatchFile, createReadStream, watchFile, mkdtempSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { tmpdir, platform, arch } from 'node:os';
|
|
2
4
|
import path from 'node:path';
|
|
5
|
+
import { inflateRawSync } from 'node:zlib';
|
|
3
6
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
7
|
|
|
5
8
|
/** 构建时替换为当前发布包的实际版本。 */
|
|
6
9
|
const HB_SDK_VERSION = typeof undefined === 'string'
|
|
7
10
|
? undefined
|
|
8
|
-
: '0.8.
|
|
11
|
+
: '0.8.1-alpha.10';
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* iframe 与调试台页面的开发期 console 捕获转发。
|
|
@@ -25,11 +28,306 @@ const HB_SDK_VERSION = typeof undefined === 'string'
|
|
|
25
28
|
* 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
|
|
26
29
|
* `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
|
|
27
30
|
*/
|
|
28
|
-
const MINI_DEV_IFRAME_WINDOW_NAME = '__hb_sdk_mini_dev__';
|
|
29
31
|
const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
|
|
30
32
|
/** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
|
|
31
33
|
const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* network.request 诊断日志的纯脱敏工具。
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
|
|
40
|
+
* modules observability、devtools Browser Dev Host / proxy 共用,并满足 boundary
|
|
41
|
+
*(devtools 不得直接 import runtime capability modules)。
|
|
42
|
+
*/
|
|
43
|
+
/** 敏感字段名:header / query / body key 共用。 */
|
|
44
|
+
const SENSITIVE_KEY_PATTERN = /(authorization|cookie|password|passwd|secret|token|pkey|private[_-]?key|access[_-]?key|client[_-]?secret|api[_-]?key|apikey|session|jwt|refresh[_-]?token|csrf)/i;
|
|
45
|
+
/**
|
|
46
|
+
* 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
|
|
47
|
+
*/
|
|
48
|
+
function sanitizeNetworkRequestUrl(url) {
|
|
49
|
+
if (!url) {
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const parsed = new URL(url);
|
|
54
|
+
parsed.username = '';
|
|
55
|
+
parsed.password = '';
|
|
56
|
+
redactUrlSearchParams(parsed.searchParams);
|
|
57
|
+
if (parsed.hash && parsed.hash.length > 1) {
|
|
58
|
+
const hashBody = parsed.hash.slice(1);
|
|
59
|
+
if (hashBody.includes('=')) {
|
|
60
|
+
const hashParams = new URLSearchParams(hashBody);
|
|
61
|
+
if (redactUrlSearchParams(hashParams)) {
|
|
62
|
+
parsed.hash = hashParams.toString();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return parsed.toString();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return redactSensitivePairsInRawText(url);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
|
|
74
|
+
*/
|
|
75
|
+
function sanitizeDiagnosticMessage(message) {
|
|
76
|
+
if (!message) {
|
|
77
|
+
return message;
|
|
78
|
+
}
|
|
79
|
+
return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
|
|
80
|
+
}
|
|
81
|
+
function isSensitiveNetworkKey(key) {
|
|
82
|
+
return SENSITIVE_KEY_PATTERN.test(key.trim());
|
|
83
|
+
}
|
|
84
|
+
function redactUrlSearchParams(params) {
|
|
85
|
+
let changed = false;
|
|
86
|
+
for (const key of [...params.keys()]) {
|
|
87
|
+
if (isSensitiveNetworkKey(key)) {
|
|
88
|
+
params.set(key, '[redacted]');
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return changed;
|
|
93
|
+
}
|
|
94
|
+
function redactSensitivePairsInRawText(text) {
|
|
95
|
+
return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
|
|
96
|
+
if (!isSensitiveNetworkKey(key)) {
|
|
97
|
+
return full;
|
|
98
|
+
}
|
|
99
|
+
return `${prefix}${key}=[redacted]`;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const DEVICE_LOG_PATH = '/__hb_sdk__/device-logs';
|
|
104
|
+
const registryKey = Symbol.for('@heybox/hb-sdk/device-logs');
|
|
105
|
+
const registry = globalThis;
|
|
106
|
+
const stores = (registry[registryKey] ??= new Map());
|
|
107
|
+
function registerDeviceLogs(port, store) {
|
|
108
|
+
stores.set(port, store);
|
|
109
|
+
return () => {
|
|
110
|
+
if (stores.get(port) === store)
|
|
111
|
+
stores.delete(port);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
class DeviceLogStore {
|
|
115
|
+
sandboxRequests = new Map();
|
|
116
|
+
listSandbox(id, path) {
|
|
117
|
+
const device = this.devices.get(id);
|
|
118
|
+
if (!device || Date.now() - device.lastSeen > 15000)
|
|
119
|
+
return Promise.resolve({ error: '设备已离线' });
|
|
120
|
+
if (path !== '.' && (!path || path.length > 1024 || /[\\\\\u0000-\u001f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')))
|
|
121
|
+
return Promise.resolve({ error: '无效目录路径' });
|
|
122
|
+
if (this.sandboxRequests.size >= 64)
|
|
123
|
+
return Promise.resolve({ error: '查询繁忙,请稍后重试' });
|
|
124
|
+
const requestId = randomUUID();
|
|
125
|
+
return new Promise(resolve => {
|
|
126
|
+
const timer = setTimeout(() => { this.sandboxRequests.delete(requestId); resolve({ error: '设备查询超时' }); }, 10000);
|
|
127
|
+
this.sandboxRequests.set(requestId, { id, path, requestId, resolve: value => { clearTimeout(timer); this.sandboxRequests.delete(requestId); resolve(value); } });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
uploadToken = randomBytes(32).toString('hex');
|
|
131
|
+
epoch = randomUUID();
|
|
132
|
+
expiresAt = Date.now() + 12 * 60 * 60 * 1000;
|
|
133
|
+
cursor = 0;
|
|
134
|
+
logs = [];
|
|
135
|
+
devices = new Map();
|
|
136
|
+
query(params) {
|
|
137
|
+
const after = Math.max(0, Number(params.get('after')) || 0);
|
|
138
|
+
const limit = Math.min(500, Math.max(1, Number(params.get('limit')) || 100));
|
|
139
|
+
const selected = this.logs.filter((log) => log.cursor > after &&
|
|
140
|
+
(!params.get('device') || log.deviceSessionId === params.get('device')) &&
|
|
141
|
+
(!params.get('level') || log.level === params.get('level')) &&
|
|
142
|
+
(!params.get('source') || log.source === params.get('source')) &&
|
|
143
|
+
(!params.get('callId') || log.callId === params.get('callId')) &&
|
|
144
|
+
(!params.get('id') || String(log.cursor) === params.get('id')) &&
|
|
145
|
+
(!params.get('since') || log.timestamp >= Number(params.get('since'))) &&
|
|
146
|
+
(!params.get('query') || log.message.toLowerCase().includes(params.get('query').toLowerCase())));
|
|
147
|
+
const items = selected.slice(0, limit);
|
|
148
|
+
return {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
epoch: this.epoch,
|
|
151
|
+
items,
|
|
152
|
+
nextCursor: selected.length > limit ? items.at(-1).cursor : this.cursor,
|
|
153
|
+
hasMore: selected.length > limit,
|
|
154
|
+
gap: after > 0 && after < (this.logs[0]?.cursor ?? 1) - 1,
|
|
155
|
+
devices: [...this.devices.values()].map(({ id, name, platform, connectedAt, lastSeen }) => ({
|
|
156
|
+
id,
|
|
157
|
+
name,
|
|
158
|
+
platform,
|
|
159
|
+
connectedAt,
|
|
160
|
+
lastSeen,
|
|
161
|
+
online: Date.now() - lastSeen < 15000,
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async upload(req, res) {
|
|
166
|
+
const reply = (status, value) => {
|
|
167
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
168
|
+
res.end(JSON.stringify(value));
|
|
169
|
+
};
|
|
170
|
+
if (req.method !== 'POST') {
|
|
171
|
+
reply(405, {});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (Date.now() > this.expiresAt) {
|
|
175
|
+
reply(410, { code: 'EXPIRED' });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let text = '';
|
|
179
|
+
try {
|
|
180
|
+
req.setEncoding('utf8');
|
|
181
|
+
for await (const chunk of req) {
|
|
182
|
+
text += chunk;
|
|
183
|
+
if (Buffer.byteLength(text) > 65536) {
|
|
184
|
+
reply(413, {});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const body = JSON.parse(text);
|
|
189
|
+
if (body.register === true) {
|
|
190
|
+
if (req.headers['x-hb-log-token'] !== this.uploadToken) {
|
|
191
|
+
reply(403, {});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const registrationId = typeof body.registrationId === 'string' && /^[a-f0-9]{32}$/.test(body.registrationId) ? body.registrationId : undefined;
|
|
195
|
+
const existing = registrationId && [...this.devices.values()].find(device => device.registrationId === registrationId);
|
|
196
|
+
if (existing) {
|
|
197
|
+
reply(200, { id: existing.id, token: existing.token });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (this.devices.size >= 64) {
|
|
201
|
+
reply(429, {});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const id = randomUUID();
|
|
205
|
+
const platform = ['iOS', 'Android', 'Windows', 'macOS', 'Browser'].includes(body.platform) ? body.platform : 'Browser';
|
|
206
|
+
const device = {
|
|
207
|
+
id,
|
|
208
|
+
platform,
|
|
209
|
+
registrationId,
|
|
210
|
+
connectedAt: Date.now(),
|
|
211
|
+
name: `${platform} · ${this.devices.size + 1}`,
|
|
212
|
+
lastSeen: Date.now(),
|
|
213
|
+
sequence: 0,
|
|
214
|
+
token: randomBytes(32).toString('hex'),
|
|
215
|
+
windowStart: Date.now(),
|
|
216
|
+
count: 0,
|
|
217
|
+
};
|
|
218
|
+
this.devices.set(id, device);
|
|
219
|
+
reply(200, { id, token: device.token });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const device = this.devices.get(body.id);
|
|
223
|
+
if (!device || req.headers['x-hb-log-token'] !== device.token) {
|
|
224
|
+
reply(403, {});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!Array.isArray(body.logs) || body.logs.length > 50) {
|
|
228
|
+
reply(400, {});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (Date.now() - device.windowStart > 1000) {
|
|
232
|
+
device.windowStart = Date.now();
|
|
233
|
+
device.count = 0;
|
|
234
|
+
}
|
|
235
|
+
if (device.count + body.logs.length > 200) {
|
|
236
|
+
reply(429, {});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
device.count += body.logs.length;
|
|
240
|
+
device.lastSeen = Date.now();
|
|
241
|
+
if (body.sandboxResult && typeof body.sandboxResult === 'object') {
|
|
242
|
+
const result = body.sandboxResult;
|
|
243
|
+
const pending = this.sandboxRequests.get(result.requestId);
|
|
244
|
+
if (pending?.id === device.id) {
|
|
245
|
+
const entries = Array.isArray(result.entries) ? result.entries.slice(0, 200).filter((entry) => entry && ['file', 'directory'].includes(entry.kind) && typeof entry.name === 'string' && entry.name.length <= 255 && !/[\\\\/\u0000-\u001f]/.test(entry.name) && !['.', '..'].includes(entry.name)).map((entry) => ({ name: entry.name, kind: entry.kind })) : [];
|
|
246
|
+
pending.resolve({ deviceSessionId: device.id, requestId: pending.requestId, path: pending.path, timestamp: Date.now(), entries, error: result.error ? '设备不支持或无权读取此目录' : undefined, truncated: result.truncated === true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const log of body.logs) {
|
|
250
|
+
if (!log || typeof log !== 'object')
|
|
251
|
+
continue;
|
|
252
|
+
if (!Number.isSafeInteger(log.sequence) ||
|
|
253
|
+
log.sequence <= device.sequence ||
|
|
254
|
+
!Number.isFinite(log.timestamp) ||
|
|
255
|
+
!['log', 'debug', 'info', 'warn', 'error'].includes(log.level) ||
|
|
256
|
+
typeof log.message !== 'string')
|
|
257
|
+
continue;
|
|
258
|
+
device.sequence = log.sequence;
|
|
259
|
+
this.logs.push({
|
|
260
|
+
cursor: ++this.cursor,
|
|
261
|
+
deviceSessionId: device.id,
|
|
262
|
+
sequence: log.sequence,
|
|
263
|
+
timestamp: log.timestamp,
|
|
264
|
+
receivedAt: Date.now(),
|
|
265
|
+
level: log.level,
|
|
266
|
+
source: log.source === 'sdk' ? 'sdk' : 'app',
|
|
267
|
+
message: sanitizeDiagnosticMessage(log.message)
|
|
268
|
+
.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)(["'\s:=]+)([^\s,}]+)/gi, '$1$2[redacted]')
|
|
269
|
+
.slice(0, 8192),
|
|
270
|
+
truncated: log.truncated === true || log.message.length > 8192,
|
|
271
|
+
...(typeof log.method === 'string' ? { method: log.method.slice(0, 100) } : {}),
|
|
272
|
+
...(typeof log.callId === 'string' ? { callId: log.callId.slice(0, 100) } : {}),
|
|
273
|
+
...(typeof log.errorCode === 'string' && /^[A-Z0-9_]{1,100}$/.test(log.errorCode) ? { errorCode: log.errorCode } : {}),
|
|
274
|
+
...(log.outcome === 'success' || log.outcome === 'error' ? { outcome: log.outcome } : {}),
|
|
275
|
+
...(Number.isFinite(log.durationMs) ? { durationMs: Math.max(0, log.durationMs) } : {}),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (this.logs.length > 5000)
|
|
279
|
+
this.logs.splice(0, this.logs.length - 5000);
|
|
280
|
+
reply(200, { sequence: device.sequence, sandboxRequest: [...this.sandboxRequests.values()].find(request => request.id === device.id) ? (() => { const request = [...this.sandboxRequests.values()].find(request => request.id === device.id); return { requestId: request.requestId, path: request.path }; })() : undefined });
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
reply(400, { code: 'INVALID_LOG_BATCH' });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** 同源开发页面使用有界批量回传;传输失败不会递归写入 console。 */
|
|
288
|
+
function deviceLogBootstrap(token) {
|
|
289
|
+
return `<script>(function(){if(window.__hb_sdk_device_logs_installed__)return;
|
|
290
|
+
window.__hb_sdk_device_logs_installed__=true;
|
|
291
|
+
window.__hb_sdk_mini_dev_console_installed__=true;
|
|
292
|
+
var endpoint='${DEVICE_LOG_PATH}',credential='${token}',id='',queue=[],seq=0,busy=false,dropped=0;
|
|
293
|
+
var registrationId=Array.from(crypto.getRandomValues(new Uint8Array(16)),function(n){return n.toString(16).padStart(2,'0')}).join('');
|
|
294
|
+
var send=window.fetch.bind(window);
|
|
295
|
+
var sandboxResult;
|
|
296
|
+
function format(v){try{return typeof v==='string'?v:v instanceof Error?v.stack||v.message:JSON.stringify(v,function(k,value){return /authorization|cookie|password|secret|token|pkey|api.?key/i.test(k)?'[redacted]':value})}catch(e){return '[无法序列化]'}}function add(level,args,source){if(queue.length>=500){queue.shift();
|
|
297
|
+
dropped++}var message=args.map(format).join(' ');
|
|
298
|
+
message=message.replace(/https?:\\/\\/[^\\s]+/g,function(raw){try{var url=new URL(raw);
|
|
299
|
+
url.username='';
|
|
300
|
+
url.password='';
|
|
301
|
+
url.search='';
|
|
302
|
+
url.hash='';
|
|
303
|
+
return url.href}catch(e){return '[URL]'}});
|
|
304
|
+
message=message.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)([\\"'\\s:=]+)([^\\s,}]+)/gi,'$1$2[redacted]');
|
|
305
|
+
queue.push({truncated:message.length>8192,sequence:++seq,timestamp:Date.now(),level:level,source:source||'app',message:message.slice(0,8192)})}window.__hbSdkDeviceLog=function(event){add(event.outcome==='error'?'error':'info',[event.method+' · '+event.outcome+' · '+event.durationMs+' ms'],'sdk');
|
|
306
|
+
Object.assign(queue[queue.length-1],event)};
|
|
307
|
+
['log','debug','info','warn','error'].forEach(function(level){var original=console[level];
|
|
308
|
+
console[level]=function(){add(level,Array.prototype.slice.call(arguments));
|
|
309
|
+
return original.apply(this,arguments)}});
|
|
310
|
+
window.addEventListener('error',function(e){add('error',[e.message])});
|
|
311
|
+
window.addEventListener('unhandledrejection',function(e){add('error',[e.reason])});
|
|
312
|
+
async function flush(){if(busy)return;
|
|
313
|
+
busy=true;
|
|
314
|
+
try{if(!id){var ua=navigator.userAgent;
|
|
315
|
+
var platform=window.name==='__hb_sdk_mini_dev__'?'Browser':/Android/i.test(ua)?'Android':/iPhone|iPad/i.test(ua)?'iOS':/Windows/i.test(ua)?'Windows':/Mac/i.test(ua)?'macOS':'Browser';
|
|
316
|
+
var r=await send(endpoint,{method:'POST',headers:{'content-type':'application/json','x-hb-log-token':credential},body:JSON.stringify({register:true,platform:platform,registrationId:registrationId}),signal:AbortSignal.timeout(5000)});
|
|
317
|
+
if(!r.ok)return;
|
|
318
|
+
var d=await r.json();
|
|
319
|
+
id=d.id;
|
|
320
|
+
credential=d.token}if(dropped){add('warn',['日志缓冲已丢弃 '+dropped+' 条']);
|
|
321
|
+
dropped=0}var batch=queue.slice(0,50);
|
|
322
|
+
while(batch.length>1&&new TextEncoder().encode(JSON.stringify(batch)).length>48000)batch.pop();
|
|
323
|
+
var result=await send(endpoint,{method:'POST',headers:{'content-type':'application/json','x-hb-log-token':credential},body:JSON.stringify({id:id,logs:batch,sandboxResult:sandboxResult}),signal:AbortSignal.timeout(5000)});
|
|
324
|
+
if(result.ok){var ack=await result.json();sandboxResult=undefined;
|
|
325
|
+
if(ack.sandboxRequest){try{var entries=await window.__hbSdkSandboxList(ack.sandboxRequest.path);sandboxResult={requestId:ack.sandboxRequest.requestId,entries:entries.slice(0,200),truncated:entries.length>200}}catch(e){sandboxResult={requestId:ack.sandboxRequest.requestId,error:true}}}
|
|
326
|
+
queue=queue.filter(function(l){return l.sequence>ack.sequence})}}catch(e){}finally{busy=false}}setInterval(flush,1000);
|
|
327
|
+
flush()})();
|
|
328
|
+
</script>`;
|
|
329
|
+
}
|
|
330
|
+
|
|
33
331
|
var re = {exports: {}};
|
|
34
332
|
|
|
35
333
|
var constants;
|
|
@@ -2818,6 +3116,8 @@ var semverExports = requireSemver();
|
|
|
2818
3116
|
const MINI_PROGRAM_MESSAGE_NAMESPACE = 'heybox:miniprogram';
|
|
2819
3117
|
const MINI_PROGRAM_MESSAGE_VERSION = 2;
|
|
2820
3118
|
const MINI_PROGRAM_BRIDGE_NONCE_PARAM = 'hb_mini_bridge_nonce';
|
|
3119
|
+
/** Companion alias 的跨构建、Runtime 与 Host 规范。 */
|
|
3120
|
+
const COMPANION_ALIAS_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
2821
3121
|
|
|
2822
3122
|
const MINI_PROGRAM_PERMISSION_KEYS = [
|
|
2823
3123
|
'userInfo',
|
|
@@ -2828,6 +3128,7 @@ const MINI_PROGRAM_PERMISSION_KEYS = [
|
|
|
2828
3128
|
'clipboard',
|
|
2829
3129
|
'leaderboard',
|
|
2830
3130
|
'network',
|
|
3131
|
+
'companion',
|
|
2831
3132
|
];
|
|
2832
3133
|
const MINI_PROGRAM_PERMISSION_CATALOG = [
|
|
2833
3134
|
{
|
|
@@ -2940,6 +3241,29 @@ const MINI_PROGRAM_PERMISSION_CATALOG = [
|
|
|
2940
3241
|
methods: ['network.request', 'network.download'],
|
|
2941
3242
|
conditions: ['声明后仍需平台批准,并继续执行网络安全策略。'],
|
|
2942
3243
|
},
|
|
3244
|
+
{
|
|
3245
|
+
key: 'companion',
|
|
3246
|
+
name: '桌面辅助应用',
|
|
3247
|
+
description: '下载、准备并运行随当前小程序版本发布的受管桌面程序。',
|
|
3248
|
+
access: 'platform-approval',
|
|
3249
|
+
risk: 'high',
|
|
3250
|
+
config: {},
|
|
3251
|
+
methods: [
|
|
3252
|
+
'companion.getInfo',
|
|
3253
|
+
'companion.getPreparationStatus',
|
|
3254
|
+
'companion.prepare',
|
|
3255
|
+
'companion.cancelPreparation',
|
|
3256
|
+
'companion.getActiveSession',
|
|
3257
|
+
'companion.launch',
|
|
3258
|
+
'companion.session.write',
|
|
3259
|
+
'companion.session.closeStdin',
|
|
3260
|
+
'companion.session.terminate',
|
|
3261
|
+
'companion.session.ackOutput',
|
|
3262
|
+
'companion.openAttachment',
|
|
3263
|
+
'companion.showNotification',
|
|
3264
|
+
],
|
|
3265
|
+
conditions: ['声明后仍需平台批准,并继续执行可信用户手势、Host 能力和当前版本 Manifest 校验。'],
|
|
3266
|
+
},
|
|
2943
3267
|
];
|
|
2944
3268
|
new Set(MINI_PROGRAM_PERMISSION_KEYS);
|
|
2945
3269
|
|
|
@@ -2954,7 +3278,7 @@ function parseMiniappPermissions(value, options) {
|
|
|
2954
3278
|
}
|
|
2955
3279
|
return { declared: false };
|
|
2956
3280
|
}
|
|
2957
|
-
if (!isRecord$
|
|
3281
|
+
if (!isRecord$2(value)) {
|
|
2958
3282
|
throw new Error(`${sourceLabel} 必须是 JSON 对象`);
|
|
2959
3283
|
}
|
|
2960
3284
|
const knownKeys = new Set(MINIAPP_PERMISSION_KEYS);
|
|
@@ -2968,7 +3292,7 @@ function parseMiniappPermissions(value, options) {
|
|
|
2968
3292
|
continue;
|
|
2969
3293
|
const declaration = value[key];
|
|
2970
3294
|
const itemLabel = `${sourceLabel}.${key}`;
|
|
2971
|
-
if (!isRecord$
|
|
3295
|
+
if (!isRecord$2(declaration))
|
|
2972
3296
|
throw new Error(`${itemLabel} 必须是 JSON 对象`);
|
|
2973
3297
|
const definition = MINI_PROGRAM_PERMISSION_CATALOG.find((item) => item.key === key);
|
|
2974
3298
|
if (!definition)
|
|
@@ -3002,7 +3326,7 @@ function requiresExplicitPermissions(sdkVersion) {
|
|
|
3002
3326
|
function getMissingPermissionsWarning(sourceLabel = 'package.json#heybox.permissions') {
|
|
3003
3327
|
return `${sourceLabel} 尚未声明。当前按 0.8 兼容行为构建;0.9.0 起将拒绝构建,请尽快补充权限声明(无权限需求时配置 {})。`;
|
|
3004
3328
|
}
|
|
3005
|
-
function isRecord$
|
|
3329
|
+
function isRecord$2(value) {
|
|
3006
3330
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3007
3331
|
}
|
|
3008
3332
|
|
|
@@ -3024,6 +3348,7 @@ function renderMiniappManifest(manifest) {
|
|
|
3024
3348
|
sdkVersion: manifest.sdkVersion,
|
|
3025
3349
|
platforms,
|
|
3026
3350
|
...(manifest.permissions === undefined ? {} : { permissions: manifest.permissions }),
|
|
3351
|
+
...(manifest.companions === undefined ? {} : { companions: manifest.companions }),
|
|
3027
3352
|
};
|
|
3028
3353
|
return `${JSON.stringify(output, null, 2)}\n`;
|
|
3029
3354
|
}
|
|
@@ -3082,14 +3407,30 @@ function findNearestPackageJsonPath(startDir) {
|
|
|
3082
3407
|
function readMiniappVersionFromPackageJson(root) {
|
|
3083
3408
|
return readMiniappPackageJson(root, true).version;
|
|
3084
3409
|
}
|
|
3410
|
+
/** Companion source 始终相对声明所在 package.json,而非 Vite 的 HTML root。 */
|
|
3411
|
+
function readMiniappCompanionsFromPackageJson(root) {
|
|
3412
|
+
const { packageJson, packageJsonPath } = readMiniappPackageJson(root, false);
|
|
3413
|
+
const heybox = readMiniappHeyboxConfig(packageJson);
|
|
3414
|
+
const declarations = heybox?.companions;
|
|
3415
|
+
if (declarations !== undefined && (!isRecord$1(declarations) || Object.keys(declarations).length === 0)) {
|
|
3416
|
+
throw new Error('package.json#heybox.companions 必须是非空对象');
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
declarations: declarations,
|
|
3420
|
+
projectRoot: path.dirname(packageJsonPath),
|
|
3421
|
+
packageJsonPath,
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
function readMiniappPlatformsFromPackageJson(root) {
|
|
3425
|
+
const { packageJson } = readMiniappPackageJson(root, false);
|
|
3426
|
+
const heybox = readMiniappHeyboxConfig(packageJson);
|
|
3427
|
+
return validateMiniappPlatforms(heybox?.platforms, 'package.json#heybox.platforms');
|
|
3428
|
+
}
|
|
3085
3429
|
function readMiniappPermissionsFromPackageJson(root, sdkVersion) {
|
|
3086
3430
|
const { packageJson } = readMiniappPackageJson(root, false);
|
|
3087
|
-
const heybox = packageJson
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
}
|
|
3091
|
-
const present = isRecord(heybox) && Object.prototype.hasOwnProperty.call(heybox, 'permissions');
|
|
3092
|
-
return parseMiniappPermissions(isRecord(heybox) ? heybox.permissions : undefined, {
|
|
3431
|
+
const heybox = readMiniappHeyboxConfig(packageJson);
|
|
3432
|
+
const present = heybox !== undefined && Object.prototype.hasOwnProperty.call(heybox, 'permissions');
|
|
3433
|
+
return parseMiniappPermissions(heybox?.permissions, {
|
|
3093
3434
|
present,
|
|
3094
3435
|
sdkVersion,
|
|
3095
3436
|
});
|
|
@@ -3113,9 +3454,17 @@ function readMiniappPackageJson(root, requireVersion) {
|
|
|
3113
3454
|
if (requireVersion && (typeof packageJson.version !== 'string' || packageJson.version.trim() === '')) {
|
|
3114
3455
|
throw new Error('@heybox/hb-sdk 提示:package.json.version 必须是非空字符串');
|
|
3115
3456
|
}
|
|
3116
|
-
return { packageJson, version: typeof packageJson.version === 'string' ? packageJson.version.trim() : '' };
|
|
3457
|
+
return { packageJson, packageJsonPath, version: typeof packageJson.version === 'string' ? packageJson.version.trim() : '' };
|
|
3117
3458
|
}
|
|
3118
|
-
function
|
|
3459
|
+
function readMiniappHeyboxConfig(packageJson) {
|
|
3460
|
+
if (packageJson.heybox === undefined)
|
|
3461
|
+
return undefined;
|
|
3462
|
+
if (!isRecord$1(packageJson.heybox)) {
|
|
3463
|
+
throw new Error('@heybox/hb-sdk 提示:package.json#heybox 必须是 JSON 对象');
|
|
3464
|
+
}
|
|
3465
|
+
return packageJson.heybox;
|
|
3466
|
+
}
|
|
3467
|
+
function isRecord$1(value) {
|
|
3119
3468
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3120
3469
|
}
|
|
3121
3470
|
function formatReason(error) {
|
|
@@ -3125,6 +3474,646 @@ function formatReason(error) {
|
|
|
3125
3474
|
return String(error);
|
|
3126
3475
|
}
|
|
3127
3476
|
|
|
3477
|
+
/** 检查原生入口的文件格式与目标架构;文件后缀和执行位不能代替此门禁。 */
|
|
3478
|
+
function assertCompanionExecutable(bytes, target) {
|
|
3479
|
+
const fail = () => { throw new Error('Companion 入口必须是目标架构的原生可执行文件,禁止脚本或损坏的二进制'); };
|
|
3480
|
+
if (target === 'windows-x64') {
|
|
3481
|
+
if (bytes.length < 64 || bytes.readUInt16LE(0) !== 0x5a4d)
|
|
3482
|
+
fail();
|
|
3483
|
+
const pe = bytes.readUInt32LE(60);
|
|
3484
|
+
if (pe < 64 || pe > bytes.length - 24 || bytes.readUInt32LE(pe) !== 0x00004550
|
|
3485
|
+
|| bytes.readUInt16LE(pe + 4) !== 0x8664)
|
|
3486
|
+
fail();
|
|
3487
|
+
const optionalSize = bytes.readUInt16LE(pe + 20);
|
|
3488
|
+
const characteristics = bytes.readUInt16LE(pe + 22);
|
|
3489
|
+
if (optionalSize < 112 || pe + 24 + optionalSize > bytes.length
|
|
3490
|
+
|| bytes.readUInt16LE(pe + 24) !== 0x20b || !(characteristics & 2) || (characteristics & 0x2000))
|
|
3491
|
+
fail();
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
const thin = (slice) => {
|
|
3495
|
+
if (slice.length < 32)
|
|
3496
|
+
return false;
|
|
3497
|
+
const magic = slice.readUInt32LE(0);
|
|
3498
|
+
if (magic !== 0xfeedfacf && magic !== 0xcffaedfe)
|
|
3499
|
+
return false;
|
|
3500
|
+
const read = (offset) => magic === 0xfeedfacf ? slice.readUInt32LE(offset) : slice.readUInt32BE(offset);
|
|
3501
|
+
return read(4) === 0x0100000c && read(12) === 2 && read(20) <= slice.length - 32;
|
|
3502
|
+
};
|
|
3503
|
+
if (thin(bytes))
|
|
3504
|
+
return;
|
|
3505
|
+
if (bytes.length < 8)
|
|
3506
|
+
fail();
|
|
3507
|
+
const magic = bytes.readUInt32BE(0);
|
|
3508
|
+
if (![0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic))
|
|
3509
|
+
fail();
|
|
3510
|
+
const little = magic === 0xbebafeca || magic === 0xbfbafeca;
|
|
3511
|
+
const wide = magic === 0xcafebabf || magic === 0xbfbafeca;
|
|
3512
|
+
const read = (offset) => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
|
|
3513
|
+
const count = read(4);
|
|
3514
|
+
const stride = wide ? 32 : 20;
|
|
3515
|
+
if (!count || count > 64 || 8 + count * stride > bytes.length)
|
|
3516
|
+
fail();
|
|
3517
|
+
let arm64 = false;
|
|
3518
|
+
const ranges = [];
|
|
3519
|
+
for (let i = 0; i < count; i++) {
|
|
3520
|
+
const record = 8 + i * stride;
|
|
3521
|
+
const integer = (offset) => {
|
|
3522
|
+
if (!wide)
|
|
3523
|
+
return read(offset);
|
|
3524
|
+
const value = little ? bytes.readBigUInt64LE(offset) : bytes.readBigUInt64BE(offset);
|
|
3525
|
+
return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : -1;
|
|
3526
|
+
};
|
|
3527
|
+
const offset = integer(record + 8);
|
|
3528
|
+
const size = integer(record + (wide ? 16 : 12));
|
|
3529
|
+
if (offset < 8 + count * stride || size < 1 || offset > bytes.length - size
|
|
3530
|
+
|| ranges.some(([start, end]) => offset < end && offset + size > start))
|
|
3531
|
+
fail();
|
|
3532
|
+
ranges.push([offset, offset + size]);
|
|
3533
|
+
if (read(record) === 0x0100000c) {
|
|
3534
|
+
if (!thin(bytes.subarray(offset, offset + size)))
|
|
3535
|
+
fail();
|
|
3536
|
+
arm64 = true;
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
if (!arm64)
|
|
3540
|
+
fail();
|
|
3541
|
+
}
|
|
3542
|
+
/** 只解析 bundle 主字典的 CFBundleExecutable,兼容 XML 与 binary plist。 */
|
|
3543
|
+
function companionBundleExecutable(plist) {
|
|
3544
|
+
let name;
|
|
3545
|
+
if (plist.subarray(0, 8).toString() === 'bplist00') {
|
|
3546
|
+
if (plist.length < 40)
|
|
3547
|
+
throw new Error('Companion Info.plist 损坏');
|
|
3548
|
+
const trailer = plist.length - 32;
|
|
3549
|
+
const offsetSize = plist[trailer + 6];
|
|
3550
|
+
const refSize = plist[trailer + 7];
|
|
3551
|
+
const integer = (offset, size) => {
|
|
3552
|
+
if (size < 1 || size > 8 || offset < 0 || offset + size > plist.length)
|
|
3553
|
+
throw new Error('Companion Info.plist 损坏');
|
|
3554
|
+
let value = 0;
|
|
3555
|
+
for (let i = 0; i < size; i++)
|
|
3556
|
+
value = value * 256 + plist[offset + i];
|
|
3557
|
+
if (!Number.isSafeInteger(value))
|
|
3558
|
+
throw new Error('Companion Info.plist 过大');
|
|
3559
|
+
return value;
|
|
3560
|
+
};
|
|
3561
|
+
const u64 = (offset) => {
|
|
3562
|
+
const value = plist.readBigUInt64BE(offset);
|
|
3563
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER))
|
|
3564
|
+
throw new Error('Companion Info.plist 过大');
|
|
3565
|
+
return Number(value);
|
|
3566
|
+
};
|
|
3567
|
+
const count = u64(trailer + 8), top = u64(trailer + 16), table = u64(trailer + 24);
|
|
3568
|
+
if (count > 10000 || top >= count || table < 8 || table + count * offsetSize > trailer)
|
|
3569
|
+
throw new Error('Companion Info.plist 损坏');
|
|
3570
|
+
const object = (ref) => {
|
|
3571
|
+
if (ref >= count)
|
|
3572
|
+
throw new Error('Companion Info.plist 损坏');
|
|
3573
|
+
const offset = integer(table + ref * offsetSize, offsetSize);
|
|
3574
|
+
if (offset < 8 || offset >= table)
|
|
3575
|
+
throw new Error('Companion Info.plist 损坏');
|
|
3576
|
+
const type = plist[offset] >> 4;
|
|
3577
|
+
let length = plist[offset] & 15, data = offset + 1;
|
|
3578
|
+
if (length === 15) {
|
|
3579
|
+
const marker = plist[data++];
|
|
3580
|
+
if ((marker >> 4) !== 1)
|
|
3581
|
+
throw new Error('Companion Info.plist 损坏');
|
|
3582
|
+
const size = 2 ** (marker & 15);
|
|
3583
|
+
length = integer(data, size);
|
|
3584
|
+
data += size;
|
|
3585
|
+
}
|
|
3586
|
+
if (length > 10000 || data + length * (type === 13 ? refSize * 2 : type === 6 ? 2 : 1) > table)
|
|
3587
|
+
throw new Error('Companion Info.plist 损坏');
|
|
3588
|
+
return { type, length, data };
|
|
3589
|
+
};
|
|
3590
|
+
const string = (ref) => {
|
|
3591
|
+
const item = object(ref);
|
|
3592
|
+
if (item.type === 5)
|
|
3593
|
+
return plist.subarray(item.data, item.data + item.length).toString('ascii');
|
|
3594
|
+
if (item.type === 6)
|
|
3595
|
+
return Buffer.from(plist.subarray(item.data, item.data + item.length * 2)).swap16().toString('utf16le');
|
|
3596
|
+
return undefined;
|
|
3597
|
+
};
|
|
3598
|
+
const root = object(top);
|
|
3599
|
+
if (root.type !== 13)
|
|
3600
|
+
throw new Error('Companion Info.plist 必须是字典');
|
|
3601
|
+
for (let i = 0; i < root.length; i++) {
|
|
3602
|
+
if (string(integer(root.data + i * refSize, refSize)) === 'CFBundleExecutable') {
|
|
3603
|
+
if (name !== undefined)
|
|
3604
|
+
throw new Error('Companion Info.plist 含重复入口');
|
|
3605
|
+
name = string(integer(root.data + (root.length + i) * refSize, refSize));
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
else {
|
|
3610
|
+
const xml = plist.toString('utf8').replace(/<!--[\s\S]*?-->/g, '');
|
|
3611
|
+
const matches = [...xml.matchAll(/<key>\s*CFBundleExecutable\s*<\/key>\s*<string>([^<]*)<\/string>/g)];
|
|
3612
|
+
if (matches.length === 1)
|
|
3613
|
+
name = matches[0][1];
|
|
3614
|
+
}
|
|
3615
|
+
if (typeof name !== 'string' || !/^[^/\\\u0000-\u001f\u007f&]{1,255}$/.test(name) || name === '.' || name === '..')
|
|
3616
|
+
throw new Error('Companion Info.plist 缺少安全的 CFBundleExecutable');
|
|
3617
|
+
return name;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
const MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
3621
|
+
const MAX_ENTRIES = 10_000;
|
|
3622
|
+
/** 双次读取校验输入稳定性;保留 .app 安全链接以避免破坏签名。 */
|
|
3623
|
+
function packCompanionDirectory(root, label) {
|
|
3624
|
+
root = realpathSync(root);
|
|
3625
|
+
const first = snapshot(root, label);
|
|
3626
|
+
let second;
|
|
3627
|
+
try {
|
|
3628
|
+
second = snapshot(root, label);
|
|
3629
|
+
}
|
|
3630
|
+
catch (cause) {
|
|
3631
|
+
throw new Error(`${label}.source 产物读取期间发生变化,请等待编译完成后重试`, { cause });
|
|
3632
|
+
}
|
|
3633
|
+
if (first.fingerprint !== second.fingerprint)
|
|
3634
|
+
throw new Error(`${label}.source 产物读取期间发生变化,请等待编译完成后重试`);
|
|
3635
|
+
return zip(first.entries);
|
|
3636
|
+
}
|
|
3637
|
+
function snapshot(root, label) {
|
|
3638
|
+
const entries = [];
|
|
3639
|
+
const hash = createHash('sha256');
|
|
3640
|
+
let total = 0;
|
|
3641
|
+
const inside = (candidate) => {
|
|
3642
|
+
const relative = path.relative(root, candidate);
|
|
3643
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
3644
|
+
};
|
|
3645
|
+
const checked = new Set();
|
|
3646
|
+
function checkCycles(diskPath, active) {
|
|
3647
|
+
if (active.size > 128)
|
|
3648
|
+
throw new Error(`${label}.source 目录层级过深`);
|
|
3649
|
+
const real = realpathSync(diskPath);
|
|
3650
|
+
if (!inside(real))
|
|
3651
|
+
throw new Error(`${label}.source 符号链接逃逸产物目录`);
|
|
3652
|
+
if (active.has(real))
|
|
3653
|
+
throw new Error(`${label}.source 包含循环符号链接`);
|
|
3654
|
+
if (checked.has(real))
|
|
3655
|
+
return;
|
|
3656
|
+
if (checked.size >= MAX_ENTRIES)
|
|
3657
|
+
throw new Error(`${label}.source 文件数超限`);
|
|
3658
|
+
if (lstatSync(real).isDirectory()) {
|
|
3659
|
+
const next = new Set(active).add(real);
|
|
3660
|
+
for (const child of readdirSync(real).sort())
|
|
3661
|
+
checkCycles(path.join(real, child), next);
|
|
3662
|
+
}
|
|
3663
|
+
checked.add(real);
|
|
3664
|
+
}
|
|
3665
|
+
checkCycles(root, new Set());
|
|
3666
|
+
function visit(diskPath, name, ancestors) {
|
|
3667
|
+
const stat = lstatSync(diskPath);
|
|
3668
|
+
hash.update(JSON.stringify([name, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs, stat.mode]));
|
|
3669
|
+
if (stat.isSymbolicLink()) {
|
|
3670
|
+
const link = readlinkSync(diskPath);
|
|
3671
|
+
const parts = name.split('/');
|
|
3672
|
+
const bundleIndex = parts.findIndex(segment => segment.endsWith('.app'));
|
|
3673
|
+
if (bundleIndex < 0 || !link || /[\u0000-\u001f\u007f]/.test(link) || path.isAbsolute(link) || link.includes('\\') || /^[A-Za-z]:/.test(link)) {
|
|
3674
|
+
throw new Error(`${label}.source 仅允许 .app 内部的相对符号链接`);
|
|
3675
|
+
}
|
|
3676
|
+
const resolvedLink = realpathSync(diskPath);
|
|
3677
|
+
const bundle = path.join(root, ...parts.slice(0, bundleIndex + 1));
|
|
3678
|
+
const lexicalLink = path.relative(bundle, path.resolve(path.dirname(diskPath), link));
|
|
3679
|
+
const relativeLink = path.relative(bundle, resolvedLink);
|
|
3680
|
+
if ([lexicalLink, relativeLink].some(relative => relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)))
|
|
3681
|
+
throw new Error(`${label}.source 符号链接逃逸产物目录`);
|
|
3682
|
+
if (ancestors.has(resolvedLink))
|
|
3683
|
+
throw new Error(`${label}.source 包含循环符号链接`);
|
|
3684
|
+
const targetStat = lstatSync(resolvedLink);
|
|
3685
|
+
if (!targetStat.isDirectory() && !targetStat.isFile())
|
|
3686
|
+
throw new Error(`${label}.source 不允许特殊文件`);
|
|
3687
|
+
if (entries.length >= MAX_ENTRIES)
|
|
3688
|
+
throw new Error(`${label}.source 文件数超限`);
|
|
3689
|
+
hash.update(link);
|
|
3690
|
+
entries.push({ name, bytes: Buffer.from(link), directory: false, executable: false, symlink: true });
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
const real = realpathSync(diskPath);
|
|
3694
|
+
if (!inside(real))
|
|
3695
|
+
throw new Error(`${label}.source 符号链接逃逸产物目录`);
|
|
3696
|
+
if (ancestors.has(real))
|
|
3697
|
+
throw new Error(`${label}.source 包含循环符号链接`);
|
|
3698
|
+
const resolved = lstatSync(real);
|
|
3699
|
+
if (!resolved.isDirectory() && !resolved.isFile())
|
|
3700
|
+
throw new Error(`${label}.source 不允许特殊文件`);
|
|
3701
|
+
if (name) {
|
|
3702
|
+
if (entries.length >= MAX_ENTRIES)
|
|
3703
|
+
throw new Error(`${label}.source 文件数超限`);
|
|
3704
|
+
if (resolved.isFile() && (total += resolved.size) > MAX_BYTES)
|
|
3705
|
+
throw new Error(`${label}.source 文件大小预算超限`);
|
|
3706
|
+
const bytes = resolved.isFile() ? readFileSync(real) : Buffer.alloc(0);
|
|
3707
|
+
hash.update(bytes);
|
|
3708
|
+
entries.push({ name, bytes, directory: resolved.isDirectory(), executable: (resolved.mode & 0o111) !== 0 });
|
|
3709
|
+
}
|
|
3710
|
+
if (resolved.isDirectory()) {
|
|
3711
|
+
const next = new Set(ancestors).add(real);
|
|
3712
|
+
for (const child of readdirSync(real).sort())
|
|
3713
|
+
visit(path.join(real, child), name ? `${name}/${child}` : child, next);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
visit(root, '', new Set());
|
|
3717
|
+
return { entries, fingerprint: hash.digest('hex') };
|
|
3718
|
+
}
|
|
3719
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, value) => {
|
|
3720
|
+
let crc = value;
|
|
3721
|
+
for (let bit = 0; bit < 8; bit++)
|
|
3722
|
+
crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
|
|
3723
|
+
return crc >>> 0;
|
|
3724
|
+
});
|
|
3725
|
+
function crc32(bytes) {
|
|
3726
|
+
let crc = 0xffffffff;
|
|
3727
|
+
for (const byte of bytes) {
|
|
3728
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ byte) & 0xff];
|
|
3729
|
+
}
|
|
3730
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
3731
|
+
}
|
|
3732
|
+
/** 使用 ZIP store 避免不同压缩器版本改变摘要,也避免高压缩率资源触发解压预算门禁。 */
|
|
3733
|
+
function zip(entries) {
|
|
3734
|
+
const locals = [];
|
|
3735
|
+
const records = [];
|
|
3736
|
+
let offset = 0;
|
|
3737
|
+
for (const entry of entries) {
|
|
3738
|
+
const name = Buffer.from(entry.name + (entry.directory ? '/' : ''));
|
|
3739
|
+
if (name.length > 65535)
|
|
3740
|
+
throw new Error('Companion 产物路径过长');
|
|
3741
|
+
const crc = crc32(entry.bytes);
|
|
3742
|
+
const local = Buffer.alloc(30 + name.length);
|
|
3743
|
+
local.writeUInt32LE(0x04034b50);
|
|
3744
|
+
local.writeUInt16LE(20, 4);
|
|
3745
|
+
local.writeUInt16LE(0x800, 6);
|
|
3746
|
+
local.writeUInt16LE(33, 12);
|
|
3747
|
+
local.writeUInt32LE(crc, 14);
|
|
3748
|
+
local.writeUInt32LE(entry.bytes.length, 18);
|
|
3749
|
+
local.writeUInt32LE(entry.bytes.length, 22);
|
|
3750
|
+
local.writeUInt16LE(name.length, 26);
|
|
3751
|
+
name.copy(local, 30);
|
|
3752
|
+
const record = Buffer.alloc(46 + name.length);
|
|
3753
|
+
record.writeUInt32LE(0x02014b50);
|
|
3754
|
+
record.writeUInt16LE(0x314, 4);
|
|
3755
|
+
record.writeUInt16LE(20, 6);
|
|
3756
|
+
record.writeUInt16LE(0x800, 8);
|
|
3757
|
+
record.writeUInt16LE(33, 14);
|
|
3758
|
+
record.writeUInt32LE(crc, 16);
|
|
3759
|
+
record.writeUInt32LE(entry.bytes.length, 20);
|
|
3760
|
+
record.writeUInt32LE(entry.bytes.length, 24);
|
|
3761
|
+
record.writeUInt16LE(name.length, 28);
|
|
3762
|
+
const mode = entry.symlink ? 0o120777 : entry.directory ? 0o040755 : entry.executable ? 0o100755 : 0o100644;
|
|
3763
|
+
record.writeUInt32LE((mode * 65536) >>> 0, 38);
|
|
3764
|
+
record.writeUInt32LE(offset, 42);
|
|
3765
|
+
name.copy(record, 46);
|
|
3766
|
+
locals.push(local, entry.bytes);
|
|
3767
|
+
records.push(record);
|
|
3768
|
+
offset += local.length + entry.bytes.length;
|
|
3769
|
+
}
|
|
3770
|
+
const central = Buffer.concat(records);
|
|
3771
|
+
const end = Buffer.alloc(22);
|
|
3772
|
+
end.writeUInt32LE(0x06054b50);
|
|
3773
|
+
end.writeUInt16LE(entries.length, 8);
|
|
3774
|
+
end.writeUInt16LE(entries.length, 10);
|
|
3775
|
+
end.writeUInt32LE(central.length, 12);
|
|
3776
|
+
end.writeUInt32LE(offset, 16);
|
|
3777
|
+
return Buffer.concat([...locals, central, end]);
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
const MINIAPP_COMPANION_TARGETS = ['windows-x64', 'macos-arm64'];
|
|
3781
|
+
const MINIAPP_COMPANION_CAPABILITIES = ['stdio', 'attachments', 'notifications'];
|
|
3782
|
+
|
|
3783
|
+
const MAX_ENTRY_COUNT = 10_000;
|
|
3784
|
+
const MAX_UNCOMPRESSED_BYTES = 2 * 1024 * 1024 * 1024;
|
|
3785
|
+
const MAX_COMPRESSION_RATIO = 200;
|
|
3786
|
+
const MAX_DISPLAY_NAME_CODE_POINTS = 120;
|
|
3787
|
+
const MAX_ARG_COUNT = 64;
|
|
3788
|
+
const MAX_ARG_BYTES = 1024;
|
|
3789
|
+
const MAX_ARGS_TOTAL_BYTES = 16_384;
|
|
3790
|
+
function prepareMiniappCompanions(declarations, options) {
|
|
3791
|
+
if (declarations === undefined)
|
|
3792
|
+
return undefined;
|
|
3793
|
+
if (!isRecord(declarations) || Object.keys(declarations).length === 0) {
|
|
3794
|
+
throw new Error('package.json#heybox.companions 必须是非空对象');
|
|
3795
|
+
}
|
|
3796
|
+
const output = {};
|
|
3797
|
+
for (const alias of Object.keys(declarations).sort()) {
|
|
3798
|
+
if (!COMPANION_ALIAS_PATTERN.test(alias))
|
|
3799
|
+
throw new Error(`Companion alias 不合法:${alias}`);
|
|
3800
|
+
const definition = declarations[alias];
|
|
3801
|
+
assertExactKeys(definition, ['displayName', 'capabilities', 'targets'], `companions.${alias}`);
|
|
3802
|
+
if (typeof definition.displayName !== 'string' || !definition.displayName.trim()) {
|
|
3803
|
+
throw new Error(`companions.${alias}.displayName 必须是非空字符串`);
|
|
3804
|
+
}
|
|
3805
|
+
if (Array.from(definition.displayName.trim()).length > MAX_DISPLAY_NAME_CODE_POINTS) {
|
|
3806
|
+
throw new Error(`companions.${alias}.displayName 最多允许 ${MAX_DISPLAY_NAME_CODE_POINTS} 个 Unicode 字符`);
|
|
3807
|
+
}
|
|
3808
|
+
const capabilities = validateCapabilities(definition.capabilities, alias);
|
|
3809
|
+
if (!isRecord(definition.targets) || Object.keys(definition.targets).length === 0) {
|
|
3810
|
+
throw new Error(`companions.${alias}.targets 必须是非空对象`);
|
|
3811
|
+
}
|
|
3812
|
+
const targets = {};
|
|
3813
|
+
for (const targetName of Object.keys(definition.targets).sort()) {
|
|
3814
|
+
if (!MINIAPP_COMPANION_TARGETS.includes(targetName)) {
|
|
3815
|
+
throw new Error(`companions.${alias}.targets 包含不支持的平台:${targetName}`);
|
|
3816
|
+
}
|
|
3817
|
+
const platform = targetName.startsWith('windows-') ? 'windows' : 'macos';
|
|
3818
|
+
if (options.platforms && !options.platforms.includes(platform)) {
|
|
3819
|
+
throw new Error(`companions.${alias}.targets.${targetName} 未包含在 package.json#heybox.platforms 中`);
|
|
3820
|
+
}
|
|
3821
|
+
const target = definition.targets[targetName];
|
|
3822
|
+
if (!target)
|
|
3823
|
+
continue;
|
|
3824
|
+
if (options.targets && !options.targets.includes(targetName))
|
|
3825
|
+
continue;
|
|
3826
|
+
targets[targetName] = prepareTarget(alias, targetName, target, options);
|
|
3827
|
+
}
|
|
3828
|
+
if (!options.targets || Object.keys(targets).length)
|
|
3829
|
+
output[alias] = { displayName: definition.displayName.trim(), capabilities, targets };
|
|
3830
|
+
}
|
|
3831
|
+
return options.targets && Object.keys(output).length === 0 ? undefined : output;
|
|
3832
|
+
}
|
|
3833
|
+
function prepareTarget(alias, targetName, target, options) {
|
|
3834
|
+
const label = `companions.${alias}.targets.${targetName}`;
|
|
3835
|
+
assertExactKeys(target, ['source', 'entrypoint', 'elevation', 'args'], label);
|
|
3836
|
+
if (typeof target.source !== 'string' || !target.source.trim())
|
|
3837
|
+
throw new Error(`${label}.source 必须是非空字符串`);
|
|
3838
|
+
if (!isRecord(target.entrypoint))
|
|
3839
|
+
throw new Error(`${label}.entrypoint 必须是对象`);
|
|
3840
|
+
assertExactKeys(target.entrypoint, ['kind', 'path'], `${label}.entrypoint`);
|
|
3841
|
+
const entrypointPath = validateArchivePath(target.entrypoint.path, `${label}.entrypoint.path`);
|
|
3842
|
+
validateEntrypoint(targetName, target.entrypoint.kind, entrypointPath, label);
|
|
3843
|
+
const elevation = target.elevation ?? 'none';
|
|
3844
|
+
if (elevation !== 'none')
|
|
3845
|
+
throw new Error(`${label}.elevation V1 仅支持 none,暂不支持管理员权限启动`);
|
|
3846
|
+
const args = validateArgs(target.args, label);
|
|
3847
|
+
const projectRoot = realpathSync(options.root);
|
|
3848
|
+
const outputRoot = resolveRealPathIfPresent(options.outputRoot);
|
|
3849
|
+
const sourcePath = path.resolve(projectRoot, target.source);
|
|
3850
|
+
if (!isPathInside(projectRoot, sourcePath))
|
|
3851
|
+
throw new Error(`${label}.source 必须位于项目目录内`);
|
|
3852
|
+
if (isPathInside(outputRoot, sourcePath, true))
|
|
3853
|
+
throw new Error(`${label}.source 不能位于构建输出目录内`);
|
|
3854
|
+
const sourceStat = lstatSync(sourcePath);
|
|
3855
|
+
if (sourceStat.isFile())
|
|
3856
|
+
throw new Error(`${label}.source 必须是产物目录;请将旧 ZIP 解压到 companion 目录并修改 source,SDK 会自动打包`);
|
|
3857
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory())
|
|
3858
|
+
throw new Error(`${label}.source 必须是普通产物目录`);
|
|
3859
|
+
const realSourcePath = realpathSync(sourcePath);
|
|
3860
|
+
if (!isPathInside(projectRoot, realSourcePath))
|
|
3861
|
+
throw new Error(`${label}.source 不能通过符号链接逃逸项目目录`);
|
|
3862
|
+
if (isPathInside(outputRoot, realSourcePath, true))
|
|
3863
|
+
throw new Error(`${label}.source 不能位于构建输出目录内`);
|
|
3864
|
+
if (isPathInside(realSourcePath, outputRoot, true))
|
|
3865
|
+
throw new Error(`${label}.source 不能包含构建输出目录`);
|
|
3866
|
+
const archive = packCompanionDirectory(realSourcePath, label);
|
|
3867
|
+
const entries = inspectZipArchive(archive, label);
|
|
3868
|
+
assertEntrypointExists(entries, target.entrypoint.kind, entrypointPath, label);
|
|
3869
|
+
const readEntry = (name, visited = new Set()) => {
|
|
3870
|
+
if (visited.has(name))
|
|
3871
|
+
throw new Error(`${label} 包含循环符号链接`);
|
|
3872
|
+
visited.add(name);
|
|
3873
|
+
const segments = name.split('/');
|
|
3874
|
+
for (let end = 1; end <= segments.length; end++) {
|
|
3875
|
+
const prefix = segments.slice(0, end).join('/');
|
|
3876
|
+
const link = entries.find(item => item.name === prefix && item.symlink);
|
|
3877
|
+
if (link) {
|
|
3878
|
+
const target = archive.subarray(link.dataStart, link.localEnd).toString('utf8');
|
|
3879
|
+
const resolved = path.posix.join(path.posix.dirname(prefix), target, ...segments.slice(end));
|
|
3880
|
+
validateArchivePath(resolved, `${label} 符号链接目标`);
|
|
3881
|
+
return readEntry(resolved, visited);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
const entry = entries.find((item) => item.name === name && !item.directory);
|
|
3885
|
+
if (!entry)
|
|
3886
|
+
throw new Error(`${label} 的入口文件在 ZIP 中不存在`);
|
|
3887
|
+
const compressed = archive.subarray(entry.dataStart, entry.localEnd);
|
|
3888
|
+
const bytes = entry.compressionMethod === 0 ? compressed
|
|
3889
|
+
: entry.compressionMethod === 8 ? inflateRawSync(compressed, { maxOutputLength: Math.max(1, entry.uncompressedSize) })
|
|
3890
|
+
: undefined;
|
|
3891
|
+
if (!bytes || bytes.length !== entry.uncompressedSize)
|
|
3892
|
+
throw new Error(`${label} 的入口 ZIP 数据损坏或压缩格式不支持`);
|
|
3893
|
+
return bytes;
|
|
3894
|
+
};
|
|
3895
|
+
const executablePath = target.entrypoint.kind === 'executable' ? entrypointPath
|
|
3896
|
+
: `${entrypointPath}/Contents/MacOS/${companionBundleExecutable(readEntry(`${entrypointPath}/Contents/Info.plist`))}`;
|
|
3897
|
+
assertCompanionExecutable(readEntry(executablePath), targetName);
|
|
3898
|
+
const relativePath = `companions/${alias}/${targetName}.zip`;
|
|
3899
|
+
const destination = path.join(options.outputRoot, ...relativePath.split('/'));
|
|
3900
|
+
if (options.copyArtifacts !== false) {
|
|
3901
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
3902
|
+
const temporaryDestination = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
3903
|
+
try {
|
|
3904
|
+
writeFileSync(temporaryDestination, archive, { flag: 'wx' });
|
|
3905
|
+
renameSync(temporaryDestination, destination);
|
|
3906
|
+
}
|
|
3907
|
+
finally {
|
|
3908
|
+
rmSync(temporaryDestination, { force: true });
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
const size = archive.length;
|
|
3912
|
+
options.onPreparedArtifact?.({ archive, path: relativePath, size, sourcePath: realSourcePath });
|
|
3913
|
+
return {
|
|
3914
|
+
path: relativePath,
|
|
3915
|
+
size,
|
|
3916
|
+
sha256: createHash('sha256').update(archive).digest('hex'),
|
|
3917
|
+
entrypoint: { kind: target.entrypoint.kind, path: entrypointPath },
|
|
3918
|
+
elevation,
|
|
3919
|
+
args,
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
function inspectZipArchive(archive, label) {
|
|
3923
|
+
const eocd = findEndOfCentralDirectory(archive);
|
|
3924
|
+
const entryCount = archive.readUInt16LE(eocd + 10);
|
|
3925
|
+
if (entryCount !== archive.readUInt16LE(eocd + 8))
|
|
3926
|
+
throw new Error(`${label}.source ZIP 条目数量不一致`);
|
|
3927
|
+
const centralSize = archive.readUInt32LE(eocd + 12);
|
|
3928
|
+
const centralOffset = archive.readUInt32LE(eocd + 16);
|
|
3929
|
+
if (entryCount === 0 || entryCount > MAX_ENTRY_COUNT)
|
|
3930
|
+
throw new Error(`${label}.source ZIP 文件数不合法`);
|
|
3931
|
+
if (centralOffset + centralSize > eocd)
|
|
3932
|
+
throw new Error(`${label}.source ZIP 中央目录损坏`);
|
|
3933
|
+
const names = new Set();
|
|
3934
|
+
const foldedNames = new Set();
|
|
3935
|
+
const entries = [];
|
|
3936
|
+
let cursor = centralOffset;
|
|
3937
|
+
let totalUncompressed = 0;
|
|
3938
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
3939
|
+
if (cursor + 46 > archive.length || archive.readUInt32LE(cursor) !== 0x02014b50)
|
|
3940
|
+
throw new Error(`${label}.source ZIP 中央目录损坏`);
|
|
3941
|
+
const flags = archive.readUInt16LE(cursor + 8);
|
|
3942
|
+
const compressionMethod = archive.readUInt16LE(cursor + 10);
|
|
3943
|
+
const crc32 = archive.readUInt32LE(cursor + 16);
|
|
3944
|
+
const compressedSize = archive.readUInt32LE(cursor + 20);
|
|
3945
|
+
const uncompressedSize = archive.readUInt32LE(cursor + 24);
|
|
3946
|
+
const nameLength = archive.readUInt16LE(cursor + 28);
|
|
3947
|
+
const extraLength = archive.readUInt16LE(cursor + 30);
|
|
3948
|
+
const commentLength = archive.readUInt16LE(cursor + 32);
|
|
3949
|
+
const externalAttributes = archive.readUInt32LE(cursor + 38);
|
|
3950
|
+
const localOffset = archive.readUInt32LE(cursor + 42);
|
|
3951
|
+
const end = cursor + 46 + nameLength + extraLength + commentLength;
|
|
3952
|
+
if (end > archive.length || (flags & 1) !== 0)
|
|
3953
|
+
throw new Error(`${label}.source ZIP 包含加密或损坏的条目`);
|
|
3954
|
+
if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff)
|
|
3955
|
+
throw new Error(`${label}.source ZIP 暂不支持 ZIP64`);
|
|
3956
|
+
const centralName = archive.subarray(cursor + 46, cursor + 46 + nameLength);
|
|
3957
|
+
const name = centralName.toString('utf8');
|
|
3958
|
+
if ((flags & 0x800) === 0 && /[^\x00-\x7f]/.test(name))
|
|
3959
|
+
throw new Error(`${label}.source ZIP 非 ASCII 路径必须声明 UTF-8`);
|
|
3960
|
+
const normalized = validateArchivePath(name.replace(/\/$/, ''), `${label}.source ZIP 条目`);
|
|
3961
|
+
const unixMode = externalAttributes >>> 16;
|
|
3962
|
+
const fileType = unixMode & 0o170000;
|
|
3963
|
+
if (fileType && fileType !== 0o100000 && fileType !== 0o040000 && fileType !== 0o120000)
|
|
3964
|
+
throw new Error(`${label}.source ZIP 不允许特殊文件:${normalized}`);
|
|
3965
|
+
const folded = normalized.toLocaleLowerCase('en-US');
|
|
3966
|
+
if (names.has(normalized) || foldedNames.has(folded))
|
|
3967
|
+
throw new Error(`${label}.source ZIP 包含重复或大小写冲突条目:${normalized}`);
|
|
3968
|
+
names.add(normalized);
|
|
3969
|
+
foldedNames.add(folded);
|
|
3970
|
+
totalUncompressed += uncompressedSize;
|
|
3971
|
+
if (totalUncompressed > MAX_UNCOMPRESSED_BYTES ||
|
|
3972
|
+
(compressedSize === 0 ? uncompressedSize > 0 : uncompressedSize / compressedSize > MAX_COMPRESSION_RATIO)) {
|
|
3973
|
+
throw new Error(`${label}.source ZIP 解压预算超限`);
|
|
3974
|
+
}
|
|
3975
|
+
const local = inspectLocalEntry(archive, {
|
|
3976
|
+
centralOffset,
|
|
3977
|
+
compressedSize,
|
|
3978
|
+
compressionMethod,
|
|
3979
|
+
crc32,
|
|
3980
|
+
flags,
|
|
3981
|
+
localOffset,
|
|
3982
|
+
name: centralName,
|
|
3983
|
+
uncompressedSize,
|
|
3984
|
+
}, label);
|
|
3985
|
+
entries.push({ name: normalized, directory: name.endsWith('/'), symlink: fileType === 0o120000, ...local });
|
|
3986
|
+
cursor = end;
|
|
3987
|
+
}
|
|
3988
|
+
if (cursor !== centralOffset + centralSize)
|
|
3989
|
+
throw new Error(`${label}.source ZIP 中央目录长度不一致`);
|
|
3990
|
+
const orderedLocalEntries = [...entries].sort((left, right) => left.localStart - right.localStart);
|
|
3991
|
+
for (let index = 1; index < orderedLocalEntries.length; index += 1) {
|
|
3992
|
+
if (orderedLocalEntries[index].localStart < orderedLocalEntries[index - 1].localEnd) {
|
|
3993
|
+
throw new Error(`${label}.source ZIP local entry 范围重叠`);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
return entries;
|
|
3997
|
+
}
|
|
3998
|
+
function inspectLocalEntry(archive, expected, label) {
|
|
3999
|
+
const start = expected.localOffset;
|
|
4000
|
+
if (start + 30 > expected.centralOffset || archive.readUInt32LE(start) !== 0x04034b50) {
|
|
4001
|
+
throw new Error(`${label}.source ZIP local header 损坏`);
|
|
4002
|
+
}
|
|
4003
|
+
const flags = archive.readUInt16LE(start + 6);
|
|
4004
|
+
const compressionMethod = archive.readUInt16LE(start + 8);
|
|
4005
|
+
const crc32 = archive.readUInt32LE(start + 14);
|
|
4006
|
+
const compressedSize = archive.readUInt32LE(start + 18);
|
|
4007
|
+
const uncompressedSize = archive.readUInt32LE(start + 22);
|
|
4008
|
+
const nameLength = archive.readUInt16LE(start + 26);
|
|
4009
|
+
const extraLength = archive.readUInt16LE(start + 28);
|
|
4010
|
+
const nameStart = start + 30;
|
|
4011
|
+
const dataStart = nameStart + nameLength + extraLength;
|
|
4012
|
+
const dataEnd = dataStart + expected.compressedSize;
|
|
4013
|
+
if (dataEnd > expected.centralOffset || !archive.subarray(nameStart, nameStart + nameLength).equals(expected.name)) {
|
|
4014
|
+
throw new Error(`${label}.source ZIP central/local 路径或数据范围不一致`);
|
|
4015
|
+
}
|
|
4016
|
+
if (flags !== expected.flags || compressionMethod !== expected.compressionMethod) {
|
|
4017
|
+
throw new Error(`${label}.source ZIP central/local metadata 不一致`);
|
|
4018
|
+
}
|
|
4019
|
+
if ((flags & 0x08) === 0 &&
|
|
4020
|
+
(crc32 !== expected.crc32 || compressedSize !== expected.compressedSize || uncompressedSize !== expected.uncompressedSize)) {
|
|
4021
|
+
throw new Error(`${label}.source ZIP central/local 大小或 CRC 不一致`);
|
|
4022
|
+
}
|
|
4023
|
+
return { localStart: start, localEnd: dataEnd, dataStart, compressionMethod, uncompressedSize: expected.uncompressedSize };
|
|
4024
|
+
}
|
|
4025
|
+
function findEndOfCentralDirectory(archive) {
|
|
4026
|
+
const minimum = Math.max(0, archive.length - 65_557);
|
|
4027
|
+
for (let offset = archive.length - 22; offset >= minimum; offset -= 1) {
|
|
4028
|
+
if (archive.readUInt32LE(offset) === 0x06054b50) {
|
|
4029
|
+
if (archive.readUInt16LE(offset + 4) !== 0 || archive.readUInt16LE(offset + 6) !== 0)
|
|
4030
|
+
throw new Error('Companion ZIP 不支持多磁盘归档');
|
|
4031
|
+
return offset;
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
throw new Error('Companion source 不是有效 ZIP');
|
|
4035
|
+
}
|
|
4036
|
+
function validateArchivePath(value, label) {
|
|
4037
|
+
if (typeof value !== 'string' || !value || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value))
|
|
4038
|
+
throw new Error(`${label} 必须是 ZIP 内相对 POSIX 路径`);
|
|
4039
|
+
const segments = value.split('/');
|
|
4040
|
+
if (segments.some((segment) => !segment || segment === '.' || segment === '..') || /[\u0000-\u001f\u007f]/.test(value))
|
|
4041
|
+
throw new Error(`${label} 包含不安全路径`);
|
|
4042
|
+
return value;
|
|
4043
|
+
}
|
|
4044
|
+
function validateEntrypoint(target, kind, entrypointPath, label) {
|
|
4045
|
+
if (kind !== 'executable' && kind !== 'app-bundle')
|
|
4046
|
+
throw new Error(`${label}.entrypoint.kind 不合法`);
|
|
4047
|
+
if (target === 'windows-x64' && (kind !== 'executable' || !entrypointPath.toLowerCase().endsWith('.exe')))
|
|
4048
|
+
throw new Error(`${label} 必须声明 .exe executable`);
|
|
4049
|
+
if (target === 'macos-arm64' && kind === 'app-bundle' && !entrypointPath.endsWith('.app'))
|
|
4050
|
+
throw new Error(`${label} 的 app-bundle 必须指向 .app`);
|
|
4051
|
+
}
|
|
4052
|
+
function assertEntrypointExists(entries, kind, entrypointPath, label) {
|
|
4053
|
+
const exists = kind === 'app-bundle'
|
|
4054
|
+
? entries.some((entry) => entry.name === entrypointPath || entry.name.startsWith(`${entrypointPath}/`))
|
|
4055
|
+
: entries.some((entry) => entry.name === entrypointPath && !entry.directory);
|
|
4056
|
+
if (!exists)
|
|
4057
|
+
throw new Error(`${label}.entrypoint 在 ZIP 中不存在:${entrypointPath}`);
|
|
4058
|
+
}
|
|
4059
|
+
function validateCapabilities(value, alias) {
|
|
4060
|
+
if (value === undefined)
|
|
4061
|
+
return [];
|
|
4062
|
+
if (!Array.isArray(value))
|
|
4063
|
+
throw new Error(`companions.${alias}.capabilities 必须是数组`);
|
|
4064
|
+
const result = [];
|
|
4065
|
+
for (const capability of value) {
|
|
4066
|
+
if (!MINIAPP_COMPANION_CAPABILITIES.includes(capability))
|
|
4067
|
+
throw new Error(`companions.${alias}.capabilities 包含未知能力:${capability}`);
|
|
4068
|
+
if (result.includes(capability))
|
|
4069
|
+
throw new Error(`companions.${alias}.capabilities 包含重复能力:${capability}`);
|
|
4070
|
+
result.push(capability);
|
|
4071
|
+
}
|
|
4072
|
+
return result.sort((left, right) => MINIAPP_COMPANION_CAPABILITIES.indexOf(left) - MINIAPP_COMPANION_CAPABILITIES.indexOf(right));
|
|
4073
|
+
}
|
|
4074
|
+
function validateArgs(value, label) {
|
|
4075
|
+
if (value === undefined)
|
|
4076
|
+
return [];
|
|
4077
|
+
if (!Array.isArray(value) || value.length > MAX_ARG_COUNT)
|
|
4078
|
+
throw new Error(`${label}.args 不合法`);
|
|
4079
|
+
let totalBytes = 0;
|
|
4080
|
+
for (const item of value) {
|
|
4081
|
+
if (typeof item !== 'string' || /[\u0000\r\n]/.test(item))
|
|
4082
|
+
throw new Error(`${label}.args 不合法`);
|
|
4083
|
+
const bytes = Buffer.byteLength(item, 'utf8');
|
|
4084
|
+
if (bytes > MAX_ARG_BYTES)
|
|
4085
|
+
throw new Error(`${label}.args 单项最多允许 ${MAX_ARG_BYTES} UTF-8 bytes`);
|
|
4086
|
+
totalBytes += bytes;
|
|
4087
|
+
}
|
|
4088
|
+
if (totalBytes > MAX_ARGS_TOTAL_BYTES)
|
|
4089
|
+
throw new Error(`${label}.args 总计最多允许 ${MAX_ARGS_TOTAL_BYTES} UTF-8 bytes`);
|
|
4090
|
+
return [...value];
|
|
4091
|
+
}
|
|
4092
|
+
function resolveRealPathIfPresent(value) {
|
|
4093
|
+
try {
|
|
4094
|
+
return realpathSync(value);
|
|
4095
|
+
}
|
|
4096
|
+
catch {
|
|
4097
|
+
const absolute = path.resolve(value);
|
|
4098
|
+
const parent = path.dirname(absolute);
|
|
4099
|
+
return parent === absolute ? absolute : path.join(resolveRealPathIfPresent(parent), path.basename(absolute));
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
function isPathInside(root, candidate, allowEqual = false) {
|
|
4103
|
+
const relative = path.relative(root, candidate);
|
|
4104
|
+
return ((allowEqual && relative === '') || (relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)));
|
|
4105
|
+
}
|
|
4106
|
+
function assertExactKeys(value, allowed, label) {
|
|
4107
|
+
if (!isRecord(value))
|
|
4108
|
+
throw new Error(`${label} 必须是对象`);
|
|
4109
|
+
const unknown = Object.keys(value).find((key) => !allowed.includes(key));
|
|
4110
|
+
if (unknown)
|
|
4111
|
+
throw new Error(`${label} 包含未知字段:${unknown}`);
|
|
4112
|
+
}
|
|
4113
|
+
function isRecord(value) {
|
|
4114
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4115
|
+
}
|
|
4116
|
+
|
|
3128
4117
|
const UNDEFINED_CODE_POINTS = new Set([
|
|
3129
4118
|
65534, 65535, 131070, 131071, 196606, 196607, 262142, 262143, 327678, 327679, 393214,
|
|
3130
4119
|
393215, 458750, 458751, 524286, 524287, 589822, 589823, 655358, 655359, 720894,
|
|
@@ -11889,7 +12878,7 @@ function shouldSkipMiniappPlatformCspFromEnv(env = process.env) {
|
|
|
11889
12878
|
* 检测同款 INSTALL_FLAG,不会重复 patch。
|
|
11890
12879
|
*/
|
|
11891
12880
|
function injectMiniDevConsoleBootstrap(html) {
|
|
11892
|
-
const bootstrap = `<script>(function(){var LEVELS=['log','debug','info','warn','error'];var FLAG='${MINI_DEV_CONSOLE_INSTALL_FLAG}';var TYPE='${MINI_DEV_CONSOLE_EVENT_TYPE}';if(window
|
|
12881
|
+
const bootstrap = `<script>(function(){var LEVELS=['log','debug','info','warn','error'];var FLAG='${MINI_DEV_CONSOLE_INSTALL_FLAG}';var TYPE='${MINI_DEV_CONSOLE_EVENT_TYPE}';if(window[FLAG])return;var patched=false;for(var i=0;i<LEVELS.length;i++)(function(level){var original=console[level];if(typeof original!=='function')return;console[level]=function(){try{parent.postMessage({type:TYPE,detail:{level:level,args:Array.prototype.slice.call(arguments),timestamp:Date.now()}},'*')}catch(e){}return original.apply(this,arguments)};patched=true})(LEVELS[i]);if(patched)window[FLAG]=true})();</script>`;
|
|
11893
12882
|
const headIndex = html.indexOf('<head>');
|
|
11894
12883
|
if (headIndex < 0) {
|
|
11895
12884
|
return html;
|
|
@@ -11900,9 +12889,29 @@ function injectMiniDevConsoleBootstrap(html) {
|
|
|
11900
12889
|
const sdkVersionPlaceholder = ['__HB_SDK', 'VERSION__'].join('_');
|
|
11901
12890
|
const sdkVersionBuildConstant = ['__HB_SDK_BUILD', 'VERSION__'].join('_');
|
|
11902
12891
|
const miniDevLoggingConstant = ['__HB_SDK_DEV', 'LOGGING__'].join('_');
|
|
11903
|
-
|
|
11904
|
-
|
|
12892
|
+
const DEV_MANIFEST_PATH = '/__hb_sdk__/companion/manifest.json';
|
|
12893
|
+
const DEV_TOKEN_HEADER = 'x-hb-sdk-companion-dev-token';
|
|
12894
|
+
const DEV_DIGEST_HEADER = 'x-hb-sdk-companion-manifest-sha256';
|
|
12895
|
+
const DEV_ERROR_MESSAGE = 'Companion 开发资源暂时不可用';
|
|
12896
|
+
const DEV_COMPANION_PATH_PATTERN = /^\/__hb_sdk__\/companions\/([a-z][a-z0-9-]{0,31})\/(windows-x64|macos-arm64)\.zip$/;
|
|
12897
|
+
const HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV = 'HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN';
|
|
12898
|
+
const devSnapshotStoreKey = Symbol.for('@heybox/hb-sdk/dev-companion-snapshots');
|
|
12899
|
+
const devSnapshotRegistry = globalThis;
|
|
12900
|
+
if (!devSnapshotRegistry[devSnapshotStoreKey]) {
|
|
12901
|
+
devSnapshotRegistry[devSnapshotStoreKey] = new Map();
|
|
12902
|
+
process.once('exit', () => {
|
|
12903
|
+
for (const store of devSnapshotRegistry[devSnapshotStoreKey].values()) {
|
|
12904
|
+
for (const snapshot of store.snapshots.values())
|
|
12905
|
+
rmSync(snapshot.root, { force: true, recursive: true });
|
|
12906
|
+
}
|
|
12907
|
+
});
|
|
12908
|
+
}
|
|
12909
|
+
const devSnapshotStores = devSnapshotRegistry[devSnapshotStoreKey];
|
|
12910
|
+
function miniappManifest() {
|
|
12911
|
+
const includeCompanions = process.env.HB_SDK_SKIP_COMPANIONS !== '1';
|
|
12912
|
+
const deviceLogs = new DeviceLogStore();
|
|
11905
12913
|
const skipPlatformCsp = shouldSkipMiniappPlatformCspFromEnv();
|
|
12914
|
+
let companionArtifactAccessToken = parseCompanionArtifactAccessToken(process.env[HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV]);
|
|
11906
12915
|
let root = process.cwd();
|
|
11907
12916
|
let outDir = 'dist';
|
|
11908
12917
|
let command = 'build';
|
|
@@ -11937,6 +12946,211 @@ function miniappManifest(options) {
|
|
|
11937
12946
|
command = resolved.command;
|
|
11938
12947
|
hmrWebSocketUrl = resolveHmrWebSocketUrl(resolved);
|
|
11939
12948
|
},
|
|
12949
|
+
configureServer(server) {
|
|
12950
|
+
let unregisterLogs;
|
|
12951
|
+
const registerLogs = () => {
|
|
12952
|
+
const address = server.httpServer?.address();
|
|
12953
|
+
if (address && typeof address !== 'string')
|
|
12954
|
+
unregisterLogs = registerDeviceLogs(address.port, deviceLogs);
|
|
12955
|
+
};
|
|
12956
|
+
server.httpServer?.once('listening', registerLogs);
|
|
12957
|
+
server.httpServer?.once('close', () => unregisterLogs?.());
|
|
12958
|
+
if (server.httpServer?.listening)
|
|
12959
|
+
registerLogs();
|
|
12960
|
+
let store = devSnapshotStores.get(root);
|
|
12961
|
+
companionArtifactAccessToken ??= store?.token;
|
|
12962
|
+
if (!store || store.token !== companionArtifactAccessToken) {
|
|
12963
|
+
store = { token: companionArtifactAccessToken ?? '', snapshots: new Map() };
|
|
12964
|
+
devSnapshotStores.set(root, store);
|
|
12965
|
+
}
|
|
12966
|
+
const snapshots = store.snapshots;
|
|
12967
|
+
if (server.restart) {
|
|
12968
|
+
const restart = server.restart.bind(server);
|
|
12969
|
+
server.restart = async (forceOptimize) => {
|
|
12970
|
+
store.restarting = true;
|
|
12971
|
+
try {
|
|
12972
|
+
await restart(forceOptimize);
|
|
12973
|
+
}
|
|
12974
|
+
finally {
|
|
12975
|
+
store.restarting = false;
|
|
12976
|
+
}
|
|
12977
|
+
};
|
|
12978
|
+
}
|
|
12979
|
+
let dirty = true;
|
|
12980
|
+
let latestDigest;
|
|
12981
|
+
let changedAt = 0;
|
|
12982
|
+
let sourceRoots = [];
|
|
12983
|
+
const watchPaths = new Set();
|
|
12984
|
+
const invalidate = () => {
|
|
12985
|
+
dirty = true;
|
|
12986
|
+
changedAt = Date.now();
|
|
12987
|
+
// 目录自身的变化发现新增/删除子项;文件监听发现原地编译与资源覆盖。
|
|
12988
|
+
updateWatchPaths(sourceRoots);
|
|
12989
|
+
};
|
|
12990
|
+
const updateWatchPaths = (files) => {
|
|
12991
|
+
sourceRoots = files;
|
|
12992
|
+
const next = new Set(files.flatMap(collectCompanionWatchPaths));
|
|
12993
|
+
for (const file of watchPaths)
|
|
12994
|
+
if (!next.has(file)) {
|
|
12995
|
+
unwatchFile(file, invalidate);
|
|
12996
|
+
watchPaths.delete(file);
|
|
12997
|
+
}
|
|
12998
|
+
for (const file of next)
|
|
12999
|
+
if (!watchPaths.has(file)) {
|
|
13000
|
+
watchFile(file, { interval: 500, persistent: false }, invalidate);
|
|
13001
|
+
watchPaths.add(file);
|
|
13002
|
+
}
|
|
13003
|
+
};
|
|
13004
|
+
updateWatchPaths([path.join(root, 'package.json')]);
|
|
13005
|
+
server.httpServer?.once('close', () => {
|
|
13006
|
+
for (const file of watchPaths)
|
|
13007
|
+
unwatchFile(file, invalidate);
|
|
13008
|
+
if (store.restarting)
|
|
13009
|
+
return;
|
|
13010
|
+
for (const snapshot of snapshots.values())
|
|
13011
|
+
rmSync(snapshot.root, { force: true, recursive: true });
|
|
13012
|
+
if (devSnapshotStores.get(root) === store)
|
|
13013
|
+
devSnapshotStores.delete(root);
|
|
13014
|
+
});
|
|
13015
|
+
const getSnapshot = (requestedDigest) => {
|
|
13016
|
+
if (requestedDigest)
|
|
13017
|
+
return snapshots.get(requestedDigest);
|
|
13018
|
+
if (!dirty && latestDigest)
|
|
13019
|
+
return snapshots.get(latestDigest);
|
|
13020
|
+
if (Date.now() - changedAt < 200)
|
|
13021
|
+
throw new Error('Companion 产物准备中');
|
|
13022
|
+
const snapshotRoot = mkdtempSync(path.join(tmpdir(), 'hb-sdk-companion-snapshot-'));
|
|
13023
|
+
chmodSync(snapshotRoot, 0o700);
|
|
13024
|
+
try {
|
|
13025
|
+
const config = readMiniappCompanionsFromPackageJson(root);
|
|
13026
|
+
const sources = Object.values(config.declarations ?? {}).flatMap(definition => definition && typeof definition === 'object' && definition.targets && typeof definition.targets === 'object'
|
|
13027
|
+
? Object.entries(definition.targets).flatMap(([name, target]) => currentCompanionTargets().includes(name) && target && typeof target.source === 'string'
|
|
13028
|
+
? [path.resolve(config.projectRoot, target.source)] : []) : [])
|
|
13029
|
+
.filter(source => source.startsWith(`${config.projectRoot}${path.sep}`));
|
|
13030
|
+
updateWatchPaths([config.packageJsonPath, ...sources]);
|
|
13031
|
+
const snapshot = prepareDevSnapshot(root, outDir, readMiniappPlatformsFromPackageJson(root), config.declarations, snapshotRoot, config.projectRoot);
|
|
13032
|
+
const digest = createHash('sha256').update(snapshot.manifestBytes).digest('hex');
|
|
13033
|
+
if (snapshots.has(digest))
|
|
13034
|
+
rmSync(snapshotRoot, { force: true, recursive: true });
|
|
13035
|
+
else {
|
|
13036
|
+
const totalBytes = [...snapshots.values(), snapshot].reduce((total, item) => total + [...item.artifacts.values()].reduce((size, artifact) => size + artifact.size, 0), 0);
|
|
13037
|
+
if (snapshots.size >= 32 || totalBytes > 1024 * 1024 * 1024) {
|
|
13038
|
+
throw new Error('Companion 快照存储已达到上限,请结束旧调试会话后重启 dev');
|
|
13039
|
+
}
|
|
13040
|
+
snapshots.set(digest, { ...snapshot, root: snapshotRoot });
|
|
13041
|
+
}
|
|
13042
|
+
latestDigest = digest;
|
|
13043
|
+
dirty = false;
|
|
13044
|
+
return snapshots.get(digest);
|
|
13045
|
+
}
|
|
13046
|
+
catch (error) {
|
|
13047
|
+
rmSync(snapshotRoot, { force: true, recursive: true });
|
|
13048
|
+
throw error;
|
|
13049
|
+
}
|
|
13050
|
+
};
|
|
13051
|
+
server.middlewares.use(async (request, response, next) => {
|
|
13052
|
+
const pathname = parseRequestPathname(request.url);
|
|
13053
|
+
if (pathname === DEVICE_LOG_PATH) {
|
|
13054
|
+
const origin = request.headers.origin;
|
|
13055
|
+
let allowed = false;
|
|
13056
|
+
try {
|
|
13057
|
+
allowed = Boolean(origin && new URL(origin).host === request.headers.host);
|
|
13058
|
+
}
|
|
13059
|
+
catch { /* 拒绝畸形 Origin。 */ }
|
|
13060
|
+
if (!allowed) {
|
|
13061
|
+
response.writeHead(403).end();
|
|
13062
|
+
return;
|
|
13063
|
+
}
|
|
13064
|
+
await deviceLogs.upload(request, response);
|
|
13065
|
+
return;
|
|
13066
|
+
}
|
|
13067
|
+
const companionMatch = pathname?.match(DEV_COMPANION_PATH_PATTERN);
|
|
13068
|
+
if (pathname !== DEV_MANIFEST_PATH && !companionMatch) {
|
|
13069
|
+
next();
|
|
13070
|
+
return;
|
|
13071
|
+
}
|
|
13072
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
13073
|
+
response.statusCode = 405;
|
|
13074
|
+
response.setHeader('Allow', 'GET, HEAD');
|
|
13075
|
+
response.end();
|
|
13076
|
+
return;
|
|
13077
|
+
}
|
|
13078
|
+
if (!companionArtifactAccessToken ||
|
|
13079
|
+
!isNumericLoopbackRequest(request) ||
|
|
13080
|
+
!isNumericLoopbackHost(request.headers.host) ||
|
|
13081
|
+
!hasValidCompanionArtifactToken(request, companionArtifactAccessToken)) {
|
|
13082
|
+
response.statusCode = 403;
|
|
13083
|
+
response.end();
|
|
13084
|
+
return;
|
|
13085
|
+
}
|
|
13086
|
+
try {
|
|
13087
|
+
const requestedDigest = request.headers[DEV_DIGEST_HEADER];
|
|
13088
|
+
if (requestedDigest !== undefined && (typeof requestedDigest !== 'string' || !/^[a-f0-9]{64}$/.test(requestedDigest))) {
|
|
13089
|
+
response.statusCode = 400;
|
|
13090
|
+
response.end();
|
|
13091
|
+
return;
|
|
13092
|
+
}
|
|
13093
|
+
const snapshot = getSnapshot(requestedDigest);
|
|
13094
|
+
if (!snapshot) {
|
|
13095
|
+
response.statusCode = 404;
|
|
13096
|
+
response.end();
|
|
13097
|
+
return;
|
|
13098
|
+
}
|
|
13099
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
13100
|
+
response.setHeader('X-Content-Type-Options', 'nosniff');
|
|
13101
|
+
if (pathname === DEV_MANIFEST_PATH) {
|
|
13102
|
+
response.statusCode = 200;
|
|
13103
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
13104
|
+
response.setHeader('Content-Length', snapshot.manifestBytes.length);
|
|
13105
|
+
response.end(request.method === 'HEAD' ? undefined : snapshot.manifestBytes);
|
|
13106
|
+
return;
|
|
13107
|
+
}
|
|
13108
|
+
const [, alias, target] = companionMatch;
|
|
13109
|
+
const artifact = snapshot.artifacts.get(`companions/${alias}/${target}.zip`);
|
|
13110
|
+
if (!artifact || `/__hb_sdk__/${artifact.path}` !== pathname) {
|
|
13111
|
+
response.statusCode = 404;
|
|
13112
|
+
response.end();
|
|
13113
|
+
return;
|
|
13114
|
+
}
|
|
13115
|
+
response.statusCode = 200;
|
|
13116
|
+
response.setHeader('Content-Type', 'application/zip');
|
|
13117
|
+
response.setHeader('Content-Length', artifact.size);
|
|
13118
|
+
if (request.method === 'HEAD') {
|
|
13119
|
+
response.end();
|
|
13120
|
+
return;
|
|
13121
|
+
}
|
|
13122
|
+
const stream = createReadStream(artifact.sourcePath);
|
|
13123
|
+
const stopStreaming = () => stream.destroy();
|
|
13124
|
+
request.once('aborted', stopStreaming);
|
|
13125
|
+
response.once('close', stopStreaming);
|
|
13126
|
+
stream.once('close', () => {
|
|
13127
|
+
request.off('aborted', stopStreaming);
|
|
13128
|
+
response.off('close', stopStreaming);
|
|
13129
|
+
});
|
|
13130
|
+
stream.on('error', () => {
|
|
13131
|
+
if (response.headersSent) {
|
|
13132
|
+
response.destroy();
|
|
13133
|
+
return;
|
|
13134
|
+
}
|
|
13135
|
+
response.statusCode = 500;
|
|
13136
|
+
response.removeHeader('Content-Length');
|
|
13137
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
13138
|
+
response.end(DEV_ERROR_MESSAGE);
|
|
13139
|
+
});
|
|
13140
|
+
stream.pipe(response);
|
|
13141
|
+
}
|
|
13142
|
+
catch (error) {
|
|
13143
|
+
response.statusCode = 500;
|
|
13144
|
+
const message = error instanceof Error ? error.message : '';
|
|
13145
|
+
response.setHeader('X-Hb-Sdk-Companion-Error', /ENOENT/.test(message) ? 'artifact-missing'
|
|
13146
|
+
: /快照存储/.test(message) ? 'snapshot-limit'
|
|
13147
|
+
: /产物准备中/.test(message) ? 'artifact-updating'
|
|
13148
|
+
: /package.json|JSON|permissions/.test(message) ? 'invalid-manifest' : 'invalid-artifact');
|
|
13149
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
13150
|
+
response.end(DEV_ERROR_MESSAGE);
|
|
13151
|
+
}
|
|
13152
|
+
});
|
|
13153
|
+
},
|
|
11940
13154
|
buildEnd(error) {
|
|
11941
13155
|
if (error) {
|
|
11942
13156
|
priorBuildError = priorBuildError ?? toError(error);
|
|
@@ -11947,8 +13161,14 @@ function miniappManifest(options) {
|
|
|
11947
13161
|
...(command === 'serve' ? { hmrWebSocketUrl } : {}),
|
|
11948
13162
|
skipPlatformCsp,
|
|
11949
13163
|
});
|
|
11950
|
-
return command === 'serve'
|
|
11951
|
-
|
|
13164
|
+
return command === 'serve'
|
|
13165
|
+
? injectMiniDevConsoleBootstrap(enforced.replace('<head>', `<head>${deviceLogBootstrap(deviceLogs.uploadToken)}`))
|
|
13166
|
+
: enforced;
|
|
13167
|
+
},
|
|
13168
|
+
async closeBundle() {
|
|
13169
|
+
// Vite dev 的关闭/配置重载同样会触发此钩子,不执行发布打包。
|
|
13170
|
+
if (command === 'serve')
|
|
13171
|
+
return;
|
|
11952
13172
|
// Rollup 在 buildStart/transform 失败后仍会调用 closeBundle。
|
|
11953
13173
|
// 若此时再抛「缺 index.html」等二次错误,会掩盖真实失败原因。
|
|
11954
13174
|
if (priorBuildError) {
|
|
@@ -11956,9 +13176,15 @@ function miniappManifest(options) {
|
|
|
11956
13176
|
}
|
|
11957
13177
|
const version = validateMiniappPackageVersionForBuild(readMiniappVersionFromPackageJson(root));
|
|
11958
13178
|
const sdkVersion = resolveSdkVersion();
|
|
13179
|
+
const platforms = readMiniappPlatformsFromPackageJson(root);
|
|
11959
13180
|
const parsedPermissions = readMiniappPermissionsFromPackageJson(root, sdkVersion);
|
|
13181
|
+
const companionConfig = readMiniappCompanionsFromPackageJson(root);
|
|
13182
|
+
const companionDeclarations = includeCompanions ? companionConfig.declarations : undefined;
|
|
11960
13183
|
if (!parsedPermissions.declared)
|
|
11961
13184
|
this.warn(getMissingPermissionsWarning());
|
|
13185
|
+
if (companionDeclarations && !parsedPermissions.permissions?.companion) {
|
|
13186
|
+
throw new Error('配置 companions 时必须声明 package.json#heybox.permissions.companion.enabled=true');
|
|
13187
|
+
}
|
|
11962
13188
|
const outputRoot = path.resolve(root, outDir);
|
|
11963
13189
|
const htmlFiles = collectHtmlFiles(outputRoot);
|
|
11964
13190
|
const unexpectedHtmlFiles = htmlFiles.filter((file) => path.relative(outputRoot, file) !== 'index.html');
|
|
@@ -11973,16 +13199,104 @@ function miniappManifest(options) {
|
|
|
11973
13199
|
}
|
|
11974
13200
|
writeFileSync(indexHtmlPath, enforceMiniappHtmlPolicy(readFileSync(indexHtmlPath, 'utf8'), { skipPlatformCsp }));
|
|
11975
13201
|
const manifestPath = path.join(outputRoot, 'manifest.json');
|
|
13202
|
+
const companions = prepareMiniappCompanions(companionDeclarations, { root: companionConfig.projectRoot, outputRoot, platforms });
|
|
11976
13203
|
mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
11977
13204
|
writeFileSync(manifestPath, renderMiniappManifest({
|
|
11978
13205
|
version,
|
|
11979
13206
|
sdkVersion,
|
|
11980
13207
|
platforms,
|
|
11981
13208
|
...(parsedPermissions.permissions === undefined ? {} : { permissions: parsedPermissions.permissions }),
|
|
13209
|
+
...(companions === undefined ? {} : { companions }),
|
|
11982
13210
|
}));
|
|
11983
13211
|
},
|
|
11984
13212
|
};
|
|
11985
13213
|
}
|
|
13214
|
+
function prepareDevSnapshot(root, outDir, platforms, declarations, snapshotRoot, projectRoot = root) {
|
|
13215
|
+
const version = validateMiniappPackageVersionForBuild(readMiniappVersionFromPackageJson(root));
|
|
13216
|
+
const sdkVersion = resolveSdkVersion();
|
|
13217
|
+
const permissions = readMiniappPermissionsFromPackageJson(root, sdkVersion).permissions;
|
|
13218
|
+
if (declarations && !permissions?.companion) {
|
|
13219
|
+
throw new Error('配置 companions 时必须声明 package.json#heybox.permissions.companion.enabled=true');
|
|
13220
|
+
}
|
|
13221
|
+
const artifacts = new Map();
|
|
13222
|
+
const companions = prepareMiniappCompanions(declarations, {
|
|
13223
|
+
root: projectRoot,
|
|
13224
|
+
outputRoot: path.resolve(root, outDir),
|
|
13225
|
+
platforms,
|
|
13226
|
+
targets: currentCompanionTargets(),
|
|
13227
|
+
copyArtifacts: false,
|
|
13228
|
+
onPreparedArtifact: ({ archive, path: artifactPath, size }) => {
|
|
13229
|
+
const sourcePath = path.join(snapshotRoot, ...artifactPath.split('/'));
|
|
13230
|
+
mkdirSync(path.dirname(sourcePath), { mode: 0o700, recursive: true });
|
|
13231
|
+
writeFileSync(sourcePath, archive, { flag: 'wx', mode: 0o600 });
|
|
13232
|
+
artifacts.set(artifactPath, { path: artifactPath, size, sourcePath });
|
|
13233
|
+
},
|
|
13234
|
+
});
|
|
13235
|
+
const manifest = {
|
|
13236
|
+
version,
|
|
13237
|
+
sdkVersion,
|
|
13238
|
+
platforms,
|
|
13239
|
+
...(permissions === undefined ? {} : { permissions }),
|
|
13240
|
+
...(companions === undefined ? {} : { companions }),
|
|
13241
|
+
};
|
|
13242
|
+
return { artifacts, manifestBytes: Buffer.from(renderMiniappManifest(manifest)) };
|
|
13243
|
+
}
|
|
13244
|
+
function currentCompanionTargets() {
|
|
13245
|
+
if (platform() === 'win32' && arch() === 'x64')
|
|
13246
|
+
return ['windows-x64'];
|
|
13247
|
+
if (platform() === 'darwin' && arch() === 'arm64')
|
|
13248
|
+
return ['macos-arm64'];
|
|
13249
|
+
return [];
|
|
13250
|
+
}
|
|
13251
|
+
/** 不跟随链接扫描;合法 .app 链接的最终目标仍在此树内,由普通路径负责监听。 */
|
|
13252
|
+
function collectCompanionWatchPaths(root) {
|
|
13253
|
+
const paths = [root];
|
|
13254
|
+
try {
|
|
13255
|
+
if (lstatSync(root).isDirectory()) {
|
|
13256
|
+
for (const name of readdirSync(root))
|
|
13257
|
+
paths.push(...collectCompanionWatchPaths(path.join(root, name)));
|
|
13258
|
+
}
|
|
13259
|
+
}
|
|
13260
|
+
catch {
|
|
13261
|
+
// 缺失目录也保留监听,允许第一次编译或原子替换后自动恢复。
|
|
13262
|
+
}
|
|
13263
|
+
return paths;
|
|
13264
|
+
}
|
|
13265
|
+
function parseRequestPathname(value) {
|
|
13266
|
+
if (!value)
|
|
13267
|
+
return undefined;
|
|
13268
|
+
try {
|
|
13269
|
+
const url = new URL(value, 'http://127.0.0.1');
|
|
13270
|
+
return url.search || url.hash ? undefined : url.pathname;
|
|
13271
|
+
}
|
|
13272
|
+
catch {
|
|
13273
|
+
return undefined;
|
|
13274
|
+
}
|
|
13275
|
+
}
|
|
13276
|
+
function isNumericLoopbackRequest(request) {
|
|
13277
|
+
const remoteAddress = request.socket.remoteAddress;
|
|
13278
|
+
return remoteAddress === '127.0.0.1' || remoteAddress === '::1' || remoteAddress === '::ffff:127.0.0.1';
|
|
13279
|
+
}
|
|
13280
|
+
function isNumericLoopbackHost(host) {
|
|
13281
|
+
if (!host)
|
|
13282
|
+
return false;
|
|
13283
|
+
const match = host.match(/^(?:127\.0\.0\.1|\[::1\]):(\d{1,5})$/);
|
|
13284
|
+
if (!match)
|
|
13285
|
+
return false;
|
|
13286
|
+
const port = Number(match[1]);
|
|
13287
|
+
return port >= 1 && port <= 65_535;
|
|
13288
|
+
}
|
|
13289
|
+
function parseCompanionArtifactAccessToken(value) {
|
|
13290
|
+
return value && /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;
|
|
13291
|
+
}
|
|
13292
|
+
function hasValidCompanionArtifactToken(request, expected) {
|
|
13293
|
+
const value = request.headers[DEV_TOKEN_HEADER];
|
|
13294
|
+
const candidate = Buffer.alloc(43);
|
|
13295
|
+
if (typeof value === 'string')
|
|
13296
|
+
Buffer.from(value).copy(candidate, 0, 0, candidate.length);
|
|
13297
|
+
const matches = timingSafeEqual(candidate, Buffer.from(expected));
|
|
13298
|
+
return matches && typeof value === 'string' && /^[A-Za-z0-9_-]{43}$/.test(value);
|
|
13299
|
+
}
|
|
11986
13300
|
function assertMiniappViteBase(base) {
|
|
11987
13301
|
if (base !== undefined && base !== '' && base !== './') {
|
|
11988
13302
|
throw new Error(`工坊小程序 Vite base 仅支持空字符串或 "./",当前值:${base}`);
|
|
@@ -12025,4 +13339,4 @@ function resolveHmrWebSocketUrl(resolved) {
|
|
|
12025
13339
|
return `${protocol}://${host}${port ? `:${port}` : ''}`;
|
|
12026
13340
|
}
|
|
12027
13341
|
|
|
12028
|
-
export { MINIAPP_PLATFORM_VALUES, miniappManifest };
|
|
13342
|
+
export { HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV, MINIAPP_PLATFORM_VALUES, miniappManifest };
|