infinitecampus-mcp 2.3.5 → 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
@@ -34704,6 +34704,9 @@ var API_KEY_RE = new RegExp([
34704
34704
  "whsec_[A-Za-z0-9]{16,}\\b"
34705
34705
  // webhook signing secret (Stripe-style)
34706
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");
34707
34710
 
34708
34711
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
34709
34712
  function textResult(data) {
@@ -38188,7 +38191,7 @@ function loadAccount(env = process.env) {
38188
38191
  // package.json
38189
38192
  var package_default = {
38190
38193
  name: "infinitecampus-mcp",
38191
- version: "2.3.5",
38194
+ version: "2.3.6",
38192
38195
  mcpName: "io.github.chrischall/infinitecampus-mcp",
38193
38196
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
38194
38197
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38230,7 +38233,7 @@ var package_default = {
38230
38233
  "test:watch": "vitest"
38231
38234
  },
38232
38235
  dependencies: {
38233
- "@chrischall/mcp-utils": "^0.10.3",
38236
+ "@chrischall/mcp-utils": "^0.12.0",
38234
38237
  "@fetchproxy/bootstrap": "^1.0.0",
38235
38238
  "@fetchproxy/server": "^1.0.0",
38236
38239
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38318,10 +38321,163 @@ async function resolveAuth() {
38318
38321
  // src/client.ts
38319
38322
  import { writeFile as writeFile2, stat } from "fs/promises";
38320
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
38321
38469
  var SESSION_TTL_MS = 5 * 60 * 60 * 1e3;
38322
38470
  var ICClient = class {
38323
38471
  accounts = /* @__PURE__ */ new Map();
38324
- 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();
38325
38481
  linkedTo = /* @__PURE__ */ new Map();
38326
38482
  // linkedDistrictName → primaryDistrictName
38327
38483
  primaryName;
@@ -38335,6 +38491,8 @@ var ICClient = class {
38335
38491
  */
38336
38492
  fetchproxyMode = false;
38337
38493
  fetchproxyDiscoveryRan = false;
38494
+ /** fetchproxy cookies, consumed by the primary manager's first login. */
38495
+ preloaded = null;
38338
38496
  /**
38339
38497
  * `preloaded` is the fetchproxy escape hatch: when set, the client treats
38340
38498
  * the supplied cookies as a freshly-completed login on the primary account.
@@ -38347,21 +38505,66 @@ var ICClient = class {
38347
38505
  constructor(account2, opts = {}) {
38348
38506
  this.accounts.set(account2.name, account2);
38349
38507
  this.primaryName = account2.name;
38508
+ this.managers.set(account2.name, this.makeManager(account2));
38350
38509
  if (opts.preloaded) {
38351
- this.sessions.set(account2.name, {
38352
- cookie: opts.preloaded.cookieHeader,
38353
- xsrfToken: opts.preloaded.xsrfToken,
38354
- loggedInAt: Date.now(),
38355
- loginInFlight: null
38356
- });
38510
+ this.preloaded = opts.preloaded;
38357
38511
  this.fetchproxyMode = true;
38358
38512
  }
38359
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
+ }
38360
38563
  async ensureDiscovery() {
38361
- await this.ensureSession(this.accounts.get(this.primaryName));
38564
+ const session = await this.managers.get(this.primaryName).ensure();
38362
38565
  if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan) {
38363
38566
  this.fetchproxyDiscoveryRan = true;
38364
- await this.discoverLinkedDistricts(this.accounts.get(this.primaryName));
38567
+ await this.discoverLinkedDistricts(this.accounts.get(this.primaryName), session);
38365
38568
  }
38366
38569
  }
38367
38570
  listDistricts() {
@@ -38394,50 +38597,37 @@ var ICClient = class {
38394
38597
  account2 = this.accounts.get(district);
38395
38598
  if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
38396
38599
  }
38397
- await this.ensureSession(account2);
38600
+ const session = await this.managers.get(account2.name).ensure();
38398
38601
  if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan && account2.name === this.primaryName) {
38399
38602
  this.fetchproxyDiscoveryRan = true;
38400
- await this.discoverLinkedDistricts(account2);
38401
- }
38402
- return this.doRequest(account2, path, opts, false);
38403
- }
38404
- async ensureSession(account2) {
38405
- let s = this.sessions.get(account2.name);
38406
- if (s && Date.now() - s.loggedInAt < SESSION_TTL_MS) return;
38407
- if (s?.loginInFlight) {
38408
- await s.loginInFlight;
38409
- return;
38410
- }
38411
- if (!s) {
38412
- s = { cookie: "", xsrfToken: "", loggedInAt: 0, loginInFlight: null };
38413
- this.sessions.set(account2.name, s);
38414
- }
38415
- s.loginInFlight = this.login(account2);
38416
- try {
38417
- await s.loginInFlight;
38418
- } finally {
38419
- s.loginInFlight = null;
38603
+ await this.discoverLinkedDistricts(account2, session);
38420
38604
  }
38605
+ return this.doRequest(account2, path, opts);
38421
38606
  }
38422
38607
  async login(account2) {
38423
38608
  const primaryName = this.linkedTo.get(account2.name);
38424
38609
  if (primaryName) {
38425
- this.sessions.delete(account2.name);
38426
- const primaryAccount = this.accounts.get(primaryName);
38427
- await this.ensureSession(primaryAccount);
38428
- 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) {
38429
38613
  throw new AuthFailedError(
38430
38614
  account2.name,
38431
38615
  "linked-district session could not be re-established via the primary district",
38432
38616
  { credentialHint: false }
38433
38617
  );
38434
38618
  }
38435
- return;
38619
+ return restored;
38620
+ }
38621
+ if (this.preloaded) {
38622
+ const { cookieHeader, xsrfToken } = this.preloaded;
38623
+ this.preloaded = null;
38624
+ return { cookieHeader, xsrfToken };
38436
38625
  }
38437
38626
  if (!account2.username || !account2.password) {
38438
38627
  throw new AuthFailedError(
38439
38628
  account2.name,
38440
- "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 }
38441
38631
  );
38442
38632
  }
38443
38633
  const postRes = await fetch(
@@ -38470,17 +38660,17 @@ var ICClient = class {
38470
38660
  if (!cookies.cookieHeader) {
38471
38661
  throw new AuthFailedError(account2.name, "login response missing session cookies");
38472
38662
  }
38473
- const session = this.sessions.get(account2.name);
38474
- session.cookie = cookies.cookieHeader;
38475
- session.xsrfToken = cookies.xsrfToken;
38476
- session.loggedInAt = Date.now();
38477
- 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;
38478
38669
  }
38479
- async discoverLinkedDistricts(account2) {
38670
+ async discoverLinkedDistricts(account2, session) {
38480
38671
  try {
38481
- const session = this.sessions.get(account2.name);
38482
38672
  const baseHeaders = {
38483
- Cookie: session.cookie,
38673
+ Cookie: session.cookieHeader,
38484
38674
  Accept: "application/json",
38485
38675
  "Content-Type": "application/json",
38486
38676
  Referer: `${account2.baseUrl}/campus/nav-wrapper/`,
@@ -38548,13 +38738,14 @@ var ICClient = class {
38548
38738
  password: "(linked)"
38549
38739
  };
38550
38740
  this.accounts.set(linked.districtName, syntheticAccount);
38551
- this.sessions.set(linked.districtName, {
38552
- cookie: switchCookies.cookieHeader,
38553
- xsrfToken: switchCookies.xsrfToken,
38554
- loggedInAt: Date.now(),
38555
- loginInFlight: null
38556
- });
38557
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
+ });
38558
38749
  console.error(`[ic] Linked district discovered: ${linked.districtName} (${linked.appName})`);
38559
38750
  } catch (e) {
38560
38751
  console.error(`[ic] CUPS flow failed for ${linked.districtName}: ${e instanceof Error ? e.message : e}`);
@@ -38580,12 +38771,11 @@ var ICClient = class {
38580
38771
  }
38581
38772
  const account2 = this.accounts.get(district);
38582
38773
  if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
38583
- await this.ensureSession(account2);
38584
- const session = this.sessions.get(account2.name);
38774
+ const session = await this.managers.get(account2.name).ensure();
38585
38775
  const url2 = /^https?:\/\//i.test(path) ? path : `${account2.baseUrl}${path}`;
38586
38776
  const res = await fetch(url2, {
38587
38777
  headers: {
38588
- Cookie: session.cookie,
38778
+ Cookie: session.cookieHeader,
38589
38779
  ...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {}
38590
38780
  }
38591
38781
  });
@@ -38598,34 +38788,23 @@ var ICClient = class {
38598
38788
  contentType: res.headers.get("content-type") ?? "application/octet-stream"
38599
38789
  };
38600
38790
  }
38601
- async doRequest(account2, path, opts, isRetry) {
38602
- const session = this.sessions.get(account2.name);
38791
+ async doRequest(account2, path, opts) {
38792
+ const mgr = this.managers.get(account2.name);
38603
38793
  const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json";
38604
- const res = await fetch(`${account2.baseUrl}${path}`, {
38605
- method: opts.method ?? "GET",
38606
- headers: {
38607
- Cookie: session.cookie,
38608
- Accept: accept,
38609
- ...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {},
38610
- ...opts.headers ?? {}
38611
- },
38612
- body: opts.body
38613
- });
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
+ );
38614
38806
  if (res.status === 401) {
38615
- if (isRetry) throw new SessionExpiredError(account2.name);
38616
- this.sessions.delete(account2.name);
38617
- const primaryName = this.linkedTo.get(account2.name);
38618
- if (primaryName) {
38619
- this.sessions.delete(primaryName);
38620
- for (const [linked] of this.linkedTo) {
38621
- this.sessions.delete(linked);
38622
- }
38623
- const primaryAccount = this.accounts.get(primaryName);
38624
- await this.ensureSession(primaryAccount);
38625
- } else {
38626
- await this.ensureSession(account2);
38627
- }
38628
- return this.doRequest(account2, path, opts, true);
38807
+ throw new SessionExpiredError(account2.name);
38629
38808
  }
38630
38809
  if (res.status >= 500) throw new PortalUnreachableError(account2.name, res.status);
38631
38810
  if (!res.ok) throw new Error(`IC ${res.status} ${res.statusText} for ${path}`);
@@ -38638,14 +38817,11 @@ var ICClient = class {
38638
38817
  };
38639
38818
  function parseSetCookies(headers) {
38640
38819
  const raw = headers.getSetCookie?.() ?? [];
38641
- const entries = raw.length > 0 ? raw : splitFallback(headers.get("set-cookie"));
38642
- const { cookies, cookieHeader } = parseCookieJar(entries);
38820
+ const { cookies, cookieHeader } = parseCookieJar(
38821
+ raw.length > 0 ? raw : headers.get("set-cookie")
38822
+ );
38643
38823
  return { cookieHeader, xsrfToken: cookies["XSRF-TOKEN"] ?? "" };
38644
38824
  }
38645
- function splitFallback(header) {
38646
- if (!header) return [];
38647
- return header.split(",").map((s) => s.trim());
38648
- }
38649
38825
  var UnknownDistrictError = class extends Error {
38650
38826
  constructor(district, available) {
38651
38827
  super(`Unknown district '${district}'. Configured: [${available.join(", ")}]`);
@@ -38662,6 +38838,7 @@ var AuthFailedError = class extends Error {
38662
38838
  * "Check IC_USERNAME and IC_PASSWORD" suffix — used for failures where the
38663
38839
  * credentials are known-good (e.g. a linked-district CUPS/SSO re-discovery
38664
38840
  * failure) and pointing the user at their creds would be misleading.
38841
+ * @param opts.permanent See {@link AuthFailedError.permanent}.
38665
38842
  */
38666
38843
  constructor(district, reason, opts) {
38667
38844
  const detail = reason ? ` (${reason})` : "";
@@ -38670,9 +38847,17 @@ var AuthFailedError = class extends Error {
38670
38847
  this.district = district;
38671
38848
  this.reason = reason;
38672
38849
  this.name = "AuthFailedError";
38850
+ this.permanent = opts?.permanent ?? false;
38673
38851
  }
38674
38852
  district;
38675
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;
38676
38861
  };
38677
38862
  var PortalUnreachableError = class extends Error {
38678
38863
  constructor(district, status) {
@@ -38717,40 +38902,6 @@ var FileExistsError = class extends Error {
38717
38902
  path;
38718
38903
  };
38719
38904
 
38720
- // src/tools/_shared.ts
38721
- function textContent(data) {
38722
- return textResult(data);
38723
- }
38724
- function is404(e) {
38725
- return e instanceof Error && e.message.startsWith("IC 404 ");
38726
- }
38727
- function featureDisabled(feature, district, data = []) {
38728
- return textContent({ warning: "FeatureDisabled", feature, district, data });
38729
- }
38730
- async function findStudent(client, district, studentId) {
38731
- const students = await client.request(district, "/campus/api/portal/students");
38732
- return students.find((s) => String(s.personID) === studentId) ?? null;
38733
- }
38734
- function studentNotFound(studentId) {
38735
- return textContent({ error: "StudentNotFound", studentId });
38736
- }
38737
- async function checkFeatureDisabled(client, district, studentId, student, flag, toolName, emptyData = []) {
38738
- const structureID = student.enrollments?.[0]?.structureID;
38739
- if (!structureID) return null;
38740
- try {
38741
- const features = await client.getFeatures(district, structureID, studentId);
38742
- if (features[flag] === false) return featureDisabled(toolName, district, emptyData);
38743
- return null;
38744
- } catch (e) {
38745
- console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
38746
- return null;
38747
- }
38748
- }
38749
- function toArray(value) {
38750
- if (value === null || value === void 0) return [];
38751
- return Array.isArray(value) ? value : [value];
38752
- }
38753
-
38754
38905
  // src/tools/districts.ts
38755
38906
  function registerDistrictTools(server, client) {
38756
38907
  server.registerTool("ic_list_districts", {
@@ -38759,7 +38910,7 @@ function registerDistrictTools(server, client) {
38759
38910
  }, async () => {
38760
38911
  await client.ensureDiscovery();
38761
38912
  const data = client.listDistricts();
38762
- return textContent(data);
38913
+ return textResult(data);
38763
38914
  });
38764
38915
  }
38765
38916
 
@@ -38775,7 +38926,7 @@ function registerStudentTools(server, client) {
38775
38926
  }, async (rawArgs) => {
38776
38927
  const args = argsSchema.parse(rawArgs);
38777
38928
  const data = await client.request(args.district, "/campus/api/portal/students");
38778
- return textContent(data);
38929
+ return textResult(data);
38779
38930
  });
38780
38931
  }
38781
38932
 
@@ -38795,7 +38946,7 @@ function registerScheduleTools(server, client) {
38795
38946
  const args = argsSchema2.parse(rawArgs);
38796
38947
  const params = new URLSearchParams({ personID: args.studentId });
38797
38948
  const data = await client.request(args.district, `/campus/resources/portal/roster?${params}`);
38798
- return textContent(data);
38949
+ return textResult(data);
38799
38950
  });
38800
38951
  }
38801
38952
 
@@ -38833,7 +38984,7 @@ function registerAssignmentTools(server, client) {
38833
38984
  if (args.missingOnly) {
38834
38985
  data = data.filter((a) => a.missing);
38835
38986
  }
38836
- return textContent(data);
38987
+ return textResult(data);
38837
38988
  });
38838
38989
  }
38839
38990
 
@@ -38853,10 +39004,41 @@ function registerGradeTools(server, client) {
38853
39004
  const params = new URLSearchParams({ personID: args.studentId });
38854
39005
  if (args.termId) params.set("termID", args.termId);
38855
39006
  const data = await client.request(args.district, `/campus/resources/portal/grades?${params}`);
38856
- return textContent(data);
39007
+ return textResult(data);
38857
39008
  });
38858
39009
  }
38859
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
+
38860
39042
  // src/tools/attendance.ts
38861
39043
  var argsSchema5 = external_exports.object({
38862
39044
  district: external_exports.string(),
@@ -38921,7 +39103,7 @@ function registerAttendanceTools(server, client) {
38921
39103
  results.push({ ...entry, terms: trimmedTerms });
38922
39104
  }
38923
39105
  }
38924
- return textContent(results);
39106
+ return textResult(results);
38925
39107
  } catch (e) {
38926
39108
  if (is404(e)) return featureDisabled("attendance", args.district);
38927
39109
  throw e;
@@ -38952,7 +39134,7 @@ function registerBehaviorTools(server, client) {
38952
39134
  if (args.until) params.set("endDate", args.until);
38953
39135
  try {
38954
39136
  const data = await client.request(args.district, `/campus/resources/portal/behavior?${params}`);
38955
- return textContent(data);
39137
+ return textResult(data);
38956
39138
  } catch (e) {
38957
39139
  if (is404(e)) return featureDisabled("behavior", args.district);
38958
39140
  throw e;
@@ -38992,7 +39174,7 @@ function registerFoodServiceTools(server, client) {
38992
39174
  if (args.until) params.set("endDate", args.until);
38993
39175
  try {
38994
39176
  const data = await client.request(args.district, `/campus/resources/portal/foodService?${params}`);
38995
- return textContent(data);
39177
+ return textResult(data);
38996
39178
  } catch (e) {
38997
39179
  if (is404(e)) return featureDisabled("foodService", args.district, EMPTY_FOOD);
38998
39180
  throw e;
@@ -39012,16 +39194,50 @@ var NAMED_ENTITIES = {
39012
39194
  function extractPlainTextFromHtml(html) {
39013
39195
  if (!html)
39014
39196
  return "";
39015
- let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ");
39197
+ let text = stripElementContent(stripElementContent(html, "script"), "style");
39016
39198
  text = text.replace(/<[^>]+>/g, " ");
39017
- text = text.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
39018
- 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));
39019
39201
  text = text.replace(/&([a-zA-Z]+);/g, (whole, name) => {
39020
39202
  const decoded = NAMED_ENTITIES[name.toLowerCase()];
39021
39203
  return decoded ?? whole;
39022
39204
  });
39023
39205
  return text.replace(/\s+/g, " ").trim();
39024
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
+ }
39025
39241
 
39026
39242
  // src/tools/messages.ts
39027
39243
  var listArgs = external_exports.object({
@@ -39116,7 +39332,7 @@ function registerMessageTools(server, client) {
39116
39332
  return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
39117
39333
  });
39118
39334
  const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
39119
- return textContent({ notifications, inbox, announcements });
39335
+ return textResult({ notifications, inbox, announcements });
39120
39336
  });
39121
39337
  server.registerTool("ic_get_message", {
39122
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.",
@@ -39127,7 +39343,7 @@ function registerMessageTools(server, client) {
39127
39343
  const path = normalizeMessageUrl(args.messageUrl);
39128
39344
  const html = await client.request(args.district, path, { responseType: "text" });
39129
39345
  const parsed = parseMessageHtml(html ?? "", path);
39130
- return textContent(parsed);
39346
+ return textResult(parsed);
39131
39347
  });
39132
39348
  }
39133
39349
 
@@ -39167,7 +39383,7 @@ function registerDocumentTools(server, client) {
39167
39383
  if (d.endYear !== void 0) out.endYear = d.endYear;
39168
39384
  return out;
39169
39385
  });
39170
- return textContent(trimmed);
39386
+ return textResult(trimmed);
39171
39387
  } catch (e) {
39172
39388
  if (is404(e)) return featureDisabled("documents", args.district);
39173
39389
  throw e;
@@ -39183,10 +39399,10 @@ function registerDocumentTools(server, client) {
39183
39399
  const meta3 = await client.download(args.district, args.documentId, args.destinationPath, {
39184
39400
  overwrite: args.overwrite ?? false
39185
39401
  });
39186
- return textContent(meta3);
39402
+ return textResult(meta3);
39187
39403
  } catch (e) {
39188
39404
  if (e instanceof Error && e.message.startsWith("IC download 404")) {
39189
- return textContent({ warning: "FeatureDisabled", feature: "documents", district: args.district });
39405
+ return textResult({ warning: "FeatureDisabled", feature: "documents", district: args.district });
39190
39406
  }
39191
39407
  throw e;
39192
39408
  }
@@ -39242,7 +39458,7 @@ function registerCalendarTools(server, client) {
39242
39458
  terms: trimmedTerms
39243
39459
  });
39244
39460
  }
39245
- return textContent(result);
39461
+ return textResult(result);
39246
39462
  });
39247
39463
  }
39248
39464
 
@@ -39333,7 +39549,7 @@ function registerAttendanceEventsTools(server, client) {
39333
39549
  results.push(trimmed);
39334
39550
  }
39335
39551
  }
39336
- return textContent(results);
39552
+ return textResult(results);
39337
39553
  } catch (e) {
39338
39554
  if (is404(e)) return featureDisabled("attendance_events", args.district);
39339
39555
  throw e;
@@ -39389,7 +39605,7 @@ function registerRecentGradesTools(server, client) {
39389
39605
  }
39390
39606
  return out;
39391
39607
  });
39392
- return textContent(trimmed);
39608
+ return textResult(trimmed);
39393
39609
  });
39394
39610
  }
39395
39611
 
@@ -39444,7 +39660,7 @@ function registerTeacherTools(server, client) {
39444
39660
  const [teachersRaw, counselorsRaw] = await Promise.all([teachersPromise, counselorsPromise]);
39445
39661
  const teachers = toArray(teachersRaw).map((t) => trimRecord(t));
39446
39662
  const counselors = toArray(counselorsRaw).map((c) => trimRecord(c));
39447
- return textContent({ counselors, teachers });
39663
+ return textResult({ counselors, teachers });
39448
39664
  });
39449
39665
  }
39450
39666
 
@@ -39495,7 +39711,7 @@ function registerAssessmentTools(server, client) {
39495
39711
  if (feature404 && result.length === 0) {
39496
39712
  return featureDisabled("assessments", args.district);
39497
39713
  }
39498
- return textContent(result);
39714
+ return textResult(result);
39499
39715
  });
39500
39716
  }
39501
39717
 
@@ -39539,7 +39755,7 @@ function registerFeeTools(server, client) {
39539
39755
  feeAssignments: assignments.ok ? assignments.value : [],
39540
39756
  ...issues.length > 0 ? { issues } : {}
39541
39757
  };
39542
- return textContent(response);
39758
+ return textResult(response);
39543
39759
  });
39544
39760
  }
39545
39761
 
@@ -39567,7 +39783,7 @@ function registerFeaturesTools(server, client) {
39567
39783
  features
39568
39784
  });
39569
39785
  }
39570
- return textContent(result);
39786
+ return textResult(result);
39571
39787
  });
39572
39788
  }
39573
39789
 
@@ -39591,7 +39807,7 @@ try {
39591
39807
  }
39592
39808
  var COMMON = {
39593
39809
  name: "infinitecampus",
39594
- version: "2.3.5"
39810
+ version: "2.3.6"
39595
39811
  // x-release-please-version
39596
39812
  };
39597
39813
  if (account) {