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/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 = resolve.call(this, root, ref);
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 resolve(root, ref) {
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 resolve(baseURI, relativeURI, options) {
3652
+ function resolve2(baseURI, relativeURI, options) {
3653
3653
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3654
- const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
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 = randomBytes(16).toString("base64");
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 dirname2, join as join2 } from "path";
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((resolve) => setTimeout(resolve, pollInterval));
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((resolve, reject) => {
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
- resolve(parseResult.data);
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((resolve, reject) => {
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(resolve, interval);
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((resolve) => setTimeout(resolve, pollInterval));
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((resolve) => {
34673
+ return new Promise((resolve2) => {
34654
34674
  const json2 = serializeMessage(message);
34655
34675
  if (this._stdout.write(json2)) {
34656
- resolve();
34676
+ resolve2();
34657
34677
  } else {
34658
- this._stdout.once("drain", resolve);
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 = 2;
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((resolve, reject) => {
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
- resolve({ role: "peer" });
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
- resolve({ role: "host", server });
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((resolve, reject) => {
36211
- resolveOwnSession = resolve;
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 = concatBytes(ownSessionNonce, extNonce);
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((resolve) => {
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(() => resolve());
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((resolve, reject) => {
36416
- ws.once("open", () => resolve());
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((resolve, reject) => {
36447
- resolveFirstReady = resolve;
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 FetchproxyScopeError = class extends FetchproxyProtocolError {
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 = "FetchproxyScopeError";
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
- return SCOPE_REJECTION.test(error51) ? new FetchproxyScopeError(error51) : new FetchproxyProtocolError(error51);
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((resolve) => {
37261
- this.pending.set(id, resolve);
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((resolve) => {
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
- resolve({
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: `https://${host}/`,
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((resolve) => {
37630
- this.pendingReadCookies.set(id, resolve);
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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((resolve, reject) => {
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.4.4",
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:watch": "vitest"
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.14.0",
38623
- "@fetchproxy/bootstrap": "^1.11.0",
38624
- "@fetchproxy/server": "^1.7.0",
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 writeFile2, stat } from "fs/promises";
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 = this.now();
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
- void session;
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) return;
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) return;
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 = dirname(destinationPath);
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 writeFile2(destinationPath, buf);
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: join2(dirname2(fileURLToPath(import.meta.url)), "..", ".env"),
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.4.4"
41010
+ version: "2.5.0"
40219
41011
  // x-release-please-version
40220
41012
  };
40221
41013
  if (account) {