ofw-mcp 2.19.0 → 2.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +1029 -390
- package/dist/index.js +1 -1
- package/dist/tools/messages.js +9 -9
- package/dist/tools/user.js +1 -1
- package/package.json +5 -5
- package/server.json +2 -2
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:
|
|
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 =
|
|
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,
|
|
8821
|
+
function isRecursive(inst, stack, resolve4) {
|
|
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,
|
|
8831
|
+
const answer = isRecursive(child, stack, resolve4);
|
|
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,
|
|
8842
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : 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 ?? (
|
|
8906
|
+
const inner = def._cachedInner ?? (resolve4 ? 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((
|
|
27806
|
+
return new Promise((resolve4, 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
|
-
|
|
27813
|
+
resolve4();
|
|
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((
|
|
28607
|
+
return new Promise((resolve4, 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
|
|
28675
|
+
if (options?.allowInputRequired === true) return resolve4(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
|
|
28687
|
+
return resolve4(this._resolveNonCompleteResult(decoded, flow));
|
|
28681
28688
|
}
|
|
28682
28689
|
const result = decoded.result;
|
|
28683
28690
|
validateStandardSchema(resultSchema, result).then((parseResult) => {
|
|
28684
|
-
if (parseResult.success)
|
|
28691
|
+
if (parseResult.success) resolve4(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 =
|
|
31603
|
+
let _sch = resolve4.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
|
|
31629
|
+
function resolve4(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
|
|
32079
|
+
function resolve4(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,
|
|
32085
|
+
function resolveComponent(base, relative2, options, skipNormalization) {
|
|
32079
32086
|
const target = {};
|
|
32080
32087
|
if (!skipNormalization) {
|
|
32081
32088
|
base = parse3(serialize(base, options), options);
|
|
32082
|
-
|
|
32089
|
+
relative2 = parse3(serialize(relative2, options), options);
|
|
32083
32090
|
}
|
|
32084
32091
|
options = options || {};
|
|
32085
|
-
if (!options.tolerant &&
|
|
32086
|
-
target.scheme =
|
|
32087
|
-
target.userinfo =
|
|
32088
|
-
target.host =
|
|
32089
|
-
target.port =
|
|
32090
|
-
target.path = removeDotSegments(
|
|
32091
|
-
target.query =
|
|
32092
|
+
if (!options.tolerant && relative2.scheme) {
|
|
32093
|
+
target.scheme = relative2.scheme;
|
|
32094
|
+
target.userinfo = relative2.userinfo;
|
|
32095
|
+
target.host = relative2.host;
|
|
32096
|
+
target.port = relative2.port;
|
|
32097
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
32098
|
+
target.query = relative2.query;
|
|
32092
32099
|
} else {
|
|
32093
|
-
if (
|
|
32094
|
-
target.userinfo =
|
|
32095
|
-
target.host =
|
|
32096
|
-
target.port =
|
|
32097
|
-
target.path = removeDotSegments(
|
|
32098
|
-
target.query =
|
|
32100
|
+
if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
|
|
32101
|
+
target.userinfo = relative2.userinfo;
|
|
32102
|
+
target.host = relative2.host;
|
|
32103
|
+
target.port = relative2.port;
|
|
32104
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
32105
|
+
target.query = relative2.query;
|
|
32099
32106
|
} else {
|
|
32100
|
-
if (!
|
|
32107
|
+
if (!relative2.path) {
|
|
32101
32108
|
target.path = base.path;
|
|
32102
|
-
if (
|
|
32109
|
+
if (relative2.query !== void 0) target.query = relative2.query;
|
|
32103
32110
|
else target.query = base.query;
|
|
32104
32111
|
} else {
|
|
32105
|
-
if (
|
|
32112
|
+
if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
|
|
32106
32113
|
else {
|
|
32107
|
-
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" +
|
|
32108
|
-
else if (!base.path) target.path =
|
|
32109
|
-
else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
32114
|
+
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
|
|
32115
|
+
else if (!base.path) target.path = relative2.path;
|
|
32116
|
+
else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
|
|
32110
32117
|
target.path = removeDotSegments(target.path);
|
|
32111
32118
|
}
|
|
32112
|
-
target.query =
|
|
32119
|
+
target.query = relative2.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 =
|
|
32127
|
+
target.fragment = relative2.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:
|
|
32253
|
+
resolve: resolve4,
|
|
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((
|
|
37626
|
+
return new Promise((resolve4, 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
|
-
|
|
37641
|
+
resolve4();
|
|
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
|
-
|
|
37648
|
+
resolve4();
|
|
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((
|
|
37702
|
+
return await new Promise((resolve4) => {
|
|
37696
37703
|
const waiter = () => {
|
|
37697
37704
|
clearTimeout(timer);
|
|
37698
|
-
|
|
37705
|
+
resolve4(true);
|
|
37699
37706
|
};
|
|
37700
37707
|
const timer = setTimeout(() => {
|
|
37701
37708
|
this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
|
|
37702
|
-
|
|
37709
|
+
resolve4(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 = /([?&](?:
|
|
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 =
|
|
38079
|
-
|
|
38080
|
-
|
|
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
|
-
|
|
38203
|
-
|
|
38204
|
-
|
|
38205
|
-
|
|
38206
|
-
|
|
38207
|
-
|
|
38208
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 (!
|
|
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 =
|
|
38600
|
+
const dir = dirname2(filePath);
|
|
38473
38601
|
const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
|
|
38474
|
-
const dirExisted =
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
39573
|
-
|
|
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((
|
|
40483
|
+
return new Promise((resolve4, 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
|
-
|
|
40499
|
+
resolve4({ 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
|
-
|
|
40507
|
+
resolve4({ 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 {
|
|
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
|
|
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 === "" || !
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
40578
|
-
await chmod(tmp, 384);
|
|
40579
|
-
await rename(tmp, path);
|
|
40881
|
+
return parseIdentity(await readFile(path, "utf8"));
|
|
40580
40882
|
} catch (e) {
|
|
40581
|
-
|
|
40582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
40910
|
+
await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
|
|
40911
|
+
const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
|
|
40589
40912
|
try {
|
|
40590
|
-
|
|
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
|
-
|
|
40593
|
-
|
|
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 !== "" &&
|
|
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
|
-
|
|
40700
|
-
|
|
40701
|
-
|
|
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((
|
|
40808
|
-
resolveOwnSession =
|
|
41156
|
+
ownSessionReady = new Promise((resolve4, reject) => {
|
|
41157
|
+
resolveOwnSession = resolve4;
|
|
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
|
|
41437
|
+
let result;
|
|
41063
41438
|
try {
|
|
41064
|
-
|
|
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
|
-
|
|
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((
|
|
41559
|
+
close: () => new Promise((resolve4) => {
|
|
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(() =>
|
|
41567
|
+
opts.httpServer.close(() => resolve4());
|
|
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
|
-
|
|
41606
|
+
const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
|
|
41607
|
+
await new Promise((resolve4, reject) => {
|
|
41608
|
+
let timer;
|
|
41193
41609
|
const onOpen = () => {
|
|
41194
41610
|
cleanup();
|
|
41195
|
-
|
|
41611
|
+
resolve4();
|
|
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
|
-
|
|
41268
|
-
|
|
41269
|
-
|
|
41270
|
-
|
|
41695
|
+
let sessionPromise;
|
|
41696
|
+
let sessionPromiseSettled = false;
|
|
41697
|
+
function resetSessionPromise() {
|
|
41698
|
+
sessionPromiseSettled = false;
|
|
41699
|
+
sessionPromise = new Promise((resolve4, reject) => {
|
|
41700
|
+
resolveFirstReady = (st) => {
|
|
41701
|
+
sessionPromiseSettled = true;
|
|
41702
|
+
resolve4(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
|
-
|
|
41933
|
+
// A getter: `extension-disconnected` replaces the promise (B-BUG-5).
|
|
41934
|
+
get session() {
|
|
41935
|
+
return sessionPromise;
|
|
41936
|
+
},
|
|
41488
41937
|
sendInner: async (inner) => {
|
|
41489
|
-
|
|
41490
|
-
|
|
41491
|
-
|
|
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: [
|
|
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
|
-
|
|
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((
|
|
42322
|
-
this.pending.set(id,
|
|
42852
|
+
const pending = this.guardPending(new Promise((resolve4) => {
|
|
42853
|
+
this.pending.set(id, resolve4);
|
|
42323
42854
|
}));
|
|
42324
|
-
|
|
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((
|
|
42876
|
+
new Promise((resolve4) => {
|
|
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
|
-
|
|
42881
|
+
resolve4({
|
|
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
|
|
43025
|
+
const isAbsolute5 = path.startsWith("http://") || path.startsWith("https://");
|
|
42481
43026
|
let host;
|
|
42482
|
-
if (
|
|
43027
|
+
if (isAbsolute5) {
|
|
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 =
|
|
43036
|
+
const url2 = isAbsolute5 ? 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
42708
|
-
*
|
|
42709
|
-
*
|
|
42710
|
-
*
|
|
42711
|
-
*
|
|
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((
|
|
42745
|
-
this.pendingReadCookies.set(id,
|
|
43301
|
+
const pending = this.guardPending(new Promise((resolve4) => {
|
|
43302
|
+
this.pendingReadCookies.set(id, resolve4);
|
|
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((
|
|
42804
|
-
this.pendingWriteCookies.set(id, { resolve:
|
|
43360
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43361
|
+
this.pendingWriteCookies.set(id, { resolve: resolve4, reject });
|
|
42805
43362
|
}));
|
|
42806
43363
|
await this.sendInnerFrame(inner);
|
|
42807
|
-
return this._withVerbTimeout(
|
|
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((
|
|
42867
|
-
this.pendingStorage.set(id, { resolve:
|
|
43431
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43432
|
+
this.pendingStorage.set(id, { resolve: resolve4, 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
|
-
|
|
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((
|
|
42972
|
-
this.pendingCapture.set(id, { resolve:
|
|
43490
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43491
|
+
this.pendingCapture.set(id, { resolve: resolve4, 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
|
-
|
|
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((
|
|
43066
|
-
this.pendingRedirect.set(id, { resolve:
|
|
43538
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43539
|
+
this.pendingRedirect.set(id, { resolve: resolve4, 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
|
|
43594
|
+
const result = await once();
|
|
43098
43595
|
this.recordSuccess();
|
|
43099
43596
|
return result;
|
|
43100
43597
|
} catch (err) {
|
|
43101
|
-
|
|
43102
|
-
|
|
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
|
|
43109
|
-
|
|
43110
|
-
|
|
43111
|
-
|
|
43112
|
-
|
|
43113
|
-
|
|
43114
|
-
|
|
43115
|
-
|
|
43116
|
-
|
|
43117
|
-
|
|
43118
|
-
|
|
43119
|
-
|
|
43120
|
-
|
|
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((
|
|
43157
|
-
this.pendingDownload.set(id, { resolve:
|
|
43634
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43635
|
+
this.pendingDownload.set(id, { resolve: resolve4, 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((
|
|
43216
|
-
this.pendingIdb.set(id, { resolve:
|
|
43695
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43696
|
+
this.pendingIdb.set(id, { resolve: resolve4, 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((
|
|
43256
|
-
this.pendingStorage.set(id, { resolve:
|
|
43735
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43736
|
+
this.pendingStorage.set(id, { resolve: resolve4, 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((resolve4, reject) => {
|
|
43782
|
+
this.pendingDomList.set(id, { resolve: resolve4, 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((
|
|
43302
|
-
this.pendingGraphql.set(id, { resolve:
|
|
43827
|
+
const pending = this.guardPending(new Promise((resolve4, reject) => {
|
|
43828
|
+
this.pendingGraphql.set(id, { resolve: resolve4, reject });
|
|
43303
43829
|
}));
|
|
43304
|
-
|
|
43305
|
-
|
|
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
|
-
|
|
43492
|
-
|
|
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
|
+
}
|
|
43498
44110
|
}
|
|
43499
|
-
|
|
43500
|
-
|
|
43501
|
-
|
|
43502
|
-
|
|
43503
|
-
|
|
43504
|
-
|
|
43505
|
-
|
|
43506
|
-
|
|
43507
|
-
|
|
43508
|
-
|
|
43509
|
-
|
|
43510
|
-
|
|
43511
|
-
|
|
43512
|
-
|
|
43513
|
-
|
|
43514
|
-
|
|
43515
|
-
this.
|
|
43516
|
-
|
|
43517
|
-
|
|
43518
|
-
|
|
43519
|
-
|
|
43520
|
-
|
|
43521
|
-
|
|
43522
|
-
|
|
43523
|
-
|
|
43524
|
-
|
|
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
|
+
}
|
|
44147
|
+
}
|
|
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.
|
|
44654
|
+
version: "2.19.2",
|
|
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.
|
|
44050
|
-
"@fetchproxy/bootstrap": "^3.0
|
|
44688
|
+
"@chrischall/mcp-utils": "^2.4.0",
|
|
44689
|
+
"@fetchproxy/bootstrap": "^3.2.0",
|
|
44051
44690
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
44052
|
-
dotenv: "^
|
|
44053
|
-
zod: "^4.6.
|
|
44691
|
+
dotenv: "^18.0.0",
|
|
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 =
|
|
44825
|
+
const dir = dirname3(fileURLToPath(import.meta.url));
|
|
44187
44826
|
await loadDotenvSafely({ path: join5(dir, "..", ".env") });
|
|
44188
44827
|
} catch {
|
|
44189
44828
|
}
|
|
@@ -44658,7 +45297,7 @@ function registerUserTools(server, client2) {
|
|
|
44658
45297
|
});
|
|
44659
45298
|
server.registerTool("ofw_get_notifications", {
|
|
44660
45299
|
description: "Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.",
|
|
44661
|
-
annotations: { readOnlyHint:
|
|
45300
|
+
annotations: { readOnlyHint: true }
|
|
44662
45301
|
}, async () => {
|
|
44663
45302
|
const data = await client2.request("GET", "/pub/v1/users/useraccountstatus");
|
|
44664
45303
|
return jsonResponse(data);
|
|
@@ -44666,7 +45305,7 @@ function registerUserTools(server, client2) {
|
|
|
44666
45305
|
}
|
|
44667
45306
|
|
|
44668
45307
|
// src/tools/healthcheck.ts
|
|
44669
|
-
function registerHealthcheckTools(server, client2,
|
|
45308
|
+
function registerHealthcheckTools(server, client2, resolve4 = resolveAuth) {
|
|
44670
45309
|
registerCredentialHealthcheckTool({
|
|
44671
45310
|
server,
|
|
44672
45311
|
prefix: "ofw",
|
|
@@ -44677,7 +45316,7 @@ function registerHealthcheckTools(server, client2, resolve3 = resolveAuth) {
|
|
|
44677
45316
|
probePath: "/pub/v2/profiles",
|
|
44678
45317
|
resolveCredential: async () => {
|
|
44679
45318
|
try {
|
|
44680
|
-
const auth = await
|
|
45319
|
+
const auth = await resolve4();
|
|
44681
45320
|
return {
|
|
44682
45321
|
source: auth.source,
|
|
44683
45322
|
// Never the token. Expiry is the fact that explains a connector
|
|
@@ -46519,7 +47158,7 @@ async function extractAttachment(bytes, mimeType, fileName, opts = {}) {
|
|
|
46519
47158
|
|
|
46520
47159
|
// src/tools/attachments.ts
|
|
46521
47160
|
import { readFileSync as readFileSync2, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
46522
|
-
import { basename, dirname as
|
|
47161
|
+
import { basename, dirname as dirname4, extname } from "node:path";
|
|
46523
47162
|
var MIME_BY_EXT = {
|
|
46524
47163
|
".pdf": "application/pdf",
|
|
46525
47164
|
".png": "image/png",
|
|
@@ -46599,7 +47238,7 @@ var NodeAttachmentIO = class {
|
|
|
46599
47238
|
}
|
|
46600
47239
|
}
|
|
46601
47240
|
writeDownload(dest, bytes) {
|
|
46602
|
-
mkdirSync2(
|
|
47241
|
+
mkdirSync2(dirname4(dest), { recursive: true });
|
|
46603
47242
|
writeFileSync2(dest, bytes);
|
|
46604
47243
|
}
|
|
46605
47244
|
};
|
|
@@ -46943,7 +47582,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
46943
47582
|
});
|
|
46944
47583
|
server.registerTool("ofw_list_messages", {
|
|
46945
47584
|
description: 'List messages from the local OurFamilyWizard cache. Supports filtering by folder, date range, and a substring query on subject+body. Pagination is offset-based (1-based `page`) but if you know what you want (a date range, a topic), prefer the filters over walking pages \u2014 the cache may have 1000+ messages. Results are newest-first by default; `sort:"oldest"` starts at the old end of a range instead of paging to it. Returns an explicit `complete` boolean describing the RESULT SET: true means "this is every message on OurFamilyWizard matching these filters as of freshness.asOf" \u2014 check it before asserting a count. An empty result from a cache that is not verified-fresh is REFUSED (result:"UNVERIFIED_EMPTY") rather than reported as an absence; pass autoRefresh:true to sync and answer instead.',
|
|
46946
|
-
annotations: { readOnlyHint:
|
|
47585
|
+
annotations: { readOnlyHint: true },
|
|
46947
47586
|
inputSchema: external_exports.object({
|
|
46948
47587
|
folderId: external_exports.string().describe('Folder name: "inbox", "sent", or "both" (default "both")').optional(),
|
|
46949
47588
|
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
@@ -47038,7 +47677,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
47038
47677
|
});
|
|
47039
47678
|
server.registerTool("ofw_get_message", {
|
|
47040
47679
|
description: 'Get a single OurFamilyWizard message OR draft by ID. Reads from local cache when available; otherwise fetches from OFW \u2014 and for an UNREAD INBOX message that fetch marks it read and stamps a "First Viewed" time the co-parent can see, which is part of the record and cannot be undone. Pass allowMarkRead:false to refuse such a fetch instead (cached bodies, sent messages and already-read messages are unaffected, because none of them stamp anything). For ids that match a draft (in the drafts cache), the response carries folder="drafts" and the body/subject/recipients reflect the drafts cache (which ofw_sync_messages keeps fresh) \u2014 drafts have no `fromUser`, and `sentAt`/`fetchedBodyAt` mirror the draft\'s `modifiedAt`. For inbox/sent messages, folder is "inbox" or "sent" as before.',
|
|
47041
|
-
annotations: { readOnlyHint: false },
|
|
47680
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
47042
47681
|
inputSchema: external_exports.object({
|
|
47043
47682
|
messageId: external_exports.string().describe("Message ID (also accepts draft IDs \u2014 drafts are routed via the drafts cache)"),
|
|
47044
47683
|
allowMarkRead: external_exports.boolean().describe("Default true (the long-standing behaviour). Set false to refuse a fetch that would mark an unread INBOX message as READ on OurFamilyWizard \u2014 an irreversible, co-parent-visible change to the record. Reads that cannot stamp anything (a cached body, a sent message, an already-read message) still succeed. The server-wide OFW_ALLOW_MARK_READ=false is a ceiling this argument cannot raise.").optional(),
|
|
@@ -47433,7 +48072,7 @@ ${JSON.stringify(
|
|
|
47433
48072
|
}
|
|
47434
48073
|
server.registerTool("ofw_list_drafts", {
|
|
47435
48074
|
description: 'List draft messages, verified against OurFamilyWizard in ONE call: when the local drafts cache is not verified-fresh, a cheap drafts sync runs first by default (verify:true), so the answer is server-confirmed without a second call. Pass verify:false to answer purely from the cache (no OFW requests). Returns an explicit `complete` boolean describing the RESULT SET: true means "these are ALL the drafts on OurFamilyWizard as of freshness.asOf" \u2014 check it before saying "you have N drafts". Each draft carries its `draftKey` (stable across the create-then-delete churn of editing) when one is known. An empty result from a cache that is not verified-fresh is REFUSED (result:"UNVERIFIED_EMPTY"); pass autoRefresh:true to sync and answer instead.',
|
|
47436
|
-
annotations: { readOnlyHint: false },
|
|
48075
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
47437
48076
|
inputSchema: external_exports.object({
|
|
47438
48077
|
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
47439
48078
|
size: external_exports.number().int().min(1).describe("Drafts per page (default 50)").optional(),
|
|
@@ -47531,7 +48170,7 @@ ${JSON.stringify(
|
|
|
47531
48170
|
});
|
|
47532
48171
|
if (allowDrafts) server.registerTool("ofw_save_draft", {
|
|
47533
48172
|
description: "Save a message as a draft in OurFamilyWizard. RECIPIENTS: OurFamilyWizard does NOT persist recipients on drafts \u2014 recipientIds are accepted but the saved draft comes back with none (documented OFW behavior, noted once in the response, not warned about; supply recipientIds at send time instead). IDENTITY: the response leads with `draftKey`, the stable identity that survives editing \u2014 key off it, because the `id` changes on EVERY edit (replacing a draft creates a NEW draft and deletes the old one; OFW's update-in-place endpoint silently no-ops, so we never use it). Pass messageId to replace an existing draft; the response.id will be the NEW id, and a transparency NOTE documents the swap and which fields were carried over. THREADING: if replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included). The threading verdict is read from OFW's full echo (replyToId/inReplyTo/showContext) \u2014 a warning appears ONLY when the reply linkage was genuinely dropped or re-targeted, and the response's top-level replyToId/inReplyTo always agree with its listData. Attach files via myFileIDs (from ofw_upload_attachment). After saving, the tool re-fetches the draft from OFW, and the returned `revision` reflects that authoritative state (so it will match on your next edit). SAFETY: because replacing DESTROYS the old draft rather than merging, passing messageId first re-reads that draft from OFW and REFUSES the write if its subject/body/recipients changed since you read it (drafts edited in the OFW web app do not bump any timestamp, so the local cache can be silently behind). A pure replyToId normalization by OFW is NOT treated as a conflict. The refusal returns the current server body under serverBody \u2014 merge your edit into it and retry with expectedRevision.",
|
|
47534
|
-
annotations: { readOnlyHint: false },
|
|
48173
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
47535
48174
|
inputSchema: external_exports.object({
|
|
47536
48175
|
subject: external_exports.string().describe("Message subject"),
|
|
47537
48176
|
body: external_exports.string().describe("Message body text"),
|
|
@@ -47712,7 +48351,7 @@ ${text}` : text);
|
|
|
47712
48351
|
});
|
|
47713
48352
|
server.registerTool("ofw_get_unread_sent", {
|
|
47714
48353
|
description: 'List sent messages that have not been read by one or more recipients. Reads from local cache. Returns `complete` describing whether every sent message was scanned. An empty SENT cache that is not verified-fresh is REFUSED (result:"UNVERIFIED_EMPTY") rather than reported as "nothing sent"; pass autoRefresh:true to sync and answer instead.',
|
|
47715
|
-
annotations: { readOnlyHint:
|
|
48354
|
+
annotations: { readOnlyHint: true },
|
|
47716
48355
|
inputSchema: external_exports.object({
|
|
47717
48356
|
page: external_exports.number().int().min(1).describe("Page (default 1)").optional(),
|
|
47718
48357
|
size: external_exports.number().int().min(1).describe("Per page (default 50)").optional(),
|
|
@@ -47829,7 +48468,7 @@ ${text}` : text);
|
|
|
47829
48468
|
});
|
|
47830
48469
|
server.registerTool("ofw_download_attachment", {
|
|
47831
48470
|
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:
|
|
48471
|
+
annotations: { readOnlyHint: true },
|
|
47833
48472
|
inputSchema: external_exports.object({
|
|
47834
48473
|
fileId: external_exports.number().describe("Attachment file id (from ofw_get_message \u2192 attachments[].fileId)"),
|
|
47835
48474
|
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(),
|
|
@@ -47916,7 +48555,7 @@ ${text}` : text);
|
|
|
47916
48555
|
});
|
|
47917
48556
|
server.registerTool("ofw_sync_messages", {
|
|
47918
48557
|
description: "Sync messages from OurFamilyWizard into the local cache. Returns counts per folder and a list of unread inbox messages whose bodies were NOT fetched (to avoid mark-as-read on OFW). Call ofw_get_message(id) on those to read them. EVERY call re-checks the newest page first, so new messages are picked up promptly even while an old-history backfill is still running; only then does it spend what is left of its budget advancing that backfill. Pass deep:true to walk all OFW pages instead of stopping at the first all-cached page (use to backfill suspected gaps). Sync is BOUNDED and RESUMABLE: on hosted deployments a per-call OFW-request budget (env OFW_SYNC_MAX_REQUESTS, or the maxRequests argument) caps how far one call walks; when the budget is hit the response reports done:false with a note \u2014 call again with the SAME arguments to resume. done:false means older history is still being backfilled; it does NOT mean recent messages are missing. Local installs are unbounded by default (done is always true).",
|
|
47919
|
-
annotations: { readOnlyHint: false },
|
|
48558
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
47920
48559
|
inputSchema: external_exports.object({
|
|
47921
48560
|
folders: external_exports.array(external_exports.enum(["inbox", "sent", "drafts"])).min(1).describe("Folders to sync (default: all three). Must be non-empty if given \u2014 an empty list would sync nothing while reporting success.").optional(),
|
|
47922
48561
|
fetchUnreadBodies: external_exports.boolean().describe('If true, also fetch bodies for unread inbox messages \u2014 which marks each one READ on OurFamilyWizard and stamps a co-parent-visible "First Viewed" time that cannot be undone. Defaults to the OFW_FETCH_UNREAD_BODIES env var (false unless set), and is forced off entirely when OFW_ALLOW_MARK_READ=false.').optional(),
|
|
@@ -47942,7 +48581,7 @@ ${text}` : text);
|
|
|
47942
48581
|
});
|
|
47943
48582
|
server.registerTool("ofw_check_freshness", {
|
|
47944
48583
|
description: 'Cheaply confirm whether the local cache still matches OurFamilyWizard, WITHOUT running a full sync. Use this before asserting anything about current state \u2014 especially "draft X is still sitting unsent". Costs one OFW request for the folder check plus one per messageId. For each folder it returns the live server count next to the cached count. For each id it returns a LIVE lifecycle `state` \u2014 "draft" | "sent" | "received" | "deleted" | "unknown" \u2014 alongside `folder`, `sentAt`, `existsOnServer` and a content comparison. `state` is the field that answers "is this still a draft?": a draft that has been SENT still exists on the server, so existsOnServer:true never distinguished the two. A cached draft whose state is no longer "draft" reports inSync:false even when its text is byte-identical. Content is compared by revision hash, because OFW draft timestamps do NOT change when a draft is edited in the web app. Does not fetch bodies into the cache, does not touch attachments, and does not depend on sync state. For draftKeys, or a full live draft inventory, use ofw_status.',
|
|
47945
|
-
annotations: { readOnlyHint:
|
|
48584
|
+
annotations: { readOnlyHint: true },
|
|
47946
48585
|
inputSchema: external_exports.object({
|
|
47947
48586
|
folders: external_exports.array(external_exports.enum(["inbox", "sent", "drafts"])).min(1).describe("Folders to compare cached vs live counts for. Defaults to all three when messageIds is not given. Must be non-empty if given.").optional(),
|
|
47948
48587
|
messageIds: external_exports.array(external_exports.number()).describe(`Specific ids to verify against OFW (max ${MAX_FRESHNESS_IDS}). Ids cached as drafts, as sent messages, or as already-read inbox messages are probed freely \u2014 none of those can stamp the record. Anything else is skipped \u2014 see allowMarkRead.`).optional(),
|
|
@@ -48001,7 +48640,7 @@ ${text}` : text);
|
|
|
48001
48640
|
});
|
|
48002
48641
|
server.registerTool("ofw_status", {
|
|
48003
48642
|
description: 'ONE live call that answers "where does everything stand?". This is the call that should back any status summary about drafts or specific messages \u2014 never session memory, and never a cached read alone. With no arguments it returns the FULL current draft inventory, verified against OurFamilyWizard. Pass ids and/or draftKeys to get each one\'s live lifecycle `state` ("draft" | "sent" | "received" | "deleted" | "unknown") with `sentAt` and `viewedAt`. A draftKey is the stable identity ofw_save_draft returns: editing a draft mints a new OFW id every time (create-then-delete), so the key is the only way to ask "what happened to the thing I was working on?" \u2014 it resolves to the chain\'s current id and keeps resolving after the draft is SENT (state:"sent" with sentMessageId). The top-level `complete` is true ONLY when every part of this snapshot was verified live; if it is false, do not state a draft count or a lifecycle claim from this payload.',
|
|
48004
|
-
annotations: { readOnlyHint:
|
|
48643
|
+
annotations: { readOnlyHint: true },
|
|
48005
48644
|
inputSchema: external_exports.object({
|
|
48006
48645
|
ids: external_exports.array(external_exports.number()).describe(`Message/draft ids to resolve to a live state (combined with draftKeys, max ${MAX_FRESHNESS_IDS} probes per call).`).optional(),
|
|
48007
48646
|
draftKeys: external_exports.array(external_exports.string()).describe("Stable draft keys (from ofw_save_draft / ofw_list_drafts) to resolve to their CURRENT id and state.").optional(),
|
|
@@ -48378,8 +49017,8 @@ function registerJournalTools(server, client2) {
|
|
|
48378
49017
|
|
|
48379
49018
|
// src/cache/node.ts
|
|
48380
49019
|
import { DatabaseSync } from "node:sqlite";
|
|
48381
|
-
import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as
|
|
48382
|
-
import { dirname as
|
|
49020
|
+
import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync3 } from "node:fs";
|
|
49021
|
+
import { dirname as dirname5 } from "node:path";
|
|
48383
49022
|
|
|
48384
49023
|
// src/cache/store.ts
|
|
48385
49024
|
function rowFromDb(r) {
|
|
@@ -48963,10 +49602,10 @@ var NodeSqlDriver = class {
|
|
|
48963
49602
|
}
|
|
48964
49603
|
};
|
|
48965
49604
|
function enforceCachePermissions(dbPath) {
|
|
48966
|
-
chmodSync2(
|
|
49605
|
+
chmodSync2(dirname5(dbPath), 448);
|
|
48967
49606
|
chmodSync2(dbPath, 384);
|
|
48968
49607
|
for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
48969
|
-
if (
|
|
49608
|
+
if (existsSync3(sibling)) chmodSync2(sibling, 384);
|
|
48970
49609
|
}
|
|
48971
49610
|
}
|
|
48972
49611
|
var OFWCache = class _OFWCache extends LocalCacheStore {
|
|
@@ -48977,7 +49616,7 @@ var OFWCache = class _OFWCache extends LocalCacheStore {
|
|
|
48977
49616
|
db;
|
|
48978
49617
|
static open(path) {
|
|
48979
49618
|
const memory = path === ":memory:";
|
|
48980
|
-
if (!memory) mkdirSync3(
|
|
49619
|
+
if (!memory) mkdirSync3(dirname5(path), { recursive: true });
|
|
48981
49620
|
const db = new DatabaseSync(path);
|
|
48982
49621
|
if (!memory) enforceCachePermissions(path);
|
|
48983
49622
|
db.exec("PRAGMA journal_mode = WAL");
|
|
@@ -49007,7 +49646,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
49007
49646
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
49008
49647
|
await runMcp({
|
|
49009
49648
|
name: "ofw",
|
|
49010
|
-
version: "2.19.
|
|
49649
|
+
version: "2.19.2",
|
|
49011
49650
|
// x-release-please-version
|
|
49012
49651
|
deps: client,
|
|
49013
49652
|
tools: [
|