@modootoday/datalab-extension-mcp 1.8.6 → 1.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/dist/{browsers-LYXYQPZD.js → browsers-KBFQARNA.js} +1 -1
- package/dist/{chunk-FR64UB36.js → chunk-DR3Q6KT2.js} +54 -18
- package/dist/{chunk-TPYHNPF4.js → chunk-U7AIWUSN.js} +661 -143
- 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-YXH45HHP.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,
|
|
@@ -14386,6 +14387,7 @@ function date4(params) {
|
|
|
14386
14387
|
return /* @__PURE__ */ _coercedDate(ZodDate, params);
|
|
14387
14388
|
}
|
|
14388
14389
|
config(en_default());
|
|
14390
|
+
var BRIDGE_PROTOCOL_VERSION = 1;
|
|
14389
14391
|
var SUPPORTED_PROTOCOL_VERSIONS = [1];
|
|
14390
14392
|
var MCP_TOOL_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
14391
14393
|
var BridgeToolDescriptorSchema = external_exports.object({
|
|
@@ -14436,6 +14438,8 @@ var HelloSchema = external_exports.object({
|
|
|
14436
14438
|
capabilities: external_exports.array(external_exports.string()).default([])
|
|
14437
14439
|
});
|
|
14438
14440
|
var BRIDGE_AUTH_CAPABILITY = "bridge-auth-bearer";
|
|
14441
|
+
var BROWSER_ROUTING_CAPABILITY = "browser-routing-v1";
|
|
14442
|
+
var LOCAL_FILE_REQUEST_CAPABILITY = "local-file-request-v1";
|
|
14439
14443
|
function bearerToken(header) {
|
|
14440
14444
|
if (typeof header !== "string") return null;
|
|
14441
14445
|
const match = /^Bearer (.+)$/i.exec(header.trim());
|
|
@@ -14457,8 +14461,19 @@ var HelloNackReasonSchema = external_exports.enum([
|
|
|
14457
14461
|
/** No version in common. `supported` tells the extension what to do. */
|
|
14458
14462
|
"version_mismatch",
|
|
14459
14463
|
/** Origin header absent or not our extension. */
|
|
14460
|
-
"forbidden_origin"
|
|
14464
|
+
"forbidden_origin",
|
|
14465
|
+
/** This browser already has an active event stream. */
|
|
14466
|
+
"session_active"
|
|
14461
14467
|
]);
|
|
14468
|
+
var BrowserRoutingSchema = external_exports.object({
|
|
14469
|
+
targetBrowserId: external_exports.string(),
|
|
14470
|
+
explicit: external_exports.boolean(),
|
|
14471
|
+
fallbackFromPinned: external_exports.boolean(),
|
|
14472
|
+
connectedBrowserCount: external_exports.number().int().nonnegative()
|
|
14473
|
+
});
|
|
14474
|
+
var BrowserRouteEchoSchema = external_exports.object({
|
|
14475
|
+
browserId: external_exports.string()
|
|
14476
|
+
});
|
|
14462
14477
|
var HelloNackSchema = external_exports.object({
|
|
14463
14478
|
t: external_exports.literal("hello_nack"),
|
|
14464
14479
|
reason: HelloNackReasonSchema,
|
|
@@ -14471,7 +14486,8 @@ var BridgeRequestSchema = external_exports.discriminatedUnion("method", [
|
|
|
14471
14486
|
external_exports.object({
|
|
14472
14487
|
t: external_exports.literal("req"),
|
|
14473
14488
|
id: external_exports.string(),
|
|
14474
|
-
method: external_exports.literal("tools/list")
|
|
14489
|
+
method: external_exports.literal("tools/list"),
|
|
14490
|
+
routing: BrowserRoutingSchema.optional()
|
|
14475
14491
|
}),
|
|
14476
14492
|
external_exports.object({
|
|
14477
14493
|
t: external_exports.literal("req"),
|
|
@@ -14479,7 +14495,8 @@ var BridgeRequestSchema = external_exports.discriminatedUnion("method", [
|
|
|
14479
14495
|
method: external_exports.literal("tools/call"),
|
|
14480
14496
|
/** Must be on the read-only allowlist — enforced extension-side. */
|
|
14481
14497
|
name: external_exports.string(),
|
|
14482
|
-
args: external_exports.record(external_exports.string(), external_exports.unknown())
|
|
14498
|
+
args: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
14499
|
+
routing: BrowserRoutingSchema.optional()
|
|
14483
14500
|
})
|
|
14484
14501
|
]);
|
|
14485
14502
|
var elapsedMs = external_exports.number().nonnegative().optional();
|
|
@@ -14489,7 +14506,8 @@ var BridgeResultSchema = external_exports.union([
|
|
|
14489
14506
|
id: external_exports.string(),
|
|
14490
14507
|
ok: external_exports.literal(true),
|
|
14491
14508
|
result: external_exports.unknown(),
|
|
14492
|
-
ms: elapsedMs
|
|
14509
|
+
ms: elapsedMs,
|
|
14510
|
+
route: BrowserRouteEchoSchema.optional()
|
|
14493
14511
|
}),
|
|
14494
14512
|
external_exports.object({
|
|
14495
14513
|
t: external_exports.literal("res"),
|
|
@@ -14499,7 +14517,8 @@ var BridgeResultSchema = external_exports.union([
|
|
|
14499
14517
|
message: external_exports.string(),
|
|
14500
14518
|
// Carried on failure too: the timing of a call that FAILED is exactly
|
|
14501
14519
|
// what tells a timeout apart from a refusal.
|
|
14502
|
-
ms: elapsedMs
|
|
14520
|
+
ms: elapsedMs,
|
|
14521
|
+
route: BrowserRouteEchoSchema.optional()
|
|
14503
14522
|
})
|
|
14504
14523
|
]);
|
|
14505
14524
|
var BridgeHeartbeatSchema = external_exports.object({
|
|
@@ -14517,7 +14536,8 @@ var BridgeUpstreamSchema = external_exports.union([
|
|
|
14517
14536
|
external_exports.object({
|
|
14518
14537
|
t: external_exports.literal("tools"),
|
|
14519
14538
|
id: external_exports.string(),
|
|
14520
|
-
tools: external_exports.array(BridgeToolDescriptorSchema)
|
|
14539
|
+
tools: external_exports.array(BridgeToolDescriptorSchema),
|
|
14540
|
+
route: BrowserRouteEchoSchema.optional()
|
|
14521
14541
|
})
|
|
14522
14542
|
]);
|
|
14523
14543
|
var UPSTREAM_FRAME_TYPES = ["res", "tools"];
|
|
@@ -14647,6 +14667,7 @@ function resolveCredential(presented, known) {
|
|
|
14647
14667
|
return root.ok ? { kind: "root" } : REFUSED;
|
|
14648
14668
|
}
|
|
14649
14669
|
var READ_ONLY_TOOLS = [
|
|
14670
|
+
"datalab_browsers",
|
|
14650
14671
|
// --- Search-ad estimates. Uses the user's own advertiser API credentials. ---
|
|
14651
14672
|
"ad_average_position_bid",
|
|
14652
14673
|
"ad_competition_density",
|
|
@@ -14988,6 +15009,10 @@ var REASON_POLICY = Object.freeze({
|
|
|
14988
15009
|
*/
|
|
14989
15010
|
disconnected: Object.freeze({ retryable: true }),
|
|
14990
15011
|
superseded: Object.freeze({ retryable: true }),
|
|
15012
|
+
browser_detached: Object.freeze({
|
|
15013
|
+
retryable: true,
|
|
15014
|
+
retryAfterMs: 1e3
|
|
15015
|
+
}),
|
|
14991
15016
|
// --- The request itself must change ---
|
|
14992
15017
|
unknown_tool: Object.freeze({ retryable: false }),
|
|
14993
15018
|
invalid_args: Object.freeze({ retryable: false }),
|
|
@@ -15004,6 +15029,7 @@ var REASON_POLICY = Object.freeze({
|
|
|
15004
15029
|
// --- The connection itself must be repaired ---
|
|
15005
15030
|
not_connected: Object.freeze({ retryable: false }),
|
|
15006
15031
|
unauthorized: Object.freeze({ retryable: false }),
|
|
15032
|
+
browser_unknown: Object.freeze({ retryable: false }),
|
|
15007
15033
|
// --- Staging: the request must change, except where nobody knows yet ---
|
|
15008
15034
|
stale_fence: Object.freeze({ retryable: false }),
|
|
15009
15035
|
already_sealed: Object.freeze({ retryable: false }),
|
|
@@ -15164,6 +15190,7 @@ var BY_PREFIX = [
|
|
|
15164
15190
|
];
|
|
15165
15191
|
var GALLERY = ["gallery_image_add"];
|
|
15166
15192
|
var EXPLICIT = [
|
|
15193
|
+
{ slug: "connection", tools: ["datalab_browsers"] },
|
|
15167
15194
|
{ slug: "gallery", tools: GALLERY },
|
|
15168
15195
|
{ slug: "blog-brief", tools: BLOG_BRIEF },
|
|
15169
15196
|
{ slug: "blog-insights", tools: BLOG_INSIGHTS },
|
|
@@ -15174,6 +15201,11 @@ var EXPLICIT = [
|
|
|
15174
15201
|
{ slug: "benchmark", tools: ["benchmark_gap"] }
|
|
15175
15202
|
];
|
|
15176
15203
|
var META = [
|
|
15204
|
+
{
|
|
15205
|
+
slug: "connection",
|
|
15206
|
+
label: "\uC5F0\uACB0 \uBE0C\uB77C\uC6B0\uC800",
|
|
15207
|
+
summary: "\uC5EC\uB7EC \uBE0C\uB77C\uC6B0\uC800\xB7\uD504\uB85C\uD544 \uC911 \uB3C4\uAD6C\uB97C \uC2E4\uD589\uD560 \uACF3\uC744 \uACE0\uB978\uB2E4"
|
|
15208
|
+
},
|
|
15177
15209
|
{
|
|
15178
15210
|
slug: "blog-brief",
|
|
15179
15211
|
label: "\uC624\uB298\uC758 \uBE14\uB85C\uADF8 \uBE0C\uB9AC\uD551",
|
|
@@ -15310,6 +15342,12 @@ var CALL_TOOL = "datalab_call";
|
|
|
15310
15342
|
var CONFIRM_STATUS_TOOL = "datalab_confirm_status";
|
|
15311
15343
|
var SESSION_STATE_TOOL = "datalab_session_state";
|
|
15312
15344
|
var BROWSERS_TOOL = "datalab_browsers";
|
|
15345
|
+
function browserProperty() {
|
|
15346
|
+
return {
|
|
15347
|
+
type: "string",
|
|
15348
|
+
description: `${BROWSERS_TOOL} \uAC00 \uB3CC\uB824\uC900 \uC2E4\uD589 \uB300\uC0C1 \uBE0C\uB77C\uC6B0\uC800 id`
|
|
15349
|
+
};
|
|
15350
|
+
}
|
|
15313
15351
|
var DESCRIBE_PAGE_SIZE = 10;
|
|
15314
15352
|
function discoveryDescriptors(entries) {
|
|
15315
15353
|
const sets = buildToolsets(entries.map((e) => e.name));
|
|
@@ -15334,7 +15372,8 @@ function discoveryDescriptors(entries) {
|
|
|
15334
15372
|
withSchemas: {
|
|
15335
15373
|
type: "boolean",
|
|
15336
15374
|
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
|
-
}
|
|
15375
|
+
},
|
|
15376
|
+
browser: browserProperty()
|
|
15338
15377
|
},
|
|
15339
15378
|
additionalProperties: false
|
|
15340
15379
|
}
|
|
@@ -15351,7 +15390,8 @@ function discoveryDescriptors(entries) {
|
|
|
15351
15390
|
items: { type: "string" },
|
|
15352
15391
|
maxItems: DESCRIBE_PAGE_SIZE,
|
|
15353
15392
|
description: `\uC2A4\uD0A4\uB9C8\uB97C \uBCFC \uB3C4\uAD6C \uC774\uB984 (\uD55C \uBC88\uC5D0 ${DESCRIBE_PAGE_SIZE}\uAC1C\uAE4C\uC9C0)`
|
|
15354
|
-
}
|
|
15393
|
+
},
|
|
15394
|
+
browser: browserProperty()
|
|
15355
15395
|
},
|
|
15356
15396
|
required: ["tools"],
|
|
15357
15397
|
additionalProperties: false
|
|
@@ -15369,7 +15409,8 @@ function discoveryDescriptors(entries) {
|
|
|
15369
15409
|
args: {
|
|
15370
15410
|
type: "object",
|
|
15371
15411
|
description: `\uADF8 \uB3C4\uAD6C\uC758 \uC778\uC790. ${DESCRIBE_TOOL} \uC758 \uC2A4\uD0A4\uB9C8\uB97C \uB530\uB978\uB2E4`
|
|
15372
|
-
}
|
|
15412
|
+
},
|
|
15413
|
+
browser: browserProperty()
|
|
15373
15414
|
},
|
|
15374
15415
|
required: ["tool"],
|
|
15375
15416
|
additionalProperties: false
|
|
@@ -15385,7 +15426,8 @@ function discoveryDescriptors(entries) {
|
|
|
15385
15426
|
ticket: {
|
|
15386
15427
|
type: "string",
|
|
15387
15428
|
description: `${CALL_TOOL} \uC774 \uB3CC\uB824\uC900 ticket \uAC12 \uADF8\uB300\uB85C`
|
|
15388
|
-
}
|
|
15429
|
+
},
|
|
15430
|
+
browser: browserProperty()
|
|
15389
15431
|
},
|
|
15390
15432
|
required: ["ticket"],
|
|
15391
15433
|
additionalProperties: false
|
|
@@ -15399,7 +15441,7 @@ function sessionStateDescriptor() {
|
|
|
15399
15441
|
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
15442
|
inputSchema: {
|
|
15401
15443
|
type: "object",
|
|
15402
|
-
properties: {},
|
|
15444
|
+
properties: { browser: browserProperty() },
|
|
15403
15445
|
additionalProperties: false
|
|
15404
15446
|
},
|
|
15405
15447
|
annotations: LOOKUP_ANNOTATIONS
|
|
@@ -15408,7 +15450,7 @@ function sessionStateDescriptor() {
|
|
|
15408
15450
|
function browsersDescriptor() {
|
|
15409
15451
|
return {
|
|
15410
15452
|
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 \
|
|
15453
|
+
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
15454
|
inputSchema: {
|
|
15413
15455
|
type: "object",
|
|
15414
15456
|
properties: {},
|
|
@@ -15421,11 +15463,9 @@ function allowlistNames() {
|
|
|
15421
15463
|
return [.../* @__PURE__ */ new Set([...READ_ONLY_TOOLS, ...Object.keys(TOOL_TIERS)])];
|
|
15422
15464
|
}
|
|
15423
15465
|
function staticDiscoveryCatalog() {
|
|
15424
|
-
const entries = allowlistNames().map(
|
|
15425
|
-
name,
|
|
15426
|
-
|
|
15427
|
-
inputSchema: {}
|
|
15428
|
-
}));
|
|
15466
|
+
const entries = allowlistNames().map(
|
|
15467
|
+
(name) => name === BROWSERS_TOOL ? browsersDescriptor() : { name, description: "", inputSchema: {} }
|
|
15468
|
+
);
|
|
15429
15469
|
return [
|
|
15430
15470
|
...discoveryDescriptors(entries),
|
|
15431
15471
|
sessionStateDescriptor(),
|
|
@@ -15590,6 +15630,7 @@ var Bridge = class {
|
|
|
15590
15630
|
#sessions = /* @__PURE__ */ new Map();
|
|
15591
15631
|
#idTag = daemonIdTag();
|
|
15592
15632
|
#sessionSeq = 0;
|
|
15633
|
+
#defaultKey = null;
|
|
15593
15634
|
/** Consecutive handshakes refused for our own token. See `#noteStaleToken`. */
|
|
15594
15635
|
#badTokenStreak = 0;
|
|
15595
15636
|
#bucketTokens = 0;
|
|
@@ -15607,7 +15648,13 @@ var Bridge = class {
|
|
|
15607
15648
|
* single key there is never more than one to choose between.
|
|
15608
15649
|
*/
|
|
15609
15650
|
#primary() {
|
|
15610
|
-
|
|
15651
|
+
if (this.#defaultKey !== null) {
|
|
15652
|
+
const held = this.#sessions.get(this.#defaultKey);
|
|
15653
|
+
if (held?.streamActive) return held;
|
|
15654
|
+
}
|
|
15655
|
+
for (const entry of this.#sessions.values()) {
|
|
15656
|
+
if (entry.streamActive) return entry;
|
|
15657
|
+
}
|
|
15611
15658
|
return null;
|
|
15612
15659
|
}
|
|
15613
15660
|
/**
|
|
@@ -15618,11 +15665,12 @@ var Bridge = class {
|
|
|
15618
15665
|
* landing in the older one and never learn why the newer one saw nothing.
|
|
15619
15666
|
*/
|
|
15620
15667
|
connectedBrowsers() {
|
|
15621
|
-
const keys = [...this.#sessions.
|
|
15622
|
-
|
|
15668
|
+
const keys = [...this.#sessions.values()].filter((entry) => entry.streamActive).map((entry) => entry.key);
|
|
15669
|
+
const primary = this.#defaultKey === null ? this.#primary()?.key ?? null : this.#defaultKey;
|
|
15670
|
+
return keys.map((id) => ({
|
|
15623
15671
|
id,
|
|
15624
15672
|
name: this.#deps.browserLabels.get(id) ?? null,
|
|
15625
|
-
primary:
|
|
15673
|
+
primary: primary === id
|
|
15626
15674
|
}));
|
|
15627
15675
|
}
|
|
15628
15676
|
/**
|
|
@@ -15649,6 +15697,10 @@ var Bridge = class {
|
|
|
15649
15697
|
session,
|
|
15650
15698
|
tokenHash: auth.tokenHash,
|
|
15651
15699
|
authRequired: auth.authRequired,
|
|
15700
|
+
routingRequired: auth.routingRequired,
|
|
15701
|
+
fileRequestBound: auth.fileRequestBound,
|
|
15702
|
+
streamActive: false,
|
|
15703
|
+
filePermits: /* @__PURE__ */ new Map(),
|
|
15652
15704
|
pending: new PendingRegistry({
|
|
15653
15705
|
now: this.#deps.now,
|
|
15654
15706
|
timeoutMs: this.#deps.timeoutMs,
|
|
@@ -15693,9 +15745,26 @@ var Bridge = class {
|
|
|
15693
15745
|
}
|
|
15694
15746
|
return null;
|
|
15695
15747
|
}
|
|
15748
|
+
sessionForKey(key) {
|
|
15749
|
+
return this.#sessions.get(key)?.session ?? null;
|
|
15750
|
+
}
|
|
15751
|
+
legacySessionKey() {
|
|
15752
|
+
return this.#sessions.has(LEGACY_SESSION_KEY) ? LEGACY_SESSION_KEY : null;
|
|
15753
|
+
}
|
|
15696
15754
|
/** Is a panel connected and ready to serve tool calls? */
|
|
15697
15755
|
get connected() {
|
|
15698
|
-
return this.#
|
|
15756
|
+
return this.#primary() !== null;
|
|
15757
|
+
}
|
|
15758
|
+
activateSession(key, sessionId) {
|
|
15759
|
+
const entry = this.#sessions.get(key);
|
|
15760
|
+
if (!entry || entry.session.sessionId !== sessionId) return false;
|
|
15761
|
+
entry.streamActive = true;
|
|
15762
|
+
if (this.#defaultKey === null) this.#defaultKey = key;
|
|
15763
|
+
return true;
|
|
15764
|
+
}
|
|
15765
|
+
isSessionStreamActive(key, sessionId) {
|
|
15766
|
+
const entry = this.#sessions.get(key);
|
|
15767
|
+
return entry?.session.sessionId === sessionId && entry.streamActive;
|
|
15699
15768
|
}
|
|
15700
15769
|
/**
|
|
15701
15770
|
* The last catalog the panel reported, empty if it has never connected in
|
|
@@ -15723,18 +15792,161 @@ var Bridge = class {
|
|
|
15723
15792
|
* the same as that browser being here, and this gates a route that reads
|
|
15724
15793
|
* files off the operator's disk.
|
|
15725
15794
|
*/
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
if (this.
|
|
15729
|
-
return
|
|
15795
|
+
#fileCredential(entry, presented) {
|
|
15796
|
+
const hash2 = digestToken(presented);
|
|
15797
|
+
if (!timingSafeEqual(hash2, entry.tokenHash) || this.#deps.wasRevoked(hash2)) {
|
|
15798
|
+
return null;
|
|
15799
|
+
}
|
|
15800
|
+
if (entry.key === LEGACY_SESSION_KEY) {
|
|
15801
|
+
return this.verifyRootToken(presented) ? "root" : null;
|
|
15730
15802
|
}
|
|
15803
|
+
const current = this.#deps.claimedBrowsers.get(entry.key);
|
|
15804
|
+
return current !== void 0 && timingSafeEqual(hash2, current) ? "claimed_browser" : null;
|
|
15805
|
+
}
|
|
15806
|
+
#fileCredentialClass(presented) {
|
|
15807
|
+
if (this.verifyRootToken(presented)) return "root";
|
|
15731
15808
|
const hash2 = digestToken(presented);
|
|
15809
|
+
for (const current of this.#deps.claimedBrowsers.values()) {
|
|
15810
|
+
if (timingSafeEqual(hash2, current)) return "claimed_browser";
|
|
15811
|
+
}
|
|
15812
|
+
return "unknown";
|
|
15813
|
+
}
|
|
15814
|
+
authorizeLegacyFileCredential(presented) {
|
|
15815
|
+
if (typeof presented !== "string" || presented.length === 0) {
|
|
15816
|
+
return {
|
|
15817
|
+
ok: false,
|
|
15818
|
+
reason: "file_session_token_missing",
|
|
15819
|
+
credential: "missing"
|
|
15820
|
+
};
|
|
15821
|
+
}
|
|
15822
|
+
const credential = this.#fileCredentialClass(presented);
|
|
15732
15823
|
for (const entry of this.#sessions.values()) {
|
|
15733
|
-
const
|
|
15734
|
-
if (
|
|
15735
|
-
return
|
|
15824
|
+
const accepted = this.#fileCredential(entry, presented);
|
|
15825
|
+
if (accepted && !entry.fileRequestBound) {
|
|
15826
|
+
return {
|
|
15827
|
+
ok: true,
|
|
15828
|
+
key: entry.key,
|
|
15829
|
+
sessionId: entry.session.sessionId,
|
|
15830
|
+
credential: accepted,
|
|
15831
|
+
requestBound: false
|
|
15832
|
+
};
|
|
15833
|
+
}
|
|
15736
15834
|
}
|
|
15737
|
-
return
|
|
15835
|
+
return {
|
|
15836
|
+
ok: false,
|
|
15837
|
+
reason: "file_session_token_mismatch",
|
|
15838
|
+
credential
|
|
15839
|
+
};
|
|
15840
|
+
}
|
|
15841
|
+
authorizeFileRequest(input) {
|
|
15842
|
+
const entry = [...this.#sessions.values()].find(
|
|
15843
|
+
(candidate) => candidate.session.sessionId === input.sessionId
|
|
15844
|
+
);
|
|
15845
|
+
if (!entry) {
|
|
15846
|
+
return {
|
|
15847
|
+
ok: false,
|
|
15848
|
+
reason: "file_session_not_live",
|
|
15849
|
+
credential: typeof input.token === "string" && input.token.length > 0 ? this.#fileCredentialClass(input.token) : "missing"
|
|
15850
|
+
};
|
|
15851
|
+
}
|
|
15852
|
+
if (typeof input.token !== "string" || input.token.length === 0) {
|
|
15853
|
+
return {
|
|
15854
|
+
ok: false,
|
|
15855
|
+
reason: "file_session_token_missing",
|
|
15856
|
+
credential: "missing",
|
|
15857
|
+
key: entry.key,
|
|
15858
|
+
sessionId: entry.session.sessionId,
|
|
15859
|
+
...input.requestId === void 0 ? {} : { requestId: input.requestId }
|
|
15860
|
+
};
|
|
15861
|
+
}
|
|
15862
|
+
const credential = this.#fileCredential(entry, input.token);
|
|
15863
|
+
if (!credential) {
|
|
15864
|
+
return {
|
|
15865
|
+
ok: false,
|
|
15866
|
+
reason: "file_session_token_mismatch",
|
|
15867
|
+
credential: this.#fileCredentialClass(input.token),
|
|
15868
|
+
key: entry.key,
|
|
15869
|
+
sessionId: entry.session.sessionId,
|
|
15870
|
+
...input.requestId === void 0 ? {} : { requestId: input.requestId }
|
|
15871
|
+
};
|
|
15872
|
+
}
|
|
15873
|
+
if (!entry.fileRequestBound) {
|
|
15874
|
+
return {
|
|
15875
|
+
ok: true,
|
|
15876
|
+
key: entry.key,
|
|
15877
|
+
sessionId: entry.session.sessionId,
|
|
15878
|
+
...input.requestId === void 0 ? {} : { requestId: input.requestId },
|
|
15879
|
+
credential,
|
|
15880
|
+
requestBound: false
|
|
15881
|
+
};
|
|
15882
|
+
}
|
|
15883
|
+
const now = (this.#deps.now ?? Date.now)();
|
|
15884
|
+
for (const [requestId, permit2] of entry.filePermits) {
|
|
15885
|
+
if (permit2.expiresAt <= now) entry.filePermits.delete(requestId);
|
|
15886
|
+
}
|
|
15887
|
+
if (input.requestId === void 0) {
|
|
15888
|
+
return {
|
|
15889
|
+
ok: false,
|
|
15890
|
+
reason: "file_request_expired",
|
|
15891
|
+
credential,
|
|
15892
|
+
key: entry.key,
|
|
15893
|
+
sessionId: entry.session.sessionId
|
|
15894
|
+
};
|
|
15895
|
+
}
|
|
15896
|
+
const permit = entry.filePermits.get(input.requestId);
|
|
15897
|
+
if (!permit) {
|
|
15898
|
+
return {
|
|
15899
|
+
ok: false,
|
|
15900
|
+
reason: "file_request_expired",
|
|
15901
|
+
credential,
|
|
15902
|
+
key: entry.key,
|
|
15903
|
+
sessionId: entry.session.sessionId,
|
|
15904
|
+
requestId: input.requestId
|
|
15905
|
+
};
|
|
15906
|
+
}
|
|
15907
|
+
if (permit.path !== input.path || permit.tool !== input.tool) {
|
|
15908
|
+
return {
|
|
15909
|
+
ok: false,
|
|
15910
|
+
reason: "file_request_mismatch",
|
|
15911
|
+
credential,
|
|
15912
|
+
key: entry.key,
|
|
15913
|
+
sessionId: entry.session.sessionId,
|
|
15914
|
+
requestId: input.requestId
|
|
15915
|
+
};
|
|
15916
|
+
}
|
|
15917
|
+
return {
|
|
15918
|
+
ok: true,
|
|
15919
|
+
key: entry.key,
|
|
15920
|
+
sessionId: entry.session.sessionId,
|
|
15921
|
+
requestId: input.requestId,
|
|
15922
|
+
credential,
|
|
15923
|
+
requestBound: true
|
|
15924
|
+
};
|
|
15925
|
+
}
|
|
15926
|
+
consumeFileRequest(auth) {
|
|
15927
|
+
const entry = this.#sessions.get(auth.key);
|
|
15928
|
+
if (!entry || !entry.streamActive || entry.session.sessionId !== auth.sessionId) {
|
|
15929
|
+
return false;
|
|
15930
|
+
}
|
|
15931
|
+
if (!auth.requestBound) return true;
|
|
15932
|
+
if (auth.requestId === void 0 || !entry.filePermits.has(auth.requestId)) {
|
|
15933
|
+
return false;
|
|
15934
|
+
}
|
|
15935
|
+
entry.filePermits.delete(auth.requestId);
|
|
15936
|
+
return true;
|
|
15937
|
+
}
|
|
15938
|
+
cancelFileRequest(input) {
|
|
15939
|
+
const entry = [...this.#sessions.values()].find(
|
|
15940
|
+
(candidate) => candidate.session.sessionId === input.sessionId
|
|
15941
|
+
);
|
|
15942
|
+
if (!entry || !entry.streamActive) return "file_session_not_live";
|
|
15943
|
+
if (typeof input.token !== "string" || !this.#fileCredential(entry, input.token)) {
|
|
15944
|
+
return "file_session_token_mismatch";
|
|
15945
|
+
}
|
|
15946
|
+
if (!entry.filePermits.delete(input.requestId)) {
|
|
15947
|
+
return "file_request_expired";
|
|
15948
|
+
}
|
|
15949
|
+
return null;
|
|
15738
15950
|
}
|
|
15739
15951
|
/**
|
|
15740
15952
|
* The extension is right about the token, and we can be wrong about it.
|
|
@@ -15825,6 +16037,14 @@ var Bridge = class {
|
|
|
15825
16037
|
};
|
|
15826
16038
|
}
|
|
15827
16039
|
const key = sessionKeyFor(credential);
|
|
16040
|
+
if (this.#sessions.get(key)?.streamActive) {
|
|
16041
|
+
return {
|
|
16042
|
+
t: "hello_nack",
|
|
16043
|
+
reason: "session_active",
|
|
16044
|
+
supported: [BRIDGE_PROTOCOL_VERSION],
|
|
16045
|
+
message: "This browser already has an active panel session."
|
|
16046
|
+
};
|
|
16047
|
+
}
|
|
15828
16048
|
const session = {
|
|
15829
16049
|
// A counter, not a timestamp: a panel reload produces two handshakes
|
|
15830
16050
|
// inside one millisecond, and the new session must never be
|
|
@@ -15836,7 +16056,16 @@ var Bridge = class {
|
|
|
15836
16056
|
const authRequired = parsed.data.capabilities.includes(
|
|
15837
16057
|
BRIDGE_AUTH_CAPABILITY
|
|
15838
16058
|
);
|
|
15839
|
-
|
|
16059
|
+
const routingRequired = credential.kind === "claimed" && parsed.data.capabilities.includes(BROWSER_ROUTING_CAPABILITY);
|
|
16060
|
+
const fileRequestBound = parsed.data.capabilities.includes(
|
|
16061
|
+
LOCAL_FILE_REQUEST_CAPABILITY
|
|
16062
|
+
);
|
|
16063
|
+
this.#installSession(key, session, {
|
|
16064
|
+
tokenHash,
|
|
16065
|
+
authRequired,
|
|
16066
|
+
routingRequired,
|
|
16067
|
+
fileRequestBound
|
|
16068
|
+
});
|
|
15840
16069
|
const limit = this.#deps.rateLimit ?? DEFAULT_RATE_LIMIT;
|
|
15841
16070
|
this.#bucketTokens = limit.capacity;
|
|
15842
16071
|
this.#bucketRefilledAt = (this.#deps.now ?? Date.now)();
|
|
@@ -15847,7 +16076,11 @@ var Bridge = class {
|
|
|
15847
16076
|
sessionId: session.sessionId,
|
|
15848
16077
|
// Echoed only when it is actually on, so the panel can tell a server that
|
|
15849
16078
|
// is holding it to the header from one that predates the gate entirely.
|
|
15850
|
-
capabilities:
|
|
16079
|
+
capabilities: [
|
|
16080
|
+
...authRequired ? [BRIDGE_AUTH_CAPABILITY] : [],
|
|
16081
|
+
...routingRequired ? [BROWSER_ROUTING_CAPABILITY] : [],
|
|
16082
|
+
...fileRequestBound ? [LOCAL_FILE_REQUEST_CAPABILITY] : []
|
|
16083
|
+
]
|
|
15851
16084
|
};
|
|
15852
16085
|
}
|
|
15853
16086
|
/**
|
|
@@ -15896,21 +16129,27 @@ var Bridge = class {
|
|
|
15896
16129
|
return this.#settle(
|
|
15897
16130
|
frame.id,
|
|
15898
16131
|
{ ok: true, result: frame.tools },
|
|
15899
|
-
presented
|
|
16132
|
+
presented,
|
|
16133
|
+
frame.route?.browserId
|
|
15900
16134
|
);
|
|
15901
16135
|
}
|
|
15902
16136
|
if (frame.ok) {
|
|
15903
|
-
|
|
16137
|
+
const settled2 = this.#settle(
|
|
15904
16138
|
frame.id,
|
|
15905
16139
|
{
|
|
15906
16140
|
ok: true,
|
|
15907
16141
|
result: frame.result,
|
|
15908
16142
|
...frame.ms === void 0 ? {} : { ms: frame.ms }
|
|
15909
16143
|
},
|
|
15910
|
-
presented
|
|
16144
|
+
presented,
|
|
16145
|
+
frame.route?.browserId
|
|
15911
16146
|
);
|
|
16147
|
+
if (settled2 === "settled" && !(typeof frame.result === "object" && frame.result !== null && frame.result.status === "awaiting_confirm")) {
|
|
16148
|
+
this.#discardFilePermit(frame.id);
|
|
16149
|
+
}
|
|
16150
|
+
return settled2;
|
|
15912
16151
|
}
|
|
15913
|
-
|
|
16152
|
+
const settled = this.#settle(
|
|
15914
16153
|
frame.id,
|
|
15915
16154
|
{
|
|
15916
16155
|
ok: false,
|
|
@@ -15918,8 +16157,11 @@ var Bridge = class {
|
|
|
15918
16157
|
message: frame.message,
|
|
15919
16158
|
...frame.ms === void 0 ? {} : { ms: frame.ms }
|
|
15920
16159
|
},
|
|
15921
|
-
presented
|
|
16160
|
+
presented,
|
|
16161
|
+
frame.route?.browserId
|
|
15922
16162
|
);
|
|
16163
|
+
if (settled === "settled") this.#discardFilePermit(frame.id);
|
|
16164
|
+
return settled;
|
|
15923
16165
|
}
|
|
15924
16166
|
/**
|
|
15925
16167
|
* The id is the only axis a reply carries, and the prefixes make it unique
|
|
@@ -15931,15 +16173,28 @@ var Bridge = class {
|
|
|
15931
16173
|
* mint the same one — and would report "unknown", which reads as an ordinary
|
|
15932
16174
|
* late reply.
|
|
15933
16175
|
*/
|
|
15934
|
-
#settle(id, outcome, presented) {
|
|
16176
|
+
#settle(id, outcome, presented, browserId) {
|
|
15935
16177
|
for (const entry of this.#sessions.values()) {
|
|
15936
16178
|
if (!entry.pending.owns(id)) continue;
|
|
15937
16179
|
if (!this.#mayAct(entry, presented)) return "unauthorized";
|
|
16180
|
+
if (entry.routingRequired && browserId !== entry.key) {
|
|
16181
|
+
entry.pending.settle(id, {
|
|
16182
|
+
ok: false,
|
|
16183
|
+
reason: "browser_unknown",
|
|
16184
|
+
message: "The responding browser did not match the routed browser."
|
|
16185
|
+
});
|
|
16186
|
+
return "settled";
|
|
16187
|
+
}
|
|
15938
16188
|
entry.pending.settle(id, outcome);
|
|
15939
16189
|
return "settled";
|
|
15940
16190
|
}
|
|
15941
16191
|
return "dropped";
|
|
15942
16192
|
}
|
|
16193
|
+
#discardFilePermit(id) {
|
|
16194
|
+
for (const entry of this.#sessions.values()) {
|
|
16195
|
+
if (entry.filePermits.delete(id)) return;
|
|
16196
|
+
}
|
|
16197
|
+
}
|
|
15943
16198
|
/** One panel went away. Nothing it owed can land, so settle that session now. */
|
|
15944
16199
|
disconnect(key) {
|
|
15945
16200
|
this.#closeSession(
|
|
@@ -15957,6 +16212,10 @@ var Bridge = class {
|
|
|
15957
16212
|
let expired = 0;
|
|
15958
16213
|
for (const entry of this.#sessions.values()) {
|
|
15959
16214
|
expired += entry.pending.sweep();
|
|
16215
|
+
const now = (this.#deps.now ?? Date.now)();
|
|
16216
|
+
for (const [requestId, permit] of entry.filePermits) {
|
|
16217
|
+
if (permit.expiresAt <= now) entry.filePermits.delete(requestId);
|
|
16218
|
+
}
|
|
15960
16219
|
}
|
|
15961
16220
|
return expired;
|
|
15962
16221
|
}
|
|
@@ -15967,10 +16226,11 @@ var Bridge = class {
|
|
|
15967
16226
|
* shape, catalog size — with no membership check. Membership is enforced
|
|
15968
16227
|
* where the session and the execution live, in the panel's own executor.
|
|
15969
16228
|
*/
|
|
15970
|
-
async listTools() {
|
|
15971
|
-
const out = await this.#request(
|
|
15972
|
-
method: "tools/list"
|
|
15973
|
-
|
|
16229
|
+
async listTools(browser) {
|
|
16230
|
+
const out = await this.#request(
|
|
16231
|
+
{ method: "tools/list" },
|
|
16232
|
+
browser
|
|
16233
|
+
);
|
|
15974
16234
|
if (!out.ok) throw new BridgeError(out.reason, out.message);
|
|
15975
16235
|
const { tools, rejected } = projectTools(out.result);
|
|
15976
16236
|
if (tools.length > 0) {
|
|
@@ -15990,13 +16250,16 @@ var Bridge = class {
|
|
|
15990
16250
|
* `bridgeMs - toolMs` is the transport and the queue. A single total cannot
|
|
15991
16251
|
* tell a slow tool from a slow wire, and that is the question being asked.
|
|
15992
16252
|
*/
|
|
15993
|
-
async callTool(name, args) {
|
|
16253
|
+
async callTool(name, args, browser) {
|
|
15994
16254
|
const startedAt = (this.#deps.now ?? Date.now)();
|
|
15995
|
-
const out = await this.#request(
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16255
|
+
const out = await this.#request(
|
|
16256
|
+
{
|
|
16257
|
+
method: "tools/call",
|
|
16258
|
+
name,
|
|
16259
|
+
args
|
|
16260
|
+
},
|
|
16261
|
+
browser
|
|
16262
|
+
);
|
|
16000
16263
|
const bridgeMs = (this.#deps.now ?? Date.now)() - startedAt;
|
|
16001
16264
|
if (!out.ok) {
|
|
16002
16265
|
throw new BridgeError(out.reason, out.message, {
|
|
@@ -16030,8 +16293,28 @@ var Bridge = class {
|
|
|
16030
16293
|
this.#bucketTokens -= 1;
|
|
16031
16294
|
return true;
|
|
16032
16295
|
}
|
|
16033
|
-
async #request(req) {
|
|
16034
|
-
|
|
16296
|
+
async #request(req, browser) {
|
|
16297
|
+
let entry;
|
|
16298
|
+
if (browser === void 0) {
|
|
16299
|
+
entry = this.#primary();
|
|
16300
|
+
} else {
|
|
16301
|
+
const known = this.#deps.claimedBrowsers.has(browser) || browser === LEGACY_SESSION_KEY && this.#sessions.has(browser);
|
|
16302
|
+
if (!known) {
|
|
16303
|
+
return {
|
|
16304
|
+
ok: false,
|
|
16305
|
+
reason: "browser_unknown",
|
|
16306
|
+
message: "The requested browser is not registered."
|
|
16307
|
+
};
|
|
16308
|
+
}
|
|
16309
|
+
entry = this.#sessions.get(browser) ?? null;
|
|
16310
|
+
if (!entry?.streamActive) {
|
|
16311
|
+
return {
|
|
16312
|
+
ok: false,
|
|
16313
|
+
reason: "browser_detached",
|
|
16314
|
+
message: "The requested browser is registered but not connected."
|
|
16315
|
+
};
|
|
16316
|
+
}
|
|
16317
|
+
}
|
|
16035
16318
|
if (!entry) {
|
|
16036
16319
|
return {
|
|
16037
16320
|
ok: false,
|
|
@@ -16047,23 +16330,47 @@ var Bridge = class {
|
|
|
16047
16330
|
};
|
|
16048
16331
|
}
|
|
16049
16332
|
const id = entry.pending.nextId();
|
|
16333
|
+
const routing = entry.routingRequired ? {
|
|
16334
|
+
targetBrowserId: entry.key,
|
|
16335
|
+
explicit: browser !== void 0,
|
|
16336
|
+
fallbackFromPinned: browser === void 0 && this.#defaultKey !== null && this.#defaultKey !== entry.key,
|
|
16337
|
+
connectedBrowserCount: this.connectedBrowsers().length
|
|
16338
|
+
} : void 0;
|
|
16339
|
+
const delegatedTool = req.method === "tools/call" && req.name === CALL_TOOL ? req.args["tool"] : void 0;
|
|
16340
|
+
const delegatedArgs = req.method === "tools/call" && typeof req.args["args"] === "object" && req.args["args"] !== null && !Array.isArray(req.args["args"]) ? req.args["args"] : null;
|
|
16341
|
+
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;
|
|
16342
|
+
const fileArgs = delegatedArgs ?? (req.method === "tools/call" ? req.args : null);
|
|
16343
|
+
const path = fileArgs?.["path"];
|
|
16344
|
+
if (entry.fileRequestBound && fileTool !== null && typeof path === "string" && path.length > 0) {
|
|
16345
|
+
entry.filePermits.set(id, {
|
|
16346
|
+
expiresAt: (this.#deps.now ?? Date.now)() + (this.#deps.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
16347
|
+
tool: fileTool,
|
|
16348
|
+
path
|
|
16349
|
+
});
|
|
16350
|
+
}
|
|
16050
16351
|
const settled = entry.pending.register(id);
|
|
16051
16352
|
const delivered = this.#deps.send(
|
|
16052
16353
|
{
|
|
16053
16354
|
t: "req",
|
|
16054
16355
|
id,
|
|
16055
|
-
...req
|
|
16356
|
+
...req,
|
|
16357
|
+
...routing ? { routing } : {}
|
|
16056
16358
|
},
|
|
16057
16359
|
entry.key
|
|
16058
16360
|
);
|
|
16059
16361
|
if (!delivered) {
|
|
16362
|
+
entry.filePermits.delete(id);
|
|
16060
16363
|
entry.pending.settle(id, {
|
|
16061
16364
|
ok: false,
|
|
16062
16365
|
reason: "not_connected",
|
|
16063
16366
|
message: BRIDGE_USER_MESSAGES.panelClosed
|
|
16064
16367
|
});
|
|
16065
16368
|
}
|
|
16066
|
-
|
|
16369
|
+
const outcome = await settled;
|
|
16370
|
+
if (outcome.ok && (browser !== void 0 || this.#defaultKey === null)) {
|
|
16371
|
+
this.#defaultKey = entry.key;
|
|
16372
|
+
}
|
|
16373
|
+
return outcome;
|
|
16067
16374
|
}
|
|
16068
16375
|
};
|
|
16069
16376
|
var BridgeError = class extends Error {
|
|
@@ -16444,8 +16751,43 @@ function timingMeta(timing) {
|
|
|
16444
16751
|
}
|
|
16445
16752
|
};
|
|
16446
16753
|
}
|
|
16754
|
+
var ROUTED_FACADES = /* @__PURE__ */ new Set([
|
|
16755
|
+
CATALOG_TOOL,
|
|
16756
|
+
DESCRIBE_TOOL,
|
|
16757
|
+
CALL_TOOL,
|
|
16758
|
+
CONFIRM_STATUS_TOOL,
|
|
16759
|
+
SESSION_STATE_TOOL
|
|
16760
|
+
]);
|
|
16447
16761
|
function ownTools(stage) {
|
|
16448
|
-
|
|
16762
|
+
const browsers = staticDiscoveryCatalog().find(
|
|
16763
|
+
(tool) => tool.name === BROWSERS_TOOL
|
|
16764
|
+
);
|
|
16765
|
+
return [
|
|
16766
|
+
...browsers ? [browsers] : [],
|
|
16767
|
+
...stage ? mediaToolDescriptors() : []
|
|
16768
|
+
];
|
|
16769
|
+
}
|
|
16770
|
+
function withOwnTools(tools, stage) {
|
|
16771
|
+
const canonical = new Map(
|
|
16772
|
+
staticDiscoveryCatalog().map((tool) => [tool.name, tool])
|
|
16773
|
+
);
|
|
16774
|
+
const out = tools.map((tool) => {
|
|
16775
|
+
const name = typeof tool["name"] === "string" ? tool["name"] : "";
|
|
16776
|
+
const facade = canonical.get(name);
|
|
16777
|
+
if (!facade || !ROUTED_FACADES.has(name)) return tool;
|
|
16778
|
+
return { ...tool, inputSchema: facade.inputSchema };
|
|
16779
|
+
});
|
|
16780
|
+
const names = new Set(
|
|
16781
|
+
out.flatMap(
|
|
16782
|
+
(tool) => typeof tool["name"] === "string" ? [tool["name"]] : []
|
|
16783
|
+
)
|
|
16784
|
+
);
|
|
16785
|
+
for (const tool of ownTools(stage)) {
|
|
16786
|
+
if (typeof tool["name"] !== "string" || names.has(tool["name"])) continue;
|
|
16787
|
+
names.add(tool["name"]);
|
|
16788
|
+
out.push(tool);
|
|
16789
|
+
}
|
|
16790
|
+
return out;
|
|
16449
16791
|
}
|
|
16450
16792
|
function rpcToolError(id, err) {
|
|
16451
16793
|
const policy = policyFor(err.reason);
|
|
@@ -16487,7 +16829,7 @@ function frameMcpResponse(payload, accept) {
|
|
|
16487
16829
|
function offlineCatalog(bridge, stage, id, accept) {
|
|
16488
16830
|
const base = bridge.lastKnownTools.length > 0 ? bridge.lastKnownTools : staticDiscoveryCatalog();
|
|
16489
16831
|
return frameMcpResponse(
|
|
16490
|
-
rpcResult(id, { tools:
|
|
16832
|
+
rpcResult(id, { tools: withOwnTools(base, stage) }),
|
|
16491
16833
|
accept
|
|
16492
16834
|
);
|
|
16493
16835
|
}
|
|
@@ -16508,7 +16850,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16508
16850
|
try {
|
|
16509
16851
|
const { tools } = await bridge.listTools();
|
|
16510
16852
|
return frameMcpResponse(
|
|
16511
|
-
rpcResult(id, { tools:
|
|
16853
|
+
rpcResult(id, { tools: withOwnTools(tools, stage) }),
|
|
16512
16854
|
accept
|
|
16513
16855
|
);
|
|
16514
16856
|
} catch (err) {
|
|
@@ -16527,7 +16869,23 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16527
16869
|
accept
|
|
16528
16870
|
);
|
|
16529
16871
|
}
|
|
16530
|
-
|
|
16872
|
+
let args = typeof params.arguments === "object" && params.arguments !== null ? params.arguments : {};
|
|
16873
|
+
let browser;
|
|
16874
|
+
if (ROUTED_FACADES.has(params.name) && Object.hasOwn(args, "browser")) {
|
|
16875
|
+
if (typeof args["browser"] !== "string" || args["browser"].length === 0 || args["browser"] !== args["browser"].trim()) {
|
|
16876
|
+
return frameMcpResponse(
|
|
16877
|
+
rpcError(
|
|
16878
|
+
id,
|
|
16879
|
+
CODE_INVALID_PARAMS,
|
|
16880
|
+
"browser must be a connected browser id."
|
|
16881
|
+
),
|
|
16882
|
+
accept
|
|
16883
|
+
);
|
|
16884
|
+
}
|
|
16885
|
+
browser = args["browser"];
|
|
16886
|
+
args = { ...args };
|
|
16887
|
+
delete args["browser"];
|
|
16888
|
+
}
|
|
16531
16889
|
if (params.name === BROWSERS_TOOL) {
|
|
16532
16890
|
return frameMcpResponse(
|
|
16533
16891
|
rpcResult(id, {
|
|
@@ -16556,7 +16914,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
|
|
|
16556
16914
|
);
|
|
16557
16915
|
}
|
|
16558
16916
|
try {
|
|
16559
|
-
const call = await bridge.callTool(params.name, args);
|
|
16917
|
+
const call = browser === void 0 ? await bridge.callTool(params.name, args) : await bridge.callTool(params.name, args, browser);
|
|
16560
16918
|
return frameMcpResponse(
|
|
16561
16919
|
rpcResult(id, {
|
|
16562
16920
|
content: [{ type: "text", text: JSON.stringify(call.result) }],
|
|
@@ -16610,17 +16968,16 @@ async function openLocalImage(path) {
|
|
|
16610
16968
|
message: "\uC804\uCCB4 \uACBD\uB85C\uB97C \uC54C\uB824 \uC8FC\uC138\uC694(\uC608: /Users/me/photo.jpg)."
|
|
16611
16969
|
};
|
|
16612
16970
|
}
|
|
16613
|
-
let
|
|
16971
|
+
let initial;
|
|
16614
16972
|
try {
|
|
16615
|
-
|
|
16616
|
-
if (
|
|
16973
|
+
initial = await lstat(path);
|
|
16974
|
+
if (initial.isSymbolicLink()) {
|
|
16617
16975
|
return {
|
|
16618
16976
|
ok: false,
|
|
16619
16977
|
reason: "not_a_file",
|
|
16620
|
-
message: "\
|
|
16978
|
+
message: "\uC2EC\uBCFC\uB9AD \uB9C1\uD06C\uB294 \uC0AC\uC9C4 \uD30C\uC77C\uB85C \uC5F4 \uC218 \uC5C6\uC5B4\uC694."
|
|
16621
16979
|
};
|
|
16622
16980
|
}
|
|
16623
|
-
size = info.size;
|
|
16624
16981
|
} catch {
|
|
16625
16982
|
return {
|
|
16626
16983
|
ok: false,
|
|
@@ -16628,47 +16985,80 @@ async function openLocalImage(path) {
|
|
|
16628
16985
|
message: "\uADF8 \uACBD\uB85C\uC5D0\uC11C \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
16629
16986
|
};
|
|
16630
16987
|
}
|
|
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;
|
|
16988
|
+
let handle;
|
|
16642
16989
|
try {
|
|
16643
|
-
const
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16990
|
+
const noFollow = constants.O_NOFOLLOW;
|
|
16991
|
+
if (process.platform !== "win32" && typeof noFollow !== "number") {
|
|
16992
|
+
return {
|
|
16993
|
+
ok: false,
|
|
16994
|
+
reason: "unreadable",
|
|
16995
|
+
message: "\uC774 \uD658\uACBD\uC5D0\uC11C\uB294 \uD30C\uC77C\uC744 \uC548\uC804\uD558\uAC8C \uC5F4 \uC218 \uC5C6\uC5B4\uC694."
|
|
16996
|
+
};
|
|
16997
|
+
}
|
|
16998
|
+
handle = await open(
|
|
16999
|
+
path,
|
|
17000
|
+
constants.O_RDONLY | (process.platform === "win32" ? 0 : noFollow)
|
|
17001
|
+
);
|
|
17002
|
+
const info = await handle.stat();
|
|
17003
|
+
if (initial.dev !== info.dev || initial.ino !== info.ino) {
|
|
17004
|
+
await handle.close();
|
|
17005
|
+
return {
|
|
17006
|
+
ok: false,
|
|
17007
|
+
reason: "unreadable",
|
|
17008
|
+
message: "\uD30C\uC77C\uC744 \uC5EC\uB294 \uB3D9\uC548 \uACBD\uB85C\uAC00 \uBC14\uB00C\uC5C8\uC5B4\uC694."
|
|
17009
|
+
};
|
|
17010
|
+
}
|
|
17011
|
+
if (!info.isFile()) {
|
|
17012
|
+
await handle.close();
|
|
17013
|
+
return {
|
|
17014
|
+
ok: false,
|
|
17015
|
+
reason: "not_a_file",
|
|
17016
|
+
message: "\uADF8 \uACBD\uB85C\uB294 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694."
|
|
17017
|
+
};
|
|
16650
17018
|
}
|
|
17019
|
+
if (info.size > MAX_FILE_BYTES) {
|
|
17020
|
+
await handle.close();
|
|
17021
|
+
return {
|
|
17022
|
+
ok: false,
|
|
17023
|
+
reason: "too_large",
|
|
17024
|
+
message: `\uC0AC\uC9C4\uC774 \uB108\uBB34 \uCEE4\uC694(\uCD5C\uB300 ${String(MAX_FILE_BYTES / 1024 / 1024)}MB).`
|
|
17025
|
+
};
|
|
17026
|
+
}
|
|
17027
|
+
if (info.size === 0) {
|
|
17028
|
+
await handle.close();
|
|
17029
|
+
return { ok: false, reason: "empty", message: "\uBE48 \uD30C\uC77C\uC774\uC5D0\uC694." };
|
|
17030
|
+
}
|
|
17031
|
+
const head = new Uint8Array(12);
|
|
17032
|
+
await handle.read(head, 0, head.byteLength, 0);
|
|
17033
|
+
const contentType = sniff(head);
|
|
17034
|
+
if (contentType === null) {
|
|
17035
|
+
await handle.close();
|
|
17036
|
+
return {
|
|
17037
|
+
ok: false,
|
|
17038
|
+
reason: "not_an_image",
|
|
17039
|
+
message: "\uC774\uBBF8\uC9C0 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694(PNG\xB7JPEG\xB7GIF\xB7WebP\uB9CC \uB123\uC744 \uC218 \uC788\uC5B4\uC694)."
|
|
17040
|
+
};
|
|
17041
|
+
}
|
|
17042
|
+
return {
|
|
17043
|
+
ok: true,
|
|
17044
|
+
stream: handle.createReadStream({
|
|
17045
|
+
autoClose: true,
|
|
17046
|
+
start: 0,
|
|
17047
|
+
end: info.size - 1
|
|
17048
|
+
}),
|
|
17049
|
+
contentType,
|
|
17050
|
+
byteLength: info.size,
|
|
17051
|
+
fileName: basename(path)
|
|
17052
|
+
};
|
|
16651
17053
|
} catch {
|
|
17054
|
+
await handle?.close().catch(() => {
|
|
17055
|
+
});
|
|
16652
17056
|
return {
|
|
16653
17057
|
ok: false,
|
|
16654
17058
|
reason: "unreadable",
|
|
16655
17059
|
message: "\uD30C\uC77C\uC744 \uC5F4\uC9C0 \uBABB\uD588\uC5B4\uC694."
|
|
16656
17060
|
};
|
|
16657
17061
|
}
|
|
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
17062
|
}
|
|
16673
17063
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
16674
17064
|
var DEFAULT_TOOLS_CHANGED_DEBOUNCE_MS = 600;
|
|
@@ -16685,6 +17075,9 @@ function headerValue(raw) {
|
|
|
16685
17075
|
const trimmed = one?.trim();
|
|
16686
17076
|
return trimmed ? trimmed.slice(0, 128) : void 0;
|
|
16687
17077
|
}
|
|
17078
|
+
function correlationValue(value, max) {
|
|
17079
|
+
return /^[a-zA-Z0-9_-]+$/.test(value) ? value.slice(0, max) : "redacted";
|
|
17080
|
+
}
|
|
16688
17081
|
function createHttpBridge(opts) {
|
|
16689
17082
|
const host = opts.host ?? "127.0.0.1";
|
|
16690
17083
|
if (!isLoopbackHost(host)) {
|
|
@@ -16763,22 +17156,34 @@ function createHttpBridge(opts) {
|
|
|
16763
17156
|
`);
|
|
16764
17157
|
return true;
|
|
16765
17158
|
};
|
|
16766
|
-
const installStream = (key, res) => {
|
|
17159
|
+
const installStream = (key, sessionId, res) => {
|
|
16767
17160
|
if (streams.has(key)) {
|
|
16768
|
-
throw new Error(
|
|
17161
|
+
throw new Error("stream slot is still occupied");
|
|
16769
17162
|
}
|
|
16770
17163
|
const heartbeat = setInterval(() => {
|
|
16771
17164
|
send({ t: "hb", at: Date.now() }, key);
|
|
16772
17165
|
}, opts.heartbeatMs ?? HEARTBEAT_INTERVAL_MS);
|
|
16773
17166
|
heartbeat.unref?.();
|
|
16774
|
-
streams.set(key, { res, heartbeat });
|
|
17167
|
+
streams.set(key, { res, sessionId, heartbeat, fileStreams: /* @__PURE__ */ new Set() });
|
|
17168
|
+
if (!opts.bridge.activateSession(key, sessionId)) {
|
|
17169
|
+
clearInterval(heartbeat);
|
|
17170
|
+
deleteStreamEntry(key);
|
|
17171
|
+
throw new Error("stream session is no longer current");
|
|
17172
|
+
}
|
|
16775
17173
|
lifecycle?.retain?.();
|
|
16776
17174
|
};
|
|
17175
|
+
const deleteStreamEntry = (key) => {
|
|
17176
|
+
streams.delete(key);
|
|
17177
|
+
};
|
|
16777
17178
|
const removeStream = (key) => {
|
|
16778
17179
|
const stream = streams.get(key);
|
|
16779
17180
|
if (!stream) return null;
|
|
16780
17181
|
clearInterval(stream.heartbeat);
|
|
16781
|
-
|
|
17182
|
+
for (const fileStream of stream.fileStreams) {
|
|
17183
|
+
fileStream.destroy(new Error("browser session ended"));
|
|
17184
|
+
}
|
|
17185
|
+
stream.fileStreams.clear();
|
|
17186
|
+
deleteStreamEntry(key);
|
|
16782
17187
|
lifecycle?.release?.();
|
|
16783
17188
|
return stream.res;
|
|
16784
17189
|
};
|
|
@@ -16789,14 +17194,6 @@ function createHttpBridge(opts) {
|
|
|
16789
17194
|
opts.bridge.disconnect(key);
|
|
16790
17195
|
if (wasPrimary) scheduleToolsDrop();
|
|
16791
17196
|
};
|
|
16792
|
-
const retireStaleSocket = (key) => {
|
|
16793
|
-
const stale = removeStream(key);
|
|
16794
|
-
if (!stale) return;
|
|
16795
|
-
try {
|
|
16796
|
-
stale.end();
|
|
16797
|
-
} catch {
|
|
16798
|
-
}
|
|
16799
|
-
};
|
|
16800
17197
|
const server = createServer((req, res) => {
|
|
16801
17198
|
const url2 = new URL(req.url ?? "/", `http://${host}`);
|
|
16802
17199
|
const origin = req.headers.origin;
|
|
@@ -16841,10 +17238,6 @@ function createHttpBridge(opts) {
|
|
|
16841
17238
|
if (req.method === "POST" && url2.pathname === "/bridge/hello") {
|
|
16842
17239
|
void readJson(req, MAX_BODY_BYTES).then((body) => {
|
|
16843
17240
|
const out = opts.bridge.handshake(body, origin);
|
|
16844
|
-
if (out.t === "hello_ack") {
|
|
16845
|
-
const key = opts.bridge.keyForSession(out.sessionId);
|
|
16846
|
-
if (key !== null) retireStaleSocket(key);
|
|
16847
|
-
}
|
|
16848
17241
|
json2(res, out.t === "hello_ack" ? 200 : 403, out);
|
|
16849
17242
|
}).catch(
|
|
16850
17243
|
() => json2(res, 400, {
|
|
@@ -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
|
+
});
|
|
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
|
+
});
|
|
16959
17394
|
return;
|
|
16960
17395
|
}
|
|
16961
|
-
|
|
16962
|
-
|
|
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,
|