@modootoday/datalab-extension-mcp 1.8.6 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- // ../mcp-server/dist/chunk-Y2BX76F2.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,11 +15662,12 @@ var Bridge = class {
15618
15662
  * landing in the older one and never learn why the newer one saw nothing.
15619
15663
  */
15620
15664
  connectedBrowsers() {
15621
- const keys = [...this.#sessions.keys()];
15622
- return keys.map((id, i) => ({
15665
+ const keys = [...this.#sessions.values()].filter((entry) => entry.streamActive).map((entry) => entry.key);
15666
+ const primary = this.#defaultKey === null ? this.#primary()?.key ?? null : this.#defaultKey;
15667
+ return keys.map((id) => ({
15623
15668
  id,
15624
15669
  name: this.#deps.browserLabels.get(id) ?? null,
15625
- primary: i === 0
15670
+ primary: primary === id
15626
15671
  }));
15627
15672
  }
15628
15673
  /**
@@ -15649,6 +15694,10 @@ var Bridge = class {
15649
15694
  session,
15650
15695
  tokenHash: auth.tokenHash,
15651
15696
  authRequired: auth.authRequired,
15697
+ routingRequired: auth.routingRequired,
15698
+ fileRequestBound: auth.fileRequestBound,
15699
+ streamActive: false,
15700
+ filePermits: /* @__PURE__ */ new Map(),
15652
15701
  pending: new PendingRegistry({
15653
15702
  now: this.#deps.now,
15654
15703
  timeoutMs: this.#deps.timeoutMs,
@@ -15693,9 +15742,25 @@ var Bridge = class {
15693
15742
  }
15694
15743
  return null;
15695
15744
  }
15745
+ sessionForKey(key) {
15746
+ return this.#sessions.get(key)?.session ?? null;
15747
+ }
15748
+ legacySessionKey() {
15749
+ return this.#sessions.has(LEGACY_SESSION_KEY) ? LEGACY_SESSION_KEY : null;
15750
+ }
15696
15751
  /** Is a panel connected and ready to serve tool calls? */
15697
15752
  get connected() {
15698
- return this.#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;
15699
15764
  }
15700
15765
  /**
15701
15766
  * The last catalog the panel reported, empty if it has never connected in
@@ -15723,18 +15788,161 @@ var Bridge = class {
15723
15788
  * the same as that browser being here, and this gates a route that reads
15724
15789
  * files off the operator's disk.
15725
15790
  */
15726
- verifyLiveSessionToken(presented) {
15727
- if (typeof presented !== "string" || presented.length === 0) return false;
15728
- if (this.verifyRootToken(presented)) {
15729
- 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;
15730
15795
  }
15796
+ if (entry.key === LEGACY_SESSION_KEY) {
15797
+ return this.verifyRootToken(presented) ? "root" : null;
15798
+ }
15799
+ const current = this.#deps.claimedBrowsers.get(entry.key);
15800
+ return current !== void 0 && timingSafeEqual(hash2, current) ? "claimed_browser" : null;
15801
+ }
15802
+ #fileCredentialClass(presented) {
15803
+ if (this.verifyRootToken(presented)) return "root";
15731
15804
  const hash2 = digestToken(presented);
15805
+ for (const current of this.#deps.claimedBrowsers.values()) {
15806
+ if (timingSafeEqual(hash2, current)) return "claimed_browser";
15807
+ }
15808
+ return "unknown";
15809
+ }
15810
+ authorizeLegacyFileCredential(presented) {
15811
+ if (typeof presented !== "string" || presented.length === 0) {
15812
+ return {
15813
+ ok: false,
15814
+ reason: "file_session_token_missing",
15815
+ credential: "missing"
15816
+ };
15817
+ }
15818
+ const credential = this.#fileCredentialClass(presented);
15732
15819
  for (const entry of this.#sessions.values()) {
15733
- const expected = this.#deps.claimedBrowsers.get(entry.key);
15734
- if (expected !== void 0 && timingSafeEqual(hash2, expected))
15735
- 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
+ }
15736
15830
  }
15737
- 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;
15738
15946
  }
15739
15947
  /**
15740
15948
  * The extension is right about the token, and we can be wrong about it.
@@ -15836,7 +16044,16 @@ var Bridge = class {
15836
16044
  const authRequired = parsed.data.capabilities.includes(
15837
16045
  BRIDGE_AUTH_CAPABILITY
15838
16046
  );
15839
- 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
+ });
15840
16057
  const limit = this.#deps.rateLimit ?? DEFAULT_RATE_LIMIT;
15841
16058
  this.#bucketTokens = limit.capacity;
15842
16059
  this.#bucketRefilledAt = (this.#deps.now ?? Date.now)();
@@ -15847,7 +16064,11 @@ var Bridge = class {
15847
16064
  sessionId: session.sessionId,
15848
16065
  // Echoed only when it is actually on, so the panel can tell a server that
15849
16066
  // is holding it to the header from one that predates the gate entirely.
15850
- capabilities: authRequired ? [BRIDGE_AUTH_CAPABILITY] : []
16067
+ capabilities: [
16068
+ ...authRequired ? [BRIDGE_AUTH_CAPABILITY] : [],
16069
+ ...routingRequired ? [BROWSER_ROUTING_CAPABILITY] : [],
16070
+ ...fileRequestBound ? [LOCAL_FILE_REQUEST_CAPABILITY] : []
16071
+ ]
15851
16072
  };
15852
16073
  }
15853
16074
  /**
@@ -15896,21 +16117,27 @@ var Bridge = class {
15896
16117
  return this.#settle(
15897
16118
  frame.id,
15898
16119
  { ok: true, result: frame.tools },
15899
- presented
16120
+ presented,
16121
+ frame.route?.browserId
15900
16122
  );
15901
16123
  }
15902
16124
  if (frame.ok) {
15903
- return this.#settle(
16125
+ const settled2 = this.#settle(
15904
16126
  frame.id,
15905
16127
  {
15906
16128
  ok: true,
15907
16129
  result: frame.result,
15908
16130
  ...frame.ms === void 0 ? {} : { ms: frame.ms }
15909
16131
  },
15910
- presented
16132
+ presented,
16133
+ frame.route?.browserId
15911
16134
  );
16135
+ if (settled2 === "settled" && !(typeof frame.result === "object" && frame.result !== null && frame.result.status === "awaiting_confirm")) {
16136
+ this.#discardFilePermit(frame.id);
16137
+ }
16138
+ return settled2;
15912
16139
  }
15913
- return this.#settle(
16140
+ const settled = this.#settle(
15914
16141
  frame.id,
15915
16142
  {
15916
16143
  ok: false,
@@ -15918,8 +16145,11 @@ var Bridge = class {
15918
16145
  message: frame.message,
15919
16146
  ...frame.ms === void 0 ? {} : { ms: frame.ms }
15920
16147
  },
15921
- presented
16148
+ presented,
16149
+ frame.route?.browserId
15922
16150
  );
16151
+ if (settled === "settled") this.#discardFilePermit(frame.id);
16152
+ return settled;
15923
16153
  }
15924
16154
  /**
15925
16155
  * The id is the only axis a reply carries, and the prefixes make it unique
@@ -15931,15 +16161,28 @@ var Bridge = class {
15931
16161
  * mint the same one — and would report "unknown", which reads as an ordinary
15932
16162
  * late reply.
15933
16163
  */
15934
- #settle(id, outcome, presented) {
16164
+ #settle(id, outcome, presented, browserId) {
15935
16165
  for (const entry of this.#sessions.values()) {
15936
16166
  if (!entry.pending.owns(id)) continue;
15937
16167
  if (!this.#mayAct(entry, presented)) return "unauthorized";
16168
+ if (entry.routingRequired && browserId !== entry.key) {
16169
+ entry.pending.settle(id, {
16170
+ ok: false,
16171
+ reason: "browser_unknown",
16172
+ message: "The responding browser did not match the routed browser."
16173
+ });
16174
+ return "settled";
16175
+ }
15938
16176
  entry.pending.settle(id, outcome);
15939
16177
  return "settled";
15940
16178
  }
15941
16179
  return "dropped";
15942
16180
  }
16181
+ #discardFilePermit(id) {
16182
+ for (const entry of this.#sessions.values()) {
16183
+ if (entry.filePermits.delete(id)) return;
16184
+ }
16185
+ }
15943
16186
  /** One panel went away. Nothing it owed can land, so settle that session now. */
15944
16187
  disconnect(key) {
15945
16188
  this.#closeSession(
@@ -15957,6 +16200,10 @@ var Bridge = class {
15957
16200
  let expired = 0;
15958
16201
  for (const entry of this.#sessions.values()) {
15959
16202
  expired += entry.pending.sweep();
16203
+ const now = (this.#deps.now ?? Date.now)();
16204
+ for (const [requestId, permit] of entry.filePermits) {
16205
+ if (permit.expiresAt <= now) entry.filePermits.delete(requestId);
16206
+ }
15960
16207
  }
15961
16208
  return expired;
15962
16209
  }
@@ -15967,10 +16214,11 @@ var Bridge = class {
15967
16214
  * shape, catalog size — with no membership check. Membership is enforced
15968
16215
  * where the session and the execution live, in the panel's own executor.
15969
16216
  */
15970
- async listTools() {
15971
- const out = await this.#request({
15972
- method: "tools/list"
15973
- });
16217
+ async listTools(browser) {
16218
+ const out = await this.#request(
16219
+ { method: "tools/list" },
16220
+ browser
16221
+ );
15974
16222
  if (!out.ok) throw new BridgeError(out.reason, out.message);
15975
16223
  const { tools, rejected } = projectTools(out.result);
15976
16224
  if (tools.length > 0) {
@@ -15990,13 +16238,16 @@ var Bridge = class {
15990
16238
  * `bridgeMs - toolMs` is the transport and the queue. A single total cannot
15991
16239
  * tell a slow tool from a slow wire, and that is the question being asked.
15992
16240
  */
15993
- async callTool(name, args) {
16241
+ async callTool(name, args, browser) {
15994
16242
  const startedAt = (this.#deps.now ?? Date.now)();
15995
- const out = await this.#request({
15996
- method: "tools/call",
15997
- name,
15998
- args
15999
- });
16243
+ const out = await this.#request(
16244
+ {
16245
+ method: "tools/call",
16246
+ name,
16247
+ args
16248
+ },
16249
+ browser
16250
+ );
16000
16251
  const bridgeMs = (this.#deps.now ?? Date.now)() - startedAt;
16001
16252
  if (!out.ok) {
16002
16253
  throw new BridgeError(out.reason, out.message, {
@@ -16030,8 +16281,28 @@ var Bridge = class {
16030
16281
  this.#bucketTokens -= 1;
16031
16282
  return true;
16032
16283
  }
16033
- async #request(req) {
16034
- 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
+ }
16035
16306
  if (!entry) {
16036
16307
  return {
16037
16308
  ok: false,
@@ -16047,23 +16318,47 @@ var Bridge = class {
16047
16318
  };
16048
16319
  }
16049
16320
  const id = entry.pending.nextId();
16321
+ const routing = entry.routingRequired ? {
16322
+ targetBrowserId: entry.key,
16323
+ explicit: browser !== void 0,
16324
+ fallbackFromPinned: browser === void 0 && this.#defaultKey !== null && this.#defaultKey !== entry.key,
16325
+ connectedBrowserCount: this.connectedBrowsers().length
16326
+ } : void 0;
16327
+ const delegatedTool = req.method === "tools/call" && req.name === CALL_TOOL ? req.args["tool"] : void 0;
16328
+ const delegatedArgs = req.method === "tools/call" && typeof req.args["args"] === "object" && req.args["args"] !== null && !Array.isArray(req.args["args"]) ? req.args["args"] : null;
16329
+ const fileTool = req.method === "tools/call" && (req.name === "editor_insert_image" || req.name === "gallery_image_add") ? req.name : delegatedTool === "editor_insert_image" || delegatedTool === "gallery_image_add" ? delegatedTool : null;
16330
+ const fileArgs = delegatedArgs ?? (req.method === "tools/call" ? req.args : null);
16331
+ const path = fileArgs?.["path"];
16332
+ if (entry.fileRequestBound && fileTool !== null && typeof path === "string" && path.length > 0) {
16333
+ entry.filePermits.set(id, {
16334
+ expiresAt: (this.#deps.now ?? Date.now)() + (this.#deps.timeoutMs ?? DEFAULT_TIMEOUT_MS),
16335
+ tool: fileTool,
16336
+ path
16337
+ });
16338
+ }
16050
16339
  const settled = entry.pending.register(id);
16051
16340
  const delivered = this.#deps.send(
16052
16341
  {
16053
16342
  t: "req",
16054
16343
  id,
16055
- ...req
16344
+ ...req,
16345
+ ...routing ? { routing } : {}
16056
16346
  },
16057
16347
  entry.key
16058
16348
  );
16059
16349
  if (!delivered) {
16350
+ entry.filePermits.delete(id);
16060
16351
  entry.pending.settle(id, {
16061
16352
  ok: false,
16062
16353
  reason: "not_connected",
16063
16354
  message: BRIDGE_USER_MESSAGES.panelClosed
16064
16355
  });
16065
16356
  }
16066
- 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;
16067
16362
  }
16068
16363
  };
16069
16364
  var BridgeError = class extends Error {
@@ -16444,8 +16739,43 @@ function timingMeta(timing) {
16444
16739
  }
16445
16740
  };
16446
16741
  }
16742
+ var ROUTED_FACADES = /* @__PURE__ */ new Set([
16743
+ CATALOG_TOOL,
16744
+ DESCRIBE_TOOL,
16745
+ CALL_TOOL,
16746
+ CONFIRM_STATUS_TOOL,
16747
+ SESSION_STATE_TOOL
16748
+ ]);
16447
16749
  function ownTools(stage) {
16448
- 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;
16449
16779
  }
16450
16780
  function rpcToolError(id, err) {
16451
16781
  const policy = policyFor(err.reason);
@@ -16487,7 +16817,7 @@ function frameMcpResponse(payload, accept) {
16487
16817
  function offlineCatalog(bridge, stage, id, accept) {
16488
16818
  const base = bridge.lastKnownTools.length > 0 ? bridge.lastKnownTools : staticDiscoveryCatalog();
16489
16819
  return frameMcpResponse(
16490
- rpcResult(id, { tools: [...base, ...ownTools(stage)] }),
16820
+ rpcResult(id, { tools: withOwnTools(base, stage) }),
16491
16821
  accept
16492
16822
  );
16493
16823
  }
@@ -16508,7 +16838,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
16508
16838
  try {
16509
16839
  const { tools } = await bridge.listTools();
16510
16840
  return frameMcpResponse(
16511
- rpcResult(id, { tools: [...tools, ...ownTools(stage)] }),
16841
+ rpcResult(id, { tools: withOwnTools(tools, stage) }),
16512
16842
  accept
16513
16843
  );
16514
16844
  } catch (err) {
@@ -16527,7 +16857,23 @@ async function handleMcpRequest(bridge, body, accept, stage) {
16527
16857
  accept
16528
16858
  );
16529
16859
  }
16530
- 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
+ }
16531
16877
  if (params.name === BROWSERS_TOOL) {
16532
16878
  return frameMcpResponse(
16533
16879
  rpcResult(id, {
@@ -16556,7 +16902,7 @@ async function handleMcpRequest(bridge, body, accept, stage) {
16556
16902
  );
16557
16903
  }
16558
16904
  try {
16559
- const call = await bridge.callTool(params.name, args);
16905
+ const call = browser === void 0 ? await bridge.callTool(params.name, args) : await bridge.callTool(params.name, args, browser);
16560
16906
  return frameMcpResponse(
16561
16907
  rpcResult(id, {
16562
16908
  content: [{ type: "text", text: JSON.stringify(call.result) }],
@@ -16610,17 +16956,16 @@ async function openLocalImage(path) {
16610
16956
  message: "\uC804\uCCB4 \uACBD\uB85C\uB97C \uC54C\uB824 \uC8FC\uC138\uC694(\uC608: /Users/me/photo.jpg)."
16611
16957
  };
16612
16958
  }
16613
- let size;
16959
+ let initial;
16614
16960
  try {
16615
- const info = await stat(path);
16616
- if (!info.isFile()) {
16961
+ initial = await lstat(path);
16962
+ if (initial.isSymbolicLink()) {
16617
16963
  return {
16618
16964
  ok: false,
16619
16965
  reason: "not_a_file",
16620
- 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."
16621
16967
  };
16622
16968
  }
16623
- size = info.size;
16624
16969
  } catch {
16625
16970
  return {
16626
16971
  ok: false,
@@ -16628,47 +16973,80 @@ async function openLocalImage(path) {
16628
16973
  message: "\uADF8 \uACBD\uB85C\uC5D0\uC11C \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694."
16629
16974
  };
16630
16975
  }
16631
- if (size > MAX_FILE_BYTES) {
16632
- return {
16633
- ok: false,
16634
- reason: "too_large",
16635
- message: `\uC0AC\uC9C4\uC774 \uB108\uBB34 \uCEE4\uC694(\uCD5C\uB300 ${String(MAX_FILE_BYTES / 1024 / 1024)}MB).`
16636
- };
16637
- }
16638
- if (size === 0) {
16639
- return { ok: false, reason: "empty", message: "\uBE48 \uD30C\uC77C\uC774\uC5D0\uC694." };
16640
- }
16641
- let contentType = null;
16976
+ let handle;
16642
16977
  try {
16643
- const fh = await open(path, "r");
16644
- try {
16645
- const head = new Uint8Array(12);
16646
- await fh.read(head, 0, 12, 0);
16647
- contentType = sniff(head);
16648
- } finally {
16649
- 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
+ };
16985
+ }
16986
+ handle = await open(
16987
+ path,
16988
+ constants.O_RDONLY | (process.platform === "win32" ? 0 : noFollow)
16989
+ );
16990
+ const info = await handle.stat();
16991
+ if (initial.dev !== info.dev || initial.ino !== info.ino) {
16992
+ await handle.close();
16993
+ return {
16994
+ ok: false,
16995
+ reason: "unreadable",
16996
+ message: "\uD30C\uC77C\uC744 \uC5EC\uB294 \uB3D9\uC548 \uACBD\uB85C\uAC00 \uBC14\uB00C\uC5C8\uC5B4\uC694."
16997
+ };
16998
+ }
16999
+ if (!info.isFile()) {
17000
+ await handle.close();
17001
+ return {
17002
+ ok: false,
17003
+ reason: "not_a_file",
17004
+ message: "\uADF8 \uACBD\uB85C\uB294 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694."
17005
+ };
16650
17006
  }
17007
+ if (info.size > MAX_FILE_BYTES) {
17008
+ await handle.close();
17009
+ return {
17010
+ ok: false,
17011
+ reason: "too_large",
17012
+ message: `\uC0AC\uC9C4\uC774 \uB108\uBB34 \uCEE4\uC694(\uCD5C\uB300 ${String(MAX_FILE_BYTES / 1024 / 1024)}MB).`
17013
+ };
17014
+ }
17015
+ if (info.size === 0) {
17016
+ await handle.close();
17017
+ return { ok: false, reason: "empty", message: "\uBE48 \uD30C\uC77C\uC774\uC5D0\uC694." };
17018
+ }
17019
+ const head = new Uint8Array(12);
17020
+ await handle.read(head, 0, head.byteLength, 0);
17021
+ const contentType = sniff(head);
17022
+ if (contentType === null) {
17023
+ await handle.close();
17024
+ return {
17025
+ ok: false,
17026
+ reason: "not_an_image",
17027
+ message: "\uC774\uBBF8\uC9C0 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694(PNG\xB7JPEG\xB7GIF\xB7WebP\uB9CC \uB123\uC744 \uC218 \uC788\uC5B4\uC694)."
17028
+ };
17029
+ }
17030
+ return {
17031
+ ok: true,
17032
+ stream: handle.createReadStream({
17033
+ autoClose: true,
17034
+ start: 0,
17035
+ end: info.size - 1
17036
+ }),
17037
+ contentType,
17038
+ byteLength: info.size,
17039
+ fileName: basename(path)
17040
+ };
16651
17041
  } catch {
17042
+ await handle?.close().catch(() => {
17043
+ });
16652
17044
  return {
16653
17045
  ok: false,
16654
17046
  reason: "unreadable",
16655
17047
  message: "\uD30C\uC77C\uC744 \uC5F4\uC9C0 \uBABB\uD588\uC5B4\uC694."
16656
17048
  };
16657
17049
  }
16658
- if (contentType === null) {
16659
- return {
16660
- ok: false,
16661
- reason: "not_an_image",
16662
- message: "\uC774\uBBF8\uC9C0 \uD30C\uC77C\uC774 \uC544\uB2C8\uC5D0\uC694(PNG\xB7JPEG\xB7GIF\xB7WebP\uB9CC \uB123\uC744 \uC218 \uC788\uC5B4\uC694)."
16663
- };
16664
- }
16665
- return {
16666
- ok: true,
16667
- stream: createReadStream(path),
16668
- contentType,
16669
- byteLength: size,
16670
- fileName: basename(path)
16671
- };
16672
17050
  }
16673
17051
  var MAX_BODY_BYTES = 64 * 1024;
16674
17052
  var DEFAULT_TOOLS_CHANGED_DEBOUNCE_MS = 600;
@@ -16685,6 +17063,9 @@ function headerValue(raw) {
16685
17063
  const trimmed = one?.trim();
16686
17064
  return trimmed ? trimmed.slice(0, 128) : void 0;
16687
17065
  }
17066
+ function correlationValue(value, max) {
17067
+ return /^[a-zA-Z0-9_-]+$/.test(value) ? value.slice(0, max) : "redacted";
17068
+ }
16688
17069
  function createHttpBridge(opts) {
16689
17070
  const host = opts.host ?? "127.0.0.1";
16690
17071
  if (!isLoopbackHost(host)) {
@@ -16763,22 +17144,34 @@ function createHttpBridge(opts) {
16763
17144
  `);
16764
17145
  return true;
16765
17146
  };
16766
- const installStream = (key, res) => {
17147
+ const installStream = (key, sessionId, res) => {
16767
17148
  if (streams.has(key)) {
16768
- throw new Error(`stream key ${key} is still occupied`);
17149
+ throw new Error("stream slot is still occupied");
16769
17150
  }
16770
17151
  const heartbeat = setInterval(() => {
16771
17152
  send({ t: "hb", at: Date.now() }, key);
16772
17153
  }, opts.heartbeatMs ?? HEARTBEAT_INTERVAL_MS);
16773
17154
  heartbeat.unref?.();
16774
- streams.set(key, { res, heartbeat });
17155
+ streams.set(key, { res, sessionId, heartbeat, fileStreams: /* @__PURE__ */ new Set() });
17156
+ if (!opts.bridge.activateSession(key, sessionId)) {
17157
+ clearInterval(heartbeat);
17158
+ deleteStreamEntry(key);
17159
+ throw new Error("stream session is no longer current");
17160
+ }
16775
17161
  lifecycle?.retain?.();
16776
17162
  };
17163
+ const deleteStreamEntry = (key) => {
17164
+ streams.delete(key);
17165
+ };
16777
17166
  const removeStream = (key) => {
16778
17167
  const stream = streams.get(key);
16779
17168
  if (!stream) return null;
16780
17169
  clearInterval(stream.heartbeat);
16781
- 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);
16782
17175
  lifecycle?.release?.();
16783
17176
  return stream.res;
16784
17177
  };
@@ -16857,14 +17250,10 @@ function createHttpBridge(opts) {
16857
17250
  return;
16858
17251
  }
16859
17252
  if (req.method === "GET" && url2.pathname === "/bridge/events") {
16860
- if (!opts.bridge.connected) {
16861
- json2(res, 403, { error: "handshake required" });
16862
- return;
16863
- }
16864
17253
  const claimed = url2.searchParams.get("session");
16865
- const key = claimed === null ? opts.bridge.sessionKey : opts.bridge.keyForSession(claimed);
17254
+ const key = claimed === null ? opts.bridge.legacySessionKey() : opts.bridge.keyForSession(claimed);
16866
17255
  if (key === null) {
16867
- opts.log?.(`refused a stream for session ${claimed ?? "unnamed"}`);
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?.(`refused an unauthorised stream for ${key}`);
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
- 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);
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
- if (!opts.bridge.verifyLiveSessionToken(
16956
- typeof b?.token === "string" ? b.token : ""
16957
- )) {
16958
- 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
+ });
17366
+ return;
17367
+ }
17368
+ const auth = hasSessionId ? opts.bridge.authorizeFileRequest({
17369
+ sessionId: b.sessionId,
17370
+ ...hasRequestId ? { requestId: b.requestId } : {},
17371
+ token,
17372
+ path,
17373
+ ...typeof b?.tool === "string" ? { tool: b.tool } : {}
17374
+ }) : opts.bridge.authorizeLegacyFileCredential(token);
17375
+ if (!auth.ok) {
17376
+ opts.log?.(
17377
+ `file request refused: reason=${auth.reason} credential=${auth.credential} tokenPresent=${String(token.length > 0)} sessionMatch=${String(auth.key !== void 0)} open=not-entered`
17378
+ );
17379
+ json2(res, 403, {
17380
+ error: "unauthorized",
17381
+ message: "\uC0AC\uC9C4\uC744 \uAC00\uC838\uC62C \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC5B4\uC694."
17382
+ });
16959
17383
  return;
16960
17384
  }
16961
- const out = await openLocalImage(
16962
- typeof b?.path === "string" ? b.path : ""
17385
+ const streamEntry = streams.get(auth.key);
17386
+ if (!streamEntry || streamEntry.sessionId !== auth.sessionId || !opts.bridge.isSessionStreamActive(auth.key, auth.sessionId) || !opts.bridge.consumeFileRequest(auth)) {
17387
+ opts.log?.(
17388
+ `file request refused: reason=file_session_not_live credential=${auth.credential} tokenPresent=true sessionMatch=false open=not-entered`
17389
+ );
17390
+ json2(res, 403, {
17391
+ error: "unauthorized",
17392
+ message: "\uC0AC\uC9C4\uC744 \uAC00\uC838\uC62C \uC5F0\uACB0\uC774 \uC5F4\uB824 \uC788\uC9C0 \uC54A\uC544\uC694."
17393
+ });
17394
+ return;
17395
+ }
17396
+ opts.log?.(
17397
+ `file request authorised: browser=${correlationValue(auth.key, 8)} session=${correlationValue(auth.sessionId, 32)} request=${auth.requestId ? correlationValue(auth.requestId, 96) : "legacy"} credential=${auth.credential} registryMatch=${String(auth.credential === "claimed_browser")} open=entered`
16963
17398
  );
16964
- if (!out.ok) {
16965
- 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" });
16966
17454
  return;
16967
17455
  }
16968
- res.writeHead(200, {
16969
- "content-type": out.contentType,
16970
- "content-length": String(out.byteLength),
16971
- // The extension shows this in its confirmation, so the user knows
16972
- // which file is about to be used.
16973
- "x-file-name": encodeURIComponent(out.fileName),
16974
- "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" }
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.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";
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
- 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;
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) opts.claimed.delete(claim.browserId);
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
- 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) {
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,