infinitecampus-mcp 2.4.3 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/auth.js +12 -0
- package/dist/bundle.js +544 -79
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Infinite Campus (Campus Parent) — grades, attendance, assignments, messages, documents",
|
|
10
|
-
"version": "2.4.
|
|
10
|
+
"version": "2.4.5"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Infinite Campus",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents via natural language",
|
|
18
|
-
"version": "2.4.
|
|
18
|
+
"version": "2.4.5",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
3
|
"displayName": "Infinite Campus",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.5",
|
|
5
5
|
"description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/auth.js
CHANGED
|
@@ -115,6 +115,18 @@ function lifterFor(host) {
|
|
|
115
115
|
// <district>.infinitecampus.org, etc.). Declare just this host
|
|
116
116
|
// — each district is its own root, no wildcard needed.
|
|
117
117
|
domains: [host],
|
|
118
|
+
// IC is a Tomcat app deployed at /campus, and Tomcat scopes JSESSIONID
|
|
119
|
+
// to the servlet context by default:
|
|
120
|
+
//
|
|
121
|
+
// JSESSIONID=...; Path=/campus; Secure; HttpOnly; SameSite=Lax
|
|
122
|
+
//
|
|
123
|
+
// chrome.cookies.get matches that Path against the URL it is given, so a
|
|
124
|
+
// read aimed at the origin root silently returns nothing — which is
|
|
125
|
+
// indistinguishable from "the user is signed out", and is why this path
|
|
126
|
+
// never worked on any tenant. Verified live: without this the lift found
|
|
127
|
+
// no cookies while a fetch through the same tab returned authenticated
|
|
128
|
+
// data; with it, JSESSIONID + XSRF-TOKEN both come back.
|
|
129
|
+
storagePath: '/campus',
|
|
118
130
|
declare: {
|
|
119
131
|
// JSESSIONID is HttpOnly (chrome.cookies.get sees it; the
|
|
120
132
|
// security gate is the declared key list). XSRF-TOKEN is
|
package/dist/bundle.js
CHANGED
|
@@ -3651,7 +3651,12 @@ var require_fast_uri = __commonJS({
|
|
|
3651
3651
|
}
|
|
3652
3652
|
function resolve(baseURI, relativeURI, options) {
|
|
3653
3653
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3654
|
-
const
|
|
3654
|
+
const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
|
|
3655
|
+
const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
|
|
3656
|
+
if (baseMalformed || relativeMalformed) {
|
|
3657
|
+
throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
|
|
3658
|
+
}
|
|
3659
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
|
|
3655
3660
|
schemelessOptions.skipEscape = true;
|
|
3656
3661
|
return serialize(resolved, schemelessOptions);
|
|
3657
3662
|
}
|
|
@@ -3777,6 +3782,7 @@ var require_fast_uri = __commonJS({
|
|
|
3777
3782
|
}
|
|
3778
3783
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3779
3784
|
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
3785
|
+
var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
|
|
3780
3786
|
function getParseError(parsed, matches) {
|
|
3781
3787
|
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3782
3788
|
return 'URI path must start with "/" when authority is present.';
|
|
@@ -3811,6 +3817,20 @@ var require_fast_uri = __commonJS({
|
|
|
3811
3817
|
parsed.error = "URI authority must not contain a literal backslash.";
|
|
3812
3818
|
malformedAuthorityOrPort = true;
|
|
3813
3819
|
}
|
|
3820
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
|
|
3821
|
+
if (introducerMatch !== null) {
|
|
3822
|
+
const region = introducerMatch[1];
|
|
3823
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, "");
|
|
3824
|
+
if (normalizedRegion.length >= 2) {
|
|
3825
|
+
if (normalizedRegion.slice(0, 2) !== "//") {
|
|
3826
|
+
parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
|
|
3827
|
+
malformedAuthorityOrPort = true;
|
|
3828
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
3829
|
+
parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
|
|
3830
|
+
malformedAuthorityOrPort = true;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3814
3834
|
const matches = uri.match(URI_PARSE);
|
|
3815
3835
|
if (matches) {
|
|
3816
3836
|
parsed.scheme = matches[1];
|
|
@@ -10573,7 +10593,7 @@ var require_websocket_server = __commonJS({
|
|
|
10573
10593
|
});
|
|
10574
10594
|
|
|
10575
10595
|
// src/index.ts
|
|
10576
|
-
import { dirname as dirname2, join as
|
|
10596
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
10577
10597
|
import { fileURLToPath } from "url";
|
|
10578
10598
|
|
|
10579
10599
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -34854,8 +34874,15 @@ var pageSchema = {
|
|
|
34854
34874
|
};
|
|
34855
34875
|
|
|
34856
34876
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
34857
|
-
var PROTOCOL_VERSION =
|
|
34877
|
+
var PROTOCOL_VERSION = 3;
|
|
34858
34878
|
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
34879
|
+
function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
|
|
34880
|
+
const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
|
|
34881
|
+
out.set(mcpHelloNonce, 0);
|
|
34882
|
+
out.set(extHelloNonce, mcpHelloNonce.length);
|
|
34883
|
+
out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
|
|
34884
|
+
return out;
|
|
34885
|
+
}
|
|
34859
34886
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
34860
34887
|
"fetch",
|
|
34861
34888
|
"read_cookies",
|
|
@@ -34866,7 +34893,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34866
34893
|
"read_indexed_db",
|
|
34867
34894
|
"read_dom",
|
|
34868
34895
|
"download",
|
|
34869
|
-
"graphql"
|
|
34896
|
+
"graphql",
|
|
34897
|
+
"write_cookies"
|
|
34870
34898
|
]);
|
|
34871
34899
|
|
|
34872
34900
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -34977,6 +35005,15 @@ function assertHttpUrl(x, label) {
|
|
|
34977
35005
|
throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
|
|
34978
35006
|
}
|
|
34979
35007
|
}
|
|
35008
|
+
function assertCookiePath(x, label) {
|
|
35009
|
+
assertString(x, label);
|
|
35010
|
+
if (!x.startsWith("/") || x.startsWith("//")) {
|
|
35011
|
+
throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
|
|
35012
|
+
}
|
|
35013
|
+
if (x.includes("?") || x.includes("#") || x.includes("\\")) {
|
|
35014
|
+
throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
|
|
35015
|
+
}
|
|
35016
|
+
}
|
|
34980
35017
|
function assertHttpsOriginOnly(x, label) {
|
|
34981
35018
|
assertString(x, label);
|
|
34982
35019
|
let u;
|
|
@@ -35421,13 +35458,43 @@ function validateInnerRequest(raw) {
|
|
|
35421
35458
|
}
|
|
35422
35459
|
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35423
35460
|
assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
|
|
35461
|
+
if (raw.init.path !== void 0)
|
|
35462
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35424
35463
|
for (const k of Object.keys(raw.init)) {
|
|
35425
|
-
if (k !== "origin" && k !== "keys") {
|
|
35464
|
+
if (k !== "origin" && k !== "keys" && k !== "path") {
|
|
35426
35465
|
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
|
|
35427
35466
|
}
|
|
35428
35467
|
}
|
|
35429
35468
|
return raw;
|
|
35430
35469
|
}
|
|
35470
|
+
if (raw.op === "write_cookies") {
|
|
35471
|
+
assertObject(raw.init, "inner.init");
|
|
35472
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35473
|
+
if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
|
|
35474
|
+
throw new ProtocolError("inner.init.cookies: must be a non-empty array");
|
|
35475
|
+
}
|
|
35476
|
+
for (const [i, entry] of raw.init.cookies.entries()) {
|
|
35477
|
+
assertObject(entry, `inner.init.cookies[${i}]`);
|
|
35478
|
+
assertString(entry.name, `inner.init.cookies[${i}].name`);
|
|
35479
|
+
if (!SCOPE_KEY_RE.test(entry.name)) {
|
|
35480
|
+
throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
|
|
35481
|
+
}
|
|
35482
|
+
assertString(entry.value, `inner.init.cookies[${i}].value`);
|
|
35483
|
+
for (const k of Object.keys(entry)) {
|
|
35484
|
+
if (k !== "name" && k !== "value") {
|
|
35485
|
+
throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35486
|
+
}
|
|
35487
|
+
}
|
|
35488
|
+
}
|
|
35489
|
+
if (raw.init.path !== void 0)
|
|
35490
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35491
|
+
for (const k of Object.keys(raw.init)) {
|
|
35492
|
+
if (k !== "origin" && k !== "cookies" && k !== "path") {
|
|
35493
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
|
|
35494
|
+
}
|
|
35495
|
+
}
|
|
35496
|
+
return raw;
|
|
35497
|
+
}
|
|
35431
35498
|
if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
|
|
35432
35499
|
assertObject(raw.init, "inner.init");
|
|
35433
35500
|
if (raw.init.origin === void 0) {
|
|
@@ -35614,7 +35681,7 @@ function validateInnerRequest(raw) {
|
|
|
35614
35681
|
}
|
|
35615
35682
|
return raw;
|
|
35616
35683
|
}
|
|
35617
|
-
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download", "graphql_query"; got ${JSON.stringify(raw.op)}`);
|
|
35684
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
|
|
35618
35685
|
}
|
|
35619
35686
|
function assertNonEmptyKeyArray(value, label) {
|
|
35620
35687
|
if (!Array.isArray(value)) {
|
|
@@ -35675,6 +35742,18 @@ function validateInnerResponse(raw) {
|
|
|
35675
35742
|
}
|
|
35676
35743
|
return raw;
|
|
35677
35744
|
}
|
|
35745
|
+
if (op === "write_cookies") {
|
|
35746
|
+
if (raw.written === void 0) {
|
|
35747
|
+
throw new ProtocolError("inner.written: missing on write_cookies response");
|
|
35748
|
+
}
|
|
35749
|
+
if (!Array.isArray(raw.written)) {
|
|
35750
|
+
throw new ProtocolError("inner.written: must be an array");
|
|
35751
|
+
}
|
|
35752
|
+
for (const [i, name] of raw.written.entries()) {
|
|
35753
|
+
assertString(name, `inner.written[${i}]`);
|
|
35754
|
+
}
|
|
35755
|
+
return raw;
|
|
35756
|
+
}
|
|
35678
35757
|
if (op === "read_local_storage" || op === "read_session_storage") {
|
|
35679
35758
|
if (raw.values === void 0) {
|
|
35680
35759
|
throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
|
|
@@ -36152,6 +36231,148 @@ async function awaitSessionReady(ready, opts) {
|
|
|
36152
36231
|
}
|
|
36153
36232
|
}
|
|
36154
36233
|
|
|
36234
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36235
|
+
import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
36236
|
+
import { join as join2 } from "node:path";
|
|
36237
|
+
|
|
36238
|
+
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36239
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36240
|
+
import { join } from "node:path";
|
|
36241
|
+
import { homedir } from "node:os";
|
|
36242
|
+
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36243
|
+
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36244
|
+
function defaultIdentityDir() {
|
|
36245
|
+
return join(homedir(), ".fetchproxy", "identity");
|
|
36246
|
+
}
|
|
36247
|
+
function safeIdentityFileBase(serverName) {
|
|
36248
|
+
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36249
|
+
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36250
|
+
}
|
|
36251
|
+
return serverName.replace(/\//g, "_");
|
|
36252
|
+
}
|
|
36253
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36254
|
+
const safeFile = safeIdentityFileBase(serverName);
|
|
36255
|
+
const path = join(dir, `${safeFile}.json`);
|
|
36256
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36257
|
+
try {
|
|
36258
|
+
const raw = await readFile(path, "utf8");
|
|
36259
|
+
const j2 = JSON.parse(raw);
|
|
36260
|
+
return {
|
|
36261
|
+
x25519Priv: fromB64(j2.x25519Priv),
|
|
36262
|
+
x25519Pub: fromB64(j2.x25519Pub),
|
|
36263
|
+
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36264
|
+
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36265
|
+
createdAt: j2.createdAt
|
|
36266
|
+
};
|
|
36267
|
+
} catch (e) {
|
|
36268
|
+
if (e.code !== "ENOENT")
|
|
36269
|
+
throw e;
|
|
36270
|
+
}
|
|
36271
|
+
const x = await generateX25519();
|
|
36272
|
+
const ed = await generateEd25519();
|
|
36273
|
+
const id = {
|
|
36274
|
+
x25519Priv: x.privateKey,
|
|
36275
|
+
x25519Pub: x.publicKey,
|
|
36276
|
+
ed25519Priv: ed.privateKey,
|
|
36277
|
+
ed25519Pub: ed.publicKey,
|
|
36278
|
+
createdAt: Date.now()
|
|
36279
|
+
};
|
|
36280
|
+
const j = {
|
|
36281
|
+
x25519Priv: toB64(id.x25519Priv),
|
|
36282
|
+
x25519Pub: toB64(id.x25519Pub),
|
|
36283
|
+
ed25519Priv: toB64(id.ed25519Priv),
|
|
36284
|
+
ed25519Pub: toB64(id.ed25519Pub),
|
|
36285
|
+
createdAt: id.createdAt
|
|
36286
|
+
};
|
|
36287
|
+
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36288
|
+
await chmod(path, 384);
|
|
36289
|
+
return id;
|
|
36290
|
+
}
|
|
36291
|
+
|
|
36292
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36293
|
+
function fileExtensionTrust(args) {
|
|
36294
|
+
return {
|
|
36295
|
+
allowNew: args.allowNew,
|
|
36296
|
+
location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36297
|
+
read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36298
|
+
write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
|
|
36299
|
+
};
|
|
36300
|
+
}
|
|
36301
|
+
var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
|
|
36302
|
+
function allowNewExtensionIdentity(explicit, env = process.env) {
|
|
36303
|
+
if (explicit !== void 0)
|
|
36304
|
+
return explicit;
|
|
36305
|
+
return env[TRUST_NEW_EXTENSION_ENV] === "1";
|
|
36306
|
+
}
|
|
36307
|
+
function decideExtensionTrust(args) {
|
|
36308
|
+
const { pin, hello, allowNew, serverName } = args;
|
|
36309
|
+
if (!pin)
|
|
36310
|
+
return { decision: "first-use" };
|
|
36311
|
+
if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
|
|
36312
|
+
return { decision: "pinned" };
|
|
36313
|
+
}
|
|
36314
|
+
const trustPath = args.location ?? extensionTrustPathHint(serverName);
|
|
36315
|
+
if (allowNew) {
|
|
36316
|
+
return {
|
|
36317
|
+
decision: "replace",
|
|
36318
|
+
message: `[fetchproxy] ${serverName}: accepting a NEW extension identity because ${TRUST_NEW_EXTENSION_ENV}=1 \u2014 re-pinning. Unset it once the browser you expect is connected.`
|
|
36319
|
+
};
|
|
36320
|
+
}
|
|
36321
|
+
return {
|
|
36322
|
+
decision: "refused",
|
|
36323
|
+
message: `[fetchproxy] ${serverName}: refusing an extension whose identity is not the one this MCP paired with. If you re-installed the extension or moved to another browser, re-pair deliberately: run this MCP once with ${TRUST_NEW_EXTENSION_ENV}=1, or delete ${trustPath}. If you did neither, something else is answering as your browser.`
|
|
36324
|
+
};
|
|
36325
|
+
}
|
|
36326
|
+
function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
|
|
36327
|
+
return join2(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
|
|
36328
|
+
}
|
|
36329
|
+
function extensionTrustPathHint(serverName) {
|
|
36330
|
+
try {
|
|
36331
|
+
return extensionTrustPath(serverName);
|
|
36332
|
+
} catch {
|
|
36333
|
+
return join2(defaultIdentityDir(), "<server-name>.extension-trust.json");
|
|
36334
|
+
}
|
|
36335
|
+
}
|
|
36336
|
+
function isPin(x) {
|
|
36337
|
+
if (!x || typeof x !== "object")
|
|
36338
|
+
return false;
|
|
36339
|
+
const r = x;
|
|
36340
|
+
return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
|
|
36341
|
+
}
|
|
36342
|
+
async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
|
|
36343
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36344
|
+
let raw;
|
|
36345
|
+
try {
|
|
36346
|
+
raw = await readFile2(path, "utf8");
|
|
36347
|
+
} catch (e) {
|
|
36348
|
+
if (e.code === "ENOENT")
|
|
36349
|
+
return null;
|
|
36350
|
+
throw e;
|
|
36351
|
+
}
|
|
36352
|
+
let parsed;
|
|
36353
|
+
try {
|
|
36354
|
+
parsed = JSON.parse(raw);
|
|
36355
|
+
} catch {
|
|
36356
|
+
throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
|
|
36357
|
+
}
|
|
36358
|
+
if (!isPin(parsed)) {
|
|
36359
|
+
throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
|
|
36360
|
+
}
|
|
36361
|
+
return {
|
|
36362
|
+
identityX25519Pub: parsed.identityX25519Pub,
|
|
36363
|
+
identityEd25519Pub: parsed.identityEd25519Pub,
|
|
36364
|
+
pinnedAt: parsed.pinnedAt
|
|
36365
|
+
};
|
|
36366
|
+
}
|
|
36367
|
+
async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
|
|
36368
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36369
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
36370
|
+
const tmp = `${path}.tmp`;
|
|
36371
|
+
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
|
|
36372
|
+
await chmod2(tmp, 384);
|
|
36373
|
+
await rename(tmp, path);
|
|
36374
|
+
}
|
|
36375
|
+
|
|
36155
36376
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
36156
36377
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
36157
36378
|
var enc2 = new TextEncoder();
|
|
@@ -36205,9 +36426,12 @@ async function startHost(opts) {
|
|
|
36205
36426
|
}
|
|
36206
36427
|
resetSessionPromise();
|
|
36207
36428
|
let extensionHello = null;
|
|
36429
|
+
let extensionClaim = null;
|
|
36208
36430
|
wss.on("connection", (ws) => {
|
|
36209
36431
|
let identified = null;
|
|
36210
36432
|
let peerMcpId = null;
|
|
36433
|
+
let closed = false;
|
|
36434
|
+
let pinOnReady = false;
|
|
36211
36435
|
ws.on("message", async (data) => {
|
|
36212
36436
|
try {
|
|
36213
36437
|
let frame;
|
|
@@ -36219,10 +36443,43 @@ async function startHost(opts) {
|
|
|
36219
36443
|
return;
|
|
36220
36444
|
}
|
|
36221
36445
|
if (frame.type === "hello" && frame.role === "extension") {
|
|
36222
|
-
if (extensionWs) {
|
|
36446
|
+
if (extensionWs || extensionClaim) {
|
|
36223
36447
|
ws.close(1008, "extension already connected");
|
|
36224
36448
|
return;
|
|
36225
36449
|
}
|
|
36450
|
+
extensionClaim = ws;
|
|
36451
|
+
let pin;
|
|
36452
|
+
try {
|
|
36453
|
+
pin = await opts.extensionTrust.read();
|
|
36454
|
+
} catch (e) {
|
|
36455
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36456
|
+
if (extensionClaim === ws)
|
|
36457
|
+
extensionClaim = null;
|
|
36458
|
+
ws.close(1008, "extension pin unreadable");
|
|
36459
|
+
return;
|
|
36460
|
+
}
|
|
36461
|
+
const outcome = decideExtensionTrust({
|
|
36462
|
+
pin,
|
|
36463
|
+
hello: frame,
|
|
36464
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36465
|
+
serverName: opts.ownServerName,
|
|
36466
|
+
location: opts.extensionTrust.location
|
|
36467
|
+
});
|
|
36468
|
+
if (outcome.decision === "refused") {
|
|
36469
|
+
console.warn(outcome.message);
|
|
36470
|
+
if (extensionClaim === ws)
|
|
36471
|
+
extensionClaim = null;
|
|
36472
|
+
ws.close(1008, "extension identity is not the pinned one");
|
|
36473
|
+
return;
|
|
36474
|
+
}
|
|
36475
|
+
if (outcome.decision === "replace")
|
|
36476
|
+
console.warn(outcome.message);
|
|
36477
|
+
if (closed || ws.readyState !== import_websocket.default.OPEN) {
|
|
36478
|
+
if (extensionClaim === ws)
|
|
36479
|
+
extensionClaim = null;
|
|
36480
|
+
return;
|
|
36481
|
+
}
|
|
36482
|
+
pinOnReady = outcome.decision !== "pinned";
|
|
36226
36483
|
identified = "extension";
|
|
36227
36484
|
extensionWs = ws;
|
|
36228
36485
|
extensionHello = frame;
|
|
@@ -36234,6 +36491,8 @@ async function startHost(opts) {
|
|
|
36234
36491
|
console.error("[fetchproxy] onPairCode threw:", e);
|
|
36235
36492
|
}
|
|
36236
36493
|
}
|
|
36494
|
+
for (const slot of peers.values())
|
|
36495
|
+
slot.ws.send(JSON.stringify(frame));
|
|
36237
36496
|
ws.send(JSON.stringify(ownHello));
|
|
36238
36497
|
for (const slot of peers.values()) {
|
|
36239
36498
|
ws.send(JSON.stringify(slot.helloFrame));
|
|
@@ -36269,6 +36528,8 @@ async function startHost(opts) {
|
|
|
36269
36528
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
36270
36529
|
if (extensionWs)
|
|
36271
36530
|
extensionWs.send(JSON.stringify(frame));
|
|
36531
|
+
if (extensionHello)
|
|
36532
|
+
ws.send(JSON.stringify(extensionHello));
|
|
36272
36533
|
return;
|
|
36273
36534
|
}
|
|
36274
36535
|
if (frame.type === "ready") {
|
|
@@ -36280,7 +36541,7 @@ async function startHost(opts) {
|
|
|
36280
36541
|
}
|
|
36281
36542
|
const extEdPub = fromB64(extensionHello.identityEd25519Pub);
|
|
36282
36543
|
const extNonce = fromB64(extensionHello.sessionNonce);
|
|
36283
|
-
const msg =
|
|
36544
|
+
const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
|
|
36284
36545
|
const sig = fromB64(frame.sessionSig);
|
|
36285
36546
|
let sigOk = false;
|
|
36286
36547
|
try {
|
|
@@ -36293,6 +36554,18 @@ async function startHost(opts) {
|
|
|
36293
36554
|
ws.close(1008, "extension session signature invalid");
|
|
36294
36555
|
return;
|
|
36295
36556
|
}
|
|
36557
|
+
if (pinOnReady) {
|
|
36558
|
+
pinOnReady = false;
|
|
36559
|
+
try {
|
|
36560
|
+
await opts.extensionTrust.write({
|
|
36561
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36562
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36563
|
+
pinnedAt: Date.now()
|
|
36564
|
+
});
|
|
36565
|
+
} catch (e) {
|
|
36566
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36567
|
+
}
|
|
36568
|
+
}
|
|
36296
36569
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36297
36570
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
36298
36571
|
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36346,6 +36619,9 @@ async function startHost(opts) {
|
|
|
36346
36619
|
}
|
|
36347
36620
|
});
|
|
36348
36621
|
ws.on("close", () => {
|
|
36622
|
+
closed = true;
|
|
36623
|
+
if (extensionClaim === ws)
|
|
36624
|
+
extensionClaim = null;
|
|
36349
36625
|
if (identified === "extension" && extensionWs === ws) {
|
|
36350
36626
|
extensionWs = null;
|
|
36351
36627
|
extensionHello = null;
|
|
@@ -36436,11 +36712,84 @@ async function startPeer(opts) {
|
|
|
36436
36712
|
resolveFirstReady = resolve;
|
|
36437
36713
|
rejectFirstReady = reject;
|
|
36438
36714
|
});
|
|
36715
|
+
let extensionHello = null;
|
|
36716
|
+
let warnedUnverifiable = false;
|
|
36717
|
+
let cachedPin = void 0;
|
|
36718
|
+
const authenticateExtension = async (sessionSig, extensionSessionPub) => {
|
|
36719
|
+
if (!extensionHello) {
|
|
36720
|
+
if (opts.requireExtensionIdentity) {
|
|
36721
|
+
console.error(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity, so this session cannot be verified \u2014 refusing. Upgrade the MCP holding the bridge port to 1.12.0 or later.`);
|
|
36722
|
+
return false;
|
|
36723
|
+
}
|
|
36724
|
+
if (!warnedUnverifiable) {
|
|
36725
|
+
warnedUnverifiable = true;
|
|
36726
|
+
console.warn(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity (pre-1.12.0), so this peer cannot verify which browser it is talking to. Upgrade the MCP holding the bridge port to close this.`);
|
|
36727
|
+
}
|
|
36728
|
+
return true;
|
|
36729
|
+
}
|
|
36730
|
+
const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
|
|
36731
|
+
let sigOk = false;
|
|
36732
|
+
try {
|
|
36733
|
+
sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
|
|
36734
|
+
} catch {
|
|
36735
|
+
sigOk = false;
|
|
36736
|
+
}
|
|
36737
|
+
if (!sigOk) {
|
|
36738
|
+
console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
|
|
36739
|
+
return false;
|
|
36740
|
+
}
|
|
36741
|
+
if (cachedPin === void 0) {
|
|
36742
|
+
try {
|
|
36743
|
+
cachedPin = await opts.extensionTrust.read();
|
|
36744
|
+
} catch (e) {
|
|
36745
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36746
|
+
return false;
|
|
36747
|
+
}
|
|
36748
|
+
}
|
|
36749
|
+
const pin = cachedPin;
|
|
36750
|
+
const outcome = decideExtensionTrust({
|
|
36751
|
+
pin,
|
|
36752
|
+
hello: extensionHello,
|
|
36753
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36754
|
+
serverName: opts.serverName,
|
|
36755
|
+
location: opts.extensionTrust.location
|
|
36756
|
+
});
|
|
36757
|
+
if (outcome.decision === "refused") {
|
|
36758
|
+
console.warn(outcome.message);
|
|
36759
|
+
return false;
|
|
36760
|
+
}
|
|
36761
|
+
if (outcome.decision === "replace")
|
|
36762
|
+
console.warn(outcome.message);
|
|
36763
|
+
if (outcome.decision !== "pinned") {
|
|
36764
|
+
try {
|
|
36765
|
+
const written = {
|
|
36766
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36767
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36768
|
+
pinnedAt: Date.now()
|
|
36769
|
+
};
|
|
36770
|
+
await opts.extensionTrust.write(written);
|
|
36771
|
+
cachedPin = written;
|
|
36772
|
+
} catch (e) {
|
|
36773
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36774
|
+
}
|
|
36775
|
+
}
|
|
36776
|
+
return true;
|
|
36777
|
+
};
|
|
36439
36778
|
const onMessage = async (data) => {
|
|
36440
36779
|
try {
|
|
36441
36780
|
const raw = JSON.parse(data.toString());
|
|
36442
36781
|
const frame = validateFrame(raw);
|
|
36782
|
+
if (frame.type === "hello" && frame.role === "extension") {
|
|
36783
|
+
extensionHello = frame;
|
|
36784
|
+
return;
|
|
36785
|
+
}
|
|
36443
36786
|
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
36787
|
+
const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
|
|
36788
|
+
if (!authorised) {
|
|
36789
|
+
ws.close(1008, "extension identity refused");
|
|
36790
|
+
rejectFirstReady(new Error("peer: extension identity refused"));
|
|
36791
|
+
return;
|
|
36792
|
+
}
|
|
36444
36793
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36445
36794
|
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
36446
36795
|
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36521,57 +36870,6 @@ async function startPeer(opts) {
|
|
|
36521
36870
|
return handle;
|
|
36522
36871
|
}
|
|
36523
36872
|
|
|
36524
|
-
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36525
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36526
|
-
import { join } from "node:path";
|
|
36527
|
-
import { homedir } from "node:os";
|
|
36528
|
-
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36529
|
-
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36530
|
-
function defaultIdentityDir() {
|
|
36531
|
-
return join(homedir(), ".fetchproxy", "identity");
|
|
36532
|
-
}
|
|
36533
|
-
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36534
|
-
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36535
|
-
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36536
|
-
}
|
|
36537
|
-
const safeFile = serverName.replace(/\//g, "_");
|
|
36538
|
-
const path = join(dir, `${safeFile}.json`);
|
|
36539
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36540
|
-
try {
|
|
36541
|
-
const raw = await readFile(path, "utf8");
|
|
36542
|
-
const j2 = JSON.parse(raw);
|
|
36543
|
-
return {
|
|
36544
|
-
x25519Priv: fromB64(j2.x25519Priv),
|
|
36545
|
-
x25519Pub: fromB64(j2.x25519Pub),
|
|
36546
|
-
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36547
|
-
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36548
|
-
createdAt: j2.createdAt
|
|
36549
|
-
};
|
|
36550
|
-
} catch (e) {
|
|
36551
|
-
if (e.code !== "ENOENT")
|
|
36552
|
-
throw e;
|
|
36553
|
-
}
|
|
36554
|
-
const x = await generateX25519();
|
|
36555
|
-
const ed = await generateEd25519();
|
|
36556
|
-
const id = {
|
|
36557
|
-
x25519Priv: x.privateKey,
|
|
36558
|
-
x25519Pub: x.publicKey,
|
|
36559
|
-
ed25519Priv: ed.privateKey,
|
|
36560
|
-
ed25519Pub: ed.publicKey,
|
|
36561
|
-
createdAt: Date.now()
|
|
36562
|
-
};
|
|
36563
|
-
const j = {
|
|
36564
|
-
x25519Priv: toB64(id.x25519Priv),
|
|
36565
|
-
x25519Pub: toB64(id.x25519Pub),
|
|
36566
|
-
ed25519Priv: toB64(id.ed25519Priv),
|
|
36567
|
-
ed25519Pub: toB64(id.ed25519Pub),
|
|
36568
|
-
createdAt: id.createdAt
|
|
36569
|
-
};
|
|
36570
|
-
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36571
|
-
await chmod(path, 384);
|
|
36572
|
-
return id;
|
|
36573
|
-
}
|
|
36574
|
-
|
|
36575
36873
|
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36576
36874
|
function classifyFetchError(error51) {
|
|
36577
36875
|
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
@@ -36653,6 +36951,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
36653
36951
|
this.hint = hint;
|
|
36654
36952
|
}
|
|
36655
36953
|
};
|
|
36954
|
+
var FetchproxyHintedError = class extends FetchproxyProtocolError {
|
|
36955
|
+
/** The extension's raw rejection, unmodified. */
|
|
36956
|
+
originalError;
|
|
36957
|
+
/** What the user should actually do, in prose. */
|
|
36958
|
+
hint;
|
|
36959
|
+
constructor(originalError, hint) {
|
|
36960
|
+
super(`${originalError} \u2014 ${hint}`);
|
|
36961
|
+
this.name = "FetchproxyHintedError";
|
|
36962
|
+
this.originalError = originalError;
|
|
36963
|
+
this.hint = hint;
|
|
36964
|
+
}
|
|
36965
|
+
};
|
|
36966
|
+
var FetchproxyScopeError = class extends FetchproxyHintedError {
|
|
36967
|
+
constructor(originalError) {
|
|
36968
|
+
super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
|
|
36969
|
+
this.name = "FetchproxyScopeError";
|
|
36970
|
+
}
|
|
36971
|
+
};
|
|
36972
|
+
var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
36973
|
+
constructor(originalError) {
|
|
36974
|
+
super(originalError, "open a tab on that host and sign in, then re-run. This is not a version problem and does not need an update.");
|
|
36975
|
+
this.name = "FetchproxyNoTabError";
|
|
36976
|
+
}
|
|
36977
|
+
};
|
|
36978
|
+
var SCOPE_REJECTION = /not in declared/;
|
|
36979
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
36980
|
+
function protocolErrorFrom(error51) {
|
|
36981
|
+
if (SCOPE_REJECTION.test(error51))
|
|
36982
|
+
return new FetchproxyScopeError(error51);
|
|
36983
|
+
if (NO_TAB_REJECTION.test(error51))
|
|
36984
|
+
return new FetchproxyNoTabError(error51);
|
|
36985
|
+
return new FetchproxyProtocolError(error51);
|
|
36986
|
+
}
|
|
36656
36987
|
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36657
36988
|
url;
|
|
36658
36989
|
timeoutMs;
|
|
@@ -36682,6 +37013,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
36682
37013
|
}
|
|
36683
37014
|
};
|
|
36684
37015
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
37016
|
+
function normalizeCookiePath(path) {
|
|
37017
|
+
if (path === void 0 || path === "")
|
|
37018
|
+
return void 0;
|
|
37019
|
+
const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
37020
|
+
try {
|
|
37021
|
+
assertCookiePath(trimmed, "path");
|
|
37022
|
+
} catch (e) {
|
|
37023
|
+
throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
|
|
37024
|
+
}
|
|
37025
|
+
return trimmed;
|
|
37026
|
+
}
|
|
36685
37027
|
function assertSubdomainLabel(label) {
|
|
36686
37028
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
36687
37029
|
throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
|
|
@@ -36746,6 +37088,9 @@ var FetchproxyServer = class {
|
|
|
36746
37088
|
// them off from `pending` (fetch) and `pendingReadCookies` (legacy
|
|
36747
37089
|
// string-shape) so the response routing in `onInner` stays linear.
|
|
36748
37090
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
37091
|
+
// 1.12.0+: write-cookies awaiters resolve the list of names actually
|
|
37092
|
+
// written, so a caller can confirm rather than assume.
|
|
37093
|
+
pendingWriteCookies = /* @__PURE__ */ new Map();
|
|
36749
37094
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36750
37095
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36751
37096
|
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
@@ -36872,6 +37217,8 @@ var FetchproxyServer = class {
|
|
|
36872
37217
|
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
36873
37218
|
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
36874
37219
|
identityDir: opts.identityDir,
|
|
37220
|
+
allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
|
|
37221
|
+
requireExtensionIdentity: opts.requireExtensionIdentity,
|
|
36875
37222
|
onPairCode: opts.onPairCode
|
|
36876
37223
|
};
|
|
36877
37224
|
}
|
|
@@ -36971,7 +37318,8 @@ var FetchproxyServer = class {
|
|
|
36971
37318
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36972
37319
|
ownDomSelectors: this.opts.domSelectors,
|
|
36973
37320
|
ownGraphqlOps: this.opts.graphqlOps,
|
|
36974
|
-
onPairCode: this.opts.onPairCode
|
|
37321
|
+
onPairCode: this.opts.onPairCode,
|
|
37322
|
+
extensionTrust: this.extensionTrust()
|
|
36975
37323
|
});
|
|
36976
37324
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36977
37325
|
this.hostHandle.onExtensionDisconnect(() => {
|
|
@@ -37000,7 +37348,9 @@ var FetchproxyServer = class {
|
|
|
37000
37348
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37001
37349
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37002
37350
|
domSelectors: this.opts.domSelectors,
|
|
37003
|
-
graphqlOps: this.opts.graphqlOps
|
|
37351
|
+
graphqlOps: this.opts.graphqlOps,
|
|
37352
|
+
extensionTrust: this.extensionTrust(),
|
|
37353
|
+
requireExtensionIdentity: this.opts.requireExtensionIdentity
|
|
37004
37354
|
});
|
|
37005
37355
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37006
37356
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37139,6 +37489,23 @@ var FetchproxyServer = class {
|
|
|
37139
37489
|
markActive() {
|
|
37140
37490
|
this.noteActivityForKeepalive();
|
|
37141
37491
|
}
|
|
37492
|
+
/**
|
|
37493
|
+
* #208: this MCP's pin on the extension's identity, stored beside its own
|
|
37494
|
+
* identity key and so following `identityDir` wherever the caller put it.
|
|
37495
|
+
*
|
|
37496
|
+
* `allowNewExtensionIdentity` falls back to an environment variable when the
|
|
37497
|
+
* caller expressed no opinion, because the thirteen MCPs that construct this
|
|
37498
|
+
* class are separate packages: an operator whose extension re-install has
|
|
37499
|
+
* just locked all of them out needs one lever that does not require patching
|
|
37500
|
+
* every one of them.
|
|
37501
|
+
*/
|
|
37502
|
+
extensionTrust() {
|
|
37503
|
+
return fileExtensionTrust({
|
|
37504
|
+
serverName: this.opts.serverName,
|
|
37505
|
+
dir: this.opts.identityDir,
|
|
37506
|
+
allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
|
|
37507
|
+
});
|
|
37508
|
+
}
|
|
37142
37509
|
noteActivityForKeepalive() {
|
|
37143
37510
|
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
37144
37511
|
if (intervalMs <= 0)
|
|
@@ -37202,6 +37569,7 @@ var FetchproxyServer = class {
|
|
|
37202
37569
|
this.pending.delete(id);
|
|
37203
37570
|
this.pendingReadCookies.delete(id);
|
|
37204
37571
|
this.pendingStorage.delete(id);
|
|
37572
|
+
this.pendingWriteCookies.delete(id);
|
|
37205
37573
|
this.pendingCapture.delete(id);
|
|
37206
37574
|
this.pendingRedirect.delete(id);
|
|
37207
37575
|
this.pendingDownload.delete(id);
|
|
@@ -37323,7 +37691,7 @@ var FetchproxyServer = class {
|
|
|
37323
37691
|
port: this.opts.port
|
|
37324
37692
|
});
|
|
37325
37693
|
}
|
|
37326
|
-
return
|
|
37694
|
+
return protocolErrorFrom(result.error);
|
|
37327
37695
|
}
|
|
37328
37696
|
/**
|
|
37329
37697
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -37359,10 +37727,20 @@ var FetchproxyServer = class {
|
|
|
37359
37727
|
}
|
|
37360
37728
|
const url2 = isAbsolute ? path : `https://${host}${path}`;
|
|
37361
37729
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
37730
|
+
let tabUrl = `https://${host}/`;
|
|
37731
|
+
if (opts.viaTab !== void 0) {
|
|
37732
|
+
try {
|
|
37733
|
+
new URL(opts.viaTab);
|
|
37734
|
+
} catch {
|
|
37735
|
+
throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
|
|
37736
|
+
}
|
|
37737
|
+
assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
|
|
37738
|
+
tabUrl = opts.viaTab;
|
|
37739
|
+
}
|
|
37362
37740
|
const init = {
|
|
37363
37741
|
url: url2,
|
|
37364
37742
|
method,
|
|
37365
|
-
tabUrl
|
|
37743
|
+
tabUrl,
|
|
37366
37744
|
headers: opts.headers,
|
|
37367
37745
|
body: opts.body
|
|
37368
37746
|
};
|
|
@@ -37574,9 +37952,14 @@ var FetchproxyServer = class {
|
|
|
37574
37952
|
let inner;
|
|
37575
37953
|
if (opts.keys !== void 0) {
|
|
37576
37954
|
this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
|
|
37955
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
37577
37956
|
const initV3 = {
|
|
37957
|
+
// Origin stays BARE. The path travels as its own validated field —
|
|
37958
|
+
// `assertHttpsOriginOnly` deliberately refuses a path here so one
|
|
37959
|
+
// cannot be used to re-point the read past the domain gate.
|
|
37578
37960
|
origin: `https://${host}`,
|
|
37579
|
-
keys: [...opts.keys]
|
|
37961
|
+
keys: [...opts.keys],
|
|
37962
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
37580
37963
|
};
|
|
37581
37964
|
inner = { type: "request", id, op: "read_cookies", init: initV3 };
|
|
37582
37965
|
} else {
|
|
@@ -37589,10 +37972,65 @@ var FetchproxyServer = class {
|
|
|
37589
37972
|
await this.sendInnerFrame(inner);
|
|
37590
37973
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
37591
37974
|
if (!result.ok) {
|
|
37592
|
-
throw
|
|
37975
|
+
throw protocolErrorFrom(result.error);
|
|
37593
37976
|
}
|
|
37594
37977
|
return result.cookies;
|
|
37595
37978
|
}
|
|
37979
|
+
/**
|
|
37980
|
+
* 1.12.0+: overwrite the value of cookies this MCP already declares.
|
|
37981
|
+
*
|
|
37982
|
+
* The bridge's only write verb, and it exists for one failure class. Sites
|
|
37983
|
+
* that ROTATE a credential cookie hand back a new value on every refresh; if
|
|
37984
|
+
* the MCP refreshes and keeps the result to itself, the copy in the browser's
|
|
37985
|
+
* cookie jar is dead, and the user gets signed out of a tab they never
|
|
37986
|
+
* touched — usually reported to them as "inactivity". Writing the rotated
|
|
37987
|
+
* value back is the only thing that repairs it.
|
|
37988
|
+
*
|
|
37989
|
+
* Requires `'write_cookies'` in capabilities, which the user approves at pair
|
|
37990
|
+
* time as its own line. Every name must ALSO be in declared `cookieKeys`: a
|
|
37991
|
+
* write can never reach a cookie the MCP was not already trusted to read, so
|
|
37992
|
+
* granting it cannot widen which cookies are in play — only what may be done
|
|
37993
|
+
* to the ones already listed.
|
|
37994
|
+
*
|
|
37995
|
+
* The extension refuses the whole request unless every named cookie already
|
|
37996
|
+
* exists; this refreshes a value in place and deliberately cannot author new
|
|
37997
|
+
* cookies. Returns the names actually written.
|
|
37998
|
+
*/
|
|
37999
|
+
async writeCookies(opts) {
|
|
38000
|
+
if (!this.opts.capabilities.includes("write_cookies")) {
|
|
38001
|
+
throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
38002
|
+
}
|
|
38003
|
+
const names = Object.keys(opts.cookies);
|
|
38004
|
+
if (names.length === 0) {
|
|
38005
|
+
throw new Error("FetchproxyServer.writeCookies(): no cookies given");
|
|
38006
|
+
}
|
|
38007
|
+
await this.ensureConnected();
|
|
38008
|
+
this.throwIfPendingPair();
|
|
38009
|
+
if (opts.subdomain !== void 0)
|
|
38010
|
+
assertSubdomainLabel(opts.subdomain);
|
|
38011
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
38012
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
38013
|
+
this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
|
|
38014
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
38015
|
+
const id = this.nextRequestId++;
|
|
38016
|
+
const inner = {
|
|
38017
|
+
type: "request",
|
|
38018
|
+
id,
|
|
38019
|
+
op: "write_cookies",
|
|
38020
|
+
init: {
|
|
38021
|
+
// Bare origin, same invariant as the read path: a path must never be
|
|
38022
|
+
// able to move the request past the domain gate.
|
|
38023
|
+
origin: `https://${host}`,
|
|
38024
|
+
cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
|
|
38025
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
38026
|
+
}
|
|
38027
|
+
};
|
|
38028
|
+
const pending = new Promise((resolve, reject) => {
|
|
38029
|
+
this.pendingWriteCookies.set(id, { resolve, reject });
|
|
38030
|
+
});
|
|
38031
|
+
await this.sendInnerFrame(inner);
|
|
38032
|
+
return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
|
|
38033
|
+
}
|
|
37596
38034
|
/**
|
|
37597
38035
|
* 0.3.0+: read declared localStorage keys from the user's signed-in
|
|
37598
38036
|
* tab. Requires `'read_local_storage'` in capabilities AND each key
|
|
@@ -38139,7 +38577,7 @@ var FetchproxyServer = class {
|
|
|
38139
38577
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
38140
38578
|
}
|
|
38141
38579
|
} else {
|
|
38142
|
-
storageCb.reject(
|
|
38580
|
+
storageCb.reject(protocolErrorFrom(inner.error));
|
|
38143
38581
|
}
|
|
38144
38582
|
return;
|
|
38145
38583
|
}
|
|
@@ -38153,7 +38591,7 @@ var FetchproxyServer = class {
|
|
|
38153
38591
|
captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
|
|
38154
38592
|
}
|
|
38155
38593
|
} else {
|
|
38156
|
-
captureCb.reject(
|
|
38594
|
+
captureCb.reject(protocolErrorFrom(inner.error));
|
|
38157
38595
|
}
|
|
38158
38596
|
return;
|
|
38159
38597
|
}
|
|
@@ -38167,7 +38605,7 @@ var FetchproxyServer = class {
|
|
|
38167
38605
|
redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
|
|
38168
38606
|
}
|
|
38169
38607
|
} else {
|
|
38170
|
-
redirectCb.reject(
|
|
38608
|
+
redirectCb.reject(protocolErrorFrom(inner.error));
|
|
38171
38609
|
}
|
|
38172
38610
|
return;
|
|
38173
38611
|
}
|
|
@@ -38181,7 +38619,7 @@ var FetchproxyServer = class {
|
|
|
38181
38619
|
idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
|
|
38182
38620
|
}
|
|
38183
38621
|
} else {
|
|
38184
|
-
idbCb.reject(
|
|
38622
|
+
idbCb.reject(protocolErrorFrom(inner.error));
|
|
38185
38623
|
}
|
|
38186
38624
|
return;
|
|
38187
38625
|
}
|
|
@@ -38195,7 +38633,7 @@ var FetchproxyServer = class {
|
|
|
38195
38633
|
downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
|
|
38196
38634
|
}
|
|
38197
38635
|
} else {
|
|
38198
|
-
downloadCb.reject(
|
|
38636
|
+
downloadCb.reject(protocolErrorFrom(inner.error));
|
|
38199
38637
|
}
|
|
38200
38638
|
return;
|
|
38201
38639
|
}
|
|
@@ -38209,7 +38647,17 @@ var FetchproxyServer = class {
|
|
|
38209
38647
|
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
38210
38648
|
}
|
|
38211
38649
|
} else {
|
|
38212
|
-
graphqlCb.reject(
|
|
38650
|
+
graphqlCb.reject(protocolErrorFrom(inner.error));
|
|
38651
|
+
}
|
|
38652
|
+
return;
|
|
38653
|
+
}
|
|
38654
|
+
const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
|
|
38655
|
+
if (writeCookiesCb) {
|
|
38656
|
+
this.pendingWriteCookies.delete(inner.id);
|
|
38657
|
+
if (inner.ok && inner.op === "write_cookies") {
|
|
38658
|
+
writeCookiesCb.resolve([...inner.written]);
|
|
38659
|
+
} else {
|
|
38660
|
+
writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
|
|
38213
38661
|
}
|
|
38214
38662
|
return;
|
|
38215
38663
|
}
|
|
@@ -38252,6 +38700,9 @@ var FetchproxyServer = class {
|
|
|
38252
38700
|
for (const { reject } of this.pendingStorage.values())
|
|
38253
38701
|
reject(err);
|
|
38254
38702
|
this.pendingStorage.clear();
|
|
38703
|
+
for (const { reject } of this.pendingWriteCookies.values())
|
|
38704
|
+
reject(err);
|
|
38705
|
+
this.pendingWriteCookies.clear();
|
|
38255
38706
|
for (const { reject } of this.pendingCapture.values())
|
|
38256
38707
|
reject(err);
|
|
38257
38708
|
this.pendingCapture.clear();
|
|
@@ -38402,7 +38853,9 @@ async function runOneLift(opts) {
|
|
|
38402
38853
|
if (opts.declare.cookies.length > 0) {
|
|
38403
38854
|
const joined = await server.readCookies({
|
|
38404
38855
|
keys: opts.declare.cookies,
|
|
38405
|
-
...storageDomainOpts
|
|
38856
|
+
...storageDomainOpts,
|
|
38857
|
+
// Cookie-only: the other buckets are origin-scoped and ignore path.
|
|
38858
|
+
...opts.storagePath !== void 0 ? { path: opts.storagePath } : {}
|
|
38406
38859
|
});
|
|
38407
38860
|
for (const piece of joined.split("; ")) {
|
|
38408
38861
|
if (!piece)
|
|
@@ -38532,7 +38985,7 @@ function loadAccount(env = process.env) {
|
|
|
38532
38985
|
// package.json
|
|
38533
38986
|
var package_default = {
|
|
38534
38987
|
name: "infinitecampus-mcp",
|
|
38535
|
-
version: "2.4.
|
|
38988
|
+
version: "2.4.5",
|
|
38536
38989
|
mcpName: "io.github.chrischall/infinitecampus-mcp",
|
|
38537
38990
|
description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
38538
38991
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38575,8 +39028,8 @@ var package_default = {
|
|
|
38575
39028
|
},
|
|
38576
39029
|
dependencies: {
|
|
38577
39030
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
38578
|
-
"@fetchproxy/bootstrap": "^
|
|
38579
|
-
"@fetchproxy/server": "^
|
|
39031
|
+
"@fetchproxy/bootstrap": "^2.0.0",
|
|
39032
|
+
"@fetchproxy/server": "^2.0.0",
|
|
38580
39033
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38581
39034
|
dotenv: "^17.4.2",
|
|
38582
39035
|
"node-html-parser": "^9.0.0",
|
|
@@ -38622,6 +39075,18 @@ function lifterFor(host) {
|
|
|
38622
39075
|
// <district>.infinitecampus.org, etc.). Declare just this host
|
|
38623
39076
|
// — each district is its own root, no wildcard needed.
|
|
38624
39077
|
domains: [host],
|
|
39078
|
+
// IC is a Tomcat app deployed at /campus, and Tomcat scopes JSESSIONID
|
|
39079
|
+
// to the servlet context by default:
|
|
39080
|
+
//
|
|
39081
|
+
// JSESSIONID=...; Path=/campus; Secure; HttpOnly; SameSite=Lax
|
|
39082
|
+
//
|
|
39083
|
+
// chrome.cookies.get matches that Path against the URL it is given, so a
|
|
39084
|
+
// read aimed at the origin root silently returns nothing — which is
|
|
39085
|
+
// indistinguishable from "the user is signed out", and is why this path
|
|
39086
|
+
// never worked on any tenant. Verified live: without this the lift found
|
|
39087
|
+
// no cookies while a fetch through the same tab returned authenticated
|
|
39088
|
+
// data; with it, JSESSIONID + XSRF-TOKEN both come back.
|
|
39089
|
+
storagePath: "/campus",
|
|
38625
39090
|
declare: {
|
|
38626
39091
|
// JSESSIONID is HttpOnly (chrome.cookies.get sees it; the
|
|
38627
39092
|
// security gate is the declared key list). XSRF-TOKEN is
|
|
@@ -38672,7 +39137,7 @@ async function liftBrowserSession(account2) {
|
|
|
38672
39137
|
}
|
|
38673
39138
|
|
|
38674
39139
|
// src/client.ts
|
|
38675
|
-
import { writeFile as
|
|
39140
|
+
import { writeFile as writeFile3, stat } from "fs/promises";
|
|
38676
39141
|
import { dirname } from "path";
|
|
38677
39142
|
|
|
38678
39143
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
@@ -39132,7 +39597,7 @@ var ICClient = class {
|
|
|
39132
39597
|
});
|
|
39133
39598
|
if (!res.ok) throw new Error(`IC download ${res.status} for ${path}`);
|
|
39134
39599
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
39135
|
-
await
|
|
39600
|
+
await writeFile3(destinationPath, buf);
|
|
39136
39601
|
return {
|
|
39137
39602
|
path: destinationPath,
|
|
39138
39603
|
bytes: buf.byteLength,
|
|
@@ -40140,7 +40605,7 @@ function registerFeaturesTools(server, client) {
|
|
|
40140
40605
|
|
|
40141
40606
|
// src/index.ts
|
|
40142
40607
|
await loadDotenvSafely({
|
|
40143
|
-
path:
|
|
40608
|
+
path: join3(dirname2(fileURLToPath(import.meta.url)), "..", ".env"),
|
|
40144
40609
|
override: false
|
|
40145
40610
|
});
|
|
40146
40611
|
var AI_NOTICE = "[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.";
|
|
@@ -40158,7 +40623,7 @@ try {
|
|
|
40158
40623
|
}
|
|
40159
40624
|
var COMMON = {
|
|
40160
40625
|
name: "infinitecampus",
|
|
40161
|
-
version: "2.4.
|
|
40626
|
+
version: "2.4.5"
|
|
40162
40627
|
// x-release-please-version
|
|
40163
40628
|
};
|
|
40164
40629
|
if (account) {
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ catch (e) {
|
|
|
57
57
|
// actionable stderr message (banner) instead of a crash loop.
|
|
58
58
|
const COMMON = {
|
|
59
59
|
name: 'infinitecampus',
|
|
60
|
-
version: '2.4.
|
|
60
|
+
version: '2.4.5', // x-release-please-version
|
|
61
61
|
};
|
|
62
62
|
if (account) {
|
|
63
63
|
const client = new ICClient(account, { refreshSession });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
4
4
|
"mcpName": "io.github.chrischall/infinitecampus-mcp",
|
|
5
5
|
"description": "Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
46
|
-
"@fetchproxy/bootstrap": "^
|
|
47
|
-
"@fetchproxy/server": "^
|
|
46
|
+
"@fetchproxy/bootstrap": "^2.0.0",
|
|
47
|
+
"@fetchproxy/server": "^2.0.0",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"dotenv": "^17.4.2",
|
|
50
50
|
"node-html-parser": "^9.0.0",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/infinitecampus-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.4.
|
|
9
|
+
"version": "2.4.5",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.4.
|
|
14
|
+
"version": "2.4.5",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|