@modootoday/datalab-extension-mcp 1.8.6 → 1.8.7
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/README.en.md +5 -5
- package/README.md +5 -5
- package/dist/{browsers-LYXYQPZD.js → browsers-RT4ZPGXZ.js} +1 -1
- package/dist/{chunk-FR64UB36.js → chunk-JOEFFC3W.js} +51 -17
- package/dist/{chunk-TPYHNPF4.js → chunk-YTMY2DAN.js} +648 -130
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ../mcp-server/dist/chunk-
|
|
1
|
+
// ../mcp-server/dist/chunk-CWKYOEBJ.js
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
4
4
|
import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
|
|
@@ -6,8 +6,9 @@ import { join } from "path";
|
|
|
6
6
|
import {
|
|
7
7
|
createServer
|
|
8
8
|
} from "http";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { pipeline } from "stream/promises";
|
|
10
|
+
import { constants } from "fs";
|
|
11
|
+
import { lstat, open } from "fs/promises";
|
|
11
12
|
import { basename, isAbsolute } from "path";
|
|
12
13
|
import {
|
|
13
14
|
appendFileSync,
|
|
@@ -14436,6 +14437,8 @@ var HelloSchema = external_exports.object({
|
|
|
14436
14437
|
capabilities: external_exports.array(external_exports.string()).default([])
|
|
14437
14438
|
});
|
|
14438
14439
|
var BRIDGE_AUTH_CAPABILITY = "bridge-auth-bearer";
|
|
14440
|
+
var BROWSER_ROUTING_CAPABILITY = "browser-routing-v1";
|
|
14441
|
+
var LOCAL_FILE_REQUEST_CAPABILITY = "local-file-request-v1";
|
|
14439
14442
|
function bearerToken(header) {
|
|
14440
14443
|
if (typeof header !== "string") return null;
|
|
14441
14444
|
const match = /^Bearer (.+)$/i.exec(header.trim());
|
|
@@ -14459,6 +14462,15 @@ var HelloNackReasonSchema = external_exports.enum([
|
|
|
14459
14462
|
/** Origin header absent or not our extension. */
|
|
14460
14463
|
"forbidden_origin"
|
|
14461
14464
|
]);
|
|
14465
|
+
var BrowserRoutingSchema = external_exports.object({
|
|
14466
|
+
targetBrowserId: external_exports.string(),
|
|
14467
|
+
explicit: external_exports.boolean(),
|
|
14468
|
+
fallbackFromPinned: external_exports.boolean(),
|
|
14469
|
+
connectedBrowserCount: external_exports.number().int().nonnegative()
|
|
14470
|
+
});
|
|
14471
|
+
var BrowserRouteEchoSchema = external_exports.object({
|
|
14472
|
+
browserId: external_exports.string()
|
|
14473
|
+
});
|
|
14462
14474
|
var HelloNackSchema = external_exports.object({
|
|
14463
14475
|
t: external_exports.literal("hello_nack"),
|
|
14464
14476
|
reason: HelloNackReasonSchema,
|
|
@@ -14471,7 +14483,8 @@ var BridgeRequestSchema = external_exports.discriminatedUnion("method", [
|
|
|
14471
14483
|
external_exports.object({
|
|
14472
14484
|
t: external_exports.literal("req"),
|
|
14473
14485
|
id: external_exports.string(),
|
|
14474
|
-
method: external_exports.literal("tools/list")
|
|
14486
|
+
method: external_exports.literal("tools/list"),
|
|
14487
|
+
routing: BrowserRoutingSchema.optional()
|
|
14475
14488
|
}),
|
|
14476
14489
|
external_exports.object({
|
|
14477
14490
|
t: external_exports.literal("req"),
|
|
@@ -14479,7 +14492,8 @@ var BridgeRequestSchema = external_exports.discriminatedUnion("method", [
|
|
|
14479
14492
|
method: external_exports.literal("tools/call"),
|
|
14480
14493
|
/** Must be on the read-only allowlist — enforced extension-side. */
|
|
14481
14494
|
name: external_exports.string(),
|
|
14482
|
-
args: external_exports.record(external_exports.string(), external_exports.unknown())
|
|
14495
|
+
args: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
14496
|
+
routing: BrowserRoutingSchema.optional()
|
|
14483
14497
|
})
|
|
14484
14498
|
]);
|
|
14485
14499
|
var elapsedMs = external_exports.number().nonnegative().optional();
|
|
@@ -14489,7 +14503,8 @@ var BridgeResultSchema = external_exports.union([
|
|
|
14489
14503
|
id: external_exports.string(),
|
|
14490
14504
|
ok: external_exports.literal(true),
|
|
14491
14505
|
result: external_exports.unknown(),
|
|
14492
|
-
ms: elapsedMs
|
|
14506
|
+
ms: elapsedMs,
|
|
14507
|
+
route: BrowserRouteEchoSchema.optional()
|
|
14493
14508
|
}),
|
|
14494
14509
|
external_exports.object({
|
|
14495
14510
|
t: external_exports.literal("res"),
|
|
@@ -14499,7 +14514,8 @@ var BridgeResultSchema = external_exports.union([
|
|
|
14499
14514
|
message: external_exports.string(),
|
|
14500
14515
|
// Carried on failure too: the timing of a call that FAILED is exactly
|
|
14501
14516
|
// what tells a timeout apart from a refusal.
|
|
14502
|
-
ms: elapsedMs
|
|
14517
|
+
ms: elapsedMs,
|
|
14518
|
+
route: BrowserRouteEchoSchema.optional()
|
|
14503
14519
|
})
|
|
14504
14520
|
]);
|
|
14505
14521
|
var BridgeHeartbeatSchema = external_exports.object({
|
|
@@ -14517,7 +14533,8 @@ var BridgeUpstreamSchema = external_exports.union([
|
|
|
14517
14533
|
external_exports.object({
|
|
14518
14534
|
t: external_exports.literal("tools"),
|
|
14519
14535
|
id: external_exports.string(),
|
|
14520
|
-
tools: external_exports.array(BridgeToolDescriptorSchema)
|
|
14536
|
+
tools: external_exports.array(BridgeToolDescriptorSchema),
|
|
14537
|
+
route: BrowserRouteEchoSchema.optional()
|
|
14521
14538
|
})
|
|
14522
14539
|
]);
|
|
14523
14540
|
var UPSTREAM_FRAME_TYPES = ["res", "tools"];
|
|
@@ -14647,6 +14664,7 @@ function resolveCredential(presented, known) {
|
|
|
14647
14664
|
return root.ok ? { kind: "root" } : REFUSED;
|
|
14648
14665
|
}
|
|
14649
14666
|
var READ_ONLY_TOOLS = [
|
|
14667
|
+
"datalab_browsers",
|
|
14650
14668
|
// --- Search-ad estimates. Uses the user's own advertiser API credentials. ---
|
|
14651
14669
|
"ad_average_position_bid",
|
|
14652
14670
|
"ad_competition_density",
|
|
@@ -14988,6 +15006,10 @@ var REASON_POLICY = Object.freeze({
|
|
|
14988
15006
|
*/
|
|
14989
15007
|
disconnected: Object.freeze({ retryable: true }),
|
|
14990
15008
|
superseded: Object.freeze({ retryable: true }),
|
|
15009
|
+
browser_detached: Object.freeze({
|
|
15010
|
+
retryable: true,
|
|
15011
|
+
retryAfterMs: 1e3
|
|
15012
|
+
}),
|
|
14991
15013
|
// --- The request itself must change ---
|
|
14992
15014
|
unknown_tool: Object.freeze({ retryable: false }),
|
|
14993
15015
|
invalid_args: Object.freeze({ retryable: false }),
|
|
@@ -15004,6 +15026,7 @@ var REASON_POLICY = Object.freeze({
|
|
|
15004
15026
|
// --- The connection itself must be repaired ---
|
|
15005
15027
|
not_connected: Object.freeze({ retryable: false }),
|
|
15006
15028
|
unauthorized: Object.freeze({ retryable: false }),
|
|
15029
|
+
browser_unknown: Object.freeze({ retryable: false }),
|
|
15007
15030
|
// --- Staging: the request must change, except where nobody knows yet ---
|
|
15008
15031
|
stale_fence: Object.freeze({ retryable: false }),
|
|
15009
15032
|
already_sealed: Object.freeze({ retryable: false }),
|
|
@@ -15164,6 +15187,7 @@ var BY_PREFIX = [
|
|
|
15164
15187
|
];
|
|
15165
15188
|
var GALLERY = ["gallery_image_add"];
|
|
15166
15189
|
var EXPLICIT = [
|
|
15190
|
+
{ slug: "connection", tools: ["datalab_browsers"] },
|
|
15167
15191
|
{ slug: "gallery", tools: GALLERY },
|
|
15168
15192
|
{ slug: "blog-brief", tools: BLOG_BRIEF },
|
|
15169
15193
|
{ slug: "blog-insights", tools: BLOG_INSIGHTS },
|
|
@@ -15174,6 +15198,11 @@ var EXPLICIT = [
|
|
|
15174
15198
|
{ slug: "benchmark", tools: ["benchmark_gap"] }
|
|
15175
15199
|
];
|
|
15176
15200
|
var META = [
|
|
15201
|
+
{
|
|
15202
|
+
slug: "connection",
|
|
15203
|
+
label: "\uC5F0\uACB0 \uBE0C\uB77C\uC6B0\uC800",
|
|
15204
|
+
summary: "\uC5EC\uB7EC \uBE0C\uB77C\uC6B0\uC800\xB7\uD504\uB85C\uD544 \uC911 \uB3C4\uAD6C\uB97C \uC2E4\uD589\uD560 \uACF3\uC744 \uACE0\uB978\uB2E4"
|
|
15205
|
+
},
|
|
15177
15206
|
{
|
|
15178
15207
|
slug: "blog-brief",
|
|
15179
15208
|
label: "\uC624\uB298\uC758 \uBE14\uB85C\uADF8 \uBE0C\uB9AC\uD551",
|
|
@@ -15310,6 +15339,12 @@ var CALL_TOOL = "datalab_call";
|
|
|
15310
15339
|
var CONFIRM_STATUS_TOOL = "datalab_confirm_status";
|
|
15311
15340
|
var SESSION_STATE_TOOL = "datalab_session_state";
|
|
15312
15341
|
var BROWSERS_TOOL = "datalab_browsers";
|
|
15342
|
+
function browserProperty() {
|
|
15343
|
+
return {
|
|
15344
|
+
type: "string",
|
|
15345
|
+
description: `${BROWSERS_TOOL} \uAC00 \uB3CC\uB824\uC900 \uC2E4\uD589 \uB300\uC0C1 \uBE0C\uB77C\uC6B0\uC800 id`
|
|
15346
|
+
};
|
|
15347
|
+
}
|
|
15313
15348
|
var DESCRIBE_PAGE_SIZE = 10;
|
|
15314
15349
|
function discoveryDescriptors(entries) {
|
|
15315
15350
|
const sets = buildToolsets(entries.map((e) => e.name));
|
|
@@ -15334,7 +15369,8 @@ function discoveryDescriptors(entries) {
|
|
|
15334
15369
|
withSchemas: {
|
|
15335
15370
|
type: "boolean",
|
|
15336
15371
|
description: `\uC778\uC790 \uC2A4\uD0A4\uB9C8\uAE4C\uC9C0 \uD568\uAED8 \uBC1B\uB294\uB2E4 \u2014 \uADF8\uB7EC\uBA74 ${DESCRIBE_TOOL} \uC5C6\uC774 \uBC14\uB85C \uC2E4\uD589\uD560 \uC218 \uC788\uB2E4. toolset \uC774\uB098 query \uC640 \uAC19\uC774 \uC904 \uB54C\uB9CC \uC4F4\uB2E4`
|
|
15337
|
-
}
|
|
15372
|
+
},
|
|
15373
|
+
browser: browserProperty()
|
|
15338
15374
|
},
|
|
15339
15375
|
additionalProperties: false
|
|
15340
15376
|
}
|
|
@@ -15351,7 +15387,8 @@ function discoveryDescriptors(entries) {
|
|
|
15351
15387
|
items: { type: "string" },
|
|
15352
15388
|
maxItems: DESCRIBE_PAGE_SIZE,
|
|
15353
15389
|
description: `\uC2A4\uD0A4\uB9C8\uB97C \uBCFC \uB3C4\uAD6C \uC774\uB984 (\uD55C \uBC88\uC5D0 ${DESCRIBE_PAGE_SIZE}\uAC1C\uAE4C\uC9C0)`
|
|
15354
|
-
}
|
|
15390
|
+
},
|
|
15391
|
+
browser: browserProperty()
|
|
15355
15392
|
},
|
|
15356
15393
|
required: ["tools"],
|
|
15357
15394
|
additionalProperties: false
|
|
@@ -15369,7 +15406,8 @@ function discoveryDescriptors(entries) {
|
|
|
15369
15406
|
args: {
|
|
15370
15407
|
type: "object",
|
|
15371
15408
|
description: `\uADF8 \uB3C4\uAD6C\uC758 \uC778\uC790. ${DESCRIBE_TOOL} \uC758 \uC2A4\uD0A4\uB9C8\uB97C \uB530\uB978\uB2E4`
|
|
15372
|
-
}
|
|
15409
|
+
},
|
|
15410
|
+
browser: browserProperty()
|
|
15373
15411
|
},
|
|
15374
15412
|
required: ["tool"],
|
|
15375
15413
|
additionalProperties: false
|
|
@@ -15385,7 +15423,8 @@ function discoveryDescriptors(entries) {
|
|
|
15385
15423
|
ticket: {
|
|
15386
15424
|
type: "string",
|
|
15387
15425
|
description: `${CALL_TOOL} \uC774 \uB3CC\uB824\uC900 ticket \uAC12 \uADF8\uB300\uB85C`
|
|
15388
|
-
}
|
|
15426
|
+
},
|
|
15427
|
+
browser: browserProperty()
|
|
15389
15428
|
},
|
|
15390
15429
|
required: ["ticket"],
|
|
15391
15430
|
additionalProperties: false
|
|
@@ -15399,7 +15438,7 @@ function sessionStateDescriptor() {
|
|
|
15399
15438
|
description: "\uC9C0\uAE08 \uC774 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uD655\uC778 \uCC3D \uC5C6\uC774 \uC5BC\uB9C8\uB098 \uB354 \uC791\uC5C5\uD560 \uC218 \uC788\uB294\uC9C0, \uADF8\uB9AC\uACE0 \uC791\uC5C5\uD560 \uB300\uC0C1\uC774 \uC2E4\uC81C\uB85C \uC5F4\uB824 \uC788\uB294\uC9C0 \uC54C\uB824\uC900\uB2E4. \uC5EC\uB7EC \uBC88\uC758 \uD3B8\uC9D1\uC774\uB098 \uBC30\uCE58\uB97C \uC2DC\uC791\uD558\uAE30 \uC804\uC5D0 \uBA3C\uC800 \uBD80\uB978\uB2E4 \u2014 \uB0A8\uC740 \uC2DC\uAC04\uC774 \uC9E7\uC73C\uBA74 \uB3C4\uC911\uC5D0 \uD655\uC778 \uCC3D\uC774 \uB728\uAE30 \uC2DC\uC791\uD558\uACE0, \uC0AC\uC6A9\uC790\uAC00 \uC790\uB9AC\uC5D0 \uC5C6\uC73C\uBA74 \uAC70\uAE30\uC11C \uBA48\uCD98\uB2E4. surfaces \uB294 \uAE00\uC4F0\uAE30 \uCC3D\xB7\uC0AC\uC9C4 \uD3B8\uC9D1\uAE30\xB7\uC601\uC0C1 \uD3B8\uC9D1\uAE30\uAC00 \uAC01\uAC01 \uC900\uBE44\uB410\uB294\uC9C0 \uB9D0\uD574\uC900\uB2E4. ready \uAC00 \uC544\uB2CC \uD45C\uBA74\uC744 \uC4F0\uB294 \uACC4\uD68D\uC740 \uB9E4 \uD638\uCD9C\uC774 \uC2E4\uD328\uD558\uB2C8, \uACC4\uD68D\uC744 \uC138\uC6B0\uAE30 \uC804\uC5D0 \uD655\uC778\uD558\uACE0 \uD544\uC694\uD558\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC5F4\uC5B4 \uB2EC\uB77C\uACE0 \uC548\uB0B4\uD55C\uB2E4.",
|
|
15400
15439
|
inputSchema: {
|
|
15401
15440
|
type: "object",
|
|
15402
|
-
properties: {},
|
|
15441
|
+
properties: { browser: browserProperty() },
|
|
15403
15442
|
additionalProperties: false
|
|
15404
15443
|
},
|
|
15405
15444
|
annotations: LOOKUP_ANNOTATIONS
|
|
@@ -15408,7 +15447,7 @@ function sessionStateDescriptor() {
|
|
|
15408
15447
|
function browsersDescriptor() {
|
|
15409
15448
|
return {
|
|
15410
15449
|
name: BROWSERS_TOOL,
|
|
15411
|
-
description: "\uC774 \uCEF4\uD4E8\uD130\uC5D0 \uC9C0\uAE08 \uC5F0\uACB0\uB3FC \uC788\uB294 \uBE0C\uB77C\uC6B0\uC800\uB97C \uC54C\uB824\uC900\uB2E4. \uD06C\uB86C\xB7\uC6E8\uC77C\xB7\uC5E3\uC9C0 \uC5B4\uB290 \uAC83\uC774\uB4E0, \uD504\uB85C\uD544\uC744 \uC5EC\uB7EC \uAC1C \uC5F0\uACB0\uD574 \uB454 \uC0AC\uC6A9\uC790\uC5D0\uAC8C\uB294 \uB3C4\uAD6C\uAC00 \uC5B4\uB514\uC11C \uC2E4\uD589\uB418\uB294\uC9C0\uAC00 \uACB0\uACFC\uB97C \uBC14\uAFBC\uB2E4 \u2014 \uC5F4\uB824 \uC788\uB294 \uAE00\uB3C4 \uB85C\uADF8\uC778\uD55C \uACC4\uC815\uB3C4 \uD504\uB85C\uD544\uB9C8\uB2E4 \uB2E4\uB974\uB2E4. \uBAA9\uB85D\uC758 id \uB97C \uB2E4\uB978 \uB3C4\uAD6C \uD638\uCD9C\uC758 browser \uC778\uC790\uC5D0 \uB123\uC73C\uBA74 \uADF8 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2E4\uD589\uB41C\uB2E4. \uC0DD\uB7B5\uD558\uBA74 \
|
|
15450
|
+
description: "\uC774 \uCEF4\uD4E8\uD130\uC5D0 \uC9C0\uAE08 \uC5F0\uACB0\uB3FC \uC788\uB294 \uBE0C\uB77C\uC6B0\uC800\uB97C \uC54C\uB824\uC900\uB2E4. \uD06C\uB86C\xB7\uC6E8\uC77C\xB7\uC5E3\uC9C0 \uC5B4\uB290 \uAC83\uC774\uB4E0, \uD504\uB85C\uD544\uC744 \uC5EC\uB7EC \uAC1C \uC5F0\uACB0\uD574 \uB454 \uC0AC\uC6A9\uC790\uC5D0\uAC8C\uB294 \uB3C4\uAD6C\uAC00 \uC5B4\uB514\uC11C \uC2E4\uD589\uB418\uB294\uC9C0\uAC00 \uACB0\uACFC\uB97C \uBC14\uAFBC\uB2E4 \u2014 \uC5F4\uB824 \uC788\uB294 \uAE00\uB3C4 \uB85C\uADF8\uC778\uD55C \uACC4\uC815\uB3C4 \uD504\uB85C\uD544\uB9C8\uB2E4 \uB2E4\uB974\uB2E4. \uBAA9\uB85D\uC758 id \uB97C \uB2E4\uB978 \uB3C4\uAD6C \uD638\uCD9C\uC758 browser \uC778\uC790\uC5D0 \uB123\uC73C\uBA74 \uADF8 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2E4\uD589\uB41C\uB2E4. \uC0DD\uB7B5\uD558\uBA74 \uACE0\uC815\uB41C \uAE30\uBCF8 \uBE0C\uB77C\uC6B0\uC800\uB97C \uC0AC\uC6A9\uD558\uBA70, \uADF8 \uC5F0\uACB0\uC774 \uB04A\uAE34 \uB3D9\uC548 \uBCC0\uACBD \uC791\uC5C5\uC740 \uB2E4\uB978 \uBE0C\uB77C\uC6B0\uC800\uB85C \uC790\uB3D9 \uC2B9\uACC4\uD558\uC9C0 \uC54A\uB294\uB2E4. \uB458 \uC774\uC0C1\uC774\uBA74 \uC5B4\uB514\uC11C \uD560\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD558\uACE0 \uC9C0\uC815\uD55C\uB2E4. \uC774 \uB3C4\uAD6C\uB294 \uC870\uD68C\uB9CC \uD558\uBA70 \uC0C8 \uBE0C\uB77C\uC6B0\uC800\uB97C \uB4F1\uB85D\uD558\uC9C0\uB294 \uBABB\uD55C\uB2E4 \u2014 \uB4F1\uB85D \uCF54\uB4DC\uB294 \uC0AC\uC6A9\uC790\uAC00 \uD328\uB110\uC5D0\uC11C \uC9C1\uC811 \uB9CC\uB4E0\uB2E4.",
|
|
15412
15451
|
inputSchema: {
|
|
15413
15452
|
type: "object",
|
|
15414
15453
|
properties: {},
|
|
@@ -15421,11 +15460,9 @@ function allowlistNames() {
|
|
|
15421
15460
|
return [.../* @__PURE__ */ new Set([...READ_ONLY_TOOLS, ...Object.keys(TOOL_TIERS)])];
|
|
15422
15461
|
}
|
|
15423
15462
|
function staticDiscoveryCatalog() {
|
|
15424
|
-
const entries = allowlistNames().map(
|
|
15425
|
-
name,
|
|
15426
|
-
|
|
15427
|
-
inputSchema: {}
|
|
15428
|
-
}));
|
|
15463
|
+
const entries = allowlistNames().map(
|
|
15464
|
+
(name) => name === BROWSERS_TOOL ? browsersDescriptor() : { name, description: "", inputSchema: {} }
|
|
15465
|
+
);
|
|
15429
15466
|
return [
|
|
15430
15467
|
...discoveryDescriptors(entries),
|
|
15431
15468
|
sessionStateDescriptor(),
|
|
@@ -15590,6 +15627,7 @@ var Bridge = class {
|
|
|
15590
15627
|
#sessions = /* @__PURE__ */ new Map();
|
|
15591
15628
|
#idTag = daemonIdTag();
|
|
15592
15629
|
#sessionSeq = 0;
|
|
15630
|
+
#defaultKey = null;
|
|
15593
15631
|
/** Consecutive handshakes refused for our own token. See `#noteStaleToken`. */
|
|
15594
15632
|
#badTokenStreak = 0;
|
|
15595
15633
|
#bucketTokens = 0;
|
|
@@ -15607,7 +15645,13 @@ var Bridge = class {
|
|
|
15607
15645
|
* single key there is never more than one to choose between.
|
|
15608
15646
|
*/
|
|
15609
15647
|
#primary() {
|
|
15610
|
-
|
|
15648
|
+
if (this.#defaultKey !== null) {
|
|
15649
|
+
const held = this.#sessions.get(this.#defaultKey);
|
|
15650
|
+
if (held?.streamActive) return held;
|
|
15651
|
+
}
|
|
15652
|
+
for (const entry of this.#sessions.values()) {
|
|
15653
|
+
if (entry.streamActive) return entry;
|
|
15654
|
+
}
|
|
15611
15655
|
return null;
|
|
15612
15656
|
}
|
|
15613
15657
|
/**
|
|
@@ -15618,11 +15662,12 @@ var Bridge = class {
|
|
|
15618
15662
|
* landing in the older one and never learn why the newer one saw nothing.
|
|
15619
15663
|
*/
|
|
15620
15664
|
connectedBrowsers() {
|
|
15621
|
-
const keys = [...this.#sessions.
|
|
15622
|
-
|
|
15665
|
+
const keys = [...this.#sessions.values()].filter((entry) => entry.streamActive).map((entry) => entry.key);
|
|
15666
|
+
const primary = this.#defaultKey === null ? this.#primary()?.key ?? null : this.#defaultKey;
|
|
15667
|
+
return keys.map((id) => ({
|
|
15623
15668
|
id,
|
|
15624
15669
|
name: this.#deps.browserLabels.get(id) ?? null,
|
|
15625
|
-
primary:
|
|
15670
|
+
primary: primary === id
|
|
15626
15671
|
}));
|
|
15627
15672
|
}
|
|
15628
15673
|
/**
|
|
@@ -15649,6 +15694,10 @@ var Bridge = class {
|
|
|
15649
15694
|
session,
|
|
15650
15695
|
tokenHash: auth.tokenHash,
|
|
15651
15696
|
authRequired: auth.authRequired,
|
|
15697
|
+
routingRequired: auth.routingRequired,
|
|
15698
|
+
fileRequestBound: auth.fileRequestBound,
|
|
15699
|
+
streamActive: false,
|
|
15700
|
+
filePermits: /* @__PURE__ */ new Map(),
|
|
15652
15701
|
pending: new PendingRegistry({
|
|
15653
15702
|
now: this.#deps.now,
|
|
15654
15703
|
timeoutMs: this.#deps.timeoutMs,
|
|
@@ -15693,9 +15742,25 @@ var Bridge = class {
|
|
|
15693
15742
|
}
|
|
15694
15743
|
return null;
|
|
15695
15744
|
}
|
|
15745
|
+
sessionForKey(key) {
|
|
15746
|
+
return this.#sessions.get(key)?.session ?? null;
|
|
15747
|
+
}
|
|
15748
|
+
legacySessionKey() {
|
|
15749
|
+
return this.#sessions.has(LEGACY_SESSION_KEY) ? LEGACY_SESSION_KEY : null;
|
|
15750
|
+
}
|
|
15696
15751
|
/** Is a panel connected and ready to serve tool calls? */
|
|
15697
15752
|
get connected() {
|
|
15698
|
-
return this.#
|
|
15753
|
+
return this.#primary() !== null;
|
|
15754
|
+
}
|
|
15755
|
+
activateSession(key, sessionId) {
|
|
15756
|
+
const entry = this.#sessions.get(key);
|
|
15757
|
+
if (!entry || entry.session.sessionId !== sessionId) return false;
|
|
15758
|
+
entry.streamActive = true;
|
|
15759
|
+
return true;
|
|
15760
|
+
}
|
|
15761
|
+
isSessionStreamActive(key, sessionId) {
|
|
15762
|
+
const entry = this.#sessions.get(key);
|
|
15763
|
+
return entry?.session.sessionId === sessionId && entry.streamActive;
|
|
15699
15764
|
}
|
|
15700
15765
|
/**
|
|
15701
15766
|
* The last catalog the panel reported, empty if it has never connected in
|
|
@@ -15723,18 +15788,161 @@ var Bridge = class {
|
|
|
15723
15788
|
* the same as that browser being here, and this gates a route that reads
|
|
15724
15789
|
* files off the operator's disk.
|
|
15725
15790
|
*/
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
if (this.
|
|
15729
|
-
return
|
|
15791
|
+
#fileCredential(entry, presented) {
|
|
15792
|
+
const hash2 = digestToken(presented);
|
|
15793
|
+
if (!timingSafeEqual(hash2, entry.tokenHash) || this.#deps.wasRevoked(hash2)) {
|
|
15794
|
+
return null;
|
|
15730
15795
|
}
|
|
15796
|
+
if (entry.key === LEGACY_SESSION_KEY) {
|
|
15797
|
+
return this.verifyRootToken(presented) ? "root" : null;
|
|
15798
|
+
}
|
|
15799
|
+
const current = this.#deps.claimedBrowsers.get(entry.key);
|
|
15800
|
+
return current !== void 0 && timingSafeEqual(hash2, current) ? "claimed_browser" : null;
|
|
15801
|
+
}
|
|
15802
|
+
#fileCredentialClass(presented) {
|
|
15803
|
+
if (this.verifyRootToken(presented)) return "root";
|
|
15731
15804
|
const hash2 = digestToken(presented);
|
|
15805
|
+
for (const current of this.#deps.claimedBrowsers.values()) {
|
|
15806
|
+
if (timingSafeEqual(hash2, current)) return "claimed_browser";
|
|
15807
|
+
}
|
|
15808
|
+
return "unknown";
|
|
15809
|
+
}
|
|
15810
|
+
authorizeLegacyFileCredential(presented) {
|
|
15811
|
+
if (typeof presented !== "string" || presented.length === 0) {
|
|
15812
|
+
return {
|
|
15813
|
+
ok: false,
|
|
15814
|
+
reason: "file_session_token_missing",
|
|
15815
|
+
credential: "missing"
|
|
15816
|
+
};
|
|
15817
|
+
}
|
|
15818
|
+
const credential = this.#fileCredentialClass(presented);
|
|
15732
15819
|
for (const entry of this.#sessions.values()) {
|
|
15733
|
-
const
|
|
15734
|
-
if (
|
|
15735
|
-
return
|
|
15820
|
+
const accepted = this.#fileCredential(entry, presented);
|
|
15821
|
+
if (accepted && !entry.fileRequestBound) {
|
|
15822
|
+
return {
|
|
15823
|
+
ok: true,
|
|
15824
|
+
key: entry.key,
|
|
15825
|
+
sessionId: entry.session.sessionId,
|
|
15826
|
+
credential: accepted,
|
|
15827
|
+
requestBound: false
|
|
15828
|
+
};
|
|
15829
|
+
}
|
|
15736
15830
|
}
|
|
15737
|
-
return
|
|
15831
|
+
return {
|
|
15832
|
+
ok: false,
|
|
15833
|
+
reason: "file_session_token_mismatch",
|
|
15834
|
+
credential
|
|
15835
|
+
};
|
|
15836
|
+
}
|
|
15837
|
+
authorizeFileRequest(input) {
|
|
15838
|
+
const entry = [...this.#sessions.values()].find(
|
|
15839
|
+
(candidate) => candidate.session.sessionId === input.sessionId
|
|
15840
|
+
);
|
|
15841
|
+
if (!entry) {
|
|
15842
|
+
return {
|
|
15843
|
+
ok: false,
|
|
15844
|
+
reason: "file_session_not_live",
|
|
15845
|
+
credential: typeof input.token === "string" && input.token.length > 0 ? this.#fileCredentialClass(input.token) : "missing"
|
|
15846
|
+
};
|
|
15847
|
+
}
|
|
15848
|
+
if (typeof input.token !== "string" || input.token.length === 0) {
|
|
15849
|
+
return {
|
|
15850
|
+
ok: false,
|
|
15851
|
+
reason: "file_session_token_missing",
|
|
15852
|
+
credential: "missing",
|
|
15853
|
+
key: entry.key,
|
|
15854
|
+
sessionId: entry.session.sessionId,
|
|
15855
|
+
...input.requestId === void 0 ? {} : { requestId: input.requestId }
|
|
15856
|
+
};
|
|
15857
|
+
}
|
|
15858
|
+
const credential = this.#fileCredential(entry, input.token);
|
|
15859
|
+
if (!credential) {
|
|
15860
|
+
return {
|
|
15861
|
+
ok: false,
|
|
15862
|
+
reason: "file_session_token_mismatch",
|
|
15863
|
+
credential: this.#fileCredentialClass(input.token),
|
|
15864
|
+
key: entry.key,
|
|
15865
|
+
sessionId: entry.session.sessionId,
|
|
15866
|
+
...input.requestId === void 0 ? {} : { requestId: input.requestId }
|
|
15867
|
+
};
|
|
15868
|
+
}
|
|
15869
|
+
if (!entry.fileRequestBound) {
|
|
15870
|
+
return {
|
|
15871
|
+
ok: true,
|
|
15872
|
+
key: entry.key,
|
|
15873
|
+
sessionId: entry.session.sessionId,
|
|
15874
|
+
...input.requestId === void 0 ? {} : { requestId: input.requestId },
|
|
15875
|
+
credential,
|
|
15876
|
+
requestBound: false
|
|
15877
|
+
};
|
|
15878
|
+
}
|
|
15879
|
+
const now = (this.#deps.now ?? Date.now)();
|
|
15880
|
+
for (const [requestId, permit2] of entry.filePermits) {
|
|
15881
|
+
if (permit2.expiresAt <= now) entry.filePermits.delete(requestId);
|
|
15882
|
+
}
|
|
15883
|
+
if (input.requestId === void 0) {
|
|
15884
|
+
return {
|
|
15885
|
+
ok: false,
|
|
15886
|
+
reason: "file_request_expired",
|
|
15887
|
+
credential,
|
|
15888
|
+
key: entry.key,
|
|
15889
|
+
sessionId: entry.session.sessionId
|
|
15890
|
+
};
|
|
15891
|
+
}
|
|
15892
|
+
const permit = entry.filePermits.get(input.requestId);
|
|
15893
|
+
if (!permit) {
|
|
15894
|
+
return {
|
|
15895
|
+
ok: false,
|
|
15896
|
+
reason: "file_request_expired",
|
|
15897
|
+
credential,
|
|
15898
|
+
key: entry.key,
|
|
15899
|
+
sessionId: entry.session.sessionId,
|
|
15900
|
+
requestId: input.requestId
|
|
15901
|
+
};
|
|
15902
|
+
}
|
|
15903
|
+
if (permit.path !== input.path || permit.tool !== input.tool) {
|
|
15904
|
+
return {
|
|
15905
|
+
ok: false,
|
|
15906
|
+
reason: "file_request_mismatch",
|
|
15907
|
+
credential,
|
|
15908
|
+
key: entry.key,
|
|
15909
|
+
sessionId: entry.session.sessionId,
|
|
15910
|
+
requestId: input.requestId
|
|
15911
|
+
};
|
|
15912
|
+
}
|
|
15913
|
+
return {
|
|
15914
|
+
ok: true,
|
|
15915
|
+
key: entry.key,
|
|
15916
|
+
sessionId: entry.session.sessionId,
|
|
15917
|
+
requestId: input.requestId,
|
|
15918
|
+
credential,
|
|
15919
|
+
requestBound: true
|
|
15920
|
+
};
|
|
15921
|
+
}
|
|
15922
|
+
consumeFileRequest(auth) {
|
|
15923
|
+
const entry = this.#sessions.get(auth.key);
|
|
15924
|
+
if (!entry || !entry.streamActive || entry.session.sessionId !== auth.sessionId) {
|
|
15925
|
+
return false;
|
|
15926
|
+
}
|
|
15927
|
+
if (!auth.requestBound) return true;
|
|
15928
|
+
if (auth.requestId === void 0 || !entry.filePermits.has(auth.requestId)) {
|
|
15929
|
+
return false;
|
|
15930
|
+
}
|
|
15931
|
+
entry.filePermits.delete(auth.requestId);
|
|
15932
|
+
return true;
|
|
15933
|
+
}
|
|
15934
|
+
cancelFileRequest(input) {
|
|
15935
|
+
const entry = [...this.#sessions.values()].find(
|
|
15936
|
+
(candidate) => candidate.session.sessionId === input.sessionId
|
|
15937
|
+
);
|
|
15938
|
+
if (!entry || !entry.streamActive) return "file_session_not_live";
|
|
15939
|
+
if (typeof input.token !== "string" || !this.#fileCredential(entry, input.token)) {
|
|
15940
|
+
return "file_session_token_mismatch";
|
|
15941
|
+
}
|
|
15942
|
+
if (!entry.filePermits.delete(input.requestId)) {
|
|
15943
|
+
return "file_request_expired";
|
|
15944
|
+
}
|
|
15945
|
+
return null;
|
|
15738
15946
|
}
|
|
15739
15947
|
/**
|
|
15740
15948
|
* The extension is right about the token, and we can be wrong about it.
|
|
@@ -15836,7 +16044,16 @@ var Bridge = class {
|
|
|
15836
16044
|
const authRequired = parsed.data.capabilities.includes(
|
|
15837
16045
|
BRIDGE_AUTH_CAPABILITY
|
|
15838
16046
|
);
|
|
15839
|
-
|
|
16047
|
+
const routingRequired = credential.kind === "claimed" && parsed.data.capabilities.includes(BROWSER_ROUTING_CAPABILITY);
|
|
16048
|
+
const fileRequestBound = parsed.data.capabilities.includes(
|
|
16049
|
+
LOCAL_FILE_REQUEST_CAPABILITY
|
|
16050
|
+
);
|
|
16051
|
+
this.#installSession(key, session, {
|
|
16052
|
+
tokenHash,
|
|
16053
|
+
authRequired,
|
|
16054
|
+
routingRequired,
|
|
16055
|
+
fileRequestBound
|
|
16056
|
+
});
|
|
15840
16057
|
const limit = this.#deps.rateLimit ?? DEFAULT_RATE_LIMIT;
|
|
15841
16058
|
this.#bucketTokens = limit.capacity;
|
|
15842
16059
|
this.#bucketRefilledAt = (this.#deps.now ?? Date.now)();
|
|
@@ -15847,7 +16064,11 @@ var Bridge = class {
|
|
|
15847
16064
|
sessionId: session.sessionId,
|
|
15848
16065
|
// Echoed only when it is actually on, so the panel can tell a server that
|
|
15849
16066
|
// is holding it to the header from one that predates the gate entirely.
|
|
15850
|
-
capabilities:
|
|
16067
|
+
capabilities: [
|
|
16068
|
+
...authRequired ? [BRIDGE_AUTH_CAPABILITY] : [],
|
|
16069
|
+
...routingRequired ? [BROWSER_ROUTING_CAPABILITY] : [],
|
|
16070
|
+
...fileRequestBound ? [LOCAL_FILE_REQUEST_CAPABILITY] : []
|
|
16071
|
+
]
|
|
15851
16072
|
};
|
|
15852
16073
|
}
|
|
15853
16074
|
/**
|
|
@@ -15896,21 +16117,27 @@ var Bridge = class {
|
|
|
15896
16117
|
return this.#settle(
|
|
15897
16118
|
frame.id,
|
|
15898
16119
|
{ ok: true, result: frame.tools },
|
|
15899
|
-
presented
|
|
16120
|
+
presented,
|
|
16121
|
+
frame.route?.browserId
|
|
15900
16122
|
);
|
|
15901
16123
|
}
|
|
15902
16124
|
if (frame.ok) {
|
|
15903
|
-
|
|
16125
|
+
const settled2 = this.#settle(
|
|
15904
16126
|
frame.id,
|
|
15905
16127
|
{
|
|
15906
16128
|
ok: true,
|
|
15907
16129
|
result: frame.result,
|
|
15908
16130
|
...frame.ms === void 0 ? {} : { ms: frame.ms }
|
|
15909
16131
|
},
|
|
15910
|
-
presented
|
|
16132
|
+
presented,
|
|
16133
|
+
frame.route?.browserId
|
|
15911
16134
|
);
|
|
16135
|
+
if (settled2 === "settled" && !(typeof frame.result === "object" && frame.result !== null && frame.result.status === "awaiting_confirm")) {
|
|
16136
|
+
this.#discardFilePermit(frame.id);
|
|
16137
|
+
}
|
|
16138
|
+
return settled2;
|
|
15912
16139
|
}
|
|
15913
|
-
|
|
16140
|
+
const settled = this.#settle(
|
|
15914
16141
|
frame.id,
|
|
15915
16142
|
{
|
|
15916
16143
|
ok: false,
|
|
@@ -15918,8 +16145,11 @@ var Bridge = class {
|
|
|
15918
16145
|
message: frame.message,
|
|
15919
16146
|
...frame.ms === void 0 ? {} : { ms: frame.ms }
|
|
15920
16147
|
},
|
|
15921
|
-
presented
|
|
16148
|
+
presented,
|
|
16149
|
+
frame.route?.browserId
|
|
15922
16150
|
);
|
|
16151
|
+
if (settled === "settled") this.#discardFilePermit(frame.id);
|
|
16152
|
+
return settled;
|
|
15923
16153
|
}
|
|
15924
16154
|
/**
|
|
15925
16155
|
* The id is the only axis a reply carries, and the prefixes make it unique
|
|
@@ -15931,15 +16161,28 @@ var Bridge = class {
|
|
|
15931
16161
|
* mint the same one — and would report "unknown", which reads as an ordinary
|
|
15932
16162
|
* late reply.
|
|
15933
16163
|
*/
|
|
15934
|
-
#settle(id, outcome, presented) {
|
|
16164
|
+
#settle(id, outcome, presented, browserId) {
|
|
15935
16165
|
for (const entry of this.#sessions.values()) {
|
|
15936
16166
|
if (!entry.pending.owns(id)) continue;
|
|
15937
16167
|
if (!this.#mayAct(entry, presented)) return "unauthorized";
|
|
16168
|
+
if (entry.routingRequired && browserId !== entry.key) {
|
|
16169
|
+
entry.pending.settle(id, {
|
|
16170
|
+
ok: false,
|
|
16171
|
+
reason: "browser_unknown",
|
|
16172
|
+
message: "The responding browser did not match the routed browser."
|
|
16173
|
+
});
|
|
16174
|
+
return "settled";
|
|
16175
|
+
}
|
|
15938
16176
|
entry.pending.settle(id, outcome);
|
|
15939
16177
|
return "settled";
|
|
15940
16178
|
}
|
|
15941
16179
|
return "dropped";
|
|
15942
16180
|
}
|
|
16181
|
+
#discardFilePermit(id) {
|
|
16182
|
+
for (const entry of this.#sessions.values()) {
|
|
16183
|
+
if (entry.filePermits.delete(id)) return;
|
|
16184
|
+
}
|
|
16185
|
+
}
|
|
15943
16186
|
/** One panel went away. Nothing it owed can land, so settle that session now. */
|
|
15944
16187
|
disconnect(key) {
|
|
15945
16188
|
this.#closeSession(
|
|
@@ -15957,6 +16200,10 @@ var Bridge = class {
|
|
|
15957
16200
|
let expired = 0;
|
|
15958
16201
|
for (const entry of this.#sessions.values()) {
|
|
15959
16202
|
expired += entry.pending.sweep();
|
|
16203
|
+
const now = (this.#deps.now ?? Date.now)();
|
|
16204
|
+
for (const [requestId, permit] of entry.filePermits) {
|
|
16205
|
+
if (permit.expiresAt <= now) entry.filePermits.delete(requestId);
|
|
16206
|
+
}
|
|
15960
16207
|
}
|
|
15961
16208
|
return expired;
|
|
15962
16209
|
}
|
|
@@ -15967,10 +16214,11 @@ var Bridge = class {
|
|
|
15967
16214
|
* shape, catalog size — with no membership check. Membership is enforced
|
|
15968
16215
|
* where the session and the execution live, in the panel's own executor.
|
|
15969
16216
|
*/
|
|
15970
|
-
async listTools() {
|
|
15971
|
-
const out = await this.#request(
|
|
15972
|
-
method: "tools/list"
|
|
15973
|
-
|
|
16217
|
+
async listTools(browser) {
|
|
16218
|
+
const out = await this.#request(
|
|
16219
|
+
{ method: "tools/list" },
|
|
16220
|
+
browser
|
|
16221
|
+
);
|
|
15974
16222
|
if (!out.ok) throw new BridgeError(out.reason, out.message);
|
|
15975
16223
|
const { tools, rejected } = projectTools(out.result);
|
|
15976
16224
|
if (tools.length > 0) {
|
|
@@ -15990,13 +16238,16 @@ var Bridge = class {
|
|
|
15990
16238
|
* `bridgeMs - toolMs` is the transport and the queue. A single total cannot
|
|
15991
16239
|
* tell a slow tool from a slow wire, and that is the question being asked.
|
|
15992
16240
|
*/
|
|
15993
|
-
async callTool(name, args) {
|
|
16241
|
+
async callTool(name, args, browser) {
|
|
15994
16242
|
const startedAt = (this.#deps.now ?? Date.now)();
|
|
15995
|
-
const out = await this.#request(
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16243
|
+
const out = await this.#request(
|
|
16244
|
+
{
|
|
16245
|
+
method: "tools/call",
|
|
16246
|
+
name,
|
|
16247
|
+
args
|
|
16248
|
+
},
|
|
16249
|
+
browser
|
|
16250
|
+
);
|
|
16000
16251
|
const bridgeMs = (this.#deps.now ?? Date.now)() - startedAt;
|
|
16001
16252
|
if (!out.ok) {
|
|
16002
16253
|
throw new BridgeError(out.reason, out.message, {
|
|
@@ -16030,8 +16281,28 @@ var Bridge = class {
|
|
|
16030
16281
|
this.#bucketTokens -= 1;
|
|
16031
16282
|
return true;
|
|
16032
16283
|
}
|
|
16033
|
-
async #request(req) {
|
|
16034
|
-
|
|
16284
|
+
async #request(req, browser) {
|
|
16285
|
+
let entry;
|
|
16286
|
+
if (browser === void 0) {
|
|
16287
|
+
entry = this.#primary();
|
|
16288
|
+
} else {
|
|
16289
|
+
const known = this.#deps.claimedBrowsers.has(browser) || browser === LEGACY_SESSION_KEY && this.#sessions.has(browser);
|
|
16290
|
+
if (!known) {
|
|
16291
|
+
return {
|
|
16292
|
+
ok: false,
|
|
16293
|
+
reason: "browser_unknown",
|
|
16294
|
+
message: "The requested browser is not registered."
|
|
16295
|
+
};
|
|
16296
|
+
}
|
|
16297
|
+
entry = this.#sessions.get(browser) ?? null;
|
|
16298
|
+
if (!entry?.streamActive) {
|
|
16299
|
+
return {
|
|
16300
|
+
ok: false,
|
|
16301
|
+
reason: "browser_detached",
|
|
16302
|
+
message: "The requested browser is registered but not connected."
|
|
16303
|
+
};
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
16035
16306
|
if (!entry) {
|
|
16036
16307
|
return {
|
|
16037
16308
|
ok: false,
|
|
@@ -16047,23 +16318,47 @@ var Bridge = class {
|
|
|
16047
16318
|
};
|
|
16048
16319
|
}
|
|
16049
16320
|
const id = entry.pending.nextId();
|
|
16321
|
+
const routing = entry.routingRequired ? {
|
|
16322
|
+
targetBrowserId: entry.key,
|
|
16323
|
+
explicit: browser !== void 0,
|
|
16324
|
+
fallbackFromPinned: browser === void 0 && this.#defaultKey !== null && this.#defaultKey !== entry.key,
|
|
16325
|
+
connectedBrowserCount: this.connectedBrowsers().length
|
|
16326
|
+
} : void 0;
|
|
16327
|
+
const delegatedTool = req.method === "tools/call" && req.name === CALL_TOOL ? req.args["tool"] : void 0;
|
|
16328
|
+
const delegatedArgs = req.method === "tools/call" && typeof req.args["args"] === "object" && req.args["args"] !== null && !Array.isArray(req.args["args"]) ? req.args["args"] : null;
|
|
16329
|
+
const fileTool = req.method === "tools/call" && (req.name === "editor_insert_image" || req.name === "gallery_image_add") ? req.name : delegatedTool === "editor_insert_image" || delegatedTool === "gallery_image_add" ? delegatedTool : null;
|
|
16330
|
+
const fileArgs = delegatedArgs ?? (req.method === "tools/call" ? req.args : null);
|
|
16331
|
+
const path = fileArgs?.["path"];
|
|
16332
|
+
if (entry.fileRequestBound && fileTool !== null && typeof path === "string" && path.length > 0) {
|
|
16333
|
+
entry.filePermits.set(id, {
|
|
16334
|
+
expiresAt: (this.#deps.now ?? Date.now)() + (this.#deps.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
16335
|
+
tool: fileTool,
|
|
16336
|
+
path
|
|
16337
|
+
});
|
|
16338
|
+
}
|
|
16050
16339
|
const settled = entry.pending.register(id);
|
|
16051
16340
|
const delivered = this.#deps.send(
|
|
16052
16341
|
{
|
|
16053
16342
|
t: "req",
|
|
16054
16343
|
id,
|
|
16055
|
-
...req
|
|
16344
|
+
...req,
|
|
16345
|
+
...routing ? { routing } : {}
|
|
16056
16346
|
},
|
|
16057
16347
|
entry.key
|
|
16058
16348
|
);
|
|
16059
16349
|
if (!delivered) {
|
|
16350
|
+
entry.filePermits.delete(id);
|
|
16060
16351
|
entry.pending.settle(id, {
|
|
16061
16352
|
ok: false,
|
|
16062
16353
|
reason: "not_connected",
|
|
16063
16354
|
message: BRIDGE_USER_MESSAGES.panelClosed
|
|
16064
16355
|
});
|
|
16065
16356
|
}
|
|
16066
|
-
|
|
16357
|
+
const outcome = await settled;
|
|
16358
|
+
if (outcome.ok && (browser !== void 0 || this.#defaultKey === null)) {
|
|
16359
|
+
this.#defaultKey = entry.key;
|
|
16360
|
+
}
|
|
16361
|
+
return outcome;
|
|
16067
16362
|
}
|
|
16068
16363
|
};
|
|
16069
16364
|
var BridgeError = class extends Error {
|
|
@@ -16444,8 +16739,43 @@ function timingMeta(timing) {
|
|
|
16444
16739
|
}
|
|
16445
16740
|
};
|
|
16446
16741
|
}
|
|
16742
|
+
var ROUTED_FACADES = /* @__PURE__ */ new Set([
|
|
16743
|
+
CATALOG_TOOL,
|
|
16744
|
+
DESCRIBE_TOOL,
|
|
16745
|
+
CALL_TOOL,
|
|
16746
|
+
CONFIRM_STATUS_TOOL,
|
|
16747
|
+
SESSION_STATE_TOOL
|
|
16748
|
+
]);
|
|
16447
16749
|
function ownTools(stage) {
|
|
16448
|
-
|
|
16750
|
+
const browsers = staticDiscoveryCatalog().find(
|
|
16751
|
+
(tool) => tool.name === BROWSERS_TOOL
|
|
16752
|
+
);
|
|
16753
|
+
return [
|
|
16754
|
+
...browsers ? [browsers] : [],
|
|
16755
|
+
...stage ? mediaToolDescriptors() : []
|
|
16756
|
+
];
|
|
16757
|
+
}
|
|
16758
|
+
function withOwnTools(tools, stage) {
|
|
16759
|
+
const canonical = new Map(
|
|
16760
|
+
staticDiscoveryCatalog().map((tool) => [tool.name, tool])
|
|
16761
|
+
);
|
|
16762
|
+
const out = tools.map((tool) => {
|
|
16763
|
+
const name = typeof tool["name"] === "string" ? tool["name"] : "";
|
|
16764
|
+
const facade = canonical.get(name);
|
|
16765
|
+
if (!facade || !ROUTED_FACADES.has(name)) return tool;
|
|
16766
|
+
return { ...tool, inputSchema: facade.inputSchema };
|
|
16767
|
+
});
|
|
16768
|
+
const names = new Set(
|
|
16769
|
+
out.flatMap(
|
|
16770
|
+
(tool) => typeof tool["name"] === "string" ? [tool["name"]] : []
|
|
16771
|
+
)
|
|
16772
|
+
);
|
|
16773
|
+
for (const tool of ownTools(stage)) {
|
|
16774
|
+
if (typeof tool["name"] !== "string" || names.has(tool["name"])) continue;
|
|
16775
|
+
names.add(tool["name"]);
|
|
16776
|
+
out.push(tool);
|
|
16777
|
+
}
|
|
16778
|
+
return out;
|
|
16449
16779
|
}
|
|
16450
16780
|
function rpcToolError(id, err) {
|
|
16451
16781
|
const policy = policyFor(err.reason);
|
|
@@ -16487,7 +16817,7 @@ function frameMcpResponse(payload, accept) {
|
|
|
16487
16817
|
function offlineCatalog(bridge, stage, id, accept) {
|
|
16488
16818
|
const base = bridge.lastKnownTools.length > 0 ? bridge.lastKnownTools : staticDiscoveryCatalog();
|
|
16489
16819
|
return frameMcpResponse(
|
|
16490
|
-
rpcResult(id, { tools:
|
|
16820
|
+
rpcResult(id, { tools: withOwnTools(base, stage) }),
|
|
16491
16821
|
accept
|
|
16492
16822
|
);
|
|
16493
16823
|
}
|
|
@@ -16508,7 +16838,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16508
16838
|
try {
|
|
16509
16839
|
const { tools } = await bridge.listTools();
|
|
16510
16840
|
return frameMcpResponse(
|
|
16511
|
-
rpcResult(id, { tools:
|
|
16841
|
+
rpcResult(id, { tools: withOwnTools(tools, stage) }),
|
|
16512
16842
|
accept
|
|
16513
16843
|
);
|
|
16514
16844
|
} catch (err) {
|
|
@@ -16527,7 +16857,23 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16527
16857
|
accept
|
|
16528
16858
|
);
|
|
16529
16859
|
}
|
|
16530
|
-
|
|
16860
|
+
let args = typeof params.arguments === "object" && params.arguments !== null ? params.arguments : {};
|
|
16861
|
+
let browser;
|
|
16862
|
+
if (ROUTED_FACADES.has(params.name) && Object.hasOwn(args, "browser")) {
|
|
16863
|
+
if (typeof args["browser"] !== "string" || args["browser"].length === 0 || args["browser"] !== args["browser"].trim()) {
|
|
16864
|
+
return frameMcpResponse(
|
|
16865
|
+
rpcError(
|
|
16866
|
+
id,
|
|
16867
|
+
CODE_INVALID_PARAMS,
|
|
16868
|
+
"browser must be a connected browser id."
|
|
16869
|
+
),
|
|
16870
|
+
accept
|
|
16871
|
+
);
|
|
16872
|
+
}
|
|
16873
|
+
browser = args["browser"];
|
|
16874
|
+
args = { ...args };
|
|
16875
|
+
delete args["browser"];
|
|
16876
|
+
}
|
|
16531
16877
|
if (params.name === BROWSERS_TOOL) {
|
|
16532
16878
|
return frameMcpResponse(
|
|
16533
16879
|
rpcResult(id, {
|
|
@@ -16556,7 +16902,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16556
16902
|
);
|
|
16557
16903
|
}
|
|
16558
16904
|
try {
|
|
16559
|
-
const call = await bridge.callTool(params.name, args);
|
|
16905
|
+
const call = browser === void 0 ? await bridge.callTool(params.name, args) : await bridge.callTool(params.name, args, browser);
|
|
16560
16906
|
return frameMcpResponse(
|
|
16561
16907
|
rpcResult(id, {
|
|
16562
16908
|
content: [{ type: "text", text: JSON.stringify(call.result) }],
|
|
@@ -16610,17 +16956,16 @@ async function openLocalImage(path) {
|
|
|
16610
16956
|
message: "\uC804\uCCB4 \uACBD\uB85C\uB97C \uC54C\uB824 \uC8FC\uC138\uC694(\uC608: /Users/me/photo.jpg)."
|
|
16611
16957
|
};
|
|
16612
16958
|
}
|
|
16613
|
-
let
|
|
16959
|
+
let initial;
|
|
16614
16960
|
try {
|
|
16615
|
-
|
|
16616
|
-
if (
|
|
16961
|
+
initial = await lstat(path);
|
|
16962
|
+
if (initial.isSymbolicLink()) {
|
|
16617
16963
|
return {
|
|
16618
16964
|
ok: false,
|
|
16619
16965
|
reason: "not_a_file",
|
|
16620
|
-
message: "\
|
|
16966
|
+
message: "\uC2EC\uBCFC\uB9AD \uB9C1\uD06C\uB294 \uC0AC\uC9C4 \uD30C\uC77C\uB85C \uC5F4 \uC218 \uC5C6\uC5B4\uC694."
|
|
16621
16967
|
};
|
|
16622
16968
|
}
|
|
16623
|
-
size = info.size;
|
|
16624
16969
|
} catch {
|
|
16625
16970
|
return {
|
|
16626
16971
|
ok: false,
|
|
@@ -16628,47 +16973,80 @@ async function openLocalImage(path) {
|
|
|
16628
16973
|
message: "\uADF8 \uACBD\uB85C\uC5D0\uC11C \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
16629
16974
|
};
|
|
16630
16975
|
}
|
|
16631
|
-
|
|
16632
|
-
return {
|
|
16633
|
-
ok: false,
|
|
16634
|
-
reason: "too_large",
|
|
16635
|
-
message: `\uC0AC\uC9C4\uC774 \uB108\uBB34 \uCEE4\uC694(\uCD5C\uB300 ${String(MAX_FILE_BYTES / 1024 / 1024)}MB).`
|
|
16636
|
-
};
|
|
16637
|
-
}
|
|
16638
|
-
if (size === 0) {
|
|
16639
|
-
return { ok: false, reason: "empty", message: "\uBE48 \uD30C\uC77C\uC774\uC5D0\uC694." };
|
|
16640
|
-
}
|
|
16641
|
-
let contentType = null;
|
|
16976
|
+
let handle;
|
|
16642
16977
|
try {
|
|
16643
|
-
const
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16978
|
+
const noFollow = constants.O_NOFOLLOW;
|
|
16979
|
+
if (process.platform !== "win32" && typeof noFollow !== "number") {
|
|
16980
|
+
return {
|
|
16981
|
+
ok: false,
|
|
16982
|
+
reason: "unreadable",
|
|
16983
|
+
message: "\uC774 \uD658\uACBD\uC5D0\uC11C\uB294 \uD30C\uC77C\uC744 \uC548\uC804\uD558\uAC8C \uC5F4 \uC218 \uC5C6\uC5B4\uC694."
|
|
16984
|
+
};
|
|
16985
|
+
}
|
|
16986
|
+
handle = await open(
|
|
16987
|
+
path,
|
|
16988
|
+
constants.O_RDONLY | (process.platform === "win32" ? 0 : noFollow)
|
|
16989
|
+
);
|
|
16990
|
+
const info = await handle.stat();
|
|
16991
|
+
if (initial.dev !== info.dev || initial.ino !== info.ino) {
|
|
16992
|
+
await handle.close();
|
|
16993
|
+
return {
|
|
16994
|
+
ok: false,
|
|
16995
|
+
reason: "unreadable",
|
|
16996
|
+
message: "\uD30C\uC77C\uC744 \uC5EC\uB294 \uB3D9\uC548 \uACBD\uB85C\uAC00 \uBC14\uB00C\uC5C8\uC5B4\uC694."
|
|
16997
|
+
};
|
|
16998
|
+
}
|
|
16999
|
+
if (!info.isFile()) {
|
|
17000
|
+
await handle.close();
|
|
17001
|
+
return {
|
|
17002
|
+
ok: false,
|
|
17003
|
+
reason: "not_a_file",
|
|
17004
|
+
message: "\uADF8 \uACBD\uB85C\uB294 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694."
|
|
17005
|
+
};
|
|
16650
17006
|
}
|
|
17007
|
+
if (info.size > MAX_FILE_BYTES) {
|
|
17008
|
+
await handle.close();
|
|
17009
|
+
return {
|
|
17010
|
+
ok: false,
|
|
17011
|
+
reason: "too_large",
|
|
17012
|
+
message: `\uC0AC\uC9C4\uC774 \uB108\uBB34 \uCEE4\uC694(\uCD5C\uB300 ${String(MAX_FILE_BYTES / 1024 / 1024)}MB).`
|
|
17013
|
+
};
|
|
17014
|
+
}
|
|
17015
|
+
if (info.size === 0) {
|
|
17016
|
+
await handle.close();
|
|
17017
|
+
return { ok: false, reason: "empty", message: "\uBE48 \uD30C\uC77C\uC774\uC5D0\uC694." };
|
|
17018
|
+
}
|
|
17019
|
+
const head = new Uint8Array(12);
|
|
17020
|
+
await handle.read(head, 0, head.byteLength, 0);
|
|
17021
|
+
const contentType = sniff(head);
|
|
17022
|
+
if (contentType === null) {
|
|
17023
|
+
await handle.close();
|
|
17024
|
+
return {
|
|
17025
|
+
ok: false,
|
|
17026
|
+
reason: "not_an_image",
|
|
17027
|
+
message: "\uC774\uBBF8\uC9C0 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694(PNG\xB7JPEG\xB7GIF\xB7WebP\uB9CC \uB123\uC744 \uC218 \uC788\uC5B4\uC694)."
|
|
17028
|
+
};
|
|
17029
|
+
}
|
|
17030
|
+
return {
|
|
17031
|
+
ok: true,
|
|
17032
|
+
stream: handle.createReadStream({
|
|
17033
|
+
autoClose: true,
|
|
17034
|
+
start: 0,
|
|
17035
|
+
end: info.size - 1
|
|
17036
|
+
}),
|
|
17037
|
+
contentType,
|
|
17038
|
+
byteLength: info.size,
|
|
17039
|
+
fileName: basename(path)
|
|
17040
|
+
};
|
|
16651
17041
|
} catch {
|
|
17042
|
+
await handle?.close().catch(() => {
|
|
17043
|
+
});
|
|
16652
17044
|
return {
|
|
16653
17045
|
ok: false,
|
|
16654
17046
|
reason: "unreadable",
|
|
16655
17047
|
message: "\uD30C\uC77C\uC744 \uC5F4\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
16656
17048
|
};
|
|
16657
17049
|
}
|
|
16658
|
-
if (contentType === null) {
|
|
16659
|
-
return {
|
|
16660
|
-
ok: false,
|
|
16661
|
-
reason: "not_an_image",
|
|
16662
|
-
message: "\uC774\uBBF8\uC9C0 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694(PNG\xB7JPEG\xB7GIF\xB7WebP\uB9CC \uB123\uC744 \uC218 \uC788\uC5B4\uC694)."
|
|
16663
|
-
};
|
|
16664
|
-
}
|
|
16665
|
-
return {
|
|
16666
|
-
ok: true,
|
|
16667
|
-
stream: createReadStream(path),
|
|
16668
|
-
contentType,
|
|
16669
|
-
byteLength: size,
|
|
16670
|
-
fileName: basename(path)
|
|
16671
|
-
};
|
|
16672
17050
|
}
|
|
16673
17051
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
16674
17052
|
var DEFAULT_TOOLS_CHANGED_DEBOUNCE_MS = 600;
|
|
@@ -16685,6 +17063,9 @@ function headerValue(raw) {
|
|
|
16685
17063
|
const trimmed = one?.trim();
|
|
16686
17064
|
return trimmed ? trimmed.slice(0, 128) : void 0;
|
|
16687
17065
|
}
|
|
17066
|
+
function correlationValue(value, max) {
|
|
17067
|
+
return /^[a-zA-Z0-9_-]+$/.test(value) ? value.slice(0, max) : "redacted";
|
|
17068
|
+
}
|
|
16688
17069
|
function createHttpBridge(opts) {
|
|
16689
17070
|
const host = opts.host ?? "127.0.0.1";
|
|
16690
17071
|
if (!isLoopbackHost(host)) {
|
|
@@ -16763,22 +17144,34 @@ function createHttpBridge(opts) {
|
|
|
16763
17144
|
`);
|
|
16764
17145
|
return true;
|
|
16765
17146
|
};
|
|
16766
|
-
const installStream = (key, res) => {
|
|
17147
|
+
const installStream = (key, sessionId, res) => {
|
|
16767
17148
|
if (streams.has(key)) {
|
|
16768
|
-
throw new Error(
|
|
17149
|
+
throw new Error("stream slot is still occupied");
|
|
16769
17150
|
}
|
|
16770
17151
|
const heartbeat = setInterval(() => {
|
|
16771
17152
|
send({ t: "hb", at: Date.now() }, key);
|
|
16772
17153
|
}, opts.heartbeatMs ?? HEARTBEAT_INTERVAL_MS);
|
|
16773
17154
|
heartbeat.unref?.();
|
|
16774
|
-
streams.set(key, { res, heartbeat });
|
|
17155
|
+
streams.set(key, { res, sessionId, heartbeat, fileStreams: /* @__PURE__ */ new Set() });
|
|
17156
|
+
if (!opts.bridge.activateSession(key, sessionId)) {
|
|
17157
|
+
clearInterval(heartbeat);
|
|
17158
|
+
deleteStreamEntry(key);
|
|
17159
|
+
throw new Error("stream session is no longer current");
|
|
17160
|
+
}
|
|
16775
17161
|
lifecycle?.retain?.();
|
|
16776
17162
|
};
|
|
17163
|
+
const deleteStreamEntry = (key) => {
|
|
17164
|
+
streams.delete(key);
|
|
17165
|
+
};
|
|
16777
17166
|
const removeStream = (key) => {
|
|
16778
17167
|
const stream = streams.get(key);
|
|
16779
17168
|
if (!stream) return null;
|
|
16780
17169
|
clearInterval(stream.heartbeat);
|
|
16781
|
-
|
|
17170
|
+
for (const fileStream of stream.fileStreams) {
|
|
17171
|
+
fileStream.destroy(new Error("browser session ended"));
|
|
17172
|
+
}
|
|
17173
|
+
stream.fileStreams.clear();
|
|
17174
|
+
deleteStreamEntry(key);
|
|
16782
17175
|
lifecycle?.release?.();
|
|
16783
17176
|
return stream.res;
|
|
16784
17177
|
};
|
|
@@ -16857,14 +17250,10 @@ function createHttpBridge(opts) {
|
|
|
16857
17250
|
return;
|
|
16858
17251
|
}
|
|
16859
17252
|
if (req.method === "GET" && url2.pathname === "/bridge/events") {
|
|
16860
|
-
if (!opts.bridge.connected) {
|
|
16861
|
-
json2(res, 403, { error: "handshake required" });
|
|
16862
|
-
return;
|
|
16863
|
-
}
|
|
16864
17253
|
const claimed = url2.searchParams.get("session");
|
|
16865
|
-
const key = claimed === null ? opts.bridge.
|
|
17254
|
+
const key = claimed === null ? opts.bridge.legacySessionKey() : opts.bridge.keyForSession(claimed);
|
|
16866
17255
|
if (key === null) {
|
|
16867
|
-
opts.log?.(
|
|
17256
|
+
opts.log?.("refused a stream for an unknown session");
|
|
16868
17257
|
res.writeHead(200, { "content-type": "text/event-stream" });
|
|
16869
17258
|
res.end();
|
|
16870
17259
|
return;
|
|
@@ -16873,7 +17262,7 @@ function createHttpBridge(opts) {
|
|
|
16873
17262
|
key,
|
|
16874
17263
|
bearerToken(req.headers.authorization)
|
|
16875
17264
|
)) {
|
|
16876
|
-
opts.log?.(
|
|
17265
|
+
opts.log?.("refused an unauthorised stream");
|
|
16877
17266
|
json2(res, 403, { error: "unauthorized" });
|
|
16878
17267
|
return;
|
|
16879
17268
|
}
|
|
@@ -16889,7 +17278,18 @@ function createHttpBridge(opts) {
|
|
|
16889
17278
|
// by nature.
|
|
16890
17279
|
"x-accel-buffering": "no"
|
|
16891
17280
|
});
|
|
16892
|
-
|
|
17281
|
+
const sessionId = claimed ?? opts.bridge.sessionForKey(key)?.sessionId ?? "";
|
|
17282
|
+
if (!sessionId) {
|
|
17283
|
+
res.end();
|
|
17284
|
+
return;
|
|
17285
|
+
}
|
|
17286
|
+
try {
|
|
17287
|
+
installStream(key, sessionId, res);
|
|
17288
|
+
} catch {
|
|
17289
|
+
res.end();
|
|
17290
|
+
return;
|
|
17291
|
+
}
|
|
17292
|
+
send({ t: "hb", at: Date.now() }, key);
|
|
16893
17293
|
if (key === opts.bridge.sessionKey) {
|
|
16894
17294
|
cancelToolsDrop();
|
|
16895
17295
|
notifyToolsChanged();
|
|
@@ -16952,29 +17352,115 @@ function createHttpBridge(opts) {
|
|
|
16952
17352
|
if (req.method === "POST" && url2.pathname === "/bridge/file") {
|
|
16953
17353
|
void readJson(req, MAX_BODY_BYTES).then(async (body) => {
|
|
16954
17354
|
const b = body;
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
)
|
|
16958
|
-
|
|
17355
|
+
const token = typeof b?.token === "string" ? b.token : "";
|
|
17356
|
+
const path = typeof b?.path === "string" ? b.path : "";
|
|
17357
|
+
const hasSessionId = b !== null && Object.hasOwn(b, "sessionId");
|
|
17358
|
+
const hasRequestId = b !== null && Object.hasOwn(b, "requestId");
|
|
17359
|
+
const invalidSessionId = hasSessionId && (typeof b?.sessionId !== "string" || b.sessionId.length === 0);
|
|
17360
|
+
const invalidRequestId = hasRequestId && (typeof b?.requestId !== "string" || b.requestId.length === 0);
|
|
17361
|
+
if (invalidSessionId || invalidRequestId) {
|
|
17362
|
+
json2(res, 403, {
|
|
17363
|
+
error: "unauthorized",
|
|
17364
|
+
message: "\uC0AC\uC9C4 \uC694\uCCAD\uC758 \uC5F0\uACB0 \uC815\uBCF4\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
17365
|
+
});
|
|
17366
|
+
return;
|
|
17367
|
+
}
|
|
17368
|
+
const auth = hasSessionId ? opts.bridge.authorizeFileRequest({
|
|
17369
|
+
sessionId: b.sessionId,
|
|
17370
|
+
...hasRequestId ? { requestId: b.requestId } : {},
|
|
17371
|
+
token,
|
|
17372
|
+
path,
|
|
17373
|
+
...typeof b?.tool === "string" ? { tool: b.tool } : {}
|
|
17374
|
+
}) : opts.bridge.authorizeLegacyFileCredential(token);
|
|
17375
|
+
if (!auth.ok) {
|
|
17376
|
+
opts.log?.(
|
|
17377
|
+
`file request refused: reason=${auth.reason} credential=${auth.credential} tokenPresent=${String(token.length > 0)} sessionMatch=${String(auth.key !== void 0)} open=not-entered`
|
|
17378
|
+
);
|
|
17379
|
+
json2(res, 403, {
|
|
17380
|
+
error: "unauthorized",
|
|
17381
|
+
message: "\uC0AC\uC9C4\uC744 \uAC00\uC838\uC62C \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
17382
|
+
});
|
|
16959
17383
|
return;
|
|
16960
17384
|
}
|
|
16961
|
-
const
|
|
16962
|
-
|
|
17385
|
+
const streamEntry = streams.get(auth.key);
|
|
17386
|
+
if (!streamEntry || streamEntry.sessionId !== auth.sessionId || !opts.bridge.isSessionStreamActive(auth.key, auth.sessionId) || !opts.bridge.consumeFileRequest(auth)) {
|
|
17387
|
+
opts.log?.(
|
|
17388
|
+
`file request refused: reason=file_session_not_live credential=${auth.credential} tokenPresent=true sessionMatch=false open=not-entered`
|
|
17389
|
+
);
|
|
17390
|
+
json2(res, 403, {
|
|
17391
|
+
error: "unauthorized",
|
|
17392
|
+
message: "\uC0AC\uC9C4\uC744 \uAC00\uC838\uC62C \uC5F0\uACB0\uC774 \uC5F4\uB824 \uC788\uC9C0 \uC54A\uC544\uC694."
|
|
17393
|
+
});
|
|
17394
|
+
return;
|
|
17395
|
+
}
|
|
17396
|
+
opts.log?.(
|
|
17397
|
+
`file request authorised: browser=${correlationValue(auth.key, 8)} session=${correlationValue(auth.sessionId, 32)} request=${auth.requestId ? correlationValue(auth.requestId, 96) : "legacy"} credential=${auth.credential} registryMatch=${String(auth.credential === "claimed_browser")} open=entered`
|
|
16963
17398
|
);
|
|
16964
|
-
|
|
16965
|
-
|
|
17399
|
+
let source = null;
|
|
17400
|
+
let owner = null;
|
|
17401
|
+
let peerClosed = false;
|
|
17402
|
+
const stop = () => {
|
|
17403
|
+
peerClosed = true;
|
|
17404
|
+
source?.destroy();
|
|
17405
|
+
};
|
|
17406
|
+
req.once("aborted", stop);
|
|
17407
|
+
res.once("close", stop);
|
|
17408
|
+
try {
|
|
17409
|
+
const out = await (opts.openLocalImage ?? openLocalImage)(path);
|
|
17410
|
+
if (!out.ok) {
|
|
17411
|
+
if (!peerClosed) {
|
|
17412
|
+
json2(res, 400, { error: out.reason, message: out.message });
|
|
17413
|
+
}
|
|
17414
|
+
return;
|
|
17415
|
+
}
|
|
17416
|
+
source = out.stream;
|
|
17417
|
+
if (peerClosed) {
|
|
17418
|
+
source.destroy();
|
|
17419
|
+
return;
|
|
17420
|
+
}
|
|
17421
|
+
const current = streams.get(auth.key);
|
|
17422
|
+
if (current !== streamEntry || current.sessionId !== auth.sessionId || !opts.bridge.isSessionStreamActive(auth.key, auth.sessionId)) {
|
|
17423
|
+
source.destroy(new Error("browser session ended"));
|
|
17424
|
+
json2(res, 403, {
|
|
17425
|
+
error: "unauthorized",
|
|
17426
|
+
message: "\uC0AC\uC9C4\uC744 \uAC00\uC838\uC62C \uC5F0\uACB0\uC774 \uC5F4\uB824 \uC788\uC9C0 \uC54A\uC544\uC694."
|
|
17427
|
+
});
|
|
17428
|
+
return;
|
|
17429
|
+
}
|
|
17430
|
+
res.writeHead(200, {
|
|
17431
|
+
"content-type": out.contentType,
|
|
17432
|
+
"content-length": String(out.byteLength),
|
|
17433
|
+
"x-file-name": encodeURIComponent(out.fileName),
|
|
17434
|
+
"cache-control": "no-store"
|
|
17435
|
+
});
|
|
17436
|
+
owner = current;
|
|
17437
|
+
owner.fileStreams.add(source);
|
|
17438
|
+
await pipeline(source, res);
|
|
17439
|
+
} catch {
|
|
17440
|
+
if (!res.writableEnded) res.destroy();
|
|
17441
|
+
} finally {
|
|
17442
|
+
req.off("aborted", stop);
|
|
17443
|
+
res.off("close", stop);
|
|
17444
|
+
if (source) owner?.fileStreams.delete(source);
|
|
17445
|
+
}
|
|
17446
|
+
}).catch(() => json2(res, 400, { error: "bad request" }));
|
|
17447
|
+
return;
|
|
17448
|
+
}
|
|
17449
|
+
if (req.method === "POST" && url2.pathname === "/bridge/file/cancel") {
|
|
17450
|
+
void readJson(req, MAX_BODY_BYTES).then((body) => {
|
|
17451
|
+
const b = body;
|
|
17452
|
+
if (typeof b?.token !== "string" || typeof b.sessionId !== "string" || typeof b.requestId !== "string") {
|
|
17453
|
+
json2(res, 403, { error: "unauthorized" });
|
|
16966
17454
|
return;
|
|
16967
17455
|
}
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
|
|
16973
|
-
|
|
16974
|
-
|
|
17456
|
+
const reason = opts.bridge.cancelFileRequest({
|
|
17457
|
+
token: b.token,
|
|
17458
|
+
sessionId: b.sessionId,
|
|
17459
|
+
requestId: b.requestId
|
|
17460
|
+
});
|
|
17461
|
+
json2(res, reason === null ? 200 : 403, {
|
|
17462
|
+
...reason === null ? { ok: true } : { error: "unauthorized" }
|
|
16975
17463
|
});
|
|
16976
|
-
out.stream.pipe(res);
|
|
16977
|
-
out.stream.on("error", () => res.destroy());
|
|
16978
17464
|
}).catch(() => json2(res, 400, { error: "bad request" }));
|
|
16979
17465
|
return;
|
|
16980
17466
|
}
|
|
@@ -17056,6 +17542,12 @@ function createHttpBridge(opts) {
|
|
|
17056
17542
|
return {
|
|
17057
17543
|
server,
|
|
17058
17544
|
send,
|
|
17545
|
+
disconnect: (key) => {
|
|
17546
|
+
const wasPrimary = key === opts.bridge.sessionKey;
|
|
17547
|
+
removeStream(key)?.end();
|
|
17548
|
+
opts.bridge.disconnect(key);
|
|
17549
|
+
if (wasPrimary) scheduleToolsDrop();
|
|
17550
|
+
},
|
|
17059
17551
|
close: () => new Promise((resolve) => {
|
|
17060
17552
|
cancelToolsDrop();
|
|
17061
17553
|
opts.bridge.disconnectAll();
|
|
@@ -17308,7 +17800,7 @@ function isRegistration(row) {
|
|
|
17308
17800
|
const r = row;
|
|
17309
17801
|
const id = r["browserId"];
|
|
17310
17802
|
const hash2 = r["tokenHash"];
|
|
17311
|
-
return (id === null || typeof id === "string") && typeof hash2 === "string" && hash2
|
|
17803
|
+
return (id === null || typeof id === "string" && id.length > 0) && typeof hash2 === "string" && /^[a-f0-9]{64}$/.test(hash2) && typeof r["label"] === "string" && typeof r["createdAt"] === "string";
|
|
17312
17804
|
}
|
|
17313
17805
|
function readLedger(path) {
|
|
17314
17806
|
let raw;
|
|
@@ -17332,8 +17824,20 @@ function readLedger(path) {
|
|
|
17332
17824
|
if (!parsed.every(isRegistration)) {
|
|
17333
17825
|
return { state: "unreadable", reason: "a row is not a browser" };
|
|
17334
17826
|
}
|
|
17827
|
+
const browserIds = /* @__PURE__ */ new Set();
|
|
17828
|
+
const tokenHashes = /* @__PURE__ */ new Set();
|
|
17829
|
+
for (const row of parsed) {
|
|
17830
|
+
if (tokenHashes.has(row.tokenHash) || row.browserId !== null && browserIds.has(row.browserId)) {
|
|
17831
|
+
return { state: "unreadable", reason: "duplicate browser authority" };
|
|
17832
|
+
}
|
|
17833
|
+
tokenHashes.add(row.tokenHash);
|
|
17834
|
+
if (row.browserId !== null) browserIds.add(row.browserId);
|
|
17835
|
+
}
|
|
17335
17836
|
return { state: "ok", rows: parsed };
|
|
17336
17837
|
}
|
|
17838
|
+
function ledgerOverlapsToken(rows, tokenHash) {
|
|
17839
|
+
return rows.some((row) => row.tokenHash === tokenHash);
|
|
17840
|
+
}
|
|
17337
17841
|
async function writeLedger(path, claims) {
|
|
17338
17842
|
const queued = (writing.get(path) ?? Promise.resolve()).then(
|
|
17339
17843
|
() => writeOnce(path, claims),
|
|
@@ -17385,7 +17889,11 @@ function createBrowserRegistry(opts) {
|
|
|
17385
17889
|
if (read.state === "unreadable") {
|
|
17386
17890
|
throw new Error(`browser registry is unreadable: ${read.reason}`);
|
|
17387
17891
|
}
|
|
17388
|
-
|
|
17892
|
+
const rows = read.state === "ok" ? [...read.rows] : [];
|
|
17893
|
+
if (opts.forbiddenTokenHash && ledgerOverlapsToken(rows, opts.forbiddenTokenHash)) {
|
|
17894
|
+
throw new Error("browser registry overlaps the root authority");
|
|
17895
|
+
}
|
|
17896
|
+
return rows;
|
|
17389
17897
|
};
|
|
17390
17898
|
return {
|
|
17391
17899
|
async add(label) {
|
|
@@ -17436,7 +17944,10 @@ function createBrowserRegistry(opts) {
|
|
|
17436
17944
|
for (const claim of claims) {
|
|
17437
17945
|
if (!matches(claim, handle)) continue;
|
|
17438
17946
|
revoked.add(claim.tokenHash);
|
|
17439
|
-
if (claim.browserId !== null)
|
|
17947
|
+
if (claim.browserId !== null) {
|
|
17948
|
+
opts.claimed.delete(claim.browserId);
|
|
17949
|
+
opts.onRevoke?.(claim.browserId);
|
|
17950
|
+
}
|
|
17440
17951
|
}
|
|
17441
17952
|
return true;
|
|
17442
17953
|
},
|
|
@@ -17471,7 +17982,9 @@ function runDaemon(env, deps = {}) {
|
|
|
17471
17982
|
const claimedBrowsers = /* @__PURE__ */ new Map();
|
|
17472
17983
|
const browserLabels = /* @__PURE__ */ new Map();
|
|
17473
17984
|
const ledger = readLedger(ledgerPath(deps.home));
|
|
17474
|
-
|
|
17985
|
+
const rootTokenHash = digestToken(token);
|
|
17986
|
+
const ledgerAuthorityValid = ledger.state !== "ok" || !ledgerOverlapsToken(ledger.rows, rootTokenHash);
|
|
17987
|
+
if (ledger.state === "ok" && ledgerAuthorityValid) {
|
|
17475
17988
|
for (const [id, hash2] of claimedDigestsFrom(ledger.rows)) {
|
|
17476
17989
|
claimedBrowsers.set(id, hash2);
|
|
17477
17990
|
}
|
|
@@ -17484,6 +17997,9 @@ function runDaemon(env, deps = {}) {
|
|
|
17484
17997
|
`browser registry is unreadable (${ledger.reason}); registering is off`
|
|
17485
17998
|
);
|
|
17486
17999
|
}
|
|
18000
|
+
if (!ledgerAuthorityValid) {
|
|
18001
|
+
log("browser registry overlaps the root authority; registering is off");
|
|
18002
|
+
}
|
|
17487
18003
|
const bridge = new Bridge({
|
|
17488
18004
|
send: (frame, key) => http.send(frame, key),
|
|
17489
18005
|
// Step aside once our own token looks stale. This process is detached
|
|
@@ -17522,7 +18038,9 @@ function runDaemon(env, deps = {}) {
|
|
|
17522
18038
|
const mediaStage = createMediaStage({ root: join4(stateDir, "media") });
|
|
17523
18039
|
const registry2 = createBrowserRegistry({
|
|
17524
18040
|
path: ledgerPath(deps.home),
|
|
17525
|
-
claimed: claimedBrowsers
|
|
18041
|
+
claimed: claimedBrowsers,
|
|
18042
|
+
forbiddenTokenHash: rootTokenHash,
|
|
18043
|
+
onRevoke: (browserId) => http.disconnect(browserId)
|
|
17526
18044
|
});
|
|
17527
18045
|
const http = createHttpBridge({
|
|
17528
18046
|
bridge,
|