canvas-parent-mcp 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -25548,7 +25548,7 @@ var require_websocket = __commonJS({
25548
25548
  var http = __require("http");
25549
25549
  var net = __require("net");
25550
25550
  var tls = __require("tls");
25551
- var { randomBytes: randomBytes3, createHash } = __require("crypto");
25551
+ var { randomBytes: randomBytes4, createHash } = __require("crypto");
25552
25552
  var { Duplex, Readable } = __require("stream");
25553
25553
  var { URL: URL2 } = __require("url");
25554
25554
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -26086,7 +26086,7 @@ var require_websocket = __commonJS({
26086
26086
  }
26087
26087
  }
26088
26088
  const defaultPort = isSecure ? 443 : 80;
26089
- const key = randomBytes3(16).toString("base64");
26089
+ const key = randomBytes4(16).toString("base64");
26090
26090
  const request = isSecure ? https.request : http.request;
26091
26091
  const protocolSet = /* @__PURE__ */ new Set();
26092
26092
  let perMessageDeflate;
@@ -26983,6 +26983,20 @@ var require_websocket_server = __commonJS({
26983
26983
  import { dirname as dirname3, join as join4 } from "path";
26984
26984
  import { fileURLToPath } from "url";
26985
26985
 
26986
+ // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
26987
+ import { AsyncLocalStorage } from "node:async_hooks";
26988
+ var storage = new AsyncLocalStorage();
26989
+ function withCallSignal(signal, fn, request) {
26990
+ return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
26991
+ }
26992
+
26993
+ // node_modules/@chrischall/mcp-utils/dist/caller/index.js
26994
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
26995
+ var storage2 = new AsyncLocalStorage2();
26996
+ function withCallerCapabilities(capabilities, fn) {
26997
+ return capabilities ? storage2.run(capabilities, fn) : fn();
26998
+ }
26999
+
26986
27000
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
26987
27001
  var __create2 = Object.create;
26988
27002
  var __defProp2 = Object.defineProperty;
@@ -41662,13 +41676,28 @@ var API_KEY_RE = new RegExp([
41662
41676
  "whsec_[A-Za-z0-9]{16,}\\b"
41663
41677
  // webhook signing secret (Stripe-style)
41664
41678
  ].map((p) => `\\b${p}`).join("|"), "g");
41665
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
41679
+ var QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
41680
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
41681
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
41682
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
41666
41683
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
41667
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
41668
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
41669
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
41684
+ var JSON_SECRET_KEYS = [
41685
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
41686
+ "client[_-]?secret",
41687
+ "api[_-]?secret",
41688
+ "(?:x[_-])?api[_-]?key",
41689
+ "private[_-]?key",
41690
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
41691
+ "(?:proxy-)?authorization",
41692
+ "password",
41693
+ "passwd",
41694
+ "secret",
41695
+ "token"
41696
+ ].join("|");
41697
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
41698
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
41670
41699
  function redactSecrets(text) {
41671
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
41700
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
41672
41701
  }
41673
41702
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
41674
41703
  const str = text === null || text === void 0 ? "" : String(text);
@@ -61447,17 +61476,73 @@ Hint: ${err.hint}`);
61447
61476
  }
61448
61477
  throw err;
61449
61478
  }
61479
+ function mcpRequestFrom(args) {
61480
+ const ctx = args.at(-1);
61481
+ if (typeof ctx !== "object" || ctx === null)
61482
+ return void 0;
61483
+ const req = ctx.mcpReq;
61484
+ if (typeof req !== "object" || req === null)
61485
+ return void 0;
61486
+ return typeof req.notify === "function" ? req : void 0;
61487
+ }
61488
+ function callSignalFrom(args) {
61489
+ const ctx = args.at(-1);
61490
+ if (typeof ctx !== "object" || ctx === null)
61491
+ return void 0;
61492
+ const req = ctx.mcpReq;
61493
+ if (typeof req !== "object" || req === null)
61494
+ return void 0;
61495
+ const signal = req.signal;
61496
+ return signal instanceof AbortSignal ? signal : void 0;
61497
+ }
61498
+ function declaredCapabilitiesFrom(server) {
61499
+ const inner = server.server;
61500
+ if (typeof inner !== "object" || inner === null)
61501
+ return void 0;
61502
+ const read = inner.getClientCapabilities;
61503
+ if (typeof read !== "function")
61504
+ return void 0;
61505
+ try {
61506
+ const declared = read.call(inner);
61507
+ return typeof declared === "object" && declared !== null ? declared : void 0;
61508
+ } catch {
61509
+ return void 0;
61510
+ }
61511
+ }
61450
61512
  function surfaceToolHints(server) {
61451
61513
  const register2 = server.registerTool.bind(server);
61452
- server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => {
61453
- let result;
61454
- try {
61455
- result = cb(...args);
61456
- } catch (err) {
61457
- return hintResultOrRethrow(err);
61458
- }
61459
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
61460
- });
61514
+ server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
61515
+ // THE CALLER'S CANCELLATION, made ambient for the whole handler
61516
+ // (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
61517
+ // measured against @modelcontextprotocol/server 2.0.0, a cancelled
61518
+ // call aborts `ctx.mcpReq.signal` with the caller's reason and the
61519
+ // handler runs to completion regardless — so the HTTP request stays
61520
+ // in flight, the child keeps burning metered CPU, and the upstream
61521
+ // keeps being hit for somebody who has gone. claude.ai sent 101
61522
+ // cancellations in the week to 2026-09-20.
61523
+ //
61524
+ // Here because this is the one wrapper every tool already passes
61525
+ // through: threading the signal by hand would mean editing several
61526
+ // hundred handlers and missing exactly the ones nobody edits.
61527
+ withCallSignal(callSignalFrom(args), () => (
61528
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
61529
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
61530
+ // the wrapper — a 2026-07-28 request answers for itself off its own
61531
+ // envelope — but a guarded tool that cannot tell whether the caller
61532
+ // can be shown a prompt returns one the SDK then refuses to deliver,
61533
+ // and that refusal reaches the caller as an unexplained protocol
61534
+ // error it had no chance to catch.
61535
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
61536
+ let result;
61537
+ try {
61538
+ result = cb(...args);
61539
+ } catch (err) {
61540
+ return hintResultOrRethrow(err);
61541
+ }
61542
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
61543
+ })
61544
+ ), mcpRequestFrom(args))
61545
+ ));
61461
61546
  }
61462
61547
  async function createMcpServer(opts) {
61463
61548
  const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
@@ -61472,13 +61557,47 @@ async function createMcpServer(opts) {
61472
61557
  }
61473
61558
  return server;
61474
61559
  }
61560
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
61561
+ var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
61475
61562
  function withGracefulShutdown(target, opts = {}) {
61476
61563
  const shouldExit = opts.exit ?? true;
61564
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
61565
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
61566
+ }
61567
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
61568
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
61569
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
61570
+ }
61571
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
61477
61572
  let shuttingDown = false;
61573
+ let firstSignalAt = 0;
61574
+ let exited = false;
61575
+ const exitOnce = () => {
61576
+ if (exited)
61577
+ return;
61578
+ exited = true;
61579
+ process.exit(0);
61580
+ };
61478
61581
  const handler = (signal) => {
61479
- if (shuttingDown)
61582
+ if (shuttingDown) {
61583
+ if (Date.now() - firstSignalAt < repeatGraceMs)
61584
+ return;
61585
+ if (shouldExit) {
61586
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
61587
+ exitOnce();
61588
+ }
61480
61589
  return;
61590
+ }
61481
61591
  shuttingDown = true;
61592
+ firstSignalAt = Date.now();
61593
+ let timer;
61594
+ if (shouldExit) {
61595
+ timer = setTimeout(() => {
61596
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
61597
+ exitOnce();
61598
+ }, timeoutMs);
61599
+ timer.unref?.();
61600
+ }
61482
61601
  void (async () => {
61483
61602
  try {
61484
61603
  if (opts.onSignal)
@@ -61487,8 +61606,10 @@ function withGracefulShutdown(target, opts = {}) {
61487
61606
  } catch (err) {
61488
61607
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
61489
61608
  } finally {
61609
+ if (timer !== void 0)
61610
+ clearTimeout(timer);
61490
61611
  if (shouldExit)
61491
- process.exit(0);
61612
+ exitOnce();
61492
61613
  }
61493
61614
  })();
61494
61615
  };
@@ -61695,6 +61816,33 @@ var pageSchema = {
61695
61816
  };
61696
61817
 
61697
61818
  // node_modules/@chrischall/mcp-utils/dist/auth/index.js
61819
+ var OAuth2RefreshError = class extends McpToolError {
61820
+ status;
61821
+ constructor(status, message, opts) {
61822
+ super(message, opts);
61823
+ this.name = "OAuth2RefreshError";
61824
+ this.status = status;
61825
+ Object.setPrototypeOf(this, new.target.prototype);
61826
+ }
61827
+ };
61828
+ var OAuth2RotationPersistError = class extends McpToolError {
61829
+ persistenceFailure = true;
61830
+ result;
61831
+ constructor(result, cause) {
61832
+ super(`OAuth2 refresh token rotated, but saving it failed: ${truncateErrorMessage(messageOf2(cause), 200)}`, {
61833
+ hint: "The new refresh token is still held in memory; fix the storage problem before the process restarts.",
61834
+ cause
61835
+ });
61836
+ this.name = "OAuth2RotationPersistError";
61837
+ this.result = result;
61838
+ Object.setPrototypeOf(this, new.target.prototype);
61839
+ }
61840
+ };
61841
+ function isRetryable(err) {
61842
+ if (!(err instanceof OAuth2RefreshError))
61843
+ return true;
61844
+ return err.status >= 500 || err.status === 429 || err.status === 408;
61845
+ }
61698
61846
  var sleep2 = (ms) => ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
61699
61847
  function createOAuth2Refresher(opts) {
61700
61848
  const doFetch = opts.fetchImpl ?? fetch;
@@ -61702,10 +61850,13 @@ function createOAuth2Refresher(opts) {
61702
61850
  const maxRetries = opts.retry?.count ?? 0;
61703
61851
  const retryDelayMs = opts.retry?.delayMs ?? 0;
61704
61852
  let inFlight = null;
61853
+ let currentRefreshToken = opts.refreshToken;
61854
+ const spent = /* @__PURE__ */ new Set();
61855
+ let rotatedTo;
61705
61856
  async function exchangeOnce() {
61706
61857
  const body = new URLSearchParams({
61707
61858
  grant_type: grantType,
61708
- refresh_token: opts.refreshToken,
61859
+ refresh_token: currentRefreshToken,
61709
61860
  ...opts.params
61710
61861
  }).toString();
61711
61862
  const res = await doFetch(opts.endpoint, {
@@ -61718,7 +61869,7 @@ function createOAuth2Refresher(opts) {
61718
61869
  });
61719
61870
  if (!res.ok) {
61720
61871
  const errText = await res.text().catch(() => "");
61721
- throw createHelpfulError(`OAuth2 token refresh failed: ${res.status} ${res.statusText}: ${truncateErrorMessage(errText, 200)}`, { hint: "The refresh token may be expired or revoked \u2014 re-authenticate." });
61872
+ throw new OAuth2RefreshError(res.status, `OAuth2 token refresh failed: ${res.status} ${res.statusText}: ${truncateErrorMessage(errText, 200)}`, { hint: "The refresh token may be expired or revoked \u2014 re-authenticate." });
61722
61873
  }
61723
61874
  const data = await res.json().catch(() => null);
61724
61875
  const accessToken = data?.access_token;
@@ -61730,6 +61881,11 @@ function createOAuth2Refresher(opts) {
61730
61881
  const result = { accessToken };
61731
61882
  if (typeof data?.refresh_token === "string" && data.refresh_token.length > 0) {
61732
61883
  result.refreshToken = data.refresh_token;
61884
+ if (data.refresh_token !== currentRefreshToken) {
61885
+ spent.add(currentRefreshToken);
61886
+ currentRefreshToken = data.refresh_token;
61887
+ rotatedTo = data.refresh_token;
61888
+ }
61733
61889
  }
61734
61890
  if (typeof data?.expires_in === "number") {
61735
61891
  result.expiresIn = data.expires_in;
@@ -61744,16 +61900,32 @@ function createOAuth2Refresher(opts) {
61744
61900
  return await exchangeOnce();
61745
61901
  } catch (e) {
61746
61902
  lastErr = e;
61903
+ if (!isRetryable(e))
61904
+ break;
61747
61905
  if (attempt < maxRetries)
61748
61906
  await sleep2(retryDelayMs);
61749
61907
  }
61750
61908
  }
61751
61909
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
61752
61910
  }
61753
- return function refresh() {
61911
+ return function refresh(refreshToken) {
61754
61912
  if (inFlight)
61755
61913
  return inFlight;
61756
- const p = exchangeWithRetry().finally(() => {
61914
+ if (typeof refreshToken === "string" && refreshToken.length > 0 && !spent.has(refreshToken)) {
61915
+ currentRefreshToken = refreshToken;
61916
+ }
61917
+ const p = (async () => {
61918
+ rotatedTo = void 0;
61919
+ const result = await exchangeWithRetry();
61920
+ if (rotatedTo !== void 0 && opts.onRotate) {
61921
+ try {
61922
+ await opts.onRotate(rotatedTo);
61923
+ } catch (err) {
61924
+ throw new OAuth2RotationPersistError(result, err);
61925
+ }
61926
+ }
61927
+ return result;
61928
+ })().finally(() => {
61757
61929
  if (inFlight === p)
61758
61930
  inFlight = null;
61759
61931
  });
@@ -61761,6 +61933,9 @@ function createOAuth2Refresher(opts) {
61761
61933
  return p;
61762
61934
  };
61763
61935
  }
61936
+ function messageOf2(err) {
61937
+ return err instanceof Error ? err.message : String(err);
61938
+ }
61764
61939
 
61765
61940
  // node_modules/@fetchproxy/protocol/dist/crypto.js
61766
61941
  var subtle = globalThis.crypto.subtle;
@@ -61959,6 +62134,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
61959
62134
  "capture_redirect",
61960
62135
  "read_indexed_db",
61961
62136
  "read_dom",
62137
+ "read_dom_list",
61962
62138
  "download",
61963
62139
  "graphql",
61964
62140
  "write_cookies"
@@ -62459,6 +62635,79 @@ function assertDomSelectorsArray(value, label) {
62459
62635
  }
62460
62636
  }
62461
62637
  }
62638
+ function assertDomListSelectorsArray(value, label) {
62639
+ if (!Array.isArray(value)) {
62640
+ throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
62641
+ }
62642
+ const seen = /* @__PURE__ */ new Set();
62643
+ for (let i = 0; i < value.length; i++) {
62644
+ const entry = value[i];
62645
+ assertObject(entry, `${label}[${i}]`);
62646
+ if (entry.name === void 0) {
62647
+ throw new ProtocolError3(`${label}[${i}].name: missing`);
62648
+ }
62649
+ if (entry.itemSelector === void 0) {
62650
+ throw new ProtocolError3(`${label}[${i}].itemSelector: missing`);
62651
+ }
62652
+ if (entry.fields === void 0) {
62653
+ throw new ProtocolError3(`${label}[${i}].fields: missing`);
62654
+ }
62655
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
62656
+ throw new ProtocolError3(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
62657
+ }
62658
+ if (typeof entry.itemSelector !== "string" || !DOM_SELECTOR_RE.test(entry.itemSelector)) {
62659
+ throw new ProtocolError3(`${label}[${i}].itemSelector: invalid ${JSON.stringify(entry.itemSelector)}`);
62660
+ }
62661
+ if (!Array.isArray(entry.fields) || entry.fields.length === 0) {
62662
+ throw new ProtocolError3(`${label}[${i}].fields: expected non-empty array`);
62663
+ }
62664
+ const fieldNames = /* @__PURE__ */ new Set();
62665
+ for (let j = 0; j < entry.fields.length; j++) {
62666
+ const field = entry.fields[j];
62667
+ const fieldLabel = `${label}[${i}].fields[${j}]`;
62668
+ assertObject(field, fieldLabel);
62669
+ if (field.name === void 0) {
62670
+ throw new ProtocolError3(`${fieldLabel}.name: missing`);
62671
+ }
62672
+ if (typeof field.name !== "string" || !SCOPE_KEY_RE.test(field.name)) {
62673
+ throw new ProtocolError3(`${fieldLabel}.name: invalid ${JSON.stringify(field.name)}`);
62674
+ }
62675
+ if (field.selector !== void 0) {
62676
+ if (typeof field.selector !== "string" || !DOM_SELECTOR_RE.test(field.selector)) {
62677
+ throw new ProtocolError3(`${fieldLabel}.selector: invalid ${JSON.stringify(field.selector)}`);
62678
+ }
62679
+ }
62680
+ if (field.attribute !== void 0) {
62681
+ if (typeof field.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(field.attribute)) {
62682
+ throw new ProtocolError3(`${fieldLabel}.attribute: invalid ${JSON.stringify(field.attribute)}`);
62683
+ }
62684
+ }
62685
+ if (fieldNames.has(field.name)) {
62686
+ throw new ProtocolError3(`${label}[${i}].fields: duplicate name ${JSON.stringify(field.name)}`);
62687
+ }
62688
+ fieldNames.add(field.name);
62689
+ for (const k of Object.keys(field)) {
62690
+ if (k !== "name" && k !== "selector" && k !== "attribute") {
62691
+ throw new ProtocolError3(`${fieldLabel}: unexpected field ${JSON.stringify(k)}`);
62692
+ }
62693
+ }
62694
+ }
62695
+ if (entry.maxItems !== void 0) {
62696
+ if (typeof entry.maxItems !== "number" || !Number.isInteger(entry.maxItems) || entry.maxItems < 1 || entry.maxItems > 1e3) {
62697
+ throw new ProtocolError3(`${label}[${i}].maxItems: invalid ${JSON.stringify(entry.maxItems)}`);
62698
+ }
62699
+ }
62700
+ if (seen.has(entry.name)) {
62701
+ throw new ProtocolError3(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
62702
+ }
62703
+ seen.add(entry.name);
62704
+ for (const k of Object.keys(entry)) {
62705
+ if (k !== "name" && k !== "itemSelector" && k !== "fields" && k !== "maxItems") {
62706
+ throw new ProtocolError3(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
62707
+ }
62708
+ }
62709
+ }
62710
+ }
62462
62711
  function assertGraphqlOpsArray(value, label) {
62463
62712
  if (!Array.isArray(value)) {
62464
62713
  throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
@@ -62505,6 +62754,8 @@ function validateFrame(raw) {
62505
62754
  return validateHelloRejected(raw);
62506
62755
  if (t === "extension-disconnected")
62507
62756
  return { type: "extension-disconnected" };
62757
+ if (t === "peer-gone")
62758
+ return validatePeerGone(raw);
62508
62759
  throw new ProtocolError3(`unknown frame type: ${String(t)}`);
62509
62760
  }
62510
62761
  function validateHello(raw) {
@@ -62575,16 +62826,11 @@ function validateHello(raw) {
62575
62826
  if (raw.domSelectors !== void 0) {
62576
62827
  assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
62577
62828
  }
62578
- if (raw.accepts !== void 0) {
62579
- if (!Array.isArray(raw.accepts)) {
62580
- throw new ProtocolError3("hello.accepts: expected array");
62581
- }
62582
- for (const a of raw.accepts) {
62583
- if (typeof a !== "string") {
62584
- throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
62585
- }
62586
- }
62829
+ if (raw.domListSelectors !== void 0) {
62830
+ assertDomListSelectorsArray(raw.domListSelectors, "hello.domListSelectors");
62587
62831
  }
62832
+ if (raw.accepts !== void 0)
62833
+ assertAcceptsList(raw.accepts);
62588
62834
  if (raw.graphqlOps !== void 0) {
62589
62835
  assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
62590
62836
  }
@@ -62606,6 +62852,8 @@ function validateHello(raw) {
62606
62852
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
62607
62853
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
62608
62854
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
62855
+ if (raw.accepts !== void 0)
62856
+ assertAcceptsList(raw.accepts);
62609
62857
  return raw;
62610
62858
  }
62611
62859
  throw new ProtocolError3(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
@@ -62661,6 +62909,27 @@ function validatePairPending(raw) {
62661
62909
  }
62662
62910
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
62663
62911
  }
62912
+ function assertAcceptsList(accepts) {
62913
+ if (!Array.isArray(accepts)) {
62914
+ throw new ProtocolError3("hello.accepts: expected array");
62915
+ }
62916
+ for (const a of accepts) {
62917
+ if (typeof a !== "string") {
62918
+ throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
62919
+ }
62920
+ }
62921
+ }
62922
+ function validatePeerGone(raw) {
62923
+ assertString(raw.mcpId, "peer-gone.mcpId");
62924
+ if (!isValidMcpId(raw.mcpId))
62925
+ throw new ProtocolError3("peer-gone.mcpId: invalid format");
62926
+ for (const k of Object.keys(raw)) {
62927
+ if (k !== "type" && k !== "mcpId") {
62928
+ throw new ProtocolError3(`peer-gone: unexpected field ${JSON.stringify(k)}`);
62929
+ }
62930
+ }
62931
+ return { type: "peer-gone", mcpId: raw.mcpId };
62932
+ }
62664
62933
  function validateHelloRejected(raw) {
62665
62934
  assertString(raw.mcpId, "hello-rejected.mcpId");
62666
62935
  if (!isValidMcpId(raw.mcpId))
@@ -62910,6 +63179,24 @@ function validateInnerRequest(raw) {
62910
63179
  }
62911
63180
  return raw;
62912
63181
  }
63182
+ if (raw.op === "read_dom_list") {
63183
+ assertObject(raw.init, "inner.init");
63184
+ if (raw.init.origin === void 0)
63185
+ throw new ProtocolError3("inner.init.origin: missing");
63186
+ if (raw.init.name === void 0)
63187
+ throw new ProtocolError3("inner.init.name: missing");
63188
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
63189
+ assertString(raw.init.name, "inner.init.name");
63190
+ if (raw.init.name.length === 0) {
63191
+ throw new ProtocolError3("inner.init.name: must be non-empty");
63192
+ }
63193
+ for (const k of Object.keys(raw.init)) {
63194
+ if (k !== "origin" && k !== "name") {
63195
+ throw new ProtocolError3(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom_list`);
63196
+ }
63197
+ }
63198
+ return raw;
63199
+ }
62913
63200
  if (raw.op === "graphql_query") {
62914
63201
  assertObject(raw.init, "inner.init");
62915
63202
  if (raw.init.name === void 0)
@@ -62957,7 +63244,7 @@ function validateInnerRequest(raw) {
62957
63244
  }
62958
63245
  return raw;
62959
63246
  }
62960
- throw new ProtocolError3(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
63247
+ throw new ProtocolError3(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "read_dom_list", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
62961
63248
  }
62962
63249
  function assertNonEmptyKeyArray(value, label) {
62963
63250
  if (!Array.isArray(value)) {
@@ -63065,6 +63352,18 @@ function validateInnerResponse(raw) {
63065
63352
  assertStringMap(raw.values, "inner.values");
63066
63353
  return raw;
63067
63354
  }
63355
+ if (op === "read_dom_list") {
63356
+ if (raw.rows === void 0) {
63357
+ throw new ProtocolError3("inner.rows: missing on read_dom_list response");
63358
+ }
63359
+ if (!Array.isArray(raw.rows)) {
63360
+ throw new ProtocolError3("inner.rows: expected array, got " + typeof raw.rows);
63361
+ }
63362
+ for (const [i, row] of raw.rows.entries()) {
63363
+ assertStringMap(row, `inner.rows[${i}]`);
63364
+ }
63365
+ return raw;
63366
+ }
63068
63367
  if (op === "graphql_query") {
63069
63368
  if (raw.data === void 0) {
63070
63369
  throw new ProtocolError3("inner.data: missing on graphql_query response");
@@ -63166,12 +63465,6 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner, direction) {
63166
63465
  ciphertext: toB64(ct)
63167
63466
  };
63168
63467
  }
63169
- async function openEncryptedFrame(sessionKey, frame, direction) {
63170
- const result = await openEncryptedFrameDetailed(sessionKey, frame, direction);
63171
- if (result.stage === "ok")
63172
- return result.inner;
63173
- throw result.error instanceof Error ? result.error : new Error(String(result.error));
63174
- }
63175
63468
  async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
63176
63469
  let pt;
63177
63470
  try {
@@ -63323,6 +63616,18 @@ async function buildServerHello(opts) {
63323
63616
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
63324
63617
  }));
63325
63618
  }
63619
+ if (opts.domListSelectors && opts.domListSelectors.length > 0) {
63620
+ hello.domListSelectors = opts.domListSelectors.map((d) => ({
63621
+ name: d.name,
63622
+ itemSelector: d.itemSelector,
63623
+ fields: d.fields.map((f) => ({
63624
+ name: f.name,
63625
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
63626
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
63627
+ })),
63628
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
63629
+ }));
63630
+ }
63326
63631
  if (opts.graphqlOps && opts.graphqlOps.length > 0) {
63327
63632
  hello.graphqlOps = opts.graphqlOps.map((d) => ({
63328
63633
  name: d.name,
@@ -63491,11 +63796,12 @@ async function awaitSessionReady(ready, opts) {
63491
63796
  }
63492
63797
 
63493
63798
  // node_modules/@fetchproxy/server/dist/extension-trust.js
63494
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
63799
+ import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2, rm as rm2 } from "node:fs/promises";
63800
+ import { randomBytes as randomBytes2 } from "node:crypto";
63495
63801
  import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
63496
63802
 
63497
63803
  // node_modules/@fetchproxy/server/dist/identity.js
63498
- import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
63804
+ import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
63499
63805
  import { randomBytes } from "node:crypto";
63500
63806
  import { isAbsolute, join } from "node:path";
63501
63807
  import { homedir } from "node:os";
@@ -63563,7 +63869,7 @@ async function openIdentityDir(dir) {
63563
63869
  }
63564
63870
  }
63565
63871
  var STAGING_BYTES = 6;
63566
- async function sweepStagingFiles(dir, serverName) {
63872
+ async function sweepStagingFiles(dir, serverName, opts = {}) {
63567
63873
  const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
63568
63874
  const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
63569
63875
  let names;
@@ -63572,35 +63878,80 @@ async function sweepStagingFiles(dir, serverName) {
63572
63878
  } catch {
63573
63879
  return;
63574
63880
  }
63575
- await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join(dir, name)).catch(() => void 0)));
63881
+ const cutoff = opts.olderThanMs === void 0 ? null : Date.now() - opts.olderThanMs;
63882
+ await Promise.all(names.filter((name) => staging.test(name)).map(async (name) => {
63883
+ const path = join(dir, name);
63884
+ if (cutoff !== null) {
63885
+ const st = await lstat(path).catch(() => null);
63886
+ if (st === null || st.mtimeMs > cutoff)
63887
+ return;
63888
+ }
63889
+ await unlink(path).catch(() => void 0);
63890
+ }));
63576
63891
  }
63577
- async function writeIdentityFile(dir, serverName, id) {
63892
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
63578
63893
  const path = identityFilePath(dir, serverName);
63579
63894
  await openIdentityDir(dir);
63580
- await sweepStagingFiles(dir, serverName);
63581
- const tmp = `${path}.${randomBytes(STAGING_BYTES).toString("hex")}.tmp`;
63582
63895
  try {
63583
- await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
63584
- await chmod(tmp, 384);
63585
- await rename(tmp, path);
63896
+ return parseIdentity(await readFile(path, "utf8"));
63586
63897
  } catch (e) {
63587
- await rm(tmp, { force: true });
63588
- throw e;
63898
+ if (e.code !== "ENOENT" && !(e instanceof SyntaxError))
63899
+ throw e;
63900
+ }
63901
+ let unreadable = null;
63902
+ for (let attempt = 0; attempt < 5; attempt++) {
63903
+ const id = await generateIdentity();
63904
+ if (await publishIdentityIfAbsent(dir, serverName, id))
63905
+ return id;
63906
+ try {
63907
+ return parseIdentity(await readFile(path, "utf8"));
63908
+ } catch (e) {
63909
+ if (e.code === "ENOENT") {
63910
+ continue;
63911
+ }
63912
+ if (!(e instanceof SyntaxError))
63913
+ throw e;
63914
+ unreadable = e;
63915
+ await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
63916
+ }
63589
63917
  }
63590
- return path;
63918
+ if (unreadable !== null)
63919
+ throw unreadable;
63920
+ throw new Error(`fetchproxy: could not create an identity for ${JSON.stringify(serverName)} in ${dir}`);
63591
63921
  }
63592
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
63922
+ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
63923
+ async function publishIdentityIfAbsent(dir, serverName, id) {
63593
63924
  const path = identityFilePath(dir, serverName);
63594
- await openIdentityDir(dir);
63925
+ await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
63926
+ const tmp = `${path}.${randomBytes(STAGING_BYTES).toString("hex")}.tmp`;
63595
63927
  try {
63596
- return parseIdentity(await readFile(path, "utf8"));
63928
+ await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
63929
+ await chmod(tmp, 384);
63930
+ try {
63931
+ await link(tmp, path);
63932
+ } catch (e) {
63933
+ const code = e.code;
63934
+ if (code === void 0 || !NO_HARD_LINKS.has(code))
63935
+ throw e;
63936
+ try {
63937
+ await writeFile(path, serializeIdentity(id), { mode: 384, flag: "wx" });
63938
+ } catch (writeErr) {
63939
+ if (writeErr.code !== "EEXIST") {
63940
+ await rm(path, { force: true }).catch(() => void 0);
63941
+ }
63942
+ throw writeErr;
63943
+ }
63944
+ await chmod(path, 384);
63945
+ }
63946
+ return true;
63597
63947
  } catch (e) {
63598
- if (e.code !== "ENOENT")
63599
- throw e;
63948
+ const code = e.code;
63949
+ if (code === "EEXIST" || code === "ENOENT")
63950
+ return false;
63951
+ throw e;
63952
+ } finally {
63953
+ await rm(tmp, { force: true });
63600
63954
  }
63601
- const id = await generateIdentity();
63602
- await writeIdentityFile(dir, serverName, id);
63603
- return id;
63604
63955
  }
63605
63956
 
63606
63957
  // node_modules/@fetchproxy/server/dist/extension-trust.js
@@ -63701,10 +64052,15 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
63701
64052
  async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
63702
64053
  const path = extensionTrustPath(serverName, dir);
63703
64054
  await mkdir2(dir, { recursive: true, mode: 448 });
63704
- const tmp = `${path}.tmp`;
63705
- await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
63706
- await chmod2(tmp, 384);
63707
- await rename2(tmp, path);
64055
+ const tmp = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
64056
+ try {
64057
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384, flag: "wx" });
64058
+ await chmod2(tmp, 384);
64059
+ await rename2(tmp, path);
64060
+ } catch (e) {
64061
+ await rm2(tmp, { force: true });
64062
+ throw e;
64063
+ }
63708
64064
  }
63709
64065
 
63710
64066
  // node_modules/@fetchproxy/server/dist/host.js
@@ -63741,6 +64097,7 @@ function originVerdict(origin, allowLocal) {
63741
64097
  var HANDSHAKE_TIMEOUT_MS = 15e3;
63742
64098
  var MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
63743
64099
  var enc4 = new TextEncoder();
64100
+ var OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE = 3;
63744
64101
  async function startHost(opts) {
63745
64102
  const allowLocalOrigins = envAllowsLocalOrigins();
63746
64103
  if (allowLocalOrigins) {
@@ -63784,6 +64141,7 @@ async function startHost(opts) {
63784
64141
  localStoragePointers: opts.ownLocalStoragePointers,
63785
64142
  sessionStoragePointers: opts.ownSessionStoragePointers,
63786
64143
  domSelectors: opts.ownDomSelectors,
64144
+ domListSelectors: opts.ownDomListSelectors,
63787
64145
  graphqlOps: opts.ownGraphqlOps
63788
64146
  };
63789
64147
  const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
@@ -63854,6 +64212,31 @@ async function startHost(opts) {
63854
64212
  return null;
63855
64213
  }
63856
64214
  };
64215
+ let ownDecryptFailures = 0;
64216
+ const rehandshakeOwnSession = async (ws) => {
64217
+ const hello = extensionHello;
64218
+ if (extensionWs !== ws || !hello)
64219
+ return;
64220
+ console.warn(`[fetchproxy] ${opts.ownServerName}: ${OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE} frames in a row from the extension failed authentication under this MCP's session key \u2014 re-handshaking this MCP's session (the extension stays connected).`);
64221
+ ownSession = dropOwnSessionAndEphemeral();
64222
+ resetSessionPromise();
64223
+ const mintedKeypair = await generateSessionKeypair();
64224
+ const mintedHello = await buildServerHello({
64225
+ ...ownHelloBase,
64226
+ sessionPub: mintedKeypair.publicKey,
64227
+ answersExtNonce: fromB64(hello.sessionNonce)
64228
+ });
64229
+ if (extensionWs !== ws || extensionHello !== hello || ownSession !== null) {
64230
+ mintedKeypair.privateKey.fill(0);
64231
+ return;
64232
+ }
64233
+ installOwnEphemeral({
64234
+ nonce: fromB64(mintedHello.sessionNonce),
64235
+ pub: mintedKeypair.publicKey,
64236
+ priv: mintedKeypair.privateKey
64237
+ });
64238
+ ws.send(JSON.stringify(mintedHello));
64239
+ };
63857
64240
  wss.on("connection", (ws) => {
63858
64241
  let identified = null;
63859
64242
  let peerMcpId = null;
@@ -64044,6 +64427,7 @@ async function startHost(opts) {
64044
64427
  if (extensionWs !== ws)
64045
64428
  return;
64046
64429
  ownSession = new SessionState(key);
64430
+ ownDecryptFailures = 0;
64047
64431
  ownPendingPairCode = null;
64048
64432
  resolveOwnSession(ownSession);
64049
64433
  } else {
@@ -64065,15 +64449,42 @@ async function startHost(opts) {
64065
64449
  }
64066
64450
  if (claim2 !== "ok")
64067
64451
  return;
64068
- let inner;
64452
+ let result;
64069
64453
  try {
64070
- inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
64454
+ result = await openEncryptedFrameDetailed(session.sessionKey, frame, "e2s");
64071
64455
  } catch (e) {
64072
64456
  session.releaseInboundSeq(frame.seq);
64073
64457
  throw e;
64074
64458
  }
64459
+ if (result.stage === "decrypt-failed") {
64460
+ session.releaseInboundSeq(frame.seq);
64461
+ console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) that failed authentication \u2014 most likely a straggler from a previous session.`);
64462
+ if (session === ownSession) {
64463
+ ownDecryptFailures += 1;
64464
+ if (ownDecryptFailures >= OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE) {
64465
+ ownDecryptFailures = 0;
64466
+ void rehandshakeOwnSession(ws).catch((e) => console.error(`[fetchproxy] ${opts.ownServerName}: re-handshake failed:`, e));
64467
+ }
64468
+ }
64469
+ return;
64470
+ }
64471
+ if (session === ownSession)
64472
+ ownDecryptFailures = 0;
64075
64473
  session.commitInboundSeq(frame.seq);
64076
- ownInnerListeners.forEach((cb) => cb(inner));
64474
+ if (result.stage === "ok") {
64475
+ ownInnerListeners.forEach((cb) => cb(result.inner));
64476
+ } else {
64477
+ console.error("[fetchproxy] host: received a frame that decrypted OK but failed validation:", result.error);
64478
+ const recoveredId = result.recoveredId;
64479
+ if (recoveredId !== void 0) {
64480
+ ownInnerListeners.forEach((cb) => cb({
64481
+ type: "response",
64482
+ id: recoveredId,
64483
+ ok: false,
64484
+ error: `malformed response failed protocol validation: ${String(result.error)}`
64485
+ }));
64486
+ }
64487
+ }
64077
64488
  } else {
64078
64489
  const slot = peers.get(frame.mcpId);
64079
64490
  if (slot)
@@ -64132,6 +64543,7 @@ async function startHost(opts) {
64132
64543
  rejectOwnSession(new Error("extension disconnected before ready"));
64133
64544
  }
64134
64545
  ownSession = dropOwnSessionAndEphemeral();
64546
+ ownDecryptFailures = 0;
64135
64547
  ownPendingPairCode = null;
64136
64548
  resetSessionPromise();
64137
64549
  disconnectListeners.forEach((cb) => cb());
@@ -64146,8 +64558,15 @@ async function startHost(opts) {
64146
64558
  }
64147
64559
  }
64148
64560
  if (identified === "peer" && peerMcpId) {
64149
- if (peers.get(peerMcpId)?.ws === ws)
64561
+ if (peers.get(peerMcpId)?.ws === ws) {
64150
64562
  peers.delete(peerMcpId);
64563
+ if (extensionWs && extensionHello?.accepts?.includes("peer-gone")) {
64564
+ try {
64565
+ extensionWs.send(JSON.stringify({ type: "peer-gone", mcpId: peerMcpId }));
64566
+ } catch {
64567
+ }
64568
+ }
64569
+ }
64151
64570
  }
64152
64571
  });
64153
64572
  });
@@ -64190,12 +64609,18 @@ async function startHost(opts) {
64190
64609
  }
64191
64610
 
64192
64611
  // node_modules/@fetchproxy/server/dist/peer.js
64612
+ var PEER_DIAL_TIMEOUT_MS = 5e3;
64193
64613
  var enc5 = new TextEncoder();
64614
+ function notSentError() {
64615
+ return new Error("fetchproxy: request not sent \u2014 the connection to the bridge host closed before it went out, so nothing reached the browser. It is safe to retry.");
64616
+ }
64194
64617
  async function startPeer(opts) {
64195
64618
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
64196
64619
  maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
64197
64620
  });
64621
+ const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
64198
64622
  await new Promise((resolve2, reject) => {
64623
+ let timer;
64199
64624
  const onOpen = () => {
64200
64625
  cleanup();
64201
64626
  resolve2();
@@ -64205,11 +64630,21 @@ async function startPeer(opts) {
64205
64630
  reject(e);
64206
64631
  };
64207
64632
  const cleanup = () => {
64633
+ if (timer !== void 0)
64634
+ clearTimeout(timer);
64208
64635
  ws.off("open", onOpen);
64209
64636
  ws.off("error", onError);
64210
64637
  };
64211
64638
  ws.once("open", onOpen);
64212
64639
  ws.once("error", onError);
64640
+ if (dialTimeoutMs > 0) {
64641
+ timer = setTimeout(() => {
64642
+ cleanup();
64643
+ ws.on("error", () => void 0);
64644
+ ws.terminate();
64645
+ reject(new Error(`fetchproxy: the process holding ${opts.host}:${opts.port} did not answer the WebSocket upgrade within ${dialTimeoutMs}ms \u2014 it may be wedged or not a fetchproxy host. Restart the MCP that owns the port (or free it) and retry.`));
64646
+ }, dialTimeoutMs);
64647
+ }
64213
64648
  });
64214
64649
  ws.on("error", (e) => {
64215
64650
  console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
@@ -64228,6 +64663,7 @@ async function startPeer(opts) {
64228
64663
  captureHeaders: opts.captureHeaders,
64229
64664
  indexedDbScopes: opts.indexedDbScopes,
64230
64665
  domSelectors: opts.domSelectors,
64666
+ domListSelectors: opts.domListSelectors,
64231
64667
  localStoragePointers: opts.localStoragePointers,
64232
64668
  sessionStoragePointers: opts.sessionStoragePointers,
64233
64669
  graphqlOps: opts.graphqlOps,
@@ -64251,6 +64687,7 @@ async function startPeer(opts) {
64251
64687
  const renegotiateListeners = [];
64252
64688
  const pendingPairListeners = [];
64253
64689
  const closeListeners = [];
64690
+ const extensionDisconnectListeners = [];
64254
64691
  let session = null;
64255
64692
  let sessionEphemeral = null;
64256
64693
  function dropSessionEphemeral() {
@@ -64270,10 +64707,24 @@ async function startPeer(opts) {
64270
64707
  let warnedUnverifiablePairCode = false;
64271
64708
  let resolveFirstReady;
64272
64709
  let rejectFirstReady;
64273
- const sessionPromise = new Promise((resolve2, reject) => {
64274
- resolveFirstReady = resolve2;
64275
- rejectFirstReady = reject;
64276
- });
64710
+ let sessionPromise;
64711
+ let sessionPromiseSettled = false;
64712
+ function resetSessionPromise() {
64713
+ sessionPromiseSettled = false;
64714
+ sessionPromise = new Promise((resolve2, reject) => {
64715
+ resolveFirstReady = (st) => {
64716
+ sessionPromiseSettled = true;
64717
+ resolve2(st);
64718
+ };
64719
+ rejectFirstReady = (e) => {
64720
+ sessionPromiseSettled = true;
64721
+ reject(e);
64722
+ };
64723
+ });
64724
+ sessionPromise.catch(() => {
64725
+ });
64726
+ }
64727
+ resetSessionPromise();
64277
64728
  let extensionHello = null;
64278
64729
  let extensionGone = false;
64279
64730
  let cachedPin = void 0;
@@ -64370,6 +64821,12 @@ async function startPeer(opts) {
64370
64821
  extensionGone = true;
64371
64822
  dropSessionEphemeral();
64372
64823
  pendingPairCode = null;
64824
+ if (session !== null) {
64825
+ session = null;
64826
+ if (sessionPromiseSettled)
64827
+ resetSessionPromise();
64828
+ }
64829
+ extensionDisconnectListeners.forEach((cb) => cb());
64373
64830
  return;
64374
64831
  }
64375
64832
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
@@ -64486,19 +64943,30 @@ async function startPeer(opts) {
64486
64943
  }
64487
64944
  closeListeners.forEach((cb) => cb());
64488
64945
  });
64489
- sessionPromise.catch(() => {
64490
- });
64491
64946
  const handle = {
64492
64947
  ws,
64493
- session: sessionPromise,
64948
+ // A getter: `extension-disconnected` replaces the promise (B-BUG-5).
64949
+ get session() {
64950
+ return sessionPromise;
64951
+ },
64494
64952
  sendInner: async (inner) => {
64495
- await awaitSessionReady(sessionPromise, {
64496
- mcpId: opts.mcpId,
64497
- pendingPairCode: () => pendingPairCode
64498
- });
64953
+ try {
64954
+ await awaitSessionReady(sessionPromise, {
64955
+ mcpId: opts.mcpId,
64956
+ pendingPairCode: () => pendingPairCode
64957
+ });
64958
+ } catch (e) {
64959
+ if (ws.readyState !== import_websocket.default.OPEN)
64960
+ throw notSentError();
64961
+ throw e;
64962
+ }
64499
64963
  const s = session;
64964
+ if (s === null)
64965
+ throw new Error("peer: extension disconnected");
64500
64966
  const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
64501
64967
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
64968
+ if (ws.readyState !== import_websocket.default.OPEN)
64969
+ throw notSentError();
64502
64970
  ws.send(JSON.stringify(sealed));
64503
64971
  },
64504
64972
  onInner: (cb) => {
@@ -64516,6 +64984,9 @@ async function startPeer(opts) {
64516
64984
  onClose: (cb) => {
64517
64985
  closeListeners.push(cb);
64518
64986
  },
64987
+ onExtensionDisconnect: (cb) => {
64988
+ extensionDisconnectListeners.push(cb);
64989
+ },
64519
64990
  close: () => ws.close()
64520
64991
  };
64521
64992
  return handle;
@@ -64523,15 +64994,15 @@ async function startPeer(opts) {
64523
64994
 
64524
64995
  // node_modules/@fetchproxy/server/dist/error-kind.js
64525
64996
  function classifyFetchError(error62) {
64997
+ if (/^(in-page )?fetch threw:/.test(error62)) {
64998
+ return "tab_fetch_failed";
64999
+ }
64526
65000
  if (/Could not establish connection/i.test(error62) || /Receiving end does not exist/i.test(error62)) {
64527
65001
  return "content_script_unreachable";
64528
65002
  }
64529
65003
  if (/^tab fetch failed:/.test(error62)) {
64530
65004
  return "tab_fetch_failed";
64531
65005
  }
64532
- if (/^(in-page )?fetch threw:/.test(error62)) {
64533
- return "tab_fetch_failed";
64534
- }
64535
65006
  if (/^no tab matching /.test(error62)) {
64536
65007
  return "no_tab";
64537
65008
  }
@@ -64723,6 +65194,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
64723
65194
  * the message says which number that was and where it came from.
64724
65195
  */
64725
65196
  requestedTimeoutMs;
65197
+ /**
65198
+ * B-BUG-1: whether re-sending this request is safe. A timeout only means
65199
+ * the reply did not arrive in time — the extension may already have run the
65200
+ * request in the tab — so this is `false` for a non-idempotent `fetch`
65201
+ * (anything but GET/HEAD/OPTIONS) the caller did not mark `retryOnTimeout`,
65202
+ * for `download` and `writeCookies`, and for `graphqlQuery` unless the
65203
+ * caller marked it `retryOnTimeout` (a declared operation may be a
65204
+ * mutation). `retryOnceOnTimeout` honours it. Defaults to `true` (the read
65205
+ * verbs).
65206
+ */
65207
+ retrySafe;
64726
65208
  constructor(args) {
64727
65209
  const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
64728
65210
  const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
@@ -64735,6 +65217,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
64735
65217
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
64736
65218
  this.retryAttempted = args.retryAttempted ?? false;
64737
65219
  this.requestedTimeoutMs = args.requestedTimeoutMs ?? null;
65220
+ this.retrySafe = args.retrySafe ?? true;
64738
65221
  }
64739
65222
  };
64740
65223
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -64774,6 +65257,32 @@ function assertUrlInDomains(field, url4, domains) {
64774
65257
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
64775
65258
  }
64776
65259
  var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
65260
+ function parseJsonBody(response) {
65261
+ if (response.status === 204 || response.body.trim() === "")
65262
+ return null;
65263
+ return JSON.parse(response.body);
65264
+ }
65265
+ var TIMEOUT_RETRY_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
65266
+ function isRetrySafeOnTimeout(method, retryOnTimeout) {
65267
+ if (retryOnTimeout !== void 0)
65268
+ return retryOnTimeout;
65269
+ return TIMEOUT_RETRY_SAFE_METHODS.has(method.toUpperCase());
65270
+ }
65271
+ function isResendableRequest(inner) {
65272
+ switch (inner.op) {
65273
+ case "fetch":
65274
+ return isRetrySafeOnTimeout(inner.init.method);
65275
+ case "write_cookies":
65276
+ case "download":
65277
+ // A declared GraphQL operation may be a mutation; the server holds only
65278
+ // its name, so it cannot tell. `graphqlQuery({ retryOnTimeout: true })`
65279
+ // passes `resendable` explicitly for a known query.
65280
+ case "graphql_query":
65281
+ return false;
65282
+ default:
65283
+ return true;
65284
+ }
65285
+ }
64777
65286
  var FetchproxyServer = class {
64778
65287
  /**
64779
65288
  * Bridge role. `null` until the first verb call (or an explicit
@@ -64822,12 +65331,22 @@ var FetchproxyServer = class {
64822
65331
  pendingRedirect = /* @__PURE__ */ new Map();
64823
65332
  // 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
64824
65333
  pendingIdb = /* @__PURE__ */ new Map();
65334
+ // 3.1.0+: read_dom_list awaiters resolve an array of field-value rows —
65335
+ // a different shape from `pendingStorage`'s single map, so its own map.
65336
+ pendingDomList = /* @__PURE__ */ new Map();
64825
65337
  // download awaiters resolve the saved-file metadata (path + size + mime).
64826
65338
  pendingDownload = /* @__PURE__ */ new Map();
64827
65339
  // 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
64828
65340
  // shape is operation-specific, so the awaiter resolves `unknown` and the
64829
65341
  // caller narrows.
64830
65342
  pendingGraphql = /* @__PURE__ */ new Map();
65343
+ /**
65344
+ * B-BUG-6: the frames of in-flight requests that are safe to send again
65345
+ * if the host that relayed them exits before replying (see
65346
+ * `recoverFromHostLoss`). Keyed by request id; an entry outlives its
65347
+ * request only until the next prune or response.
65348
+ */
65349
+ resendable = /* @__PURE__ */ new Map();
64831
65350
  mcpId = null;
64832
65351
  identity = null;
64833
65352
  // 0.5.3+: in-flight role-election / handle-start promise. Set the
@@ -64873,7 +65392,7 @@ var FetchproxyServer = class {
64873
65392
  }
64874
65393
  for (const c of opts.capabilities) {
64875
65394
  if (!KNOWN_CAPABILITIES.has(c)) {
64876
- throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ["fetch", "read_cookies"]`);
65395
+ throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ${JSON.stringify([...KNOWN_CAPABILITIES])}`);
64877
65396
  }
64878
65397
  }
64879
65398
  capabilities = [...opts.capabilities];
@@ -64920,6 +65439,16 @@ var FetchproxyServer = class {
64920
65439
  selector: d.selector,
64921
65440
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
64922
65441
  })),
65442
+ domListSelectors: (opts.domListSelectors ?? []).map((d) => ({
65443
+ name: d.name,
65444
+ itemSelector: d.itemSelector,
65445
+ fields: d.fields.map((f) => ({
65446
+ name: f.name,
65447
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
65448
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
65449
+ })),
65450
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
65451
+ })),
64923
65452
  graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
64924
65453
  name: d.name,
64925
65454
  operationName: d.operationName
@@ -65049,6 +65578,7 @@ var FetchproxyServer = class {
65049
65578
  ownLocalStoragePointers: this.opts.localStoragePointers,
65050
65579
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
65051
65580
  ownDomSelectors: this.opts.domSelectors,
65581
+ ownDomListSelectors: this.opts.domListSelectors,
65052
65582
  ownGraphqlOps: this.opts.graphqlOps,
65053
65583
  onPairCode: this.opts.onPairCode,
65054
65584
  extensionTrust: this.extensionTrust()
@@ -65080,6 +65610,7 @@ var FetchproxyServer = class {
65080
65610
  localStoragePointers: this.opts.localStoragePointers,
65081
65611
  sessionStoragePointers: this.opts.sessionStoragePointers,
65082
65612
  domSelectors: this.opts.domSelectors,
65613
+ domListSelectors: this.opts.domListSelectors,
65083
65614
  graphqlOps: this.opts.graphqlOps,
65084
65615
  extensionTrust: this.extensionTrust(),
65085
65616
  requireExtensionIdentity: this.opts.requireExtensionIdentity
@@ -65089,6 +65620,10 @@ var FetchproxyServer = class {
65089
65620
  this.stopKeepalive();
65090
65621
  this.rejectAllPending();
65091
65622
  });
65623
+ this.peerHandle.onExtensionDisconnect(() => {
65624
+ this.stopKeepalive();
65625
+ this.rejectAllPending();
65626
+ });
65092
65627
  this.peerHandle.onPendingPair((code) => {
65093
65628
  this.rejectAllPending(this.pairingErrorMessage(code));
65094
65629
  });
@@ -65100,9 +65635,9 @@ var FetchproxyServer = class {
65100
65635
  if (this.closing || this.peerHandle === null)
65101
65636
  return;
65102
65637
  this.stopKeepalive();
65103
- this.rejectAllPending();
65104
65638
  this.peerHandle = null;
65105
65639
  this.role = null;
65640
+ void this.recoverFromHostLoss();
65106
65641
  });
65107
65642
  }
65108
65643
  }
@@ -65123,7 +65658,7 @@ var FetchproxyServer = class {
65123
65658
  * only when the bridge itself failed (no signed-in tab, extension
65124
65659
  * offline, etc.).
65125
65660
  */
65126
- async fetch(init) {
65661
+ async fetch(init, fetchOpts = {}) {
65127
65662
  await this.ensureConnected();
65128
65663
  const pendingCode = this.currentPendingPairCode();
65129
65664
  if (pendingCode !== null) {
@@ -65135,16 +65670,17 @@ var FetchproxyServer = class {
65135
65670
  retryAttempted: false
65136
65671
  };
65137
65672
  }
65138
- const first = await this._fetchOnceWithTimeout(init);
65673
+ const first = await this._fetchOnceWithTimeout(init, fetchOpts);
65139
65674
  const reviveMs = this.opts.bridgeReviveDelayMs;
65140
65675
  const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
65141
65676
  if (isColdStartSymptom) {
65142
65677
  this.lastEvictionDetectedAt = Date.now();
65143
65678
  }
65144
- if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
65679
+ const retryable = isColdStartSymptom && (first.kind === "content_script_unreachable" || isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout));
65680
+ if (retryable && reviveMs !== void 0 && reviveMs > 0) {
65145
65681
  this.lazyReviveAttempts += 1;
65146
65682
  await new Promise((r) => setTimeout(r, reviveMs));
65147
- const second = await this._fetchOnceWithTimeout(init);
65683
+ const second = await this._fetchOnceWithTimeout(init, fetchOpts);
65148
65684
  if (second.ok) {
65149
65685
  this.lazyReviveSuccesses += 1;
65150
65686
  this.recordSuccess();
@@ -65293,13 +65829,16 @@ var FetchproxyServer = class {
65293
65829
  * one of the op maps — request ids are unique) so it doesn't leak until the
65294
65830
  * server closes, then rethrow.
65295
65831
  */
65296
- async sendInnerFrame(inner) {
65832
+ async sendInnerFrame(inner, opts = {}) {
65297
65833
  try {
65298
65834
  if (this.hostHandle) {
65299
65835
  await this.hostHandle.sendOwnInner(inner);
65300
65836
  } else if (this.peerHandle) {
65301
65837
  await this.peerHandle.sendInner(inner);
65302
65838
  }
65839
+ if (inner.type === "request" && (opts.resendable ?? isResendableRequest(inner))) {
65840
+ this.noteResendable(inner);
65841
+ }
65303
65842
  } catch (err) {
65304
65843
  if ("id" in inner && typeof inner.id === "number") {
65305
65844
  const { id } = inner;
@@ -65311,6 +65850,7 @@ var FetchproxyServer = class {
65311
65850
  this.pendingRedirect.delete(id);
65312
65851
  this.pendingDownload.delete(id);
65313
65852
  this.pendingIdb.delete(id);
65853
+ this.pendingDomList.delete(id);
65314
65854
  this.pendingGraphql.delete(id);
65315
65855
  }
65316
65856
  throw err;
@@ -65321,13 +65861,25 @@ var FetchproxyServer = class {
65321
65861
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
65322
65862
  * the throwing surface is the convenience methods.
65323
65863
  */
65324
- async _fetchOnceWithTimeout(init) {
65864
+ async _fetchOnceWithTimeout(init, fetchOpts = {}) {
65325
65865
  const id = this.nextRequestId++;
65326
65866
  const inner = { type: "request", id, op: "fetch", init };
65327
65867
  const pending = this.guardPending(new Promise((resolve2) => {
65328
65868
  this.pending.set(id, resolve2);
65329
65869
  }));
65330
- await this.sendInnerFrame(inner);
65870
+ try {
65871
+ await this.sendInnerFrame(inner, {
65872
+ resendable: isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout)
65873
+ });
65874
+ } catch (err) {
65875
+ return {
65876
+ ok: false,
65877
+ error: err instanceof Error ? err.message : String(err),
65878
+ kind: "session_not_ready",
65879
+ retryAttempted: false,
65880
+ cause: err
65881
+ };
65882
+ }
65331
65883
  const timeoutMs = this.opts.fetchTimeoutMs;
65332
65884
  if (timeoutMs === void 0 || timeoutMs <= 0)
65333
65885
  return pending;
@@ -65401,7 +65953,7 @@ var FetchproxyServer = class {
65401
65953
  });
65402
65954
  return pending;
65403
65955
  }
65404
- async _withVerbTimeout(pending, pendingMap, id, url4, requestedTimeoutMs) {
65956
+ async _withVerbTimeout(pending, pendingMap, id, url4, requestedTimeoutMs, retrySafe = true) {
65405
65957
  const transportMs = this.opts.fetchTimeoutMs;
65406
65958
  if (transportMs === void 0 || transportMs <= 0)
65407
65959
  return pending;
@@ -65422,6 +65974,7 @@ var FetchproxyServer = class {
65422
65974
  port: this.opts.port,
65423
65975
  elapsedMs: Date.now() - start,
65424
65976
  retryAttempted: false,
65977
+ retrySafe,
65425
65978
  graceMs,
65426
65979
  ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
65427
65980
  }));
@@ -65439,7 +65992,7 @@ var FetchproxyServer = class {
65439
65992
  * logic re-implemented inline) `captureRequestHeader()` agree on what
65440
65993
  * to throw.
65441
65994
  */
65442
- _typedErrorFor(result, url4, op, retryAttempted) {
65995
+ _typedErrorFor(result, url4, op, retryAttempted, retrySafe = true) {
65443
65996
  if (result.kind === "timeout") {
65444
65997
  return new FetchproxyTimeoutError({
65445
65998
  url: url4,
@@ -65447,7 +66000,8 @@ var FetchproxyServer = class {
65447
66000
  role: this.role,
65448
66001
  port: this.opts.port,
65449
66002
  elapsedMs: result.elapsedMs,
65450
- retryAttempted
66003
+ retryAttempted,
66004
+ retrySafe
65451
66005
  });
65452
66006
  }
65453
66007
  if (result.kind === "content_script_unreachable") {
@@ -65521,9 +66075,12 @@ var FetchproxyServer = class {
65521
66075
  // every version before this option existed.
65522
66076
  ...opts.credentials === "omit" ? { credentials: "omit" } : {}
65523
66077
  };
65524
- const result = await this.fetch(init);
66078
+ const result = await this.fetch(init, opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {});
65525
66079
  if (!result.ok) {
65526
- throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
66080
+ if (result.kind === "session_not_ready" && result.cause instanceof Error) {
66081
+ throw result.cause;
66082
+ }
66083
+ throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, opts.retryOnTimeout));
65527
66084
  }
65528
66085
  const response = {
65529
66086
  status: result.status,
@@ -65563,17 +66120,21 @@ var FetchproxyServer = class {
65563
66120
  * GET a path and parse the response body as JSON. Throws
65564
66121
  * `FetchproxyHttpError` if the status is outside the default 2xx
65565
66122
  * happy-path set (`[200, 201, 202, 204]`); pass a custom
65566
- * `expectStatus` to override.
66123
+ * `expectStatus` to override. A 204 or an empty/whitespace body
66124
+ * resolves to `null` (the declared `T` does not say so, to keep
66125
+ * existing call sites compiling — include `| null` in `T` if the
66126
+ * endpoint can answer empty).
65567
66127
  */
65568
66128
  async getJson(path, opts = {}) {
65569
66129
  const response = await this.get(path, this.applyJsonDefaults(opts));
65570
- return JSON.parse(response.body);
66130
+ return parseJsonBody(response);
65571
66131
  }
65572
66132
  /**
65573
66133
  * POST a JSON body and parse the response body as JSON. The body is
65574
66134
  * `JSON.stringify`'d; `Content-Type: application/json` is set unless
65575
66135
  * the caller already provided one. Defaults `expectStatus` to the 2xx
65576
- * happy-path set.
66136
+ * happy-path set. A 204 or an empty/whitespace body resolves to `null`
66137
+ * rather than throwing after the write already happened (see `getJson`).
65577
66138
  */
65578
66139
  async postJson(path, body, opts = {}) {
65579
66140
  const headers = { ...opts.headers ?? {} };
@@ -65585,7 +66146,7 @@ var FetchproxyServer = class {
65585
66146
  headers,
65586
66147
  body: body === void 0 ? void 0 : JSON.stringify(body)
65587
66148
  });
65588
- return JSON.parse(response.body);
66149
+ return parseJsonBody(response);
65589
66150
  }
65590
66151
  /**
65591
66152
  * GET a path and return the response body as a string. Throws
@@ -65606,7 +66167,8 @@ var FetchproxyServer = class {
65606
66167
  * - adds `Content-Type: application/json` only for a non-GET request
65607
66168
  * that carries a `body` (and only if the caller didn't set one);
65608
66169
  * - `JSON.stringify`s the body (GET / no-body sends nothing);
65609
- * - treats a `204` or an empty body as `data: null` (no parse);
66170
+ * - treats a `204`, an empty body, or a whitespace-only body as
66171
+ * `data: null` (no parse), the same rule as getJson/postJson;
65610
66172
  * - otherwise `JSON.parse`s the body.
65611
66173
  *
65612
66174
  * Scope is serialization + header defaults + 204-handling +
@@ -65620,6 +66182,11 @@ var FetchproxyServer = class {
65620
66182
  * Bridge-level failures (no signed-in tab, SW down, timeout) still
65621
66183
  * throw the typed errors via `request()`, exactly like the verb
65622
66184
  * helpers — only successful round-trips (any HTTP status) return.
66185
+ *
66186
+ * `retryOnTimeout` is `RequestOpts.retryOnTimeout`: a timed-out POST is
66187
+ * NOT re-sent unless the caller says it is safe to repeat. A read-only POST
66188
+ * (a search, a GraphQL query) should pass `true` to keep the cold-start
66189
+ * timeout retry (#90).
65623
66190
  */
65624
66191
  async requestJson(method, path, opts = {}) {
65625
66192
  const isGet = method.toUpperCase() === "GET";
@@ -65633,7 +66200,8 @@ var FetchproxyServer = class {
65633
66200
  headers,
65634
66201
  body: sendBody ? JSON.stringify(opts.body) : void 0,
65635
66202
  ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
65636
- ...opts.domain !== void 0 ? { domain: opts.domain } : {}
66203
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {},
66204
+ ...opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {}
65637
66205
  });
65638
66206
  const result = {
65639
66207
  ok: true,
@@ -65641,12 +66209,9 @@ var FetchproxyServer = class {
65641
66209
  url: response.url,
65642
66210
  body: response.body
65643
66211
  };
65644
- if (response.status === 204 || response.body === "") {
65645
- return { data: null, result };
65646
- }
65647
66212
  let data;
65648
66213
  try {
65649
- data = JSON.parse(response.body);
66214
+ data = parseJsonBody(response);
65650
66215
  } catch (e) {
65651
66216
  throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
65652
66217
  }
@@ -65703,18 +66268,19 @@ var FetchproxyServer = class {
65703
66268
  };
65704
66269
  }
65705
66270
  /**
65706
- * Snapshot the user's non-HttpOnly cookies for the chosen domain.
66271
+ * Read the user's cookies for the chosen domain.
65707
66272
  *
65708
66273
  * Requires `'read_cookies'` in `FetchproxyServerOpts.capabilities`.
65709
66274
  * Throws a developer-facing `Error` at the call site if the MCP did
65710
66275
  * not declare the capability — this is a programming mistake, not a
65711
66276
  * runtime condition.
65712
66277
  *
65713
- * The returned string is the raw `document.cookie` value (semicolon-
65714
- * separated `k=v` pairs). HttpOnly cookies are NOT visible to page JS
65715
- * and are therefore not included; the underlying threat model assumes
65716
- * the cookies that matter for the auth bootstrap (session tokens, csrf
65717
- * cookies that the page itself reads) are non-HttpOnly.
66278
+ * The returned string is semicolon-separated `k=v` pairs. With `keys`
66279
+ * (each in the declared `cookieKeys`), the extension reads each name via
66280
+ * `chrome.cookies.get`, which INCLUDES HttpOnly cookies — the login
66281
+ * session cookie among them; that is what cookie-session MCPs use it
66282
+ * for. Without `keys` (legacy), it is the tab's `document.cookie`, which
66283
+ * omits HttpOnly cookies. See docs/SECURITY.md §T-cookie-exfil.
65718
66284
  *
65719
66285
  * Throws `FetchproxyProtocolError` if the bridge could not deliver
65720
66286
  * the request (no signed-in tab, extension offline, etc.).
@@ -65810,7 +66376,15 @@ var FetchproxyServer = class {
65810
66376
  this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
65811
66377
  }));
65812
66378
  await this.sendInnerFrame(inner);
65813
- return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
66379
+ return this._withVerbTimeout(
66380
+ pending,
66381
+ this.pendingWriteCookies,
66382
+ id,
66383
+ `https://${host}`,
66384
+ void 0,
66385
+ // A write: never re-sent after an ambiguous timeout.
66386
+ false
66387
+ );
65814
66388
  }
65815
66389
  /**
65816
66390
  * 0.3.0+: read declared localStorage keys from the user's signed-in
@@ -65913,53 +66487,7 @@ var FetchproxyServer = class {
65913
66487
  throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
65914
66488
  }
65915
66489
  const callOpts = { ...resolved2, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
65916
- try {
65917
- const result = await this._captureRequestHeaderOnce(callOpts);
65918
- this.recordSuccess();
65919
- return result;
65920
- } catch (err) {
65921
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
65922
- if (!swDown) {
65923
- this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
65924
- throw err;
65925
- }
65926
- this.lastEvictionDetectedAt = Date.now();
65927
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
65928
- if (reviveMs > 0) {
65929
- this.lazyReviveAttempts += 1;
65930
- await new Promise((r) => setTimeout(r, reviveMs));
65931
- try {
65932
- const result = await this._captureRequestHeaderOnce(callOpts);
65933
- this.lazyReviveSuccesses += 1;
65934
- this.recordSuccess();
65935
- return result;
65936
- } catch (retryErr) {
65937
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
65938
- if (!stillDown) {
65939
- this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
65940
- throw retryErr;
65941
- }
65942
- this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
65943
- throw new FetchproxyBridgeDownError({
65944
- originalError: retryErr.message,
65945
- retryAttempted: true,
65946
- op: "capture_request_header",
65947
- url: `https://${resolved2.host}${resolved2.path ?? "/*"}`,
65948
- role: this.role,
65949
- port: this.opts.port
65950
- });
65951
- }
65952
- }
65953
- this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
65954
- throw new FetchproxyBridgeDownError({
65955
- originalError: err.message,
65956
- retryAttempted: false,
65957
- op: "capture_request_header",
65958
- url: `https://${resolved2.host}${resolved2.path ?? "/*"}`,
65959
- role: this.role,
65960
- port: this.opts.port
65961
- });
65962
- }
66490
+ return this.withLazyRevive("capture_request_header", `https://${resolved2.host}${resolved2.path ?? "/*"}`, () => this._captureRequestHeaderOnce(callOpts));
65963
66491
  }
65964
66492
  async _captureRequestHeaderOnce(opts) {
65965
66493
  const id = this.nextRequestId++;
@@ -66008,53 +66536,7 @@ var FetchproxyServer = class {
66008
66536
  }
66009
66537
  await this.ensureConnected();
66010
66538
  this.throwIfPendingPair();
66011
- try {
66012
- const result = await this._captureRedirectOnce(opts);
66013
- this.recordSuccess();
66014
- return result;
66015
- } catch (err) {
66016
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
66017
- if (!swDown) {
66018
- this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
66019
- throw err;
66020
- }
66021
- this.lastEvictionDetectedAt = Date.now();
66022
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
66023
- if (reviveMs > 0) {
66024
- this.lazyReviveAttempts += 1;
66025
- await new Promise((r) => setTimeout(r, reviveMs));
66026
- try {
66027
- const result = await this._captureRedirectOnce(opts);
66028
- this.lazyReviveSuccesses += 1;
66029
- this.recordSuccess();
66030
- return result;
66031
- } catch (retryErr) {
66032
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
66033
- if (!stillDown) {
66034
- this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
66035
- throw retryErr;
66036
- }
66037
- this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
66038
- throw new FetchproxyBridgeDownError({
66039
- originalError: retryErr.message,
66040
- retryAttempted: true,
66041
- op: "capture_redirect",
66042
- url: `https://${opts.host}${opts.path ?? "/*"}`,
66043
- role: this.role,
66044
- port: this.opts.port
66045
- });
66046
- }
66047
- }
66048
- this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
66049
- throw new FetchproxyBridgeDownError({
66050
- originalError: err.message,
66051
- retryAttempted: false,
66052
- op: "capture_redirect",
66053
- url: `https://${opts.host}${opts.path ?? "/*"}`,
66054
- role: this.role,
66055
- port: this.opts.port
66056
- });
66057
- }
66539
+ return this.withLazyRevive("capture_redirect", `https://${opts.host}${opts.path ?? "/*"}`, () => this._captureRedirectOnce(opts));
66058
66540
  }
66059
66541
  async _captureRedirectOnce(opts) {
66060
66542
  const id = this.nextRequestId++;
@@ -66099,52 +66581,57 @@ var FetchproxyServer = class {
66099
66581
  assertUrlInDomains("download url", opts.url, this.opts.domains);
66100
66582
  await this.ensureConnected();
66101
66583
  this.throwIfPendingPair();
66584
+ return this.withLazyRevive("download", opts.url, () => this._downloadOnce(opts));
66585
+ }
66586
+ /**
66587
+ * B-QUAL-1: the lazy-revive policy for the throwing verbs
66588
+ * (`capture_request_header`, `capture_redirect`, `download`), in one place.
66589
+ * Runs `once`; on the SW-eviction symptom (`content_script_unreachable` —
66590
+ * the request provably never reached a tab, so re-sending is safe) stamps
66591
+ * the eviction counter, waits `bridgeReviveDelayMs` and retries once, then
66592
+ * surfaces a `FetchproxyBridgeDownError`. Every other error is recorded and
66593
+ * rethrown untouched. `fetch()` keeps its own envelope-shaped variant.
66594
+ */
66595
+ async withLazyRevive(op, url4, once) {
66596
+ const isSwDown = (e) => e instanceof FetchproxyProtocolError && classifyFetchError(e.message) === "content_script_unreachable";
66597
+ const bridgeDown = (e, retryAttempted) => {
66598
+ this.recordFailure(`${op} bridge-down: ${e.message}`);
66599
+ return new FetchproxyBridgeDownError({
66600
+ originalError: e.message,
66601
+ retryAttempted,
66602
+ op,
66603
+ url: url4,
66604
+ role: this.role,
66605
+ port: this.opts.port
66606
+ });
66607
+ };
66102
66608
  try {
66103
- const result = await this._downloadOnce(opts);
66609
+ const result = await once();
66104
66610
  this.recordSuccess();
66105
66611
  return result;
66106
66612
  } catch (err) {
66107
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
66108
- if (!swDown) {
66109
- this.recordFailure(`download: ${err.message ?? String(err)}`);
66613
+ if (!isSwDown(err)) {
66614
+ this.recordFailure(`${op}: ${err.message ?? String(err)}`);
66110
66615
  throw err;
66111
66616
  }
66112
66617
  this.lastEvictionDetectedAt = Date.now();
66113
66618
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
66114
- if (reviveMs > 0) {
66115
- this.lazyReviveAttempts += 1;
66116
- await new Promise((r) => setTimeout(r, reviveMs));
66117
- try {
66118
- const result = await this._downloadOnce(opts);
66119
- this.lazyReviveSuccesses += 1;
66120
- this.recordSuccess();
66121
- return result;
66122
- } catch (retryErr) {
66123
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
66124
- if (!stillDown) {
66125
- this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
66126
- throw retryErr;
66127
- }
66128
- this.recordFailure(`download bridge-down: ${retryErr.message}`);
66129
- throw new FetchproxyBridgeDownError({
66130
- originalError: retryErr.message,
66131
- retryAttempted: true,
66132
- op: "download",
66133
- url: opts.url,
66134
- role: this.role,
66135
- port: this.opts.port
66136
- });
66619
+ if (reviveMs <= 0)
66620
+ throw bridgeDown(err, false);
66621
+ this.lazyReviveAttempts += 1;
66622
+ await new Promise((r) => setTimeout(r, reviveMs));
66623
+ try {
66624
+ const result = await once();
66625
+ this.lazyReviveSuccesses += 1;
66626
+ this.recordSuccess();
66627
+ return result;
66628
+ } catch (retryErr) {
66629
+ if (!isSwDown(retryErr)) {
66630
+ this.recordFailure(`${op}: ${retryErr.message ?? String(retryErr)}`);
66631
+ throw retryErr;
66137
66632
  }
66633
+ throw bridgeDown(retryErr, true);
66138
66634
  }
66139
- this.recordFailure(`download bridge-down: ${err.message}`);
66140
- throw new FetchproxyBridgeDownError({
66141
- originalError: err.message,
66142
- retryAttempted: false,
66143
- op: "download",
66144
- url: opts.url,
66145
- role: this.role,
66146
- port: this.opts.port
66147
- });
66148
66635
  }
66149
66636
  }
66150
66637
  async _downloadOnce(opts) {
@@ -66173,7 +66660,9 @@ var FetchproxyServer = class {
66173
66660
  // CAP and need the identical explanation; missing it then was the point
66174
66661
  // of #279 — "both verbs" was counted off the two call sites in view
66175
66662
  // rather than off the type. Three is still the number.
66176
- opts.timeoutMs
66663
+ opts.timeoutMs,
66664
+ // A second download saves a duplicate "file (1)".
66665
+ false
66177
66666
  );
66178
66667
  }
66179
66668
  /**
@@ -66264,6 +66753,52 @@ var FetchproxyServer = class {
66264
66753
  await this.sendInnerFrame(inner);
66265
66754
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
66266
66755
  }
66756
+ /**
66757
+ * 3.1.0+: read a declared REPEATED DOM structure from the user's
66758
+ * signed-in tab. Requires `'read_dom_list'` in capabilities AND `name` to
66759
+ * match a declared `domListSelectors` entry. The extension resolves the
66760
+ * entry's `itemSelector` via `document.querySelectorAll` in the matched
66761
+ * tab's DOM (isolated world), then resolves each declared field against
66762
+ * every matched item in turn — no page-JS execution, same as `readDom`.
66763
+ *
66764
+ * Returns one row per matched item, in document order, truncated to the
66765
+ * entry's declared `maxItems` when set. A field absent on a given item is
66766
+ * omitted from that item's row (the row itself is still returned). Throws
66767
+ * `FetchproxyProtocolError` on bridge failures and a plain `Error` on
66768
+ * developer mistakes (undeclared capability, undeclared name).
66769
+ *
66770
+ * Reads only what is CURRENTLY rendered — a virtualized or lazily-loaded
66771
+ * list returns only its visible rows. A caller that needs more scrolls
66772
+ * the list (out of band) and calls again.
66773
+ */
66774
+ async readDomList(opts) {
66775
+ if (!this.opts.capabilities.includes("read_dom_list")) {
66776
+ throw new Error('FetchproxyServer.readDomList(): MCP did not declare "read_dom_list" in capabilities');
66777
+ }
66778
+ await this.ensureConnected();
66779
+ this.throwIfPendingPair();
66780
+ if (typeof opts.name !== "string" || opts.name.length === 0) {
66781
+ throw new Error("FetchproxyServer.readDomList: opts.name must be a non-empty string");
66782
+ }
66783
+ this.assertScopeSubset([opts.name], this.opts.domListSelectors.map((d) => d.name), "domListSelectors");
66784
+ if (opts.subdomain !== void 0)
66785
+ assertSubdomainLabel(opts.subdomain);
66786
+ const baseDomain = this.resolveBaseDomain(opts.domain);
66787
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
66788
+ const origin = `https://${host}`;
66789
+ const id = this.nextRequestId++;
66790
+ const inner = {
66791
+ type: "request",
66792
+ id,
66793
+ op: "read_dom_list",
66794
+ init: { origin, name: opts.name }
66795
+ };
66796
+ const pending = this.guardPending(new Promise((resolve2, reject) => {
66797
+ this.pendingDomList.set(id, { resolve: resolve2, reject });
66798
+ }));
66799
+ await this.sendInnerFrame(inner);
66800
+ return this._withVerbTimeout(pending, this.pendingDomList, id, origin);
66801
+ }
66267
66802
  /**
66268
66803
  * 1.x+: run a declared GraphQL operation through the page's own Apollo
66269
66804
  * client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
@@ -66307,8 +66842,9 @@ var FetchproxyServer = class {
66307
66842
  const pending = this.guardPending(new Promise((resolve2, reject) => {
66308
66843
  this.pendingGraphql.set(id, { resolve: resolve2, reject });
66309
66844
  }));
66310
- await this.sendInnerFrame(inner);
66311
- return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
66845
+ const retrySafe = opts.retryOnTimeout === true;
66846
+ await this.sendInnerFrame(inner, { resendable: retrySafe });
66847
+ return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name, void 0, retrySafe);
66312
66848
  }
66313
66849
  assertScopeSubset(requested, declared, label) {
66314
66850
  const undeclared = undeclaredKeys(requested, declared);
@@ -66345,6 +66881,7 @@ var FetchproxyServer = class {
66345
66881
  onInner(inner) {
66346
66882
  if (inner.type !== "response")
66347
66883
  return;
66884
+ this.resendable.delete(inner.id);
66348
66885
  this.lastExtensionMessageAt = Date.now();
66349
66886
  const fetchCb = this.pending.get(inner.id);
66350
66887
  if (fetchCb) {
@@ -66433,6 +66970,20 @@ var FetchproxyServer = class {
66433
66970
  }
66434
66971
  return;
66435
66972
  }
66973
+ const domListCb = this.pendingDomList.get(inner.id);
66974
+ if (domListCb) {
66975
+ this.pendingDomList.delete(inner.id);
66976
+ if (inner.ok) {
66977
+ if (inner.op === "read_dom_list" && inner.rows) {
66978
+ domListCb.resolve(inner.rows.map((row) => ({ ...row })));
66979
+ } else {
66980
+ domListCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_dom_list awaiter`));
66981
+ }
66982
+ } else {
66983
+ domListCb.reject(protocolErrorFrom(inner.error));
66984
+ }
66985
+ return;
66986
+ }
66436
66987
  const downloadCb = this.pendingDownload.get(inner.id);
66437
66988
  if (downloadCb) {
66438
66989
  this.pendingDownload.delete(inner.id);
@@ -66492,42 +67043,133 @@ var FetchproxyServer = class {
66492
67043
  }
66493
67044
  }
66494
67045
  }
66495
- rejectAllPending(reason = "extension disconnected") {
67046
+ rejectAllPending(reason = "extension disconnected", keep = /* @__PURE__ */ new Set()) {
67047
+ for (const id of this.allPendingIds()) {
67048
+ if (!keep.has(id))
67049
+ this.rejectPendingId(id, reason);
67050
+ }
67051
+ if (keep.size === 0)
67052
+ this.resendable.clear();
67053
+ }
67054
+ /** Every request id with a live awaiter, across all op maps. */
67055
+ allPendingIds() {
67056
+ const ids = [];
67057
+ for (const map2 of this.pendingMaps())
67058
+ for (const id of map2.keys())
67059
+ ids.push(id);
67060
+ return ids;
67061
+ }
67062
+ pendingMaps() {
67063
+ return [
67064
+ this.pending,
67065
+ this.pendingReadCookies,
67066
+ this.pendingStorage,
67067
+ this.pendingWriteCookies,
67068
+ this.pendingCapture,
67069
+ this.pendingRedirect,
67070
+ this.pendingIdb,
67071
+ this.pendingDomList,
67072
+ this.pendingDownload,
67073
+ this.pendingGraphql
67074
+ ];
67075
+ }
67076
+ isPendingId(id) {
67077
+ return this.pendingMaps().some((m) => m.has(id));
67078
+ }
67079
+ /** Fail the one awaiter registered under `id`, in its own map's shape. */
67080
+ rejectPendingId(id, reason) {
66496
67081
  const err = new FetchproxyProtocolError(reason);
66497
- for (const cb of this.pending.values()) {
66498
- cb({
67082
+ this.resendable.delete(id);
67083
+ const fetchCb = this.pending.get(id);
67084
+ if (fetchCb) {
67085
+ this.pending.delete(id);
67086
+ fetchCb({
66499
67087
  ok: false,
66500
67088
  error: err.message,
66501
67089
  kind: classifyFetchError(err.message),
66502
67090
  retryAttempted: false
66503
67091
  });
67092
+ return;
67093
+ }
67094
+ const cookieCb = this.pendingReadCookies.get(id);
67095
+ if (cookieCb) {
67096
+ this.pendingReadCookies.delete(id);
67097
+ cookieCb({ ok: false, error: err.message });
67098
+ return;
66504
67099
  }
66505
- this.pending.clear();
66506
- for (const cb of this.pendingReadCookies.values()) {
66507
- cb({ ok: false, error: err.message });
66508
- }
66509
- this.pendingReadCookies.clear();
66510
- for (const { reject } of this.pendingStorage.values())
66511
- reject(err);
66512
- this.pendingStorage.clear();
66513
- for (const { reject } of this.pendingWriteCookies.values())
66514
- reject(err);
66515
- this.pendingWriteCookies.clear();
66516
- for (const { reject } of this.pendingCapture.values())
66517
- reject(err);
66518
- this.pendingCapture.clear();
66519
- for (const { reject } of this.pendingRedirect.values())
66520
- reject(err);
66521
- this.pendingRedirect.clear();
66522
- for (const { reject } of this.pendingIdb.values())
66523
- reject(err);
66524
- this.pendingIdb.clear();
66525
- for (const { reject } of this.pendingDownload.values())
66526
- reject(err);
66527
- this.pendingDownload.clear();
66528
- for (const { reject } of this.pendingGraphql.values())
66529
- reject(err);
66530
- this.pendingGraphql.clear();
67100
+ for (const map2 of [
67101
+ this.pendingStorage,
67102
+ this.pendingWriteCookies,
67103
+ this.pendingCapture,
67104
+ this.pendingRedirect,
67105
+ this.pendingIdb,
67106
+ this.pendingDomList,
67107
+ this.pendingDownload,
67108
+ this.pendingGraphql
67109
+ ]) {
67110
+ const entry = map2.get(id);
67111
+ if (entry) {
67112
+ map2.delete(id);
67113
+ entry.reject(err);
67114
+ return;
67115
+ }
67116
+ }
67117
+ }
67118
+ noteResendable(inner) {
67119
+ this.resendable.set(inner.id, inner);
67120
+ if (this.resendable.size > 256) {
67121
+ for (const id of [...this.resendable.keys()]) {
67122
+ if (!this.isPendingId(id))
67123
+ this.resendable.delete(id);
67124
+ }
67125
+ }
67126
+ }
67127
+ /**
67128
+ * B-BUG-6: the host this peer relayed through has gone — most often a
67129
+ * short-lived process (a bootstrap lift, an `fpx` call) that won the
67130
+ * election and then closed. Fail only the in-flight requests that may
67131
+ * already have run (a non-idempotent fetch, a cookie write, a download);
67132
+ * re-elect and send the rest again. The replies to the originals died with
67133
+ * the old host, and each re-sent request keeps its awaiter and its
67134
+ * original deadline.
67135
+ */
67136
+ async recoverFromHostLoss() {
67137
+ const toResend = /* @__PURE__ */ new Map();
67138
+ for (const [id, inner] of this.resendable) {
67139
+ if (this.isPendingId(id))
67140
+ toResend.set(id, inner);
67141
+ }
67142
+ this.resendable.clear();
67143
+ this.rejectAllPending("fetchproxy: the MCP process holding the bridge port exited while this request was in flight. It was not re-sent because it may already have run in the browser \u2014 check before retrying.", new Set(toResend.keys()));
67144
+ if (toResend.size === 0)
67145
+ return;
67146
+ const lost = (id, e) => this.rejectPendingId(id, `fetchproxy: the bridge host exited and reconnecting failed: ${e instanceof Error ? e.message : String(e)}`);
67147
+ try {
67148
+ await this.ensureConnected();
67149
+ } catch (e) {
67150
+ for (const id of toResend.keys())
67151
+ lost(id, e);
67152
+ return;
67153
+ }
67154
+ for (const [id, inner] of toResend) {
67155
+ if (!this.isPendingId(id))
67156
+ continue;
67157
+ try {
67158
+ await this.resendInnerFrame(inner);
67159
+ } catch (e) {
67160
+ lost(id, e);
67161
+ }
67162
+ }
67163
+ }
67164
+ /** Send an already-registered request again, keeping its awaiter on failure. */
67165
+ async resendInnerFrame(inner) {
67166
+ if (this.hostHandle)
67167
+ await this.hostHandle.sendOwnInner(inner);
67168
+ else if (this.peerHandle)
67169
+ await this.peerHandle.sendInner(inner);
67170
+ else
67171
+ throw new Error("no bridge handle after re-election");
67172
+ this.noteResendable(inner);
66531
67173
  }
66532
67174
  /**
66533
67175
  * 0.5.2+: read the current pair-pending pair code from whichever handle
@@ -66630,6 +67272,12 @@ async function runOneLift(opts) {
66630
67272
  if (envVal !== void 0 && envVal !== "" && envVal !== "0" && envVal !== "false") {
66631
67273
  throw new BootstrapDisabledError(opts.serverName, envVar);
66632
67274
  }
67275
+ for (const bucket of ["cookies", "localStorage", "sessionStorage"]) {
67276
+ const glob = opts.declare[bucket].find((k) => k.endsWith("*"));
67277
+ if (glob !== void 0) {
67278
+ throw new Error(`fetchproxy bootstrap: declare.${bucket} contains ${JSON.stringify(glob)}, a glob. A lift reads each key by its exact name, so a glob can never match \u2014 list the concrete key names instead.`);
67279
+ }
67280
+ }
66633
67281
  const factory = opts._serverFactory ?? defaultFactory;
66634
67282
  const indexedDb = opts.declare.indexedDb ?? [];
66635
67283
  const localStoragePointers = opts.declare.localStoragePointers ?? [];
@@ -66887,7 +67535,7 @@ function loadAccount(env = process.env) {
66887
67535
  // package.json
66888
67536
  var package_default = {
66889
67537
  name: "canvas-parent-mcp",
66890
- version: "2.1.1",
67538
+ version: "2.1.2",
66891
67539
  mcpName: "io.github.chrischall/canvas-parent-mcp",
66892
67540
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
66893
67541
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -66934,12 +67582,12 @@ var package_default = {
66934
67582
  typecheck: "tsc -p tsconfig.json --noEmit"
66935
67583
  },
66936
67584
  dependencies: {
66937
- "@chrischall/mcp-utils": "^1.0.0",
66938
- "@fetchproxy/bootstrap": "^3.0.1",
66939
- "@fetchproxy/server": "^3.0.1",
67585
+ "@chrischall/mcp-utils": "^2.4.0",
67586
+ "@fetchproxy/bootstrap": "^3.2.0",
67587
+ "@fetchproxy/server": "^3.2.0",
66940
67588
  "@modelcontextprotocol/server": "^2.0.0",
66941
67589
  dotenv: "^18.0.0",
66942
- zod: "^4.6.2"
67590
+ zod: "^4.6.5"
66943
67591
  },
66944
67592
  devDependencies: {
66945
67593
  "@modelcontextprotocol/client": "^2.0.0",
@@ -67199,7 +67847,7 @@ import { writeFile as writeFile3, stat } from "fs/promises";
67199
67847
  import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
67200
67848
  import { dirname, join as join3, resolve } from "node:path";
67201
67849
  import { homedir as homedir2 } from "node:os";
67202
- import { randomBytes as randomBytes2, createHmac } from "node:crypto";
67850
+ import { randomBytes as randomBytes3, createHmac } from "node:crypto";
67203
67851
  function bindingDigest(secret, salt) {
67204
67852
  return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
67205
67853
  }
@@ -67251,7 +67899,7 @@ function createFileStatePersistence(opts) {
67251
67899
  },
67252
67900
  save(state) {
67253
67901
  const dir = dirname(filePath);
67254
- const tmp = `${filePath}.tmp-${randomBytes2(6).toString("hex")}`;
67902
+ const tmp = `${filePath}.tmp-${randomBytes3(6).toString("hex")}`;
67255
67903
  const dirExisted = existsSync(dir);
67256
67904
  try {
67257
67905
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -67259,7 +67907,7 @@ function createFileStatePersistence(opts) {
67259
67907
  chmodSync(dir, 448);
67260
67908
  let boundTo;
67261
67909
  if (secret !== void 0) {
67262
- const salt = randomBytes2(16).toString("hex");
67910
+ const salt = randomBytes3(16).toString("hex");
67263
67911
  boundTo = { salt, digest: bindingDigest(secret, salt) };
67264
67912
  }
67265
67913
  const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
@@ -67729,7 +68377,7 @@ var CanvasClient = class {
67729
68377
  const res = await this.doRawRequest(path, opts);
67730
68378
  const text = await res.text();
67731
68379
  if (opts.responseType === "text") return text;
67732
- return parseJsonBody(text) ?? null;
68380
+ return parseJsonBody2(text) ?? null;
67733
68381
  }
67734
68382
  async requestPaginated(path, opts = {}) {
67735
68383
  const perPage = opts.perPage ?? DEFAULT_PER_PAGE;
@@ -67739,7 +68387,7 @@ var CanvasClient = class {
67739
68387
  for (let page = 0; page < maxPages; page++) {
67740
68388
  const res = await this.doRawRequest(url4, opts);
67741
68389
  const text = await res.text();
67742
- const parsed = parseJsonBody(text) ?? [];
68390
+ const parsed = parseJsonBody2(text) ?? [];
67743
68391
  for (const item of parsed) out.push(item);
67744
68392
  const linkHeader = res.headers.get("link");
67745
68393
  const next = linkHeader ? parseLinkHeader(linkHeader).next : void 0;
@@ -67880,7 +68528,7 @@ var CanvasClient = class {
67880
68528
  };
67881
68529
  }
67882
68530
  };
67883
- function parseJsonBody(text) {
68531
+ function parseJsonBody2(text) {
67884
68532
  if (!text) return null;
67885
68533
  const stripped = text.replace(/^while\(1\);/, "");
67886
68534
  return JSON.parse(stripped);
@@ -68358,7 +69006,7 @@ try {
68358
69006
  }
68359
69007
  await runMcp({
68360
69008
  name: "canvas",
68361
- version: "2.1.1",
69009
+ version: "2.1.2",
68362
69010
  // x-release-please-version
68363
69011
  tools: [
68364
69012
  // Outside the `if (resolved)` on purpose: an unconfigured server must