@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.
- package/CHANGELOG.md +56 -0
- package/README.md +2 -0
- package/dist/cli-chunks/{build-CglqyB9Z.cjs → build-9hbhrRbU.cjs} +29 -25
- package/dist/cli-chunks/{context-CP7W_8aR.cjs → context-7rmakzS_.cjs} +122 -23
- package/dist/cli-chunks/{create-DldsTIVt.cjs → create-BkX4rtRP.cjs} +1 -1
- package/dist/cli-chunks/{dev-BG0icySa.cjs → dev-DiZGT4FQ.cjs} +142 -108
- package/dist/cli-chunks/{doctor-Cd6Xq0m-.cjs → doctor-BcpqXVRh.cjs} +1 -1
- package/dist/cli-chunks/{index-jXyfZKy2.cjs → index-6HfcwZ_r.cjs} +3 -3
- package/dist/cli-chunks/{index-paN77avR.cjs → index-DofTxdoX.cjs} +14 -14
- package/dist/cli-chunks/{login-DlD9n_AF.cjs → login-DUiejZcD.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-vHezGsg7.cjs → project-vite-CfD8Bp1K.cjs} +1 -1
- package/dist/cli-chunks/{remote-O5_nRdZN.cjs → remote-A4-PKgOn.cjs} +462 -190
- package/dist/cli-chunks/{runtime-gate-DfMJQGH9.cjs → runtime-gate-BWlU-R4h.cjs} +3 -3
- package/dist/cli-chunks/{index-v4-6fbXX.cjs → runtime-permission-env-DKrhgVM3.cjs} +276 -49
- package/dist/cli-chunks/{session-CiQplems.cjs → session-D5u3XzHN.cjs} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/devtools/mock-host/main.js +609 -6
- package/dist/index.cjs.js +403 -7
- package/dist/index.esm.js +403 -7
- package/dist/miniapp-publish.cjs.js +205 -0
- package/dist/miniapp-publish.esm.js +201 -1
- package/dist/vite.cjs.js +17 -2
- package/dist/vite.esm.js +17 -2
- package/package.json +2 -2
- package/skill/SKILL.md +7 -6
- package/skill/references/api-root.md +7 -2
- package/skill/references/cli.md +12 -0
- package/skill/references/examples.md +17 -2
- package/skill/references/safety-boundaries.md +2 -0
- package/skill/scripts/sync-references.mjs +17 -2
- package/skill/skill.json +4 -4
- package/types/core/network-sanitize.d.ts +31 -0
- package/types/miniapp-publish/index.d.ts +77 -0
- package/types/modules/network/index.d.ts +2 -0
- package/types/modules/network/observability.d.ts +39 -0
- package/types/modules/network/request-shape.d.ts +44 -0
- package/types/vite/html-policy.d.ts +1 -0
- package/types/vite/runtime-permission-env.d.ts +9 -0
package/dist/index.esm.js
CHANGED
|
@@ -1,3 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* network.request 请求体形态校验与宿主失败诊断文案。
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* App Host 的 heybox 客户端代发 transport 仅声明 form/json,不支持 multipart。
|
|
6
|
+
* 本地 Mock 若直接走 fetch 会“能通”,上线后却变成难以理解的 500 network_error。
|
|
7
|
+
* 因此在 SDK 侧统一提前拒绝 multipart,并在宿主伪装 HTTP 失败时给出可操作提示。
|
|
8
|
+
*/
|
|
9
|
+
/** multipart 不被 App Host 支持时的稳定说明(开发者可见)。 */
|
|
10
|
+
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。';
|
|
11
|
+
/**
|
|
12
|
+
* 读取请求头中的 Content-Type(大小写不敏感)。
|
|
13
|
+
*/
|
|
14
|
+
function readNetworkContentType(headers) {
|
|
15
|
+
if (!headers) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
19
|
+
if (key.trim().toLowerCase() === 'content-type' && typeof value === 'string') {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 判断 Content-Type 是否声明为 multipart/form-data。
|
|
27
|
+
*/
|
|
28
|
+
function isMultipartContentType(contentType) {
|
|
29
|
+
return typeof contentType === 'string' && contentType.toLowerCase().includes('multipart/form-data');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 判断字符串 body 是否像手写 multipart 实体。
|
|
33
|
+
*/
|
|
34
|
+
function looksLikeMultipartBody(data) {
|
|
35
|
+
if (typeof data !== 'string') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const trimmed = data.trimStart();
|
|
39
|
+
return trimmed.startsWith('--') && /content-disposition\s*:\s*form-data/i.test(data);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 判断公开请求配置是否使用了 Host 不支持的 multipart 形态。
|
|
43
|
+
*/
|
|
44
|
+
function isUnsupportedMultipartNetworkRequest(config) {
|
|
45
|
+
return isMultipartContentType(readNetworkContentType(config.headers)) || looksLikeMultipartBody(config.data);
|
|
46
|
+
}
|
|
47
|
+
function isPlainObject$2(value) {
|
|
48
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 从宿主返回的“完成态”响应中提取更清晰的失败原因。
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Host 可能把 transport 不支持等内部错误包装成 HTTP 500 + `{ status: 'network_error', msg }`。
|
|
55
|
+
* 此时 `status=500` 并不代表目标站点返回了 500。
|
|
56
|
+
*/
|
|
57
|
+
function describeHostNetworkFailure(response) {
|
|
58
|
+
const data = response.data;
|
|
59
|
+
const hostStatus = isPlainObject$2(data) && typeof data.status === 'string' ? data.status : undefined;
|
|
60
|
+
const hostMsg = isPlainObject$2(data) && typeof data.msg === 'string'
|
|
61
|
+
? data.msg
|
|
62
|
+
: isPlainObject$2(data) && typeof data.message === 'string'
|
|
63
|
+
? data.message
|
|
64
|
+
: undefined;
|
|
65
|
+
const isHostNetworkError = hostStatus === 'network_error';
|
|
66
|
+
const mentionsUnsupportedShape = typeof hostMsg === 'string' &&
|
|
67
|
+
(hostMsg.includes('unsupported-request-shape') || hostMsg.includes('unsupported_request_shape'));
|
|
68
|
+
const requestLooksMultipart = isUnsupportedMultipartNetworkRequest(response.config || {});
|
|
69
|
+
if (!isHostNetworkError && !mentionsUnsupportedShape && !(requestLooksMultipart && response.status >= 500)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const lines = [
|
|
73
|
+
'原因: 这是宿主网络层失败,不是目标 URL 的业务 HTTP 响应。',
|
|
74
|
+
];
|
|
75
|
+
if (hostMsg) {
|
|
76
|
+
lines.push(`宿主信息: ${hostMsg}`);
|
|
77
|
+
}
|
|
78
|
+
if (requestLooksMultipart || mentionsUnsupportedShape) {
|
|
79
|
+
lines.push(NETWORK_REQUEST_MULTIPART_UNSUPPORTED_MESSAGE);
|
|
80
|
+
}
|
|
81
|
+
else if (isHostNetworkError) {
|
|
82
|
+
lines.push('请检查请求 URL、headers、body 编码与 Host 网络权限;本地 Mock 与 App Host 能力并不完全一致。');
|
|
83
|
+
}
|
|
84
|
+
return lines.join(' ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* network.request 诊断日志的纯脱敏工具。
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
|
|
92
|
+
* modules observability、devtools Mock Host / proxy 共用,并满足 boundary
|
|
93
|
+
*(devtools 不得直接 import runtime capability modules)。
|
|
94
|
+
*/
|
|
95
|
+
const DATA_PREVIEW_MAX_CHARS = 500;
|
|
96
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
97
|
+
'authorization',
|
|
98
|
+
'cookie',
|
|
99
|
+
'proxy-authorization',
|
|
100
|
+
'set-cookie',
|
|
101
|
+
'token',
|
|
102
|
+
'x-pkey',
|
|
103
|
+
'api-key',
|
|
104
|
+
'apikey',
|
|
105
|
+
'x-api-key',
|
|
106
|
+
'x-auth-token',
|
|
107
|
+
'x-access-token',
|
|
108
|
+
'session',
|
|
109
|
+
'session-id',
|
|
110
|
+
'jwt',
|
|
111
|
+
'refresh-token',
|
|
112
|
+
'x-csrf-token',
|
|
113
|
+
]);
|
|
114
|
+
/** 敏感字段名:header / query / body key 共用。 */
|
|
115
|
+
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;
|
|
116
|
+
/**
|
|
117
|
+
* 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
|
|
118
|
+
*/
|
|
119
|
+
function sanitizeNetworkRequestUrl(url) {
|
|
120
|
+
if (!url) {
|
|
121
|
+
return url;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const parsed = new URL(url);
|
|
125
|
+
parsed.username = '';
|
|
126
|
+
parsed.password = '';
|
|
127
|
+
redactUrlSearchParams(parsed.searchParams);
|
|
128
|
+
if (parsed.hash && parsed.hash.length > 1) {
|
|
129
|
+
const hashBody = parsed.hash.slice(1);
|
|
130
|
+
if (hashBody.includes('=')) {
|
|
131
|
+
const hashParams = new URLSearchParams(hashBody);
|
|
132
|
+
if (redactUrlSearchParams(hashParams)) {
|
|
133
|
+
parsed.hash = hashParams.toString();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return parsed.toString();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return redactSensitivePairsInRawText(url);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 脱敏请求/响应头;敏感名(含 heybox 前缀)替换为 `[redacted]`。
|
|
145
|
+
*/
|
|
146
|
+
function sanitizeNetworkHeaders(headers) {
|
|
147
|
+
if (!headers) {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
|
|
151
|
+
key,
|
|
152
|
+
isSensitiveNetworkHeaderName(key) ? '[redacted]' : value,
|
|
153
|
+
]));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 预览请求/响应 data:递归脱敏敏感字段,截断长文本与大数组。
|
|
157
|
+
*/
|
|
158
|
+
function previewNetworkData(data) {
|
|
159
|
+
if (data === undefined) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
if (typeof data === 'string') {
|
|
163
|
+
return previewNetworkStringData(data);
|
|
164
|
+
}
|
|
165
|
+
if (typeof data === 'number' || typeof data === 'boolean' || data === null) {
|
|
166
|
+
return data;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const sanitized = sanitizeNetworkDataValue(data);
|
|
170
|
+
const serialized = JSON.stringify(sanitized);
|
|
171
|
+
if (serialized === undefined) {
|
|
172
|
+
return '[unserializable]';
|
|
173
|
+
}
|
|
174
|
+
if (serialized.length <= DATA_PREVIEW_MAX_CHARS) {
|
|
175
|
+
return sanitized;
|
|
176
|
+
}
|
|
177
|
+
return truncateText(serialized, DATA_PREVIEW_MAX_CHARS);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return '[unserializable]';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
|
|
185
|
+
*/
|
|
186
|
+
function sanitizeDiagnosticMessage(message) {
|
|
187
|
+
if (!message) {
|
|
188
|
+
return message;
|
|
189
|
+
}
|
|
190
|
+
return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
|
|
191
|
+
}
|
|
192
|
+
function isSensitiveNetworkHeaderName(headerName) {
|
|
193
|
+
const normalized = headerName.trim().toLowerCase();
|
|
194
|
+
return (SENSITIVE_HEADER_NAMES.has(normalized) ||
|
|
195
|
+
isSensitiveNetworkKey(normalized) ||
|
|
196
|
+
normalized.startsWith('x-heybox-') ||
|
|
197
|
+
normalized.startsWith('x-xhh-'));
|
|
198
|
+
}
|
|
199
|
+
function isSensitiveNetworkKey(key) {
|
|
200
|
+
return SENSITIVE_KEY_PATTERN.test(key.trim());
|
|
201
|
+
}
|
|
202
|
+
function previewNetworkStringData(data) {
|
|
203
|
+
const trimmed = data.trim();
|
|
204
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
205
|
+
try {
|
|
206
|
+
return previewNetworkData(JSON.parse(trimmed));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// fall through
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (trimmed.includes('=') && !trimmed.includes('\n') && trimmed.length <= DATA_PREVIEW_MAX_CHARS * 2) {
|
|
213
|
+
try {
|
|
214
|
+
const params = new URLSearchParams(trimmed);
|
|
215
|
+
if ([...params.keys()].length > 0) {
|
|
216
|
+
const redacted = Object.fromEntries([...params.entries()].map(([key, value]) => [
|
|
217
|
+
key,
|
|
218
|
+
isSensitiveNetworkKey(key) ? '[redacted]' : value,
|
|
219
|
+
]));
|
|
220
|
+
return previewNetworkData(redacted);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// fall through
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return truncateText(redactSensitivePairsInRawText(data), DATA_PREVIEW_MAX_CHARS);
|
|
228
|
+
}
|
|
229
|
+
function sanitizeNetworkDataValue(value) {
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
return value.slice(0, 20).map(item => sanitizeNetworkDataValue(item));
|
|
232
|
+
}
|
|
233
|
+
if (!isPlainObject$1(value)) {
|
|
234
|
+
if (typeof value === 'string') {
|
|
235
|
+
return truncateText(value, DATA_PREVIEW_MAX_CHARS);
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
240
|
+
key,
|
|
241
|
+
isSensitiveNetworkKey(key) ? '[redacted]' : sanitizeNetworkDataValue(item),
|
|
242
|
+
]));
|
|
243
|
+
}
|
|
244
|
+
function redactUrlSearchParams(params) {
|
|
245
|
+
let changed = false;
|
|
246
|
+
for (const key of [...params.keys()]) {
|
|
247
|
+
if (isSensitiveNetworkKey(key)) {
|
|
248
|
+
params.set(key, '[redacted]');
|
|
249
|
+
changed = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return changed;
|
|
253
|
+
}
|
|
254
|
+
function redactSensitivePairsInRawText(text) {
|
|
255
|
+
return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
|
|
256
|
+
if (!isSensitiveNetworkKey(key)) {
|
|
257
|
+
return full;
|
|
258
|
+
}
|
|
259
|
+
return `${prefix}${key}=[redacted]`;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function truncateText(value, maxChars) {
|
|
263
|
+
if (value.length <= maxChars) {
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
return `${value.slice(0, maxChars)}…(truncated, total=${value.length})`;
|
|
267
|
+
}
|
|
268
|
+
function isPlainObject$1(value) {
|
|
269
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
270
|
+
}
|
|
271
|
+
|
|
1
272
|
/**
|
|
2
273
|
* SDK 对外抛出的标准 bridge / runtime 错误类型。
|
|
3
274
|
*
|
|
@@ -60,7 +331,14 @@ class HbMiniProgramNetworkError extends Error {
|
|
|
60
331
|
* @param response 已完成请求的标准化网络响应。
|
|
61
332
|
*/
|
|
62
333
|
constructor(response) {
|
|
63
|
-
|
|
334
|
+
const method = (response.config.method || 'GET').toUpperCase();
|
|
335
|
+
// 诊断文案使用脱敏 URL,避免 query 中的 token 等经 error.message 泄漏。
|
|
336
|
+
const url = sanitizeNetworkRequestUrl(response.config.url || '');
|
|
337
|
+
const statusText = response.statusText ? ` ${response.statusText}` : '';
|
|
338
|
+
const baseMessage = `network.request failed with status ${response.status}${statusText}: ${method} ${url}`;
|
|
339
|
+
// 宿主可能把 transport 不支持等内部错误伪装成 HTTP 500;补充可操作提示。
|
|
340
|
+
const hostHint = describeHostNetworkFailure(response);
|
|
341
|
+
super(hostHint ? `${baseMessage}\n${hostHint}` : baseMessage);
|
|
64
342
|
this.name = 'HbMiniProgramNetworkError';
|
|
65
343
|
this.status = response.status;
|
|
66
344
|
this.data = response.data;
|
|
@@ -321,7 +599,7 @@ function createMessageId() {
|
|
|
321
599
|
/** 构建时替换为当前发布包的实际版本。 */
|
|
322
600
|
const HB_SDK_VERSION = typeof undefined === 'string'
|
|
323
601
|
? undefined
|
|
324
|
-
: '0.6.
|
|
602
|
+
: '0.6.9-alpha.0';
|
|
325
603
|
|
|
326
604
|
/**
|
|
327
605
|
* 判断未知数据是否符合小程序 bridge 消息信封。
|
|
@@ -888,6 +1166,77 @@ function createStorageModule(requester) {
|
|
|
888
1166
|
};
|
|
889
1167
|
}
|
|
890
1168
|
|
|
1169
|
+
const LOG_PREFIX = '[hb-sdk][network.request]';
|
|
1170
|
+
/**
|
|
1171
|
+
* 记录 `network.request` 失败,保证异常路径在开发者控制台可观测。
|
|
1172
|
+
*
|
|
1173
|
+
* @remarks
|
|
1174
|
+
* - 仅用于诊断,不改变错误抛出语义。
|
|
1175
|
+
* - 会脱敏 token/cookie 等敏感头与字段,并截断 body 预览。
|
|
1176
|
+
* - detail 构造与 logger 调用均包在 try 内,日志失败不得影响业务错误抛出。
|
|
1177
|
+
*/
|
|
1178
|
+
function logNetworkRequestFailure(config, error, extras) {
|
|
1179
|
+
try {
|
|
1180
|
+
const consoleRef = typeof console === 'undefined' ? undefined : console;
|
|
1181
|
+
const logger = consoleRef?.warn || consoleRef?.error || consoleRef?.log;
|
|
1182
|
+
if (!logger) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const detail = createNetworkRequestFailureLog(config, error, extras);
|
|
1186
|
+
logger.call(consoleRef, LOG_PREFIX, formatNetworkRequestFailureMessage(detail), detail);
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
// 日志失败不得影响业务错误抛出。
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function createNetworkRequestFailureLog(config, error, extras) {
|
|
1193
|
+
const method = (config.method || 'GET').toUpperCase();
|
|
1194
|
+
const detail = {
|
|
1195
|
+
kind: extras.kind,
|
|
1196
|
+
method,
|
|
1197
|
+
url: sanitizeNetworkRequestUrl(config.url),
|
|
1198
|
+
};
|
|
1199
|
+
if (typeof extras.status === 'number') {
|
|
1200
|
+
detail.status = extras.status;
|
|
1201
|
+
}
|
|
1202
|
+
else if (error instanceof HbMiniProgramNetworkError) {
|
|
1203
|
+
detail.status = error.status;
|
|
1204
|
+
}
|
|
1205
|
+
if (typeof extras.statusText === 'string' && extras.statusText) {
|
|
1206
|
+
detail.statusText = extras.statusText;
|
|
1207
|
+
}
|
|
1208
|
+
// NetworkError 已有 method/url/status 结构化字段;不再回写 message,避免 raw URL 二次泄漏。
|
|
1209
|
+
if (!(error instanceof HbMiniProgramNetworkError)) {
|
|
1210
|
+
if (error instanceof HbMiniProgramSDKError) {
|
|
1211
|
+
detail.code = error.code;
|
|
1212
|
+
detail.message = sanitizeDiagnosticMessage(error.message);
|
|
1213
|
+
}
|
|
1214
|
+
else if (error instanceof Error) {
|
|
1215
|
+
detail.message = sanitizeDiagnosticMessage(error.message);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (config.timeout !== undefined) {
|
|
1219
|
+
detail.timeout = config.timeout;
|
|
1220
|
+
}
|
|
1221
|
+
if (config.withCredentials !== undefined) {
|
|
1222
|
+
detail.withCredentials = config.withCredentials;
|
|
1223
|
+
}
|
|
1224
|
+
const headers = sanitizeNetworkHeaders(extras.headers || config.headers);
|
|
1225
|
+
if (Object.keys(headers).length > 0) {
|
|
1226
|
+
detail.headers = headers;
|
|
1227
|
+
}
|
|
1228
|
+
const dataPreview = previewNetworkData(extras.data !== undefined ? extras.data : config.data);
|
|
1229
|
+
if (dataPreview !== undefined) {
|
|
1230
|
+
detail.dataPreview = dataPreview;
|
|
1231
|
+
}
|
|
1232
|
+
return detail;
|
|
1233
|
+
}
|
|
1234
|
+
function formatNetworkRequestFailureMessage(detail) {
|
|
1235
|
+
const statusPart = detail.status === undefined ? '' : ` status=${detail.status}`;
|
|
1236
|
+
const codePart = detail.code ? ` code=${detail.code}` : '';
|
|
1237
|
+
return `${detail.kind} ${detail.method} ${detail.url}${statusPart}${codePart}`;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
891
1240
|
const DEFAULT_VALIDATE_STATUS = status => status >= 200 && status < 300;
|
|
892
1241
|
function isPlainObject(value) {
|
|
893
1242
|
return Object.prototype.toString.call(value) === '[object Object]';
|
|
@@ -931,6 +1280,9 @@ function normalizeHeaders(headers) {
|
|
|
931
1280
|
return Object.fromEntries(Object.entries(headers).flatMap(([key, value]) => (typeof value === 'string' ? [[key, value]] : [])));
|
|
932
1281
|
}
|
|
933
1282
|
function toNetworkResponse(payload, config) {
|
|
1283
|
+
if (payload == null || typeof payload !== 'object') {
|
|
1284
|
+
throw createSDKError('INVALID_NETWORK_RESPONSE', 'network.request 返回了无效的响应', payload);
|
|
1285
|
+
}
|
|
934
1286
|
if (typeof payload.status !== 'number' || Number.isNaN(payload.status)) {
|
|
935
1287
|
throw createSDKError('INVALID_NETWORK_RESPONSE', 'network.request 返回了无效的 status', payload);
|
|
936
1288
|
}
|
|
@@ -965,12 +1317,56 @@ function toNetworkResponse(payload, config) {
|
|
|
965
1317
|
*/
|
|
966
1318
|
async function request(requester, config) {
|
|
967
1319
|
const validateStatus = config.validateStatus || DEFAULT_VALIDATE_STATUS;
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1320
|
+
try {
|
|
1321
|
+
assertSupportedPublicNetworkRequest(config);
|
|
1322
|
+
const responsePayload = await requester.request(NETWORK_REQUEST_METHOD, toRequestPayload(config));
|
|
1323
|
+
const response = toNetworkResponse(responsePayload, config);
|
|
1324
|
+
if (!validateStatus(response.status)) {
|
|
1325
|
+
const error = new HbMiniProgramNetworkError(response);
|
|
1326
|
+
logNetworkRequestFailure(config, error, {
|
|
1327
|
+
kind: 'http_status',
|
|
1328
|
+
status: response.status,
|
|
1329
|
+
statusText: response.statusText,
|
|
1330
|
+
data: response.data,
|
|
1331
|
+
headers: response.headers,
|
|
1332
|
+
});
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
return response;
|
|
1336
|
+
}
|
|
1337
|
+
catch (error) {
|
|
1338
|
+
if (error instanceof HbMiniProgramNetworkError) {
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
if (error instanceof HbMiniProgramSDKError && error.code === 'INVALID_PARAMS') {
|
|
1342
|
+
logNetworkRequestFailure(config, error, {
|
|
1343
|
+
kind: 'bridge_error',
|
|
1344
|
+
});
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
if (error instanceof HbMiniProgramSDKError && error.code === 'INVALID_NETWORK_RESPONSE') {
|
|
1348
|
+
logNetworkRequestFailure(config, error, {
|
|
1349
|
+
kind: 'invalid_response',
|
|
1350
|
+
data: error.data,
|
|
1351
|
+
});
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
logNetworkRequestFailure(config, error, {
|
|
1355
|
+
kind: error instanceof HbMiniProgramSDKError ? 'bridge_error' : 'unknown',
|
|
1356
|
+
});
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* 在进入 bridge 前拒绝 Host 能力明确不支持的公开请求形态。
|
|
1362
|
+
*
|
|
1363
|
+
* @remarks
|
|
1364
|
+
* 提前失败可避免本地 Mock(fetch)与 App Host(heybox)能力差造成“本地成功、上线 500”。
|
|
1365
|
+
*/
|
|
1366
|
+
function assertSupportedPublicNetworkRequest(config) {
|
|
1367
|
+
if (isUnsupportedMultipartNetworkRequest(config)) {
|
|
1368
|
+
throw createSDKError('INVALID_PARAMS', NETWORK_REQUEST_MULTIPART_UNSUPPORTED_MESSAGE);
|
|
972
1369
|
}
|
|
973
|
-
return response;
|
|
974
1370
|
}
|
|
975
1371
|
/**
|
|
976
1372
|
* 创建 network 模块。
|
|
@@ -2836,6 +2836,10 @@ const UPDATE_SQUARE_DISPLAY_API_PATH = '/mall/developer/user_miniprogram/square_
|
|
|
2836
2836
|
const LIST_USER_MINIPROGRAM_VERSION_API_PATH = '/mall/developer/user_miniprogram/version/list';
|
|
2837
2837
|
const FNV_OFFSET = 0x811c9dc5;
|
|
2838
2838
|
const FNV_PRIME = 0x01000193;
|
|
2839
|
+
const MINIAPP_UPLOAD_SESSION_REF_LENGTH = 10;
|
|
2840
|
+
const MINIAPP_ARTIFACT_SIGNATURE_MAGIC = 'heybox-user-miniprogram-artifact\0v1\0';
|
|
2841
|
+
const UNPAIRED_SURROGATE_PATTERN = /[\ud800-\udbff](?![\udc00-\udfff])|(^|[^\ud800-\udbff])[\udc00-\udfff]/;
|
|
2842
|
+
const textEncoder = new TextEncoder();
|
|
2839
2843
|
function normalizeRelativePath(relativePath) {
|
|
2840
2844
|
return String(relativePath || '')
|
|
2841
2845
|
.replace(/\\/g, '/')
|
|
@@ -2929,6 +2933,202 @@ function shouldUploadDistFile(relativePath) {
|
|
|
2929
2933
|
}
|
|
2930
2934
|
return true;
|
|
2931
2935
|
}
|
|
2936
|
+
/**
|
|
2937
|
+
* 校验构建产物路径和大小,并按路径 UTF-8 字节序排序。
|
|
2938
|
+
*
|
|
2939
|
+
* @param files 待处理的构建产物文件。
|
|
2940
|
+
* @returns 保留输入附加字段的有序文件列表。
|
|
2941
|
+
* @throws 路径不规范、路径重复或文件大小非法时抛出错误。
|
|
2942
|
+
*/
|
|
2943
|
+
function prepareMiniappArtifactFiles(files) {
|
|
2944
|
+
if (files.length > 0xffffffff) {
|
|
2945
|
+
throw new Error('构建产物文件数量超出签名协议上限');
|
|
2946
|
+
}
|
|
2947
|
+
const usedPaths = new Set();
|
|
2948
|
+
const prepared = files.map(file => {
|
|
2949
|
+
const relativePath = normalizeMiniappArtifactRelativePath(file.relativePath);
|
|
2950
|
+
if (usedPaths.has(relativePath)) {
|
|
2951
|
+
throw new Error(`构建产物包含重复文件路径:${relativePath}`);
|
|
2952
|
+
}
|
|
2953
|
+
usedPaths.add(relativePath);
|
|
2954
|
+
validateArtifactFileSize(file.size, relativePath);
|
|
2955
|
+
return { ...file, relativePath };
|
|
2956
|
+
});
|
|
2957
|
+
return prepared.sort((left, right) => compareBytes(textEncoder.encode(left.relativePath), textEncoder.encode(right.relativePath)));
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* 按服务端 session key 前缀校验最终上传路径的 UTF-8 字节长度。
|
|
2961
|
+
*
|
|
2962
|
+
* @param files 待上传的构建产物文件。
|
|
2963
|
+
* @param options 小程序和长度限制配置。
|
|
2964
|
+
* @returns 第一个超长路径的错误提示;全部合法时返回 `undefined`。
|
|
2965
|
+
*/
|
|
2966
|
+
function validateMiniappUploadPathLengths(files, options) {
|
|
2967
|
+
const maxLength = options.maxLength ?? ACTIVITY_UPLOAD_KEY_MAX_LENGTH;
|
|
2968
|
+
const prefix = `/u/${getMiniProgramUploadAlias(options.miniProgramId)}/${'x'.repeat(MINIAPP_UPLOAD_SESSION_REF_LENGTH)}/`;
|
|
2969
|
+
const prefixLength = textEncoder.encode(prefix).length;
|
|
2970
|
+
const tooLongFile = files.find(file => {
|
|
2971
|
+
const relativePath = normalizeMiniappArtifactRelativePath(file.relativePath);
|
|
2972
|
+
return prefixLength + textEncoder.encode(relativePath).length > maxLength;
|
|
2973
|
+
});
|
|
2974
|
+
return tooLongFile ? `文件路径过长,请缩短构建产物文件名或目录层级:${tooLongFile.relativePath}` : undefined;
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* 创建 v1 整包签名头。
|
|
2978
|
+
*
|
|
2979
|
+
* @param fileCount 参与签名的文件数量。
|
|
2980
|
+
* @returns 包含协议标识和文件数量的字节数组。
|
|
2981
|
+
* @throws 文件数量超出协议范围时抛出错误。
|
|
2982
|
+
*/
|
|
2983
|
+
function createMiniappArtifactSignatureHeader(fileCount) {
|
|
2984
|
+
if (!Number.isInteger(fileCount) || fileCount < 0 || fileCount > 0xffffffff) {
|
|
2985
|
+
throw new Error('构建产物文件数量超出签名协议上限');
|
|
2986
|
+
}
|
|
2987
|
+
return concatBytes(textEncoder.encode(MINIAPP_ARTIFACT_SIGNATURE_MAGIC), encodeUint32BE(fileCount));
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* 创建单个文件的 v1 framing header。
|
|
2991
|
+
*
|
|
2992
|
+
* @param file 已规范化或待校验的构建产物文件。
|
|
2993
|
+
* @returns 包含路径长度、路径和文件大小的字节数组。
|
|
2994
|
+
* @throws 文件路径或大小非法时抛出错误。
|
|
2995
|
+
*/
|
|
2996
|
+
function createMiniappArtifactFileHeader(file) {
|
|
2997
|
+
const relativePath = normalizeMiniappArtifactRelativePath(file.relativePath);
|
|
2998
|
+
validateArtifactFileSize(file.size, relativePath);
|
|
2999
|
+
const pathBytes = textEncoder.encode(relativePath);
|
|
3000
|
+
return concatBytes(encodeUint32BE(pathBytes.length), pathBytes, encodeUint64BE(file.size));
|
|
3001
|
+
}
|
|
3002
|
+
/**
|
|
3003
|
+
* 校验服务端 session 文件映射,并计算仍需上传的文件。
|
|
3004
|
+
*
|
|
3005
|
+
* @param localFiles 本地构建产物文件。
|
|
3006
|
+
* @param serverFiles 服务端签发的文件映射。
|
|
3007
|
+
* @param uploadedKeys 服务端已确认上传成功的 key。
|
|
3008
|
+
* @returns 完整映射、待上传文件和已确认 key。
|
|
3009
|
+
* @throws 服务端映射与本地产物不一致时抛出错误。
|
|
3010
|
+
*/
|
|
3011
|
+
function resolveMiniappUploadSessionFiles(localFiles, serverFiles, uploadedKeys) {
|
|
3012
|
+
const preparedFiles = prepareMiniappArtifactFiles(localFiles);
|
|
3013
|
+
if (serverFiles.length !== preparedFiles.length) {
|
|
3014
|
+
throw new Error(`上传会话文件数量异常:期望 ${preparedFiles.length},实际 ${serverFiles.length}`);
|
|
3015
|
+
}
|
|
3016
|
+
const mappings = new Map();
|
|
3017
|
+
const serverSizes = new Map();
|
|
3018
|
+
const knownKeys = new Set();
|
|
3019
|
+
for (const serverFile of serverFiles) {
|
|
3020
|
+
const relativePath = normalizeMiniappArtifactRelativePath(serverFile.relative_path);
|
|
3021
|
+
if (mappings.has(relativePath)) {
|
|
3022
|
+
throw new Error(`上传会话返回重复文件路径:${relativePath}`);
|
|
3023
|
+
}
|
|
3024
|
+
const key = String(serverFile.key || '');
|
|
3025
|
+
if (!key) {
|
|
3026
|
+
throw new Error(`上传会话未返回文件 key:${relativePath}`);
|
|
3027
|
+
}
|
|
3028
|
+
if (knownKeys.has(key)) {
|
|
3029
|
+
throw new Error(`上传会话返回重复文件 key:${relativePath}`);
|
|
3030
|
+
}
|
|
3031
|
+
const size = Number(serverFile.size);
|
|
3032
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
3033
|
+
throw new Error(`上传会话返回文件大小异常:${relativePath}`);
|
|
3034
|
+
}
|
|
3035
|
+
mappings.set(relativePath, key);
|
|
3036
|
+
serverSizes.set(relativePath, size);
|
|
3037
|
+
knownKeys.add(key);
|
|
3038
|
+
}
|
|
3039
|
+
const files = preparedFiles.map(file => {
|
|
3040
|
+
const key = mappings.get(file.relativePath);
|
|
3041
|
+
if (!key) {
|
|
3042
|
+
throw new Error(`上传会话缺少文件映射:${file.relativePath}`);
|
|
3043
|
+
}
|
|
3044
|
+
if (serverSizes.get(file.relativePath) !== file.size) {
|
|
3045
|
+
throw new Error(`上传会话文件大小与本地构建不一致:${file.relativePath}`);
|
|
3046
|
+
}
|
|
3047
|
+
return { file, key };
|
|
3048
|
+
});
|
|
3049
|
+
const completed = new Set();
|
|
3050
|
+
for (const rawKey of uploadedKeys) {
|
|
3051
|
+
const key = String(rawKey || '');
|
|
3052
|
+
if (!knownKeys.has(key)) {
|
|
3053
|
+
throw new Error('上传会话返回了不属于当前产物的已上传文件');
|
|
3054
|
+
}
|
|
3055
|
+
completed.add(key);
|
|
3056
|
+
}
|
|
3057
|
+
return {
|
|
3058
|
+
files,
|
|
3059
|
+
missingFiles: files.filter(file => !completed.has(file.key)),
|
|
3060
|
+
uploadedKeys: [...completed],
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
function normalizeMiniappArtifactRelativePath(relativePath) {
|
|
3064
|
+
const value = String(relativePath || '');
|
|
3065
|
+
if (!value) {
|
|
3066
|
+
throw new Error('构建产物包含空文件路径');
|
|
3067
|
+
}
|
|
3068
|
+
if (value.includes('\\')) {
|
|
3069
|
+
throw new Error(`构建产物路径不能包含反斜杠:${value}`);
|
|
3070
|
+
}
|
|
3071
|
+
if (value.startsWith('/') || value.endsWith('/') || /^[a-zA-Z]:\//.test(value)) {
|
|
3072
|
+
throw new Error(`构建产物必须使用相对文件路径:${value}`);
|
|
3073
|
+
}
|
|
3074
|
+
if (containsControlCharacter(value) || UNPAIRED_SURROGATE_PATTERN.test(value)) {
|
|
3075
|
+
throw new Error(`构建产物路径包含非法字符:${value}`);
|
|
3076
|
+
}
|
|
3077
|
+
if (value.split('/').some(segment => !segment || segment === '.' || segment === '..')) {
|
|
3078
|
+
throw new Error(`构建产物路径包含非法目录段:${value}`);
|
|
3079
|
+
}
|
|
3080
|
+
if (value.includes(',')) {
|
|
3081
|
+
throw new Error(`构建产物路径不能包含逗号:${value}`);
|
|
3082
|
+
}
|
|
3083
|
+
if (value.includes('*') || value.includes('?')) {
|
|
3084
|
+
throw new Error(`构建产物路径不能包含通配符:${value}`);
|
|
3085
|
+
}
|
|
3086
|
+
return value;
|
|
3087
|
+
}
|
|
3088
|
+
function containsControlCharacter(value) {
|
|
3089
|
+
for (const character of value) {
|
|
3090
|
+
const code = character.charCodeAt(0);
|
|
3091
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
|
|
3092
|
+
return true;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
function validateArtifactFileSize(size, relativePath) {
|
|
3098
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
3099
|
+
throw new Error(`构建产物文件大小无效:${relativePath}`);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
function encodeUint32BE(value) {
|
|
3103
|
+
const bytes = new Uint8Array(4);
|
|
3104
|
+
new DataView(bytes.buffer).setUint32(0, value, false);
|
|
3105
|
+
return bytes;
|
|
3106
|
+
}
|
|
3107
|
+
function encodeUint64BE(value) {
|
|
3108
|
+
const bytes = new Uint8Array(8);
|
|
3109
|
+
const view = new DataView(bytes.buffer);
|
|
3110
|
+
view.setUint32(0, Math.floor(value / 0x100000000), false);
|
|
3111
|
+
view.setUint32(4, value % 0x100000000, false);
|
|
3112
|
+
return bytes;
|
|
3113
|
+
}
|
|
3114
|
+
function concatBytes(...parts) {
|
|
3115
|
+
const result = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
|
|
3116
|
+
let offset = 0;
|
|
3117
|
+
for (const part of parts) {
|
|
3118
|
+
result.set(part, offset);
|
|
3119
|
+
offset += part.length;
|
|
3120
|
+
}
|
|
3121
|
+
return result;
|
|
3122
|
+
}
|
|
3123
|
+
function compareBytes(left, right) {
|
|
3124
|
+
const length = Math.min(left.length, right.length);
|
|
3125
|
+
for (let index = 0; index < length; index += 1) {
|
|
3126
|
+
if (left[index] !== right[index]) {
|
|
3127
|
+
return left[index] - right[index];
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
return left.length - right.length;
|
|
3131
|
+
}
|
|
2932
3132
|
|
|
2933
3133
|
exports.ACTIVITY_UPLOAD_KEY_MAX_LENGTH = ACTIVITY_UPLOAD_KEY_MAX_LENGTH;
|
|
2934
3134
|
exports.CREATE_USER_MINIPROGRAM_API_PATH = CREATE_USER_MINIPROGRAM_API_PATH;
|
|
@@ -2955,11 +3155,16 @@ exports.USER_MINIPROGRAM_ACCESS_STATUS_API_PATH = USER_MINIPROGRAM_ACCESS_STATUS
|
|
|
2955
3155
|
exports.USER_MINIPROGRAM_PREVIEW_ALLOWLIST_API_PATH = USER_MINIPROGRAM_PREVIEW_ALLOWLIST_API_PATH;
|
|
2956
3156
|
exports.USER_MINIPROGRAM_VERSION_PREVIEW_INFO_API_PATH = USER_MINIPROGRAM_VERSION_PREVIEW_INFO_API_PATH;
|
|
2957
3157
|
exports.WITHDRAW_USER_MINIPROGRAM_VERSION_API_PATH = WITHDRAW_USER_MINIPROGRAM_VERSION_API_PATH;
|
|
3158
|
+
exports.createMiniappArtifactFileHeader = createMiniappArtifactFileHeader;
|
|
3159
|
+
exports.createMiniappArtifactSignatureHeader = createMiniappArtifactSignatureHeader;
|
|
2958
3160
|
exports.getMiniProgramUploadAlias = getMiniProgramUploadAlias;
|
|
2959
3161
|
exports.getMiniappUploadKey = getMiniappUploadKey;
|
|
2960
3162
|
exports.isValidVersion = isValidMiniappManifestVersion;
|
|
2961
3163
|
exports.normalizeRelativePath = normalizeRelativePath;
|
|
3164
|
+
exports.prepareMiniappArtifactFiles = prepareMiniappArtifactFiles;
|
|
2962
3165
|
exports.relativePathContainsNodeModulesSegment = relativePathContainsNodeModulesSegment;
|
|
3166
|
+
exports.resolveMiniappUploadSessionFiles = resolveMiniappUploadSessionFiles;
|
|
2963
3167
|
exports.shouldUploadDistFile = shouldUploadDistFile;
|
|
3168
|
+
exports.validateMiniappUploadPathLengths = validateMiniappUploadPathLengths;
|
|
2964
3169
|
exports.validateUploadPaths = validateUploadPaths;
|
|
2965
3170
|
exports.validateUploadTotalSize = validateUploadTotalSize;
|