@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.
- package/CHANGELOG.md +48 -1
- package/README.md +5 -0
- package/dist/cli-chunks/{build-0sZ873iU.cjs → build-BqGFRLe-.cjs} +2 -2
- package/dist/cli-chunks/{context-DNQEI7m7.cjs → context-DTr3hRTv.cjs} +6 -19
- package/dist/cli-chunks/{create-BkE71M2U.cjs → create-OkiiiH4I.cjs} +1 -1
- package/dist/cli-chunks/{dev-_tBlQ2WO.cjs → dev-j7W9R7ew.cjs} +798 -139
- package/dist/cli-chunks/{doctor-B1wk8IpC.cjs → doctor-BO0vkDny.cjs} +1 -1
- package/dist/cli-chunks/{index-BbDsxI94.cjs → index-CZuvk1Rg.cjs} +1 -1
- package/dist/cli-chunks/{index-CIucOD2H.cjs → index-CyEqCbeu.cjs} +24 -21
- package/dist/cli-chunks/{login-CnjEKlFT.cjs → login-CuAGltGs.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-ESaxoAE1.cjs → project-vite-BUY9DgYo.cjs} +1 -1
- package/dist/cli-chunks/{remote-BaGzge7R.cjs → remote-tPMcxgpi.cjs} +4 -4
- package/dist/cli-chunks/{session-C9KHqijL.cjs → session-BUjFX2U2.cjs} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/devtools/{mock-host → browser-dev-host}/index.html +3 -7
- package/dist/devtools/browser-dev-host/main.js +9835 -0
- package/dist/index.cjs.js +40 -118
- package/dist/index.esm.js +40 -118
- package/dist/protocol.cjs.js +71 -346
- package/dist/protocol.esm.js +72 -342
- package/dist/vite.cjs.js +1 -4
- package/dist/vite.esm.js +1 -4
- package/package.json +26 -9
- package/skill/SKILL.md +5 -5
- package/skill/references/api-protocol.md +102 -114
- package/skill/references/api-root.md +1 -1
- package/skill/references/cli.md +4 -2
- package/skill/references/recipes.md +2 -0
- package/skill/references/safety-boundaries.md +1 -0
- package/skill/scripts/sync-references.mjs +5 -1
- package/skill/skill.json +4 -4
- package/types/core/network-sanitize.d.ts +1 -1
- package/types/modules/auth/index.d.ts +3 -1
- package/types/modules/user/types.d.ts +2 -53
- package/types/protocol/capabilities.d.ts +2 -352
- package/types/protocol/constants.d.ts +1 -14
- package/types/protocol/dev-session.d.ts +3 -3
- package/types/protocol/guards.d.ts +1 -8
- package/types/protocol/runtime-permissions.d.ts +2 -41
- package/types/protocol/types.d.ts +3 -150
- package/types/protocol.d.ts +8 -23
- package/dist/devtools/mock-host/main.js +0 -7751
- package/types/protocol/network-policy.d.ts +0 -10
- 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-
|
|
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-
|
|
14
|
-
var projectVite = require('./project-vite-
|
|
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-
|
|
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
|
-
/**
|
|
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
|
|
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 >
|
|
214
|
-
reject(
|
|
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('
|
|
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(
|
|
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
|
|
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
|
|
276
|
+
function readBrowserDevHostErrorStatus(error) {
|
|
277
277
|
return isObjectLike(error) && typeof error.status === 'number' ? error.status : 500;
|
|
278
278
|
}
|
|
279
|
-
function
|
|
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: '
|
|
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
|
|
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
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
390
|
+
logBrowserDevHostNetworkProxyDiagnostic('http_status', payload, result);
|
|
383
391
|
}
|
|
384
392
|
writeNetworkJsonResponse(response, 200, result, cors);
|
|
385
393
|
}
|
|
386
394
|
catch (error) {
|
|
387
|
-
|
|
388
|
-
writeNetworkJsonResponse(response,
|
|
395
|
+
logBrowserDevHostNetworkProxyDiagnostic('proxy_error', undefined, undefined, error);
|
|
396
|
+
writeNetworkJsonResponse(response, readBrowserDevHostErrorStatus(error), toBrowserDevHostErrorPayload(error), cors);
|
|
389
397
|
}
|
|
390
398
|
}
|
|
391
|
-
function
|
|
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][
|
|
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
|
|
432
|
-
async function
|
|
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
|
|
442
|
+
throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是非空字符串');
|
|
435
443
|
}
|
|
436
|
-
const url =
|
|
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 =
|
|
439
|
-
|
|
440
|
-
const body =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 <=
|
|
483
|
-
const pinnedTarget = await
|
|
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 ===
|
|
501
|
-
throw
|
|
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
|
-
|
|
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
|
|
528
|
+
throw createBrowserDevHostError(502, 'TOO_MANY_REDIRECTS', 'network.request redirect limit exceeded');
|
|
511
529
|
}
|
|
512
|
-
async function
|
|
530
|
+
async function resolveBrowserDevHostNetworkTarget(url, lookupHostname, allowedHttpOrigins) {
|
|
513
531
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
514
|
-
throw
|
|
532
|
+
throw createBrowserDevHostError(403, 'NETWORK_TARGET_DENIED', 'network.request redirect 仅支持 HTTP(S) URL');
|
|
515
533
|
}
|
|
516
534
|
if (url.username || url.password) {
|
|
517
|
-
throw
|
|
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
|
|
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 >
|
|
641
|
+
if (size > MAX_BROWSER_DEV_HOST_NETWORK_RESPONSE_BODY) {
|
|
565
642
|
await reader.cancel();
|
|
566
|
-
throw
|
|
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
|
|
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
|
|
655
|
+
throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', 'network.request url 必须是合法 URL');
|
|
579
656
|
}
|
|
580
657
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
581
|
-
throw
|
|
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
|
|
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 (
|
|
599
|
-
throw
|
|
675
|
+
if (isSensitiveBrowserDevHostNetworkHeader(key)) {
|
|
676
|
+
throw createBrowserDevHostError(400, 'INVALID_NETWORK_REQUEST', `network.request 不允许设置敏感请求头: ${key}`);
|
|
600
677
|
}
|
|
601
|
-
if (typeof value === 'string' && !
|
|
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
|
|
684
|
+
function isSensitiveBrowserDevHostNetworkHeader(key) {
|
|
608
685
|
return ['authorization', 'cookie', 'proxy-authorization'].includes(key.toLowerCase());
|
|
609
686
|
}
|
|
610
|
-
function
|
|
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
|
|
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
|
|
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
|
|
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) >
|
|
646
|
-
throw
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
886
|
-
const
|
|
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
|
|
891
|
-
path.resolve(__dirname, 'devtools/
|
|
892
|
-
path.resolve(__dirname, '../devtools/
|
|
893
|
-
path.resolve(__dirname, '../../devtools/
|
|
894
|
-
path.resolve(__dirname, '../../../devtools/
|
|
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
|
|
897
|
-
const root = options.root ??
|
|
898
|
-
|
|
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 =
|
|
908
|
-
const
|
|
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 =
|
|
1372
|
+
bootstrapJson = serializeBrowserDevHostBootstrap(bootstrap);
|
|
935
1373
|
},
|
|
936
|
-
networkUrls:
|
|
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:
|
|
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
|
|
951
|
-
const found = candidates.find(
|
|
1389
|
+
function resolveBrowserDevHostRoot(candidates = BROWSER_DEV_HOST_ROOT_CANDIDATES) {
|
|
1390
|
+
const found = candidates.find(isCompleteMiniProgramBrowserDevHostRoot);
|
|
952
1391
|
if (!found) {
|
|
953
|
-
throw
|
|
1392
|
+
throw createMissingBrowserDevHostError();
|
|
954
1393
|
}
|
|
955
1394
|
return found;
|
|
956
1395
|
}
|
|
957
|
-
function
|
|
958
|
-
if (!
|
|
959
|
-
throw
|
|
1396
|
+
function assertCompleteMiniProgramBrowserDevHostRoot(root) {
|
|
1397
|
+
if (!isCompleteMiniProgramBrowserDevHostRoot(root)) {
|
|
1398
|
+
throw createMissingBrowserDevHostError();
|
|
960
1399
|
}
|
|
961
1400
|
}
|
|
962
|
-
function
|
|
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
|
|
966
|
-
return new Error('未找到完整的 hb-sdk
|
|
1404
|
+
function createMissingBrowserDevHostError() {
|
|
1405
|
+
return new Error('未找到完整的 hb-sdk browser dev host 静态产物。请先执行 @heybox/hb-sdk 的 build:browser-dev-host。');
|
|
967
1406
|
}
|
|
968
|
-
function
|
|
969
|
-
return (options.lanAddresses ?? []).map((address) =>
|
|
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
|
|
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
|
|
1419
|
+
function serializeBrowserDevHostBootstrap(bootstrap) {
|
|
981
1420
|
const serialized = JSON.stringify(bootstrap);
|
|
982
1421
|
if (serialized === undefined) {
|
|
983
|
-
throw new TypeError('
|
|
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
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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 ===
|
|
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') ||
|
|
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 ===
|
|
1069
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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: '
|
|
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.
|
|
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
|
|
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
|
|
2198
|
+
let browserDevHost;
|
|
1541
2199
|
try {
|
|
1542
|
-
|
|
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.
|
|
1558
|
-
root: runtime.
|
|
1559
|
-
rootCandidates: runtime.
|
|
2216
|
+
port: options.browserDevHostPort ?? DEFAULT_BROWSER_DEV_HOST_PORT,
|
|
2217
|
+
root: runtime.browserDevHostRoot,
|
|
2218
|
+
rootCandidates: runtime.browserDevHostRootCandidates,
|
|
1560
2219
|
runtimePermissions: devLaunchContext.runtimePermissions,
|
|
1561
|
-
}), { successText: '
|
|
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(() =>
|
|
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
|
-
|
|
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('
|
|
1608
|
-
logger.info(`
|
|
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
|
|
1611
|
-
logger.info('Mobile APP: scan the QR code in
|
|
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(
|
|
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 权限,
|
|
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 权限,
|
|
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.
|
|
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.
|
|
2376
|
+
const overrides = context.createBrowserDevPermissionState({
|
|
1718
2377
|
runtimePermissions,
|
|
1719
2378
|
});
|
|
1720
2379
|
try {
|