infinitecampus-mcp 2.3.5 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/dist/bundle.js +487 -151
- package/dist/client.js +161 -114
- package/dist/index.js +1 -1
- package/dist/tools/_shared.js +2 -11
- package/dist/tools/assessments.js +3 -2
- package/dist/tools/assignments.js +2 -2
- package/dist/tools/attendance.js +3 -2
- package/dist/tools/attendance_events.js +3 -2
- package/dist/tools/behavior.js +3 -2
- package/dist/tools/calendar.js +3 -2
- package/dist/tools/districts.js +2 -2
- package/dist/tools/documents.js +5 -4
- package/dist/tools/features.js +3 -2
- package/dist/tools/fees.js +3 -2
- package/dist/tools/foodservice.js +3 -2
- package/dist/tools/grades.js +2 -2
- package/dist/tools/messages.js +4 -3
- package/dist/tools/recent_grades.js +2 -2
- package/dist/tools/schedule.js +2 -2
- package/dist/tools/students.js +2 -2
- package/dist/tools/teachers.js +3 -2
- package/package.json +4 -4
- package/server.json +2 -2
- package/skills/infinitecampus-api/SKILL.md +142 -0
- package/skills/infinitecampus-api/references/endpoints.md +225 -0
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) {
|
|
@@ -34842,6 +34845,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34842
34845
|
"capture_request_header",
|
|
34843
34846
|
"capture_redirect",
|
|
34844
34847
|
"read_indexed_db",
|
|
34848
|
+
"read_dom",
|
|
34845
34849
|
"download"
|
|
34846
34850
|
]);
|
|
34847
34851
|
|
|
@@ -35143,6 +35147,44 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35143
35147
|
}
|
|
35144
35148
|
}
|
|
35145
35149
|
}
|
|
35150
|
+
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35151
|
+
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35152
|
+
function assertDomSelectorsArray(value, label) {
|
|
35153
|
+
if (!Array.isArray(value)) {
|
|
35154
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35155
|
+
}
|
|
35156
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35157
|
+
for (let i = 0; i < value.length; i++) {
|
|
35158
|
+
const entry = value[i];
|
|
35159
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35160
|
+
if (entry.name === void 0) {
|
|
35161
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35162
|
+
}
|
|
35163
|
+
if (entry.selector === void 0) {
|
|
35164
|
+
throw new ProtocolError(`${label}[${i}].selector: missing`);
|
|
35165
|
+
}
|
|
35166
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35167
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35168
|
+
}
|
|
35169
|
+
if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
|
|
35170
|
+
throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
|
|
35171
|
+
}
|
|
35172
|
+
if (entry.attribute !== void 0) {
|
|
35173
|
+
if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
|
|
35174
|
+
throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
|
|
35175
|
+
}
|
|
35176
|
+
}
|
|
35177
|
+
if (seen.has(entry.name)) {
|
|
35178
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35179
|
+
}
|
|
35180
|
+
seen.add(entry.name);
|
|
35181
|
+
for (const k of Object.keys(entry)) {
|
|
35182
|
+
if (k !== "name" && k !== "selector" && k !== "attribute") {
|
|
35183
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35184
|
+
}
|
|
35185
|
+
}
|
|
35186
|
+
}
|
|
35187
|
+
}
|
|
35146
35188
|
function validateFrame(raw) {
|
|
35147
35189
|
assertObject(raw, "frame");
|
|
35148
35190
|
const t = raw.type;
|
|
@@ -35218,6 +35260,9 @@ function validateHello(raw) {
|
|
|
35218
35260
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35219
35261
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35220
35262
|
}
|
|
35263
|
+
if (raw.domSelectors !== void 0) {
|
|
35264
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35265
|
+
}
|
|
35221
35266
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35222
35267
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35223
35268
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35452,6 +35497,21 @@ function validateInnerRequest(raw) {
|
|
|
35452
35497
|
}
|
|
35453
35498
|
return raw;
|
|
35454
35499
|
}
|
|
35500
|
+
if (raw.op === "read_dom") {
|
|
35501
|
+
assertObject(raw.init, "inner.init");
|
|
35502
|
+
if (raw.init.origin === void 0)
|
|
35503
|
+
throw new ProtocolError("inner.init.origin: missing");
|
|
35504
|
+
if (raw.init.names === void 0)
|
|
35505
|
+
throw new ProtocolError("inner.init.names: missing");
|
|
35506
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35507
|
+
assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
|
|
35508
|
+
for (const k of Object.keys(raw.init)) {
|
|
35509
|
+
if (k !== "origin" && k !== "names") {
|
|
35510
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
|
|
35511
|
+
}
|
|
35512
|
+
}
|
|
35513
|
+
return raw;
|
|
35514
|
+
}
|
|
35455
35515
|
if (raw.op === "download") {
|
|
35456
35516
|
assertObject(raw.init, "inner.init");
|
|
35457
35517
|
if (raw.init.url === void 0) {
|
|
@@ -35477,7 +35537,7 @@ function validateInnerRequest(raw) {
|
|
|
35477
35537
|
}
|
|
35478
35538
|
return raw;
|
|
35479
35539
|
}
|
|
35480
|
-
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35540
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35481
35541
|
}
|
|
35482
35542
|
function assertNonEmptyKeyArray(value, label) {
|
|
35483
35543
|
if (!Array.isArray(value)) {
|
|
@@ -35562,6 +35622,13 @@ function validateInnerResponse(raw) {
|
|
|
35562
35622
|
assertObject(raw.values, "inner.values");
|
|
35563
35623
|
return raw;
|
|
35564
35624
|
}
|
|
35625
|
+
if (op === "read_dom") {
|
|
35626
|
+
if (raw.values === void 0) {
|
|
35627
|
+
throw new ProtocolError("inner.values: missing on read_dom response");
|
|
35628
|
+
}
|
|
35629
|
+
assertStringMap(raw.values, "inner.values");
|
|
35630
|
+
return raw;
|
|
35631
|
+
}
|
|
35565
35632
|
if (op === "download") {
|
|
35566
35633
|
assertObject(raw.value, "inner.value");
|
|
35567
35634
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -35901,6 +35968,13 @@ async function buildServerHello(opts) {
|
|
|
35901
35968
|
jsonPointer: d.jsonPointer
|
|
35902
35969
|
}));
|
|
35903
35970
|
}
|
|
35971
|
+
if (opts.domSelectors && opts.domSelectors.length > 0) {
|
|
35972
|
+
hello.domSelectors = opts.domSelectors.map((d) => ({
|
|
35973
|
+
name: d.name,
|
|
35974
|
+
selector: d.selector,
|
|
35975
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
35976
|
+
}));
|
|
35977
|
+
}
|
|
35904
35978
|
return hello;
|
|
35905
35979
|
}
|
|
35906
35980
|
|
|
@@ -35990,7 +36064,8 @@ async function startHost(opts) {
|
|
|
35990
36064
|
captureHeaders: opts.ownCaptureHeaders,
|
|
35991
36065
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
35992
36066
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
35993
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36067
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36068
|
+
domSelectors: opts.ownDomSelectors
|
|
35994
36069
|
});
|
|
35995
36070
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
35996
36071
|
let extensionWs = null;
|
|
@@ -36225,6 +36300,7 @@ async function startPeer(opts) {
|
|
|
36225
36300
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36226
36301
|
captureHeaders: opts.captureHeaders,
|
|
36227
36302
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36303
|
+
domSelectors: opts.domSelectors,
|
|
36228
36304
|
localStoragePointers: opts.localStoragePointers,
|
|
36229
36305
|
sessionStoragePointers: opts.sessionStoragePointers
|
|
36230
36306
|
});
|
|
@@ -36632,6 +36708,11 @@ var FetchproxyServer = class {
|
|
|
36632
36708
|
key: d.key,
|
|
36633
36709
|
jsonPointer: d.jsonPointer
|
|
36634
36710
|
})),
|
|
36711
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36712
|
+
name: d.name,
|
|
36713
|
+
selector: d.selector,
|
|
36714
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36715
|
+
})),
|
|
36635
36716
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36636
36717
|
// adapter was about to set these to the same numbers anyway; the
|
|
36637
36718
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36752,6 +36833,7 @@ var FetchproxyServer = class {
|
|
|
36752
36833
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36753
36834
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36754
36835
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36836
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36755
36837
|
onPairCode: this.opts.onPairCode
|
|
36756
36838
|
});
|
|
36757
36839
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36779,7 +36861,8 @@ var FetchproxyServer = class {
|
|
|
36779
36861
|
captureHeaders: this.opts.captureHeaders,
|
|
36780
36862
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36781
36863
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36782
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36864
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36865
|
+
domSelectors: this.opts.domSelectors
|
|
36783
36866
|
});
|
|
36784
36867
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36785
36868
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37754,6 +37837,46 @@ var FetchproxyServer = class {
|
|
|
37754
37837
|
await this.sendInnerFrame(inner);
|
|
37755
37838
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37756
37839
|
}
|
|
37840
|
+
/**
|
|
37841
|
+
* 1.4.0+: read declared DOM values from the user's signed-in tab.
|
|
37842
|
+
* Requires `'read_dom'` in capabilities AND every requested `name` to
|
|
37843
|
+
* match a declared `domSelectors` entry. The extension reads each
|
|
37844
|
+
* declared selector from the matched tab's DOM (isolated-world
|
|
37845
|
+
* `querySelector`, value or attribute) — no page-JS execution.
|
|
37846
|
+
*
|
|
37847
|
+
* Returns a `Record<string, string>` of `name → value`, with names
|
|
37848
|
+
* whose element (or attribute) was absent omitted. Throws
|
|
37849
|
+
* `FetchproxyProtocolError` on bridge failures and a plain `Error` on
|
|
37850
|
+
* developer mistakes (undeclared capability, undeclared name).
|
|
37851
|
+
*/
|
|
37852
|
+
async readDom(opts) {
|
|
37853
|
+
if (!this.opts.capabilities.includes("read_dom")) {
|
|
37854
|
+
throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
|
|
37855
|
+
}
|
|
37856
|
+
await this.ensureConnected();
|
|
37857
|
+
this.throwIfPendingPair();
|
|
37858
|
+
if (!Array.isArray(opts.names) || opts.names.length === 0) {
|
|
37859
|
+
throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
|
|
37860
|
+
}
|
|
37861
|
+
this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
|
|
37862
|
+
if (opts.subdomain !== void 0)
|
|
37863
|
+
assertSubdomainLabel(opts.subdomain);
|
|
37864
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
37865
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
37866
|
+
const origin = `https://${host}`;
|
|
37867
|
+
const id = this.nextRequestId++;
|
|
37868
|
+
const inner = {
|
|
37869
|
+
type: "request",
|
|
37870
|
+
id,
|
|
37871
|
+
op: "read_dom",
|
|
37872
|
+
init: { origin, names: [...opts.names] }
|
|
37873
|
+
};
|
|
37874
|
+
const pending = new Promise((resolve, reject) => {
|
|
37875
|
+
this.pendingStorage.set(id, { resolve, reject });
|
|
37876
|
+
});
|
|
37877
|
+
await this.sendInnerFrame(inner);
|
|
37878
|
+
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
37879
|
+
}
|
|
37757
37880
|
assertScopeSubset(requested, declared, label) {
|
|
37758
37881
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37759
37882
|
if (undeclared.length > 0) {
|
|
@@ -37825,7 +37948,7 @@ var FetchproxyServer = class {
|
|
|
37825
37948
|
if (storageCb) {
|
|
37826
37949
|
this.pendingStorage.delete(inner.id);
|
|
37827
37950
|
if (inner.ok) {
|
|
37828
|
-
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
|
|
37951
|
+
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
|
|
37829
37952
|
storageCb.resolve({ ...inner.values });
|
|
37830
37953
|
} else {
|
|
37831
37954
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -38188,7 +38311,7 @@ function loadAccount(env = process.env) {
|
|
|
38188
38311
|
// package.json
|
|
38189
38312
|
var package_default = {
|
|
38190
38313
|
name: "infinitecampus-mcp",
|
|
38191
|
-
version: "2.
|
|
38314
|
+
version: "2.4.0",
|
|
38192
38315
|
mcpName: "io.github.chrischall/infinitecampus-mcp",
|
|
38193
38316
|
description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
38194
38317
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38230,19 +38353,19 @@ var package_default = {
|
|
|
38230
38353
|
"test:watch": "vitest"
|
|
38231
38354
|
},
|
|
38232
38355
|
dependencies: {
|
|
38233
|
-
"@chrischall/mcp-utils": "^0.
|
|
38356
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
38234
38357
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
38235
38358
|
"@fetchproxy/server": "^1.0.0",
|
|
38236
38359
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38237
38360
|
dotenv: "^17.4.2",
|
|
38238
|
-
"node-html-parser": "^
|
|
38361
|
+
"node-html-parser": "^9.0.0",
|
|
38239
38362
|
zod: "^4.4.3"
|
|
38240
38363
|
},
|
|
38241
38364
|
devDependencies: {
|
|
38242
38365
|
"@types/node": "^26.0.0",
|
|
38243
38366
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38244
38367
|
esbuild: "^0.28.0",
|
|
38245
|
-
typescript: "^
|
|
38368
|
+
typescript: "^7.0.2",
|
|
38246
38369
|
vitest: "^4.1.7"
|
|
38247
38370
|
}
|
|
38248
38371
|
};
|
|
@@ -38318,10 +38441,163 @@ async function resolveAuth() {
|
|
|
38318
38441
|
// src/client.ts
|
|
38319
38442
|
import { writeFile as writeFile2, stat } from "fs/promises";
|
|
38320
38443
|
import { dirname } from "path";
|
|
38444
|
+
|
|
38445
|
+
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
38446
|
+
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
38447
|
+
var CookieSessionManager = class {
|
|
38448
|
+
session;
|
|
38449
|
+
inFlight;
|
|
38450
|
+
/** A cached *permanent* config error: once set, every `ensure` rethrows it. */
|
|
38451
|
+
permanentError;
|
|
38452
|
+
/** When the current session was minted (login) or installed (seed) — for maxAgeMs. */
|
|
38453
|
+
sessionAt = 0;
|
|
38454
|
+
loginFn;
|
|
38455
|
+
isExpiredFn;
|
|
38456
|
+
isPermanentErrorFn;
|
|
38457
|
+
maxAgeMs;
|
|
38458
|
+
now;
|
|
38459
|
+
onReplayLoginErrorFn;
|
|
38460
|
+
constructor(opts) {
|
|
38461
|
+
this.loginFn = opts.login;
|
|
38462
|
+
this.isExpiredFn = opts.isExpired ?? (() => false);
|
|
38463
|
+
this.isPermanentErrorFn = opts.isPermanentError ?? (() => false);
|
|
38464
|
+
this.maxAgeMs = opts.maxAgeMs;
|
|
38465
|
+
this.now = opts.now ?? Date.now;
|
|
38466
|
+
this.onReplayLoginErrorFn = opts.onReplayLoginError;
|
|
38467
|
+
}
|
|
38468
|
+
/** The current session, or `undefined` before the first successful login. */
|
|
38469
|
+
get current() {
|
|
38470
|
+
return this.session;
|
|
38471
|
+
}
|
|
38472
|
+
/** True when {@link CookieSessionManagerOptions.maxAgeMs} says the session is too old. */
|
|
38473
|
+
isStale() {
|
|
38474
|
+
return this.maxAgeMs !== void 0 && this.now() - this.sessionAt >= this.maxAgeMs;
|
|
38475
|
+
}
|
|
38476
|
+
/**
|
|
38477
|
+
* Return the current session, or single-flight a login if there is none.
|
|
38478
|
+
*
|
|
38479
|
+
* Concurrent callers share one in-flight login; the in-flight promise is
|
|
38480
|
+
* cleared on settle (success OR failure) so a rejected login never sticks and
|
|
38481
|
+
* the next call retries. A login error classified permanent by
|
|
38482
|
+
* {@link CookieSessionManagerOptions.isPermanentError} is cached and rethrown
|
|
38483
|
+
* on every later call; a transient error is not cached (next call retries).
|
|
38484
|
+
* With {@link CookieSessionManagerOptions.maxAgeMs} set, a session past its
|
|
38485
|
+
* TTL is invalidated first, so the call falls through to a (single-flight)
|
|
38486
|
+
* re-login.
|
|
38487
|
+
*/
|
|
38488
|
+
async ensure() {
|
|
38489
|
+
if (this.session !== void 0) {
|
|
38490
|
+
if (!this.isStale())
|
|
38491
|
+
return this.session;
|
|
38492
|
+
this.invalidate();
|
|
38493
|
+
}
|
|
38494
|
+
if (this.permanentError !== void 0)
|
|
38495
|
+
throw this.permanentError;
|
|
38496
|
+
if (this.inFlight === void 0) {
|
|
38497
|
+
this.inFlight = this.runLogin();
|
|
38498
|
+
}
|
|
38499
|
+
return this.inFlight;
|
|
38500
|
+
}
|
|
38501
|
+
/**
|
|
38502
|
+
* Install an externally-minted session (e.g. infinitecampus's CUPS linked-
|
|
38503
|
+
* district discovery, which mints sessions outside {@link
|
|
38504
|
+
* CookieSessionManagerOptions.login}). The seed becomes the current session
|
|
38505
|
+
* with a fresh {@link CookieSessionManagerOptions.maxAgeMs} clock, and any
|
|
38506
|
+
* in-flight login is DETACHED: its waiters still receive its result, but a
|
|
38507
|
+
* late resolution will not overwrite the seed. A cached permanent config
|
|
38508
|
+
* error is left intact — the login path is still misconfigured, and the next
|
|
38509
|
+
* post-seed re-login should keep saying so.
|
|
38510
|
+
*/
|
|
38511
|
+
seed(session) {
|
|
38512
|
+
this.session = session;
|
|
38513
|
+
this.sessionAt = this.now();
|
|
38514
|
+
this.inFlight = void 0;
|
|
38515
|
+
}
|
|
38516
|
+
/**
|
|
38517
|
+
* One login attempt. Self-clears `inFlight` on settle so a rejected login
|
|
38518
|
+
* never sticks, and stamps the resulting session only while it's still the
|
|
38519
|
+
* live attempt (an `invalidate()` mid-flight cleared `inFlight` — a stale
|
|
38520
|
+
* resolution must not re-stamp the dropped session over the new state).
|
|
38521
|
+
*/
|
|
38522
|
+
runLogin() {
|
|
38523
|
+
const holder = {};
|
|
38524
|
+
holder.p = (async () => {
|
|
38525
|
+
try {
|
|
38526
|
+
const session = await this.loginFn();
|
|
38527
|
+
if (this.inFlight === holder.p) {
|
|
38528
|
+
this.session = session;
|
|
38529
|
+
this.sessionAt = this.now();
|
|
38530
|
+
}
|
|
38531
|
+
return session;
|
|
38532
|
+
} catch (err) {
|
|
38533
|
+
if (this.isPermanentErrorFn(err))
|
|
38534
|
+
this.permanentError = err;
|
|
38535
|
+
throw err;
|
|
38536
|
+
} finally {
|
|
38537
|
+
if (this.inFlight === holder.p)
|
|
38538
|
+
this.inFlight = void 0;
|
|
38539
|
+
}
|
|
38540
|
+
})();
|
|
38541
|
+
return holder.p;
|
|
38542
|
+
}
|
|
38543
|
+
/**
|
|
38544
|
+
* Drop the current session (and any in-flight login) so the next
|
|
38545
|
+
* {@link ensure} re-runs {@link CookieSessionManagerOptions.login}. Does NOT
|
|
38546
|
+
* clear a cached permanent config error — that only clears on a fresh process
|
|
38547
|
+
* (new config). Used to recover from a detected session expiry.
|
|
38548
|
+
*/
|
|
38549
|
+
invalidate() {
|
|
38550
|
+
this.session = void 0;
|
|
38551
|
+
this.inFlight = void 0;
|
|
38552
|
+
}
|
|
38553
|
+
/**
|
|
38554
|
+
* Run an authenticated `call` with the current session and reactive
|
|
38555
|
+
* expiry-replay. `call` receives the session and returns a `Response`. If
|
|
38556
|
+
* {@link CookieSessionManagerOptions.isExpired} flags the response, the
|
|
38557
|
+
* session is invalidated, a single-flight re-login runs, and `call` is
|
|
38558
|
+
* replayed EXACTLY once. A persistent expiry surfaces the (second) response
|
|
38559
|
+
* rather than looping. If the re-login itself fails, the original
|
|
38560
|
+
* (expired-looking) response is returned so the caller can surface a clean
|
|
38561
|
+
* sign-in error rather than the login failure.
|
|
38562
|
+
*
|
|
38563
|
+
* `call` resolves to `R` (the generic response type, default {@link Response});
|
|
38564
|
+
* the manager passes it untouched to {@link CookieSessionManagerOptions.isExpired}
|
|
38565
|
+
* and returns it untouched, so a custom transport type flows through cleanly.
|
|
38566
|
+
*/
|
|
38567
|
+
async withSession(call) {
|
|
38568
|
+
const session = await this.ensure();
|
|
38569
|
+
const res = await call(session);
|
|
38570
|
+
if (!await this.isExpiredFn(res))
|
|
38571
|
+
return res;
|
|
38572
|
+
if (this.current === session)
|
|
38573
|
+
this.invalidate();
|
|
38574
|
+
let fresh;
|
|
38575
|
+
try {
|
|
38576
|
+
fresh = await this.ensure();
|
|
38577
|
+
} catch (err) {
|
|
38578
|
+
this.onReplayLoginErrorFn?.(err);
|
|
38579
|
+
return res;
|
|
38580
|
+
}
|
|
38581
|
+
return call(fresh);
|
|
38582
|
+
}
|
|
38583
|
+
};
|
|
38584
|
+
function createCookieSessionManager(opts) {
|
|
38585
|
+
return new CookieSessionManager(opts);
|
|
38586
|
+
}
|
|
38587
|
+
|
|
38588
|
+
// src/client.ts
|
|
38321
38589
|
var SESSION_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
38322
38590
|
var ICClient = class {
|
|
38323
38591
|
accounts = /* @__PURE__ */ new Map();
|
|
38324
|
-
|
|
38592
|
+
/**
|
|
38593
|
+
* One CookieSessionManager per district (primary + each discovered linked
|
|
38594
|
+
* district). The manager owns the session lifecycle: single-flight login,
|
|
38595
|
+
* `withSession` exactly-one 401 replay, `invalidate()`, permanent-error
|
|
38596
|
+
* caching, proactive `maxAgeMs` (SESSION_TTL_MS) expiry, and — for linked
|
|
38597
|
+
* districts — the CUPS-minted session installed via `seed()` during
|
|
38598
|
+
* discovery.
|
|
38599
|
+
*/
|
|
38600
|
+
managers = /* @__PURE__ */ new Map();
|
|
38325
38601
|
linkedTo = /* @__PURE__ */ new Map();
|
|
38326
38602
|
// linkedDistrictName → primaryDistrictName
|
|
38327
38603
|
primaryName;
|
|
@@ -38335,6 +38611,8 @@ var ICClient = class {
|
|
|
38335
38611
|
*/
|
|
38336
38612
|
fetchproxyMode = false;
|
|
38337
38613
|
fetchproxyDiscoveryRan = false;
|
|
38614
|
+
/** fetchproxy cookies, consumed by the primary manager's first login. */
|
|
38615
|
+
preloaded = null;
|
|
38338
38616
|
/**
|
|
38339
38617
|
* `preloaded` is the fetchproxy escape hatch: when set, the client treats
|
|
38340
38618
|
* the supplied cookies as a freshly-completed login on the primary account.
|
|
@@ -38347,21 +38625,66 @@ var ICClient = class {
|
|
|
38347
38625
|
constructor(account2, opts = {}) {
|
|
38348
38626
|
this.accounts.set(account2.name, account2);
|
|
38349
38627
|
this.primaryName = account2.name;
|
|
38628
|
+
this.managers.set(account2.name, this.makeManager(account2));
|
|
38350
38629
|
if (opts.preloaded) {
|
|
38351
|
-
this.
|
|
38352
|
-
cookie: opts.preloaded.cookieHeader,
|
|
38353
|
-
xsrfToken: opts.preloaded.xsrfToken,
|
|
38354
|
-
loggedInAt: Date.now(),
|
|
38355
|
-
loginInFlight: null
|
|
38356
|
-
});
|
|
38630
|
+
this.preloaded = opts.preloaded;
|
|
38357
38631
|
this.fetchproxyMode = true;
|
|
38358
38632
|
}
|
|
38359
38633
|
}
|
|
38634
|
+
/**
|
|
38635
|
+
* Build the per-district CookieSessionManager.
|
|
38636
|
+
*
|
|
38637
|
+
* - `maxAgeMs: SESSION_TTL_MS` makes `ensure()` proactively invalidate and
|
|
38638
|
+
* re-login a session older than 5h (single-flight), replacing IC's old
|
|
38639
|
+
* hand-rolled `ensureFresh` wrapper. `now` reads `Date.now` at call time so
|
|
38640
|
+
* the clock stays mockable in tests.
|
|
38641
|
+
* - `isPermanentError` caches the one config error the process can never
|
|
38642
|
+
* recover from (fetchproxy no-creds) so `ensure()` fails fast instead of
|
|
38643
|
+
* pointlessly retrying a login that can never succeed.
|
|
38644
|
+
* - `onReplayLoginError` rethrows the re-login failure from `withSession`'s
|
|
38645
|
+
* expiry-replay path. By default that failure is swallowed and the stale
|
|
38646
|
+
* 401 returned; rethrowing surfaces the actionable error (e.g. fetchproxy's
|
|
38647
|
+
* "sign back into your IC portal in the browser") to the caller instead of
|
|
38648
|
+
* a generic SessionExpiredError — the behavior IC's `lastLoginError`
|
|
38649
|
+
* bookkeeping used to provide.
|
|
38650
|
+
*/
|
|
38651
|
+
makeManager(account2) {
|
|
38652
|
+
return createCookieSessionManager({
|
|
38653
|
+
login: () => this.login(account2),
|
|
38654
|
+
isExpired: (res) => this.detectExpiredSession(account2.name, res),
|
|
38655
|
+
isPermanentError: (e) => e instanceof AuthFailedError && e.permanent,
|
|
38656
|
+
maxAgeMs: SESSION_TTL_MS,
|
|
38657
|
+
now: () => Date.now(),
|
|
38658
|
+
onReplayLoginError: (err) => {
|
|
38659
|
+
throw err;
|
|
38660
|
+
}
|
|
38661
|
+
});
|
|
38662
|
+
}
|
|
38663
|
+
/**
|
|
38664
|
+
* `isExpired` predicate for `withSession`: a 401 means the session died.
|
|
38665
|
+
* Side effect for linked districts: the shared CUPS SSO umbrella died with
|
|
38666
|
+
* it, so drop the primary and every *sibling* linked session too — the
|
|
38667
|
+
* linked district's re-login (via `login()`'s linked branch) then re-logs
|
|
38668
|
+
* the primary in, whose discovery re-establishes all of them. `withSession`
|
|
38669
|
+
* invalidates the requesting district itself. Mirrors the old hand-rolled
|
|
38670
|
+
* doRequest 401 handling.
|
|
38671
|
+
*/
|
|
38672
|
+
detectExpiredSession(district, res) {
|
|
38673
|
+
if (res.status !== 401) return false;
|
|
38674
|
+
const primaryName = this.linkedTo.get(district);
|
|
38675
|
+
if (primaryName) {
|
|
38676
|
+
this.managers.get(primaryName).invalidate();
|
|
38677
|
+
for (const linked of this.linkedTo.keys()) {
|
|
38678
|
+
if (linked !== district) this.managers.get(linked).invalidate();
|
|
38679
|
+
}
|
|
38680
|
+
}
|
|
38681
|
+
return true;
|
|
38682
|
+
}
|
|
38360
38683
|
async ensureDiscovery() {
|
|
38361
|
-
await this.
|
|
38684
|
+
const session = await this.managers.get(this.primaryName).ensure();
|
|
38362
38685
|
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan) {
|
|
38363
38686
|
this.fetchproxyDiscoveryRan = true;
|
|
38364
|
-
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName));
|
|
38687
|
+
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName), session);
|
|
38365
38688
|
}
|
|
38366
38689
|
}
|
|
38367
38690
|
listDistricts() {
|
|
@@ -38394,50 +38717,37 @@ var ICClient = class {
|
|
|
38394
38717
|
account2 = this.accounts.get(district);
|
|
38395
38718
|
if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
38396
38719
|
}
|
|
38397
|
-
await this.
|
|
38720
|
+
const session = await this.managers.get(account2.name).ensure();
|
|
38398
38721
|
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan && account2.name === this.primaryName) {
|
|
38399
38722
|
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;
|
|
38723
|
+
await this.discoverLinkedDistricts(account2, session);
|
|
38420
38724
|
}
|
|
38725
|
+
return this.doRequest(account2, path, opts);
|
|
38421
38726
|
}
|
|
38422
38727
|
async login(account2) {
|
|
38423
38728
|
const primaryName = this.linkedTo.get(account2.name);
|
|
38424
38729
|
if (primaryName) {
|
|
38425
|
-
this.
|
|
38426
|
-
const
|
|
38427
|
-
|
|
38428
|
-
if (!this.sessions.has(account2.name)) {
|
|
38730
|
+
await this.managers.get(primaryName).ensure();
|
|
38731
|
+
const restored = this.managers.get(account2.name).current;
|
|
38732
|
+
if (!restored) {
|
|
38429
38733
|
throw new AuthFailedError(
|
|
38430
38734
|
account2.name,
|
|
38431
38735
|
"linked-district session could not be re-established via the primary district",
|
|
38432
38736
|
{ credentialHint: false }
|
|
38433
38737
|
);
|
|
38434
38738
|
}
|
|
38435
|
-
return;
|
|
38739
|
+
return restored;
|
|
38740
|
+
}
|
|
38741
|
+
if (this.preloaded) {
|
|
38742
|
+
const { cookieHeader, xsrfToken } = this.preloaded;
|
|
38743
|
+
this.preloaded = null;
|
|
38744
|
+
return { cookieHeader, xsrfToken };
|
|
38436
38745
|
}
|
|
38437
38746
|
if (!account2.username || !account2.password) {
|
|
38438
38747
|
throw new AuthFailedError(
|
|
38439
38748
|
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"
|
|
38749
|
+
"session expired and no IC_USERNAME/IC_PASSWORD set \u2014 sign back into your IC portal in the browser and restart the MCP",
|
|
38750
|
+
{ permanent: true }
|
|
38441
38751
|
);
|
|
38442
38752
|
}
|
|
38443
38753
|
const postRes = await fetch(
|
|
@@ -38470,17 +38780,17 @@ var ICClient = class {
|
|
|
38470
38780
|
if (!cookies.cookieHeader) {
|
|
38471
38781
|
throw new AuthFailedError(account2.name, "login response missing session cookies");
|
|
38472
38782
|
}
|
|
38473
|
-
const session =
|
|
38474
|
-
|
|
38475
|
-
|
|
38476
|
-
|
|
38477
|
-
await this.discoverLinkedDistricts(account2);
|
|
38783
|
+
const session = {
|
|
38784
|
+
cookieHeader: cookies.cookieHeader,
|
|
38785
|
+
xsrfToken: cookies.xsrfToken
|
|
38786
|
+
};
|
|
38787
|
+
await this.discoverLinkedDistricts(account2, session);
|
|
38788
|
+
return session;
|
|
38478
38789
|
}
|
|
38479
|
-
async discoverLinkedDistricts(account2) {
|
|
38790
|
+
async discoverLinkedDistricts(account2, session) {
|
|
38480
38791
|
try {
|
|
38481
|
-
const session = this.sessions.get(account2.name);
|
|
38482
38792
|
const baseHeaders = {
|
|
38483
|
-
Cookie: session.
|
|
38793
|
+
Cookie: session.cookieHeader,
|
|
38484
38794
|
Accept: "application/json",
|
|
38485
38795
|
"Content-Type": "application/json",
|
|
38486
38796
|
Referer: `${account2.baseUrl}/campus/nav-wrapper/`,
|
|
@@ -38548,13 +38858,14 @@ var ICClient = class {
|
|
|
38548
38858
|
password: "(linked)"
|
|
38549
38859
|
};
|
|
38550
38860
|
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
38861
|
this.linkedTo.set(linked.districtName, account2.name);
|
|
38862
|
+
if (!this.managers.has(linked.districtName)) {
|
|
38863
|
+
this.managers.set(linked.districtName, this.makeManager(syntheticAccount));
|
|
38864
|
+
}
|
|
38865
|
+
this.managers.get(linked.districtName).seed({
|
|
38866
|
+
cookieHeader: switchCookies.cookieHeader,
|
|
38867
|
+
xsrfToken: switchCookies.xsrfToken
|
|
38868
|
+
});
|
|
38558
38869
|
console.error(`[ic] Linked district discovered: ${linked.districtName} (${linked.appName})`);
|
|
38559
38870
|
} catch (e) {
|
|
38560
38871
|
console.error(`[ic] CUPS flow failed for ${linked.districtName}: ${e instanceof Error ? e.message : e}`);
|
|
@@ -38580,12 +38891,11 @@ var ICClient = class {
|
|
|
38580
38891
|
}
|
|
38581
38892
|
const account2 = this.accounts.get(district);
|
|
38582
38893
|
if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
38583
|
-
await this.
|
|
38584
|
-
const session = this.sessions.get(account2.name);
|
|
38894
|
+
const session = await this.managers.get(account2.name).ensure();
|
|
38585
38895
|
const url2 = /^https?:\/\//i.test(path) ? path : `${account2.baseUrl}${path}`;
|
|
38586
38896
|
const res = await fetch(url2, {
|
|
38587
38897
|
headers: {
|
|
38588
|
-
Cookie: session.
|
|
38898
|
+
Cookie: session.cookieHeader,
|
|
38589
38899
|
...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {}
|
|
38590
38900
|
}
|
|
38591
38901
|
});
|
|
@@ -38598,34 +38908,23 @@ var ICClient = class {
|
|
|
38598
38908
|
contentType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
38599
38909
|
};
|
|
38600
38910
|
}
|
|
38601
|
-
async doRequest(account2, path, opts
|
|
38602
|
-
const
|
|
38911
|
+
async doRequest(account2, path, opts) {
|
|
38912
|
+
const mgr = this.managers.get(account2.name);
|
|
38603
38913
|
const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json";
|
|
38604
|
-
const res = await
|
|
38605
|
-
|
|
38606
|
-
|
|
38607
|
-
|
|
38608
|
-
|
|
38609
|
-
|
|
38610
|
-
|
|
38611
|
-
|
|
38612
|
-
|
|
38613
|
-
|
|
38914
|
+
const res = await mgr.withSession(
|
|
38915
|
+
async (session) => fetch(`${account2.baseUrl}${path}`, {
|
|
38916
|
+
method: opts.method ?? "GET",
|
|
38917
|
+
headers: {
|
|
38918
|
+
Cookie: session.cookieHeader,
|
|
38919
|
+
Accept: accept,
|
|
38920
|
+
...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {},
|
|
38921
|
+
...opts.headers ?? {}
|
|
38922
|
+
},
|
|
38923
|
+
body: opts.body
|
|
38924
|
+
})
|
|
38925
|
+
);
|
|
38614
38926
|
if (res.status === 401) {
|
|
38615
|
-
|
|
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);
|
|
38927
|
+
throw new SessionExpiredError(account2.name);
|
|
38629
38928
|
}
|
|
38630
38929
|
if (res.status >= 500) throw new PortalUnreachableError(account2.name, res.status);
|
|
38631
38930
|
if (!res.ok) throw new Error(`IC ${res.status} ${res.statusText} for ${path}`);
|
|
@@ -38638,14 +38937,11 @@ var ICClient = class {
|
|
|
38638
38937
|
};
|
|
38639
38938
|
function parseSetCookies(headers) {
|
|
38640
38939
|
const raw = headers.getSetCookie?.() ?? [];
|
|
38641
|
-
const
|
|
38642
|
-
|
|
38940
|
+
const { cookies, cookieHeader } = parseCookieJar(
|
|
38941
|
+
raw.length > 0 ? raw : headers.get("set-cookie")
|
|
38942
|
+
);
|
|
38643
38943
|
return { cookieHeader, xsrfToken: cookies["XSRF-TOKEN"] ?? "" };
|
|
38644
38944
|
}
|
|
38645
|
-
function splitFallback(header) {
|
|
38646
|
-
if (!header) return [];
|
|
38647
|
-
return header.split(",").map((s) => s.trim());
|
|
38648
|
-
}
|
|
38649
38945
|
var UnknownDistrictError = class extends Error {
|
|
38650
38946
|
constructor(district, available) {
|
|
38651
38947
|
super(`Unknown district '${district}'. Configured: [${available.join(", ")}]`);
|
|
@@ -38662,6 +38958,7 @@ var AuthFailedError = class extends Error {
|
|
|
38662
38958
|
* "Check IC_USERNAME and IC_PASSWORD" suffix — used for failures where the
|
|
38663
38959
|
* credentials are known-good (e.g. a linked-district CUPS/SSO re-discovery
|
|
38664
38960
|
* failure) and pointing the user at their creds would be misleading.
|
|
38961
|
+
* @param opts.permanent See {@link AuthFailedError.permanent}.
|
|
38665
38962
|
*/
|
|
38666
38963
|
constructor(district, reason, opts) {
|
|
38667
38964
|
const detail = reason ? ` (${reason})` : "";
|
|
@@ -38670,9 +38967,17 @@ var AuthFailedError = class extends Error {
|
|
|
38670
38967
|
this.district = district;
|
|
38671
38968
|
this.reason = reason;
|
|
38672
38969
|
this.name = "AuthFailedError";
|
|
38970
|
+
this.permanent = opts?.permanent ?? false;
|
|
38673
38971
|
}
|
|
38674
38972
|
district;
|
|
38675
38973
|
reason;
|
|
38974
|
+
/**
|
|
38975
|
+
* A *permanent* configuration failure: retrying the login inside this
|
|
38976
|
+
* process can never succeed (e.g. fetchproxy mode with no creds — the user
|
|
38977
|
+
* must re-sign-in in the browser and restart). CookieSessionManager caches
|
|
38978
|
+
* and rethrows permanent errors instead of retrying.
|
|
38979
|
+
*/
|
|
38980
|
+
permanent;
|
|
38676
38981
|
};
|
|
38677
38982
|
var PortalUnreachableError = class extends Error {
|
|
38678
38983
|
constructor(district, status) {
|
|
@@ -38717,40 +39022,6 @@ var FileExistsError = class extends Error {
|
|
|
38717
39022
|
path;
|
|
38718
39023
|
};
|
|
38719
39024
|
|
|
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
39025
|
// src/tools/districts.ts
|
|
38755
39026
|
function registerDistrictTools(server, client) {
|
|
38756
39027
|
server.registerTool("ic_list_districts", {
|
|
@@ -38759,7 +39030,7 @@ function registerDistrictTools(server, client) {
|
|
|
38759
39030
|
}, async () => {
|
|
38760
39031
|
await client.ensureDiscovery();
|
|
38761
39032
|
const data = client.listDistricts();
|
|
38762
|
-
return
|
|
39033
|
+
return textResult(data);
|
|
38763
39034
|
});
|
|
38764
39035
|
}
|
|
38765
39036
|
|
|
@@ -38775,7 +39046,7 @@ function registerStudentTools(server, client) {
|
|
|
38775
39046
|
}, async (rawArgs) => {
|
|
38776
39047
|
const args = argsSchema.parse(rawArgs);
|
|
38777
39048
|
const data = await client.request(args.district, "/campus/api/portal/students");
|
|
38778
|
-
return
|
|
39049
|
+
return textResult(data);
|
|
38779
39050
|
});
|
|
38780
39051
|
}
|
|
38781
39052
|
|
|
@@ -38795,7 +39066,7 @@ function registerScheduleTools(server, client) {
|
|
|
38795
39066
|
const args = argsSchema2.parse(rawArgs);
|
|
38796
39067
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
38797
39068
|
const data = await client.request(args.district, `/campus/resources/portal/roster?${params}`);
|
|
38798
|
-
return
|
|
39069
|
+
return textResult(data);
|
|
38799
39070
|
});
|
|
38800
39071
|
}
|
|
38801
39072
|
|
|
@@ -38833,7 +39104,7 @@ function registerAssignmentTools(server, client) {
|
|
|
38833
39104
|
if (args.missingOnly) {
|
|
38834
39105
|
data = data.filter((a) => a.missing);
|
|
38835
39106
|
}
|
|
38836
|
-
return
|
|
39107
|
+
return textResult(data);
|
|
38837
39108
|
});
|
|
38838
39109
|
}
|
|
38839
39110
|
|
|
@@ -38853,10 +39124,41 @@ function registerGradeTools(server, client) {
|
|
|
38853
39124
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
38854
39125
|
if (args.termId) params.set("termID", args.termId);
|
|
38855
39126
|
const data = await client.request(args.district, `/campus/resources/portal/grades?${params}`);
|
|
38856
|
-
return
|
|
39127
|
+
return textResult(data);
|
|
38857
39128
|
});
|
|
38858
39129
|
}
|
|
38859
39130
|
|
|
39131
|
+
// src/tools/_shared.ts
|
|
39132
|
+
function is404(e) {
|
|
39133
|
+
return e instanceof Error && e.message.startsWith("IC 404 ");
|
|
39134
|
+
}
|
|
39135
|
+
function featureDisabled(feature, district, data = []) {
|
|
39136
|
+
return textResult({ warning: "FeatureDisabled", feature, district, data });
|
|
39137
|
+
}
|
|
39138
|
+
async function findStudent(client, district, studentId) {
|
|
39139
|
+
const students = await client.request(district, "/campus/api/portal/students");
|
|
39140
|
+
return students.find((s) => String(s.personID) === studentId) ?? null;
|
|
39141
|
+
}
|
|
39142
|
+
function studentNotFound(studentId) {
|
|
39143
|
+
return textResult({ error: "StudentNotFound", studentId });
|
|
39144
|
+
}
|
|
39145
|
+
async function checkFeatureDisabled(client, district, studentId, student, flag, toolName, emptyData = []) {
|
|
39146
|
+
const structureID = student.enrollments?.[0]?.structureID;
|
|
39147
|
+
if (!structureID) return null;
|
|
39148
|
+
try {
|
|
39149
|
+
const features = await client.getFeatures(district, structureID, studentId);
|
|
39150
|
+
if (features[flag] === false) return featureDisabled(toolName, district, emptyData);
|
|
39151
|
+
return null;
|
|
39152
|
+
} catch (e) {
|
|
39153
|
+
console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
|
|
39154
|
+
return null;
|
|
39155
|
+
}
|
|
39156
|
+
}
|
|
39157
|
+
function toArray(value) {
|
|
39158
|
+
if (value === null || value === void 0) return [];
|
|
39159
|
+
return Array.isArray(value) ? value : [value];
|
|
39160
|
+
}
|
|
39161
|
+
|
|
38860
39162
|
// src/tools/attendance.ts
|
|
38861
39163
|
var argsSchema5 = external_exports.object({
|
|
38862
39164
|
district: external_exports.string(),
|
|
@@ -38921,7 +39223,7 @@ function registerAttendanceTools(server, client) {
|
|
|
38921
39223
|
results.push({ ...entry, terms: trimmedTerms });
|
|
38922
39224
|
}
|
|
38923
39225
|
}
|
|
38924
|
-
return
|
|
39226
|
+
return textResult(results);
|
|
38925
39227
|
} catch (e) {
|
|
38926
39228
|
if (is404(e)) return featureDisabled("attendance", args.district);
|
|
38927
39229
|
throw e;
|
|
@@ -38952,7 +39254,7 @@ function registerBehaviorTools(server, client) {
|
|
|
38952
39254
|
if (args.until) params.set("endDate", args.until);
|
|
38953
39255
|
try {
|
|
38954
39256
|
const data = await client.request(args.district, `/campus/resources/portal/behavior?${params}`);
|
|
38955
|
-
return
|
|
39257
|
+
return textResult(data);
|
|
38956
39258
|
} catch (e) {
|
|
38957
39259
|
if (is404(e)) return featureDisabled("behavior", args.district);
|
|
38958
39260
|
throw e;
|
|
@@ -38992,7 +39294,7 @@ function registerFoodServiceTools(server, client) {
|
|
|
38992
39294
|
if (args.until) params.set("endDate", args.until);
|
|
38993
39295
|
try {
|
|
38994
39296
|
const data = await client.request(args.district, `/campus/resources/portal/foodService?${params}`);
|
|
38995
|
-
return
|
|
39297
|
+
return textResult(data);
|
|
38996
39298
|
} catch (e) {
|
|
38997
39299
|
if (is404(e)) return featureDisabled("foodService", args.district, EMPTY_FOOD);
|
|
38998
39300
|
throw e;
|
|
@@ -39012,16 +39314,50 @@ var NAMED_ENTITIES = {
|
|
|
39012
39314
|
function extractPlainTextFromHtml(html) {
|
|
39013
39315
|
if (!html)
|
|
39014
39316
|
return "";
|
|
39015
|
-
let text = html
|
|
39317
|
+
let text = stripElementContent(stripElementContent(html, "script"), "style");
|
|
39016
39318
|
text = text.replace(/<[^>]+>/g, " ");
|
|
39017
|
-
text = text.replace(/&#(\d+);/g, (
|
|
39018
|
-
text = text.replace(/&#x([0-9a-fA-F]+);/g, (
|
|
39319
|
+
text = text.replace(/&#(\d+);/g, (whole, d) => codePointOr(Number(d), whole));
|
|
39320
|
+
text = text.replace(/&#x([0-9a-fA-F]+);/g, (whole, h) => codePointOr(parseInt(h, 16), whole));
|
|
39019
39321
|
text = text.replace(/&([a-zA-Z]+);/g, (whole, name) => {
|
|
39020
39322
|
const decoded = NAMED_ENTITIES[name.toLowerCase()];
|
|
39021
39323
|
return decoded ?? whole;
|
|
39022
39324
|
});
|
|
39023
39325
|
return text.replace(/\s+/g, " ").trim();
|
|
39024
39326
|
}
|
|
39327
|
+
function stripElementContent(html, tag) {
|
|
39328
|
+
const lower = html.toLowerCase();
|
|
39329
|
+
const open = `<${tag}`;
|
|
39330
|
+
const close = `</${tag}>`;
|
|
39331
|
+
let out = "";
|
|
39332
|
+
let i = 0;
|
|
39333
|
+
for (; ; ) {
|
|
39334
|
+
const start = lower.indexOf(open, i);
|
|
39335
|
+
if (start < 0) {
|
|
39336
|
+
out += html.slice(i);
|
|
39337
|
+
return out;
|
|
39338
|
+
}
|
|
39339
|
+
const after = lower[start + open.length];
|
|
39340
|
+
if (after !== void 0 && after !== ">" && after !== " " && after !== " " && after !== "\n" && after !== "\r" && after !== "\f" && after !== "/") {
|
|
39341
|
+
out += html.slice(i, start + open.length);
|
|
39342
|
+
i = start + open.length;
|
|
39343
|
+
continue;
|
|
39344
|
+
}
|
|
39345
|
+
out += html.slice(i, start) + " ";
|
|
39346
|
+
const closeAt = lower.indexOf(close, start + open.length);
|
|
39347
|
+
if (closeAt < 0)
|
|
39348
|
+
return out;
|
|
39349
|
+
i = closeAt + close.length;
|
|
39350
|
+
}
|
|
39351
|
+
}
|
|
39352
|
+
function codePointOr(code, raw) {
|
|
39353
|
+
if (!Number.isInteger(code) || code < 0 || code > 1114111)
|
|
39354
|
+
return raw;
|
|
39355
|
+
try {
|
|
39356
|
+
return String.fromCodePoint(code);
|
|
39357
|
+
} catch {
|
|
39358
|
+
return raw;
|
|
39359
|
+
}
|
|
39360
|
+
}
|
|
39025
39361
|
|
|
39026
39362
|
// src/tools/messages.ts
|
|
39027
39363
|
var listArgs = external_exports.object({
|
|
@@ -39116,7 +39452,7 @@ function registerMessageTools(server, client) {
|
|
|
39116
39452
|
return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
|
|
39117
39453
|
});
|
|
39118
39454
|
const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
|
|
39119
|
-
return
|
|
39455
|
+
return textResult({ notifications, inbox, announcements });
|
|
39120
39456
|
});
|
|
39121
39457
|
server.registerTool("ic_get_message", {
|
|
39122
39458
|
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 +39463,7 @@ function registerMessageTools(server, client) {
|
|
|
39127
39463
|
const path = normalizeMessageUrl(args.messageUrl);
|
|
39128
39464
|
const html = await client.request(args.district, path, { responseType: "text" });
|
|
39129
39465
|
const parsed = parseMessageHtml(html ?? "", path);
|
|
39130
|
-
return
|
|
39466
|
+
return textResult(parsed);
|
|
39131
39467
|
});
|
|
39132
39468
|
}
|
|
39133
39469
|
|
|
@@ -39167,7 +39503,7 @@ function registerDocumentTools(server, client) {
|
|
|
39167
39503
|
if (d.endYear !== void 0) out.endYear = d.endYear;
|
|
39168
39504
|
return out;
|
|
39169
39505
|
});
|
|
39170
|
-
return
|
|
39506
|
+
return textResult(trimmed);
|
|
39171
39507
|
} catch (e) {
|
|
39172
39508
|
if (is404(e)) return featureDisabled("documents", args.district);
|
|
39173
39509
|
throw e;
|
|
@@ -39183,10 +39519,10 @@ function registerDocumentTools(server, client) {
|
|
|
39183
39519
|
const meta3 = await client.download(args.district, args.documentId, args.destinationPath, {
|
|
39184
39520
|
overwrite: args.overwrite ?? false
|
|
39185
39521
|
});
|
|
39186
|
-
return
|
|
39522
|
+
return textResult(meta3);
|
|
39187
39523
|
} catch (e) {
|
|
39188
39524
|
if (e instanceof Error && e.message.startsWith("IC download 404")) {
|
|
39189
|
-
return
|
|
39525
|
+
return textResult({ warning: "FeatureDisabled", feature: "documents", district: args.district });
|
|
39190
39526
|
}
|
|
39191
39527
|
throw e;
|
|
39192
39528
|
}
|
|
@@ -39242,7 +39578,7 @@ function registerCalendarTools(server, client) {
|
|
|
39242
39578
|
terms: trimmedTerms
|
|
39243
39579
|
});
|
|
39244
39580
|
}
|
|
39245
|
-
return
|
|
39581
|
+
return textResult(result);
|
|
39246
39582
|
});
|
|
39247
39583
|
}
|
|
39248
39584
|
|
|
@@ -39333,7 +39669,7 @@ function registerAttendanceEventsTools(server, client) {
|
|
|
39333
39669
|
results.push(trimmed);
|
|
39334
39670
|
}
|
|
39335
39671
|
}
|
|
39336
|
-
return
|
|
39672
|
+
return textResult(results);
|
|
39337
39673
|
} catch (e) {
|
|
39338
39674
|
if (is404(e)) return featureDisabled("attendance_events", args.district);
|
|
39339
39675
|
throw e;
|
|
@@ -39389,7 +39725,7 @@ function registerRecentGradesTools(server, client) {
|
|
|
39389
39725
|
}
|
|
39390
39726
|
return out;
|
|
39391
39727
|
});
|
|
39392
|
-
return
|
|
39728
|
+
return textResult(trimmed);
|
|
39393
39729
|
});
|
|
39394
39730
|
}
|
|
39395
39731
|
|
|
@@ -39444,7 +39780,7 @@ function registerTeacherTools(server, client) {
|
|
|
39444
39780
|
const [teachersRaw, counselorsRaw] = await Promise.all([teachersPromise, counselorsPromise]);
|
|
39445
39781
|
const teachers = toArray(teachersRaw).map((t) => trimRecord(t));
|
|
39446
39782
|
const counselors = toArray(counselorsRaw).map((c) => trimRecord(c));
|
|
39447
|
-
return
|
|
39783
|
+
return textResult({ counselors, teachers });
|
|
39448
39784
|
});
|
|
39449
39785
|
}
|
|
39450
39786
|
|
|
@@ -39495,7 +39831,7 @@ function registerAssessmentTools(server, client) {
|
|
|
39495
39831
|
if (feature404 && result.length === 0) {
|
|
39496
39832
|
return featureDisabled("assessments", args.district);
|
|
39497
39833
|
}
|
|
39498
|
-
return
|
|
39834
|
+
return textResult(result);
|
|
39499
39835
|
});
|
|
39500
39836
|
}
|
|
39501
39837
|
|
|
@@ -39539,7 +39875,7 @@ function registerFeeTools(server, client) {
|
|
|
39539
39875
|
feeAssignments: assignments.ok ? assignments.value : [],
|
|
39540
39876
|
...issues.length > 0 ? { issues } : {}
|
|
39541
39877
|
};
|
|
39542
|
-
return
|
|
39878
|
+
return textResult(response);
|
|
39543
39879
|
});
|
|
39544
39880
|
}
|
|
39545
39881
|
|
|
@@ -39567,7 +39903,7 @@ function registerFeaturesTools(server, client) {
|
|
|
39567
39903
|
features
|
|
39568
39904
|
});
|
|
39569
39905
|
}
|
|
39570
|
-
return
|
|
39906
|
+
return textResult(result);
|
|
39571
39907
|
});
|
|
39572
39908
|
}
|
|
39573
39909
|
|
|
@@ -39591,7 +39927,7 @@ try {
|
|
|
39591
39927
|
}
|
|
39592
39928
|
var COMMON = {
|
|
39593
39929
|
name: "infinitecampus",
|
|
39594
|
-
version: "2.
|
|
39930
|
+
version: "2.4.0"
|
|
39595
39931
|
// x-release-please-version
|
|
39596
39932
|
};
|
|
39597
39933
|
if (account) {
|