@heybox/hb-sdk 0.8.2-alpha.1 → 0.8.2-alpha.5

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 (76) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +5 -1
  3. package/dist/cli-chunks/{build-DFwk1Xza.cjs → build-D2T3ky_9.cjs} +5 -5
  4. package/dist/cli-chunks/{context-DPwjjbIf.cjs → context-DEgmdpSL.cjs} +326 -104
  5. package/dist/cli-chunks/{create-Cbk708wm.cjs → create-DKqQhrG5.cjs} +5 -1
  6. package/dist/cli-chunks/{dev-B1dCAh3V.cjs → dev-BEXM7cmq.cjs} +17 -9
  7. package/dist/cli-chunks/{doctor-KX6cHAbM.cjs → doctor-B7UixGrd.cjs} +1 -1
  8. package/dist/cli-chunks/{index-B5GNdZS6.cjs → index-BtwfuoWI.cjs} +15 -17
  9. package/dist/cli-chunks/{index-D1CfvRqX.cjs → index-C69OtRug.cjs} +2 -2
  10. package/dist/cli-chunks/{index.esm-BvuovqBQ.cjs → index.esm-C86KlVdK.cjs} +11 -10
  11. package/dist/cli-chunks/{login-CqdIa3XD.cjs → login-Bp_HMx-i.cjs} +2 -2
  12. package/dist/cli-chunks/{project-vite-Bw3UJTnC.cjs → project-vite-DZxipd13.cjs} +1 -1
  13. package/dist/cli-chunks/{remote-t0V-fuAW.cjs → remote-B_sVsbQP.cjs} +80 -60
  14. package/dist/cli-chunks/runtime-gate-GR9jZd37.cjs +58 -0
  15. package/dist/cli-chunks/{runtime-gate-BWb8QIcX.cjs → runtime-permission-env-COWketgE.cjs} +4533 -40
  16. package/dist/cli-chunks/{session-DFY2dygN.cjs → session-6-AApcTZ.cjs} +1 -1
  17. package/dist/cli-chunks/{skill-D3xHcK8w.cjs → skill-CPDLk7t7.cjs} +2 -2
  18. package/dist/cli-chunks/{version-DkVq4CXN.cjs → version-hHrSPRwW.cjs} +1 -1
  19. package/dist/cli.cjs +1 -1
  20. package/dist/devtools/browser-dev-host/assets/browser-dev-host-CpwQQhA1.js +101 -0
  21. package/dist/devtools/browser-dev-host/assets/{desktop-app-launch-8J62MOi_.js → desktop-app-launch-BdWBOIvY.js} +1 -1
  22. package/dist/devtools/browser-dev-host/assets/{index-CrsPiYXy.js → index-BQSEynx6.js} +3 -3
  23. package/dist/devtools/browser-dev-host/index.html +2 -2
  24. package/dist/index.cjs.js +946 -42
  25. package/dist/index.esm.js +946 -42
  26. package/dist/miniapp-publish.cjs.js +9410 -122
  27. package/dist/miniapp-publish.esm.js +9406 -123
  28. package/dist/protocol.cjs.js +805 -0
  29. package/dist/protocol.esm.js +805 -0
  30. package/dist/templates/vanilla-vite-js/assets/heybox-placeholder.png +0 -0
  31. package/dist/templates/vanilla-vite-js/package.json.ejs +3 -0
  32. package/dist/vite.cjs.js +843 -507
  33. package/dist/vite.esm.js +844 -508
  34. package/package.json +8 -7
  35. package/skill/SKILL.md +5 -1
  36. package/skill/references/api-root.md +194 -9
  37. package/skill/references/cli.md +3 -3
  38. package/skill/references/recipes.md +43 -4
  39. package/skill/skill.json +5 -5
  40. package/types/core/errors.d.ts +33 -9
  41. package/types/core/handshake-state.d.ts +32 -2
  42. package/types/core/singleton.d.ts +265 -6
  43. package/types/miniapp-manifest/basic-info-browser.d.ts +9 -0
  44. package/types/miniapp-manifest/basic-info-image-header.d.ts +6 -0
  45. package/types/miniapp-manifest/basic-info-node.d.ts +17 -0
  46. package/types/miniapp-manifest/basic-info-placeholder.d.ts +1 -0
  47. package/types/miniapp-manifest/basic-info.d.ts +12 -0
  48. package/types/miniapp-manifest/bindings.d.ts +65 -0
  49. package/types/miniapp-manifest/schema.d.ts +143 -20
  50. package/types/miniapp-publish/index.d.ts +433 -29
  51. package/types/modules/auth/index.d.ts +159 -5
  52. package/types/modules/cloud/index.d.ts +514 -40
  53. package/types/modules/companion/index.d.ts +1518 -17
  54. package/types/modules/device/index.d.ts +140 -9
  55. package/types/modules/environment/index.d.ts +172 -4
  56. package/types/modules/files/types.d.ts +1396 -21
  57. package/types/modules/navigation/app-pages/types.d.ts +81 -24
  58. package/types/modules/navigation/index.d.ts +230 -7
  59. package/types/modules/network/index.d.ts +297 -26
  60. package/types/modules/share/copy-link.d.ts +35 -5
  61. package/types/modules/share/index.d.ts +210 -0
  62. package/types/modules/share/screenshot.d.ts +23 -5
  63. package/types/modules/share/show-share-menu.d.ts +18 -5
  64. package/types/modules/share/types.d.ts +148 -23
  65. package/types/modules/storage/index.d.ts +139 -9
  66. package/types/modules/ui/index.d.ts +249 -18
  67. package/types/modules/user/get-info.d.ts +29 -8
  68. package/types/modules/user/index.d.ts +157 -0
  69. package/types/modules/user/revoke-authorization.d.ts +26 -2
  70. package/types/modules/user/steam-game-list.d.ts +96 -15
  71. package/types/modules/viewport/index.d.ts +158 -22
  72. package/types/protocol/types.d.ts +19 -1
  73. package/types/vite/index.d.ts +159 -1
  74. package/dist/cli-chunks/runtime-permission-env-BgYjqHIE.cjs +0 -3942
  75. package/dist/devtools/browser-dev-host/assets/browser-dev-host-DqPIIvha.js +0 -101
  76. /package/types/{vite → miniapp-manifest}/runtime-gate.d.ts +0 -0
package/dist/vite.esm.js CHANGED
@@ -1,397 +1,76 @@
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';
1
+ import { createHash, randomUUID, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { constants as constants$1, readFileSync, existsSync, realpathSync, lstatSync, readdirSync, readlinkSync, mkdirSync, writeFileSync, renameSync, rmSync, unwatchFile, createReadStream, watchFile, mkdtempSync, chmodSync } from 'node:fs';
3
+ import { readFile, mkdir, realpath, writeFile, stat, open } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import sharp from 'sharp';
6
+ import { tmpdir, platform, arch } from 'node:os';
5
7
  import { inflateRawSync } from 'node:zlib';
6
8
  import { AsyncLocalStorage } from 'node:async_hooks';
7
9
 
8
- /** 构建时替换为当前发布包的实际版本。 */
9
- const HB_SDK_VERSION = typeof undefined === 'string'
10
- ? undefined
11
- : '0.8.2-alpha.1';
10
+ var re = {exports: {}};
12
11
 
13
- /**
14
- * iframe 与调试台页面的开发期 console 捕获转发。
15
- *
16
- * 双条件门控(iframe 转发):
17
- * 1. 构建期:`hb-sdk dev`(vite serve)注入 `false`;`hb-sdk build` /
18
- * `vite build`(生产)永不注入或替换为 `false`,本模块被死代码消除。
19
- * 2. 运行期:仅在 `window.name` 为 `__hb_sdk_mini_dev__` 的调试 iframe 中激活,
20
- * 用户设备上的正式运行环境没有该标记。
21
- *
22
- * 无侵入 patch `console.log/debug/info/warn/error`:原样放行(DevTools 不受影响)+
23
- * 通过 `window.parent.postMessage` 转发给调试台(CSP 的 connect-src 不约束 postMessage,
24
- * 网络上报由调试台页面代理)。发送全程静默:失败不得回打 console,避免自触发死循环。
25
- *
26
- * 激活时机:由 SDK 默认单例初始化时调用(业务 `import '@heybox/hb-sdk'` 即生效);
27
- * iframe 标记由调试台 `iframe.name` 与 vite dev 注入的无依赖 bootstrap 共同设置。
28
- * 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
29
- * `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
30
- */
31
- const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
32
- /** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
33
- const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
12
+ var constants;
13
+ var hasRequiredConstants;
34
14
 
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
- });
15
+ function requireConstants () {
16
+ if (hasRequiredConstants) return constants;
17
+ hasRequiredConstants = 1;
18
+
19
+ // Note: this is the semver.org version of the spec that it implements
20
+ // Not necessarily the package version of this code.
21
+ const SEMVER_SPEC_VERSION = '2.0.0';
22
+
23
+ const MAX_LENGTH = 256;
24
+ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
25
+ /* istanbul ignore next */ 9007199254740991;
26
+
27
+ // Max safe segment length for coercion.
28
+ const MAX_SAFE_COMPONENT_LENGTH = 16;
29
+
30
+ // Max safe length for a build identifier. The max length minus 6 characters for
31
+ // the shortest version with a build 0.0.0+BUILD.
32
+ const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6;
33
+
34
+ const RELEASE_TYPES = [
35
+ 'major',
36
+ 'premajor',
37
+ 'minor',
38
+ 'preminor',
39
+ 'patch',
40
+ 'prepatch',
41
+ 'prerelease',
42
+ ];
43
+
44
+ constants = {
45
+ MAX_LENGTH,
46
+ MAX_SAFE_COMPONENT_LENGTH,
47
+ MAX_SAFE_BUILD_LENGTH,
48
+ MAX_SAFE_INTEGER,
49
+ RELEASE_TYPES,
50
+ SEMVER_SPEC_VERSION,
51
+ FLAG_INCLUDE_PRERELEASE: 0b001,
52
+ FLAG_LOOSE: 0b010,
53
+ };
54
+ return constants;
101
55
  }
102
56
 
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
-
331
- var re = {exports: {}};
332
-
333
- var constants;
334
- var hasRequiredConstants;
335
-
336
- function requireConstants () {
337
- if (hasRequiredConstants) return constants;
338
- hasRequiredConstants = 1;
339
-
340
- // Note: this is the semver.org version of the spec that it implements
341
- // Not necessarily the package version of this code.
342
- const SEMVER_SPEC_VERSION = '2.0.0';
343
-
344
- const MAX_LENGTH = 256;
345
- const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
346
- /* istanbul ignore next */ 9007199254740991;
347
-
348
- // Max safe segment length for coercion.
349
- const MAX_SAFE_COMPONENT_LENGTH = 16;
350
-
351
- // Max safe length for a build identifier. The max length minus 6 characters for
352
- // the shortest version with a build 0.0.0+BUILD.
353
- const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6;
354
-
355
- const RELEASE_TYPES = [
356
- 'major',
357
- 'premajor',
358
- 'minor',
359
- 'preminor',
360
- 'patch',
361
- 'prepatch',
362
- 'prerelease',
363
- ];
364
-
365
- constants = {
366
- MAX_LENGTH,
367
- MAX_SAFE_COMPONENT_LENGTH,
368
- MAX_SAFE_BUILD_LENGTH,
369
- MAX_SAFE_INTEGER,
370
- RELEASE_TYPES,
371
- SEMVER_SPEC_VERSION,
372
- FLAG_INCLUDE_PRERELEASE: 0b001,
373
- FLAG_LOOSE: 0b010,
374
- };
375
- return constants;
376
- }
377
-
378
- var debug_1;
379
- var hasRequiredDebug;
380
-
381
- function requireDebug () {
382
- if (hasRequiredDebug) return debug_1;
383
- hasRequiredDebug = 1;
384
-
385
- const debug = (
386
- typeof process === 'object' &&
387
- process.env &&
388
- process.env.NODE_DEBUG &&
389
- /\bsemver\b/i.test(process.env.NODE_DEBUG)
390
- ) ? (...args) => console.error('SEMVER', ...args)
391
- : () => {};
392
-
393
- debug_1 = debug;
394
- return debug_1;
57
+ var debug_1;
58
+ var hasRequiredDebug;
59
+
60
+ function requireDebug () {
61
+ if (hasRequiredDebug) return debug_1;
62
+ hasRequiredDebug = 1;
63
+
64
+ const debug = (
65
+ typeof process === 'object' &&
66
+ process.env &&
67
+ process.env.NODE_DEBUG &&
68
+ /\bsemver\b/i.test(process.env.NODE_DEBUG)
69
+ ) ? (...args) => console.error('SEMVER', ...args)
70
+ : () => {};
71
+
72
+ debug_1 = debug;
73
+ return debug_1;
395
74
  }
396
75
 
397
76
  var hasRequiredRe;
@@ -3009,111 +2688,635 @@ function requireSubset () {
3009
2688
  subset_1 = subset;
3010
2689
  return subset_1;
3011
2690
  }
3012
-
3013
- var semver;
3014
- var hasRequiredSemver;
3015
-
3016
- function requireSemver () {
3017
- if (hasRequiredSemver) return semver;
3018
- hasRequiredSemver = 1;
3019
-
3020
- // just pre-load all the stuff that index.js lazily exports
3021
- const internalRe = requireRe();
3022
- const constants = requireConstants();
3023
- const SemVer = requireSemver$1();
3024
- const identifiers = requireIdentifiers();
3025
- const parse = requireParse();
3026
- const valid = requireValid$1();
3027
- const clean = requireClean();
3028
- const inc = requireInc();
3029
- const diff = requireDiff();
3030
- const major = requireMajor();
3031
- const minor = requireMinor();
3032
- const patch = requirePatch();
3033
- const prerelease = requirePrerelease();
3034
- const compare = requireCompare();
3035
- const rcompare = requireRcompare();
3036
- const compareLoose = requireCompareLoose();
3037
- const compareBuild = requireCompareBuild();
3038
- const sort = requireSort();
3039
- const rsort = requireRsort();
3040
- const gt = requireGt();
3041
- const lt = requireLt();
3042
- const eq = requireEq();
3043
- const neq = requireNeq();
3044
- const gte = requireGte();
3045
- const lte = requireLte();
3046
- const cmp = requireCmp();
3047
- const coerce = requireCoerce();
3048
- const truncate = requireTruncate();
3049
- const Comparator = requireComparator();
3050
- const Range = requireRange();
3051
- const satisfies = requireSatisfies();
3052
- const toComparators = requireToComparators();
3053
- const maxSatisfying = requireMaxSatisfying();
3054
- const minSatisfying = requireMinSatisfying();
3055
- const minVersion = requireMinVersion();
3056
- const validRange = requireValid();
3057
- const outside = requireOutside();
3058
- const gtr = requireGtr();
3059
- const ltr = requireLtr();
3060
- const intersects = requireIntersects();
3061
- const simplifyRange = requireSimplify();
3062
- const subset = requireSubset();
3063
- semver = {
3064
- parse,
3065
- valid,
3066
- clean,
3067
- inc,
3068
- diff,
3069
- major,
3070
- minor,
3071
- patch,
3072
- prerelease,
3073
- compare,
3074
- rcompare,
3075
- compareLoose,
3076
- compareBuild,
3077
- sort,
3078
- rsort,
3079
- gt,
3080
- lt,
3081
- eq,
3082
- neq,
3083
- gte,
3084
- lte,
3085
- cmp,
3086
- coerce,
3087
- truncate,
3088
- Comparator,
3089
- Range,
3090
- satisfies,
3091
- toComparators,
3092
- maxSatisfying,
3093
- minSatisfying,
3094
- minVersion,
3095
- validRange,
3096
- outside,
3097
- gtr,
3098
- ltr,
3099
- intersects,
3100
- simplifyRange,
3101
- subset,
3102
- SemVer,
3103
- re: internalRe.re,
3104
- src: internalRe.src,
3105
- tokens: internalRe.t,
3106
- SEMVER_SPEC_VERSION: constants.SEMVER_SPEC_VERSION,
3107
- RELEASE_TYPES: constants.RELEASE_TYPES,
3108
- compareIdentifiers: identifiers.compareIdentifiers,
3109
- rcompareIdentifiers: identifiers.rcompareIdentifiers,
3110
- };
3111
- return semver;
2691
+
2692
+ var semver;
2693
+ var hasRequiredSemver;
2694
+
2695
+ function requireSemver () {
2696
+ if (hasRequiredSemver) return semver;
2697
+ hasRequiredSemver = 1;
2698
+
2699
+ // just pre-load all the stuff that index.js lazily exports
2700
+ const internalRe = requireRe();
2701
+ const constants = requireConstants();
2702
+ const SemVer = requireSemver$1();
2703
+ const identifiers = requireIdentifiers();
2704
+ const parse = requireParse();
2705
+ const valid = requireValid$1();
2706
+ const clean = requireClean();
2707
+ const inc = requireInc();
2708
+ const diff = requireDiff();
2709
+ const major = requireMajor();
2710
+ const minor = requireMinor();
2711
+ const patch = requirePatch();
2712
+ const prerelease = requirePrerelease();
2713
+ const compare = requireCompare();
2714
+ const rcompare = requireRcompare();
2715
+ const compareLoose = requireCompareLoose();
2716
+ const compareBuild = requireCompareBuild();
2717
+ const sort = requireSort();
2718
+ const rsort = requireRsort();
2719
+ const gt = requireGt();
2720
+ const lt = requireLt();
2721
+ const eq = requireEq();
2722
+ const neq = requireNeq();
2723
+ const gte = requireGte();
2724
+ const lte = requireLte();
2725
+ const cmp = requireCmp();
2726
+ const coerce = requireCoerce();
2727
+ const truncate = requireTruncate();
2728
+ const Comparator = requireComparator();
2729
+ const Range = requireRange();
2730
+ const satisfies = requireSatisfies();
2731
+ const toComparators = requireToComparators();
2732
+ const maxSatisfying = requireMaxSatisfying();
2733
+ const minSatisfying = requireMinSatisfying();
2734
+ const minVersion = requireMinVersion();
2735
+ const validRange = requireValid();
2736
+ const outside = requireOutside();
2737
+ const gtr = requireGtr();
2738
+ const ltr = requireLtr();
2739
+ const intersects = requireIntersects();
2740
+ const simplifyRange = requireSimplify();
2741
+ const subset = requireSubset();
2742
+ semver = {
2743
+ parse,
2744
+ valid,
2745
+ clean,
2746
+ inc,
2747
+ diff,
2748
+ major,
2749
+ minor,
2750
+ patch,
2751
+ prerelease,
2752
+ compare,
2753
+ rcompare,
2754
+ compareLoose,
2755
+ compareBuild,
2756
+ sort,
2757
+ rsort,
2758
+ gt,
2759
+ lt,
2760
+ eq,
2761
+ neq,
2762
+ gte,
2763
+ lte,
2764
+ cmp,
2765
+ coerce,
2766
+ truncate,
2767
+ Comparator,
2768
+ Range,
2769
+ satisfies,
2770
+ toComparators,
2771
+ maxSatisfying,
2772
+ minSatisfying,
2773
+ minVersion,
2774
+ validRange,
2775
+ outside,
2776
+ gtr,
2777
+ ltr,
2778
+ intersects,
2779
+ simplifyRange,
2780
+ subset,
2781
+ SemVer,
2782
+ re: internalRe.re,
2783
+ src: internalRe.src,
2784
+ tokens: internalRe.t,
2785
+ SEMVER_SPEC_VERSION: constants.SEMVER_SPEC_VERSION,
2786
+ RELEASE_TYPES: constants.RELEASE_TYPES,
2787
+ compareIdentifiers: identifiers.compareIdentifiers,
2788
+ rcompareIdentifiers: identifiers.rcompareIdentifiers,
2789
+ };
2790
+ return semver;
2791
+ }
2792
+
2793
+ var semverExports = requireSemver();
2794
+
2795
+ const BASIC_INFO_MAX_BYTES = 10 * 1024 * 1024;
2796
+ const BASIC_INFO_MAX_PIXELS = 40_000_000;
2797
+ /** 按 NFKC 和 Unicode 空白规范化名称,并校验 1–40 个字符及禁用字符。 */
2798
+ function normalizeBasicInfoName(raw) {
2799
+ if (typeof raw !== 'string')
2800
+ throw new Error('heybox.name 必须是字符串');
2801
+ // 与 Go TrimSpace 一致(JS trim 额外移除 BOM,故使用 Unicode White_Space)。
2802
+ const name = raw.normalize('NFKC').replace(/^\p{White_Space}+|\p{White_Space}+$/gu, '');
2803
+ if (Array.from(name).length < 1 || Array.from(name).length > 40 || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}<>&"']/u.test(name) || !/[\p{L}\p{N}]/u.test(name)) {
2804
+ throw new Error('名称须包含 1–40 个字符及文字或数字,不能包含控制字符或 HTML 特殊字符');
2805
+ }
2806
+ return name;
2807
+ }
2808
+ function validateBasicInfoPath(raw) {
2809
+ if (typeof raw !== 'string' || !raw || /[\\:%?#\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(raw))
2810
+ throw new Error('资料图片必须是项目内的规范相对路径');
2811
+ const forbidden = new Set(['', '.', '..', '.git', 'node_modules', 'dist', '.hb-sdk', '.vite', '.cache', '.tmp', 'tmp']);
2812
+ if (raw.split('/').some((part) => forbidden.has(part)))
2813
+ throw new Error('资料图片路径包含禁止的目录');
2814
+ if (!/\.(png|jpe?g|gif|webp)$/i.test(raw))
2815
+ throw new Error('资料图片仅支持 JPEG、PNG、GIF、WebP');
2816
+ return raw;
2817
+ }
2818
+ /** 解析项目或构建资料;仅早于 0.8.2 且三项全部缺失时返回 undefined。 */
2819
+ function parseMiniappBasicInfo(value, sdkVersion = '0.8.2') {
2820
+ sdkVersion = sdkVersion.trim();
2821
+ if (!semverExports.valid(sdkVersion) || sdkVersion.startsWith('v') || sdkVersion.includes('+'))
2822
+ throw new Error('manifest.sdkVersion 必须是合法 SemVer');
2823
+ const fields = ['name', 'icon', 'coverImages'];
2824
+ const present = fields.filter((key) => Object.prototype.hasOwnProperty.call(value, key));
2825
+ if (!present.length && semverExports.lt(sdkVersion, '0.8.2'))
2826
+ return undefined;
2827
+ if (present.length !== fields.length)
2828
+ throw new Error('请配置 heybox.name、heybox.icon、heybox.coverImages,或运行 remote bind/dev/remote deploy 迁移已发布资料');
2829
+ const name = normalizeBasicInfoName(value.name);
2830
+ const icon = validateBasicInfoPath(value.icon);
2831
+ if (!Array.isArray(value.coverImages) || value.coverImages.length < 1 || value.coverImages.length > 5)
2832
+ throw new Error('coverImages 必须包含 1–5 张图片');
2833
+ const coverImages = value.coverImages.map(validateBasicInfoPath);
2834
+ if (new Set(coverImages).size !== coverImages.length)
2835
+ throw new Error('封面路径不能重复');
2836
+ return { name, icon, coverImages };
2837
+ }
2838
+
2839
+ async function inspectBasicInfoImage(bytes, sourcePath, kind) {
2840
+ const inspected = await inspectDecodedBasicInfoImage(bytes, kind);
2841
+ const extensions = { jpg: ['jpg', 'jpeg'], png: ['png'], gif: ['gif'], webp: ['webp'] };
2842
+ const ext = sourcePath.split('.').pop().toLowerCase();
2843
+ if (!extensions[inspected.extension]?.includes(ext))
2844
+ throw new Error('资料图片格式与扩展名不一致');
2845
+ return inspected;
2846
+ }
2847
+ async function inspectDecodedBasicInfoImage(bytes, kind) {
2848
+ if (!bytes.length || bytes.length > BASIC_INFO_MAX_BYTES)
2849
+ throw new Error('资料图片不得为空或超过 10 MiB');
2850
+ const decoder = sharp(bytes, { limitInputPixels: BASIC_INFO_MAX_PIXELS, failOn: 'warning' });
2851
+ const metadata = await decoder.metadata();
2852
+ const extensions = { jpeg: 'jpg', png: 'png', gif: 'gif', webp: 'webp' };
2853
+ if (!metadata.format || !extensions[metadata.format])
2854
+ throw new Error('资料图片格式不支持');
2855
+ const width = metadata.width ?? 0;
2856
+ const height = metadata.pageHeight ?? metadata.height ?? 0;
2857
+ if (!width || !height || width * height > BASIC_INFO_MAX_PIXELS)
2858
+ throw new Error('资料图片不得超过 4000 万像素');
2859
+ if (kind === 'icon' && (width !== 200 || height !== 200))
2860
+ throw new Error('icon 必须是 200×200');
2861
+ // 强制解码而非只信任图片头;不将解码结果写回图片。
2862
+ await decoder.stats();
2863
+ return { sha256: createHash('sha256').update(bytes).digest('hex'), extension: extensions[metadata.format] };
2864
+ }
2865
+ async function readBasicInfoSource(projectRoot, relative, kind) {
2866
+ validateBasicInfoPath(relative);
2867
+ const root = await realpath(projectRoot);
2868
+ const target = await realpath(path.join(root, relative));
2869
+ const resolvedRelative = path.relative(root, target).split(path.sep).join('/');
2870
+ validateBasicInfoPath(resolvedRelative);
2871
+ if (resolvedRelative.startsWith('../') || path.isAbsolute(resolvedRelative))
2872
+ throw new Error('资料图片不能逃逸项目目录');
2873
+ const info = await stat(target);
2874
+ if (!info.isFile() || info.size > BASIC_INFO_MAX_BYTES)
2875
+ throw new Error('资料图片必须为不超过 10 MiB 的普通文件');
2876
+ const file = await open(target, constants$1.O_RDONLY | constants$1.O_NOFOLLOW);
2877
+ try {
2878
+ const actual = await file.stat();
2879
+ if (!actual.isFile() || actual.size > BASIC_INFO_MAX_BYTES)
2880
+ throw new Error('资料图片文件已变化');
2881
+ const bytes = await file.readFile();
2882
+ return { bytes, ...(await inspectBasicInfoImage(bytes, relative, kind)) };
2883
+ }
2884
+ finally {
2885
+ await file.close();
2886
+ }
2887
+ }
2888
+ async function prepareMiniappBasicInfo(projectRoot, outputRoot, sdkVersion) {
2889
+ const pkg = JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8'));
2890
+ const profile = parseMiniappBasicInfo(pkg.heybox ?? {}, sdkVersion);
2891
+ if (!profile)
2892
+ return undefined;
2893
+ await mkdir(outputRoot, { recursive: true });
2894
+ const output = { ...profile, coverImages: [] };
2895
+ const entries = [
2896
+ { path: profile.icon, kind: 'icon', label: 'icon' },
2897
+ ...profile.coverImages.map((p, i) => ({ path: p, kind: 'cover', label: `cover-${String(i + 1).padStart(2, '0')}` })),
2898
+ ];
2899
+ for (const entry of entries) {
2900
+ const sourceReal = await realpath(path.join(projectRoot, entry.path));
2901
+ const outputReal = await realpath(outputRoot);
2902
+ const fromOutput = path.relative(outputReal, sourceReal);
2903
+ if (!fromOutput.startsWith('..') && !path.isAbsolute(fromOutput))
2904
+ throw new Error('资料源文件不能位于构建输出目录');
2905
+ const image = await readBasicInfoSource(projectRoot, entry.path, entry.kind);
2906
+ const relative = `_hb/basic-info/${entry.label}.${image.sha256.slice(0, 12)}.${image.extension}`;
2907
+ const target = path.join(outputRoot, relative);
2908
+ await mkdir(outputRoot, { recursive: true });
2909
+ await ensureBasicInfoDirectory(outputRoot, '_hb/basic-info');
2910
+ // 全字节比较保证截断哈希冲突和 public 保留路径冲突不会静默覆盖。
2911
+ try {
2912
+ await writeFile(target, image.bytes, { flag: 'wx' });
2913
+ }
2914
+ catch (error) {
2915
+ if (error.code !== 'EEXIST' ||
2916
+ !(await readBasicInfoSource(outputRoot, relative, entry.kind).then((existing) => existing.bytes.equals(image.bytes), () => false)))
2917
+ throw new Error('构建资料保留目录冲突,请移除未知文件');
2918
+ }
2919
+ if (entry.kind === 'icon')
2920
+ output.icon = relative;
2921
+ else
2922
+ output.coverImages.push(relative);
2923
+ }
2924
+ return output;
2925
+ }
2926
+ /** 逐级校验真实目录,避免保留目录或迁移目录中的符号链接将写入带到项目外。 */
2927
+ async function ensureBasicInfoDirectory(rootPath, relative) {
2928
+ const root = await realpath(rootPath);
2929
+ let current = root;
2930
+ for (const part of relative.split('/')) {
2931
+ if (!part || part === '.' || part === '..')
2932
+ throw new Error('资料目录不合法');
2933
+ current = path.join(current, part);
2934
+ await mkdir(current).catch((error) => {
2935
+ if (error.code !== 'EEXIST')
2936
+ throw error;
2937
+ });
2938
+ const resolved = await realpath(current);
2939
+ const within = path.relative(root, resolved);
2940
+ if (within.startsWith('..') || path.isAbsolute(within))
2941
+ throw new Error('资料目录不能逃逸项目');
2942
+ }
2943
+ }
2944
+
2945
+ /** 构建时替换为当前发布包的实际版本。 */
2946
+ const HB_SDK_VERSION = typeof undefined === 'string'
2947
+ ? undefined
2948
+ : '0.8.2-alpha.5';
2949
+
2950
+ /**
2951
+ * iframe 与调试台页面的开发期 console 捕获转发。
2952
+ *
2953
+ * 双条件门控(iframe 转发):
2954
+ * 1. 构建期:`hb-sdk dev`(vite serve)注入 `false`;`hb-sdk build` /
2955
+ * `vite build`(生产)永不注入或替换为 `false`,本模块被死代码消除。
2956
+ * 2. 运行期:仅在 `window.name` 为 `__hb_sdk_mini_dev__` 的调试 iframe 中激活,
2957
+ * 用户设备上的正式运行环境没有该标记。
2958
+ *
2959
+ * 无侵入 patch `console.log/debug/info/warn/error`:原样放行(DevTools 不受影响)+
2960
+ * 通过 `window.parent.postMessage` 转发给调试台(CSP 的 connect-src 不约束 postMessage,
2961
+ * 网络上报由调试台页面代理)。发送全程静默:失败不得回打 console,避免自触发死循环。
2962
+ *
2963
+ * 激活时机:由 SDK 默认单例初始化时调用(业务 `import '@heybox/hb-sdk'` 即生效);
2964
+ * iframe 标记由调试台 `iframe.name` 与 vite dev 注入的无依赖 bootstrap 共同设置。
2965
+ * 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
2966
+ * `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
2967
+ */
2968
+ const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
2969
+ /** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
2970
+ const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
2971
+
2972
+ /**
2973
+ * network.request 诊断日志的纯脱敏工具。
2974
+ *
2975
+ * @remarks
2976
+ * 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
2977
+ * modules observability、devtools Browser Dev Host / proxy 共用,并满足 boundary
2978
+ *(devtools 不得直接 import runtime capability modules)。
2979
+ */
2980
+ /** 敏感字段名:header / query / body key 共用。 */
2981
+ 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;
2982
+ /**
2983
+ * 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
2984
+ */
2985
+ function sanitizeNetworkRequestUrl(url) {
2986
+ if (!url) {
2987
+ return url;
2988
+ }
2989
+ try {
2990
+ const parsed = new URL(url);
2991
+ parsed.username = '';
2992
+ parsed.password = '';
2993
+ redactUrlSearchParams(parsed.searchParams);
2994
+ if (parsed.hash && parsed.hash.length > 1) {
2995
+ const hashBody = parsed.hash.slice(1);
2996
+ if (hashBody.includes('=')) {
2997
+ const hashParams = new URLSearchParams(hashBody);
2998
+ if (redactUrlSearchParams(hashParams)) {
2999
+ parsed.hash = hashParams.toString();
3000
+ }
3001
+ }
3002
+ }
3003
+ return parsed.toString();
3004
+ }
3005
+ catch {
3006
+ return redactSensitivePairsInRawText(url);
3007
+ }
3008
+ }
3009
+ /**
3010
+ * 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
3011
+ */
3012
+ function sanitizeDiagnosticMessage(message) {
3013
+ if (!message) {
3014
+ return message;
3015
+ }
3016
+ return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
3017
+ }
3018
+ function isSensitiveNetworkKey(key) {
3019
+ return SENSITIVE_KEY_PATTERN.test(key.trim());
3020
+ }
3021
+ function redactUrlSearchParams(params) {
3022
+ let changed = false;
3023
+ for (const key of [...params.keys()]) {
3024
+ if (isSensitiveNetworkKey(key)) {
3025
+ params.set(key, '[redacted]');
3026
+ changed = true;
3027
+ }
3028
+ }
3029
+ return changed;
3030
+ }
3031
+ function redactSensitivePairsInRawText(text) {
3032
+ return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
3033
+ if (!isSensitiveNetworkKey(key)) {
3034
+ return full;
3035
+ }
3036
+ return `${prefix}${key}=[redacted]`;
3037
+ });
3038
+ }
3039
+
3040
+ const DEVICE_LOG_PATH = '/__hb_sdk__/device-logs';
3041
+ const registryKey = Symbol.for('@heybox/hb-sdk/device-logs');
3042
+ const registry = globalThis;
3043
+ const stores = (registry[registryKey] ??= new Map());
3044
+ function registerDeviceLogs(port, store) {
3045
+ stores.set(port, store);
3046
+ return () => {
3047
+ if (stores.get(port) === store)
3048
+ stores.delete(port);
3049
+ };
3050
+ }
3051
+ class DeviceLogStore {
3052
+ sandboxRequests = new Map();
3053
+ listSandbox(id, path) {
3054
+ const device = this.devices.get(id);
3055
+ if (!device || Date.now() - device.lastSeen > 15000)
3056
+ return Promise.resolve({ error: '设备已离线' });
3057
+ if (path !== '.' && (!path || path.length > 1024 || /[\\\\\u0000-\u001f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')))
3058
+ return Promise.resolve({ error: '无效目录路径' });
3059
+ if (this.sandboxRequests.size >= 64)
3060
+ return Promise.resolve({ error: '查询繁忙,请稍后重试' });
3061
+ const requestId = randomUUID();
3062
+ return new Promise(resolve => {
3063
+ const timer = setTimeout(() => { this.sandboxRequests.delete(requestId); resolve({ error: '设备查询超时' }); }, 10000);
3064
+ this.sandboxRequests.set(requestId, { id, path, requestId, resolve: value => { clearTimeout(timer); this.sandboxRequests.delete(requestId); resolve(value); } });
3065
+ });
3066
+ }
3067
+ uploadToken = randomBytes(32).toString('hex');
3068
+ epoch = randomUUID();
3069
+ expiresAt = Date.now() + 12 * 60 * 60 * 1000;
3070
+ cursor = 0;
3071
+ logs = [];
3072
+ devices = new Map();
3073
+ query(params) {
3074
+ const after = Math.max(0, Number(params.get('after')) || 0);
3075
+ const limit = Math.min(500, Math.max(1, Number(params.get('limit')) || 100));
3076
+ const selected = this.logs.filter((log) => log.cursor > after &&
3077
+ (!params.get('device') || log.deviceSessionId === params.get('device')) &&
3078
+ (!params.get('level') || log.level === params.get('level')) &&
3079
+ (!params.get('source') || log.source === params.get('source')) &&
3080
+ (!params.get('callId') || log.callId === params.get('callId')) &&
3081
+ (!params.get('id') || String(log.cursor) === params.get('id')) &&
3082
+ (!params.get('since') || log.timestamp >= Number(params.get('since'))) &&
3083
+ (!params.get('query') || log.message.toLowerCase().includes(params.get('query').toLowerCase())));
3084
+ const items = selected.slice(0, limit);
3085
+ return {
3086
+ schemaVersion: 1,
3087
+ epoch: this.epoch,
3088
+ items,
3089
+ nextCursor: selected.length > limit ? items.at(-1).cursor : this.cursor,
3090
+ hasMore: selected.length > limit,
3091
+ gap: after > 0 && after < (this.logs[0]?.cursor ?? 1) - 1,
3092
+ devices: [...this.devices.values()].map(({ id, name, platform, connectedAt, lastSeen }) => ({
3093
+ id,
3094
+ name,
3095
+ platform,
3096
+ connectedAt,
3097
+ lastSeen,
3098
+ online: Date.now() - lastSeen < 15000,
3099
+ })),
3100
+ };
3101
+ }
3102
+ async upload(req, res) {
3103
+ const reply = (status, value) => {
3104
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
3105
+ res.end(JSON.stringify(value));
3106
+ };
3107
+ if (req.method !== 'POST') {
3108
+ reply(405, {});
3109
+ return;
3110
+ }
3111
+ if (Date.now() > this.expiresAt) {
3112
+ reply(410, { code: 'EXPIRED' });
3113
+ return;
3114
+ }
3115
+ let text = '';
3116
+ try {
3117
+ req.setEncoding('utf8');
3118
+ for await (const chunk of req) {
3119
+ text += chunk;
3120
+ if (Buffer.byteLength(text) > 65536) {
3121
+ reply(413, {});
3122
+ return;
3123
+ }
3124
+ }
3125
+ const body = JSON.parse(text);
3126
+ if (body.register === true) {
3127
+ if (req.headers['x-hb-log-token'] !== this.uploadToken) {
3128
+ reply(403, {});
3129
+ return;
3130
+ }
3131
+ const registrationId = typeof body.registrationId === 'string' && /^[a-f0-9]{32}$/.test(body.registrationId) ? body.registrationId : undefined;
3132
+ const existing = registrationId && [...this.devices.values()].find(device => device.registrationId === registrationId);
3133
+ if (existing) {
3134
+ reply(200, { id: existing.id, token: existing.token });
3135
+ return;
3136
+ }
3137
+ if (this.devices.size >= 64) {
3138
+ reply(429, {});
3139
+ return;
3140
+ }
3141
+ const id = randomUUID();
3142
+ const platform = ['iOS', 'Android', 'Windows', 'macOS', 'Browser'].includes(body.platform) ? body.platform : 'Browser';
3143
+ const device = {
3144
+ id,
3145
+ platform,
3146
+ registrationId,
3147
+ connectedAt: Date.now(),
3148
+ name: `${platform} · ${this.devices.size + 1}`,
3149
+ lastSeen: Date.now(),
3150
+ sequence: 0,
3151
+ token: randomBytes(32).toString('hex'),
3152
+ windowStart: Date.now(),
3153
+ count: 0,
3154
+ };
3155
+ this.devices.set(id, device);
3156
+ reply(200, { id, token: device.token });
3157
+ return;
3158
+ }
3159
+ const device = this.devices.get(body.id);
3160
+ if (!device || req.headers['x-hb-log-token'] !== device.token) {
3161
+ reply(403, {});
3162
+ return;
3163
+ }
3164
+ if (!Array.isArray(body.logs) || body.logs.length > 50) {
3165
+ reply(400, {});
3166
+ return;
3167
+ }
3168
+ if (Date.now() - device.windowStart > 1000) {
3169
+ device.windowStart = Date.now();
3170
+ device.count = 0;
3171
+ }
3172
+ if (device.count + body.logs.length > 200) {
3173
+ reply(429, {});
3174
+ return;
3175
+ }
3176
+ device.count += body.logs.length;
3177
+ device.lastSeen = Date.now();
3178
+ if (body.sandboxResult && typeof body.sandboxResult === 'object') {
3179
+ const result = body.sandboxResult;
3180
+ const pending = this.sandboxRequests.get(result.requestId);
3181
+ if (pending?.id === device.id) {
3182
+ 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 })) : [];
3183
+ pending.resolve({ deviceSessionId: device.id, requestId: pending.requestId, path: pending.path, timestamp: Date.now(), entries, error: result.error ? '设备不支持或无权读取此目录' : undefined, truncated: result.truncated === true });
3184
+ }
3185
+ }
3186
+ for (const log of body.logs) {
3187
+ if (!log || typeof log !== 'object')
3188
+ continue;
3189
+ if (!Number.isSafeInteger(log.sequence) ||
3190
+ log.sequence <= device.sequence ||
3191
+ !Number.isFinite(log.timestamp) ||
3192
+ !['log', 'debug', 'info', 'warn', 'error'].includes(log.level) ||
3193
+ typeof log.message !== 'string')
3194
+ continue;
3195
+ device.sequence = log.sequence;
3196
+ this.logs.push({
3197
+ cursor: ++this.cursor,
3198
+ deviceSessionId: device.id,
3199
+ sequence: log.sequence,
3200
+ timestamp: log.timestamp,
3201
+ receivedAt: Date.now(),
3202
+ level: log.level,
3203
+ source: log.source === 'sdk' ? 'sdk' : 'app',
3204
+ message: sanitizeDiagnosticMessage(log.message)
3205
+ .replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)(["'\s:=]+)([^\s,}]+)/gi, '$1$2[redacted]')
3206
+ .slice(0, 8192),
3207
+ truncated: log.truncated === true || log.message.length > 8192,
3208
+ ...(typeof log.method === 'string' ? { method: log.method.slice(0, 100) } : {}),
3209
+ ...(typeof log.callId === 'string' ? { callId: log.callId.slice(0, 100) } : {}),
3210
+ ...(typeof log.errorCode === 'string' && /^[A-Z0-9_]{1,100}$/.test(log.errorCode) ? { errorCode: log.errorCode } : {}),
3211
+ ...(log.outcome === 'success' || log.outcome === 'error' ? { outcome: log.outcome } : {}),
3212
+ ...(Number.isFinite(log.durationMs) ? { durationMs: Math.max(0, log.durationMs) } : {}),
3213
+ });
3214
+ }
3215
+ if (this.logs.length > 5000)
3216
+ this.logs.splice(0, this.logs.length - 5000);
3217
+ 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 });
3218
+ }
3219
+ catch {
3220
+ reply(400, { code: 'INVALID_LOG_BATCH' });
3221
+ }
3222
+ }
3223
+ }
3224
+ /** 同源开发页面使用有界批量回传;传输失败不会递归写入 console。 */
3225
+ function deviceLogBootstrap(token) {
3226
+ return `<script>(function(){if(window.__hb_sdk_device_logs_installed__)return;
3227
+ window.__hb_sdk_device_logs_installed__=true;
3228
+ window.__hb_sdk_mini_dev_console_installed__=true;
3229
+ var endpoint='${DEVICE_LOG_PATH}',credential='${token}',id='',queue=[],seq=0,busy=false,dropped=0;
3230
+ var registrationId=Array.from(crypto.getRandomValues(new Uint8Array(16)),function(n){return n.toString(16).padStart(2,'0')}).join('');
3231
+ var send=window.fetch.bind(window);
3232
+ var sandboxResult;
3233
+ 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();
3234
+ dropped++}var message=args.map(format).join(' ');
3235
+ message=message.replace(/https?:\\/\\/[^\\s]+/g,function(raw){try{var url=new URL(raw);
3236
+ url.username='';
3237
+ url.password='';
3238
+ url.search='';
3239
+ url.hash='';
3240
+ return url.href}catch(e){return '[URL]'}});
3241
+ message=message.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)([\\"'\\s:=]+)([^\\s,}]+)/gi,'$1$2[redacted]');
3242
+ 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');
3243
+ Object.assign(queue[queue.length-1],event)};
3244
+ ['log','debug','info','warn','error'].forEach(function(level){var original=console[level];
3245
+ console[level]=function(){add(level,Array.prototype.slice.call(arguments));
3246
+ return original.apply(this,arguments)}});
3247
+ window.addEventListener('error',function(e){add('error',[e.message])});
3248
+ window.addEventListener('unhandledrejection',function(e){add('error',[e.reason])});
3249
+ async function flush(){if(busy)return;
3250
+ busy=true;
3251
+ try{if(!id){var ua=navigator.userAgent;
3252
+ 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';
3253
+ 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)});
3254
+ if(!r.ok)return;
3255
+ var d=await r.json();
3256
+ id=d.id;
3257
+ credential=d.token}if(dropped){add('warn',['日志缓冲已丢弃 '+dropped+' 条']);
3258
+ dropped=0}var batch=queue.slice(0,50);
3259
+ while(batch.length>1&&new TextEncoder().encode(JSON.stringify(batch)).length>48000)batch.pop();
3260
+ 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)});
3261
+ if(result.ok){var ack=await result.json();sandboxResult=undefined;
3262
+ 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}}}
3263
+ queue=queue.filter(function(l){return l.sequence>ack.sequence})}}catch(e){}finally{busy=false}}setInterval(flush,1000);
3264
+ flush()})();
3265
+ </script>`;
3112
3266
  }
3113
3267
 
3114
- var semverExports = requireSemver();
3115
-
3268
+ /** 单个小程序版本最多声明的关联游戏数量。 */
3116
3269
  const MINIAPP_BIND_APP_IDS_MAX_COUNT = 20;
3270
+ /**
3271
+ * 校验并规范化小程序关联的游戏 App ID。
3272
+ *
3273
+ * @param value - 不可信声明。`undefined` 和 `[]` 均返回空绑定;其他值必须是最多 20 项的
3274
+ * 字符串数组,不接受 `null`、单个字符串、number 数组或类数组对象。
3275
+ * @param options - 可选诊断来源;`sourceLabel` 默认 `package.json#heybox.bindAppIds`,并原样
3276
+ * 作为所有错误消息的前缀。
3277
+ * @returns 新建、按 appid 数值升序排列且经 `Object.freeze` 冻结的 `readonly string[]`。
3278
+ * 输入数组及其顺序不会被修改;空输入也返回新的冻结空数组。
3279
+ * @throws `Error` `${sourceLabel} 必须是字符串数组`:`value` 不是数组。
3280
+ * @throws `Error` `${sourceLabel} 最多包含 20 个游戏 appid`:输入数组超过 20 项;数量检查
3281
+ * 先于逐项格式检查。
3282
+ * @throws `Error` `${sourceLabel} 包含非法游戏 appid:<value>`:任一项不是 string,或不匹配
3283
+ * `/^[1-9]\d*$/`。因此空串、首尾空白、`0`、前导零、正负号、小数、指数和非十进制字符
3284
+ * 都不会被自动修正。
3285
+ * @throws `Error` `${sourceLabel} 包含超出正 int32 范围的游戏 appid:<value>`:十进制值
3286
+ * 大于 `2147483647` 或不能成为安全整数。
3287
+ * @throws `Error` `${sourceLabel} 包含重复游戏 appid:<value>`:规范字符串在同一数组重复。
3288
+ *
3289
+ * @remarks
3290
+ * 合法范围是闭区间 `1..2147483647`,每项在输出中仍保持十进制 string。排序按数值而不是
3291
+ * 字典序执行,所以 `['3513350', '730', '10']` 输出为 `['10', '730', '3513350']`。
3292
+ * 函数先复制后排序并冻结结果,不排序或冻结调用方传入的数组。
3293
+ *
3294
+ * 这是同步、纯离线的格式规范化函数,不访问网络,也不确认 appid 是否真实存在、可绑定或
3295
+ * 属于当前开发者。`miniappManifest()` 用它校验 `package.json#heybox.bindAppIds` 并把非空结果
3296
+ * 写入 Manifest;远端 deploy 预检和提审才校验游戏可用性。缺失和空数组在普通构建中都表示
3297
+ * 无绑定,函数本身不处理 `remote deploy --from-version` 的历史快照继承语义。
3298
+ *
3299
+ * `bindAppIds` 是版本发布元数据,不是 Runtime 权限,也不会因调用
3300
+ * [navigation.openAppPage](/reference/sdk/navigation/openAppPage) 而变化。后者的 `appId` 是一次
3301
+ * 页面导航参数,允许 string 或 number、只要求字符串化后非空,不适用本函数的数组、int32、
3302
+ * 前导零、去重、20 项或排序规则。
3303
+ *
3304
+ * @example
3305
+ * ```ts
3306
+ * import { parseMiniappBindAppIds } from '@heybox/hb-sdk/miniapp-publish'
3307
+ *
3308
+ * const declared = ['3513350', '730', '10']
3309
+ * const appIds = parseMiniappBindAppIds(declared)
3310
+ *
3311
+ * console.log(appIds) // ['10', '730', '3513350']
3312
+ * console.log(declared) // ['3513350', '730', '10'],输入顺序不变
3313
+ * console.log(Object.isFrozen(appIds)) // true
3314
+ * ```
3315
+ *
3316
+ * @see [miniappManifest](/reference/symbols/vite/functions/miniappManifest)
3317
+ * @see [navigation.openAppPage](/reference/sdk/navigation/openAppPage)
3318
+ * @see [CLI 与远端发布](/guide/cli#生产构建)
3319
+ */
3117
3320
  function parseMiniappBindAppIds(value, options = {}) {
3118
3321
  const sourceLabel = options.sourceLabel ?? 'package.json#heybox.bindAppIds';
3119
3322
  if (value === undefined)
@@ -3142,8 +3345,25 @@ function parseMiniappBindAppIds(value, options = {}) {
3142
3345
  return Object.freeze(appIds);
3143
3346
  }
3144
3347
 
3348
+ /**
3349
+ * 所有小程序 iframe bridge envelope 共用的固定 namespace。
3350
+ *
3351
+ * @remarks 该值用于排除无关 `postMessage` 数据,不负责验证消息来源;接收方仍须校验 source 和 nonce,
3352
+ * 并在 origin 可以固定且协议模式要求时校验 origin。
3353
+ */
3145
3354
  const MINI_PROGRAM_MESSAGE_NAMESPACE = 'heybox:miniprogram';
3355
+ /**
3356
+ * 新消息默认发送的当前 bridge wire 版本。
3357
+ *
3358
+ * @remarks 接收 guard 为握手协商继续接受 v1 与 v2 envelope;cancel 与 operation progress 等 v2-only 消息仍必须使用 v2。
3359
+ */
3146
3360
  const MINI_PROGRAM_MESSAGE_VERSION = 2;
3361
+ /**
3362
+ * Runtime 在 iframe URL 中传递 bridge nonce 的 query 参数名。
3363
+ *
3364
+ * @remarks nonce 用于把消息关联到当前 iframe 实例,不是独立的身份认证凭据;接收方还必须校验 source,
3365
+ * 并在 origin 可以固定且协议模式要求时校验 origin。
3366
+ */
3147
3367
  const MINI_PROGRAM_BRIDGE_NONCE_PARAM = 'hb_mini_bridge_nonce';
3148
3368
  /** Companion alias 的跨构建、Runtime 与 Host 规范。 */
3149
3369
  const COMPANION_ALIAS_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
@@ -3359,6 +3579,11 @@ function isRecord$3(value) {
3359
3579
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3360
3580
  }
3361
3581
 
3582
+ /**
3583
+ * `package.json#heybox.platforms` 接受的平台值,顺序同时是 Manifest 的规范化输出顺序。
3584
+ *
3585
+ * @remarks 项目仍应只声明实际完成适配并经过验收的平台;模板默认值不是自动兼容承诺。
3586
+ */
3362
3587
  const MINIAPP_PLATFORM_VALUES = ['android', 'ios', 'ohos', 'windows', 'macos', 'linux'];
3363
3588
  /** 窗口尺寸字段的脏值兜底上限;真正的上限是宿主侧显示器 workArea。 */
3364
3589
  const MINIAPP_WINDOW_SIZE_MAX = 16_384;
@@ -3381,6 +3606,7 @@ function renderMiniappManifest(manifest) {
3381
3606
  const output = {
3382
3607
  version: manifest.version,
3383
3608
  sdkVersion: manifest.sdkVersion,
3609
+ ...(manifest.name === undefined ? {} : { name: manifest.name, icon: manifest.icon, coverImages: manifest.coverImages }),
3384
3610
  platforms,
3385
3611
  ...(manifest.permissions === undefined ? {} : { permissions: manifest.permissions }),
3386
3612
  ...(manifest.companions === undefined ? {} : { companions: manifest.companions }),
@@ -12746,29 +12972,29 @@ function renderRuntimeGateScript() {
12746
12972
  function removePlatformGateNodes(node) {
12747
12973
  if (!('childNodes' in node))
12748
12974
  return;
12749
- node.childNodes = node.childNodes.filter(child => !('tagName' in child) || !hasAttr(child, RUNTIME_GATE_MARKER));
12975
+ node.childNodes = node.childNodes.filter((child) => !('tagName' in child) || !hasAttr(child, RUNTIME_GATE_MARKER));
12750
12976
  for (const child of node.childNodes)
12751
12977
  removePlatformGateNodes(child);
12752
12978
  }
12753
12979
  function readAttr(element, name) {
12754
- return element.attrs.find(attr => attr.name.toLowerCase() === name)?.value;
12980
+ return element.attrs.find((attr) => attr.name.toLowerCase() === name)?.value;
12755
12981
  }
12756
12982
  function hasAttr(element, name) {
12757
12983
  return readAttr(element, name) !== undefined;
12758
12984
  }
12759
12985
  function setAttr(element, name, value) {
12760
- const attr = element.attrs.find(item => item.name.toLowerCase() === name);
12986
+ const attr = element.attrs.find((item) => item.name.toLowerCase() === name);
12761
12987
  if (attr)
12762
12988
  attr.value = value;
12763
12989
  else
12764
12990
  element.attrs.push({ name, value });
12765
12991
  }
12766
12992
  function removeAttr(element, name) {
12767
- element.attrs = element.attrs.filter(attr => attr.name.toLowerCase() !== name);
12993
+ element.attrs = element.attrs.filter((attr) => attr.name.toLowerCase() !== name);
12768
12994
  }
12769
12995
  function findElement$1(node, tagName) {
12770
12996
  let found;
12771
- walk$1(node, element => {
12997
+ walk$1(node, (element) => {
12772
12998
  if (!found && element.tagName === tagName)
12773
12999
  found = element;
12774
13000
  });
@@ -12787,10 +13013,7 @@ function walk$1(node, visit) {
12787
13013
  const FORBIDDEN_RESOURCE_HINTS = new Set(['dns-prefetch', 'preconnect', 'prerender']);
12788
13014
  const RELATIVE_URL_BASE = new URL('https://heybox-relative.invalid/');
12789
13015
  const OFFICIAL_RESOURCE_ROOT_DOMAINS = ['xiaoheihe.cn', 'max-c.com', 'debugmode.cn', 'maxjia.com'];
12790
- const OFFICIAL_RESOURCE_SOURCES = OFFICIAL_RESOURCE_ROOT_DOMAINS.flatMap(rootDomain => [
12791
- `https://${rootDomain}`,
12792
- `https://*.${rootDomain}`,
12793
- ]);
13016
+ const OFFICIAL_RESOURCE_SOURCES = OFFICIAL_RESOURCE_ROOT_DOMAINS.flatMap((rootDomain) => [`https://${rootDomain}`, `https://*.${rootDomain}`]);
12794
13017
  const URL_ATTRIBUTES = {
12795
13018
  audio: ['src'],
12796
13019
  embed: ['src'],
@@ -12827,14 +13050,12 @@ function enforceMiniappHtmlPolicy(html, options = {}) {
12827
13050
  throw new Error('入口 index.html 必须包含 <head>');
12828
13051
  }
12829
13052
  removePlatformCspMarkers(document);
12830
- walk(document, element => validateElement(element));
13053
+ walk(document, (element) => validateElement(element));
12831
13054
  injectMiniappRuntimeGate(document);
12832
13055
  if (options.skipPlatformCsp) {
12833
13056
  return serialize(document);
12834
13057
  }
12835
- const content = options.hmrWebSocketUrl
12836
- ? PRODUCTION_CSP.replace("connect-src 'none'", `connect-src ${options.hmrWebSocketUrl}`)
12837
- : PRODUCTION_CSP;
13058
+ const content = options.hmrWebSocketUrl ? PRODUCTION_CSP.replace("connect-src 'none'", `connect-src ${options.hmrWebSocketUrl}`) : PRODUCTION_CSP;
12838
13059
  head.childNodes.unshift({
12839
13060
  nodeName: 'meta',
12840
13061
  tagName: 'meta',
@@ -12852,7 +13073,7 @@ function enforceMiniappHtmlPolicy(html, options = {}) {
12852
13073
  }
12853
13074
  function validateElement(element) {
12854
13075
  const tagName = element.tagName.toLowerCase();
12855
- const attrs = new Map(element.attrs.map(attr => [attr.name.toLowerCase(), attr.value]));
13076
+ const attrs = new Map(element.attrs.map((attr) => [attr.name.toLowerCase(), attr.value]));
12856
13077
  if (tagName === 'meta' && attrs.get('http-equiv')?.toLowerCase() === 'refresh') {
12857
13078
  throw new Error('index.html 不允许 meta refresh');
12858
13079
  }
@@ -12864,7 +13085,7 @@ function validateElement(element) {
12864
13085
  }
12865
13086
  if (tagName === 'link') {
12866
13087
  const rels = (attrs.get('rel') ?? '').toLowerCase().split(/\s+/).filter(Boolean);
12867
- const forbidden = rels.find(rel => FORBIDDEN_RESOURCE_HINTS.has(rel));
13088
+ const forbidden = rels.find((rel) => FORBIDDEN_RESOURCE_HINTS.has(rel));
12868
13089
  if (forbidden) {
12869
13090
  throw new Error(`index.html 不允许 rel="${forbidden}"`);
12870
13091
  }
@@ -12881,7 +13102,10 @@ function validateElement(element) {
12881
13102
  for (const attribute of URL_ATTRIBUTES[tagName] ?? []) {
12882
13103
  const value = attrs.get(attribute);
12883
13104
  if (attribute === 'srcset') {
12884
- for (const candidate of (value ?? '').split(',').map(item => item.trim().split(/\s+/)[0]).filter(Boolean)) {
13105
+ for (const candidate of (value ?? '')
13106
+ .split(',')
13107
+ .map((item) => item.trim().split(/\s+/)[0])
13108
+ .filter(Boolean)) {
12885
13109
  validateResourceUrl(candidate, `<${tagName} ${attribute}>`, true, supportsOfficialResourceUrl(tagName, attribute));
12886
13110
  }
12887
13111
  }
@@ -12893,10 +13117,10 @@ function validateElement(element) {
12893
13117
  function removePlatformCspMarkers(node) {
12894
13118
  if (!('childNodes' in node))
12895
13119
  return;
12896
- node.childNodes = node.childNodes.filter(child => {
13120
+ node.childNodes = node.childNodes.filter((child) => {
12897
13121
  if (!('tagName' in child))
12898
13122
  return true;
12899
- return !child.attrs.some(attr => attr.name.toLowerCase() === 'data-heybox-platform-csp');
13123
+ return !child.attrs.some((attr) => attr.name.toLowerCase() === 'data-heybox-platform-csp');
12900
13124
  });
12901
13125
  for (const child of node.childNodes)
12902
13126
  removePlatformCspMarkers(child);
@@ -12932,7 +13156,7 @@ function isOfficialResourceUrl(value) {
12932
13156
  const hostname = url.hostname.toLowerCase();
12933
13157
  if (hostname.endsWith('.'))
12934
13158
  return false;
12935
- return OFFICIAL_RESOURCE_ROOT_DOMAINS.some(rootDomain => hostname === rootDomain || hostname.endsWith(`.${rootDomain}`));
13159
+ return OFFICIAL_RESOURCE_ROOT_DOMAINS.some((rootDomain) => hostname === rootDomain || hostname.endsWith(`.${rootDomain}`));
12936
13160
  }
12937
13161
  catch {
12938
13162
  return false;
@@ -12957,7 +13181,7 @@ function isRelativeUrl(value, normalized) {
12957
13181
  }
12958
13182
  function findElement(node, tagName) {
12959
13183
  let found;
12960
- walk(node, element => {
13184
+ walk(node, (element) => {
12961
13185
  if (!found && element.tagName === tagName)
12962
13186
  found = element;
12963
13187
  });
@@ -13022,7 +13246,19 @@ const DEV_ERROR_MESSAGE = 'Companion 开发资源暂时不可用';
13022
13246
  /** 与 Companion manifest 一致的单响应上限,避免本地调试入口被大体积声明撑爆。 */
13023
13247
  const DEV_MINI_PROGRAM_MANIFEST_MAX_BYTES = 1024 * 1024;
13024
13248
  const DEV_COMPANION_PATH_PATTERN = /^\/__hb_sdk__\/companions\/([a-z][a-z0-9-]{0,31})\/(windows-x64|macos-arm64)\.zip$/;
13249
+ /**
13250
+ * `hb-sdk dev` 为本地 Companion 产物访问注入的进程环境变量名。
13251
+ *
13252
+ * @remarks
13253
+ * 该变量由 CLI 管理,不是项目配置项。应用代码不应读取、设置、记录或转发它的值。
13254
+ */
13025
13255
  const HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV = 'HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN';
13256
+ /**
13257
+ * `hb-sdk dev` 为本地小程序 Manifest 访问注入的进程环境变量名。
13258
+ *
13259
+ * @remarks
13260
+ * 该变量由 CLI 管理,不是项目配置项。应用代码不应读取、设置、记录或转发它的值。
13261
+ */
13026
13262
  const HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN_ENV = 'HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN';
13027
13263
  const devSnapshotStoreKey = Symbol.for('@heybox/hb-sdk/dev-companion-snapshots');
13028
13264
  const devSnapshotRegistry = globalThis;
@@ -13036,6 +13272,104 @@ if (!devSnapshotRegistry[devSnapshotStoreKey]) {
13036
13272
  });
13037
13273
  }
13038
13274
  const devSnapshotStores = devSnapshotRegistry[devSnapshotStoreKey];
13275
+ /**
13276
+ * 创建工坊小程序使用的 Vite 插件。
13277
+ *
13278
+ * @returns 可直接加入 Vite `plugins` 数组的
13279
+ * [MiniappManifestPlugin](/reference/symbols/vite/interfaces/MiniappManifestPlugin)。插件固定名为
13280
+ * `heybox-miniapp-manifest`,并提供 `config`、`configResolved`、`configureServer`、`buildEnd`、
13281
+ * `transformIndexHtml`、`closeBundle` hooks;这些 hooks 由 Vite 调用,应用代码不应手动调用。
13282
+ * @throws `Error` `config` 阶段发现 Vite `base` 不是 `''` 或 `'./'`,或无法解析当前 SDK 版本时抛出。
13283
+ * @throws `Error` `configResolved` 阶段发现已解析插件列表中 `miniappManifest()` 数量不是一个时抛出。
13284
+ * @throws `Error` HTML 缺少 `<head>`,包含 `meta refresh`、`<base>`、禁止的 resource hint,或资源 URL
13285
+ * 不符合小程序 HTML 策略时,`transformIndexHtml` / 生产产物复检抛出。
13286
+ * @throws `Error` 生产构建无法读取或解析最近的 `package.json`,`version`、`heybox.platforms`、
13287
+ * `heybox.permissions`、`heybox.window`、`heybox.bindAppIds` 或 `heybox.companions` 声明非法时抛出。
13288
+ * @throws `Error` 生产 `build.outDir` 缺少根 `index.html`、递归发现额外 HTML,或 Companion 权限、
13289
+ * 目标平台、来源目录、归档结构、入口二进制不符合约束时抛出;更早的 Rollup/Vite 错误会原样优先重抛。
13290
+ *
13291
+ * @remarks
13292
+ * 该函数必须从 `@heybox/hb-sdk/vite` 导入,在 `vite.config.ts` 中无参调用且只注册一次;它不是
13293
+ * iframe 业务代码 API,也没有 options 对象。小程序声明统一来自从 Vite `root` 向上找到的最近一个
13294
+ * `package.json`,插件只读取和规范化,不会改写源文件:
13295
+ *
13296
+ * - `version`:发布版本;trim 后必须是完整 SemVer,可含 prerelease,不允许 build metadata 或模板值
13297
+ * `0.0.0`;
13298
+ * - `heybox.platforms`:项目实际承诺适配的非空、无重复平台 allowlist;输出按平台词表规范排序;
13299
+ * - `heybox.permissions`:严格权限对象;未知权限/字段或错误类型会失败,`enabled: false` 项不写入
13300
+ * Manifest。旧版本缺失声明时可能警告,新硬切版本会失败;
13301
+ * - `heybox.window`:可选的新 PC 开窗初值、最小尺寸和 `resizable` 声明;省略时不写该字段;
13302
+ * - `heybox.bindAppIds`:可选的最多 20 个正 int32 字符串 App ID;校验、去重并按数值排序,空数组
13303
+ * 在本地产物中等同不写;
13304
+ * - `heybox.companions`:可选的受管桌面程序声明;`source` 相对上述 `package.json` 所在目录解析,
13305
+ * 必须同时声明启用的 `companion` 权限和匹配的 Windows/macOS 目标平台。
13306
+ *
13307
+ * 产物中的 `sdkVersion` 来自当前实际安装的 `@heybox/hb-sdk`,不是项目可配置字段。Vite options、页面
13308
+ * 代码和普通环境变量不能覆盖上述 package 声明;插件也不会联网确认 App ID、平台批准或远端版本状态。
13309
+ *
13310
+ * Hook 行为如下:
13311
+ *
13312
+ * - `config`:省略 `base` 时补为 `'./'`,显式值只接受 `''` / `'./'`;注入 SDK 构建版本,且仅
13313
+ * `vite serve` 注入开发日志编译常量;
13314
+ * - `configResolved`:确认插件唯一性,记录真实 `root`、`build.outDir`、`serve|build` 命令和精确 HMR
13315
+ * WebSocket 地址;直接 `vite build` 使用该实际 `outDir`,`hb-sdk build` 则由 CLI 固定为 `dist/`;
13316
+ * - `configureServer`:为 `hb-sdk dev` 注册设备日志、本地小程序 Manifest 与 Companion Manifest/ZIP
13317
+ * 中间件;敏感路由只接受数值 loopback socket/Host、`GET|HEAD` 和 CLI 注入的独立短期令牌,快照在
13318
+ * 配置重载时按 digest 保留,并在服务真正关闭时清理;
13319
+ * - `transformIndexHtml`:校验 HTML URL/标签策略,注入 Runtime Gate;默认注入平台 CSP,serve 时
13320
+ * `connect-src` 只开放解析出的 HMR WebSocket,并额外注入调试日志 bootstrap;
13321
+ * - `buildEnd`:记住更早的 Rollup/Vite 错误,避免 `closeBundle` 的二次产物错误遮盖根因;
13322
+ * - `closeBundle`:serve 时直接完成;build 时复检最终 HTML,要求整个输出树只有根 `index.html`,再于
13323
+ * `build.outDir` 写入 `manifest.json`,并把 Companion 目录打包成
13324
+ * `companions/<alias>/<target>.zip`,记录 size、SHA-256、固定入口与 args。
13325
+ *
13326
+ * HTML 策略会隔离业务 script/modulepreload,直到父 Runtime Gate 以 namespace、版本、nonce、source
13327
+ * 以及 referrer 可确定时的 origin 完成探测;失败时显示只能在小黑盒 App 内打开的安全页。策略拒绝自动跳转、`<base>`、危险
13328
+ * resource hints 和不受支持的外部 URL,并按标签只允许相对 URL、明确允许的 `data:` / `blob:` 或
13329
+ * 小黑盒官方 HTTPS 资源。开发者自行提供的 CSP 会保留;插件管理的 CSP marker 可重复转换而不会叠加。
13330
+ *
13331
+ * 平台 CSP 默认始终保留,直接 `vite build` / `hb-sdk build` 不会因项目声明
13332
+ * `heybox.permissions.network.useOfficialDomain` 而跳过。只有官方 `hb-sdk dev` 或
13333
+ * `hb-sdk remote deploy` 将当前版本的 `network` 声明与远端批准结果求交并确认最终启用后,才会
13334
+ * 建立 verified 上下文供插件读取内部
13335
+ * `HB_SDK_RUNTIME_PERMISSION_CONTEXT` + `HB_SDK_RUNTIME_USE_OFFICIAL_DOMAIN` 组合跳过平台 CSP;单独设置
13336
+ * 任一值无效。0.8 兼容期对完全缺失权限声明的项目仍按迁移规则处理;`useOfficialDomain` 不参与
13337
+ * CSP 跳过决定。Runtime Gate、HTML 校验和 Manifest 生成始终保留。
13338
+ *
13339
+ * `HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN`、`HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN`、
13340
+ * `HB_SDK_SKIP_COMPANIONS` 以及上述 CSP 上下文均是 CLI/发布链路内部开关,不是项目配置项。它们在
13341
+ * 插件创建时捕获;开发服务配置重载只会从同一服务的内存快照恢复 CLI 令牌。业务代码和 Vite 配置
13342
+ * 不应手工设置、读取、记录或转发令牌值。正式声明必须继续写在 `package.json#heybox`,内部环境变量
13343
+ * 不能替代声明校验。
13344
+ *
13345
+ * `manifest.json` 是 release pipeline 元数据,不是部署后供页面 fetch 的 CDN 资源。发布链路使用
13346
+ * [isValidVersion](/reference/symbols/miniapp-publish/functions/isValidVersion) 复核版本、
13347
+ * [resolveMiniappManifestPlatforms](/reference/symbols/miniapp-publish/functions/resolveMiniappManifestPlatforms)
13348
+ * 解析平台硬切规则、
13349
+ * [parseMiniappBindAppIds](/reference/symbols/miniapp-publish/functions/parseMiniappBindAppIds) 复核游戏绑定,
13350
+ * 并通过 [shouldUploadDistFile](/reference/symbols/miniapp-publish/functions/shouldUploadDistFile) 将根
13351
+ * `manifest.json` 排除出普通 CDN 文件。Web/Companion 上传预算由
13352
+ * [validateUploadTotalSize](/reference/symbols/miniapp-publish/functions/validateUploadTotalSize) 等发布 helper
13353
+ * 独立校验,不能用插件成功代替 deploy 预检。
13354
+ *
13355
+ * @example
13356
+ * ```ts
13357
+ * import vue from '@vitejs/plugin-vue'
13358
+ * import { miniappManifest } from '@heybox/hb-sdk/vite'
13359
+ * import { defineConfig } from 'vite'
13360
+ *
13361
+ * export default defineConfig({
13362
+ * // base 省略时 miniappManifest() 会补为 './'
13363
+ * plugins: [vue(), miniappManifest()],
13364
+ * })
13365
+ * ```
13366
+ *
13367
+ * @see [MiniappManifestPlugin](/reference/symbols/vite/interfaces/MiniappManifestPlugin) 返回插件及各 hook 类型。
13368
+ * @see [安装](/guide/installation) Vite 接入、相对资源路径和构建命令。
13369
+ * @see [权限声明](/guide/permissions) `heybox.permissions` 严格 schema 与迁移规则。
13370
+ * @see [窗口尺寸与可缩放性](/guide/window-size) `heybox.window` 字段与 Host 消费语义。
13371
+ * @see [受管桌面程序](/guide/companion) Companion 声明、归档和本地联调边界。
13372
+ */
13039
13373
  function miniappManifest() {
13040
13374
  const includeCompanions = process.env.HB_SDK_SKIP_COMPANIONS !== '1';
13041
13375
  const deviceLogs = new DeviceLogStore();
@@ -13354,10 +13688,12 @@ function miniappManifest() {
13354
13688
  throw new Error('构建产物必须包含入口 dist/index.html');
13355
13689
  }
13356
13690
  writeFileSync(indexHtmlPath, enforceMiniappHtmlPolicy(readFileSync(indexHtmlPath, 'utf8'), { skipPlatformCsp }));
13691
+ const basicInfo = await prepareMiniappBasicInfo(companionConfig.projectRoot, outputRoot, sdkVersion);
13357
13692
  const manifestPath = path.join(outputRoot, 'manifest.json');
13358
13693
  const companions = prepareMiniappCompanions(companionDeclarations, { root: companionConfig.projectRoot, outputRoot, platforms });
13359
13694
  mkdirSync(path.dirname(manifestPath), { recursive: true });
13360
13695
  writeFileSync(manifestPath, renderMiniappManifest({
13696
+ ...basicInfo,
13361
13697
  version,
13362
13698
  sdkVersion,
13363
13699
  platforms,