@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.
@@ -1,4 +1,4 @@
1
- // ../mcp-server/dist/chunk-MNJBE7B3.js
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 { createReadStream } from "fs";
10
- import { open, stat } from "fs/promises";
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 \uAC00\uC7A5 \uBA3C\uC800 \uC5F0\uACB0\uB41C \uAC83\uC5D0\uC11C \uC2E4\uD589\uB418\uB2C8, \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.",
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((name) => ({
15425
- name,
15426
- description: "",
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
- for (const entry of this.#sessions.values()) return entry;
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.keys()];
15622
- return keys.map((id, i) => ({ id, primary: i === 0 }));
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.#sessions.size > 0;
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
- verifyLiveSessionToken(presented) {
15723
- if (typeof presented !== "string" || presented.length === 0) return false;
15724
- if (this.verifyRootToken(presented)) {
15725
- return this.#sessions.has(LEGACY_SESSION_KEY);
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 expected = this.#deps.claimedBrowsers.get(entry.key);
15730
- if (expected !== void 0 && timingSafeEqual(hash2, expected))
15731
- return true;
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 false;
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
- this.#installSession(key, session, { tokenHash, authRequired });
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: authRequired ? [BRIDGE_AUTH_CAPABILITY] : []
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
- return this.#settle(
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
- return this.#settle(
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
- method: "tools/call",
15993
- name,
15994
- args
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
- const entry = this.#primary();
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
- return settled;
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
- return stage ? mediaToolDescriptors() : [];
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: [...base, ...ownTools(stage)] }),
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: [...tools, ...ownTools(stage)] }),
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
- const args = typeof params.arguments === "object" && params.arguments !== null ? params.arguments : {};
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 size;
16959
+ let initial;
16610
16960
  try {
16611
- const info = await stat(path);
16612
- if (!info.isFile()) {
16961
+ initial = await lstat(path);
16962
+ if (initial.isSymbolicLink()) {
16613
16963
  return {
16614
16964
  ok: false,
16615
16965
  reason: "not_a_file",
16616
- message: "\uADF8 \uACBD\uB85C\uB294 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694."
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
- if (size > MAX_FILE_BYTES) {
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 fh = await open(path, "r");
16640
- try {
16641
- const head = new Uint8Array(12);
16642
- await fh.read(head, 0, 12, 0);
16643
- contentType = sniff(head);
16644
- } finally {
16645
- await fh.close();
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(`stream key ${key} is still occupied`);
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
- streams.delete(key);
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.sessionKey : opts.bridge.keyForSession(claimed);
17254
+ const key = claimed === null ? opts.bridge.legacySessionKey() : opts.bridge.keyForSession(claimed);
16862
17255
  if (key === null) {
16863
- opts.log?.(`refused a stream for session ${claimed ?? "unnamed"}`);
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?.(`refused an unauthorised stream for ${key}`);
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
- installStream(key, res);
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
- if (!opts.bridge.verifyLiveSessionToken(
16952
- typeof b?.token === "string" ? b.token : ""
16953
- )) {
16954
- json2(res, 403, { error: "unauthorized" });
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 out = await openLocalImage(
16958
- typeof b?.path === "string" ? b.path : ""
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
- if (!out.ok) {
16961
- json2(res, 400, { error: out.reason, message: out.message });
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
- res.writeHead(200, {
16965
- "content-type": out.contentType,
16966
- "content-length": String(out.byteLength),
16967
- // The extension shows this in its confirmation, so the user knows
16968
- // which file is about to be used.
16969
- "x-file-name": encodeURIComponent(out.fileName),
16970
- "cache-control": "no-store"
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.length > 0 && typeof r["label"] === "string" && typeof r["createdAt"] === "string";
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
- return read.state === "ok" ? [...read.rows] : [];
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) opts.claimed.delete(claim.browserId);
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
- if (ledger.state === "ok") {
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
- claimBrowser: (browserId, tokenHash) => registry2.claim(browserId, tokenHash),
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,