ofw-mcp 2.19.1 → 2.19.3

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
@@ -2279,7 +2279,7 @@ var require_websocket = __commonJS({
2279
2279
  var http = __require("http");
2280
2280
  var net = __require("net");
2281
2281
  var tls = __require("tls");
2282
- var { randomBytes: randomBytes3, createHash: createHash2 } = __require("crypto");
2282
+ var { randomBytes: randomBytes4, createHash: createHash2 } = __require("crypto");
2283
2283
  var { Duplex, Readable } = __require("stream");
2284
2284
  var { URL: URL2 } = __require("url");
2285
2285
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2817,7 +2817,7 @@ var require_websocket = __commonJS({
2817
2817
  }
2818
2818
  }
2819
2819
  const defaultPort = isSecure ? 443 : 80;
2820
- const key = randomBytes3(16).toString("base64");
2820
+ const key = randomBytes4(16).toString("base64");
2821
2821
  const request = isSecure ? https.request : http.request;
2822
2822
  const protocolSet = /* @__PURE__ */ new Set();
2823
2823
  let perMessageDeflate;
@@ -3713,8 +3713,8 @@ var require_websocket_server = __commonJS({
3713
3713
  // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
3714
3714
  import { AsyncLocalStorage } from "node:async_hooks";
3715
3715
  var storage = new AsyncLocalStorage();
3716
- function withCallSignal(signal, fn) {
3717
- return signal ? storage.run({ signal }, fn) : fn();
3716
+ function withCallSignal(signal, fn, request) {
3717
+ return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
3718
3718
  }
3719
3719
  function currentCallSignal() {
3720
3720
  return storage.getStore()?.signal;
@@ -3728,6 +3728,13 @@ function withAmbientCancellation(own2) {
3728
3728
  return AbortSignal.any([own2, ambient]);
3729
3729
  }
3730
3730
 
3731
+ // node_modules/@chrischall/mcp-utils/dist/caller/index.js
3732
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
3733
+ var storage2 = new AsyncLocalStorage2();
3734
+ function withCallerCapabilities(capabilities, fn) {
3735
+ return capabilities ? storage2.run(capabilities, fn) : fn();
3736
+ }
3737
+
3731
3738
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
3732
3739
  var __create2 = Object.create;
3733
3740
  var __defProp2 = Object.defineProperty;
@@ -8811,7 +8818,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
8811
8818
  var NONE = 0;
8812
8819
  var ASSUMED = 1;
8813
8820
  var PROVEN = 2;
8814
- function isRecursive(inst, stack, resolve3) {
8821
+ function isRecursive(inst, stack, resolve6) {
8815
8822
  const cached2 = recursive.get(inst);
8816
8823
  if (cached2 !== void 0)
8817
8824
  return cached2 ? PROVEN : NONE;
@@ -8821,7 +8828,7 @@ function isRecursive(inst, stack, resolve3) {
8821
8828
  let result = NONE;
8822
8829
  const check2 = (child) => {
8823
8830
  if (result !== PROVEN && child?._zod) {
8824
- const answer = isRecursive(child, stack, resolve3);
8831
+ const answer = isRecursive(child, stack, resolve6);
8825
8832
  if (answer > result)
8826
8833
  result = answer;
8827
8834
  }
@@ -8832,7 +8839,7 @@ function isRecursive(inst, stack, resolve3) {
8832
8839
  const desc = Object.getOwnPropertyDescriptor(sh, key);
8833
8840
  if (spread && !desc.enumerable)
8834
8841
  continue;
8835
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
8842
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
8836
8843
  if (child > answer)
8837
8844
  answer = child;
8838
8845
  }
@@ -8896,7 +8903,7 @@ function isRecursive(inst, stack, resolve3) {
8896
8903
  break;
8897
8904
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
8898
8905
  case "lazy": {
8899
- const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
8906
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
8900
8907
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
8901
8908
  break;
8902
8909
  }
@@ -27796,14 +27803,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
27796
27803
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
27797
27804
  }
27798
27805
  function sleep(ms, signal) {
27799
- return new Promise((resolve3, reject) => {
27806
+ return new Promise((resolve6, reject) => {
27800
27807
  if (signal?.aborted) {
27801
27808
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
27802
27809
  return;
27803
27810
  }
27804
27811
  const timer = setTimeout(() => {
27805
27812
  signal?.removeEventListener("abort", onAbort);
27806
- resolve3();
27813
+ resolve6();
27807
27814
  }, ms);
27808
27815
  const onAbort = () => {
27809
27816
  clearTimeout(timer);
@@ -28597,7 +28604,7 @@ var Protocol = class {
28597
28604
  const flowStartedAt = Date.now();
28598
28605
  let onAbort;
28599
28606
  let cleanupMessageId;
28600
- return new Promise((resolve3, reject) => {
28607
+ return new Promise((resolve6, reject) => {
28601
28608
  const earlyReject = (error62) => {
28602
28609
  reject(error62);
28603
28610
  };
@@ -28665,7 +28672,7 @@ var Protocol = class {
28665
28672
  }
28666
28673
  if (decoded.kind === "invalid") return reject(decoded.error);
28667
28674
  if (decoded.kind === "input_required") {
28668
- if (options?.allowInputRequired === true) return resolve3(manualInputRequiredValue(decoded));
28675
+ if (options?.allowInputRequired === true) return resolve6(manualInputRequiredValue(decoded));
28669
28676
  const flow = {
28670
28677
  codec: codec2,
28671
28678
  request,
@@ -28677,11 +28684,11 @@ var Protocol = class {
28677
28684
  params
28678
28685
  }, resultSchema, legOptions)
28679
28686
  };
28680
- return resolve3(this._resolveNonCompleteResult(decoded, flow));
28687
+ return resolve6(this._resolveNonCompleteResult(decoded, flow));
28681
28688
  }
28682
28689
  const result = decoded.result;
28683
28690
  validateStandardSchema(resultSchema, result).then((parseResult) => {
28684
- if (parseResult.success) resolve3(parseResult.data);
28691
+ if (parseResult.success) resolve6(parseResult.data);
28685
28692
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
28686
28693
  }, reject);
28687
28694
  });
@@ -31593,7 +31600,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
31593
31600
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
31594
31601
  const schOrFunc = root.refs[ref];
31595
31602
  if (schOrFunc) return schOrFunc;
31596
- let _sch = resolve3.call(this, root, ref);
31603
+ let _sch = resolve6.call(this, root, ref);
31597
31604
  if (_sch === void 0) {
31598
31605
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
31599
31606
  const { schemaId } = this.opts;
@@ -31619,7 +31626,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
31619
31626
  function sameSchemaEnv(s1, s2) {
31620
31627
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
31621
31628
  }
31622
- function resolve3(root, ref) {
31629
+ function resolve6(root, ref) {
31623
31630
  let sch;
31624
31631
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
31625
31632
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -32069,47 +32076,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32069
32076
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
32070
32077
  return uri;
32071
32078
  }
32072
- function resolve3(baseURI, relativeURI, options) {
32079
+ function resolve6(baseURI, relativeURI, options) {
32073
32080
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
32074
32081
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
32075
32082
  schemelessOptions.skipEscape = true;
32076
32083
  return serialize(resolved, schemelessOptions);
32077
32084
  }
32078
- function resolveComponent(base, relative, options, skipNormalization) {
32085
+ function resolveComponent(base, relative3, options, skipNormalization) {
32079
32086
  const target = {};
32080
32087
  if (!skipNormalization) {
32081
32088
  base = parse3(serialize(base, options), options);
32082
- relative = parse3(serialize(relative, options), options);
32089
+ relative3 = parse3(serialize(relative3, options), options);
32083
32090
  }
32084
32091
  options = options || {};
32085
- if (!options.tolerant && relative.scheme) {
32086
- target.scheme = relative.scheme;
32087
- target.userinfo = relative.userinfo;
32088
- target.host = relative.host;
32089
- target.port = relative.port;
32090
- target.path = removeDotSegments(relative.path || "");
32091
- target.query = relative.query;
32092
+ if (!options.tolerant && relative3.scheme) {
32093
+ target.scheme = relative3.scheme;
32094
+ target.userinfo = relative3.userinfo;
32095
+ target.host = relative3.host;
32096
+ target.port = relative3.port;
32097
+ target.path = removeDotSegments(relative3.path || "");
32098
+ target.query = relative3.query;
32092
32099
  } else {
32093
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
32094
- target.userinfo = relative.userinfo;
32095
- target.host = relative.host;
32096
- target.port = relative.port;
32097
- target.path = removeDotSegments(relative.path || "");
32098
- target.query = relative.query;
32100
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
32101
+ target.userinfo = relative3.userinfo;
32102
+ target.host = relative3.host;
32103
+ target.port = relative3.port;
32104
+ target.path = removeDotSegments(relative3.path || "");
32105
+ target.query = relative3.query;
32099
32106
  } else {
32100
- if (!relative.path) {
32107
+ if (!relative3.path) {
32101
32108
  target.path = base.path;
32102
- if (relative.query !== void 0) target.query = relative.query;
32109
+ if (relative3.query !== void 0) target.query = relative3.query;
32103
32110
  else target.query = base.query;
32104
32111
  } else {
32105
- if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
32112
+ if (relative3.path[0] === "/") target.path = removeDotSegments(relative3.path);
32106
32113
  else {
32107
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
32108
- else if (!base.path) target.path = relative.path;
32109
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
32114
+ if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative3.path;
32115
+ else if (!base.path) target.path = relative3.path;
32116
+ else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
32110
32117
  target.path = removeDotSegments(target.path);
32111
32118
  }
32112
- target.query = relative.query;
32119
+ target.query = relative3.query;
32113
32120
  }
32114
32121
  target.userinfo = base.userinfo;
32115
32122
  target.host = base.host;
@@ -32117,7 +32124,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32117
32124
  }
32118
32125
  target.scheme = base.scheme;
32119
32126
  }
32120
- target.fragment = relative.fragment;
32127
+ target.fragment = relative3.fragment;
32121
32128
  return target;
32122
32129
  }
32123
32130
  function equal(uriA, uriB, options) {
@@ -32243,7 +32250,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32243
32250
  const fastUri = {
32244
32251
  SCHEMES,
32245
32252
  normalize,
32246
- resolve: resolve3,
32253
+ resolve: resolve6,
32247
32254
  resolveComponent,
32248
32255
  equal,
32249
32256
  serialize,
@@ -37616,7 +37623,7 @@ var StdioServerTransport = class {
37616
37623
  }
37617
37624
  send(message) {
37618
37625
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
37619
- return new Promise((resolve3, reject) => {
37626
+ return new Promise((resolve6, reject) => {
37620
37627
  const json2 = serializeMessage(message);
37621
37628
  let settled = false;
37622
37629
  const onError = (error62) => {
@@ -37631,14 +37638,14 @@ var StdioServerTransport = class {
37631
37638
  settled = true;
37632
37639
  this._stdout.off("error", onError);
37633
37640
  this._stdout.off("drain", onDrain);
37634
- resolve3();
37641
+ resolve6();
37635
37642
  };
37636
37643
  this._stdout.once("error", onError);
37637
37644
  if (this._stdout.write(json2)) {
37638
37645
  if (settled) return;
37639
37646
  settled = true;
37640
37647
  this._stdout.off("error", onError);
37641
- resolve3();
37648
+ resolve6();
37642
37649
  } else if (!settled) this._stdout.once("drain", onDrain);
37643
37650
  });
37644
37651
  }
@@ -37692,14 +37699,14 @@ var StdioConnectionChannel = class {
37692
37699
  */
37693
37700
  async whenRequestsAnswered(timeoutMs) {
37694
37701
  if (this._closed || this._pendingRequests.size === 0) return true;
37695
- return await new Promise((resolve3) => {
37702
+ return await new Promise((resolve6) => {
37696
37703
  const waiter = () => {
37697
37704
  clearTimeout(timer);
37698
- resolve3(true);
37705
+ resolve6(true);
37699
37706
  };
37700
37707
  const timer = setTimeout(() => {
37701
37708
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
37702
- resolve3(false);
37709
+ resolve6(false);
37703
37710
  }, timeoutMs);
37704
37711
  this._drainWaiters.push(waiter);
37705
37712
  });
@@ -38073,13 +38080,28 @@ var API_KEY_RE = new RegExp([
38073
38080
  "whsec_[A-Za-z0-9]{16,}\\b"
38074
38081
  // webhook signing secret (Stripe-style)
38075
38082
  ].map((p) => `\\b${p}`).join("|"), "g");
38076
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
38083
+ 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;
38084
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
38085
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
38086
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
38077
38087
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
38078
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
38079
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
38080
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
38088
+ var JSON_SECRET_KEYS = [
38089
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
38090
+ "client[_-]?secret",
38091
+ "api[_-]?secret",
38092
+ "(?:x[_-])?api[_-]?key",
38093
+ "private[_-]?key",
38094
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
38095
+ "(?:proxy-)?authorization",
38096
+ "password",
38097
+ "passwd",
38098
+ "secret",
38099
+ "token"
38100
+ ].join("|");
38101
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
38102
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
38081
38103
  function redactSecrets(text) {
38082
- 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]");
38104
+ 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]");
38083
38105
  }
38084
38106
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
38085
38107
  const str = text === null || text === void 0 ? "" : String(text);
@@ -38173,6 +38195,15 @@ Hint: ${err.hint}`);
38173
38195
  }
38174
38196
  throw err;
38175
38197
  }
38198
+ function mcpRequestFrom(args) {
38199
+ const ctx = args.at(-1);
38200
+ if (typeof ctx !== "object" || ctx === null)
38201
+ return void 0;
38202
+ const req = ctx.mcpReq;
38203
+ if (typeof req !== "object" || req === null)
38204
+ return void 0;
38205
+ return typeof req.notify === "function" ? req : void 0;
38206
+ }
38176
38207
  function callSignalFrom(args) {
38177
38208
  const ctx = args.at(-1);
38178
38209
  if (typeof ctx !== "object" || ctx === null)
@@ -38183,6 +38214,20 @@ function callSignalFrom(args) {
38183
38214
  const signal = req.signal;
38184
38215
  return signal instanceof AbortSignal ? signal : void 0;
38185
38216
  }
38217
+ function declaredCapabilitiesFrom(server) {
38218
+ const inner = server.server;
38219
+ if (typeof inner !== "object" || inner === null)
38220
+ return void 0;
38221
+ const read = inner.getClientCapabilities;
38222
+ if (typeof read !== "function")
38223
+ return void 0;
38224
+ try {
38225
+ const declared = read.call(inner);
38226
+ return typeof declared === "object" && declared !== null ? declared : void 0;
38227
+ } catch {
38228
+ return void 0;
38229
+ }
38230
+ }
38186
38231
  function surfaceToolHints(server) {
38187
38232
  const register2 = server.registerTool.bind(server);
38188
38233
  server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
@@ -38198,15 +38243,24 @@ function surfaceToolHints(server) {
38198
38243
  // Here because this is the one wrapper every tool already passes
38199
38244
  // through: threading the signal by hand would mean editing several
38200
38245
  // hundred handlers and missing exactly the ones nobody edits.
38201
- withCallSignal(callSignalFrom(args), () => {
38202
- let result;
38203
- try {
38204
- result = cb(...args);
38205
- } catch (err) {
38206
- return hintResultOrRethrow(err);
38207
- }
38208
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
38209
- })
38246
+ withCallSignal(callSignalFrom(args), () => (
38247
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
38248
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
38249
+ // the wrapper — a 2026-07-28 request answers for itself off its own
38250
+ // envelope — but a guarded tool that cannot tell whether the caller
38251
+ // can be shown a prompt returns one the SDK then refuses to deliver,
38252
+ // and that refusal reaches the caller as an unexplained protocol
38253
+ // error it had no chance to catch.
38254
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
38255
+ let result;
38256
+ try {
38257
+ result = cb(...args);
38258
+ } catch (err) {
38259
+ return hintResultOrRethrow(err);
38260
+ }
38261
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
38262
+ })
38263
+ ), mcpRequestFrom(args))
38210
38264
  ));
38211
38265
  }
38212
38266
  async function createMcpServer(opts) {
@@ -38222,13 +38276,47 @@ async function createMcpServer(opts) {
38222
38276
  }
38223
38277
  return server;
38224
38278
  }
38279
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
38280
+ var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
38225
38281
  function withGracefulShutdown(target, opts = {}) {
38226
38282
  const shouldExit = opts.exit ?? true;
38283
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
38284
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
38285
+ }
38286
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
38287
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
38288
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
38289
+ }
38290
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
38227
38291
  let shuttingDown = false;
38292
+ let firstSignalAt = 0;
38293
+ let exited = false;
38294
+ const exitOnce = () => {
38295
+ if (exited)
38296
+ return;
38297
+ exited = true;
38298
+ process.exit(0);
38299
+ };
38228
38300
  const handler = (signal) => {
38229
- if (shuttingDown)
38301
+ if (shuttingDown) {
38302
+ if (Date.now() - firstSignalAt < repeatGraceMs)
38303
+ return;
38304
+ if (shouldExit) {
38305
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
38306
+ exitOnce();
38307
+ }
38230
38308
  return;
38309
+ }
38231
38310
  shuttingDown = true;
38311
+ firstSignalAt = Date.now();
38312
+ let timer;
38313
+ if (shouldExit) {
38314
+ timer = setTimeout(() => {
38315
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
38316
+ exitOnce();
38317
+ }, timeoutMs);
38318
+ timer.unref?.();
38319
+ }
38232
38320
  void (async () => {
38233
38321
  try {
38234
38322
  if (opts.onSignal)
@@ -38237,8 +38325,10 @@ function withGracefulShutdown(target, opts = {}) {
38237
38325
  } catch (err) {
38238
38326
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
38239
38327
  } finally {
38328
+ if (timer !== void 0)
38329
+ clearTimeout(timer);
38240
38330
  if (shouldExit)
38241
- process.exit(0);
38331
+ exitOnce();
38242
38332
  }
38243
38333
  })();
38244
38334
  };
@@ -38324,10 +38414,48 @@ async function loadDotenvSafely(opts = {}) {
38324
38414
 
38325
38415
  // node_modules/@chrischall/mcp-utils/dist/fs/index.js
38326
38416
  import { openAsBlob } from "node:fs";
38417
+
38418
+ // node_modules/@chrischall/mcp-utils/dist/fs/confine.js
38419
+ import { existsSync, realpathSync } from "node:fs";
38420
+ import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
38421
+ function isInside(root, target) {
38422
+ const rel = relative(root, target);
38423
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute2(rel);
38424
+ }
38425
+ function realPathAllowingMissing(p) {
38426
+ const abs = resolve2(p);
38427
+ let existing = abs;
38428
+ const tail = [];
38429
+ while (!existsSync(existing)) {
38430
+ const parent = dirname(existing);
38431
+ if (parent === existing)
38432
+ break;
38433
+ tail.unshift(existing.slice(parent.length).replace(/^[/\\]+/, ""));
38434
+ existing = parent;
38435
+ }
38436
+ let real;
38437
+ try {
38438
+ real = realpathSync(existing);
38439
+ } catch {
38440
+ real = existing;
38441
+ }
38442
+ return tail.length > 0 ? resolve2(real, ...tail) : real;
38443
+ }
38444
+ function assertPathWithinRoots(path, roots) {
38445
+ const target = realPathAllowingMissing(expandPath(path));
38446
+ for (const root of roots) {
38447
+ if (isInside(realPathAllowingMissing(expandPath(root)), target))
38448
+ return target;
38449
+ }
38450
+ throw new Error(`Path is outside the allowed directories: ${path}`);
38451
+ }
38452
+
38453
+ // node_modules/@chrischall/mcp-utils/dist/fs/index.js
38327
38454
  async function fileBlob(path, opts = {}) {
38455
+ const target = opts.allowedRoots ? assertPathWithinRoots(path, opts.allowedRoots) : path;
38328
38456
  let blob;
38329
38457
  try {
38330
- blob = await openAsBlob(path, opts.type !== void 0 ? { type: opts.type } : void 0);
38458
+ blob = await openAsBlob(target, opts.type !== void 0 ? { type: opts.type } : void 0);
38331
38459
  } catch {
38332
38460
  throw new Error(`Cannot read file for upload: ${path}`);
38333
38461
  }
@@ -38415,8 +38543,8 @@ async function resolveAuthPattern(pattern) {
38415
38543
  }
38416
38544
 
38417
38545
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
38418
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
38419
- import { dirname, join as join2, resolve as resolve2 } from "node:path";
38546
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
38547
+ import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
38420
38548
  import { homedir as homedir2 } from "node:os";
38421
38549
  import { randomBytes, createHmac } from "node:crypto";
38422
38550
  function bindingDigest(secret, salt) {
@@ -38439,7 +38567,7 @@ function createFileStatePersistence(opts) {
38439
38567
  const secret = opts.boundTo;
38440
38568
  return {
38441
38569
  load() {
38442
- if (!existsSync(filePath))
38570
+ if (!existsSync2(filePath))
38443
38571
  return null;
38444
38572
  try {
38445
38573
  const raw = JSON.parse(readFileSync(filePath, "utf8"));
@@ -38469,9 +38597,9 @@ function createFileStatePersistence(opts) {
38469
38597
  }
38470
38598
  },
38471
38599
  save(state) {
38472
- const dir = dirname(filePath);
38600
+ const dir = dirname2(filePath);
38473
38601
  const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
38474
- const dirExisted = existsSync(dir);
38602
+ const dirExisted = existsSync2(dir);
38475
38603
  try {
38476
38604
  mkdirSync(dir, { recursive: true, mode: 448 });
38477
38605
  if (!dirExisted)
@@ -38488,7 +38616,7 @@ function createFileStatePersistence(opts) {
38488
38616
  chmodSync(filePath, 384);
38489
38617
  } catch (err) {
38490
38618
  try {
38491
- if (existsSync(tmp))
38619
+ if (existsSync2(tmp))
38492
38620
  unlinkSync(tmp);
38493
38621
  } catch {
38494
38622
  }
@@ -38497,7 +38625,7 @@ function createFileStatePersistence(opts) {
38497
38625
  },
38498
38626
  clear() {
38499
38627
  try {
38500
- if (existsSync(filePath))
38628
+ if (existsSync2(filePath))
38501
38629
  unlinkSync(filePath);
38502
38630
  } catch {
38503
38631
  }
@@ -38518,7 +38646,7 @@ function resolveStateFile(opts) {
38518
38646
  return homeOf(opts.env);
38519
38647
  if (override.startsWith("~/"))
38520
38648
  return join2(homeOf(opts.env), override.slice(2));
38521
- return resolve2(override);
38649
+ return resolve3(override);
38522
38650
  }
38523
38651
  const dirOpts = { env: opts.env };
38524
38652
  if (opts.subdir !== void 0)
@@ -38531,7 +38659,39 @@ function defaultIsRefreshRevoked(err) {
38531
38659
  return false;
38532
38660
  if (err instanceof ApiError && err.status >= 500)
38533
38661
  return false;
38534
- return true;
38662
+ return !isTransientFailure(err);
38663
+ }
38664
+ function isPersistenceFailure(err) {
38665
+ return err !== null && typeof err === "object" && err.persistenceFailure === true;
38666
+ }
38667
+ var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
38668
+ "ECONNRESET",
38669
+ "ECONNREFUSED",
38670
+ "ECONNABORTED",
38671
+ "ETIMEDOUT",
38672
+ "ENOTFOUND",
38673
+ "EAI_AGAIN",
38674
+ "ENETUNREACH",
38675
+ "ENETDOWN",
38676
+ "EHOSTUNREACH",
38677
+ "EPIPE"
38678
+ ]);
38679
+ function isTransientFailure(err) {
38680
+ let cur = err;
38681
+ for (let depth = 0; depth < 4 && cur !== null && typeof cur === "object"; depth++) {
38682
+ const e = cur;
38683
+ if (typeof e.status === "number" && (e.status >= 500 || e.status === 429 || e.status === 408))
38684
+ return true;
38685
+ if (e.name === "AbortError" || e.name === "TimeoutError")
38686
+ return true;
38687
+ if (typeof e.code === "string" && (TRANSIENT_NETWORK_CODES.has(e.code) || e.code.startsWith("UND_ERR_"))) {
38688
+ return true;
38689
+ }
38690
+ if (e.name === "TypeError" && e.message === "fetch failed")
38691
+ return true;
38692
+ cur = e.cause;
38693
+ }
38694
+ return false;
38535
38695
  }
38536
38696
  function isBearerTokens(raw) {
38537
38697
  if (raw === null || typeof raw !== "object")
@@ -38698,6 +38858,8 @@ var TokenManager = class {
38698
38858
  async reBootstrap(err) {
38699
38859
  if (err instanceof StatePersistenceError)
38700
38860
  throw err.cause;
38861
+ if (isPersistenceFailure(err))
38862
+ throw err;
38701
38863
  if (this.bootstrapFn === void 0)
38702
38864
  throw err;
38703
38865
  if (!this.isRefreshRevokedFn(err))
@@ -38713,6 +38875,10 @@ var TokenManager = class {
38713
38875
  try {
38714
38876
  await this.refreshNow();
38715
38877
  } catch (err) {
38878
+ const held = this.tokens;
38879
+ if (!(err instanceof StatePersistenceError) && held !== void 0 && this.now() < held.expiresAt && !this.isRefreshRevokedFn(err)) {
38880
+ return held.accessToken;
38881
+ }
38716
38882
  return (await this.reBootstrap(err)).accessToken;
38717
38883
  }
38718
38884
  tokens = this.tokens ?? tokens;
@@ -38753,7 +38919,7 @@ var TokenManager = class {
38753
38919
  };
38754
38920
 
38755
38921
  // src/client.ts
38756
- import { dirname as dirname2, join as join5 } from "path";
38922
+ import { dirname as dirname3, join as join5 } from "path";
38757
38923
  import { fileURLToPath } from "url";
38758
38924
 
38759
38925
  // node_modules/@fetchproxy/protocol/dist/crypto.js
@@ -38953,6 +39119,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
38953
39119
  "capture_redirect",
38954
39120
  "read_indexed_db",
38955
39121
  "read_dom",
39122
+ "read_dom_list",
38956
39123
  "download",
38957
39124
  "graphql",
38958
39125
  "write_cookies"
@@ -39453,6 +39620,79 @@ function assertDomSelectorsArray(value, label) {
39453
39620
  }
39454
39621
  }
39455
39622
  }
39623
+ function assertDomListSelectorsArray(value, label) {
39624
+ if (!Array.isArray(value)) {
39625
+ throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
39626
+ }
39627
+ const seen = /* @__PURE__ */ new Set();
39628
+ for (let i = 0; i < value.length; i++) {
39629
+ const entry = value[i];
39630
+ assertObject(entry, `${label}[${i}]`);
39631
+ if (entry.name === void 0) {
39632
+ throw new ProtocolError3(`${label}[${i}].name: missing`);
39633
+ }
39634
+ if (entry.itemSelector === void 0) {
39635
+ throw new ProtocolError3(`${label}[${i}].itemSelector: missing`);
39636
+ }
39637
+ if (entry.fields === void 0) {
39638
+ throw new ProtocolError3(`${label}[${i}].fields: missing`);
39639
+ }
39640
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
39641
+ throw new ProtocolError3(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
39642
+ }
39643
+ if (typeof entry.itemSelector !== "string" || !DOM_SELECTOR_RE.test(entry.itemSelector)) {
39644
+ throw new ProtocolError3(`${label}[${i}].itemSelector: invalid ${JSON.stringify(entry.itemSelector)}`);
39645
+ }
39646
+ if (!Array.isArray(entry.fields) || entry.fields.length === 0) {
39647
+ throw new ProtocolError3(`${label}[${i}].fields: expected non-empty array`);
39648
+ }
39649
+ const fieldNames = /* @__PURE__ */ new Set();
39650
+ for (let j = 0; j < entry.fields.length; j++) {
39651
+ const field = entry.fields[j];
39652
+ const fieldLabel = `${label}[${i}].fields[${j}]`;
39653
+ assertObject(field, fieldLabel);
39654
+ if (field.name === void 0) {
39655
+ throw new ProtocolError3(`${fieldLabel}.name: missing`);
39656
+ }
39657
+ if (typeof field.name !== "string" || !SCOPE_KEY_RE.test(field.name)) {
39658
+ throw new ProtocolError3(`${fieldLabel}.name: invalid ${JSON.stringify(field.name)}`);
39659
+ }
39660
+ if (field.selector !== void 0) {
39661
+ if (typeof field.selector !== "string" || !DOM_SELECTOR_RE.test(field.selector)) {
39662
+ throw new ProtocolError3(`${fieldLabel}.selector: invalid ${JSON.stringify(field.selector)}`);
39663
+ }
39664
+ }
39665
+ if (field.attribute !== void 0) {
39666
+ if (typeof field.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(field.attribute)) {
39667
+ throw new ProtocolError3(`${fieldLabel}.attribute: invalid ${JSON.stringify(field.attribute)}`);
39668
+ }
39669
+ }
39670
+ if (fieldNames.has(field.name)) {
39671
+ throw new ProtocolError3(`${label}[${i}].fields: duplicate name ${JSON.stringify(field.name)}`);
39672
+ }
39673
+ fieldNames.add(field.name);
39674
+ for (const k of Object.keys(field)) {
39675
+ if (k !== "name" && k !== "selector" && k !== "attribute") {
39676
+ throw new ProtocolError3(`${fieldLabel}: unexpected field ${JSON.stringify(k)}`);
39677
+ }
39678
+ }
39679
+ }
39680
+ if (entry.maxItems !== void 0) {
39681
+ if (typeof entry.maxItems !== "number" || !Number.isInteger(entry.maxItems) || entry.maxItems < 1 || entry.maxItems > 1e3) {
39682
+ throw new ProtocolError3(`${label}[${i}].maxItems: invalid ${JSON.stringify(entry.maxItems)}`);
39683
+ }
39684
+ }
39685
+ if (seen.has(entry.name)) {
39686
+ throw new ProtocolError3(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
39687
+ }
39688
+ seen.add(entry.name);
39689
+ for (const k of Object.keys(entry)) {
39690
+ if (k !== "name" && k !== "itemSelector" && k !== "fields" && k !== "maxItems") {
39691
+ throw new ProtocolError3(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
39692
+ }
39693
+ }
39694
+ }
39695
+ }
39456
39696
  function assertGraphqlOpsArray(value, label) {
39457
39697
  if (!Array.isArray(value)) {
39458
39698
  throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
@@ -39499,6 +39739,8 @@ function validateFrame(raw) {
39499
39739
  return validateHelloRejected(raw);
39500
39740
  if (t === "extension-disconnected")
39501
39741
  return { type: "extension-disconnected" };
39742
+ if (t === "peer-gone")
39743
+ return validatePeerGone(raw);
39502
39744
  throw new ProtocolError3(`unknown frame type: ${String(t)}`);
39503
39745
  }
39504
39746
  function validateHello(raw) {
@@ -39569,16 +39811,11 @@ function validateHello(raw) {
39569
39811
  if (raw.domSelectors !== void 0) {
39570
39812
  assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
39571
39813
  }
39572
- if (raw.accepts !== void 0) {
39573
- if (!Array.isArray(raw.accepts)) {
39574
- throw new ProtocolError3("hello.accepts: expected array");
39575
- }
39576
- for (const a of raw.accepts) {
39577
- if (typeof a !== "string") {
39578
- throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
39579
- }
39580
- }
39814
+ if (raw.domListSelectors !== void 0) {
39815
+ assertDomListSelectorsArray(raw.domListSelectors, "hello.domListSelectors");
39581
39816
  }
39817
+ if (raw.accepts !== void 0)
39818
+ assertAcceptsList(raw.accepts);
39582
39819
  if (raw.graphqlOps !== void 0) {
39583
39820
  assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
39584
39821
  }
@@ -39600,6 +39837,8 @@ function validateHello(raw) {
39600
39837
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
39601
39838
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
39602
39839
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
39840
+ if (raw.accepts !== void 0)
39841
+ assertAcceptsList(raw.accepts);
39603
39842
  return raw;
39604
39843
  }
39605
39844
  throw new ProtocolError3(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
@@ -39655,6 +39894,27 @@ function validatePairPending(raw) {
39655
39894
  }
39656
39895
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
39657
39896
  }
39897
+ function assertAcceptsList(accepts) {
39898
+ if (!Array.isArray(accepts)) {
39899
+ throw new ProtocolError3("hello.accepts: expected array");
39900
+ }
39901
+ for (const a of accepts) {
39902
+ if (typeof a !== "string") {
39903
+ throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
39904
+ }
39905
+ }
39906
+ }
39907
+ function validatePeerGone(raw) {
39908
+ assertString(raw.mcpId, "peer-gone.mcpId");
39909
+ if (!isValidMcpId(raw.mcpId))
39910
+ throw new ProtocolError3("peer-gone.mcpId: invalid format");
39911
+ for (const k of Object.keys(raw)) {
39912
+ if (k !== "type" && k !== "mcpId") {
39913
+ throw new ProtocolError3(`peer-gone: unexpected field ${JSON.stringify(k)}`);
39914
+ }
39915
+ }
39916
+ return { type: "peer-gone", mcpId: raw.mcpId };
39917
+ }
39658
39918
  function validateHelloRejected(raw) {
39659
39919
  assertString(raw.mcpId, "hello-rejected.mcpId");
39660
39920
  if (!isValidMcpId(raw.mcpId))
@@ -39904,6 +40164,24 @@ function validateInnerRequest(raw) {
39904
40164
  }
39905
40165
  return raw;
39906
40166
  }
40167
+ if (raw.op === "read_dom_list") {
40168
+ assertObject(raw.init, "inner.init");
40169
+ if (raw.init.origin === void 0)
40170
+ throw new ProtocolError3("inner.init.origin: missing");
40171
+ if (raw.init.name === void 0)
40172
+ throw new ProtocolError3("inner.init.name: missing");
40173
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
40174
+ assertString(raw.init.name, "inner.init.name");
40175
+ if (raw.init.name.length === 0) {
40176
+ throw new ProtocolError3("inner.init.name: must be non-empty");
40177
+ }
40178
+ for (const k of Object.keys(raw.init)) {
40179
+ if (k !== "origin" && k !== "name") {
40180
+ throw new ProtocolError3(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom_list`);
40181
+ }
40182
+ }
40183
+ return raw;
40184
+ }
39907
40185
  if (raw.op === "graphql_query") {
39908
40186
  assertObject(raw.init, "inner.init");
39909
40187
  if (raw.init.name === void 0)
@@ -39951,7 +40229,7 @@ function validateInnerRequest(raw) {
39951
40229
  }
39952
40230
  return raw;
39953
40231
  }
39954
- 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)}`);
40232
+ 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)}`);
39955
40233
  }
39956
40234
  function assertNonEmptyKeyArray(value, label) {
39957
40235
  if (!Array.isArray(value)) {
@@ -40059,6 +40337,18 @@ function validateInnerResponse(raw) {
40059
40337
  assertStringMap(raw.values, "inner.values");
40060
40338
  return raw;
40061
40339
  }
40340
+ if (op === "read_dom_list") {
40341
+ if (raw.rows === void 0) {
40342
+ throw new ProtocolError3("inner.rows: missing on read_dom_list response");
40343
+ }
40344
+ if (!Array.isArray(raw.rows)) {
40345
+ throw new ProtocolError3("inner.rows: expected array, got " + typeof raw.rows);
40346
+ }
40347
+ for (const [i, row] of raw.rows.entries()) {
40348
+ assertStringMap(row, `inner.rows[${i}]`);
40349
+ }
40350
+ return raw;
40351
+ }
40062
40352
  if (op === "graphql_query") {
40063
40353
  if (raw.data === void 0) {
40064
40354
  throw new ProtocolError3("inner.data: missing on graphql_query response");
@@ -40160,12 +40450,6 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner, direction) {
40160
40450
  ciphertext: toB64(ct)
40161
40451
  };
40162
40452
  }
40163
- async function openEncryptedFrame(sessionKey, frame, direction) {
40164
- const result = await openEncryptedFrameDetailed(sessionKey, frame, direction);
40165
- if (result.stage === "ok")
40166
- return result.inner;
40167
- throw result.error instanceof Error ? result.error : new Error(String(result.error));
40168
- }
40169
40453
  async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
40170
40454
  let pt;
40171
40455
  try {
@@ -40196,7 +40480,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
40196
40480
  async function electRole(opts) {
40197
40481
  const server = createServer();
40198
40482
  const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
40199
- return new Promise((resolve3, reject) => {
40483
+ return new Promise((resolve6, reject) => {
40200
40484
  let timer;
40201
40485
  const clearBindTimer = () => {
40202
40486
  if (timer) {
@@ -40212,7 +40496,7 @@ async function electRole(opts) {
40212
40496
  server.close();
40213
40497
  } catch {
40214
40498
  }
40215
- resolve3({ role: "peer" });
40499
+ resolve6({ role: "peer" });
40216
40500
  } else {
40217
40501
  reject(e);
40218
40502
  }
@@ -40220,7 +40504,7 @@ async function electRole(opts) {
40220
40504
  const onListening = () => {
40221
40505
  clearBindTimer();
40222
40506
  server.removeListener("error", onError);
40223
- resolve3({ role: "host", server });
40507
+ resolve6({ role: "host", server });
40224
40508
  };
40225
40509
  server.once("error", onError);
40226
40510
  server.once("listening", onListening);
@@ -40317,6 +40601,18 @@ async function buildServerHello(opts) {
40317
40601
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
40318
40602
  }));
40319
40603
  }
40604
+ if (opts.domListSelectors && opts.domListSelectors.length > 0) {
40605
+ hello.domListSelectors = opts.domListSelectors.map((d) => ({
40606
+ name: d.name,
40607
+ itemSelector: d.itemSelector,
40608
+ fields: d.fields.map((f) => ({
40609
+ name: f.name,
40610
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
40611
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
40612
+ })),
40613
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
40614
+ }));
40615
+ }
40320
40616
  if (opts.graphqlOps && opts.graphqlOps.length > 0) {
40321
40617
  hello.graphqlOps = opts.graphqlOps.map((d) => ({
40322
40618
  name: d.name,
@@ -40485,13 +40781,14 @@ async function awaitSessionReady(ready, opts) {
40485
40781
  }
40486
40782
 
40487
40783
  // node_modules/@fetchproxy/server/dist/extension-trust.js
40488
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
40489
- import { isAbsolute as isAbsolute3, join as join4 } from "node:path";
40784
+ 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";
40785
+ import { randomBytes as randomBytes3 } from "node:crypto";
40786
+ import { isAbsolute as isAbsolute4, join as join4 } from "node:path";
40490
40787
 
40491
40788
  // node_modules/@fetchproxy/server/dist/identity.js
40492
- import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
40789
+ import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
40493
40790
  import { randomBytes as randomBytes2 } from "node:crypto";
40494
- import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
40791
+ import { isAbsolute as isAbsolute3, join as join3 } from "node:path";
40495
40792
  import { homedir as homedir3 } from "node:os";
40496
40793
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
40497
40794
  var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
@@ -40500,7 +40797,7 @@ function envIdentityDir() {
40500
40797
  if (raw === void 0)
40501
40798
  return void 0;
40502
40799
  const dir = raw.trim();
40503
- if (dir === "" || !isAbsolute2(dir))
40800
+ if (dir === "" || !isAbsolute3(dir))
40504
40801
  return void 0;
40505
40802
  return dir;
40506
40803
  }
@@ -40557,7 +40854,7 @@ async function openIdentityDir(dir) {
40557
40854
  }
40558
40855
  }
40559
40856
  var STAGING_BYTES = 6;
40560
- async function sweepStagingFiles(dir, serverName) {
40857
+ async function sweepStagingFiles(dir, serverName, opts = {}) {
40561
40858
  const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
40562
40859
  const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
40563
40860
  let names;
@@ -40566,35 +40863,80 @@ async function sweepStagingFiles(dir, serverName) {
40566
40863
  } catch {
40567
40864
  return;
40568
40865
  }
40569
- await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join3(dir, name)).catch(() => void 0)));
40866
+ const cutoff = opts.olderThanMs === void 0 ? null : Date.now() - opts.olderThanMs;
40867
+ await Promise.all(names.filter((name) => staging.test(name)).map(async (name) => {
40868
+ const path = join3(dir, name);
40869
+ if (cutoff !== null) {
40870
+ const st = await lstat(path).catch(() => null);
40871
+ if (st === null || st.mtimeMs > cutoff)
40872
+ return;
40873
+ }
40874
+ await unlink(path).catch(() => void 0);
40875
+ }));
40570
40876
  }
40571
- async function writeIdentityFile(dir, serverName, id) {
40877
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
40572
40878
  const path = identityFilePath(dir, serverName);
40573
40879
  await openIdentityDir(dir);
40574
- await sweepStagingFiles(dir, serverName);
40575
- const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
40576
40880
  try {
40577
- await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
40578
- await chmod(tmp, 384);
40579
- await rename(tmp, path);
40881
+ return parseIdentity(await readFile(path, "utf8"));
40580
40882
  } catch (e) {
40581
- await rm(tmp, { force: true });
40582
- throw e;
40883
+ if (e.code !== "ENOENT" && !(e instanceof SyntaxError))
40884
+ throw e;
40885
+ }
40886
+ let unreadable = null;
40887
+ for (let attempt = 0; attempt < 5; attempt++) {
40888
+ const id = await generateIdentity();
40889
+ if (await publishIdentityIfAbsent(dir, serverName, id))
40890
+ return id;
40891
+ try {
40892
+ return parseIdentity(await readFile(path, "utf8"));
40893
+ } catch (e) {
40894
+ if (e.code === "ENOENT") {
40895
+ continue;
40896
+ }
40897
+ if (!(e instanceof SyntaxError))
40898
+ throw e;
40899
+ unreadable = e;
40900
+ await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
40901
+ }
40583
40902
  }
40584
- return path;
40903
+ if (unreadable !== null)
40904
+ throw unreadable;
40905
+ throw new Error(`fetchproxy: could not create an identity for ${JSON.stringify(serverName)} in ${dir}`);
40585
40906
  }
40586
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
40907
+ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
40908
+ async function publishIdentityIfAbsent(dir, serverName, id) {
40587
40909
  const path = identityFilePath(dir, serverName);
40588
- await openIdentityDir(dir);
40910
+ await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
40911
+ const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
40589
40912
  try {
40590
- return parseIdentity(await readFile(path, "utf8"));
40913
+ await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
40914
+ await chmod(tmp, 384);
40915
+ try {
40916
+ await link(tmp, path);
40917
+ } catch (e) {
40918
+ const code = e.code;
40919
+ if (code === void 0 || !NO_HARD_LINKS.has(code))
40920
+ throw e;
40921
+ try {
40922
+ await writeFile(path, serializeIdentity(id), { mode: 384, flag: "wx" });
40923
+ } catch (writeErr) {
40924
+ if (writeErr.code !== "EEXIST") {
40925
+ await rm(path, { force: true }).catch(() => void 0);
40926
+ }
40927
+ throw writeErr;
40928
+ }
40929
+ await chmod(path, 384);
40930
+ }
40931
+ return true;
40591
40932
  } catch (e) {
40592
- if (e.code !== "ENOENT")
40593
- throw e;
40933
+ const code = e.code;
40934
+ if (code === "EEXIST" || code === "ENOENT")
40935
+ return false;
40936
+ throw e;
40937
+ } finally {
40938
+ await rm(tmp, { force: true });
40594
40939
  }
40595
- const id = await generateIdentity();
40596
- await writeIdentityFile(dir, serverName, id);
40597
- return id;
40598
40940
  }
40599
40941
 
40600
40942
  // node_modules/@fetchproxy/server/dist/extension-trust.js
@@ -40604,7 +40946,7 @@ function envTrustDir(env = process.env) {
40604
40946
  if (raw === void 0)
40605
40947
  return void 0;
40606
40948
  const dir = raw.trim();
40607
- if (dir !== "" && isAbsolute3(dir))
40949
+ if (dir !== "" && isAbsolute4(dir))
40608
40950
  return dir;
40609
40951
  console.warn(`[fetchproxy] ignoring ${TRUST_DIR_ENV}=${JSON.stringify(raw)}: it is not an absolute path. The extension pin will be written beside the identity instead, which is not where you pointed it \u2014 if that directory is read-only, no pin is ever kept.`);
40610
40952
  return void 0;
@@ -40695,10 +41037,15 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
40695
41037
  async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
40696
41038
  const path = extensionTrustPath(serverName, dir);
40697
41039
  await mkdir2(dir, { recursive: true, mode: 448 });
40698
- const tmp = `${path}.tmp`;
40699
- await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
40700
- await chmod2(tmp, 384);
40701
- await rename2(tmp, path);
41040
+ const tmp = `${path}.${randomBytes3(6).toString("hex")}.tmp`;
41041
+ try {
41042
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384, flag: "wx" });
41043
+ await chmod2(tmp, 384);
41044
+ await rename2(tmp, path);
41045
+ } catch (e) {
41046
+ await rm2(tmp, { force: true });
41047
+ throw e;
41048
+ }
40702
41049
  }
40703
41050
 
40704
41051
  // node_modules/@fetchproxy/server/dist/host.js
@@ -40735,6 +41082,7 @@ function originVerdict(origin, allowLocal) {
40735
41082
  var HANDSHAKE_TIMEOUT_MS = 15e3;
40736
41083
  var MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
40737
41084
  var enc4 = new TextEncoder();
41085
+ var OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE = 3;
40738
41086
  async function startHost(opts) {
40739
41087
  const allowLocalOrigins = envAllowsLocalOrigins();
40740
41088
  if (allowLocalOrigins) {
@@ -40778,6 +41126,7 @@ async function startHost(opts) {
40778
41126
  localStoragePointers: opts.ownLocalStoragePointers,
40779
41127
  sessionStoragePointers: opts.ownSessionStoragePointers,
40780
41128
  domSelectors: opts.ownDomSelectors,
41129
+ domListSelectors: opts.ownDomListSelectors,
40781
41130
  graphqlOps: opts.ownGraphqlOps
40782
41131
  };
40783
41132
  const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
@@ -40804,8 +41153,8 @@ async function startHost(opts) {
40804
41153
  let rejectOwnSession;
40805
41154
  let ownSessionReady;
40806
41155
  function resetSessionPromise() {
40807
- ownSessionReady = new Promise((resolve3, reject) => {
40808
- resolveOwnSession = resolve3;
41156
+ ownSessionReady = new Promise((resolve6, reject) => {
41157
+ resolveOwnSession = resolve6;
40809
41158
  rejectOwnSession = reject;
40810
41159
  });
40811
41160
  ownSessionReady.catch(() => {
@@ -40848,6 +41197,31 @@ async function startHost(opts) {
40848
41197
  return null;
40849
41198
  }
40850
41199
  };
41200
+ let ownDecryptFailures = 0;
41201
+ const rehandshakeOwnSession = async (ws) => {
41202
+ const hello = extensionHello;
41203
+ if (extensionWs !== ws || !hello)
41204
+ return;
41205
+ 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).`);
41206
+ ownSession = dropOwnSessionAndEphemeral();
41207
+ resetSessionPromise();
41208
+ const mintedKeypair = await generateSessionKeypair();
41209
+ const mintedHello = await buildServerHello({
41210
+ ...ownHelloBase,
41211
+ sessionPub: mintedKeypair.publicKey,
41212
+ answersExtNonce: fromB64(hello.sessionNonce)
41213
+ });
41214
+ if (extensionWs !== ws || extensionHello !== hello || ownSession !== null) {
41215
+ mintedKeypair.privateKey.fill(0);
41216
+ return;
41217
+ }
41218
+ installOwnEphemeral({
41219
+ nonce: fromB64(mintedHello.sessionNonce),
41220
+ pub: mintedKeypair.publicKey,
41221
+ priv: mintedKeypair.privateKey
41222
+ });
41223
+ ws.send(JSON.stringify(mintedHello));
41224
+ };
40851
41225
  wss.on("connection", (ws) => {
40852
41226
  let identified = null;
40853
41227
  let peerMcpId = null;
@@ -41038,6 +41412,7 @@ async function startHost(opts) {
41038
41412
  if (extensionWs !== ws)
41039
41413
  return;
41040
41414
  ownSession = new SessionState(key);
41415
+ ownDecryptFailures = 0;
41041
41416
  ownPendingPairCode = null;
41042
41417
  resolveOwnSession(ownSession);
41043
41418
  } else {
@@ -41059,15 +41434,42 @@ async function startHost(opts) {
41059
41434
  }
41060
41435
  if (claim2 !== "ok")
41061
41436
  return;
41062
- let inner;
41437
+ let result;
41063
41438
  try {
41064
- inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
41439
+ result = await openEncryptedFrameDetailed(session.sessionKey, frame, "e2s");
41065
41440
  } catch (e) {
41066
41441
  session.releaseInboundSeq(frame.seq);
41067
41442
  throw e;
41068
41443
  }
41444
+ if (result.stage === "decrypt-failed") {
41445
+ session.releaseInboundSeq(frame.seq);
41446
+ console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) that failed authentication \u2014 most likely a straggler from a previous session.`);
41447
+ if (session === ownSession) {
41448
+ ownDecryptFailures += 1;
41449
+ if (ownDecryptFailures >= OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE) {
41450
+ ownDecryptFailures = 0;
41451
+ void rehandshakeOwnSession(ws).catch((e) => console.error(`[fetchproxy] ${opts.ownServerName}: re-handshake failed:`, e));
41452
+ }
41453
+ }
41454
+ return;
41455
+ }
41456
+ if (session === ownSession)
41457
+ ownDecryptFailures = 0;
41069
41458
  session.commitInboundSeq(frame.seq);
41070
- ownInnerListeners.forEach((cb) => cb(inner));
41459
+ if (result.stage === "ok") {
41460
+ ownInnerListeners.forEach((cb) => cb(result.inner));
41461
+ } else {
41462
+ console.error("[fetchproxy] host: received a frame that decrypted OK but failed validation:", result.error);
41463
+ const recoveredId = result.recoveredId;
41464
+ if (recoveredId !== void 0) {
41465
+ ownInnerListeners.forEach((cb) => cb({
41466
+ type: "response",
41467
+ id: recoveredId,
41468
+ ok: false,
41469
+ error: `malformed response failed protocol validation: ${String(result.error)}`
41470
+ }));
41471
+ }
41472
+ }
41071
41473
  } else {
41072
41474
  const slot = peers.get(frame.mcpId);
41073
41475
  if (slot)
@@ -41126,6 +41528,7 @@ async function startHost(opts) {
41126
41528
  rejectOwnSession(new Error("extension disconnected before ready"));
41127
41529
  }
41128
41530
  ownSession = dropOwnSessionAndEphemeral();
41531
+ ownDecryptFailures = 0;
41129
41532
  ownPendingPairCode = null;
41130
41533
  resetSessionPromise();
41131
41534
  disconnectListeners.forEach((cb) => cb());
@@ -41140,13 +41543,20 @@ async function startHost(opts) {
41140
41543
  }
41141
41544
  }
41142
41545
  if (identified === "peer" && peerMcpId) {
41143
- if (peers.get(peerMcpId)?.ws === ws)
41546
+ if (peers.get(peerMcpId)?.ws === ws) {
41144
41547
  peers.delete(peerMcpId);
41548
+ if (extensionWs && extensionHello?.accepts?.includes("peer-gone")) {
41549
+ try {
41550
+ extensionWs.send(JSON.stringify({ type: "peer-gone", mcpId: peerMcpId }));
41551
+ } catch {
41552
+ }
41553
+ }
41554
+ }
41145
41555
  }
41146
41556
  });
41147
41557
  });
41148
41558
  return {
41149
- close: () => new Promise((resolve3) => {
41559
+ close: () => new Promise((resolve6) => {
41150
41560
  for (const client2 of wss.clients) {
41151
41561
  try {
41152
41562
  client2.terminate();
@@ -41154,7 +41564,7 @@ async function startHost(opts) {
41154
41564
  }
41155
41565
  }
41156
41566
  wss.close(() => {
41157
- opts.httpServer.close(() => resolve3());
41567
+ opts.httpServer.close(() => resolve6());
41158
41568
  });
41159
41569
  }),
41160
41570
  sendOwnInner: async (inner) => {
@@ -41184,26 +41594,42 @@ async function startHost(opts) {
41184
41594
  }
41185
41595
 
41186
41596
  // node_modules/@fetchproxy/server/dist/peer.js
41597
+ var PEER_DIAL_TIMEOUT_MS = 5e3;
41187
41598
  var enc5 = new TextEncoder();
41599
+ function notSentError() {
41600
+ 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.");
41601
+ }
41188
41602
  async function startPeer(opts) {
41189
41603
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
41190
41604
  maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
41191
41605
  });
41192
- await new Promise((resolve3, reject) => {
41606
+ const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
41607
+ await new Promise((resolve6, reject) => {
41608
+ let timer;
41193
41609
  const onOpen = () => {
41194
41610
  cleanup();
41195
- resolve3();
41611
+ resolve6();
41196
41612
  };
41197
41613
  const onError = (e) => {
41198
41614
  cleanup();
41199
41615
  reject(e);
41200
41616
  };
41201
41617
  const cleanup = () => {
41618
+ if (timer !== void 0)
41619
+ clearTimeout(timer);
41202
41620
  ws.off("open", onOpen);
41203
41621
  ws.off("error", onError);
41204
41622
  };
41205
41623
  ws.once("open", onOpen);
41206
41624
  ws.once("error", onError);
41625
+ if (dialTimeoutMs > 0) {
41626
+ timer = setTimeout(() => {
41627
+ cleanup();
41628
+ ws.on("error", () => void 0);
41629
+ ws.terminate();
41630
+ 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.`));
41631
+ }, dialTimeoutMs);
41632
+ }
41207
41633
  });
41208
41634
  ws.on("error", (e) => {
41209
41635
  console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
@@ -41222,6 +41648,7 @@ async function startPeer(opts) {
41222
41648
  captureHeaders: opts.captureHeaders,
41223
41649
  indexedDbScopes: opts.indexedDbScopes,
41224
41650
  domSelectors: opts.domSelectors,
41651
+ domListSelectors: opts.domListSelectors,
41225
41652
  localStoragePointers: opts.localStoragePointers,
41226
41653
  sessionStoragePointers: opts.sessionStoragePointers,
41227
41654
  graphqlOps: opts.graphqlOps,
@@ -41245,6 +41672,7 @@ async function startPeer(opts) {
41245
41672
  const renegotiateListeners = [];
41246
41673
  const pendingPairListeners = [];
41247
41674
  const closeListeners = [];
41675
+ const extensionDisconnectListeners = [];
41248
41676
  let session = null;
41249
41677
  let sessionEphemeral = null;
41250
41678
  function dropSessionEphemeral() {
@@ -41264,10 +41692,24 @@ async function startPeer(opts) {
41264
41692
  let warnedUnverifiablePairCode = false;
41265
41693
  let resolveFirstReady;
41266
41694
  let rejectFirstReady;
41267
- const sessionPromise = new Promise((resolve3, reject) => {
41268
- resolveFirstReady = resolve3;
41269
- rejectFirstReady = reject;
41270
- });
41695
+ let sessionPromise;
41696
+ let sessionPromiseSettled = false;
41697
+ function resetSessionPromise() {
41698
+ sessionPromiseSettled = false;
41699
+ sessionPromise = new Promise((resolve6, reject) => {
41700
+ resolveFirstReady = (st) => {
41701
+ sessionPromiseSettled = true;
41702
+ resolve6(st);
41703
+ };
41704
+ rejectFirstReady = (e) => {
41705
+ sessionPromiseSettled = true;
41706
+ reject(e);
41707
+ };
41708
+ });
41709
+ sessionPromise.catch(() => {
41710
+ });
41711
+ }
41712
+ resetSessionPromise();
41271
41713
  let extensionHello = null;
41272
41714
  let extensionGone = false;
41273
41715
  let cachedPin = void 0;
@@ -41364,6 +41806,12 @@ async function startPeer(opts) {
41364
41806
  extensionGone = true;
41365
41807
  dropSessionEphemeral();
41366
41808
  pendingPairCode = null;
41809
+ if (session !== null) {
41810
+ session = null;
41811
+ if (sessionPromiseSettled)
41812
+ resetSessionPromise();
41813
+ }
41814
+ extensionDisconnectListeners.forEach((cb) => cb());
41367
41815
  return;
41368
41816
  }
41369
41817
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
@@ -41480,19 +41928,30 @@ async function startPeer(opts) {
41480
41928
  }
41481
41929
  closeListeners.forEach((cb) => cb());
41482
41930
  });
41483
- sessionPromise.catch(() => {
41484
- });
41485
41931
  const handle = {
41486
41932
  ws,
41487
- session: sessionPromise,
41933
+ // A getter: `extension-disconnected` replaces the promise (B-BUG-5).
41934
+ get session() {
41935
+ return sessionPromise;
41936
+ },
41488
41937
  sendInner: async (inner) => {
41489
- await awaitSessionReady(sessionPromise, {
41490
- mcpId: opts.mcpId,
41491
- pendingPairCode: () => pendingPairCode
41492
- });
41938
+ try {
41939
+ await awaitSessionReady(sessionPromise, {
41940
+ mcpId: opts.mcpId,
41941
+ pendingPairCode: () => pendingPairCode
41942
+ });
41943
+ } catch (e) {
41944
+ if (ws.readyState !== import_websocket.default.OPEN)
41945
+ throw notSentError();
41946
+ throw e;
41947
+ }
41493
41948
  const s = session;
41949
+ if (s === null)
41950
+ throw new Error("peer: extension disconnected");
41494
41951
  const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
41495
41952
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
41953
+ if (ws.readyState !== import_websocket.default.OPEN)
41954
+ throw notSentError();
41496
41955
  ws.send(JSON.stringify(sealed));
41497
41956
  },
41498
41957
  onInner: (cb) => {
@@ -41510,6 +41969,9 @@ async function startPeer(opts) {
41510
41969
  onClose: (cb) => {
41511
41970
  closeListeners.push(cb);
41512
41971
  },
41972
+ onExtensionDisconnect: (cb) => {
41973
+ extensionDisconnectListeners.push(cb);
41974
+ },
41513
41975
  close: () => ws.close()
41514
41976
  };
41515
41977
  return handle;
@@ -41517,15 +41979,15 @@ async function startPeer(opts) {
41517
41979
 
41518
41980
  // node_modules/@fetchproxy/server/dist/error-kind.js
41519
41981
  function classifyFetchError(error62) {
41982
+ if (/^(in-page )?fetch threw:/.test(error62)) {
41983
+ return "tab_fetch_failed";
41984
+ }
41520
41985
  if (/Could not establish connection/i.test(error62) || /Receiving end does not exist/i.test(error62)) {
41521
41986
  return "content_script_unreachable";
41522
41987
  }
41523
41988
  if (/^tab fetch failed:/.test(error62)) {
41524
41989
  return "tab_fetch_failed";
41525
41990
  }
41526
- if (/^(in-page )?fetch threw:/.test(error62)) {
41527
- return "tab_fetch_failed";
41528
- }
41529
41991
  if (/^no tab matching /.test(error62)) {
41530
41992
  return "no_tab";
41531
41993
  }
@@ -41717,6 +42179,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
41717
42179
  * the message says which number that was and where it came from.
41718
42180
  */
41719
42181
  requestedTimeoutMs;
42182
+ /**
42183
+ * B-BUG-1: whether re-sending this request is safe. A timeout only means
42184
+ * the reply did not arrive in time — the extension may already have run the
42185
+ * request in the tab — so this is `false` for a non-idempotent `fetch`
42186
+ * (anything but GET/HEAD/OPTIONS) the caller did not mark `retryOnTimeout`,
42187
+ * for `download` and `writeCookies`, and for `graphqlQuery` unless the
42188
+ * caller marked it `retryOnTimeout` (a declared operation may be a
42189
+ * mutation). `retryOnceOnTimeout` honours it. Defaults to `true` (the read
42190
+ * verbs).
42191
+ */
42192
+ retrySafe;
41720
42193
  constructor(args) {
41721
42194
  const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
41722
42195
  const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
@@ -41729,6 +42202,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
41729
42202
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
41730
42203
  this.retryAttempted = args.retryAttempted ?? false;
41731
42204
  this.requestedTimeoutMs = args.requestedTimeoutMs ?? null;
42205
+ this.retrySafe = args.retrySafe ?? true;
41732
42206
  }
41733
42207
  };
41734
42208
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -41768,6 +42242,32 @@ function assertUrlInDomains(field, url2, domains) {
41768
42242
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
41769
42243
  }
41770
42244
  var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
42245
+ function parseJsonBody(response) {
42246
+ if (response.status === 204 || response.body.trim() === "")
42247
+ return null;
42248
+ return JSON.parse(response.body);
42249
+ }
42250
+ var TIMEOUT_RETRY_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
42251
+ function isRetrySafeOnTimeout(method, retryOnTimeout) {
42252
+ if (retryOnTimeout !== void 0)
42253
+ return retryOnTimeout;
42254
+ return TIMEOUT_RETRY_SAFE_METHODS.has(method.toUpperCase());
42255
+ }
42256
+ function isResendableRequest(inner) {
42257
+ switch (inner.op) {
42258
+ case "fetch":
42259
+ return isRetrySafeOnTimeout(inner.init.method);
42260
+ case "write_cookies":
42261
+ case "download":
42262
+ // A declared GraphQL operation may be a mutation; the server holds only
42263
+ // its name, so it cannot tell. `graphqlQuery({ retryOnTimeout: true })`
42264
+ // passes `resendable` explicitly for a known query.
42265
+ case "graphql_query":
42266
+ return false;
42267
+ default:
42268
+ return true;
42269
+ }
42270
+ }
41771
42271
  var FetchproxyServer = class {
41772
42272
  /**
41773
42273
  * Bridge role. `null` until the first verb call (or an explicit
@@ -41816,12 +42316,22 @@ var FetchproxyServer = class {
41816
42316
  pendingRedirect = /* @__PURE__ */ new Map();
41817
42317
  // 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
41818
42318
  pendingIdb = /* @__PURE__ */ new Map();
42319
+ // 3.1.0+: read_dom_list awaiters resolve an array of field-value rows —
42320
+ // a different shape from `pendingStorage`'s single map, so its own map.
42321
+ pendingDomList = /* @__PURE__ */ new Map();
41819
42322
  // download awaiters resolve the saved-file metadata (path + size + mime).
41820
42323
  pendingDownload = /* @__PURE__ */ new Map();
41821
42324
  // 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
41822
42325
  // shape is operation-specific, so the awaiter resolves `unknown` and the
41823
42326
  // caller narrows.
41824
42327
  pendingGraphql = /* @__PURE__ */ new Map();
42328
+ /**
42329
+ * B-BUG-6: the frames of in-flight requests that are safe to send again
42330
+ * if the host that relayed them exits before replying (see
42331
+ * `recoverFromHostLoss`). Keyed by request id; an entry outlives its
42332
+ * request only until the next prune or response.
42333
+ */
42334
+ resendable = /* @__PURE__ */ new Map();
41825
42335
  mcpId = null;
41826
42336
  identity = null;
41827
42337
  // 0.5.3+: in-flight role-election / handle-start promise. Set the
@@ -41867,7 +42377,7 @@ var FetchproxyServer = class {
41867
42377
  }
41868
42378
  for (const c of opts.capabilities) {
41869
42379
  if (!KNOWN_CAPABILITIES.has(c)) {
41870
- throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ["fetch", "read_cookies"]`);
42380
+ throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ${JSON.stringify([...KNOWN_CAPABILITIES])}`);
41871
42381
  }
41872
42382
  }
41873
42383
  capabilities = [...opts.capabilities];
@@ -41914,6 +42424,16 @@ var FetchproxyServer = class {
41914
42424
  selector: d.selector,
41915
42425
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
41916
42426
  })),
42427
+ domListSelectors: (opts.domListSelectors ?? []).map((d) => ({
42428
+ name: d.name,
42429
+ itemSelector: d.itemSelector,
42430
+ fields: d.fields.map((f) => ({
42431
+ name: f.name,
42432
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
42433
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
42434
+ })),
42435
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
42436
+ })),
41917
42437
  graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
41918
42438
  name: d.name,
41919
42439
  operationName: d.operationName
@@ -42043,6 +42563,7 @@ var FetchproxyServer = class {
42043
42563
  ownLocalStoragePointers: this.opts.localStoragePointers,
42044
42564
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
42045
42565
  ownDomSelectors: this.opts.domSelectors,
42566
+ ownDomListSelectors: this.opts.domListSelectors,
42046
42567
  ownGraphqlOps: this.opts.graphqlOps,
42047
42568
  onPairCode: this.opts.onPairCode,
42048
42569
  extensionTrust: this.extensionTrust()
@@ -42074,6 +42595,7 @@ var FetchproxyServer = class {
42074
42595
  localStoragePointers: this.opts.localStoragePointers,
42075
42596
  sessionStoragePointers: this.opts.sessionStoragePointers,
42076
42597
  domSelectors: this.opts.domSelectors,
42598
+ domListSelectors: this.opts.domListSelectors,
42077
42599
  graphqlOps: this.opts.graphqlOps,
42078
42600
  extensionTrust: this.extensionTrust(),
42079
42601
  requireExtensionIdentity: this.opts.requireExtensionIdentity
@@ -42083,6 +42605,10 @@ var FetchproxyServer = class {
42083
42605
  this.stopKeepalive();
42084
42606
  this.rejectAllPending();
42085
42607
  });
42608
+ this.peerHandle.onExtensionDisconnect(() => {
42609
+ this.stopKeepalive();
42610
+ this.rejectAllPending();
42611
+ });
42086
42612
  this.peerHandle.onPendingPair((code) => {
42087
42613
  this.rejectAllPending(this.pairingErrorMessage(code));
42088
42614
  });
@@ -42094,9 +42620,9 @@ var FetchproxyServer = class {
42094
42620
  if (this.closing || this.peerHandle === null)
42095
42621
  return;
42096
42622
  this.stopKeepalive();
42097
- this.rejectAllPending();
42098
42623
  this.peerHandle = null;
42099
42624
  this.role = null;
42625
+ void this.recoverFromHostLoss();
42100
42626
  });
42101
42627
  }
42102
42628
  }
@@ -42117,7 +42643,7 @@ var FetchproxyServer = class {
42117
42643
  * only when the bridge itself failed (no signed-in tab, extension
42118
42644
  * offline, etc.).
42119
42645
  */
42120
- async fetch(init) {
42646
+ async fetch(init, fetchOpts = {}) {
42121
42647
  await this.ensureConnected();
42122
42648
  const pendingCode = this.currentPendingPairCode();
42123
42649
  if (pendingCode !== null) {
@@ -42129,16 +42655,17 @@ var FetchproxyServer = class {
42129
42655
  retryAttempted: false
42130
42656
  };
42131
42657
  }
42132
- const first = await this._fetchOnceWithTimeout(init);
42658
+ const first = await this._fetchOnceWithTimeout(init, fetchOpts);
42133
42659
  const reviveMs = this.opts.bridgeReviveDelayMs;
42134
42660
  const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
42135
42661
  if (isColdStartSymptom) {
42136
42662
  this.lastEvictionDetectedAt = Date.now();
42137
42663
  }
42138
- if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
42664
+ const retryable = isColdStartSymptom && (first.kind === "content_script_unreachable" || isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout));
42665
+ if (retryable && reviveMs !== void 0 && reviveMs > 0) {
42139
42666
  this.lazyReviveAttempts += 1;
42140
42667
  await new Promise((r) => setTimeout(r, reviveMs));
42141
- const second = await this._fetchOnceWithTimeout(init);
42668
+ const second = await this._fetchOnceWithTimeout(init, fetchOpts);
42142
42669
  if (second.ok) {
42143
42670
  this.lazyReviveSuccesses += 1;
42144
42671
  this.recordSuccess();
@@ -42287,13 +42814,16 @@ var FetchproxyServer = class {
42287
42814
  * one of the op maps — request ids are unique) so it doesn't leak until the
42288
42815
  * server closes, then rethrow.
42289
42816
  */
42290
- async sendInnerFrame(inner) {
42817
+ async sendInnerFrame(inner, opts = {}) {
42291
42818
  try {
42292
42819
  if (this.hostHandle) {
42293
42820
  await this.hostHandle.sendOwnInner(inner);
42294
42821
  } else if (this.peerHandle) {
42295
42822
  await this.peerHandle.sendInner(inner);
42296
42823
  }
42824
+ if (inner.type === "request" && (opts.resendable ?? isResendableRequest(inner))) {
42825
+ this.noteResendable(inner);
42826
+ }
42297
42827
  } catch (err) {
42298
42828
  if ("id" in inner && typeof inner.id === "number") {
42299
42829
  const { id } = inner;
@@ -42305,6 +42835,7 @@ var FetchproxyServer = class {
42305
42835
  this.pendingRedirect.delete(id);
42306
42836
  this.pendingDownload.delete(id);
42307
42837
  this.pendingIdb.delete(id);
42838
+ this.pendingDomList.delete(id);
42308
42839
  this.pendingGraphql.delete(id);
42309
42840
  }
42310
42841
  throw err;
@@ -42315,13 +42846,25 @@ var FetchproxyServer = class {
42315
42846
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
42316
42847
  * the throwing surface is the convenience methods.
42317
42848
  */
42318
- async _fetchOnceWithTimeout(init) {
42849
+ async _fetchOnceWithTimeout(init, fetchOpts = {}) {
42319
42850
  const id = this.nextRequestId++;
42320
42851
  const inner = { type: "request", id, op: "fetch", init };
42321
- const pending = this.guardPending(new Promise((resolve3) => {
42322
- this.pending.set(id, resolve3);
42852
+ const pending = this.guardPending(new Promise((resolve6) => {
42853
+ this.pending.set(id, resolve6);
42323
42854
  }));
42324
- await this.sendInnerFrame(inner);
42855
+ try {
42856
+ await this.sendInnerFrame(inner, {
42857
+ resendable: isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout)
42858
+ });
42859
+ } catch (err) {
42860
+ return {
42861
+ ok: false,
42862
+ error: err instanceof Error ? err.message : String(err),
42863
+ kind: "session_not_ready",
42864
+ retryAttempted: false,
42865
+ cause: err
42866
+ };
42867
+ }
42325
42868
  const timeoutMs = this.opts.fetchTimeoutMs;
42326
42869
  if (timeoutMs === void 0 || timeoutMs <= 0)
42327
42870
  return pending;
@@ -42330,12 +42873,12 @@ var FetchproxyServer = class {
42330
42873
  try {
42331
42874
  return await Promise.race([
42332
42875
  pending,
42333
- new Promise((resolve3) => {
42876
+ new Promise((resolve6) => {
42334
42877
  timer = setTimeout(() => {
42335
42878
  this.pending.delete(id);
42336
42879
  const elapsedMs = Date.now() - start;
42337
42880
  const error62 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
42338
- resolve3({
42881
+ resolve6({
42339
42882
  ok: false,
42340
42883
  error: error62,
42341
42884
  kind: "timeout",
@@ -42395,7 +42938,7 @@ var FetchproxyServer = class {
42395
42938
  });
42396
42939
  return pending;
42397
42940
  }
42398
- async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
42941
+ async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs, retrySafe = true) {
42399
42942
  const transportMs = this.opts.fetchTimeoutMs;
42400
42943
  if (transportMs === void 0 || transportMs <= 0)
42401
42944
  return pending;
@@ -42416,6 +42959,7 @@ var FetchproxyServer = class {
42416
42959
  port: this.opts.port,
42417
42960
  elapsedMs: Date.now() - start,
42418
42961
  retryAttempted: false,
42962
+ retrySafe,
42419
42963
  graceMs,
42420
42964
  ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
42421
42965
  }));
@@ -42433,7 +42977,7 @@ var FetchproxyServer = class {
42433
42977
  * logic re-implemented inline) `captureRequestHeader()` agree on what
42434
42978
  * to throw.
42435
42979
  */
42436
- _typedErrorFor(result, url2, op, retryAttempted) {
42980
+ _typedErrorFor(result, url2, op, retryAttempted, retrySafe = true) {
42437
42981
  if (result.kind === "timeout") {
42438
42982
  return new FetchproxyTimeoutError({
42439
42983
  url: url2,
@@ -42441,7 +42985,8 @@ var FetchproxyServer = class {
42441
42985
  role: this.role,
42442
42986
  port: this.opts.port,
42443
42987
  elapsedMs: result.elapsedMs,
42444
- retryAttempted
42988
+ retryAttempted,
42989
+ retrySafe
42445
42990
  });
42446
42991
  }
42447
42992
  if (result.kind === "content_script_unreachable") {
@@ -42477,9 +43022,9 @@ var FetchproxyServer = class {
42477
43022
  if (opts.subdomain !== void 0)
42478
43023
  assertSubdomainLabel(opts.subdomain);
42479
43024
  const baseDomain = this.resolveBaseDomain(opts.domain);
42480
- const isAbsolute4 = path.startsWith("http://") || path.startsWith("https://");
43025
+ const isAbsolute6 = path.startsWith("http://") || path.startsWith("https://");
42481
43026
  let host;
42482
- if (isAbsolute4) {
43027
+ if (isAbsolute6) {
42483
43028
  try {
42484
43029
  host = new URL(path).host;
42485
43030
  } catch {
@@ -42488,7 +43033,7 @@ var FetchproxyServer = class {
42488
43033
  } else {
42489
43034
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
42490
43035
  }
42491
- const url2 = isAbsolute4 ? path : `https://${host}${path}`;
43036
+ const url2 = isAbsolute6 ? path : `https://${host}${path}`;
42492
43037
  assertUrlInDomains("request url", url2, this.opts.domains);
42493
43038
  let tabUrl = `https://${host}/`;
42494
43039
  if (opts.viaTab !== void 0) {
@@ -42515,9 +43060,12 @@ var FetchproxyServer = class {
42515
43060
  // every version before this option existed.
42516
43061
  ...opts.credentials === "omit" ? { credentials: "omit" } : {}
42517
43062
  };
42518
- const result = await this.fetch(init);
43063
+ const result = await this.fetch(init, opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {});
42519
43064
  if (!result.ok) {
42520
- throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
43065
+ if (result.kind === "session_not_ready" && result.cause instanceof Error) {
43066
+ throw result.cause;
43067
+ }
43068
+ throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, opts.retryOnTimeout));
42521
43069
  }
42522
43070
  const response = {
42523
43071
  status: result.status,
@@ -42557,17 +43105,21 @@ var FetchproxyServer = class {
42557
43105
  * GET a path and parse the response body as JSON. Throws
42558
43106
  * `FetchproxyHttpError` if the status is outside the default 2xx
42559
43107
  * happy-path set (`[200, 201, 202, 204]`); pass a custom
42560
- * `expectStatus` to override.
43108
+ * `expectStatus` to override. A 204 or an empty/whitespace body
43109
+ * resolves to `null` (the declared `T` does not say so, to keep
43110
+ * existing call sites compiling — include `| null` in `T` if the
43111
+ * endpoint can answer empty).
42561
43112
  */
42562
43113
  async getJson(path, opts = {}) {
42563
43114
  const response = await this.get(path, this.applyJsonDefaults(opts));
42564
- return JSON.parse(response.body);
43115
+ return parseJsonBody(response);
42565
43116
  }
42566
43117
  /**
42567
43118
  * POST a JSON body and parse the response body as JSON. The body is
42568
43119
  * `JSON.stringify`'d; `Content-Type: application/json` is set unless
42569
43120
  * the caller already provided one. Defaults `expectStatus` to the 2xx
42570
- * happy-path set.
43121
+ * happy-path set. A 204 or an empty/whitespace body resolves to `null`
43122
+ * rather than throwing after the write already happened (see `getJson`).
42571
43123
  */
42572
43124
  async postJson(path, body, opts = {}) {
42573
43125
  const headers = { ...opts.headers ?? {} };
@@ -42579,7 +43131,7 @@ var FetchproxyServer = class {
42579
43131
  headers,
42580
43132
  body: body === void 0 ? void 0 : JSON.stringify(body)
42581
43133
  });
42582
- return JSON.parse(response.body);
43134
+ return parseJsonBody(response);
42583
43135
  }
42584
43136
  /**
42585
43137
  * GET a path and return the response body as a string. Throws
@@ -42600,7 +43152,8 @@ var FetchproxyServer = class {
42600
43152
  * - adds `Content-Type: application/json` only for a non-GET request
42601
43153
  * that carries a `body` (and only if the caller didn't set one);
42602
43154
  * - `JSON.stringify`s the body (GET / no-body sends nothing);
42603
- * - treats a `204` or an empty body as `data: null` (no parse);
43155
+ * - treats a `204`, an empty body, or a whitespace-only body as
43156
+ * `data: null` (no parse), the same rule as getJson/postJson;
42604
43157
  * - otherwise `JSON.parse`s the body.
42605
43158
  *
42606
43159
  * Scope is serialization + header defaults + 204-handling +
@@ -42614,6 +43167,11 @@ var FetchproxyServer = class {
42614
43167
  * Bridge-level failures (no signed-in tab, SW down, timeout) still
42615
43168
  * throw the typed errors via `request()`, exactly like the verb
42616
43169
  * helpers — only successful round-trips (any HTTP status) return.
43170
+ *
43171
+ * `retryOnTimeout` is `RequestOpts.retryOnTimeout`: a timed-out POST is
43172
+ * NOT re-sent unless the caller says it is safe to repeat. A read-only POST
43173
+ * (a search, a GraphQL query) should pass `true` to keep the cold-start
43174
+ * timeout retry (#90).
42617
43175
  */
42618
43176
  async requestJson(method, path, opts = {}) {
42619
43177
  const isGet = method.toUpperCase() === "GET";
@@ -42627,7 +43185,8 @@ var FetchproxyServer = class {
42627
43185
  headers,
42628
43186
  body: sendBody ? JSON.stringify(opts.body) : void 0,
42629
43187
  ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
42630
- ...opts.domain !== void 0 ? { domain: opts.domain } : {}
43188
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {},
43189
+ ...opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {}
42631
43190
  });
42632
43191
  const result = {
42633
43192
  ok: true,
@@ -42635,12 +43194,9 @@ var FetchproxyServer = class {
42635
43194
  url: response.url,
42636
43195
  body: response.body
42637
43196
  };
42638
- if (response.status === 204 || response.body === "") {
42639
- return { data: null, result };
42640
- }
42641
43197
  let data;
42642
43198
  try {
42643
- data = JSON.parse(response.body);
43199
+ data = parseJsonBody(response);
42644
43200
  } catch (e) {
42645
43201
  throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
42646
43202
  }
@@ -42697,18 +43253,19 @@ var FetchproxyServer = class {
42697
43253
  };
42698
43254
  }
42699
43255
  /**
42700
- * Snapshot the user's non-HttpOnly cookies for the chosen domain.
43256
+ * Read the user's cookies for the chosen domain.
42701
43257
  *
42702
43258
  * Requires `'read_cookies'` in `FetchproxyServerOpts.capabilities`.
42703
43259
  * Throws a developer-facing `Error` at the call site if the MCP did
42704
43260
  * not declare the capability — this is a programming mistake, not a
42705
43261
  * runtime condition.
42706
43262
  *
42707
- * The returned string is the raw `document.cookie` value (semicolon-
42708
- * separated `k=v` pairs). HttpOnly cookies are NOT visible to page JS
42709
- * and are therefore not included; the underlying threat model assumes
42710
- * the cookies that matter for the auth bootstrap (session tokens, csrf
42711
- * cookies that the page itself reads) are non-HttpOnly.
43263
+ * The returned string is semicolon-separated `k=v` pairs. With `keys`
43264
+ * (each in the declared `cookieKeys`), the extension reads each name via
43265
+ * `chrome.cookies.get`, which INCLUDES HttpOnly cookies — the login
43266
+ * session cookie among them; that is what cookie-session MCPs use it
43267
+ * for. Without `keys` (legacy), it is the tab's `document.cookie`, which
43268
+ * omits HttpOnly cookies. See docs/SECURITY.md §T-cookie-exfil.
42712
43269
  *
42713
43270
  * Throws `FetchproxyProtocolError` if the bridge could not deliver
42714
43271
  * the request (no signed-in tab, extension offline, etc.).
@@ -42741,8 +43298,8 @@ var FetchproxyServer = class {
42741
43298
  const tabUrl = `https://${host}/`;
42742
43299
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
42743
43300
  }
42744
- const pending = this.guardPending(new Promise((resolve3) => {
42745
- this.pendingReadCookies.set(id, resolve3);
43301
+ const pending = this.guardPending(new Promise((resolve6) => {
43302
+ this.pendingReadCookies.set(id, resolve6);
42746
43303
  }));
42747
43304
  await this.sendInnerFrame(inner);
42748
43305
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -42800,11 +43357,19 @@ var FetchproxyServer = class {
42800
43357
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
42801
43358
  }
42802
43359
  };
42803
- const pending = this.guardPending(new Promise((resolve3, reject) => {
42804
- this.pendingWriteCookies.set(id, { resolve: resolve3, reject });
43360
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43361
+ this.pendingWriteCookies.set(id, { resolve: resolve6, reject });
42805
43362
  }));
42806
43363
  await this.sendInnerFrame(inner);
42807
- return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
43364
+ return this._withVerbTimeout(
43365
+ pending,
43366
+ this.pendingWriteCookies,
43367
+ id,
43368
+ `https://${host}`,
43369
+ void 0,
43370
+ // A write: never re-sent after an ambiguous timeout.
43371
+ false
43372
+ );
42808
43373
  }
42809
43374
  /**
42810
43375
  * 0.3.0+: read declared localStorage keys from the user's signed-in
@@ -42863,8 +43428,8 @@ var FetchproxyServer = class {
42863
43428
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
42864
43429
  }
42865
43430
  };
42866
- const pending = this.guardPending(new Promise((resolve3, reject) => {
42867
- this.pendingStorage.set(id, { resolve: resolve3, reject });
43431
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43432
+ this.pendingStorage.set(id, { resolve: resolve6, reject });
42868
43433
  }));
42869
43434
  await this.sendInnerFrame(inner);
42870
43435
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -42907,53 +43472,7 @@ var FetchproxyServer = class {
42907
43472
  throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
42908
43473
  }
42909
43474
  const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
42910
- try {
42911
- const result = await this._captureRequestHeaderOnce(callOpts);
42912
- this.recordSuccess();
42913
- return result;
42914
- } catch (err) {
42915
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
42916
- if (!swDown) {
42917
- this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
42918
- throw err;
42919
- }
42920
- this.lastEvictionDetectedAt = Date.now();
42921
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
42922
- if (reviveMs > 0) {
42923
- this.lazyReviveAttempts += 1;
42924
- await new Promise((r) => setTimeout(r, reviveMs));
42925
- try {
42926
- const result = await this._captureRequestHeaderOnce(callOpts);
42927
- this.lazyReviveSuccesses += 1;
42928
- this.recordSuccess();
42929
- return result;
42930
- } catch (retryErr) {
42931
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
42932
- if (!stillDown) {
42933
- this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
42934
- throw retryErr;
42935
- }
42936
- this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
42937
- throw new FetchproxyBridgeDownError({
42938
- originalError: retryErr.message,
42939
- retryAttempted: true,
42940
- op: "capture_request_header",
42941
- url: `https://${resolved.host}${resolved.path ?? "/*"}`,
42942
- role: this.role,
42943
- port: this.opts.port
42944
- });
42945
- }
42946
- }
42947
- this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
42948
- throw new FetchproxyBridgeDownError({
42949
- originalError: err.message,
42950
- retryAttempted: false,
42951
- op: "capture_request_header",
42952
- url: `https://${resolved.host}${resolved.path ?? "/*"}`,
42953
- role: this.role,
42954
- port: this.opts.port
42955
- });
42956
- }
43475
+ return this.withLazyRevive("capture_request_header", `https://${resolved.host}${resolved.path ?? "/*"}`, () => this._captureRequestHeaderOnce(callOpts));
42957
43476
  }
42958
43477
  async _captureRequestHeaderOnce(opts) {
42959
43478
  const id = this.nextRequestId++;
@@ -42968,8 +43487,8 @@ var FetchproxyServer = class {
42968
43487
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
42969
43488
  }
42970
43489
  };
42971
- const pending = this.guardPending(new Promise((resolve3, reject) => {
42972
- this.pendingCapture.set(id, { resolve: resolve3, reject });
43490
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43491
+ this.pendingCapture.set(id, { resolve: resolve6, reject });
42973
43492
  }));
42974
43493
  await this.sendInnerFrame(inner);
42975
43494
  return this._withVerbTimeout(
@@ -43002,53 +43521,7 @@ var FetchproxyServer = class {
43002
43521
  }
43003
43522
  await this.ensureConnected();
43004
43523
  this.throwIfPendingPair();
43005
- try {
43006
- const result = await this._captureRedirectOnce(opts);
43007
- this.recordSuccess();
43008
- return result;
43009
- } catch (err) {
43010
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
43011
- if (!swDown) {
43012
- this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
43013
- throw err;
43014
- }
43015
- this.lastEvictionDetectedAt = Date.now();
43016
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
43017
- if (reviveMs > 0) {
43018
- this.lazyReviveAttempts += 1;
43019
- await new Promise((r) => setTimeout(r, reviveMs));
43020
- try {
43021
- const result = await this._captureRedirectOnce(opts);
43022
- this.lazyReviveSuccesses += 1;
43023
- this.recordSuccess();
43024
- return result;
43025
- } catch (retryErr) {
43026
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
43027
- if (!stillDown) {
43028
- this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
43029
- throw retryErr;
43030
- }
43031
- this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
43032
- throw new FetchproxyBridgeDownError({
43033
- originalError: retryErr.message,
43034
- retryAttempted: true,
43035
- op: "capture_redirect",
43036
- url: `https://${opts.host}${opts.path ?? "/*"}`,
43037
- role: this.role,
43038
- port: this.opts.port
43039
- });
43040
- }
43041
- }
43042
- this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
43043
- throw new FetchproxyBridgeDownError({
43044
- originalError: err.message,
43045
- retryAttempted: false,
43046
- op: "capture_redirect",
43047
- url: `https://${opts.host}${opts.path ?? "/*"}`,
43048
- role: this.role,
43049
- port: this.opts.port
43050
- });
43051
- }
43524
+ return this.withLazyRevive("capture_redirect", `https://${opts.host}${opts.path ?? "/*"}`, () => this._captureRedirectOnce(opts));
43052
43525
  }
43053
43526
  async _captureRedirectOnce(opts) {
43054
43527
  const id = this.nextRequestId++;
@@ -43062,8 +43535,8 @@ var FetchproxyServer = class {
43062
43535
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
43063
43536
  }
43064
43537
  };
43065
- const pending = this.guardPending(new Promise((resolve3, reject) => {
43066
- this.pendingRedirect.set(id, { resolve: resolve3, reject });
43538
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43539
+ this.pendingRedirect.set(id, { resolve: resolve6, reject });
43067
43540
  }));
43068
43541
  await this.sendInnerFrame(inner);
43069
43542
  return this._withVerbTimeout(
@@ -43093,52 +43566,57 @@ var FetchproxyServer = class {
43093
43566
  assertUrlInDomains("download url", opts.url, this.opts.domains);
43094
43567
  await this.ensureConnected();
43095
43568
  this.throwIfPendingPair();
43569
+ return this.withLazyRevive("download", opts.url, () => this._downloadOnce(opts));
43570
+ }
43571
+ /**
43572
+ * B-QUAL-1: the lazy-revive policy for the throwing verbs
43573
+ * (`capture_request_header`, `capture_redirect`, `download`), in one place.
43574
+ * Runs `once`; on the SW-eviction symptom (`content_script_unreachable` —
43575
+ * the request provably never reached a tab, so re-sending is safe) stamps
43576
+ * the eviction counter, waits `bridgeReviveDelayMs` and retries once, then
43577
+ * surfaces a `FetchproxyBridgeDownError`. Every other error is recorded and
43578
+ * rethrown untouched. `fetch()` keeps its own envelope-shaped variant.
43579
+ */
43580
+ async withLazyRevive(op, url2, once) {
43581
+ const isSwDown = (e) => e instanceof FetchproxyProtocolError && classifyFetchError(e.message) === "content_script_unreachable";
43582
+ const bridgeDown = (e, retryAttempted) => {
43583
+ this.recordFailure(`${op} bridge-down: ${e.message}`);
43584
+ return new FetchproxyBridgeDownError({
43585
+ originalError: e.message,
43586
+ retryAttempted,
43587
+ op,
43588
+ url: url2,
43589
+ role: this.role,
43590
+ port: this.opts.port
43591
+ });
43592
+ };
43096
43593
  try {
43097
- const result = await this._downloadOnce(opts);
43594
+ const result = await once();
43098
43595
  this.recordSuccess();
43099
43596
  return result;
43100
43597
  } catch (err) {
43101
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
43102
- if (!swDown) {
43103
- this.recordFailure(`download: ${err.message ?? String(err)}`);
43598
+ if (!isSwDown(err)) {
43599
+ this.recordFailure(`${op}: ${err.message ?? String(err)}`);
43104
43600
  throw err;
43105
43601
  }
43106
43602
  this.lastEvictionDetectedAt = Date.now();
43107
43603
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
43108
- if (reviveMs > 0) {
43109
- this.lazyReviveAttempts += 1;
43110
- await new Promise((r) => setTimeout(r, reviveMs));
43111
- try {
43112
- const result = await this._downloadOnce(opts);
43113
- this.lazyReviveSuccesses += 1;
43114
- this.recordSuccess();
43115
- return result;
43116
- } catch (retryErr) {
43117
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
43118
- if (!stillDown) {
43119
- this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
43120
- throw retryErr;
43121
- }
43122
- this.recordFailure(`download bridge-down: ${retryErr.message}`);
43123
- throw new FetchproxyBridgeDownError({
43124
- originalError: retryErr.message,
43125
- retryAttempted: true,
43126
- op: "download",
43127
- url: opts.url,
43128
- role: this.role,
43129
- port: this.opts.port
43130
- });
43604
+ if (reviveMs <= 0)
43605
+ throw bridgeDown(err, false);
43606
+ this.lazyReviveAttempts += 1;
43607
+ await new Promise((r) => setTimeout(r, reviveMs));
43608
+ try {
43609
+ const result = await once();
43610
+ this.lazyReviveSuccesses += 1;
43611
+ this.recordSuccess();
43612
+ return result;
43613
+ } catch (retryErr) {
43614
+ if (!isSwDown(retryErr)) {
43615
+ this.recordFailure(`${op}: ${retryErr.message ?? String(retryErr)}`);
43616
+ throw retryErr;
43131
43617
  }
43618
+ throw bridgeDown(retryErr, true);
43132
43619
  }
43133
- this.recordFailure(`download bridge-down: ${err.message}`);
43134
- throw new FetchproxyBridgeDownError({
43135
- originalError: err.message,
43136
- retryAttempted: false,
43137
- op: "download",
43138
- url: opts.url,
43139
- role: this.role,
43140
- port: this.opts.port
43141
- });
43142
43620
  }
43143
43621
  }
43144
43622
  async _downloadOnce(opts) {
@@ -43153,8 +43631,8 @@ var FetchproxyServer = class {
43153
43631
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
43154
43632
  }
43155
43633
  };
43156
- const pending = this.guardPending(new Promise((resolve3, reject) => {
43157
- this.pendingDownload.set(id, { resolve: resolve3, reject });
43634
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43635
+ this.pendingDownload.set(id, { resolve: resolve6, reject });
43158
43636
  }));
43159
43637
  await this.sendInnerFrame(inner);
43160
43638
  return this._withVerbTimeout(
@@ -43167,7 +43645,9 @@ var FetchproxyServer = class {
43167
43645
  // CAP and need the identical explanation; missing it then was the point
43168
43646
  // of #279 — "both verbs" was counted off the two call sites in view
43169
43647
  // rather than off the type. Three is still the number.
43170
- opts.timeoutMs
43648
+ opts.timeoutMs,
43649
+ // A second download saves a duplicate "file (1)".
43650
+ false
43171
43651
  );
43172
43652
  }
43173
43653
  /**
@@ -43212,8 +43692,8 @@ var FetchproxyServer = class {
43212
43692
  keys: [...opts.keys]
43213
43693
  }
43214
43694
  };
43215
- const pending = this.guardPending(new Promise((resolve3, reject) => {
43216
- this.pendingIdb.set(id, { resolve: resolve3, reject });
43695
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43696
+ this.pendingIdb.set(id, { resolve: resolve6, reject });
43217
43697
  }));
43218
43698
  await this.sendInnerFrame(inner);
43219
43699
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -43252,12 +43732,58 @@ var FetchproxyServer = class {
43252
43732
  op: "read_dom",
43253
43733
  init: { origin, names: [...opts.names] }
43254
43734
  };
43255
- const pending = this.guardPending(new Promise((resolve3, reject) => {
43256
- this.pendingStorage.set(id, { resolve: resolve3, reject });
43735
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43736
+ this.pendingStorage.set(id, { resolve: resolve6, reject });
43257
43737
  }));
43258
43738
  await this.sendInnerFrame(inner);
43259
43739
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
43260
43740
  }
43741
+ /**
43742
+ * 3.1.0+: read a declared REPEATED DOM structure from the user's
43743
+ * signed-in tab. Requires `'read_dom_list'` in capabilities AND `name` to
43744
+ * match a declared `domListSelectors` entry. The extension resolves the
43745
+ * entry's `itemSelector` via `document.querySelectorAll` in the matched
43746
+ * tab's DOM (isolated world), then resolves each declared field against
43747
+ * every matched item in turn — no page-JS execution, same as `readDom`.
43748
+ *
43749
+ * Returns one row per matched item, in document order, truncated to the
43750
+ * entry's declared `maxItems` when set. A field absent on a given item is
43751
+ * omitted from that item's row (the row itself is still returned). Throws
43752
+ * `FetchproxyProtocolError` on bridge failures and a plain `Error` on
43753
+ * developer mistakes (undeclared capability, undeclared name).
43754
+ *
43755
+ * Reads only what is CURRENTLY rendered — a virtualized or lazily-loaded
43756
+ * list returns only its visible rows. A caller that needs more scrolls
43757
+ * the list (out of band) and calls again.
43758
+ */
43759
+ async readDomList(opts) {
43760
+ if (!this.opts.capabilities.includes("read_dom_list")) {
43761
+ throw new Error('FetchproxyServer.readDomList(): MCP did not declare "read_dom_list" in capabilities');
43762
+ }
43763
+ await this.ensureConnected();
43764
+ this.throwIfPendingPair();
43765
+ if (typeof opts.name !== "string" || opts.name.length === 0) {
43766
+ throw new Error("FetchproxyServer.readDomList: opts.name must be a non-empty string");
43767
+ }
43768
+ this.assertScopeSubset([opts.name], this.opts.domListSelectors.map((d) => d.name), "domListSelectors");
43769
+ if (opts.subdomain !== void 0)
43770
+ assertSubdomainLabel(opts.subdomain);
43771
+ const baseDomain = this.resolveBaseDomain(opts.domain);
43772
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
43773
+ const origin = `https://${host}`;
43774
+ const id = this.nextRequestId++;
43775
+ const inner = {
43776
+ type: "request",
43777
+ id,
43778
+ op: "read_dom_list",
43779
+ init: { origin, name: opts.name }
43780
+ };
43781
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43782
+ this.pendingDomList.set(id, { resolve: resolve6, reject });
43783
+ }));
43784
+ await this.sendInnerFrame(inner);
43785
+ return this._withVerbTimeout(pending, this.pendingDomList, id, origin);
43786
+ }
43261
43787
  /**
43262
43788
  * 1.x+: run a declared GraphQL operation through the page's own Apollo
43263
43789
  * client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
@@ -43298,11 +43824,12 @@ var FetchproxyServer = class {
43298
43824
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
43299
43825
  }
43300
43826
  };
43301
- const pending = this.guardPending(new Promise((resolve3, reject) => {
43302
- this.pendingGraphql.set(id, { resolve: resolve3, reject });
43827
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43828
+ this.pendingGraphql.set(id, { resolve: resolve6, reject });
43303
43829
  }));
43304
- await this.sendInnerFrame(inner);
43305
- return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
43830
+ const retrySafe = opts.retryOnTimeout === true;
43831
+ await this.sendInnerFrame(inner, { resendable: retrySafe });
43832
+ return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name, void 0, retrySafe);
43306
43833
  }
43307
43834
  assertScopeSubset(requested, declared, label) {
43308
43835
  const undeclared = undeclaredKeys(requested, declared);
@@ -43339,6 +43866,7 @@ var FetchproxyServer = class {
43339
43866
  onInner(inner) {
43340
43867
  if (inner.type !== "response")
43341
43868
  return;
43869
+ this.resendable.delete(inner.id);
43342
43870
  this.lastExtensionMessageAt = Date.now();
43343
43871
  const fetchCb = this.pending.get(inner.id);
43344
43872
  if (fetchCb) {
@@ -43427,6 +43955,20 @@ var FetchproxyServer = class {
43427
43955
  }
43428
43956
  return;
43429
43957
  }
43958
+ const domListCb = this.pendingDomList.get(inner.id);
43959
+ if (domListCb) {
43960
+ this.pendingDomList.delete(inner.id);
43961
+ if (inner.ok) {
43962
+ if (inner.op === "read_dom_list" && inner.rows) {
43963
+ domListCb.resolve(inner.rows.map((row) => ({ ...row })));
43964
+ } else {
43965
+ domListCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_dom_list awaiter`));
43966
+ }
43967
+ } else {
43968
+ domListCb.reject(protocolErrorFrom(inner.error));
43969
+ }
43970
+ return;
43971
+ }
43430
43972
  const downloadCb = this.pendingDownload.get(inner.id);
43431
43973
  if (downloadCb) {
43432
43974
  this.pendingDownload.delete(inner.id);
@@ -43486,42 +44028,133 @@ var FetchproxyServer = class {
43486
44028
  }
43487
44029
  }
43488
44030
  }
43489
- rejectAllPending(reason = "extension disconnected") {
44031
+ rejectAllPending(reason = "extension disconnected", keep = /* @__PURE__ */ new Set()) {
44032
+ for (const id of this.allPendingIds()) {
44033
+ if (!keep.has(id))
44034
+ this.rejectPendingId(id, reason);
44035
+ }
44036
+ if (keep.size === 0)
44037
+ this.resendable.clear();
44038
+ }
44039
+ /** Every request id with a live awaiter, across all op maps. */
44040
+ allPendingIds() {
44041
+ const ids = [];
44042
+ for (const map2 of this.pendingMaps())
44043
+ for (const id of map2.keys())
44044
+ ids.push(id);
44045
+ return ids;
44046
+ }
44047
+ pendingMaps() {
44048
+ return [
44049
+ this.pending,
44050
+ this.pendingReadCookies,
44051
+ this.pendingStorage,
44052
+ this.pendingWriteCookies,
44053
+ this.pendingCapture,
44054
+ this.pendingRedirect,
44055
+ this.pendingIdb,
44056
+ this.pendingDomList,
44057
+ this.pendingDownload,
44058
+ this.pendingGraphql
44059
+ ];
44060
+ }
44061
+ isPendingId(id) {
44062
+ return this.pendingMaps().some((m) => m.has(id));
44063
+ }
44064
+ /** Fail the one awaiter registered under `id`, in its own map's shape. */
44065
+ rejectPendingId(id, reason) {
43490
44066
  const err = new FetchproxyProtocolError(reason);
43491
- for (const cb of this.pending.values()) {
43492
- cb({
44067
+ this.resendable.delete(id);
44068
+ const fetchCb = this.pending.get(id);
44069
+ if (fetchCb) {
44070
+ this.pending.delete(id);
44071
+ fetchCb({
43493
44072
  ok: false,
43494
44073
  error: err.message,
43495
44074
  kind: classifyFetchError(err.message),
43496
44075
  retryAttempted: false
43497
44076
  });
44077
+ return;
44078
+ }
44079
+ const cookieCb = this.pendingReadCookies.get(id);
44080
+ if (cookieCb) {
44081
+ this.pendingReadCookies.delete(id);
44082
+ cookieCb({ ok: false, error: err.message });
44083
+ return;
44084
+ }
44085
+ for (const map2 of [
44086
+ this.pendingStorage,
44087
+ this.pendingWriteCookies,
44088
+ this.pendingCapture,
44089
+ this.pendingRedirect,
44090
+ this.pendingIdb,
44091
+ this.pendingDomList,
44092
+ this.pendingDownload,
44093
+ this.pendingGraphql
44094
+ ]) {
44095
+ const entry = map2.get(id);
44096
+ if (entry) {
44097
+ map2.delete(id);
44098
+ entry.reject(err);
44099
+ return;
44100
+ }
44101
+ }
44102
+ }
44103
+ noteResendable(inner) {
44104
+ this.resendable.set(inner.id, inner);
44105
+ if (this.resendable.size > 256) {
44106
+ for (const id of [...this.resendable.keys()]) {
44107
+ if (!this.isPendingId(id))
44108
+ this.resendable.delete(id);
44109
+ }
44110
+ }
44111
+ }
44112
+ /**
44113
+ * B-BUG-6: the host this peer relayed through has gone — most often a
44114
+ * short-lived process (a bootstrap lift, an `fpx` call) that won the
44115
+ * election and then closed. Fail only the in-flight requests that may
44116
+ * already have run (a non-idempotent fetch, a cookie write, a download);
44117
+ * re-elect and send the rest again. The replies to the originals died with
44118
+ * the old host, and each re-sent request keeps its awaiter and its
44119
+ * original deadline.
44120
+ */
44121
+ async recoverFromHostLoss() {
44122
+ const toResend = /* @__PURE__ */ new Map();
44123
+ for (const [id, inner] of this.resendable) {
44124
+ if (this.isPendingId(id))
44125
+ toResend.set(id, inner);
44126
+ }
44127
+ this.resendable.clear();
44128
+ 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()));
44129
+ if (toResend.size === 0)
44130
+ return;
44131
+ const lost = (id, e) => this.rejectPendingId(id, `fetchproxy: the bridge host exited and reconnecting failed: ${e instanceof Error ? e.message : String(e)}`);
44132
+ try {
44133
+ await this.ensureConnected();
44134
+ } catch (e) {
44135
+ for (const id of toResend.keys())
44136
+ lost(id, e);
44137
+ return;
44138
+ }
44139
+ for (const [id, inner] of toResend) {
44140
+ if (!this.isPendingId(id))
44141
+ continue;
44142
+ try {
44143
+ await this.resendInnerFrame(inner);
44144
+ } catch (e) {
44145
+ lost(id, e);
44146
+ }
43498
44147
  }
43499
- this.pending.clear();
43500
- for (const cb of this.pendingReadCookies.values()) {
43501
- cb({ ok: false, error: err.message });
43502
- }
43503
- this.pendingReadCookies.clear();
43504
- for (const { reject } of this.pendingStorage.values())
43505
- reject(err);
43506
- this.pendingStorage.clear();
43507
- for (const { reject } of this.pendingWriteCookies.values())
43508
- reject(err);
43509
- this.pendingWriteCookies.clear();
43510
- for (const { reject } of this.pendingCapture.values())
43511
- reject(err);
43512
- this.pendingCapture.clear();
43513
- for (const { reject } of this.pendingRedirect.values())
43514
- reject(err);
43515
- this.pendingRedirect.clear();
43516
- for (const { reject } of this.pendingIdb.values())
43517
- reject(err);
43518
- this.pendingIdb.clear();
43519
- for (const { reject } of this.pendingDownload.values())
43520
- reject(err);
43521
- this.pendingDownload.clear();
43522
- for (const { reject } of this.pendingGraphql.values())
43523
- reject(err);
43524
- this.pendingGraphql.clear();
44148
+ }
44149
+ /** Send an already-registered request again, keeping its awaiter on failure. */
44150
+ async resendInnerFrame(inner) {
44151
+ if (this.hostHandle)
44152
+ await this.hostHandle.sendOwnInner(inner);
44153
+ else if (this.peerHandle)
44154
+ await this.peerHandle.sendInner(inner);
44155
+ else
44156
+ throw new Error("no bridge handle after re-election");
44157
+ this.noteResendable(inner);
43525
44158
  }
43526
44159
  /**
43527
44160
  * 0.5.2+: read the current pair-pending pair code from whichever handle
@@ -43614,6 +44247,12 @@ async function runOneLift(opts) {
43614
44247
  if (envVal !== void 0 && envVal !== "" && envVal !== "0" && envVal !== "false") {
43615
44248
  throw new BootstrapDisabledError(opts.serverName, envVar);
43616
44249
  }
44250
+ for (const bucket of ["cookies", "localStorage", "sessionStorage"]) {
44251
+ const glob = opts.declare[bucket].find((k) => k.endsWith("*"));
44252
+ if (glob !== void 0) {
44253
+ 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.`);
44254
+ }
44255
+ }
43617
44256
  const factory = opts._serverFactory ?? defaultFactory;
43618
44257
  const indexedDb = opts.declare.indexedDb ?? [];
43619
44258
  const localStoragePointers = opts.declare.localStoragePointers ?? [];
@@ -44012,7 +44651,7 @@ async function loginWithPassword(username, password) {
44012
44651
  // package.json
44013
44652
  var package_default = {
44014
44653
  name: "ofw-mcp",
44015
- version: "2.19.1",
44654
+ version: "2.19.3",
44016
44655
  license: "MIT",
44017
44656
  mcpName: "io.github.chrischall/ofw-mcp",
44018
44657
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -44046,11 +44685,11 @@ var package_default = {
44046
44685
  typecheck: "tsc -p tsconfig.json --noEmit"
44047
44686
  },
44048
44687
  dependencies: {
44049
- "@chrischall/mcp-utils": "^2.0.0",
44050
- "@fetchproxy/bootstrap": "^3.0.1",
44688
+ "@chrischall/mcp-utils": "^2.4.0",
44689
+ "@fetchproxy/bootstrap": "^3.2.0",
44051
44690
  "@modelcontextprotocol/server": "^2.0.0",
44052
44691
  dotenv: "^18.0.0",
44053
- zod: "^4.6.2"
44692
+ zod: "^4.6.5"
44054
44693
  },
44055
44694
  devDependencies: {
44056
44695
  "@modelcontextprotocol/client": "^2.0.0",
@@ -44183,7 +44822,7 @@ function reportCacheWriteFailure(err) {
44183
44822
 
44184
44823
  // src/client.ts
44185
44824
  try {
44186
- const dir = dirname2(fileURLToPath(import.meta.url));
44825
+ const dir = dirname3(fileURLToPath(import.meta.url));
44187
44826
  await loadDotenvSafely({ path: join5(dir, "..", ".env") });
44188
44827
  } catch {
44189
44828
  }
@@ -44529,6 +45168,34 @@ function parseTimestampValue(key, value, assumeNaiveIn) {
44529
45168
  }
44530
45169
  return null;
44531
45170
  }
45171
+ function naiveWallClockOf(instant, tz) {
45172
+ return isoWithOffset(instant, tz, "");
45173
+ }
45174
+ function nowNaiveWallClock(tz = displayTimeZone()) {
45175
+ return naiveWallClockOf(/* @__PURE__ */ new Date(), tz);
45176
+ }
45177
+ function toNaiveWallClock(value, tz = displayTimeZone()) {
45178
+ const raw = value.trim();
45179
+ if (DATE_ONLY.test(raw)) return raw;
45180
+ if (RFC3339_WITH_OFFSET.test(raw)) {
45181
+ const instant = parseTimestampValue("", raw, tz);
45182
+ return instant ? naiveWallClockOf(instant, tz) : null;
45183
+ }
45184
+ const naive = NAIVE_DATE_TIME.exec(raw);
45185
+ if (!naive) return null;
45186
+ const [, y, mo, d, h, mi, s, frac] = naive;
45187
+ const parts = {
45188
+ year: Number(y),
45189
+ month: Number(mo),
45190
+ day: Number(d),
45191
+ hour: Number(h),
45192
+ minute: Number(mi),
45193
+ second: Number(s ?? "0")
45194
+ };
45195
+ if (!isRealCalendarDate(parts)) return null;
45196
+ const ms = frac ? `.${frac.padEnd(3, "0").slice(0, 3)}` : "";
45197
+ return `${y}-${mo}-${d}T${h}:${mi}:${pad(parts.second)}${ms}`;
45198
+ }
44532
45199
  function walk(node2, tz) {
44533
45200
  if (Array.isArray(node2)) {
44534
45201
  for (const item of node2) walk(item, tz);
@@ -44631,17 +45298,43 @@ var PostMessagesResponseSchema = external_exports.looseObject({
44631
45298
  id: external_exports.number().optional(),
44632
45299
  entityId: external_exports.number().optional()
44633
45300
  }).nullable();
45301
+ var UnconfirmedWriteError = class extends Error {
45302
+ constructor(postedId, cause) {
45303
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
45304
+ this.postedId = postedId;
45305
+ this.name = "UnconfirmedWriteError";
45306
+ }
45307
+ postedId;
45308
+ };
45309
+ function isDefinitiveRejection(e) {
45310
+ if (!(e instanceof Error)) return false;
45311
+ if (/^Rate limited by OFW API/.test(e.message)) return true;
45312
+ const m = /OFW API error: (4\d\d)\b/.exec(e.message);
45313
+ return m !== null && m[1] !== "408";
45314
+ }
44634
45315
  async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
45316
+ let posted;
45317
+ try {
45318
+ posted = await client2.request("POST", "/pub/v3/messages", payload);
45319
+ } catch (e) {
45320
+ throw isDefinitiveRejection(e) ? e : new UnconfirmedWriteError(null, e);
45321
+ }
44635
45322
  const raw = parseLenient(
44636
45323
  PostMessagesResponseSchema,
44637
- await client2.request("POST", "/pub/v3/messages", payload),
45324
+ posted,
44638
45325
  { label: "ofw-mcp", context: `POST /pub/v3/messages (${ctx})`, mode: "strict" }
44639
45326
  );
44640
45327
  const id = typeof raw?.id === "number" ? raw.id : typeof raw?.entityId === "number" ? raw.entityId : null;
44641
45328
  if (id === null) return { id: null, detail: null, raw };
45329
+ let fetched;
45330
+ try {
45331
+ fetched = await client2.request("GET", `/pub/v3/messages/${id}`);
45332
+ } catch (e) {
45333
+ throw new UnconfirmedWriteError(id, e);
45334
+ }
44642
45335
  const detail = parseLenient(
44643
45336
  detailSchema,
44644
- await client2.request("GET", `/pub/v3/messages/${id}`),
45337
+ fetched,
44645
45338
  { label: "ofw-mcp", context: `GET /pub/v3/messages/{id} (${ctx})`, mode: "strict" }
44646
45339
  );
44647
45340
  return { id, detail, raw };
@@ -44666,7 +45359,7 @@ function registerUserTools(server, client2) {
44666
45359
  }
44667
45360
 
44668
45361
  // src/tools/healthcheck.ts
44669
- function registerHealthcheckTools(server, client2, resolve3 = resolveAuth) {
45362
+ function registerHealthcheckTools(server, client2, resolve6 = resolveAuth) {
44670
45363
  registerCredentialHealthcheckTool({
44671
45364
  server,
44672
45365
  prefix: "ofw",
@@ -44677,7 +45370,7 @@ function registerHealthcheckTools(server, client2, resolve3 = resolveAuth) {
44677
45370
  probePath: "/pub/v2/profiles",
44678
45371
  resolveCredential: async () => {
44679
45372
  try {
44680
- const auth = await resolve3();
45373
+ const auth = await resolve6();
44681
45374
  return {
44682
45375
  source: auth.source,
44683
45376
  // Never the token. Expiry is the fact that explains a connector
@@ -44883,7 +45576,7 @@ async function walkPages(client2, folder, folderId, opts, store) {
44883
45576
  folder,
44884
45577
  subject: item.subject ?? "(no subject)",
44885
45578
  fromUser: item.from?.name ?? "",
44886
- sentAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
45579
+ sentAt: item.date?.dateTime ?? nowNaiveWallClock(),
44887
45580
  recipients: mapRecipients(detailRecipients ?? item.recipients),
44888
45581
  body,
44889
45582
  fetchedBodyAt,
@@ -45026,7 +45719,7 @@ async function syncDrafts(client2, draftsFolderId, store, budget) {
45026
45719
  body: detail.body ?? "",
45027
45720
  recipients: mapRecipients(item.recipients),
45028
45721
  replyToId: threadedReplyTo(item),
45029
- modifiedAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
45722
+ modifiedAt: item.date?.dateTime ?? nowNaiveWallClock(),
45030
45723
  listData: item
45031
45724
  });
45032
45725
  }
@@ -45150,6 +45843,11 @@ function getAttachmentsDir() {
45150
45843
  if (override && override.trim().length > 0) return override.trim();
45151
45844
  return join6(homedir4(), "Downloads", "ofw-mcp");
45152
45845
  }
45846
+ function getUploadDir() {
45847
+ const override = process.env.OFW_UPLOAD_DIR;
45848
+ if (override && override.trim().length > 0) return override.trim();
45849
+ return getAttachmentsDir();
45850
+ }
45153
45851
  function getWriteMode() {
45154
45852
  const raw = process.env.OFW_WRITE_MODE;
45155
45853
  if (typeof raw !== "string" || raw.trim().length === 0) return "all";
@@ -45826,6 +46524,8 @@ function findPart(zip, conventional, fileName) {
45826
46524
 
45827
46525
  // src/extract/spreadsheet.ts
45828
46526
  var DEFAULT_MAX_CELLS = 2e5;
46527
+ var DEFAULT_MAX_GRID_CELLS = 2e6;
46528
+ var MAX_COLUMNS = 16384;
45829
46529
  var BUILTIN_DATE_FORMATS = /* @__PURE__ */ new Set([14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47]);
45830
46530
  var SERIAL_EPOCH_OFFSET = 25569;
45831
46531
  function excelSerialToIso(serial, date1904) {
@@ -45844,11 +46544,11 @@ function isDateFormat(numFmtId, formatCode) {
45844
46544
  return /[ymdhs]/i.test(bare);
45845
46545
  }
45846
46546
  function columnIndex(ref) {
45847
- const m = /^([A-Z]+)/.exec(ref);
46547
+ const m = /^([A-Z]{1,3})(?![A-Z])/.exec(ref);
45848
46548
  if (!m) return null;
45849
46549
  let index = 0;
45850
46550
  for (const ch of m[1]) index = index * 26 + (ch.charCodeAt(0) - 64);
45851
- return index - 1;
46551
+ return index > MAX_COLUMNS ? null : index - 1;
45852
46552
  }
45853
46553
  function csvField(value) {
45854
46554
  return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
@@ -45903,7 +46603,7 @@ function cellValue(attrs, inner, ctx) {
45903
46603
  }
45904
46604
  }
45905
46605
  }
45906
- function parseSheet(xml, name, ctx, maxCells) {
46606
+ function parseSheet(xml, name, ctx, maxCells, maxGridCells) {
45907
46607
  const rows = [];
45908
46608
  let cols = 0;
45909
46609
  let cells = 0;
@@ -45914,14 +46614,20 @@ function parseSheet(xml, name, ctx, maxCells) {
45914
46614
  break;
45915
46615
  }
45916
46616
  const values = [];
46617
+ let rowCols = cols;
45917
46618
  for (const cell of elements(row.inner, "c")) {
45918
46619
  const ref = attr(cell.attrs, "r");
45919
46620
  const index = ref === null ? null : columnIndex(ref);
45920
46621
  if (index === null) continue;
45921
46622
  values[index] = cellValue(cell.attrs, cell.inner, ctx);
45922
46623
  cells++;
45923
- if (index + 1 > cols) cols = index + 1;
46624
+ if (index + 1 > rowCols) rowCols = index + 1;
45924
46625
  }
46626
+ if ((rows.length + 1) * rowCols > maxGridCells) {
46627
+ truncated = true;
46628
+ break;
46629
+ }
46630
+ cols = rowCols;
45925
46631
  rows.push(values);
45926
46632
  }
45927
46633
  return { name, rows: rows.length, cols, csv: toCsv(rows, cols), ...truncated ? { truncated } : {} };
@@ -45940,6 +46646,7 @@ async function extractXlsx(bytes, opts = {}) {
45940
46646
  date1904: /<workbookPr[^>]*date1904="(1|true)"/i.test(workbookXml)
45941
46647
  };
45942
46648
  const maxCells = opts.maxCells ?? DEFAULT_MAX_CELLS;
46649
+ const maxGridCells = opts.maxGridCells ?? DEFAULT_MAX_GRID_CELLS;
45943
46650
  const sheets = [];
45944
46651
  const omitted = [];
45945
46652
  let index = 0;
@@ -45957,7 +46664,7 @@ async function extractXlsx(bytes, opts = {}) {
45957
46664
  omitted.push(`${name} (sheet part not found in the workbook)`);
45958
46665
  continue;
45959
46666
  }
45960
- sheets.push(parseSheet(xml, name, ctx, maxCells));
46667
+ sheets.push(parseSheet(xml, name, ctx, maxCells, maxGridCells));
45961
46668
  }
45962
46669
  const truncated = sheets.some((s) => s.truncated);
45963
46670
  return {
@@ -46017,12 +46724,16 @@ function parseDelimitedRows(text, delimiter) {
46017
46724
  }
46018
46725
  return rows;
46019
46726
  }
46020
- function extractDelimited(text, name, delimiter) {
46021
- const rows = parseDelimitedRows(text, delimiter);
46727
+ function extractDelimited(text, name, delimiter, maxGridCells = DEFAULT_MAX_GRID_CELLS) {
46728
+ let rows = parseDelimitedRows(text, delimiter);
46022
46729
  const cols = rows.reduce((max, r) => Math.max(max, r.length), 0);
46730
+ const keep = cols === 0 ? rows.length : Math.floor(maxGridCells / cols);
46731
+ const truncated = rows.length > keep;
46732
+ if (truncated) rows = rows.slice(0, keep);
46023
46733
  return {
46024
46734
  kind: "spreadsheet",
46025
- sheets: [{ name, rows: rows.length, cols, csv: toCsv(rows, cols) }]
46735
+ sheets: [{ name, rows: rows.length, cols, csv: toCsv(rows, cols), ...truncated ? { truncated } : {} }],
46736
+ ...truncated ? { truncated } : {}
46026
46737
  };
46027
46738
  }
46028
46739
 
@@ -46518,8 +47229,17 @@ async function extractAttachment(bytes, mimeType, fileName, opts = {}) {
46518
47229
  }
46519
47230
 
46520
47231
  // src/tools/attachments.ts
46521
- import { readFileSync as readFileSync2, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
46522
- import { basename, dirname as dirname3, extname } from "node:path";
47232
+ import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
47233
+ import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute5, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
47234
+ function isWithin(root, candidate) {
47235
+ const rel = relative2(root, candidate);
47236
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute5(rel);
47237
+ }
47238
+ function deepestExisting(path) {
47239
+ let current = path;
47240
+ while (!existsSync3(current)) current = dirname4(current);
47241
+ return current;
47242
+ }
46523
47243
  var MIME_BY_EXT = {
46524
47244
  ".pdf": "application/pdf",
46525
47245
  ".png": "image/png",
@@ -46580,15 +47300,28 @@ function resolveDownloadMime(bytes, headerMime, fileName) {
46580
47300
  function isHostRenderableImage(mime) {
46581
47301
  return HOST_RENDERABLE_IMAGE_MIMES.has(mime);
46582
47302
  }
47303
+ var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
46583
47304
  var NodeAttachmentIO = class {
46584
47305
  supportsDisk = true;
46585
47306
  async resolveUpload(path) {
46586
- const abs = expandPath(path);
46587
- const stat = statSync(abs);
47307
+ const root = resolve4(getUploadDir());
47308
+ const abs = resolve4(root, path.startsWith("~") ? expandPath(path) : path);
47309
+ const outside = new Error(`Refusing to upload ${abs}: it is outside the upload directory (${root}). Only files placed in that directory can be uploaded \u2014 ask the user to copy the file there, or set OFW_UPLOAD_DIR.`);
47310
+ if (!isWithin(root, abs)) throw outside;
47311
+ const real = realpathSync2(abs);
47312
+ const realRoot = realpathSync2(root);
47313
+ if (!isWithin(realRoot, real)) throw outside;
47314
+ if (relative2(realRoot, real).split(sep2).some((segment) => segment.startsWith("."))) {
47315
+ throw new Error(`Refusing to upload ${abs}: hidden files and files in hidden directories (dotfiles, credential stores) are never uploaded.`);
47316
+ }
47317
+ const stat = statSync(real);
46588
47318
  if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
47319
+ if (stat.size > MAX_UPLOAD_BYTES) {
47320
+ throw new Error(`Refusing to upload ${abs}: it is too large (${stat.size} bytes; the limit is ${MAX_UPLOAD_BYTES}).`);
47321
+ }
46589
47322
  const fileName = basename(abs);
46590
47323
  const mimeType = mimeFromName(fileName);
46591
- const blob = await fileBlob(abs, { type: mimeType });
47324
+ const blob = await fileBlob(real, { type: mimeType });
46592
47325
  return { blob, fileName, mimeType, sizeBytes: stat.size };
46593
47326
  }
46594
47327
  readDownloaded(path) {
@@ -46598,9 +47331,29 @@ var NodeAttachmentIO = class {
46598
47331
  return null;
46599
47332
  }
46600
47333
  }
46601
- writeDownload(dest, bytes) {
46602
- mkdirSync2(dirname3(dest), { recursive: true });
46603
- writeFileSync2(dest, bytes);
47334
+ writeDownload(dest, bytes, { root, overwrite }) {
47335
+ mkdirSync2(root, { recursive: true });
47336
+ const realRoot = realpathSync2(root);
47337
+ const parent = dirname4(dest);
47338
+ const anchor2 = realpathSync2(deepestExisting(parent));
47339
+ if (anchor2 !== realRoot && !isWithin(realRoot, anchor2)) {
47340
+ throw new Error(`Refusing to write ${dest}: it resolves outside the attachments directory (${root}).`);
47341
+ }
47342
+ mkdirSync2(parent, { recursive: true });
47343
+ if (overwrite) {
47344
+ try {
47345
+ unlinkSync2(dest);
47346
+ } catch {
47347
+ }
47348
+ }
47349
+ try {
47350
+ writeFileSync2(dest, bytes, { flag: "wx", mode: 384 });
47351
+ } catch (e) {
47352
+ if (e.code === "EEXIST") {
47353
+ throw new Error(`Refusing to overwrite ${dest}: a file already exists there. Pass force:true to replace it, or choose another saveTo.`);
47354
+ }
47355
+ throw e;
47356
+ }
46604
47357
  }
46605
47358
  };
46606
47359
 
@@ -46664,7 +47417,7 @@ async function buildInlineDelivery(input2) {
46664
47417
  }
46665
47418
 
46666
47419
  // src/tools/messages.ts
46667
- import { basename as basename2, join as join7 } from "node:path";
47420
+ import { basename as basename2, join as join7, resolve as resolve5 } from "node:path";
46668
47421
 
46669
47422
  // src/tools/pagination.ts
46670
47423
  function pageState(input2) {
@@ -46948,8 +47701,8 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
46948
47701
  folderId: external_exports.string().describe('Folder name: "inbox", "sent", or "both" (default "both")').optional(),
46949
47702
  page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
46950
47703
  size: external_exports.number().int().min(1).describe("Messages per page (default 50)").optional(),
46951
- since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive)").optional(),
46952
- until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive)").optional(),
47704
+ since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive). A value with an offset or Z is compared as that instant; a naive value is read as the account's local time (DISPLAY_TZ)").optional(),
47705
+ until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive). A value with an offset or Z is compared as that instant; a naive value is read as the account's local time (DISPLAY_TZ)").optional(),
46953
47706
  q: external_exports.string().describe("Substring match on subject AND body (case-insensitive). Use to find messages on a specific topic.").optional(),
46954
47707
  sort: external_exports.enum(["newest", "oldest"]).describe('Result order: "newest" (default, newest first) or "oldest" (oldest first). This decides which end a truncated page keeps \u2014 with "newest" page 1 of a wide date range holds its most RECENT slice, with "oldest" its earliest. Use "oldest" to start at the old end of a range instead of paging to it.').optional(),
46955
47708
  autoRefresh: external_exports.boolean().describe(AUTO_REFRESH_DESC).optional(),
@@ -46976,9 +47729,25 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
46976
47729
  note: 'No lookup was performed. This says NOTHING about what is in the cache \u2014 do not read it as "no messages".'
46977
47730
  });
46978
47731
  }
47732
+ const bounds = {};
47733
+ for (const key of ["since", "until"]) {
47734
+ const value2 = args[key];
47735
+ if (value2 === void 0) continue;
47736
+ const converted = toNaiveWallClock(value2);
47737
+ if (converted === null) {
47738
+ return jsonErrorResponse({
47739
+ result: "INVALID_DATE",
47740
+ reason: `${key} must be an ISO date (YYYY-MM-DD) or datetime, optionally with an offset or Z (got ${JSON.stringify(value2)}).`,
47741
+ remedy: `Re-call with ${key} as e.g. "2026-07-27" or "2026-07-27T22:00:00-04:00".`,
47742
+ complete: false,
47743
+ note: 'No lookup was performed. This says NOTHING about what is in the cache \u2014 do not read it as "no messages".'
47744
+ });
47745
+ }
47746
+ bounds[key] = converted;
47747
+ }
46979
47748
  const cache = cacheProvider();
46980
47749
  const folders = folder === void 0 ? ["inbox", "sent"] : [folder];
46981
- const filter = { folder, since: args.since, until: args.until, q: args.q };
47750
+ const filter = { folder, since: bounds.since, until: bounds.until, q: args.q };
46982
47751
  const { value, refreshed, unverifiedEmpty } = await guardedCacheRead({
46983
47752
  client: client2,
46984
47753
  cache,
@@ -47152,7 +47921,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47152
47921
  folder,
47153
47922
  subject: detail.subject,
47154
47923
  fromUser: detail.from?.name ?? "",
47155
- sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
47924
+ sentAt: detail.date?.dateTime ?? nowNaiveWallClock(),
47156
47925
  recipients: mapRecipients(detail.recipients),
47157
47926
  body: detail.body ?? "",
47158
47927
  fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -47169,7 +47938,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47169
47938
  return jsonResponse({ ...viewOne(view, withReadState(row)), attachments, freshness });
47170
47939
  });
47171
47940
  if (allowSend) server.registerTool("ofw_send_message", {
47172
- description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted.",
47941
+ description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. If the send request times out or drops without a definitive answer, the result is SEND_UNCONFIRMED: the message may already have been delivered, so do NOT retry until a sent-folder sync (or ourfamilywizard.com) shows it did not go out. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted.",
47173
47942
  annotations: { destructiveHint: true },
47174
47943
  inputSchema: external_exports.object({
47175
47944
  subject: external_exports.string().describe("Message subject. Required unless draftId/messageId is given (then it overrides the server draft's subject).").optional(),
@@ -47248,15 +48017,30 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47248
48017
  chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
47249
48018
  }
47250
48019
  const myFileIDs = args.myFileIDs ?? serverDraft?.files ?? [];
47251
- const { id: newId, detail, raw } = await postMessageAndRefetch(client2, {
47252
- subject,
47253
- body,
47254
- recipientIds,
47255
- attachments: { myFileIDs },
47256
- draft: false,
47257
- includeOriginal: resolvedReplyTo !== null,
47258
- replyToId: resolvedReplyTo
47259
- }, SentDetailSchema, "ofw_send_message");
48020
+ let posted;
48021
+ try {
48022
+ posted = await postMessageAndRefetch(client2, {
48023
+ subject,
48024
+ body,
48025
+ recipientIds,
48026
+ attachments: { myFileIDs },
48027
+ draft: false,
48028
+ includeOriginal: resolvedReplyTo !== null,
48029
+ replyToId: resolvedReplyTo
48030
+ }, SentDetailSchema, "ofw_send_message");
48031
+ } catch (e) {
48032
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
48033
+ const reason = e.postedId !== null ? `OFW accepted the send (message id ${e.postedId}) but re-reading it to confirm failed: ${e.message}. The message WAS very likely delivered.` : `The send request failed without a definitive answer from OFW: ${e.message}. The message MAY HAVE BEEN DELIVERED to the recipient.`;
48034
+ return jsonErrorResponse({
48035
+ result: "SEND_UNCONFIRMED",
48036
+ mayHaveBeenDelivered: true,
48037
+ sentMessageId: e.postedId,
48038
+ reason,
48039
+ remedy: 'Do NOT retry the send blindly. First run ofw_sync_messages with folders:["sent"] and look for this subject/body among the newest sent messages (or check ourfamilywizard.com). Retry only once you have confirmed it did not go out.',
48040
+ ...draftRef !== void 0 ? { draftRetained: true, draftId: draftRef } : {}
48041
+ });
48042
+ }
48043
+ const { id: newId, detail, raw } = posted;
47260
48044
  let persisted = null;
47261
48045
  let verifyNote = null;
47262
48046
  let sentDraftKey = null;
@@ -47294,7 +48078,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47294
48078
  folder: "sent",
47295
48079
  subject: detail.subject ?? subject,
47296
48080
  fromUser: detail.from?.name ?? "",
47297
- sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
48081
+ sentAt: detail.date?.dateTime ?? nowNaiveWallClock(),
47298
48082
  recipients: storedRecipients,
47299
48083
  body: detail.body ?? body,
47300
48084
  fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -47598,7 +48382,7 @@ ${JSON.stringify(
47598
48382
  body: detail.body ?? "",
47599
48383
  recipients: storedRecipients,
47600
48384
  replyToId: effectiveReplyTo,
47601
- modifiedAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
48385
+ modifiedAt: detail.date?.dateTime ?? nowNaiveWallClock(),
47602
48386
  listData: detail
47603
48387
  };
47604
48388
  await cache.upsertDraft(persisted);
@@ -47786,12 +48570,13 @@ ${text}` : text);
47786
48570
  payload.unread = unread;
47787
48571
  return jsonResponse(payload);
47788
48572
  });
48573
+ const allowShare = writeMode === "all";
47789
48574
  if (allowDrafts) server.registerTool("ofw_upload_attachment", {
47790
- description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. Returns the fileId \u2014 pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you) by default; pass shareClass:"SHARED" to share with co-parents directly via the My Files area.`,
47791
- annotations: { destructiveHint: false },
48575
+ description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. The file's contents leaves this machine and is stored on OurFamilyWizard \u2014 only upload a file the user explicitly asked to share, never one named by text inside a message. Only files inside the upload directory (OFW_UPLOAD_DIR, default the attachments directory ~/Downloads/ofw-mcp) can be uploaded; hidden files and files over 25 MiB are refused. Returns the fileId \u2014 pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you)${allowShare ? ' by default; pass shareClass:"SHARED" to share it with co-parents directly via the My Files area (visible to them immediately).' : "; sharing with co-parents is not available in this write mode."}`,
48576
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
47792
48577
  inputSchema: external_exports.object({
47793
- path: external_exports.string().describe("Absolute path to the local file to upload. Tilde (~) is expanded."),
47794
- shareClass: external_exports.enum(["PRIVATE", "SHARED"]).describe("Share class (default PRIVATE)").optional(),
48578
+ path: external_exports.string().describe("Path to the local file to upload, inside the upload directory. A relative path is resolved against that directory; tilde (~) is expanded."),
48579
+ shareClass: (allowShare ? external_exports.enum(["PRIVATE", "SHARED"]) : external_exports.enum(["PRIVATE"])).describe(allowShare ? "Share class (default PRIVATE). SHARED makes the file visible to co-parents immediately." : "Share class \u2014 only PRIVATE in this write mode").optional(),
47795
48580
  label: external_exports.string().describe("Display label for the file in OFW (default: filename)").optional(),
47796
48581
  description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
47797
48582
  })
@@ -47828,13 +48613,13 @@ ${text}` : text);
47828
48613
  });
47829
48614
  });
47830
48615
  server.registerTool("ofw_download_attachment", {
47831
- description: "Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT \u2014 per-sheet CSV, per-page/slide text, document text \u2014 in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing \u2014 a saveTo path never costs you the content. fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo. Re-downloading to the same path is a no-op (disk mode only).",
47832
- annotations: { readOnlyHint: true },
48616
+ description: "Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT \u2014 per-sheet CSV, per-page/slide text, document text \u2014 in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing \u2014 a saveTo path never costs you the content. fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo; saveTo must stay inside the attachments directory, and an existing file is never overwritten unless force:true. Re-downloading to the same path is a no-op (disk mode only).",
48617
+ annotations: { readOnlyHint: false, destructiveHint: false },
47833
48618
  inputSchema: external_exports.object({
47834
48619
  fileId: external_exports.number().describe("Attachment file id (from ofw_get_message \u2192 attachments[].fileId)"),
47835
48620
  inline: external_exports.boolean().describe("If true, return content inline as MCP content blocks and skip the disk write. If false, write to disk and return the path \u2014 except on a hosted deployment with no filesystem, where inline is forced (forcedInline:true) so the content is still returned. If omitted, falls back to the OFW_INLINE_ATTACHMENTS env var (default: false = disk).").optional(),
47836
- saveTo: external_exports.string().describe("Absolute path or directory to write to. If a directory, the OFW filename is used. Default: ~/Downloads/ofw-mcp/<fileId>-<filename>. Ignored when inline is in effect.").optional(),
47837
- force: external_exports.boolean().describe("Re-download even if already on disk. Default false. Ignored when inline:true (inline always fetches fresh bytes, or reuses an on-disk copy if present).").optional(),
48621
+ saveTo: external_exports.string().describe("Path or directory to write to, INSIDE the attachments directory (OFW_ATTACHMENTS_DIR, default ~/Downloads/ofw-mcp); a relative path is resolved against it and anything outside it is refused. If a directory (trailing /), the OFW filename is used. Default: <attachments dir>/<fileId>-<filename>. An existing file is not overwritten unless force:true. Ignored when inline is in effect.").optional(),
48622
+ force: external_exports.boolean().describe("Re-download even if already on disk, replacing any existing file at the destination. Default false. Ignored when inline:true (inline always fetches fresh bytes, or reuses an on-disk copy if present).").optional(),
47838
48623
  extract: external_exports.boolean().describe("Whether to extract readable content from the file. Default: on for inline delivery of any non-image type, off in disk mode. Set false to get the raw bytes inline instead of extracted text (e.g. to hash or re-upload the file); set true in disk mode to get both the saved path and the extracted content.").optional(),
47839
48624
  maxChars: external_exports.number().int().min(500).max(5e5).describe("Ceiling on extracted characters (default 50000). Over it, content is clipped on a row/line boundary, `truncated` is set, and anything dropped whole is listed in `extracted.omitted`.").optional(),
47840
48625
  parts: external_exports.string().describe('Which sheets / slides / pages to extract, e.g. "1-3,5" (1-based positions) or a sheet name like "2026". A bare number matches either a position or a name. Omit for everything. Unselected parts are listed in `extracted.omitted`.').optional()
@@ -47877,12 +48662,16 @@ ${text}` : text);
47877
48662
  }
47878
48663
  let dest;
47879
48664
  const safeName = basename2(cached2.fileName);
48665
+ const root = resolve5(getAttachmentsDir());
47880
48666
  if (args.saveTo) {
47881
48667
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
47882
- const abs = expandPath2(args.saveTo);
48668
+ const abs = resolve5(root, args.saveTo.startsWith("~") ? expandPath2(args.saveTo) : args.saveTo);
47883
48669
  dest = isDirArg ? join7(abs, `${fileId}-${safeName}`) : abs;
48670
+ if (!isWithin(root, dest)) {
48671
+ throw new Error(`Refusing to save to ${dest}: it is outside the attachments directory (${root}). Downloads can only be written inside it \u2014 pass a path or subdirectory under it, or set OFW_ATTACHMENTS_DIR to move it.`);
48672
+ }
47884
48673
  } else {
47885
- dest = join7(getAttachmentsDir(), `${fileId}-${safeName}`);
48674
+ dest = join7(root, `${fileId}-${safeName}`);
47886
48675
  }
47887
48676
  const extractOnDisk = args.extract === true;
47888
48677
  if (!args.force && cached2.downloadedPath === dest) {
@@ -47901,7 +48690,7 @@ ${text}` : text);
47901
48690
  }
47902
48691
  }
47903
48692
  const response = await client2.requestBinary("GET", `/pub/v1/myfiles/${fileId}/data`);
47904
- attachmentIO.writeDownload(dest, response.body);
48693
+ attachmentIO.writeDownload(dest, response.body, { root, overwrite: args.force === true });
47905
48694
  await cache.markAttachmentDownloaded(fileId, dest);
47906
48695
  const fileName = response.suggestedFileName ?? cached2.fileName;
47907
48696
  const mimeType = resolveDownloadMime(response.body, response.contentType ?? cached2.mimeType, fileName);
@@ -48378,8 +49167,8 @@ function registerJournalTools(server, client2) {
48378
49167
 
48379
49168
  // src/cache/node.ts
48380
49169
  import { DatabaseSync } from "node:sqlite";
48381
- import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync2 } from "node:fs";
48382
- import { dirname as dirname4 } from "node:path";
49170
+ import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync4 } from "node:fs";
49171
+ import { dirname as dirname5 } from "node:path";
48383
49172
 
48384
49173
  // src/cache/store.ts
48385
49174
  function rowFromDb(r) {
@@ -48963,10 +49752,10 @@ var NodeSqlDriver = class {
48963
49752
  }
48964
49753
  };
48965
49754
  function enforceCachePermissions(dbPath) {
48966
- chmodSync2(dirname4(dbPath), 448);
49755
+ chmodSync2(dirname5(dbPath), 448);
48967
49756
  chmodSync2(dbPath, 384);
48968
49757
  for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
48969
- if (existsSync2(sibling)) chmodSync2(sibling, 384);
49758
+ if (existsSync4(sibling)) chmodSync2(sibling, 384);
48970
49759
  }
48971
49760
  }
48972
49761
  var OFWCache = class _OFWCache extends LocalCacheStore {
@@ -48977,7 +49766,7 @@ var OFWCache = class _OFWCache extends LocalCacheStore {
48977
49766
  db;
48978
49767
  static open(path) {
48979
49768
  const memory = path === ":memory:";
48980
- if (!memory) mkdirSync3(dirname4(path), { recursive: true });
49769
+ if (!memory) mkdirSync3(dirname5(path), { recursive: true });
48981
49770
  const db = new DatabaseSync(path);
48982
49771
  if (!memory) enforceCachePermissions(path);
48983
49772
  db.exec("PRAGMA journal_mode = WAL");
@@ -49007,7 +49796,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
49007
49796
  var nodeAttachmentIO = new NodeAttachmentIO();
49008
49797
  await runMcp({
49009
49798
  name: "ofw",
49010
- version: "2.19.1",
49799
+ version: "2.19.3",
49011
49800
  // x-release-please-version
49012
49801
  deps: client,
49013
49802
  tools: [