infinitecampus-mcp 2.3.4 → 2.3.6

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
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
13
13
  throw Error('Dynamic require of "' + x + '" is not supported');
14
14
  });
15
15
  var __commonJS = (cb, mod) => function __require2() {
16
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ try {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ } catch (e) {
19
+ throw mod = 0, e;
20
+ }
17
21
  };
18
22
  var __export = (target, all) => {
19
23
  for (var name in all)
@@ -34685,6 +34689,25 @@ async function runMcp(opts) {
34685
34689
  return server;
34686
34690
  }
34687
34691
 
34692
+ // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34693
+ var API_KEY_RE = new RegExp([
34694
+ "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
34695
+ // OpenAI / Anthropic (incl. sk-ant-…)
34696
+ "gh[pousr]_[A-Za-z0-9]{36,}\\b",
34697
+ // GitHub ghp_/gho_/ghu_/ghs_/ghr_
34698
+ "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
34699
+ // Slack
34700
+ "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
34701
+ // Google API key (39 chars total)
34702
+ "AKIA[0-9A-Z]{16}\\b",
34703
+ // AWS access key id (20 chars total)
34704
+ "whsec_[A-Za-z0-9]{16,}\\b"
34705
+ // webhook signing secret (Stripe-style)
34706
+ ].map((p) => `\\b${p}`).join("|"), "g");
34707
+ var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
34708
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
34709
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
34710
+
34688
34711
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
34689
34712
  function textResult(data) {
34690
34713
  return {
@@ -34737,23 +34760,38 @@ async function loadDotenvSafely(opts = {}) {
34737
34760
  }
34738
34761
 
34739
34762
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
34740
- var MAX_AGE_ZERO_RE = /(?:^|;)\s*Max-Age\s*=\s*0\s*(?:;|$)/i;
34741
- var EXPIRES_EPOCH_RE = /(?:^|;)\s*Expires\s*=\s*Thu,\s*01\s*Jan\s*1970/i;
34763
+ var MAX_AGE_RE = /(?:^|;)\s*Max-Age\s*=\s*(-?\d+)\s*(?:;|$)/i;
34764
+ var EXPIRES_RE = /(?:^|;)\s*Expires\s*=\s*([^;]+)/i;
34765
+ var EXPIRES_DELETION_YEAR = 2e3;
34766
+ function isDeletionCookie(entry) {
34767
+ const maxAge = MAX_AGE_RE.exec(entry);
34768
+ if (maxAge)
34769
+ return Number(maxAge[1]) <= 0;
34770
+ const expires = EXPIRES_RE.exec(entry);
34771
+ if (expires && expires[1]) {
34772
+ const when = new Date(expires[1].trim());
34773
+ if (!Number.isNaN(when.getTime()))
34774
+ return when.getUTCFullYear() < EXPIRES_DELETION_YEAR;
34775
+ }
34776
+ return false;
34777
+ }
34778
+ function parseNameValue(entry) {
34779
+ const nameValue = (entry.split(";")[0] ?? "").trim();
34780
+ const eqIdx = nameValue.indexOf("=");
34781
+ if (eqIdx < 1)
34782
+ return null;
34783
+ return { name: nameValue.slice(0, eqIdx).trim(), value: nameValue.slice(eqIdx + 1).trim() };
34784
+ }
34742
34785
  function parseCookieJar(setCookieHeaders) {
34743
- const entries = setCookieHeaders == null ? [] : Array.isArray(setCookieHeaders) ? setCookieHeaders : splitSetCookie(setCookieHeaders);
34786
+ const entries = setCookieEntries(setCookieHeaders);
34744
34787
  const jar = /* @__PURE__ */ new Map();
34745
34788
  for (const entry of entries) {
34746
- if (MAX_AGE_ZERO_RE.test(entry) || EXPIRES_EPOCH_RE.test(entry))
34747
- continue;
34748
- const nameValue = (entry.split(";")[0] ?? "").trim();
34749
- const eqIdx = nameValue.indexOf("=");
34750
- if (eqIdx < 1)
34789
+ if (isDeletionCookie(entry))
34751
34790
  continue;
34752
- const name = nameValue.slice(0, eqIdx).trim();
34753
- const value = nameValue.slice(eqIdx + 1).trim();
34754
- if (!value)
34791
+ const pair = parseNameValue(entry);
34792
+ if (!pair || !pair.value)
34755
34793
  continue;
34756
- jar.set(name, value);
34794
+ jar.set(pair.name, pair.value);
34757
34795
  }
34758
34796
  const cookies = {};
34759
34797
  for (const [k, v] of jar)
@@ -34761,6 +34799,18 @@ function parseCookieJar(setCookieHeaders) {
34761
34799
  const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
34762
34800
  return { cookies, cookieHeader };
34763
34801
  }
34802
+ function setCookieEntries(source2) {
34803
+ if (source2 == null)
34804
+ return [];
34805
+ if (Array.isArray(source2))
34806
+ return source2;
34807
+ if (typeof source2 === "string")
34808
+ return splitSetCookie(source2);
34809
+ if (typeof source2.getSetCookie === "function")
34810
+ return source2.getSetCookie();
34811
+ const joined = source2.get("set-cookie");
34812
+ return joined ? splitSetCookie(joined) : [];
34813
+ }
34764
34814
  function splitSetCookie(header) {
34765
34815
  return header.split(/,(?=\s*[^;,\s]+\s*=)/).map((s) => s.trim()).filter((s) => s.length > 0);
34766
34816
  }
@@ -34771,6 +34821,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
34771
34821
  var NonEmptyString = external_exports.string().min(1);
34772
34822
  var IsoDate = external_exports.iso.date();
34773
34823
  var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34824
+ var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
34825
+ var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
34774
34826
  var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
34775
34827
  var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34776
34828
  var paginationSchema = {
@@ -35738,10 +35790,20 @@ async function openEncryptedFrame(sessionKey, frame) {
35738
35790
 
35739
35791
  // node_modules/@fetchproxy/server/dist/election.js
35740
35792
  import { createServer, Server as HttpServer } from "node:http";
35793
+ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
35741
35794
  async function electRole(opts) {
35742
35795
  const server = createServer();
35796
+ const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
35743
35797
  return new Promise((resolve, reject) => {
35798
+ let timer;
35799
+ const clearBindTimer = () => {
35800
+ if (timer) {
35801
+ clearTimeout(timer);
35802
+ timer = void 0;
35803
+ }
35804
+ };
35744
35805
  const onError = (e) => {
35806
+ clearBindTimer();
35745
35807
  server.removeListener("listening", onListening);
35746
35808
  if (e.code === "EADDRINUSE") {
35747
35809
  try {
@@ -35754,11 +35816,25 @@ async function electRole(opts) {
35754
35816
  }
35755
35817
  };
35756
35818
  const onListening = () => {
35819
+ clearBindTimer();
35757
35820
  server.removeListener("error", onError);
35758
35821
  resolve({ role: "host", server });
35759
35822
  };
35760
35823
  server.once("error", onError);
35761
35824
  server.once("listening", onListening);
35825
+ if (bindTimeoutMs > 0) {
35826
+ timer = setTimeout(() => {
35827
+ server.removeListener("error", onError);
35828
+ server.removeListener("listening", onListening);
35829
+ try {
35830
+ server.close();
35831
+ } catch {
35832
+ }
35833
+ reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
35834
+ }, bindTimeoutMs);
35835
+ if (typeof timer.unref === "function")
35836
+ timer.unref();
35837
+ }
35762
35838
  server.listen(opts.port, opts.host);
35763
35839
  });
35764
35840
  }
@@ -35851,6 +35927,44 @@ var SessionState = class {
35851
35927
  }
35852
35928
  };
35853
35929
 
35930
+ // node_modules/@fetchproxy/server/dist/session-ready.js
35931
+ var SESSION_READY_TIMEOUT_MS = 3e4;
35932
+ var FetchproxySessionNotReadyError = class extends Error {
35933
+ reason;
35934
+ pairCode;
35935
+ mcpId;
35936
+ hint;
35937
+ constructor(info) {
35938
+ const pairing = info.pairCode !== null && info.pairCode !== "";
35939
+ const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
35940
+ super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
35941
+ this.name = "FetchproxySessionNotReadyError";
35942
+ this.reason = pairing ? "pair-required" : "not-ready";
35943
+ this.pairCode = pairing ? info.pairCode : null;
35944
+ this.mcpId = info.mcpId;
35945
+ this.hint = hint;
35946
+ Object.setPrototypeOf(this, new.target.prototype);
35947
+ }
35948
+ };
35949
+ async function awaitSessionReady(ready, opts) {
35950
+ const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
35951
+ if (ms <= 0)
35952
+ return ready;
35953
+ let timer;
35954
+ const timeout = new Promise((_, reject) => {
35955
+ timer = setTimeout(() => {
35956
+ reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
35957
+ }, ms);
35958
+ timer.unref?.();
35959
+ });
35960
+ try {
35961
+ return await Promise.race([ready, timeout]);
35962
+ } finally {
35963
+ if (timer)
35964
+ clearTimeout(timer);
35965
+ }
35966
+ }
35967
+
35854
35968
  // node_modules/@fetchproxy/server/dist/host.js
35855
35969
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
35856
35970
  var enc2 = new TextEncoder();
@@ -35938,6 +36052,29 @@ async function startHost(opts) {
35938
36052
  return;
35939
36053
  }
35940
36054
  if (frame.type === "hello" && frame.role === "server") {
36055
+ const peerEdPub = fromB64(frame.identityEd25519Pub);
36056
+ const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
36057
+ const peerSig = fromB64(frame.sessionSig);
36058
+ let peerSigOk = false;
36059
+ try {
36060
+ peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
36061
+ } catch {
36062
+ peerSigOk = false;
36063
+ }
36064
+ if (!peerSigOk) {
36065
+ console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
36066
+ ws.close(1008, "peer hello signature invalid");
36067
+ return;
36068
+ }
36069
+ const existing = peers.get(frame.mcpId);
36070
+ if (existing && existing.ws !== ws) {
36071
+ const existingEdPub = existing.helloFrame.identityEd25519Pub;
36072
+ if (existingEdPub !== frame.identityEd25519Pub) {
36073
+ console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
36074
+ ws.close(1008, "mcpId already registered to another identity");
36075
+ return;
36076
+ }
36077
+ }
35941
36078
  identified = "peer";
35942
36079
  peerMcpId = frame.mcpId;
35943
36080
  peers.set(frame.mcpId, { ws, helloFrame: frame });
@@ -36030,8 +36167,10 @@ async function startHost(opts) {
36030
36167
  resetSessionPromise();
36031
36168
  disconnectListeners.forEach((cb) => cb());
36032
36169
  }
36033
- if (identified === "peer" && peerMcpId)
36034
- peers.delete(peerMcpId);
36170
+ if (identified === "peer" && peerMcpId) {
36171
+ if (peers.get(peerMcpId)?.ws === ws)
36172
+ peers.delete(peerMcpId);
36173
+ }
36035
36174
  });
36036
36175
  });
36037
36176
  return {
@@ -36047,7 +36186,10 @@ async function startHost(opts) {
36047
36186
  });
36048
36187
  }),
36049
36188
  sendOwnInner: async (inner) => {
36050
- const session = await ownSessionReady;
36189
+ const session = await awaitSessionReady(ownSessionReady, {
36190
+ mcpId: opts.ownMcpId,
36191
+ pendingPairCode: () => ownPendingPairCode
36192
+ });
36051
36193
  if (!extensionWs)
36052
36194
  throw new Error("host: no extension connected");
36053
36195
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
@@ -36152,7 +36294,10 @@ async function startPeer(opts) {
36152
36294
  ws,
36153
36295
  session: sessionPromise,
36154
36296
  sendInner: async (inner) => {
36155
- await sessionPromise;
36297
+ await awaitSessionReady(sessionPromise, {
36298
+ mcpId: opts.mcpId,
36299
+ pendingPairCode: () => pendingPairCode
36300
+ });
36156
36301
  const s = session;
36157
36302
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
36158
36303
  ws.send(JSON.stringify(sealed));
@@ -36818,6 +36963,35 @@ var FetchproxyServer = class {
36818
36963
  this.keepAliveTimer = null;
36819
36964
  }
36820
36965
  }
36966
+ /**
36967
+ * Send an inner request frame via whichever bridge handle is active. If the
36968
+ * send throws (e.g. `FetchproxySessionNotReadyError` — the session never
36969
+ * confirmed), the frame never reached the bridge, so no reply will arrive:
36970
+ * drop the just-registered pending resolver for this id (it lives in exactly
36971
+ * one of the op maps — request ids are unique) so it doesn't leak until the
36972
+ * server closes, then rethrow.
36973
+ */
36974
+ async sendInnerFrame(inner) {
36975
+ try {
36976
+ if (this.hostHandle) {
36977
+ await this.hostHandle.sendOwnInner(inner);
36978
+ } else if (this.peerHandle) {
36979
+ await this.peerHandle.sendInner(inner);
36980
+ }
36981
+ } catch (err) {
36982
+ if ("id" in inner && typeof inner.id === "number") {
36983
+ const { id } = inner;
36984
+ this.pending.delete(id);
36985
+ this.pendingReadCookies.delete(id);
36986
+ this.pendingStorage.delete(id);
36987
+ this.pendingCapture.delete(id);
36988
+ this.pendingRedirect.delete(id);
36989
+ this.pendingDownload.delete(id);
36990
+ this.pendingIdb.delete(id);
36991
+ }
36992
+ throw err;
36993
+ }
36994
+ }
36821
36995
  /**
36822
36996
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
36823
36997
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
@@ -36829,11 +37003,7 @@ var FetchproxyServer = class {
36829
37003
  const pending = new Promise((resolve) => {
36830
37004
  this.pending.set(id, resolve);
36831
37005
  });
36832
- if (this.hostHandle) {
36833
- await this.hostHandle.sendOwnInner(inner);
36834
- } else if (this.peerHandle) {
36835
- await this.peerHandle.sendInner(inner);
36836
- }
37006
+ await this.sendInnerFrame(inner);
36837
37007
  const timeoutMs = this.opts.fetchTimeoutMs;
36838
37008
  if (timeoutMs === void 0 || timeoutMs <= 0)
36839
37009
  return pending;
@@ -36862,6 +37032,51 @@ var FetchproxyServer = class {
36862
37032
  clearTimeout(timer);
36863
37033
  }
36864
37034
  }
37035
+ /**
37036
+ * FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
37037
+ *
37038
+ * Before this, only `fetch()` raced its `pending` reply against a timer
37039
+ * (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
37040
+ * `capture_request_header`, `capture_redirect`, `download`, and
37041
+ * `read_indexed_db` awaited their `pending` promise with no race, so a
37042
+ * wedged extension hung the tool call indefinitely.
37043
+ *
37044
+ * `pending` is the already-registered reply promise. `pendingMap`/`id`
37045
+ * point at the op-specific map entry so we can drop it on expiry exactly
37046
+ * as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
37047
+ * resolve into a stale resolver / leak. On timeout we reject with a
37048
+ * `FetchproxyTimeoutError` (the same throwable the convenience methods
37049
+ * already surface for fetch timeouts). `0`/unset opts out (unbounded),
37050
+ * matching the fetch path.
37051
+ */
37052
+ async _withVerbTimeout(pending, pendingMap, id, url2) {
37053
+ const timeoutMs = this.opts.fetchTimeoutMs;
37054
+ if (timeoutMs === void 0 || timeoutMs <= 0)
37055
+ return pending;
37056
+ let timer;
37057
+ const start = Date.now();
37058
+ try {
37059
+ return await Promise.race([
37060
+ pending,
37061
+ new Promise((_resolve, reject) => {
37062
+ timer = setTimeout(() => {
37063
+ pendingMap.delete(id);
37064
+ reject(new FetchproxyTimeoutError({
37065
+ url: url2,
37066
+ timeoutMs,
37067
+ role: this.role,
37068
+ port: this.opts.port,
37069
+ elapsedMs: Date.now() - start,
37070
+ retryAttempted: false
37071
+ }));
37072
+ }, timeoutMs);
37073
+ })
37074
+ ]);
37075
+ } finally {
37076
+ if (timer)
37077
+ clearTimeout(timer);
37078
+ }
37079
+ }
36865
37080
  /**
36866
37081
  * Map an `ok:false` fetch result to its typed throwable. Centralizes
36867
37082
  * the kind-to-error-class switch so `request()` and (via the same
@@ -37152,12 +37367,8 @@ var FetchproxyServer = class {
37152
37367
  const pending = new Promise((resolve) => {
37153
37368
  this.pendingReadCookies.set(id, resolve);
37154
37369
  });
37155
- if (this.hostHandle) {
37156
- await this.hostHandle.sendOwnInner(inner);
37157
- } else if (this.peerHandle) {
37158
- await this.peerHandle.sendInner(inner);
37159
- }
37160
- const result = await pending;
37370
+ await this.sendInnerFrame(inner);
37371
+ const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37161
37372
  if (!result.ok) {
37162
37373
  throw new FetchproxyProtocolError(result.error);
37163
37374
  }
@@ -37223,12 +37434,8 @@ var FetchproxyServer = class {
37223
37434
  const pending = new Promise((resolve, reject) => {
37224
37435
  this.pendingStorage.set(id, { resolve, reject });
37225
37436
  });
37226
- if (this.hostHandle) {
37227
- await this.hostHandle.sendOwnInner(inner);
37228
- } else if (this.peerHandle) {
37229
- await this.peerHandle.sendInner(inner);
37230
- }
37231
- return pending;
37437
+ await this.sendInnerFrame(inner);
37438
+ return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
37232
37439
  }
37233
37440
  /**
37234
37441
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
@@ -37332,12 +37539,8 @@ var FetchproxyServer = class {
37332
37539
  const pending = new Promise((resolve, reject) => {
37333
37540
  this.pendingCapture.set(id, { resolve, reject });
37334
37541
  });
37335
- if (this.hostHandle) {
37336
- await this.hostHandle.sendOwnInner(inner);
37337
- } else if (this.peerHandle) {
37338
- await this.peerHandle.sendInner(inner);
37339
- }
37340
- return pending;
37542
+ await this.sendInnerFrame(inner);
37543
+ return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
37341
37544
  }
37342
37545
  /**
37343
37546
  * Snapshot the redirect target URL of the next request the browser
@@ -37421,12 +37624,8 @@ var FetchproxyServer = class {
37421
37624
  const pending = new Promise((resolve, reject) => {
37422
37625
  this.pendingRedirect.set(id, { resolve, reject });
37423
37626
  });
37424
- if (this.hostHandle) {
37425
- await this.hostHandle.sendOwnInner(inner);
37426
- } else if (this.peerHandle) {
37427
- await this.peerHandle.sendInner(inner);
37428
- }
37429
- return pending;
37627
+ await this.sendInnerFrame(inner);
37628
+ return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
37430
37629
  }
37431
37630
  /**
37432
37631
  * Download `url` through the BROWSER's own network stack via
@@ -37507,12 +37706,8 @@ var FetchproxyServer = class {
37507
37706
  const pending = new Promise((resolve, reject) => {
37508
37707
  this.pendingDownload.set(id, { resolve, reject });
37509
37708
  });
37510
- if (this.hostHandle) {
37511
- await this.hostHandle.sendOwnInner(inner);
37512
- } else if (this.peerHandle) {
37513
- await this.peerHandle.sendInner(inner);
37514
- }
37515
- return pending;
37709
+ await this.sendInnerFrame(inner);
37710
+ return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
37516
37711
  }
37517
37712
  /**
37518
37713
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -37559,12 +37754,8 @@ var FetchproxyServer = class {
37559
37754
  const pending = new Promise((resolve, reject) => {
37560
37755
  this.pendingIdb.set(id, { resolve, reject });
37561
37756
  });
37562
- if (this.hostHandle) {
37563
- await this.hostHandle.sendOwnInner(inner);
37564
- } else if (this.peerHandle) {
37565
- await this.peerHandle.sendInner(inner);
37566
- }
37567
- return pending;
37757
+ await this.sendInnerFrame(inner);
37758
+ return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37568
37759
  }
37569
37760
  assertScopeSubset(requested, declared, label) {
37570
37761
  const undeclared = undeclaredKeys(requested, declared);
@@ -38000,7 +38191,7 @@ function loadAccount(env = process.env) {
38000
38191
  // package.json
38001
38192
  var package_default = {
38002
38193
  name: "infinitecampus-mcp",
38003
- version: "2.3.4",
38194
+ version: "2.3.6",
38004
38195
  mcpName: "io.github.chrischall/infinitecampus-mcp",
38005
38196
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
38006
38197
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38042,16 +38233,16 @@ var package_default = {
38042
38233
  "test:watch": "vitest"
38043
38234
  },
38044
38235
  dependencies: {
38045
- "@chrischall/mcp-utils": "^0.5.0",
38236
+ "@chrischall/mcp-utils": "^0.12.0",
38046
38237
  "@fetchproxy/bootstrap": "^1.0.0",
38047
38238
  "@fetchproxy/server": "^1.0.0",
38048
38239
  "@modelcontextprotocol/sdk": "^1.29.0",
38049
38240
  dotenv: "^17.4.2",
38050
- "node-html-parser": "^7.1.0",
38241
+ "node-html-parser": "^8.0.2",
38051
38242
  zod: "^4.4.3"
38052
38243
  },
38053
38244
  devDependencies: {
38054
- "@types/node": "^25.9.1",
38245
+ "@types/node": "^26.0.0",
38055
38246
  "@vitest/coverage-v8": "^4.1.7",
38056
38247
  esbuild: "^0.28.0",
38057
38248
  typescript: "^6.0.3",
@@ -38130,10 +38321,163 @@ async function resolveAuth() {
38130
38321
  // src/client.ts
38131
38322
  import { writeFile as writeFile2, stat } from "fs/promises";
38132
38323
  import { dirname } from "path";
38324
+
38325
+ // node_modules/@chrischall/mcp-utils/dist/session/index.js
38326
+ var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
38327
+ var CookieSessionManager = class {
38328
+ session;
38329
+ inFlight;
38330
+ /** A cached *permanent* config error: once set, every `ensure` rethrows it. */
38331
+ permanentError;
38332
+ /** When the current session was minted (login) or installed (seed) — for maxAgeMs. */
38333
+ sessionAt = 0;
38334
+ loginFn;
38335
+ isExpiredFn;
38336
+ isPermanentErrorFn;
38337
+ maxAgeMs;
38338
+ now;
38339
+ onReplayLoginErrorFn;
38340
+ constructor(opts) {
38341
+ this.loginFn = opts.login;
38342
+ this.isExpiredFn = opts.isExpired ?? (() => false);
38343
+ this.isPermanentErrorFn = opts.isPermanentError ?? (() => false);
38344
+ this.maxAgeMs = opts.maxAgeMs;
38345
+ this.now = opts.now ?? Date.now;
38346
+ this.onReplayLoginErrorFn = opts.onReplayLoginError;
38347
+ }
38348
+ /** The current session, or `undefined` before the first successful login. */
38349
+ get current() {
38350
+ return this.session;
38351
+ }
38352
+ /** True when {@link CookieSessionManagerOptions.maxAgeMs} says the session is too old. */
38353
+ isStale() {
38354
+ return this.maxAgeMs !== void 0 && this.now() - this.sessionAt >= this.maxAgeMs;
38355
+ }
38356
+ /**
38357
+ * Return the current session, or single-flight a login if there is none.
38358
+ *
38359
+ * Concurrent callers share one in-flight login; the in-flight promise is
38360
+ * cleared on settle (success OR failure) so a rejected login never sticks and
38361
+ * the next call retries. A login error classified permanent by
38362
+ * {@link CookieSessionManagerOptions.isPermanentError} is cached and rethrown
38363
+ * on every later call; a transient error is not cached (next call retries).
38364
+ * With {@link CookieSessionManagerOptions.maxAgeMs} set, a session past its
38365
+ * TTL is invalidated first, so the call falls through to a (single-flight)
38366
+ * re-login.
38367
+ */
38368
+ async ensure() {
38369
+ if (this.session !== void 0) {
38370
+ if (!this.isStale())
38371
+ return this.session;
38372
+ this.invalidate();
38373
+ }
38374
+ if (this.permanentError !== void 0)
38375
+ throw this.permanentError;
38376
+ if (this.inFlight === void 0) {
38377
+ this.inFlight = this.runLogin();
38378
+ }
38379
+ return this.inFlight;
38380
+ }
38381
+ /**
38382
+ * Install an externally-minted session (e.g. infinitecampus's CUPS linked-
38383
+ * district discovery, which mints sessions outside {@link
38384
+ * CookieSessionManagerOptions.login}). The seed becomes the current session
38385
+ * with a fresh {@link CookieSessionManagerOptions.maxAgeMs} clock, and any
38386
+ * in-flight login is DETACHED: its waiters still receive its result, but a
38387
+ * late resolution will not overwrite the seed. A cached permanent config
38388
+ * error is left intact — the login path is still misconfigured, and the next
38389
+ * post-seed re-login should keep saying so.
38390
+ */
38391
+ seed(session) {
38392
+ this.session = session;
38393
+ this.sessionAt = this.now();
38394
+ this.inFlight = void 0;
38395
+ }
38396
+ /**
38397
+ * One login attempt. Self-clears `inFlight` on settle so a rejected login
38398
+ * never sticks, and stamps the resulting session only while it's still the
38399
+ * live attempt (an `invalidate()` mid-flight cleared `inFlight` — a stale
38400
+ * resolution must not re-stamp the dropped session over the new state).
38401
+ */
38402
+ runLogin() {
38403
+ const holder = {};
38404
+ holder.p = (async () => {
38405
+ try {
38406
+ const session = await this.loginFn();
38407
+ if (this.inFlight === holder.p) {
38408
+ this.session = session;
38409
+ this.sessionAt = this.now();
38410
+ }
38411
+ return session;
38412
+ } catch (err) {
38413
+ if (this.isPermanentErrorFn(err))
38414
+ this.permanentError = err;
38415
+ throw err;
38416
+ } finally {
38417
+ if (this.inFlight === holder.p)
38418
+ this.inFlight = void 0;
38419
+ }
38420
+ })();
38421
+ return holder.p;
38422
+ }
38423
+ /**
38424
+ * Drop the current session (and any in-flight login) so the next
38425
+ * {@link ensure} re-runs {@link CookieSessionManagerOptions.login}. Does NOT
38426
+ * clear a cached permanent config error — that only clears on a fresh process
38427
+ * (new config). Used to recover from a detected session expiry.
38428
+ */
38429
+ invalidate() {
38430
+ this.session = void 0;
38431
+ this.inFlight = void 0;
38432
+ }
38433
+ /**
38434
+ * Run an authenticated `call` with the current session and reactive
38435
+ * expiry-replay. `call` receives the session and returns a `Response`. If
38436
+ * {@link CookieSessionManagerOptions.isExpired} flags the response, the
38437
+ * session is invalidated, a single-flight re-login runs, and `call` is
38438
+ * replayed EXACTLY once. A persistent expiry surfaces the (second) response
38439
+ * rather than looping. If the re-login itself fails, the original
38440
+ * (expired-looking) response is returned so the caller can surface a clean
38441
+ * sign-in error rather than the login failure.
38442
+ *
38443
+ * `call` resolves to `R` (the generic response type, default {@link Response});
38444
+ * the manager passes it untouched to {@link CookieSessionManagerOptions.isExpired}
38445
+ * and returns it untouched, so a custom transport type flows through cleanly.
38446
+ */
38447
+ async withSession(call) {
38448
+ const session = await this.ensure();
38449
+ const res = await call(session);
38450
+ if (!await this.isExpiredFn(res))
38451
+ return res;
38452
+ if (this.current === session)
38453
+ this.invalidate();
38454
+ let fresh;
38455
+ try {
38456
+ fresh = await this.ensure();
38457
+ } catch (err) {
38458
+ this.onReplayLoginErrorFn?.(err);
38459
+ return res;
38460
+ }
38461
+ return call(fresh);
38462
+ }
38463
+ };
38464
+ function createCookieSessionManager(opts) {
38465
+ return new CookieSessionManager(opts);
38466
+ }
38467
+
38468
+ // src/client.ts
38133
38469
  var SESSION_TTL_MS = 5 * 60 * 60 * 1e3;
38134
38470
  var ICClient = class {
38135
38471
  accounts = /* @__PURE__ */ new Map();
38136
- sessions = /* @__PURE__ */ new Map();
38472
+ /**
38473
+ * One CookieSessionManager per district (primary + each discovered linked
38474
+ * district). The manager owns the session lifecycle: single-flight login,
38475
+ * `withSession` exactly-one 401 replay, `invalidate()`, permanent-error
38476
+ * caching, proactive `maxAgeMs` (SESSION_TTL_MS) expiry, and — for linked
38477
+ * districts — the CUPS-minted session installed via `seed()` during
38478
+ * discovery.
38479
+ */
38480
+ managers = /* @__PURE__ */ new Map();
38137
38481
  linkedTo = /* @__PURE__ */ new Map();
38138
38482
  // linkedDistrictName → primaryDistrictName
38139
38483
  primaryName;
@@ -38147,6 +38491,8 @@ var ICClient = class {
38147
38491
  */
38148
38492
  fetchproxyMode = false;
38149
38493
  fetchproxyDiscoveryRan = false;
38494
+ /** fetchproxy cookies, consumed by the primary manager's first login. */
38495
+ preloaded = null;
38150
38496
  /**
38151
38497
  * `preloaded` is the fetchproxy escape hatch: when set, the client treats
38152
38498
  * the supplied cookies as a freshly-completed login on the primary account.
@@ -38159,21 +38505,66 @@ var ICClient = class {
38159
38505
  constructor(account2, opts = {}) {
38160
38506
  this.accounts.set(account2.name, account2);
38161
38507
  this.primaryName = account2.name;
38508
+ this.managers.set(account2.name, this.makeManager(account2));
38162
38509
  if (opts.preloaded) {
38163
- this.sessions.set(account2.name, {
38164
- cookie: opts.preloaded.cookieHeader,
38165
- xsrfToken: opts.preloaded.xsrfToken,
38166
- loggedInAt: Date.now(),
38167
- loginInFlight: null
38168
- });
38510
+ this.preloaded = opts.preloaded;
38169
38511
  this.fetchproxyMode = true;
38170
38512
  }
38171
38513
  }
38514
+ /**
38515
+ * Build the per-district CookieSessionManager.
38516
+ *
38517
+ * - `maxAgeMs: SESSION_TTL_MS` makes `ensure()` proactively invalidate and
38518
+ * re-login a session older than 5h (single-flight), replacing IC's old
38519
+ * hand-rolled `ensureFresh` wrapper. `now` reads `Date.now` at call time so
38520
+ * the clock stays mockable in tests.
38521
+ * - `isPermanentError` caches the one config error the process can never
38522
+ * recover from (fetchproxy no-creds) so `ensure()` fails fast instead of
38523
+ * pointlessly retrying a login that can never succeed.
38524
+ * - `onReplayLoginError` rethrows the re-login failure from `withSession`'s
38525
+ * expiry-replay path. By default that failure is swallowed and the stale
38526
+ * 401 returned; rethrowing surfaces the actionable error (e.g. fetchproxy's
38527
+ * "sign back into your IC portal in the browser") to the caller instead of
38528
+ * a generic SessionExpiredError — the behavior IC's `lastLoginError`
38529
+ * bookkeeping used to provide.
38530
+ */
38531
+ makeManager(account2) {
38532
+ return createCookieSessionManager({
38533
+ login: () => this.login(account2),
38534
+ isExpired: (res) => this.detectExpiredSession(account2.name, res),
38535
+ isPermanentError: (e) => e instanceof AuthFailedError && e.permanent,
38536
+ maxAgeMs: SESSION_TTL_MS,
38537
+ now: () => Date.now(),
38538
+ onReplayLoginError: (err) => {
38539
+ throw err;
38540
+ }
38541
+ });
38542
+ }
38543
+ /**
38544
+ * `isExpired` predicate for `withSession`: a 401 means the session died.
38545
+ * Side effect for linked districts: the shared CUPS SSO umbrella died with
38546
+ * it, so drop the primary and every *sibling* linked session too — the
38547
+ * linked district's re-login (via `login()`'s linked branch) then re-logs
38548
+ * the primary in, whose discovery re-establishes all of them. `withSession`
38549
+ * invalidates the requesting district itself. Mirrors the old hand-rolled
38550
+ * doRequest 401 handling.
38551
+ */
38552
+ detectExpiredSession(district, res) {
38553
+ if (res.status !== 401) return false;
38554
+ const primaryName = this.linkedTo.get(district);
38555
+ if (primaryName) {
38556
+ this.managers.get(primaryName).invalidate();
38557
+ for (const linked of this.linkedTo.keys()) {
38558
+ if (linked !== district) this.managers.get(linked).invalidate();
38559
+ }
38560
+ }
38561
+ return true;
38562
+ }
38172
38563
  async ensureDiscovery() {
38173
- await this.ensureSession(this.accounts.get(this.primaryName));
38564
+ const session = await this.managers.get(this.primaryName).ensure();
38174
38565
  if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan) {
38175
38566
  this.fetchproxyDiscoveryRan = true;
38176
- await this.discoverLinkedDistricts(this.accounts.get(this.primaryName));
38567
+ await this.discoverLinkedDistricts(this.accounts.get(this.primaryName), session);
38177
38568
  }
38178
38569
  }
38179
38570
  listDistricts() {
@@ -38206,50 +38597,37 @@ var ICClient = class {
38206
38597
  account2 = this.accounts.get(district);
38207
38598
  if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
38208
38599
  }
38209
- await this.ensureSession(account2);
38600
+ const session = await this.managers.get(account2.name).ensure();
38210
38601
  if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan && account2.name === this.primaryName) {
38211
38602
  this.fetchproxyDiscoveryRan = true;
38212
- await this.discoverLinkedDistricts(account2);
38213
- }
38214
- return this.doRequest(account2, path, opts, false);
38215
- }
38216
- async ensureSession(account2) {
38217
- let s = this.sessions.get(account2.name);
38218
- if (s && Date.now() - s.loggedInAt < SESSION_TTL_MS) return;
38219
- if (s?.loginInFlight) {
38220
- await s.loginInFlight;
38221
- return;
38222
- }
38223
- if (!s) {
38224
- s = { cookie: "", xsrfToken: "", loggedInAt: 0, loginInFlight: null };
38225
- this.sessions.set(account2.name, s);
38226
- }
38227
- s.loginInFlight = this.login(account2);
38228
- try {
38229
- await s.loginInFlight;
38230
- } finally {
38231
- s.loginInFlight = null;
38603
+ await this.discoverLinkedDistricts(account2, session);
38232
38604
  }
38605
+ return this.doRequest(account2, path, opts);
38233
38606
  }
38234
38607
  async login(account2) {
38235
38608
  const primaryName = this.linkedTo.get(account2.name);
38236
38609
  if (primaryName) {
38237
- this.sessions.delete(account2.name);
38238
- const primaryAccount = this.accounts.get(primaryName);
38239
- await this.ensureSession(primaryAccount);
38240
- if (!this.sessions.has(account2.name)) {
38610
+ await this.managers.get(primaryName).ensure();
38611
+ const restored = this.managers.get(account2.name).current;
38612
+ if (!restored) {
38241
38613
  throw new AuthFailedError(
38242
38614
  account2.name,
38243
38615
  "linked-district session could not be re-established via the primary district",
38244
38616
  { credentialHint: false }
38245
38617
  );
38246
38618
  }
38247
- return;
38619
+ return restored;
38620
+ }
38621
+ if (this.preloaded) {
38622
+ const { cookieHeader, xsrfToken } = this.preloaded;
38623
+ this.preloaded = null;
38624
+ return { cookieHeader, xsrfToken };
38248
38625
  }
38249
38626
  if (!account2.username || !account2.password) {
38250
38627
  throw new AuthFailedError(
38251
38628
  account2.name,
38252
- "session expired and no IC_USERNAME/IC_PASSWORD set \u2014 sign back into your IC portal in the browser and restart the MCP"
38629
+ "session expired and no IC_USERNAME/IC_PASSWORD set \u2014 sign back into your IC portal in the browser and restart the MCP",
38630
+ { permanent: true }
38253
38631
  );
38254
38632
  }
38255
38633
  const postRes = await fetch(
@@ -38282,17 +38660,17 @@ var ICClient = class {
38282
38660
  if (!cookies.cookieHeader) {
38283
38661
  throw new AuthFailedError(account2.name, "login response missing session cookies");
38284
38662
  }
38285
- const session = this.sessions.get(account2.name);
38286
- session.cookie = cookies.cookieHeader;
38287
- session.xsrfToken = cookies.xsrfToken;
38288
- session.loggedInAt = Date.now();
38289
- await this.discoverLinkedDistricts(account2);
38663
+ const session = {
38664
+ cookieHeader: cookies.cookieHeader,
38665
+ xsrfToken: cookies.xsrfToken
38666
+ };
38667
+ await this.discoverLinkedDistricts(account2, session);
38668
+ return session;
38290
38669
  }
38291
- async discoverLinkedDistricts(account2) {
38670
+ async discoverLinkedDistricts(account2, session) {
38292
38671
  try {
38293
- const session = this.sessions.get(account2.name);
38294
38672
  const baseHeaders = {
38295
- Cookie: session.cookie,
38673
+ Cookie: session.cookieHeader,
38296
38674
  Accept: "application/json",
38297
38675
  "Content-Type": "application/json",
38298
38676
  Referer: `${account2.baseUrl}/campus/nav-wrapper/`,
@@ -38360,13 +38738,14 @@ var ICClient = class {
38360
38738
  password: "(linked)"
38361
38739
  };
38362
38740
  this.accounts.set(linked.districtName, syntheticAccount);
38363
- this.sessions.set(linked.districtName, {
38364
- cookie: switchCookies.cookieHeader,
38365
- xsrfToken: switchCookies.xsrfToken,
38366
- loggedInAt: Date.now(),
38367
- loginInFlight: null
38368
- });
38369
38741
  this.linkedTo.set(linked.districtName, account2.name);
38742
+ if (!this.managers.has(linked.districtName)) {
38743
+ this.managers.set(linked.districtName, this.makeManager(syntheticAccount));
38744
+ }
38745
+ this.managers.get(linked.districtName).seed({
38746
+ cookieHeader: switchCookies.cookieHeader,
38747
+ xsrfToken: switchCookies.xsrfToken
38748
+ });
38370
38749
  console.error(`[ic] Linked district discovered: ${linked.districtName} (${linked.appName})`);
38371
38750
  } catch (e) {
38372
38751
  console.error(`[ic] CUPS flow failed for ${linked.districtName}: ${e instanceof Error ? e.message : e}`);
@@ -38392,12 +38771,11 @@ var ICClient = class {
38392
38771
  }
38393
38772
  const account2 = this.accounts.get(district);
38394
38773
  if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
38395
- await this.ensureSession(account2);
38396
- const session = this.sessions.get(account2.name);
38774
+ const session = await this.managers.get(account2.name).ensure();
38397
38775
  const url2 = /^https?:\/\//i.test(path) ? path : `${account2.baseUrl}${path}`;
38398
38776
  const res = await fetch(url2, {
38399
38777
  headers: {
38400
- Cookie: session.cookie,
38778
+ Cookie: session.cookieHeader,
38401
38779
  ...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {}
38402
38780
  }
38403
38781
  });
@@ -38410,34 +38788,23 @@ var ICClient = class {
38410
38788
  contentType: res.headers.get("content-type") ?? "application/octet-stream"
38411
38789
  };
38412
38790
  }
38413
- async doRequest(account2, path, opts, isRetry) {
38414
- const session = this.sessions.get(account2.name);
38791
+ async doRequest(account2, path, opts) {
38792
+ const mgr = this.managers.get(account2.name);
38415
38793
  const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json";
38416
- const res = await fetch(`${account2.baseUrl}${path}`, {
38417
- method: opts.method ?? "GET",
38418
- headers: {
38419
- Cookie: session.cookie,
38420
- Accept: accept,
38421
- ...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {},
38422
- ...opts.headers ?? {}
38423
- },
38424
- body: opts.body
38425
- });
38794
+ const res = await mgr.withSession(
38795
+ async (session) => fetch(`${account2.baseUrl}${path}`, {
38796
+ method: opts.method ?? "GET",
38797
+ headers: {
38798
+ Cookie: session.cookieHeader,
38799
+ Accept: accept,
38800
+ ...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {},
38801
+ ...opts.headers ?? {}
38802
+ },
38803
+ body: opts.body
38804
+ })
38805
+ );
38426
38806
  if (res.status === 401) {
38427
- if (isRetry) throw new SessionExpiredError(account2.name);
38428
- this.sessions.delete(account2.name);
38429
- const primaryName = this.linkedTo.get(account2.name);
38430
- if (primaryName) {
38431
- this.sessions.delete(primaryName);
38432
- for (const [linked] of this.linkedTo) {
38433
- this.sessions.delete(linked);
38434
- }
38435
- const primaryAccount = this.accounts.get(primaryName);
38436
- await this.ensureSession(primaryAccount);
38437
- } else {
38438
- await this.ensureSession(account2);
38439
- }
38440
- return this.doRequest(account2, path, opts, true);
38807
+ throw new SessionExpiredError(account2.name);
38441
38808
  }
38442
38809
  if (res.status >= 500) throw new PortalUnreachableError(account2.name, res.status);
38443
38810
  if (!res.ok) throw new Error(`IC ${res.status} ${res.statusText} for ${path}`);
@@ -38450,14 +38817,11 @@ var ICClient = class {
38450
38817
  };
38451
38818
  function parseSetCookies(headers) {
38452
38819
  const raw = headers.getSetCookie?.() ?? [];
38453
- const entries = raw.length > 0 ? raw : splitFallback(headers.get("set-cookie"));
38454
- const { cookies, cookieHeader } = parseCookieJar(entries);
38820
+ const { cookies, cookieHeader } = parseCookieJar(
38821
+ raw.length > 0 ? raw : headers.get("set-cookie")
38822
+ );
38455
38823
  return { cookieHeader, xsrfToken: cookies["XSRF-TOKEN"] ?? "" };
38456
38824
  }
38457
- function splitFallback(header) {
38458
- if (!header) return [];
38459
- return header.split(",").map((s) => s.trim());
38460
- }
38461
38825
  var UnknownDistrictError = class extends Error {
38462
38826
  constructor(district, available) {
38463
38827
  super(`Unknown district '${district}'. Configured: [${available.join(", ")}]`);
@@ -38474,6 +38838,7 @@ var AuthFailedError = class extends Error {
38474
38838
  * "Check IC_USERNAME and IC_PASSWORD" suffix — used for failures where the
38475
38839
  * credentials are known-good (e.g. a linked-district CUPS/SSO re-discovery
38476
38840
  * failure) and pointing the user at their creds would be misleading.
38841
+ * @param opts.permanent See {@link AuthFailedError.permanent}.
38477
38842
  */
38478
38843
  constructor(district, reason, opts) {
38479
38844
  const detail = reason ? ` (${reason})` : "";
@@ -38482,9 +38847,17 @@ var AuthFailedError = class extends Error {
38482
38847
  this.district = district;
38483
38848
  this.reason = reason;
38484
38849
  this.name = "AuthFailedError";
38850
+ this.permanent = opts?.permanent ?? false;
38485
38851
  }
38486
38852
  district;
38487
38853
  reason;
38854
+ /**
38855
+ * A *permanent* configuration failure: retrying the login inside this
38856
+ * process can never succeed (e.g. fetchproxy mode with no creds — the user
38857
+ * must re-sign-in in the browser and restart). CookieSessionManager caches
38858
+ * and rethrows permanent errors instead of retrying.
38859
+ */
38860
+ permanent;
38488
38861
  };
38489
38862
  var PortalUnreachableError = class extends Error {
38490
38863
  constructor(district, status) {
@@ -38529,40 +38902,6 @@ var FileExistsError = class extends Error {
38529
38902
  path;
38530
38903
  };
38531
38904
 
38532
- // src/tools/_shared.ts
38533
- function textContent(data) {
38534
- return textResult(data);
38535
- }
38536
- function is404(e) {
38537
- return e instanceof Error && e.message.startsWith("IC 404 ");
38538
- }
38539
- function featureDisabled(feature, district, data = []) {
38540
- return textContent({ warning: "FeatureDisabled", feature, district, data });
38541
- }
38542
- async function findStudent(client, district, studentId) {
38543
- const students = await client.request(district, "/campus/api/portal/students");
38544
- return students.find((s) => String(s.personID) === studentId) ?? null;
38545
- }
38546
- function studentNotFound(studentId) {
38547
- return textContent({ error: "StudentNotFound", studentId });
38548
- }
38549
- async function checkFeatureDisabled(client, district, studentId, student, flag, toolName, emptyData = []) {
38550
- const structureID = student.enrollments?.[0]?.structureID;
38551
- if (!structureID) return null;
38552
- try {
38553
- const features = await client.getFeatures(district, structureID, studentId);
38554
- if (features[flag] === false) return featureDisabled(toolName, district, emptyData);
38555
- return null;
38556
- } catch (e) {
38557
- console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
38558
- return null;
38559
- }
38560
- }
38561
- function toArray(value) {
38562
- if (value === null || value === void 0) return [];
38563
- return Array.isArray(value) ? value : [value];
38564
- }
38565
-
38566
38905
  // src/tools/districts.ts
38567
38906
  function registerDistrictTools(server, client) {
38568
38907
  server.registerTool("ic_list_districts", {
@@ -38571,7 +38910,7 @@ function registerDistrictTools(server, client) {
38571
38910
  }, async () => {
38572
38911
  await client.ensureDiscovery();
38573
38912
  const data = client.listDistricts();
38574
- return textContent(data);
38913
+ return textResult(data);
38575
38914
  });
38576
38915
  }
38577
38916
 
@@ -38587,7 +38926,7 @@ function registerStudentTools(server, client) {
38587
38926
  }, async (rawArgs) => {
38588
38927
  const args = argsSchema.parse(rawArgs);
38589
38928
  const data = await client.request(args.district, "/campus/api/portal/students");
38590
- return textContent(data);
38929
+ return textResult(data);
38591
38930
  });
38592
38931
  }
38593
38932
 
@@ -38607,7 +38946,7 @@ function registerScheduleTools(server, client) {
38607
38946
  const args = argsSchema2.parse(rawArgs);
38608
38947
  const params = new URLSearchParams({ personID: args.studentId });
38609
38948
  const data = await client.request(args.district, `/campus/resources/portal/roster?${params}`);
38610
- return textContent(data);
38949
+ return textResult(data);
38611
38950
  });
38612
38951
  }
38613
38952
 
@@ -38645,7 +38984,7 @@ function registerAssignmentTools(server, client) {
38645
38984
  if (args.missingOnly) {
38646
38985
  data = data.filter((a) => a.missing);
38647
38986
  }
38648
- return textContent(data);
38987
+ return textResult(data);
38649
38988
  });
38650
38989
  }
38651
38990
 
@@ -38665,10 +39004,41 @@ function registerGradeTools(server, client) {
38665
39004
  const params = new URLSearchParams({ personID: args.studentId });
38666
39005
  if (args.termId) params.set("termID", args.termId);
38667
39006
  const data = await client.request(args.district, `/campus/resources/portal/grades?${params}`);
38668
- return textContent(data);
39007
+ return textResult(data);
38669
39008
  });
38670
39009
  }
38671
39010
 
39011
+ // src/tools/_shared.ts
39012
+ function is404(e) {
39013
+ return e instanceof Error && e.message.startsWith("IC 404 ");
39014
+ }
39015
+ function featureDisabled(feature, district, data = []) {
39016
+ return textResult({ warning: "FeatureDisabled", feature, district, data });
39017
+ }
39018
+ async function findStudent(client, district, studentId) {
39019
+ const students = await client.request(district, "/campus/api/portal/students");
39020
+ return students.find((s) => String(s.personID) === studentId) ?? null;
39021
+ }
39022
+ function studentNotFound(studentId) {
39023
+ return textResult({ error: "StudentNotFound", studentId });
39024
+ }
39025
+ async function checkFeatureDisabled(client, district, studentId, student, flag, toolName, emptyData = []) {
39026
+ const structureID = student.enrollments?.[0]?.structureID;
39027
+ if (!structureID) return null;
39028
+ try {
39029
+ const features = await client.getFeatures(district, structureID, studentId);
39030
+ if (features[flag] === false) return featureDisabled(toolName, district, emptyData);
39031
+ return null;
39032
+ } catch (e) {
39033
+ console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
39034
+ return null;
39035
+ }
39036
+ }
39037
+ function toArray(value) {
39038
+ if (value === null || value === void 0) return [];
39039
+ return Array.isArray(value) ? value : [value];
39040
+ }
39041
+
38672
39042
  // src/tools/attendance.ts
38673
39043
  var argsSchema5 = external_exports.object({
38674
39044
  district: external_exports.string(),
@@ -38733,7 +39103,7 @@ function registerAttendanceTools(server, client) {
38733
39103
  results.push({ ...entry, terms: trimmedTerms });
38734
39104
  }
38735
39105
  }
38736
- return textContent(results);
39106
+ return textResult(results);
38737
39107
  } catch (e) {
38738
39108
  if (is404(e)) return featureDisabled("attendance", args.district);
38739
39109
  throw e;
@@ -38764,7 +39134,7 @@ function registerBehaviorTools(server, client) {
38764
39134
  if (args.until) params.set("endDate", args.until);
38765
39135
  try {
38766
39136
  const data = await client.request(args.district, `/campus/resources/portal/behavior?${params}`);
38767
- return textContent(data);
39137
+ return textResult(data);
38768
39138
  } catch (e) {
38769
39139
  if (is404(e)) return featureDisabled("behavior", args.district);
38770
39140
  throw e;
@@ -38804,7 +39174,7 @@ function registerFoodServiceTools(server, client) {
38804
39174
  if (args.until) params.set("endDate", args.until);
38805
39175
  try {
38806
39176
  const data = await client.request(args.district, `/campus/resources/portal/foodService?${params}`);
38807
- return textContent(data);
39177
+ return textResult(data);
38808
39178
  } catch (e) {
38809
39179
  if (is404(e)) return featureDisabled("foodService", args.district, EMPTY_FOOD);
38810
39180
  throw e;
@@ -38824,16 +39194,50 @@ var NAMED_ENTITIES = {
38824
39194
  function extractPlainTextFromHtml(html) {
38825
39195
  if (!html)
38826
39196
  return "";
38827
- let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ");
39197
+ let text = stripElementContent(stripElementContent(html, "script"), "style");
38828
39198
  text = text.replace(/<[^>]+>/g, " ");
38829
- text = text.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
38830
- text = text.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)));
39199
+ text = text.replace(/&#(\d+);/g, (whole, d) => codePointOr(Number(d), whole));
39200
+ text = text.replace(/&#x([0-9a-fA-F]+);/g, (whole, h) => codePointOr(parseInt(h, 16), whole));
38831
39201
  text = text.replace(/&([a-zA-Z]+);/g, (whole, name) => {
38832
39202
  const decoded = NAMED_ENTITIES[name.toLowerCase()];
38833
39203
  return decoded ?? whole;
38834
39204
  });
38835
39205
  return text.replace(/\s+/g, " ").trim();
38836
39206
  }
39207
+ function stripElementContent(html, tag) {
39208
+ const lower = html.toLowerCase();
39209
+ const open = `<${tag}`;
39210
+ const close = `</${tag}>`;
39211
+ let out = "";
39212
+ let i = 0;
39213
+ for (; ; ) {
39214
+ const start = lower.indexOf(open, i);
39215
+ if (start < 0) {
39216
+ out += html.slice(i);
39217
+ return out;
39218
+ }
39219
+ const after = lower[start + open.length];
39220
+ if (after !== void 0 && after !== ">" && after !== " " && after !== " " && after !== "\n" && after !== "\r" && after !== "\f" && after !== "/") {
39221
+ out += html.slice(i, start + open.length);
39222
+ i = start + open.length;
39223
+ continue;
39224
+ }
39225
+ out += html.slice(i, start) + " ";
39226
+ const closeAt = lower.indexOf(close, start + open.length);
39227
+ if (closeAt < 0)
39228
+ return out;
39229
+ i = closeAt + close.length;
39230
+ }
39231
+ }
39232
+ function codePointOr(code, raw) {
39233
+ if (!Number.isInteger(code) || code < 0 || code > 1114111)
39234
+ return raw;
39235
+ try {
39236
+ return String.fromCodePoint(code);
39237
+ } catch {
39238
+ return raw;
39239
+ }
39240
+ }
38837
39241
 
38838
39242
  // src/tools/messages.ts
38839
39243
  var listArgs = external_exports.object({
@@ -38928,7 +39332,7 @@ function registerMessageTools(server, client) {
38928
39332
  return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
38929
39333
  });
38930
39334
  const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
38931
- return textContent({ notifications, inbox, announcements });
39335
+ return textResult({ notifications, inbox, announcements });
38932
39336
  });
38933
39337
  server.registerTool("ic_get_message", {
38934
39338
  description: "Fetch the HTML body of an inbox message and return it parsed into { subject, date, body, url }. Takes a `messageUrl` which is the `url` field from an item returned by ic_list_messages' inbox section (e.g. 'portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...'). Relative and /campus/-prefixed URLs are both accepted. Note: fetching the HTML body may mark the message as read on some district configurations; probe against an empty inbox could not confirm the side effect.",
@@ -38939,7 +39343,7 @@ function registerMessageTools(server, client) {
38939
39343
  const path = normalizeMessageUrl(args.messageUrl);
38940
39344
  const html = await client.request(args.district, path, { responseType: "text" });
38941
39345
  const parsed = parseMessageHtml(html ?? "", path);
38942
- return textContent(parsed);
39346
+ return textResult(parsed);
38943
39347
  });
38944
39348
  }
38945
39349
 
@@ -38979,7 +39383,7 @@ function registerDocumentTools(server, client) {
38979
39383
  if (d.endYear !== void 0) out.endYear = d.endYear;
38980
39384
  return out;
38981
39385
  });
38982
- return textContent(trimmed);
39386
+ return textResult(trimmed);
38983
39387
  } catch (e) {
38984
39388
  if (is404(e)) return featureDisabled("documents", args.district);
38985
39389
  throw e;
@@ -38995,10 +39399,10 @@ function registerDocumentTools(server, client) {
38995
39399
  const meta3 = await client.download(args.district, args.documentId, args.destinationPath, {
38996
39400
  overwrite: args.overwrite ?? false
38997
39401
  });
38998
- return textContent(meta3);
39402
+ return textResult(meta3);
38999
39403
  } catch (e) {
39000
39404
  if (e instanceof Error && e.message.startsWith("IC download 404")) {
39001
- return textContent({ warning: "FeatureDisabled", feature: "documents", district: args.district });
39405
+ return textResult({ warning: "FeatureDisabled", feature: "documents", district: args.district });
39002
39406
  }
39003
39407
  throw e;
39004
39408
  }
@@ -39054,7 +39458,7 @@ function registerCalendarTools(server, client) {
39054
39458
  terms: trimmedTerms
39055
39459
  });
39056
39460
  }
39057
- return textContent(result);
39461
+ return textResult(result);
39058
39462
  });
39059
39463
  }
39060
39464
 
@@ -39145,7 +39549,7 @@ function registerAttendanceEventsTools(server, client) {
39145
39549
  results.push(trimmed);
39146
39550
  }
39147
39551
  }
39148
- return textContent(results);
39552
+ return textResult(results);
39149
39553
  } catch (e) {
39150
39554
  if (is404(e)) return featureDisabled("attendance_events", args.district);
39151
39555
  throw e;
@@ -39201,7 +39605,7 @@ function registerRecentGradesTools(server, client) {
39201
39605
  }
39202
39606
  return out;
39203
39607
  });
39204
- return textContent(trimmed);
39608
+ return textResult(trimmed);
39205
39609
  });
39206
39610
  }
39207
39611
 
@@ -39256,7 +39660,7 @@ function registerTeacherTools(server, client) {
39256
39660
  const [teachersRaw, counselorsRaw] = await Promise.all([teachersPromise, counselorsPromise]);
39257
39661
  const teachers = toArray(teachersRaw).map((t) => trimRecord(t));
39258
39662
  const counselors = toArray(counselorsRaw).map((c) => trimRecord(c));
39259
- return textContent({ counselors, teachers });
39663
+ return textResult({ counselors, teachers });
39260
39664
  });
39261
39665
  }
39262
39666
 
@@ -39307,7 +39711,7 @@ function registerAssessmentTools(server, client) {
39307
39711
  if (feature404 && result.length === 0) {
39308
39712
  return featureDisabled("assessments", args.district);
39309
39713
  }
39310
- return textContent(result);
39714
+ return textResult(result);
39311
39715
  });
39312
39716
  }
39313
39717
 
@@ -39351,7 +39755,7 @@ function registerFeeTools(server, client) {
39351
39755
  feeAssignments: assignments.ok ? assignments.value : [],
39352
39756
  ...issues.length > 0 ? { issues } : {}
39353
39757
  };
39354
- return textContent(response);
39758
+ return textResult(response);
39355
39759
  });
39356
39760
  }
39357
39761
 
@@ -39379,7 +39783,7 @@ function registerFeaturesTools(server, client) {
39379
39783
  features
39380
39784
  });
39381
39785
  }
39382
- return textContent(result);
39786
+ return textResult(result);
39383
39787
  });
39384
39788
  }
39385
39789
 
@@ -39403,7 +39807,7 @@ try {
39403
39807
  }
39404
39808
  var COMMON = {
39405
39809
  name: "infinitecampus",
39406
- version: "2.3.4"
39810
+ version: "2.3.6"
39407
39811
  // x-release-please-version
39408
39812
  };
39409
39813
  if (account) {