@modootoday/datalab-extension-mcp 1.8.5 → 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-HTFKS44A.js → browsers-RT4ZPGXZ.js} +1 -1
- package/dist/{chunk-SN25XJOG.js → chunk-JOEFFC3W.js} +51 -17
- package/dist/{chunk-KV7HFKJB.js → chunk-YTMY2DAN.js} +670 -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,8 +15662,13 @@ 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) => ({
|
|
15668
|
+
id,
|
|
15669
|
+
name: this.#deps.browserLabels.get(id) ?? null,
|
|
15670
|
+
primary: primary === id
|
|
15671
|
+
}));
|
|
15623
15672
|
}
|
|
15624
15673
|
/**
|
|
15625
15674
|
* The only place a registry is born, and it is born already in the map.
|
|
@@ -15645,6 +15694,10 @@ var Bridge = class {
|
|
|
15645
15694
|
session,
|
|
15646
15695
|
tokenHash: auth.tokenHash,
|
|
15647
15696
|
authRequired: auth.authRequired,
|
|
15697
|
+
routingRequired: auth.routingRequired,
|
|
15698
|
+
fileRequestBound: auth.fileRequestBound,
|
|
15699
|
+
streamActive: false,
|
|
15700
|
+
filePermits: /* @__PURE__ */ new Map(),
|
|
15648
15701
|
pending: new PendingRegistry({
|
|
15649
15702
|
now: this.#deps.now,
|
|
15650
15703
|
timeoutMs: this.#deps.timeoutMs,
|
|
@@ -15689,9 +15742,25 @@ var Bridge = class {
|
|
|
15689
15742
|
}
|
|
15690
15743
|
return null;
|
|
15691
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
|
+
}
|
|
15692
15751
|
/** Is a panel connected and ready to serve tool calls? */
|
|
15693
15752
|
get connected() {
|
|
15694
|
-
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;
|
|
15695
15764
|
}
|
|
15696
15765
|
/**
|
|
15697
15766
|
* The last catalog the panel reported, empty if it has never connected in
|
|
@@ -15719,18 +15788,161 @@ var Bridge = class {
|
|
|
15719
15788
|
* the same as that browser being here, and this gates a route that reads
|
|
15720
15789
|
* files off the operator's disk.
|
|
15721
15790
|
*/
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
if (this.
|
|
15725
|
-
return
|
|
15791
|
+
#fileCredential(entry, presented) {
|
|
15792
|
+
const hash2 = digestToken(presented);
|
|
15793
|
+
if (!timingSafeEqual(hash2, entry.tokenHash) || this.#deps.wasRevoked(hash2)) {
|
|
15794
|
+
return null;
|
|
15726
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";
|
|
15727
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);
|
|
15728
15819
|
for (const entry of this.#sessions.values()) {
|
|
15729
|
-
const
|
|
15730
|
-
if (
|
|
15731
|
-
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
|
+
}
|
|
15732
15830
|
}
|
|
15733
|
-
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;
|
|
15734
15946
|
}
|
|
15735
15947
|
/**
|
|
15736
15948
|
* The extension is right about the token, and we can be wrong about it.
|
|
@@ -15832,7 +16044,16 @@ var Bridge = class {
|
|
|
15832
16044
|
const authRequired = parsed.data.capabilities.includes(
|
|
15833
16045
|
BRIDGE_AUTH_CAPABILITY
|
|
15834
16046
|
);
|
|
15835
|
-
|
|
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
|
+
});
|
|
15836
16057
|
const limit = this.#deps.rateLimit ?? DEFAULT_RATE_LIMIT;
|
|
15837
16058
|
this.#bucketTokens = limit.capacity;
|
|
15838
16059
|
this.#bucketRefilledAt = (this.#deps.now ?? Date.now)();
|
|
@@ -15843,7 +16064,11 @@ var Bridge = class {
|
|
|
15843
16064
|
sessionId: session.sessionId,
|
|
15844
16065
|
// Echoed only when it is actually on, so the panel can tell a server that
|
|
15845
16066
|
// is holding it to the header from one that predates the gate entirely.
|
|
15846
|
-
capabilities:
|
|
16067
|
+
capabilities: [
|
|
16068
|
+
...authRequired ? [BRIDGE_AUTH_CAPABILITY] : [],
|
|
16069
|
+
...routingRequired ? [BROWSER_ROUTING_CAPABILITY] : [],
|
|
16070
|
+
...fileRequestBound ? [LOCAL_FILE_REQUEST_CAPABILITY] : []
|
|
16071
|
+
]
|
|
15847
16072
|
};
|
|
15848
16073
|
}
|
|
15849
16074
|
/**
|
|
@@ -15892,21 +16117,27 @@ var Bridge = class {
|
|
|
15892
16117
|
return this.#settle(
|
|
15893
16118
|
frame.id,
|
|
15894
16119
|
{ ok: true, result: frame.tools },
|
|
15895
|
-
presented
|
|
16120
|
+
presented,
|
|
16121
|
+
frame.route?.browserId
|
|
15896
16122
|
);
|
|
15897
16123
|
}
|
|
15898
16124
|
if (frame.ok) {
|
|
15899
|
-
|
|
16125
|
+
const settled2 = this.#settle(
|
|
15900
16126
|
frame.id,
|
|
15901
16127
|
{
|
|
15902
16128
|
ok: true,
|
|
15903
16129
|
result: frame.result,
|
|
15904
16130
|
...frame.ms === void 0 ? {} : { ms: frame.ms }
|
|
15905
16131
|
},
|
|
15906
|
-
presented
|
|
16132
|
+
presented,
|
|
16133
|
+
frame.route?.browserId
|
|
15907
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;
|
|
15908
16139
|
}
|
|
15909
|
-
|
|
16140
|
+
const settled = this.#settle(
|
|
15910
16141
|
frame.id,
|
|
15911
16142
|
{
|
|
15912
16143
|
ok: false,
|
|
@@ -15914,8 +16145,11 @@ var Bridge = class {
|
|
|
15914
16145
|
message: frame.message,
|
|
15915
16146
|
...frame.ms === void 0 ? {} : { ms: frame.ms }
|
|
15916
16147
|
},
|
|
15917
|
-
presented
|
|
16148
|
+
presented,
|
|
16149
|
+
frame.route?.browserId
|
|
15918
16150
|
);
|
|
16151
|
+
if (settled === "settled") this.#discardFilePermit(frame.id);
|
|
16152
|
+
return settled;
|
|
15919
16153
|
}
|
|
15920
16154
|
/**
|
|
15921
16155
|
* The id is the only axis a reply carries, and the prefixes make it unique
|
|
@@ -15927,15 +16161,28 @@ var Bridge = class {
|
|
|
15927
16161
|
* mint the same one — and would report "unknown", which reads as an ordinary
|
|
15928
16162
|
* late reply.
|
|
15929
16163
|
*/
|
|
15930
|
-
#settle(id, outcome, presented) {
|
|
16164
|
+
#settle(id, outcome, presented, browserId) {
|
|
15931
16165
|
for (const entry of this.#sessions.values()) {
|
|
15932
16166
|
if (!entry.pending.owns(id)) continue;
|
|
15933
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
|
+
}
|
|
15934
16176
|
entry.pending.settle(id, outcome);
|
|
15935
16177
|
return "settled";
|
|
15936
16178
|
}
|
|
15937
16179
|
return "dropped";
|
|
15938
16180
|
}
|
|
16181
|
+
#discardFilePermit(id) {
|
|
16182
|
+
for (const entry of this.#sessions.values()) {
|
|
16183
|
+
if (entry.filePermits.delete(id)) return;
|
|
16184
|
+
}
|
|
16185
|
+
}
|
|
15939
16186
|
/** One panel went away. Nothing it owed can land, so settle that session now. */
|
|
15940
16187
|
disconnect(key) {
|
|
15941
16188
|
this.#closeSession(
|
|
@@ -15953,6 +16200,10 @@ var Bridge = class {
|
|
|
15953
16200
|
let expired = 0;
|
|
15954
16201
|
for (const entry of this.#sessions.values()) {
|
|
15955
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
|
+
}
|
|
15956
16207
|
}
|
|
15957
16208
|
return expired;
|
|
15958
16209
|
}
|
|
@@ -15963,10 +16214,11 @@ var Bridge = class {
|
|
|
15963
16214
|
* shape, catalog size — with no membership check. Membership is enforced
|
|
15964
16215
|
* where the session and the execution live, in the panel's own executor.
|
|
15965
16216
|
*/
|
|
15966
|
-
async listTools() {
|
|
15967
|
-
const out = await this.#request(
|
|
15968
|
-
method: "tools/list"
|
|
15969
|
-
|
|
16217
|
+
async listTools(browser) {
|
|
16218
|
+
const out = await this.#request(
|
|
16219
|
+
{ method: "tools/list" },
|
|
16220
|
+
browser
|
|
16221
|
+
);
|
|
15970
16222
|
if (!out.ok) throw new BridgeError(out.reason, out.message);
|
|
15971
16223
|
const { tools, rejected } = projectTools(out.result);
|
|
15972
16224
|
if (tools.length > 0) {
|
|
@@ -15986,13 +16238,16 @@ var Bridge = class {
|
|
|
15986
16238
|
* `bridgeMs - toolMs` is the transport and the queue. A single total cannot
|
|
15987
16239
|
* tell a slow tool from a slow wire, and that is the question being asked.
|
|
15988
16240
|
*/
|
|
15989
|
-
async callTool(name, args) {
|
|
16241
|
+
async callTool(name, args, browser) {
|
|
15990
16242
|
const startedAt = (this.#deps.now ?? Date.now)();
|
|
15991
|
-
const out = await this.#request(
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
16243
|
+
const out = await this.#request(
|
|
16244
|
+
{
|
|
16245
|
+
method: "tools/call",
|
|
16246
|
+
name,
|
|
16247
|
+
args
|
|
16248
|
+
},
|
|
16249
|
+
browser
|
|
16250
|
+
);
|
|
15996
16251
|
const bridgeMs = (this.#deps.now ?? Date.now)() - startedAt;
|
|
15997
16252
|
if (!out.ok) {
|
|
15998
16253
|
throw new BridgeError(out.reason, out.message, {
|
|
@@ -16026,8 +16281,28 @@ var Bridge = class {
|
|
|
16026
16281
|
this.#bucketTokens -= 1;
|
|
16027
16282
|
return true;
|
|
16028
16283
|
}
|
|
16029
|
-
async #request(req) {
|
|
16030
|
-
|
|
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
|
+
}
|
|
16031
16306
|
if (!entry) {
|
|
16032
16307
|
return {
|
|
16033
16308
|
ok: false,
|
|
@@ -16043,23 +16318,47 @@ var Bridge = class {
|
|
|
16043
16318
|
};
|
|
16044
16319
|
}
|
|
16045
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
|
+
}
|
|
16046
16339
|
const settled = entry.pending.register(id);
|
|
16047
16340
|
const delivered = this.#deps.send(
|
|
16048
16341
|
{
|
|
16049
16342
|
t: "req",
|
|
16050
16343
|
id,
|
|
16051
|
-
...req
|
|
16344
|
+
...req,
|
|
16345
|
+
...routing ? { routing } : {}
|
|
16052
16346
|
},
|
|
16053
16347
|
entry.key
|
|
16054
16348
|
);
|
|
16055
16349
|
if (!delivered) {
|
|
16350
|
+
entry.filePermits.delete(id);
|
|
16056
16351
|
entry.pending.settle(id, {
|
|
16057
16352
|
ok: false,
|
|
16058
16353
|
reason: "not_connected",
|
|
16059
16354
|
message: BRIDGE_USER_MESSAGES.panelClosed
|
|
16060
16355
|
});
|
|
16061
16356
|
}
|
|
16062
|
-
|
|
16357
|
+
const outcome = await settled;
|
|
16358
|
+
if (outcome.ok && (browser !== void 0 || this.#defaultKey === null)) {
|
|
16359
|
+
this.#defaultKey = entry.key;
|
|
16360
|
+
}
|
|
16361
|
+
return outcome;
|
|
16063
16362
|
}
|
|
16064
16363
|
};
|
|
16065
16364
|
var BridgeError = class extends Error {
|
|
@@ -16440,8 +16739,43 @@ function timingMeta(timing) {
|
|
|
16440
16739
|
}
|
|
16441
16740
|
};
|
|
16442
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
|
+
]);
|
|
16443
16749
|
function ownTools(stage) {
|
|
16444
|
-
|
|
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;
|
|
16445
16779
|
}
|
|
16446
16780
|
function rpcToolError(id, err) {
|
|
16447
16781
|
const policy = policyFor(err.reason);
|
|
@@ -16483,7 +16817,7 @@ function frameMcpResponse(payload, accept) {
|
|
|
16483
16817
|
function offlineCatalog(bridge, stage, id, accept) {
|
|
16484
16818
|
const base = bridge.lastKnownTools.length > 0 ? bridge.lastKnownTools : staticDiscoveryCatalog();
|
|
16485
16819
|
return frameMcpResponse(
|
|
16486
|
-
rpcResult(id, { tools:
|
|
16820
|
+
rpcResult(id, { tools: withOwnTools(base, stage) }),
|
|
16487
16821
|
accept
|
|
16488
16822
|
);
|
|
16489
16823
|
}
|
|
@@ -16504,7 +16838,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16504
16838
|
try {
|
|
16505
16839
|
const { tools } = await bridge.listTools();
|
|
16506
16840
|
return frameMcpResponse(
|
|
16507
|
-
rpcResult(id, { tools:
|
|
16841
|
+
rpcResult(id, { tools: withOwnTools(tools, stage) }),
|
|
16508
16842
|
accept
|
|
16509
16843
|
);
|
|
16510
16844
|
} catch (err) {
|
|
@@ -16523,7 +16857,23 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16523
16857
|
accept
|
|
16524
16858
|
);
|
|
16525
16859
|
}
|
|
16526
|
-
|
|
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
|
+
}
|
|
16527
16877
|
if (params.name === BROWSERS_TOOL) {
|
|
16528
16878
|
return frameMcpResponse(
|
|
16529
16879
|
rpcResult(id, {
|
|
@@ -16552,7 +16902,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16552
16902
|
);
|
|
16553
16903
|
}
|
|
16554
16904
|
try {
|
|
16555
|
-
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);
|
|
16556
16906
|
return frameMcpResponse(
|
|
16557
16907
|
rpcResult(id, {
|
|
16558
16908
|
content: [{ type: "text", text: JSON.stringify(call.result) }],
|
|
@@ -16606,17 +16956,16 @@ async function openLocalImage(path) {
|
|
|
16606
16956
|
message: "\uC804\uCCB4 \uACBD\uB85C\uB97C \uC54C\uB824 \uC8FC\uC138\uC694(\uC608: /Users/me/photo.jpg)."
|
|
16607
16957
|
};
|
|
16608
16958
|
}
|
|
16609
|
-
let
|
|
16959
|
+
let initial;
|
|
16610
16960
|
try {
|
|
16611
|
-
|
|
16612
|
-
if (
|
|
16961
|
+
initial = await lstat(path);
|
|
16962
|
+
if (initial.isSymbolicLink()) {
|
|
16613
16963
|
return {
|
|
16614
16964
|
ok: false,
|
|
16615
16965
|
reason: "not_a_file",
|
|
16616
|
-
message: "\
|
|
16966
|
+
message: "\uC2EC\uBCFC\uB9AD \uB9C1\uD06C\uB294 \uC0AC\uC9C4 \uD30C\uC77C\uB85C \uC5F4 \uC218 \uC5C6\uC5B4\uC694."
|
|
16617
16967
|
};
|
|
16618
16968
|
}
|
|
16619
|
-
size = info.size;
|
|
16620
16969
|
} catch {
|
|
16621
16970
|
return {
|
|
16622
16971
|
ok: false,
|
|
@@ -16624,47 +16973,80 @@ async function openLocalImage(path) {
|
|
|
16624
16973
|
message: "\uADF8 \uACBD\uB85C\uC5D0\uC11C \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
16625
16974
|
};
|
|
16626
16975
|
}
|
|
16627
|
-
|
|
16628
|
-
return {
|
|
16629
|
-
ok: false,
|
|
16630
|
-
reason: "too_large",
|
|
16631
|
-
message: `\uC0AC\uC9C4\uC774 \uB108\uBB34 \uCEE4\uC694(\uCD5C\uB300 ${String(MAX_FILE_BYTES / 1024 / 1024)}MB).`
|
|
16632
|
-
};
|
|
16633
|
-
}
|
|
16634
|
-
if (size === 0) {
|
|
16635
|
-
return { ok: false, reason: "empty", message: "\uBE48 \uD30C\uC77C\uC774\uC5D0\uC694." };
|
|
16636
|
-
}
|
|
16637
|
-
let contentType = null;
|
|
16976
|
+
let handle;
|
|
16638
16977
|
try {
|
|
16639
|
-
const
|
|
16640
|
-
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16645
|
-
|
|
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
|
+
};
|
|
16646
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
|
+
};
|
|
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
|
+
};
|
|
16647
17041
|
} catch {
|
|
17042
|
+
await handle?.close().catch(() => {
|
|
17043
|
+
});
|
|
16648
17044
|
return {
|
|
16649
17045
|
ok: false,
|
|
16650
17046
|
reason: "unreadable",
|
|
16651
17047
|
message: "\uD30C\uC77C\uC744 \uC5F4\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
16652
17048
|
};
|
|
16653
17049
|
}
|
|
16654
|
-
if (contentType === null) {
|
|
16655
|
-
return {
|
|
16656
|
-
ok: false,
|
|
16657
|
-
reason: "not_an_image",
|
|
16658
|
-
message: "\uC774\uBBF8\uC9C0 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694(PNG\xB7JPEG\xB7GIF\xB7WebP\uB9CC \uB123\uC744 \uC218 \uC788\uC5B4\uC694)."
|
|
16659
|
-
};
|
|
16660
|
-
}
|
|
16661
|
-
return {
|
|
16662
|
-
ok: true,
|
|
16663
|
-
stream: createReadStream(path),
|
|
16664
|
-
contentType,
|
|
16665
|
-
byteLength: size,
|
|
16666
|
-
fileName: basename(path)
|
|
16667
|
-
};
|
|
16668
17050
|
}
|
|
16669
17051
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
16670
17052
|
var DEFAULT_TOOLS_CHANGED_DEBOUNCE_MS = 600;
|
|
@@ -16681,6 +17063,9 @@ function headerValue(raw) {
|
|
|
16681
17063
|
const trimmed = one?.trim();
|
|
16682
17064
|
return trimmed ? trimmed.slice(0, 128) : void 0;
|
|
16683
17065
|
}
|
|
17066
|
+
function correlationValue(value, max) {
|
|
17067
|
+
return /^[a-zA-Z0-9_-]+$/.test(value) ? value.slice(0, max) : "redacted";
|
|
17068
|
+
}
|
|
16684
17069
|
function createHttpBridge(opts) {
|
|
16685
17070
|
const host = opts.host ?? "127.0.0.1";
|
|
16686
17071
|
if (!isLoopbackHost(host)) {
|
|
@@ -16759,22 +17144,34 @@ function createHttpBridge(opts) {
|
|
|
16759
17144
|
`);
|
|
16760
17145
|
return true;
|
|
16761
17146
|
};
|
|
16762
|
-
const installStream = (key, res) => {
|
|
17147
|
+
const installStream = (key, sessionId, res) => {
|
|
16763
17148
|
if (streams.has(key)) {
|
|
16764
|
-
throw new Error(
|
|
17149
|
+
throw new Error("stream slot is still occupied");
|
|
16765
17150
|
}
|
|
16766
17151
|
const heartbeat = setInterval(() => {
|
|
16767
17152
|
send({ t: "hb", at: Date.now() }, key);
|
|
16768
17153
|
}, opts.heartbeatMs ?? HEARTBEAT_INTERVAL_MS);
|
|
16769
17154
|
heartbeat.unref?.();
|
|
16770
|
-
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
|
+
}
|
|
16771
17161
|
lifecycle?.retain?.();
|
|
16772
17162
|
};
|
|
17163
|
+
const deleteStreamEntry = (key) => {
|
|
17164
|
+
streams.delete(key);
|
|
17165
|
+
};
|
|
16773
17166
|
const removeStream = (key) => {
|
|
16774
17167
|
const stream = streams.get(key);
|
|
16775
17168
|
if (!stream) return null;
|
|
16776
17169
|
clearInterval(stream.heartbeat);
|
|
16777
|
-
|
|
17170
|
+
for (const fileStream of stream.fileStreams) {
|
|
17171
|
+
fileStream.destroy(new Error("browser session ended"));
|
|
17172
|
+
}
|
|
17173
|
+
stream.fileStreams.clear();
|
|
17174
|
+
deleteStreamEntry(key);
|
|
16778
17175
|
lifecycle?.release?.();
|
|
16779
17176
|
return stream.res;
|
|
16780
17177
|
};
|
|
@@ -16853,14 +17250,10 @@ function createHttpBridge(opts) {
|
|
|
16853
17250
|
return;
|
|
16854
17251
|
}
|
|
16855
17252
|
if (req.method === "GET" && url2.pathname === "/bridge/events") {
|
|
16856
|
-
if (!opts.bridge.connected) {
|
|
16857
|
-
json2(res, 403, { error: "handshake required" });
|
|
16858
|
-
return;
|
|
16859
|
-
}
|
|
16860
17253
|
const claimed = url2.searchParams.get("session");
|
|
16861
|
-
const key = claimed === null ? opts.bridge.
|
|
17254
|
+
const key = claimed === null ? opts.bridge.legacySessionKey() : opts.bridge.keyForSession(claimed);
|
|
16862
17255
|
if (key === null) {
|
|
16863
|
-
opts.log?.(
|
|
17256
|
+
opts.log?.("refused a stream for an unknown session");
|
|
16864
17257
|
res.writeHead(200, { "content-type": "text/event-stream" });
|
|
16865
17258
|
res.end();
|
|
16866
17259
|
return;
|
|
@@ -16869,7 +17262,7 @@ function createHttpBridge(opts) {
|
|
|
16869
17262
|
key,
|
|
16870
17263
|
bearerToken(req.headers.authorization)
|
|
16871
17264
|
)) {
|
|
16872
|
-
opts.log?.(
|
|
17265
|
+
opts.log?.("refused an unauthorised stream");
|
|
16873
17266
|
json2(res, 403, { error: "unauthorized" });
|
|
16874
17267
|
return;
|
|
16875
17268
|
}
|
|
@@ -16885,7 +17278,18 @@ function createHttpBridge(opts) {
|
|
|
16885
17278
|
// by nature.
|
|
16886
17279
|
"x-accel-buffering": "no"
|
|
16887
17280
|
});
|
|
16888
|
-
|
|
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);
|
|
16889
17293
|
if (key === opts.bridge.sessionKey) {
|
|
16890
17294
|
cancelToolsDrop();
|
|
16891
17295
|
notifyToolsChanged();
|
|
@@ -16948,29 +17352,115 @@ function createHttpBridge(opts) {
|
|
|
16948
17352
|
if (req.method === "POST" && url2.pathname === "/bridge/file") {
|
|
16949
17353
|
void readJson(req, MAX_BODY_BYTES).then(async (body) => {
|
|
16950
17354
|
const b = body;
|
|
16951
|
-
|
|
16952
|
-
|
|
16953
|
-
)
|
|
16954
|
-
|
|
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
|
+
});
|
|
16955
17366
|
return;
|
|
16956
17367
|
}
|
|
16957
|
-
const
|
|
16958
|
-
|
|
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
|
+
});
|
|
17383
|
+
return;
|
|
17384
|
+
}
|
|
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`
|
|
16959
17398
|
);
|
|
16960
|
-
|
|
16961
|
-
|
|
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" });
|
|
16962
17454
|
return;
|
|
16963
17455
|
}
|
|
16964
|
-
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
|
|
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" }
|
|
16971
17463
|
});
|
|
16972
|
-
out.stream.pipe(res);
|
|
16973
|
-
out.stream.on("error", () => res.destroy());
|
|
16974
17464
|
}).catch(() => json2(res, 400, { error: "bad request" }));
|
|
16975
17465
|
return;
|
|
16976
17466
|
}
|
|
@@ -17052,6 +17542,12 @@ function createHttpBridge(opts) {
|
|
|
17052
17542
|
return {
|
|
17053
17543
|
server,
|
|
17054
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
|
+
},
|
|
17055
17551
|
close: () => new Promise((resolve) => {
|
|
17056
17552
|
cancelToolsDrop();
|
|
17057
17553
|
opts.bridge.disconnectAll();
|
|
@@ -17304,7 +17800,7 @@ function isRegistration(row) {
|
|
|
17304
17800
|
const r = row;
|
|
17305
17801
|
const id = r["browserId"];
|
|
17306
17802
|
const hash2 = r["tokenHash"];
|
|
17307
|
-
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";
|
|
17308
17804
|
}
|
|
17309
17805
|
function readLedger(path) {
|
|
17310
17806
|
let raw;
|
|
@@ -17328,8 +17824,20 @@ function readLedger(path) {
|
|
|
17328
17824
|
if (!parsed.every(isRegistration)) {
|
|
17329
17825
|
return { state: "unreadable", reason: "a row is not a browser" };
|
|
17330
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
|
+
}
|
|
17331
17836
|
return { state: "ok", rows: parsed };
|
|
17332
17837
|
}
|
|
17838
|
+
function ledgerOverlapsToken(rows, tokenHash) {
|
|
17839
|
+
return rows.some((row) => row.tokenHash === tokenHash);
|
|
17840
|
+
}
|
|
17333
17841
|
async function writeLedger(path, claims) {
|
|
17334
17842
|
const queued = (writing.get(path) ?? Promise.resolve()).then(
|
|
17335
17843
|
() => writeOnce(path, claims),
|
|
@@ -17381,7 +17889,11 @@ function createBrowserRegistry(opts) {
|
|
|
17381
17889
|
if (read.state === "unreadable") {
|
|
17382
17890
|
throw new Error(`browser registry is unreadable: ${read.reason}`);
|
|
17383
17891
|
}
|
|
17384
|
-
|
|
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;
|
|
17385
17897
|
};
|
|
17386
17898
|
return {
|
|
17387
17899
|
async add(label) {
|
|
@@ -17432,7 +17944,10 @@ function createBrowserRegistry(opts) {
|
|
|
17432
17944
|
for (const claim of claims) {
|
|
17433
17945
|
if (!matches(claim, handle)) continue;
|
|
17434
17946
|
revoked.add(claim.tokenHash);
|
|
17435
|
-
if (claim.browserId !== null)
|
|
17947
|
+
if (claim.browserId !== null) {
|
|
17948
|
+
opts.claimed.delete(claim.browserId);
|
|
17949
|
+
opts.onRevoke?.(claim.browserId);
|
|
17950
|
+
}
|
|
17436
17951
|
}
|
|
17437
17952
|
return true;
|
|
17438
17953
|
},
|
|
@@ -17465,17 +17980,26 @@ function runDaemon(env, deps = {}) {
|
|
|
17465
17980
|
}
|
|
17466
17981
|
});
|
|
17467
17982
|
const claimedBrowsers = /* @__PURE__ */ new Map();
|
|
17983
|
+
const browserLabels = /* @__PURE__ */ new Map();
|
|
17468
17984
|
const ledger = readLedger(ledgerPath(deps.home));
|
|
17469
|
-
|
|
17985
|
+
const rootTokenHash = digestToken(token);
|
|
17986
|
+
const ledgerAuthorityValid = ledger.state !== "ok" || !ledgerOverlapsToken(ledger.rows, rootTokenHash);
|
|
17987
|
+
if (ledger.state === "ok" && ledgerAuthorityValid) {
|
|
17470
17988
|
for (const [id, hash2] of claimedDigestsFrom(ledger.rows)) {
|
|
17471
17989
|
claimedBrowsers.set(id, hash2);
|
|
17472
17990
|
}
|
|
17991
|
+
for (const row of ledger.rows) {
|
|
17992
|
+
if (row.browserId !== null) browserLabels.set(row.browserId, row.label);
|
|
17993
|
+
}
|
|
17473
17994
|
}
|
|
17474
17995
|
if (ledger.state === "unreadable") {
|
|
17475
17996
|
log(
|
|
17476
17997
|
`browser registry is unreadable (${ledger.reason}); registering is off`
|
|
17477
17998
|
);
|
|
17478
17999
|
}
|
|
18000
|
+
if (!ledgerAuthorityValid) {
|
|
18001
|
+
log("browser registry overlaps the root authority; registering is off");
|
|
18002
|
+
}
|
|
17479
18003
|
const bridge = new Bridge({
|
|
17480
18004
|
send: (frame, key) => http.send(frame, key),
|
|
17481
18005
|
// Step aside once our own token looks stale. This process is detached
|
|
@@ -17492,7 +18016,21 @@ function runDaemon(env, deps = {}) {
|
|
|
17492
18016
|
// Empty until a browser is registered, and nothing issues registrations
|
|
17493
18017
|
// yet — so every panel still shares the one slot it always had.
|
|
17494
18018
|
claimedBrowsers,
|
|
17495
|
-
|
|
18019
|
+
browserLabels,
|
|
18020
|
+
claimBrowser: (browserId, tokenHash) => {
|
|
18021
|
+
const took = registry2.claim(browserId, tokenHash);
|
|
18022
|
+
if (took) {
|
|
18023
|
+
const now = readLedger(ledgerPath(deps.home));
|
|
18024
|
+
if (now.state === "ok") {
|
|
18025
|
+
for (const row of now.rows) {
|
|
18026
|
+
if (row.browserId === browserId) {
|
|
18027
|
+
browserLabels.set(browserId, row.label);
|
|
18028
|
+
}
|
|
18029
|
+
}
|
|
18030
|
+
}
|
|
18031
|
+
}
|
|
18032
|
+
return took;
|
|
18033
|
+
},
|
|
17496
18034
|
wasRevoked: (tokenHash) => registry2.wasRevoked(tokenHash),
|
|
17497
18035
|
log
|
|
17498
18036
|
});
|
|
@@ -17500,7 +18038,9 @@ function runDaemon(env, deps = {}) {
|
|
|
17500
18038
|
const mediaStage = createMediaStage({ root: join4(stateDir, "media") });
|
|
17501
18039
|
const registry2 = createBrowserRegistry({
|
|
17502
18040
|
path: ledgerPath(deps.home),
|
|
17503
|
-
claimed: claimedBrowsers
|
|
18041
|
+
claimed: claimedBrowsers,
|
|
18042
|
+
forbiddenTokenHash: rootTokenHash,
|
|
18043
|
+
onRevoke: (browserId) => http.disconnect(browserId)
|
|
17504
18044
|
});
|
|
17505
18045
|
const http = createHttpBridge({
|
|
17506
18046
|
bridge,
|