@modootoday/datalab-extension-mcp 1.8.6 → 1.8.8

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