infinitecampus-mcp 2.4.4 → 2.5.0
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 +950 -158
- package/dist/client.js +161 -13
- package/dist/index.js +1 -1
- package/dist/session-cache.js +95 -0
- package/mint.yaml +72 -0
- package/package.json +10 -7
- package/server.json +14 -2
package/dist/bundle.js
CHANGED
|
@@ -2991,7 +2991,7 @@ var require_compile = __commonJS({
|
|
|
2991
2991
|
const schOrFunc = root.refs[ref];
|
|
2992
2992
|
if (schOrFunc)
|
|
2993
2993
|
return schOrFunc;
|
|
2994
|
-
let _sch =
|
|
2994
|
+
let _sch = resolve2.call(this, root, ref);
|
|
2995
2995
|
if (_sch === void 0) {
|
|
2996
2996
|
const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
2997
2997
|
const { schemaId } = this.opts;
|
|
@@ -3018,7 +3018,7 @@ var require_compile = __commonJS({
|
|
|
3018
3018
|
function sameSchemaEnv(s1, s2) {
|
|
3019
3019
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3020
3020
|
}
|
|
3021
|
-
function
|
|
3021
|
+
function resolve2(root, ref) {
|
|
3022
3022
|
let sch;
|
|
3023
3023
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3024
3024
|
ref = sch;
|
|
@@ -3649,9 +3649,14 @@ var require_fast_uri = __commonJS({
|
|
|
3649
3649
|
}
|
|
3650
3650
|
return uri;
|
|
3651
3651
|
}
|
|
3652
|
-
function
|
|
3652
|
+
function resolve2(baseURI, relativeURI, options) {
|
|
3653
3653
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3654
|
-
const
|
|
3654
|
+
const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
|
|
3655
|
+
const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
|
|
3656
|
+
if (baseMalformed || relativeMalformed) {
|
|
3657
|
+
throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
|
|
3658
|
+
}
|
|
3659
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
|
|
3655
3660
|
schemelessOptions.skipEscape = true;
|
|
3656
3661
|
return serialize(resolved, schemelessOptions);
|
|
3657
3662
|
}
|
|
@@ -3777,6 +3782,7 @@ var require_fast_uri = __commonJS({
|
|
|
3777
3782
|
}
|
|
3778
3783
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3779
3784
|
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
3785
|
+
var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
|
|
3780
3786
|
function getParseError(parsed, matches) {
|
|
3781
3787
|
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3782
3788
|
return 'URI path must start with "/" when authority is present.';
|
|
@@ -3811,6 +3817,20 @@ var require_fast_uri = __commonJS({
|
|
|
3811
3817
|
parsed.error = "URI authority must not contain a literal backslash.";
|
|
3812
3818
|
malformedAuthorityOrPort = true;
|
|
3813
3819
|
}
|
|
3820
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
|
|
3821
|
+
if (introducerMatch !== null) {
|
|
3822
|
+
const region = introducerMatch[1];
|
|
3823
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, "");
|
|
3824
|
+
if (normalizedRegion.length >= 2) {
|
|
3825
|
+
if (normalizedRegion.slice(0, 2) !== "//") {
|
|
3826
|
+
parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
|
|
3827
|
+
malformedAuthorityOrPort = true;
|
|
3828
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
3829
|
+
parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
|
|
3830
|
+
malformedAuthorityOrPort = true;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3814
3834
|
const matches = uri.match(URI_PARSE);
|
|
3815
3835
|
if (matches) {
|
|
3816
3836
|
parsed.scheme = matches[1];
|
|
@@ -3913,7 +3933,7 @@ var require_fast_uri = __commonJS({
|
|
|
3913
3933
|
var fastUri = {
|
|
3914
3934
|
SCHEMES,
|
|
3915
3935
|
normalize,
|
|
3916
|
-
resolve,
|
|
3936
|
+
resolve: resolve2,
|
|
3917
3937
|
resolveComponent,
|
|
3918
3938
|
equal,
|
|
3919
3939
|
serialize,
|
|
@@ -9141,7 +9161,7 @@ var require_websocket = __commonJS({
|
|
|
9141
9161
|
var http = __require("http");
|
|
9142
9162
|
var net = __require("net");
|
|
9143
9163
|
var tls = __require("tls");
|
|
9144
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
9164
|
+
var { randomBytes: randomBytes2, createHash } = __require("crypto");
|
|
9145
9165
|
var { Duplex, Readable } = __require("stream");
|
|
9146
9166
|
var { URL: URL2 } = __require("url");
|
|
9147
9167
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -9679,7 +9699,7 @@ var require_websocket = __commonJS({
|
|
|
9679
9699
|
}
|
|
9680
9700
|
}
|
|
9681
9701
|
const defaultPort = isSecure ? 443 : 80;
|
|
9682
|
-
const key =
|
|
9702
|
+
const key = randomBytes2(16).toString("base64");
|
|
9683
9703
|
const request = isSecure ? https.request : http.request;
|
|
9684
9704
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
9685
9705
|
let perMessageDeflate;
|
|
@@ -10573,7 +10593,7 @@ var require_websocket_server = __commonJS({
|
|
|
10573
10593
|
});
|
|
10574
10594
|
|
|
10575
10595
|
// src/index.ts
|
|
10576
|
-
import { dirname as
|
|
10596
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
10577
10597
|
import { fileURLToPath } from "url";
|
|
10578
10598
|
|
|
10579
10599
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -32534,7 +32554,7 @@ var Protocol = class {
|
|
|
32534
32554
|
return;
|
|
32535
32555
|
}
|
|
32536
32556
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
32537
|
-
await new Promise((
|
|
32557
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
32538
32558
|
options?.signal?.throwIfAborted();
|
|
32539
32559
|
}
|
|
32540
32560
|
} catch (error51) {
|
|
@@ -32551,7 +32571,7 @@ var Protocol = class {
|
|
|
32551
32571
|
*/
|
|
32552
32572
|
request(request, resultSchema, options) {
|
|
32553
32573
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
32554
|
-
return new Promise((
|
|
32574
|
+
return new Promise((resolve2, reject) => {
|
|
32555
32575
|
const earlyReject = (error51) => {
|
|
32556
32576
|
reject(error51);
|
|
32557
32577
|
};
|
|
@@ -32629,7 +32649,7 @@ var Protocol = class {
|
|
|
32629
32649
|
if (!parseResult.success) {
|
|
32630
32650
|
reject(parseResult.error);
|
|
32631
32651
|
} else {
|
|
32632
|
-
|
|
32652
|
+
resolve2(parseResult.data);
|
|
32633
32653
|
}
|
|
32634
32654
|
} catch (error51) {
|
|
32635
32655
|
reject(error51);
|
|
@@ -32890,12 +32910,12 @@ var Protocol = class {
|
|
|
32890
32910
|
}
|
|
32891
32911
|
} catch {
|
|
32892
32912
|
}
|
|
32893
|
-
return new Promise((
|
|
32913
|
+
return new Promise((resolve2, reject) => {
|
|
32894
32914
|
if (signal.aborted) {
|
|
32895
32915
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
32896
32916
|
return;
|
|
32897
32917
|
}
|
|
32898
|
-
const timeoutId = setTimeout(
|
|
32918
|
+
const timeoutId = setTimeout(resolve2, interval);
|
|
32899
32919
|
signal.addEventListener("abort", () => {
|
|
32900
32920
|
clearTimeout(timeoutId);
|
|
32901
32921
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -33986,7 +34006,7 @@ var McpServer = class {
|
|
|
33986
34006
|
let task = createTaskResult.task;
|
|
33987
34007
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
33988
34008
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
33989
|
-
await new Promise((
|
|
34009
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
33990
34010
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
33991
34011
|
if (!updatedTask) {
|
|
33992
34012
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -34650,20 +34670,95 @@ var StdioServerTransport = class {
|
|
|
34650
34670
|
this.onclose?.();
|
|
34651
34671
|
}
|
|
34652
34672
|
send(message) {
|
|
34653
|
-
return new Promise((
|
|
34673
|
+
return new Promise((resolve2) => {
|
|
34654
34674
|
const json2 = serializeMessage(message);
|
|
34655
34675
|
if (this._stdout.write(json2)) {
|
|
34656
|
-
|
|
34676
|
+
resolve2();
|
|
34657
34677
|
} else {
|
|
34658
|
-
this._stdout.once("drain",
|
|
34678
|
+
this._stdout.once("drain", resolve2);
|
|
34659
34679
|
}
|
|
34660
34680
|
});
|
|
34661
34681
|
}
|
|
34662
34682
|
};
|
|
34663
34683
|
|
|
34684
|
+
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
34685
|
+
var McpToolError = class extends Error {
|
|
34686
|
+
/** Actionable remediation text, when one applies. */
|
|
34687
|
+
hint;
|
|
34688
|
+
constructor(message, opts) {
|
|
34689
|
+
super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
34690
|
+
this.name = "McpToolError";
|
|
34691
|
+
if (opts?.hint !== void 0)
|
|
34692
|
+
this.hint = opts.hint;
|
|
34693
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
34694
|
+
}
|
|
34695
|
+
};
|
|
34696
|
+
var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
34697
|
+
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
34698
|
+
var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
|
|
34699
|
+
var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
|
|
34700
|
+
var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
|
|
34701
|
+
var API_KEY_RE = new RegExp([
|
|
34702
|
+
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
34703
|
+
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
34704
|
+
"gh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
34705
|
+
// GitHub ghp_/gho_/ghu_/ghs_/ghr_
|
|
34706
|
+
"xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
|
|
34707
|
+
// Slack
|
|
34708
|
+
"AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
|
|
34709
|
+
// Google API key (39 chars total)
|
|
34710
|
+
"AKIA[0-9A-Z]{16}\\b",
|
|
34711
|
+
// AWS access key id (20 chars total)
|
|
34712
|
+
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
34713
|
+
// webhook signing secret (Stripe-style)
|
|
34714
|
+
].map((p) => `\\b${p}`).join("|"), "g");
|
|
34715
|
+
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
34716
|
+
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
34717
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
34718
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
34719
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
34720
|
+
function redactSecrets(text) {
|
|
34721
|
+
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]");
|
|
34722
|
+
}
|
|
34723
|
+
|
|
34724
|
+
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34725
|
+
function textResult(data) {
|
|
34726
|
+
return {
|
|
34727
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
34728
|
+
};
|
|
34729
|
+
}
|
|
34730
|
+
function errorResult(message) {
|
|
34731
|
+
return {
|
|
34732
|
+
content: [{ type: "text", text: redactSecrets(message) }],
|
|
34733
|
+
isError: true
|
|
34734
|
+
};
|
|
34735
|
+
}
|
|
34736
|
+
|
|
34664
34737
|
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
34738
|
+
function hintResultOrRethrow(err) {
|
|
34739
|
+
if (err instanceof McpToolError && err.hint) {
|
|
34740
|
+
return errorResult(`${err.message}
|
|
34741
|
+
|
|
34742
|
+
Hint: ${err.hint}`);
|
|
34743
|
+
}
|
|
34744
|
+
throw err;
|
|
34745
|
+
}
|
|
34746
|
+
function surfaceToolHints(server) {
|
|
34747
|
+
const register = server.registerTool.bind(server);
|
|
34748
|
+
server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
|
|
34749
|
+
let result;
|
|
34750
|
+
try {
|
|
34751
|
+
result = cb(...args);
|
|
34752
|
+
} catch (err) {
|
|
34753
|
+
return hintResultOrRethrow(err);
|
|
34754
|
+
}
|
|
34755
|
+
return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
|
|
34756
|
+
});
|
|
34757
|
+
}
|
|
34665
34758
|
async function createMcpServer(opts) {
|
|
34666
34759
|
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
34760
|
+
if (opts.surfaceHints !== false)
|
|
34761
|
+
surfaceToolHints(server);
|
|
34667
34762
|
if (opts.banner !== void 0) {
|
|
34668
34763
|
console.error(opts.banner);
|
|
34669
34764
|
}
|
|
@@ -34708,32 +34803,6 @@ async function runMcp(opts) {
|
|
|
34708
34803
|
return server;
|
|
34709
34804
|
}
|
|
34710
34805
|
|
|
34711
|
-
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
34712
|
-
var API_KEY_RE = new RegExp([
|
|
34713
|
-
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
34714
|
-
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
34715
|
-
"gh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
34716
|
-
// GitHub ghp_/gho_/ghu_/ghs_/ghr_
|
|
34717
|
-
"xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
|
|
34718
|
-
// Slack
|
|
34719
|
-
"AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
|
|
34720
|
-
// Google API key (39 chars total)
|
|
34721
|
-
"AKIA[0-9A-Z]{16}\\b",
|
|
34722
|
-
// AWS access key id (20 chars total)
|
|
34723
|
-
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
34724
|
-
// webhook signing secret (Stripe-style)
|
|
34725
|
-
].map((p) => `\\b${p}`).join("|"), "g");
|
|
34726
|
-
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
34727
|
-
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
34728
|
-
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
34729
|
-
|
|
34730
|
-
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34731
|
-
function textResult(data) {
|
|
34732
|
-
return {
|
|
34733
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
34734
|
-
};
|
|
34735
|
-
}
|
|
34736
|
-
|
|
34737
34806
|
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
34738
34807
|
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
34739
34808
|
function readEnvVar(key, opts = {}) {
|
|
@@ -34854,8 +34923,15 @@ var pageSchema = {
|
|
|
34854
34923
|
};
|
|
34855
34924
|
|
|
34856
34925
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
34857
|
-
var PROTOCOL_VERSION =
|
|
34926
|
+
var PROTOCOL_VERSION = 3;
|
|
34858
34927
|
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
34928
|
+
function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
|
|
34929
|
+
const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
|
|
34930
|
+
out.set(mcpHelloNonce, 0);
|
|
34931
|
+
out.set(extHelloNonce, mcpHelloNonce.length);
|
|
34932
|
+
out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
|
|
34933
|
+
return out;
|
|
34934
|
+
}
|
|
34859
34935
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
34860
34936
|
"fetch",
|
|
34861
34937
|
"read_cookies",
|
|
@@ -34866,7 +34942,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34866
34942
|
"read_indexed_db",
|
|
34867
34943
|
"read_dom",
|
|
34868
34944
|
"download",
|
|
34869
|
-
"graphql"
|
|
34945
|
+
"graphql",
|
|
34946
|
+
"write_cookies"
|
|
34870
34947
|
]);
|
|
34871
34948
|
|
|
34872
34949
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35439,6 +35516,34 @@ function validateInnerRequest(raw) {
|
|
|
35439
35516
|
}
|
|
35440
35517
|
return raw;
|
|
35441
35518
|
}
|
|
35519
|
+
if (raw.op === "write_cookies") {
|
|
35520
|
+
assertObject(raw.init, "inner.init");
|
|
35521
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35522
|
+
if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
|
|
35523
|
+
throw new ProtocolError("inner.init.cookies: must be a non-empty array");
|
|
35524
|
+
}
|
|
35525
|
+
for (const [i, entry] of raw.init.cookies.entries()) {
|
|
35526
|
+
assertObject(entry, `inner.init.cookies[${i}]`);
|
|
35527
|
+
assertString(entry.name, `inner.init.cookies[${i}].name`);
|
|
35528
|
+
if (!SCOPE_KEY_RE.test(entry.name)) {
|
|
35529
|
+
throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
|
|
35530
|
+
}
|
|
35531
|
+
assertString(entry.value, `inner.init.cookies[${i}].value`);
|
|
35532
|
+
for (const k of Object.keys(entry)) {
|
|
35533
|
+
if (k !== "name" && k !== "value") {
|
|
35534
|
+
throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35535
|
+
}
|
|
35536
|
+
}
|
|
35537
|
+
}
|
|
35538
|
+
if (raw.init.path !== void 0)
|
|
35539
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35540
|
+
for (const k of Object.keys(raw.init)) {
|
|
35541
|
+
if (k !== "origin" && k !== "cookies" && k !== "path") {
|
|
35542
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
|
|
35543
|
+
}
|
|
35544
|
+
}
|
|
35545
|
+
return raw;
|
|
35546
|
+
}
|
|
35442
35547
|
if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
|
|
35443
35548
|
assertObject(raw.init, "inner.init");
|
|
35444
35549
|
if (raw.init.origin === void 0) {
|
|
@@ -35625,7 +35730,7 @@ function validateInnerRequest(raw) {
|
|
|
35625
35730
|
}
|
|
35626
35731
|
return raw;
|
|
35627
35732
|
}
|
|
35628
|
-
throw new ProtocolError(`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"; got ${JSON.stringify(raw.op)}`);
|
|
35733
|
+
throw new ProtocolError(`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)}`);
|
|
35629
35734
|
}
|
|
35630
35735
|
function assertNonEmptyKeyArray(value, label) {
|
|
35631
35736
|
if (!Array.isArray(value)) {
|
|
@@ -35686,6 +35791,18 @@ function validateInnerResponse(raw) {
|
|
|
35686
35791
|
}
|
|
35687
35792
|
return raw;
|
|
35688
35793
|
}
|
|
35794
|
+
if (op === "write_cookies") {
|
|
35795
|
+
if (raw.written === void 0) {
|
|
35796
|
+
throw new ProtocolError("inner.written: missing on write_cookies response");
|
|
35797
|
+
}
|
|
35798
|
+
if (!Array.isArray(raw.written)) {
|
|
35799
|
+
throw new ProtocolError("inner.written: must be an array");
|
|
35800
|
+
}
|
|
35801
|
+
for (const [i, name] of raw.written.entries()) {
|
|
35802
|
+
assertString(name, `inner.written[${i}]`);
|
|
35803
|
+
}
|
|
35804
|
+
return raw;
|
|
35805
|
+
}
|
|
35689
35806
|
if (op === "read_local_storage" || op === "read_session_storage") {
|
|
35690
35807
|
if (raw.values === void 0) {
|
|
35691
35808
|
throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
|
|
@@ -35979,7 +36096,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
|
|
|
35979
36096
|
async function electRole(opts) {
|
|
35980
36097
|
const server = createServer();
|
|
35981
36098
|
const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
|
|
35982
|
-
return new Promise((
|
|
36099
|
+
return new Promise((resolve2, reject) => {
|
|
35983
36100
|
let timer;
|
|
35984
36101
|
const clearBindTimer = () => {
|
|
35985
36102
|
if (timer) {
|
|
@@ -35995,7 +36112,7 @@ async function electRole(opts) {
|
|
|
35995
36112
|
server.close();
|
|
35996
36113
|
} catch {
|
|
35997
36114
|
}
|
|
35998
|
-
|
|
36115
|
+
resolve2({ role: "peer" });
|
|
35999
36116
|
} else {
|
|
36000
36117
|
reject(e);
|
|
36001
36118
|
}
|
|
@@ -36003,7 +36120,7 @@ async function electRole(opts) {
|
|
|
36003
36120
|
const onListening = () => {
|
|
36004
36121
|
clearBindTimer();
|
|
36005
36122
|
server.removeListener("error", onError);
|
|
36006
|
-
|
|
36123
|
+
resolve2({ role: "host", server });
|
|
36007
36124
|
};
|
|
36008
36125
|
server.once("error", onError);
|
|
36009
36126
|
server.once("listening", onListening);
|
|
@@ -36163,6 +36280,148 @@ async function awaitSessionReady(ready, opts) {
|
|
|
36163
36280
|
}
|
|
36164
36281
|
}
|
|
36165
36282
|
|
|
36283
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36284
|
+
import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
36285
|
+
import { join as join2 } from "node:path";
|
|
36286
|
+
|
|
36287
|
+
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36288
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36289
|
+
import { join } from "node:path";
|
|
36290
|
+
import { homedir } from "node:os";
|
|
36291
|
+
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36292
|
+
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36293
|
+
function defaultIdentityDir() {
|
|
36294
|
+
return join(homedir(), ".fetchproxy", "identity");
|
|
36295
|
+
}
|
|
36296
|
+
function safeIdentityFileBase(serverName) {
|
|
36297
|
+
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36298
|
+
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36299
|
+
}
|
|
36300
|
+
return serverName.replace(/\//g, "_");
|
|
36301
|
+
}
|
|
36302
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36303
|
+
const safeFile = safeIdentityFileBase(serverName);
|
|
36304
|
+
const path = join(dir, `${safeFile}.json`);
|
|
36305
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36306
|
+
try {
|
|
36307
|
+
const raw = await readFile(path, "utf8");
|
|
36308
|
+
const j2 = JSON.parse(raw);
|
|
36309
|
+
return {
|
|
36310
|
+
x25519Priv: fromB64(j2.x25519Priv),
|
|
36311
|
+
x25519Pub: fromB64(j2.x25519Pub),
|
|
36312
|
+
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36313
|
+
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36314
|
+
createdAt: j2.createdAt
|
|
36315
|
+
};
|
|
36316
|
+
} catch (e) {
|
|
36317
|
+
if (e.code !== "ENOENT")
|
|
36318
|
+
throw e;
|
|
36319
|
+
}
|
|
36320
|
+
const x = await generateX25519();
|
|
36321
|
+
const ed = await generateEd25519();
|
|
36322
|
+
const id = {
|
|
36323
|
+
x25519Priv: x.privateKey,
|
|
36324
|
+
x25519Pub: x.publicKey,
|
|
36325
|
+
ed25519Priv: ed.privateKey,
|
|
36326
|
+
ed25519Pub: ed.publicKey,
|
|
36327
|
+
createdAt: Date.now()
|
|
36328
|
+
};
|
|
36329
|
+
const j = {
|
|
36330
|
+
x25519Priv: toB64(id.x25519Priv),
|
|
36331
|
+
x25519Pub: toB64(id.x25519Pub),
|
|
36332
|
+
ed25519Priv: toB64(id.ed25519Priv),
|
|
36333
|
+
ed25519Pub: toB64(id.ed25519Pub),
|
|
36334
|
+
createdAt: id.createdAt
|
|
36335
|
+
};
|
|
36336
|
+
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36337
|
+
await chmod(path, 384);
|
|
36338
|
+
return id;
|
|
36339
|
+
}
|
|
36340
|
+
|
|
36341
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36342
|
+
function fileExtensionTrust(args) {
|
|
36343
|
+
return {
|
|
36344
|
+
allowNew: args.allowNew,
|
|
36345
|
+
location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36346
|
+
read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36347
|
+
write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
|
|
36348
|
+
};
|
|
36349
|
+
}
|
|
36350
|
+
var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
|
|
36351
|
+
function allowNewExtensionIdentity(explicit, env = process.env) {
|
|
36352
|
+
if (explicit !== void 0)
|
|
36353
|
+
return explicit;
|
|
36354
|
+
return env[TRUST_NEW_EXTENSION_ENV] === "1";
|
|
36355
|
+
}
|
|
36356
|
+
function decideExtensionTrust(args) {
|
|
36357
|
+
const { pin, hello, allowNew, serverName } = args;
|
|
36358
|
+
if (!pin)
|
|
36359
|
+
return { decision: "first-use" };
|
|
36360
|
+
if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
|
|
36361
|
+
return { decision: "pinned" };
|
|
36362
|
+
}
|
|
36363
|
+
const trustPath = args.location ?? extensionTrustPathHint(serverName);
|
|
36364
|
+
if (allowNew) {
|
|
36365
|
+
return {
|
|
36366
|
+
decision: "replace",
|
|
36367
|
+
message: `[fetchproxy] ${serverName}: accepting a NEW extension identity because ${TRUST_NEW_EXTENSION_ENV}=1 \u2014 re-pinning. Unset it once the browser you expect is connected.`
|
|
36368
|
+
};
|
|
36369
|
+
}
|
|
36370
|
+
return {
|
|
36371
|
+
decision: "refused",
|
|
36372
|
+
message: `[fetchproxy] ${serverName}: refusing an extension whose identity is not the one this MCP paired with. If you re-installed the extension or moved to another browser, re-pair deliberately: run this MCP once with ${TRUST_NEW_EXTENSION_ENV}=1, or delete ${trustPath}. If you did neither, something else is answering as your browser.`
|
|
36373
|
+
};
|
|
36374
|
+
}
|
|
36375
|
+
function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
|
|
36376
|
+
return join2(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
|
|
36377
|
+
}
|
|
36378
|
+
function extensionTrustPathHint(serverName) {
|
|
36379
|
+
try {
|
|
36380
|
+
return extensionTrustPath(serverName);
|
|
36381
|
+
} catch {
|
|
36382
|
+
return join2(defaultIdentityDir(), "<server-name>.extension-trust.json");
|
|
36383
|
+
}
|
|
36384
|
+
}
|
|
36385
|
+
function isPin(x) {
|
|
36386
|
+
if (!x || typeof x !== "object")
|
|
36387
|
+
return false;
|
|
36388
|
+
const r = x;
|
|
36389
|
+
return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
|
|
36390
|
+
}
|
|
36391
|
+
async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
|
|
36392
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36393
|
+
let raw;
|
|
36394
|
+
try {
|
|
36395
|
+
raw = await readFile2(path, "utf8");
|
|
36396
|
+
} catch (e) {
|
|
36397
|
+
if (e.code === "ENOENT")
|
|
36398
|
+
return null;
|
|
36399
|
+
throw e;
|
|
36400
|
+
}
|
|
36401
|
+
let parsed;
|
|
36402
|
+
try {
|
|
36403
|
+
parsed = JSON.parse(raw);
|
|
36404
|
+
} catch {
|
|
36405
|
+
throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
|
|
36406
|
+
}
|
|
36407
|
+
if (!isPin(parsed)) {
|
|
36408
|
+
throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
|
|
36409
|
+
}
|
|
36410
|
+
return {
|
|
36411
|
+
identityX25519Pub: parsed.identityX25519Pub,
|
|
36412
|
+
identityEd25519Pub: parsed.identityEd25519Pub,
|
|
36413
|
+
pinnedAt: parsed.pinnedAt
|
|
36414
|
+
};
|
|
36415
|
+
}
|
|
36416
|
+
async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
|
|
36417
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36418
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
36419
|
+
const tmp = `${path}.tmp`;
|
|
36420
|
+
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
|
|
36421
|
+
await chmod2(tmp, 384);
|
|
36422
|
+
await rename(tmp, path);
|
|
36423
|
+
}
|
|
36424
|
+
|
|
36166
36425
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
36167
36426
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
36168
36427
|
var enc2 = new TextEncoder();
|
|
@@ -36207,8 +36466,8 @@ async function startHost(opts) {
|
|
|
36207
36466
|
let rejectOwnSession;
|
|
36208
36467
|
let ownSessionReady;
|
|
36209
36468
|
function resetSessionPromise() {
|
|
36210
|
-
ownSessionReady = new Promise((
|
|
36211
|
-
resolveOwnSession =
|
|
36469
|
+
ownSessionReady = new Promise((resolve2, reject) => {
|
|
36470
|
+
resolveOwnSession = resolve2;
|
|
36212
36471
|
rejectOwnSession = reject;
|
|
36213
36472
|
});
|
|
36214
36473
|
ownSessionReady.catch(() => {
|
|
@@ -36216,9 +36475,12 @@ async function startHost(opts) {
|
|
|
36216
36475
|
}
|
|
36217
36476
|
resetSessionPromise();
|
|
36218
36477
|
let extensionHello = null;
|
|
36478
|
+
let extensionClaim = null;
|
|
36219
36479
|
wss.on("connection", (ws) => {
|
|
36220
36480
|
let identified = null;
|
|
36221
36481
|
let peerMcpId = null;
|
|
36482
|
+
let closed = false;
|
|
36483
|
+
let pinOnReady = false;
|
|
36222
36484
|
ws.on("message", async (data) => {
|
|
36223
36485
|
try {
|
|
36224
36486
|
let frame;
|
|
@@ -36230,10 +36492,43 @@ async function startHost(opts) {
|
|
|
36230
36492
|
return;
|
|
36231
36493
|
}
|
|
36232
36494
|
if (frame.type === "hello" && frame.role === "extension") {
|
|
36233
|
-
if (extensionWs) {
|
|
36495
|
+
if (extensionWs || extensionClaim) {
|
|
36234
36496
|
ws.close(1008, "extension already connected");
|
|
36235
36497
|
return;
|
|
36236
36498
|
}
|
|
36499
|
+
extensionClaim = ws;
|
|
36500
|
+
let pin;
|
|
36501
|
+
try {
|
|
36502
|
+
pin = await opts.extensionTrust.read();
|
|
36503
|
+
} catch (e) {
|
|
36504
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36505
|
+
if (extensionClaim === ws)
|
|
36506
|
+
extensionClaim = null;
|
|
36507
|
+
ws.close(1008, "extension pin unreadable");
|
|
36508
|
+
return;
|
|
36509
|
+
}
|
|
36510
|
+
const outcome = decideExtensionTrust({
|
|
36511
|
+
pin,
|
|
36512
|
+
hello: frame,
|
|
36513
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36514
|
+
serverName: opts.ownServerName,
|
|
36515
|
+
location: opts.extensionTrust.location
|
|
36516
|
+
});
|
|
36517
|
+
if (outcome.decision === "refused") {
|
|
36518
|
+
console.warn(outcome.message);
|
|
36519
|
+
if (extensionClaim === ws)
|
|
36520
|
+
extensionClaim = null;
|
|
36521
|
+
ws.close(1008, "extension identity is not the pinned one");
|
|
36522
|
+
return;
|
|
36523
|
+
}
|
|
36524
|
+
if (outcome.decision === "replace")
|
|
36525
|
+
console.warn(outcome.message);
|
|
36526
|
+
if (closed || ws.readyState !== import_websocket.default.OPEN) {
|
|
36527
|
+
if (extensionClaim === ws)
|
|
36528
|
+
extensionClaim = null;
|
|
36529
|
+
return;
|
|
36530
|
+
}
|
|
36531
|
+
pinOnReady = outcome.decision !== "pinned";
|
|
36237
36532
|
identified = "extension";
|
|
36238
36533
|
extensionWs = ws;
|
|
36239
36534
|
extensionHello = frame;
|
|
@@ -36245,6 +36540,8 @@ async function startHost(opts) {
|
|
|
36245
36540
|
console.error("[fetchproxy] onPairCode threw:", e);
|
|
36246
36541
|
}
|
|
36247
36542
|
}
|
|
36543
|
+
for (const slot of peers.values())
|
|
36544
|
+
slot.ws.send(JSON.stringify(frame));
|
|
36248
36545
|
ws.send(JSON.stringify(ownHello));
|
|
36249
36546
|
for (const slot of peers.values()) {
|
|
36250
36547
|
ws.send(JSON.stringify(slot.helloFrame));
|
|
@@ -36280,6 +36577,8 @@ async function startHost(opts) {
|
|
|
36280
36577
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
36281
36578
|
if (extensionWs)
|
|
36282
36579
|
extensionWs.send(JSON.stringify(frame));
|
|
36580
|
+
if (extensionHello)
|
|
36581
|
+
ws.send(JSON.stringify(extensionHello));
|
|
36283
36582
|
return;
|
|
36284
36583
|
}
|
|
36285
36584
|
if (frame.type === "ready") {
|
|
@@ -36291,7 +36590,7 @@ async function startHost(opts) {
|
|
|
36291
36590
|
}
|
|
36292
36591
|
const extEdPub = fromB64(extensionHello.identityEd25519Pub);
|
|
36293
36592
|
const extNonce = fromB64(extensionHello.sessionNonce);
|
|
36294
|
-
const msg =
|
|
36593
|
+
const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
|
|
36295
36594
|
const sig = fromB64(frame.sessionSig);
|
|
36296
36595
|
let sigOk = false;
|
|
36297
36596
|
try {
|
|
@@ -36304,6 +36603,18 @@ async function startHost(opts) {
|
|
|
36304
36603
|
ws.close(1008, "extension session signature invalid");
|
|
36305
36604
|
return;
|
|
36306
36605
|
}
|
|
36606
|
+
if (pinOnReady) {
|
|
36607
|
+
pinOnReady = false;
|
|
36608
|
+
try {
|
|
36609
|
+
await opts.extensionTrust.write({
|
|
36610
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36611
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36612
|
+
pinnedAt: Date.now()
|
|
36613
|
+
});
|
|
36614
|
+
} catch (e) {
|
|
36615
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36616
|
+
}
|
|
36617
|
+
}
|
|
36307
36618
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36308
36619
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
36309
36620
|
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36357,6 +36668,9 @@ async function startHost(opts) {
|
|
|
36357
36668
|
}
|
|
36358
36669
|
});
|
|
36359
36670
|
ws.on("close", () => {
|
|
36671
|
+
closed = true;
|
|
36672
|
+
if (extensionClaim === ws)
|
|
36673
|
+
extensionClaim = null;
|
|
36360
36674
|
if (identified === "extension" && extensionWs === ws) {
|
|
36361
36675
|
extensionWs = null;
|
|
36362
36676
|
extensionHello = null;
|
|
@@ -36374,7 +36688,7 @@ async function startHost(opts) {
|
|
|
36374
36688
|
});
|
|
36375
36689
|
});
|
|
36376
36690
|
return {
|
|
36377
|
-
close: () => new Promise((
|
|
36691
|
+
close: () => new Promise((resolve2) => {
|
|
36378
36692
|
for (const client of wss.clients) {
|
|
36379
36693
|
try {
|
|
36380
36694
|
client.terminate();
|
|
@@ -36382,7 +36696,7 @@ async function startHost(opts) {
|
|
|
36382
36696
|
}
|
|
36383
36697
|
}
|
|
36384
36698
|
wss.close(() => {
|
|
36385
|
-
opts.httpServer.close(() =>
|
|
36699
|
+
opts.httpServer.close(() => resolve2());
|
|
36386
36700
|
});
|
|
36387
36701
|
}),
|
|
36388
36702
|
sendOwnInner: async (inner) => {
|
|
@@ -36412,8 +36726,8 @@ async function startHost(opts) {
|
|
|
36412
36726
|
var enc3 = new TextEncoder();
|
|
36413
36727
|
async function startPeer(opts) {
|
|
36414
36728
|
const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
|
|
36415
|
-
await new Promise((
|
|
36416
|
-
ws.once("open", () =>
|
|
36729
|
+
await new Promise((resolve2, reject) => {
|
|
36730
|
+
ws.once("open", () => resolve2());
|
|
36417
36731
|
ws.once("error", reject);
|
|
36418
36732
|
});
|
|
36419
36733
|
const hello = await buildServerHello({
|
|
@@ -36443,15 +36757,88 @@ async function startPeer(opts) {
|
|
|
36443
36757
|
let pendingPairCode = null;
|
|
36444
36758
|
let resolveFirstReady;
|
|
36445
36759
|
let rejectFirstReady;
|
|
36446
|
-
const sessionPromise = new Promise((
|
|
36447
|
-
resolveFirstReady =
|
|
36760
|
+
const sessionPromise = new Promise((resolve2, reject) => {
|
|
36761
|
+
resolveFirstReady = resolve2;
|
|
36448
36762
|
rejectFirstReady = reject;
|
|
36449
36763
|
});
|
|
36764
|
+
let extensionHello = null;
|
|
36765
|
+
let warnedUnverifiable = false;
|
|
36766
|
+
let cachedPin = void 0;
|
|
36767
|
+
const authenticateExtension = async (sessionSig, extensionSessionPub) => {
|
|
36768
|
+
if (!extensionHello) {
|
|
36769
|
+
if (opts.requireExtensionIdentity) {
|
|
36770
|
+
console.error(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity, so this session cannot be verified \u2014 refusing. Upgrade the MCP holding the bridge port to 1.12.0 or later.`);
|
|
36771
|
+
return false;
|
|
36772
|
+
}
|
|
36773
|
+
if (!warnedUnverifiable) {
|
|
36774
|
+
warnedUnverifiable = true;
|
|
36775
|
+
console.warn(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity (pre-1.12.0), so this peer cannot verify which browser it is talking to. Upgrade the MCP holding the bridge port to close this.`);
|
|
36776
|
+
}
|
|
36777
|
+
return true;
|
|
36778
|
+
}
|
|
36779
|
+
const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
|
|
36780
|
+
let sigOk = false;
|
|
36781
|
+
try {
|
|
36782
|
+
sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
|
|
36783
|
+
} catch {
|
|
36784
|
+
sigOk = false;
|
|
36785
|
+
}
|
|
36786
|
+
if (!sigOk) {
|
|
36787
|
+
console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
|
|
36788
|
+
return false;
|
|
36789
|
+
}
|
|
36790
|
+
if (cachedPin === void 0) {
|
|
36791
|
+
try {
|
|
36792
|
+
cachedPin = await opts.extensionTrust.read();
|
|
36793
|
+
} catch (e) {
|
|
36794
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36795
|
+
return false;
|
|
36796
|
+
}
|
|
36797
|
+
}
|
|
36798
|
+
const pin = cachedPin;
|
|
36799
|
+
const outcome = decideExtensionTrust({
|
|
36800
|
+
pin,
|
|
36801
|
+
hello: extensionHello,
|
|
36802
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36803
|
+
serverName: opts.serverName,
|
|
36804
|
+
location: opts.extensionTrust.location
|
|
36805
|
+
});
|
|
36806
|
+
if (outcome.decision === "refused") {
|
|
36807
|
+
console.warn(outcome.message);
|
|
36808
|
+
return false;
|
|
36809
|
+
}
|
|
36810
|
+
if (outcome.decision === "replace")
|
|
36811
|
+
console.warn(outcome.message);
|
|
36812
|
+
if (outcome.decision !== "pinned") {
|
|
36813
|
+
try {
|
|
36814
|
+
const written = {
|
|
36815
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36816
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36817
|
+
pinnedAt: Date.now()
|
|
36818
|
+
};
|
|
36819
|
+
await opts.extensionTrust.write(written);
|
|
36820
|
+
cachedPin = written;
|
|
36821
|
+
} catch (e) {
|
|
36822
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36823
|
+
}
|
|
36824
|
+
}
|
|
36825
|
+
return true;
|
|
36826
|
+
};
|
|
36450
36827
|
const onMessage = async (data) => {
|
|
36451
36828
|
try {
|
|
36452
36829
|
const raw = JSON.parse(data.toString());
|
|
36453
36830
|
const frame = validateFrame(raw);
|
|
36831
|
+
if (frame.type === "hello" && frame.role === "extension") {
|
|
36832
|
+
extensionHello = frame;
|
|
36833
|
+
return;
|
|
36834
|
+
}
|
|
36454
36835
|
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
36836
|
+
const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
|
|
36837
|
+
if (!authorised) {
|
|
36838
|
+
ws.close(1008, "extension identity refused");
|
|
36839
|
+
rejectFirstReady(new Error("peer: extension identity refused"));
|
|
36840
|
+
return;
|
|
36841
|
+
}
|
|
36455
36842
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36456
36843
|
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
36457
36844
|
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36532,57 +36919,6 @@ async function startPeer(opts) {
|
|
|
36532
36919
|
return handle;
|
|
36533
36920
|
}
|
|
36534
36921
|
|
|
36535
|
-
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36536
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36537
|
-
import { join } from "node:path";
|
|
36538
|
-
import { homedir } from "node:os";
|
|
36539
|
-
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36540
|
-
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36541
|
-
function defaultIdentityDir() {
|
|
36542
|
-
return join(homedir(), ".fetchproxy", "identity");
|
|
36543
|
-
}
|
|
36544
|
-
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36545
|
-
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36546
|
-
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36547
|
-
}
|
|
36548
|
-
const safeFile = serverName.replace(/\//g, "_");
|
|
36549
|
-
const path = join(dir, `${safeFile}.json`);
|
|
36550
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36551
|
-
try {
|
|
36552
|
-
const raw = await readFile(path, "utf8");
|
|
36553
|
-
const j2 = JSON.parse(raw);
|
|
36554
|
-
return {
|
|
36555
|
-
x25519Priv: fromB64(j2.x25519Priv),
|
|
36556
|
-
x25519Pub: fromB64(j2.x25519Pub),
|
|
36557
|
-
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36558
|
-
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36559
|
-
createdAt: j2.createdAt
|
|
36560
|
-
};
|
|
36561
|
-
} catch (e) {
|
|
36562
|
-
if (e.code !== "ENOENT")
|
|
36563
|
-
throw e;
|
|
36564
|
-
}
|
|
36565
|
-
const x = await generateX25519();
|
|
36566
|
-
const ed = await generateEd25519();
|
|
36567
|
-
const id = {
|
|
36568
|
-
x25519Priv: x.privateKey,
|
|
36569
|
-
x25519Pub: x.publicKey,
|
|
36570
|
-
ed25519Priv: ed.privateKey,
|
|
36571
|
-
ed25519Pub: ed.publicKey,
|
|
36572
|
-
createdAt: Date.now()
|
|
36573
|
-
};
|
|
36574
|
-
const j = {
|
|
36575
|
-
x25519Priv: toB64(id.x25519Priv),
|
|
36576
|
-
x25519Pub: toB64(id.x25519Pub),
|
|
36577
|
-
ed25519Priv: toB64(id.ed25519Priv),
|
|
36578
|
-
ed25519Pub: toB64(id.ed25519Pub),
|
|
36579
|
-
createdAt: id.createdAt
|
|
36580
|
-
};
|
|
36581
|
-
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36582
|
-
await chmod(path, 384);
|
|
36583
|
-
return id;
|
|
36584
|
-
}
|
|
36585
|
-
|
|
36586
36922
|
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36587
36923
|
function classifyFetchError(error51) {
|
|
36588
36924
|
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
@@ -36623,6 +36959,17 @@ function classifyBridgeError(err) {
|
|
|
36623
36959
|
}
|
|
36624
36960
|
|
|
36625
36961
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
36962
|
+
function envWsPort() {
|
|
36963
|
+
const raw = process.env.FETCHPROXY_WS_PORT;
|
|
36964
|
+
if (raw === void 0 || raw.trim() === "")
|
|
36965
|
+
return void 0;
|
|
36966
|
+
if (!/^\d+$/.test(raw.trim()))
|
|
36967
|
+
return void 0;
|
|
36968
|
+
const port = Number(raw.trim());
|
|
36969
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
36970
|
+
return void 0;
|
|
36971
|
+
return port;
|
|
36972
|
+
}
|
|
36626
36973
|
var FetchproxyProtocolError = class extends Error {
|
|
36627
36974
|
constructor(message) {
|
|
36628
36975
|
super(message);
|
|
@@ -36664,21 +37011,38 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
36664
37011
|
this.hint = hint;
|
|
36665
37012
|
}
|
|
36666
37013
|
};
|
|
36667
|
-
var
|
|
37014
|
+
var FetchproxyHintedError = class extends FetchproxyProtocolError {
|
|
36668
37015
|
/** The extension's raw rejection, unmodified. */
|
|
36669
37016
|
originalError;
|
|
37017
|
+
/** What the user should actually do, in prose. */
|
|
36670
37018
|
hint;
|
|
36671
|
-
constructor(originalError) {
|
|
36672
|
-
const hint = "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.";
|
|
37019
|
+
constructor(originalError, hint) {
|
|
36673
37020
|
super(`${originalError} \u2014 ${hint}`);
|
|
36674
|
-
this.name = "
|
|
37021
|
+
this.name = "FetchproxyHintedError";
|
|
36675
37022
|
this.originalError = originalError;
|
|
36676
37023
|
this.hint = hint;
|
|
36677
37024
|
}
|
|
36678
37025
|
};
|
|
37026
|
+
var FetchproxyScopeError = class extends FetchproxyHintedError {
|
|
37027
|
+
constructor(originalError) {
|
|
37028
|
+
super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
|
|
37029
|
+
this.name = "FetchproxyScopeError";
|
|
37030
|
+
}
|
|
37031
|
+
};
|
|
37032
|
+
var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
37033
|
+
constructor(originalError) {
|
|
37034
|
+
super(originalError, "open a tab on that host and sign in, then re-run. This is not a version problem and does not need an update.");
|
|
37035
|
+
this.name = "FetchproxyNoTabError";
|
|
37036
|
+
}
|
|
37037
|
+
};
|
|
36679
37038
|
var SCOPE_REJECTION = /not in declared/;
|
|
37039
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
36680
37040
|
function protocolErrorFrom(error51) {
|
|
36681
|
-
|
|
37041
|
+
if (SCOPE_REJECTION.test(error51))
|
|
37042
|
+
return new FetchproxyScopeError(error51);
|
|
37043
|
+
if (NO_TAB_REJECTION.test(error51))
|
|
37044
|
+
return new FetchproxyNoTabError(error51);
|
|
37045
|
+
return new FetchproxyProtocolError(error51);
|
|
36682
37046
|
}
|
|
36683
37047
|
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36684
37048
|
url;
|
|
@@ -36784,6 +37148,9 @@ var FetchproxyServer = class {
|
|
|
36784
37148
|
// them off from `pending` (fetch) and `pendingReadCookies` (legacy
|
|
36785
37149
|
// string-shape) so the response routing in `onInner` stays linear.
|
|
36786
37150
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
37151
|
+
// 1.12.0+: write-cookies awaiters resolve the list of names actually
|
|
37152
|
+
// written, so a caller can confirm rather than assume.
|
|
37153
|
+
pendingWriteCookies = /* @__PURE__ */ new Map();
|
|
36787
37154
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36788
37155
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36789
37156
|
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
@@ -36850,7 +37217,7 @@ var FetchproxyServer = class {
|
|
|
36850
37217
|
}
|
|
36851
37218
|
}
|
|
36852
37219
|
this.opts = {
|
|
36853
|
-
port: opts.port ?? 37149,
|
|
37220
|
+
port: opts.port ?? envWsPort() ?? 37149,
|
|
36854
37221
|
host: opts.host ?? "127.0.0.1",
|
|
36855
37222
|
serverName: opts.serverName,
|
|
36856
37223
|
version: opts.version,
|
|
@@ -36910,6 +37277,8 @@ var FetchproxyServer = class {
|
|
|
36910
37277
|
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
36911
37278
|
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
36912
37279
|
identityDir: opts.identityDir,
|
|
37280
|
+
allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
|
|
37281
|
+
requireExtensionIdentity: opts.requireExtensionIdentity,
|
|
36913
37282
|
onPairCode: opts.onPairCode
|
|
36914
37283
|
};
|
|
36915
37284
|
}
|
|
@@ -37009,7 +37378,8 @@ var FetchproxyServer = class {
|
|
|
37009
37378
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37010
37379
|
ownDomSelectors: this.opts.domSelectors,
|
|
37011
37380
|
ownGraphqlOps: this.opts.graphqlOps,
|
|
37012
|
-
onPairCode: this.opts.onPairCode
|
|
37381
|
+
onPairCode: this.opts.onPairCode,
|
|
37382
|
+
extensionTrust: this.extensionTrust()
|
|
37013
37383
|
});
|
|
37014
37384
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
37015
37385
|
this.hostHandle.onExtensionDisconnect(() => {
|
|
@@ -37038,7 +37408,9 @@ var FetchproxyServer = class {
|
|
|
37038
37408
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37039
37409
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37040
37410
|
domSelectors: this.opts.domSelectors,
|
|
37041
|
-
graphqlOps: this.opts.graphqlOps
|
|
37411
|
+
graphqlOps: this.opts.graphqlOps,
|
|
37412
|
+
extensionTrust: this.extensionTrust(),
|
|
37413
|
+
requireExtensionIdentity: this.opts.requireExtensionIdentity
|
|
37042
37414
|
});
|
|
37043
37415
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37044
37416
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37177,6 +37549,23 @@ var FetchproxyServer = class {
|
|
|
37177
37549
|
markActive() {
|
|
37178
37550
|
this.noteActivityForKeepalive();
|
|
37179
37551
|
}
|
|
37552
|
+
/**
|
|
37553
|
+
* #208: this MCP's pin on the extension's identity, stored beside its own
|
|
37554
|
+
* identity key and so following `identityDir` wherever the caller put it.
|
|
37555
|
+
*
|
|
37556
|
+
* `allowNewExtensionIdentity` falls back to an environment variable when the
|
|
37557
|
+
* caller expressed no opinion, because the thirteen MCPs that construct this
|
|
37558
|
+
* class are separate packages: an operator whose extension re-install has
|
|
37559
|
+
* just locked all of them out needs one lever that does not require patching
|
|
37560
|
+
* every one of them.
|
|
37561
|
+
*/
|
|
37562
|
+
extensionTrust() {
|
|
37563
|
+
return fileExtensionTrust({
|
|
37564
|
+
serverName: this.opts.serverName,
|
|
37565
|
+
dir: this.opts.identityDir,
|
|
37566
|
+
allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
|
|
37567
|
+
});
|
|
37568
|
+
}
|
|
37180
37569
|
noteActivityForKeepalive() {
|
|
37181
37570
|
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
37182
37571
|
if (intervalMs <= 0)
|
|
@@ -37240,6 +37629,7 @@ var FetchproxyServer = class {
|
|
|
37240
37629
|
this.pending.delete(id);
|
|
37241
37630
|
this.pendingReadCookies.delete(id);
|
|
37242
37631
|
this.pendingStorage.delete(id);
|
|
37632
|
+
this.pendingWriteCookies.delete(id);
|
|
37243
37633
|
this.pendingCapture.delete(id);
|
|
37244
37634
|
this.pendingRedirect.delete(id);
|
|
37245
37635
|
this.pendingDownload.delete(id);
|
|
@@ -37257,8 +37647,8 @@ var FetchproxyServer = class {
|
|
|
37257
37647
|
async _fetchOnceWithTimeout(init) {
|
|
37258
37648
|
const id = this.nextRequestId++;
|
|
37259
37649
|
const inner = { type: "request", id, op: "fetch", init };
|
|
37260
|
-
const pending = new Promise((
|
|
37261
|
-
this.pending.set(id,
|
|
37650
|
+
const pending = new Promise((resolve2) => {
|
|
37651
|
+
this.pending.set(id, resolve2);
|
|
37262
37652
|
});
|
|
37263
37653
|
await this.sendInnerFrame(inner);
|
|
37264
37654
|
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
@@ -37269,12 +37659,12 @@ var FetchproxyServer = class {
|
|
|
37269
37659
|
try {
|
|
37270
37660
|
return await Promise.race([
|
|
37271
37661
|
pending,
|
|
37272
|
-
new Promise((
|
|
37662
|
+
new Promise((resolve2) => {
|
|
37273
37663
|
timer = setTimeout(() => {
|
|
37274
37664
|
this.pending.delete(id);
|
|
37275
37665
|
const elapsedMs = Date.now() - start;
|
|
37276
37666
|
const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
|
|
37277
|
-
|
|
37667
|
+
resolve2({
|
|
37278
37668
|
ok: false,
|
|
37279
37669
|
error: error51,
|
|
37280
37670
|
kind: "timeout",
|
|
@@ -37397,10 +37787,20 @@ var FetchproxyServer = class {
|
|
|
37397
37787
|
}
|
|
37398
37788
|
const url2 = isAbsolute ? path : `https://${host}${path}`;
|
|
37399
37789
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
37790
|
+
let tabUrl = `https://${host}/`;
|
|
37791
|
+
if (opts.viaTab !== void 0) {
|
|
37792
|
+
try {
|
|
37793
|
+
new URL(opts.viaTab);
|
|
37794
|
+
} catch {
|
|
37795
|
+
throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
|
|
37796
|
+
}
|
|
37797
|
+
assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
|
|
37798
|
+
tabUrl = opts.viaTab;
|
|
37799
|
+
}
|
|
37400
37800
|
const init = {
|
|
37401
37801
|
url: url2,
|
|
37402
37802
|
method,
|
|
37403
|
-
tabUrl
|
|
37803
|
+
tabUrl,
|
|
37404
37804
|
headers: opts.headers,
|
|
37405
37805
|
body: opts.body
|
|
37406
37806
|
};
|
|
@@ -37626,8 +38026,8 @@ var FetchproxyServer = class {
|
|
|
37626
38026
|
const tabUrl = `https://${host}/`;
|
|
37627
38027
|
inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
|
|
37628
38028
|
}
|
|
37629
|
-
const pending = new Promise((
|
|
37630
|
-
this.pendingReadCookies.set(id,
|
|
38029
|
+
const pending = new Promise((resolve2) => {
|
|
38030
|
+
this.pendingReadCookies.set(id, resolve2);
|
|
37631
38031
|
});
|
|
37632
38032
|
await this.sendInnerFrame(inner);
|
|
37633
38033
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
@@ -37636,6 +38036,61 @@ var FetchproxyServer = class {
|
|
|
37636
38036
|
}
|
|
37637
38037
|
return result.cookies;
|
|
37638
38038
|
}
|
|
38039
|
+
/**
|
|
38040
|
+
* 1.12.0+: overwrite the value of cookies this MCP already declares.
|
|
38041
|
+
*
|
|
38042
|
+
* The bridge's only write verb, and it exists for one failure class. Sites
|
|
38043
|
+
* that ROTATE a credential cookie hand back a new value on every refresh; if
|
|
38044
|
+
* the MCP refreshes and keeps the result to itself, the copy in the browser's
|
|
38045
|
+
* cookie jar is dead, and the user gets signed out of a tab they never
|
|
38046
|
+
* touched — usually reported to them as "inactivity". Writing the rotated
|
|
38047
|
+
* value back is the only thing that repairs it.
|
|
38048
|
+
*
|
|
38049
|
+
* Requires `'write_cookies'` in capabilities, which the user approves at pair
|
|
38050
|
+
* time as its own line. Every name must ALSO be in declared `cookieKeys`: a
|
|
38051
|
+
* write can never reach a cookie the MCP was not already trusted to read, so
|
|
38052
|
+
* granting it cannot widen which cookies are in play — only what may be done
|
|
38053
|
+
* to the ones already listed.
|
|
38054
|
+
*
|
|
38055
|
+
* The extension refuses the whole request unless every named cookie already
|
|
38056
|
+
* exists; this refreshes a value in place and deliberately cannot author new
|
|
38057
|
+
* cookies. Returns the names actually written.
|
|
38058
|
+
*/
|
|
38059
|
+
async writeCookies(opts) {
|
|
38060
|
+
if (!this.opts.capabilities.includes("write_cookies")) {
|
|
38061
|
+
throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
38062
|
+
}
|
|
38063
|
+
const names = Object.keys(opts.cookies);
|
|
38064
|
+
if (names.length === 0) {
|
|
38065
|
+
throw new Error("FetchproxyServer.writeCookies(): no cookies given");
|
|
38066
|
+
}
|
|
38067
|
+
await this.ensureConnected();
|
|
38068
|
+
this.throwIfPendingPair();
|
|
38069
|
+
if (opts.subdomain !== void 0)
|
|
38070
|
+
assertSubdomainLabel(opts.subdomain);
|
|
38071
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
38072
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
38073
|
+
this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
|
|
38074
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
38075
|
+
const id = this.nextRequestId++;
|
|
38076
|
+
const inner = {
|
|
38077
|
+
type: "request",
|
|
38078
|
+
id,
|
|
38079
|
+
op: "write_cookies",
|
|
38080
|
+
init: {
|
|
38081
|
+
// Bare origin, same invariant as the read path: a path must never be
|
|
38082
|
+
// able to move the request past the domain gate.
|
|
38083
|
+
origin: `https://${host}`,
|
|
38084
|
+
cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
|
|
38085
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
38086
|
+
}
|
|
38087
|
+
};
|
|
38088
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38089
|
+
this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
|
|
38090
|
+
});
|
|
38091
|
+
await this.sendInnerFrame(inner);
|
|
38092
|
+
return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
|
|
38093
|
+
}
|
|
37639
38094
|
/**
|
|
37640
38095
|
* 0.3.0+: read declared localStorage keys from the user's signed-in
|
|
37641
38096
|
* tab. Requires `'read_local_storage'` in capabilities AND each key
|
|
@@ -37693,8 +38148,8 @@ var FetchproxyServer = class {
|
|
|
37693
38148
|
...opts.pointers ? { pointers: { ...opts.pointers } } : {}
|
|
37694
38149
|
}
|
|
37695
38150
|
};
|
|
37696
|
-
const pending = new Promise((
|
|
37697
|
-
this.pendingStorage.set(id, { resolve, reject });
|
|
38151
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38152
|
+
this.pendingStorage.set(id, { resolve: resolve2, reject });
|
|
37698
38153
|
});
|
|
37699
38154
|
await this.sendInnerFrame(inner);
|
|
37700
38155
|
return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
|
|
@@ -37798,8 +38253,8 @@ var FetchproxyServer = class {
|
|
|
37798
38253
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37799
38254
|
}
|
|
37800
38255
|
};
|
|
37801
|
-
const pending = new Promise((
|
|
37802
|
-
this.pendingCapture.set(id, { resolve, reject });
|
|
38256
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38257
|
+
this.pendingCapture.set(id, { resolve: resolve2, reject });
|
|
37803
38258
|
});
|
|
37804
38259
|
await this.sendInnerFrame(inner);
|
|
37805
38260
|
return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
@@ -37883,8 +38338,8 @@ var FetchproxyServer = class {
|
|
|
37883
38338
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37884
38339
|
}
|
|
37885
38340
|
};
|
|
37886
|
-
const pending = new Promise((
|
|
37887
|
-
this.pendingRedirect.set(id, { resolve, reject });
|
|
38341
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38342
|
+
this.pendingRedirect.set(id, { resolve: resolve2, reject });
|
|
37888
38343
|
});
|
|
37889
38344
|
await this.sendInnerFrame(inner);
|
|
37890
38345
|
return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
@@ -37965,8 +38420,8 @@ var FetchproxyServer = class {
|
|
|
37965
38420
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37966
38421
|
}
|
|
37967
38422
|
};
|
|
37968
|
-
const pending = new Promise((
|
|
37969
|
-
this.pendingDownload.set(id, { resolve, reject });
|
|
38423
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38424
|
+
this.pendingDownload.set(id, { resolve: resolve2, reject });
|
|
37970
38425
|
});
|
|
37971
38426
|
await this.sendInnerFrame(inner);
|
|
37972
38427
|
return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
|
|
@@ -38013,8 +38468,8 @@ var FetchproxyServer = class {
|
|
|
38013
38468
|
keys: [...opts.keys]
|
|
38014
38469
|
}
|
|
38015
38470
|
};
|
|
38016
|
-
const pending = new Promise((
|
|
38017
|
-
this.pendingIdb.set(id, { resolve, reject });
|
|
38471
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38472
|
+
this.pendingIdb.set(id, { resolve: resolve2, reject });
|
|
38018
38473
|
});
|
|
38019
38474
|
await this.sendInnerFrame(inner);
|
|
38020
38475
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
@@ -38053,8 +38508,8 @@ var FetchproxyServer = class {
|
|
|
38053
38508
|
op: "read_dom",
|
|
38054
38509
|
init: { origin, names: [...opts.names] }
|
|
38055
38510
|
};
|
|
38056
|
-
const pending = new Promise((
|
|
38057
|
-
this.pendingStorage.set(id, { resolve, reject });
|
|
38511
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38512
|
+
this.pendingStorage.set(id, { resolve: resolve2, reject });
|
|
38058
38513
|
});
|
|
38059
38514
|
await this.sendInnerFrame(inner);
|
|
38060
38515
|
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
@@ -38099,8 +38554,8 @@ var FetchproxyServer = class {
|
|
|
38099
38554
|
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
38100
38555
|
}
|
|
38101
38556
|
};
|
|
38102
|
-
const pending = new Promise((
|
|
38103
|
-
this.pendingGraphql.set(id, { resolve, reject });
|
|
38557
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38558
|
+
this.pendingGraphql.set(id, { resolve: resolve2, reject });
|
|
38104
38559
|
});
|
|
38105
38560
|
await this.sendInnerFrame(inner);
|
|
38106
38561
|
return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
|
|
@@ -38256,6 +38711,16 @@ var FetchproxyServer = class {
|
|
|
38256
38711
|
}
|
|
38257
38712
|
return;
|
|
38258
38713
|
}
|
|
38714
|
+
const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
|
|
38715
|
+
if (writeCookiesCb) {
|
|
38716
|
+
this.pendingWriteCookies.delete(inner.id);
|
|
38717
|
+
if (inner.ok && inner.op === "write_cookies") {
|
|
38718
|
+
writeCookiesCb.resolve([...inner.written]);
|
|
38719
|
+
} else {
|
|
38720
|
+
writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
|
|
38721
|
+
}
|
|
38722
|
+
return;
|
|
38723
|
+
}
|
|
38259
38724
|
const cookiesCb = this.pendingReadCookies.get(inner.id);
|
|
38260
38725
|
if (cookiesCb) {
|
|
38261
38726
|
this.pendingReadCookies.delete(inner.id);
|
|
@@ -38295,6 +38760,9 @@ var FetchproxyServer = class {
|
|
|
38295
38760
|
for (const { reject } of this.pendingStorage.values())
|
|
38296
38761
|
reject(err);
|
|
38297
38762
|
this.pendingStorage.clear();
|
|
38763
|
+
for (const { reject } of this.pendingWriteCookies.values())
|
|
38764
|
+
reject(err);
|
|
38765
|
+
this.pendingWriteCookies.clear();
|
|
38298
38766
|
for (const { reject } of this.pendingCapture.values())
|
|
38299
38767
|
reject(err);
|
|
38300
38768
|
this.pendingCapture.clear();
|
|
@@ -38577,7 +39045,7 @@ function loadAccount(env = process.env) {
|
|
|
38577
39045
|
// package.json
|
|
38578
39046
|
var package_default = {
|
|
38579
39047
|
name: "infinitecampus-mcp",
|
|
38580
|
-
version: "2.
|
|
39048
|
+
version: "2.5.0",
|
|
38581
39049
|
mcpName: "io.github.chrischall/infinitecampus-mcp",
|
|
38582
39050
|
description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
38583
39051
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38609,19 +39077,22 @@ var package_default = {
|
|
|
38609
39077
|
".claude-plugin",
|
|
38610
39078
|
"skills",
|
|
38611
39079
|
".mcp.json",
|
|
38612
|
-
"server.json"
|
|
39080
|
+
"server.json",
|
|
39081
|
+
"mint.yaml"
|
|
38613
39082
|
],
|
|
38614
39083
|
scripts: {
|
|
38615
39084
|
build: "tsc && npm run bundle",
|
|
38616
39085
|
bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
|
|
38617
39086
|
dev: "node --env-file=.env dist/index.js",
|
|
38618
|
-
test: "vitest run",
|
|
38619
|
-
"test:
|
|
39087
|
+
test: "npm run typecheck && vitest run",
|
|
39088
|
+
"test:coverage": "npm run typecheck && vitest run --coverage",
|
|
39089
|
+
"test:watch": "vitest",
|
|
39090
|
+
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
38620
39091
|
},
|
|
38621
39092
|
dependencies: {
|
|
38622
|
-
"@chrischall/mcp-utils": "^0.
|
|
38623
|
-
"@fetchproxy/bootstrap": "^
|
|
38624
|
-
"@fetchproxy/server": "^
|
|
39093
|
+
"@chrischall/mcp-utils": "^0.18.0",
|
|
39094
|
+
"@fetchproxy/bootstrap": "^2.0.0",
|
|
39095
|
+
"@fetchproxy/server": "^2.0.0",
|
|
38625
39096
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38626
39097
|
dotenv: "^17.4.2",
|
|
38627
39098
|
"node-html-parser": "^9.0.0",
|
|
@@ -38729,11 +39200,129 @@ async function liftBrowserSession(account2) {
|
|
|
38729
39200
|
}
|
|
38730
39201
|
|
|
38731
39202
|
// src/client.ts
|
|
38732
|
-
import { writeFile as
|
|
38733
|
-
import { dirname } from "path";
|
|
39203
|
+
import { writeFile as writeFile3, stat } from "fs/promises";
|
|
39204
|
+
import { dirname as dirname2 } from "path";
|
|
38734
39205
|
|
|
38735
39206
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
39207
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
39208
|
+
import { dirname, join as join3, resolve } from "node:path";
|
|
39209
|
+
import { homedir as homedir2 } from "node:os";
|
|
39210
|
+
import { randomBytes, createHmac } from "node:crypto";
|
|
39211
|
+
function bindingDigest(secret, salt) {
|
|
39212
|
+
return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
|
|
39213
|
+
}
|
|
39214
|
+
function isEnvelope(raw) {
|
|
39215
|
+
return raw !== null && typeof raw === "object" && raw.v === 1 && "state" in raw;
|
|
39216
|
+
}
|
|
39217
|
+
var StatePersistenceError = class extends Error {
|
|
39218
|
+
cause;
|
|
39219
|
+
constructor(cause) {
|
|
39220
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
39221
|
+
this.name = "StatePersistenceError";
|
|
39222
|
+
this.cause = cause;
|
|
39223
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39224
|
+
}
|
|
39225
|
+
};
|
|
39226
|
+
function createFileStatePersistence(opts) {
|
|
39227
|
+
const { filePath, validate } = opts;
|
|
39228
|
+
const secret = opts.boundTo;
|
|
39229
|
+
return {
|
|
39230
|
+
load() {
|
|
39231
|
+
if (!existsSync(filePath))
|
|
39232
|
+
return null;
|
|
39233
|
+
try {
|
|
39234
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
39235
|
+
let state;
|
|
39236
|
+
if (isEnvelope(raw)) {
|
|
39237
|
+
const b = raw.boundTo;
|
|
39238
|
+
if (secret === void 0) {
|
|
39239
|
+
if (b !== void 0)
|
|
39240
|
+
return null;
|
|
39241
|
+
} else {
|
|
39242
|
+
if (b === void 0)
|
|
39243
|
+
return null;
|
|
39244
|
+
if (bindingDigest(secret, b.salt) !== b.digest)
|
|
39245
|
+
return null;
|
|
39246
|
+
}
|
|
39247
|
+
state = raw.state;
|
|
39248
|
+
} else {
|
|
39249
|
+
if (secret !== void 0)
|
|
39250
|
+
return null;
|
|
39251
|
+
state = raw;
|
|
39252
|
+
}
|
|
39253
|
+
if (validate !== void 0)
|
|
39254
|
+
return validate(state);
|
|
39255
|
+
return state;
|
|
39256
|
+
} catch {
|
|
39257
|
+
return null;
|
|
39258
|
+
}
|
|
39259
|
+
},
|
|
39260
|
+
save(state) {
|
|
39261
|
+
const dir = dirname(filePath);
|
|
39262
|
+
const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
|
|
39263
|
+
const dirExisted = existsSync(dir);
|
|
39264
|
+
try {
|
|
39265
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
39266
|
+
if (!dirExisted)
|
|
39267
|
+
chmodSync(dir, 448);
|
|
39268
|
+
let boundTo;
|
|
39269
|
+
if (secret !== void 0) {
|
|
39270
|
+
const salt = randomBytes(16).toString("hex");
|
|
39271
|
+
boundTo = { salt, digest: bindingDigest(secret, salt) };
|
|
39272
|
+
}
|
|
39273
|
+
const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
|
|
39274
|
+
writeFileSync(tmp, JSON.stringify(envelope, null, 2), { mode: 384 });
|
|
39275
|
+
chmodSync(tmp, 384);
|
|
39276
|
+
renameSync(tmp, filePath);
|
|
39277
|
+
chmodSync(filePath, 384);
|
|
39278
|
+
} catch (err) {
|
|
39279
|
+
try {
|
|
39280
|
+
if (existsSync(tmp))
|
|
39281
|
+
unlinkSync(tmp);
|
|
39282
|
+
} catch {
|
|
39283
|
+
}
|
|
39284
|
+
throw err;
|
|
39285
|
+
}
|
|
39286
|
+
},
|
|
39287
|
+
clear() {
|
|
39288
|
+
try {
|
|
39289
|
+
if (existsSync(filePath))
|
|
39290
|
+
unlinkSync(filePath);
|
|
39291
|
+
} catch {
|
|
39292
|
+
}
|
|
39293
|
+
}
|
|
39294
|
+
};
|
|
39295
|
+
}
|
|
39296
|
+
function homeOf(env) {
|
|
39297
|
+
return readEnvVar("HOME", { env }) ?? homedir2();
|
|
39298
|
+
}
|
|
39299
|
+
function resolveStateDir(opts = {}) {
|
|
39300
|
+
const env = opts.env;
|
|
39301
|
+
return join3(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
|
|
39302
|
+
}
|
|
39303
|
+
function resolveStateFile(opts) {
|
|
39304
|
+
const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
|
|
39305
|
+
if (override !== void 0) {
|
|
39306
|
+
if (override === "~")
|
|
39307
|
+
return homeOf(opts.env);
|
|
39308
|
+
if (override.startsWith("~/"))
|
|
39309
|
+
return join3(homeOf(opts.env), override.slice(2));
|
|
39310
|
+
return resolve(override);
|
|
39311
|
+
}
|
|
39312
|
+
const dirOpts = { env: opts.env };
|
|
39313
|
+
if (opts.subdir !== void 0)
|
|
39314
|
+
dirOpts.subdir = opts.subdir;
|
|
39315
|
+
return join3(resolveStateDir(dirOpts), opts.fileName);
|
|
39316
|
+
}
|
|
38736
39317
|
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
39318
|
+
function isPersistedCookieSession(raw) {
|
|
39319
|
+
if (raw === null || typeof raw !== "object")
|
|
39320
|
+
return false;
|
|
39321
|
+
const r = raw;
|
|
39322
|
+
if (typeof r.sessionAt !== "number" || !Number.isFinite(r.sessionAt))
|
|
39323
|
+
return false;
|
|
39324
|
+
return typeof r.session === "object" && r.session !== null;
|
|
39325
|
+
}
|
|
38737
39326
|
var CookieSessionManager = class {
|
|
38738
39327
|
session;
|
|
38739
39328
|
inFlight;
|
|
@@ -38747,6 +39336,17 @@ var CookieSessionManager = class {
|
|
|
38747
39336
|
maxAgeMs;
|
|
38748
39337
|
now;
|
|
38749
39338
|
onReplayLoginErrorFn;
|
|
39339
|
+
persistence;
|
|
39340
|
+
onPersistErrorFn;
|
|
39341
|
+
/** Persistence is consulted once per process; a miss must not be re-read. */
|
|
39342
|
+
persistenceRead = false;
|
|
39343
|
+
/**
|
|
39344
|
+
* Serializes persistence writes. `seed()` and `invalidate()` are synchronous
|
|
39345
|
+
* by contract and so fire-and-forget their save/clear; with an async backend a
|
|
39346
|
+
* slow save could otherwise land AFTER the clear that followed it and leave an
|
|
39347
|
+
* invalidated session on disk.
|
|
39348
|
+
*/
|
|
39349
|
+
persistChain = Promise.resolve();
|
|
38750
39350
|
constructor(opts) {
|
|
38751
39351
|
this.loginFn = opts.login;
|
|
38752
39352
|
this.isExpiredFn = opts.isExpired ?? (() => false);
|
|
@@ -38754,6 +39354,8 @@ var CookieSessionManager = class {
|
|
|
38754
39354
|
this.maxAgeMs = opts.maxAgeMs;
|
|
38755
39355
|
this.now = opts.now ?? Date.now;
|
|
38756
39356
|
this.onReplayLoginErrorFn = opts.onReplayLoginError;
|
|
39357
|
+
this.persistence = opts.persistence;
|
|
39358
|
+
this.onPersistErrorFn = opts.onPersistError;
|
|
38757
39359
|
}
|
|
38758
39360
|
/** The current session, or `undefined` before the first successful login. */
|
|
38759
39361
|
get current() {
|
|
@@ -38802,6 +39404,8 @@ var CookieSessionManager = class {
|
|
|
38802
39404
|
this.session = session;
|
|
38803
39405
|
this.sessionAt = this.now();
|
|
38804
39406
|
this.inFlight = void 0;
|
|
39407
|
+
this.persistenceRead = true;
|
|
39408
|
+
void this.persist(session, this.sessionAt);
|
|
38805
39409
|
}
|
|
38806
39410
|
/**
|
|
38807
39411
|
* One login attempt. Self-clears `inFlight` on settle so a rejected login
|
|
@@ -38813,13 +39417,25 @@ var CookieSessionManager = class {
|
|
|
38813
39417
|
const holder = {};
|
|
38814
39418
|
holder.p = (async () => {
|
|
38815
39419
|
try {
|
|
39420
|
+
const restored = this.persistence !== void 0 && !this.persistenceRead ? await this.restoreFromPersistence() : null;
|
|
39421
|
+
if (restored !== null) {
|
|
39422
|
+
if (this.inFlight === holder.p) {
|
|
39423
|
+
this.session = restored.session;
|
|
39424
|
+
this.sessionAt = restored.sessionAt;
|
|
39425
|
+
}
|
|
39426
|
+
return restored.session;
|
|
39427
|
+
}
|
|
38816
39428
|
const session = await this.loginFn();
|
|
39429
|
+
const at = this.now();
|
|
38817
39430
|
if (this.inFlight === holder.p) {
|
|
38818
39431
|
this.session = session;
|
|
38819
|
-
this.sessionAt =
|
|
39432
|
+
this.sessionAt = at;
|
|
38820
39433
|
}
|
|
39434
|
+
await this.persist(session, at);
|
|
38821
39435
|
return session;
|
|
38822
39436
|
} catch (err) {
|
|
39437
|
+
if (err instanceof StatePersistenceError)
|
|
39438
|
+
throw err.cause;
|
|
38823
39439
|
if (this.isPermanentErrorFn(err))
|
|
38824
39440
|
this.permanentError = err;
|
|
38825
39441
|
throw err;
|
|
@@ -38839,6 +39455,71 @@ var CookieSessionManager = class {
|
|
|
38839
39455
|
invalidate() {
|
|
38840
39456
|
this.session = void 0;
|
|
38841
39457
|
this.inFlight = void 0;
|
|
39458
|
+
void this.clearPersisted();
|
|
39459
|
+
}
|
|
39460
|
+
/**
|
|
39461
|
+
* The persisted session, if there is one worth using. Read at most once per
|
|
39462
|
+
* process — after that the in-memory session (or its absence) is the truth,
|
|
39463
|
+
* so an invalidate() cannot be undone by a stale file.
|
|
39464
|
+
*/
|
|
39465
|
+
async restoreFromPersistence() {
|
|
39466
|
+
if (this.persistence === void 0 || this.persistenceRead)
|
|
39467
|
+
return null;
|
|
39468
|
+
this.persistenceRead = true;
|
|
39469
|
+
try {
|
|
39470
|
+
let raw = null;
|
|
39471
|
+
await this.enqueuePersist(async () => {
|
|
39472
|
+
raw = await this.persistence?.load();
|
|
39473
|
+
});
|
|
39474
|
+
if (!isPersistedCookieSession(raw))
|
|
39475
|
+
return null;
|
|
39476
|
+
if (this.maxAgeMs !== void 0 && this.now() - raw.sessionAt >= this.maxAgeMs)
|
|
39477
|
+
return null;
|
|
39478
|
+
return raw;
|
|
39479
|
+
} catch {
|
|
39480
|
+
return null;
|
|
39481
|
+
}
|
|
39482
|
+
}
|
|
39483
|
+
/**
|
|
39484
|
+
* Append a persistence op to the chain, preserving call order.
|
|
39485
|
+
*
|
|
39486
|
+
* The RETURNED promise can reject — that is the whole `StatePersistenceError`
|
|
39487
|
+
* path, and the awaited login write depends on it. Only the retained chain is
|
|
39488
|
+
* swallowed, so one failed write cannot poison every later one.
|
|
39489
|
+
*/
|
|
39490
|
+
enqueuePersist(op) {
|
|
39491
|
+
const next = this.persistChain.then(op);
|
|
39492
|
+
this.persistChain = next.catch(() => void 0);
|
|
39493
|
+
return next;
|
|
39494
|
+
}
|
|
39495
|
+
/** Write the session. Silent unless `onPersistError` throws. */
|
|
39496
|
+
persist(session, sessionAt) {
|
|
39497
|
+
return this.enqueuePersist(async () => {
|
|
39498
|
+
if (this.persistence === void 0)
|
|
39499
|
+
return;
|
|
39500
|
+
try {
|
|
39501
|
+
await this.persistence.save({ session, sessionAt });
|
|
39502
|
+
} catch (err) {
|
|
39503
|
+
if (this.onPersistErrorFn === void 0)
|
|
39504
|
+
return;
|
|
39505
|
+
try {
|
|
39506
|
+
this.onPersistErrorFn(err);
|
|
39507
|
+
} catch (hookErr) {
|
|
39508
|
+
throw new StatePersistenceError(hookErr);
|
|
39509
|
+
}
|
|
39510
|
+
}
|
|
39511
|
+
});
|
|
39512
|
+
}
|
|
39513
|
+
/** Discard the persisted session. Never throws. */
|
|
39514
|
+
clearPersisted() {
|
|
39515
|
+
return this.enqueuePersist(async () => {
|
|
39516
|
+
if (this.persistence?.clear === void 0)
|
|
39517
|
+
return;
|
|
39518
|
+
try {
|
|
39519
|
+
await this.persistence.clear();
|
|
39520
|
+
} catch {
|
|
39521
|
+
}
|
|
39522
|
+
});
|
|
38842
39523
|
}
|
|
38843
39524
|
/**
|
|
38844
39525
|
* Run an authenticated `call` with the current session and reactive
|
|
@@ -38875,6 +39556,46 @@ function createCookieSessionManager(opts) {
|
|
|
38875
39556
|
return new CookieSessionManager(opts);
|
|
38876
39557
|
}
|
|
38877
39558
|
|
|
39559
|
+
// src/session-cache.ts
|
|
39560
|
+
function sessionCachePath(env = process.env, district) {
|
|
39561
|
+
const seg = (district ?? "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").slice(0, 64);
|
|
39562
|
+
return resolveStateFile({
|
|
39563
|
+
env,
|
|
39564
|
+
envVar: "IC_SESSION_FILE",
|
|
39565
|
+
subdir: ".infinitecampus-mcp",
|
|
39566
|
+
fileName: seg === "" ? "session.json" : `session-${seg}.json`
|
|
39567
|
+
});
|
|
39568
|
+
}
|
|
39569
|
+
function isStored(raw) {
|
|
39570
|
+
if (raw === null || typeof raw !== "object") return false;
|
|
39571
|
+
const r = raw;
|
|
39572
|
+
if (typeof r.sessionAt !== "number") return false;
|
|
39573
|
+
const s = r.session;
|
|
39574
|
+
if (s === null || typeof s !== "object") return false;
|
|
39575
|
+
if (typeof s.cookieHeader !== "string" || s.cookieHeader === "") return false;
|
|
39576
|
+
return typeof s.xsrfToken === "string";
|
|
39577
|
+
}
|
|
39578
|
+
function createSessionCache(opts) {
|
|
39579
|
+
const env = opts.env ?? process.env;
|
|
39580
|
+
if (!parseBoolEnv("IC_SESSION_CACHE", { env, default: true })) return null;
|
|
39581
|
+
const username = opts.username ?? readEnvVar("IC_USERNAME", { env });
|
|
39582
|
+
const password = opts.password ?? readEnvVar("IC_PASSWORD", { env });
|
|
39583
|
+
if (opts.browserBacked === true) return null;
|
|
39584
|
+
const boundTo = username && password ? ["login", opts.baseUrl, username.trim().toLowerCase(), password].join("\0") : null;
|
|
39585
|
+
if (boundTo === null) return null;
|
|
39586
|
+
return createFileStatePersistence({
|
|
39587
|
+
filePath: sessionCachePath(env, opts.district),
|
|
39588
|
+
boundTo,
|
|
39589
|
+
validate: (raw) => isStored(raw) ? raw : null
|
|
39590
|
+
});
|
|
39591
|
+
}
|
|
39592
|
+
function reportCacheWriteFailure(err) {
|
|
39593
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
39594
|
+
console.error(
|
|
39595
|
+
`[infinitecampus-mcp] could not cache the session (${detail}); continuing without the cache \u2014 every restart will authenticate again until this is fixed.`
|
|
39596
|
+
);
|
|
39597
|
+
}
|
|
39598
|
+
|
|
38878
39599
|
// src/client.ts
|
|
38879
39600
|
var SESSION_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
38880
39601
|
var ICClient = class {
|
|
@@ -38900,6 +39621,32 @@ var ICClient = class {
|
|
|
38900
39621
|
* first call to `ensureDiscovery()`.
|
|
38901
39622
|
*/
|
|
38902
39623
|
fetchproxyMode = false;
|
|
39624
|
+
/**
|
|
39625
|
+
* Whether CUPS linked-district discovery has run IN THIS PROCESS. Not "is
|
|
39626
|
+
* there a session" — a session restored from the on-disk cache is real while
|
|
39627
|
+
* discovery has not run, and conflating the two leaves linked districts
|
|
39628
|
+
* invisible. See {@link InfiniteCampusClient.ensureDiscovery}.
|
|
39629
|
+
*/
|
|
39630
|
+
discoveryDone = false;
|
|
39631
|
+
/**
|
|
39632
|
+
* The in-flight discovery, so concurrent callers await the SAME one.
|
|
39633
|
+
*
|
|
39634
|
+
* A bare boolean is not enough: `discoveryDone` flips as soon as the CUPS
|
|
39635
|
+
* probe answers, but `accounts`/`linkedTo` are only populated further down.
|
|
39636
|
+
* A second request for a linked district landing in that window would read
|
|
39637
|
+
* the flag as done, find `accounts` still empty, and throw
|
|
39638
|
+
* UnknownDistrictError for a district that was moments from existing.
|
|
39639
|
+
*/
|
|
39640
|
+
discoveryInFlight = null;
|
|
39641
|
+
/**
|
|
39642
|
+
* The session the last discovery attempt used.
|
|
39643
|
+
*
|
|
39644
|
+
* `login()` runs discovery on every fresh mint. If that probe fails without
|
|
39645
|
+
* latching, `ensureDiscovery` would immediately re-probe with the very same
|
|
39646
|
+
* session and fail identically — two requests for one answer. Retrying is
|
|
39647
|
+
* right; retrying the same session inside one call is not.
|
|
39648
|
+
*/
|
|
39649
|
+
lastDiscoverySession = null;
|
|
38903
39650
|
/**
|
|
38904
39651
|
* fetchproxy lift. Called by the primary manager's login — on the first
|
|
38905
39652
|
* request AND on every renewal, which is what keeps the path alive past
|
|
@@ -38920,11 +39667,11 @@ var ICClient = class {
|
|
|
38920
39667
|
constructor(account2, opts = {}) {
|
|
38921
39668
|
this.accounts.set(account2.name, account2);
|
|
38922
39669
|
this.primaryName = account2.name;
|
|
38923
|
-
this.managers.set(account2.name, this.makeManager(account2));
|
|
38924
39670
|
if (opts.refreshSession) {
|
|
38925
39671
|
this.refreshSession = opts.refreshSession;
|
|
38926
39672
|
this.fetchproxyMode = true;
|
|
38927
39673
|
}
|
|
39674
|
+
this.managers.set(account2.name, this.makeManager(account2));
|
|
38928
39675
|
}
|
|
38929
39676
|
/**
|
|
38930
39677
|
* Build the per-district CookieSessionManager.
|
|
@@ -38944,8 +39691,17 @@ var ICClient = class {
|
|
|
38944
39691
|
* bookkeeping used to provide.
|
|
38945
39692
|
*/
|
|
38946
39693
|
makeManager(account2) {
|
|
39694
|
+
const cache = account2.name === this.primaryName ? createSessionCache({
|
|
39695
|
+
district: account2.name,
|
|
39696
|
+
baseUrl: account2.baseUrl,
|
|
39697
|
+
username: account2.username,
|
|
39698
|
+
password: account2.password,
|
|
39699
|
+
browserBacked: this.fetchproxyMode
|
|
39700
|
+
}) : null;
|
|
38947
39701
|
return createCookieSessionManager({
|
|
38948
39702
|
login: () => this.login(account2),
|
|
39703
|
+
persistence: cache ?? void 0,
|
|
39704
|
+
onPersistError: reportCacheWriteFailure,
|
|
38949
39705
|
isExpired: (res) => this.detectExpiredSession(account2.name, res),
|
|
38950
39706
|
isPermanentError: (e) => e instanceof AuthFailedError && e.permanent,
|
|
38951
39707
|
maxAgeMs: SESSION_TTL_MS,
|
|
@@ -38977,7 +39733,24 @@ var ICClient = class {
|
|
|
38977
39733
|
}
|
|
38978
39734
|
async ensureDiscovery() {
|
|
38979
39735
|
const session = await this.managers.get(this.primaryName).ensure();
|
|
38980
|
-
|
|
39736
|
+
if (this.discoveryDone) return;
|
|
39737
|
+
if (this.discoveryInFlight !== null) {
|
|
39738
|
+
await this.discoveryInFlight;
|
|
39739
|
+
return;
|
|
39740
|
+
}
|
|
39741
|
+
const primary = this.accounts.get(this.primaryName);
|
|
39742
|
+
const manager = this.managers.get(this.primaryName);
|
|
39743
|
+
this.discoveryInFlight = (async () => {
|
|
39744
|
+
if (this.lastDiscoverySession === session) {
|
|
39745
|
+
await manager.ensure();
|
|
39746
|
+
return;
|
|
39747
|
+
}
|
|
39748
|
+
if (await this.discoverLinkedDistricts(primary, session)) return;
|
|
39749
|
+
await manager.ensure();
|
|
39750
|
+
})().finally(() => {
|
|
39751
|
+
this.discoveryInFlight = null;
|
|
39752
|
+
});
|
|
39753
|
+
await this.discoveryInFlight;
|
|
38981
39754
|
}
|
|
38982
39755
|
listDistricts() {
|
|
38983
39756
|
return [...this.accounts.values()].map((a) => ({
|
|
@@ -39075,7 +39848,14 @@ var ICClient = class {
|
|
|
39075
39848
|
await this.discoverLinkedDistricts(account2, session);
|
|
39076
39849
|
return session;
|
|
39077
39850
|
}
|
|
39851
|
+
/**
|
|
39852
|
+
* @returns whether the CUPS probe ANSWERED. `false` means the session was
|
|
39853
|
+
* rejected (and has been invalidated), so the caller can mint a fresh one and
|
|
39854
|
+
* try once more — without that, recovery lands a call too late and the first
|
|
39855
|
+
* `ic_list_districts` after a restart silently omits linked districts.
|
|
39856
|
+
*/
|
|
39078
39857
|
async discoverLinkedDistricts(account2, session) {
|
|
39858
|
+
this.lastDiscoverySession = session;
|
|
39079
39859
|
try {
|
|
39080
39860
|
const baseHeaders = {
|
|
39081
39861
|
Cookie: session.cookieHeader,
|
|
@@ -39089,14 +39869,23 @@ var ICClient = class {
|
|
|
39089
39869
|
`${account2.baseUrl}/campus/api/campus/authentication/cups/linkedAccounts`,
|
|
39090
39870
|
{ headers: baseHeaders }
|
|
39091
39871
|
);
|
|
39092
|
-
if (!laRes.ok)
|
|
39872
|
+
if (!laRes.ok) {
|
|
39873
|
+
if (laRes.status === 401 || laRes.status === 403) {
|
|
39874
|
+
this.managers.get(account2.name)?.invalidate();
|
|
39875
|
+
return false;
|
|
39876
|
+
}
|
|
39877
|
+
return true;
|
|
39878
|
+
}
|
|
39093
39879
|
const laData = await laRes.json();
|
|
39094
|
-
if (!laData.accounts?.length)
|
|
39880
|
+
if (!laData.accounts?.length) {
|
|
39881
|
+
this.discoveryDone = true;
|
|
39882
|
+
return true;
|
|
39883
|
+
}
|
|
39095
39884
|
const [origRes, currRes] = await Promise.all([
|
|
39096
39885
|
fetch(`${account2.baseUrl}/campus/api/campus/user/userAccountSwitch/originalDistrict`, { headers: baseHeaders }),
|
|
39097
39886
|
fetch(`${account2.baseUrl}/campus/api/campus/districts/current`, { headers: baseHeaders })
|
|
39098
39887
|
]);
|
|
39099
|
-
if (!origRes.ok || !currRes.ok) return;
|
|
39888
|
+
if (!origRes.ok || !currRes.ok) return true;
|
|
39100
39889
|
const origData = await origRes.json();
|
|
39101
39890
|
const currData = await currRes.json();
|
|
39102
39891
|
for (const linked of laData.accounts) {
|
|
@@ -39159,8 +39948,11 @@ var ICClient = class {
|
|
|
39159
39948
|
console.error(`[ic] CUPS flow failed for ${linked.districtName}: ${e instanceof Error ? e.message : e}`);
|
|
39160
39949
|
}
|
|
39161
39950
|
}
|
|
39951
|
+
this.discoveryDone = true;
|
|
39952
|
+
return true;
|
|
39162
39953
|
} catch (e) {
|
|
39163
39954
|
console.error(`[ic] Linked district discovery failed: ${e instanceof Error ? e.message : e}`);
|
|
39955
|
+
return true;
|
|
39164
39956
|
}
|
|
39165
39957
|
}
|
|
39166
39958
|
async download(district, path, destinationPath, opts = {}) {
|
|
@@ -39171,7 +39963,7 @@ var ICClient = class {
|
|
|
39171
39963
|
}
|
|
39172
39964
|
if (destStat?.isDirectory()) throw new InvalidPathError(destinationPath);
|
|
39173
39965
|
if (destStat && !opts.overwrite) throw new FileExistsError(destinationPath);
|
|
39174
|
-
const parent =
|
|
39966
|
+
const parent = dirname2(destinationPath);
|
|
39175
39967
|
try {
|
|
39176
39968
|
await stat(parent);
|
|
39177
39969
|
} catch {
|
|
@@ -39189,7 +39981,7 @@ var ICClient = class {
|
|
|
39189
39981
|
});
|
|
39190
39982
|
if (!res.ok) throw new Error(`IC download ${res.status} for ${path}`);
|
|
39191
39983
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
39192
|
-
await
|
|
39984
|
+
await writeFile3(destinationPath, buf);
|
|
39193
39985
|
return {
|
|
39194
39986
|
path: destinationPath,
|
|
39195
39987
|
bytes: buf.byteLength,
|
|
@@ -40197,7 +40989,7 @@ function registerFeaturesTools(server, client) {
|
|
|
40197
40989
|
|
|
40198
40990
|
// src/index.ts
|
|
40199
40991
|
await loadDotenvSafely({
|
|
40200
|
-
path:
|
|
40992
|
+
path: join4(dirname3(fileURLToPath(import.meta.url)), "..", ".env"),
|
|
40201
40993
|
override: false
|
|
40202
40994
|
});
|
|
40203
40995
|
var AI_NOTICE = "[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.";
|
|
@@ -40215,7 +41007,7 @@ try {
|
|
|
40215
41007
|
}
|
|
40216
41008
|
var COMMON = {
|
|
40217
41009
|
name: "infinitecampus",
|
|
40218
|
-
version: "2.
|
|
41010
|
+
version: "2.5.0"
|
|
40219
41011
|
// x-release-please-version
|
|
40220
41012
|
};
|
|
40221
41013
|
if (account) {
|