@heybox/hb-sdk 0.7.3 → 0.7.4-alpha.4

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 (44) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +5 -0
  3. package/dist/cli-chunks/{build-0sZ873iU.cjs → build-BqGFRLe-.cjs} +2 -2
  4. package/dist/cli-chunks/{context-DNQEI7m7.cjs → context-DTr3hRTv.cjs} +6 -19
  5. package/dist/cli-chunks/{create-BkE71M2U.cjs → create-OkiiiH4I.cjs} +1 -1
  6. package/dist/cli-chunks/{dev-_tBlQ2WO.cjs → dev-j7W9R7ew.cjs} +798 -139
  7. package/dist/cli-chunks/{doctor-B1wk8IpC.cjs → doctor-BO0vkDny.cjs} +1 -1
  8. package/dist/cli-chunks/{index-BbDsxI94.cjs → index-CZuvk1Rg.cjs} +1 -1
  9. package/dist/cli-chunks/{index-CIucOD2H.cjs → index-CyEqCbeu.cjs} +24 -21
  10. package/dist/cli-chunks/{login-CnjEKlFT.cjs → login-CuAGltGs.cjs} +2 -2
  11. package/dist/cli-chunks/{project-vite-ESaxoAE1.cjs → project-vite-BUY9DgYo.cjs} +1 -1
  12. package/dist/cli-chunks/{remote-BaGzge7R.cjs → remote-tPMcxgpi.cjs} +4 -4
  13. package/dist/cli-chunks/{session-C9KHqijL.cjs → session-BUjFX2U2.cjs} +1 -1
  14. package/dist/cli.cjs +1 -1
  15. package/dist/devtools/{mock-host → browser-dev-host}/index.html +3 -7
  16. package/dist/devtools/browser-dev-host/main.js +9835 -0
  17. package/dist/index.cjs.js +40 -118
  18. package/dist/index.esm.js +40 -118
  19. package/dist/protocol.cjs.js +71 -346
  20. package/dist/protocol.esm.js +72 -342
  21. package/dist/vite.cjs.js +1 -4
  22. package/dist/vite.esm.js +1 -4
  23. package/package.json +26 -9
  24. package/skill/SKILL.md +5 -5
  25. package/skill/references/api-protocol.md +102 -114
  26. package/skill/references/api-root.md +1 -1
  27. package/skill/references/cli.md +4 -2
  28. package/skill/references/recipes.md +2 -0
  29. package/skill/references/safety-boundaries.md +1 -0
  30. package/skill/scripts/sync-references.mjs +5 -1
  31. package/skill/skill.json +4 -4
  32. package/types/core/network-sanitize.d.ts +1 -1
  33. package/types/modules/auth/index.d.ts +3 -1
  34. package/types/modules/user/types.d.ts +2 -53
  35. package/types/protocol/capabilities.d.ts +2 -352
  36. package/types/protocol/constants.d.ts +1 -14
  37. package/types/protocol/dev-session.d.ts +3 -3
  38. package/types/protocol/guards.d.ts +1 -8
  39. package/types/protocol/runtime-permissions.d.ts +2 -41
  40. package/types/protocol/types.d.ts +3 -150
  41. package/types/protocol.d.ts +8 -23
  42. package/dist/devtools/mock-host/main.js +0 -7751
  43. package/types/protocol/network-policy.d.ts +0 -10
  44. package/types/protocol/trusted-user-gesture.d.ts +0 -6
@@ -6,15 +6,15 @@ var path = require('node:path');
6
6
  var net = require('node:net');
7
7
  var promises = require('node:dns/promises');
8
8
  var node_http = require('node:http');
9
- var context = require('./context-DNQEI7m7.cjs');
9
+ var context = require('./context-DTr3hRTv.cjs');
10
10
  var node_crypto = require('node:crypto');
11
11
  var undici = require('undici');
12
12
  var browser = require('./browser-RAy8e8cV.cjs');
13
- var index = require('./index-CIucOD2H.cjs');
14
- var projectVite = require('./project-vite-ESaxoAE1.cjs');
13
+ var index = require('./index-CyEqCbeu.cjs');
14
+ var projectVite = require('./project-vite-BUY9DgYo.cjs');
15
15
  var fs$1 = require('node:fs/promises');
16
16
  var runtimePermissionEnv = require('./runtime-permission-env-D-8_jPG3.cjs');
17
- require('./session-C9KHqijL.cjs');
17
+ require('./session-BUjFX2U2.cjs');
18
18
  require('fs');
19
19
  require('constants');
20
20
  require('stream');
@@ -197,12 +197,12 @@ async function getPorts(options) {
197
197
  }
198
198
 
199
199
  /** Dev Shell 读取局域网 Dev Session endpoint 的 query 参数。 */
200
- /** Mock Host 创建和读取 Dev Session 的固定路径。 */
200
+ /** Browser Dev Host 创建和读取 Dev Session 的固定路径。 */
201
201
  const MINI_PROGRAM_DEV_CONTEXT_PATH = '/__hb_sdk__/dev-context';
202
202
  /** Dev Session 当前使用的 schema 版本。 */
203
203
  const MINI_PROGRAM_DEV_SESSION_SCHEMA_VERSION = 1;
204
204
 
205
- const MOCK_HOST_JSON_BODY_LIMIT = 1024 * 1024;
205
+ const BROWSER_DEV_HOST_JSON_BODY_LIMIT = 1024 * 1024;
206
206
  function readJsonRequestBody(request) {
207
207
  return new Promise((resolve, reject) => {
208
208
  const chunks = [];
@@ -210,8 +210,8 @@ function readJsonRequestBody(request) {
210
210
  request.on('data', (chunk) => {
211
211
  const buffer = Buffer.from(chunk);
212
212
  size += buffer.byteLength;
213
- if (size > MOCK_HOST_JSON_BODY_LIMIT) {
214
- reject(createMockHostError(413, 'REQUEST_ENTITY_TOO_LARGE', 'mock network proxy request body too large'));
213
+ if (size > BROWSER_DEV_HOST_JSON_BODY_LIMIT) {
214
+ reject(createBrowserDevHostError(413, 'REQUEST_ENTITY_TOO_LARGE', 'Browser Dev Host network proxy request body too large'));
215
215
  request.destroy();
216
216
  return;
217
217
  }
@@ -219,7 +219,7 @@ function readJsonRequestBody(request) {
219
219
  });
220
220
  request.on('error', reject);
221
221
  request.on('aborted', () => {
222
- reject('mock network proxy request aborted');
222
+ reject('Browser Dev Host network proxy request aborted');
223
223
  });
224
224
  request.on('end', () => {
225
225
  const rawBody = Buffer.concat(chunks).toString('utf8');
@@ -231,7 +231,7 @@ function readJsonRequestBody(request) {
231
231
  resolve(JSON.parse(rawBody));
232
232
  }
233
233
  catch {
234
- reject(createMockHostError(400, 'INVALID_JSON', 'mock network proxy received invalid JSON'));
234
+ reject(createBrowserDevHostError(400, 'INVALID_JSON', 'Browser Dev Host network proxy received invalid JSON'));
235
235
  }
236
236
  });
237
237
  });
@@ -267,16 +267,16 @@ function isRecord$1(value) {
267
267
  function isObjectLike(value) {
268
268
  return (typeof value === 'object' || typeof value === 'function') && value !== null;
269
269
  }
270
- function createMockHostError(status, code, message) {
270
+ function createBrowserDevHostError(status, code, message) {
271
271
  const error = new Error(message);
272
272
  error.code = code;
273
273
  error.status = status;
274
274
  return error;
275
275
  }
276
- function readMockHostErrorStatus(error) {
276
+ function readBrowserDevHostErrorStatus(error) {
277
277
  return isObjectLike(error) && typeof error.status === 'number' ? error.status : 500;
278
278
  }
279
- function toMockHostErrorPayload(error) {
279
+ function toBrowserDevHostErrorPayload(error) {
280
280
  if (isObjectLike(error) && typeof error.code === 'string' && typeof error.message === 'string') {
281
281
  return {
282
282
  code: error.code,
@@ -284,7 +284,7 @@ function toMockHostErrorPayload(error) {
284
284
  };
285
285
  }
286
286
  return {
287
- code: 'MOCK_NETWORK_ERROR',
287
+ code: 'BROWSER_DEV_HOST_NETWORK_ERROR',
288
288
  message: readErrorMessage(error),
289
289
  };
290
290
  }
@@ -297,7 +297,7 @@ function readErrorMessage(error) {
297
297
  *
298
298
  * @remarks
299
299
  * 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
300
- * modules observability、devtools Mock Host / proxy 共用,并满足 boundary
300
+ * modules observability、devtools Browser Dev Host / proxy 共用,并满足 boundary
301
301
  *(devtools 不得直接 import runtime capability modules)。
302
302
  */
303
303
  /** 敏感字段名:header / query / body key 共用。 */
@@ -360,10 +360,13 @@ function redactSensitivePairsInRawText(text) {
360
360
  });
361
361
  }
362
362
 
363
- const MAX_MOCK_NETWORK_REDIRECTS = 5;
364
- const MAX_MOCK_NETWORK_REQUEST_BODY = 2 * 1024 * 1024;
365
- const MAX_MOCK_NETWORK_RESPONSE_BODY = 5 * 1024 * 1024;
366
- async function handleMockNetworkProxy(request, response, fetchImpl, lookupHostname, cors = false) {
363
+ const MAX_BROWSER_DEV_HOST_NETWORK_REDIRECTS = 5;
364
+ const MAX_BROWSER_DEV_HOST_NETWORK_REQUEST_BODY = 2 * 1024 * 1024;
365
+ const MAX_BROWSER_DEV_HOST_NETWORK_RESPONSE_BODY = 5 * 1024 * 1024;
366
+ const DENIED_NETWORK_TARGETS = createDeniedNetworkTargets();
367
+ const CREDENTIAL_HEADER_RE = /^(?:authentication-info|authorization|cookie|cookie2|proxy-authenticate|proxy-authentication-info|proxy-authorization|set-cookie|set-cookie2|www-authenticate|pkey|x-pkey|x-access-token|x-auth-token|x-api-key)$/i;
368
+ async function handleBrowserDevHostNetworkProxy(request, response, fetchImpl, lookupHostname, options) {
369
+ const cors = options.cors === true;
367
370
  if (request.method === 'OPTIONS') {
368
371
  writeNetworkJsonResponse(response, 204, undefined, cors);
369
372
  return;
@@ -371,24 +374,29 @@ async function handleMockNetworkProxy(request, response, fetchImpl, lookupHostna
371
374
  if (request.method !== 'POST') {
372
375
  writeNetworkJsonResponse(response, 405, {
373
376
  code: 'METHOD_NOT_ALLOWED',
374
- message: 'mock network proxy only supports POST',
377
+ message: 'Browser Dev Host network proxy only supports POST',
375
378
  }, cors);
376
379
  return;
377
380
  }
378
381
  try {
379
382
  const payload = await readJsonRequestBody(request);
380
- const result = await requestMockNetwork(payload, fetchImpl, lookupHostname);
383
+ const requestAbort = new AbortController();
384
+ if (request.aborted)
385
+ requestAbort.abort();
386
+ else
387
+ request.once('aborted', () => requestAbort.abort());
388
+ const result = await requestBrowserDevHostNetwork(payload, fetchImpl, lookupHostname, options.allowedHttpOrigins, requestAbort.signal);
381
389
  if (typeof result.status === 'number' && (result.status < 200 || result.status >= 300)) {
382
- logMockNetworkProxyDiagnostic('http_status', payload, result);
390
+ logBrowserDevHostNetworkProxyDiagnostic('http_status', payload, result);
383
391
  }
384
392
  writeNetworkJsonResponse(response, 200, result, cors);
385
393
  }
386
394
  catch (error) {
387
- logMockNetworkProxyDiagnostic('proxy_error', undefined, undefined, error);
388
- writeNetworkJsonResponse(response, readMockHostErrorStatus(error), toMockHostErrorPayload(error), cors);
395
+ logBrowserDevHostNetworkProxyDiagnostic('proxy_error', undefined, undefined, error);
396
+ writeNetworkJsonResponse(response, readBrowserDevHostErrorStatus(error), toBrowserDevHostErrorPayload(error), cors);
389
397
  }
390
398
  }
391
- function logMockNetworkProxyDiagnostic(phase, payload, result, error) {
399
+ function logBrowserDevHostNetworkProxyDiagnostic(phase, payload, result, error) {
392
400
  try {
393
401
  const consoleRef = typeof console === 'undefined' ? undefined : console;
394
402
  const logger = consoleRef?.warn || consoleRef?.error || consoleRef?.log;
@@ -415,7 +423,7 @@ function logMockNetworkProxyDiagnostic(phase, payload, result, error) {
415
423
  detail.message = sanitizeDiagnosticMessage(readErrorMessage(error));
416
424
  }
417
425
  const summaryUrl = typeof detail.url === 'string' ? detail.url : '';
418
- logger.call(consoleRef, '[hb-sdk][mock-network]', `${phase}${summaryUrl ? ` ${summaryUrl}` : ''}`, detail);
426
+ logger.call(consoleRef, '[hb-sdk][browser-dev-host][network]', `${phase}${summaryUrl ? ` ${summaryUrl}` : ''}`, detail);
419
427
  }
420
428
  catch {
421
429
  // ignore diagnostic failures
@@ -428,59 +436,65 @@ function writeNetworkJsonResponse(response, status, body, cors) {
428
436
  }
429
437
  writeJsonResponse(response, status, body);
430
438
  }
431
- const MOCK_NETWORK_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。';
432
- async function requestMockNetwork(payload, fetchImpl, lookupHostname) {
439
+ const BROWSER_DEV_HOST_NETWORK_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。';
440
+ async function requestBrowserDevHostNetwork(payload, fetchImpl, lookupHostname, allowedHttpOrigins, requestSignal) {
433
441
  if (!isRecord$1(payload) || typeof payload.url !== 'string' || !payload.url.trim()) {
434
- throw createMockHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是非空字符串');
442
+ throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是非空字符串');
435
443
  }
436
- const url = createMockNetworkUrl(payload.url, payload.params);
444
+ const url = createBrowserDevHostNetworkUrl(payload.url, payload.params);
437
445
  const method = typeof payload.method === 'string' && payload.method.trim() ? payload.method.trim().toUpperCase() : 'GET';
438
- const headers = normalizeMockNetworkHeaders(payload.headers);
439
- assertSupportedMockNetworkRequestShape(headers, payload.data);
440
- const body = createMockNetworkBody(method, payload.data, headers);
446
+ const headers = normalizeBrowserDevHostNetworkHeaders(payload.headers);
447
+ assertSupportedBrowserDevHostNetworkRequestShape(headers, payload.data);
448
+ const body = createBrowserDevHostNetworkBody(method, payload.data, headers);
441
449
  const controller = new AbortController();
450
+ const onRequestAbort = () => controller.abort(requestSignal.reason);
451
+ requestSignal.addEventListener('abort', onRequestAbort, { once: true });
452
+ if (requestSignal.aborted)
453
+ onRequestAbort();
442
454
  const timeout = Number(payload.timeout) > 0 ? Number(payload.timeout) : 10000;
443
455
  const timer = setTimeout(() => controller.abort(), timeout);
444
456
  const pinnedTargets = new Map();
445
457
  const dispatcher = createPinnedNetworkDispatcher(pinnedTargets);
446
458
  try {
447
- const response = await fetchMockNetworkWithRedirects({
459
+ const response = await fetchBrowserDevHostNetworkWithRedirects({
448
460
  body,
449
461
  dispatcher,
450
462
  fetchImpl,
451
463
  headers,
452
464
  lookupHostname,
465
+ allowedHttpOrigins,
453
466
  method,
454
467
  pinnedTargets,
455
468
  signal: controller.signal,
456
469
  url,
457
470
  });
458
471
  const contentType = response.headers.get('content-type') || '';
459
- const text = await readLimitedMockNetworkResponse(response);
472
+ const text = await readLimitedBrowserDevHostNetworkResponse(response);
460
473
  return {
461
474
  data: contentType.includes('application/json') ? safeJsonParse(text) : text,
462
475
  status: response.status,
463
476
  statusText: response.statusText,
464
- headers: Object.fromEntries(response.headers.entries()),
477
+ headers: Object.fromEntries([...response.headers.entries()].filter(([name]) => !isCredentialHeader(name))),
465
478
  };
466
479
  }
467
480
  catch (error) {
468
481
  if (isObjectLike(error) && typeof error.status === 'number') {
469
482
  throw error;
470
483
  }
471
- throw createMockHostError(502, 'MOCK_NETWORK_REQUEST_FAILED', error instanceof Error && error.name === 'AbortError' ? `network.request timeout after ${timeout}ms` : readErrorMessage(error));
484
+ throw createBrowserDevHostError(502, 'BROWSER_DEV_HOST_NETWORK_REQUEST_FAILED', error instanceof Error && error.name === 'AbortError' ? `network.request timeout after ${timeout}ms` : readErrorMessage(error));
472
485
  }
473
486
  finally {
487
+ requestSignal.removeEventListener('abort', onRequestAbort);
474
488
  clearTimeout(timer);
475
489
  await dispatcher.close();
476
490
  }
477
491
  }
478
- async function fetchMockNetworkWithRedirects(options) {
492
+ async function fetchBrowserDevHostNetworkWithRedirects(options) {
479
493
  let url = options.url;
480
494
  let method = options.method;
481
495
  let body = options.body;
482
- for (let redirectCount = 0; redirectCount <= MAX_MOCK_NETWORK_REDIRECTS; redirectCount += 1) {
483
- const pinnedTarget = await resolveMockNetworkTarget(url, options.lookupHostname);
496
+ for (let redirectCount = 0; redirectCount <= MAX_BROWSER_DEV_HOST_NETWORK_REDIRECTS; redirectCount += 1) {
497
+ const pinnedTarget = await resolveBrowserDevHostNetworkTarget(url, options.lookupHostname, options.allowedHttpOrigins);
484
498
  options.pinnedTargets.set(createPinnedNetworkTargetKey(url), pinnedTarget);
485
499
  const response = await options.fetchImpl(url.toString(), {
486
500
  body,
@@ -497,24 +511,31 @@ async function fetchMockNetworkWithRedirects(options) {
497
511
  if (!location) {
498
512
  return response;
499
513
  }
500
- if (redirectCount === MAX_MOCK_NETWORK_REDIRECTS) {
501
- throw createMockHostError(502, 'TOO_MANY_REDIRECTS', 'network.request redirect limit exceeded');
514
+ if (redirectCount === MAX_BROWSER_DEV_HOST_NETWORK_REDIRECTS) {
515
+ throw createBrowserDevHostError(502, 'TOO_MANY_REDIRECTS', 'network.request redirect limit exceeded');
502
516
  }
503
517
  await response.body?.cancel();
504
- url = new URL(location, url);
518
+ const redirectedUrl = new URL(location, url);
519
+ if (redirectedUrl.origin !== url.origin) {
520
+ throw createBrowserDevHostError(403, 'NETWORK_REDIRECT_DENIED', 'network.request 不允许跨 origin redirect');
521
+ }
522
+ url = redirectedUrl;
505
523
  if (response.status === 303 || ((response.status === 301 || response.status === 302) && method === 'POST')) {
506
524
  method = 'GET';
507
525
  body = undefined;
508
526
  }
509
527
  }
510
- throw createMockHostError(502, 'TOO_MANY_REDIRECTS', 'network.request redirect limit exceeded');
528
+ throw createBrowserDevHostError(502, 'TOO_MANY_REDIRECTS', 'network.request redirect limit exceeded');
511
529
  }
512
- async function resolveMockNetworkTarget(url, lookupHostname) {
530
+ async function resolveBrowserDevHostNetworkTarget(url, lookupHostname, allowedHttpOrigins) {
513
531
  if (!['http:', 'https:'].includes(url.protocol)) {
514
- throw createMockHostError(403, 'NETWORK_TARGET_DENIED', 'network.request redirect 仅支持 HTTP(S) URL');
532
+ throw createBrowserDevHostError(403, 'NETWORK_TARGET_DENIED', 'network.request redirect 仅支持 HTTP(S) URL');
515
533
  }
516
534
  if (url.username || url.password) {
517
- throw createMockHostError(403, 'NETWORK_TARGET_DENIED', 'network.request URL 不允许包含凭据');
535
+ throw createBrowserDevHostError(403, 'NETWORK_TARGET_DENIED', 'network.request URL 不允许包含凭据');
536
+ }
537
+ if (url.protocol === 'http:' && !allowedHttpOrigins.has(url.origin)) {
538
+ throw createBrowserDevHostError(403, 'NETWORK_TARGET_DENIED', 'network.request HTTP 仅允许已声明的本地开发 origin');
518
539
  }
519
540
  const hostname = normalizeNetworkHostname(url.hostname);
520
541
  const addressFamily = net.isIP(hostname);
@@ -522,8 +543,64 @@ async function resolveMockNetworkTarget(url, lookupHostname) {
522
543
  if (!addresses.length) {
523
544
  throw new Error('network.request target DNS lookup returned no addresses');
524
545
  }
546
+ if (url.protocol === 'https:' && addresses.some(({ address, family }) => isDeniedNetworkAddress(address, family))) {
547
+ throw createBrowserDevHostError(403, 'NETWORK_TARGET_DENIED', 'network.request 目标地址不允许访问');
548
+ }
525
549
  return addresses[0];
526
550
  }
551
+ function createDeniedNetworkTargets() {
552
+ const list = new net.BlockList();
553
+ for (const [network, prefix] of [
554
+ ['0.0.0.0', 8],
555
+ ['10.0.0.0', 8],
556
+ ['100.64.0.0', 10],
557
+ ['127.0.0.0', 8],
558
+ ['169.254.0.0', 16],
559
+ ['172.16.0.0', 12],
560
+ ['192.0.0.0', 24],
561
+ ['192.0.2.0', 24],
562
+ ['192.168.0.0', 16],
563
+ ['198.18.0.0', 15],
564
+ ['198.51.100.0', 24],
565
+ ['203.0.113.0', 24],
566
+ ['224.0.0.0', 4],
567
+ ['240.0.0.0', 4],
568
+ ]) {
569
+ list.addSubnet(network, prefix, 'ipv4');
570
+ }
571
+ for (const [network, prefix] of [
572
+ ['::', 128],
573
+ ['::1', 128],
574
+ ['64:ff9b::', 96],
575
+ ['64:ff9b:1::', 48],
576
+ ['2001::', 32],
577
+ ['2001:10::', 28],
578
+ ['2001:20::', 28],
579
+ ['2001:db8::', 32],
580
+ ['2002::', 16],
581
+ ['fc00::', 7],
582
+ ['fe80::', 10],
583
+ ['ff00::', 8],
584
+ ]) {
585
+ list.addSubnet(network, prefix, 'ipv6');
586
+ }
587
+ return list;
588
+ }
589
+ function isDeniedNetworkAddress(address, family) {
590
+ const normalized = normalizeNetworkHostname(address);
591
+ if (family === 4 || net.isIP(normalized) === 4)
592
+ return DENIED_NETWORK_TARGETS.check(normalized, 'ipv4');
593
+ if (family === 6 || net.isIP(normalized) === 6) {
594
+ if (normalized.toLowerCase().startsWith('::ffff:'))
595
+ return true;
596
+ return DENIED_NETWORK_TARGETS.check(normalized, 'ipv6');
597
+ }
598
+ return true;
599
+ }
600
+ function isCredentialHeader(name) {
601
+ const normalized = name.trim().toLowerCase();
602
+ return CREDENTIAL_HEADER_RE.test(normalized) || normalized.startsWith('x-heybox-') || normalized.startsWith('x-xhh-');
603
+ }
527
604
  function createPinnedNetworkDispatcher(pinnedTargets) {
528
605
  const connect = undici.buildConnector({});
529
606
  return new undici.Agent({
@@ -548,7 +625,7 @@ function createPinnedNetworkTargetKey(url) {
548
625
  function normalizeNetworkHostname(hostname) {
549
626
  return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
550
627
  }
551
- async function readLimitedMockNetworkResponse(response) {
628
+ async function readLimitedBrowserDevHostNetworkResponse(response) {
552
629
  if (!response.body) {
553
630
  return '';
554
631
  }
@@ -561,24 +638,24 @@ async function readLimitedMockNetworkResponse(response) {
561
638
  break;
562
639
  }
563
640
  size += value.byteLength;
564
- if (size > MAX_MOCK_NETWORK_RESPONSE_BODY) {
641
+ if (size > MAX_BROWSER_DEV_HOST_NETWORK_RESPONSE_BODY) {
565
642
  await reader.cancel();
566
- throw createMockHostError(502, 'NETWORK_RESPONSE_TOO_LARGE', 'network.request response body too large');
643
+ throw createBrowserDevHostError(502, 'NETWORK_RESPONSE_TOO_LARGE', 'network.request response body too large');
567
644
  }
568
645
  chunks.push(value);
569
646
  }
570
647
  return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8');
571
648
  }
572
- function createMockNetworkUrl(input, params) {
649
+ function createBrowserDevHostNetworkUrl(input, params) {
573
650
  let url;
574
651
  try {
575
652
  url = new URL(input);
576
653
  }
577
654
  catch {
578
- throw createMockHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是合法 URL');
655
+ throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是合法 URL');
579
656
  }
580
657
  if (!['http:', 'https:'].includes(url.protocol)) {
581
- throw createMockHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request 仅支持 HTTP(S) URL');
658
+ throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request 仅支持 HTTP(S) URL');
582
659
  }
583
660
  if (isRecord$1(params)) {
584
661
  Object.entries(params).forEach(([key, value]) => {
@@ -589,25 +666,25 @@ function createMockNetworkUrl(input, params) {
589
666
  }
590
667
  return url;
591
668
  }
592
- function normalizeMockNetworkHeaders(headers) {
669
+ function normalizeBrowserDevHostNetworkHeaders(headers) {
593
670
  const normalized = new Headers();
594
671
  if (!isRecord$1(headers)) {
595
672
  return normalized;
596
673
  }
597
674
  Object.entries(headers).forEach(([key, value]) => {
598
- if (isSensitiveMockNetworkHeader(key)) {
599
- throw createMockHostError(400, 'INVALID_NETWORK_REQUEST', `network.request 不允许设置敏感请求头: ${key}`);
675
+ if (isSensitiveBrowserDevHostNetworkHeader(key)) {
676
+ throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', `network.request 不允许设置敏感请求头: ${key}`);
600
677
  }
601
- if (typeof value === 'string' && !isBlockedMockNetworkHeader(key)) {
678
+ if (typeof value === 'string' && !isBlockedBrowserDevHostNetworkHeader(key)) {
602
679
  normalized.set(key, value);
603
680
  }
604
681
  });
605
682
  return normalized;
606
683
  }
607
- function isSensitiveMockNetworkHeader(key) {
684
+ function isSensitiveBrowserDevHostNetworkHeader(key) {
608
685
  return ['authorization', 'cookie', 'proxy-authorization'].includes(key.toLowerCase());
609
686
  }
610
- function isBlockedMockNetworkHeader(key) {
687
+ function isBlockedBrowserDevHostNetworkHeader(key) {
611
688
  return [
612
689
  'connection',
613
690
  'content-length',
@@ -624,10 +701,10 @@ function isBlockedMockNetworkHeader(key) {
624
701
  /**
625
702
  * 与 App Host 对齐:Mock 不再放行 multipart,避免“本地成功、上线失败”。
626
703
  */
627
- function assertSupportedMockNetworkRequestShape(headers, data) {
704
+ function assertSupportedBrowserDevHostNetworkRequestShape(headers, data) {
628
705
  const contentType = headers.get('content-type') || '';
629
706
  if (contentType.toLowerCase().includes('multipart/form-data') || looksLikeMultipartBody(data)) {
630
- throw createMockHostError(400, 'INVALID_NETWORK_REQUEST', MOCK_NETWORK_MULTIPART_UNSUPPORTED_MESSAGE);
707
+ throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', BROWSER_DEV_HOST_NETWORK_MULTIPART_UNSUPPORTED_MESSAGE);
631
708
  }
632
709
  }
633
710
  function looksLikeMultipartBody(data) {
@@ -637,13 +714,13 @@ function looksLikeMultipartBody(data) {
637
714
  const trimmed = data.trimStart();
638
715
  return trimmed.startsWith('--') && /content-disposition\s*:\s*form-data/i.test(data);
639
716
  }
640
- function createMockNetworkBody(method, data, headers) {
717
+ function createBrowserDevHostNetworkBody(method, data, headers) {
641
718
  if (data === undefined || method === 'GET' || method === 'HEAD') {
642
719
  return undefined;
643
720
  }
644
721
  const body = typeof data === 'string' ? data : JSON.stringify(data);
645
- if (Buffer.byteLength(body) > MAX_MOCK_NETWORK_REQUEST_BODY) {
646
- throw createMockHostError(413, 'REQUEST_ENTITY_TOO_LARGE', 'network.request data exceeds 2097152 bytes');
722
+ if (Buffer.byteLength(body) > MAX_BROWSER_DEV_HOST_NETWORK_REQUEST_BODY) {
723
+ throw createBrowserDevHostError(413, 'REQUEST_ENTITY_TOO_LARGE', 'network.request data exceeds 2097152 bytes');
647
724
  }
648
725
  if (typeof data === 'string') {
649
726
  return body;
@@ -691,7 +768,7 @@ function createDevSessionRoutes(options) {
691
768
  reapExpiredDevSessions(devSessions, expiredDevSessions, options.now());
692
769
  revokeDevSessionForOrigin(devSessions, validated.miniUrlOrigin);
693
770
  if (devSessions.size >= MAX_ACTIVE_DEV_SESSIONS) {
694
- throw createMockHostError(429, 'DEV_SESSION_LIMIT_REACHED', 'active Dev Session limit reached');
771
+ throw createBrowserDevHostError(429, 'DEV_SESSION_LIMIT_REACHED', 'active Dev Session limit reached');
695
772
  }
696
773
  const expiresAt = options.now() + DEV_SESSION_TTL_MS;
697
774
  const token = createDevSessionToken(devSessions);
@@ -712,7 +789,7 @@ function createDevSessionRoutes(options) {
712
789
  writeJsonResponse(response, 201, result);
713
790
  }
714
791
  catch (error) {
715
- const status = readMockHostErrorStatus(error);
792
+ const status = readBrowserDevHostErrorStatus(error);
716
793
  writeJsonResponse(response, status === 500 ? 400 : status, {
717
794
  code: 'INVALID_DEV_SESSION',
718
795
  message: readErrorMessage(error),
@@ -802,7 +879,10 @@ async function handleTokenNetworkProxy(request, response, token, runtime) {
802
879
  });
803
880
  return;
804
881
  }
805
- await handleMockNetworkProxy(request, response, runtime.fetchImpl, runtime.lookupHostname, true);
882
+ await handleBrowserDevHostNetworkProxy(request, response, runtime.fetchImpl, runtime.lookupHostname, {
883
+ allowedHttpOrigins: new Set([resolved.session.miniUrlOrigin]),
884
+ cors: true,
885
+ });
806
886
  }
807
887
  function resolveDevSession(token, devSessions, expiredDevSessions, now) {
808
888
  const session = devSessions.get(token);
@@ -866,6 +946,355 @@ function writeDevSessionCorsResponse(response, status, serializedBody) {
866
946
  response.end(serializedBody ?? '');
867
947
  }
868
948
 
949
+ const LOGIN_CLIENT = 'hb-sdk-browser-dev-host';
950
+ const LOGIN_ENVIRONMENT = 'development';
951
+ const LOGIN_CALLBACK_PATH = '/session/callback';
952
+ const SESSION_COOKIE_NAME = 'hb_sdk_browser_dev_session';
953
+ const LOGIN_INTENT_TTL_MS = 5 * 60 * 1000;
954
+ const SESSION_TTL_MS = 8 * 60 * 60 * 1000;
955
+ class BrowserDevSessionInvalidatedError extends Error {
956
+ code = 'BROWSER_DEV_SESSION_INVALIDATED';
957
+ constructor() {
958
+ super('Browser Dev Host session is no longer valid');
959
+ this.name = 'BrowserDevSessionInvalidatedError';
960
+ }
961
+ }
962
+ class BrowserDevSessionError extends Error {
963
+ status;
964
+ code;
965
+ constructor(status, code, message) {
966
+ super(message);
967
+ this.status = status;
968
+ this.code = code;
969
+ this.name = 'BrowserDevSessionError';
970
+ }
971
+ }
972
+ function createBrowserDevHostSessionController(options = {}) {
973
+ const now = options.now ?? Date.now;
974
+ const randomBytes = options.randomBytes ?? node_crypto.randomBytes;
975
+ const loginIntents = new Map();
976
+ const sessions = new Map();
977
+ const revisionListeners = new Set();
978
+ let revision = 0;
979
+ return {
980
+ csrfToken: createOpaqueValue(randomBytes, 32),
981
+ async createLoginIntent(input) {
982
+ const service = options.loginService;
983
+ if (!service) {
984
+ throw new BrowserDevSessionError(503, 'LOGIN_SERVICE_UNAVAILABLE', 'Browser Dev Host login service is unavailable');
985
+ }
986
+ reapExpiredState();
987
+ const state = createUniqueValue(loginIntents, randomBytes);
988
+ const verifier = createOpaqueValue(randomBytes, 32);
989
+ const redirectUri = `${normalizeOrigin(input.origin)}${LOGIN_CALLBACK_PATH}`;
990
+ const returnPath = normalizeReturnPath(input.returnPath);
991
+ const codeChallenge = node_crypto.createHash('sha256').update(verifier).digest('base64url');
992
+ loginIntents.set(state, {
993
+ expiresAt: now() + LOGIN_INTENT_TTL_MS,
994
+ redirectUri,
995
+ returnPath,
996
+ verifier,
997
+ });
998
+ try {
999
+ const result = await service.createAuthorizationUrl({
1000
+ client: LOGIN_CLIENT,
1001
+ codeChallenge,
1002
+ codeChallengeMethod: 'S256',
1003
+ environment: LOGIN_ENVIRONMENT,
1004
+ redirectUri,
1005
+ state,
1006
+ });
1007
+ const authorizationUrl = normalizeAuthorizationUrl(result.authorizationUrl, {
1008
+ codeChallenge,
1009
+ redirectUri,
1010
+ state,
1011
+ });
1012
+ return { authorizationUrl, state };
1013
+ }
1014
+ catch (error) {
1015
+ loginIntents.delete(state);
1016
+ if (error instanceof BrowserDevSessionError)
1017
+ throw error;
1018
+ throw new BrowserDevSessionError(502, 'LOGIN_SERVICE_ERROR', 'Browser Dev Host login service failed');
1019
+ }
1020
+ },
1021
+ async completeLogin(input) {
1022
+ const service = options.loginService;
1023
+ if (!service) {
1024
+ throw new BrowserDevSessionError(503, 'LOGIN_SERVICE_UNAVAILABLE', 'Browser Dev Host login service is unavailable');
1025
+ }
1026
+ reapExpiredState();
1027
+ const intent = loginIntents.get(input.state);
1028
+ if (!intent || intent.redirectUri !== `${normalizeOrigin(input.origin)}${LOGIN_CALLBACK_PATH}`) {
1029
+ throw new BrowserDevSessionError(400, 'INVALID_LOGIN_STATE', 'Browser Dev Host login state is invalid');
1030
+ }
1031
+ loginIntents.delete(input.state);
1032
+ if (!input.code || input.code.length > 4096) {
1033
+ throw new BrowserDevSessionError(400, 'INVALID_LOGIN_CODE', 'Browser Dev Host login code is invalid');
1034
+ }
1035
+ let exchanged;
1036
+ try {
1037
+ exchanged = await service.exchangeCode({
1038
+ client: LOGIN_CLIENT,
1039
+ code: input.code,
1040
+ codeVerifier: intent.verifier,
1041
+ environment: LOGIN_ENVIRONMENT,
1042
+ redirectUri: intent.redirectUri,
1043
+ });
1044
+ }
1045
+ catch {
1046
+ throw new BrowserDevSessionError(502, 'LOGIN_EXCHANGE_FAILED', 'Browser Dev Host login exchange failed');
1047
+ }
1048
+ const userId = normalizeUserId(exchanged.userId);
1049
+ const credential = normalizeCredential(exchanged.credential);
1050
+ const sessionId = createUniqueValue(sessions, randomBytes);
1051
+ const expiresAt = now() + SESSION_TTL_MS;
1052
+ const expiryTimer = setTimeout(() => {
1053
+ if (sessions.delete(sessionId))
1054
+ broadcastRevision(revisionListeners, ++revision);
1055
+ }, Math.max(0, expiresAt - now()));
1056
+ expiryTimer.unref?.();
1057
+ sessions.set(sessionId, {
1058
+ credential,
1059
+ expiresAt,
1060
+ expiryTimer,
1061
+ userId,
1062
+ });
1063
+ broadcastRevision(revisionListeners, ++revision);
1064
+ return {
1065
+ redirectPath: intent.returnPath,
1066
+ setCookie: createSessionCookie(sessionId),
1067
+ };
1068
+ },
1069
+ readStatus(cookieHeader) {
1070
+ reapExpiredState();
1071
+ const session = readSession(cookieHeader, sessions);
1072
+ return session ? { authenticated: true, revision, userId: session.userId } : { authenticated: false, revision };
1073
+ },
1074
+ logout(cookieHeader) {
1075
+ const sessionId = readSessionId(cookieHeader);
1076
+ const session = sessionId ? sessions.get(sessionId) : undefined;
1077
+ if (sessionId && session) {
1078
+ clearTimeout(session.expiryTimer);
1079
+ sessions.delete(sessionId);
1080
+ broadcastRevision(revisionListeners, ++revision);
1081
+ }
1082
+ return { clearCookie: clearSessionCookie(), revision };
1083
+ },
1084
+ async request(cookieHeader, request, signal) {
1085
+ const service = options.loginService;
1086
+ const sessionId = readSessionId(cookieHeader);
1087
+ const session = sessionId ? sessions.get(sessionId) : undefined;
1088
+ if (!service || !session || now() >= session.expiresAt) {
1089
+ if (sessionId && sessions.delete(sessionId))
1090
+ broadcastRevision(revisionListeners, ++revision);
1091
+ throw new BrowserDevSessionError(401, 'SESSION_UNAUTHENTICATED', 'Browser Dev Host session is unauthenticated');
1092
+ }
1093
+ assertAuthenticatedRequest(request);
1094
+ try {
1095
+ const result = await service.request({ credential: session.credential, request, signal: signal ?? new AbortController().signal });
1096
+ if (containsCredentialOrSensitiveResponse(result, session.credential)) {
1097
+ clearTimeout(session.expiryTimer);
1098
+ sessions.delete(sessionId);
1099
+ broadcastRevision(revisionListeners, ++revision);
1100
+ throw new BrowserDevSessionError(502, 'CREDENTIAL_EGRESS_BLOCKED', 'Browser Dev Host blocked a sensitive response');
1101
+ }
1102
+ return result;
1103
+ }
1104
+ catch (error) {
1105
+ if (error instanceof BrowserDevSessionError)
1106
+ throw error;
1107
+ if (error instanceof BrowserDevSessionInvalidatedError || readErrorCode(error) === 'BROWSER_DEV_SESSION_INVALIDATED') {
1108
+ clearTimeout(session.expiryTimer);
1109
+ sessions.delete(sessionId);
1110
+ broadcastRevision(revisionListeners, ++revision);
1111
+ throw new BrowserDevSessionError(401, 'SESSION_INVALIDATED', 'Browser Dev Host session was invalidated');
1112
+ }
1113
+ throw new BrowserDevSessionError(502, 'AUTHENTICATED_REQUEST_FAILED', 'Browser Dev Host authenticated request failed');
1114
+ }
1115
+ },
1116
+ onAccountRevision(listener) {
1117
+ revisionListeners.add(listener);
1118
+ return () => revisionListeners.delete(listener);
1119
+ },
1120
+ dispose() {
1121
+ loginIntents.clear();
1122
+ for (const session of sessions.values())
1123
+ clearTimeout(session.expiryTimer);
1124
+ sessions.clear();
1125
+ revisionListeners.clear();
1126
+ },
1127
+ };
1128
+ function reapExpiredState() {
1129
+ const removedSession = reapExpired(loginIntents, sessions, now());
1130
+ if (removedSession)
1131
+ broadcastRevision(revisionListeners, ++revision);
1132
+ }
1133
+ }
1134
+ function createOpaqueValue(randomBytes, size) {
1135
+ return randomBytes(size).toString('base64url');
1136
+ }
1137
+ function createUniqueValue(map, randomBytes) {
1138
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1139
+ const value = createOpaqueValue(randomBytes, 32);
1140
+ if (!map.has(value))
1141
+ return value;
1142
+ }
1143
+ throw new BrowserDevSessionError(500, 'RANDOM_VALUE_COLLISION', 'Browser Dev Host could not allocate a random value');
1144
+ }
1145
+ function normalizeOrigin(value) {
1146
+ const url = new URL(value);
1147
+ if (url.origin !== value || url.protocol !== 'http:') {
1148
+ throw new BrowserDevSessionError(400, 'INVALID_LOGIN_ORIGIN', 'Browser Dev Host login origin is invalid');
1149
+ }
1150
+ return url.origin;
1151
+ }
1152
+ function normalizeReturnPath(value) {
1153
+ if (!value.startsWith('/') || value.startsWith('//') || value.includes('#') || /[\r\n]/.test(value)) {
1154
+ throw new BrowserDevSessionError(400, 'INVALID_RETURN_PATH', 'Browser Dev Host login return path is invalid');
1155
+ }
1156
+ const url = new URL(value, 'http://127.0.0.1');
1157
+ if (url.origin !== 'http://127.0.0.1' || url.pathname === LOGIN_CALLBACK_PATH) {
1158
+ throw new BrowserDevSessionError(400, 'INVALID_RETURN_PATH', 'Browser Dev Host login return path is invalid');
1159
+ }
1160
+ return `${url.pathname}${url.search}`;
1161
+ }
1162
+ function normalizeAuthorizationUrl(value, expected) {
1163
+ const url = new URL(value);
1164
+ if (url.protocol !== 'https:' ||
1165
+ [...url.searchParams.keys()].some(isSensitiveQueryKey) ||
1166
+ url.searchParams.get('state') !== expected.state ||
1167
+ url.searchParams.get('code_challenge') !== expected.codeChallenge ||
1168
+ url.searchParams.get('code_challenge_method') !== 'S256' ||
1169
+ url.searchParams.get('redirect_uri') !== expected.redirectUri ||
1170
+ url.searchParams.get('client_id') !== LOGIN_CLIENT ||
1171
+ url.searchParams.get('environment') !== LOGIN_ENVIRONMENT) {
1172
+ throw new BrowserDevSessionError(502, 'INVALID_AUTHORIZATION_URL', 'Browser Dev Host authorization URL is invalid');
1173
+ }
1174
+ return url.toString();
1175
+ }
1176
+ function isSensitiveQueryKey(key) {
1177
+ const normalized = key.toLowerCase().replaceAll('_', '-');
1178
+ return (normalized === 'credential' ||
1179
+ normalized === 'cookie' ||
1180
+ normalized === 'pkey' ||
1181
+ normalized === 'token' ||
1182
+ normalized.endsWith('-token') ||
1183
+ normalized === 'authorization');
1184
+ }
1185
+ function normalizeUserId(value) {
1186
+ const userId = typeof value === 'string' ? value.trim() : '';
1187
+ if (!userId || userId.length > 256) {
1188
+ throw new BrowserDevSessionError(502, 'INVALID_LOGIN_USER', 'Browser Dev Host login user is invalid');
1189
+ }
1190
+ return userId;
1191
+ }
1192
+ function createSessionCookie(sessionId) {
1193
+ return `${SESSION_COOKIE_NAME}=${sessionId}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}`;
1194
+ }
1195
+ function clearSessionCookie() {
1196
+ return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`;
1197
+ }
1198
+ function readSessionId(cookieHeader) {
1199
+ if (!cookieHeader)
1200
+ return undefined;
1201
+ for (const item of cookieHeader.split(';')) {
1202
+ const separator = item.indexOf('=');
1203
+ if (separator < 0 || item.slice(0, separator).trim() !== SESSION_COOKIE_NAME)
1204
+ continue;
1205
+ const value = item.slice(separator + 1).trim();
1206
+ return /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;
1207
+ }
1208
+ return undefined;
1209
+ }
1210
+ function readSession(cookieHeader, sessions) {
1211
+ const sessionId = readSessionId(cookieHeader);
1212
+ return sessionId ? sessions.get(sessionId) : undefined;
1213
+ }
1214
+ function reapExpired(loginIntents, sessions, now) {
1215
+ let removedSession = false;
1216
+ for (const [state, intent] of loginIntents)
1217
+ if (now >= intent.expiresAt)
1218
+ loginIntents.delete(state);
1219
+ for (const [sessionId, session] of sessions) {
1220
+ if (now >= session.expiresAt) {
1221
+ clearTimeout(session.expiryTimer);
1222
+ sessions.delete(sessionId);
1223
+ removedSession = true;
1224
+ }
1225
+ }
1226
+ return removedSession;
1227
+ }
1228
+ function assertAuthenticatedRequest(request) {
1229
+ if (!request || typeof request !== 'object' || !request.target || typeof request.target !== 'object') {
1230
+ throw new BrowserDevSessionError(400, 'INVALID_AUTHENTICATED_REQUEST', 'Browser Dev Host authenticated request is invalid');
1231
+ }
1232
+ const serialized = JSON.stringify(request);
1233
+ if (serialized === undefined || serialized.length > 2 * 1024 * 1024) {
1234
+ throw new BrowserDevSessionError(413, 'AUTHENTICATED_REQUEST_TOO_LARGE', 'Browser Dev Host authenticated request is too large');
1235
+ }
1236
+ if (containsSensitiveKey(request, new Set(), 0)) {
1237
+ throw new BrowserDevSessionError(400, 'INVALID_AUTHENTICATED_REQUEST', 'Browser Dev Host authenticated request is invalid');
1238
+ }
1239
+ }
1240
+ function containsCredentialOrSensitiveResponse(value, credential) {
1241
+ if (value === credential)
1242
+ return true;
1243
+ if (typeof credential === 'string' && credential.length >= 8) {
1244
+ try {
1245
+ if (JSON.stringify(value)?.includes(credential))
1246
+ return true;
1247
+ }
1248
+ catch {
1249
+ return true;
1250
+ }
1251
+ }
1252
+ return containsSensitiveKey(value, new Set(), 0);
1253
+ }
1254
+ function normalizeCredential(value) {
1255
+ if (typeof value !== 'string' || value.length < 16 || value.length > 16 * 1024) {
1256
+ throw new BrowserDevSessionError(502, 'INVALID_LOGIN_CREDENTIAL', 'Browser Dev Host login credential is invalid');
1257
+ }
1258
+ return value;
1259
+ }
1260
+ function containsSensitiveKey(value, seen, depth) {
1261
+ if (depth > 16 || value === null || typeof value !== 'object')
1262
+ return false;
1263
+ if (seen.has(value))
1264
+ return false;
1265
+ seen.add(value);
1266
+ if (Array.isArray(value))
1267
+ return value.some((item) => containsSensitiveKey(item, seen, depth + 1));
1268
+ for (const [key, child] of Object.entries(value)) {
1269
+ const normalized = key.toLowerCase().replaceAll('_', '-');
1270
+ if (normalized === 'credential' ||
1271
+ normalized === 'cookie' ||
1272
+ normalized === 'set-cookie' ||
1273
+ normalized === 'pkey' ||
1274
+ normalized === 'token' ||
1275
+ normalized.endsWith('-token') ||
1276
+ normalized === 'authorization') {
1277
+ return true;
1278
+ }
1279
+ if (containsSensitiveKey(child, seen, depth + 1))
1280
+ return true;
1281
+ }
1282
+ return false;
1283
+ }
1284
+ function readErrorCode(error) {
1285
+ return typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
1286
+ }
1287
+ function broadcastRevision(listeners, revision) {
1288
+ for (const listener of listeners) {
1289
+ try {
1290
+ listener(revision);
1291
+ }
1292
+ catch {
1293
+ // 观察者失败不能阻断 vault 失效。
1294
+ }
1295
+ }
1296
+ }
1297
+
869
1298
  async function selectAvailablePort(options) {
870
1299
  const maxAttempts = options.maxAttempts ?? 20;
871
1300
  const port = await options.getPort({
@@ -882,30 +1311,36 @@ function createPortCandidates(startPort, count) {
882
1311
  }
883
1312
 
884
1313
  const MINI_PROGRAM_URL_QUERY_PARAM$1 = 'mini_url';
885
- const MOCK_HOST_BOOTSTRAP_PATH = '/__hb_sdk_bootstrap__';
886
- const MOCK_NETWORK_PROXY_PATH = '/__hb_sdk_mock_network__';
1314
+ const BROWSER_DEV_HOST_BOOTSTRAP_PATH = '/__hb_sdk_bootstrap__';
1315
+ const BROWSER_DEV_HOST_NETWORK_PATH = '/__hb_sdk__/network';
887
1316
  const DEV_PERMISSIONS_PATH = '/__hb_sdk__/dev-permissions';
888
1317
  const DEV_LISTEN_HOST$1 = '0.0.0.0';
889
1318
  const LOCAL_DEV_URL_HOST$1 = '127.0.0.1';
890
- const MOCK_HOST_ROOT_CANDIDATES = [
891
- path.resolve(__dirname, 'devtools/mock-host'),
892
- path.resolve(__dirname, '../devtools/mock-host'),
893
- path.resolve(__dirname, '../../devtools/mock-host'),
894
- path.resolve(__dirname, '../../../devtools/mock-host'),
1319
+ const BROWSER_DEV_HOST_ROOT_CANDIDATES = [
1320
+ path.resolve(__dirname, 'devtools/browser-dev-host'),
1321
+ path.resolve(__dirname, '../devtools/browser-dev-host'),
1322
+ path.resolve(__dirname, '../../devtools/browser-dev-host'),
1323
+ path.resolve(__dirname, '../../../devtools/browser-dev-host'),
895
1324
  ];
896
- async function startMiniProgramMockHostServer(options) {
897
- const root = options.root ?? resolveMiniProgramMockHostRoot(options.rootCandidates);
898
- assertCompleteMiniProgramMockHostRoot(root);
1325
+ async function startBrowserDevHostServer(options) {
1326
+ const root = options.root ?? resolveBrowserDevHostRoot(options.rootCandidates);
1327
+ assertCompleteMiniProgramBrowserDevHostRoot(root);
1328
+ const sessionController = createBrowserDevHostSessionController({
1329
+ loginService: options.loginService,
1330
+ now: options.now,
1331
+ });
899
1332
  const bootstrap = {
1333
+ csrfToken: sessionController.csrfToken,
900
1334
  defaultLanAddressId: options.defaultLanAddressId,
901
1335
  devContext: options.devContext,
902
1336
  lanAddresses: options.lanAddresses ?? [],
1337
+ loginAvailable: options.loginService !== undefined,
903
1338
  macAppProtocol: options.macAppProtocol,
904
1339
  nativeAppLaunchUnavailableReason: options.nativeAppLaunchUnavailableReason,
905
1340
  runtimePermissions: options.runtimePermissions,
906
1341
  };
907
- let bootstrapJson = serializeMockHostBootstrap(bootstrap);
908
- const server = createMiniProgramMockHostServer(root, {
1342
+ let bootstrapJson = serializeBrowserDevHostBootstrap(bootstrap);
1343
+ const serverRuntime = createBrowserDevHostServer(root, {
909
1344
  appUrl: options.appUrl,
910
1345
  allowedMiniUrlOrigins: new Set((options.lanAddresses ?? []).map(({ appUrl }) => new URL(appUrl).origin)),
911
1346
  readBootstrapJson: () => bootstrapJson,
@@ -914,12 +1349,15 @@ async function startMiniProgramMockHostServer(options) {
914
1349
  lookupHostname: options.lookupHostname ?? (async (hostname) => promises.lookup(hostname, { all: true, verbatim: true })),
915
1350
  now: options.now ?? Date.now,
916
1351
  permissionController: options.permissionController,
1352
+ sessionController,
917
1353
  });
1354
+ const server = serverRuntime.server;
918
1355
  const port = await listenHttpServer(server, {
919
1356
  port: options.port,
920
1357
  }, options.getPort ?? getPorts);
921
1358
  return {
922
1359
  close: () => new Promise((resolve) => {
1360
+ serverRuntime.dispose();
923
1361
  server.close(() => resolve());
924
1362
  }),
925
1363
  updateBootstrap: (next) => {
@@ -931,56 +1369,57 @@ async function startMiniProgramMockHostServer(options) {
931
1369
  delete bootstrap.macAppProtocol;
932
1370
  else if (next.macAppProtocol !== undefined)
933
1371
  bootstrap.macAppProtocol = next.macAppProtocol;
934
- bootstrapJson = serializeMockHostBootstrap(bootstrap);
1372
+ bootstrapJson = serializeBrowserDevHostBootstrap(bootstrap);
935
1373
  },
936
- networkUrls: createMiniProgramMockHostNetworkUrls({
1374
+ networkUrls: createBrowserDevHostNetworkUrls({
1375
+ appUrl: options.appUrl,
937
1376
  lanAddresses: options.lanAddresses,
938
1377
  port,
939
1378
  }),
940
1379
  port,
941
1380
  root,
942
1381
  server,
943
- url: createMiniProgramMockHostUrlWithHost({
1382
+ url: createBrowserDevHostUrlWithHost({
944
1383
  appUrl: options.appUrl,
945
1384
  host: LOCAL_DEV_URL_HOST$1,
946
1385
  port,
947
1386
  }),
948
1387
  };
949
1388
  }
950
- function resolveMiniProgramMockHostRoot(candidates = MOCK_HOST_ROOT_CANDIDATES) {
951
- const found = candidates.find(isCompleteMiniProgramMockHostRoot);
1389
+ function resolveBrowserDevHostRoot(candidates = BROWSER_DEV_HOST_ROOT_CANDIDATES) {
1390
+ const found = candidates.find(isCompleteMiniProgramBrowserDevHostRoot);
952
1391
  if (!found) {
953
- throw createMissingMockHostError();
1392
+ throw createMissingBrowserDevHostError();
954
1393
  }
955
1394
  return found;
956
1395
  }
957
- function assertCompleteMiniProgramMockHostRoot(root) {
958
- if (!isCompleteMiniProgramMockHostRoot(root)) {
959
- throw createMissingMockHostError();
1396
+ function assertCompleteMiniProgramBrowserDevHostRoot(root) {
1397
+ if (!isCompleteMiniProgramBrowserDevHostRoot(root)) {
1398
+ throw createMissingBrowserDevHostError();
960
1399
  }
961
1400
  }
962
- function isCompleteMiniProgramMockHostRoot(root) {
1401
+ function isCompleteMiniProgramBrowserDevHostRoot(root) {
963
1402
  return fs.existsSync(path.join(root, 'index.html')) && fs.existsSync(path.join(root, 'main.js'));
964
1403
  }
965
- function createMissingMockHostError() {
966
- return new Error('未找到完整的 hb-sdk mock host 静态产物。请先执行 @heybox/hb-sdk 的 build:mock-host。');
1404
+ function createMissingBrowserDevHostError() {
1405
+ return new Error('未找到完整的 hb-sdk browser dev host 静态产物。请先执行 @heybox/hb-sdk 的 build:browser-dev-host。');
967
1406
  }
968
- function createMiniProgramMockHostNetworkUrls(options) {
969
- return (options.lanAddresses ?? []).map((address) => createMiniProgramMockHostUrlWithHost({
1407
+ function createBrowserDevHostNetworkUrls(options) {
1408
+ return (options.lanAddresses ?? []).map((address) => createBrowserDevHostUrlWithHost({
970
1409
  appUrl: address.appUrl,
971
1410
  host: address.address,
972
1411
  port: options.port,
973
1412
  }));
974
1413
  }
975
- function createMiniProgramMockHostUrlWithHost(options) {
1414
+ function createBrowserDevHostUrlWithHost(options) {
976
1415
  const url = new URL(`http://${options.host}:${options.port}/`);
977
1416
  url.searchParams.set(MINI_PROGRAM_URL_QUERY_PARAM$1, options.appUrl);
978
1417
  return url.toString();
979
1418
  }
980
- function serializeMockHostBootstrap(bootstrap) {
1419
+ function serializeBrowserDevHostBootstrap(bootstrap) {
981
1420
  const serialized = JSON.stringify(bootstrap);
982
1421
  if (serialized === undefined) {
983
- throw new TypeError('Mock host bootstrap must be JSON serializable');
1422
+ throw new TypeError('Browser Dev Host bootstrap must be JSON serializable');
984
1423
  }
985
1424
  const serializedBootstrap = JSON.parse(serialized);
986
1425
  if (bootstrap.runtimePermissions !== undefined && !Object.prototype.hasOwnProperty.call(serializedBootstrap, 'runtimePermissions')) {
@@ -988,24 +1427,49 @@ function serializeMockHostBootstrap(bootstrap) {
988
1427
  }
989
1428
  return serialized;
990
1429
  }
991
- function createMiniProgramMockHostServer(root, runtime) {
1430
+ function createBrowserDevHostServer(root, runtime) {
992
1431
  const devSessionRoutes = createDevSessionRoutes({
993
1432
  allowedMiniUrlOrigins: runtime.allowedMiniUrlOrigins,
994
1433
  fetchImpl: runtime.fetchImpl,
995
1434
  lookupHostname: runtime.lookupHostname,
996
1435
  now: runtime.now,
997
1436
  });
998
- return node_http.createServer(async (request, response) => {
999
- const requestUrl = new URL(request.url, 'http://localhost');
1000
- const pathname = decodeURIComponent(requestUrl.pathname);
1001
- const devSessionRoute = devSessionRoutes.match(pathname);
1437
+ const eventStreams = new Set();
1438
+ const unsubscribeRevision = runtime.sessionController.onAccountRevision((revision) => {
1439
+ const data = JSON.stringify({ revision });
1440
+ for (const stream of eventStreams)
1441
+ stream.write(`event: account-invalidated\ndata: ${data}\n\n`);
1442
+ });
1443
+ const server = node_http.createServer(async (request, response) => {
1002
1444
  const loopbackPeer = runtime.isLoopbackPeer(request.socket.remoteAddress);
1003
- // Dev Session 需要允许手机局域网访问;其余 mock 页仍限制本机。
1445
+ let requestUrl;
1446
+ try {
1447
+ requestUrl = new URL(request.url, 'http://localhost');
1448
+ }
1449
+ catch {
1450
+ writeBadRequest(response);
1451
+ return;
1452
+ }
1453
+ // LAN 只允许固定 token 路由;在任何 URI decode 前先完成窄匹配。
1454
+ if (!loopbackPeer && !devSessionRoutes.match(requestUrl.pathname)) {
1455
+ writeForbidden(response);
1456
+ return;
1457
+ }
1458
+ let pathname;
1459
+ try {
1460
+ pathname = decodeURIComponent(requestUrl.pathname);
1461
+ }
1462
+ catch {
1463
+ writeBadRequest(response);
1464
+ return;
1465
+ }
1466
+ const devSessionRoute = devSessionRoutes.match(pathname);
1467
+ // Dev Session 需要允许手机局域网访问;其余 Browser Dev Host 页面仍限制本机。
1004
1468
  if (!loopbackPeer && !devSessionRoute) {
1005
1469
  writeForbidden(response);
1006
1470
  return;
1007
1471
  }
1008
- if (isMockHostDocumentPath(pathname) && !requestUrl.searchParams.has(MINI_PROGRAM_URL_QUERY_PARAM$1)) {
1472
+ if (isBrowserDevHostDocumentPath(pathname) && !requestUrl.searchParams.has(MINI_PROGRAM_URL_QUERY_PARAM$1)) {
1009
1473
  const targetUrl = new URL(requestUrl);
1010
1474
  targetUrl.pathname = '/';
1011
1475
  targetUrl.searchParams.set(MINI_PROGRAM_URL_QUERY_PARAM$1, runtime.appUrl);
@@ -1016,7 +1480,7 @@ function createMiniProgramMockHostServer(root, runtime) {
1016
1480
  response.end();
1017
1481
  return;
1018
1482
  }
1019
- if (pathname === MOCK_HOST_BOOTSTRAP_PATH) {
1483
+ if (pathname === BROWSER_DEV_HOST_BOOTSTRAP_PATH) {
1020
1484
  if (!loopbackPeer || !hasNumericLoopbackHost(request)) {
1021
1485
  writeForbidden(response);
1022
1486
  return;
@@ -1028,6 +1492,95 @@ function createMiniProgramMockHostServer(root, runtime) {
1028
1492
  writeSerializedJsonResponse(response, 200, runtime.readBootstrapJson());
1029
1493
  return;
1030
1494
  }
1495
+ if (pathname === '/session/status') {
1496
+ if (!loopbackPeer || !hasNumericLoopbackHost(request)) {
1497
+ writeForbidden(response);
1498
+ return;
1499
+ }
1500
+ if (request.method !== 'GET') {
1501
+ writeMethodNotAllowed(response, 'GET');
1502
+ return;
1503
+ }
1504
+ const status = runtime.sessionController.readStatus(request.headers.cookie);
1505
+ writeJsonResponse(response, 200, {
1506
+ authenticated: status.authenticated,
1507
+ revision: status.revision,
1508
+ ...(status.authenticated ? { user_id: status.userId } : {}),
1509
+ });
1510
+ return;
1511
+ }
1512
+ if (pathname === '/session/login-intent') {
1513
+ if (!isValidSensitiveRequest(request, loopbackPeer, runtime.sessionController.csrfToken) || request.method !== 'POST') {
1514
+ if (request.method !== 'POST')
1515
+ writeMethodNotAllowed(response, 'POST');
1516
+ else
1517
+ writeForbidden(response);
1518
+ return;
1519
+ }
1520
+ await handleLoginIntent(request, response, runtime.sessionController);
1521
+ return;
1522
+ }
1523
+ if (pathname === '/session/callback') {
1524
+ if (!loopbackPeer || !hasNumericLoopbackHost(request)) {
1525
+ writeForbidden(response);
1526
+ return;
1527
+ }
1528
+ if (request.method !== 'GET') {
1529
+ writeMethodNotAllowed(response, 'GET');
1530
+ return;
1531
+ }
1532
+ await handleLoginCallback(request, response, requestUrl, runtime.sessionController);
1533
+ return;
1534
+ }
1535
+ if (pathname === '/session/logout') {
1536
+ if (!isValidSensitiveRequest(request, loopbackPeer, runtime.sessionController.csrfToken) || request.method !== 'POST') {
1537
+ if (request.method !== 'POST')
1538
+ writeMethodNotAllowed(response, 'POST');
1539
+ else
1540
+ writeForbidden(response);
1541
+ return;
1542
+ }
1543
+ const result = runtime.sessionController.logout(request.headers.cookie);
1544
+ response.setHeader('set-cookie', result.clearCookie);
1545
+ writeJsonResponse(response, 200, { revision: result.revision });
1546
+ return;
1547
+ }
1548
+ if (pathname === '/session/request') {
1549
+ if (!isValidSensitiveRequest(request, loopbackPeer, runtime.sessionController.csrfToken) || request.method !== 'POST') {
1550
+ if (request.method !== 'POST')
1551
+ writeMethodNotAllowed(response, 'POST');
1552
+ else
1553
+ writeForbidden(response);
1554
+ return;
1555
+ }
1556
+ await handleAuthenticatedRequest(request, response, runtime.sessionController);
1557
+ return;
1558
+ }
1559
+ if (pathname === '/session/events') {
1560
+ if (!loopbackPeer || !hasNumericLoopbackHost(request)) {
1561
+ writeForbidden(response);
1562
+ return;
1563
+ }
1564
+ if (request.method !== 'GET') {
1565
+ writeMethodNotAllowed(response, 'GET');
1566
+ return;
1567
+ }
1568
+ const status = runtime.sessionController.readStatus(request.headers.cookie);
1569
+ if (!status.authenticated) {
1570
+ writeJsonResponse(response, 401, { code: 'SESSION_UNAUTHENTICATED' });
1571
+ return;
1572
+ }
1573
+ response.writeHead(200, {
1574
+ 'cache-control': 'no-store',
1575
+ connection: 'keep-alive',
1576
+ 'content-type': 'text/event-stream; charset=utf-8',
1577
+ 'x-accel-buffering': 'no',
1578
+ });
1579
+ response.write(`event: account-revision\ndata: ${JSON.stringify({ revision: status.revision })}\n\n`);
1580
+ eventStreams.add(response);
1581
+ request.once('close', () => eventStreams.delete(response));
1582
+ return;
1583
+ }
1031
1584
  if (pathname === DEV_PERMISSIONS_PATH && runtime.permissionController) {
1032
1585
  if (!loopbackPeer || !hasNumericLoopbackHost(request)) {
1033
1586
  writeForbidden(response);
@@ -1037,7 +1590,9 @@ function createMiniProgramMockHostServer(root, runtime) {
1037
1590
  writeJsonResponse(response, 200, await runtime.permissionController.get());
1038
1591
  return;
1039
1592
  }
1040
- if ((request.method !== 'PUT' && request.method !== 'DELETE') || !hasValidMutationOrigin(request)) {
1593
+ if ((request.method !== 'PUT' && request.method !== 'DELETE') ||
1594
+ !hasValidMutationOrigin(request) ||
1595
+ !hasValidCsrf(request, runtime.sessionController.csrfToken)) {
1041
1596
  if (request.method !== 'PUT' && request.method !== 'DELETE') {
1042
1597
  writeMethodNotAllowed(response, 'GET, PUT, DELETE');
1043
1598
  }
@@ -1050,14 +1605,17 @@ function createMiniProgramMockHostServer(root, runtime) {
1050
1605
  return;
1051
1606
  }
1052
1607
  if (pathname === MINI_PROGRAM_DEV_CONTEXT_PATH) {
1053
- if (!loopbackPeer || !hasNumericLoopbackHost(request) || !hasValidMutationOrigin(request)) {
1054
- writeForbidden(response);
1055
- return;
1056
- }
1057
1608
  if (request.method !== 'POST') {
1058
1609
  writeMethodNotAllowed(response, 'POST');
1059
1610
  return;
1060
1611
  }
1612
+ if (!loopbackPeer ||
1613
+ !hasNumericLoopbackHost(request) ||
1614
+ !hasValidMutationOrigin(request) ||
1615
+ !hasValidCsrf(request, runtime.sessionController.csrfToken)) {
1616
+ writeForbidden(response);
1617
+ return;
1618
+ }
1061
1619
  await devSessionRoutes.create(request, response);
1062
1620
  return;
1063
1621
  }
@@ -1065,12 +1623,15 @@ function createMiniProgramMockHostServer(root, runtime) {
1065
1623
  await devSessionRoutes.handle(devSessionRoute, request, response);
1066
1624
  return;
1067
1625
  }
1068
- if (pathname === MOCK_NETWORK_PROXY_PATH) {
1069
- if (!loopbackPeer || !hasNumericLoopbackHost(request) || !hasValidMutationOrigin(request)) {
1626
+ if (pathname === BROWSER_DEV_HOST_NETWORK_PATH) {
1627
+ const csrfValid = request.method !== 'POST' || hasValidCsrf(request, runtime.sessionController.csrfToken);
1628
+ if (!loopbackPeer || !hasNumericLoopbackHost(request) || !hasValidMutationOrigin(request) || !csrfValid) {
1070
1629
  writeForbidden(response);
1071
1630
  return;
1072
1631
  }
1073
- await handleMockNetworkProxy(request, response, runtime.fetchImpl, runtime.lookupHostname);
1632
+ await handleBrowserDevHostNetworkProxy(request, response, runtime.fetchImpl, runtime.lookupHostname, {
1633
+ allowedHttpOrigins: new Set([runtime.appUrl, ...runtime.allowedMiniUrlOrigins].map((value) => new URL(value).origin)),
1634
+ });
1074
1635
  return;
1075
1636
  }
1076
1637
  const targetPath = resolveStaticPath(root, pathname);
@@ -1097,38 +1658,131 @@ function createMiniProgramMockHostServer(root, runtime) {
1097
1658
  response.end('Not found');
1098
1659
  }
1099
1660
  });
1661
+ return {
1662
+ server,
1663
+ dispose() {
1664
+ unsubscribeRevision();
1665
+ for (const stream of eventStreams)
1666
+ stream.end();
1667
+ eventStreams.clear();
1668
+ runtime.sessionController.dispose();
1669
+ },
1670
+ };
1671
+ }
1672
+ async function handleLoginIntent(request, response, controller) {
1673
+ try {
1674
+ const payload = await readJsonRequestBody(request);
1675
+ if (!isRecord$1(payload) || Object.keys(payload).some((key) => key !== 'return_path') || typeof payload.return_path !== 'string') {
1676
+ throw new BrowserDevSessionError(400, 'INVALID_LOGIN_INTENT', 'Browser Dev Host login intent is invalid');
1677
+ }
1678
+ const result = await controller.createLoginIntent({
1679
+ origin: readRequestOrigin(request),
1680
+ returnPath: payload.return_path,
1681
+ });
1682
+ writeJsonResponse(response, 201, { authorization_url: result.authorizationUrl });
1683
+ }
1684
+ catch (error) {
1685
+ writeSessionError(response, error);
1686
+ }
1687
+ }
1688
+ async function handleLoginCallback(request, response, requestUrl, controller) {
1689
+ try {
1690
+ const keys = [...requestUrl.searchParams.keys()];
1691
+ if (keys.some((key) => key !== 'code' && key !== 'state')) {
1692
+ throw new BrowserDevSessionError(400, 'INVALID_LOGIN_CALLBACK', 'Browser Dev Host login callback is invalid');
1693
+ }
1694
+ const code = requestUrl.searchParams.get('code');
1695
+ const state = requestUrl.searchParams.get('state');
1696
+ if (!code || !state) {
1697
+ throw new BrowserDevSessionError(400, 'INVALID_LOGIN_CALLBACK', 'Browser Dev Host login callback is invalid');
1698
+ }
1699
+ const result = await controller.completeLogin({
1700
+ code,
1701
+ origin: readRequestOrigin(request),
1702
+ state,
1703
+ });
1704
+ response.writeHead(302, {
1705
+ 'cache-control': 'no-store',
1706
+ location: result.redirectPath,
1707
+ 'referrer-policy': 'no-referrer',
1708
+ 'set-cookie': result.setCookie,
1709
+ });
1710
+ response.end();
1711
+ }
1712
+ catch (error) {
1713
+ response.writeHead(302, {
1714
+ 'cache-control': 'no-store',
1715
+ location: '/',
1716
+ 'referrer-policy': 'no-referrer',
1717
+ });
1718
+ response.end();
1719
+ }
1720
+ }
1721
+ async function handleAuthenticatedRequest(request, response, controller) {
1722
+ const abort = new AbortController();
1723
+ request.once('aborted', () => abort.abort());
1724
+ try {
1725
+ const payload = await readJsonRequestBody(request);
1726
+ const result = await controller.request(request.headers.cookie, payload, abort.signal);
1727
+ writeJsonResponse(response, 200, result);
1728
+ }
1729
+ catch (error) {
1730
+ writeSessionError(response, error);
1731
+ }
1732
+ }
1733
+ function writeSessionError(response, error) {
1734
+ const sessionError = error instanceof BrowserDevSessionError
1735
+ ? error
1736
+ : new BrowserDevSessionError(500, 'BROWSER_DEV_SESSION_ERROR', 'Browser Dev Host session request failed');
1737
+ writeJsonResponse(response, sessionError.status, {
1738
+ code: sessionError.code,
1739
+ message: sessionError.message,
1740
+ });
1741
+ }
1742
+ function readRequestOrigin(request) {
1743
+ const host = request.headers.host;
1744
+ if (!host)
1745
+ throw new BrowserDevSessionError(400, 'INVALID_LOGIN_ORIGIN', 'Browser Dev Host login origin is invalid');
1746
+ return `http://${host}`;
1747
+ }
1748
+ function isValidSensitiveRequest(request, loopbackPeer, csrfToken) {
1749
+ return loopbackPeer && hasNumericLoopbackHost(request) && hasValidMutationOrigin(request) && hasValidCsrf(request, csrfToken);
1750
+ }
1751
+ function hasValidCsrf(request, csrfToken) {
1752
+ const value = request.headers['x-hb-sdk-csrf'];
1753
+ return typeof value === 'string' && value === csrfToken;
1100
1754
  }
1101
1755
  async function handleDevPermissionMutation(request, response, controller) {
1102
1756
  try {
1103
1757
  if (request.method === 'DELETE') {
1104
1758
  if (hasRequestBody(request)) {
1105
1759
  if (!request.headers['content-type']?.toLowerCase().startsWith('application/json')) {
1106
- throw createMockHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions body must be JSON');
1760
+ throw createBrowserDevHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions body must be JSON');
1107
1761
  }
1108
1762
  const payload = await readJsonRequestBody(request);
1109
1763
  if (!isRecord$1(payload) || Object.keys(payload).length > 0) {
1110
- throw createMockHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions reset body is invalid');
1764
+ throw createBrowserDevHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions reset body is invalid');
1111
1765
  }
1112
1766
  }
1113
1767
  writeJsonResponse(response, 200, await controller.reset());
1114
1768
  return;
1115
1769
  }
1116
1770
  if (!request.headers['content-type']?.toLowerCase().startsWith('application/json')) {
1117
- throw createMockHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions body must be JSON');
1771
+ throw createBrowserDevHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions body must be JSON');
1118
1772
  }
1119
1773
  const payload = await readJsonRequestBody(request);
1120
1774
  if (!isRecord$1(payload) || Object.keys(payload).length !== 1 || !Object.hasOwn(payload, 'runtime_permissions')) {
1121
- throw createMockHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions update body is invalid');
1775
+ throw createBrowserDevHostError(400, 'INVALID_DEV_PERMISSIONS', 'Dev permissions update body is invalid');
1122
1776
  }
1123
1777
  const parsed = context.parseMiniProgramRuntimePermissions(payload.runtime_permissions);
1124
1778
  if (!parsed.valid) {
1125
- throw createMockHostError(400, 'INVALID_DEV_PERMISSIONS', 'runtime_permissions is invalid');
1779
+ throw createBrowserDevHostError(400, 'INVALID_DEV_PERMISSIONS', 'runtime_permissions is invalid');
1126
1780
  }
1127
1781
  const runtimePermissions = JSON.parse(JSON.stringify(payload.runtime_permissions));
1128
1782
  writeJsonResponse(response, 200, await controller.save(runtimePermissions));
1129
1783
  }
1130
1784
  catch (error) {
1131
- const errorStatus = readMockHostErrorStatus(error);
1785
+ const errorStatus = readBrowserDevHostErrorStatus(error);
1132
1786
  const status = request.method === 'DELETE' && errorStatus === 500 ? 502 : errorStatus;
1133
1787
  writeJsonResponse(response, status, {
1134
1788
  code: request.method === 'DELETE' ? 'DEV_PERMISSIONS_RESET_FAILED' : 'DEV_PERMISSIONS_UPDATE_FAILED',
@@ -1150,6 +1804,10 @@ function writeForbidden(response) {
1150
1804
  response.writeHead(403, { 'cache-control': 'no-store' });
1151
1805
  response.end();
1152
1806
  }
1807
+ function writeBadRequest(response) {
1808
+ response.writeHead(400, { 'cache-control': 'no-store' });
1809
+ response.end();
1810
+ }
1153
1811
  function hasValidMutationOrigin(request) {
1154
1812
  const origin = request.headers.origin;
1155
1813
  const host = request.headers.host;
@@ -1180,7 +1838,7 @@ function hasNumericLoopbackHost(request) {
1180
1838
  function isLoopbackAddress(address) {
1181
1839
  return address === '::1' || address === '127.0.0.1' || address?.startsWith('127.') === true || address?.startsWith('::ffff:127.') === true;
1182
1840
  }
1183
- function isMockHostDocumentPath(pathname) {
1841
+ function isBrowserDevHostDocumentPath(pathname) {
1184
1842
  return pathname === '/' || pathname === '/index.html';
1185
1843
  }
1186
1844
  function resolveStaticPath(root, pathname) {
@@ -1214,7 +1872,7 @@ async function listenHttpServer(server, options, getPortImpl) {
1214
1872
  const port = await selectAvailablePort({
1215
1873
  getPort: getPortImpl,
1216
1874
  host: DEV_LISTEN_HOST$1,
1217
- label: 'mock host',
1875
+ label: 'browser dev host',
1218
1876
  startPort: options.port,
1219
1877
  });
1220
1878
  await new Promise((resolve, reject) => {
@@ -1347,7 +2005,7 @@ function createHeyboxProtocol(payload) {
1347
2005
  /** 构建时替换为当前发布包的实际版本。 */
1348
2006
  const HB_SDK_VERSION = typeof undefined === 'string'
1349
2007
  ? undefined
1350
- : '0.7.3';
2008
+ : '0.7.4-alpha.4';
1351
2009
 
1352
2010
  class DevPermissionOverrideStoreError extends Error {
1353
2011
  code;
@@ -1470,7 +2128,7 @@ function isNodeError(error) {
1470
2128
  }
1471
2129
 
1472
2130
  const DEFAULT_APP_PORT = 5173;
1473
- const DEFAULT_MOCK_PORT = 5174;
2131
+ const DEFAULT_BROWSER_DEV_HOST_PORT = 5174;
1474
2132
  const DEFAULT_DEV_CONTEXT_TIMEOUT_MS = 3000;
1475
2133
  const DEV_LISTEN_HOST = '0.0.0.0';
1476
2134
  const LOCAL_DEV_URL_HOST = '127.0.0.1';
@@ -1537,9 +2195,9 @@ async function runDevCommand(options, runtime = {}) {
1537
2195
  viteNetworkUrls: appServer.resolvedUrls?.network ?? [],
1538
2196
  });
1539
2197
  const closers = [() => appServer.close()];
1540
- let mockHost;
2198
+ let browserDevHost;
1541
2199
  try {
1542
- mockHost = await logger.task('正在启动 Mock runtime host', () => startMiniProgramMockHostServer({
2200
+ browserDevHost = await logger.task('正在启动 Browser Dev Host', () => startBrowserDevHostServer({
1543
2201
  appUrl,
1544
2202
  devContext: devLaunchContext,
1545
2203
  fetchImpl,
@@ -1547,6 +2205,7 @@ async function runDevCommand(options, runtime = {}) {
1547
2205
  defaultLanAddressId: lanAddresses[0]?.id,
1548
2206
  lanAddresses,
1549
2207
  lookupHostname: runtime.lookupHostname,
2208
+ loginService: runtime.loginService,
1550
2209
  macAppProtocol: createMacAppProtocol(appUrl, {
1551
2210
  miniProgramId: devLaunchContext.miniProgramId,
1552
2211
  localDevLaunchToken: devLaunchContext.localDevLaunchToken,
@@ -1554,17 +2213,17 @@ async function runDevCommand(options, runtime = {}) {
1554
2213
  sdkVersion: HB_SDK_VERSION,
1555
2214
  }),
1556
2215
  permissionController,
1557
- port: options.mockPort ?? DEFAULT_MOCK_PORT,
1558
- root: runtime.mockHostRoot,
1559
- rootCandidates: runtime.mockHostRootCandidates,
2216
+ port: options.browserDevHostPort ?? DEFAULT_BROWSER_DEV_HOST_PORT,
2217
+ root: runtime.browserDevHostRoot,
2218
+ rootCandidates: runtime.browserDevHostRootCandidates,
1560
2219
  runtimePermissions: devLaunchContext.runtimePermissions,
1561
- }), { successText: 'Mock runtime host 已启动' });
2220
+ }), { successText: 'Browser Dev Host 已启动' });
1562
2221
  }
1563
2222
  catch (error) {
1564
2223
  await closeAll(closers);
1565
2224
  throw error;
1566
2225
  }
1567
- closers.push(() => mockHost.close());
2226
+ closers.push(() => browserDevHost.close());
1568
2227
  if (devLaunchContext.source === 'remote' && devLaunchContext.miniProgramId) {
1569
2228
  const tokenSession = startLocalDevLaunchTokenSession({
1570
2229
  env: runtime.env,
@@ -1578,7 +2237,7 @@ async function runDevCommand(options, runtime = {}) {
1578
2237
  ...(localDevLaunchToken ? { localDevLaunchToken } : {}),
1579
2238
  runtimePermissions: devLaunchContext.runtimePermissions,
1580
2239
  };
1581
- mockHost.updateBootstrap({
2240
+ browserDevHost.updateBootstrap({
1582
2241
  devContext: tokenContext,
1583
2242
  lanAddresses: createLanAddressCandidates({
1584
2243
  appUrl,
@@ -1604,17 +2263,17 @@ async function runDevCommand(options, runtime = {}) {
1604
2263
  });
1605
2264
  closers.push(tokenSession.close);
1606
2265
  }
1607
- logger.success('Mock runtime host 已就绪');
1608
- logger.info(`Mock runtime host: ${mockHost.url}`);
2266
+ logger.success('Browser Dev Host 已就绪');
2267
+ logger.info(`Browser Dev Host: ${browserDevHost.url}`);
1609
2268
  logger.info(`Mini program URL: ${appUrl}`);
1610
- logger.info('Mac APP: use the button in Mock runtime host');
1611
- logger.info('Mobile APP: scan the QR code in Mock runtime host');
2269
+ logger.info('Mac APP: use the button in Browser Dev Host');
2270
+ logger.info('Mobile APP: scan the QR code in Browser Dev Host');
1612
2271
  if (devLaunchContext.source === 'anonymous') {
1613
2272
  logger.warn('当前使用匿名本地沙箱;真机调试可用,但不会携带远端小程序身份或权限。');
1614
2273
  }
1615
2274
  if (options.open !== false) {
1616
2275
  logger.debug('正在打开浏览器调试页');
1617
- void openUrl(mockHost.url);
2276
+ void openUrl(browserDevHost.url);
1618
2277
  }
1619
2278
  installShutdownHandlers(closers, runtime.process ?? process);
1620
2279
  }
@@ -1625,7 +2284,7 @@ async function loadDevLaunchContext(options) {
1625
2284
  if (options.requireRemote) {
1626
2285
  throw new Error(anonymousReason);
1627
2286
  }
1628
- options.logger.warn(`未读取到远端 Runtime 权限,Mock runtime 将默认拒绝受管能力:${anonymousReason}`);
2287
+ options.logger.warn(`未读取到远端 Runtime 权限,Browser Dev Host 将默认拒绝受管能力:${anonymousReason}`);
1629
2288
  return {
1630
2289
  source: 'anonymous',
1631
2290
  warning: anonymousReason,
@@ -1655,7 +2314,7 @@ async function loadDevLaunchContext(options) {
1655
2314
  throw error;
1656
2315
  }
1657
2316
  options.logger.debug(`远端 dev context 失败详情:${index.readRedactedErrorMessage(error, { verbose: true })}`);
1658
- options.logger.warn(`未读取到远端 Runtime 权限,Mock runtime 将默认拒绝受管能力:${DEV_CONTEXT_UNAVAILABLE_WARNING}`);
2317
+ options.logger.warn(`未读取到远端 Runtime 权限,Browser Dev Host 将默认拒绝受管能力:${DEV_CONTEXT_UNAVAILABLE_WARNING}`);
1659
2318
  return {
1660
2319
  source: 'anonymous',
1661
2320
  warning: DEV_CONTEXT_UNAVAILABLE_WARNING,
@@ -1677,7 +2336,7 @@ function createDevPermissionController(options) {
1677
2336
  source: devContext.source,
1678
2337
  ...(miniProgramId ? { mini_program_id: miniProgramId } : {}),
1679
2338
  ...(isRuntimePermissionsSnapshot(devContext.runtimePermissions) ? { remote_permissions: devContext.runtimePermissions } : {}),
1680
- ...(overrides ? { override_permissions: context.createMiniProgramMockRuntimePermissions(overrides) } : {}),
2339
+ ...(overrides ? { override_permissions: context.createBrowserDevRuntimePermissions(overrides) } : {}),
1681
2340
  ...(error ? { persistence_error: error } : {}),
1682
2341
  };
1683
2342
  };
@@ -1714,7 +2373,7 @@ function createDevPermissionController(options) {
1714
2373
  return createConfiguration(undefined);
1715
2374
  },
1716
2375
  async save(runtimePermissions) {
1717
- const overrides = context.createMiniProgramMockPermissionState({
2376
+ const overrides = context.createBrowserDevPermissionState({
1718
2377
  runtimePermissions,
1719
2378
  });
1720
2379
  try {