@heybox/hb-sdk 0.6.8-alpha.2 → 0.6.8
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 +42 -0
- package/README.md +2 -0
- package/dist/cli-chunks/{build-CglqyB9Z.cjs → build-D4crgajb.cjs} +29 -25
- package/dist/cli-chunks/{context-CP7W_8aR.cjs → context-fRWWSXDj.cjs} +122 -23
- package/dist/cli-chunks/{create-DldsTIVt.cjs → create-BU_EFgDz.cjs} +1 -1
- package/dist/cli-chunks/{dev-BG0icySa.cjs → dev-BrkChQ7v.cjs} +142 -108
- package/dist/cli-chunks/{doctor-Cd6Xq0m-.cjs → doctor-DZXVO4rL.cjs} +1 -1
- package/dist/cli-chunks/{index-paN77avR.cjs → index-CSjJrnkO.cjs} +14 -14
- package/dist/cli-chunks/{index-jXyfZKy2.cjs → index-DxH41nsb.cjs} +3 -3
- package/dist/cli-chunks/{login-DlD9n_AF.cjs → login-DLVozxrs.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-vHezGsg7.cjs → project-vite-CwmMfCk3.cjs} +1 -1
- package/dist/cli-chunks/{remote-O5_nRdZN.cjs → remote-CfpkkC9I.cjs} +40 -22
- package/dist/cli-chunks/{runtime-gate-DfMJQGH9.cjs → runtime-gate-CgN_v4Te.cjs} +3 -3
- package/dist/cli-chunks/{index-v4-6fbXX.cjs → runtime-permission-env-B0jK9Rt1.cjs} +71 -0
- package/dist/cli-chunks/{session-CiQplems.cjs → session-DB7ARwm4.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/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 +4 -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/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
|
@@ -24,7 +24,7 @@ function isManagedMiniProgramRuntimePermissionKey(key) {
|
|
|
24
24
|
* @returns 解析状态和通过校验的受管权限。
|
|
25
25
|
*/
|
|
26
26
|
function parseMiniProgramRuntimePermissions(snapshot) {
|
|
27
|
-
if (!isRecord$
|
|
27
|
+
if (!isRecord$3(snapshot) || snapshot.schema_version !== 1 || !Array.isArray(snapshot.entries)) {
|
|
28
28
|
return { valid: false, permissions: {} };
|
|
29
29
|
}
|
|
30
30
|
if (snapshot.revision !== undefined && (typeof snapshot.revision !== 'number' || !Number.isInteger(snapshot.revision) || snapshot.revision < 0)) {
|
|
@@ -33,11 +33,11 @@ function parseMiniProgramRuntimePermissions(snapshot) {
|
|
|
33
33
|
const seenKeys = new Set();
|
|
34
34
|
const permissions = {};
|
|
35
35
|
for (const rawEntry of snapshot.entries) {
|
|
36
|
-
if (!isRecord$
|
|
36
|
+
if (!isRecord$3(rawEntry) || typeof rawEntry.key !== 'string' || !rawEntry.key.trim()) {
|
|
37
37
|
return { valid: false, permissions: {} };
|
|
38
38
|
}
|
|
39
39
|
const key = rawEntry.key.trim();
|
|
40
|
-
if (seenKeys.has(key) || (rawEntry.status !== 'enabled' && rawEntry.status !== 'disabled') || !isRecord$
|
|
40
|
+
if (seenKeys.has(key) || (rawEntry.status !== 'enabled' && rawEntry.status !== 'disabled') || !isRecord$3(rawEntry.config)) {
|
|
41
41
|
return { valid: false, permissions: {} };
|
|
42
42
|
}
|
|
43
43
|
seenKeys.add(key);
|
|
@@ -55,7 +55,7 @@ function parseMiniProgramRuntimePermissions(snapshot) {
|
|
|
55
55
|
}
|
|
56
56
|
return { valid: true, permissions };
|
|
57
57
|
}
|
|
58
|
-
function isRecord$
|
|
58
|
+
function isRecord$3(value) {
|
|
59
59
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -4194,6 +4194,293 @@ function readStringEnum(value, allowedValues, message) {
|
|
|
4194
4194
|
return value;
|
|
4195
4195
|
}
|
|
4196
4196
|
|
|
4197
|
+
const LOG_PREFIX = '[hb-sdk-runtime][network.request]';
|
|
4198
|
+
const DATA_PREVIEW_MAX_CHARS$1 = 500;
|
|
4199
|
+
const SENSITIVE_HEADER_NAMES$1 = new Set([
|
|
4200
|
+
'authorization',
|
|
4201
|
+
'cookie',
|
|
4202
|
+
'proxy-authorization',
|
|
4203
|
+
'set-cookie',
|
|
4204
|
+
'token',
|
|
4205
|
+
'x-pkey',
|
|
4206
|
+
'api-key',
|
|
4207
|
+
'apikey',
|
|
4208
|
+
'x-api-key',
|
|
4209
|
+
'x-auth-token',
|
|
4210
|
+
'x-access-token',
|
|
4211
|
+
'session',
|
|
4212
|
+
'session-id',
|
|
4213
|
+
'jwt',
|
|
4214
|
+
'refresh-token',
|
|
4215
|
+
'x-csrf-token',
|
|
4216
|
+
]);
|
|
4217
|
+
/** 与 SDK sanitize 保持同一敏感 key 规则,避免双轨漂移。 */
|
|
4218
|
+
const SENSITIVE_KEY_PATTERN$1 = /(authorization|cookie|password|passwd|secret|token|pkey|private[_-]?key|access[_-]?key|client[_-]?secret|api[_-]?key|apikey|session|jwt|refresh[_-]?token|csrf)/i;
|
|
4219
|
+
/**
|
|
4220
|
+
* 记录 Runtime 侧 `network.request` 异常或非 2xx 结果,便于 Host / 调试面板观测。
|
|
4221
|
+
*
|
|
4222
|
+
* @remarks
|
|
4223
|
+
* 不改变 bridge 语义:非 2xx 仍作为 success payload 返回;仅输出诊断日志。
|
|
4224
|
+
* detail 构造与 logger 调用均包在 try 内,日志失败不得影响 bridge 请求路径。
|
|
4225
|
+
*
|
|
4226
|
+
* 日志阶段与 SDK 侧关系:
|
|
4227
|
+
* - Runtime 固定按 HTTP 2xx 判定 `http_status`;
|
|
4228
|
+
* - SDK 以业务 `validateStatus` 为准,自定义校验可能导致两端日志不对称。
|
|
4229
|
+
*/
|
|
4230
|
+
function logRuntimeNetworkFailure(options) {
|
|
4231
|
+
try {
|
|
4232
|
+
const consoleRef = typeof console === 'undefined' ? undefined : console;
|
|
4233
|
+
const logger = consoleRef?.warn || consoleRef?.error || consoleRef?.log;
|
|
4234
|
+
if (!logger) {
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4237
|
+
const detail = createRuntimeNetworkFailureDetail(options);
|
|
4238
|
+
logger.call(consoleRef, LOG_PREFIX, formatRuntimeNetworkFailureMessage(detail), detail);
|
|
4239
|
+
}
|
|
4240
|
+
catch {
|
|
4241
|
+
// 日志失败不得影响 bridge 请求路径。
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
function createRuntimeNetworkFailureDetail(options) {
|
|
4245
|
+
const method = (options.request?.method || readPayloadMethod(options.payload) || 'GET').toUpperCase();
|
|
4246
|
+
const url = sanitizeRuntimeNetworkUrl(options.request?.url || readPayloadUrl(options.payload) || '');
|
|
4247
|
+
const detail = {
|
|
4248
|
+
phase: options.phase,
|
|
4249
|
+
method,
|
|
4250
|
+
url,
|
|
4251
|
+
};
|
|
4252
|
+
if (options.response && typeof options.response.status === 'number') {
|
|
4253
|
+
detail.status = options.response.status;
|
|
4254
|
+
}
|
|
4255
|
+
if (options.response?.statusText) {
|
|
4256
|
+
detail.statusText = options.response.statusText;
|
|
4257
|
+
}
|
|
4258
|
+
const errorInfo = readRuntimeNetworkErrorInfo(options.error);
|
|
4259
|
+
if (errorInfo.message) {
|
|
4260
|
+
detail.message = sanitizeRuntimeDiagnosticMessage(errorInfo.message);
|
|
4261
|
+
}
|
|
4262
|
+
if (errorInfo.code) {
|
|
4263
|
+
detail.code = errorInfo.code;
|
|
4264
|
+
}
|
|
4265
|
+
// http_status:响应头写入 headers,请求头写入 requestHeaders;其余阶段优先请求头。
|
|
4266
|
+
const requestHeaders = sanitizeRuntimeNetworkHeaders(options.request?.headers);
|
|
4267
|
+
const responseHeaders = sanitizeRuntimeNetworkHeaders(options.response?.headers);
|
|
4268
|
+
if (options.phase === 'http_status') {
|
|
4269
|
+
if (responseHeaders && Object.keys(responseHeaders).length > 0) {
|
|
4270
|
+
detail.headers = responseHeaders;
|
|
4271
|
+
}
|
|
4272
|
+
if (requestHeaders && Object.keys(requestHeaders).length > 0) {
|
|
4273
|
+
detail.requestHeaders = requestHeaders;
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
else if (requestHeaders && Object.keys(requestHeaders).length > 0) {
|
|
4277
|
+
detail.headers = requestHeaders;
|
|
4278
|
+
}
|
|
4279
|
+
else if (responseHeaders && Object.keys(responseHeaders).length > 0) {
|
|
4280
|
+
detail.headers = responseHeaders;
|
|
4281
|
+
}
|
|
4282
|
+
const dataPreview = previewRuntimeNetworkData(options.response?.data !== undefined ? options.response.data : options.request?.data);
|
|
4283
|
+
if (dataPreview !== undefined) {
|
|
4284
|
+
detail.dataPreview = dataPreview;
|
|
4285
|
+
}
|
|
4286
|
+
if (options.request?.timeout !== undefined) {
|
|
4287
|
+
detail.timeout = options.request.timeout;
|
|
4288
|
+
}
|
|
4289
|
+
if (options.request?.withCredentials !== undefined) {
|
|
4290
|
+
detail.withCredentials = options.request.withCredentials;
|
|
4291
|
+
}
|
|
4292
|
+
return detail;
|
|
4293
|
+
}
|
|
4294
|
+
function formatRuntimeNetworkFailureMessage(detail) {
|
|
4295
|
+
const phase = String(detail.phase || 'unknown');
|
|
4296
|
+
const method = String(detail.method || 'GET');
|
|
4297
|
+
const url = String(detail.url || '');
|
|
4298
|
+
const statusPart = typeof detail.status === 'number' ? ` status=${detail.status}` : '';
|
|
4299
|
+
const codePart = detail.code ? ` code=${detail.code}` : '';
|
|
4300
|
+
return `${phase} ${method} ${url}${statusPart}${codePart}`;
|
|
4301
|
+
}
|
|
4302
|
+
function readPayloadUrl(payload) {
|
|
4303
|
+
if (!isRecord$2(payload) || typeof payload.url !== 'string') {
|
|
4304
|
+
return undefined;
|
|
4305
|
+
}
|
|
4306
|
+
return payload.url;
|
|
4307
|
+
}
|
|
4308
|
+
function readPayloadMethod(payload) {
|
|
4309
|
+
if (!isRecord$2(payload) || typeof payload.method !== 'string') {
|
|
4310
|
+
return undefined;
|
|
4311
|
+
}
|
|
4312
|
+
return payload.method;
|
|
4313
|
+
}
|
|
4314
|
+
function readRuntimeNetworkErrorInfo(error) {
|
|
4315
|
+
if (error instanceof Error) {
|
|
4316
|
+
const code = isRecord$2(error) && typeof error.code === 'string' ? error.code : undefined;
|
|
4317
|
+
return {
|
|
4318
|
+
message: error.message,
|
|
4319
|
+
...(code ? { code } : {}),
|
|
4320
|
+
};
|
|
4321
|
+
}
|
|
4322
|
+
if (isRecord$2(error)) {
|
|
4323
|
+
const code = typeof error.code === 'string' ? error.code : undefined;
|
|
4324
|
+
const message = typeof error.message === 'string' ? error.message : undefined;
|
|
4325
|
+
if (code || message) {
|
|
4326
|
+
return {
|
|
4327
|
+
...(code ? { code } : {}),
|
|
4328
|
+
...(message ? { message } : {}),
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
if (error !== undefined) {
|
|
4333
|
+
return { message: String(error) };
|
|
4334
|
+
}
|
|
4335
|
+
return {};
|
|
4336
|
+
}
|
|
4337
|
+
function sanitizeRuntimeNetworkUrl(url) {
|
|
4338
|
+
if (!url) {
|
|
4339
|
+
return url;
|
|
4340
|
+
}
|
|
4341
|
+
try {
|
|
4342
|
+
const parsed = new URL(url);
|
|
4343
|
+
parsed.username = '';
|
|
4344
|
+
parsed.password = '';
|
|
4345
|
+
redactRuntimeUrlSearchParams(parsed.searchParams);
|
|
4346
|
+
if (parsed.hash && parsed.hash.length > 1) {
|
|
4347
|
+
const hashBody = parsed.hash.slice(1);
|
|
4348
|
+
if (hashBody.includes('=')) {
|
|
4349
|
+
const hashParams = new URLSearchParams(hashBody);
|
|
4350
|
+
if (redactRuntimeUrlSearchParams(hashParams)) {
|
|
4351
|
+
parsed.hash = hashParams.toString();
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
return parsed.toString();
|
|
4356
|
+
}
|
|
4357
|
+
catch {
|
|
4358
|
+
return redactRuntimeSensitivePairsInRawText(url);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
function sanitizeRuntimeDiagnosticMessage(message) {
|
|
4362
|
+
if (!message) {
|
|
4363
|
+
return message;
|
|
4364
|
+
}
|
|
4365
|
+
return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeRuntimeNetworkUrl(matched.replace(/[),.;]+$/g, '')));
|
|
4366
|
+
}
|
|
4367
|
+
function sanitizeRuntimeNetworkHeaders(headers) {
|
|
4368
|
+
if (!headers) {
|
|
4369
|
+
return undefined;
|
|
4370
|
+
}
|
|
4371
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
|
|
4372
|
+
key,
|
|
4373
|
+
isSensitiveRuntimeNetworkHeaderName(key) ? '[redacted]' : value,
|
|
4374
|
+
]));
|
|
4375
|
+
}
|
|
4376
|
+
function previewRuntimeNetworkData(data) {
|
|
4377
|
+
if (data === undefined) {
|
|
4378
|
+
return undefined;
|
|
4379
|
+
}
|
|
4380
|
+
if (typeof data === 'string') {
|
|
4381
|
+
return previewRuntimeNetworkStringData(data);
|
|
4382
|
+
}
|
|
4383
|
+
if (typeof data === 'number' || typeof data === 'boolean' || data === null) {
|
|
4384
|
+
return data;
|
|
4385
|
+
}
|
|
4386
|
+
try {
|
|
4387
|
+
const sanitized = sanitizeRuntimeNetworkDataValue(data);
|
|
4388
|
+
const serialized = JSON.stringify(sanitized);
|
|
4389
|
+
if (serialized === undefined) {
|
|
4390
|
+
return '[unserializable]';
|
|
4391
|
+
}
|
|
4392
|
+
if (serialized.length <= DATA_PREVIEW_MAX_CHARS$1) {
|
|
4393
|
+
return sanitized;
|
|
4394
|
+
}
|
|
4395
|
+
return truncateText$1(serialized, DATA_PREVIEW_MAX_CHARS$1);
|
|
4396
|
+
}
|
|
4397
|
+
catch {
|
|
4398
|
+
return '[unserializable]';
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
function previewRuntimeNetworkStringData(data) {
|
|
4402
|
+
const trimmed = data.trim();
|
|
4403
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
4404
|
+
try {
|
|
4405
|
+
return previewRuntimeNetworkData(JSON.parse(trimmed));
|
|
4406
|
+
}
|
|
4407
|
+
catch {
|
|
4408
|
+
// fall through
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
if (trimmed.includes('=') && !trimmed.includes('\n') && trimmed.length <= DATA_PREVIEW_MAX_CHARS$1 * 2) {
|
|
4412
|
+
try {
|
|
4413
|
+
const params = new URLSearchParams(trimmed);
|
|
4414
|
+
if ([...params.keys()].length > 0) {
|
|
4415
|
+
return previewRuntimeNetworkData(Object.fromEntries([...params.entries()].map(([key, value]) => [
|
|
4416
|
+
key,
|
|
4417
|
+
isSensitiveRuntimeNetworkKey(key) ? '[redacted]' : value,
|
|
4418
|
+
])));
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
catch {
|
|
4422
|
+
// fall through
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
return truncateText$1(redactRuntimeSensitivePairsInRawText(data), DATA_PREVIEW_MAX_CHARS$1);
|
|
4426
|
+
}
|
|
4427
|
+
function sanitizeRuntimeNetworkDataValue(value) {
|
|
4428
|
+
if (Array.isArray(value)) {
|
|
4429
|
+
return value.slice(0, 20).map(item => sanitizeRuntimeNetworkDataValue(item));
|
|
4430
|
+
}
|
|
4431
|
+
// 与 SDK 对齐:仅递归 plain object,避免 Date / Error 等特殊对象行为不一致。
|
|
4432
|
+
if (!isPlainObject$1(value)) {
|
|
4433
|
+
if (typeof value === 'string') {
|
|
4434
|
+
return truncateText$1(value, DATA_PREVIEW_MAX_CHARS$1);
|
|
4435
|
+
}
|
|
4436
|
+
return value;
|
|
4437
|
+
}
|
|
4438
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
4439
|
+
key,
|
|
4440
|
+
isSensitiveRuntimeNetworkKey(key) ? '[redacted]' : sanitizeRuntimeNetworkDataValue(item),
|
|
4441
|
+
]));
|
|
4442
|
+
}
|
|
4443
|
+
function isSensitiveRuntimeNetworkHeaderName(headerName) {
|
|
4444
|
+
const normalized = headerName.trim().toLowerCase();
|
|
4445
|
+
return (SENSITIVE_HEADER_NAMES$1.has(normalized) ||
|
|
4446
|
+
isSensitiveRuntimeNetworkKey(normalized) ||
|
|
4447
|
+
normalized.startsWith('x-heybox-') ||
|
|
4448
|
+
normalized.startsWith('x-xhh-'));
|
|
4449
|
+
}
|
|
4450
|
+
function isSensitiveRuntimeNetworkKey(key) {
|
|
4451
|
+
return SENSITIVE_KEY_PATTERN$1.test(key.trim());
|
|
4452
|
+
}
|
|
4453
|
+
function redactRuntimeUrlSearchParams(params) {
|
|
4454
|
+
let changed = false;
|
|
4455
|
+
for (const key of [...params.keys()]) {
|
|
4456
|
+
if (isSensitiveRuntimeNetworkKey(key)) {
|
|
4457
|
+
params.set(key, '[redacted]');
|
|
4458
|
+
changed = true;
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
return changed;
|
|
4462
|
+
}
|
|
4463
|
+
function redactRuntimeSensitivePairsInRawText(text) {
|
|
4464
|
+
return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key) => {
|
|
4465
|
+
if (!isSensitiveRuntimeNetworkKey(key)) {
|
|
4466
|
+
return full;
|
|
4467
|
+
}
|
|
4468
|
+
return `${prefix}${key}=[redacted]`;
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
function truncateText$1(value, maxChars) {
|
|
4472
|
+
if (value.length <= maxChars) {
|
|
4473
|
+
return value;
|
|
4474
|
+
}
|
|
4475
|
+
return `${value.slice(0, maxChars)}…(truncated, total=${value.length})`;
|
|
4476
|
+
}
|
|
4477
|
+
function isPlainObject$1(value) {
|
|
4478
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
4479
|
+
}
|
|
4480
|
+
function isRecord$2(value) {
|
|
4481
|
+
return typeof value === 'object' && value !== null;
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4197
4484
|
const NETWORK_REQUEST_ALLOWED_KEYS = new Set([
|
|
4198
4485
|
'url',
|
|
4199
4486
|
'method',
|
|
@@ -4227,9 +4514,42 @@ const NETWORK_HTTP_METHODS = new Set([
|
|
|
4227
4514
|
'HEAD',
|
|
4228
4515
|
'OPTIONS',
|
|
4229
4516
|
]);
|
|
4517
|
+
/** 与 App Host heybox transport 能力对齐:不支持 multipart。 */
|
|
4518
|
+
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。';
|
|
4230
4519
|
/** network.request:校验桥接参数并委托平台适配层完成真实 HTTP 交换。 */
|
|
4231
4520
|
async function requestMiniProgramRuntimeNetwork(payload, platformAdapter) {
|
|
4232
|
-
|
|
4521
|
+
let request;
|
|
4522
|
+
try {
|
|
4523
|
+
request = readMiniProgramRuntimeNetworkRequest(payload);
|
|
4524
|
+
}
|
|
4525
|
+
catch (error) {
|
|
4526
|
+
logRuntimeNetworkFailure({
|
|
4527
|
+
phase: 'validate',
|
|
4528
|
+
payload,
|
|
4529
|
+
error,
|
|
4530
|
+
});
|
|
4531
|
+
throw error;
|
|
4532
|
+
}
|
|
4533
|
+
try {
|
|
4534
|
+
const response = await platformAdapter.network.request(request);
|
|
4535
|
+
if (typeof response?.status === 'number' && (response.status < 200 || response.status >= 300)) {
|
|
4536
|
+
// 非 2xx 仍走 bridge success payload;此处只补诊断日志,方便对照抓包与 SDK 侧异常。
|
|
4537
|
+
logRuntimeNetworkFailure({
|
|
4538
|
+
phase: 'http_status',
|
|
4539
|
+
request,
|
|
4540
|
+
response,
|
|
4541
|
+
});
|
|
4542
|
+
}
|
|
4543
|
+
return response;
|
|
4544
|
+
}
|
|
4545
|
+
catch (error) {
|
|
4546
|
+
logRuntimeNetworkFailure({
|
|
4547
|
+
phase: 'adapter',
|
|
4548
|
+
request,
|
|
4549
|
+
error,
|
|
4550
|
+
});
|
|
4551
|
+
throw error;
|
|
4552
|
+
}
|
|
4233
4553
|
}
|
|
4234
4554
|
function readMiniProgramRuntimeNetworkRequest(payload) {
|
|
4235
4555
|
assertMiniProgramRuntimeRecord(payload, 'network.request 参数必须是对象');
|
|
@@ -4259,8 +4579,30 @@ function readMiniProgramRuntimeNetworkRequest(payload) {
|
|
|
4259
4579
|
if (payload.withCredentials !== undefined) {
|
|
4260
4580
|
request.withCredentials = readMiniProgramRuntimeBoolean(payload.withCredentials, 'network.request withCredentials 必须是布尔值');
|
|
4261
4581
|
}
|
|
4582
|
+
assertSupportedNetworkRequestShape(request);
|
|
4262
4583
|
return request;
|
|
4263
4584
|
}
|
|
4585
|
+
/**
|
|
4586
|
+
* 拒绝 Host 客户端代发不支持的请求体形态,避免伪装成目标站 HTTP 500。
|
|
4587
|
+
*/
|
|
4588
|
+
function assertSupportedNetworkRequestShape(request) {
|
|
4589
|
+
if (hasMultipartContentType(request.headers) || looksLikeMultipartBody(request.data)) {
|
|
4590
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', NETWORK_REQUEST_MULTIPART_UNSUPPORTED_MESSAGE);
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
function hasMultipartContentType(headers) {
|
|
4594
|
+
if (!headers) {
|
|
4595
|
+
return false;
|
|
4596
|
+
}
|
|
4597
|
+
return Object.entries(headers).some(([key, value]) => key.trim().toLowerCase() === 'content-type' && value.toLowerCase().includes('multipart/form-data'));
|
|
4598
|
+
}
|
|
4599
|
+
function looksLikeMultipartBody(data) {
|
|
4600
|
+
if (typeof data !== 'string') {
|
|
4601
|
+
return false;
|
|
4602
|
+
}
|
|
4603
|
+
const trimmed = data.trimStart();
|
|
4604
|
+
return trimmed.startsWith('--') && /content-disposition\s*:\s*form-data/i.test(data);
|
|
4605
|
+
}
|
|
4264
4606
|
function assertNetworkAllowedKeys(payload) {
|
|
4265
4607
|
const invalidKeys = Object.keys(payload).filter(key => !NETWORK_REQUEST_ALLOWED_KEYS.has(key));
|
|
4266
4608
|
if (invalidKeys.length > 0) {
|
|
@@ -6683,6 +7025,221 @@ function isPlainRecord(value) {
|
|
|
6683
7025
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6684
7026
|
}
|
|
6685
7027
|
|
|
7028
|
+
/**
|
|
7029
|
+
* network.request 诊断日志的纯脱敏工具。
|
|
7030
|
+
*
|
|
7031
|
+
* @remarks
|
|
7032
|
+
* 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
|
|
7033
|
+
* modules observability、devtools Mock Host / proxy 共用,并满足 boundary
|
|
7034
|
+
*(devtools 不得直接 import runtime capability modules)。
|
|
7035
|
+
*/
|
|
7036
|
+
const DATA_PREVIEW_MAX_CHARS = 500;
|
|
7037
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
7038
|
+
'authorization',
|
|
7039
|
+
'cookie',
|
|
7040
|
+
'proxy-authorization',
|
|
7041
|
+
'set-cookie',
|
|
7042
|
+
'token',
|
|
7043
|
+
'x-pkey',
|
|
7044
|
+
'api-key',
|
|
7045
|
+
'apikey',
|
|
7046
|
+
'x-api-key',
|
|
7047
|
+
'x-auth-token',
|
|
7048
|
+
'x-access-token',
|
|
7049
|
+
'session',
|
|
7050
|
+
'session-id',
|
|
7051
|
+
'jwt',
|
|
7052
|
+
'refresh-token',
|
|
7053
|
+
'x-csrf-token',
|
|
7054
|
+
]);
|
|
7055
|
+
/** 敏感字段名:header / query / body key 共用。 */
|
|
7056
|
+
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;
|
|
7057
|
+
/**
|
|
7058
|
+
* 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
|
|
7059
|
+
*/
|
|
7060
|
+
function sanitizeNetworkRequestUrl(url) {
|
|
7061
|
+
if (!url) {
|
|
7062
|
+
return url;
|
|
7063
|
+
}
|
|
7064
|
+
try {
|
|
7065
|
+
const parsed = new URL(url);
|
|
7066
|
+
parsed.username = '';
|
|
7067
|
+
parsed.password = '';
|
|
7068
|
+
redactUrlSearchParams(parsed.searchParams);
|
|
7069
|
+
if (parsed.hash && parsed.hash.length > 1) {
|
|
7070
|
+
const hashBody = parsed.hash.slice(1);
|
|
7071
|
+
if (hashBody.includes('=')) {
|
|
7072
|
+
const hashParams = new URLSearchParams(hashBody);
|
|
7073
|
+
if (redactUrlSearchParams(hashParams)) {
|
|
7074
|
+
parsed.hash = hashParams.toString();
|
|
7075
|
+
}
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
return parsed.toString();
|
|
7079
|
+
}
|
|
7080
|
+
catch {
|
|
7081
|
+
return redactSensitivePairsInRawText(url);
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
/**
|
|
7085
|
+
* 脱敏请求/响应头;敏感名(含 heybox 前缀)替换为 `[redacted]`。
|
|
7086
|
+
*/
|
|
7087
|
+
function sanitizeNetworkHeaders(headers) {
|
|
7088
|
+
if (!headers) {
|
|
7089
|
+
return {};
|
|
7090
|
+
}
|
|
7091
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
|
|
7092
|
+
key,
|
|
7093
|
+
isSensitiveNetworkHeaderName(key) ? '[redacted]' : value,
|
|
7094
|
+
]));
|
|
7095
|
+
}
|
|
7096
|
+
/**
|
|
7097
|
+
* 预览请求/响应 data:递归脱敏敏感字段,截断长文本与大数组。
|
|
7098
|
+
*/
|
|
7099
|
+
function previewNetworkData(data) {
|
|
7100
|
+
if (data === undefined) {
|
|
7101
|
+
return undefined;
|
|
7102
|
+
}
|
|
7103
|
+
if (typeof data === 'string') {
|
|
7104
|
+
return previewNetworkStringData(data);
|
|
7105
|
+
}
|
|
7106
|
+
if (typeof data === 'number' || typeof data === 'boolean' || data === null) {
|
|
7107
|
+
return data;
|
|
7108
|
+
}
|
|
7109
|
+
try {
|
|
7110
|
+
const sanitized = sanitizeNetworkDataValue(data);
|
|
7111
|
+
const serialized = JSON.stringify(sanitized);
|
|
7112
|
+
if (serialized === undefined) {
|
|
7113
|
+
return '[unserializable]';
|
|
7114
|
+
}
|
|
7115
|
+
if (serialized.length <= DATA_PREVIEW_MAX_CHARS) {
|
|
7116
|
+
return sanitized;
|
|
7117
|
+
}
|
|
7118
|
+
return truncateText(serialized, DATA_PREVIEW_MAX_CHARS);
|
|
7119
|
+
}
|
|
7120
|
+
catch {
|
|
7121
|
+
return '[unserializable]';
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
/**
|
|
7125
|
+
* 递归脱敏日志对象(payload / result):敏感 key、headers、url 字段统一处理。
|
|
7126
|
+
*/
|
|
7127
|
+
function redactNetworkLogValue(value) {
|
|
7128
|
+
if (Array.isArray(value)) {
|
|
7129
|
+
return value.slice(0, 20).map(item => redactNetworkLogValue(item));
|
|
7130
|
+
}
|
|
7131
|
+
if (typeof value === 'string') {
|
|
7132
|
+
if (looksLikeUrl(value)) {
|
|
7133
|
+
return sanitizeNetworkRequestUrl(value);
|
|
7134
|
+
}
|
|
7135
|
+
return previewNetworkStringData(value);
|
|
7136
|
+
}
|
|
7137
|
+
if (typeof value === 'number' || typeof value === 'boolean' || value === null || value === undefined) {
|
|
7138
|
+
return value;
|
|
7139
|
+
}
|
|
7140
|
+
if (!isPlainObject(value)) {
|
|
7141
|
+
return previewNetworkData(value);
|
|
7142
|
+
}
|
|
7143
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
7144
|
+
const normalized = key.trim().toLowerCase();
|
|
7145
|
+
if (isSensitiveNetworkKey(key)) {
|
|
7146
|
+
return [key, '[redacted]'];
|
|
7147
|
+
}
|
|
7148
|
+
if (normalized === 'headers' && isPlainObject(item)) {
|
|
7149
|
+
return [
|
|
7150
|
+
key,
|
|
7151
|
+
sanitizeNetworkHeaders(Object.fromEntries(Object.entries(item).flatMap(([headerKey, headerValue]) => typeof headerValue === 'string' ? [[headerKey, headerValue]] : []))),
|
|
7152
|
+
];
|
|
7153
|
+
}
|
|
7154
|
+
if ((normalized === 'url' || normalized.endsWith('_url') || normalized.endsWith('url')) && typeof item === 'string') {
|
|
7155
|
+
return [key, sanitizeNetworkRequestUrl(item)];
|
|
7156
|
+
}
|
|
7157
|
+
return [key, redactNetworkLogValue(item)];
|
|
7158
|
+
}));
|
|
7159
|
+
}
|
|
7160
|
+
function isSensitiveNetworkHeaderName(headerName) {
|
|
7161
|
+
const normalized = headerName.trim().toLowerCase();
|
|
7162
|
+
return (SENSITIVE_HEADER_NAMES.has(normalized) ||
|
|
7163
|
+
isSensitiveNetworkKey(normalized) ||
|
|
7164
|
+
normalized.startsWith('x-heybox-') ||
|
|
7165
|
+
normalized.startsWith('x-xhh-'));
|
|
7166
|
+
}
|
|
7167
|
+
function isSensitiveNetworkKey(key) {
|
|
7168
|
+
return SENSITIVE_KEY_PATTERN.test(key.trim());
|
|
7169
|
+
}
|
|
7170
|
+
function previewNetworkStringData(data) {
|
|
7171
|
+
const trimmed = data.trim();
|
|
7172
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
7173
|
+
try {
|
|
7174
|
+
return previewNetworkData(JSON.parse(trimmed));
|
|
7175
|
+
}
|
|
7176
|
+
catch {
|
|
7177
|
+
// fall through
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
if (trimmed.includes('=') && !trimmed.includes('\n') && trimmed.length <= DATA_PREVIEW_MAX_CHARS * 2) {
|
|
7181
|
+
try {
|
|
7182
|
+
const params = new URLSearchParams(trimmed);
|
|
7183
|
+
if ([...params.keys()].length > 0) {
|
|
7184
|
+
const redacted = Object.fromEntries([...params.entries()].map(([key, value]) => [
|
|
7185
|
+
key,
|
|
7186
|
+
isSensitiveNetworkKey(key) ? '[redacted]' : value,
|
|
7187
|
+
]));
|
|
7188
|
+
return previewNetworkData(redacted);
|
|
7189
|
+
}
|
|
7190
|
+
}
|
|
7191
|
+
catch {
|
|
7192
|
+
// fall through
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
return truncateText(redactSensitivePairsInRawText(data), DATA_PREVIEW_MAX_CHARS);
|
|
7196
|
+
}
|
|
7197
|
+
function sanitizeNetworkDataValue(value) {
|
|
7198
|
+
if (Array.isArray(value)) {
|
|
7199
|
+
return value.slice(0, 20).map(item => sanitizeNetworkDataValue(item));
|
|
7200
|
+
}
|
|
7201
|
+
if (!isPlainObject(value)) {
|
|
7202
|
+
if (typeof value === 'string') {
|
|
7203
|
+
return truncateText(value, DATA_PREVIEW_MAX_CHARS);
|
|
7204
|
+
}
|
|
7205
|
+
return value;
|
|
7206
|
+
}
|
|
7207
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
7208
|
+
key,
|
|
7209
|
+
isSensitiveNetworkKey(key) ? '[redacted]' : sanitizeNetworkDataValue(item),
|
|
7210
|
+
]));
|
|
7211
|
+
}
|
|
7212
|
+
function redactUrlSearchParams(params) {
|
|
7213
|
+
let changed = false;
|
|
7214
|
+
for (const key of [...params.keys()]) {
|
|
7215
|
+
if (isSensitiveNetworkKey(key)) {
|
|
7216
|
+
params.set(key, '[redacted]');
|
|
7217
|
+
changed = true;
|
|
7218
|
+
}
|
|
7219
|
+
}
|
|
7220
|
+
return changed;
|
|
7221
|
+
}
|
|
7222
|
+
function redactSensitivePairsInRawText(text) {
|
|
7223
|
+
return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
|
|
7224
|
+
if (!isSensitiveNetworkKey(key)) {
|
|
7225
|
+
return full;
|
|
7226
|
+
}
|
|
7227
|
+
return `${prefix}${key}=[redacted]`;
|
|
7228
|
+
});
|
|
7229
|
+
}
|
|
7230
|
+
function looksLikeUrl(value) {
|
|
7231
|
+
return /^https?:\/\//i.test(value.trim()) || /^[a-z][a-z0-9+.-]*:\/\//i.test(value.trim());
|
|
7232
|
+
}
|
|
7233
|
+
function truncateText(value, maxChars) {
|
|
7234
|
+
if (value.length <= maxChars) {
|
|
7235
|
+
return value;
|
|
7236
|
+
}
|
|
7237
|
+
return `${value.slice(0, maxChars)}…(truncated, total=${value.length})`;
|
|
7238
|
+
}
|
|
7239
|
+
function isPlainObject(value) {
|
|
7240
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
7241
|
+
}
|
|
7242
|
+
|
|
6686
7243
|
const NETWORK_PROXY_PATH = '/__hb_sdk_mock_network__';
|
|
6687
7244
|
const MOCK_HOST_BOOTSTRAP_PATH = '/__hb_sdk_bootstrap__';
|
|
6688
7245
|
const DEV_PERMISSIONS_PATH = '/__hb_sdk__/dev-permissions';
|
|
@@ -7021,7 +7578,23 @@ async function handleRequest(message) {
|
|
|
7021
7578
|
const bridgeError = toMockBridgeError(error);
|
|
7022
7579
|
postResponse(message, undefined, bridgeError);
|
|
7023
7580
|
postEvent('error', bridgeError);
|
|
7024
|
-
|
|
7581
|
+
if (message.method === NETWORK_REQUEST_METHOD) {
|
|
7582
|
+
console.warn('[hb-sdk][mock-host][network.request]', 'bridge_error', {
|
|
7583
|
+
payload: redactNetworkLogValue(message.payload),
|
|
7584
|
+
error: bridgeError,
|
|
7585
|
+
});
|
|
7586
|
+
}
|
|
7587
|
+
pushLog(message.method, {
|
|
7588
|
+
...(message.method === NETWORK_REQUEST_METHOD
|
|
7589
|
+
? {
|
|
7590
|
+
summary: 'bridge_error',
|
|
7591
|
+
payload: redactNetworkLogValue(message.payload),
|
|
7592
|
+
}
|
|
7593
|
+
: {
|
|
7594
|
+
payload: message.payload,
|
|
7595
|
+
}),
|
|
7596
|
+
error: bridgeError,
|
|
7597
|
+
});
|
|
7025
7598
|
}
|
|
7026
7599
|
}
|
|
7027
7600
|
function createRequestLogDetail(method, payload, result) {
|
|
@@ -7033,11 +7606,41 @@ function createRequestLogDetail(method, payload, result) {
|
|
|
7033
7606
|
protocol: latestOpenAppPage?.options.protocol,
|
|
7034
7607
|
};
|
|
7035
7608
|
}
|
|
7609
|
+
if (method === NETWORK_REQUEST_METHOD) {
|
|
7610
|
+
return createNetworkRequestLogDetail(payload, result);
|
|
7611
|
+
}
|
|
7036
7612
|
return {
|
|
7037
7613
|
payload,
|
|
7038
7614
|
result,
|
|
7039
7615
|
};
|
|
7040
7616
|
}
|
|
7617
|
+
function createNetworkRequestLogDetail(payload, result) {
|
|
7618
|
+
const requestUrl = isRecord(payload) && typeof payload.url === 'string' ? payload.url : '';
|
|
7619
|
+
const sanitizedUrl = requestUrl ? sanitizeNetworkRequestUrl(requestUrl) : '';
|
|
7620
|
+
const requestMethod = isRecord(payload) && typeof payload.method === 'string' ? payload.method.toUpperCase() : 'GET';
|
|
7621
|
+
const status = isRecord(result) && typeof result.status === 'number' ? result.status : undefined;
|
|
7622
|
+
const statusText = isRecord(result) && typeof result.statusText === 'string' ? result.statusText : undefined;
|
|
7623
|
+
const ok = typeof status === 'number' ? status >= 200 && status < 300 : undefined;
|
|
7624
|
+
const summary = [
|
|
7625
|
+
`${requestMethod} ${sanitizedUrl || '(missing url)'}`,
|
|
7626
|
+
status === undefined ? 'status=?' : `status=${status}`,
|
|
7627
|
+
ok === false ? 'FAILED' : ok === true ? 'OK' : 'UNKNOWN',
|
|
7628
|
+
].join(' · ');
|
|
7629
|
+
if (ok === false) {
|
|
7630
|
+
console.warn('[hb-sdk][mock-host][network.request]', summary, {
|
|
7631
|
+
payload: redactNetworkLogValue(payload),
|
|
7632
|
+
result: redactNetworkLogValue(result),
|
|
7633
|
+
});
|
|
7634
|
+
}
|
|
7635
|
+
return {
|
|
7636
|
+
summary,
|
|
7637
|
+
ok,
|
|
7638
|
+
status,
|
|
7639
|
+
statusText,
|
|
7640
|
+
payload: redactNetworkLogValue(payload),
|
|
7641
|
+
result: redactNetworkLogValue(result),
|
|
7642
|
+
};
|
|
7643
|
+
}
|
|
7041
7644
|
async function requestNetwork(config) {
|
|
7042
7645
|
const response = await fetch(NETWORK_PROXY_PATH, {
|
|
7043
7646
|
method: 'POST',
|