@heybox/hb-sdk 0.6.8-alpha.2 → 0.6.9-alpha.0

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 (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +2 -0
  3. package/dist/cli-chunks/{build-CglqyB9Z.cjs → build-9hbhrRbU.cjs} +29 -25
  4. package/dist/cli-chunks/{context-CP7W_8aR.cjs → context-7rmakzS_.cjs} +122 -23
  5. package/dist/cli-chunks/{create-DldsTIVt.cjs → create-BkX4rtRP.cjs} +1 -1
  6. package/dist/cli-chunks/{dev-BG0icySa.cjs → dev-DiZGT4FQ.cjs} +142 -108
  7. package/dist/cli-chunks/{doctor-Cd6Xq0m-.cjs → doctor-BcpqXVRh.cjs} +1 -1
  8. package/dist/cli-chunks/{index-jXyfZKy2.cjs → index-6HfcwZ_r.cjs} +3 -3
  9. package/dist/cli-chunks/{index-paN77avR.cjs → index-DofTxdoX.cjs} +14 -14
  10. package/dist/cli-chunks/{login-DlD9n_AF.cjs → login-DUiejZcD.cjs} +2 -2
  11. package/dist/cli-chunks/{project-vite-vHezGsg7.cjs → project-vite-CfD8Bp1K.cjs} +1 -1
  12. package/dist/cli-chunks/{remote-O5_nRdZN.cjs → remote-A4-PKgOn.cjs} +462 -190
  13. package/dist/cli-chunks/{runtime-gate-DfMJQGH9.cjs → runtime-gate-BWlU-R4h.cjs} +3 -3
  14. package/dist/cli-chunks/{index-v4-6fbXX.cjs → runtime-permission-env-DKrhgVM3.cjs} +276 -49
  15. package/dist/cli-chunks/{session-CiQplems.cjs → session-D5u3XzHN.cjs} +1 -1
  16. package/dist/cli.cjs +1 -1
  17. package/dist/devtools/mock-host/main.js +609 -6
  18. package/dist/index.cjs.js +403 -7
  19. package/dist/index.esm.js +403 -7
  20. package/dist/miniapp-publish.cjs.js +205 -0
  21. package/dist/miniapp-publish.esm.js +201 -1
  22. package/dist/vite.cjs.js +17 -2
  23. package/dist/vite.esm.js +17 -2
  24. package/package.json +2 -2
  25. package/skill/SKILL.md +7 -6
  26. package/skill/references/api-root.md +7 -2
  27. package/skill/references/cli.md +12 -0
  28. package/skill/references/examples.md +17 -2
  29. package/skill/references/safety-boundaries.md +2 -0
  30. package/skill/scripts/sync-references.mjs +17 -2
  31. package/skill/skill.json +4 -4
  32. package/types/core/network-sanitize.d.ts +31 -0
  33. package/types/miniapp-publish/index.d.ts +77 -0
  34. package/types/modules/network/index.d.ts +2 -0
  35. package/types/modules/network/observability.d.ts +39 -0
  36. package/types/modules/network/request-shape.d.ts +44 -0
  37. package/types/vite/html-policy.d.ts +1 -0
  38. package/types/vite/runtime-permission-env.d.ts +9 -0
package/dist/index.cjs.js CHANGED
@@ -2,6 +2,277 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ /**
6
+ * network.request 请求体形态校验与宿主失败诊断文案。
7
+ *
8
+ * @remarks
9
+ * App Host 的 heybox 客户端代发 transport 仅声明 form/json,不支持 multipart。
10
+ * 本地 Mock 若直接走 fetch 会“能通”,上线后却变成难以理解的 500 network_error。
11
+ * 因此在 SDK 侧统一提前拒绝 multipart,并在宿主伪装 HTTP 失败时给出可操作提示。
12
+ */
13
+ /** multipart 不被 App Host 支持时的稳定说明(开发者可见)。 */
14
+ const NETWORK_REQUEST_MULTIPART_UNSUPPORTED_MESSAGE = 'network.request 不支持 multipart/form-data。App Host 客户端代发仅支持 application/x-www-form-urlencoded(form)与 application/json;请将 data 编码为 form 字符串(例如 new URLSearchParams({...}).toString())并设置 Content-Type: application/x-www-form-urlencoded。文件上传请使用专用上传能力,勿手写 multipart boundary。';
15
+ /**
16
+ * 读取请求头中的 Content-Type(大小写不敏感)。
17
+ */
18
+ function readNetworkContentType(headers) {
19
+ if (!headers) {
20
+ return undefined;
21
+ }
22
+ for (const [key, value] of Object.entries(headers)) {
23
+ if (key.trim().toLowerCase() === 'content-type' && typeof value === 'string') {
24
+ return value;
25
+ }
26
+ }
27
+ return undefined;
28
+ }
29
+ /**
30
+ * 判断 Content-Type 是否声明为 multipart/form-data。
31
+ */
32
+ function isMultipartContentType(contentType) {
33
+ return typeof contentType === 'string' && contentType.toLowerCase().includes('multipart/form-data');
34
+ }
35
+ /**
36
+ * 判断字符串 body 是否像手写 multipart 实体。
37
+ */
38
+ function looksLikeMultipartBody(data) {
39
+ if (typeof data !== 'string') {
40
+ return false;
41
+ }
42
+ const trimmed = data.trimStart();
43
+ return trimmed.startsWith('--') && /content-disposition\s*:\s*form-data/i.test(data);
44
+ }
45
+ /**
46
+ * 判断公开请求配置是否使用了 Host 不支持的 multipart 形态。
47
+ */
48
+ function isUnsupportedMultipartNetworkRequest(config) {
49
+ return isMultipartContentType(readNetworkContentType(config.headers)) || looksLikeMultipartBody(config.data);
50
+ }
51
+ function isPlainObject$2(value) {
52
+ return Object.prototype.toString.call(value) === '[object Object]';
53
+ }
54
+ /**
55
+ * 从宿主返回的“完成态”响应中提取更清晰的失败原因。
56
+ *
57
+ * @remarks
58
+ * Host 可能把 transport 不支持等内部错误包装成 HTTP 500 + `{ status: 'network_error', msg }`。
59
+ * 此时 `status=500` 并不代表目标站点返回了 500。
60
+ */
61
+ function describeHostNetworkFailure(response) {
62
+ const data = response.data;
63
+ const hostStatus = isPlainObject$2(data) && typeof data.status === 'string' ? data.status : undefined;
64
+ const hostMsg = isPlainObject$2(data) && typeof data.msg === 'string'
65
+ ? data.msg
66
+ : isPlainObject$2(data) && typeof data.message === 'string'
67
+ ? data.message
68
+ : undefined;
69
+ const isHostNetworkError = hostStatus === 'network_error';
70
+ const mentionsUnsupportedShape = typeof hostMsg === 'string' &&
71
+ (hostMsg.includes('unsupported-request-shape') || hostMsg.includes('unsupported_request_shape'));
72
+ const requestLooksMultipart = isUnsupportedMultipartNetworkRequest(response.config || {});
73
+ if (!isHostNetworkError && !mentionsUnsupportedShape && !(requestLooksMultipart && response.status >= 500)) {
74
+ return undefined;
75
+ }
76
+ const lines = [
77
+ '原因: 这是宿主网络层失败,不是目标 URL 的业务 HTTP 响应。',
78
+ ];
79
+ if (hostMsg) {
80
+ lines.push(`宿主信息: ${hostMsg}`);
81
+ }
82
+ if (requestLooksMultipart || mentionsUnsupportedShape) {
83
+ lines.push(NETWORK_REQUEST_MULTIPART_UNSUPPORTED_MESSAGE);
84
+ }
85
+ else if (isHostNetworkError) {
86
+ lines.push('请检查请求 URL、headers、body 编码与 Host 网络权限;本地 Mock 与 App Host 能力并不完全一致。');
87
+ }
88
+ return lines.join(' ');
89
+ }
90
+
91
+ /**
92
+ * network.request 诊断日志的纯脱敏工具。
93
+ *
94
+ * @remarks
95
+ * 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
96
+ * modules observability、devtools Mock Host / proxy 共用,并满足 boundary
97
+ *(devtools 不得直接 import runtime capability modules)。
98
+ */
99
+ const DATA_PREVIEW_MAX_CHARS = 500;
100
+ const SENSITIVE_HEADER_NAMES = new Set([
101
+ 'authorization',
102
+ 'cookie',
103
+ 'proxy-authorization',
104
+ 'set-cookie',
105
+ 'token',
106
+ 'x-pkey',
107
+ 'api-key',
108
+ 'apikey',
109
+ 'x-api-key',
110
+ 'x-auth-token',
111
+ 'x-access-token',
112
+ 'session',
113
+ 'session-id',
114
+ 'jwt',
115
+ 'refresh-token',
116
+ 'x-csrf-token',
117
+ ]);
118
+ /** 敏感字段名:header / query / body key 共用。 */
119
+ 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;
120
+ /**
121
+ * 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
122
+ */
123
+ function sanitizeNetworkRequestUrl(url) {
124
+ if (!url) {
125
+ return url;
126
+ }
127
+ try {
128
+ const parsed = new URL(url);
129
+ parsed.username = '';
130
+ parsed.password = '';
131
+ redactUrlSearchParams(parsed.searchParams);
132
+ if (parsed.hash && parsed.hash.length > 1) {
133
+ const hashBody = parsed.hash.slice(1);
134
+ if (hashBody.includes('=')) {
135
+ const hashParams = new URLSearchParams(hashBody);
136
+ if (redactUrlSearchParams(hashParams)) {
137
+ parsed.hash = hashParams.toString();
138
+ }
139
+ }
140
+ }
141
+ return parsed.toString();
142
+ }
143
+ catch {
144
+ return redactSensitivePairsInRawText(url);
145
+ }
146
+ }
147
+ /**
148
+ * 脱敏请求/响应头;敏感名(含 heybox 前缀)替换为 `[redacted]`。
149
+ */
150
+ function sanitizeNetworkHeaders(headers) {
151
+ if (!headers) {
152
+ return {};
153
+ }
154
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
155
+ key,
156
+ isSensitiveNetworkHeaderName(key) ? '[redacted]' : value,
157
+ ]));
158
+ }
159
+ /**
160
+ * 预览请求/响应 data:递归脱敏敏感字段,截断长文本与大数组。
161
+ */
162
+ function previewNetworkData(data) {
163
+ if (data === undefined) {
164
+ return undefined;
165
+ }
166
+ if (typeof data === 'string') {
167
+ return previewNetworkStringData(data);
168
+ }
169
+ if (typeof data === 'number' || typeof data === 'boolean' || data === null) {
170
+ return data;
171
+ }
172
+ try {
173
+ const sanitized = sanitizeNetworkDataValue(data);
174
+ const serialized = JSON.stringify(sanitized);
175
+ if (serialized === undefined) {
176
+ return '[unserializable]';
177
+ }
178
+ if (serialized.length <= DATA_PREVIEW_MAX_CHARS) {
179
+ return sanitized;
180
+ }
181
+ return truncateText(serialized, DATA_PREVIEW_MAX_CHARS);
182
+ }
183
+ catch {
184
+ return '[unserializable]';
185
+ }
186
+ }
187
+ /**
188
+ * 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
189
+ */
190
+ function sanitizeDiagnosticMessage(message) {
191
+ if (!message) {
192
+ return message;
193
+ }
194
+ return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
195
+ }
196
+ function isSensitiveNetworkHeaderName(headerName) {
197
+ const normalized = headerName.trim().toLowerCase();
198
+ return (SENSITIVE_HEADER_NAMES.has(normalized) ||
199
+ isSensitiveNetworkKey(normalized) ||
200
+ normalized.startsWith('x-heybox-') ||
201
+ normalized.startsWith('x-xhh-'));
202
+ }
203
+ function isSensitiveNetworkKey(key) {
204
+ return SENSITIVE_KEY_PATTERN.test(key.trim());
205
+ }
206
+ function previewNetworkStringData(data) {
207
+ const trimmed = data.trim();
208
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
209
+ try {
210
+ return previewNetworkData(JSON.parse(trimmed));
211
+ }
212
+ catch {
213
+ // fall through
214
+ }
215
+ }
216
+ if (trimmed.includes('=') && !trimmed.includes('\n') && trimmed.length <= DATA_PREVIEW_MAX_CHARS * 2) {
217
+ try {
218
+ const params = new URLSearchParams(trimmed);
219
+ if ([...params.keys()].length > 0) {
220
+ const redacted = Object.fromEntries([...params.entries()].map(([key, value]) => [
221
+ key,
222
+ isSensitiveNetworkKey(key) ? '[redacted]' : value,
223
+ ]));
224
+ return previewNetworkData(redacted);
225
+ }
226
+ }
227
+ catch {
228
+ // fall through
229
+ }
230
+ }
231
+ return truncateText(redactSensitivePairsInRawText(data), DATA_PREVIEW_MAX_CHARS);
232
+ }
233
+ function sanitizeNetworkDataValue(value) {
234
+ if (Array.isArray(value)) {
235
+ return value.slice(0, 20).map(item => sanitizeNetworkDataValue(item));
236
+ }
237
+ if (!isPlainObject$1(value)) {
238
+ if (typeof value === 'string') {
239
+ return truncateText(value, DATA_PREVIEW_MAX_CHARS);
240
+ }
241
+ return value;
242
+ }
243
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
244
+ key,
245
+ isSensitiveNetworkKey(key) ? '[redacted]' : sanitizeNetworkDataValue(item),
246
+ ]));
247
+ }
248
+ function redactUrlSearchParams(params) {
249
+ let changed = false;
250
+ for (const key of [...params.keys()]) {
251
+ if (isSensitiveNetworkKey(key)) {
252
+ params.set(key, '[redacted]');
253
+ changed = true;
254
+ }
255
+ }
256
+ return changed;
257
+ }
258
+ function redactSensitivePairsInRawText(text) {
259
+ return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
260
+ if (!isSensitiveNetworkKey(key)) {
261
+ return full;
262
+ }
263
+ return `${prefix}${key}=[redacted]`;
264
+ });
265
+ }
266
+ function truncateText(value, maxChars) {
267
+ if (value.length <= maxChars) {
268
+ return value;
269
+ }
270
+ return `${value.slice(0, maxChars)}…(truncated, total=${value.length})`;
271
+ }
272
+ function isPlainObject$1(value) {
273
+ return Object.prototype.toString.call(value) === '[object Object]';
274
+ }
275
+
5
276
  /**
6
277
  * SDK 对外抛出的标准 bridge / runtime 错误类型。
7
278
  *
@@ -64,7 +335,14 @@ class HbMiniProgramNetworkError extends Error {
64
335
  * @param response 已完成请求的标准化网络响应。
65
336
  */
66
337
  constructor(response) {
67
- super(`network.request failed with status ${response.status}`);
338
+ const method = (response.config.method || 'GET').toUpperCase();
339
+ // 诊断文案使用脱敏 URL,避免 query 中的 token 等经 error.message 泄漏。
340
+ const url = sanitizeNetworkRequestUrl(response.config.url || '');
341
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
342
+ const baseMessage = `network.request failed with status ${response.status}${statusText}: ${method} ${url}`;
343
+ // 宿主可能把 transport 不支持等内部错误伪装成 HTTP 500;补充可操作提示。
344
+ const hostHint = describeHostNetworkFailure(response);
345
+ super(hostHint ? `${baseMessage}\n${hostHint}` : baseMessage);
68
346
  this.name = 'HbMiniProgramNetworkError';
69
347
  this.status = response.status;
70
348
  this.data = response.data;
@@ -325,7 +603,7 @@ function createMessageId() {
325
603
  /** 构建时替换为当前发布包的实际版本。 */
326
604
  const HB_SDK_VERSION = typeof undefined === 'string'
327
605
  ? undefined
328
- : '0.6.8-alpha.2';
606
+ : '0.6.9-alpha.0';
329
607
 
330
608
  /**
331
609
  * 判断未知数据是否符合小程序 bridge 消息信封。
@@ -892,6 +1170,77 @@ function createStorageModule(requester) {
892
1170
  };
893
1171
  }
894
1172
 
1173
+ const LOG_PREFIX = '[hb-sdk][network.request]';
1174
+ /**
1175
+ * 记录 `network.request` 失败,保证异常路径在开发者控制台可观测。
1176
+ *
1177
+ * @remarks
1178
+ * - 仅用于诊断,不改变错误抛出语义。
1179
+ * - 会脱敏 token/cookie 等敏感头与字段,并截断 body 预览。
1180
+ * - detail 构造与 logger 调用均包在 try 内,日志失败不得影响业务错误抛出。
1181
+ */
1182
+ function logNetworkRequestFailure(config, error, extras) {
1183
+ try {
1184
+ const consoleRef = typeof console === 'undefined' ? undefined : console;
1185
+ const logger = consoleRef?.warn || consoleRef?.error || consoleRef?.log;
1186
+ if (!logger) {
1187
+ return;
1188
+ }
1189
+ const detail = createNetworkRequestFailureLog(config, error, extras);
1190
+ logger.call(consoleRef, LOG_PREFIX, formatNetworkRequestFailureMessage(detail), detail);
1191
+ }
1192
+ catch {
1193
+ // 日志失败不得影响业务错误抛出。
1194
+ }
1195
+ }
1196
+ function createNetworkRequestFailureLog(config, error, extras) {
1197
+ const method = (config.method || 'GET').toUpperCase();
1198
+ const detail = {
1199
+ kind: extras.kind,
1200
+ method,
1201
+ url: sanitizeNetworkRequestUrl(config.url),
1202
+ };
1203
+ if (typeof extras.status === 'number') {
1204
+ detail.status = extras.status;
1205
+ }
1206
+ else if (error instanceof HbMiniProgramNetworkError) {
1207
+ detail.status = error.status;
1208
+ }
1209
+ if (typeof extras.statusText === 'string' && extras.statusText) {
1210
+ detail.statusText = extras.statusText;
1211
+ }
1212
+ // NetworkError 已有 method/url/status 结构化字段;不再回写 message,避免 raw URL 二次泄漏。
1213
+ if (!(error instanceof HbMiniProgramNetworkError)) {
1214
+ if (error instanceof HbMiniProgramSDKError) {
1215
+ detail.code = error.code;
1216
+ detail.message = sanitizeDiagnosticMessage(error.message);
1217
+ }
1218
+ else if (error instanceof Error) {
1219
+ detail.message = sanitizeDiagnosticMessage(error.message);
1220
+ }
1221
+ }
1222
+ if (config.timeout !== undefined) {
1223
+ detail.timeout = config.timeout;
1224
+ }
1225
+ if (config.withCredentials !== undefined) {
1226
+ detail.withCredentials = config.withCredentials;
1227
+ }
1228
+ const headers = sanitizeNetworkHeaders(extras.headers || config.headers);
1229
+ if (Object.keys(headers).length > 0) {
1230
+ detail.headers = headers;
1231
+ }
1232
+ const dataPreview = previewNetworkData(extras.data !== undefined ? extras.data : config.data);
1233
+ if (dataPreview !== undefined) {
1234
+ detail.dataPreview = dataPreview;
1235
+ }
1236
+ return detail;
1237
+ }
1238
+ function formatNetworkRequestFailureMessage(detail) {
1239
+ const statusPart = detail.status === undefined ? '' : ` status=${detail.status}`;
1240
+ const codePart = detail.code ? ` code=${detail.code}` : '';
1241
+ return `${detail.kind} ${detail.method} ${detail.url}${statusPart}${codePart}`;
1242
+ }
1243
+
895
1244
  const DEFAULT_VALIDATE_STATUS = status => status >= 200 && status < 300;
896
1245
  function isPlainObject(value) {
897
1246
  return Object.prototype.toString.call(value) === '[object Object]';
@@ -935,6 +1284,9 @@ function normalizeHeaders(headers) {
935
1284
  return Object.fromEntries(Object.entries(headers).flatMap(([key, value]) => (typeof value === 'string' ? [[key, value]] : [])));
936
1285
  }
937
1286
  function toNetworkResponse(payload, config) {
1287
+ if (payload == null || typeof payload !== 'object') {
1288
+ throw createSDKError('INVALID_NETWORK_RESPONSE', 'network.request 返回了无效的响应', payload);
1289
+ }
938
1290
  if (typeof payload.status !== 'number' || Number.isNaN(payload.status)) {
939
1291
  throw createSDKError('INVALID_NETWORK_RESPONSE', 'network.request 返回了无效的 status', payload);
940
1292
  }
@@ -969,12 +1321,56 @@ function toNetworkResponse(payload, config) {
969
1321
  */
970
1322
  async function request(requester, config) {
971
1323
  const validateStatus = config.validateStatus || DEFAULT_VALIDATE_STATUS;
972
- const responsePayload = await requester.request(NETWORK_REQUEST_METHOD, toRequestPayload(config));
973
- const response = toNetworkResponse(responsePayload, config);
974
- if (!validateStatus(response.status)) {
975
- throw new HbMiniProgramNetworkError(response);
1324
+ try {
1325
+ assertSupportedPublicNetworkRequest(config);
1326
+ const responsePayload = await requester.request(NETWORK_REQUEST_METHOD, toRequestPayload(config));
1327
+ const response = toNetworkResponse(responsePayload, config);
1328
+ if (!validateStatus(response.status)) {
1329
+ const error = new HbMiniProgramNetworkError(response);
1330
+ logNetworkRequestFailure(config, error, {
1331
+ kind: 'http_status',
1332
+ status: response.status,
1333
+ statusText: response.statusText,
1334
+ data: response.data,
1335
+ headers: response.headers,
1336
+ });
1337
+ throw error;
1338
+ }
1339
+ return response;
1340
+ }
1341
+ catch (error) {
1342
+ if (error instanceof HbMiniProgramNetworkError) {
1343
+ throw error;
1344
+ }
1345
+ if (error instanceof HbMiniProgramSDKError && error.code === 'INVALID_PARAMS') {
1346
+ logNetworkRequestFailure(config, error, {
1347
+ kind: 'bridge_error',
1348
+ });
1349
+ throw error;
1350
+ }
1351
+ if (error instanceof HbMiniProgramSDKError && error.code === 'INVALID_NETWORK_RESPONSE') {
1352
+ logNetworkRequestFailure(config, error, {
1353
+ kind: 'invalid_response',
1354
+ data: error.data,
1355
+ });
1356
+ throw error;
1357
+ }
1358
+ logNetworkRequestFailure(config, error, {
1359
+ kind: error instanceof HbMiniProgramSDKError ? 'bridge_error' : 'unknown',
1360
+ });
1361
+ throw error;
1362
+ }
1363
+ }
1364
+ /**
1365
+ * 在进入 bridge 前拒绝 Host 能力明确不支持的公开请求形态。
1366
+ *
1367
+ * @remarks
1368
+ * 提前失败可避免本地 Mock(fetch)与 App Host(heybox)能力差造成“本地成功、上线 500”。
1369
+ */
1370
+ function assertSupportedPublicNetworkRequest(config) {
1371
+ if (isUnsupportedMultipartNetworkRequest(config)) {
1372
+ throw createSDKError('INVALID_PARAMS', NETWORK_REQUEST_MULTIPART_UNSUPPORTED_MESSAGE);
976
1373
  }
977
- return response;
978
1374
  }
979
1375
  /**
980
1376
  * 创建 network 模块。